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