diff --git a/README.md b/README.md index d67f387..78b2456 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,16 @@ config :pythonx, :uv_init, """ ``` +Additionally, you can configure a specific version of the uv package manager for Pythonx to use. This can impact the available Python versions. + +```elixir +import Config + +config :pythonx, :uv_init, + ..., + uv_version: "0.7.21" +``` + With that, you can use `Pythonx.eval/2` and other APIs in your application. The downloads will happen at compile time, and the interpreter will get initialized automatically on boot. All necessary diff --git a/lib/pythonx.ex b/lib/pythonx.ex index d50b802..1e5453a 100644 --- a/lib/pythonx.ex +++ b/lib/pythonx.ex @@ -12,7 +12,7 @@ defmodule Pythonx do @type encoder :: (term(), encoder() -> Object.t()) - @doc ~S''' + @doc ~s''' Installs Python and dependencies using [uv](https://docs.astral.sh/uv) package manager and initializes the interpreter. @@ -49,13 +49,16 @@ defmodule Pythonx do * `:force` - if true, runs with empty project cache. Defaults to `false`. + * `:uv_version` - select the version of the uv package manager to use. + Defaults to `#{inspect(Pythonx.Uv.default_uv_version())}`. + ''' @spec uv_init(String.t(), keyword()) :: :ok def uv_init(pyproject_toml, opts \\ []) when is_binary(pyproject_toml) and is_list(opts) do - opts = Keyword.validate!(opts, force: false) + opts = Keyword.validate!(opts, force: false, uv_version: Pythonx.Uv.default_uv_version()) Pythonx.Uv.fetch(pyproject_toml, false, opts) - Pythonx.Uv.init(pyproject_toml, false) + Pythonx.Uv.init(pyproject_toml, false, Keyword.take(opts, [:uv_version])) end # Initializes the Python interpreter. diff --git a/lib/pythonx/application.ex b/lib/pythonx/application.ex index 1be6aa8..e6bea9f 100644 --- a/lib/pythonx/application.ex +++ b/lib/pythonx/application.ex @@ -22,9 +22,14 @@ defmodule Pythonx.Application do # If configured, we fetch Python and dependencies at compile time # and we automatically initialize the interpreter on boot. - if pyproject_toml = Application.compile_env(:pythonx, :uv_init)[:pyproject_toml] do - Pythonx.Uv.fetch(pyproject_toml, true) - defp maybe_uv_init(), do: Pythonx.Uv.init(unquote(pyproject_toml), true) + uv_init_env = Application.compile_env(:pythonx, :uv_init) + pyproject_toml = uv_init_env[:pyproject_toml] + uv_version = uv_init_env[:uv_version] || Pythonx.Uv.default_uv_version() + opts = [uv_version: uv_version] + + if pyproject_toml do + Pythonx.Uv.fetch(pyproject_toml, true, opts) + defp maybe_uv_init(), do: Pythonx.Uv.init(unquote(pyproject_toml), true, unquote(opts)) else defp maybe_uv_init(), do: :noop end diff --git a/lib/pythonx/uv.ex b/lib/pythonx/uv.ex index e274c3f..6cc147e 100644 --- a/lib/pythonx/uv.ex +++ b/lib/pythonx/uv.ex @@ -3,17 +3,17 @@ defmodule Pythonx.Uv do require Logger - @uv_version "0.5.21" + def default_uv_version(), do: "0.8.5" @doc """ Fetches Python and dependencies based on the given configuration. """ @spec fetch(String.t(), boolean(), keyword()) :: :ok def fetch(pyproject_toml, priv?, opts \\ []) do - opts = Keyword.validate!(opts, force: false) + opts = Keyword.validate!(opts, force: false, uv_version: default_uv_version()) - project_dir = project_dir(pyproject_toml, priv?) - python_install_dir = python_install_dir(priv?) + project_dir = project_dir(pyproject_toml, priv?, opts[:uv_version]) + python_install_dir = python_install_dir(priv?, opts[:uv_version]) if opts[:force] || priv? do _ = File.rm_rf(project_dir) @@ -30,7 +30,8 @@ defmodule Pythonx.Uv do # We always use uv-managed Python, so the paths are predictable. if run!(["sync", "--python-preference", "only-managed"], cd: project_dir, - env: %{"UV_PYTHON_INSTALL_DIR" => python_install_dir} + env: %{"UV_PYTHON_INSTALL_DIR" => python_install_dir}, + uv_version: opts[:uv_version] ) != 0 do _ = File.rm_rf(project_dir) raise "fetching Python and dependencies failed, see standard output for details" @@ -40,15 +41,15 @@ defmodule Pythonx.Uv do :ok end - defp python_install_dir(priv?) do + defp python_install_dir(priv?, uv_version) do if priv? do Path.join(:code.priv_dir(:pythonx), "uv/python") else - Path.join(cache_dir(), "python") + Path.join(cache_dir(uv_version), "python") end end - defp project_dir(pyproject_toml, priv?) do + defp project_dir(pyproject_toml, priv?, uv_version) do if priv? do Path.join(:code.priv_dir(:pythonx), "uv/project") else @@ -57,7 +58,7 @@ defmodule Pythonx.Uv do |> :erlang.md5() |> Base.encode32(case: :lower, padding: false) - Path.join([cache_dir(), "projects", cache_id]) + Path.join([cache_dir(uv_version), "projects", cache_id]) end end @@ -66,8 +67,9 @@ defmodule Pythonx.Uv do fetched by `fetch/3`. """ @spec init(String.t(), boolean()) :: :ok - def init(pyproject_toml, priv?) do - project_dir = project_dir(pyproject_toml, priv?) + def init(pyproject_toml, priv?, opts \\ []) do + opts = Keyword.validate!(opts, uv_version: default_uv_version()) + project_dir = project_dir(pyproject_toml, priv?, opts[:uv_version]) # Uv stores Python installations in versioned directories in the # Python install dir. To find the versioned name for this project, @@ -91,7 +93,7 @@ defmodule Pythonx.Uv do {:unix, _osname} -> Path.basename(Path.dirname(abs_executable_dir)) end - root_dir = Path.join(python_install_dir(priv?), versioned_dir_name) + root_dir = Path.join(python_install_dir(priv?, opts[:uv_version]), versioned_dir_name) case :os.type() do {:win32, _osname} -> @@ -158,10 +160,11 @@ defmodule Pythonx.Uv do defp make_windows_slashes(path), do: String.replace(path, "/", "\\") defp run!(args, opts) do - path = uv_path() + {uv_version, opts} = Keyword.pop(opts, :uv_version, default_uv_version()) + path = uv_path(uv_version) if not File.exists?(path) do - download!() + download!(uv_version) end {_stream, status} = @@ -170,13 +173,13 @@ defmodule Pythonx.Uv do status end - defp uv_path() do - Path.join([cache_dir(), "bin", "uv"]) + defp uv_path(uv_version) do + Path.join([cache_dir(uv_version), "bin", "uv"]) end @version Mix.Project.config()[:version] - defp cache_dir() do + defp cache_dir(uv_version) do base_dir = if dir = System.get_env("PYTHONX_CACHE_DIR") do Path.expand(dir) @@ -184,17 +187,17 @@ defmodule Pythonx.Uv do :filename.basedir(:user_cache, "pythonx") end - Path.join([base_dir, @version, "uv", @uv_version]) + Path.join([base_dir, @version, "uv", uv_version]) end - defp download!() do + defp download!(uv_version) do {archive_type, archive_name} = archive_name() - url = "https://github.com/astral-sh/uv/releases/download/#{@uv_version}/#{archive_name}" + url = "https://github.com/astral-sh/uv/releases/download/#{uv_version}/#{archive_name}" Logger.debug("Downloading uv archive from #{url}") archive_binary = Pythonx.Utils.fetch_body!(url) - path = uv_path() + path = uv_path(uv_version) {:ok, uv_binary} = extract_executable(archive_type, archive_binary) File.mkdir_p!(Path.dirname(path)) File.write!(path, uv_binary)