diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index bc8e5a76..cfdf6c11 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -77,6 +77,13 @@ jobs: - name: Check Conda version run: conda info --all + - name: Create Conda Environments + run: | + conda create -n test-env1 python=3.12 -y + conda create -n test-env-no-python -y + conda create -p ./prefix-envs/.conda1 python=3.12 -y + conda create -p ./prefix-envs/.conda-nopy -y + - name: Install pipenv run: pip install pipenv diff --git a/Cargo.lock b/Cargo.lock index 95e273da..63833a3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,6 +195,7 @@ name = "pet" version = "0.1.0" dependencies = [ "clap", + "lazy_static", "log", "pet-conda", "pet-core", @@ -211,6 +212,9 @@ dependencies = [ "pet-virtualenvwrapper", "pet-windows-registry", "pet-windows-store", + "regex", + "serde", + "serde_json", ] [[package]] diff --git a/crates/pet-homebrew/src/lib.rs b/crates/pet-homebrew/src/lib.rs index c9e3e56b..36e63567 100644 --- a/crates/pet-homebrew/src/lib.rs +++ b/crates/pet-homebrew/src/lib.rs @@ -7,7 +7,10 @@ use environments::get_python_info; use pet_core::{ os_environment::Environment, python_environment::PythonEnvironment, Locator, LocatorResult, }; -use pet_utils::{env::PythonEnv, executable::resolve_symlink}; +use pet_utils::{ + env::PythonEnv, + executable::{find_executables, resolve_symlink}, +}; use std::{collections::HashSet, path::PathBuf}; mod env_variables; @@ -84,24 +87,25 @@ impl Locator for Homebrew { let mut reported: HashSet = HashSet::new(); let mut environments: Vec = vec![]; for homebrew_prefix_bin in get_homebrew_prefix_bin(&self.environment) { - for file in std::fs::read_dir(&homebrew_prefix_bin) - .ok()? - .filter_map(Result::ok) - .filter(|f| { - let file_name = f.file_name().to_str().unwrap_or_default().to_lowercase(); - file_name.starts_with("python") - // If this file name is `python3`, then ignore this for now. + for file in find_executables(&homebrew_prefix_bin).iter().filter(|f| { + let file_name = f + .file_name() + .unwrap_or_default() + .to_str() + .unwrap_or_default() + .to_lowercase(); + file_name.starts_with("python") + // If this file name is `python3`, then ignore this for now. // We would prefer to use `python3.x` instead of `python3`. // That way its more consistent and future proof && file_name != "python3" && file_name != "python" - }) - { + }) { // Sometimes we end up with other python installs in the Homebrew bin directory. // E.g. /usr/local/bin is treated as a location where homebrew can be found (homebrew bin) // However this is a very generic location, and we might end up with other python installs here. // Hence call `resolve` to correctly identify homebrew python installs. - let env_to_resolve = PythonEnv::new(file.path(), None, None); + let env_to_resolve = PythonEnv::new(file.clone(), None, None); if let Some(env) = resolve(&env_to_resolve, &mut reported) { environments.push(env); } diff --git a/crates/pet-pyenv/src/environments.rs b/crates/pet-pyenv/src/environments.rs index 86a72aae..27d7d10f 100644 --- a/crates/pet-pyenv/src/environments.rs +++ b/crates/pet-pyenv/src/environments.rs @@ -9,7 +9,7 @@ use pet_core::{ python_environment::{PythonEnvironment, PythonEnvironmentBuilder, PythonEnvironmentCategory}, LocatorResult, }; -use pet_utils::{executable::find_executable, pyvenv_cfg::PyVenvCfg}; +use pet_utils::{executable::find_executable, headers::Headers, pyvenv_cfg::PyVenvCfg}; use regex::Regex; use std::{fs, path::Path, sync::Arc}; @@ -72,6 +72,8 @@ pub fn get_pure_python_environment( ) -> Option { let file_name = path.file_name()?.to_string_lossy().to_string(); let version = get_version(&file_name)?; + // If we can get the version from the header files, thats more accurate. + let version = Headers::get_version(path).unwrap_or(version.clone()); let arch = if file_name.ends_with("-win32") { Some(Architecture::X86) diff --git a/crates/pet-reporter/src/environment.rs b/crates/pet-reporter/src/environment.rs index 0f053e6c..6c71feab 100644 --- a/crates/pet-reporter/src/environment.rs +++ b/crates/pet-reporter/src/environment.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +use log::error; use pet_core::{ arch::Architecture, python_environment::{PythonEnvironment, PythonEnvironmentCategory}, @@ -116,3 +117,17 @@ impl Environment { } } } + +pub fn get_environment_key(env: &PythonEnvironment) -> Option<&PathBuf> { + if let Some(exe) = &env.executable { + Some(exe) + } else if let Some(prefix) = &env.prefix { + Some(prefix) + } else { + error!( + "Failed to report environment due to lack of exe & prefix: {:?}", + env + ); + None + } +} diff --git a/crates/pet-reporter/src/jsonrpc.rs b/crates/pet-reporter/src/jsonrpc.rs index 24850484..8baa508a 100644 --- a/crates/pet-reporter/src/jsonrpc.rs +++ b/crates/pet-reporter/src/jsonrpc.rs @@ -1,9 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::{environment::Environment, manager::Manager}; +use crate::{ + environment::{get_environment_key, Environment}, + manager::Manager, +}; use env_logger::Builder; -use log::{error, LevelFilter}; +use log::LevelFilter; use pet_core::{manager::EnvManager, python_environment::PythonEnvironment, reporter::Reporter}; use pet_jsonrpc::send_message; use serde::{Deserialize, Serialize}; @@ -45,20 +48,6 @@ pub fn create_reporter() -> impl Reporter { } } -fn get_environment_key(env: &PythonEnvironment) -> Option<&PathBuf> { - if let Some(exe) = &env.executable { - Some(exe) - } else if let Some(prefix) = &env.prefix { - Some(prefix) - } else { - error!( - "Failed to report environment due to lack of exe & prefix: {:?}", - env - ); - None - } -} - #[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)] pub enum LogLevel { #[serde(rename = "debug")] diff --git a/crates/pet-reporter/src/lib.rs b/crates/pet-reporter/src/lib.rs index 75a9edf8..0d17a80f 100644 --- a/crates/pet-reporter/src/lib.rs +++ b/crates/pet-reporter/src/lib.rs @@ -5,3 +5,4 @@ mod environment; pub mod jsonrpc; mod manager; pub mod stdio; +pub mod test; diff --git a/crates/pet-reporter/src/stdio.rs b/crates/pet-reporter/src/stdio.rs index 1e9290a4..607f5cba 100644 --- a/crates/pet-reporter/src/stdio.rs +++ b/crates/pet-reporter/src/stdio.rs @@ -1,9 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::{environment::Environment, manager::Manager}; +use crate::{ + environment::{get_environment_key, Environment}, + manager::Manager, +}; use env_logger::Builder; -use log::{error, LevelFilter}; +use log::LevelFilter; use pet_core::{manager::EnvManager, python_environment::PythonEnvironment, reporter::Reporter}; use serde::{Deserialize, Serialize}; use std::{ @@ -46,20 +49,6 @@ pub fn create_reporter() -> impl Reporter { } } -fn get_environment_key(env: &PythonEnvironment) -> Option<&PathBuf> { - if let Some(exe) = &env.executable { - Some(exe) - } else if let Some(prefix) = &env.prefix { - Some(prefix) - } else { - error!( - "Failed to report environment due to lack of exe & prefix: {:?}", - env - ); - None - } -} - #[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)] pub enum LogLevel { #[serde(rename = "debug")] diff --git a/crates/pet-reporter/src/test.rs b/crates/pet-reporter/src/test.rs new file mode 100644 index 00000000..762584c9 --- /dev/null +++ b/crates/pet-reporter/src/test.rs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::environment::get_environment_key; +use pet_core::{manager::EnvManager, python_environment::PythonEnvironment, reporter::Reporter}; +use std::collections::HashMap; +use std::{ + path::PathBuf, + sync::{Arc, Mutex}, +}; + +pub struct TestReporter { + pub reported_managers: Arc>>, + pub reported_environments: Arc>>, +} + +impl Reporter for TestReporter { + fn report_manager(&self, manager: &EnvManager) { + let mut reported_managers = self.reported_managers.lock().unwrap(); + if !reported_managers.contains_key(&manager.executable) { + reported_managers.insert(manager.executable.clone(), manager.clone()); + } + } + + fn report_environment(&self, env: &PythonEnvironment) { + if let Some(key) = get_environment_key(env) { + let mut reported_environments = self.reported_environments.lock().unwrap(); + if !reported_environments.contains_key(key) { + reported_environments.insert(key.clone(), env.clone()); + } + } + } +} + +pub fn create_reporter() -> TestReporter { + TestReporter { + reported_managers: Arc::new(Mutex::new(HashMap::new())), + reported_environments: Arc::new(Mutex::new(HashMap::new())), + } +} diff --git a/crates/pet-utils/src/executable.rs b/crates/pet-utils/src/executable.rs index ad822575..6dd89851 100644 --- a/crates/pet-utils/src/executable.rs +++ b/crates/pet-utils/src/executable.rs @@ -103,22 +103,24 @@ fn is_python_executable_name(exe: &Path) -> bool { // If the real file == exe, then it is not a symlink. pub fn resolve_symlink(exe: &Path) -> Option { let name = exe.file_name()?.to_string_lossy(); - // TODO: What is -config and -build? + // In bin directory of homebrew, we have files like python-build, python-config, python3-config if !name.starts_with("python") || name.ends_with("-config") || name.ends_with("-build") { return None; } - - // If the file == symlink, then it is not a symlink. - // We already have the resolved file, no need to return that again. - if let Ok(real_file) = fs::read_link(exe) { - if real_file == exe { - None - } else { - Some(real_file) + if let Ok(metadata) = std::fs::symlink_metadata(exe) { + if metadata.is_file() || !metadata.file_type().is_symlink() { + return Some(exe.to_path_buf()); } - } else { - None + if let Ok(readlink) = std::fs::canonicalize(exe) { + if readlink == exe { + return None; + } else { + return Some(readlink); + } + } + return Some(exe.to_path_buf()); } + Some(exe.to_path_buf()) } // Given a list of executables, return the one with the shortest path. diff --git a/crates/pet-utils/src/headers.rs b/crates/pet-utils/src/headers.rs index a771fd4b..ee80ac1e 100644 --- a/crates/pet-utils/src/headers.rs +++ b/crates/pet-utils/src/headers.rs @@ -6,7 +6,7 @@ use regex::Regex; use std::{fs, path::Path}; lazy_static! { - static ref VERSION: Regex = Regex::new(r#"#define\s+PY_VERSION\s+"((\d+\.?)*)"#) + static ref VERSION: Regex = Regex::new(r#"#define\s+PY_VERSION\s+"((\d+\.?)*.*)\""#) .expect("error parsing Version regex for partchlevel.h"); } @@ -33,9 +33,31 @@ pub fn get_version(path: &Path) -> Option { if path.ends_with(bin) { path.pop(); } - let headers_path = if cfg!(windows) { "Headers" } else { "include" }; - let patchlevel_h = path.join(headers_path).join("patchlevel.h"); - let contents = fs::read_to_string(patchlevel_h).ok()?; + let headers_path = path.join(if cfg!(windows) { "Headers" } else { "include" }); + let patchlevel_h = headers_path.join("patchlevel.h"); + let mut contents = "".to_string(); + if let Ok(result) = fs::read_to_string(patchlevel_h) { + contents = result; + } else if fs::metadata(&headers_path).is_err() { + // Such a path does not exist, get out. + return None; + } else { + // Try the other path + // Sometimes we have it in a sub directory such as `python3.10` + if let Ok(readdir) = fs::read_dir(&headers_path) { + for path in readdir.filter_map(Result::ok).map(|e| e.path()) { + if let Ok(metadata) = fs::metadata(&path) { + if metadata.is_dir() { + let patchlevel_h = path.join("patchlevel.h"); + if let Ok(result) = fs::read_to_string(patchlevel_h) { + contents = result; + break; + } + } + } + } + } + } for line in contents.lines() { if let Some(captures) = VERSION.captures(line) { if let Some(value) = captures.get(1) { diff --git a/crates/pet-utils/src/path.rs b/crates/pet-utils/src/path.rs index 27c18ce3..6af051c0 100644 --- a/crates/pet-utils/src/path.rs +++ b/crates/pet-utils/src/path.rs @@ -8,6 +8,11 @@ use std::{ // Similar to fs::canonicalize, but ignores UNC paths and returns the path as is (for windows). pub fn normalize>(path: P) -> PathBuf { + // On unix do not use canonicalize, results in weird issues with homebrew paths + if cfg!(unix) { + return path.as_ref().to_path_buf(); + } + if let Ok(resolved) = fs::canonicalize(&path) { if cfg!(unix) { return resolved; diff --git a/crates/pet-utils/tests/sys_prefix_test.rs b/crates/pet-utils/tests/sys_prefix_test.rs index 64b917dc..c7e39bc1 100644 --- a/crates/pet-utils/tests/sys_prefix_test.rs +++ b/crates/pet-utils/tests/sys_prefix_test.rs @@ -74,4 +74,12 @@ fn version_from_header_files() { let path: PathBuf = resolve_test_path(&["unix", "headers", "python3.9.9", "bin"]).into(); let version = SysPrefix::get_version(&path).unwrap(); assert_eq!(version, "3.9.9"); + + let path: PathBuf = resolve_test_path(&["unix", "headers", "python3.10-dev", "bin"]).into(); + let version = SysPrefix::get_version(&path).unwrap(); + assert_eq!(version, "3.10.14+"); + + let path: PathBuf = resolve_test_path(&["unix", "headers", "python3.13", "bin"]).into(); + let version = SysPrefix::get_version(&path).unwrap(); + assert_eq!(version, "3.13.0a5"); } diff --git a/crates/pet-utils/tests/unix/headers/python3.10-dev/bin/python3 b/crates/pet-utils/tests/unix/headers/python3.10-dev/bin/python3 new file mode 100644 index 00000000..e69de29b diff --git a/crates/pet-utils/tests/unix/headers/python3.10-dev/bin/python3.9.9 b/crates/pet-utils/tests/unix/headers/python3.10-dev/bin/python3.9.9 new file mode 100644 index 00000000..e69de29b diff --git a/crates/pet-utils/tests/unix/headers/python3.10-dev/include/python3.10/patchlevel.h b/crates/pet-utils/tests/unix/headers/python3.10-dev/include/python3.10/patchlevel.h new file mode 100644 index 00000000..df9bbf6a --- /dev/null +++ b/crates/pet-utils/tests/unix/headers/python3.10-dev/include/python3.10/patchlevel.h @@ -0,0 +1,35 @@ + +/* Python version identification scheme. + + When the major or minor version changes, the VERSION variable in + configure.ac must also be changed. + + There is also (independent) API version information in modsupport.h. +*/ + +/* Values for PY_RELEASE_LEVEL */ +#define PY_RELEASE_LEVEL_ALPHA 0xA +#define PY_RELEASE_LEVEL_BETA 0xB +#define PY_RELEASE_LEVEL_GAMMA 0xC /* For release candidates */ +#define PY_RELEASE_LEVEL_FINAL 0xF /* Serial should be 0 here */ + /* Higher for patch releases */ + +/* Version parsed out into numeric values */ +/*--start constants--*/ +#define PY_MAJOR_VERSION 3 +#define PY_MINOR_VERSION 10 +#define PY_MICRO_VERSION 14 +#define PY_RELEASE_LEVEL PY_RELEASE_LEVEL_FINAL +#define PY_RELEASE_SERIAL 0 + +/* Version as a string */ +#define PY_VERSION "3.10.14+" +/*--end constants--*/ + +/* Version as a single 4-byte hex number, e.g. 0x010502B2 == 1.5.2b2. + Use this for numeric comparisons, e.g. #if PY_VERSION_HEX >= ... */ +#define PY_VERSION_HEX ((PY_MAJOR_VERSION << 24) | \ + (PY_MINOR_VERSION << 16) | \ + (PY_MICRO_VERSION << 8) | \ + (PY_RELEASE_LEVEL << 4) | \ + (PY_RELEASE_SERIAL << 0)) diff --git a/crates/pet-utils/tests/unix/headers/python3.13/bin/python3 b/crates/pet-utils/tests/unix/headers/python3.13/bin/python3 new file mode 100644 index 00000000..e69de29b diff --git a/crates/pet-utils/tests/unix/headers/python3.13/bin/python3.9.9 b/crates/pet-utils/tests/unix/headers/python3.13/bin/python3.9.9 new file mode 100644 index 00000000..e69de29b diff --git a/crates/pet-utils/tests/unix/headers/python3.13/include/python3.13/patchlevel.h b/crates/pet-utils/tests/unix/headers/python3.13/include/python3.13/patchlevel.h new file mode 100644 index 00000000..ea22ad28 --- /dev/null +++ b/crates/pet-utils/tests/unix/headers/python3.13/include/python3.13/patchlevel.h @@ -0,0 +1,35 @@ + +/* Python version identification scheme. + + When the major or minor version changes, the VERSION variable in + configure.ac must also be changed. + + There is also (independent) API version information in modsupport.h. +*/ + +/* Values for PY_RELEASE_LEVEL */ +#define PY_RELEASE_LEVEL_ALPHA 0xA +#define PY_RELEASE_LEVEL_BETA 0xB +#define PY_RELEASE_LEVEL_GAMMA 0xC /* For release candidates */ +#define PY_RELEASE_LEVEL_FINAL 0xF /* Serial should be 0 here */ + /* Higher for patch releases */ + +/* Version parsed out into numeric values */ +/*--start constants--*/ +#define PY_MAJOR_VERSION 3 +#define PY_MINOR_VERSION 13 +#define PY_MICRO_VERSION 0 +#define PY_RELEASE_LEVEL PY_RELEASE_LEVEL_ALPHA +#define PY_RELEASE_SERIAL 5 + +/* Version as a string */ +#define PY_VERSION "3.13.0a5" +/*--end constants--*/ + +/* Version as a single 4-byte hex number, e.g. 0x010502B2 == 1.5.2b2. + Use this for numeric comparisons, e.g. #if PY_VERSION_HEX >= ... */ +#define PY_VERSION_HEX ((PY_MAJOR_VERSION << 24) | \ + (PY_MINOR_VERSION << 16) | \ + (PY_MICRO_VERSION << 8) | \ + (PY_RELEASE_LEVEL << 4) | \ + (PY_RELEASE_SERIAL << 0)) diff --git a/crates/pet/Cargo.toml b/crates/pet/Cargo.toml index 941b208a..1a590a8c 100644 --- a/crates/pet/Cargo.toml +++ b/crates/pet/Cargo.toml @@ -25,3 +25,12 @@ pet-pipenv = { path = "../pet-pipenv" } pet-global-virtualenvs = { path = "../pet-global-virtualenvs" } log = "0.4.21" clap = { version = "4.5.4", features = ["derive"] } + +[dev_dependencies] +serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0.93" +regex = "1.10.4" +lazy_static = "1.4.0" + +[features] +ci = [] diff --git a/crates/pet/src/lib.rs b/crates/pet/src/lib.rs index d1f73918..66783add 100644 --- a/crates/pet/src/lib.rs +++ b/crates/pet/src/lib.rs @@ -3,7 +3,7 @@ use pet_reporter::{self, jsonrpc, stdio}; -mod locators; +pub mod locators; pub fn find_and_report_envs_jsonrpc() { jsonrpc::initialize_logger(log::LevelFilter::Trace); diff --git a/crates/pet/src/locators.rs b/crates/pet/src/locators.rs index 8b10c5b6..a646bc75 100644 --- a/crates/pet/src/locators.rs +++ b/crates/pet/src/locators.rs @@ -28,7 +28,7 @@ pub fn find_and_report_envs(reporter: &dyn Reporter) { // 1. Find using known global locators. let mut threads = find_using_global_finders(); - // // Step 2: Search in some global locations for virtual envs. + // Step 2: Search in some global locations for virtual envs. threads.push(thread::spawn(find_in_global_virtual_env_dirs)); // Step 3: Finally find in the current PATH variable diff --git a/crates/pet/tests/ci_test.rs b/crates/pet/tests/ci_test.rs new file mode 100644 index 00000000..d7fd9cc5 --- /dev/null +++ b/crates/pet/tests/ci_test.rs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use common::resolve_test_path; +use lazy_static::lazy_static; +use pet_core::{ + arch::Architecture, + python_environment::{PythonEnvironment, PythonEnvironmentCategory}, +}; +use regex::Regex; +use serde::Deserialize; + +lazy_static! { + static ref PYTHON_VERSION: Regex = Regex::new("([\\d+\\.?]*).*") + .expect("error parsing Version regex for Python Version in test"); +} + +mod common; + +#[cfg(unix)] +#[cfg_attr(feature = "ci", test)] +#[allow(dead_code)] +// We should detect the conda install along with the base env +fn verify_validity_of_discovered_envs() { + use std::thread; + + use pet::locators; + use pet_reporter::{stdio, test}; + + stdio::initialize_logger(log::LevelFilter::Warn); + let reporter = test::create_reporter(); + locators::find_and_report_envs(&reporter); + + let environments = reporter + .reported_environments + .lock() + .unwrap() + .clone() + .into_values() + .collect::>(); + let mut threads = vec![]; + for environment in environments { + if environment.executable.is_none() { + continue; + } + threads.push(thread::spawn(move || { + verify_validity_of_interpreter_info(environment.clone()); + })); + } + for thread in threads { + thread.join().unwrap(); + } +} + +fn verify_validity_of_interpreter_info(environment: PythonEnvironment) { + let run_command = get_python_run_command(&environment); + let interpreter_info = get_python_interpreter_info(&run_command); + + // Home brew has too many syminks, unfortunately its not easy to test in CI. + if environment.category != PythonEnvironmentCategory::Homebrew { + let expected_executable = environment.executable.clone().unwrap(); + assert_eq!( + expected_executable.to_str().unwrap(), + interpreter_info.clone().executable, + "Executable mismatch for {:?}", + environment.clone() + ); + } + if let Some(prefix) = environment.clone().prefix { + assert_eq!( + prefix.to_str().unwrap(), + interpreter_info.clone().sysPrefix, + "Prefix mismatch for {:?}", + environment.clone() + ); + } + if let Some(arch) = environment.clone().arch { + let expected_arch = if interpreter_info.clone().is64Bit { + Architecture::X64 + } else { + Architecture::X86 + }; + assert_eq!( + arch, + expected_arch, + "Architecture mismatch for {:?}", + environment.clone() + ); + } + if let Some(version) = environment.clone().version { + let expected_version = &interpreter_info.clone().sysVersion; + let version = get_version(&version); + assert!( + expected_version.starts_with(&version), + "Version mismatch for (expected {:?} to start with {:?}) for {:?}", + expected_version, + version, + environment.clone() + ); + } +} + +#[allow(dead_code)] +fn get_conda_exe() -> &'static str { + // On CI we expect conda to be in the current path. + "conda" +} + +#[derive(Deserialize, Clone)] +struct InterpreterInfo { + #[allow(non_snake_case)] + sysPrefix: String, + executable: String, + #[allow(non_snake_case)] + sysVersion: String, + #[allow(non_snake_case)] + is64Bit: bool, + // #[allow(non_snake_case)] + // versionInfo: (u16, u16, u16, String, u16), +} + +fn get_python_run_command(env: &PythonEnvironment) -> Vec { + if env.clone().category == PythonEnvironmentCategory::Conda { + if env.executable.is_none() { + panic!("Conda environment without executable"); + } + let conda_exe = match env.manager.clone() { + Some(manager) => manager.executable.to_str().unwrap_or_default().to_string(), + None => get_conda_exe().to_string(), + }; + if let Some(name) = env.name.clone() { + return vec![ + conda_exe, + "run".to_string(), + "-n".to_string(), + name, + "python".to_string(), + ]; + } else if let Some(prefix) = env.prefix.clone() { + return vec![ + conda_exe, + "run".to_string(), + "-p".to_string(), + prefix.to_str().unwrap_or_default().to_string(), + "python".to_string(), + ]; + } else { + panic!("Conda environment without name or prefix") + } + } else { + vec![env + .executable + .clone() + .expect("Python environment without executable") + .to_str() + .unwrap() + .to_string()] + } +} + +fn get_python_interpreter_info(cli: &Vec) -> InterpreterInfo { + let mut cli = cli.clone(); + cli.push( + resolve_test_path(&["interpreterInfo.py"]) + .to_str() + .unwrap_or_default() + .to_string(), + ); + // Spawn `conda --version` to get the version of conda as a string + let output = std::process::Command::new(cli.first().unwrap()) + .args(&cli[1..]) + .output() + .expect(format!("Failed to execute command {:?}", cli).as_str()); + let output = String::from_utf8(output.stdout).unwrap(); + let output = output + .split_once("503bebe7-c838-4cea-a1bc-0f2963bcb657") + .unwrap() + .1; + let info: InterpreterInfo = serde_json::from_str(&output).unwrap(); + info +} + +fn get_version(value: &String) -> String { + // Regex to extract just the d.d.d version from the full version string + let captures = PYTHON_VERSION.captures(value).unwrap(); + let version = captures.get(1).unwrap().as_str().to_string(); + if version.ends_with('.') { + version[..version.len() - 1].to_string() + } else { + version + } +} diff --git a/crates/pet/tests/common.rs b/crates/pet/tests/common.rs new file mode 100644 index 00000000..2267530c --- /dev/null +++ b/crates/pet/tests/common.rs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::path::PathBuf; + +#[allow(dead_code)] +pub fn resolve_test_path(paths: &[&str]) -> PathBuf { + let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests"); + + paths.iter().for_each(|p| root.push(p)); + + root +} diff --git a/crates/pet/tests/interpreterInfo.py b/crates/pet/tests/interpreterInfo.py new file mode 100644 index 00000000..b6fc669b --- /dev/null +++ b/crates/pet/tests/interpreterInfo.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import sys + +obj = {} +obj["versionInfo"] = tuple(sys.version_info) +obj["sysPrefix"] = sys.prefix +obj["sysVersion"] = sys.version +obj["is64Bit"] = sys.maxsize > 2**32 +obj["executable"] = sys.executable + +# Everything after this is the information we need +print("503bebe7-c838-4cea-a1bc-0f2963bcb657") +print(json.dumps(obj))