Skip to content

Get conda env name from history file #217

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, 2025
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
115 changes: 87 additions & 28 deletions crates/pet-conda/src/environments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,8 @@ impl CondaEnvironment {

pub fn to_python_environment(&self, conda_manager: Option<EnvManager>) -> PythonEnvironment {
#[allow(unused_assignments)]
let mut name: Option<String> = None;
if is_conda_install(&self.prefix) {
name = Some("base".to_string());
} else {
name = self
.prefix
.file_name()
.map(|name| name.to_str().unwrap_or_default().to_string());
}
// if the conda install folder is parent of the env folder, then we can use named activation.
// E.g. conda env is = <conda install>/envs/<env name>
// Then we can use `<conda install>/bin/conda activate -n <env name>`
if let Some(conda_dir) = &self.conda_dir {
if !self.prefix.starts_with(conda_dir) {
name = None;
}
}
let name = get_conda_env_name(&self.prefix, &self.prefix, &self.conda_dir);

// This is a root env.
let builder = PythonEnvironmentBuilder::new(Some(PythonEnvironmentKind::Conda))
.executable(self.executable.clone())
Expand Down Expand Up @@ -143,29 +128,93 @@ pub fn get_conda_installation_used_to_create_conda_env(env_path: &Path) -> Optio

// First look for the conda-meta/history file in the environment folder.
// This could be a conda envirment (not root) but has `conda` installed in it.
if let Some(line) = get_conda_creation_line_from_history(env_path) {
// Sample lines
// # cmd: <conda install directory>\Scripts\conda-script.py create -n samlpe1
// # cmd: <conda install directory>\Scripts\conda-script.py create -p <full path>
// # cmd: /Users/donjayamanne/miniconda3/bin/conda create -n conda1
if let Some(conda_dir) = get_conda_dir_from_cmd(line) {
if is_conda_install(&conda_dir) {
return Some(conda_dir);
}
}
}

// Possible the env_path is the root conda install folder.
if is_conda_install(env_path) {
Some(env_path.to_path_buf())
} else {
None
}
}

pub fn get_conda_creation_line_from_history(env_path: &Path) -> Option<String> {
let conda_meta_history = env_path.join("conda-meta").join("history");
if let Ok(reader) = std::fs::read_to_string(conda_meta_history.clone()) {
if let Some(line) = reader.lines().map(|l| l.trim()).find(|l| {
l.to_lowercase().starts_with("# cmd:") && l.to_lowercase().contains(" create -")
}) {
trace!(
"Conda creation line for {:?} is from history file is {:?}",
env_path,
line
);
return Some(line.into());
}
}

None
}

fn get_conda_env_name(
env_path: &Path,
prefix: &Path,
conda_dir: &Option<PathBuf>,
) -> Option<String> {
let mut name: Option<String>;
if is_conda_install(prefix) {
name = Some("base".to_string());
} else {
name = prefix
.file_name()
.map(|name| name.to_str().unwrap_or_default().to_string());
}
// if the conda install folder is parent of the env folder, then we can use named activation.
// E.g. conda env is = <conda install>/envs/<env name>
// Then we can use `<conda install>/bin/conda activate -n <env name>`
if let Some(conda_dir) = conda_dir {
if !prefix.starts_with(conda_dir) {
name = get_conda_env_name_from_history_file(env_path, prefix);
}
}

name
}

/**
* The conda-meta/history file in conda environments contain the command used to create the conda environment.
* And example is `# cmd: <conda install directory>\Scripts\conda-script.py create -n sample``
* And example is `# cmd: conda create -n sample``
*
* This function returns the name of the conda environment.
*/
fn get_conda_env_name_from_history_file(env_path: &Path, prefix: &Path) -> Option<String> {
let name = prefix
.file_name()
.map(|name| name.to_str().unwrap_or_default().to_string());

if let Some(name) = name {
if let Some(line) = get_conda_creation_line_from_history(env_path) {
// Sample lines
// # cmd: <conda install directory>\Scripts\conda-script.py create -n samlpe1
// # cmd: <conda install directory>\Scripts\conda-script.py create -p <full path>
// # cmd: /Users/donjayamanne/miniconda3/bin/conda create -n conda1
if let Some(conda_dir) = get_conda_dir_from_cmd(line.into()) {
if is_conda_install(&conda_dir) {
return Some(conda_dir);
}
if is_conda_env_name_in_cmd(line, &name) {
return Some(name);
}
}
}

// Possible the env_path is the root conda install folder.
if is_conda_install(env_path) {
Some(env_path.to_path_buf())
} else {
None
}
None
}

fn get_conda_dir_from_cmd(cmd_line: String) -> Option<PathBuf> {
Expand Down Expand Up @@ -229,6 +278,16 @@ fn get_conda_dir_from_cmd(cmd_line: String) -> Option<PathBuf> {
None
}

fn is_conda_env_name_in_cmd(cmd_line: String, name: &str) -> bool {
// Sample lines
// # cmd: <conda install directory>\Scripts\conda-script.py create -n samlpe1
// # cmd: <conda install directory>\Scripts\conda-script.py create -p <full path>
// # cmd: /Users/donjayamanne/miniconda3/bin/conda create -n conda1
// # cmd_line: "# cmd: /usr/bin/conda create -p ./prefix-envs/.conda1 python=3.12 -y"
// Look for "-n <name>" in the command line
cmd_line.contains(format!("-n {:?}", name).as_str())
}

pub fn get_activation_command(
env: &CondaEnvironment,
manager: &EnvManager,
Expand Down
51 changes: 26 additions & 25 deletions crates/pet/tests/ci_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ fn verify_validity_of_discovered_envs() {
);

let environments = reporter.environments.lock().unwrap().clone();
let mut threads = vec![];
// let mut threads = vec![];
for environment in environments {
if environment.executable.is_none() {
continue;
Expand All @@ -101,32 +101,32 @@ fn verify_validity_of_discovered_envs() {
// 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);
}));
// threads.push(thread::spawn(move || {
verify_validity_of_interpreter_info(e);
// }));
let e = environment.clone();
threads.push(thread::spawn(move || {
for exe in &e.clone().symlinks.unwrap_or_default() {
// Verification 2:
// For each enviornment, given the executable verify we can get the exact same information
// Using the `locator.try_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.
verify_we_can_get_same_env_info_using_from_with_exe(exe, environment.clone());
// Verification 4 & 5:
// Similarly for each environment use resolve method and verify we get the exact same information.
verify_we_can_get_same_env_info_using_resolve_with_exe(exe, environment.clone());
// Verification 6:
// Given the exe, verify we can use the `find` method in JSON RPC to get the details, without spawning Python.
verify_we_can_get_same_env_info_using_find_with_exe(exe, environment.clone());
}
}));
}
for thread in threads {
thread.join().unwrap();
// threads.push(thread::spawn(move || {
for exe in &e.clone().symlinks.unwrap_or_default() {
// Verification 2:
// For each enviornment, given the executable verify we can get the exact same information
// Using the `locator.try_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.
verify_we_can_get_same_env_info_using_from_with_exe(exe, environment.clone());
// Verification 4 & 5:
// Similarly for each environment use resolve method and verify we get the exact same information.
verify_we_can_get_same_env_info_using_resolve_with_exe(exe, environment.clone());
// Verification 6:
// Given the exe, verify we can use the `find` method in JSON RPC to get the details, without spawning Python.
verify_we_can_get_same_env_info_using_find_with_exe(exe, environment.clone());
}
// }));
}
// for thread in threads {
// thread.join().unwrap();
// }
}

#[cfg(unix)]
Expand Down Expand Up @@ -767,6 +767,7 @@ fn get_python_interpreter_info(cli: &Vec<String>) -> InterpreterInfo {
.output()
.expect(format!("Failed to execute command {cli:?}").as_str());
let output = String::from_utf8(output.stdout).unwrap();
trace!("Get Interpreter Info: {:?} => {:?}", cli, output);
let output = output
.split_once("503bebe7-c838-4cea-a1bc-0f2963bcb657")
.unwrap()
Expand Down
Loading