defmodule Safe.Version do @moduledoc """ Version resolution and lock file management for the SAFE binary. Resolves the SAFE binary version against a semver constraint, reads/writes the `safe.lock` file (JSON format). """ @constraint_string "~> 1.5.0" @constraint Version.parse_requirement!(@constraint_string) @lock_file "safe.lock" @doc "Returns the semver constraint used to resolve SAFE binary versions." def constraint, do: @constraint @doc "Returns the mix_safe plugin version (compiled from mix.exs at build time)." def plugin_version do Mix.Project.config()[:version] end @doc "Returns true if the version satisfies the `~> 1.5.0` constraint." def compatible?(version_string) do case Version.parse(normalize_version(version_string)) do {:ok, %Version{pre: [_ | _]}} -> false {:ok, v} -> Version.match?(v, @constraint_string) _ -> false end end @doc """ Picks the latest stable version from `versions_map` (a map of version string => platform checksums) that satisfies the constraint. Pre-releases are excluded. """ def resolve_version(versions_map) do result = versions_map |> Map.keys() |> Enum.filter(&compatible?/1) |> Enum.sort(fn a, b -> case {Version.parse(normalize_version(a)), Version.parse(normalize_version(b))} do {{:ok, va}, {:ok, vb}} -> Version.compare(va, vb) == :gt _ -> false end end) |> List.first() case result do nil -> {:error, :no_compatible_version} v -> {:ok, v} end end @doc """ Reads the pinned SAFE version from `/safe.lock`. Returns `{:ok, version_string}` or `{:error, :not_found}`. """ def read_lock(project_dir) do path = lock_path(project_dir) case File.read(path) do {:ok, contents} -> case Jason.decode(contents) do {:ok, %{"version" => version}} when is_binary(version) -> {:ok, version} _ -> {:error, {:lock_parse_error, path}} end {:error, :enoent} -> {:error, :not_found} {:error, reason} -> {:error, {:lock_read_error, reason}} end end @doc """ Writes the pinned SAFE version to `/safe.lock` in JSON format. """ def write_lock(project_dir, version) do contents = Jason.encode!(%{"version" => version}) case File.write(lock_path(project_dir), contents) do :ok -> :ok {:error, reason} -> {:error, {:lock_write_error, reason}} end end # --------------------------------------------------------------------------- # Private helpers # --------------------------------------------------------------------------- defp lock_path(project_dir), do: Path.join(project_dir, @lock_file) defp normalize_version(v), do: v end