Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ env_logger = "0.11.8"
log = "0.4.28"
reqwest = { version = "0.12.24", features = ["blocking"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
serde_yaml_ng = "0.10.0"
sysinfo = "0.37.2"
tar = "0.4.44"
Expand Down
2 changes: 1 addition & 1 deletion shell/csm.ps1
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
$env:_CSM_SHELL = "powershell"

function csm {
$CSM_BIN = Get-Command csm -CommandType Application | Select-Object -ExpandProperty Source
$CSM_BIN = Get-Command csm -CommandType Application | Select-Object -ExpandProperty Source -First 1
if ($args.Length -ge 2 -and $args[0] -eq "env" -and ($args[1] -eq "activate" -or $args[1] -eq "deactivate")) {
$output = & $CSM_BIN @args | Out-String
if ($output.Trim()) {
Expand Down
41 changes: 30 additions & 11 deletions src/env.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::csmrc::Config;
use crate::micromamba::{MicromambaResult, micromamba};
use crate::micromamba::{self, MicromambaResult, micromamba};
use crate::shell::SupportedShell;

use log::{debug, error, info};
Expand All @@ -11,11 +11,11 @@ use std::process::ExitCode;
#[derive(Debug, clap::Subcommand)]
pub enum Subcommand {
/// Create an environment
Create(CreateArgs),
Create(CommonEnvArgs),
/// Activate an environment
Activate,
Activate(CommonEnvArgs),
/// Deactivate an environment
Deactivate,
Deactivate(CommonEnvArgs),
/// Run an executable in an environment
Run(RunArgs),
/// ???
Expand All @@ -29,7 +29,7 @@ pub enum Subcommand {
}

#[derive(Debug, clap::Args)]
pub struct CreateArgs {
pub struct CommonEnvArgs {
/// If specified, the name of the environment. If not specified, csm will
/// look to robotmk-env.yaml for a "name" field to use instead. As a last
/// resort, the current directory name will be used
Expand Down Expand Up @@ -154,21 +154,40 @@ pub fn run(config: Config, subcommand: Subcommand) -> ExitCode {
micromamba_args.extend(args.arguments.iter().map(|s| s.as_str()));
micromamba(&config, micromamba_args, true).exit_code()
}
Subcommand::Activate => {
// TODO: handle env name similar to run/create

Subcommand::Activate(args) => {
let Some(shell) = SupportedShell::from_csm_hook() else {
error!("Your shell does not appear to have the csm hook enabled");
error!("See 'csm init' for information on how to set up the hook");
return ExitCode::FAILURE;
};

info!("Activating...");
let Some(env_name) = determine_env_name(args.name) else {
error!("No environment name could be determined. You can specify one with --name");
return ExitCode::FAILURE;
};

info!("Activating environment '{}'...", env_name);

// NOTE: Anything to stdout here is *evaluated by the user's shell*
// Use the logging macros instead for user-facing output!
println!("{}", shell.set_env_var("CSM_TEST", "it_works"));
println!("{}", shell.set_env_var("CSM_ANOTHER", "it_works_too"));

// Start by adding the mamba prefix bin to PATH
let Some(mut env_path) = micromamba::path_for_env(&config, &env_name) else {
error!("Could not determine path for environment '{}'", env_name);
return ExitCode::FAILURE;
};
let bin = if cfg!(windows) { "Scripts" } else { "bin" };
env_path.push(bin);
println!("{}", shell.prepend_path(&env_path));

// And a few conda-specific vars
println!("{}", shell.set_env_var("CONDA_DEFAULT_ENV", &env_name));
println!(
"{}",
shell.set_env_var("CONDA_PREFIX", &env_path.to_string_lossy())
);
println!("{}", shell.set_env_var("CONDA_SHLVL", "1"));

ExitCode::SUCCESS
}
_ => {
Expand Down
18 changes: 18 additions & 0 deletions src/micromamba.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use crate::csmrc::Config;
use log::{debug, error, info};
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::io;
Expand Down Expand Up @@ -86,6 +87,12 @@ impl std::fmt::Display for DownloadError {
}
}

#[derive(Deserialize)]
pub struct MicromambaInfo {
#[serde(rename(deserialize = "env location"))]
pub env_location: String,
}

/// Return a [`Command`] ready to shell out to `micromamba` with the appropriate
/// environment variables set based on configuration.
pub fn micromamba_at(path: &str, config: &Config, args: &Vec<&str>) -> Command {
Expand Down Expand Up @@ -320,3 +327,14 @@ fn download_micromamba(config: &Config) -> Result<PathBuf, DownloadError> {

Err(DownloadError::BinNotInArchive)
}

/// Query micromamba to try to determine the path for an environment
pub fn path_for_env(config: &Config, name: &str) -> Option<PathBuf> {
let result = micromamba(config, vec!["info", "--name", name, "--json"], false);
let MicromambaResult::CapturedOutput(output) = result else {
return None;
};
let stdout = String::from_utf8_lossy(&output.stdout);
let info: MicromambaInfo = serde_json::from_str(&stdout).ok()?;
Some(info.env_location.into())
}
47 changes: 46 additions & 1 deletion src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use clap::ValueEnum;
use clap_complete::aot::Shell;
use log::{debug, warn};
use std::fmt;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use sysinfo::{ProcessesToUpdate, System};

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

/// The $PATH syntax is also shell-dependent; this function provides a way
/// prepend a directory to it for the given shell.
pub fn prepend_path(&self, path: &Path) -> String {
let str_path = path.to_string_lossy();
match self {
Self::Bash | Self::Fish | Self::Zsh => {
self.set_env_var("PATH", format!("{}:$PATH", str_path).as_ref())
}
Self::Powershell => self.set_env_var(
"PATH",
format!("{}$([IO.Path]::PathSeparator)$env:PATH", str_path).as_ref(),
),
}
}

fn env_var_codegen(&self, key: &str, value: &str) -> String {
match self {
Self::Bash | Self::Zsh => format!("export {}=\"{}\";", key, value),
Expand Down Expand Up @@ -191,3 +206,33 @@ You could run the following command to add it automatically:
)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_supportedshell_prepend_path() {
let test_path = PathBuf::from("/tmp/testing");
assert!(
SupportedShell::Bash
.prepend_path(&test_path)
.ends_with(";export PATH=\"/tmp/testing:$PATH\";")
);
assert!(
SupportedShell::Fish
.prepend_path(&test_path)
.ends_with(";set -g PATH \"/tmp/testing:$PATH\";")
);
assert!(
SupportedShell::Zsh
.prepend_path(&test_path)
.ends_with(";export PATH=\"/tmp/testing:$PATH\";")
);
assert!(
SupportedShell::Powershell
.prepend_path(&test_path)
.ends_with(";$env:PATH = \"/tmp/testing$([IO.Path]::PathSeparator)$env:PATH\";")
);
}
}
36 changes: 32 additions & 4 deletions tests/common.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#![allow(dead_code)] // https://github.com/rust-lang/rust/issues/46379

use assert_cmd::cargo::cargo_bin_cmd;
use assert_cmd::cargo::{self, cargo_bin_cmd};
use assert_cmd::cmd::Command;
use std::path::PathBuf;
use tempfile::{Builder, TempDir};
Expand Down Expand Up @@ -48,11 +48,39 @@ impl Csm {
})
}

pub fn command(&self) -> Command {
let mut command = cargo_bin_cmd!();
// Avoid reading real .csmrc
/// Try to isolate calls to csm and micromamba from the actual system as
/// much as possible, even if the user running the test has some env vars
/// already set.
fn prepare_command(&self, command: &mut Command) {
command.env("HOME", self.home_dir.path());
command.env("USERPROFILE", self.home_dir.path());
command.env_remove("CONDA_PREFIX");
command.env_remove("MAMBA_ROOT_PREFIX");
}

pub fn command(&self) -> Command {
let mut command = cargo_bin_cmd!();
self.prepare_command(&mut command);
command
}

pub fn ext_command(&self, bin: PathBuf) -> Command {
let mut command = Command::new(bin);
self.prepare_command(&mut command);

// Add csm into $PATH, in case we want to use it from a sh -c or similar
let csm_path = cargo::cargo_bin!();
let csm_bin_dir = csm_path
.parent()
.expect("Cannot get csm binary directory")
.to_string_lossy()
.replace("\\", "/");
let separator = if cfg!(windows) { ";" } else { ":" };
let path = match std::env::var("PATH") {
Ok(path) => format!("{}{}{}", path, separator, csm_bin_dir),
Err(_) => csm_bin_dir,
};
command.env("PATH", path);

command
}
Expand Down
38 changes: 38 additions & 0 deletions tests/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod common;
use common::Error;

use predicates::prelude::*;
use which::which;

/// Create an environment with `csm env create`.
fn csm_env_create(csm: &mut common::Csm, name: &str) -> Result<(), Error> {
Expand Down Expand Up @@ -156,3 +157,40 @@ fn test_csm_env_activate_no_hook() -> Result<(), Error> {
.stderr(predicate::str::contains("See 'csm init' for information"));
Ok(())
}

/// Activate an environment and call something in it, using bash.
#[cfg(feature = "__test_bash")]
#[test]
fn csm_env_activate_bash() -> Result<(), Error> {
let mut csm = common::Csm::new()?;
let _ = csm_env_create(&mut csm, "csm_env_activate_bash");
csm.ext_command(which("bash")?)
.arg("-c")
.arg(
"eval \"$(csm init bash --code)\" &&\
csm env activate -n csm_env_activate_bash &&\
robot --version",
)
.assert()
.code(251)
.stdout(predicate::str::is_match("^Robot Framework")?);
Ok(())
}

/// Activate an environment and call something in it, using powershell.
#[cfg(feature = "__test_powershell")]
#[test]
fn csm_env_activate_powershell() -> Result<(), Error> {
let mut csm = common::Csm::new()?;
let _ = csm_env_create(&mut csm, "csm_env_activate_bash");
csm.ext_command(which("pwsh")?)
.arg("-c")
.arg(format!(
"csm init powershell --code | Out-String | Invoke-Expression &&\
csm env activate -n csm_env_activate_bash &&\
robot --version",
))
.assert()
.stdout(predicate::str::is_match("^Robot Framework")?);
Ok(())
}
Loading