Skip to content

Commit 940b874

Browse files
committed
fix: deduplicate Windows Conda paths by disk casing
Preserve authoritative Windows path casing when well-known and configured Conda aliases refer to the same installation. Add a regression covering lowercase environment aliases.\n\nFixes #518\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ab3d45c commit 940b874

4 files changed

Lines changed: 71 additions & 9 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,8 @@ env_logger = "0.10.2"
2121
yaml-rust2 = "0.8.1"
2222
rayon = "1.11.0"
2323

24+
[dev-dependencies]
25+
tempfile = "3.13"
26+
2427
[features]
2528
ci = []

‎crates/pet-conda/src/environment_locations.rs‎

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,25 @@ pub fn get_conda_envs_from_environment_txt(env_vars: &EnvVariables) -> Vec<PathB
319319
envs
320320
}
321321

322+
#[cfg(windows)]
323+
fn restore_existing_leaf_case(path: PathBuf) -> PathBuf {
324+
let Some(parent) = path.parent() else {
325+
return path;
326+
};
327+
let Some(file_name) = path.file_name() else {
328+
return path;
329+
};
330+
let Ok(entries) = fs::read_dir(parent) else {
331+
return path;
332+
};
333+
334+
entries
335+
.filter_map(Result::ok)
336+
.find(|entry| entry.file_name().eq_ignore_ascii_case(file_name))
337+
.map(|entry| entry.path())
338+
.unwrap_or(path)
339+
}
340+
322341
#[cfg(windows)]
323342
pub fn get_known_conda_install_locations(
324343
env_vars: &EnvVariables,
@@ -416,15 +435,6 @@ pub fn get_known_conda_install_locations(
416435
.join("conda"),
417436
);
418437
}
419-
known_paths.sort();
420-
known_paths.dedup();
421-
// Ensure the casing of the paths are correct.
422-
// Its possible the actual path is in a different case.
423-
// E.g. instead of C:\username\miniconda it might bt C:\username\Miniconda
424-
// We use lower cases above, but it could be in any case on disc.
425-
// We do not want to have duplicates in different cases.
426-
// & we'd like to preserve the case of the original path as on disc.
427-
known_paths = known_paths.iter().map(norm_case).collect();
428438
if let Some(conda_dir) = get_conda_dir_from_exe(conda_executable) {
429439
known_paths.push(conda_dir);
430440
}
@@ -436,6 +446,13 @@ pub fn get_known_conda_install_locations(
436446
if let Some(mamba_dir) = get_conda_dir_from_exe(&find_mamba_binary(env_vars)) {
437447
known_paths.push(mamba_dir);
438448
}
449+
450+
known_paths = known_paths
451+
.into_iter()
452+
.filter(|path| path.exists())
453+
.map(norm_case)
454+
.map(restore_existing_leaf_case)
455+
.collect();
439456
known_paths.sort();
440457
known_paths.dedup();
441458

‎crates/pet-conda/tests/environment_locations_test.rs‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,44 @@ fn skips_path_lookup_when_conda_executable_provided() {
205205
locations
206206
);
207207
}
208+
209+
#[cfg(windows)]
210+
#[test]
211+
fn deduplicates_windows_install_aliases_and_preserves_disk_casing() {
212+
use common::create_env_variables;
213+
use pet_conda::environment_locations::get_conda_environment_paths;
214+
use std::fs;
215+
216+
let temp_dir = tempfile::tempdir().expect("failed to create temporary test directory");
217+
let home = temp_dir.path();
218+
let install = home.join("Miniconda3");
219+
let child = install.join("envs").join("MyEnv");
220+
221+
fs::create_dir_all(install.join("conda-meta"))
222+
.expect("failed to create base conda-meta directory");
223+
fs::create_dir_all(install.join("condabin")).expect("failed to create base condabin directory");
224+
fs::create_dir_all(child.join("conda-meta"))
225+
.expect("failed to create child conda-meta directory");
226+
227+
let conda_state = home.join(".conda");
228+
fs::create_dir_all(&conda_state).expect("failed to create .conda directory");
229+
fs::write(
230+
conda_state.join("environments.txt"),
231+
format!("{}\n{}\n", install.display(), child.display()),
232+
)
233+
.expect("failed to write environments.txt");
234+
235+
let mut env = create_env_variables(home.to_path_buf(), home.to_path_buf());
236+
env.userprofile = Some(home.to_string_lossy().into_owned());
237+
238+
let environments = get_conda_environment_paths(&env, &None);
239+
let mut local_environments = environments
240+
.into_iter()
241+
.filter(|path| path.starts_with(home))
242+
.collect::<Vec<_>>();
243+
local_environments.sort();
244+
245+
let mut expected = vec![install, child];
246+
expected.sort();
247+
assert_eq!(local_environments, expected);
248+
}

0 commit comments

Comments
 (0)