diff --git a/native_locator/src/global_virtualenvs.rs b/native_locator/src/global_virtualenvs.rs new file mode 100644 index 000000000000..cd8c2e392054 --- /dev/null +++ b/native_locator/src/global_virtualenvs.rs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::{ + known, + utils::{find_python_binary_path, get_version}, +}; +use std::{fs, path::PathBuf}; + +fn get_global_virtualenv_dirs(environment: &impl known::Environment) -> Vec { + let mut venv_dirs: Vec = vec![]; + + if let Some(work_on_home) = environment.get_env_var("WORKON_HOME".to_string()) { + if let Ok(work_on_home) = fs::canonicalize(work_on_home) { + if work_on_home.exists() { + venv_dirs.push(work_on_home); + } + } + } + + if let Some(home) = environment.get_user_home() { + let home = PathBuf::from(home); + for dir in [ + PathBuf::from("envs"), + PathBuf::from(".direnv"), + PathBuf::from(".venvs"), + PathBuf::from(".virtualenvs"), + PathBuf::from(".local").join("share").join("virtualenvs"), + ] { + let venv_dir = home.join(dir); + if venv_dir.exists() { + venv_dirs.push(venv_dir); + } + } + if cfg!(target_os = "linux") { + let envs = PathBuf::from("Envs"); + if envs.exists() { + venv_dirs.push(envs); + } + } + } + + venv_dirs +} + +pub struct PythonEnv { + pub path: PathBuf, + pub executable: PathBuf, + pub version: Option, +} + +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() { + if let Ok(dirs) = fs::read_dir(root_dir) { + for venv_dir in dirs { + if let Ok(venv_dir) = venv_dir { + let venv_dir = venv_dir.path(); + if !venv_dir.is_dir() { + continue; + } + if let Some(executable) = find_python_binary_path(&venv_dir) { + python_envs.push(PythonEnv { + path: venv_dir, + executable: executable.clone(), + version: get_version(executable.to_str().unwrap()), + }); + } + } + } + } + } + + python_envs +} diff --git a/native_locator/src/lib.rs b/native_locator/src/lib.rs index 17ce17253f77..41cb3d97bd10 100644 --- a/native_locator/src/lib.rs +++ b/native_locator/src/lib.rs @@ -8,3 +8,4 @@ pub mod logging; pub mod conda; pub mod known; pub mod pyenv; +pub mod global_virtualenvs; diff --git a/native_locator/src/main.rs b/native_locator/src/main.rs index f8305b685296..dad488f2186c 100644 --- a/native_locator/src/main.rs +++ b/native_locator/src/main.rs @@ -8,10 +8,12 @@ use messaging::{create_dispatcher, MessageDispatcher}; mod common_python; mod conda; +mod global_virtualenvs; mod homebrew; mod known; mod logging; mod messaging; +mod pipenv; mod pyenv; mod utils; mod windows_python; @@ -35,6 +37,8 @@ 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 858f74c59e8a..a996ec3964e1 100644 --- a/native_locator/src/messaging.rs +++ b/native_locator/src/messaging.rs @@ -66,6 +66,7 @@ pub enum PythonEnvironmentCategory { Pyenv, PyenvVirtualEnv, WindowsStore, + Pipenv, } #[derive(Serialize, Deserialize)] @@ -79,6 +80,10 @@ pub struct PythonEnvironment { pub sys_prefix_path: Option, pub env_manager: Option, pub python_run_command: Option>, + /** + * The project path for the Pipenv environment. + */ + pub project_path: Option, } impl PythonEnvironment { @@ -101,6 +106,30 @@ impl PythonEnvironment { sys_prefix_path, env_manager, python_run_command, + project_path: None, + } + } + pub fn new_pipenv( + python_executable_path: Option, + version: Option, + env_path: Option, + sys_prefix_path: Option, + env_manager: Option, + project_path: String, + ) -> Self { + Self { + name: None, + python_executable_path: python_executable_path.clone(), + category: PythonEnvironmentCategory::Pipenv, + version, + env_path, + sys_prefix_path, + env_manager, + python_run_command: match python_executable_path { + Some(exe) => Some(vec![exe]), + None => None, + }, + project_path: Some(project_path), } } } diff --git a/native_locator/src/pipenv.rs b/native_locator/src/pipenv.rs new file mode 100644 index 000000000000..53c0b0e248d6 --- /dev/null +++ b/native_locator/src/pipenv.rs @@ -0,0 +1,56 @@ +// 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 std::fs; +use std::path::PathBuf; + +fn get_project_folder(env: &PythonEnv) -> Option { + let project_file = env.path.join(".project"); + if project_file.exists() { + if let Ok(contents) = fs::read_to_string(project_file) { + let project_folder = PathBuf::from(contents.trim().to_string()); + if project_folder.exists() { + return Some(project_folder.to_string_lossy().to_string()); + } + } + } + + 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, + ); + + dispatcher.report_environment(env); + } + } + + None +} diff --git a/native_locator/src/pyenv.rs b/native_locator/src/pyenv.rs index 5365818dd318..3b083d30d8ce 100644 --- a/native_locator/src/pyenv.rs +++ b/native_locator/src/pyenv.rs @@ -9,6 +9,7 @@ use crate::known; use crate::messaging; use crate::messaging::EnvManager; use crate::utils::find_python_binary_path; +use crate::utils::parse_pyenv_cfg; #[cfg(windows)] fn get_home_pyenv_dir(environment: &impl known::Environment) -> Option { @@ -123,31 +124,6 @@ fn report_if_pure_python_environment( Some(()) } -#[derive(Debug)] -struct PyEnvCfg { - version: String, -} - -fn parse_pyenv_cfg(path: &PathBuf) -> Option { - let cfg = path.join("pyvenv.cfg"); - if !fs::metadata(&cfg).is_ok() { - return None; - } - - let contents = fs::read_to_string(cfg).ok()?; - let version_regex = Regex::new(r"^version\s*=\s*(\d+\.\d+\.\d+)$").unwrap(); - for line in contents.lines() { - if let Some(captures) = version_regex.captures(line) { - if let Some(value) = captures.get(1) { - return Some(PyEnvCfg { - version: value.as_str().to_string(), - }); - } - } - } - None -} - fn report_if_virtual_env_environment( executable: PathBuf, path: &PathBuf, diff --git a/native_locator/src/utils.rs b/native_locator/src/utils.rs index 93afcd5ac554..cdf7348368f3 100644 --- a/native_locator/src/utils.rs +++ b/native_locator/src/utils.rs @@ -1,12 +1,60 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +use regex::Regex; use std::{ + fs, path::{Path, PathBuf}, process::Command, }; +#[derive(Debug)] +pub struct PyEnvCfg { + pub version: String, +} + +pub fn parse_pyenv_cfg(path: &PathBuf) -> Option { + let cfg = path.join("pyvenv.cfg"); + if !fs::metadata(&cfg).is_ok() { + return None; + } + + let contents = fs::read_to_string(cfg).ok()?; + let version_regex = Regex::new(r"^version\s*=\s*(\d+\.\d+\.\d+)$").unwrap(); + let version_info_regex = Regex::new(r"^version_info\s*=\s*(\d+\.\d+\.\d+.*)$").unwrap(); + for line in contents.lines() { + if !line.contains("version") { + continue; + } + if let Some(captures) = version_regex.captures(line) { + if let Some(value) = captures.get(1) { + return Some(PyEnvCfg { + version: value.as_str().to_string(), + }); + } + } + if let Some(captures) = version_info_regex.captures(line) { + if let Some(value) = captures.get(1) { + return Some(PyEnvCfg { + version: value.as_str().to_string(), + }); + } + } + } + None +} + pub fn get_version(path: &str) -> Option { + if let Some(parent_folder) = PathBuf::from(path).parent() { + if let Some(pyenv_cfg) = parse_pyenv_cfg(&parent_folder.to_path_buf()) { + return Some(pyenv_cfg.version); + } + if let Some(parent_folder) = parent_folder.parent() { + if let Some(pyenv_cfg) = parse_pyenv_cfg(&parent_folder.to_path_buf()) { + return Some(pyenv_cfg.version); + } + } + } let output = Command::new(path) .arg("-c") .arg("import sys; print(sys.version)") diff --git a/native_locator/tests/common_python_test.rs b/native_locator/tests/common_python_test.rs index f8841ee9b6fa..7ef6b8eb3f53 100644 --- a/native_locator/tests/common_python_test.rs +++ b/native_locator/tests/common_python_test.rs @@ -27,6 +27,6 @@ fn find_python_in_path_this() { common_python::find_and_report(&mut dispatcher, &known); assert_eq!(dispatcher.messages.len(), 1); - let expected_json = json!({"envManager":null,"name":null,"pythonExecutablePath":unix_python_exe.clone(),"category":"system","version":null,"pythonRunCommand":[unix_python_exe.clone()],"envPath":unix_python.clone(),"sysPrefixPath":unix_python.clone()}); + let expected_json = json!({"envManager":null,"projectPath": null, "name":null,"pythonExecutablePath":unix_python_exe.clone(),"category":"system","version":null,"pythonRunCommand":[unix_python_exe.clone()],"envPath":unix_python.clone(),"sysPrefixPath":unix_python.clone()}); assert_messages(&[expected_json], &dispatcher); } diff --git a/native_locator/tests/conda_test.rs b/native_locator/tests/conda_test.rs index 94274c8f7db7..ad3a41181023 100644 --- a/native_locator/tests/conda_test.rs +++ b/native_locator/tests/conda_test.rs @@ -81,8 +81,8 @@ fn finds_two_conda_envs_from_txt() { 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_1 = json!({ "name": "one", "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", "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"]}); + 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], &dispatcher, diff --git a/native_locator/tests/pyenv_test.rs b/native_locator/tests/pyenv_test.rs index d5327b9bd275..ecf1ba548abf 100644 --- a/native_locator/tests/pyenv_test.rs +++ b/native_locator/tests/pyenv_test.rs @@ -75,11 +75,11 @@ fn find_pyenv_envs() { assert_eq!(dispatcher.messages.len(), 6); let expected_manager = json!({ "executablePath": pyenv_exe.clone(), "version": null }); - let expected_3_9_9 = json!({"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!({"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!({"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}); - let expected_3_13_dev = json!({"name": null,"pythonExecutablePath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.13-dev/bin/python"]), "pythonRunCommand": [join_test_paths(&[home.as_str(), ".pyenv/versions/3.13-dev/bin/python"])], "category": "pyenv","version": "3.13-dev","envPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.13-dev"]),"sysPrefixPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.13-dev"]), "envManager": expected_manager}); - let expected_3_12_1a3 = json!({"name": null,"pythonExecutablePath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1a3/bin/python"]), "pythonRunCommand": [join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1a3/bin/python"])], "category": "pyenv","version": "3.12.1a3","envPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1a3"]),"sysPrefixPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1a3"]), "envManager": expected_manager}); + 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}); + let expected_3_13_dev = json!({"projectPath": null, "name": null,"pythonExecutablePath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.13-dev/bin/python"]), "pythonRunCommand": [join_test_paths(&[home.as_str(), ".pyenv/versions/3.13-dev/bin/python"])], "category": "pyenv","version": "3.13-dev","envPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.13-dev"]),"sysPrefixPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.13-dev"]), "envManager": expected_manager}); + let expected_3_12_1a3 = json!({"projectPath": null, "name": null,"pythonExecutablePath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1a3/bin/python"]), "pythonRunCommand": [join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1a3/bin/python"])], "category": "pyenv","version": "3.12.1a3","envPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1a3"]),"sysPrefixPath": join_test_paths(&[home.as_str(), ".pyenv/versions/3.12.1a3"]), "envManager": expected_manager}); assert_messages( &[ expected_manager, diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/nativeLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/nativeLocator.ts index 07453c704f14..ec809ee798d5 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/nativeLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/nativeLocator.ts @@ -20,17 +20,21 @@ const NATIVE_LOCATOR = isWindows() interface NativeEnvInfo { name: string; - pythonExecutablePath: string[]; + pythonExecutablePath?: string; category: string; version?: string; - activatedRun?: string[]; + pythonRunCommand?: string[]; envPath?: string; sysPrefixPath?: string; + /** + * Path to the project directory when dealing with pipenv virtual environments. + */ + projectPath?: string; } interface EnvManager { tool: string; - executablePath: string[]; + executablePath: string; version?: string; } @@ -43,6 +47,8 @@ function categoryToKind(category: string): PythonEnvKind { return PythonEnvKind.System; case 'pyenv': return PythonEnvKind.Pyenv; + case 'pipenv': + return PythonEnvKind.Pipenv; case 'pyenvvirtualenv': return PythonEnvKind.VirtualEnv; case 'windowsstore': @@ -115,7 +121,8 @@ export class NativeLocator implements ILocator, IDisposable { connection.onNotification('pythonEnvironment', (data: NativeEnvInfo) => { envs.push({ kind: categoryToKind(data.category), - executablePath: data.pythonExecutablePath[0], + // TODO: What if executable is undefined? + executablePath: data.pythonExecutablePath!, envPath: data.envPath, version: parseVersion(data.version), name: data.name === '' ? undefined : data.name, @@ -124,11 +131,11 @@ export class NativeLocator implements ILocator, IDisposable { connection.onNotification('envManager', (data: EnvManager) => { switch (toolToKnownEnvironmentTool(data.tool)) { case 'Conda': { - Conda.setConda(data.executablePath[0]); + Conda.setConda(data.executablePath); break; } case 'Pyenv': { - setPyEnvBinary(data.executablePath[0]); + setPyEnvBinary(data.executablePath); break; } default: {