Skip to content

Commit 1affbc9

Browse files
authored
Support searching PATH, workspaces and some fixes (#41)
1 parent b691cc8 commit 1affbc9

File tree

46 files changed

+1079
-338
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1079
-338
lines changed

.github/workflows/pr-check.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ jobs:
133133
if: startsWith( matrix.os, 'ubuntu') || startsWith( matrix.os, 'macos')
134134
run: |
135135
pyenv install --list
136-
pyenv install 3.12.3 3.8.19
136+
pyenv install 3.12.4 3.8.19
137137
shell: bash
138138

139139
# pyenv-win install list has not updated for a while
@@ -161,15 +161,15 @@ jobs:
161161
run: cargo fetch
162162
shell: bash
163163

164-
- name: Run Tests
165-
run: cargo test --frozen --all-features -- --nocapture
166-
shell: bash
167-
168164
- name: Find Environments
169165
if: matrix.run_cli == 'yes'
170166
run: cargo run --release --target ${{ matrix.target }}
171167
shell: bash
172168

169+
- name: Run Tests
170+
run: cargo test --frozen --all-features -- --nocapture
171+
shell: bash
172+
173173
builds:
174174
name: Builds
175175
runs-on: ${{ matrix.os }}

.gitignore

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,9 @@ target/
1010
*.pdb
1111

1212
# Directory with generated environments (generally created on CI)
13-
tmp
14-
.DS_Store
13+
.DS_Store
14+
15+
# Testing directories
16+
.venv/
17+
tmp/
18+
temp/

Cargo.lock

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pet-conda/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ msvc_spectre_libs = { version = "0.1.1", features = ["error"] }
99
[dependencies]
1010
pet-fs = { path = "../pet-fs" }
1111
pet-python-utils = { path = "../pet-python-utils" }
12+
pet-virtualenv = { path = "../pet-virtualenv" }
1213
serde = { version = "1.0.152", features = ["derive"] }
1314
serde_json = "1.0.93"
1415
lazy_static = "1.4.0"

crates/pet-conda/src/conda_info.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use log::{error, trace, warn};
5+
use pet_fs::path::resolve_symlink;
6+
use std::path::PathBuf;
7+
8+
#[derive(Debug, serde::Deserialize)]
9+
pub struct CondaInfo {
10+
pub executable: PathBuf,
11+
pub envs: Vec<PathBuf>,
12+
pub conda_prefix: PathBuf,
13+
pub conda_version: String,
14+
pub envs_dirs: Vec<PathBuf>,
15+
}
16+
17+
#[derive(Debug, serde::Deserialize)]
18+
pub struct CondaInfoJson {
19+
pub envs: Option<Vec<PathBuf>>,
20+
pub conda_prefix: Option<PathBuf>,
21+
pub conda_version: Option<String>,
22+
pub envs_dirs: Option<Vec<PathBuf>>,
23+
}
24+
25+
impl CondaInfo {
26+
pub fn from(executable: Option<PathBuf>) -> Option<CondaInfo> {
27+
// let using_default = executable.is_none() || executable == Some("conda".into());
28+
// Possible we got a symlink to the conda exe, first try to resolve that.
29+
let executable = if cfg!(windows) {
30+
executable.clone().unwrap_or("conda".into())
31+
} else {
32+
let executable = executable.unwrap_or("conda".into());
33+
resolve_symlink(&executable).unwrap_or(executable)
34+
};
35+
36+
let result = std::process::Command::new(&executable)
37+
.arg("info")
38+
.arg("--json")
39+
.output();
40+
trace!("Executing Conda: {:?} info --json", executable);
41+
match result {
42+
Ok(output) => {
43+
if output.status.success() {
44+
let output = String::from_utf8_lossy(&output.stdout).to_string();
45+
match serde_json::from_str::<CondaInfoJson>(output.trim()) {
46+
Ok(info) => {
47+
let info = CondaInfo {
48+
executable: executable.clone(),
49+
envs: info.envs.unwrap_or_default().drain(..).collect(),
50+
conda_prefix: info.conda_prefix.unwrap_or_default(),
51+
conda_version: info.conda_version.unwrap_or_default(),
52+
envs_dirs: info.envs_dirs.unwrap_or_default().drain(..).collect(),
53+
};
54+
Some(info)
55+
}
56+
Err(err) => {
57+
error!(
58+
"Conda Execution for {:?} produced an output {:?} that could not be parsed as JSON",
59+
executable, err,
60+
);
61+
None
62+
}
63+
}
64+
} else {
65+
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
66+
// No point logging the message if conda is not installed or a custom conda exe wasn't provided.
67+
if executable.to_string_lossy() != "conda" {
68+
warn!(
69+
"Failed to get conda info using {:?} ({:?}) {}",
70+
executable,
71+
output.status.code().unwrap_or_default(),
72+
stderr
73+
);
74+
}
75+
None
76+
}
77+
}
78+
Err(err) => {
79+
// No point logging the message if conda is not installed or a custom conda exe wasn't provided.
80+
if executable.to_string_lossy() != "conda" {
81+
warn!("Failed to execute conda info {:?}", err);
82+
}
83+
None
84+
}
85+
}
86+
}
87+
}

crates/pet-conda/src/environment_locations.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ use std::{
1414
thread,
1515
};
1616

17-
pub fn get_conda_environment_paths(env_vars: &EnvVariables) -> Vec<PathBuf> {
17+
pub fn get_conda_environment_paths(
18+
env_vars: &EnvVariables,
19+
additional_env_dirs: &Vec<PathBuf>,
20+
) -> Vec<PathBuf> {
1821
let mut env_paths = thread::scope(|s| {
1922
let mut envs = vec![];
2023
for thread in [
@@ -26,6 +29,7 @@ pub fn get_conda_environment_paths(env_vars: &EnvVariables) -> Vec<PathBuf> {
2629
}),
2730
s.spawn(|| get_conda_environment_paths_from_conda_rc(env_vars)),
2831
s.spawn(|| get_conda_environment_paths_from_known_paths(env_vars)),
32+
s.spawn(|| get_conda_environment_paths_from_additional_paths(additional_env_dirs)),
2933
s.spawn(|| get_known_conda_install_locations(env_vars)),
3034
] {
3135
if let Ok(mut env_paths) = thread.join() {
@@ -102,6 +106,26 @@ fn get_conda_environment_paths_from_known_paths(env_vars: &EnvVariables) -> Vec<
102106
env_paths
103107
}
104108

109+
fn get_conda_environment_paths_from_additional_paths(
110+
additional_env_dirs: &Vec<PathBuf>,
111+
) -> Vec<PathBuf> {
112+
let mut env_paths: Vec<PathBuf> = vec![];
113+
for path in additional_env_dirs {
114+
if let Ok(entries) = fs::read_dir(path) {
115+
for entry in entries.filter_map(Result::ok) {
116+
let path = entry.path();
117+
if let Ok(meta) = fs::metadata(&path) {
118+
if meta.is_dir() {
119+
env_paths.push(path);
120+
}
121+
}
122+
}
123+
}
124+
}
125+
env_paths.append(&mut additional_env_dirs.clone());
126+
env_paths
127+
}
128+
105129
pub fn get_environments(conda_dir: &Path) -> Vec<PathBuf> {
106130
let mut envs: Vec<PathBuf> = vec![];
107131

crates/pet-conda/src/lib.rs

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
use conda_info::CondaInfo;
45
use env_variables::EnvVariables;
56
use environment_locations::{get_conda_environment_paths, get_environments};
67
use environments::{get_conda_environment_info, CondaEnvironment};
7-
use log::error;
8+
use log::{error, warn};
89
use manager::CondaManager;
910
use pet_core::{
10-
os_environment::Environment, python_environment::PythonEnvironment, reporter::Reporter,
11+
os_environment::Environment,
12+
python_environment::{PythonEnvironment, PythonEnvironmentCategory},
13+
reporter::Reporter,
1114
Locator, LocatorResult,
1215
};
1316
use pet_python_utils::env::PythonEnv;
17+
use pet_virtualenv::is_virtualenv;
1418
use std::{
1519
collections::HashMap,
1620
path::{Path, PathBuf},
@@ -19,6 +23,7 @@ use std::{
1923
};
2024
use utils::is_conda_install;
2125

26+
mod conda_info;
2227
pub mod conda_rc;
2328
pub mod env_variables;
2429
pub mod environment_locations;
@@ -29,8 +34,11 @@ pub mod utils;
2934

3035
pub trait CondaLocator: Send + Sync {
3136
fn find_in(&self, path: &Path) -> Option<LocatorResult>;
37+
fn find_with_conda_executable(&self, conda_executable: Option<PathBuf>) -> Option<()>;
3238
}
3339
pub struct Conda {
40+
/// Directories where conda environments are found (env_dirs returned from `conda info --json`)
41+
pub env_dirs: Arc<Mutex<Vec<PathBuf>>>,
3442
pub environments: Arc<Mutex<HashMap<PathBuf, PythonEnvironment>>>,
3543
pub managers: Arc<Mutex<HashMap<PathBuf, CondaManager>>>,
3644
pub env_vars: EnvVariables,
@@ -39,6 +47,7 @@ pub struct Conda {
3947
impl Conda {
4048
pub fn from(env: &dyn Environment) -> Conda {
4149
Conda {
50+
env_dirs: Arc::new(Mutex::new(vec![])),
4251
environments: Arc::new(Mutex::new(HashMap::new())),
4352
managers: Arc::new(Mutex::new(HashMap::new())),
4453
env_vars: EnvVariables::from(env),
@@ -47,6 +56,40 @@ impl Conda {
4756
}
4857

4958
impl CondaLocator for Conda {
59+
fn find_with_conda_executable(&self, conda_executable: Option<PathBuf>) -> Option<()> {
60+
let info = CondaInfo::from(conda_executable)?;
61+
// Have we seen these envs, if yes, then nothing to do
62+
let environments = self.environments.lock().unwrap().clone();
63+
let mut new_envs = info
64+
.envs
65+
.clone()
66+
.into_iter()
67+
.filter(|p| !environments.contains_key(p))
68+
.collect::<Vec<PathBuf>>();
69+
if new_envs.is_empty() {
70+
return None;
71+
}
72+
73+
// Oh oh, we have new envs, lets find them.
74+
let manager = CondaManager::from_info(&info.executable, &info)?;
75+
for path in new_envs.iter() {
76+
let mgr = manager.clone();
77+
if let Some(env) = get_conda_environment_info(path, &Some(mgr.clone())) {
78+
warn!(
79+
"Found a conda env {:?} using the conda exe {:?}",
80+
env.prefix, info.executable
81+
);
82+
}
83+
}
84+
85+
// Also keep track of these directories for next time
86+
let mut env_dirs = self.env_dirs.lock().unwrap();
87+
env_dirs.append(&mut new_envs);
88+
89+
// Send telemetry
90+
91+
Some(())
92+
}
5093
fn find_in(&self, conda_dir: &Path) -> Option<LocatorResult> {
5194
if !is_conda_install(conda_dir) {
5295
return None;
@@ -104,7 +147,18 @@ impl Conda {
104147
}
105148

106149
impl Locator for Conda {
150+
fn supported_categories(&self) -> Vec<PythonEnvironmentCategory> {
151+
vec![PythonEnvironmentCategory::Conda]
152+
}
107153
fn from(&self, env: &PythonEnv) -> Option<PythonEnvironment> {
154+
// Assume we create a virtual env from a conda python install,
155+
// Doing this is a big no no, but people do this.
156+
// Then the exe in the virtual env bin will be a symlink to the homebrew python install.
157+
// Hence the first part of the condition will be true, but the second part will be false.
158+
if is_virtualenv(env) {
159+
return None;
160+
}
161+
108162
if let Some(ref path) = env.prefix {
109163
let mut environments = self.environments.lock().unwrap();
110164

@@ -151,9 +205,10 @@ impl Locator for Conda {
151205
drop(environments);
152206

153207
let env_vars = self.env_vars.clone();
208+
let additional_paths = self.env_dirs.lock().unwrap().clone();
154209
thread::scope(|s| {
155210
// 1. Get a list of all know conda environments file paths
156-
let possible_conda_envs = get_conda_environment_paths(&env_vars);
211+
let possible_conda_envs = get_conda_environment_paths(&env_vars, &additional_paths);
157212
for path in possible_conda_envs {
158213
s.spawn(move || {
159214
// 2. Get the details of the conda environment

0 commit comments

Comments
 (0)