Skip to content

Commit 0b81adf

Browse files
committed
csm env unpack command
1 parent 448c734 commit 0b81adf

5 files changed

Lines changed: 188 additions & 10 deletions

File tree

Cargo.lock

Lines changed: 43 additions & 1 deletion
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 @@ clap = { version = "4.5.50", features = ["derive", "wrap_help"] }
1919
clap_complete = "4.5.60"
2020
dirs = "6.0.0"
2121
env_logger = "0.11.8"
22+
flate2 = "1.1.5"
2223
log = "0.4.28"
2324
reqwest = { version = "0.12.24", default-features = false, features = ["http2", "charset", "system-proxy", "blocking", "rustls-tls-native-roots"] }
2425
serde = { version = "1.0.228", features = ["derive"] }

src/env.rs

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::shell::SupportedShell;
44

55
use log::{debug, error, info, warn};
66
use serde::Deserialize;
7+
use std::fs;
78
use std::io::{Error, ErrorKind};
89
use std::path::{Component, Path, PathBuf};
910
use std::process::ExitCode;
@@ -21,7 +22,7 @@ pub enum Subcommand {
2122
/// Create an archive from an environment. Requires `conda-pack` to be in the environment.
2223
Pack(PackArgs),
2324
/// Unpack an archive to create an environment from it.
24-
Unpack(CommonEnvArgs),
25+
Unpack(UnpackArgs),
2526
/// List existing environments
2627
List,
2728
/// Display information about the micromamba setup
@@ -73,13 +74,23 @@ pub struct PackArgs {
7374
/// Common environment arguments
7475
#[command(flatten)]
7576
common: CommonEnvArgs,
76-
/// Output path/filename of the packed environment. If not specified, the
77-
/// same method is used to determine the environment name as for the
78-
/// "--name" parameter, and the default name is <env_name>.tar.gz
77+
/// Output path/filename of the packed environment, ending in .tar.gz. If
78+
/// not specified, the same method is used to determine the environment name
79+
/// as for the "--name" parameter, and the default name is <env_name>.tar.gz
7980
#[arg(long, short, value_name = "OUTPUT")]
8081
output: Option<String>,
8182
}
8283

84+
#[derive(Debug, clap::Args)]
85+
pub struct UnpackArgs {
86+
/// Common environment arguments
87+
#[command(flatten)]
88+
common: CommonEnvArgs,
89+
/// Path to a packed environment archive (ending in .tar.gz)
90+
#[arg(value_name = "ARCHIVE")]
91+
archive_path: PathBuf,
92+
}
93+
8394
/// Contains the fields we need from a parsed environment file.
8495
#[derive(Deserialize)]
8596
struct RobotmkEnv {
@@ -365,10 +376,74 @@ pub fn run(config: Config, subcommand: Subcommand) -> Result<(), ExitCode> {
365376
)
366377
.into()
367378
}
368-
_ => {
369-
println!("{:?}", config);
370-
println!("{:?}", subcommand);
371-
Ok(())
379+
Subcommand::Unpack(args) => {
380+
fn archive_name_to_env_name(archive_path: &Path) -> Option<String> {
381+
let filename = archive_path.file_name()?.to_str()?;
382+
filename
383+
.strip_suffix(".tar.gz")
384+
.or_else(|| filename.strip_suffix(".tgz"))
385+
.map(String::from)
386+
}
387+
let env_name = match args.common.name {
388+
Some(name) => name,
389+
None => match archive_name_to_env_name(&args.archive_path) {
390+
Some(name) => {
391+
info!(
392+
"Using '{}' as environment name, based on archive filename",
393+
name
394+
);
395+
name
396+
}
397+
None => {
398+
error!(
399+
"Could not determine environment name from archive filename. Please specify an environment name with --name."
400+
);
401+
return Err(ExitCode::FAILURE);
402+
}
403+
},
404+
};
405+
406+
// TODO: We really need a more generic error type to avoid this kind of mapping everywhere
407+
let target_env_path = micromamba::create_env_dir(&config, &env_name).map_err(|e| {
408+
error!("{}", e);
409+
ExitCode::FAILURE
410+
})?;
411+
412+
// Send the archive to flate2 to decompress and untar
413+
info!(
414+
"Unpacking archive '{}' to create environment '{}'",
415+
args.archive_path.display(),
416+
env_name
417+
);
418+
debug!("Opening '{}' for read", args.archive_path.display());
419+
let archive_file = fs::File::open(&args.archive_path).map_err(|e| {
420+
error!("{}", e);
421+
ExitCode::FAILURE
422+
})?;
423+
let decompressor = flate2::read::GzDecoder::new(archive_file);
424+
let mut archive = tar::Archive::new(decompressor);
425+
archive.unpack(&target_env_path).map_err(|e| {
426+
error!(
427+
"Could not unpack archive to '{}': {}",
428+
target_env_path.display(),
429+
e
430+
);
431+
ExitCode::FAILURE
432+
})?;
433+
info!(
434+
"Successfully unpacked environment to '{}'",
435+
target_env_path.display()
436+
);
437+
438+
info!("Running 'conda-unpack' in the new environment to fix paths...");
439+
let result = micromamba(
440+
&config,
441+
vec!["run", "--name", &env_name, "conda-unpack"],
442+
config.verbose,
443+
);
444+
let rc = result.exit_code();
445+
dump_micromamba_captured_output_on_error(&result, rc);
446+
result.into()
372447
}
373448
}
374449
}

src/micromamba.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,27 @@ pub fn bin_path_for_env(config: &Config, name: &str) -> Option<PathBuf> {
353353
let os_specific_bin = if cfg!(windows) { "Scripts" } else { "bin" };
354354
path_for_env(config, name).map(|p| p.join(os_specific_bin))
355355
}
356+
357+
/// Create a directory for a new environment, if it does not already exist.
358+
pub fn create_env_dir(config: &Config, name: &str) -> Result<PathBuf, std::io::Error> {
359+
let env_path = match path_for_env(config, name) {
360+
Some(path) => path,
361+
None => {
362+
return Err(io::Error::new(
363+
io::ErrorKind::NotFound,
364+
format!("Could not determine path for environment '{}'", name),
365+
));
366+
}
367+
};
368+
if env_path.exists() {
369+
return Err(io::Error::new(
370+
io::ErrorKind::AlreadyExists,
371+
format!(
372+
"The target environment directory '{:?}' already exists",
373+
env_path
374+
),
375+
));
376+
}
377+
std::fs::create_dir_all(&env_path)?;
378+
Ok(env_path)
379+
}

tests/env.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,43 @@ fn test_csm_env_pack_unpack() -> Result<(), Error> {
284284
.success()
285285
.stdout(predicate::str::contains("100% Completed"));
286286

287-
// TODO: unpack
287+
csm.command()
288+
.args(["env", "unpack", "-n", "unpacked", "packed.tar.gz"])
289+
.assert()
290+
.success()
291+
.stderr(predicate::str::contains(
292+
"Successfully unpacked environment",
293+
));
294+
295+
let (shell, cmd) = if cfg!(windows) {
296+
("powershell", "echo $env:CONDA_PROMPT_MODIFIER")
297+
} else {
298+
("sh", "echo $CONDA_PROMPT_MODIFIER")
299+
};
300+
301+
csm.command()
302+
.args(["env", "run", "-n", "unpacked", "--", shell, "-c", cmd])
303+
.assert()
304+
.success()
305+
.stdout(predicate::str::contains("(unpacked)"));
306+
307+
Ok(())
308+
}
309+
310+
/// Test `csm env unpack` environment name detection
311+
#[test]
312+
fn test_csm_env_unpack_detect_name() -> Result<(), Error> {
313+
let csm = common::Csm::new()?;
314+
315+
for path in ["/path/to/my_env.tar.gz", "my_env.tgz"] {
316+
csm.command()
317+
.args(["-n", "env", "unpack", path])
318+
.assert()
319+
.failure() // with -n, it won't be able to resolve paths, so it'll fail
320+
.stderr(predicate::str::contains(
321+
"Using 'my_env' as environment name",
322+
));
323+
}
288324

289325
Ok(())
290326
}

0 commit comments

Comments
 (0)