diff --git a/native_locator/src/common_python.rs b/native_locator/src/common_python.rs index ef6c057b1826..67ad94ed40a1 100644 --- a/native_locator/src/common_python.rs +++ b/native_locator/src/common_python.rs @@ -39,15 +39,12 @@ impl Locator for PythonOnPath<'_> { } Some(PythonEnvironment { display_name: None, - name: None, python_executable_path: Some(env.executable.clone()), version: env.version.clone(), category: crate::messaging::PythonEnvironmentCategory::System, env_path: env.path.clone(), - env_manager: None, - project_path: None, python_run_command: Some(vec![env.executable.to_str().unwrap().to_string()]), - arch: None, + ..Default::default() }) } diff --git a/native_locator/src/conda.rs b/native_locator/src/conda.rs index bec00693d21b..8c7164b472d6 100644 --- a/native_locator/src/conda.rs +++ b/native_locator/src/conda.rs @@ -6,6 +6,7 @@ use crate::known::Environment; use crate::locator::Locator; use crate::locator::LocatorResult; use crate::messaging; +use crate::messaging::Architecture; use crate::messaging::EnvManager; use crate::messaging::EnvManagerType; use crate::messaging::PythonEnvironment; @@ -14,9 +15,11 @@ use crate::utils::{find_python_binary_path, get_environment_key, get_environment use log::trace; use log::warn; use regex::Regex; +use serde::Deserialize; use std::collections::HashMap; use std::collections::HashSet; use std::env; +use std::fs::read_to_string; use std::path::{Path, PathBuf}; /// Specifically returns the file names that are valid for 'conda' on windows @@ -57,6 +60,13 @@ struct CondaPackage { #[allow(dead_code)] path: PathBuf, version: String, + arch: Option, +} + +#[derive(Deserialize, Debug)] +struct CondaMetaPackageStructure { + channel: Option, + // version: Option, } /// Get the path to the json file along with the version of a package in the conda environment from the 'conda-meta' directory. @@ -72,16 +82,38 @@ fn get_conda_package_json_path(path: &Path, package: &str) -> Option Some(CondaPackage { + if let Some(version) = regex.clone().ok().unwrap().captures(&file_name)?.get(1) { + let mut arch: Option = None; + // Sample contents + // { + // "build": "h966fe2a_2", + // "build_number": 2, + // "channel": "https://repo.anaconda.com/pkgs/main/win-64", + // "constrains": [], + // } + // 32bit channel is https://repo.anaconda.com/pkgs/main/win-32/ + // 64bit channel is "channel": "https://repo.anaconda.com/pkgs/main/osx-arm64", + if let Some(contents) = read_to_string(&path).ok() { + if let Some(js) = + serde_json::from_str::(&contents).ok() + { + if let Some(channel) = js.channel { + if channel.ends_with("64") { + arch = Some(Architecture::X64); + } else if channel.ends_with("32") { + arch = Some(Architecture::X86); + } + } + } + } + return Some(CondaPackage { path: path.clone(), version: version.as_str().to_string(), - }), - None => None, + arch, + }); } - } else { - None } + None }) } @@ -202,6 +234,8 @@ fn get_conda_manager(path: &PathBuf) -> Option { executable_path: conda_exe, version: Some(conda_pkg.version), tool: EnvManagerType::Conda, + company: None, + company_display_name: None, }) } @@ -213,6 +247,7 @@ struct CondaEnvironment { python_executable_path: Option, version: Option, conda_install_folder: Option, + arch: Option, } fn get_conda_environment_info(env_path: &PathBuf, named: bool) -> Option { let metadata = env_path.metadata(); @@ -229,6 +264,7 @@ fn get_conda_environment_info(env_path: &PathBuf, named: bool) -> Option Option Option Option

Option

= vec![]; let mut detected_envs: HashSet = HashSet::new(); let mut detected_managers: HashSet = HashSet::new(); - if conda_install_folder.is_dir() && conda_install_folder.exists() { - if let Some(manager) = get_conda_manager(&conda_install_folder) { - // 1. Base environment. - if let Some(env) = get_root_python_environment(&conda_install_folder, &manager) { - if let Some(env_path) = env.clone().env_path { - possible_conda_envs.remove(&env_path); - let key = env_path.to_string_lossy().to_string(); - if !detected_envs.contains(&key) { - detected_envs.insert(key); - environments.push(env); - } + if !conda_install_folder.is_dir() || !conda_install_folder.exists() { + return None; + } + + if let Some(manager) = get_conda_manager(&conda_install_folder) { + // 1. Base environment. + if let Some(env) = get_root_python_environment(&conda_install_folder, &manager) { + if let Some(env_path) = env.clone().env_path { + possible_conda_envs.remove(&env_path); + let key = env_path.to_string_lossy().to_string(); + if !detected_envs.contains(&key) { + detected_envs.insert(key); + environments.push(env); } } + } + + // 2. All environments in the `/envs` folder + let mut envs: Vec = vec![]; + if let Some(environments) = + get_environments_from_envs_folder_in_conda_directory(conda_install_folder) + { + environments.iter().for_each(|env| { + possible_conda_envs.remove(&env.env_path); + envs.push(env.clone()); + }); + } - // 2. All environments in the `/envs` folder - let mut envs: Vec = vec![]; - if let Some(environments) = - get_environments_from_envs_folder_in_conda_directory(conda_install_folder) + // 3. All environments in the environments.txt and other locations (such as `conda config --show envs_dirs`) + // Only include those environments that were created by the specific conda installation + // Ignore environments that are in the env sub directory of the conda folder, as those would have been + // tracked elsewhere, we're only interested in conda envs located in other parts of the file system created using the -p flag. + // E.g conda_install_folder is `/` + // Then all folders such as `//envs/env1` can be ignored + // As these would have been discovered in previous step. + for (key, env) in possible_conda_envs.clone().iter() { + if env + .env_path + .to_string_lossy() + .contains(conda_install_folder.to_str().unwrap()) { - environments.iter().for_each(|env| { - possible_conda_envs.remove(&env.env_path); - envs.push(env.clone()); - }); + continue; } - - // 3. All environments in the environments.txt and other locations (such as `conda config --show envs_dirs`) - // Only include those environments that were created by the specific conda installation - // Ignore environments that are in the env sub directory of the conda folder, as those would have been - // tracked elsewhere, we're only interested in conda envs located in other parts of the file system created using the -p flag. - // E.g conda_install_folder is `/` - // Then all folders such as `//envs/env1` can be ignored - // As these would have been discovered in previous step. - for (key, env) in possible_conda_envs.clone().iter() { - if env - .env_path - .to_string_lossy() - .contains(conda_install_folder.to_str().unwrap()) - { - continue; - } - if was_conda_environment_created_by_specific_conda(&env, conda_install_folder) { - envs.push(env.clone()); - possible_conda_envs.remove(key); - } + if was_conda_environment_created_by_specific_conda(&env, conda_install_folder) { + envs.push(env.clone()); + possible_conda_envs.remove(key); } + } - // Finally construct the PythonEnvironment objects - envs.iter().for_each(|env| { - let exe = env.python_executable_path.clone(); - let env = PythonEnvironment::new( - None, - Some(env.name.clone()), - exe.clone(), - messaging::PythonEnvironmentCategory::Conda, - env.version.clone(), - Some(env.env_path.clone()), - Some(manager.clone()), - get_activation_command(env, &manager), - ); - if let Some(key) = get_environment_key(&env) { - if !detected_envs.contains(&key) { - detected_envs.insert(key); - environments.push(env); - } + // Finally construct the PythonEnvironment objects + envs.iter().for_each(|env| { + let exe = env.python_executable_path.clone(); + let arch = env.arch.clone(); + let mut env = PythonEnvironment::new( + None, + Some(env.name.clone()), + exe.clone(), + messaging::PythonEnvironmentCategory::Conda, + env.version.clone(), + Some(env.env_path.clone()), + Some(manager.clone()), + get_activation_command(env, &manager), + ); + env.arch = arch; + if let Some(key) = get_environment_key(&env) { + if !detected_envs.contains(&key) { + detected_envs.insert(key); + environments.push(env); } - }); - - let key = get_environment_manager_key(&manager); - if !detected_managers.contains(&key) { - detected_managers.insert(key); - managers.push(manager); } + }); + + let key = get_environment_manager_key(&manager); + if !detected_managers.contains(&key) { + detected_managers.insert(key); + managers.push(manager); } } @@ -973,6 +1020,9 @@ impl Conda<'_> { impl CondaLocator for Conda<'_> { fn find_in(&mut self, possible_conda_folder: &PathBuf) -> Option { + if !is_conda_install_location(possible_conda_folder) { + return None; + } let mut possible_conda_envs = get_known_conda_envs_from_various_locations(self.environment); self.filter_result(get_conda_environments_in_specified_install_path( possible_conda_folder, diff --git a/native_locator/src/homebrew.rs b/native_locator/src/homebrew.rs index 8709aa16b454..78cefd2cd74b 100644 --- a/native_locator/src/homebrew.rs +++ b/native_locator/src/homebrew.rs @@ -170,6 +170,7 @@ pub struct Homebrew<'a> { } impl Homebrew<'_> { + #[cfg(unix)] pub fn with<'a>(environment: &'a impl Environment) -> Homebrew { Homebrew { environment } } diff --git a/native_locator/src/known.rs b/native_locator/src/known.rs index 6e37d897157e..bd9e7bc4de34 100644 --- a/native_locator/src/known.rs +++ b/native_locator/src/known.rs @@ -7,6 +7,7 @@ pub trait Environment { /** * Only used in tests, this is the root `/`. */ + #[allow(dead_code)] fn get_root(&self) -> Option; fn get_env_var(&self, key: String) -> Option; fn get_know_global_search_locations(&self) -> Vec; diff --git a/native_locator/src/main.rs b/native_locator/src/main.rs index 2e9989347929..4964b09cac40 100644 --- a/native_locator/src/main.rs +++ b/native_locator/src/main.rs @@ -29,7 +29,7 @@ mod windows_store; fn main() { let environment = EnvironmentApi {}; - initialize_logger(LevelFilter::Debug); + initialize_logger(LevelFilter::Trace); log::info!("Starting Native Locator"); let now = SystemTime::now(); @@ -52,6 +52,8 @@ fn main() { // Step 1: These environments take precedence over all others. // As they are very specific and guaranteed to be specific type. #[cfg(windows)] + find_environments(&mut windows_store, &mut dispatcher); + #[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); @@ -59,8 +61,6 @@ fn main() { #[cfg(unix)] find_environments(&mut homebrew_locator, &mut dispatcher); find_environments(&mut conda_locator, &mut dispatcher); - #[cfg(windows)] - find_environments(&mut windows_store, &mut dispatcher); // Step 2: Search in some global locations for virtual envs. for env in list_global_virtual_envs(&environment).iter() { diff --git a/native_locator/src/messaging.rs b/native_locator/src/messaging.rs index 1365949e703e..808da631f455 100644 --- a/native_locator/src/messaging.rs +++ b/native_locator/src/messaging.rs @@ -8,7 +8,7 @@ use crate::{ use env_logger::Builder; use log::LevelFilter; use serde::{Deserialize, Serialize}; -use std::{collections::HashSet, path::PathBuf}; +use std::{collections::HashSet, path::PathBuf, time::UNIX_EPOCH}; pub trait MessageDispatcher { fn was_environment_reported(&self, env: &PythonEnv) -> bool; @@ -32,6 +32,8 @@ pub struct EnvManager { pub executable_path: PathBuf, pub version: Option, pub tool: EnvManagerType, + pub company: Option, + pub company_display_name: Option, } impl EnvManager { @@ -40,6 +42,8 @@ impl EnvManager { executable_path, version, tool, + company: None, + company_display_name: None, } } } @@ -105,6 +109,33 @@ pub struct PythonEnvironment { */ pub project_path: Option, pub arch: Option, + pub symlinks: Option>, + pub creation_time: Option, + pub modified_time: Option, + pub company: Option, + pub company_display_name: Option, +} + +impl Default for PythonEnvironment { + fn default() -> Self { + Self { + display_name: None, + name: None, + python_executable_path: None, + category: PythonEnvironmentCategory::System, + version: None, + env_path: None, + env_manager: None, + python_run_command: None, + project_path: None, + arch: None, + symlinks: None, + creation_time: None, + modified_time: None, + company: None, + company_display_name: None, + } + } } impl PythonEnvironment { @@ -129,6 +160,11 @@ impl PythonEnvironment { python_run_command, project_path: None, arch: None, + symlinks: None, + creation_time: None, + modified_time: None, + company: None, + company_display_name: None, } } } @@ -222,12 +258,29 @@ impl MessageDispatcher for JsonRpcDispatcher { } fn report_environment(&mut self, env: PythonEnvironment) -> () { if let Some(key) = get_environment_key(&env) { + if let Some(ref manager) = env.env_manager { + self.report_environment_manager(manager.clone()); + } if !self.reported_environments.contains(&key) { self.reported_environments.insert(key); - send_message(PythonEnvironmentMessage::new(env.clone())); - } - if let Some(manager) = env.env_manager { - self.report_environment_manager(manager); + + // Get the creation and modified times. + let mut env = env.clone(); + if let Some(ref exe) = env.python_executable_path { + if let Ok(metadata) = exe.metadata() { + if let Ok(ctime) = metadata.created() { + if let Ok(ctime) = ctime.duration_since(UNIX_EPOCH) { + env.creation_time = Some(ctime.as_millis()); + } + } + if let Ok(mtime) = metadata.modified() { + if let Ok(mtime) = mtime.duration_since(UNIX_EPOCH) { + env.modified_time = Some(mtime.as_millis()); + } + } + } + } + send_message(PythonEnvironmentMessage::new(env)); } } } diff --git a/native_locator/src/pipenv.rs b/native_locator/src/pipenv.rs index 6eacb3601dba..cb49c1c6ef33 100644 --- a/native_locator/src/pipenv.rs +++ b/native_locator/src/pipenv.rs @@ -44,16 +44,13 @@ impl Locator for PipEnv { } let project_path = get_pipenv_project(env)?; Some(PythonEnvironment { - display_name: None, - name: None, python_executable_path: Some(env.executable.clone()), category: crate::messaging::PythonEnvironmentCategory::Pipenv, version: env.version.clone(), env_path: env.path.clone(), - env_manager: None, python_run_command: Some(vec![env.executable.to_str().unwrap().to_string()]), project_path: Some(project_path), - arch: None, + ..Default::default() }) } diff --git a/native_locator/src/pyenv.rs b/native_locator/src/pyenv.rs index ba0395f2738c..e87729de5eda 100644 --- a/native_locator/src/pyenv.rs +++ b/native_locator/src/pyenv.rs @@ -207,7 +207,7 @@ pub fn list_pyenv_environments( #[cfg(windows)] fn get_pyenv_manager_version( - pyenv_binary_path: &PathBuf, + _pyenv_binary_path: &PathBuf, environment: &dyn known::Environment, ) -> Option { // In windows, the version is stored in the `.pyenv/.version` file diff --git a/native_locator/src/venv.rs b/native_locator/src/venv.rs index 702bf8b6dcc9..0df22263e0f3 100644 --- a/native_locator/src/venv.rs +++ b/native_locator/src/venv.rs @@ -26,7 +26,6 @@ impl Locator for Venv { fn resolve(&self, env: &PythonEnv) -> Option { if is_venv(&env) { return Some(PythonEnvironment { - display_name: None, name: Some( env.path .clone() @@ -40,10 +39,8 @@ impl Locator for Venv { 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()]), - arch: None, + ..Default::default() }); } None diff --git a/native_locator/src/virtualenv.rs b/native_locator/src/virtualenv.rs index 209d72c5f533..9532d46faa73 100644 --- a/native_locator/src/virtualenv.rs +++ b/native_locator/src/virtualenv.rs @@ -57,7 +57,6 @@ impl Locator for VirtualEnv { fn resolve(&self, env: &PythonEnv) -> Option { if is_virtualenv(env) { return Some(PythonEnvironment { - display_name: None, name: Some( env.path .clone() @@ -71,10 +70,8 @@ impl Locator for VirtualEnv { version: env.version.clone(), category: crate::messaging::PythonEnvironmentCategory::VirtualEnv, env_path: env.path.clone(), - env_manager: None, - project_path: None, python_run_command: Some(vec![env.executable.to_str().unwrap().to_string()]), - arch: None, + ..Default::default() }); } None diff --git a/native_locator/src/virtualenvwrapper.rs b/native_locator/src/virtualenvwrapper.rs index 54728b4cb644..9a06fc2494cb 100644 --- a/native_locator/src/virtualenvwrapper.rs +++ b/native_locator/src/virtualenvwrapper.rs @@ -95,7 +95,6 @@ impl Locator for VirtualEnvWrapper<'_> { return None; } Some(PythonEnvironment { - display_name: None, name: Some( env.path .clone() @@ -109,10 +108,9 @@ impl Locator for VirtualEnvWrapper<'_> { 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, + ..Default::default() }) } diff --git a/native_locator/src/windows_registry.rs b/native_locator/src/windows_registry.rs index 4f8e97710fc2..0e7a06eddec1 100644 --- a/native_locator/src/windows_registry.rs +++ b/native_locator/src/windows_registry.rs @@ -17,16 +17,70 @@ use std::path::PathBuf; use winreg::RegKey; #[cfg(windows)] -fn get_registry_pythons_from_key(hk: &RegKey, company: &str) -> Option> { +fn get_registry_pythons_from_key( + hk: &RegKey, + conda_locator: &mut dyn CondaLocator, +) -> Option { + let mut environments = vec![]; + let mut managers: Vec = vec![]; let python_key = hk.open_subkey("Software\\Python").ok()?; - let company_key = python_key.open_subkey(company).ok()?; + for company in python_key.enum_keys().filter_map(Result::ok) { + if let Some(result) = + get_registry_pythons_from_key_for_company(&hk, &company, conda_locator) + { + managers.extend(result.managers); + environments.extend(result.environments); + } + } + + Some(LocatorResult { + environments, + managers, + }) +} - let mut pythons = vec![]; +#[cfg(windows)] +fn get_registry_pythons_from_key_for_company( + hk: &RegKey, + company: &str, + conda_locator: &mut dyn CondaLocator, +) -> Option { + use crate::messaging::Architecture; + + let mut environments = vec![]; + let mut managers: Vec = vec![]; + let python_key = hk.open_subkey("Software\\Python").ok()?; + let company_key = python_key.open_subkey(company).ok()?; + let company_display_name: Option = company_key.get_value("DisplayName").ok(); for key in company_key.enum_keys().filter_map(Result::ok) { if let Some(key) = company_key.open_subkey(key).ok() { if let Some(install_path_key) = key.open_subkey("InstallPath").ok() { let env_path: String = install_path_key.get_value("").ok().unwrap_or_default(); let env_path = PathBuf::from(env_path); + + // Possible this is a conda install folder. + if let Some(conda_result) = conda_locator.find_in(&env_path) { + for manager in conda_result.managers { + let mut mgr = manager.clone(); + mgr.company = Some(company.to_string()); + mgr.company_display_name = company_display_name.clone(); + managers.push(mgr) + } + for env in conda_result.environments { + let mut env = env.clone(); + env.company = Some(company.to_string()); + env.company_display_name = company_display_name.clone(); + if let Some(mgr) = env.env_manager { + let mut mgr = mgr.clone(); + mgr.company = Some(company.to_string()); + mgr.company_display_name = company_display_name.clone(); + env.env_manager = Some(mgr); + } + environments.push(env); + } + continue; + } + let env_path = if env_path.exists() { Some(env_path) } else { @@ -44,9 +98,11 @@ fn get_registry_pythons_from_key(hk: &RegKey, company: &str) -> Option Option Option> { - let hklm = winreg::RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE); - let hkcu = winreg::RegKey::predef(winreg::enums::HKEY_CURRENT_USER); - - let mut pythons = vec![]; - if let Some(hklm_pythons) = get_registry_pythons_from_key(&hklm, company) { - pythons.extend(hklm_pythons); - } - if let Some(hkcu_pythons) = get_registry_pythons_from_key(&hkcu, company) { - pythons.extend(hkcu_pythons); - } - Some(pythons) + Some(LocatorResult { + environments, + managers, + }) } #[cfg(windows)] -pub fn get_registry_pythons_anaconda(conda_locator: &mut dyn CondaLocator) -> LocatorResult { +fn get_registry_pythons(conda_locator: &mut dyn CondaLocator) -> Option { let hklm = winreg::RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE); let hkcu = winreg::RegKey::predef(winreg::enums::HKEY_CURRENT_USER); - let mut pythons = vec![]; - if let Some(hklm_pythons) = get_registry_pythons_from_key(&hklm, "ContinuumAnalytics") { - pythons.extend(hklm_pythons); - } - if let Some(hkcu_pythons) = get_registry_pythons_from_key(&hkcu, "ContinuumAnalytics") { - pythons.extend(hkcu_pythons); - } - - let mut environments: Vec = vec![]; + let mut environments = vec![]; let mut managers: Vec = vec![]; - for env in pythons.iter() { - if let Some(env_path) = env.clone().env_path { - if let Some(mut result) = conda_locator.find_in(&env_path) { - environments.append(&mut result.environments); - managers.append(&mut result.managers); - } - } + if let Some(result) = get_registry_pythons_from_key(&hklm, conda_locator) { + managers.extend(result.managers); + environments.extend(result.environments); } - - LocatorResult { - managers, - environments, + if let Some(result) = get_registry_pythons_from_key(&hkcu, conda_locator) { + managers.extend(result.managers); + environments.extend(result.environments); } + Some(LocatorResult { + environments, + managers, + }) } #[cfg(windows)] @@ -134,23 +176,11 @@ impl Locator for WindowsRegistry<'_> { } fn find(&mut self) -> Option { - let mut environments: Vec = vec![]; - let mut managers: Vec = vec![]; - - let mut result = get_registry_pythons("PythonCore").unwrap_or_default(); - environments.append(&mut result); - - let mut result = get_registry_pythons_anaconda(self.conda_locator) ; - environments.append(&mut result.environments); - managers.append(&mut result.managers); - - if environments.is_empty() && managers.is_empty() { - None - } else { - Some(LocatorResult { - managers, - environments, - }) + if let Some(result) = get_registry_pythons(self.conda_locator) { + if !result.environments.is_empty() && !result.managers.is_empty() { + return Some(result); + } } + None } } diff --git a/native_locator/src/windows_store.rs b/native_locator/src/windows_store.rs index 39f3a01f4ac9..f9ed251f9674 100644 --- a/native_locator/src/windows_store.rs +++ b/native_locator/src/windows_store.rs @@ -12,6 +12,8 @@ use crate::messaging::PythonEnvironment; #[cfg(windows)] use crate::utils::PythonEnv; #[cfg(windows)] +use log::{trace, warn}; +#[cfg(windows)] use std::path::Path; #[cfg(windows)] use std::path::PathBuf; @@ -29,6 +31,10 @@ pub fn is_windows_python_executable(path: &PathBuf) -> bool { fn list_windows_store_python_executables( environment: &dyn known::Environment, ) -> Option> { + use crate::messaging::Architecture; + use regex::Regex; + use std::collections::HashMap; + let mut python_envs: Vec = vec![]; let home = environment.get_user_home()?; let apps_path = Path::new(&home) @@ -37,46 +43,136 @@ fn list_windows_store_python_executables( .join("Microsoft") .join("WindowsApps"); let hkcu = winreg::RegKey::predef(winreg::enums::HKEY_CURRENT_USER); - for file in std::fs::read_dir(apps_path).ok()?.filter_map(Result::ok) { - let path = file.path(); + trace!("Searching for Windows Store Python in {:?}", apps_path); + let folder_version_regex = + Regex::new("PythonSoftwareFoundation.Python.(\\d+\\.\\d+)_.*").unwrap(); + let exe_version_regex = Regex::new("python(\\d+\\.\\d+).exe").unwrap(); + #[derive(Default)] + struct PotentialPython { + path: Option, + name: Option, + exe: Option, + version: String, + } + let mut potential_matches: HashMap = HashMap::new(); + for path in std::fs::read_dir(apps_path) + .ok()? + .filter_map(Result::ok) + .map(|f| f.path()) + { if let Some(name) = path.file_name() { - let exe = path.join("python.exe"); - if name - .to_str() - .unwrap_or_default() - .starts_with("PythonSoftwareFoundation.Python.") - && exe.is_file() - && exe.exists() - { - if let Some(result) = - get_package_display_name_and_location(name.to_string_lossy().to_string(), &hkcu) - { - let env = PythonEnvironment { - display_name: Some(result.display_name), - name: None, - python_executable_path: Some(exe.clone()), - version: None, - category: crate::messaging::PythonEnvironmentCategory::WindowsStore, - env_path: Some(PathBuf::from(result.env_path.clone())), - env_manager: None, - project_path: None, - python_run_command: Some(vec![exe.to_string_lossy().to_string()]), - arch: None, + let name = name.to_string_lossy().to_string(); + if name.starts_with("PythonSoftwareFoundation.Python.") { + let simple_version = folder_version_regex.captures(&name)?; + let simple_version = simple_version + .get(1) + .map(|m| m.as_str()) + .unwrap_or_default(); + if simple_version.len() == 0 { + continue; + } + if let Some(existing) = potential_matches.get_mut(&simple_version.to_string()) { + existing.path = Some(path.clone()); + existing.name = Some(name.clone()); + } else { + let item = PotentialPython { + path: Some(path.clone()), + name: Some(name.clone()), + version: simple_version.to_string(), + ..Default::default() }; - python_envs.push(env); + potential_matches.insert(simple_version.to_string(), item); + } + } else if name.starts_with("python") && name.ends_with(".exe") { + if name == "python.exe" || name == "python3.exe" { + // Unfortunately we have no idea what these point to. + // Even old python code didn't report these, hopefully users will not use these. + // If they do, we might have to spawn Python to find the real path and match it to one of the items discovered. + continue; + } + if let Some(simple_version) = exe_version_regex.captures(&name) { + let simple_version = simple_version + .get(1) + .map(|m| m.as_str()) + .unwrap_or_default(); + if simple_version.len() == 0 { + continue; + } + if let Some(existing) = potential_matches.get_mut(&simple_version.to_string()) { + existing.exe = Some(path.clone()); + } else { + let item = PotentialPython { + exe: Some(path.clone()), + version: simple_version.to_string(), + ..Default::default() + }; + potential_matches.insert(simple_version.to_string(), item); + } } } } } + for (_, item) in potential_matches { + if item.exe.is_none() { + warn!( + "Did not find a Windows Store exe for version {:?} that coresponds to path {:?}", + item.version, item.path + ); + continue; + } + if item.path.is_none() { + warn!( + "Did not find a Windows Store path for version {:?} that coresponds to exe {:?}", + item.version, item.exe + ); + continue; + } + let name = item.name.unwrap_or_default(); + let path = item.path.unwrap_or_default(); + let exe = item.exe.unwrap_or_default(); + let parent = path.parent()?.to_path_buf(); // This dir definitely exists. + if let Some(result) = get_package_display_name_and_location(&name, &hkcu) { + let env_path = PathBuf::from(result.env_path); + let env = PythonEnvironment { + display_name: Some(result.display_name), + python_executable_path: Some(exe.clone()), + category: crate::messaging::PythonEnvironmentCategory::WindowsStore, + env_path: Some(env_path.clone()), + python_run_command: Some(vec![exe.to_string_lossy().to_string()]), + arch: if result.is64_bit { + Some(Architecture::X64) + } else { + None + }, + version: Some(item.version.clone()), + symlinks: Some(vec![ + parent.join(format!("python{:?}.exe", item.version)), + path.join("python.exe"), + path.join("python3.exe"), + path.join(format!("python{:?}.exe", item.version)), + env_path.join("python.exe"), + env_path.join(format!("python{:?}.exe", item.version)), + ]), + ..Default::default() + }; + python_envs.push(env); + } else { + warn!( + "Failed to get package display name & location for Windows Store Package {:?}", + path + ); + } + } Some(python_envs) } #[cfg(windows)] -fn get_package_full_name_from_registry(name: String, hkcu: &RegKey) -> Option { +fn get_package_full_name_from_registry(name: &String, hkcu: &RegKey) -> Option { let key = format!("Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\AppModel\\SystemAppData\\{}\\Schemas", name); + trace!("Opening registry key {:?}", key); let package_key = hkcu.open_subkey(key).ok()?; - let value = package_key.get_value("PackageFullName").unwrap_or_default(); + let value = package_key.get_value("PackageFullName").ok()?; Some(value) } @@ -85,12 +181,14 @@ fn get_package_full_name_from_registry(name: String, hkcu: &RegKey) -> Option Option { +fn get_package_display_name_and_location(name: &String, hkcu: &RegKey) -> Option { if let Some(name) = get_package_full_name_from_registry(name, &hkcu) { let key = format!("Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\AppModel\\Repository\\Packages\\{}", name); + trace!("Opening registry key {:?}", key); let package_key = hkcu.open_subkey(key).ok()?; let display_name = package_key.get_value("DisplayName").ok()?; let env_path = package_key.get_value("PackageRootFolder").ok()?; @@ -98,6 +196,7 @@ fn get_package_display_name_and_location(name: String, hkcu: &RegKey) -> Option< return Some(StorePythonInfo { display_name, env_path, + is64_bit: name.contains("_x64_"), }); } None @@ -121,16 +220,10 @@ impl Locator for WindowsStore<'_> { fn resolve(&self, env: &PythonEnv) -> Option { if is_windows_python_executable(&env.executable) { return Some(PythonEnvironment { - display_name: None, - name: None, python_executable_path: Some(env.executable.clone()), - version: None, category: crate::messaging::PythonEnvironmentCategory::WindowsStore, - env_path: None, - env_manager: None, - project_path: None, python_run_command: Some(vec![env.executable.to_str().unwrap().to_string()]), - arch: None, + ..Default::default() }); } None diff --git a/native_locator/tests/common_python_test.rs b/native_locator/tests/common_python_test.rs index c8a5baa3b10a..ceebf4931ab6 100644 --- a/native_locator/tests/common_python_test.rs +++ b/native_locator/tests/common_python_test.rs @@ -42,6 +42,7 @@ fn find_python_in_path_this() { python_run_command: Some(vec![unix_python_exe.clone().to_str().unwrap().to_string()]), env_path: Some(user_home.clone()), arch: None, + ..Default::default() }; assert_messages( &[json!(env)], diff --git a/native_locator/tests/conda_test.rs b/native_locator/tests/conda_test.rs index 95e48917bd82..db6c1338ca9f 100644 --- a/native_locator/tests/conda_test.rs +++ b/native_locator/tests/conda_test.rs @@ -136,6 +136,8 @@ fn find_conda_exe_and_empty_envs() { executable_path: conda_exe.clone(), version: Some("4.0.2".to_string()), tool: EnvManagerType::Conda, + company: None, + company_display_name: None, }; assert_eq!(managers.len(), 1); assert_eq!(json!(expected_conda_manager), json!(managers[0])); @@ -177,12 +179,14 @@ fn find_conda_from_custom_install_location() { executable_path: conda_exe.clone(), version: Some("4.0.2".to_string()), tool: EnvManagerType::Conda, + company: None, + company_display_name: None, }; assert_eq!(json!(expected_conda_manager), json!(result.managers[0])); let expected_conda_env = PythonEnvironment { display_name: None, - name: Some("base".to_string()), + name: None, project_path: None, python_executable_path: Some(conda_dir.clone().join("bin").join("python")), category: python_finder::messaging::PythonEnvironmentCategory::Conda, @@ -197,6 +201,7 @@ fn find_conda_from_custom_install_location() { "python".to_string(), ]), arch: None, + ..Default::default() }; assert_eq!(json!(expected_conda_env), json!(result.environments[0])); @@ -245,6 +250,8 @@ fn finds_two_conda_envs_from_known_location() { executable_path: conda_exe.clone(), version: Some("4.0.2".to_string()), tool: EnvManagerType::Conda, + company: None, + company_display_name: None, }; assert_eq!(managers.len(), 1); @@ -267,6 +274,7 @@ fn finds_two_conda_envs_from_known_location() { "python".to_string(), ]), arch: None, + ..Default::default() }; let expected_conda_2 = PythonEnvironment { display_name: None, @@ -285,6 +293,7 @@ fn finds_two_conda_envs_from_known_location() { "python".to_string(), ]), arch: None, + ..Default::default() }; 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 c9782e9c5d35..132cc4160a6c 100644 --- a/native_locator/tests/pyenv_test.rs +++ b/native_locator/tests/pyenv_test.rs @@ -30,6 +30,7 @@ fn does_not_find_any_pyenv_envs_even_with_pyenv_installed() { use crate::common::{ assert_messages, create_test_environment, join_test_paths, test_file_path, }; + use python_finder::messaging::{EnvManager, EnvManagerType}; use python_finder::pyenv; use python_finder::{conda::Conda, locator::Locator}; use serde_json::json; @@ -57,14 +58,17 @@ fn does_not_find_any_pyenv_envs_even_with_pyenv_installed() { let mut locator = pyenv::PyEnv::with(&known, &mut conda); let result = locator.find().unwrap(); - let managers = result.managers; + let managers = result.clone().managers; assert_eq!(managers.len(), 1); - let expected_json = json!({"executablePath":pyenv_exe,"version":null, "tool": "pyenv"}); - assert_messages( - &[expected_json], - &managers.iter().map(|e| json!(e)).collect::>(), - ) + let expected_manager = EnvManager { + executable_path: pyenv_exe.clone(), + version: None, + tool: EnvManagerType::Pyenv, + company: None, + company_display_name: None, + }; + assert_eq!(json!(expected_manager), json!(result.managers[0])); } #[test] @@ -103,6 +107,8 @@ fn find_pyenv_envs() { executable_path: pyenv_exe.clone(), version: None, tool: EnvManagerType::Pyenv, + company: None, + company_display_name: None, }; assert_eq!(json!(expected_manager), json!(result.managers[0])); @@ -128,7 +134,8 @@ fn find_pyenv_envs() { ".pyenv/versions/3.9.9" ])), env_manager: Some(expected_manager.clone()), - arch: None + arch: None, + ..Default::default() }); let expected_virtual_env = PythonEnvironment { display_name: None, @@ -153,6 +160,7 @@ fn find_pyenv_envs() { ])), env_manager: Some(expected_manager.clone()), arch: None, + ..Default::default() }; let expected_3_12_1 = PythonEnvironment { display_name: None, @@ -177,6 +185,7 @@ fn find_pyenv_envs() { ])), env_manager: Some(expected_manager.clone()), arch: None, + ..Default::default() }; let expected_3_13_dev = PythonEnvironment { display_name: None, @@ -201,6 +210,7 @@ fn find_pyenv_envs() { ])), env_manager: Some(expected_manager.clone()), arch: None, + ..Default::default() }; let expected_3_12_1a3 = PythonEnvironment { display_name: None, @@ -225,6 +235,7 @@ fn find_pyenv_envs() { ])), env_manager: Some(expected_manager.clone()), arch: None, + ..Default::default() }; assert_messages( diff --git a/src/client/pythonEnvironments/base/info/environmentInfoService.ts b/src/client/pythonEnvironments/base/info/environmentInfoService.ts index 251834b29683..4c437431823a 100644 --- a/src/client/pythonEnvironments/base/info/environmentInfoService.ts +++ b/src/client/pythonEnvironments/base/info/environmentInfoService.ts @@ -30,6 +30,19 @@ export interface IEnvironmentInfoService { env: PythonEnvInfo, priority?: EnvironmentInfoServiceQueuePriority, ): Promise; + /** + * Get the mandatory interpreter information for the given environment. + * E.g. executable path, version and sysPrefix are considered mandatory. + * However if we only have part of the version, thats still sufficient. + * If the fully resolved and acurate information for all parts of the env is required, then + * used `getEnvironmentInfo`. + * @param env The environment to get the interpreter information for. + * @param priority The priority of the request. + */ + getMandatoryEnvironmentInfo( + env: PythonEnvInfo, + priority?: EnvironmentInfoServiceQueuePriority, + ): Promise; /** * Reset any stored interpreter information for the given environment. * @param searchLocation Search location of the environment. @@ -124,6 +137,36 @@ class EnvironmentInfoService implements IEnvironmentInfoService { return deferred.promise; } + public async getMandatoryEnvironmentInfo( + env: PythonEnvInfo, + priority?: EnvironmentInfoServiceQueuePriority, + ): Promise { + const interpreterPath = env.executable.filename; + const result = this.cache.get(normCasePath(interpreterPath)); + if (result !== undefined) { + // Another call for this environment has already been made, return its result. + return result.promise; + } + + const deferred = createDeferred(); + const info = EnvironmentInfoService.getInterpreterInfo(env, true); + if (info !== undefined) { + this.cache.set(normCasePath(interpreterPath), deferred); + deferred.resolve(info); + return info; + } + + this.cache.set(normCasePath(interpreterPath), deferred); + this._getEnvironmentInfo(env, priority) + .then((r) => { + deferred.resolve(r); + }) + .catch((ex) => { + deferred.reject(ex); + }); + return deferred.promise; + } + public async _getEnvironmentInfo( env: PythonEnvInfo, priority?: EnvironmentInfoServiceQueuePriority, @@ -213,7 +256,25 @@ class EnvironmentInfoService implements IEnvironmentInfoService { }); } - private static getInterpreterInfo(env: PythonEnvInfo): InterpreterInformation | undefined { + private static getInterpreterInfo( + env: PythonEnvInfo, + allowPartialVersions?: boolean, + ): InterpreterInformation | undefined { + if (allowPartialVersions) { + if (env.version.major > -1 && env.version.minor > -1 && env.location) { + return { + arch: env.arch, + executable: { + filename: env.executable.filename, + ctime: -1, + mtime: -1, + sysPrefix: env.location, + }, + version: env.version, + }; + } + } + if (env.version.major > -1 && env.version.minor > -1 && env.version.micro > -1 && env.location) { return { arch: env.arch, diff --git a/src/client/pythonEnvironments/base/locator.ts b/src/client/pythonEnvironments/base/locator.ts index e0af1a7a59b8..d61f530f46ab 100644 --- a/src/client/pythonEnvironments/base/locator.ts +++ b/src/client/pythonEnvironments/base/locator.ts @@ -12,6 +12,7 @@ import { PythonEnvsChangedEvent, PythonEnvsWatcher, } from './watcher'; +import type { Architecture } from '../../common/utils/platform'; /** * A single update to a previously provided Python env object. @@ -162,6 +163,9 @@ export type BasicEnvInfo = { */ pythonRunCommand?: string[]; identifiedUsingNativeLocator?: boolean; + arch?: Architecture; + ctime?: number; + mtime?: number; }; /** diff --git a/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts b/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts index 0f252320a52c..ac89d9e3aaf8 100644 --- a/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts +++ b/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts @@ -28,6 +28,10 @@ export interface NativeEnvInfo { * Path to the project directory when dealing with pipenv virtual environments. */ projectPath?: string; + arch?: 'X64' | 'X86'; + symlinks?: string[]; + creationTime?: number; + modifiedTime?: number; } export interface NativeEnvManagerInfo { @@ -60,7 +64,6 @@ class NativeGlobalPythonFinderImpl implements NativeGlobalPythonFinder { const deferred = createDeferred(); const proc = ch.spawn(NATIVE_LOCATOR, [], { env: process.env }); const disposables: Disposable[] = []; - // jsonrpc package cannot handle messages coming through too quicly. // Lets handle the messages and close the stream only when // we have got the exit event. diff --git a/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts b/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts index 5a98d0013bbf..8c02eab33359 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts @@ -137,7 +137,7 @@ export class PythonEnvsResolver implements IResolvingLocator { state.pending += 1; // It's essential we increment the pending call count before any asynchronus calls in this method. // We want this to be run even when `resolveInBackground` is called in background. - const info = await this.environmentInfoService.getEnvironmentInfo(seen[envIndex]); + const info = await this.environmentInfoService.getMandatoryEnvironmentInfo(seen[envIndex]); const old = seen[envIndex]; if (info) { const resolvedEnv = getResolvedEnv(info, seen[envIndex]); @@ -194,7 +194,9 @@ function getResolvedEnv(interpreterInfo: InterpreterInformation, environment: Py resolvedEnv.executable.sysPrefix = interpreterInfo.executable.sysPrefix; const isEnvLackingPython = getEnvPath(resolvedEnv.executable.filename, resolvedEnv.location).pathType === 'envFolderPath'; - if (isEnvLackingPython) { + // TODO: Shouldn't this only apply to conda, how else can we have an environment and not have Python in it? + // If thats the case, then this should be gated on environment.kind === PythonEnvKind.Conda + if (isEnvLackingPython && environment.kind !== PythonEnvKind.MicrosoftStore) { // Install python later into these envs might change the version, which can be confusing for users. // So avoid displaying any version until it is installed. resolvedEnv.version = getEmptyVersion(); diff --git a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts index 2fdfd34c5acc..d1ad91493eab 100644 --- a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts +++ b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts @@ -64,9 +64,17 @@ export async function resolveBasicEnv(env: BasicEnvInfo): Promise await updateEnvUsingRegistry(resolvedEnv); } setEnvDisplayString(resolvedEnv); - const { ctime, mtime } = await getFileInfo(resolvedEnv.executable.filename); - resolvedEnv.executable.ctime = ctime; - resolvedEnv.executable.mtime = mtime; + if (env.arch && !resolvedEnv.arch) { + resolvedEnv.arch = env.arch; + } + if (env.ctime && env.mtime) { + resolvedEnv.executable.ctime = env.ctime; + resolvedEnv.executable.mtime = env.mtime; + } else { + const { ctime, mtime } = await getFileInfo(resolvedEnv.executable.filename); + resolvedEnv.executable.ctime = ctime; + resolvedEnv.executable.mtime = mtime; + } const type = await getEnvType(resolvedEnv); if (type) { resolvedEnv.type = type; diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/nativeLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/nativeLocator.ts index 0d3f2a588b1a..46f4ad2ddff2 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/nativeLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/nativeLocator.ts @@ -18,6 +18,7 @@ import { } from '../common/nativePythonFinder'; import { disposeAll } from '../../../../common/utils/resourceLifecycle'; import { StopWatch } from '../../../../common/utils/stopWatch'; +import { Architecture } from '../../../../common/utils/platform'; import { sendTelemetryEvent } from '../../../../telemetry'; import { EventName } from '../../../../telemetry/constants'; @@ -67,9 +68,9 @@ function parseVersion(version?: string): PythonVersion | undefined { try { const [major, minor, micro] = version.split('.').map((v) => parseInt(v, 10)); return { - major, - minor, - micro, + major: typeof major === 'number' ? major : -1, + minor: typeof minor === 'number' ? minor : -1, + micro: typeof micro === 'number' ? micro : -1, sysVersion: version, }; } catch { @@ -113,6 +114,7 @@ export class NativeLocator implements ILocator, IDisposable { this.finder.onDidFindPythonEnvironment((data: NativeEnvInfo) => { // TODO: What if executable is undefined? if (data.pythonExecutablePath) { + const arch = (data.arch || '').toLowerCase(); envs.push({ kind: categoryToKind(data.category), executablePath: data.pythonExecutablePath, @@ -123,6 +125,11 @@ export class NativeLocator implements ILocator, IDisposable { pythonRunCommand: data.pythonRunCommand, searchLocation: data.projectPath ? Uri.file(data.projectPath) : undefined, identifiedUsingNativeLocator: true, + arch: + // eslint-disable-next-line no-nested-ternary + arch === 'x64' ? Architecture.x64 : arch === 'x86' ? Architecture.x86 : undefined, + ctime: data.creationTime, + mtime: data.modifiedTime, }); } else { environmentsWithoutPython += 1;