Skip to content

Commit e4ef0e4

Browse files
authored
Updates to python locator (#23446)
The search algorithm has been documented here https://github.com/microsoft/vscode-python/wiki/Python-Environment-Search
1 parent b4dcd52 commit e4ef0e4

16 files changed

+389
-97
lines changed

native_locator/src/common_python.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ impl Locator for PythonOnPath<'_> {
4747
env_manager: None,
4848
project_path: None,
4949
python_run_command: Some(vec![env.executable.to_str().unwrap().to_string()]),
50+
arch: None,
5051
})
5152
}
5253

native_locator/src/conda.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,7 @@ fn get_root_python_environment(path: &PathBuf, manager: &EnvManager) -> Option<P
646646
"python".to_string(),
647647
]),
648648
project_path: None,
649+
arch: None,
649650
});
650651
}
651652
None

native_locator/src/homebrew.rs

Lines changed: 225 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ use crate::{
88
utils::PythonEnv,
99
};
1010
use regex::Regex;
11-
use std::{collections::HashSet, fs::DirEntry, path::PathBuf};
11+
use std::{collections::HashSet, path::PathBuf};
1212

13-
fn is_symlinked_python_executable(path: &DirEntry) -> Option<PathBuf> {
14-
let path = path.path();
13+
fn is_symlinked_python_executable(path: &PathBuf) -> Option<PathBuf> {
1514
let name = path.file_name()?.to_string_lossy();
1615
if !name.starts_with("python") || name.ends_with("-config") || name.ends_with("-build") {
1716
return None;
@@ -23,6 +22,149 @@ fn is_symlinked_python_executable(path: &DirEntry) -> Option<PathBuf> {
2322
Some(std::fs::canonicalize(path).ok()?)
2423
}
2524

25+
fn get_homebrew_prefix_env_var(environment: &dyn Environment) -> Option<PathBuf> {
26+
if let Some(homebrew_prefix) = environment.get_env_var("HOMEBREW_PREFIX".to_string()) {
27+
let homebrew_prefix_bin = PathBuf::from(homebrew_prefix).join("bin");
28+
if homebrew_prefix_bin.exists() {
29+
return Some(homebrew_prefix_bin);
30+
}
31+
}
32+
None
33+
}
34+
35+
fn get_homebrew_prefix_bin(environment: &dyn Environment) -> Option<PathBuf> {
36+
if let Some(homebrew_prefix) = get_homebrew_prefix_env_var(environment) {
37+
return Some(homebrew_prefix);
38+
}
39+
40+
// Homebrew install folders documented here https://docs.brew.sh/Installation
41+
// /opt/homebrew for Apple Silicon,
42+
// /usr/local for macOS Intel
43+
// /home/linuxbrew/.linuxbrew for Linux
44+
[
45+
"/home/linuxbrew/.linuxbrew/bin",
46+
"/opt/homebrew/bin",
47+
"/usr/local/bin",
48+
]
49+
.iter()
50+
.map(|p| PathBuf::from(p))
51+
.find(|p| p.exists())
52+
}
53+
54+
fn get_env_path(python_exe_from_bin_dir: &PathBuf, resolved_file: &PathBuf) -> Option<PathBuf> {
55+
// If the fully resolved file path contains the words `/homebrew/` or `/linuxbrew/`
56+
// Then we know this is definitely a home brew version of python.
57+
// And in these cases we can compute the sysprefix.
58+
59+
let resolved_file = resolved_file.to_str()?;
60+
// 1. MacOS Silicon
61+
if python_exe_from_bin_dir
62+
.to_string_lossy()
63+
.to_lowercase()
64+
.starts_with("/opt/homebrew/bin/python")
65+
{
66+
// Resolved exe is something like `/opt/homebrew/Cellar/[email protected]/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12`
67+
let reg_ex = Regex::new("/opt/homebrew/Cellar/python@((\\d+\\.?)*)/(\\d+\\.?)*/Frameworks/Python.framework/Versions/(\\d+\\.?)*/bin/python(\\d+\\.?)*").unwrap();
68+
let captures = reg_ex.captures(&resolved_file)?;
69+
let version = captures.get(1).map(|m| m.as_str()).unwrap_or_default();
70+
// SysPrefix- /opt/homebrew/opt/[email protected]/Frameworks/Python.framework/Versions/3.12
71+
let sys_prefix = PathBuf::from(format!(
72+
"/opt/homebrew/opt/python@{}/Frameworks/Python.framework/Versions/{}",
73+
version, version
74+
));
75+
76+
return if sys_prefix.exists() {
77+
Some(sys_prefix)
78+
} else {
79+
None
80+
};
81+
}
82+
83+
// 2. Linux
84+
if python_exe_from_bin_dir
85+
.to_string_lossy()
86+
.to_lowercase()
87+
.starts_with("/usr/local/bin/python")
88+
{
89+
// Resolved exe is something like `/home/linuxbrew/.linuxbrew/Cellar/[email protected]/3.12.3/bin/python3.12`
90+
let reg_ex = Regex::new("/home/linuxbrew/.linuxbrew/Cellar/python@(\\d+\\.?\\d+\\.?)/(\\d+\\.?\\d+\\.?\\d+\\.?)/bin/python.*").unwrap();
91+
let captures = reg_ex.captures(&resolved_file)?;
92+
let version = captures.get(1).map(|m| m.as_str()).unwrap_or_default();
93+
let full_version = captures.get(2).map(|m| m.as_str()).unwrap_or_default();
94+
// SysPrefix- /home/linuxbrew/.linuxbrew/Cellar/[email protected]/3.12.3
95+
let sys_prefix = PathBuf::from(format!(
96+
"/home/linuxbrew/.linuxbrew/Cellar/python@{}/{}",
97+
version, full_version
98+
));
99+
100+
return if sys_prefix.exists() {
101+
Some(sys_prefix)
102+
} else {
103+
None
104+
};
105+
}
106+
107+
// 3. MacOS Intel
108+
if python_exe_from_bin_dir
109+
.to_string_lossy()
110+
.to_lowercase()
111+
.starts_with("/usr/local/bin/python")
112+
{
113+
// Resolved exe is something like `/usr/local/Cellar/[email protected]/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12`
114+
let reg_ex = Regex::new("/usr/local/Cellar/python@(\\d+\\.?\\d+\\.?)/(\\d+\\.?\\d+\\.?\\d+\\.?)/Frameworks/Python.framework/Versions/(\\d+\\.?\\d+\\.?)/bin/python.*").unwrap();
115+
let captures = reg_ex.captures(&resolved_file)?;
116+
let version = captures.get(1).map(|m| m.as_str()).unwrap_or_default();
117+
let full_version = captures.get(2).map(|m| m.as_str()).unwrap_or_default();
118+
// SysPrefix- /usr/local/Cellar/[email protected]/3.8.19/Frameworks/Python.framework/Versions/3.8
119+
let sys_prefix = PathBuf::from(format!(
120+
"/usr/local/Cellar/python@{}/{}/Frameworks/Python.framework/Versions/{}",
121+
version, full_version, version
122+
));
123+
124+
return if sys_prefix.exists() {
125+
Some(sys_prefix)
126+
} else {
127+
None
128+
};
129+
}
130+
None
131+
}
132+
133+
fn get_python_info(
134+
python_exe_from_bin_dir: &PathBuf,
135+
reported: &mut HashSet<String>,
136+
python_version_regex: &Regex,
137+
) -> Option<PythonEnvironment> {
138+
// Possible we do not have python3.12 or the like in bin directory
139+
// & we have only python3, in that case we should add python3 to the list
140+
if let Some(resolved_exe) = is_symlinked_python_executable(python_exe_from_bin_dir) {
141+
let user_friendly_exe = python_exe_from_bin_dir;
142+
let python_version = resolved_exe.to_string_lossy().to_string();
143+
let version = match python_version_regex.captures(&python_version) {
144+
Some(captures) => match captures.get(1) {
145+
Some(version) => Some(version.as_str().to_string()),
146+
None => None,
147+
},
148+
None => None,
149+
};
150+
if reported.contains(&resolved_exe.to_string_lossy().to_string()) {
151+
return None;
152+
}
153+
reported.insert(resolved_exe.to_string_lossy().to_string());
154+
return Some(PythonEnvironment::new(
155+
None,
156+
None,
157+
Some(user_friendly_exe.clone()),
158+
crate::messaging::PythonEnvironmentCategory::Homebrew,
159+
version,
160+
get_env_path(python_exe_from_bin_dir, &resolved_exe),
161+
None,
162+
Some(vec![user_friendly_exe.to_string_lossy().to_string()]),
163+
));
164+
}
165+
None
166+
}
167+
26168
pub struct Homebrew<'a> {
27169
pub environment: &'a dyn Environment,
28170
}
@@ -34,64 +176,100 @@ impl Homebrew<'_> {
34176
}
35177

36178
impl Locator for Homebrew<'_> {
37-
fn resolve(&self, _env: &PythonEnv) -> Option<PythonEnvironment> {
38-
None
179+
fn resolve(&self, env: &PythonEnv) -> Option<PythonEnvironment> {
180+
let python_regex = Regex::new(r"/(\d+\.\d+\.\d+)/").unwrap();
181+
let exe = env.executable.clone();
182+
let exe_file_name = exe.file_name()?;
183+
let mut reported: HashSet<String> = HashSet::new();
184+
if exe.starts_with("/opt/homebrew/bin/python")
185+
|| exe.starts_with("/opt/homebrew/Cellar/python@")
186+
|| exe.starts_with("/opt/homebrew/opt/python@")
187+
|| exe.starts_with("/opt/homebrew/opt/python")
188+
|| exe.starts_with("/opt/homebrew/Frameworks/Python.framework/Versions/")
189+
{
190+
// Symlink - /opt/homebrew/bin/python3.12
191+
// Symlink - /opt/homebrew/opt/python3/bin/python3.12
192+
// Symlink - /opt/homebrew/Cellar/[email protected]/3.12.3/bin/python3.12
193+
// Symlink - /opt/homebrew/opt/[email protected]/bin/python3.12
194+
// Symlink - /opt/homebrew/Cellar/[email protected]/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12
195+
// Symlink - /opt/homebrew/Cellar/[email protected]/3.12.3/Frameworks/Python.framework/Versions/Current/bin/python3.12
196+
// Symlink - /opt/homebrew/Frameworks/Python.framework/Versions/3.12/bin/python3.12
197+
// Symlink - /opt/homebrew/Frameworks/Python.framework/Versions/Current/bin/python3.12
198+
// Real exe - /opt/homebrew/Cellar/[email protected]/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12
199+
// SysPrefix- /opt/homebrew/opt/[email protected]/Frameworks/Python.framework/Versions/3.12
200+
get_python_info(
201+
&PathBuf::from("/opt/homebrew/bin").join(exe_file_name),
202+
&mut reported,
203+
&python_regex,
204+
)
205+
} else if exe.starts_with("/usr/local/bin/python")
206+
|| exe.starts_with("/usr/local/opt/python@")
207+
|| exe.starts_with("/usr/local/Cellar/python@")
208+
{
209+
// Symlink - /usr/local/bin/python3.8
210+
// Symlink - /usr/local/opt/[email protected]/bin/python3.8
211+
// Symlink - /usr/local/Cellar/[email protected]/3.8.19/bin/python3.8
212+
// Real exe - /usr/local/Cellar/[email protected]/3.8.19/Frameworks/Python.framework/Versions/3.8/bin/python3.8
213+
// SysPrefix- /usr/local/Cellar/[email protected]/3.8.19/Frameworks/Python.framework/Versions/3.8
214+
get_python_info(
215+
&PathBuf::from("/usr/local/bin").join(exe_file_name),
216+
&mut reported,
217+
&python_regex,
218+
)
219+
} else if exe.starts_with("/usr/local/bin/python")
220+
|| exe.starts_with("/home/linuxbrew/.linuxbrew/bin/python")
221+
|| exe.starts_with("/home/linuxbrew/.linuxbrew/opt/python@")
222+
|| exe.starts_with("/home/linuxbrew/.linuxbrew/Cellar/python")
223+
{
224+
// Symlink - /usr/local/bin/python3.12
225+
// Symlink - /home/linuxbrew/.linuxbrew/bin/python3.12
226+
// Symlink - /home/linuxbrew/.linuxbrew/opt/[email protected]/bin/python3.12
227+
// Real exe - /home/linuxbrew/.linuxbrew/Cellar/[email protected]/3.12.3/bin/python3.12
228+
// SysPrefix- /home/linuxbrew/.linuxbrew/Cellar/[email protected]/3.12.3
229+
230+
get_python_info(
231+
&PathBuf::from("/usr/local/bin").join(exe_file_name),
232+
&mut reported,
233+
&python_regex,
234+
)
235+
} else {
236+
None
237+
}
39238
}
40239

41240
fn find(&mut self) -> Option<LocatorResult> {
42-
let homebrew_prefix = self
43-
.environment
44-
.get_env_var("HOMEBREW_PREFIX".to_string())?;
45-
let homebrew_prefix_bin = PathBuf::from(homebrew_prefix).join("bin");
241+
let homebrew_prefix_bin = get_homebrew_prefix_bin(self.environment)?;
46242
let mut reported: HashSet<String> = HashSet::new();
47243
let python_regex = Regex::new(r"/(\d+\.\d+\.\d+)/").unwrap();
48244
let mut environments: Vec<PythonEnvironment> = vec![];
49-
for file in std::fs::read_dir(homebrew_prefix_bin)
245+
for file in std::fs::read_dir(&homebrew_prefix_bin)
50246
.ok()?
51247
.filter_map(Result::ok)
52248
{
53-
if let Some(exe) = is_symlinked_python_executable(&file) {
54-
let python_version = exe.to_string_lossy().to_string();
55-
let version = match python_regex.captures(&python_version) {
56-
Some(captures) => match captures.get(1) {
57-
Some(version) => Some(version.as_str().to_string()),
58-
None => None,
59-
},
60-
None => None,
61-
};
62-
if reported.contains(&exe.to_string_lossy().to_string()) {
249+
// If this file name is `python3`, then ignore this for now.
250+
// We would prefer to use `python3.x` instead of `python3`.
251+
// That way its more consistent and future proof
252+
if let Some(file_name) = file.file_name().to_str() {
253+
if file_name.to_lowercase() == "python3" {
63254
continue;
64255
}
65-
let env_path = match exe.parent() {
66-
Some(path) => {
67-
if let Some(name) = path.file_name() {
68-
if name.to_ascii_lowercase() == "bin"
69-
|| name.to_ascii_lowercase() == "Scripts"
70-
{
71-
Some(path.parent()?.to_path_buf())
72-
} else {
73-
Some(path.to_path_buf())
74-
}
75-
} else {
76-
None
77-
}
78-
}
79-
None => continue,
80-
};
81-
reported.insert(exe.to_string_lossy().to_string());
82-
let env = crate::messaging::PythonEnvironment::new(
83-
None,
84-
None,
85-
Some(exe.clone()),
86-
crate::messaging::PythonEnvironmentCategory::Homebrew,
87-
version,
88-
env_path,
89-
None,
90-
Some(vec![exe.to_string_lossy().to_string()]),
91-
);
256+
}
257+
258+
if let Some(env) = get_python_info(&file.path(), &mut reported, &python_regex) {
92259
environments.push(env);
93260
}
94261
}
262+
263+
// Possible we do not have python3.12 or the like in bin directory
264+
// & we have only python3, in that case we should add python3 to the list
265+
if let Some(env) = get_python_info(
266+
&homebrew_prefix_bin.join("python3"),
267+
&mut reported,
268+
&python_regex,
269+
) {
270+
environments.push(env);
271+
}
272+
95273
if environments.is_empty() {
96274
None
97275
} else {

native_locator/src/main.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ fn main() {
3737

3838
let virtualenv_locator = virtualenv::VirtualEnv::new();
3939
let venv_locator = venv::Venv::new();
40-
let virtualenvwrapper_locator = virtualenvwrapper::VirtualEnvWrapper::with(&environment);
40+
let mut virtualenvwrapper = virtualenvwrapper::VirtualEnvWrapper::with(&environment);
4141
let pipenv_locator = pipenv::PipEnv::new();
4242
let mut path_locator = common_python::PythonOnPath::with(&environment);
4343
let mut conda_locator = conda::Conda::with(&environment);
@@ -54,21 +54,35 @@ fn main() {
5454
#[cfg(windows)]
5555
find_environments(&mut windows_registry, &mut dispatcher);
5656
let mut pyenv_locator = pyenv::PyEnv::with(&environment, &mut conda_locator);
57+
find_environments(&mut virtualenvwrapper, &mut dispatcher);
5758
find_environments(&mut pyenv_locator, &mut dispatcher);
5859
#[cfg(unix)]
5960
find_environments(&mut homebrew_locator, &mut dispatcher);
6061
find_environments(&mut conda_locator, &mut dispatcher);
6162
#[cfg(windows)]
6263
find_environments(&mut windows_store, &mut dispatcher);
6364

64-
// Step 2: Search in some global locations.
65+
// Step 2: Search in some global locations for virtual envs.
6566
for env in list_global_virtual_envs(&environment).iter() {
6667
if dispatcher.was_environment_reported(&env) {
6768
continue;
6869
}
6970

70-
let _ = resolve_environment(&pipenv_locator, env, &mut dispatcher)
71-
|| resolve_environment(&virtualenvwrapper_locator, env, &mut dispatcher)
71+
// First must be homebrew, as it is the most specific and supports symlinks
72+
#[cfg(unix)]
73+
let homebrew_result = resolve_environment(&homebrew_locator, env, &mut dispatcher);
74+
#[cfg(unix)]
75+
if homebrew_result {
76+
continue;
77+
}
78+
79+
let _ = // Pipeenv before virtualenvwrapper as it is more specific.
80+
// Because pipenv environments are also virtualenvwrapper environments.
81+
resolve_environment(&pipenv_locator, env, &mut dispatcher)
82+
// Before venv, as all venvs are also virtualenvwrapper environments.
83+
|| resolve_environment(&virtualenvwrapper, env, &mut dispatcher)
84+
// Before virtualenv as this is more specific.
85+
// All venvs are also virtualenvs environments.
7286
|| resolve_environment(&venv_locator, env, &mut dispatcher)
7387
|| resolve_environment(&virtualenv_locator, env, &mut dispatcher);
7488
}

0 commit comments

Comments
 (0)