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 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 tempfile::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::with_prefix("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::with_prefix("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::with_prefix("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::with_prefix("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]
872 fn test_collect_env_vars_no_spurious_conflict_warnings() {
873 let tdir = TempDir::with_prefix("test_no_spurious_warnings").unwrap();
874 let state_path = tdir.path().join("conda-meta/state");
875 fs::create_dir_all(state_path.parent().unwrap()).unwrap();
876
877 let env_var_d = tdir.path().join("etc/conda/env_vars.d");
878 fs::create_dir_all(&env_var_d).unwrap();
879
880 let pkg_content = r#"{"PKG_VAR": "from_pkg"}"#;
882 fs::write(env_var_d.join("pkg.json"), pkg_content).unwrap();
883
884 let state_content =
886 r#"{"env_vars": {"STATE_ONLY_VAR": "state_val", "PKG_VAR": "state_override"}}"#;
887 fs::write(&state_path, state_content).unwrap();
888
889 let env_vars = collect_env_vars(tdir.path()).expect("collect_env_vars must succeed");
890
891 assert!(
893 env_vars.contains_key("STATE_ONLY_VAR"),
894 "STATE_ONLY_VAR (state-only key) must be collected"
895 );
896 assert!(
897 env_vars.contains_key("PKG_VAR"),
898 "PKG_VAR (conflict key) must be collected"
899 );
900
901 assert_eq!(env_vars["STATE_ONLY_VAR"], "state_val");
910 assert_eq!(env_vars["PKG_VAR"], "state_override");
911 }
912
913 #[test]
914 fn test_add_to_path() {
915 let prefix = PathBuf::from_str("/opt/conda").unwrap();
916 let new_paths = prefix_path_entries(&prefix, &Platform::Osx64);
917 assert_eq!(new_paths.len(), 1);
918 }
919
920 #[cfg(unix)]
921 fn create_temp_dir() -> TempDir {
922 let tempdir = TempDir::with_prefix("test").unwrap();
923 let path = tempdir.path().join("etc/conda/activate.d/");
924 fs::create_dir_all(&path).unwrap();
925
926 let script1 = path.join("script1.sh");
927
928 fs::write(script1, "").unwrap();
929
930 tempdir
931 }
932
933 #[cfg(unix)]
934 fn get_script<T: Clone + Shell + 'static>(
935 shell_type: T,
936 path_modification_behavior: PathModificationBehavior,
937 ) -> String {
938 let tdir = create_temp_dir();
939
940 let activator = Activator::from_path(tdir.path(), shell_type, Platform::Osx64).unwrap();
941
942 let test_env = HashMap::from([
944 ("FOO".to_string(), "bar".to_string()),
945 ("BAZ".to_string(), "qux".to_string()),
946 ]);
947
948 let result = activator
949 .activation(ActivationVariables {
950 conda_prefix: None,
951 path: Some(vec![
952 PathBuf::from("/usr/bin"),
953 PathBuf::from("/bin"),
954 PathBuf::from("/usr/sbin"),
955 PathBuf::from("/sbin"),
956 PathBuf::from("/usr/local/bin"),
957 ]),
958 path_modification_behavior,
959 current_env: test_env,
960 })
961 .unwrap();
962 let prefix = tdir.path().to_str().unwrap();
963 let script = result.script.contents().unwrap();
964 script.replace(prefix, "__PREFIX__")
965 }
966
967 #[test]
968 #[cfg(unix)]
969 fn test_activation_script_bash() {
970 let script = get_script(shell::Bash, PathModificationBehavior::Append);
971 insta::assert_snapshot!("test_activation_script_bash_append", script);
972 let script = get_script(shell::Bash, PathModificationBehavior::Replace);
973 insta::assert_snapshot!("test_activation_script_bash_replace", script);
974 let script = get_script(shell::Bash, PathModificationBehavior::Prepend);
975 insta::assert_snapshot!("test_activation_script_bash_prepend", script);
976 }
977
978 #[test]
979 #[cfg(unix)]
980 fn test_activation_script_zsh() {
981 let script = get_script(shell::Zsh, PathModificationBehavior::Append);
982 insta::assert_snapshot!(script);
983 }
984
985 #[test]
986 #[cfg(unix)]
987 fn test_activation_script_fish() {
988 let script = get_script(shell::Fish, PathModificationBehavior::Append);
989 insta::assert_snapshot!(script);
990 }
991
992 #[test]
993 #[cfg(unix)]
994 fn test_activation_script_powershell() {
995 let script = get_script(
996 shell::PowerShell::default(),
997 PathModificationBehavior::Append,
998 );
999 insta::assert_snapshot!("test_activation_script_powershell_append", script);
1000 let script = get_script(
1001 shell::PowerShell::default(),
1002 PathModificationBehavior::Prepend,
1003 );
1004 insta::assert_snapshot!("test_activation_script_powershell_prepend", script);
1005 let script = get_script(
1006 shell::PowerShell::default(),
1007 PathModificationBehavior::Replace,
1008 );
1009 insta::assert_snapshot!("test_activation_script_powershell_replace", script);
1010 }
1011
1012 #[test]
1013 #[cfg(unix)]
1014 fn test_activation_script_cmd() {
1015 let script = get_script(shell::CmdExe, PathModificationBehavior::Append);
1016 assert!(script.contains("\r\n"));
1017 let script = script.replace("\r\n", "\n");
1018 insta::assert_snapshot!("test_activation_script_cmd_append", script);
1021 let script =
1022 get_script(shell::CmdExe, PathModificationBehavior::Replace).replace("\r\n", "\n");
1023 insta::assert_snapshot!("test_activation_script_cmd_replace", script,);
1024 let script =
1025 get_script(shell::CmdExe, PathModificationBehavior::Prepend).replace("\r\n", "\n");
1026 insta::assert_snapshot!("test_activation_script_cmd_prepend", script);
1027 }
1028
1029 #[test]
1030 #[cfg(unix)]
1031 fn test_activation_script_xonsh() {
1032 let script = get_script(shell::Xonsh, PathModificationBehavior::Append);
1033 insta::assert_snapshot!(script);
1034 }
1035
1036 fn test_run_activation(shell: ShellEnum, with_unicode: bool) {
1037 let environment_dir = tempfile::TempDir::new().unwrap();
1038
1039 let env = if with_unicode {
1040 environment_dir.path().join("🦀")
1041 } else {
1042 environment_dir.path().to_path_buf()
1043 };
1044
1045 let state_path = env.join("conda-meta/state");
1047 fs::create_dir_all(state_path.parent().unwrap()).unwrap();
1048 let quotes = r#"{"env_vars": {"STATE": "Hello, world!"}}"#;
1049 fs::write(&state_path, quotes).unwrap();
1050
1051 let content_pkg_1 = r#"{"PKG1": "Hello, world!"}"#;
1053 let content_pkg_2 = r#"{"PKG2": "Hello, world!"}"#;
1054
1055 let env_var_d = env.join("etc/conda/env_vars.d");
1056 fs::create_dir_all(&env_var_d).expect("Could not create env vars directory");
1057
1058 let pkg1 = env_var_d.join("pkg1.json");
1059 let pkg2 = env_var_d.join("pkg2.json");
1060
1061 fs::write(pkg1, content_pkg_1).expect("could not write file");
1062 fs::write(pkg2, content_pkg_2).expect("could not write file");
1063
1064 let mut activation_script = String::new();
1066 shell
1067 .set_env_var(&mut activation_script, "SCRIPT_ENV", "Hello, world!")
1068 .unwrap();
1069
1070 let activation_script_dir = env.join("etc/conda/activate.d");
1071 fs::create_dir_all(&activation_script_dir).unwrap();
1072
1073 fs::write(
1074 activation_script_dir.join(format!("pkg1.{}", shell.extension())),
1075 activation_script,
1076 )
1077 .unwrap();
1078
1079 let activator = Activator::from_path(&env, shell.clone(), Platform::current()).unwrap();
1081 let activation_env = activator
1082 .run_activation(ActivationVariables::default(), None)
1083 .unwrap();
1084
1085 let current_env = std::env::vars().collect::<HashMap<_, _>>();
1087
1088 let mut env_diff = activation_env
1089 .into_iter()
1090 .filter(|(key, value)| current_env.get(key) != Some(value))
1091 .collect::<BTreeMap<_, _>>();
1092
1093 env_diff.remove("CONDA_SHLVL");
1095 env_diff.remove("CONDA_PREFIX");
1096 env_diff.remove("Path");
1097 env_diff.remove("PATH");
1098 env_diff.remove("LINENO");
1099
1100 insta::assert_yaml_snapshot!("after_activation", env_diff);
1101 }
1102
1103 #[test]
1104 #[cfg(windows)]
1105 fn test_run_activation_powershell() {
1106 test_run_activation(crate::shell::PowerShell::default().into(), false);
1107 test_run_activation(crate::shell::PowerShell::default().into(), true);
1108 }
1109
1110 #[test]
1111 #[cfg(windows)]
1112 fn test_run_activation_cmd() {
1113 test_run_activation(crate::shell::CmdExe.into(), false);
1114 test_run_activation(crate::shell::CmdExe.into(), true);
1115 }
1116
1117 #[test]
1118 #[cfg(unix)]
1119 fn test_run_activation_bash() {
1120 test_run_activation(crate::shell::Bash.into(), false);
1121 }
1122
1123 #[test]
1124 #[cfg(target_os = "macos")]
1125 fn test_run_activation_zsh() {
1126 test_run_activation(crate::shell::Zsh.into(), false);
1127 }
1128
1129 #[test]
1130 #[cfg(unix)]
1131 #[ignore]
1132 fn test_run_activation_fish() {
1133 test_run_activation(crate::shell::Fish.into(), false);
1134 }
1135
1136 #[test]
1137 #[cfg(unix)]
1138 #[ignore]
1139 fn test_run_activation_xonsh() {
1140 test_run_activation(crate::shell::Xonsh.into(), false);
1141 }
1142
1143 #[test]
1144 fn test_deactivation() {
1145 let tmp_dir = TempDir::with_prefix("test_deactivation").unwrap();
1146 let tmp_dir_path = tmp_dir.path();
1147
1148 let mut env_vars = IndexMap::new();
1150 env_vars.insert("TEST_VAR1".to_string(), "value1".to_string());
1151 env_vars.insert("TEST_VAR2".to_string(), "value2".to_string());
1152
1153 let shell_types = vec![
1155 ("bash", ShellEnum::Bash(shell::Bash)),
1156 ("zsh", ShellEnum::Zsh(shell::Zsh)),
1157 ("fish", ShellEnum::Fish(shell::Fish)),
1158 ("xonsh", ShellEnum::Xonsh(shell::Xonsh)),
1159 ("cmd", ShellEnum::CmdExe(shell::CmdExe)),
1160 (
1161 "powershell",
1162 ShellEnum::PowerShell(shell::PowerShell::default()),
1163 ),
1164 ("nushell", ShellEnum::NuShell(shell::NuShell)),
1165 ];
1166
1167 for (shell_name, shell_type) in shell_types {
1168 let activator = Activator {
1169 target_prefix: tmp_dir_path.to_path_buf(),
1170 shell_type: shell_type.clone(),
1171 paths: vec![tmp_dir_path.join("bin")],
1172 activation_scripts: vec![],
1173 deactivation_scripts: vec![],
1174 env_vars: env_vars.clone(),
1175 post_activation_env_vars: IndexMap::new(),
1176 platform: Platform::current(),
1177 };
1178
1179 let test_env = HashMap::new(); let result = activator
1182 .deactivation(ActivationVariables {
1183 conda_prefix: None,
1184 path: None,
1185 path_modification_behavior: PathModificationBehavior::Prepend,
1186 current_env: test_env,
1187 })
1188 .unwrap();
1189 let mut script_contents = result.script.contents().unwrap();
1190
1191 if shell_name == "cmd" {
1193 script_contents = script_contents.replace("\r\n", "\n");
1194 }
1195
1196 insta::assert_snapshot!(format!("test_deactivation_{}", shell_name), script_contents);
1197 }
1198 }
1199
1200 #[test]
1201 fn test_deactivation_when_activated() {
1202 let tmp_dir = TempDir::with_prefix("test_deactivation").unwrap();
1203 let tmp_dir_path = tmp_dir.path();
1204
1205 let mut env_vars = IndexMap::new();
1207 env_vars.insert("TEST_VAR1".to_string(), "value1".to_string());
1208 env_vars.insert("TEST_VAR2".to_string(), "value2".to_string());
1209
1210 let shell_types = vec![
1212 ("bash", ShellEnum::Bash(shell::Bash)),
1213 ("zsh", ShellEnum::Zsh(shell::Zsh)),
1214 ("fish", ShellEnum::Fish(shell::Fish)),
1215 ("xonsh", ShellEnum::Xonsh(shell::Xonsh)),
1216 ("cmd", ShellEnum::CmdExe(shell::CmdExe)),
1217 (
1218 "powershell",
1219 ShellEnum::PowerShell(shell::PowerShell::default()),
1220 ),
1221 ("nushell", ShellEnum::NuShell(shell::NuShell)),
1222 ];
1223
1224 for (shell_name, shell_type) in shell_types {
1225 let activator = Activator {
1226 target_prefix: tmp_dir_path.to_path_buf(),
1227 shell_type: shell_type.clone(),
1228 paths: vec![tmp_dir_path.join("bin")],
1229 activation_scripts: vec![],
1230 deactivation_scripts: vec![],
1231 env_vars: env_vars.clone(),
1232 post_activation_env_vars: IndexMap::new(),
1233 platform: Platform::current(),
1234 };
1235
1236 let test_env = HashMap::from([
1238 ("CONDA_SHLVL".to_string(), "1".to_string()),
1239 (
1240 "CONDA_PREFIX".to_string(),
1241 tmp_dir_path.to_str().unwrap().to_string(),
1242 ),
1243 ]);
1244 let result = activator
1245 .deactivation(ActivationVariables {
1246 conda_prefix: None,
1247 path: None,
1248 path_modification_behavior: PathModificationBehavior::Prepend,
1249 current_env: test_env,
1250 })
1251 .unwrap();
1252 let mut script_contents = result.script.contents().unwrap();
1253
1254 if shell_name == "cmd" {
1256 script_contents = script_contents.replace("\r\n", "\n");
1257 }
1258
1259 insta::assert_snapshot!(
1260 format!("test_deactivation_when_activated{}", shell_name),
1261 script_contents
1262 );
1263 }
1264 }
1265
1266 #[test]
1267 fn test_nested_deactivation() {
1268 let tmp_dir = TempDir::with_prefix("test_deactivation").unwrap();
1269 let tmp_dir_path = tmp_dir.path();
1270
1271 let mut first_env_vars = IndexMap::new();
1273 first_env_vars.insert("TEST_VAR1".to_string(), "first_value".to_string());
1274
1275 let shell_types = vec![
1277 ("bash", ShellEnum::Bash(shell::Bash)),
1278 ("zsh", ShellEnum::Zsh(shell::Zsh)),
1279 ("fish", ShellEnum::Fish(shell::Fish)),
1280 ("xonsh", ShellEnum::Xonsh(shell::Xonsh)),
1281 ("cmd", ShellEnum::CmdExe(shell::CmdExe)),
1282 (
1283 "powershell",
1284 ShellEnum::PowerShell(shell::PowerShell::default()),
1285 ),
1286 ("nushell", ShellEnum::NuShell(shell::NuShell)),
1287 ];
1288
1289 let mut second_env_vars = IndexMap::new();
1292 second_env_vars.insert("TEST_VAR1".to_string(), "second_value".to_string());
1293
1294 for (shell_name, shell_type) in &shell_types {
1295 let activator = Activator {
1296 target_prefix: tmp_dir_path.to_path_buf(),
1297 shell_type: shell_type.clone(),
1298 paths: vec![tmp_dir_path.join("bin")],
1299 activation_scripts: vec![],
1300 deactivation_scripts: vec![],
1301 env_vars: second_env_vars.clone(),
1302 post_activation_env_vars: IndexMap::new(),
1303 platform: Platform::current(),
1304 };
1305
1306 let mut existing_env_vars = HashMap::new();
1307 existing_env_vars.insert("TEST_VAR1".to_string(), "first_value".to_string());
1308 existing_env_vars.insert("CONDA_SHLVL".to_string(), "1".to_string());
1309
1310 let result = activator
1311 .activation(ActivationVariables {
1312 conda_prefix: None,
1313 path: None,
1314 path_modification_behavior: PathModificationBehavior::Prepend,
1315 current_env: existing_env_vars,
1316 })
1317 .unwrap();
1318
1319 let mut script_contents = result.script.contents().unwrap();
1320
1321 let mut prefix = tmp_dir_path.to_str().unwrap().to_string();
1323
1324 if cfg!(windows) {
1325 script_contents = script_contents.replace("\\\\", "\\");
1328 script_contents = script_contents.replace("\\", "/");
1329 script_contents = script_contents.replace(";", ":");
1330 prefix = prefix.replace("\\", "/");
1331 }
1332
1333 script_contents = script_contents.replace(&prefix, "__PREFIX__");
1334 if cfg!(windows) && *shell_name == "bash" {
1336 let unix_path = match native_path_to_unix(&prefix) {
1337 Ok(str) => str,
1338 Err(e) if e.kind() == std::io::ErrorKind::NotFound => prefix,
1339 Err(e) => panic!("Failed to convert path to unix: {e}"),
1340 };
1341 script_contents = script_contents.replace(&unix_path, "__PREFIX__");
1342 script_contents = script_contents.replace("=\"__PREFIX__\"", "=__PREFIX__");
1343 }
1344
1345 script_contents = script_contents.replace("Path", "PATH");
1347
1348 if *shell_name == "cmd" {
1350 script_contents = script_contents.replace("\r\n", "\n");
1351 }
1352
1353 insta::assert_snapshot!(
1354 format!("test_nested_deactivation_first_round{}", shell_name),
1355 script_contents
1356 );
1357
1358 let activated_env = HashMap::from([("CONDA_SHLVL".to_string(), "2".to_string())]);
1360 let result = activator
1361 .deactivation(ActivationVariables {
1362 conda_prefix: None,
1363 path: None,
1364 path_modification_behavior: PathModificationBehavior::Prepend,
1365 current_env: activated_env,
1366 })
1367 .unwrap();
1368
1369 let mut script_contents = result.script.contents().unwrap();
1370
1371 let prefix = tmp_dir_path.to_str().unwrap();
1372 script_contents = script_contents.replace(prefix, "__PREFIX__");
1373
1374 script_contents = script_contents.replace("Path", "PATH");
1376
1377 if *shell_name == "cmd" {
1379 script_contents = script_contents.replace("\r\n", "\n");
1380 }
1381
1382 insta::assert_snapshot!(
1383 format!("test_nested_deactivation_second_round{}", shell_name),
1384 script_contents
1385 );
1386 }
1387 }
1388
1389 #[test]
1390 fn test_resetting_conda_shlvl() {
1391 let tmp_dir = TempDir::with_prefix("test_deactivation").unwrap();
1392 let tmp_dir_path = tmp_dir.path();
1393
1394 let mut first_env_vars = IndexMap::new();
1396 first_env_vars.insert("TEST_VAR1".to_string(), "first_value".to_string());
1397
1398 let shell_types = vec![
1400 ("bash", ShellEnum::Bash(shell::Bash)),
1401 ("zsh", ShellEnum::Zsh(shell::Zsh)),
1402 ("fish", ShellEnum::Fish(shell::Fish)),
1403 ("xonsh", ShellEnum::Xonsh(shell::Xonsh)),
1404 ("cmd", ShellEnum::CmdExe(shell::CmdExe)),
1405 (
1406 "powershell",
1407 ShellEnum::PowerShell(shell::PowerShell::default()),
1408 ),
1409 ("nushell", ShellEnum::NuShell(shell::NuShell)),
1410 ];
1411
1412 let mut second_env_vars = IndexMap::new();
1415 second_env_vars.insert("TEST_VAR1".to_string(), "second_value".to_string());
1416
1417 for (shell_name, shell_type) in &shell_types {
1418 let activator = Activator {
1419 target_prefix: tmp_dir_path.to_path_buf(),
1420 shell_type: shell_type.clone(),
1421 paths: vec![tmp_dir_path.join("bin")],
1422 activation_scripts: vec![],
1423 deactivation_scripts: vec![],
1424 env_vars: second_env_vars.clone(),
1425 post_activation_env_vars: IndexMap::new(),
1426 platform: Platform::current(),
1427 };
1428
1429 let mut existing_env_vars = HashMap::new();
1430 existing_env_vars.insert("TEST_VAR1".to_string(), "first_value".to_string());
1431 existing_env_vars.insert("CONDA_SHLVL".to_string(), "1".to_string());
1432
1433 let result = activator
1434 .deactivation(ActivationVariables {
1435 conda_prefix: None,
1436 path: None,
1437 path_modification_behavior: PathModificationBehavior::Prepend,
1438 current_env: existing_env_vars,
1439 })
1440 .unwrap();
1441
1442 let mut script_contents = result.script.contents().unwrap();
1443
1444 if *shell_name == "cmd" {
1446 script_contents = script_contents.replace("\r\n", "\n");
1447 }
1448
1449 insta::assert_snapshot!(
1450 format!("test_resetting_conda_shlvl{}", shell_name),
1451 script_contents
1452 );
1453 }
1454 }
1455}