rattler_shell/
activation.rs

1#![deny(missing_docs)]
2
3//! This crate provides helper functions to activate and deactivate virtual
4//! environments.
5
6#[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/// Type of modification done to the `PATH` variable
29#[derive(Default, Clone)]
30pub enum PathModificationBehavior {
31    /// Replaces the complete path variable with specified paths.
32    #[default]
33    Replace,
34    /// Appends the new path variables to the path. E.g. <PATH:/new/path>
35    Append,
36    /// Prepends the new path variables to the path. E.g. "/new/path:$PATH"
37    Prepend,
38}
39
40/// A struct that contains the values of the environment variables that are
41/// relevant for the activation process. The values are stored as strings.
42/// Currently, only the `PATH` and `CONDA_PREFIX` environment variables are
43/// used.
44#[derive(Default, Clone)]
45pub struct ActivationVariables {
46    /// The value of the `CONDA_PREFIX` environment variable that contains the
47    /// activated conda prefix path
48    pub conda_prefix: Option<PathBuf>,
49
50    /// The value of the `PATH` environment variable that contains the paths to
51    /// the executables
52    pub path: Option<Vec<PathBuf>>,
53
54    /// The type of behavior of what should happen with the defined paths.
55    pub path_modification_behavior: PathModificationBehavior,
56
57    /// Current environment variables
58    pub current_env: HashMap<String, String>,
59}
60
61impl ActivationVariables {
62    /// Create a new `ActivationVariables` struct from the environment
63    /// variables.
64    pub fn from_env() -> Result<Self, std::env::VarError> {
65        // Read all environment variables here
66        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/// A struct that holds values for the activation and deactivation
78/// process of an environment, e.g. activation scripts to execute or environment
79/// variables to set.
80#[derive(Debug)]
81pub struct Activator<T: Shell + 'static> {
82    /// The path to the root of the conda environment
83    pub target_prefix: PathBuf,
84
85    /// The type of shell that is being activated
86    pub shell_type: T,
87
88    /// Paths that need to be added to the PATH environment variable
89    pub paths: Vec<PathBuf>,
90
91    /// A list of scripts to run when activating the environment
92    pub activation_scripts: Vec<PathBuf>,
93
94    /// A list of scripts to run when deactivating the environment
95    pub deactivation_scripts: Vec<PathBuf>,
96
97    /// A list of environment variables to set before running the activation
98    /// scripts. These are evaluated before `activation_scripts` have run.
99    pub env_vars: IndexMap<String, String>,
100
101    /// A list of environment variables to set after running the activation
102    /// scripts. These are evaluated after `activation_scripts` have run.
103    pub post_activation_env_vars: IndexMap<String, String>,
104
105    /// The platform for which to generate the Activator
106    pub platform: Platform,
107}
108
109/// Collect all script files that match a certain shell type from a given path.
110/// The files are sorted by their filename.
111/// If the path does not exist, an empty vector is returned.
112/// If the path is not a directory, an error is returned.
113///
114/// # Arguments
115///
116/// * `path` - The path to the directory that contains the scripts
117/// * `shell_type` - The type of shell that the scripts are for
118///
119/// # Returns
120///
121/// A vector of paths to the scripts
122///
123/// # Errors
124///
125/// If the path is not a directory, an error is returned.
126fn collect_scripts<T: Shell>(path: &Path, shell_type: &T) -> Result<Vec<PathBuf>, std::io::Error> {
127    // Check if path exists
128    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/// Error that can occur when activating a conda environment
147#[derive(thiserror::Error, Debug)]
148pub enum ActivationError {
149    /// An error that can occur when reading or writing files
150    #[error(transparent)]
151    IoError(#[from] std::io::Error),
152
153    /// An error that can occur when running a command
154    #[error(transparent)]
155    ShellError(#[from] ShellError),
156
157    /// An error that can occur when parsing JSON
158    #[error("Invalid json for environment vars: {0} in file {1:?}")]
159    InvalidEnvVarFileJson(serde_json::Error, PathBuf),
160
161    /// An error that can occur with malformed JSON when parsing files in the
162    /// `env_vars.d` directory
163    #[error("Malformed JSON: not a plain JSON object in file {file:?}")]
164    InvalidEnvVarFileJsonNoObject {
165        /// The path to the file that contains the malformed JSON
166        file: PathBuf,
167    },
168
169    /// An error that can occur when `state` file is malformed
170    #[error("Malformed JSON: file does not contain JSON object at key env_vars in file {file:?}")]
171    InvalidEnvVarFileStateFile {
172        /// The path to the file that contains the malformed JSON
173        file: PathBuf,
174    },
175
176    /// An error that occurs when writing the activation script to a file fails
177    #[error("Failed to write activation script to file {0}")]
178    FailedToWriteActivationScript(#[from] std::fmt::Error),
179
180    /// Failed to run the activation script
181    #[error("Failed to run activation script (status: {status})")]
182    FailedToRunActivationScript {
183        /// The contents of the activation script that was run
184        script: String,
185
186        /// The stdout output of executing the script
187        stdout: String,
188
189        /// The stderr output of executing the script
190        stderr: String,
191
192        /// The error code of running the script
193        status: ExitStatus,
194    },
195}
196
197/// Collect all environment variables that are set in a conda environment.
198/// The environment variables are collected from the `state` file and the
199/// `env_vars.d` directory in the given prefix and are returned as a ordered
200/// map.
201///
202/// # Arguments
203///
204/// * `prefix` - The path to the root of the conda environment
205///
206/// # Returns
207///
208/// A map of environment variables
209///
210/// # Errors
211///
212/// If the `state` file or the `env_vars.d` directory cannot be read, an error
213/// is returned.
214fn 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        // sort env var files to get a deterministic order
230        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        // load json but preserve the order of dicts - for this we use the serde
264        // preserve_order feature
265        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
293/// Return a vector of path entries that are prefixed with the given path.
294///
295/// # Arguments
296///
297/// * `prefix` - The path to prefix the path entries with
298/// * `operating_system` - The operating system that the path entries are for
299///
300/// # Returns
301///
302/// A vector of path entries
303pub 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
318/// The result of a activation. It contains the activation script and the new
319/// path entries. The activation script already sets the PATH environment
320/// variable, but for "environment stacking" purposes it's useful to have the
321/// new path entries separately.
322pub struct ActivationResult<T: Shell + 'static> {
323    /// The activation script that sets the environment variables, runs
324    /// activation/deactivation scripts and sets the new PATH environment
325    /// variable
326    pub script: ShellScript<T>,
327    /// The new path entries that are added to the PATH environment variable
328    pub path: Vec<PathBuf>,
329}
330
331impl<T: Shell + Clone> Activator<T> {
332    /// Return unique env var keys from both `env_vars` and `post_activation_env_vars` in insertion order.
333    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    // moved: apply_env_vars_with_backup now lives on `ShellScript`
342
343    /// Create a new activator for the given conda environment.
344    ///
345    /// # Arguments
346    ///
347    /// * `path` - The path to the root of the conda environment
348    /// * `shell_type` - The shell type that the activator is for
349    /// * `operating_system` - The operating system that the activator is for
350    ///
351    /// # Returns
352    ///
353    /// A new activator
354    ///
355    /// # Examples
356    ///
357    /// ```
358    /// use rattler_shell::activation::Activator;
359    /// use rattler_shell::shell;
360    /// use rattler_conda_types::Platform;
361    /// use std::path::PathBuf;
362    ///
363    /// let activator = Activator::from_path(&PathBuf::from("tests/fixtures/env_vars"), shell::Bash, Platform::Osx64).unwrap();
364    /// assert_eq!(activator.paths.len(), 1);
365    /// assert_eq!(activator.paths[0], PathBuf::from("tests/fixtures/env_vars/bin"));
366    /// ```
367    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    /// Starts a UNIX shell.
394    /// # Arguments
395    /// - `shell`: The type of shell to start. Must implement the `Shell` and
396    ///   `Copy` traits.
397    /// - `args`: A vector of arguments to pass to the shell.
398    /// - `env`: A `HashMap` containing environment variables to set in the
399    ///   shell.
400    /// - `prompt`: Prompt to the shell
401    #[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        // create a tempfile for activation
411        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        // Write custom prompt to the env file
432        temp_file.write_all(prompt.as_bytes())?;
433
434        let mut command = std::process::Command::new(shell.executable());
435        command.args(args);
436
437        // Space added before `source` to automatically ignore it in history.
438        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        // Remove automatically added `\n`, if for some reason this fails, just ignore.
444        let source_command = source_command
445            .strip_suffix('\n')
446            .unwrap_or(source_command.as_str());
447
448        // Start process and send env activation to the shell.
449        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    /// Create an activation script for a given shell and platform. This
460    /// returns a tuple of the newly computed PATH variable and the activation
461    /// script.
462    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        // prepend new paths
488        let path = [self.paths.clone(), path].concat();
489
490        script.set_path(path.as_slice(), variables.path_modification_behavior)?;
491
492        // Get the current shell level
493        // For us, zero is the starting point, so we will increment it
494        // meaning that we will set CONDA_SHLVL to 1 on the first activation.
495        let shlvl = variables
496            .current_env
497            .get("CONDA_SHLVL")
498            .and_then(|s| s.parse::<i32>().ok())
499            .unwrap_or(0);
500
501        // Set the new CONDA_SHLVL first
502        let new_shlvl = shlvl + 1;
503        script.set_env_var("CONDA_SHLVL", &new_shlvl.to_string())?;
504
505        // Save original CONDA_PREFIX value if it exists
506        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        // Set new CONDA_PREFIX
514        script.set_env_var("CONDA_PREFIX", &self.target_prefix.to_string_lossy())?;
515
516        // For each environment variable that was set during activation
517        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        // Set environment variables that should be applied after activation scripts
524        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    /// Create a deactivation script for the environment.
534    /// This returns the deactivation script that unsets environment variables
535    /// and runs deactivation scripts.
536    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        // Get the current CONDA shell level from passed environment variables
543        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                // Handle edge case: CONDA_SHLVL not set
551                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                // Just unset without restoring (each key once)
558                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                // Handle edge case: CONDA_SHLVL zero or negative
566                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                // Just unset without restoring (each key once)
572                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                // Unset the current level
580                // For each environment variable that was set during activation
581                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                // Handle CONDA_PREFIX restoration
587                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                // Update CONDA_SHLVL
593                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        // Run all deactivation scripts
602        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    /// Runs the activation script and returns the environment variables changed
613    /// in the environment after running the script.
614    ///
615    /// If the `environment` parameter is not `None`, then it will overwrite the
616    /// parent environment variables when running the activation script.
617    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        // Create a script that starts by emitting all environment variables, then runs
625        // the activation script followed by again emitting all environment
626        // variables. Any changes should then become visible.
627        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        // Create a temporary file that we can execute with our shell.
637        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        // Write the activation script to the temporary file, closing the file
643        // afterwards
644        fs::write(
645            &activation_script_path,
646            activation_detection_script.contents()?,
647        )?;
648        // Get only the path to the temporary file
649        let mut activation_command = self
650            .shell_type
651            .create_run_script_command(&activation_script_path);
652
653        // Overwrite the environment variables with the ones provided
654        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        // Parse both environments and find the difference
676        let before_env = self.shell_type.parse_env(before_env);
677        let after_env = self.shell_type.parse_env(after_env);
678
679        // Find and return the differences
680        Ok(after_env
681            .into_iter()
682            .filter(|(key, value)| before_env.get(key) != Some(value))
683            // this happens on Windows for some reason
684            // @SET "=C:=C:\Users\robostack\Programs\pixi"
685            // @SET "=ExitCode=00000000"
686            .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        // Create a dummy activation script so the activator will run it
709        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        // Build an activator with both pre and post env vars
715        let pre_env = IndexMap::from_iter([(String::from("A"), String::from("x"))]);
716
717        // Ensure we also override a pre var in post
718        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        // Normalize prefix path for consistent assertions
746        let prefix = temp_dir.path().to_str().unwrap();
747        contents = contents.replace(prefix, "__PREFIX__");
748
749        // Check ordering: pre env vars before script run, post env vars after script run
750        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        // assert order of keys
852        let mut keys = env_vars.keys();
853        let key_vec = vec![
854            "VAR1", // overwritten - should this be sorted down?
855            "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        // Create a test environment
893        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        // Filter out the \r\n line endings for the snapshot so that insta + git works
969        // smoothly
970        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        // Write some environment variables to the `conda-meta/state` folder.
996        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        // Write package specific environment variables
1002        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        // Write a script that emits a random environment variable via a shell
1015        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        // Create an activator for the environment
1030        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        // Diff with the current environment
1036        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        // Remove system specific environment variables.
1044        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        // Create an activator with some test environment variables
1098        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        // Test all shell types
1103        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            // Test edge case: CONDA_SHLVL not set (current behavior)
1129            let test_env = HashMap::new(); // Empty environment - no CONDA_SHLVL set
1130            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            // For cmd.exe, normalize line endings for snapshots
1141            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        // Create an activator with some test environment variables
1155        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        // Test all shell types
1160        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            // CONDA_SHLVL to set to the initial level ( 1 meaning that it's activated)
1186            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            // For cmd.exe, normalize line endings for snapshots
1204            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        // Create an activator with some test environment variables
1221        let mut first_env_vars = IndexMap::new();
1222        first_env_vars.insert("TEST_VAR1".to_string(), "first_value".to_string());
1223
1224        // Test all shell types
1225        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        // now lets activate again an environment
1239        // we reuse the same TEST_VAR1 variable to check that it is correctly restored
1240        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            // Normalize temporary directory paths for consistent snapshots
1271            let mut prefix = tmp_dir_path.to_str().unwrap().to_string();
1272
1273            if cfg!(windows) {
1274                // Replace backslashes with forward slashes for consistency in snapshots as well
1275                // as ; with :
1276                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            // on windows and bash it will be quoted with shlex::try_quote
1284            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            // on windows we need to replace Path with PATH
1291            script_contents = script_contents.replace("Path", "PATH");
1292
1293            // For cmd.exe, normalize line endings for snapshots
1294            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            // and now lets deactivate the environment
1304            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            // on windows we need to replace Path with PATH
1320            script_contents = script_contents.replace("Path", "PATH");
1321
1322            // For cmd.exe, normalize line endings for snapshots
1323            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        // Create an activator with some test environment variables
1340        let mut first_env_vars = IndexMap::new();
1341        first_env_vars.insert("TEST_VAR1".to_string(), "first_value".to_string());
1342
1343        // Test all shell types
1344        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        // now lets activate again an environment
1358        // we reuse the same TEST_VAR1 variable to check that it is correctly restored
1359        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            // For cmd.exe, normalize line endings for snapshots
1390            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}