diff --git a/Cargo.lock b/Cargo.lock index b1917e23..b8d454ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,6 +354,7 @@ dependencies = [ "pet-mac-python-org", "pet-mac-xcode", "pet-pipenv", + "pet-pixi", "pet-poetry", "pet-pyenv", "pet-python-utils", @@ -525,6 +526,17 @@ dependencies = [ "pet-virtualenv", ] +[[package]] +name = "pet-pixi" +version = "0.1.0" +dependencies = [ + "log", + "msvc_spectre_libs", + "pet-conda", + "pet-core", + "pet-python-utils", +] + [[package]] name = "pet-poetry" version = "0.1.0" diff --git a/crates/pet-conda/README.md b/crates/pet-conda/README.md index cbe9a2f6..429ef7ac 100644 --- a/crates/pet-conda/README.md +++ b/crates/pet-conda/README.md @@ -36,11 +36,6 @@ - Thus using the `history` file we can find the conda installation folder. This is useful in cases where conda environments are created using `-p` option. -## Known issues - -- Note: pixi seems to use conda envs internall, hence its possible to falsely identify a pixi env as a conda env. -- However pixi is not supported by this tool, hence thats not a concern. - ## Miscellanous - What if conda is installed in some custom locations that we have no idea about? diff --git a/crates/pet-core/src/lib.rs b/crates/pet-core/src/lib.rs index 033ec44b..8d123469 100644 --- a/crates/pet-core/src/lib.rs +++ b/crates/pet-core/src/lib.rs @@ -47,6 +47,7 @@ pub enum LocatorKind { MacPythonOrg, MacXCode, PipEnv, + Pixi, Poetry, PyEnv, Venv, diff --git a/crates/pet-core/src/python_environment.rs b/crates/pet-core/src/python_environment.rs index b967e129..9531a70a 100644 --- a/crates/pet-core/src/python_environment.rs +++ b/crates/pet-core/src/python_environment.rs @@ -12,6 +12,7 @@ use crate::{arch::Architecture, manager::EnvManager}; #[derive(Parser, ValueEnum, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug, Hash)] pub enum PythonEnvironmentKind { Conda, + Pixi, Homebrew, Pyenv, GlobalPaths, // Python found in global locations like PATH, /usr/bin etc. diff --git a/crates/pet-pixi/Cargo.toml b/crates/pet-pixi/Cargo.toml new file mode 100644 index 00000000..cdeb5095 --- /dev/null +++ b/crates/pet-pixi/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pet-pixi" +version = "0.1.0" +edition = "2021" + +[target.'cfg(target_os = "windows")'.dependencies] +msvc_spectre_libs = { version = "0.1.1", features = ["error"] } + +[dependencies] +pet-conda = { path = "../pet-conda" } +pet-core = { path = "../pet-core" } +pet-python-utils = { path = "../pet-python-utils" } +log = "0.4.21" diff --git a/crates/pet-pixi/README.md b/crates/pet-pixi/README.md new file mode 100644 index 00000000..f177e167 --- /dev/null +++ b/crates/pet-pixi/README.md @@ -0,0 +1,11 @@ +# Pixi + +## Notes + +- Pixi environments are detected by: + - Searching for Python interpreters in `.pixi/envs` subdirectories within workspace folders + - Checking for a `conda-meta/pixi` file in potential Pixi environment directories (`.pixi/envs/{env_name}`) + - Determining the version of the Python interpreter from the `conda-meta/python-{version}.json` file + +This process ensures fast detection without spawning processes. +Note that the Pixi locator should run before Conda since Conda could incorrectly identify Pixi environments as Conda environments. diff --git a/crates/pet-pixi/src/lib.rs b/crates/pet-pixi/src/lib.rs new file mode 100644 index 00000000..b7adfde9 --- /dev/null +++ b/crates/pet-pixi/src/lib.rs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::path::{Path, PathBuf}; + +use pet_conda::package::{CondaPackageInfo, Package}; +use pet_core::{ + env::PythonEnv, + python_environment::{PythonEnvironment, PythonEnvironmentBuilder, PythonEnvironmentKind}, + reporter::Reporter, + Locator, LocatorKind, +}; +use pet_python_utils::executable::find_executables; + +pub fn is_pixi_env(path: &Path) -> bool { + path.join("conda-meta").join("pixi").is_file() +} + +fn get_pixi_prefix(env: &PythonEnv) -> Option { + env.prefix.clone().or_else(|| { + env.executable.parent().and_then(|parent_dir| { + if is_pixi_env(parent_dir) { + Some(parent_dir.to_path_buf()) + } else if parent_dir.ends_with("bin") || parent_dir.ends_with("Scripts") { + parent_dir + .parent() + .filter(|parent| is_pixi_env(parent)) + .map(|parent| parent.to_path_buf()) + } else { + None + } + }) + }) +} + +pub struct Pixi {} + +impl Pixi { + pub fn new() -> Pixi { + Pixi {} + } +} +impl Default for Pixi { + fn default() -> Self { + Self::new() + } +} + +impl Locator for Pixi { + fn get_kind(&self) -> LocatorKind { + LocatorKind::Pixi + } + fn supported_categories(&self) -> Vec { + vec![PythonEnvironmentKind::Pixi] + } + + fn try_from(&self, env: &PythonEnv) -> Option { + get_pixi_prefix(env).and_then(|prefix| { + if !is_pixi_env(&prefix) { + return None; + } + + let name = prefix + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default() + .to_string(); + + let symlinks = find_executables(&prefix); + + let version = CondaPackageInfo::from(&prefix, &Package::Python) + .map(|package_info| package_info.version); + + Some( + PythonEnvironmentBuilder::new(Some(PythonEnvironmentKind::Pixi)) + .executable(Some(env.executable.clone())) + .name(Some(name)) + .prefix(Some(prefix)) + .symlinks(Some(symlinks)) + .version(version) + .build(), + ) + }) + } + + fn find(&self, _reporter: &dyn Reporter) {} +} diff --git a/crates/pet/Cargo.toml b/crates/pet/Cargo.toml index b8e8bfa1..11e045a7 100644 --- a/crates/pet/Cargo.toml +++ b/crates/pet/Cargo.toml @@ -16,6 +16,7 @@ pet-homebrew = { path = "../pet-homebrew" } [dependencies] pet-core = { path = "../pet-core" } pet-conda = { path = "../pet-conda" } +pet-pixi = { path = "../pet-pixi" } pet-jsonrpc = { path = "../pet-jsonrpc" } pet-fs = { path = "../pet-fs" } pet-pyenv = { path = "../pet-pyenv" } diff --git a/crates/pet/README.md b/crates/pet/README.md index 4ddd9cdd..ef275fbc 100644 --- a/crates/pet/README.md +++ b/crates/pet/README.md @@ -66,53 +66,55 @@ It could have been created by other tools like `poetry`, `pipenv`, etc. Hence we 4. Homebrew These are always stored in a custom (global) lcoation. -5. Conda + +5. Pixi + Pixi should run before Conda as its environments could be falsely identified as Conda environments. + +6. Conda - These are always stored in a custom (global) location. - At this stage Conda envs cannot be treated as anything else -- Note: pixi seems to use conda envs internall, hence its possible to falsely identify a pixi env as a conda env. -- However pixi is not supported by this tool, hence thats not a concern. -6. Poetry +7. Poetry - These are always stored in a custom (global) location. - Environments are created with a special name based on the hash of the project folder. - This needs to happen before others as these environments are general virtual environments. -7. Pipenv +8. Pipenv - These are always stored in a custom (global) location. - This needs to happen before others as these environments are general virtual environments. -8. Virtualenvwrapper +9. Virtualenvwrapper - These are always stored in a custom (global) location. - This needs to happen before others as these environments are general virtual environments. -9. Vevn +10. Vevn - A virtual environment that has a `pyvenv.cfg` file. - Note, this is a fallback for all other virtual environments. -10. Virtualenv +11. Virtualenv - A virtual environment that does not have a `pyvenv.cfg` file but has activation scripts such as `/bin/activate` or `Scripts/activate.bat` or the like. - Note, this is a fallback for all other environments. - This must happen after conda env discovery, as conda envs have activation scripts as well. -11. Mac XCode +12. Mac XCode - These are always stored in a custom (global) location. -12. Mac Command Line Tools +13. Mac Command Line Tools - These are always stored in a custom (global) location. -13. Mac Python Org +14. Mac Python Org - These are always stored in a custom (global) location. -14. Linux Python +15. Linux Python - These are always stored in a custom (global) location. diff --git a/crates/pet/src/find.rs b/crates/pet/src/find.rs index a4bc9e3e..63aefd63 100644 --- a/crates/pet/src/find.rs +++ b/crates/pet/src/find.rs @@ -10,6 +10,7 @@ use pet_core::reporter::Reporter; use pet_core::{Configuration, Locator, LocatorKind}; use pet_env_var_path::get_search_paths_from_env_variables; use pet_global_virtualenvs::list_global_virtual_envs_paths; +use pet_pixi::is_pixi_env; use pet_python_utils::executable::{ find_executable, find_executables, should_search_for_environments_in_path, }; @@ -254,7 +255,7 @@ pub fn find_python_environments_in_workspace_folder_recursive( environment_directories: &[PathBuf], ) { // When searching in a directory, give preference to some paths. - let paths_to_search_first = vec![ + let mut paths_to_search_first = vec![ // Possible this is a virtual env workspace_folder.to_path_buf(), // Optimize for finding these first. @@ -264,6 +265,15 @@ pub fn find_python_environments_in_workspace_folder_recursive( workspace_folder.join("venv"), ]; + // Add all subdirectories of .pixi/envs/** + if let Ok(reader) = fs::read_dir(workspace_folder.join(".pixi").join("envs")) { + reader + .filter_map(Result::ok) + .filter(|d| d.file_type().is_ok_and(|f| f.is_dir())) + .map(|p| p.path()) + .for_each(|p| paths_to_search_first.push(p)); + } + // Possible this is an environment. find_python_environments_in_paths_with_locators( paths_to_search_first.clone(), @@ -274,7 +284,11 @@ pub fn find_python_environments_in_workspace_folder_recursive( ); // If this is a virtual env folder, no need to scan this. - if is_virtualenv_dir(workspace_folder) || is_conda_env(workspace_folder) { + // Note: calling is_pixi_env after is_conda_env is redundant but kept for consistency. + if is_virtualenv_dir(workspace_folder) + || is_conda_env(workspace_folder) + || is_pixi_env(workspace_folder) + { return; } if let Ok(reader) = fs::read_dir(workspace_folder) { diff --git a/crates/pet/src/locators.rs b/crates/pet/src/locators.rs index 3047217d..fa5b0cc7 100644 --- a/crates/pet/src/locators.rs +++ b/crates/pet/src/locators.rs @@ -15,6 +15,7 @@ use pet_mac_commandlinetools::MacCmdLineTools; use pet_mac_python_org::MacPythonOrg; use pet_mac_xcode::MacXCode; use pet_pipenv::PipEnv; +use pet_pixi::Pixi; use pet_poetry::Poetry; use pet_pyenv::PyEnv; use pet_python_utils::env::ResolvedPythonEnv; @@ -48,10 +49,13 @@ pub fn create_locators( // 3. Pyenv Python locators.push(Arc::new(PyEnv::from(environment, conda_locator.clone()))); - // 4. Conda Python + // 4. Pixi + locators.push(Arc::new(Pixi::new())); + + // 5. Conda Python locators.push(conda_locator); - // 5. Support for Virtual Envs + // 6. Support for Virtual Envs // The order of these matter. // Basically PipEnv is a superset of VirtualEnvWrapper, which is a superset of Venv, which is a superset of VirtualEnv. locators.push(poetry_locator); @@ -61,7 +65,7 @@ pub fn create_locators( // VirtualEnv is the most generic, hence should be the last. locators.push(Arc::new(VirtualEnv::new())); - // 6. Homebrew Python + // 7. Homebrew Python if cfg!(unix) { #[cfg(unix)] use pet_homebrew::Homebrew; @@ -71,14 +75,14 @@ pub fn create_locators( locators.push(Arc::new(homebrew_locator)); } - // 7. Global Mac Python - // 8. CommandLineTools Python & xcode + // 8. Global Mac Python + // 9. CommandLineTools Python & xcode if std::env::consts::OS == "macos" { locators.push(Arc::new(MacXCode::new())); locators.push(Arc::new(MacCmdLineTools::new())); locators.push(Arc::new(MacPythonOrg::new())); } - // 9. Global Linux Python + // 10. Global Linux Python // All other Linux (not mac, & not windows) // THIS MUST BE LAST if std::env::consts::OS != "macos" && std::env::consts::OS != "windows" { diff --git a/docs/JSONRPC.md b/docs/JSONRPC.md index 31006e48..e18daef1 100644 --- a/docs/JSONRPC.md +++ b/docs/JSONRPC.md @@ -143,6 +143,7 @@ interface ResolveParams { enum PythonEnvironmentKind { Conda, + Pixi, Homebrew, Pyenv, GlobalPaths, // Python found in global locations like PATH, /usr/bin etc.