diff --git a/native_locator/src/common_python.rs b/native_locator/src/common_python.rs index 3d206529035c..ef6c057b1826 100644 --- a/native_locator/src/common_python.rs +++ b/native_locator/src/common_python.rs @@ -47,6 +47,7 @@ impl Locator for PythonOnPath<'_> { env_manager: None, project_path: None, python_run_command: Some(vec![env.executable.to_str().unwrap().to_string()]), + arch: None, }) } diff --git a/native_locator/src/conda.rs b/native_locator/src/conda.rs index 23a8689ec2a7..bec00693d21b 100644 --- a/native_locator/src/conda.rs +++ b/native_locator/src/conda.rs @@ -646,6 +646,7 @@ fn get_root_python_environment(path: &PathBuf, manager: &EnvManager) -> Option

Option { - let path = path.path(); +fn is_symlinked_python_executable(path: &PathBuf) -> Option { let name = path.file_name()?.to_string_lossy(); if !name.starts_with("python") || name.ends_with("-config") || name.ends_with("-build") { return None; @@ -23,6 +22,149 @@ fn is_symlinked_python_executable(path: &DirEntry) -> Option { Some(std::fs::canonicalize(path).ok()?) } +fn get_homebrew_prefix_env_var(environment: &dyn Environment) -> Option { + if let Some(homebrew_prefix) = environment.get_env_var("HOMEBREW_PREFIX".to_string()) { + let homebrew_prefix_bin = PathBuf::from(homebrew_prefix).join("bin"); + if homebrew_prefix_bin.exists() { + return Some(homebrew_prefix_bin); + } + } + None +} + +fn get_homebrew_prefix_bin(environment: &dyn Environment) -> Option { + if let Some(homebrew_prefix) = get_homebrew_prefix_env_var(environment) { + return Some(homebrew_prefix); + } + + // Homebrew install folders documented here https://docs.brew.sh/Installation + // /opt/homebrew for Apple Silicon, + // /usr/local for macOS Intel + // /home/linuxbrew/.linuxbrew for Linux + [ + "/home/linuxbrew/.linuxbrew/bin", + "/opt/homebrew/bin", + "/usr/local/bin", + ] + .iter() + .map(|p| PathBuf::from(p)) + .find(|p| p.exists()) +} + +fn get_env_path(python_exe_from_bin_dir: &PathBuf, resolved_file: &PathBuf) -> Option { + // If the fully resolved file path contains the words `/homebrew/` or `/linuxbrew/` + // Then we know this is definitely a home brew version of python. + // And in these cases we can compute the sysprefix. + + let resolved_file = resolved_file.to_str()?; + // 1. MacOS Silicon + if python_exe_from_bin_dir + .to_string_lossy() + .to_lowercase() + .starts_with("/opt/homebrew/bin/python") + { + // Resolved exe is something like `/opt/homebrew/Cellar/python@3.12/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12` + let reg_ex = Regex::new("/opt/homebrew/Cellar/python@((\\d+\\.?)*)/(\\d+\\.?)*/Frameworks/Python.framework/Versions/(\\d+\\.?)*/bin/python(\\d+\\.?)*").unwrap(); + let captures = reg_ex.captures(&resolved_file)?; + let version = captures.get(1).map(|m| m.as_str()).unwrap_or_default(); + // SysPrefix- /opt/homebrew/opt/python@3.12/Frameworks/Python.framework/Versions/3.12 + let sys_prefix = PathBuf::from(format!( + "/opt/homebrew/opt/python@{}/Frameworks/Python.framework/Versions/{}", + version, version + )); + + return if sys_prefix.exists() { + Some(sys_prefix) + } else { + None + }; + } + + // 2. Linux + if python_exe_from_bin_dir + .to_string_lossy() + .to_lowercase() + .starts_with("/usr/local/bin/python") + { + // Resolved exe is something like `/home/linuxbrew/.linuxbrew/Cellar/python@3.12/3.12.3/bin/python3.12` + let reg_ex = Regex::new("/home/linuxbrew/.linuxbrew/Cellar/python@(\\d+\\.?\\d+\\.?)/(\\d+\\.?\\d+\\.?\\d+\\.?)/bin/python.*").unwrap(); + let captures = reg_ex.captures(&resolved_file)?; + let version = captures.get(1).map(|m| m.as_str()).unwrap_or_default(); + let full_version = captures.get(2).map(|m| m.as_str()).unwrap_or_default(); + // SysPrefix- /home/linuxbrew/.linuxbrew/Cellar/python@3.12/3.12.3 + let sys_prefix = PathBuf::from(format!( + "/home/linuxbrew/.linuxbrew/Cellar/python@{}/{}", + version, full_version + )); + + return if sys_prefix.exists() { + Some(sys_prefix) + } else { + None + }; + } + + // 3. MacOS Intel + if python_exe_from_bin_dir + .to_string_lossy() + .to_lowercase() + .starts_with("/usr/local/bin/python") + { + // Resolved exe is something like `/usr/local/Cellar/python@3.12/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12` + let reg_ex = Regex::new("/usr/local/Cellar/python@(\\d+\\.?\\d+\\.?)/(\\d+\\.?\\d+\\.?\\d+\\.?)/Frameworks/Python.framework/Versions/(\\d+\\.?\\d+\\.?)/bin/python.*").unwrap(); + let captures = reg_ex.captures(&resolved_file)?; + let version = captures.get(1).map(|m| m.as_str()).unwrap_or_default(); + let full_version = captures.get(2).map(|m| m.as_str()).unwrap_or_default(); + // SysPrefix- /usr/local/Cellar/python@3.8/3.8.19/Frameworks/Python.framework/Versions/3.8 + let sys_prefix = PathBuf::from(format!( + "/usr/local/Cellar/python@{}/{}/Frameworks/Python.framework/Versions/{}", + version, full_version, version + )); + + return if sys_prefix.exists() { + Some(sys_prefix) + } else { + None + }; + } + None +} + +fn get_python_info( + python_exe_from_bin_dir: &PathBuf, + reported: &mut HashSet, + python_version_regex: &Regex, +) -> Option { + // Possible we do not have python3.12 or the like in bin directory + // & we have only python3, in that case we should add python3 to the list + if let Some(resolved_exe) = is_symlinked_python_executable(python_exe_from_bin_dir) { + let user_friendly_exe = python_exe_from_bin_dir; + let python_version = resolved_exe.to_string_lossy().to_string(); + let version = match python_version_regex.captures(&python_version) { + Some(captures) => match captures.get(1) { + Some(version) => Some(version.as_str().to_string()), + None => None, + }, + None => None, + }; + if reported.contains(&resolved_exe.to_string_lossy().to_string()) { + return None; + } + reported.insert(resolved_exe.to_string_lossy().to_string()); + return Some(PythonEnvironment::new( + None, + None, + Some(user_friendly_exe.clone()), + crate::messaging::PythonEnvironmentCategory::Homebrew, + version, + get_env_path(python_exe_from_bin_dir, &resolved_exe), + None, + Some(vec![user_friendly_exe.to_string_lossy().to_string()]), + )); + } + None +} + pub struct Homebrew<'a> { pub environment: &'a dyn Environment, } @@ -34,64 +176,100 @@ impl Homebrew<'_> { } impl Locator for Homebrew<'_> { - fn resolve(&self, _env: &PythonEnv) -> Option { - None + fn resolve(&self, env: &PythonEnv) -> Option { + let python_regex = Regex::new(r"/(\d+\.\d+\.\d+)/").unwrap(); + let exe = env.executable.clone(); + let exe_file_name = exe.file_name()?; + let mut reported: HashSet = HashSet::new(); + if exe.starts_with("/opt/homebrew/bin/python") + || exe.starts_with("/opt/homebrew/Cellar/python@") + || exe.starts_with("/opt/homebrew/opt/python@") + || exe.starts_with("/opt/homebrew/opt/python") + || exe.starts_with("/opt/homebrew/Frameworks/Python.framework/Versions/") + { + // Symlink - /opt/homebrew/bin/python3.12 + // Symlink - /opt/homebrew/opt/python3/bin/python3.12 + // Symlink - /opt/homebrew/Cellar/python@3.12/3.12.3/bin/python3.12 + // Symlink - /opt/homebrew/opt/python@3.12/bin/python3.12 + // Symlink - /opt/homebrew/Cellar/python@3.12/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12 + // Symlink - /opt/homebrew/Cellar/python@3.12/3.12.3/Frameworks/Python.framework/Versions/Current/bin/python3.12 + // Symlink - /opt/homebrew/Frameworks/Python.framework/Versions/3.12/bin/python3.12 + // Symlink - /opt/homebrew/Frameworks/Python.framework/Versions/Current/bin/python3.12 + // Real exe - /opt/homebrew/Cellar/python@3.12/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12 + // SysPrefix- /opt/homebrew/opt/python@3.12/Frameworks/Python.framework/Versions/3.12 + get_python_info( + &PathBuf::from("/opt/homebrew/bin").join(exe_file_name), + &mut reported, + &python_regex, + ) + } else if exe.starts_with("/usr/local/bin/python") + || exe.starts_with("/usr/local/opt/python@") + || exe.starts_with("/usr/local/Cellar/python@") + { + // Symlink - /usr/local/bin/python3.8 + // Symlink - /usr/local/opt/python@3.8/bin/python3.8 + // Symlink - /usr/local/Cellar/python@3.8/3.8.19/bin/python3.8 + // Real exe - /usr/local/Cellar/python@3.8/3.8.19/Frameworks/Python.framework/Versions/3.8/bin/python3.8 + // SysPrefix- /usr/local/Cellar/python@3.8/3.8.19/Frameworks/Python.framework/Versions/3.8 + get_python_info( + &PathBuf::from("/usr/local/bin").join(exe_file_name), + &mut reported, + &python_regex, + ) + } else if exe.starts_with("/usr/local/bin/python") + || exe.starts_with("/home/linuxbrew/.linuxbrew/bin/python") + || exe.starts_with("/home/linuxbrew/.linuxbrew/opt/python@") + || exe.starts_with("/home/linuxbrew/.linuxbrew/Cellar/python") + { + // Symlink - /usr/local/bin/python3.12 + // Symlink - /home/linuxbrew/.linuxbrew/bin/python3.12 + // Symlink - /home/linuxbrew/.linuxbrew/opt/python@3.12/bin/python3.12 + // Real exe - /home/linuxbrew/.linuxbrew/Cellar/python@3.12/3.12.3/bin/python3.12 + // SysPrefix- /home/linuxbrew/.linuxbrew/Cellar/python@3.12/3.12.3 + + get_python_info( + &PathBuf::from("/usr/local/bin").join(exe_file_name), + &mut reported, + &python_regex, + ) + } else { + None + } } fn find(&mut self) -> Option { - let homebrew_prefix = self - .environment - .get_env_var("HOMEBREW_PREFIX".to_string())?; - let homebrew_prefix_bin = PathBuf::from(homebrew_prefix).join("bin"); + let homebrew_prefix_bin = get_homebrew_prefix_bin(self.environment)?; let mut reported: HashSet = HashSet::new(); let python_regex = Regex::new(r"/(\d+\.\d+\.\d+)/").unwrap(); let mut environments: Vec = vec![]; - for file in std::fs::read_dir(homebrew_prefix_bin) + for file in std::fs::read_dir(&homebrew_prefix_bin) .ok()? .filter_map(Result::ok) { - if let Some(exe) = is_symlinked_python_executable(&file) { - let python_version = exe.to_string_lossy().to_string(); - let version = match python_regex.captures(&python_version) { - Some(captures) => match captures.get(1) { - Some(version) => Some(version.as_str().to_string()), - None => None, - }, - None => None, - }; - if reported.contains(&exe.to_string_lossy().to_string()) { + // If this file name is `python3`, then ignore this for now. + // We would prefer to use `python3.x` instead of `python3`. + // That way its more consistent and future proof + if let Some(file_name) = file.file_name().to_str() { + if file_name.to_lowercase() == "python3" { continue; } - let env_path = match exe.parent() { - Some(path) => { - if let Some(name) = path.file_name() { - if name.to_ascii_lowercase() == "bin" - || name.to_ascii_lowercase() == "Scripts" - { - Some(path.parent()?.to_path_buf()) - } else { - Some(path.to_path_buf()) - } - } else { - None - } - } - None => continue, - }; - reported.insert(exe.to_string_lossy().to_string()); - let env = crate::messaging::PythonEnvironment::new( - None, - None, - Some(exe.clone()), - crate::messaging::PythonEnvironmentCategory::Homebrew, - version, - env_path, - None, - Some(vec![exe.to_string_lossy().to_string()]), - ); + } + + if let Some(env) = get_python_info(&file.path(), &mut reported, &python_regex) { environments.push(env); } } + + // Possible we do not have python3.12 or the like in bin directory + // & we have only python3, in that case we should add python3 to the list + if let Some(env) = get_python_info( + &homebrew_prefix_bin.join("python3"), + &mut reported, + &python_regex, + ) { + environments.push(env); + } + if environments.is_empty() { None } else { diff --git a/native_locator/src/main.rs b/native_locator/src/main.rs index ee976bf756d2..2e9989347929 100644 --- a/native_locator/src/main.rs +++ b/native_locator/src/main.rs @@ -37,7 +37,7 @@ fn main() { let virtualenv_locator = virtualenv::VirtualEnv::new(); let venv_locator = venv::Venv::new(); - let virtualenvwrapper_locator = virtualenvwrapper::VirtualEnvWrapper::with(&environment); + let mut virtualenvwrapper = virtualenvwrapper::VirtualEnvWrapper::with(&environment); let pipenv_locator = pipenv::PipEnv::new(); let mut path_locator = common_python::PythonOnPath::with(&environment); let mut conda_locator = conda::Conda::with(&environment); @@ -54,6 +54,7 @@ fn main() { #[cfg(windows)] find_environments(&mut windows_registry, &mut dispatcher); let mut pyenv_locator = pyenv::PyEnv::with(&environment, &mut conda_locator); + find_environments(&mut virtualenvwrapper, &mut dispatcher); find_environments(&mut pyenv_locator, &mut dispatcher); #[cfg(unix)] find_environments(&mut homebrew_locator, &mut dispatcher); @@ -61,14 +62,27 @@ fn main() { #[cfg(windows)] find_environments(&mut windows_store, &mut dispatcher); - // Step 2: Search in some global locations. + // Step 2: Search in some global locations for virtual envs. for env in list_global_virtual_envs(&environment).iter() { if dispatcher.was_environment_reported(&env) { continue; } - let _ = resolve_environment(&pipenv_locator, env, &mut dispatcher) - || resolve_environment(&virtualenvwrapper_locator, env, &mut dispatcher) + // First must be homebrew, as it is the most specific and supports symlinks + #[cfg(unix)] + let homebrew_result = resolve_environment(&homebrew_locator, env, &mut dispatcher); + #[cfg(unix)] + if homebrew_result { + continue; + } + + let _ = // Pipeenv before virtualenvwrapper as it is more specific. + // Because pipenv environments are also virtualenvwrapper environments. + resolve_environment(&pipenv_locator, env, &mut dispatcher) + // Before venv, as all venvs are also virtualenvwrapper environments. + || resolve_environment(&virtualenvwrapper, env, &mut dispatcher) + // Before virtualenv as this is more specific. + // All venvs are also virtualenvs environments. || resolve_environment(&venv_locator, env, &mut dispatcher) || resolve_environment(&virtualenv_locator, env, &mut dispatcher); } diff --git a/native_locator/src/messaging.rs b/native_locator/src/messaging.rs index 73e708dcac5f..1365949e703e 100644 --- a/native_locator/src/messaging.rs +++ b/native_locator/src/messaging.rs @@ -80,6 +80,14 @@ pub enum PythonEnvironmentCategory { VirtualEnv, } +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +#[derive(Debug)] +pub enum Architecture { + X64, + X86, +} + #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] #[derive(Debug)] @@ -96,6 +104,7 @@ pub struct PythonEnvironment { * The project path for the Pipenv environment. */ pub project_path: Option, + pub arch: Option, } impl PythonEnvironment { @@ -119,6 +128,7 @@ impl PythonEnvironment { env_manager, python_run_command, project_path: None, + arch: None, } } } diff --git a/native_locator/src/pipenv.rs b/native_locator/src/pipenv.rs index bb5eab5776fe..6eacb3601dba 100644 --- a/native_locator/src/pipenv.rs +++ b/native_locator/src/pipenv.rs @@ -18,6 +18,17 @@ fn get_pipenv_project(env: &PythonEnv) -> Option { None } +fn is_pipenv(env: &PythonEnv) -> bool { + // If we have a Pipfile, then this is a pipenv environment. + // Else likely a virtualenvwrapper or the like. + if let Some(project_path) = get_pipenv_project(env) { + if project_path.join("Pipfile").exists() { + return true; + } + } + false +} + pub struct PipEnv {} impl PipEnv { @@ -28,6 +39,9 @@ impl PipEnv { impl Locator for PipEnv { fn resolve(&self, env: &PythonEnv) -> Option { + if !is_pipenv(env) { + return None; + } let project_path = get_pipenv_project(env)?; Some(PythonEnvironment { display_name: None, @@ -39,6 +53,7 @@ impl Locator for PipEnv { env_manager: None, python_run_command: Some(vec![env.executable.to_str().unwrap().to_string()]), project_path: Some(project_path), + arch: None, }) } diff --git a/native_locator/src/pyenv.rs b/native_locator/src/pyenv.rs index 9137a80df1f7..ba0395f2738c 100644 --- a/native_locator/src/pyenv.rs +++ b/native_locator/src/pyenv.rs @@ -66,7 +66,7 @@ fn get_pyenv_binary(environment: &dyn known::Environment) -> Option { } } -fn get_pyenv_version(folder_name: &String) -> Option { +fn get_version(folder_name: &String) -> Option { // Stable Versions = like 3.10.10 let python_regex = Regex::new(r"^(\d+\.\d+\.\d+)$").unwrap(); match python_regex.captures(&folder_name) { @@ -90,7 +90,17 @@ fn get_pyenv_version(folder_name: &String) -> Option { Some(version) => Some(version.as_str().to_string()), None => None, }, - None => None, + None => { + // win32 versions, rc Versions = like 3.11.0a-win32 + let python_regex = Regex::new(r"^(\d+\.\d+.\d+\w\d+)-win32").unwrap(); + match python_regex.captures(&folder_name) { + Some(captures) => match captures.get(1) { + Some(version) => Some(version.as_str().to_string()), + None => None, + }, + None => None, + } + } } } } @@ -103,8 +113,9 @@ fn get_pure_python_environment( path: &PathBuf, manager: &Option, ) -> Option { - let version = get_pyenv_version(&path.file_name().unwrap().to_string_lossy().to_string())?; - Some(messaging::PythonEnvironment::new( + let file_name = path.file_name()?.to_string_lossy().to_string(); + let version = get_version(&file_name)?; + let mut env = messaging::PythonEnvironment::new( None, None, Some(executable.clone()), @@ -117,7 +128,12 @@ fn get_pure_python_environment( .into_os_string() .into_string() .unwrap()]), - )) + ); + if file_name.ends_with("-win32") { + env.arch = Some(messaging::Architecture::X86); + } + + Some(env) } fn is_conda_environment(path: &PathBuf) -> bool { @@ -189,6 +205,47 @@ pub fn list_pyenv_environments( Some(envs) } +#[cfg(windows)] +fn get_pyenv_manager_version( + pyenv_binary_path: &PathBuf, + environment: &dyn known::Environment, +) -> Option { + // In windows, the version is stored in the `.pyenv/.version` file + let pyenv_dir = get_pyenv_dir(environment)?; + let mut version_file = PathBuf::from(&pyenv_dir).join(".version"); + if !version_file.exists() { + // We might have got the path `~/.pyenv/pyenv-win` + version_file = pyenv_dir.parent()?.join(".version"); + if !version_file.exists() { + return None; + } + } + let version = fs::read_to_string(version_file).ok()?; + let version_regex = Regex::new(r"(\d+\.\d+\.\d+)").unwrap(); + let captures = version_regex.captures(&version)?.get(1)?; + Some(captures.as_str().to_string()) +} + +#[cfg(unix)] +fn get_pyenv_manager_version( + pyenv_binary_path: &PathBuf, + _environment: &dyn known::Environment, +) -> Option { + // Look for version in path + // Sample /opt/homebrew/Cellar/pyenv/2.4.0/libexec/pyenv + if !pyenv_binary_path.to_string_lossy().contains("/pyenv/") { + return None; + } + // Find the real path, generally we have a symlink. + let real_path = fs::read_link(pyenv_binary_path) + .ok()? + .to_string_lossy() + .to_string(); + let version_regex = Regex::new(r"pyenv/(\d+\.\d+\.\d+)/").unwrap(); + let captures = version_regex.captures(&real_path)?.get(1)?; + Some(captures.as_str().to_string()) +} + pub struct PyEnv<'a> { pub environment: &'a dyn Environment, pub conda_locator: &'a mut dyn CondaLocator, @@ -214,7 +271,8 @@ impl Locator for PyEnv<'_> { fn find(&mut self) -> Option { let pyenv_binary = get_pyenv_binary(self.environment)?; - let manager = messaging::EnvManager::new(pyenv_binary, None, EnvManagerType::Pyenv); + let version = get_pyenv_manager_version(&pyenv_binary, self.environment); + let manager = messaging::EnvManager::new(pyenv_binary, version, EnvManagerType::Pyenv); let mut environments: Vec = vec![]; if let Some(envs) = list_pyenv_environments(&Some(manager.clone()), self.environment, self.conda_locator) diff --git a/native_locator/src/utils.rs b/native_locator/src/utils.rs index c70efe9654ef..d9a30c3a7f8a 100644 --- a/native_locator/src/utils.rs +++ b/native_locator/src/utils.rs @@ -6,7 +6,6 @@ use regex::Regex; use std::{ fs, path::{Path, PathBuf}, - process::Command, }; #[derive(Debug)] @@ -96,16 +95,7 @@ pub fn get_version(python_executable: &PathBuf) -> Option { return Some(pyenv_cfg.version); } } - - let output = Command::new(python_executable) - .arg("-c") - .arg("import sys; print(sys.version)") - .output() - .ok()?; - let output = String::from_utf8(output.stdout).ok()?; - let output = output.trim(); - let output = output.split_whitespace().next().unwrap_or(output); - Some(output.to_string()) + None } pub fn find_python_binary_path(env_path: &Path) -> Option { diff --git a/native_locator/src/venv.rs b/native_locator/src/venv.rs index 94040a536989..702bf8b6dcc9 100644 --- a/native_locator/src/venv.rs +++ b/native_locator/src/venv.rs @@ -43,6 +43,7 @@ impl Locator for Venv { env_manager: None, project_path: None, python_run_command: Some(vec![env.executable.to_str().unwrap().to_string()]), + arch: None, }); } None diff --git a/native_locator/src/virtualenv.rs b/native_locator/src/virtualenv.rs index 2a6909e63fa2..209d72c5f533 100644 --- a/native_locator/src/virtualenv.rs +++ b/native_locator/src/virtualenv.rs @@ -74,6 +74,7 @@ impl Locator for VirtualEnv { env_manager: None, project_path: None, python_run_command: Some(vec![env.executable.to_str().unwrap().to_string()]), + arch: None, }); } None diff --git a/native_locator/src/virtualenvwrapper.rs b/native_locator/src/virtualenvwrapper.rs index d55a89e09dca..54728b4cb644 100644 --- a/native_locator/src/virtualenvwrapper.rs +++ b/native_locator/src/virtualenvwrapper.rs @@ -6,6 +6,7 @@ use crate::messaging::PythonEnvironment; use crate::utils::list_python_environments; use crate::virtualenv; use crate::{known::Environment, utils::PythonEnv}; +use std::fs; use std::path::PathBuf; #[cfg(windows)] @@ -29,7 +30,7 @@ fn get_default_virtualenvwrapper_path(environment: &dyn Environment) -> Option

Option { if let Some(home) = environment.get_user_home() { - let home = PathBuf::from(home).join("virtualenvs"); + let home = PathBuf::from(home).join(".virtualenvs"); if home.exists() { return Some(home); } @@ -37,7 +38,7 @@ fn get_default_virtualenvwrapper_path(environment: &dyn Environment) -> Option

Option { +pub fn get_work_on_home_path(environment: &dyn Environment) -> Option { // The WORKON_HOME variable contains the path to the root directory of all virtualenvwrapper environments. // If the interpreter path belongs to one of them then it is a virtualenvwrapper type of environment. if let Some(work_on_home) = environment.get_env_var("WORKON_HOME".to_string()) { @@ -54,6 +55,7 @@ pub fn is_virtualenvwrapper(env: &PythonEnv, environment: &dyn Environment) -> b if env.path.is_none() { return false; } + // For environment to be a virtualenvwrapper based it has to follow these two rules: // 1. It should be in a sub-directory under the WORKON_HOME // 2. It should be a valid virtualenv environment @@ -66,6 +68,17 @@ pub fn is_virtualenvwrapper(env: &PythonEnv, environment: &dyn Environment) -> b false } +fn get_project(env: &PythonEnv) -> Option { + let project_file = env.path.clone()?.join(".project"); + if let Ok(contents) = fs::read_to_string(project_file) { + let project_folder = PathBuf::from(contents.trim().to_string()); + if project_folder.exists() { + return Some(project_folder); + } + } + None +} + pub struct VirtualEnvWrapper<'a> { pub environment: &'a dyn Environment, } @@ -78,28 +91,29 @@ impl VirtualEnvWrapper<'_> { impl Locator for VirtualEnvWrapper<'_> { fn resolve(&self, env: &PythonEnv) -> Option { - if is_virtualenvwrapper(env, self.environment) { - return Some(PythonEnvironment { - display_name: None, - name: Some( - env.path - .clone() - .expect("env.path cannot be empty for virtualenv rapper") - .file_name() - .unwrap() - .to_string_lossy() - .to_string(), - ), - python_executable_path: Some(env.executable.clone()), - version: env.version.clone(), - category: crate::messaging::PythonEnvironmentCategory::Venv, - env_path: env.path.clone(), - env_manager: None, - project_path: None, - python_run_command: Some(vec![env.executable.to_str().unwrap().to_string()]), - }); + if !is_virtualenvwrapper(env, self.environment) { + return None; } - None + Some(PythonEnvironment { + display_name: None, + name: Some( + env.path + .clone() + .expect("env.path cannot be empty for virtualenv rapper") + .file_name() + .unwrap() + .to_string_lossy() + .to_string(), + ), + python_executable_path: Some(env.executable.clone()), + version: env.version.clone(), + category: crate::messaging::PythonEnvironmentCategory::VirtualEnvWrapper, + env_path: env.path.clone(), + env_manager: None, + project_path: get_project(env), + python_run_command: Some(vec![env.executable.to_str().unwrap().to_string()]), + arch: None, + }) } fn find(&mut self) -> Option { @@ -111,7 +125,6 @@ impl Locator for VirtualEnvWrapper<'_> { environments.push(env); } }); - if environments.is_empty() { None } else { diff --git a/native_locator/src/windows_store.rs b/native_locator/src/windows_store.rs index f08622d08127..39f3a01f4ac9 100644 --- a/native_locator/src/windows_store.rs +++ b/native_locator/src/windows_store.rs @@ -61,6 +61,7 @@ fn list_windows_store_python_executables( env_manager: None, project_path: None, python_run_command: Some(vec![exe.to_string_lossy().to_string()]), + arch: None, }; python_envs.push(env); } @@ -94,8 +95,6 @@ fn get_package_display_name_and_location(name: String, hkcu: &RegKey) -> Option< let display_name = package_key.get_value("DisplayName").ok()?; let env_path = package_key.get_value("PackageRootFolder").ok()?; - let regex = regex::Regex::new("PythonSoftwareFoundation.Python.((\\d+\\.?)*)_.*").ok()?; - return Some(StorePythonInfo { display_name, env_path, @@ -131,6 +130,7 @@ impl Locator for WindowsStore<'_> { env_manager: None, project_path: None, python_run_command: Some(vec![env.executable.to_str().unwrap().to_string()]), + arch: None, }); } None diff --git a/native_locator/tests/common.rs b/native_locator/tests/common.rs index bf4c54617f16..1df03a005a73 100644 --- a/native_locator/tests/common.rs +++ b/native_locator/tests/common.rs @@ -20,10 +20,12 @@ pub fn join_test_paths(paths: &[&str]) -> PathBuf { path } +#[allow(dead_code)] pub trait TestMessages { fn get_messages(&self) -> Vec; } +#[allow(dead_code)] pub struct TestEnvironment { vars: HashMap, home: Option, diff --git a/native_locator/tests/common_python_test.rs b/native_locator/tests/common_python_test.rs index d7d71fd5cce1..c8a5baa3b10a 100644 --- a/native_locator/tests/common_python_test.rs +++ b/native_locator/tests/common_python_test.rs @@ -41,6 +41,7 @@ fn find_python_in_path_this() { version: None, python_run_command: Some(vec![unix_python_exe.clone().to_str().unwrap().to_string()]), env_path: Some(user_home.clone()), + arch: None, }; assert_messages( &[json!(env)], diff --git a/native_locator/tests/conda_test.rs b/native_locator/tests/conda_test.rs index 925219b098bb..95e48917bd82 100644 --- a/native_locator/tests/conda_test.rs +++ b/native_locator/tests/conda_test.rs @@ -196,6 +196,7 @@ fn find_conda_from_custom_install_location() { conda_dir.to_string_lossy().to_string(), "python".to_string(), ]), + arch: None, }; assert_eq!(json!(expected_conda_env), json!(result.environments[0])); @@ -265,6 +266,7 @@ fn finds_two_conda_envs_from_known_location() { "one".to_string(), "python".to_string(), ]), + arch: None, }; let expected_conda_2 = PythonEnvironment { display_name: None, @@ -282,6 +284,7 @@ fn finds_two_conda_envs_from_known_location() { "two".to_string(), "python".to_string(), ]), + arch: None, }; assert_messages( &[json!(expected_conda_1), json!(expected_conda_2)], diff --git a/native_locator/tests/pyenv_test.rs b/native_locator/tests/pyenv_test.rs index 87761114089d..c9782e9c5d35 100644 --- a/native_locator/tests/pyenv_test.rs +++ b/native_locator/tests/pyenv_test.rs @@ -71,8 +71,7 @@ fn does_not_find_any_pyenv_envs_even_with_pyenv_installed() { #[cfg(unix)] fn find_pyenv_envs() { use crate::common::{ - assert_messages, create_test_environment, join_test_paths, - test_file_path, + assert_messages, create_test_environment, join_test_paths, test_file_path, }; use python_finder::conda::Conda; use python_finder::locator::Locator; @@ -128,7 +127,8 @@ fn find_pyenv_envs() { home.to_str().unwrap(), ".pyenv/versions/3.9.9" ])), - env_manager: Some(expected_manager.clone()) + env_manager: Some(expected_manager.clone()), + arch: None }); let expected_virtual_env = PythonEnvironment { display_name: None, @@ -152,6 +152,7 @@ fn find_pyenv_envs() { ".pyenv/versions/my-virtual-env", ])), env_manager: Some(expected_manager.clone()), + arch: None, }; let expected_3_12_1 = PythonEnvironment { display_name: None, @@ -175,6 +176,7 @@ fn find_pyenv_envs() { ".pyenv/versions/3.12.1", ])), env_manager: Some(expected_manager.clone()), + arch: None, }; let expected_3_13_dev = PythonEnvironment { display_name: None, @@ -198,6 +200,7 @@ fn find_pyenv_envs() { ".pyenv/versions/3.13-dev", ])), env_manager: Some(expected_manager.clone()), + arch: None, }; let expected_3_12_1a3 = PythonEnvironment { display_name: None, @@ -221,6 +224,7 @@ fn find_pyenv_envs() { ".pyenv/versions/3.12.1a3", ])), env_manager: Some(expected_manager.clone()), + arch: None, }; assert_messages(