Skip to content

Commit 71f1a68

Browse files
feat: add uv as installer for python
UV is a very fast installer for python packages that can be 10-100x faster to resolve packages. This adds an option for Mason to use it instead of pip to resolve python packages that are installed via Mason. More info about the replacement: https://github.com/astral-sh/uv I have no relationship with uv, it is just very fast and it would be nice to have updates for packages like sqlfluff take a lot less time than they currently do to resolve during updates. fix: ensure the virtual environment is .venv for uv fix: venv dir can stay venv feat: update stdio ctx reference
1 parent 7c7318e commit 71f1a68

File tree

6 files changed

+68
-20
lines changed

6 files changed

+68
-20
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,10 @@ local DEFAULT_SETTINGS = {
231231
-- Whether to upgrade pip to the latest version in the virtual environment before installing packages.
232232
upgrade_pip = false,
233233

234+
---@since 1.8.0
235+
-- Whether to use uv to install packages instead of pip
236+
use_uv = false,
237+
234238
---@since 1.0.0
235239
-- These args will be added to `pip install` calls. Note that setting extra args might impact intended behavior
236240
-- and is not recommended.

doc/mason.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,10 @@ Example:
276276
-- Whether to upgrade pip to the latest version in the virtual environment before installing packages.
277277
upgrade_pip = false,
278278

279+
---@since 1.8.0
280+
-- Whether to use uv to install packages instead of pip
281+
use_uv = false,
282+
279283
---@since 1.0.0
280284
-- These args will be added to `pip install` calls. Note that setting extra args might impact intended behavior
281285
-- and is not recommended.

lua/mason-core/installer/compiler/compilers/pypi.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ function M.parse(source, purl)
2727
pip = {
2828
upgrade = settings.current.pip.upgrade_pip,
2929
extra_args = settings.current.pip.install_args,
30+
use_uv = settings.current.pip.use_uv,
3031
},
3132
}
3233

@@ -48,11 +49,13 @@ function M.install(ctx, source)
4849
},
4950
upgrade_pip = source.pip.upgrade,
5051
install_extra_args = source.pip.extra_args,
52+
use_uv = source.pip.use_uv,
5153
})
5254
try(pypi.install(source.package, source.version, {
5355
extra = source.extra,
5456
extra_packages = source.extra_packages,
5557
install_extra_args = source.pip.extra_args,
58+
use_uv = source.pip.use_uv,
5659
}))
5760
end)
5861
end

lua/mason-core/installer/managers/pypi.lua

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ local pep440 = require "mason-core.pep440"
99
local platform = require "mason-core.platform"
1010
local providers = require "mason-core.providers"
1111
local semver = require "mason-core.semver"
12+
local settings = require "mason.settings"
1213
local spawn = require "mason-core.spawn"
1314

1415
local M = {}
1516

17+
local use_uv = settings.current.pip.use_uv
1618
local VENV_DIR = "venv"
1719

1820
function M.venv_path(dir)
@@ -30,11 +32,20 @@ local function resolve_python3(candidates)
3032
a.scheduler()
3133
local available_candidates = _.filter(is_executable, candidates)
3234
for __, candidate in ipairs(available_candidates) do
33-
---@type string
34-
local version_output = spawn[candidate]({ "--version" }):map(_.prop "stdout"):get_or_else ""
35-
local ok, version = pcall(semver.new, version_output:match "Python (3%.%d+.%d+)")
36-
if ok then
37-
return { executable = candidate, version = version }
35+
if use_uv and candidate == "uv" then
36+
---@type string
37+
local version_output = spawn[candidate]({ "--version" }):map(_.prop "stdout"):get_or_else ""
38+
local ok, version = pcall(semver.new, version_output:match "uv (%d+.%d+.%d+).*")
39+
if ok then
40+
return { executable = candidate, version = version }
41+
end
42+
elseif not use_uv then
43+
---@type string
44+
local version_output = spawn[candidate]({ "--version" }):map(_.prop "stdout"):get_or_else ""
45+
local ok, version = pcall(semver.new, version_output:match "Python (3%.%d+.%d+)")
46+
if ok then
47+
return { executable = candidate, version = version }
48+
end
3849
end
3950
end
4051
return nil
@@ -84,14 +95,14 @@ local function create_venv(pkg)
8495
local supported_python_versions = providers.pypi.get_supported_python_versions(pkg.name, pkg.version):get_or_nil()
8596

8697
-- 1. Resolve stock python3 installation.
87-
local stock_candidates = platform.is.win and { "python", "python3" } or { "python3", "python" }
98+
local stock_candidates = platform.is.win and { "python", "python3", "uv" } or { "python3", "python", "uv" }
8899
local stock_target = resolve_python3(stock_candidates)
89100
if stock_target then
90101
log.fmt_debug("Resolved stock python3 installation version %s", stock_target.version)
91102
end
92103

93104
-- 2. Resolve suitable versioned python3 installation (python3.12, python3.11, etc.).
94-
local versioned_candidates = {}
105+
local versioned_candidates = { "uv" }
95106
if supported_python_versions ~= nil then
96107
if stock_target and not pep440_check_version(tostring(stock_target.version), supported_python_versions) then
97108
log.fmt_debug("Finding versioned candidates for %s", supported_python_versions)
@@ -111,7 +122,8 @@ local function create_venv(pkg)
111122
-- 3. If a versioned python3 installation was not found, warn the user if the stock python3 installation is outside
112123
-- the supported version range.
113124
if
114-
target == stock_target
125+
use_uv == false
126+
and target == stock_target
115127
and supported_python_versions ~= nil
116128
and not pep440_check_version(tostring(target.version), supported_python_versions)
117129
then
@@ -133,9 +145,14 @@ local function create_venv(pkg)
133145
end
134146
end
135147

136-
log.fmt_debug("Found python3 installation version=%s, executable=%s", target.version, target.executable)
137148
ctx.stdio_sink:stdout "Creating virtual environment…\n"
138-
return ctx.spawn[target.executable] { "-m", "venv", "--system-site-packages", VENV_DIR }
149+
if use_uv then
150+
log.fmt_debug("Found uv installation version=%s, executable=%s", target.version, target.executable)
151+
return ctx.spawn[target.executable] { "venv", VENV_DIR }
152+
else
153+
log.fmt_debug("Found python3 installation version=%s, executable=%s", target.version, target.executable)
154+
return ctx.spawn[target.executable] { "-m", "venv", "--system-site-packages", VENV_DIR }
155+
end
139156
end
140157

141158
---@param ctx InstallContext
@@ -161,6 +178,9 @@ end
161178
---@param args SpawnArgs
162179
local function venv_python(args)
163180
local ctx = installer.context()
181+
if use_uv then
182+
return ctx.spawn["uv"](args)
183+
end
164184
return find_venv_executable(ctx, "python"):and_then(function(python_path)
165185
return ctx.spawn[path.concat { ctx.cwd:get(), python_path }](args)
166186
end)
@@ -170,15 +190,27 @@ end
170190
---@param pkgs string[]
171191
---@param extra_args? string[]
172192
local function pip_install(pkgs, extra_args)
173-
return venv_python {
174-
"-m",
175-
"pip",
176-
"--disable-pip-version-check",
177-
"install",
178-
"--ignore-installed",
179-
extra_args or vim.NIL,
180-
pkgs,
181-
}
193+
if use_uv then
194+
return venv_python {
195+
"pip",
196+
"install",
197+
"--directory",
198+
"venv",
199+
"-U",
200+
extra_args or vim.NIL,
201+
pkgs,
202+
}
203+
else
204+
return venv_python {
205+
"-m",
206+
"pip",
207+
"--disable-pip-version-check",
208+
"install",
209+
"--ignore-installed",
210+
extra_args or vim.NIL,
211+
pkgs,
212+
}
213+
end
182214
end
183215

184216
---@async
@@ -192,7 +224,7 @@ function M.init(opts)
192224
ctx:promote_cwd()
193225
try(create_venv(opts.package))
194226

195-
if opts.upgrade_pip then
227+
if opts.upgrade_pip and not use_uv then
196228
ctx.stdio_sink:stdout "Upgrading pip inside the virtual environment…\n"
197229
try(pip_install({ "pip" }, opts.install_extra_args))
198230
end

lua/mason/settings.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ local DEFAULT_SETTINGS = {
6060
-- Whether to upgrade pip to the latest version in the virtual environment before installing packages.
6161
upgrade_pip = false,
6262

63+
---@since 1.8.0
64+
-- Whether to use uv to install packages instead of pip
65+
use_uv = false,
66+
6367
---@since 1.0.0
6468
-- These args will be added to `pip install` calls. Note that setting extra args might impact intended behavior
6569
-- and is not recommended.

tests/mason-core/installer/compiler/compilers/pypi_spec.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ describe("pypi compiler :: parsing", function()
3131
pip = {
3232
upgrade = true,
3333
extra_args = { "--proxy", "http://localghost" },
34+
use_uv = false,
3435
},
3536
},
3637
pypi.parse({ extra_packages = { "extra" } }, purl())

0 commit comments

Comments
 (0)