diff --git a/native_locator/src/conda.rs b/native_locator/src/conda.rs index be6f586ba5b4..9377185e8eed 100644 --- a/native_locator/src/conda.rs +++ b/native_locator/src/conda.rs @@ -3,6 +3,7 @@ use crate::known; use crate::messaging; +use crate::messaging::EnvManagerType; use crate::utils::find_python_binary_path; use regex::Regex; use std::env; @@ -218,15 +219,15 @@ pub fn find_conda_binary(environment: &impl known::Environment) -> Option Option { let mut parent = conda_binary.parent()?; - if parent.ends_with("bin"){ + if parent.ends_with("bin") { parent = parent.parent()?; } - if parent.ends_with("Library"){ + if parent.ends_with("Library") { parent = parent.parent()?; } let conda_python_json_path = match get_conda_package_json_path(&parent, "conda") { Some(exe) => Some(exe), - None => get_conda_package_json_path(&parent.parent()?, "conda") + None => get_conda_package_json_path(&parent.parent()?, "conda"), }?; get_version_from_meta_json(&conda_python_json_path) } @@ -387,6 +388,7 @@ pub fn find_and_report( let params = messaging::EnvManager::new( conda_binary.to_string_lossy().to_string(), get_conda_version(&conda_binary), + EnvManagerType::Conda, ); dispatcher.report_environment_manager(params); diff --git a/native_locator/src/global_virtualenvs.rs b/native_locator/src/global_virtualenvs.rs index cd8c2e392054..f86a493ecee1 100644 --- a/native_locator/src/global_virtualenvs.rs +++ b/native_locator/src/global_virtualenvs.rs @@ -1,9 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use crate::pipenv; +use crate::virtualenvwrapper; use crate::{ known, - utils::{find_python_binary_path, get_version}, + messaging::MessageDispatcher, + utils::{find_python_binary_path, get_version, PythonEnv}, }; use std::{fs, path::PathBuf}; @@ -43,12 +46,6 @@ fn get_global_virtualenv_dirs(environment: &impl known::Environment) -> Vec, -} - pub fn list_global_virtualenvs(environment: &impl known::Environment) -> Vec { let mut python_envs: Vec = vec![]; for root_dir in get_global_virtualenv_dirs(environment).iter() { @@ -73,3 +70,19 @@ pub fn list_global_virtualenvs(environment: &impl known::Environment) -> Vec Option<()> { + for env in list_global_virtualenvs(environment).iter() { + if pipenv::find_and_report(&env, dispatcher).is_some() { + continue; + } + if virtualenvwrapper::find_and_report(&env, dispatcher, environment).is_some() { + continue; + } + } + + None +} diff --git a/native_locator/src/lib.rs b/native_locator/src/lib.rs index 41cb3d97bd10..1d9439bce7d7 100644 --- a/native_locator/src/lib.rs +++ b/native_locator/src/lib.rs @@ -9,3 +9,6 @@ pub mod conda; pub mod known; pub mod pyenv; pub mod global_virtualenvs; +pub mod virtualenvwrapper; +pub mod pipenv; +pub mod virtualenv; diff --git a/native_locator/src/main.rs b/native_locator/src/main.rs index dad488f2186c..129a0973464e 100644 --- a/native_locator/src/main.rs +++ b/native_locator/src/main.rs @@ -16,6 +16,8 @@ mod messaging; mod pipenv; mod pyenv; mod utils; +mod virtualenv; +mod virtualenvwrapper; mod windows_python; fn main() { @@ -25,6 +27,8 @@ fn main() { dispatcher.log_info("Starting Native Locator"); let now = SystemTime::now(); + global_virtualenvs::find_and_report(&mut dispatcher, &environment); + // Finds python on PATH common_python::find_and_report(&mut dispatcher, &environment); @@ -37,8 +41,6 @@ fn main() { pyenv::find_and_report(&mut dispatcher, &environment); - pipenv::find_and_report(&mut dispatcher, &environment); - #[cfg(unix)] homebrew::find_and_report(&mut dispatcher, &environment); diff --git a/native_locator/src/messaging.rs b/native_locator/src/messaging.rs index a996ec3964e1..103c3b77fe4c 100644 --- a/native_locator/src/messaging.rs +++ b/native_locator/src/messaging.rs @@ -14,18 +14,27 @@ pub trait MessageDispatcher { fn log_error(&mut self, message: &str) -> (); } +#[derive(Serialize, Deserialize, Copy, Clone)] +#[serde(rename_all = "camelCase")] +pub enum EnvManagerType { + Conda, + Pyenv, +} + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EnvManager { pub executable_path: String, pub version: Option, + pub tool: EnvManagerType, } impl EnvManager { - pub fn new(executable_path: String, version: Option) -> Self { + pub fn new(executable_path: String, version: Option, tool: EnvManagerType) -> Self { Self { executable_path, version, + tool, } } } @@ -35,6 +44,7 @@ impl Clone for EnvManager { Self { executable_path: self.executable_path.clone(), version: self.version.clone(), + tool: self.tool, } } } @@ -67,6 +77,7 @@ pub enum PythonEnvironmentCategory { PyenvVirtualEnv, WindowsStore, Pipenv, + VirtualEnvWrapper, } #[derive(Serialize, Deserialize)] diff --git a/native_locator/src/pipenv.rs b/native_locator/src/pipenv.rs index 53c0b0e248d6..607ddd591113 100644 --- a/native_locator/src/pipenv.rs +++ b/native_locator/src/pipenv.rs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::global_virtualenvs::{list_global_virtualenvs, PythonEnv}; -use crate::known; use crate::messaging::{MessageDispatcher, PythonEnvironment}; +use crate::utils::PythonEnv; use std::fs; use std::path::PathBuf; -fn get_project_folder(env: &PythonEnv) -> Option { +fn get_pipenv_project(env: &PythonEnv) -> Option { let project_file = env.path.join(".project"); if project_file.exists() { if let Ok(contents) = fs::read_to_string(project_file) { @@ -21,35 +20,31 @@ fn get_project_folder(env: &PythonEnv) -> Option { None } -pub fn find_and_report( - dispatcher: &mut impl MessageDispatcher, - environment: &impl known::Environment, -) -> Option<()> { - for env in list_global_virtualenvs(environment).iter() { - if let Some(project_path) = get_project_folder(&env) { - let env_path = env - .path - .clone() - .into_os_string() - .to_string_lossy() - .to_string(); - let executable = env - .executable - .clone() - .into_os_string() - .to_string_lossy() - .to_string(); - let env = PythonEnvironment::new_pipenv( - Some(executable), - env.version.clone(), - Some(env_path.clone()), - Some(env_path), - None, - project_path, - ); +pub fn find_and_report(env: &PythonEnv, dispatcher: &mut impl MessageDispatcher) -> Option<()> { + if let Some(project_path) = get_pipenv_project(env) { + let env_path = env + .path + .clone() + .into_os_string() + .to_string_lossy() + .to_string(); + let executable = env + .executable + .clone() + .into_os_string() + .to_string_lossy() + .to_string(); + let env = PythonEnvironment::new_pipenv( + Some(executable), + env.version.clone(), + Some(env_path.clone()), + Some(env_path), + None, + project_path, + ); - dispatcher.report_environment(env); - } + dispatcher.report_environment(env); + return Some(()); } None diff --git a/native_locator/src/pyenv.rs b/native_locator/src/pyenv.rs index 15969b321464..00b39e3c3ddd 100644 --- a/native_locator/src/pyenv.rs +++ b/native_locator/src/pyenv.rs @@ -8,6 +8,7 @@ use std::path::PathBuf; use crate::known; use crate::messaging; use crate::messaging::EnvManager; +use crate::messaging::EnvManagerType; use crate::utils::find_python_binary_path; use crate::utils::parse_pyenv_cfg; @@ -156,7 +157,7 @@ pub fn find_and_report( let manager = match get_pyenv_binary(environment) { Some(pyenv_binary) => { - let manager = messaging::EnvManager::new(pyenv_binary, None); + let manager = messaging::EnvManager::new(pyenv_binary, None, EnvManagerType::Pyenv); dispatcher.report_environment_manager(manager.clone()); Some(manager) } diff --git a/native_locator/src/utils.rs b/native_locator/src/utils.rs index cdf7348368f3..6e9684faeb33 100644 --- a/native_locator/src/utils.rs +++ b/native_locator/src/utils.rs @@ -8,6 +8,13 @@ use std::{ process::Command, }; +#[derive(Debug)] +pub struct PythonEnv { + pub path: PathBuf, + pub executable: PathBuf, + pub version: Option, +} + #[derive(Debug)] pub struct PyEnvCfg { pub version: String, diff --git a/native_locator/src/virtualenv.rs b/native_locator/src/virtualenv.rs new file mode 100644 index 000000000000..b655d707798c --- /dev/null +++ b/native_locator/src/virtualenv.rs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::path::PathBuf; + +use crate::utils::PythonEnv; + +pub fn is_virtualenv(env: &PythonEnv) -> bool { + if let Some(file_path) = PathBuf::from(env.executable.clone()).parent() { + // Check if there are any activate.* files in the same directory as the interpreter. + // + // env + // |__ activate, activate.* <--- check if any of these files exist + // |__ python <--- interpreterPath + + // if let Some(parent_path) = PathBuf::from(env.) + // const directory = path.dirname(interpreterPath); + // const files = await fsapi.readdir(directory); + // const regex = /^activate(\.([A-z]|\d)+)?$/i; + if file_path.join("activate").exists() || file_path.join("activate.bat").exists() { + return true; + } + + // Support for activate.ps, etc. + match std::fs::read_dir(file_path) { + Ok(files) => { + for file in files { + if let Ok(file) = file { + if let Some(file_name) = file.file_name().to_str() { + if file_name.starts_with("activate") { + return true; + } + } + } + } + return false; + } + Err(_) => return false, + }; + } + + false +} diff --git a/native_locator/src/virtualenvwrapper.rs b/native_locator/src/virtualenvwrapper.rs new file mode 100644 index 000000000000..d234111e4a15 --- /dev/null +++ b/native_locator/src/virtualenvwrapper.rs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::messaging::{PythonEnvironment, PythonEnvironmentCategory}; +use crate::virtualenv; +use crate::{known::Environment, messaging::MessageDispatcher, utils::PythonEnv}; +use std::path::PathBuf; + +#[cfg(windows)] +fn get_default_virtualenvwrapper_path(environment: &impl Environment) -> Option { + // In Windows, the default path for WORKON_HOME is %USERPROFILE%\Envs. + // If 'Envs' is not available we should default to '.virtualenvs'. Since that + // is also valid for windows. + if let Some(home) = environment.get_user_home() { + let home = PathBuf::from(home).join("Envs"); + if home.exists() { + return Some(home.to_string_lossy().to_string()); + } + let home = PathBuf::from(home).join("virtualenvs"); + if home.exists() { + return Some(home.to_string_lossy().to_string()); + } + } + None +} + +#[cfg(unix)] +fn get_default_virtualenvwrapper_path(environment: &impl Environment) -> Option { + if let Some(home) = environment.get_user_home() { + let home = PathBuf::from(home).join("virtualenvs"); + if home.exists() { + return Some(home.to_string_lossy().to_string()); + } + } + None +} + +fn get_work_on_home_path(environment: &impl Environment) -> Option { + // The WORKON_HOME variable contains the path to the root directory of all virtualenvwrapper environments. + // If the interpreter path belongs to one of them then it is a virtualenvwrapper type of environment. + if let Some(work_on_home) = environment.get_env_var("WORKON_HOME".to_string()) { + if let Ok(work_on_home) = std::fs::canonicalize(work_on_home) { + if work_on_home.exists() { + return Some(work_on_home.to_string_lossy().to_string()); + } + } + } + get_default_virtualenvwrapper_path(environment) +} + +fn create_virtualenvwrapper_env(env: &PythonEnv) -> PythonEnvironment { + let executable = env.executable.clone().into_os_string().into_string().ok(); + let env_path = env.path.clone().into_os_string().into_string().ok(); + PythonEnvironment { + name: match env.path.file_name().to_owned() { + Some(name) => Some(name.to_string_lossy().to_owned().to_string()), + None => None, + }, + python_executable_path: executable.clone(), + category: PythonEnvironmentCategory::VirtualEnvWrapper, + version: env.version.clone(), + env_path: env_path.clone(), + sys_prefix_path: env_path, + env_manager: None, + python_run_command: match executable { + Some(exe) => Some(vec![exe]), + None => None, + }, + project_path: None, + } +} + +pub fn is_virtualenvwrapper(env: &PythonEnv, environment: &impl Environment) -> bool { + // For environment to be a virtualenvwrapper based it has to follow these two rules: + // 1. It should be in a sub-directory under the WORKON_HOME + // 2. It should be a valid virtualenv environment + if let Some(work_on_home_dir) = get_work_on_home_path(environment) { + if env.executable.starts_with(&work_on_home_dir) && virtualenv::is_virtualenv(env) { + return true; + } + } + + false +} + +pub fn find_and_report( + env: &PythonEnv, + dispatcher: &mut impl MessageDispatcher, + environment: &impl Environment, +) -> Option<()> { + if is_virtualenvwrapper(env, environment) { + dispatcher.report_environment(create_virtualenvwrapper_env(env)); + return Some(()); + } + None +} diff --git a/native_locator/tests/conda_test.rs b/native_locator/tests/conda_test.rs index ad3a41181023..e7318f378845 100644 --- a/native_locator/tests/conda_test.rs +++ b/native_locator/tests/conda_test.rs @@ -44,7 +44,7 @@ fn find_conda_exe_and_empty_envs() { conda::find_and_report(&mut dispatcher, &known); let conda_exe = join_test_paths(&[conda_dir.clone().as_str(), "conda"]); - let expected_json = json!({"executablePath":conda_exe.clone(),"version":null}); + let expected_json = json!({"executablePath":conda_exe.clone(),"version":null, "tool": "conda"}); assert_messages(&[expected_json], &dispatcher) } #[test] @@ -80,11 +80,11 @@ fn finds_two_conda_envs_from_txt() { let conda_1_exe = join_test_paths(&[conda_1.clone().as_str(), "python"]); let conda_2_exe = join_test_paths(&[conda_2.clone().as_str(), "python"]); - let expected_conda_env = json!({ "executablePath": conda_exe.clone(), "version": null}); + let expected_conda_env = json!({ "executablePath": conda_exe.clone(), "version": null, "tool": "conda"}); let expected_conda_1 = json!({ "name": "one","projectPath": null, "pythonExecutablePath": conda_1_exe.clone(), "category": "conda", "version": "10.0.1", "envPath": conda_1.clone(), "sysPrefixPath": conda_1.clone(), "envManager": null, "pythonRunCommand": [conda_exe.clone(), "run", "-n", "one", "python"]}); let expected_conda_2 = json!({ "name": "two", "projectPath": null, "pythonExecutablePath": conda_2_exe.clone(), "category": "conda", "version": null, "envPath": conda_2.clone(), "sysPrefixPath": conda_2.clone(), "envManager": null,"pythonRunCommand": [conda_exe.clone(),"run","-n","two","python"]}); assert_messages( - &[expected_conda_env, expected_conda_1, expected_conda_2], + &[expected_conda_1, expected_conda_env, expected_conda_2], &dispatcher, ) } diff --git a/native_locator/tests/pyenv_test.rs b/native_locator/tests/pyenv_test.rs index ecf1ba548abf..a73481291992 100644 --- a/native_locator/tests/pyenv_test.rs +++ b/native_locator/tests/pyenv_test.rs @@ -46,7 +46,7 @@ fn does_not_find_any_pyenv_envs_even_with_pyenv_installed() { pyenv::find_and_report(&mut dispatcher, &known); assert_eq!(dispatcher.messages.len(), 1); - let expected_json = json!({"executablePath":pyenv_exe,"version":null}); + let expected_json = json!({"executablePath":pyenv_exe,"version":null, "tool": "pyenv"}); assert_messages(&[expected_json], &dispatcher) } @@ -74,7 +74,7 @@ fn find_pyenv_envs() { pyenv::find_and_report(&mut dispatcher, &known); assert_eq!(dispatcher.messages.len(), 6); - let expected_manager = json!({ "executablePath": pyenv_exe.clone(), "version": null }); + let expected_manager = json!({ "executablePath": pyenv_exe.clone(), "version": null, "tool": "pyenv" }); let expected_3_9_9 = json!({"projectPath": null, "name": null,"pythonExecutablePath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.9.9/bin/python"]), "pythonRunCommand": [join_test_paths(&[home.as_str(), ".pyenv/versions/3.9.9/bin/python"])], "category": "pyenv","version": "3.9.9","envPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.9.9"]),"sysPrefixPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.9.9"]), "envManager": expected_manager}); let expected_virtual_env = json!({"projectPath": null, "name": "my-virtual-env", "version": "3.10.13", "category": "pyenvVirtualEnv", "envPath": join_test_paths(&[home.as_str(),".pyenv/versions/my-virtual-env"]), "pythonExecutablePath": join_test_paths(&[home.as_str(),".pyenv/versions/my-virtual-env/bin/python"]), "sysPrefixPath": join_test_paths(&[home.as_str(),".pyenv/versions/my-virtual-env"]), "pythonRunCommand": [join_test_paths(&[home.as_str(),".pyenv/versions/my-virtual-env/bin/python"])], "envManager": expected_manager}); let expected_3_12_1 = json!({"projectPath": null, "name": null,"pythonExecutablePath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1/bin/python"]), "pythonRunCommand": [join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1/bin/python"])], "category": "pyenv","version": "3.12.1","envPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1"]),"sysPrefixPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1"]), "envManager": expected_manager});