diff --git a/Cargo.lock b/Cargo.lock index 3e0837f9..562489d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ahash" @@ -90,6 +90,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + [[package]] name = "block-buffer" version = "0.10.4" @@ -214,6 +220,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "generic-array" version = "0.14.7" @@ -306,6 +328,12 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "log" version = "0.4.21" @@ -360,6 +388,7 @@ dependencies = [ "pet-python-utils", "pet-reporter", "pet-telemetry", + "pet-uv", "pet-venv", "pet-virtualenv", "pet-virtualenvwrapper", @@ -617,6 +646,19 @@ dependencies = [ "regex", ] +[[package]] +name = "pet-uv" +version = "0.1.0" +dependencies = [ + "log", + "pet-core", + "pet-fs", + "pet-python-utils", + "serde", + "serde_json", + "tempfile", +] + [[package]] name = "pet-venv" version = "0.1.0" @@ -730,6 +772,19 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "ryu" version = "1.0.18" @@ -804,6 +859,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "termcolor" version = "1.4.1" diff --git a/README.md b/README.md index 64ca2e3e..cf52ee95 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ This project will be consumed by the [Python extension](https://marketplace.visu - VirtualEnvWrapper-Win - Venv - VirtualEnv +- UV - Python on your PATH ## Features diff --git a/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/env_python_3/conda-meta/history b/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/env_python_3/conda-meta/history index 198ee3a3..fc09724d 100644 --- a/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/env_python_3/conda-meta/history +++ b/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/env_python_3/conda-meta/history @@ -1,8 +1,8 @@ ==> 2024-02-28 23:05:07 <== -# cmd: /Users/donjayamanne/Development/vsc/python-environment-tools/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/some_other_location/conda_install/bin/conda create -n conda1 +# cmd: /home/runner/work/python-environment-tools/python-environment-tools/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/some_other_location/conda_install/bin/conda create -n conda1 # conda version: 23.11.0 ==> 2024-02-28 23:08:59 <== -# cmd: /Users/donjayamanne/Development/vsc/python-environment-tools/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/some_other_location/conda_install/bin/conda install -c conda-forge --name conda1 ipykernel -y +# cmd: /home/runner/work/python-environment-tools/python-environment-tools/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/some_other_location/conda_install/bin/conda install -c conda-forge --name conda1 ipykernel -y # conda version: 23.11.0 +conda-forge/noarch::appnope-0.1.4-pyhd8ed1ab_0 +conda-forge/noarch::asttokens-2.4.1-pyhd8ed1ab_0 diff --git a/crates/pet-core/src/lib.rs b/crates/pet-core/src/lib.rs index 8d123469..1768cae6 100644 --- a/crates/pet-core/src/lib.rs +++ b/crates/pet-core/src/lib.rs @@ -50,6 +50,7 @@ pub enum LocatorKind { Pixi, Poetry, PyEnv, + Uv, Venv, VirtualEnv, VirtualEnvWrapper, diff --git a/crates/pet-core/src/manager.rs b/crates/pet-core/src/manager.rs index 7acd294a..b8d27a3e 100644 --- a/crates/pet-core/src/manager.rs +++ b/crates/pet-core/src/manager.rs @@ -9,6 +9,7 @@ pub enum EnvManagerType { Conda, Poetry, Pyenv, + Uv, } impl Ord for EnvManagerType { diff --git a/crates/pet-core/src/python_environment.rs b/crates/pet-core/src/python_environment.rs index 9531a70a..46639b2a 100644 --- a/crates/pet-core/src/python_environment.rs +++ b/crates/pet-core/src/python_environment.rs @@ -23,6 +23,7 @@ pub enum PythonEnvironmentKind { MacCommandLineTools, LinuxGlobal, MacXCode, + Uv, Venv, VirtualEnv, VirtualEnvWrapper, diff --git a/crates/pet-uv/Cargo.toml b/crates/pet-uv/Cargo.toml new file mode 100644 index 00000000..71a04f4d --- /dev/null +++ b/crates/pet-uv/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "pet-uv" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +pet-core = { path = "../pet-core" } +pet-fs = { path = "../pet-fs" } +pet-python-utils = { path = "../pet-python-utils" } +log = "0.4.21" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[dev-dependencies] +tempfile = "3.0" \ No newline at end of file diff --git a/crates/pet-uv/src/lib.rs b/crates/pet-uv/src/lib.rs new file mode 100644 index 00000000..b2fe2848 --- /dev/null +++ b/crates/pet-uv/src/lib.rs @@ -0,0 +1,399 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::path::PathBuf; + +use log::trace; +use pet_core::{ + env::PythonEnv, + manager::{EnvManager, EnvManagerType}, + os_environment::Environment, + python_environment::{PythonEnvironment, PythonEnvironmentBuilder, PythonEnvironmentKind}, + reporter::Reporter, + Locator, LocatorKind, +}; +use pet_fs::path::norm_case; + +pub struct Uv { + environment: Option>, +} + +impl Uv { + pub fn new() -> Self { + Self { environment: None } + } + + pub fn from(environment: &dyn Environment) -> Self { + Self { + environment: Some(Box::new(EnvironmentWrapper::from(environment))), + } + } + + /// Get the UV data directory where UV stores its data + fn get_uv_data_dir(&self) -> Option { + if let Some(env) = &self.environment { + // First check UV_DATA_DIR environment variable + if let Some(uv_data_dir) = env.get_env_var("UV_DATA_DIR".to_string()) { + return Some(PathBuf::from(uv_data_dir)); + } + } + + // Fall back to platform-specific default locations + if cfg!(windows) { + // Windows: %APPDATA%\uv + if let Some(env) = &self.environment { + if let Some(appdata) = env.get_env_var("APPDATA".to_string()) { + return Some(PathBuf::from(appdata).join("uv")); + } + } + } else { + // Unix-like systems: ~/.local/share/uv + if let Some(env) = &self.environment { + if let Some(home) = env.get_env_var("HOME".to_string()) { + return Some(PathBuf::from(home).join(".local").join("share").join("uv")); + } + } + } + + None + } + + /// Get the directory where UV stores Python installations + fn get_uv_python_dir(&self) -> Option { + self.get_uv_data_dir().map(|data_dir| data_dir.join("python")) + } + + /// Check if a path might be a UV-managed Python installation + fn is_uv_python(&self, env: &PythonEnv) -> bool { + if let Some(uv_python_dir) = self.get_uv_python_dir() { + let uv_python_dir = norm_case(uv_python_dir); + + // Check if the executable is under the UV python directory + if let Some(exe_parent) = env.executable.parent() { + let exe_parent = norm_case(exe_parent.to_path_buf()); + if exe_parent.starts_with(&uv_python_dir) { + return true; + } + } + + // Check if the prefix is under the UV python directory + if let Some(prefix) = &env.prefix { + let prefix = norm_case(prefix.clone()); + if prefix.starts_with(&uv_python_dir) { + return true; + } + } + } + + false + } + + /// Find UV executable in PATH + fn find_uv_executable(&self) -> Option { + if let Some(env) = &self.environment { + // Search for UV executable in known global locations + for path in env.get_know_global_search_locations() { + let uv_exe = if cfg!(windows) { + path.join("uv.exe") + } else { + path.join("uv") + }; + + if uv_exe.exists() && uv_exe.is_file() { + return Some(uv_exe); + } + } + } + None + } + + /// Discover UV-managed Python installations + fn discover_uv_pythons(&self, reporter: &dyn Reporter) { + let Some(uv_python_dir) = self.get_uv_python_dir() else { + trace!("UV python directory not found"); + return; + }; + + if !uv_python_dir.exists() { + trace!("UV python directory does not exist: {:?}", uv_python_dir); + return; + } + + trace!("Searching for UV Python installations in: {:?}", uv_python_dir); + + // Iterate through subdirectories in the UV python directory + let Ok(entries) = std::fs::read_dir(&uv_python_dir) else { + trace!("Failed to read UV python directory: {:?}", uv_python_dir); + return; + }; + + for entry in entries.flatten() { + if !entry.path().is_dir() { + continue; + } + + // Look for Python executables in each installation directory + let python_install_dir = entry.path(); + trace!("Checking UV Python installation: {:?}", python_install_dir); + + // UV typically installs Python in subdirectories like cpython-3.11.0-windows-x86_64-none + // The executable is usually in bin/ (Unix) or Scripts/ (Windows) or directly in the root + let possible_bin_dirs = if cfg!(windows) { + vec!["Scripts", "bin", "."] + } else { + vec!["bin", "."] + }; + + for bin_dir_name in possible_bin_dirs { + let bin_dir = python_install_dir.join(bin_dir_name); + if !bin_dir.exists() { + continue; + } + + // Look for Python executables + let python_exe_names = if cfg!(windows) { + vec!["python.exe", "python3.exe"] + } else { + vec!["python", "python3"] + }; + + for exe_name in python_exe_names { + let python_exe = bin_dir.join(exe_name); + if python_exe.exists() && python_exe.is_file() { + trace!("Found UV Python executable: {:?}", python_exe); + + let env = PythonEnv { + executable: python_exe.clone(), + prefix: Some(python_install_dir.clone()), + version: None, // Will be determined later if needed + symlinks: None, + }; + + if let Some(python_env) = self.try_from(&env) { + reporter.report_environment(&python_env); + } + break; // Found an executable in this bin directory, move to next installation + } + } + } + } + } +} + +impl Default for Uv { + fn default() -> Self { + Self::new() + } +} + +impl Locator for Uv { + fn get_kind(&self) -> LocatorKind { + LocatorKind::Uv + } + + fn supported_categories(&self) -> Vec { + vec![PythonEnvironmentKind::Uv] + } + + fn try_from(&self, env: &PythonEnv) -> Option { + if !self.is_uv_python(env) { + return None; + } + + trace!("Identified UV Python environment: {:?}", env.executable); + + let manager = self.find_uv_executable().map(|uv_exe| { + EnvManager::new(uv_exe, EnvManagerType::Uv, None) + }); + + Some( + PythonEnvironmentBuilder::new(Some(PythonEnvironmentKind::Uv)) + .executable(Some(env.executable.clone())) + .prefix(env.prefix.clone()) + .version(env.version.clone()) + .manager(manager) + .build(), + ) + } + + fn find(&self, reporter: &dyn Reporter) { + trace!("UV Locator: Starting search for UV Python installations"); + self.discover_uv_pythons(reporter); + } +} + +// Simple wrapper for Environment to make it work with the provided environment +struct EnvironmentWrapper { + inner: Box, +} + +impl EnvironmentWrapper { + fn from(environment: &dyn Environment) -> Self { + Self { + inner: Box::new(SimpleEnvironment::new(environment)), + } + } +} + +impl Environment for EnvironmentWrapper { + fn get_user_home(&self) -> Option { + self.inner.get_user_home() + } + + fn get_root(&self) -> Option { + self.inner.get_root() + } + + fn get_env_var(&self, key: String) -> Option { + self.inner.get_env_var(key) + } + + fn get_know_global_search_locations(&self) -> Vec { + self.inner.get_know_global_search_locations() + } +} + +// Simple environment that just forwards to system env vars for most cases +struct SimpleEnvironment { + custom_vars: std::collections::HashMap, +} + +impl SimpleEnvironment { + fn new(environment: &dyn Environment) -> Self { + let mut custom_vars = std::collections::HashMap::new(); + + // Try to get some common environment variables from the provided environment + for key in ["UV_DATA_DIR", "HOME", "USERPROFILE", "APPDATA", "PATH"] { + if let Some(value) = environment.get_env_var(key.to_string()) { + custom_vars.insert(key.to_string(), value); + } + } + + Self { custom_vars } + } +} + +impl Environment for SimpleEnvironment { + fn get_user_home(&self) -> Option { + self.custom_vars.get("HOME") + .or_else(|| self.custom_vars.get("USERPROFILE")) + .map(|h| PathBuf::from(h)) + .or_else(|| { + std::env::var("HOME") + .or_else(|_| std::env::var("USERPROFILE")) + .ok() + .map(PathBuf::from) + }) + } + + fn get_root(&self) -> Option { + None + } + + fn get_env_var(&self, key: String) -> Option { + self.custom_vars.get(&key).cloned() + .or_else(|| std::env::var(key).ok()) + } + + fn get_know_global_search_locations(&self) -> Vec { + if let Some(path_var) = self.get_env_var("PATH".to_string()) { + std::env::split_paths(&path_var) + .filter(|p| p.exists()) + .collect() + } else { + vec![] + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pet_core::os_environment::Environment; + use std::collections::HashMap; + + struct MockEnvironment { + env_vars: HashMap, + } + + impl MockEnvironment { + fn new() -> Self { + Self { + env_vars: HashMap::new(), + } + } + + fn with_env_var(mut self, key: &str, value: &str) -> Self { + self.env_vars.insert(key.to_string(), value.to_string()); + self + } + } + + impl Environment for MockEnvironment { + fn get_user_home(&self) -> Option { + self.env_vars.get("HOME").map(|h| PathBuf::from(h)) + } + + fn get_root(&self) -> Option { + None + } + + fn get_env_var(&self, key: String) -> Option { + self.env_vars.get(&key).cloned() + } + + fn get_know_global_search_locations(&self) -> Vec { + vec![] + } + } + + #[test] + fn test_get_uv_data_dir_custom() { + let env = MockEnvironment::new() + .with_env_var("UV_DATA_DIR", "/custom/uv/data"); + let uv = Uv::from(&env); + + assert_eq!( + uv.get_uv_data_dir(), + Some(PathBuf::from("/custom/uv/data")) + ); + } + + #[test] + #[cfg(windows)] + fn test_get_uv_data_dir_windows_default() { + let env = MockEnvironment::new() + .with_env_var("APPDATA", "C:\\Users\\test\\AppData\\Roaming"); + let uv = Uv::from(&env); + + assert_eq!( + uv.get_uv_data_dir(), + Some(PathBuf::from("C:\\Users\\test\\AppData\\Roaming\\uv")) + ); + } + + #[test] + #[cfg(unix)] + fn test_get_uv_data_dir_unix_default() { + let env = MockEnvironment::new() + .with_env_var("HOME", "/home/test"); + let uv = Uv::from(&env); + + assert_eq!( + uv.get_uv_data_dir(), + Some(PathBuf::from("/home/test/.local/share/uv")) + ); + } + + #[test] + fn test_get_uv_python_dir() { + let env = MockEnvironment::new() + .with_env_var("UV_DATA_DIR", "/test/uv"); + let uv = Uv::from(&env); + + assert_eq!( + uv.get_uv_python_dir(), + Some(PathBuf::from("/test/uv/python")) + ); + } +} \ No newline at end of file diff --git a/crates/pet-uv/tests/integration_test.rs b/crates/pet-uv/tests/integration_test.rs new file mode 100644 index 00000000..f4bfd992 --- /dev/null +++ b/crates/pet-uv/tests/integration_test.rs @@ -0,0 +1,149 @@ +use std::fs; +use std::path::PathBuf; +use tempfile::TempDir; + +use pet_core::{ + env::PythonEnv, + os_environment::Environment, + reporter::Reporter, + python_environment::PythonEnvironment, + Locator +}; +use pet_uv::Uv; + +struct TestReporter { + environments: std::sync::Mutex>, +} + +impl TestReporter { + fn new() -> Self { + Self { + environments: std::sync::Mutex::new(Vec::new()), + } + } + + fn get_environments(&self) -> Vec { + self.environments.lock().unwrap().clone() + } +} + +impl Reporter for TestReporter { + fn report_environment(&self, env: &PythonEnvironment) { + self.environments.lock().unwrap().push(env.clone()); + } + + fn report_manager(&self, _manager: &pet_core::manager::EnvManager) { + // Not needed for this test + } + + fn report_telemetry(&self, _event: &pet_core::telemetry::TelemetryEvent) { + // Not needed for this test + } +} + +struct TestEnvironment { + env_vars: std::collections::HashMap, +} + +impl TestEnvironment { + fn new() -> Self { + Self { + env_vars: std::collections::HashMap::new(), + } + } + + fn with_env_var(mut self, key: &str, value: &str) -> Self { + self.env_vars.insert(key.to_string(), value.to_string()); + self + } +} + +impl Environment for TestEnvironment { + fn get_user_home(&self) -> Option { + self.env_vars.get("HOME").map(|h| PathBuf::from(h)) + } + + fn get_root(&self) -> Option { + None + } + + fn get_env_var(&self, key: String) -> Option { + self.env_vars.get(&key).cloned() + } + + fn get_know_global_search_locations(&self) -> Vec { + vec![] + } +} + +#[test] +fn test_uv_locator_integration() { + // Create a temporary directory structure to mimic UV layout + let temp_dir = TempDir::new().unwrap(); + let uv_data_dir = temp_dir.path().join("uv"); + let python_dir = uv_data_dir.join("python"); + let python_install_dir = python_dir.join("cpython-3.11.5-linux-x86_64-gnu"); + let bin_dir = python_install_dir.join("bin"); + + // Create the directory structure + fs::create_dir_all(&bin_dir).unwrap(); + + // Create a fake Python executable + let python_exe = bin_dir.join("python"); + fs::write(&python_exe, "#!/usr/bin/env python3\nprint('Hello')\n").unwrap(); + + // Make it executable (Unix only) + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&python_exe, fs::Permissions::from_mode(0o755)).unwrap(); + } + + // Create test environment with UV_DATA_DIR pointing to our temp directory + let env = TestEnvironment::new() + .with_env_var("UV_DATA_DIR", uv_data_dir.to_str().unwrap()); + + // Create UV locator + let uv_locator = Uv::from(&env); + + // Test that it can identify UV python environments + let python_env = PythonEnv { + executable: python_exe.clone(), + prefix: Some(python_install_dir.clone()), + version: None, + symlinks: None, + }; + + let detected_env = uv_locator.try_from(&python_env); + assert!(detected_env.is_some()); + + let detected_env = detected_env.unwrap(); + assert_eq!(detected_env.kind, Some(pet_core::python_environment::PythonEnvironmentKind::Uv)); + assert_eq!(detected_env.executable, Some(python_exe)); + assert_eq!(detected_env.prefix, Some(python_install_dir)); + + // Test the find method + let reporter = TestReporter::new(); + uv_locator.find(&reporter); + + let found_envs = reporter.get_environments(); + assert_eq!(found_envs.len(), 1); + assert_eq!(found_envs[0].kind, Some(pet_core::python_environment::PythonEnvironmentKind::Uv)); +} + +#[test] +fn test_uv_locator_non_uv_environment() { + let env = TestEnvironment::new(); + let uv_locator = Uv::from(&env); + + // Test with a non-UV Python environment + let python_env = PythonEnv { + executable: PathBuf::from("/usr/bin/python"), + prefix: Some(PathBuf::from("/usr")), + version: None, + symlinks: None, + }; + + let detected_env = uv_locator.try_from(&python_env); + assert!(detected_env.is_none()); +} \ No newline at end of file diff --git a/crates/pet/Cargo.toml b/crates/pet/Cargo.toml index ee2efdf7..2271c2c0 100644 --- a/crates/pet/Cargo.toml +++ b/crates/pet/Cargo.toml @@ -18,6 +18,7 @@ pet-homebrew = { path = "../pet-homebrew" } pet-core = { path = "../pet-core" } pet-conda = { path = "../pet-conda" } pet-pixi = { path = "../pet-pixi" } +pet-uv = { path = "../pet-uv" } pet-jsonrpc = { path = "../pet-jsonrpc" } pet-fs = { path = "../pet-fs" } pet-pyenv = { path = "../pet-pyenv" } diff --git a/crates/pet/src/locators.rs b/crates/pet/src/locators.rs index 5b6b3e81..777025b2 100644 --- a/crates/pet/src/locators.rs +++ b/crates/pet/src/locators.rs @@ -19,6 +19,7 @@ use pet_pixi::Pixi; use pet_poetry::Poetry; use pet_pyenv::PyEnv; use pet_python_utils::env::ResolvedPythonEnv; +use pet_uv::Uv; use pet_venv::Venv; use pet_virtualenv::VirtualEnv; use pet_virtualenvwrapper::VirtualEnvWrapper; @@ -52,10 +53,13 @@ pub fn create_locators( // 4. Pixi locators.push(Arc::new(Pixi::new())); - // 5. Conda Python + // 5. UV + locators.push(Arc::new(Uv::from(environment))); + + // 6. Conda Python locators.push(conda_locator); - // 6. Support for Virtual Envs + // 7. 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); @@ -65,7 +69,7 @@ pub fn create_locators( // VirtualEnv is the most generic, hence should be the last. locators.push(Arc::new(VirtualEnv::new())); - // 7. Homebrew Python + // 8. Homebrew Python if cfg!(unix) { #[cfg(unix)] use pet_homebrew::Homebrew; @@ -75,14 +79,14 @@ pub fn create_locators( locators.push(Arc::new(homebrew_locator)); } - // 8. Global Mac Python - // 9. CommandLineTools Python & xcode + // 9. Global Mac Python + // 10. 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())); } - // 10. Global Linux Python + // 11. 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" {