diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 0c9d2951..68d35cda 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -28,9 +28,6 @@ jobs: - os: windows-latest target: x86_64-pc-windows-msvc run_cli: "yes" - - os: windows-latest - target: aarch64-pc-windows-msvc - run_cli: "no" - os: ubuntu-latest target: x86_64-unknown-linux-musl run_cli: "yes" @@ -197,26 +194,35 @@ jobs: # Some of these tests are very specific and need to be run in isolation. # E.g. we need to ensure we have a poetry project setup correctly (without .venv created using `pip -m venv .venv`). # We can try to use the previous `tests` job, but that gets very complicated. - name: Other Tests + name: Env Tests runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - - feature: ci-poetry-global + - feature: ci-poetry-global # Poetry tests with envs stored in standard location os: ubuntu-latest target: x86_64-unknown-linux-musl - - feature: ci-poetry-project + - feature: ci-poetry-project # Poetry tests, with poetry envs in project os: ubuntu-latest target: x86_64-unknown-linux-musl - - feature: ci-poetry-custom + - feature: ci-poetry-custom # Poetry tests with envs stored in a custom location os: ubuntu-latest target: x86_64-unknown-linux-musl + - feature: ci-poetry-global # Poetry tests with envs stored in standard location + os: windows-latest + target: x86_64-pc-windows-msvc + - feature: ci-poetry-project # Poetry tests, with poetry envs in project + os: windows-latest + target: x86_64-pc-windows-msvc + - feature: ci-poetry-custom # Poetry tests with envs stored in a custom location + os: windows-latest + target: x86_64-pc-windows-msvc steps: - name: Checkout uses: actions/checkout@v4 - # Setup Poetry + # region Setup Poetry - name: Set Python 3.x to PATH if: startsWith( matrix.feature, 'ci-poetry') uses: actions/setup-python@v5 @@ -225,16 +231,33 @@ jobs: - name: Set Python 3.12 to PATH if: startsWith( matrix.feature, 'ci-poetry') + id: setupPython312 uses: actions/setup-python@v5 with: python-version: "3.12" - name: Set Python 3.11 to PATH if: startsWith( matrix.feature, 'ci-poetry') + id: setupPython311 uses: actions/setup-python@v5 with: python-version: "3.11" + - name: Python 3.12 Path + if: startsWith( matrix.feature, 'ci-poetry') && startsWith( matrix.os, 'windows') + run: echo "PYTHON_3_12_PATH=${{ steps.setupPython312.outputs.python-path }}" >> $GITHUB_ENV + shell: bash + + - name: Python 3.12 Path + if: startsWith( matrix.feature, 'ci-poetry') && startsWith( matrix.os, 'windows') + run: echo $PYTHON_3_12_PATH + shell: bash + + - name: Python 3.11 Path + if: startsWith( matrix.feature, 'ci-poetry') && startsWith( matrix.os, 'windows') + run: echo "PYTHON_3_11_PATH=${{ steps.setupPython311.outputs.python-path }}" >> $GITHUB_ENV + shell: bash + - name: Install Poetry (envs globally) if: startsWith( matrix.feature, 'ci-poetry-global') uses: snok/install-poetry@93ada01c735cc8a383ce0ce2ae205a21c415379b @@ -260,6 +283,11 @@ jobs: virtualenvs-path: ~/my-custom-path installer-parallel: true + - name: Petry exe + if: startsWith( matrix.feature, 'ci-poetry') + run: which poetry + shell: bash + - name: Petry config if: startsWith( matrix.feature, 'ci-poetry') run: poetry config --list @@ -272,13 +300,23 @@ jobs: shell: bash - name: Petry virtual env setup 3.12 - if: startsWith( matrix.feature, 'ci-poetry') - run: poetry env use python3.12 + if: startsWith( matrix.feature, 'ci-poetry') && startsWith( matrix.os, 'ubuntu') + run: poetry env use 3.12 + shell: bash + + - name: Petry virtual env setup 3.12 + if: startsWith( matrix.feature, 'ci-poetry') && startsWith( matrix.os, 'windows') + run: poetry env use $PYTHON_3_12_PATH shell: bash - name: Petry virtual env setup 3.11 - if: startsWith( matrix.feature, 'ci-poetry') - run: poetry env use python3.11 + if: startsWith( matrix.feature, 'ci-poetry') && startsWith( matrix.os, 'ubuntu') + run: poetry env use 3.11 + shell: bash + + - name: Petry virtual env setup 3.11 + if: startsWith( matrix.feature, 'ci-poetry') && startsWith( matrix.os, 'windows') + run: poetry env use $PYTHON_3_11_PATH shell: bash - name: Petry list envs @@ -296,6 +334,8 @@ jobs: # run: set # shell: bash + # endregion + # Rust - name: Rust Tool Chain setup uses: dtolnay/rust-toolchain@stable @@ -366,7 +406,7 @@ jobs: shell: bash - name: Find Environments - run: cargo run --release --target ${{ matrix.target }} -- find -v + run: cargo run --release --target ${{ matrix.target }} shell: bash - name: Run Tests diff --git a/Cargo.lock b/Cargo.lock index 5b649744..586faee2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -286,6 +286,7 @@ name = "pet" version = "0.1.0" dependencies = [ "clap", + "env_logger", "lazy_static", "log", "msvc_spectre_libs", diff --git a/crates/pet-core/src/os_environment.rs b/crates/pet-core/src/os_environment.rs index 645a44a5..da800953 100644 --- a/crates/pet-core/src/os_environment.rs +++ b/crates/pet-core/src/os_environment.rs @@ -7,6 +7,7 @@ use std::{ sync::{Arc, Mutex}, }; +use log::trace; use pet_fs::path::norm_case; pub trait Environment { @@ -78,7 +79,7 @@ impl Environment for EnvironmentApi { let mut paths = env::split_paths(&self.get_env_var("PATH".to_string()).unwrap_or_default()) .collect::>(); - + trace!("Env PATH: {:?}", paths); vec![ PathBuf::from("/bin"), PathBuf::from("/etc"), diff --git a/crates/pet-mac-commandlinetools/src/lib.rs b/crates/pet-mac-commandlinetools/src/lib.rs index a621bd68..10bdcfb2 100644 --- a/crates/pet-mac-commandlinetools/src/lib.rs +++ b/crates/pet-mac-commandlinetools/src/lib.rs @@ -48,8 +48,10 @@ impl Locator for MacCmdLineTools { if !env .executable - .to_string_lossy() - .starts_with("/Library/Developer/CommandLineTools/usr/bin/python") + .starts_with("/Library/Developer/CommandLineTools/usr/bin") + && !env.executable.starts_with( + "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions", + ) { return None; } @@ -70,6 +72,33 @@ impl Locator for MacCmdLineTools { symlinks.push(symlink); } + // Possible we got the file /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3.9 + // We know that /Library/Developer/CommandLineTools/usr/bin/python3 is a symlink to the above. + if env + .executable + .starts_with("/Library/Developer/CommandLineTools/usr/bin") + { + let exe = PathBuf::from("/Library/Developer/CommandLineTools/usr/bin/python3"); + if let Some(symlink) = resolve_symlink(&exe) { + if symlinks.contains(&symlink) { + symlinks.push(symlink); + + // Rest of the files in this directory are also symlinks to the same exe. + for exe in find_executables(PathBuf::from( + "/Library/Developer/CommandLineTools/usr/bin", + )) { + if !symlinks.contains(&exe) { + if let Some(symlink) = resolve_symlink(&exe) { + if symlinks.contains(&symlink) { + symlinks.push(exe); + } + } + } + } + } + } + } + // We know /usr/bin/python3 can end up pointing to this same Python exe as well // Hence look for those symlinks as well. // Unfortunately /usr/bin/python3 is not a real symlink @@ -106,6 +135,17 @@ impl Locator for MacCmdLineTools { symlinks.sort(); symlinks.dedup(); + // Find other exes that are symlinks to the same exe in /Library/Developer/CommandLineTools/usr/bin + for exe in find_executables("/Library/Developer/CommandLineTools/usr/bin") { + if !symlinks.contains(&exe) { + if let Some(symlink) = resolve_symlink(&exe) { + if symlinks.contains(&symlink) { + symlinks.push(exe); + } + } + } + } + if prefix.is_none() { // We would have identified the symlinks by now. // Look for the one with the path `/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3.9` diff --git a/crates/pet-mac-xcode/src/lib.rs b/crates/pet-mac-xcode/src/lib.rs index 28a483f4..1c552637 100644 --- a/crates/pet-mac-xcode/src/lib.rs +++ b/crates/pet-mac-xcode/src/lib.rs @@ -7,8 +7,11 @@ use pet_core::{ Locator, }; use pet_fs::path::resolve_symlink; -use pet_python_utils::env::{PythonEnv, ResolvedPythonEnv}; use pet_python_utils::version; +use pet_python_utils::{ + env::{PythonEnv, ResolvedPythonEnv}, + executable::find_executables, +}; use pet_virtualenv::is_virtualenv; use std::path::PathBuf; @@ -65,7 +68,34 @@ impl Locator for MacXCode { // /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3.9 // Verify this and add that to the list of symlinks as well. if let Some(symlink) = resolve_symlink(&env.executable) { - symlinks.push(symlink); + symlinks.push(symlink.clone()); + + // All exes in the bin directory of the symlink are also symlinks (thats generally of the form /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3.9) + for exe in find_executables(symlink.parent().unwrap()) { + symlinks.push(exe); + } + } + + // Possible the env.executable is "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3.9" + // The symlink to the above exe is in /Applications/Xcode.app/Contents/Developer/usr/bin/python3 + // Lets try to find that, because /usr/bin/python3 could also exist and when we run python, the sys.execuctable points to the file /Applications/Xcode.app/Contents/Developer/usr/bin/python3 + // The name of the `Xcode.app` folder can be different on other machines, e.g. on CI it is `Xcode_15.0.1.app` + let xcode_folder_name = exe_str.split('/').nth(2).unwrap_or_default(); + + let bin = PathBuf::from(format!( + "/Applications/{}/Contents/Developer/usr/bin", + xcode_folder_name + )); + let exe = bin.join("python3"); + if let Some(symlink) = resolve_symlink(&exe) { + if symlinks.contains(&symlink) { + symlinks.push(exe.clone()); + + // All exes in this directory are symlinks + for exe in find_executables(bin) { + symlinks.push(exe); + } + } } // We know /usr/bin/python3 can end up pointing to this same Python exe as well diff --git a/crates/pet-poetry/src/environment_locations.rs b/crates/pet-poetry/src/environment_locations.rs index dd701045..0295ed58 100644 --- a/crates/pet-poetry/src/environment_locations.rs +++ b/crates/pet-poetry/src/environment_locations.rs @@ -15,7 +15,7 @@ use std::{ use crate::{ config::Config, env_variables::EnvVariables, environment::create_poetry_env, - pyproject_toml::PyProjectToml, + manager::PoetryManager, pyproject_toml::PyProjectToml, }; lazy_static! { @@ -26,6 +26,7 @@ lazy_static! { pub fn list_environments( env: &EnvVariables, project_dirs: &[PathBuf], + manager: Option, ) -> Option> { if project_dirs.is_empty() { return None; @@ -76,7 +77,9 @@ pub fn list_environments( .unwrap_or_default(); // Look for .venv as well, in case we create the virtual envs in the local project folder. if name.starts_with(&virtualenv_prefix) || name.starts_with(".venv") { - if let Some(env) = create_poetry_env(&virtual_env, project_dir.clone(), None) { + if let Some(env) = + create_poetry_env(&virtual_env, project_dir.clone(), manager.clone()) + { envs.push(env); } } diff --git a/crates/pet-poetry/src/lib.rs b/crates/pet-poetry/src/lib.rs index fadb2e8b..a7fcb6f9 100644 --- a/crates/pet-poetry/src/lib.rs +++ b/crates/pet-poetry/src/lib.rs @@ -105,13 +105,14 @@ impl Poetry { managers: vec![], environments: vec![], }; - if let Some(manager) = manager { + if let Some(manager) = &manager { result.managers.push(manager.to_manager()); } if let Ok(values) = self.project_dirs.lock() { let project_dirs = values.clone(); drop(values); - let envs = list_environments(&self.env_vars, &project_dirs.clone()).unwrap_or_default(); + let envs = list_environments(&self.env_vars, &project_dirs.clone(), manager) + .unwrap_or_default(); result.environments.extend(envs.clone()); } diff --git a/crates/pet-poetry/src/manager.rs b/crates/pet-poetry/src/manager.rs index db422504..ba0b02bd 100644 --- a/crates/pet-poetry/src/manager.rs +++ b/crates/pet-poetry/src/manager.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use log::trace; use pet_core::manager::{EnvManager, EnvManagerType}; use std::{env, path::PathBuf}; @@ -35,8 +36,10 @@ impl PoetryManager { if let Some(poetry_home) = &env_variables.poetry_home { if std::env::consts::OS == "windows" { search_paths.push(poetry_home.join("bin").join("poetry.exe")); + search_paths.push(poetry_home.join("venv").join("bin").join("poetry.exe")); } search_paths.push(poetry_home.join("bin").join("poetry")); + search_paths.push(poetry_home.join("venv").join("bin").join("poetry")); } if std::env::consts::OS == "windows" { if let Some(app_data) = env_variables.app_data.clone() { @@ -71,6 +74,10 @@ impl PoetryManager { app_data.join("Python").join("scripts").join("poetry"), // https://python-poetry.org/docs/#installing-with-the-official-installer ); } + search_paths.push( + // Found after installing on Windows via github actions. + home.join(".local").join("bin").join("poetry"), + ); } else if std::env::consts::OS == "macos" { search_paths.push( // https://python-poetry.org/docs/#installing-with-the-official-installer @@ -111,9 +118,16 @@ impl PoetryManager { if executable.is_file() { return Some(PoetryManager { executable }); } + if std::env::consts::OS == "windows" { + let executable = each.join("poetry.exe"); + if executable.is_file() { + return Some(PoetryManager { executable }); + } + } } } } + trace!("Poetry exe not found"); None } pub fn to_manager(&self) -> EnvManager { diff --git a/crates/pet-python-utils/src/env.rs b/crates/pet-python-utils/src/env.rs index 88b5d9eb..c6860e35 100644 --- a/crates/pet-python-utils/src/env.rs +++ b/crates/pet-python-utils/src/env.rs @@ -97,6 +97,11 @@ impl ResolvedPythonEnv { match result { Ok(output) => { let output = String::from_utf8(output.stdout).unwrap().trim().to_string(); + trace!( + "Python Execution for {:?} produced an output {:?}", + executable, + output + ); if let Some((_, output)) = output.split_once(PYTHON_INFO_JSON_SEPARATOR) { if let Ok(info) = serde_json::from_str::(output) { Some(Self { diff --git a/crates/pet-windows-registry/src/lib.rs b/crates/pet-windows-registry/src/lib.rs index c7d74b07..1c2fc8d5 100644 --- a/crates/pet-windows-registry/src/lib.rs +++ b/crates/pet-windows-registry/src/lib.rs @@ -88,8 +88,10 @@ impl Locator for WindowsRegistry { if let Some(result) = self.find_with_cache() { // Find the same env here for found_env in result.environments { - if env.executable.to_str() == env.executable.to_str() { - return Some(found_env); + if let Some(ref python_executable_path) = found_env.executable { + if python_executable_path == &env.executable { + return Some(found_env); + } } } } diff --git a/crates/pet/Cargo.toml b/crates/pet/Cargo.toml index 35ef8b0e..d114ab86 100644 --- a/crates/pet/Cargo.toml +++ b/crates/pet/Cargo.toml @@ -37,6 +37,7 @@ log = "0.4.21" clap = { version = "4.5.4", features = ["derive", "cargo"] } serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.93" +env_logger = "0.10.2" [dev-dependencies] regex = "1.10.4" diff --git a/crates/pet/src/locators.rs b/crates/pet/src/locators.rs index ee963d31..9d902f7a 100644 --- a/crates/pet/src/locators.rs +++ b/crates/pet/src/locators.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use log::{trace, warn}; +use log::{info, trace}; use pet_conda::Conda; use pet_core::arch::Architecture; use pet_core::os_environment::EnvironmentApi; @@ -114,7 +114,7 @@ pub fn identify_python_environment_using_locators( // We have no idea what this is. // We have check all of the resolvers. // Telemetry point, failed to identify env here. - warn!( + info!( "Unknown Env ({:?}) in Path resolved as {:?} and reported as {:?}", executable, resolved_env, @@ -179,7 +179,7 @@ fn find_symlinks(executable: &PathBuf) -> Option> { } #[cfg(windows)] -fn find_symlinks(executable: &PathBuf) -> Option> { +fn find_symlinks(_executable: &PathBuf) -> Option> { // In windows we will need to spawn the Python exe and then get the exes. // Lets wait and see if this is necessary. None diff --git a/crates/pet/tests/ci_poetry.rs b/crates/pet/tests/ci_poetry.rs index ecbde77d..7a0764c8 100644 --- a/crates/pet/tests/ci_poetry.rs +++ b/crates/pet/tests/ci_poetry.rs @@ -3,7 +3,6 @@ mod common; -#[cfg(unix)] #[cfg_attr(any(feature = "ci-poetry-global", feature = "ci-poetry-custom"), test)] #[allow(dead_code)] /// This is a test with Poetry for current directory with Python 3.12 and 3.11 and envs are created in regular global cache directory @@ -36,11 +35,14 @@ fn verify_ci_poetry_global() { let environments = result.environments; - result - .managers - .iter() - .find(|m| m.tool == EnvManagerType::Poetry) - .expect("Poetry manager not found"); + // On CI the poetry manager is installed using wsl, and the path isn't available on windows + if std::env::consts::OS != "windows" { + result + .managers + .iter() + .find(|m| m.tool == EnvManagerType::Poetry) + .expect("Poetry manager not found"); + } let poetry_envs = environments .iter() @@ -62,7 +64,6 @@ fn verify_ci_poetry_global() { .expect("Python 3.12 not found"); } -#[cfg(unix)] #[cfg_attr(feature = "ci-poetry-project", test)] #[allow(dead_code)] /// This is a test with Poetry for current directory with Python 3.11 and created as .venv in project directory. @@ -95,11 +96,14 @@ fn verify_ci_poetry_project() { let environments = result.environments; - result - .managers - .iter() - .find(|m| m.tool == EnvManagerType::Poetry) - .expect("Poetry manager not found"); + // On CI the poetry manager is installed using wsl, and the path isn't available on windows + if std::env::consts::OS != "windows" { + result + .managers + .iter() + .find(|m| m.tool == EnvManagerType::Poetry) + .expect("Poetry manager not found"); + } let poetry_envs = environments .iter() diff --git a/crates/pet/tests/ci_test.rs b/crates/pet/tests/ci_test.rs index 14a51e45..e2714829 100644 --- a/crates/pet/tests/ci_test.rs +++ b/crates/pet/tests/ci_test.rs @@ -1,14 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::path::PathBuf; +use std::{path::PathBuf, sync::Once}; use common::{does_version_match, resolve_test_path}; use lazy_static::lazy_static; +use log::trace; +use pet::locators::identify_python_environment_using_locators; use pet_core::{ arch::Architecture, python_environment::{PythonEnvironment, PythonEnvironmentCategory}, }; +use pet_python_utils::env::PythonEnv; use regex::Regex; use serde::Deserialize; @@ -17,19 +20,43 @@ lazy_static! { .expect("error parsing Version regex for Python Version in test"); } +static INIT: Once = Once::new(); + +/// Setup function that is only run once, even if called multiple times. +fn setup() { + INIT.call_once(|| { + env_logger::builder() + .filter(None, log::LevelFilter::Trace) + .init(); + }); +} + mod common; -#[cfg(unix)] +// #[cfg(unix)] #[cfg_attr( any( feature = "ci", feature = "ci-jupyter-container", - feature = "ci-homebrew-container" + feature = "ci-homebrew-container", + feature = "ci-poetry-global", + feature = "ci-poetry-project", + feature = "ci-poetry-custom", ), test )] #[allow(dead_code)] -// We should detect the conda install along with the base env +/// Verification 1 +/// For each discovered enviornment verify the accuracy of sys.prefix and sys.version +/// by spawning the Python executable +/// Verification 2: +/// For each enviornment, given the executable verify we can get the exact same information +/// Using the `locators.from` method (without having to find all environments). +/// I.e. we should be able to get the same information using only the executable. +/// Verification 3: +/// Similarly for each environment use one of the known symlinks and verify we can get the same information. +/// Verification 4 & 5: +/// Similarly for each environment use resolve method and verify we get the exact same information. fn verify_validity_of_discovered_envs() { use pet::{find::find_and_report_envs, locators::create_locators}; use pet_conda::Conda; @@ -37,18 +64,20 @@ fn verify_validity_of_discovered_envs() { use pet_reporter::test; use std::{env, sync::Arc, thread}; + setup(); + + let project_dir = PathBuf::from(env::var("GITHUB_WORKSPACE").unwrap_or_default()); let reporter = test::create_reporter(); let environment = EnvironmentApi::new(); let conda_locator = Arc::new(Conda::from(&environment)); let mut config = Configuration::default(); - if let Ok(cwd) = env::current_dir() { - config.search_paths = Some(vec![cwd]); - } + config.search_paths = Some(vec![project_dir.clone()]); let locators = create_locators(conda_locator.clone()); for locator in locators.iter() { locator.configure(&config); } + // Find all environments on this machine. find_and_report_envs(&reporter, Default::default(), &locators, conda_locator); let result = reporter.get_result(); @@ -58,8 +87,26 @@ fn verify_validity_of_discovered_envs() { if environment.executable.is_none() { continue; } + // Verification 1 + // For each enviornment verify the accuracy of sys.prefix and sys.version + // by spawning the Python executable + let e = environment.clone(); + threads.push(thread::spawn(move || { + verify_validity_of_interpreter_info(e); + })); + // Verification 2: + // For each enviornment, given the executable verify we can get the exact same information + // Using the `locators.from` method (without having to find all environments). + // I.e. we should be able to get the same information using only the executable. + // + // Verification 3: + // Similarly for each environment use one of the known symlinks and verify we can get the same information. + // + // TODO: Verification 4 & 5: + // Similarly for each environment use resolve method and verify we get the exact same information. + let e = environment.clone(); threads.push(thread::spawn(move || { - verify_validity_of_interpreter_info(environment.clone()); + verify_we_can_get_same_env_info_using_from(e) })); } for thread in threads { @@ -79,6 +126,7 @@ fn check_if_virtualenvwrapper_exists() { use pet_reporter::test; use std::sync::Arc; + setup(); let reporter = test::create_reporter(); let environment = EnvironmentApi::new(); let conda_locator = Arc::new(Conda::from(&environment)); @@ -123,6 +171,7 @@ fn check_if_pyenv_virtualenv_exists() { use pet_reporter::test; use std::sync::Arc; + setup(); let reporter = test::create_reporter(); let environment = EnvironmentApi::new(); let conda_locator = Arc::new(Conda::from(&environment)); @@ -234,6 +283,156 @@ fn verify_validity_of_interpreter_info(environment: PythonEnvironment) { } } +fn verify_we_can_get_same_env_info_using_from(environment: PythonEnvironment) { + for exe in &environment.clone().symlinks.unwrap_or_default() { + verify_we_can_get_same_env_inf_using_from_with_exe(exe, environment.clone()); + } +} + +fn verify_we_can_get_same_env_inf_using_from_with_exe( + executable: &PathBuf, + environment: PythonEnvironment, +) { + let mut environment = environment.clone(); + + // Assume we were given a path to the exe, then we use the `locator.from` method. + // We should be able to get the exct same information back given only the exe. + // + // Note: We will not not use the old locator objects, as we do not want any cached information. + // Hence create the locators all over again. + use pet::locators::create_locators; + use pet_conda::Conda; + use pet_core::{os_environment::EnvironmentApi, Configuration}; + use std::{env, sync::Arc}; + + let project_dir = PathBuf::from(env::var("GITHUB_WORKSPACE").unwrap_or_default()); + let os_environment = EnvironmentApi::new(); + let conda_locator = Arc::new(Conda::from(&os_environment)); + let mut config = Configuration::default(); + config.search_paths = Some(vec![project_dir.clone()]); + let locators = create_locators(conda_locator.clone()); + for locator in locators.iter() { + locator.configure(&config); + } + + let env = PythonEnv::new(executable.clone(), None, None); + let mut env = identify_python_environment_using_locators(&env, &locators, None).expect( + format!( + "Failed to resolve environment using `from` for {:?}", + environment + ) + .as_str(), + ); + trace!( + "For exe {:?} we got Environment = {:?}, To compare against {:?}", + executable, + env, + environment + ); + + // if env.category = Unknown and the executable is in PATH, then category will be GlobalPaths + if let Some(exe) = env.executable.clone() { + if let Some(bin) = exe.parent() { + if env.category == PythonEnvironmentCategory::Unknown { + let paths = env::split_paths(&env::var("PATH").ok().unwrap_or_default()) + .into_iter() + .collect::>(); + if paths.contains(&bin.to_path_buf()) { + env.category = PythonEnvironmentCategory::GlobalPaths; + } + } + } + } + + assert_eq!( + env.category, + environment.clone().category, + "Category mismatch for {:?} and {:?}", + environment, + env + ); + + if let (Some(version), Some(expected_version)) = + (environment.clone().version, env.clone().version) + { + assert!( + does_version_match(&version, &expected_version), + "Version mismatch for (expected {:?} to start with {:?}) for env = {:?} and environment = {:?}", + expected_version, + version, + env.clone(), + environment.clone() + ); + } + // We have compared the versions, now ensure they are treated as the same + // So that we can compare the objects easily + env.version = environment.clone().version; + + if let Some(prefix) = environment.clone().prefix { + if env.clone().executable == Some(PathBuf::from("/usr/local/python/current/bin/python")) + && (prefix.to_str().unwrap() == "/usr/local/python/current" + && env.clone().prefix == Some(PathBuf::from("/usr/local/python/3.10.13"))) + || (prefix.to_str().unwrap() == "/usr/local/python/3.10.13" + && env.clone().prefix == Some(PathBuf::from("/usr/local/python/current"))) + { + // known issue https://github.com/microsoft/python-environment-tools/issues/64 + env.prefix = environment.clone().prefix; + } else if env.clone().executable + == Some(PathBuf::from("/home/codespace/.python/current/bin/python")) + && (prefix.to_str().unwrap() == "/home/codespace/.python/current" + && env.clone().prefix == Some(PathBuf::from("/usr/local/python/3.10.13"))) + || (prefix.to_str().unwrap() == "/usr/local/python/3.10.13" + && env.clone().prefix == Some(PathBuf::from("/home/codespace/.python/current"))) + { + // known issue https://github.com/microsoft/python-environment-tools/issues/64 + env.prefix = environment.clone().prefix; + } + } + // known issue + env.symlinks = Some( + env.clone() + .symlinks + .unwrap_or_default() + .iter() + .filter(|p| { + // This is in the path, but not easy to figure out, unless we add support for codespaces or CI. + if p.starts_with("/Users/runner/hostedtoolcache/Python") + && p.to_string_lossy().contains("arm64") + { + false + } else { + true + } + }) + .map(|p| p.to_path_buf()) + .collect::>(), + ); + environment.symlinks = Some( + environment + .clone() + .symlinks + .unwrap_or_default() + .iter() + .filter(|p| { + // This is in the path, but not easy to figure out, unless we add support for codespaces or CI. + if p.starts_with("/Users/runner/hostedtoolcache/Python") + && p.to_string_lossy().contains("arm64") + { + false + } else { + true + } + }) + .map(|p| p.to_path_buf()) + .collect::>(), + ); + assert_eq!( + env, environment, + "Environment mismatch for {:?}", + environment + ); +} + #[cfg(unix)] #[cfg(target_os = "linux")] #[cfg_attr(feature = "ci", test)] @@ -246,6 +445,7 @@ fn verify_bin_usr_bin_user_local_are_separate_python_envs() { use pet_reporter::test; use std::sync::Arc; + setup(); let reporter = test::create_reporter(); let environment = EnvironmentApi::new(); let conda_locator = Arc::new(Conda::from(&environment));