Current section

Files

Jump to
snakebridge lib mix tasks compile snakebridge.ex
Raw

lib/mix/tasks/compile/snakebridge.ex

defmodule Mix.Tasks.Compile.Snakebridge do
@moduledoc """
Mix compiler that runs the SnakeBridge pre-pass (scan → introspect → generate).
"""
use Mix.Task.Compiler
alias SnakeBridge.Compiler.Pipeline
alias SnakeBridge.{Config, Helpers, Lock, Manifest}
@impl Mix.Task.Compiler
def run(_args) do
Mix.Task.run("loadconfig")
config = Config.load()
if Mix.env() in [:dev, :test] and compile_hint_enabled?() do
SnakeBridge.CompileShell.install()
end
cond do
skip_generation?() -> {:ok, []}
up_to_date?(config) -> {:ok, []}
true -> Pipeline.run(config)
end
end
@impl Mix.Task.Compiler
def manifests do
Mix.Task.run("loadconfig")
config = Config.load()
[
Path.join(config.metadata_dir, "manifest.json"),
"snakebridge.lock"
]
end
@doc false
defdelegate verify_generated_files_exist!(config), to: Pipeline
@doc false
defdelegate verify_symbols_present!(config, manifest), to: Pipeline
defp skip_generation? do
case System.get_env("SNAKEBRIDGE_SKIP") do
nil -> false
value -> truthy_env?(value)
end
end
defp compile_hint_enabled? do
case System.get_env("SNAKEBRIDGE_COMPILE_HINT") do
nil -> Application.get_env(:snakebridge, :compile_hint, false)
value -> truthy_env?(value)
end
end
defp truthy_env?(value) when is_binary(value) do
value in ["1", "true", "TRUE", "yes", "YES"]
end
defp up_to_date?(%Config{} = config) do
cond do
config.libraries == [] ->
true
strict_mode?(config) ->
false
not prerequisites_satisfied?(config) ->
false
true ->
manifest = Manifest.load(config)
not generate_all_missing?(config, manifest) and not used_missing?(config, manifest)
end
end
defp prerequisites_satisfied?(config) do
manifest_present?(config) and
lock_up_to_date?(config) and
generated_files_present?(config) and
helpers_present?(config) and
registry_present?()
end
defp strict_mode?(config) do
System.get_env("SNAKEBRIDGE_STRICT") == "1" or config.strict == true
end
defp manifest_present?(config) do
path = Path.join(config.metadata_dir, "manifest.json")
File.exists?(path)
end
defp lock_up_to_date?(config) do
case Lock.load() do
nil ->
false
lock ->
current_version = Application.spec(:snakebridge, :vsn) |> to_string()
lock_version = get_in(lock, ["environment", "snakebridge_version"]) || lock["version"]
lock_version == current_version and Lock.verify_generator_unchanged?(lock) and
libraries_match_lock?(config, lock)
end
end
defp libraries_match_lock?(config, lock) do
lock_libraries = Map.get(lock, "libraries", %{})
Enum.all?(config.libraries, fn library ->
case Map.get(lock_libraries, library.python_name) do
nil ->
false
info ->
normalize_version(info["requested"]) == normalize_version(library.version)
end
end)
end
defp normalize_version(nil), do: nil
defp normalize_version(:stdlib), do: "stdlib"
defp normalize_version(value) when is_binary(value), do: value
defp normalize_version(value), do: to_string(value)
defp generated_files_present?(config) do
Enum.all?(config.libraries, fn library ->
path = Path.join(config.generated_dir, "#{library.python_name}.ex")
File.exists?(path)
end)
end
defp helpers_present?(config) do
if Helpers.enabled?(config) do
dir = Path.join(config.generated_dir, "helpers")
case File.ls(dir) do
{:ok, files} -> Enum.any?(files, &String.ends_with?(&1, ".ex"))
{:error, _} -> false
end
else
true
end
end
defp registry_present? do
registry_path =
Application.get_env(:snakebridge, :registry_path) ||
Path.join([File.cwd!(), "priv", "snakebridge", "registry.json"])
File.exists?(registry_path)
end
defp generate_all_missing?(config, manifest) do
Enum.any?(config.libraries, fn library ->
library.generate == :all and not manifest_has_library?(manifest, library)
end)
end
defp manifest_has_library?(manifest, library) do
prefix = library.python_name
symbols = Map.get(manifest, "symbols", %{})
classes = Map.get(manifest, "classes", %{})
Enum.any?(symbols, fn {_key, info} ->
String.starts_with?(info["python_module"] || "", prefix)
end) or
Enum.any?(classes, fn {_key, info} ->
String.starts_with?(info["python_module"] || "", prefix)
end)
end
defp used_missing?(config, manifest) do
used_libraries = Enum.reject(config.libraries, &(&1.generate == :all))
if used_libraries == [] do
false
else
used_config = %{config | libraries: used_libraries}
detected = scanner_module().scan_project(used_config)
Manifest.missing(manifest, detected) != []
end
end
defp scanner_module do
Application.get_env(:snakebridge, :scanner, SnakeBridge.Scanner)
end
end