diff --git a/native_locator/src/common_python.rs b/native_locator/src/common_python.rs index 7e2b532fb91c..039965bae502 100644 --- a/native_locator/src/common_python.rs +++ b/native_locator/src/common_python.rs @@ -50,7 +50,7 @@ impl Locator for PythonOnPath<'_> { }) } - fn find(&self) -> Option { + fn find(&mut self) -> Option { let paths = self.environment.get_env_var("PATH".to_string())?; let bin = if cfg!(windows) { "python.exe" diff --git a/native_locator/src/conda.rs b/native_locator/src/conda.rs index 4236a7329c7c..65e0fd073292 100644 --- a/native_locator/src/conda.rs +++ b/native_locator/src/conda.rs @@ -9,81 +9,61 @@ use crate::messaging; use crate::messaging::EnvManager; use crate::messaging::EnvManagerType; use crate::messaging::PythonEnvironment; -use crate::utils::find_python_binary_path; use crate::utils::PythonEnv; +use crate::utils::{find_python_binary_path, get_environment_key, get_environment_manager_key}; +use log::warn; use regex::Regex; +use std::collections::HashSet; use std::env; use std::path::{Path, PathBuf}; -/// relative to the interpreter. This layout is common on linux/Mac. -/// -/// ``` -/// env // <--- Input can be this path -/// |-- conda-meta // <--- Returns this directory -/// |-- bin // <--- Input can be this path -/// |-- python // <--- Input can be this path -/// ``` +/// Specifically returns the file names that are valid for 'conda' on windows +/// Path is relative to the installation folder of conda. +#[cfg(windows)] +fn get_relative_paths_to_conda_executable() -> Vec { + vec![ + PathBuf::from("Scripts").join("conda.exe"), + PathBuf::from("Scripts").join("conda.bat"), + ] +} + +/// Specifically returns the file names that are valid for 'conda' on linux/Mac +/// Path is relative to the installation folder of conda. #[cfg(unix)] -fn get_conda_meta_path(any_path: &Path) -> Option { - if any_path.ends_with("bin/python") { - match any_path.parent() { - Some(parent) => match parent.parent() { - Some(parent) => Some(parent.to_path_buf().join("conda-meta")), - None => None, - }, - None => None, - } - } else if any_path.ends_with("bin") { - match any_path.parent() { - Some(parent) => Some(parent.to_path_buf().join("conda-meta")), - None => None, - } - } else { - Some(any_path.to_path_buf().join("conda-meta")) - } +fn get_relative_paths_to_conda_executable() -> Vec { + vec![PathBuf::from("bin").join("conda")] } -/// Get the conda-meta directory. For windows 'conda-meta' is in the same directory as the interpreter. -/// This layout is common in Windows. -/// -/// ``` -/// env // <--- Input can be this path -/// |-- conda-meta // <--- Returns this directory -/// |-- python.exe // <--- Input can be this path -/// ``` +/// Returns the relative path to the python executable for the conda installation. +/// Path is relative to the installation folder of conda. +/// In windows the python.exe for the conda installation is in the root folder. #[cfg(windows)] -fn get_conda_meta_path(any_path: &Path) -> Option { - if any_path.ends_with("python.exe") { - match any_path.parent() { - Some(parent) => Some(parent.to_path_buf().join("conda-meta")), - None => None, - } - } else { - Some(any_path.to_path_buf().join("conda-meta")) - } +fn get_relative_paths_to_main_python_executable() -> PathBuf { + PathBuf::from("python.exe") } -/// Check if a given path is a conda environment. A conda environment is a directory that contains -/// a 'conda-meta' directory as child. This will find 'conda-meta' in a platform agnostic way. -pub fn is_conda_environment(any_path: &Path) -> bool { - let conda_meta_path = get_conda_meta_path(any_path); - match conda_meta_path { - Some(path) => path.exists(), - None => false, - } +/// Returns the relative path to the python executable for the conda installation. +/// Path is relative to the installation folder of conda. +/// In windows the python.exe for the conda installation is in the bin folder. +#[cfg(unix)] +fn get_relative_paths_to_main_python_executable() -> PathBuf { + PathBuf::from("bin").join("python") } +#[derive(Debug)] struct CondaPackage { + #[allow(dead_code)] path: PathBuf, version: String, } /// Get the path to the json file along with the version of a package in the conda environment from the 'conda-meta' directory. -fn get_conda_package_json_path(any_path: &Path, package: &str) -> Option { +fn get_conda_package_json_path(path: &Path, package: &str) -> Option { + // conda-meta is in the root of the conda installation folder + let path = path.join("conda-meta"); let package_name = format!("{}-", package); - let conda_meta_path = get_conda_meta_path(any_path)?; let regex = Regex::new(format!("^{}-((\\d+\\.*)*)-.*.json$", package).as_str()); - std::fs::read_dir(conda_meta_path).ok()?.find_map(|entry| { + std::fs::read_dir(path).ok()?.find_map(|entry| { let path = entry.ok()?.path(); let file_name = path.file_name()?.to_string_lossy(); if file_name.starts_with(&package_name) && file_name.ends_with(".json") { @@ -100,23 +80,15 @@ fn get_conda_package_json_path(any_path: &Path, package: &str) -> Option bool { - let conda_python_json_path = get_conda_package_json_path(any_path, "python"); - match conda_python_json_path { - Some(result) => result.path.exists(), - None => false, +fn get_conda_executable(path: &PathBuf) -> Option { + for relative_path in get_relative_paths_to_conda_executable() { + let exe = path.join(&relative_path); + if exe.exists() { + return Some(exe); + } } -} -/// Get the version of the `python` package in the conda environment -pub fn get_conda_python_version(any_path: &Path) -> Option { - let conda_python_json_path = get_conda_package_json_path(any_path, "python"); - match conda_python_json_path { - Some(result) => Some(result.version.clone()), - None => None, - } + None } /// Specifically returns the file names that are valid for 'conda' on windows @@ -220,21 +192,84 @@ pub fn find_conda_binary(environment: &dyn known::Environment) -> Option Option { - let mut parent = conda_binary.parent()?; - if parent.ends_with("bin") { - parent = parent.parent()?; - } - if parent.ends_with("Library") { - parent = parent.parent()?; +fn get_conda_manager(path: &PathBuf) -> Option { + let conda_exe = get_conda_executable(path)?; + let conda_pkg = get_conda_package_json_path(path, "conda")?; + + Some(EnvManager { + executable_path: conda_exe, + version: Some(conda_pkg.version), + tool: EnvManagerType::Conda, + }) +} + +#[derive(Debug, Clone)] +struct CondaEnvironment { + name: String, + named: bool, + path: PathBuf, + python_executable_path: Option, + version: Option, +} +fn get_conda_environment_info(env_path: &PathBuf, named: bool) -> Option { + let metadata = env_path.metadata(); + match metadata { + Ok(metadata) => { + if metadata.is_dir() { + let path = env_path.clone(); + if let Some(python_binary) = find_python_binary_path(&path) { + if let Some(package_info) = get_conda_package_json_path(&path, "python") { + return Some(CondaEnvironment { + name: path.file_name()?.to_string_lossy().to_string(), + path, + named, + python_executable_path: Some(python_binary), + version: Some(package_info.version), + }); + } else { + return Some(CondaEnvironment { + name: path.file_name()?.to_string_lossy().to_string(), + path, + named, + python_executable_path: Some(python_binary), + version: None, + }); + } + } else { + return Some(CondaEnvironment { + name: path.file_name()?.to_string_lossy().to_string(), + path, + named, + python_executable_path: None, + version: None, + }); + } + } + } + Err(_) => (), } - match get_conda_package_json_path(&parent, "conda") { - Some(result) => Some(result.version), - None => match get_conda_package_json_path(&parent.parent()?, "conda") { - Some(result) => Some(result.version), - None => None, - }, + + None +} +fn get_environments_from_envs_folder_in_conda_directory( + path: &Path, +) -> Option> { + let mut envs: Vec = vec![]; + // iterate through all sub directories in the env folder + // for each sub directory, check if it has a python executable + // if it does, create a PythonEnvironment object and add it to the list + for entry in std::fs::read_dir(path.join("envs")).ok()? { + match entry { + Ok(entry) => { + if let Some(env) = get_conda_environment_info(&entry.path(), true) { + envs.push(env); + } + } + Err(_) => (), + } } + + Some(envs) } fn get_conda_envs_from_environment_txt(environment: &dyn known::Environment) -> Vec { @@ -258,133 +293,485 @@ fn get_conda_envs_from_environment_txt(environment: &dyn known::Environment) -> envs } -fn get_known_env_locations( - conda_bin: &PathBuf, +#[derive(Debug)] +struct Condarc { + env_dirs: Vec, +} + +/** + * The .condarc file contains a list of directories where conda environments are created. + * https://conda.io/projects/conda/en/latest/configuration.html#envs-dirs + * + * TODO: Search for the .condarc file in the following locations: + * https://conda.io/projects/conda/en/latest/user-guide/configuration/use-condarc.html#searching-for-condarc + */ +fn get_conda_conda_rc(environment: &dyn known::Environment) -> Option { + if let Some(home) = environment.get_user_home() { + let conda_rc = Path::new(&home).join(".condarc"); + let mut start_consuming_values = false; + match std::fs::read_to_string(conda_rc) { + Ok(reader) => { + let mut env_dirs = vec![]; + for line in reader.lines() { + if line.starts_with("envs_dirs:") && !start_consuming_values { + start_consuming_values = true; + continue; + } + if start_consuming_values { + if line.trim().starts_with("-") { + if let Some(env_dir) = line.splitn(2, '-').nth(1) { + let env_dir = PathBuf::from(env_dir.trim()); + if env_dir.exists() { + env_dirs.push(env_dir); + } + } + continue; + } else { + break; + } + } + } + return Some(Condarc { env_dirs }); + } + Err(_) => (), + } + } + None +} + +fn get_conda_envs_from_conda_rc( + root_conda_path: &PathBuf, environment: &dyn known::Environment, -) -> Vec { - let mut paths = vec![]; - let home = environment.get_user_home(); - match home { - Some(home) => { - let home = Path::new(&home); - let conda_envs = home.join(".conda").join("envs"); - paths.push(conda_envs.to_string_lossy().to_string()); +) -> Option> { + let mut envs: Vec = vec![]; + for env in get_conda_conda_rc(environment)?.env_dirs { + if let Ok(reader) = std::fs::read_dir(env) { + for entry in reader { + match entry { + Ok(entry) => { + if entry.path().is_dir() + && was_conda_environment_created_by_specific_conda( + &entry.path(), + root_conda_path, + ) + { + if let Some(env) = get_conda_environment_info(&entry.path(), false) { + envs.push(env); + } + } + } + Err(_) => (), + } + } } - None => (), } - match conda_bin.parent() { - Some(parent) => { - paths.push(parent.to_string_lossy().to_string()); - let conda_envs = parent.join("envs"); - paths.push(conda_envs.to_string_lossy().to_string()); - match parent.parent() { - Some(parent) => { - paths.push(parent.to_string_lossy().to_string()); - let conda_envs = parent.join("envs"); - paths.push(conda_envs.to_string_lossy().to_string()); + Some(envs) +} + +/** + * When we create conda environments in specific folder using the -p argument, the location of the conda executable is not know. + * If the user has multiple conda installations, any one of those could have created that specific environment. + * Fortunately the conda-meta/history file contains the path to the conda executable (script) that was used to create the environment. + * The format of the file is as follows: + * # cmd: C:\Users\user\miniconda3\Scripts\conda-script.py create --name myenv + * + * Thus all we need to do is to look for the 'cmd' line in the file and extract the path to the conda executable and match that against the path provided. + */ +fn was_conda_environment_created_by_specific_conda( + env_path: &PathBuf, + root_conda_path: &PathBuf, +) -> bool { + let conda_meta_history = env_path.join("conda-meta").join("history"); + match std::fs::read_to_string(conda_meta_history.clone()) { + Ok(reader) => { + for line in reader.lines() { + let line = line.to_lowercase(); + if line.starts_with("# cmd:") && line.contains(" create ") { + if line.contains(&root_conda_path.to_str().unwrap().to_lowercase()) { + return true; + } else { + return false; + } } - None => (), } } - None => (), + Err(_) => warn!( + "Error reading conda-meta/history file {:?}", + conda_meta_history + ), } - paths + false } -fn get_conda_envs_from_known_env_locations( - conda_bin: &PathBuf, +/** + * When we create conda environments in specific folder using the -p argument, the location of the conda executable is not know. + * If the user has multiple conda installations, any one of those could have created that specific environment. + * Fortunately the conda-meta/history file contains the path to the conda executable (script) that was used to create the environment. + * The format of the file is as follows: + * # cmd: C:\Users\user\miniconda3\Scripts\conda-script.py create --name myenv + * + * Thus all we need to do is to look for the 'cmd' line in the file and extract the path to the conda executable and match that against the path provided. + */ +fn get_environments_from_environments_txt_belonging_to_conda_directory( + path: &PathBuf, environment: &dyn known::Environment, -) -> Vec { - let mut envs = vec![]; - for location in get_known_env_locations(conda_bin, environment) { - if is_conda_environment(&Path::new(&location)) { - envs.push(location.to_string()); +) -> Option> { + let mut envs: Vec = vec![]; + for env in get_conda_envs_from_environment_txt(environment) { + // 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. + if env.contains(path.to_str().unwrap()) { + continue; } - match std::fs::read_dir(location) { - Ok(reader) => { - for entry in reader { - match entry { - Ok(entry) => { - let metadata = entry.metadata(); - match metadata { - Ok(metadata) => { - if metadata.is_dir() { - let path = entry.path(); - if is_conda_environment(&path) { - envs.push(path.to_string_lossy().to_string()); - } - } - } - Err(_) => (), - } - } - Err(_) => (), + + let env_path = PathBuf::from(env); + if !env_path.is_dir() { + continue; + } + if was_conda_environment_created_by_specific_conda(&env_path, path) { + if let Some(env) = get_conda_environment_info(&env_path, false) { + envs.push(env); + } + } + } + + Some(envs) +} + +fn get_conda_environments_from_conda_directory( + path: &PathBuf, + environment: &dyn known::Environment, +) -> Option> { + let mut all_envs: Vec = vec![]; + if let Some(envs) = get_environments_from_envs_folder_in_conda_directory(path) { + envs.iter().for_each(|env| all_envs.push(env.clone())); + } + + if let Some(envs) = + get_environments_from_environments_txt_belonging_to_conda_directory(path, environment) + { + envs.iter().for_each(|env| all_envs.push(env.clone())); + } + + if let Some(envs) = get_conda_envs_from_conda_rc(path, environment) { + envs.iter().for_each(|env| all_envs.push(env.clone())); + } + + Some(all_envs) +} + +#[cfg(windows)] +fn get_known_conda_install_locations(environment: &dyn known::Environment) -> Vec { + let user_profile = environment.get_env_var("USERPROFILE".to_string()).unwrap(); + let program_data = environment.get_env_var("PROGRAMDATA".to_string()).unwrap(); + let all_user_profile = environment + .get_env_var("ALLUSERSPROFILE".to_string()) + .unwrap(); + let home_drive = environment.get_env_var("HOMEDRIVE".to_string()).unwrap(); + let mut known_paths = vec![ + Path::new(&user_profile).join("Anaconda3"), + Path::new(&program_data).join("Anaconda3"), + Path::new(&all_user_profile).join("Anaconda3"), + Path::new(&home_drive).join("Anaconda3"), + Path::new(&user_profile).join("Miniconda3"), + Path::new(&program_data).join("Miniconda3"), + Path::new(&all_user_profile).join("Miniconda3"), + Path::new(&home_drive).join("Miniconda3"), + Path::new(&all_user_profile).join("miniforge3"), + Path::new(&home_drive).join("miniforge3"), + ]; + if let Some(home) = environment.get_user_home() { + known_paths.push(PathBuf::from(home.clone()).join("anaconda3")); + known_paths.push(PathBuf::from(home.clone()).join("miniconda3")); + known_paths.push(PathBuf::from(home.clone()).join("miniforge3")); + known_paths.push(PathBuf::from(home).join(".conda")); + } + known_paths +} + +#[cfg(unix)] +fn get_known_conda_install_locations(environment: &dyn known::Environment) -> Vec { + let mut known_paths = vec![ + PathBuf::from("/opt/anaconda3"), + PathBuf::from("/opt/miniconda3"), + PathBuf::from("/usr/local/anaconda3"), + PathBuf::from("/usr/local/miniconda3"), + PathBuf::from("/usr/anaconda3"), + PathBuf::from("/usr/miniconda3"), + PathBuf::from("/home/anaconda3"), + PathBuf::from("/home/miniconda3"), + PathBuf::from("/anaconda3"), + PathBuf::from("/miniconda3"), + PathBuf::from("/miniforge3"), + PathBuf::from("/miniforge3"), + ]; + if let Some(home) = environment.get_user_home() { + known_paths.push(PathBuf::from(home.clone()).join("anaconda3")); + known_paths.push(PathBuf::from(home.clone()).join("miniconda3")); + known_paths.push(PathBuf::from(home.clone()).join("miniforge3")); + known_paths.push(PathBuf::from(home).join(".conda")); + } + known_paths +} + +fn get_activation_command(env: &CondaEnvironment, manager: &EnvManager) -> Option> { + if env.python_executable_path.is_none() { + return None; + } + let conda_exe = manager.executable_path.to_str().unwrap().to_string(); + if env.named { + Some(vec![ + conda_exe, + "run".to_string(), + "-n".to_string(), + env.name.clone(), + "python".to_string(), + ]) + } else { + Some(vec![ + conda_exe, + "run".to_string(), + "-p".to_string(), + env.path.to_str().unwrap().to_string(), + "python".to_string(), + ]) + } +} + +fn get_root_python_environment(path: &PathBuf, manager: &EnvManager) -> Option { + let python_exe = path.join(get_relative_paths_to_main_python_executable()); + if !python_exe.exists() { + return None; + } + if let Some(package_info) = get_conda_package_json_path(&path, "python") { + let conda_exe = manager.executable_path.to_str().unwrap().to_string(); + return Some(PythonEnvironment { + name: None, + category: messaging::PythonEnvironmentCategory::Conda, + python_executable_path: Some(python_exe), + version: Some(package_info.version), + env_path: Some(path.clone()), + sys_prefix_path: Some(path.clone()), + env_manager: Some(manager.clone()), + python_run_command: Some(vec![ + conda_exe, + "run".to_string(), + "-p".to_string(), + path.to_str().unwrap().to_string(), + "python".to_string(), + ]), + project_path: None, + }); + } + None +} + +pub fn get_conda_environments_in_specified_path( + possible_conda_folder: &PathBuf, + environment: &dyn known::Environment, +) -> Option { + let mut managers: Vec = vec![]; + let mut environments: Vec = vec![]; + let mut detected_envs: HashSet = HashSet::new(); + let mut detected_managers: HashSet = HashSet::new(); + if possible_conda_folder.is_dir() && possible_conda_folder.exists() { + if let Some(manager) = get_conda_manager(&possible_conda_folder) { + let envs = + get_conda_environments_from_conda_directory(&possible_conda_folder, environment); + + if let Some(env) = get_root_python_environment(&possible_conda_folder, &manager) { + if let Some(key) = get_environment_key(&env) { + if !detected_envs.contains(&key) { + detected_envs.insert(key); + environments.push(env); } } } - Err(_) => (), + + envs.unwrap_or_default().iter().for_each(|env| { + let exe = env.python_executable_path.clone(); + let env = PythonEnvironment::new( + Some(env.name.clone()), + exe.clone(), + messaging::PythonEnvironmentCategory::Conda, + env.version.clone(), + Some(env.path.clone()), + Some(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); + } + } + }); + + let key = get_environment_manager_key(&manager); + if !detected_managers.contains(&key) { + detected_managers.insert(key); + managers.push(manager); + } } } - envs + + if managers.is_empty() && environments.is_empty() { + return None; + } + + Some(LocatorResult { + managers, + environments, + }) } -struct CondaEnv { - named: bool, - name: String, - path: PathBuf, +fn find_conda_environments_from_known_conda_install_locations( + environment: &dyn known::Environment, +) -> Option { + let mut managers: Vec = vec![]; + let mut environments: Vec = vec![]; + let mut detected_envs: HashSet = HashSet::new(); + let mut detected_managers: HashSet = HashSet::new(); + + for possible_conda_folder in get_known_conda_install_locations(environment) { + if let Some(result) = + get_conda_environments_in_specified_path(&possible_conda_folder, environment) + { + result.managers.iter().for_each(|m| { + let key = get_environment_manager_key(m); + if !detected_managers.contains(&key) { + detected_managers.insert(key); + managers.push(m.clone()); + } + }); + + result.environments.iter().for_each(|e| { + if let Some(key) = get_environment_key(e) { + if !detected_envs.contains(&key) { + detected_envs.insert(key); + environments.push(e.clone()); + } + } + }); + } + } + + if managers.is_empty() && environments.is_empty() { + return None; + } + + Some(LocatorResult { + managers, + environments, + }) +} + +pub fn get_conda_version(conda_binary: &PathBuf) -> Option { + let mut parent = conda_binary.parent()?; + if parent.ends_with("bin") { + parent = parent.parent()?; + } + if parent.ends_with("Library") { + parent = parent.parent()?; + } + match get_conda_package_json_path(&parent, "conda") { + Some(result) => Some(result.version), + None => match get_conda_package_json_path(&parent.parent()?, "conda") { + Some(result) => Some(result.version), + None => None, + }, + } } -fn get_distinct_conda_envs( - conda_bin: &PathBuf, +fn get_conda_environments_from_environments_txt_that_have_not_been_discovered( + known_environment_keys: &HashSet, + known_environment: &Vec, environment: &dyn known::Environment, -) -> Vec { - let mut envs = get_conda_envs_from_environment_txt(environment); - let mut known_envs = get_conda_envs_from_known_env_locations(conda_bin, environment); - envs.append(&mut known_envs); - envs.sort(); - envs.dedup(); - - let locations = get_known_env_locations(conda_bin, environment); - let mut conda_envs = vec![]; - for env in envs { - let env = Path::new(&env); - let mut named = false; - let mut name = "".to_string(); - for location in &locations { - let location = Path::new(location).join("envs"); - match env.strip_prefix(location) { - Ok(prefix) => { - named = true; - name = match prefix.to_str() { - Some(name) => { - let name = name.to_string(); - if name == "" { - "base".to_string() - } else { - name.to_string() - } - } - None => "base".to_string(), - }; - break; +) -> Option { + let binding = get_conda_envs_from_environment_txt(environment); + let undiscovered_environments_in_txt = binding + .iter() + .filter(|env| { + for known in known_environment_keys.iter() { + if known.contains(*env) { + return false; } - Err(_) => (), + } + true + }) + .collect::>(); + + if undiscovered_environments_in_txt.len() == 0 { + return None; + } + + // Ok, weird, we have an environment in environments.txt file that was not discovered. + // Let's try to discover it. + warn!( + "Found environments in environments.txt that were not discovered: {:?}", + undiscovered_environments_in_txt + ); + + let manager = match known_environment + .iter() + .find_map(|env| env.env_manager.as_ref()) + { + Some(manager) => Some(manager.clone()), + None => { + // Old approach of finding the conda executable. + let conda_binary = find_conda_binary(environment)?; + Some(EnvManager::new( + conda_binary.clone(), + get_conda_version(&conda_binary), + EnvManagerType::Conda, + )) + } + }; + + if let Some(manager) = manager { + let mut environments: Vec = vec![]; + for env in undiscovered_environments_in_txt { + if let Some(env) = get_conda_environment_info(&PathBuf::from(env), false) { + let exe = env.python_executable_path.clone(); + let env = PythonEnvironment::new( + Some(env.name.clone()), + exe.clone(), + messaging::PythonEnvironmentCategory::Conda, + env.version.clone(), + Some(env.path.clone()), + Some(env.path.clone()), + Some(manager.clone()), + get_activation_command(&env, &manager), + ); + environments.push(env); } } - conda_envs.push(CondaEnv { - named, - name, - path: PathBuf::from(env), - }); + if environments.len() > 0 { + return Some(LocatorResult { + managers: vec![manager], + environments, + }); + } + } else { + warn!("Could not find conda executable to discover environments in environments.txt"); } - conda_envs + + None } pub struct Conda<'a> { pub manager: Option, pub environment: &'a dyn Environment, + pub discovered_environments: HashSet, + pub discovered_managers: HashSet, +} + +pub trait CondaLocator { + fn find_in(&mut self, possible_conda_folder: &PathBuf) -> Option; } impl Conda<'_> { @@ -392,60 +779,119 @@ impl Conda<'_> { Conda { environment, manager: None, + discovered_environments: HashSet::new(), + discovered_managers: HashSet::new(), + } + } + fn filter_result(&mut self, result: Option) -> Option { + if let Some(result) = result { + let envs: Vec = result + .environments + .iter() + .filter(|e| { + if let Some(key) = get_environment_key(e) { + if self.discovered_environments.contains(&key) { + return false; + } + self.discovered_environments.insert(key); + return true; + } + false + }) + .cloned() + .collect(); + + let managers: Vec = result + .managers + .iter() + .filter(|e| { + let key = get_environment_manager_key(e); + if self.discovered_managers.contains(&key) { + return false; + } + self.discovered_managers.insert(key); + return true; + }) + .cloned() + .collect(); + + if envs.len() > 0 || managers.len() > 0 { + return Some(LocatorResult { + managers: managers, + environments: envs, + }); + } } + None + } +} + +impl CondaLocator for Conda<'_> { + fn find_in(&mut self, possible_conda_folder: &PathBuf) -> Option { + self.filter_result(get_conda_environments_in_specified_path( + possible_conda_folder, + self.environment, + )) } } impl Locator for Conda<'_> { fn resolve(&self, _env: &PythonEnv) -> Option { - // We will find everything in gather + // We will find everything in find None } - fn find(&self) -> Option { - let conda_binary = find_conda_binary(self.environment)?; - let manager = EnvManager::new( - conda_binary.clone(), - get_conda_version(&conda_binary), - EnvManagerType::Conda, - ); - - let envs = get_distinct_conda_envs(&conda_binary, self.environment); + fn find(&mut self) -> Option { + let mut managers: Vec = vec![]; let mut environments: Vec = vec![]; - for env in envs { - let executable = find_python_binary_path(Path::new(&env.path)); - let env = messaging::PythonEnvironment::new( - Some(env.name.to_string()), - executable.clone(), - messaging::PythonEnvironmentCategory::Conda, - get_conda_python_version(&env.path), - Some(env.path.clone()), - Some(env.path.clone()), - Some(manager.clone()), - if env.named { - Some(vec![ - conda_binary.to_string_lossy().to_string(), - "run".to_string(), - "-n".to_string(), - env.name.to_string(), - "python".to_string(), - ]) - } else { - Some(vec![ - conda_binary.to_string_lossy().to_string(), - "run".to_string(), - "-p".to_string(), - env.path.to_string_lossy().to_string(), - "python".to_string(), - ]) - }, - ); - - environments.push(env) + let mut detected_managers: HashSet = HashSet::new(); + + if let Some(result) = self.filter_result( + find_conda_environments_from_known_conda_install_locations(self.environment), + ) { + result.managers.iter().for_each(|m| { + let key = get_environment_manager_key(m); + detected_managers.insert(key); + managers.push(m.clone()); + }); + + result + .environments + .iter() + .for_each(|e| environments.push(e.clone())); + } + + if let Some(result) = self.filter_result( + get_conda_environments_from_environments_txt_that_have_not_been_discovered( + &self.discovered_environments, + &environments, + self.environment, + ), + ) { + result.managers.iter().for_each(|m| { + let key = get_environment_manager_key(m); + if !detected_managers.contains(&key) { + warn!("Found a new manager using the fallback mechanism: {:?}", m); + detected_managers.insert(key); + managers.push(m.clone()); + } + }); + + result.environments.iter().for_each(|e| { + warn!( + "Found a new conda environment using the fallback mechanism: {:?}", + e + ); + environments.push(e.clone()); + }); + } + + if managers.is_empty() && environments.is_empty() { + return None; } Some(LocatorResult { - managers: vec![manager], + managers, environments, }) } diff --git a/native_locator/src/conda_old.rs b/native_locator/src/conda_old.rs new file mode 100644 index 000000000000..9861c7b7a8e2 --- /dev/null +++ b/native_locator/src/conda_old.rs @@ -0,0 +1,452 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use crate::known; +use crate::known::Environment; +use crate::locator::Locator; +use crate::locator::LocatorResult; +use crate::messaging; +use crate::messaging::EnvManager; +use crate::messaging::EnvManagerType; +use crate::messaging::PythonEnvironment; +use crate::utils::find_python_binary_path; +use crate::utils::PythonEnv; +use regex::Regex; +use std::env; +use std::path::{Path, PathBuf}; + +/// relative to the interpreter. This layout is common on linux/Mac. +/// +/// ``` +/// env // <--- Input can be this path +/// |-- conda-meta // <--- Returns this directory +/// |-- bin // <--- Input can be this path +/// |-- python // <--- Input can be this path +/// ``` +#[cfg(unix)] +fn get_conda_meta_path(any_path: &Path) -> Option { + if any_path.ends_with("bin/python") { + match any_path.parent() { + Some(parent) => match parent.parent() { + Some(parent) => Some(parent.to_path_buf().join("conda-meta")), + None => None, + }, + None => None, + } + } else if any_path.ends_with("bin") { + match any_path.parent() { + Some(parent) => Some(parent.to_path_buf().join("conda-meta")), + None => None, + } + } else { + Some(any_path.to_path_buf().join("conda-meta")) + } +} + +/// Get the conda-meta directory. For windows 'conda-meta' is in the same directory as the interpreter. +/// This layout is common in Windows. +/// +/// ``` +/// env // <--- Input can be this path +/// |-- conda-meta // <--- Returns this directory +/// |-- python.exe // <--- Input can be this path +/// ``` +#[cfg(windows)] +fn get_conda_meta_path(any_path: &Path) -> Option { + if any_path.ends_with("python.exe") { + match any_path.parent() { + Some(parent) => Some(parent.to_path_buf().join("conda-meta")), + None => None, + } + } else { + Some(any_path.to_path_buf().join("conda-meta")) + } +} + +/// Check if a given path is a conda environment. A conda environment is a directory that contains +/// a 'conda-meta' directory as child. This will find 'conda-meta' in a platform agnostic way. +pub fn is_conda_environment(any_path: &Path) -> bool { + let conda_meta_path = get_conda_meta_path(any_path); + match conda_meta_path { + Some(path) => path.exists(), + None => false, + } +} + +struct CondaPackage { + path: PathBuf, + version: String, +} + +/// Get the path to the json file along with the version of a package in the conda environment from the 'conda-meta' directory. +fn get_conda_package_json_path(any_path: &Path, package: &str) -> Option { + let package_name = format!("{}-", package); + let conda_meta_path = get_conda_meta_path(any_path)?; + let regex = Regex::new(format!("^{}-((\\d+\\.*)*)-.*.json$", package).as_str()); + std::fs::read_dir(conda_meta_path).ok()?.find_map(|entry| { + let path = entry.ok()?.path(); + let file_name = path.file_name()?.to_string_lossy(); + if file_name.starts_with(&package_name) && file_name.ends_with(".json") { + match regex.clone().ok()?.captures(&file_name)?.get(1) { + Some(version) => Some(CondaPackage { + path: path.clone(), + version: version.as_str().to_string(), + }), + None => None, + } + } else { + None + } + }) +} + +/// Checks if the `python` package is installed in the conda environment +#[allow(dead_code)] +pub fn is_python_conda_env(any_path: &Path) -> bool { + let conda_python_json_path = get_conda_package_json_path(any_path, "python"); + match conda_python_json_path { + Some(result) => result.path.exists(), + None => false, + } +} + +/// Get the version of the `python` package in the conda environment +pub fn get_conda_python_version(any_path: &Path) -> Option { + let conda_python_json_path = get_conda_package_json_path(any_path, "python"); + match conda_python_json_path { + Some(result) => Some(result.version.clone()), + None => None, + } +} + +/// Specifically returns the file names that are valid for 'conda' on windows +#[cfg(windows)] +fn get_conda_bin_names() -> Vec<&'static str> { + vec!["conda.exe", "conda.bat"] +} + +/// Specifically returns the file names that are valid for 'conda' on linux/Mac +#[cfg(unix)] +fn get_conda_bin_names() -> Vec<&'static str> { + vec!["conda"] +} + +/// Find the conda binary on the PATH environment variable +fn find_conda_binary_on_path(environment: &dyn known::Environment) -> Option { + let paths = environment.get_env_var("PATH".to_string())?; + for path in env::split_paths(&paths) { + for bin in get_conda_bin_names() { + let conda_path = path.join(bin); + match std::fs::metadata(&conda_path) { + Ok(metadata) => { + if metadata.is_file() || metadata.is_symlink() { + return Some(conda_path); + } + } + Err(_) => (), + } + } + } + None +} + +#[cfg(windows)] +fn get_known_conda_locations(environment: &dyn known::Environment) -> Vec { + let user_profile = environment.get_env_var("USERPROFILE".to_string()).unwrap(); + let program_data = environment.get_env_var("PROGRAMDATA".to_string()).unwrap(); + let all_user_profile = environment + .get_env_var("ALLUSERSPROFILE".to_string()) + .unwrap(); + let home_drive = environment.get_env_var("HOMEDRIVE".to_string()).unwrap(); + let mut known_paths = vec![ + Path::new(&user_profile).join("Anaconda3\\Scripts"), + Path::new(&program_data).join("Anaconda3\\Scripts"), + Path::new(&all_user_profile).join("Anaconda3\\Scripts"), + Path::new(&home_drive).join("Anaconda3\\Scripts"), + Path::new(&user_profile).join("Miniconda3\\Scripts"), + Path::new(&program_data).join("Miniconda3\\Scripts"), + Path::new(&all_user_profile).join("Miniconda3\\Scripts"), + Path::new(&home_drive).join("Miniconda3\\Scripts"), + ]; + known_paths.append(&mut environment.get_know_global_search_locations()); + known_paths +} + +#[cfg(unix)] +fn get_known_conda_locations(environment: &dyn known::Environment) -> Vec { + let mut known_paths = vec![ + PathBuf::from("/opt/anaconda3/bin"), + PathBuf::from("/opt/miniconda3/bin"), + PathBuf::from("/usr/local/anaconda3/bin"), + PathBuf::from("/usr/local/miniconda3/bin"), + PathBuf::from("/usr/anaconda3/bin"), + PathBuf::from("/usr/miniconda3/bin"), + PathBuf::from("/home/anaconda3/bin"), + PathBuf::from("/home/miniconda3/bin"), + PathBuf::from("/anaconda3/bin"), + PathBuf::from("/miniconda3/bin"), + ]; + if let Some(home) = environment.get_user_home() { + known_paths.push(PathBuf::from(home.clone()).join("anaconda3/bin")); + known_paths.push(PathBuf::from(home).join("miniconda3/bin")); + } + known_paths.append(&mut environment.get_know_global_search_locations()); + known_paths +} + +/// Find conda binary in known locations +fn find_conda_binary_in_known_locations(environment: &dyn known::Environment) -> Option { + let conda_bin_names = get_conda_bin_names(); + let known_locations = get_known_conda_locations(environment); + for location in known_locations { + for bin in &conda_bin_names { + let conda_path = location.join(bin); + if let Some(metadata) = std::fs::metadata(&conda_path).ok() { + if metadata.is_file() || metadata.is_symlink() { + return Some(conda_path); + } + } + } + } + None +} + +/// Find the conda binary on the system +pub fn find_conda_binary(environment: &dyn known::Environment) -> Option { + let conda_binary_on_path = find_conda_binary_on_path(environment); + match conda_binary_on_path { + Some(conda_binary_on_path) => Some(conda_binary_on_path), + None => find_conda_binary_in_known_locations(environment), + } +} + +pub fn get_conda_version(conda_binary: &PathBuf) -> Option { + let mut parent = conda_binary.parent()?; + if parent.ends_with("bin") { + parent = parent.parent()?; + } + if parent.ends_with("Library") { + parent = parent.parent()?; + } + match get_conda_package_json_path(&parent, "conda") { + Some(result) => Some(result.version), + None => match get_conda_package_json_path(&parent.parent()?, "conda") { + Some(result) => Some(result.version), + None => None, + }, + } +} + +fn get_conda_envs_from_environment_txt(environment: &dyn known::Environment) -> Vec { + let mut envs = vec![]; + let home = environment.get_user_home(); + match home { + Some(home) => { + let home = Path::new(&home); + let environment_txt = home.join(".conda").join("environments.txt"); + match std::fs::read_to_string(environment_txt) { + Ok(reader) => { + for line in reader.lines() { + envs.push(line.to_string()); + } + } + Err(_) => (), + } + } + None => (), + } + envs +} + +fn get_known_env_locations( + conda_bin: &PathBuf, + environment: &dyn known::Environment, +) -> Vec { + let mut paths = vec![]; + let home = environment.get_user_home(); + match home { + Some(home) => { + let home = Path::new(&home); + let conda_envs = home.join(".conda").join("envs"); + paths.push(conda_envs.to_string_lossy().to_string()); + } + None => (), + } + + match conda_bin.parent() { + Some(parent) => { + paths.push(parent.to_string_lossy().to_string()); + let conda_envs = parent.join("envs"); + paths.push(conda_envs.to_string_lossy().to_string()); + match parent.parent() { + Some(parent) => { + paths.push(parent.to_string_lossy().to_string()); + let conda_envs = parent.join("envs"); + paths.push(conda_envs.to_string_lossy().to_string()); + } + None => (), + } + } + None => (), + } + + paths +} + +fn get_conda_envs_from_known_env_locations( + conda_bin: &PathBuf, + environment: &dyn known::Environment, +) -> Vec { + let mut envs = vec![]; + for location in get_known_env_locations(conda_bin, environment) { + if is_conda_environment(&Path::new(&location)) { + envs.push(location.to_string()); + } + match std::fs::read_dir(location) { + Ok(reader) => { + for entry in reader { + match entry { + Ok(entry) => { + let metadata = entry.metadata(); + match metadata { + Ok(metadata) => { + if metadata.is_dir() { + let path = entry.path(); + if is_conda_environment(&path) { + envs.push(path.to_string_lossy().to_string()); + } + } + } + Err(_) => (), + } + } + Err(_) => (), + } + } + } + Err(_) => (), + } + } + envs +} + +struct CondaEnv { + named: bool, + name: String, + path: PathBuf, +} + +fn get_distinct_conda_envs( + conda_bin: &PathBuf, + environment: &dyn known::Environment, +) -> Vec { + let mut envs = get_conda_envs_from_environment_txt(environment); + let mut known_envs = get_conda_envs_from_known_env_locations(conda_bin, environment); + envs.append(&mut known_envs); + envs.sort(); + envs.dedup(); + + let locations = get_known_env_locations(conda_bin, environment); + let mut conda_envs = vec![]; + for env in envs { + let env = Path::new(&env); + let mut named = false; + let mut name = "".to_string(); + for location in &locations { + let location = Path::new(location).join("envs"); + match env.strip_prefix(location) { + Ok(prefix) => { + named = true; + name = match prefix.to_str() { + Some(name) => { + let name = name.to_string(); + if name == "" { + "base".to_string() + } else { + name.to_string() + } + } + None => "base".to_string(), + }; + break; + } + Err(_) => (), + } + } + conda_envs.push(CondaEnv { + named, + name, + path: PathBuf::from(env), + }); + } + conda_envs +} + +pub struct Conda<'a> { + pub manager: Option, + pub environment: &'a dyn Environment, +} + +impl Conda<'_> { + pub fn with<'a>(environment: &'a impl Environment) -> Conda { + Conda { + environment, + manager: None, + } + } +} + +impl Locator for Conda<'_> { + fn resolve(&self, _env: &PythonEnv) -> Option { + // We will find everything in gather + None + } + + fn find(&mut self) -> Option { + let conda_binary = find_conda_binary(self.environment)?; + let manager = EnvManager::new( + conda_binary.clone(), + get_conda_version(&conda_binary), + EnvManagerType::Conda, + ); + + let envs = get_distinct_conda_envs(&conda_binary, self.environment); + let mut environments: Vec = vec![]; + for env in envs { + let executable = find_python_binary_path(Path::new(&env.path)); + let env = messaging::PythonEnvironment::new( + Some(env.name.to_string()), + executable.clone(), + messaging::PythonEnvironmentCategory::Conda, + get_conda_python_version(&env.path), + Some(env.path.clone()), + Some(env.path.clone()), + Some(manager.clone()), + if env.named { + Some(vec![ + conda_binary.to_string_lossy().to_string(), + "run".to_string(), + "-n".to_string(), + env.name.to_string(), + "python".to_string(), + ]) + } else { + Some(vec![ + conda_binary.to_string_lossy().to_string(), + "run".to_string(), + "-p".to_string(), + env.path.to_string_lossy().to_string(), + "python".to_string(), + ]) + }, + ); + + environments.push(env) + } + + Some(LocatorResult { + managers: vec![manager], + environments, + }) + } +} diff --git a/native_locator/src/homebrew.rs b/native_locator/src/homebrew.rs index 9111f5d9699d..36a8cfb8f46d 100644 --- a/native_locator/src/homebrew.rs +++ b/native_locator/src/homebrew.rs @@ -38,7 +38,7 @@ impl Locator for Homebrew<'_> { None } - fn find(&self) -> Option { + fn find(&mut self) -> Option { let homebrew_prefix = self .environment .get_env_var("HOMEBREW_PREFIX".to_string())?; diff --git a/native_locator/src/locator.rs b/native_locator/src/locator.rs index ee72ef0591a7..18d529a80564 100644 --- a/native_locator/src/locator.rs +++ b/native_locator/src/locator.rs @@ -23,5 +23,5 @@ pub trait Locator { /** * Finds all environments specific to this locator. */ - fn find(&self) -> Option; + fn find(&mut self) -> Option; } diff --git a/native_locator/src/main.rs b/native_locator/src/main.rs index 56b6dc65e237..3620fe284bda 100644 --- a/native_locator/src/main.rs +++ b/native_locator/src/main.rs @@ -24,8 +24,8 @@ mod utils; mod venv; mod virtualenv; mod virtualenvwrapper; -mod windows_store; mod windows_registry; +mod windows_store; fn main() { let environment = EnvironmentApi {}; @@ -39,27 +39,27 @@ fn main() { let venv_locator = venv::Venv::new(); let virtualenvwrapper_locator = virtualenvwrapper::VirtualEnvWrapper::with(&environment); let pipenv_locator = pipenv::PipEnv::new(); - let path_locator = common_python::PythonOnPath::with(&environment); - let pyenv_locator = pyenv::PyEnv::with(&environment); + let mut path_locator = common_python::PythonOnPath::with(&environment); + let mut conda_locator = conda::Conda::with(&environment); + let mut pyenv_locator = pyenv::PyEnv::with(&environment, &mut conda_locator); #[cfg(unix)] - let homebrew_locator = homebrew::Homebrew::with(&environment); + let mut homebrew_locator = homebrew::Homebrew::with(&environment); #[cfg(windows)] - let windows_store = windows_store::WindowsStore::with(&environment); + let mut windows_store = windows_store::WindowsStore::with(&environment); #[cfg(windows)] - let windows_registry = windows_registry::WindowsRegistry::new(); - let conda_locator = conda::Conda::with(&environment); + let mut windows_registry = windows_registry::WindowsRegistry::new(); // Step 1: These environments take precedence over all others. // As they are very specific and guaranteed to be specific type. - find_environments(&pyenv_locator, &mut dispatcher); + find_environments(&mut pyenv_locator, &mut dispatcher); #[cfg(unix)] - find_environments(&homebrew_locator, &mut dispatcher); - find_environments(&conda_locator, &mut dispatcher); + find_environments(&mut homebrew_locator, &mut dispatcher); + find_environments(&mut conda_locator, &mut dispatcher); #[cfg(windows)] - find_environments(&windows_registry, &mut dispatcher); + find_environments(&mut windows_registry, &mut dispatcher); #[cfg(windows)] - find_environments(&windows_store, &mut dispatcher); + find_environments(&mut windows_store, &mut dispatcher); // Step 2: Search in some global locations. for env in list_global_virtual_envs(&environment).iter() { @@ -74,7 +74,7 @@ fn main() { } // Step 3: Finally find in the current PATH variable - find_environments(&path_locator, &mut dispatcher); + find_environments(&mut path_locator, &mut dispatcher); match now.elapsed() { Ok(elapsed) => { @@ -100,7 +100,7 @@ fn resolve_environment( false } -fn find_environments(locator: &dyn Locator, dispatcher: &mut JsonRpcDispatcher) -> Option<()> { +fn find_environments(locator: &mut dyn Locator, dispatcher: &mut JsonRpcDispatcher) -> Option<()> { if let Some(result) = locator.find() { result .environments diff --git a/native_locator/src/messaging.rs b/native_locator/src/messaging.rs index b39cd19bb5b6..dddcb6e1b595 100644 --- a/native_locator/src/messaging.rs +++ b/native_locator/src/messaging.rs @@ -3,7 +3,7 @@ use crate::{ logging::{LogLevel, LogMessage}, - utils::PythonEnv, + utils::{get_environment_key, get_environment_manager_key, PythonEnv}, }; use env_logger::Builder; use log::LevelFilter; @@ -237,11 +237,10 @@ impl MessageDispatcher for JsonRpcDispatcher { } fn report_environment_manager(&mut self, env: EnvManager) -> () { - if let Some(key) = get_manager_key(&env) { - if !self.reported_managers.contains(&key) { - self.reported_managers.insert(key); - send_message(EnvManagerMessage::new(env)); - } + let key = get_environment_manager_key(&env); + if !self.reported_managers.contains(&key) { + self.reported_managers.insert(key); + send_message(EnvManagerMessage::new(env)); } } fn report_environment(&mut self, env: PythonEnvironment) -> () { @@ -266,17 +265,3 @@ pub fn create_dispatcher() -> JsonRpcDispatcher { reported_environments: HashSet::new(), } } - -fn get_environment_key(env: &PythonEnvironment) -> Option { - match env.python_executable_path.clone() { - Some(key) => Some(key.as_os_str().to_str()?.to_string()), - None => match env.env_path.clone() { - Some(key) => Some(key.as_os_str().to_str().unwrap().to_string()), - None => None, - }, - } -} - -fn get_manager_key(manager: &EnvManager) -> Option { - Some(manager.executable_path.to_str()?.to_string()) -} diff --git a/native_locator/src/pipenv.rs b/native_locator/src/pipenv.rs index 3e9de48ff13a..0acd00893310 100644 --- a/native_locator/src/pipenv.rs +++ b/native_locator/src/pipenv.rs @@ -42,7 +42,7 @@ impl Locator for PipEnv { )) } - fn find(&self) -> Option { + fn find(&mut self) -> Option { None } } diff --git a/native_locator/src/pyenv.rs b/native_locator/src/pyenv.rs index fb48615b7464..5832718b4c44 100644 --- a/native_locator/src/pyenv.rs +++ b/native_locator/src/pyenv.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use crate::conda::CondaLocator; use crate::known; use crate::known::Environment; use crate::locator::Locator; @@ -119,6 +120,16 @@ fn get_pure_python_environment( )) } +fn is_conda_environment(path: &PathBuf) -> bool { + if let Some(name) = path.file_name() { + let name = name.to_ascii_lowercase().to_string_lossy().to_string(); + return name.starts_with("anaconda") + || name.starts_with("miniconda") + || name.starts_with("miniforge"); + } + false +} + fn get_virtual_env_environment( executable: &PathBuf, path: &PathBuf, @@ -145,6 +156,7 @@ fn get_virtual_env_environment( pub fn list_pyenv_environments( manager: &Option, environment: &dyn known::Environment, + conda_locator: &mut dyn CondaLocator, ) -> Option> { let pyenv_dir = get_pyenv_dir(environment)?; let mut envs: Vec = vec![]; @@ -161,12 +173,16 @@ pub fn list_pyenv_environments( continue; } if let Some(executable) = find_python_binary_path(&path) { - match get_pure_python_environment(&executable, &path, manager) { - Some(env) => envs.push(env), - None => match get_virtual_env_environment(&executable, &path, manager) { - Some(env) => envs.push(env), - None => (), - }, + if let Some(env) = get_pure_python_environment(&executable, &path, manager) { + envs.push(env); + } else if let Some(env) = get_virtual_env_environment(&executable, &path, manager) { + envs.push(env); + } else if is_conda_environment(&path) { + if let Some(result) = conda_locator.find_in(&path) { + result.environments.iter().for_each(|e| { + envs.push(e.clone()); + }); + } } } } @@ -177,11 +193,18 @@ pub fn list_pyenv_environments( pub struct PyEnv<'a> { pub environment: &'a dyn Environment, + pub conda_locator: &'a mut dyn CondaLocator, } impl PyEnv<'_> { - pub fn with<'a>(environment: &'a impl Environment) -> PyEnv { - PyEnv { environment } + pub fn with<'a>( + environment: &'a impl Environment, + conda_locator: &'a mut impl CondaLocator, + ) -> PyEnv<'a> { + PyEnv { + environment, + conda_locator, + } } } @@ -191,11 +214,13 @@ impl Locator for PyEnv<'_> { None } - fn find(&self) -> Option { + fn find(&mut self) -> Option { let pyenv_binary = get_pyenv_binary(self.environment)?; let manager = messaging::EnvManager::new(pyenv_binary, None, EnvManagerType::Pyenv); let mut environments: Vec = vec![]; - if let Some(envs) = list_pyenv_environments(&Some(manager.clone()), self.environment) { + if let Some(envs) = + list_pyenv_environments(&Some(manager.clone()), self.environment, self.conda_locator) + { for env in envs { environments.push(env); } diff --git a/native_locator/src/utils.rs b/native_locator/src/utils.rs index 26aa68566019..b0cfb0e6e412 100644 --- a/native_locator/src/utils.rs +++ b/native_locator/src/utils.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +use crate::messaging::{EnvManager, PythonEnvironment}; use regex::Regex; use std::{ fs, @@ -147,3 +148,18 @@ pub fn list_python_environments(path: &PathBuf) -> Option> { Some(python_envs) } + +pub fn get_environment_key(env: &PythonEnvironment) -> Option { + if let Some(ref path) = env.python_executable_path { + return Some(path.to_string_lossy().to_string()); + } + if let Some(ref path) = env.env_path { + return Some(path.to_string_lossy().to_string()); + } + + None +} + +pub fn get_environment_manager_key(env: &EnvManager) -> String { + return env.executable_path.to_string_lossy().to_string(); +} diff --git a/native_locator/src/venv.rs b/native_locator/src/venv.rs index 24c490acafad..05ddaf7f7522 100644 --- a/native_locator/src/venv.rs +++ b/native_locator/src/venv.rs @@ -48,7 +48,7 @@ impl Locator for Venv { None } - fn find(&self) -> Option { + fn find(&mut self) -> Option { // There are no common global locations for virtual environments. // We expect the user of this class to call `is_compatible` None diff --git a/native_locator/src/virtualenv.rs b/native_locator/src/virtualenv.rs index 49926f14c0ad..2268a8253b06 100644 --- a/native_locator/src/virtualenv.rs +++ b/native_locator/src/virtualenv.rs @@ -79,7 +79,7 @@ impl Locator for VirtualEnv { None } - fn find(&self) -> Option { + fn find(&mut self) -> Option { // There are no common global locations for virtual environments. // We expect the user of this class to call `is_compatible` None diff --git a/native_locator/src/virtualenvwrapper.rs b/native_locator/src/virtualenvwrapper.rs index 1c679171c903..9e5d28e9f445 100644 --- a/native_locator/src/virtualenvwrapper.rs +++ b/native_locator/src/virtualenvwrapper.rs @@ -102,7 +102,7 @@ impl Locator for VirtualEnvWrapper<'_> { None } - fn find(&self) -> Option { + fn find(&mut self) -> Option { let work_on_home = get_work_on_home_path(self.environment)?; let envs = list_python_environments(&work_on_home)?; let mut environments: Vec = vec![]; diff --git a/native_locator/src/windows_registry.rs b/native_locator/src/windows_registry.rs index a26362a6f9e7..ef886b28dbdd 100644 --- a/native_locator/src/windows_registry.rs +++ b/native_locator/src/windows_registry.rs @@ -74,7 +74,7 @@ impl Locator for WindowsRegistry { None } - fn find(&self) -> Option { + fn find(&mut self) -> Option { let environments = get_registry_pythons("PythonCore")?; if environments.is_empty() { None diff --git a/native_locator/src/windows_store.rs b/native_locator/src/windows_store.rs index 3c31673a1193..2f2a0c2f81ce 100644 --- a/native_locator/src/windows_store.rs +++ b/native_locator/src/windows_store.rs @@ -69,7 +69,7 @@ impl Locator for WindowsStore<'_> { None } - fn find(&self) -> Option { + fn find(&mut self) -> Option { let mut environments: Vec = vec![]; if let Some(envs) = list_windows_store_python_executables(self.environment) { envs.iter().for_each(|env| { diff --git a/native_locator/tests/common_python_test.rs b/native_locator/tests/common_python_test.rs index 01e765d23c0d..a21f3349f38c 100644 --- a/native_locator/tests/common_python_test.rs +++ b/native_locator/tests/common_python_test.rs @@ -26,8 +26,7 @@ fn find_python_in_path_this() { Vec::new(), ); - let locator = common_python::PythonOnPath::with(&known); - locator.find(); + let mut locator = common_python::PythonOnPath::with(&known); let result = locator.find(); let environments = get_environments_from_result(&result); diff --git a/native_locator/tests/conda_test.rs b/native_locator/tests/conda_test.rs index 3d3a296e52b4..77fed4a2393d 100644 --- a/native_locator/tests/conda_test.rs +++ b/native_locator/tests/conda_test.rs @@ -16,7 +16,7 @@ fn does_not_find_any_conda_envs() { Vec::new(), ); - let locator = conda::Conda::with(&known); + let mut locator = conda::Conda::with(&known); let result = locator.find(); let environments = get_environments_from_result(&result); @@ -33,7 +33,8 @@ fn find_conda_exe_and_empty_envs() { use python_finder::messaging::{EnvManager, EnvManagerType}; use python_finder::{conda, locator::Locator}; use serde_json::json; - use std::{collections::HashMap, path::PathBuf}; + use std::collections::HashMap; + let user_home = test_file_path(&["tests/unix/conda_without_envs"]); let conda_dir = test_file_path(&["tests/unix/conda_without_envs"]); let known = create_test_environment( @@ -41,20 +42,24 @@ fn find_conda_exe_and_empty_envs() { "PATH".to_string(), conda_dir.clone().to_str().unwrap().to_string(), )]), - Some(PathBuf::from("SOME_BOGUS_HOME_DIR")), + Some(user_home), Vec::new(), ); - let locator = conda::Conda::with(&known); + let mut locator = conda::Conda::with(&known); let result = locator.find(); - let managers = get_managers_from_result(&result); assert_eq!(managers.len(), 1); - let conda_exe = join_test_paths(&[conda_dir.clone().to_str().unwrap(), "conda"]); + let conda_exe = join_test_paths(&[ + conda_dir.clone().to_str().unwrap(), + "anaconda3", + "bin", + "conda", + ]); let expected_conda_manager = EnvManager { executable_path: conda_exe.clone(), - version: None, + version: Some("4.0.2".to_string()), tool: EnvManagerType::Conda, }; assert_messages( @@ -75,7 +80,8 @@ fn finds_two_conda_envs_from_txt() { use std::collections::HashMap; use std::fs; - let conda_dir = test_file_path(&["tests/unix/conda"]); + let home = test_file_path(&["tests/unix/conda"]); + let conda_dir = test_file_path(&["tests/unix/conda/anaconda3"]); let conda_1 = join_test_paths(&[conda_dir.clone().to_str().unwrap(), "envs/one"]); let conda_2 = join_test_paths(&[conda_dir.clone().to_str().unwrap(), "envs/two"]); let _ = fs::write( @@ -92,24 +98,24 @@ fn finds_two_conda_envs_from_txt() { "PATH".to_string(), conda_dir.clone().to_str().unwrap().to_string(), )]), - Some(conda_dir.clone()), + Some(home), Vec::new(), ); - let locator = conda::Conda::with(&known); + let mut locator = conda::Conda::with(&known); let result = locator.find(); let managers = get_managers_from_result(&result); let environments = get_environments_from_result(&result); assert_eq!(managers.len(), 1); - let conda_exe = join_test_paths(&[conda_dir.clone().to_str().unwrap(), "conda"]); + let conda_exe = join_test_paths(&[conda_dir.clone().to_str().unwrap(), "bin", "conda"]); let conda_1_exe = join_test_paths(&[conda_1.clone().to_str().unwrap(), "python"]); let conda_2_exe = join_test_paths(&[conda_2.clone().to_str().unwrap(), "python"]); let expected_conda_manager = EnvManager { executable_path: conda_exe.clone(), - version: None, + version: Some("4.0.2".to_string()), tool: EnvManagerType::Conda, }; let expected_conda_1 = PythonEnvironment { diff --git a/native_locator/tests/pyenv_test.rs b/native_locator/tests/pyenv_test.rs index a63615a68ef1..45df29031fb7 100644 --- a/native_locator/tests/pyenv_test.rs +++ b/native_locator/tests/pyenv_test.rs @@ -9,7 +9,7 @@ fn does_not_find_any_pyenv_envs() { use crate::common::{ create_test_environment, get_environments_from_result, get_managers_from_result, }; - use python_finder::{locator::Locator, pyenv}; + use python_finder::{conda::Conda, locator::Locator, pyenv}; use std::{collections::HashMap, path::PathBuf}; let known = create_test_environment( @@ -18,8 +18,8 @@ fn does_not_find_any_pyenv_envs() { Vec::new(), ); - let locator = pyenv::PyEnv::with(&known); - locator.find(); + let mut conda = Conda::with(&known); + let mut locator = pyenv::PyEnv::with(&known, &mut conda); let result = locator.find(); assert_eq!(get_managers_from_result(&result).len(), 0); @@ -33,8 +33,8 @@ fn does_not_find_any_pyenv_envs_even_with_pyenv_installed() { assert_messages, create_test_environment, get_managers_from_result, join_test_paths, test_file_path, }; - use python_finder::locator::Locator; use python_finder::pyenv; + use python_finder::{conda::Conda, locator::Locator}; use serde_json::json; use std::{collections::HashMap, path::PathBuf}; @@ -47,7 +47,8 @@ fn does_not_find_any_pyenv_envs_even_with_pyenv_installed() { vec![PathBuf::from(homebrew_bin)], ); - let locator = pyenv::PyEnv::with(&known); + let mut conda = Conda::with(&known); + let mut locator = pyenv::PyEnv::with(&known, &mut conda); let result = locator.find(); let managers = get_managers_from_result(&result); @@ -67,6 +68,7 @@ fn find_pyenv_envs() { assert_messages, create_test_environment, get_environments_from_result, get_managers_from_result, join_test_paths, test_file_path, }; + use python_finder::conda::Conda; use python_finder::locator::Locator; use python_finder::{ messaging::{EnvManager, EnvManagerType, PythonEnvironment}, @@ -84,7 +86,8 @@ fn find_pyenv_envs() { vec![PathBuf::from(homebrew_bin)], ); - let locator = pyenv::PyEnv::with(&known); + let mut conda = Conda::with(&known); + let mut locator = pyenv::PyEnv::with(&known, &mut conda); let result = locator.find(); let managers = get_managers_from_result(&result); diff --git a/native_locator/tests/unix/conda/.conda/environments.txt b/native_locator/tests/unix/conda/.conda/environments.txt index 908019719b55..3a9e625c3050 100644 --- a/native_locator/tests/unix/conda/.conda/environments.txt +++ b/native_locator/tests/unix/conda/.conda/environments.txt @@ -1,2 +1,2 @@ -/Users/donjayamanne/Development/vsc/vscode-python/native_locator/tests/unix/conda/envs/one -/Users/donjayamanne/Development/vsc/vscode-python/native_locator/tests/unix/conda/envs/two \ No newline at end of file +/Users/donjayamanne/Development/vsc/vscode-python/native_locator/tests/unix/conda/anaconda3/envs/one +/Users/donjayamanne/Development/vsc/vscode-python/native_locator/tests/unix/conda/anaconda3/envs/two \ No newline at end of file diff --git a/native_locator/tests/unix/conda/conda b/native_locator/tests/unix/conda/anaconda3/bin/conda similarity index 100% rename from native_locator/tests/unix/conda/conda rename to native_locator/tests/unix/conda/anaconda3/bin/conda diff --git a/native_locator/tests/unix/conda/envs/one/conda-meta/python-slugify-5.0.2-pyhd3eb1b0_0.json b/native_locator/tests/unix/conda/anaconda3/conda-meta/conda-4.0.2-pyhd3eb1b0_0.json similarity index 100% rename from native_locator/tests/unix/conda/envs/one/conda-meta/python-slugify-5.0.2-pyhd3eb1b0_0.json rename to native_locator/tests/unix/conda/anaconda3/conda-meta/conda-4.0.2-pyhd3eb1b0_0.json diff --git a/native_locator/tests/unix/conda/envs/one/conda-meta/python-10.0.1-hdf0ec26_0_cpython.json b/native_locator/tests/unix/conda/anaconda3/conda-meta/python-10.0.1-hdf0ec26_0_cpython.json similarity index 100% rename from native_locator/tests/unix/conda/envs/one/conda-meta/python-10.0.1-hdf0ec26_0_cpython.json rename to native_locator/tests/unix/conda/anaconda3/conda-meta/python-10.0.1-hdf0ec26_0_cpython.json diff --git a/native_locator/tests/unix/conda/envs/one/python b/native_locator/tests/unix/conda/anaconda3/conda-meta/python-slugify-5.0.2-pyhd3eb1b0_0.json similarity index 100% rename from native_locator/tests/unix/conda/envs/one/python rename to native_locator/tests/unix/conda/anaconda3/conda-meta/python-slugify-5.0.2-pyhd3eb1b0_0.json diff --git a/native_locator/tests/unix/conda/anaconda3/envs/one/conda-meta/python-10.0.1-hdf0ec26_0_cpython.json b/native_locator/tests/unix/conda/anaconda3/envs/one/conda-meta/python-10.0.1-hdf0ec26_0_cpython.json new file mode 100644 index 000000000000..23127993ac05 --- /dev/null +++ b/native_locator/tests/unix/conda/anaconda3/envs/one/conda-meta/python-10.0.1-hdf0ec26_0_cpython.json @@ -0,0 +1 @@ +10.1.1 diff --git a/native_locator/tests/unix/conda/envs/two/python b/native_locator/tests/unix/conda/anaconda3/envs/one/conda-meta/python-slugify-5.0.2-pyhd3eb1b0_0.json similarity index 100% rename from native_locator/tests/unix/conda/envs/two/python rename to native_locator/tests/unix/conda/anaconda3/envs/one/conda-meta/python-slugify-5.0.2-pyhd3eb1b0_0.json diff --git a/native_locator/tests/unix/conda_without_envs/conda b/native_locator/tests/unix/conda/anaconda3/envs/one/python similarity index 100% rename from native_locator/tests/unix/conda_without_envs/conda rename to native_locator/tests/unix/conda/anaconda3/envs/one/python diff --git a/native_locator/tests/unix/conda/anaconda3/envs/two/python b/native_locator/tests/unix/conda/anaconda3/envs/two/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/native_locator/tests/unix/conda_without_envs/anaconda3/bin/conda b/native_locator/tests/unix/conda_without_envs/anaconda3/bin/conda new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/native_locator/tests/unix/conda_without_envs/anaconda3/conda-meta/conda-4.0.2-pyhd3eb1b0_0.json b/native_locator/tests/unix/conda_without_envs/anaconda3/conda-meta/conda-4.0.2-pyhd3eb1b0_0.json new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/native_locator/tests/unix/conda_without_envs/anaconda3/conda-meta/python-10.0.1-hdf0ec26_0_cpython.json b/native_locator/tests/unix/conda_without_envs/anaconda3/conda-meta/python-10.0.1-hdf0ec26_0_cpython.json new file mode 100644 index 000000000000..23127993ac05 --- /dev/null +++ b/native_locator/tests/unix/conda_without_envs/anaconda3/conda-meta/python-10.0.1-hdf0ec26_0_cpython.json @@ -0,0 +1 @@ +10.1.1 diff --git a/native_locator/tests/unix/conda_without_envs/anaconda3/conda-meta/python-slugify-5.0.2-pyhd3eb1b0_0.json b/native_locator/tests/unix/conda_without_envs/anaconda3/conda-meta/python-slugify-5.0.2-pyhd3eb1b0_0.json new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/nativeLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/nativeLocator.ts index 95bf67262f53..4ec6723705f8 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/nativeLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/nativeLocator.ts @@ -104,14 +104,16 @@ export class NativeLocator implements ILocator, IDisposable { promise.finally(() => disposable.dispose()); disposables.push( this.finder.onDidFindPythonEnvironment((data: NativeEnvInfo) => { - envs.push({ - kind: categoryToKind(data.category), - // TODO: What if executable is undefined? - executablePath: data.pythonExecutablePath!, - envPath: data.envPath, - version: parseVersion(data.version), - name: data.name === '' ? undefined : data.name, - }); + // TODO: What if executable is undefined? + if (data.pythonExecutablePath) { + envs.push({ + kind: categoryToKind(data.category), + executablePath: data.pythonExecutablePath, + envPath: data.envPath, + version: parseVersion(data.version), + name: data.name === '' ? undefined : data.name, + }); + } }), this.finder.onDidFindEnvironmentManager((data: NativeEnvManagerInfo) => { switch (toolToKnownEnvironmentTool(data.tool)) {