@@ -191,6 +191,7 @@ fn get_conda_env_name(
191191 // if the conda install folder is parent of the env folder, then we can use named activation.
192192 // E.g. conda env is = <conda install>/envs/<env name>
193193 // Then we can use `<conda install>/bin/conda activate -n <env name>`
194+ //
194195 if let Some ( conda_dir) = conda_dir {
195196 if !prefix. starts_with ( conda_dir) {
196197 name = get_conda_env_name_from_history_file ( env_path, prefix) ;
@@ -226,6 +227,17 @@ fn get_conda_env_name_from_history_file(env_path: &Path, prefix: &Path) -> Optio
226227 None
227228}
228229
230+ fn is_conda_env_name_in_cmd ( cmd_line : String , name : & str ) -> bool {
231+ // Sample lines
232+ // # cmd: <conda install directory>\Scripts\conda-script.py create -n samlpe1
233+ // # cmd: <conda install directory>\Scripts\conda-script.py create -p <full path>
234+ // # cmd: /Users/donjayamanne/miniconda3/bin/conda create -n conda1
235+ // # cmd_line: "# cmd: /usr/bin/conda create -p ./prefix-envs/.conda1 python=3.12 -y"
236+ // Look for "-n <name>" in the command line
237+ cmd_line. contains ( format ! ( "-n {name}" ) . as_str ( ) )
238+ || cmd_line. contains ( format ! ( "--name {name}" ) . as_str ( ) )
239+ }
240+
229241fn get_conda_dir_from_cmd ( cmd_line : String ) -> Option < PathBuf > {
230242 // Sample lines
231243 // # cmd: <conda install directory>\Scripts\conda-script.py create -n samlpe1
@@ -288,17 +300,6 @@ fn get_conda_dir_from_cmd(cmd_line: String) -> Option<PathBuf> {
288300 None
289301}
290302
291- fn is_conda_env_name_in_cmd ( cmd_line : String , name : & str ) -> bool {
292- // Sample lines
293- // # cmd: <conda install directory>\Scripts\conda-script.py create -n samlpe1
294- // # cmd: <conda install directory>\Scripts\conda-script.py create -p <full path>
295- // # cmd: /Users/donjayamanne/miniconda3/bin/conda create -n conda1
296- // # cmd_line: "# cmd: /usr/bin/conda create -p ./prefix-envs/.conda1 python=3.12 -y"
297- // Look for "-n <name>" in the command line
298- cmd_line. contains ( format ! ( "-n {name}" ) . as_str ( ) )
299- || cmd_line. contains ( format ! ( "--name {name}" ) . as_str ( ) )
300- }
301-
302303pub fn get_activation_command (
303304 env : & CondaEnvironment ,
304305 manager : & EnvManager ,
@@ -379,4 +380,149 @@ mod tests {
379380 line = "# cmd: /Users/donjayamanne/.pyenv/versions/mambaforge-22.11.1-3/lib/python3.10/site-packages/conda/__main__.py create --yes -p .conda python=3.12" ;
380381 assert ! ( !is_conda_env_name_in_cmd( line. to_string( ) , ".conda" ) ) ;
381382 }
383+
384+ /// Test that external environments (not under conda_dir) created with --prefix
385+ /// return None for name, so activation uses path instead of name.
386+ /// This is the fix for issue #329.
387+ #[ test]
388+ fn external_path_based_env_returns_none_name ( ) {
389+ // Create a temp directory simulating an external path-based conda env
390+ let temp_dir = std:: env:: temp_dir ( ) . join ( "pet_test_external_path_env" ) ;
391+ let conda_meta_dir = temp_dir. join ( ".conda" ) . join ( "conda-meta" ) ;
392+ std:: fs:: create_dir_all ( & conda_meta_dir) . unwrap ( ) ;
393+
394+ // Write a history file showing the env was created with --prefix (path-based)
395+ let history_file = conda_meta_dir. join ( "history" ) ;
396+ std:: fs:: write (
397+ & history_file,
398+ "# cmd: /usr/bin/conda create --yes --prefix .conda python=3.12\n " ,
399+ )
400+ . unwrap ( ) ;
401+
402+ let env_path = temp_dir. join ( ".conda" ) ;
403+ // conda_dir is known but env is NOT under it (external environment)
404+ let conda_dir = Some ( std:: path:: PathBuf :: from ( "/some/other/conda" ) ) ;
405+
406+ let name = get_conda_env_name ( & env_path, & env_path, & conda_dir) ;
407+ assert ! (
408+ name. is_none( ) ,
409+ "Path-based external env should return None for name, got {:?}" ,
410+ name
411+ ) ;
412+
413+ // Cleanup
414+ let _ = std:: fs:: remove_dir_all ( & temp_dir) ;
415+ }
416+
417+ /// Test that external environments (not under conda_dir) created with -n
418+ /// return the name for name-based activation, but only if the folder name matches.
419+ #[ test]
420+ fn external_name_based_env_returns_name ( ) {
421+ // Create a temp directory simulating an external name-based conda env
422+ let temp_dir = std:: env:: temp_dir ( ) . join ( "pet_test_external_name_env" ) ;
423+ let conda_meta_dir = temp_dir. join ( "myenv" ) . join ( "conda-meta" ) ;
424+ std:: fs:: create_dir_all ( & conda_meta_dir) . unwrap ( ) ;
425+
426+ // Write a history file showing the env was created with -n myenv (name-based)
427+ // Note: the folder name "myenv" matches the -n argument "myenv"
428+ let history_file = conda_meta_dir. join ( "history" ) ;
429+ std:: fs:: write (
430+ & history_file,
431+ "# cmd: /usr/bin/conda create -n myenv python=3.12\n " ,
432+ )
433+ . unwrap ( ) ;
434+
435+ let env_path = temp_dir. join ( "myenv" ) ;
436+ // conda_dir is known but env is NOT under it (external environment)
437+ let conda_dir = Some ( std:: path:: PathBuf :: from ( "/some/other/conda" ) ) ;
438+
439+ let name = get_conda_env_name ( & env_path, & env_path, & conda_dir) ;
440+ assert_eq ! (
441+ name,
442+ Some ( "myenv" . to_string( ) ) ,
443+ "Name-based external env should return the name when folder matches"
444+ ) ;
445+
446+ // Cleanup
447+ let _ = std:: fs:: remove_dir_all ( & temp_dir) ;
448+ }
449+
450+ /// Test that environments under conda_dir/envs/ return the folder name.
451+ /// This is the most common case for named conda environments.
452+ #[ test]
453+ fn env_under_conda_dir_returns_folder_name ( ) {
454+ // Create a temp directory simulating conda_dir/envs/myenv structure
455+ let temp_dir = std:: env:: temp_dir ( ) . join ( "pet_test_env_under_conda" ) ;
456+ let conda_dir = temp_dir. join ( "miniconda3" ) ;
457+ let env_path = conda_dir. join ( "envs" ) . join ( "myenv" ) ;
458+ let conda_meta_dir = env_path. join ( "conda-meta" ) ;
459+ std:: fs:: create_dir_all ( & conda_meta_dir) . unwrap ( ) ;
460+
461+ // When env is under conda_dir/envs/, name should be the folder name
462+ let name = get_conda_env_name ( & env_path, & env_path, & Some ( conda_dir) ) ;
463+ assert_eq ! (
464+ name,
465+ Some ( "myenv" . to_string( ) ) ,
466+ "Env under conda_dir/envs/ should return folder name"
467+ ) ;
468+
469+ // Cleanup
470+ let _ = std:: fs:: remove_dir_all ( & temp_dir) ;
471+ }
472+
473+ /// Test that external env with no history file returns None for name.
474+ /// This ensures safe path-based activation when we can't determine how it was created.
475+ #[ test]
476+ fn external_env_without_history_returns_none_name ( ) {
477+ // Create a temp directory simulating an external conda env without history
478+ let temp_dir = std:: env:: temp_dir ( ) . join ( "pet_test_external_no_history" ) ;
479+ let conda_meta_dir = temp_dir. join ( "myenv" ) . join ( "conda-meta" ) ;
480+ std:: fs:: create_dir_all ( & conda_meta_dir) . unwrap ( ) ;
481+ // Note: NOT creating a history file
482+
483+ let env_path = temp_dir. join ( "myenv" ) ;
484+ // conda_dir is known but env is NOT under it (external environment)
485+ let conda_dir = Some ( std:: path:: PathBuf :: from ( "/some/other/conda" ) ) ;
486+
487+ let name = get_conda_env_name ( & env_path, & env_path, & conda_dir) ;
488+ assert ! (
489+ name. is_none( ) ,
490+ "External env without history should return None for safe path-based activation, got {:?}" ,
491+ name
492+ ) ;
493+
494+ // Cleanup
495+ let _ = std:: fs:: remove_dir_all ( & temp_dir) ;
496+ }
497+
498+ /// Test that external env with history but folder name doesn't match -n argument returns None.
499+ /// This prevents wrong activation when env was moved/renamed after creation.
500+ #[ test]
501+ fn external_env_with_mismatched_name_returns_none ( ) {
502+ // Create a temp directory simulating an external conda env
503+ let temp_dir = std:: env:: temp_dir ( ) . join ( "pet_test_external_mismatch" ) ;
504+ // Folder is named "renamed_env" but was created with -n "original_name"
505+ let conda_meta_dir = temp_dir. join ( "renamed_env" ) . join ( "conda-meta" ) ;
506+ std:: fs:: create_dir_all ( & conda_meta_dir) . unwrap ( ) ;
507+
508+ let history_file = conda_meta_dir. join ( "history" ) ;
509+ std:: fs:: write (
510+ & history_file,
511+ "# cmd: /usr/bin/conda create -n original_name python=3.12\n " ,
512+ )
513+ . unwrap ( ) ;
514+
515+ let env_path = temp_dir. join ( "renamed_env" ) ;
516+ let conda_dir = Some ( std:: path:: PathBuf :: from ( "/some/other/conda" ) ) ;
517+
518+ let name = get_conda_env_name ( & env_path, & env_path, & conda_dir) ;
519+ assert ! (
520+ name. is_none( ) ,
521+ "External env with mismatched name should return None, got {:?}" ,
522+ name
523+ ) ;
524+
525+ // Cleanup
526+ let _ = std:: fs:: remove_dir_all ( & temp_dir) ;
527+ }
382528}
0 commit comments