Skip to content

Conda and Known Python Tests #23349

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,30 @@ jobs:
run: npm run test:functional
if: matrix.test-suite == 'functional'

native-tests:
name: Native Tests
# The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded.
runs-on: ${{ matrix.os }}
defaults:
run:
working-directory: ${{ env.special-working-directory }}
strategy:
fail-fast: false
matrix:
# We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used,
# macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case.
os: [ubuntu-latest, windows-latest]

steps:
- name: Checkout
uses: actions/checkout@v4
with:
path: ${{ env.special-working-directory-relative }}

- name: Native Locator tests
run: cargo test
working-directory: ${{ env.special-working-directory }}/native_locator

smoke-tests:
name: Smoke tests
# The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded.
Expand Down
8 changes: 7 additions & 1 deletion native_locator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ edition = "2021"
winreg = "0.52.0"

[dependencies]
serde = {version ="1.0.152", features = ["derive"]}
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93"
serde_repr = "0.1.10"
regex = "1.10.4"

[features]
test = []

[lib]
doctest = false
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It just disables docs validation. We don't have any tests, I don't think we need to explicitly disable it. Unless it causes issues with CI runs.

31 changes: 19 additions & 12 deletions native_locator/src/common_python.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

use crate::known;
use crate::messaging;
use crate::utils;
use std::env;
Expand All @@ -20,10 +21,10 @@ fn get_env_path(path: &str) -> Option<String> {
}
}

fn report_path_python(path: &str) {
fn report_path_python(dispatcher: &mut impl messaging::MessageDispatcher, path: &str) {
let version = utils::get_version(path);
let env_path = get_env_path(path);
messaging::send_message(messaging::PythonEnvironment::new(
dispatcher.send_message(messaging::PythonEnvironment::new(
"Python".to_string(),
vec![path.to_string()],
"System".to_string(),
Expand All @@ -33,20 +34,26 @@ fn report_path_python(path: &str) {
));
}

fn report_python_on_path() {
let bin = if cfg!(windows) {
"python.exe"
} else {
"python"
};
if let Ok(paths) = env::var("PATH") {
fn report_python_on_path(
dispatcher: &mut impl messaging::MessageDispatcher,
environment: &impl known::Environment,
) {
if let Some(paths) = environment.get_env_var("PATH".to_string()) {
let bin = if cfg!(windows) {
"python.exe"
} else {
"python"
};
env::split_paths(&paths)
.map(|p| p.join(bin))
.filter(|p| p.exists())
.for_each(|full_path| report_path_python(full_path.to_str().unwrap()));
.for_each(|full_path| report_path_python(dispatcher, full_path.to_str().unwrap()));
}
}

pub fn find_and_report() {
report_python_on_path();
pub fn find_and_report(
dispatcher: &mut impl messaging::MessageDispatcher,
environment: &impl known::Environment,
) {
report_python_on_path(dispatcher, environment);
}
81 changes: 47 additions & 34 deletions native_locator/src/conda.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ fn get_version_from_meta_json(json_file: &Path) -> Option<String> {
fn get_conda_package_json_path(any_path: &Path, package: &str) -> Option<PathBuf> {
let package_name = format!("{}-", package);
let conda_meta_path = get_conda_meta_path(any_path)?;

std::fs::read_dir(conda_meta_path).ok()?.find_map(|entry| {
let path = entry.ok()?.path();
let file_name = path.file_name()?.to_string_lossy();
Expand All @@ -97,6 +96,7 @@ fn get_conda_package_json_path(any_path: &Path, package: &str) -> Option<PathBuf
}

/// Checks if the `python` package is installed in the conda environment
#[allow(dead_code)]
pub fn is_python_conda_env(any_path: &Path) -> bool {
let conda_python_json_path = get_conda_package_json_path(any_path, "python");
match conda_python_json_path {
Expand Down Expand Up @@ -127,11 +127,9 @@ fn get_conda_bin_names() -> Vec<&'static str> {
}

/// Find the conda binary on the PATH environment variable
fn find_conda_binary_on_path() -> Option<PathBuf> {
let paths = env::var("PATH").ok()?;
let paths = env::split_paths(&paths);
for path in paths {
let path = Path::new(&path);
fn find_conda_binary_on_path(environment: &impl known::Environment) -> Option<PathBuf> {
let paths = environment.get_env_var("PATH".to_string())?;
for path in env::split_paths(&paths) {
for bin in get_conda_bin_names() {
let conda_path = path.join(bin);
match std::fs::metadata(&conda_path) {
Expand Down Expand Up @@ -161,11 +159,13 @@ fn find_python_binary_path(env_path: &Path) -> Option<PathBuf> {
}

#[cfg(windows)]
fn get_known_conda_locations() -> Vec<PathBuf> {
let user_profile = env::var("USERPROFILE").unwrap();
let program_data = env::var("PROGRAMDATA").unwrap();
let all_user_profile = env::var("ALLUSERSPROFILE").unwrap();
let home_drive = env::var("HOMEDRIVE").unwrap();
fn get_known_conda_locations(environment: &impl known::Environment) -> Vec<PathBuf> {
let user_profile = environment.get_env_var("USERPROFILE".to_string()).unwrap();
let program_data = environment.get_env_var("PROGRAMDATA".to_string()).unwrap();
let all_user_profile = environment
.get_env_var("ALLUSERSPROFILE".to_string())
.unwrap();
let home_drive = environment.get_env_var("HOMEDRIVE".to_string()).unwrap();
let mut known_paths = vec![
Path::new(&user_profile).join("Anaconda3\\Scripts"),
Path::new(&program_data).join("Anaconda3\\Scripts"),
Expand All @@ -176,12 +176,12 @@ fn get_known_conda_locations() -> Vec<PathBuf> {
Path::new(&all_user_profile).join("Miniconda3\\Scripts"),
Path::new(&home_drive).join("Miniconda3\\Scripts"),
];
known_paths.append(&mut known::get_know_global_search_locations());
known_paths.append(&mut environment.get_know_global_search_locations());
known_paths
}

#[cfg(unix)]
fn get_known_conda_locations() -> Vec<PathBuf> {
fn get_known_conda_locations(environment: &impl known::Environment) -> Vec<PathBuf> {
let mut known_paths = vec![
PathBuf::from("/opt/anaconda3/bin"),
PathBuf::from("/opt/miniconda3/bin"),
Expand All @@ -202,14 +202,14 @@ fn get_known_conda_locations() -> Vec<PathBuf> {
PathBuf::from("/anaconda3/bin"),
PathBuf::from("/miniconda3/bin"),
];
known_paths.append(&mut known::get_know_global_search_locations());
known_paths.append(&mut environment.get_know_global_search_locations());
known_paths
}

/// Find conda binary in known locations
fn find_conda_binary_in_known_locations() -> Option<PathBuf> {
fn find_conda_binary_in_known_locations(environment: &impl known::Environment) -> Option<PathBuf> {
let conda_bin_names = get_conda_bin_names();
let known_locations = get_known_conda_locations();
let known_locations = get_known_conda_locations(environment);
for location in known_locations {
for bin in &conda_bin_names {
let conda_path = location.join(bin);
Expand All @@ -223,17 +223,17 @@ fn find_conda_binary_in_known_locations() -> Option<PathBuf> {
}

/// Find the conda binary on the system
pub fn find_conda_binary() -> Option<PathBuf> {
let conda_binary_on_path = find_conda_binary_on_path();
pub fn find_conda_binary(environment: &impl known::Environment) -> Option<PathBuf> {
let conda_binary_on_path = find_conda_binary_on_path(environment);
match conda_binary_on_path {
Some(conda_binary_on_path) => Some(conda_binary_on_path),
None => find_conda_binary_in_known_locations(),
None => find_conda_binary_in_known_locations(environment),
}
}

fn get_conda_envs_from_environment_txt() -> Vec<String> {
fn get_conda_envs_from_environment_txt(environment: &impl known::Environment) -> Vec<String> {
let mut envs = vec![];
let home = known::get_user_home();
let home = environment.get_user_home();
match home {
Some(home) => {
let home = Path::new(&home);
Expand All @@ -252,9 +252,12 @@ fn get_conda_envs_from_environment_txt() -> Vec<String> {
envs
}

fn get_known_env_locations(conda_bin: PathBuf) -> Vec<String> {
fn get_known_env_locations(
conda_bin: PathBuf,
environment: &impl known::Environment,
) -> Vec<String> {
let mut paths = vec![];
let home = known::get_user_home();
let home = environment.get_user_home();
match home {
Some(home) => {
let home = Path::new(&home);
Expand Down Expand Up @@ -284,9 +287,12 @@ fn get_known_env_locations(conda_bin: PathBuf) -> Vec<String> {
paths
}

fn get_conda_envs_from_known_env_locations(conda_bin: PathBuf) -> Vec<String> {
fn get_conda_envs_from_known_env_locations(
conda_bin: PathBuf,
environment: &impl known::Environment,
) -> Vec<String> {
let mut envs = vec![];
for location in get_known_env_locations(conda_bin) {
for location in get_known_env_locations(conda_bin, environment) {
if is_conda_environment(&Path::new(&location)) {
envs.push(location.to_string());
}
Expand Down Expand Up @@ -324,14 +330,18 @@ struct CondaEnv {
path: PathBuf,
}

fn get_distinct_conda_envs(conda_bin: PathBuf) -> Vec<CondaEnv> {
let mut envs = get_conda_envs_from_environment_txt();
let mut known_envs = get_conda_envs_from_known_env_locations(conda_bin.to_path_buf());
fn get_distinct_conda_envs(
conda_bin: PathBuf,
environment: &impl known::Environment,
) -> Vec<CondaEnv> {
let mut envs = get_conda_envs_from_environment_txt(environment);
let mut known_envs =
get_conda_envs_from_known_env_locations(conda_bin.to_path_buf(), environment);
envs.append(&mut known_envs);
envs.sort();
envs.dedup();

let locations = get_known_env_locations(conda_bin);
let locations = get_known_env_locations(conda_bin, environment);
let mut conda_envs = vec![];
for env in envs {
let env = Path::new(&env);
Expand Down Expand Up @@ -367,16 +377,19 @@ fn get_distinct_conda_envs(conda_bin: PathBuf) -> Vec<CondaEnv> {
conda_envs
}

pub fn find_and_report() {
let conda_binary = find_conda_binary();
pub fn find_and_report(
dispatcher: &mut impl messaging::MessageDispatcher,
environment: &impl known::Environment,
) {
let conda_binary = find_conda_binary(environment);
match conda_binary {
Some(conda_binary) => {
let params =
messaging::EnvManager::new(vec![conda_binary.to_string_lossy().to_string()], None);
let message = messaging::EnvManagerMessage::new(params);
messaging::send_message(message);
dispatcher.send_message(message);

let envs = get_distinct_conda_envs(conda_binary.to_path_buf());
let envs = get_distinct_conda_envs(conda_binary.to_path_buf(), environment);
for env in envs {
let executable = find_python_binary_path(Path::new(&env.path));
let params = messaging::PythonEnvironment::new(
Expand Down Expand Up @@ -407,7 +420,7 @@ pub fn find_and_report() {
Some(env.path.to_string_lossy().to_string()),
);
let message = messaging::PythonEnvironmentMessage::new(params);
messaging::send_message(message);
dispatcher.send_message(message);
}
}
None => (),
Expand Down
67 changes: 49 additions & 18 deletions native_locator/src/known.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,64 @@
// Licensed under the MIT License.
use std::{env, path::PathBuf};

pub trait Environment {
fn get_user_home(&self) -> Option<String>;
fn get_env_var(&self, key: String) -> Option<String>;
fn get_know_global_search_locations(&self) -> Vec<PathBuf>;
}

pub struct EnvironmentApi {}

#[cfg(windows)]
pub fn get_know_global_search_locations() -> Vec<PathBuf> {
vec![]
impl Environment for EnvironmentApi {
fn get_user_home(&self) -> Option<String> {
get_user_home()
}
fn get_env_var(&self, key: String) -> Option<String> {
get_env_var(key)
}
fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
vec![]
}
}

#[cfg(unix)]
pub fn get_know_global_search_locations() -> Vec<PathBuf> {
vec![
PathBuf::from("/usr/bin"),
PathBuf::from("/usr/local/bin"),
PathBuf::from("/bin"),
PathBuf::from("/home/bin"),
PathBuf::from("/sbin"),
PathBuf::from("/usr/sbin"),
PathBuf::from("/usr/local/sbin"),
PathBuf::from("/home/sbin"),
PathBuf::from("/opt"),
PathBuf::from("/opt/bin"),
PathBuf::from("/opt/sbin"),
PathBuf::from("/opt/homebrew/bin"),
]
impl Environment for EnvironmentApi {
fn get_user_home(&self) -> Option<String> {
get_user_home()
}
fn get_env_var(&self, key: String) -> Option<String> {
get_env_var(key)
}
fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
vec![
PathBuf::from("/usr/bin"),
PathBuf::from("/usr/local/bin"),
PathBuf::from("/bin"),
PathBuf::from("/home/bin"),
PathBuf::from("/sbin"),
PathBuf::from("/usr/sbin"),
PathBuf::from("/usr/local/sbin"),
PathBuf::from("/home/sbin"),
PathBuf::from("/opt"),
PathBuf::from("/opt/bin"),
PathBuf::from("/opt/sbin"),
PathBuf::from("/opt/homebrew/bin"),
]
}
}

pub fn get_user_home() -> Option<String> {
fn get_user_home() -> Option<String> {
let home = env::var("HOME").or_else(|_| env::var("USERPROFILE"));
match home {
Ok(home) => Some(home),
Err(_) => None,
}
}

fn get_env_var(key: String) -> Option<String> {
match env::var(key) {
Ok(path) => Some(path),
Err(_) => None,
}
}
9 changes: 9 additions & 0 deletions native_locator/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

pub mod messaging;
pub mod utils;
pub mod common_python;
pub mod logging;
pub mod conda;
pub mod known;
Loading
Loading