Skip to content

Commit 501ec97

Browse files
committed
Export vars for 'env activate'
1 parent c49ff67 commit 501ec97

8 files changed

Lines changed: 166 additions & 17 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ env_logger = "0.11.8"
1919
log = "0.4.28"
2020
reqwest = { version = "0.12.24", features = ["blocking"] }
2121
serde = { version = "1.0.228", features = ["derive"] }
22+
serde_json = "1.0.145"
2223
serde_yaml_ng = "0.10.0"
2324
sysinfo = "0.37.2"
2425
tar = "0.4.44"

shell/csm.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
$env:_CSM_SHELL = "powershell"
22

33
function csm {
4-
$CSM_BIN = Get-Command csm -CommandType Application | Select-Object -ExpandProperty Source
4+
$CSM_BIN = Get-Command csm -CommandType Application | Select-Object -ExpandProperty Source -First 1
55
if ($args.Length -ge 2 -and $args[0] -eq "env" -and ($args[1] -eq "activate" -or $args[1] -eq "deactivate")) {
66
$output = & $CSM_BIN @args | Out-String
77
if ($output.Trim()) {

src/env.rs

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::csmrc::Config;
2-
use crate::micromamba::{MicromambaResult, micromamba};
2+
use crate::micromamba::{self, MicromambaResult, micromamba};
33
use crate::shell::SupportedShell;
44

55
use log::{debug, error, info};
@@ -11,11 +11,11 @@ use std::process::ExitCode;
1111
#[derive(Debug, clap::Subcommand)]
1212
pub enum Subcommand {
1313
/// Create an environment
14-
Create(CreateArgs),
14+
Create(CommonEnvArgs),
1515
/// Activate an environment
16-
Activate,
16+
Activate(CommonEnvArgs),
1717
/// Deactivate an environment
18-
Deactivate,
18+
Deactivate(CommonEnvArgs),
1919
/// Run an executable in an environment
2020
Run(RunArgs),
2121
/// ???
@@ -29,7 +29,7 @@ pub enum Subcommand {
2929
}
3030

3131
#[derive(Debug, clap::Args)]
32-
pub struct CreateArgs {
32+
pub struct CommonEnvArgs {
3333
/// If specified, the name of the environment. If not specified, csm will
3434
/// look to robotmk-env.yaml for a "name" field to use instead. As a last
3535
/// resort, the current directory name will be used
@@ -154,21 +154,39 @@ pub fn run(config: Config, subcommand: Subcommand) -> ExitCode {
154154
micromamba_args.extend(args.arguments.iter().map(|s| s.as_str()));
155155
micromamba(&config, micromamba_args, true).exit_code()
156156
}
157-
Subcommand::Activate => {
158-
// TODO: handle env name similar to run/create
159-
157+
Subcommand::Activate(args) => {
160158
let Some(shell) = SupportedShell::from_csm_hook() else {
161159
error!("Your shell does not appear to have the csm hook enabled");
162160
error!("See 'csm init' for information on how to set up the hook");
163161
return ExitCode::FAILURE;
164162
};
165163

166-
info!("Activating...");
164+
let Some(env_name) = determine_env_name(args.name) else {
165+
error!("No environment name could be determined. You can specify one with --name");
166+
return ExitCode::FAILURE;
167+
};
168+
169+
info!("Activating environment '{}'...", env_name);
167170

168171
// NOTE: Anything to stdout here is *evaluated by the user's shell*
169172
// Use the logging macros instead for user-facing output!
170-
println!("{}", shell.set_env_var("CSM_TEST", "it_works"));
171-
println!("{}", shell.set_env_var("CSM_ANOTHER", "it_works_too"));
173+
174+
// Start by adding the mamba prefix bin to PATH
175+
let Some(mut env_path) = micromamba::path_for_env(&config, &env_name) else {
176+
error!("Could not determine path for environment '{}'", env_name);
177+
return ExitCode::FAILURE;
178+
};
179+
env_path.push("bin");
180+
println!("{}", shell.prepend_path(&env_path));
181+
182+
// And a few conda-specific vars
183+
println!("{}", shell.set_env_var("CONDA_DEFAULT_ENV", &env_name));
184+
println!(
185+
"{}",
186+
shell.set_env_var("CONDA_PREFIX", &env_path.to_string_lossy())
187+
);
188+
println!("{}", shell.set_env_var("CONDA_SHLVL", "1"));
189+
172190
ExitCode::SUCCESS
173191
}
174192
_ => {

src/micromamba.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use crate::csmrc::Config;
44
use log::{debug, error, info};
5+
use serde::Deserialize;
56
use std::collections::HashMap;
67
use std::fs;
78
use std::io;
@@ -86,6 +87,12 @@ impl std::fmt::Display for DownloadError {
8687
}
8788
}
8889

90+
#[derive(Deserialize)]
91+
pub struct MicromambaInfo {
92+
#[serde(rename(deserialize = "env location"))]
93+
pub env_location: String,
94+
}
95+
8996
/// Return a [`Command`] ready to shell out to `micromamba` with the appropriate
9097
/// environment variables set based on configuration.
9198
pub fn micromamba_at(path: &str, config: &Config, args: &Vec<&str>) -> Command {
@@ -320,3 +327,14 @@ fn download_micromamba(config: &Config) -> Result<PathBuf, DownloadError> {
320327

321328
Err(DownloadError::BinNotInArchive)
322329
}
330+
331+
/// Query micromamba to try to determine the path for an environment
332+
pub fn path_for_env(config: &Config, name: &str) -> Option<PathBuf> {
333+
let result = micromamba(config, vec!["info", "--name", name, "--json"], false);
334+
let MicromambaResult::CapturedOutput(output) = result else {
335+
return None;
336+
};
337+
let stdout = String::from_utf8_lossy(&output.stdout);
338+
let info: MicromambaInfo = serde_json::from_str(&stdout).ok()?;
339+
Some(info.env_location.into())
340+
}

src/shell.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use clap::ValueEnum;
1111
use clap_complete::aot::Shell;
1212
use log::{debug, warn};
1313
use std::fmt;
14-
use std::path::PathBuf;
14+
use std::path::{Path, PathBuf};
1515
use sysinfo::{ProcessesToUpdate, System};
1616

1717
const BASH_WRAPPER: &str = include_str!("../shell/csm.bash");
@@ -102,6 +102,21 @@ impl SupportedShell {
102102
Self::from_str(&env_csm_shell, false).ok()
103103
}
104104

105+
/// The $PATH syntax is also shell-dependent; this function provides a way
106+
/// prepend a directory to it for the given shell.
107+
pub fn prepend_path(&self, path: &Path) -> String {
108+
let str_path = path.to_string_lossy();
109+
match self {
110+
Self::Bash | Self::Fish | Self::Zsh => {
111+
self.set_env_var("PATH", format!("{}:$PATH", str_path).as_ref())
112+
}
113+
Self::Powershell => self.set_env_var(
114+
"PATH",
115+
format!("{}$([IO.Path]::PathSeparator)$env:PATH", str_path).as_ref(),
116+
),
117+
}
118+
}
119+
105120
fn env_var_codegen(&self, key: &str, value: &str) -> String {
106121
match self {
107122
Self::Bash | Self::Zsh => format!("export {}=\"{}\";", key, value),
@@ -191,3 +206,33 @@ You could run the following command to add it automatically:
191206
)
192207
}
193208
}
209+
210+
#[cfg(test)]
211+
mod tests {
212+
use super::*;
213+
214+
#[test]
215+
fn test_supportedshell_prepend_path() {
216+
let test_path = PathBuf::from("/tmp/testing");
217+
assert!(
218+
SupportedShell::Bash
219+
.prepend_path(&test_path)
220+
.ends_with(";export PATH=\"/tmp/testing:$PATH\";")
221+
);
222+
assert!(
223+
SupportedShell::Fish
224+
.prepend_path(&test_path)
225+
.ends_with(";set -g PATH \"/tmp/testing:$PATH\";")
226+
);
227+
assert!(
228+
SupportedShell::Zsh
229+
.prepend_path(&test_path)
230+
.ends_with(";export PATH=\"/tmp/testing:$PATH\";")
231+
);
232+
assert!(
233+
SupportedShell::Powershell
234+
.prepend_path(&test_path)
235+
.ends_with(";$env:PATH = \"/tmp/testing$([IO.Path]::PathSeparator)$env:PATH\";")
236+
);
237+
}
238+
}

tests/common.rs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#![allow(dead_code)] // https://github.com/rust-lang/rust/issues/46379
22

3-
use assert_cmd::cargo::cargo_bin_cmd;
3+
use assert_cmd::cargo::{self, cargo_bin_cmd};
44
use assert_cmd::cmd::Command;
55
use std::path::PathBuf;
66
use tempfile::{Builder, TempDir};
@@ -48,11 +48,39 @@ impl Csm {
4848
})
4949
}
5050

51-
pub fn command(&self) -> Command {
52-
let mut command = cargo_bin_cmd!();
53-
// Avoid reading real .csmrc
51+
/// Try to isolate calls to csm and micromamba from the actual system as
52+
/// much as possible, even if the user running the test has some env vars
53+
/// already set.
54+
fn prepare_command(&self, command: &mut Command) {
5455
command.env("HOME", self.home_dir.path());
5556
command.env("USERPROFILE", self.home_dir.path());
57+
command.env_remove("CONDA_PREFIX");
58+
command.env_remove("MAMBA_ROOT_PREFIX");
59+
}
60+
61+
pub fn command(&self) -> Command {
62+
let mut command = cargo_bin_cmd!();
63+
self.prepare_command(&mut command);
64+
command
65+
}
66+
67+
pub fn ext_command(&self, bin: PathBuf) -> Command {
68+
let mut command = Command::new(bin);
69+
self.prepare_command(&mut command);
70+
71+
// Add csm into $PATH, in case we want to use it from a sh -c or similar
72+
let csm_path = cargo::cargo_bin!();
73+
let csm_bin_dir = csm_path
74+
.parent()
75+
.expect("Cannot get csm binary directory")
76+
.to_string_lossy()
77+
.replace("\\", "/");
78+
let separator = if cfg!(windows) { ";" } else { ":" };
79+
let path = match std::env::var("PATH") {
80+
Ok(path) => format!("{}{}{}", path, separator, csm_bin_dir),
81+
Err(_) => csm_bin_dir,
82+
};
83+
command.env("PATH", path);
5684

5785
command
5886
}

tests/env.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod common;
33
use common::Error;
44

55
use predicates::prelude::*;
6+
use which::which;
67

78
/// Create an environment with `csm env create`.
89
fn csm_env_create(csm: &mut common::Csm, name: &str) -> Result<(), Error> {
@@ -156,3 +157,40 @@ fn test_csm_env_activate_no_hook() -> Result<(), Error> {
156157
.stderr(predicate::str::contains("See 'csm init' for information"));
157158
Ok(())
158159
}
160+
161+
/// Activate an environment and call something in it, using bash.
162+
#[cfg(feature = "__test_bash")]
163+
#[test]
164+
fn csm_env_activate_bash() -> Result<(), Error> {
165+
let mut csm = common::Csm::new()?;
166+
let _ = csm_env_create(&mut csm, "csm_env_activate_bash");
167+
csm.ext_command(which("bash")?)
168+
.arg("-c")
169+
.arg(
170+
"eval \"$(csm init bash --code)\" &&\
171+
csm env activate -n csm_env_activate_bash &&\
172+
robot --version",
173+
)
174+
.assert()
175+
.code(251)
176+
.stdout(predicate::str::is_match("^Robot Framework")?);
177+
Ok(())
178+
}
179+
180+
/// Activate an environment and call something in it, using powershell.
181+
#[cfg(feature = "__test_powershell")]
182+
#[test]
183+
fn csm_env_activate_powershell() -> Result<(), Error> {
184+
let mut csm = common::Csm::new()?;
185+
let _ = csm_env_create(&mut csm, "csm_env_activate_bash");
186+
csm.ext_command(which("pwsh")?)
187+
.arg("-c")
188+
.arg(format!(
189+
"csm init powershell --code | Out-String | Invoke-Expression &&\
190+
csm env activate -n csm_env_activate_bash &&\
191+
robot --version",
192+
))
193+
.assert()
194+
.stdout(predicate::str::is_match("^Robot Framework")?);
195+
Ok(())
196+
}

0 commit comments

Comments
 (0)