1#![deny(missing_docs)]
2
3#[cfg(target_family = "unix")]
7use std::io::Write;
8use std::{
9 collections::HashMap,
10 ffi::OsStr,
11 path::{Path, PathBuf},
12 process::ExitStatus,
13};
14
15#[cfg(target_family = "unix")]
16use anyhow::{Context, Result};
17use fs_err as fs;
18use indexmap::IndexMap;
19use itertools::Itertools;
20use rattler_conda_types::Platform;
21#[cfg(target_family = "unix")]
22use rattler_pty::unix::PtySession;
23
24use crate::shell::{Shell, ShellError, ShellScript};
25
26const ENV_START_SEPARATOR: &str = "____RATTLER_ENV_START____";
27
28#[derive(Default, Clone)]
30pub enum PathModificationBehavior {
31 #[default]
33 Replace,
34 Append,
36 Prepend,
38}
39
40#[derive(Default, Clone)]
45pub struct ActivationVariables {
46 pub conda_prefix: Option<PathBuf>,
49
50 pub path: Option<Vec<PathBuf>>,
53
54 pub path_modification_behavior: PathModificationBehavior,
56
57 pub current_env: HashMap<String, String>,
59}
60
61impl ActivationVariables {
62 pub fn from_env() -> Result<Self, std::env::VarError> {
65 let current_env: HashMap<String, String> = std::env::vars().collect();
67
68 Ok(Self {
69 conda_prefix: current_env.get("CONDA_PREFIX").map(PathBuf::from),
70 path: None,
71 path_modification_behavior: PathModificationBehavior::Prepend,
72 current_env,
73 })
74 }
75}
76
77#[derive(Debug)]
81pub struct Activator<T: Shell + 'static> {
82 pub target_prefix: PathBuf,
84
85 pub shell_type: T,
87
88 pub paths: Vec<PathBuf>,
90
91 pub activation_scripts: Vec<PathBuf>,
93
94 pub deactivation_scripts: Vec<PathBuf>,
96
97 pub env_vars: IndexMap<String, String>,
100
101 pub post_activation_env_vars: IndexMap<String, String>,
104
105 pub platform: Platform,
107}
108
109fn collect_scripts<T: Shell>(path: &Path, shell_type: &T) -> Result<Vec<PathBuf>, std::io::Error> {
127 if !path.exists() {
129 return Ok(vec![]);
130 }
131
132 let paths = fs::read_dir(path)?;
133
134 let mut scripts = paths
135 .into_iter()
136 .filter_map(std::result::Result::ok)
137 .map(|r| r.path())
138 .filter(|path| shell_type.can_run_script(path))
139 .collect::<Vec<_>>();
140
141 scripts.sort();
142
143 Ok(scripts)
144}
145
146#[derive(thiserror::Error, Debug)]
148pub enum ActivationError {
149 #[error(transparent)]
151 IoError(#[from] std::io::Error),
152
153 #[error(transparent)]
155 ShellError(#[from] ShellError),
156
157 #[error("Invalid json for environment vars: {0} in file {1:?}")]
159 InvalidEnvVarFileJson(serde_json::Error, PathBuf),
160
161 #[error("Malformed JSON: not a plain JSON object in file {file:?}")]
164 InvalidEnvVarFileJsonNoObject {
165 file: PathBuf,
167 },
168
169 #[error("Malformed JSON: file does not contain JSON object at key env_vars in file {file:?}")]
171 InvalidEnvVarFileStateFile {
172 file: PathBuf,
174 },
175
176 #[error("Failed to write activation script to file {0}")]
178 FailedToWriteActivationScript(#[from] std::fmt::Error),
179
180 #[error("Failed to run activation script (status: {status})")]
182 FailedToRunActivationScript {
183 script: String,
185
186 stdout: String,
188
189 stderr: String,
191
192 status: ExitStatus,
194 },
195}
196
197fn collect_env_vars(prefix: &Path) -> Result<IndexMap<String, String>, ActivationError> {
215 let state_file = prefix.join("conda-meta/state");
216 let pkg_env_var_dir = prefix.join("etc/conda/env_vars.d");
217 let mut env_vars = IndexMap::new();
218
219 if pkg_env_var_dir.exists() {
220 let env_var_files = pkg_env_var_dir.read_dir()?;
221
222 let mut env_var_files = env_var_files
223 .into_iter()
224 .filter_map(std::result::Result::ok)
225 .map(|e| e.path())
226 .filter(|path| path.is_file())
227 .collect::<Vec<_>>();
228
229 env_var_files.sort();
231
232 let env_var_json_files = env_var_files
233 .iter()
234 .map(|path| {
235 fs::read_to_string(path)?
236 .parse::<serde_json::Value>()
237 .map_err(|e| ActivationError::InvalidEnvVarFileJson(e, path.clone()))
238 })
239 .collect::<Result<Vec<serde_json::Value>, ActivationError>>()?;
240
241 for (env_var_json, env_var_file) in env_var_json_files.iter().zip(env_var_files.iter()) {
242 let env_var_json = env_var_json.as_object().ok_or_else(|| {
243 ActivationError::InvalidEnvVarFileJsonNoObject {
244 file: pkg_env_var_dir.clone(),
245 }
246 })?;
247
248 for (key, value) in env_var_json {
249 if let Some(value) = value.as_str() {
250 env_vars.insert(key.clone(), value.to_string());
251 } else {
252 tracing::warn!(
253 "WARNING: environment variable {key} has no string value (path: {env_var_file:?})"
254 );
255 }
256 }
257 }
258 }
259
260 if state_file.exists() {
261 let state_json = fs::read_to_string(&state_file)?;
262
263 let state_json: serde_json::Value = serde_json::from_str(&state_json)
266 .map_err(|e| ActivationError::InvalidEnvVarFileJson(e, state_file.clone()))?;
267
268 let state_env_vars = state_json["env_vars"].as_object().ok_or_else(|| {
269 ActivationError::InvalidEnvVarFileStateFile {
270 file: state_file.clone(),
271 }
272 })?;
273
274 for (key, value) in state_env_vars {
275 if state_env_vars.contains_key(key) {
276 tracing::warn!(
277 "WARNING: environment variable {key} already defined in packages (path: {state_file:?})"
278 );
279 }
280
281 if let Some(value) = value.as_str() {
282 env_vars.insert(key.to_uppercase(), value.to_string());
283 } else {
284 tracing::warn!(
285 "WARNING: environment variable {key} has no string value (path: {state_file:?})"
286 );
287 }
288 }
289 }
290 Ok(env_vars)
291}
292
293pub fn prefix_path_entries(prefix: &Path, platform: &Platform) -> Vec<PathBuf> {
304 if platform.is_windows() {
305 vec![
306 prefix.to_path_buf(),
307 prefix.join("Library/mingw-w64/bin"),
308 prefix.join("Library/usr/bin"),
309 prefix.join("Library/bin"),
310 prefix.join("Scripts"),
311 prefix.join("bin"),
312 ]
313 } else {
314 vec![prefix.join("bin")]
315 }
316}
317
318pub struct ActivationResult<T: Shell + 'static> {
323 pub script: ShellScript<T>,
327 pub path: Vec<PathBuf>,
329}
330
331impl<T: Shell + Clone> Activator<T> {
332 fn unique_env_keys(&self) -> impl Iterator<Item = &str> {
334 self.env_vars
335 .keys()
336 .chain(self.post_activation_env_vars.keys())
337 .map(String::as_str)
338 .unique()
339 }
340
341 pub fn from_path(
368 path: &Path,
369 shell_type: T,
370 platform: Platform,
371 ) -> Result<Activator<T>, ActivationError> {
372 let activation_scripts = collect_scripts(&path.join("etc/conda/activate.d"), &shell_type)?;
373
374 let deactivation_scripts =
375 collect_scripts(&path.join("etc/conda/deactivate.d"), &shell_type)?;
376
377 let env_vars = collect_env_vars(path)?;
378
379 let paths = prefix_path_entries(path, &platform);
380
381 Ok(Activator {
382 target_prefix: path.to_path_buf(),
383 shell_type,
384 paths,
385 activation_scripts,
386 deactivation_scripts,
387 env_vars,
388 post_activation_env_vars: IndexMap::new(),
389 platform,
390 })
391 }
392
393 #[cfg(target_family = "unix")]
402 #[allow(dead_code)]
403 async fn start_unix_shell<T_: Shell + Copy + 'static>(
404 shell: T_,
405 args: Vec<&str>,
406 env: &HashMap<String, String>,
407 prompt: String,
408 ) -> Result<Option<i32>> {
409 const DONE_STR: &str = "RATTLER_SHELL_ACTIVATION_DONE";
410 let mut temp_file = tempfile::Builder::new()
412 .prefix("rattler_env_")
413 .suffix(&format!(".{}", shell.extension()))
414 .rand_bytes(3)
415 .tempfile()
416 .context("Failed to create tmp file")?;
417
418 let mut shell_script = ShellScript::new(shell, Platform::current());
419 for (key, value) in env {
420 shell_script
421 .set_env_var(key, value)
422 .context("Failed to set env var")?;
423 }
424
425 shell_script.echo(DONE_STR)?;
426
427 temp_file
428 .write_all(shell_script.contents()?.as_bytes())
429 .context("Failed to write shell script content")?;
430
431 temp_file.write_all(prompt.as_bytes())?;
433
434 let mut command = std::process::Command::new(shell.executable());
435 command.args(args);
436
437 let mut source_command = " ".to_string();
439 shell
440 .run_script(&mut source_command, temp_file.path())
441 .context("Failed to run the script")?;
442
443 let source_command = source_command
445 .strip_suffix('\n')
446 .unwrap_or(source_command.as_str());
447
448 let mut process = PtySession::new(command)?;
450 process
451 .send_line(source_command)
452 .context("Failed to send command to shell")?;
453
454 process
455 .interact(Some(DONE_STR))
456 .context("Failed to interact with shell process")
457 }
458
459 pub fn activation(
463 &self,
464 variables: ActivationVariables,
465 ) -> Result<ActivationResult<T>, ActivationError> {
466 let mut script = ShellScript::new(self.shell_type.clone(), self.platform);
467
468 let mut path = variables.path.clone().unwrap_or_default();
469 if let Some(conda_prefix) = variables.conda_prefix {
470 let deactivate = Activator::from_path(
471 Path::new(&conda_prefix),
472 self.shell_type.clone(),
473 self.platform,
474 )?;
475
476 for (key, _) in &deactivate.env_vars {
477 script.unset_env_var(key)?;
478 }
479
480 for deactivation_script in &deactivate.deactivation_scripts {
481 script.run_script(deactivation_script)?;
482 }
483
484 path.retain(|x| !deactivate.paths.contains(x));
485 }
486
487 let path = [self.paths.clone(), path].concat();
489
490 script.set_path(path.as_slice(), variables.path_modification_behavior)?;
491
492 let shlvl = variables
496 .current_env
497 .get("CONDA_SHLVL")
498 .and_then(|s| s.parse::<i32>().ok())
499 .unwrap_or(0);
500
501 let new_shlvl = shlvl + 1;
503 script.set_env_var("CONDA_SHLVL", &new_shlvl.to_string())?;
504
505 if let Some(existing_prefix) = variables.current_env.get("CONDA_PREFIX") {
507 script.set_env_var(
508 &format!("CONDA_ENV_SHLVL_{new_shlvl}_CONDA_PREFIX"),
509 existing_prefix,
510 )?;
511 }
512
513 script.set_env_var("CONDA_PREFIX", &self.target_prefix.to_string_lossy())?;
515
516 script.apply_env_vars_with_backup(&variables.current_env, new_shlvl, &self.env_vars)?;
518
519 for activation_script in &self.activation_scripts {
520 script.run_script(activation_script)?;
521 }
522
523 script.apply_env_vars_with_backup(
525 &variables.current_env,
526 new_shlvl,
527 &self.post_activation_env_vars,
528 )?;
529
530 Ok(ActivationResult { script, path })
531 }
532
533 pub fn deactivation(
537 &self,
538 variables: ActivationVariables,
539 ) -> Result<ActivationResult<T>, ActivationError> {
540 let mut script = ShellScript::new(self.shell_type.clone(), self.platform);
541
542 let current_conda_shlvl = variables
544 .current_env
545 .get("CONDA_SHLVL")
546 .and_then(|s| s.parse::<i32>().ok());
547
548 match current_conda_shlvl {
549 None => {
550 script
552 .echo("Warning: CONDA_SHLVL not set. This may indicate a broken workflow.")?;
553 script.echo(
554 "Proceeding to unset conda variables without restoring previous values.",
555 )?;
556
557 for key in self.unique_env_keys() {
559 script.unset_env_var(key)?;
560 }
561 script.unset_env_var("CONDA_PREFIX")?;
562 script.unset_env_var("CONDA_SHLVL")?;
563 }
564 Some(current_level) if current_level <= 0 => {
565 script.echo("Warning: CONDA_SHLVL is zero or negative. This may indicate a broken workflow.")?;
567 script.echo(
568 "Proceeding to unset conda variables without restoring previous values.",
569 )?;
570
571 for key in self.unique_env_keys() {
573 script.unset_env_var(key)?;
574 }
575 script.unset_env_var("CONDA_PREFIX")?;
576 script.unset_env_var("CONDA_SHLVL")?;
577 }
578 Some(current_level) => {
579 for key in self.unique_env_keys() {
582 let backup_key = format!("CONDA_ENV_SHLVL_{current_level}_{key}");
583 script.restore_env_var(key, &backup_key)?;
584 }
585
586 let backup_prefix = format!("CONDA_ENV_SHLVL_{current_level}_CONDA_PREFIX");
588 script.restore_env_var("CONDA_PREFIX", &backup_prefix)?;
589
590 let prev_shlvl = current_level - 1;
591
592 if prev_shlvl == 0 {
594 script.unset_env_var("CONDA_SHLVL")?;
595 } else {
596 script.set_env_var("CONDA_SHLVL", &prev_shlvl.to_string())?;
597 }
598 }
599 }
600
601 for deactivation_script in &self.deactivation_scripts {
603 script.run_script(deactivation_script)?;
604 }
605
606 Ok(ActivationResult {
607 script,
608 path: Vec::new(),
609 })
610 }
611
612 pub fn run_activation(
618 &self,
619 variables: ActivationVariables,
620 environment: Option<HashMap<&OsStr, &OsStr>>,
621 ) -> Result<HashMap<String, String>, ActivationError> {
622 let activation_script = self.activation(variables)?.script;
623
624 let mut activation_detection_script =
628 ShellScript::new(self.shell_type.clone(), self.platform);
629 activation_detection_script
630 .print_env()?
631 .echo(ENV_START_SEPARATOR)?
632 .append_script(&activation_script)
633 .echo(ENV_START_SEPARATOR)?
634 .print_env()?;
635
636 let activation_script_dir = tempfile::TempDir::new()?;
638 let activation_script_path = activation_script_dir
639 .path()
640 .join(format!("activation.{}", self.shell_type.extension()));
641
642 fs::write(
645 &activation_script_path,
646 activation_detection_script.contents()?,
647 )?;
648 let mut activation_command = self
650 .shell_type
651 .create_run_script_command(&activation_script_path);
652
653 if let Some(environment) = environment.clone() {
655 activation_command.env_clear().envs(environment);
656 }
657
658 let activation_result = activation_command.output()?;
659
660 if !activation_result.status.success() {
661 return Err(ActivationError::FailedToRunActivationScript {
662 script: activation_detection_script.contents()?,
663 stdout: String::from_utf8_lossy(&activation_result.stdout).into_owned(),
664 stderr: String::from_utf8_lossy(&activation_result.stderr).into_owned(),
665 status: activation_result.status,
666 });
667 }
668
669 let stdout = String::from_utf8_lossy(&activation_result.stdout);
670 let (before_env, rest) = stdout
671 .split_once(ENV_START_SEPARATOR)
672 .unwrap_or(("", stdout.as_ref()));
673 let (_, after_env) = rest.rsplit_once(ENV_START_SEPARATOR).unwrap_or(("", ""));
674
675 let before_env = self.shell_type.parse_env(before_env);
677 let after_env = self.shell_type.parse_env(after_env);
678
679 Ok(after_env
681 .into_iter()
682 .filter(|(key, value)| before_env.get(key) != Some(value))
683 .filter(|(key, _)| !key.is_empty())
687 .map(|(key, value)| (key.to_owned(), value.to_owned()))
688 .collect())
689 }
690}
691
692#[cfg(test)]
693mod tests {
694 use std::{collections::BTreeMap, str::FromStr};
695
696 use tempdir::TempDir;
697
698 use super::*;
699 #[cfg(unix)]
700 use crate::activation::PathModificationBehavior;
701 use crate::shell::{self, native_path_to_unix, ShellEnum};
702
703 #[test]
704 #[cfg(unix)]
705 fn test_post_activation_env_vars_applied_after_scripts_bash() {
706 let temp_dir = TempDir::new("test_post_activation_env_vars").unwrap();
707
708 let activate_dir = temp_dir.path().join("etc/conda/activate.d");
710 fs::create_dir_all(&activate_dir).unwrap();
711 let script_path = activate_dir.join("script1.sh");
712 fs::write(&script_path, "# noop\n").unwrap();
713
714 let pre_env = IndexMap::from_iter([(String::from("A"), String::from("x"))]);
716
717 let post_env = IndexMap::from_iter([
719 (String::from("B"), String::from("y")),
720 (String::from("A"), String::from("z")),
721 ]);
722
723 let activator = Activator {
724 target_prefix: temp_dir.path().to_path_buf(),
725 shell_type: shell::Bash,
726 paths: vec![temp_dir.path().join("bin")],
727 activation_scripts: vec![script_path.clone()],
728 deactivation_scripts: vec![],
729 env_vars: pre_env,
730 post_activation_env_vars: post_env,
731 platform: Platform::current(),
732 };
733
734 let result = activator
735 .activation(ActivationVariables {
736 conda_prefix: None,
737 path: None,
738 path_modification_behavior: PathModificationBehavior::Prepend,
739 current_env: HashMap::new(),
740 })
741 .unwrap();
742
743 let mut contents = result.script.contents().unwrap();
744
745 let prefix = temp_dir.path().to_str().unwrap();
747 contents = contents.replace(prefix, "__PREFIX__");
748
749 let idx_pre_a = contents.find("export A=x").expect("missing pre env A=x");
751 let idx_run = contents
752 .find(". __PREFIX__/etc/conda/activate.d/script1.sh")
753 .expect("missing activation script run");
754 let idx_post_b = contents.find("export B=y").expect("missing post env B=y");
755 let idx_post_a = contents
756 .find("export A=z")
757 .expect("missing post override A=z");
758
759 assert!(
760 idx_pre_a < idx_run,
761 "pre env var should be before activation script"
762 );
763 assert!(
764 idx_run < idx_post_b,
765 "post env var should be after activation script"
766 );
767 assert!(
768 idx_run < idx_post_a,
769 "post override should be after activation script"
770 );
771 }
772
773 #[test]
774 fn test_collect_scripts() {
775 let tdir = TempDir::new("test").unwrap();
776
777 let path = tdir.path().join("etc/conda/activate.d/");
778 fs::create_dir_all(&path).unwrap();
779
780 let script1 = path.join("script1.sh");
781 let script2 = path.join("aaa.sh");
782 let script3 = path.join("xxx.sh");
783
784 fs::write(&script1, "").unwrap();
785 fs::write(&script2, "").unwrap();
786 fs::write(&script3, "").unwrap();
787
788 let shell_type = shell::Bash;
789
790 let scripts = collect_scripts(&path, &shell_type).unwrap();
791 assert_eq!(scripts.len(), 3);
792 assert_eq!(scripts[0], script2);
793 assert_eq!(scripts[1], script1);
794 assert_eq!(scripts[2], script3);
795
796 let activator = Activator::from_path(tdir.path(), shell_type, Platform::Osx64).unwrap();
797 assert_eq!(activator.activation_scripts.len(), 3);
798 assert_eq!(activator.activation_scripts[0], script2);
799 assert_eq!(activator.activation_scripts[1], script1);
800 assert_eq!(activator.activation_scripts[2], script3);
801 }
802
803 #[test]
804 fn test_collect_env_vars() {
805 let tdir = TempDir::new("test").unwrap();
806 let path = tdir.path().join("conda-meta/state");
807 fs::create_dir_all(path.parent().unwrap()).unwrap();
808
809 let quotes = r#"{"env_vars": {"Hallo": "myval", "TEST": "itsatest", "AAA": "abcdef"}}"#;
810 fs::write(&path, quotes).unwrap();
811
812 let env_vars = collect_env_vars(tdir.path()).unwrap();
813 assert_eq!(env_vars.len(), 3);
814
815 assert_eq!(env_vars["HALLO"], "myval");
816 assert_eq!(env_vars["TEST"], "itsatest");
817 assert_eq!(env_vars["AAA"], "abcdef");
818 }
819
820 #[test]
821 fn test_collect_env_vars_with_directory() {
822 let tdir = TempDir::new("test").unwrap();
823 let state_path = tdir.path().join("conda-meta/state");
824 fs::create_dir_all(state_path.parent().unwrap()).unwrap();
825
826 let content_pkg_1 = r#"{"VAR1": "someval", "TEST": "pkg1-test", "III": "super"}"#;
827 let content_pkg_2 = r#"{"VAR1": "overwrite1", "TEST2": "pkg2-test"}"#;
828
829 let env_var_d = tdir.path().join("etc/conda/env_vars.d");
830 fs::create_dir_all(&env_var_d).expect("Could not create env vars directory");
831
832 let pkg1 = env_var_d.join("pkg1.json");
833 let pkg2 = env_var_d.join("pkg2.json");
834
835 fs::write(pkg1, content_pkg_1).expect("could not write file");
836 fs::write(pkg2, content_pkg_2).expect("could not write file");
837
838 let quotes = r#"{"env_vars": {"Hallo": "myval", "TEST": "itsatest", "AAA": "abcdef"}}"#;
839 fs::write(&state_path, quotes).unwrap();
840
841 let env_vars = collect_env_vars(tdir.path()).expect("Could not load env vars");
842 assert_eq!(env_vars.len(), 6);
843
844 assert_eq!(env_vars["VAR1"], "overwrite1");
845 assert_eq!(env_vars["TEST"], "itsatest");
846 assert_eq!(env_vars["III"], "super");
847 assert_eq!(env_vars["TEST2"], "pkg2-test");
848 assert_eq!(env_vars["HALLO"], "myval");
849 assert_eq!(env_vars["AAA"], "abcdef");
850
851 let mut keys = env_vars.keys();
853 let key_vec = vec![
854 "VAR1", "TEST", "III", "TEST2", "HALLO", "AAA",
856 ];
857
858 for key in key_vec {
859 assert_eq!(keys.next().unwrap(), key);
860 }
861 }
862
863 #[test]
864 fn test_add_to_path() {
865 let prefix = PathBuf::from_str("/opt/conda").unwrap();
866 let new_paths = prefix_path_entries(&prefix, &Platform::Osx64);
867 assert_eq!(new_paths.len(), 1);
868 }
869
870 #[cfg(unix)]
871 fn create_temp_dir() -> TempDir {
872 let tempdir = TempDir::new("test").unwrap();
873 let path = tempdir.path().join("etc/conda/activate.d/");
874 fs::create_dir_all(&path).unwrap();
875
876 let script1 = path.join("script1.sh");
877
878 fs::write(script1, "").unwrap();
879
880 tempdir
881 }
882
883 #[cfg(unix)]
884 fn get_script<T: Clone + Shell + 'static>(
885 shell_type: T,
886 path_modification_behavior: PathModificationBehavior,
887 ) -> String {
888 let tdir = create_temp_dir();
889
890 let activator = Activator::from_path(tdir.path(), shell_type, Platform::Osx64).unwrap();
891
892 let test_env = HashMap::from([
894 ("FOO".to_string(), "bar".to_string()),
895 ("BAZ".to_string(), "qux".to_string()),
896 ]);
897
898 let result = activator
899 .activation(ActivationVariables {
900 conda_prefix: None,
901 path: Some(vec![
902 PathBuf::from("/usr/bin"),
903 PathBuf::from("/bin"),
904 PathBuf::from("/usr/sbin"),
905 PathBuf::from("/sbin"),
906 PathBuf::from("/usr/local/bin"),
907 ]),
908 path_modification_behavior,
909 current_env: test_env,
910 })
911 .unwrap();
912 let prefix = tdir.path().to_str().unwrap();
913 let script = result.script.contents().unwrap();
914 script.replace(prefix, "__PREFIX__")
915 }
916
917 #[test]
918 #[cfg(unix)]
919 fn test_activation_script_bash() {
920 let script = get_script(shell::Bash, PathModificationBehavior::Append);
921 insta::assert_snapshot!("test_activation_script_bash_append", script);
922 let script = get_script(shell::Bash, PathModificationBehavior::Replace);
923 insta::assert_snapshot!("test_activation_script_bash_replace", script);
924 let script = get_script(shell::Bash, PathModificationBehavior::Prepend);
925 insta::assert_snapshot!("test_activation_script_bash_prepend", script);
926 }
927
928 #[test]
929 #[cfg(unix)]
930 fn test_activation_script_zsh() {
931 let script = get_script(shell::Zsh, PathModificationBehavior::Append);
932 insta::assert_snapshot!(script);
933 }
934
935 #[test]
936 #[cfg(unix)]
937 fn test_activation_script_fish() {
938 let script = get_script(shell::Fish, PathModificationBehavior::Append);
939 insta::assert_snapshot!(script);
940 }
941
942 #[test]
943 #[cfg(unix)]
944 fn test_activation_script_powershell() {
945 let script = get_script(
946 shell::PowerShell::default(),
947 PathModificationBehavior::Append,
948 );
949 insta::assert_snapshot!("test_activation_script_powershell_append", script);
950 let script = get_script(
951 shell::PowerShell::default(),
952 PathModificationBehavior::Prepend,
953 );
954 insta::assert_snapshot!("test_activation_script_powershell_prepend", script);
955 let script = get_script(
956 shell::PowerShell::default(),
957 PathModificationBehavior::Replace,
958 );
959 insta::assert_snapshot!("test_activation_script_powershell_replace", script);
960 }
961
962 #[test]
963 #[cfg(unix)]
964 fn test_activation_script_cmd() {
965 let script = get_script(shell::CmdExe, PathModificationBehavior::Append);
966 assert!(script.contains("\r\n"));
967 let script = script.replace("\r\n", "\n");
968 insta::assert_snapshot!("test_activation_script_cmd_append", script);
971 let script =
972 get_script(shell::CmdExe, PathModificationBehavior::Replace).replace("\r\n", "\n");
973 insta::assert_snapshot!("test_activation_script_cmd_replace", script,);
974 let script =
975 get_script(shell::CmdExe, PathModificationBehavior::Prepend).replace("\r\n", "\n");
976 insta::assert_snapshot!("test_activation_script_cmd_prepend", script);
977 }
978
979 #[test]
980 #[cfg(unix)]
981 fn test_activation_script_xonsh() {
982 let script = get_script(shell::Xonsh, PathModificationBehavior::Append);
983 insta::assert_snapshot!(script);
984 }
985
986 fn test_run_activation(shell: ShellEnum, with_unicode: bool) {
987 let environment_dir = tempfile::TempDir::new().unwrap();
988
989 let env = if with_unicode {
990 environment_dir.path().join("🦀")
991 } else {
992 environment_dir.path().to_path_buf()
993 };
994
995 let state_path = env.join("conda-meta/state");
997 fs::create_dir_all(state_path.parent().unwrap()).unwrap();
998 let quotes = r#"{"env_vars": {"STATE": "Hello, world!"}}"#;
999 fs::write(&state_path, quotes).unwrap();
1000
1001 let content_pkg_1 = r#"{"PKG1": "Hello, world!"}"#;
1003 let content_pkg_2 = r#"{"PKG2": "Hello, world!"}"#;
1004
1005 let env_var_d = env.join("etc/conda/env_vars.d");
1006 fs::create_dir_all(&env_var_d).expect("Could not create env vars directory");
1007
1008 let pkg1 = env_var_d.join("pkg1.json");
1009 let pkg2 = env_var_d.join("pkg2.json");
1010
1011 fs::write(pkg1, content_pkg_1).expect("could not write file");
1012 fs::write(pkg2, content_pkg_2).expect("could not write file");
1013
1014 let mut activation_script = String::new();
1016 shell
1017 .set_env_var(&mut activation_script, "SCRIPT_ENV", "Hello, world!")
1018 .unwrap();
1019
1020 let activation_script_dir = env.join("etc/conda/activate.d");
1021 fs::create_dir_all(&activation_script_dir).unwrap();
1022
1023 fs::write(
1024 activation_script_dir.join(format!("pkg1.{}", shell.extension())),
1025 activation_script,
1026 )
1027 .unwrap();
1028
1029 let activator = Activator::from_path(&env, shell.clone(), Platform::current()).unwrap();
1031 let activation_env = activator
1032 .run_activation(ActivationVariables::default(), None)
1033 .unwrap();
1034
1035 let current_env = std::env::vars().collect::<HashMap<_, _>>();
1037
1038 let mut env_diff = activation_env
1039 .into_iter()
1040 .filter(|(key, value)| current_env.get(key) != Some(value))
1041 .collect::<BTreeMap<_, _>>();
1042
1043 env_diff.remove("CONDA_PREFIX");
1045 env_diff.remove("Path");
1046 env_diff.remove("PATH");
1047 env_diff.remove("LINENO");
1048
1049 insta::assert_yaml_snapshot!("after_activation", env_diff);
1050 }
1051
1052 #[test]
1053 #[cfg(windows)]
1054 fn test_run_activation_powershell() {
1055 test_run_activation(crate::shell::PowerShell::default().into(), false);
1056 test_run_activation(crate::shell::PowerShell::default().into(), true);
1057 }
1058
1059 #[test]
1060 #[cfg(windows)]
1061 fn test_run_activation_cmd() {
1062 test_run_activation(crate::shell::CmdExe.into(), false);
1063 test_run_activation(crate::shell::CmdExe.into(), true);
1064 }
1065
1066 #[test]
1067 #[cfg(unix)]
1068 fn test_run_activation_bash() {
1069 test_run_activation(crate::shell::Bash.into(), false);
1070 }
1071
1072 #[test]
1073 #[cfg(target_os = "macos")]
1074 fn test_run_activation_zsh() {
1075 test_run_activation(crate::shell::Zsh.into(), false);
1076 }
1077
1078 #[test]
1079 #[cfg(unix)]
1080 #[ignore]
1081 fn test_run_activation_fish() {
1082 test_run_activation(crate::shell::Fish.into(), false);
1083 }
1084
1085 #[test]
1086 #[cfg(unix)]
1087 #[ignore]
1088 fn test_run_activation_xonsh() {
1089 test_run_activation(crate::shell::Xonsh.into(), false);
1090 }
1091
1092 #[test]
1093 fn test_deactivation() {
1094 let tmp_dir = TempDir::new("test_deactivation").unwrap();
1095 let tmp_dir_path = tmp_dir.path();
1096
1097 let mut env_vars = IndexMap::new();
1099 env_vars.insert("TEST_VAR1".to_string(), "value1".to_string());
1100 env_vars.insert("TEST_VAR2".to_string(), "value2".to_string());
1101
1102 let shell_types = vec![
1104 ("bash", ShellEnum::Bash(shell::Bash)),
1105 ("zsh", ShellEnum::Zsh(shell::Zsh)),
1106 ("fish", ShellEnum::Fish(shell::Fish)),
1107 ("xonsh", ShellEnum::Xonsh(shell::Xonsh)),
1108 ("cmd", ShellEnum::CmdExe(shell::CmdExe)),
1109 (
1110 "powershell",
1111 ShellEnum::PowerShell(shell::PowerShell::default()),
1112 ),
1113 ("nushell", ShellEnum::NuShell(shell::NuShell)),
1114 ];
1115
1116 for (shell_name, shell_type) in shell_types {
1117 let activator = Activator {
1118 target_prefix: tmp_dir_path.to_path_buf(),
1119 shell_type: shell_type.clone(),
1120 paths: vec![tmp_dir_path.join("bin")],
1121 activation_scripts: vec![],
1122 deactivation_scripts: vec![],
1123 env_vars: env_vars.clone(),
1124 post_activation_env_vars: IndexMap::new(),
1125 platform: Platform::current(),
1126 };
1127
1128 let test_env = HashMap::new(); let result = activator
1131 .deactivation(ActivationVariables {
1132 conda_prefix: None,
1133 path: None,
1134 path_modification_behavior: PathModificationBehavior::Prepend,
1135 current_env: test_env,
1136 })
1137 .unwrap();
1138 let mut script_contents = result.script.contents().unwrap();
1139
1140 if shell_name == "cmd" {
1142 script_contents = script_contents.replace("\r\n", "\n");
1143 }
1144
1145 insta::assert_snapshot!(format!("test_deactivation_{}", shell_name), script_contents);
1146 }
1147 }
1148
1149 #[test]
1150 fn test_deactivation_when_activated() {
1151 let tmp_dir = TempDir::new("test_deactivation").unwrap();
1152 let tmp_dir_path = tmp_dir.path();
1153
1154 let mut env_vars = IndexMap::new();
1156 env_vars.insert("TEST_VAR1".to_string(), "value1".to_string());
1157 env_vars.insert("TEST_VAR2".to_string(), "value2".to_string());
1158
1159 let shell_types = vec![
1161 ("bash", ShellEnum::Bash(shell::Bash)),
1162 ("zsh", ShellEnum::Zsh(shell::Zsh)),
1163 ("fish", ShellEnum::Fish(shell::Fish)),
1164 ("xonsh", ShellEnum::Xonsh(shell::Xonsh)),
1165 ("cmd", ShellEnum::CmdExe(shell::CmdExe)),
1166 (
1167 "powershell",
1168 ShellEnum::PowerShell(shell::PowerShell::default()),
1169 ),
1170 ("nushell", ShellEnum::NuShell(shell::NuShell)),
1171 ];
1172
1173 for (shell_name, shell_type) in shell_types {
1174 let activator = Activator {
1175 target_prefix: tmp_dir_path.to_path_buf(),
1176 shell_type: shell_type.clone(),
1177 paths: vec![tmp_dir_path.join("bin")],
1178 activation_scripts: vec![],
1179 deactivation_scripts: vec![],
1180 env_vars: env_vars.clone(),
1181 post_activation_env_vars: IndexMap::new(),
1182 platform: Platform::current(),
1183 };
1184
1185 let test_env = HashMap::from([
1187 ("CONDA_SHLVL".to_string(), "1".to_string()),
1188 (
1189 "CONDA_PREFIX".to_string(),
1190 tmp_dir_path.to_str().unwrap().to_string(),
1191 ),
1192 ]);
1193 let result = activator
1194 .deactivation(ActivationVariables {
1195 conda_prefix: None,
1196 path: None,
1197 path_modification_behavior: PathModificationBehavior::Prepend,
1198 current_env: test_env,
1199 })
1200 .unwrap();
1201 let mut script_contents = result.script.contents().unwrap();
1202
1203 if shell_name == "cmd" {
1205 script_contents = script_contents.replace("\r\n", "\n");
1206 }
1207
1208 insta::assert_snapshot!(
1209 format!("test_deactivation_when_activated{}", shell_name),
1210 script_contents
1211 );
1212 }
1213 }
1214
1215 #[test]
1216 fn test_nested_deactivation() {
1217 let tmp_dir = TempDir::new("test_deactivation").unwrap();
1218 let tmp_dir_path = tmp_dir.path();
1219
1220 let mut first_env_vars = IndexMap::new();
1222 first_env_vars.insert("TEST_VAR1".to_string(), "first_value".to_string());
1223
1224 let shell_types = vec![
1226 ("bash", ShellEnum::Bash(shell::Bash)),
1227 ("zsh", ShellEnum::Zsh(shell::Zsh)),
1228 ("fish", ShellEnum::Fish(shell::Fish)),
1229 ("xonsh", ShellEnum::Xonsh(shell::Xonsh)),
1230 ("cmd", ShellEnum::CmdExe(shell::CmdExe)),
1231 (
1232 "powershell",
1233 ShellEnum::PowerShell(shell::PowerShell::default()),
1234 ),
1235 ("nushell", ShellEnum::NuShell(shell::NuShell)),
1236 ];
1237
1238 let mut second_env_vars = IndexMap::new();
1241 second_env_vars.insert("TEST_VAR1".to_string(), "second_value".to_string());
1242
1243 for (shell_name, shell_type) in &shell_types {
1244 let activator = Activator {
1245 target_prefix: tmp_dir_path.to_path_buf(),
1246 shell_type: shell_type.clone(),
1247 paths: vec![tmp_dir_path.join("bin")],
1248 activation_scripts: vec![],
1249 deactivation_scripts: vec![],
1250 env_vars: second_env_vars.clone(),
1251 post_activation_env_vars: IndexMap::new(),
1252 platform: Platform::current(),
1253 };
1254
1255 let mut existing_env_vars = HashMap::new();
1256 existing_env_vars.insert("TEST_VAR1".to_string(), "first_value".to_string());
1257 existing_env_vars.insert("CONDA_SHLVL".to_string(), "1".to_string());
1258
1259 let result = activator
1260 .activation(ActivationVariables {
1261 conda_prefix: None,
1262 path: None,
1263 path_modification_behavior: PathModificationBehavior::Prepend,
1264 current_env: existing_env_vars,
1265 })
1266 .unwrap();
1267
1268 let mut script_contents = result.script.contents().unwrap();
1269
1270 let mut prefix = tmp_dir_path.to_str().unwrap().to_string();
1272
1273 if cfg!(windows) {
1274 script_contents = script_contents.replace("\\\\", "\\");
1277 script_contents = script_contents.replace("\\", "/");
1278 script_contents = script_contents.replace(";", ":");
1279 prefix = prefix.replace("\\", "/");
1280 }
1281
1282 script_contents = script_contents.replace(&prefix, "__PREFIX__");
1283 if cfg!(windows) && *shell_name == "bash" {
1285 let unix_path = native_path_to_unix(&prefix).unwrap();
1286 script_contents = script_contents.replace(&unix_path, "__PREFIX__");
1287 script_contents = script_contents.replace("=\"__PREFIX__\"", "=__PREFIX__");
1288 }
1289
1290 script_contents = script_contents.replace("Path", "PATH");
1292
1293 if *shell_name == "cmd" {
1295 script_contents = script_contents.replace("\r\n", "\n");
1296 }
1297
1298 insta::assert_snapshot!(
1299 format!("test_nested_deactivation_first_round{}", shell_name),
1300 script_contents
1301 );
1302
1303 let activated_env = HashMap::from([("CONDA_SHLVL".to_string(), "2".to_string())]);
1305 let result = activator
1306 .deactivation(ActivationVariables {
1307 conda_prefix: None,
1308 path: None,
1309 path_modification_behavior: PathModificationBehavior::Prepend,
1310 current_env: activated_env,
1311 })
1312 .unwrap();
1313
1314 let mut script_contents = result.script.contents().unwrap();
1315
1316 let prefix = tmp_dir_path.to_str().unwrap();
1317 script_contents = script_contents.replace(prefix, "__PREFIX__");
1318
1319 script_contents = script_contents.replace("Path", "PATH");
1321
1322 if *shell_name == "cmd" {
1324 script_contents = script_contents.replace("\r\n", "\n");
1325 }
1326
1327 insta::assert_snapshot!(
1328 format!("test_nested_deactivation_second_round{}", shell_name),
1329 script_contents
1330 );
1331 }
1332 }
1333
1334 #[test]
1335 fn test_resetting_conda_shlvl() {
1336 let tmp_dir = TempDir::new("test_deactivation").unwrap();
1337 let tmp_dir_path = tmp_dir.path();
1338
1339 let mut first_env_vars = IndexMap::new();
1341 first_env_vars.insert("TEST_VAR1".to_string(), "first_value".to_string());
1342
1343 let shell_types = vec![
1345 ("bash", ShellEnum::Bash(shell::Bash)),
1346 ("zsh", ShellEnum::Zsh(shell::Zsh)),
1347 ("fish", ShellEnum::Fish(shell::Fish)),
1348 ("xonsh", ShellEnum::Xonsh(shell::Xonsh)),
1349 ("cmd", ShellEnum::CmdExe(shell::CmdExe)),
1350 (
1351 "powershell",
1352 ShellEnum::PowerShell(shell::PowerShell::default()),
1353 ),
1354 ("nushell", ShellEnum::NuShell(shell::NuShell)),
1355 ];
1356
1357 let mut second_env_vars = IndexMap::new();
1360 second_env_vars.insert("TEST_VAR1".to_string(), "second_value".to_string());
1361
1362 for (shell_name, shell_type) in &shell_types {
1363 let activator = Activator {
1364 target_prefix: tmp_dir_path.to_path_buf(),
1365 shell_type: shell_type.clone(),
1366 paths: vec![tmp_dir_path.join("bin")],
1367 activation_scripts: vec![],
1368 deactivation_scripts: vec![],
1369 env_vars: second_env_vars.clone(),
1370 post_activation_env_vars: IndexMap::new(),
1371 platform: Platform::current(),
1372 };
1373
1374 let mut existing_env_vars = HashMap::new();
1375 existing_env_vars.insert("TEST_VAR1".to_string(), "first_value".to_string());
1376 existing_env_vars.insert("CONDA_SHLVL".to_string(), "1".to_string());
1377
1378 let result = activator
1379 .deactivation(ActivationVariables {
1380 conda_prefix: None,
1381 path: None,
1382 path_modification_behavior: PathModificationBehavior::Prepend,
1383 current_env: existing_env_vars,
1384 })
1385 .unwrap();
1386
1387 let mut script_contents = result.script.contents().unwrap();
1388
1389 if *shell_name == "cmd" {
1391 script_contents = script_contents.replace("\r\n", "\n");
1392 }
1393
1394 insta::assert_snapshot!(
1395 format!("test_resetting_conda_shlvl{}", shell_name),
1396 script_contents
1397 );
1398 }
1399 }
1400}