@@ -5,19 +5,20 @@ local a = require "mason-core.async"
55local installer = require " mason-core.installer"
66local log = require " mason-core.log"
77local path = require " mason-core.path"
8+ local pep440 = require " mason-core.pep440"
89local platform = require " mason-core.platform"
10+ local providers = require " mason-core.providers"
911local semver = require " mason-core.semver"
1012local spawn = require " mason-core.spawn"
1113
1214local M = {}
1315
1416local VENV_DIR = " venv"
1517
16- local is_executable = _ .compose (_ .equals (1 ), vim .fn .executable )
17-
1818--- @async
1919--- @param candidates string[]
2020local function resolve_python3 (candidates )
21+ local is_executable = _ .compose (_ .equals (1 ), vim .fn .executable )
2122 a .scheduler ()
2223 local available_candidates = _ .filter (is_executable , candidates )
2324 for __ , candidate in ipairs (available_candidates ) do
@@ -31,16 +32,33 @@ local function resolve_python3(candidates)
3132 return nil
3233end
3334
34- --- @param min_version ? Semver
35- local function get_versioned_candidates (min_version )
35+ --- @param version string
36+ --- @param specifiers string
37+ local function pep440_check_version (version , specifiers )
38+ -- The version check only implements a subset of the PEP440 specification and may error with certain inputs.
39+ local ok , result = pcall (pep440 .check_version , version , specifiers )
40+ if not ok then
41+ log .fmt_warn (
42+ " Failed to check PEP440 version compatibility for version %s with specifiers %s: %s" ,
43+ version ,
44+ specifiers ,
45+ result
46+ )
47+ return false
48+ end
49+ return result
50+ end
51+
52+ --- @param supported_python_versions string
53+ local function get_versioned_candidates (supported_python_versions )
3654 return _ .filter_map (function (pair )
3755 local version , executable = unpack (pair )
38- if not min_version or version > min_version then
39- return Optional .of (executable )
40- else
56+ if not pep440_check_version (tostring (version ), supported_python_versions ) then
4157 return Optional .empty ()
4258 end
59+ return Optional .of (executable )
4360 end , {
61+ { semver .new " 3.12.0" , " python3.12" },
4462 { semver .new " 3.11.0" , " python3.11" },
4563 { semver .new " 3.10.0" , " python3.10" },
4664 { semver .new " 3.9.0" , " python3.9" },
@@ -51,24 +69,60 @@ local function get_versioned_candidates(min_version)
5169end
5270
5371--- @async
54- local function create_venv ()
72+ --- @param pkg { name : string , version : string }
73+ local function create_venv (pkg )
74+ local ctx = installer .context ()
75+ --- @type string ?
76+ local supported_python_versions = providers .pypi .get_supported_python_versions (pkg .name , pkg .version ):get_or_nil ()
77+
78+ -- 1. Resolve stock python3 installation.
5579 local stock_candidates = platform .is .win and { " python" , " python3" } or { " python3" , " python" }
5680 local stock_target = resolve_python3 (stock_candidates )
5781 if stock_target then
5882 log .fmt_debug (" Resolved stock python3 installation version %s" , stock_target .version )
5983 end
60- local versioned_candidates = get_versioned_candidates (stock_target and stock_target .version )
61- log .debug (" Resolving versioned python3 candidates" , versioned_candidates )
84+
85+ -- 2. Resolve suitable versioned python3 installation (python3.12, python3.11, etc.).
86+ local versioned_candidates = {}
87+ if supported_python_versions ~= nil then
88+ log .fmt_debug (" Finding versioned candidates for %s" , supported_python_versions )
89+ versioned_candidates = get_versioned_candidates (supported_python_versions )
90+ end
6291 local target = resolve_python3 (versioned_candidates ) or stock_target
63- local ctx = installer . context ()
92+
6493 if not target then
65- ctx . stdio_sink . stderr (
66- (" Unable to find python3 installation. Tried the following candidates: %s.\n " ):format (
94+ return Result . failure (
95+ (" Unable to find python3 installation in PATH . Tried the following candidates: %s." ):format (
6796 _ .join (" , " , _ .concat (stock_candidates , versioned_candidates ))
6897 )
6998 )
70- return Result .failure " Failed to find python3 installation."
7199 end
100+
101+ -- 3. If a versioned python3 installation was not found, warn the user if the stock python3 installation is outside
102+ -- the supported version range.
103+ if
104+ target == stock_target
105+ and supported_python_versions ~= nil
106+ and not pep440_check_version (tostring (target .version ), supported_python_versions )
107+ then
108+ if ctx .opts .force then
109+ ctx .stdio_sink .stderr (
110+ (" Warning: The resolved python3 version %s is not compatible with the required Python versions: %s.\n " ):format (
111+ target .version ,
112+ supported_python_versions
113+ )
114+ )
115+ else
116+ ctx .stdio_sink .stderr " Run with :MasonInstall --force to bypass this version validation.\n "
117+ return Result .failure (
118+ (" Failed to find a python3 installation in PATH that meets the required versions (%s). Found version: %s." ):format (
119+ supported_python_versions ,
120+ target .version
121+ )
122+ )
123+ end
124+ end
125+
72126 log .fmt_debug (" Found python3 installation version=%s, executable=%s" , target .version , target .executable )
73127 ctx .stdio_sink .stdout " Creating virtual environment…\n "
74128 return ctx .spawn [target .executable ] { " -m" , " venv" , VENV_DIR }
@@ -118,15 +172,15 @@ local function pip_install(pkgs, extra_args)
118172end
119173
120174--- @async
121- --- @param opts { upgrade_pip : boolean , install_extra_args ?: string[] }
175+ --- @param opts { package : { name : string , version : string }, upgrade_pip : boolean , install_extra_args ?: string[] }
122176function M .init (opts )
123177 return Result .try (function (try )
124178 log .fmt_debug (" pypi: init" , opts )
125179 local ctx = installer .context ()
126180
127181 -- pip3 will hardcode the full path to venv executables, so we need to promote cwd to make sure pip uses the final destination path.
128182 ctx :promote_cwd ()
129- try (create_venv ())
183+ try (create_venv (opts . package ))
130184
131185 if opts .upgrade_pip then
132186 ctx .stdio_sink .stdout " Upgrading pip inside the virtual environment…\n "
0 commit comments