From b6e5b4f00692b37b0168188cd888917d96abbda2 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Sun, 20 Sep 2026 23:29:03 +0200 Subject: [PATCH] refactor: split ecr into library and cli crates The root package becomes a virtual workspace: `crates/ecr` holds the library (package name `ecr`) and `crates/ecr-cli` the command line front-end, which keeps installing the `ecr` binary. No behavior change. The library must not depend on CLI types, so mount::setup_mounts now takes the `no_bind` flag instead of a `&Args`. --- Cargo.lock | 12 +++++++- Cargo.toml | 46 +++------------------------- crates/ecr-cli/Cargo.toml | 28 +++++++++++++++++ {src => crates/ecr-cli/src}/cli.rs | 0 {src => crates/ecr-cli/src}/main.rs | 34 +++++--------------- crates/ecr/Cargo.toml | 43 ++++++++++++++++++++++++++ {src => crates/ecr/src}/chroot.rs | 0 {src => crates/ecr/src}/config.rs | 0 {src => crates/ecr/src}/distro.rs | 0 {src => crates/ecr/src}/download.rs | 0 {src => crates/ecr/src}/extract.rs | 0 {src => crates/ecr/src}/kernel.rs | 0 crates/ecr/src/lib.rs | 30 ++++++++++++++++++ {src => crates/ecr/src}/mount.rs | 6 ++-- {src => crates/ecr/src}/namespace.rs | 0 {src => crates/ecr/src}/qemu.rs | 0 {src => crates/ecr/src}/qemu_vm.rs | 0 {src => crates/ecr/src}/utils.rs | 3 ++ {src => crates/ecr/src}/verbose.rs | 0 19 files changed, 129 insertions(+), 73 deletions(-) create mode 100644 crates/ecr-cli/Cargo.toml rename {src => crates/ecr-cli/src}/cli.rs (100%) rename {src => crates/ecr-cli/src}/main.rs (95%) create mode 100644 crates/ecr/Cargo.toml rename {src => crates/ecr/src}/chroot.rs (100%) rename {src => crates/ecr/src}/config.rs (100%) rename {src => crates/ecr/src}/distro.rs (100%) rename {src => crates/ecr/src}/download.rs (100%) rename {src => crates/ecr/src}/extract.rs (100%) rename {src => crates/ecr/src}/kernel.rs (100%) create mode 100644 crates/ecr/src/lib.rs rename {src => crates/ecr/src}/mount.rs (99%) rename {src => crates/ecr/src}/namespace.rs (100%) rename {src => crates/ecr/src}/qemu.rs (100%) rename {src => crates/ecr/src}/qemu_vm.rs (100%) rename {src => crates/ecr/src}/utils.rs (98%) rename {src => crates/ecr/src}/verbose.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index fd092d7..5d914f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -302,7 +302,6 @@ version = "0.1.0" dependencies = [ "anyhow", "base64", - "clap", "cpio", "dirs", "flate2", @@ -323,6 +322,17 @@ dependencies = [ "zstd", ] +[[package]] +name = "ecr-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "dirs", + "ecr", + "tempfile", +] + [[package]] name = "either" version = "1.16.0" diff --git a/Cargo.toml b/Cargo.toml index ff246bc..cb72c0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,50 +1,14 @@ -[package] -name = "ecr" +[workspace] +resolver = "2" +members = ["crates/ecr", "crates/ecr-cli"] + +[workspace.package] version = "0.1.0" edition = "2021" rust-version = "1.77" -description = "Enter chroot environments with Linux namespaces" license = "MIT" authors = ["Valentin Haudiquet"] -[dependencies] -# CLI parsing -clap = { version = "4", features = ["derive", "env"] } - -# Config parsing -serde = { version = "1", features = ["derive"] } -serde_yaml = "0.9" - -# HTTP downloads -reqwest = { version = "0.13", features = ["blocking", "stream"] } - -# Tarball extraction -tar = "0.4" -flate2 = "1" -xz2 = "0.1" -zstd = "0.13" - -# Unix syscall bindings -nix = { version = "0.31", features = ["fs", "mount", "sched", "signal", "user", "process", "hostname"] } - -# Temp directories -tempfile = "3" - -# Error handling -anyhow = "1" - -# Utilities -dirs = "6" -which = "7" -cpio = "0.4" -base64 = "0.22" -tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util"] } -futures-util = "0.3" -indicatif = "0.18" -serde_json = "1" -libc = "0.2" -users = "0.11" - [profile.release] strip = true opt-level = "z" diff --git a/crates/ecr-cli/Cargo.toml b/crates/ecr-cli/Cargo.toml new file mode 100644 index 0000000..b7279cb --- /dev/null +++ b/crates/ecr-cli/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "ecr-cli" +description = "Enter chroot environments with Linux namespaces" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true + +# The installed binary keeps the `ecr` name even though the CLI package is +# `ecr-cli`; the library package owns the `ecr` name for consumers. +[[bin]] +name = "ecr" +path = "src/main.rs" + +[dependencies] +ecr = { path = "../ecr" } + +# CLI parsing +clap = { version = "4", features = ["derive", "env"] } + +# Error handling +anyhow = "1" + +# Interim: used by the CLI's inline cache/extraction orchestration until it +# moves behind the library's rootfs API +dirs = "6" +tempfile = "3" diff --git a/src/cli.rs b/crates/ecr-cli/src/cli.rs similarity index 100% rename from src/cli.rs rename to crates/ecr-cli/src/cli.rs diff --git a/src/main.rs b/crates/ecr-cli/src/main.rs similarity index 95% rename from src/main.rs rename to crates/ecr-cli/src/main.rs index 7968c62..f56dafa 100644 --- a/src/main.rs +++ b/crates/ecr-cli/src/main.rs @@ -1,37 +1,17 @@ -mod chroot; mod cli; -mod config; -mod distro; -mod download; -mod extract; -mod kernel; -mod mount; -mod namespace; -mod qemu; -mod qemu_vm; -mod utils; -mod verbose; - -/// Print to stderr only when --verbose / -v is active. -#[macro_export] -macro_rules! veprintln { - ($($arg:tt)*) => { - if $crate::verbose::is_verbose() { - eprintln!($($arg)*); - } - }; -} use anyhow::{Context, Result}; use clap::Parser; use cli::Args; -use config::Config; -use distro::{ +use ecr::chroot; +use ecr::config::Config; +use ecr::distro::{ map_arch, parse_image_ref, resolve_distro_url, resolve_distro_version, Distro, ImageSource, }; -use download::{digest_sidecar, download_image, fetch_oci_digest}; -use extract::extract_tarball; +use ecr::download::{digest_sidecar, download_image, fetch_oci_digest}; +use ecr::extract::extract_tarball; +use ecr::{kernel, mount, namespace, qemu, qemu_vm, utils, veprintln, verbose}; fn main() -> Result<()> { let args = Args::parse(); @@ -227,7 +207,7 @@ fn namespace_mode(args: Args, rootfs: std::path::PathBuf, config: Config) -> Res &rootfs_clone, &bind_paths_clone, &bind_rw_paths_clone, - &args_clone, + args_clone.no_bind, )?; // Write resolv.conf with DNS from config diff --git a/crates/ecr/Cargo.toml b/crates/ecr/Cargo.toml new file mode 100644 index 0000000..3ba33cb --- /dev/null +++ b/crates/ecr/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "ecr" +description = "Ephemeral chroot environments with Linux namespaces" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +# Config parsing +serde = { version = "1", features = ["derive"] } +serde_yaml = "0.9" + +# HTTP downloads +reqwest = { version = "0.13", features = ["blocking", "stream"] } + +# Tarball extraction +tar = "0.4" +flate2 = "1" +xz2 = "0.1" +zstd = "0.13" + +# Unix syscall bindings +nix = { version = "0.31", features = ["fs", "mount", "sched", "signal", "user", "process", "hostname"] } + +# Temp directories +tempfile = "3" + +# Error handling +anyhow = "1" + +# Utilities +dirs = "6" +which = "7" +cpio = "0.4" +base64 = "0.22" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util"] } +futures-util = "0.3" +indicatif = "0.18" +serde_json = "1" +libc = "0.2" +users = "0.11" diff --git a/src/chroot.rs b/crates/ecr/src/chroot.rs similarity index 100% rename from src/chroot.rs rename to crates/ecr/src/chroot.rs diff --git a/src/config.rs b/crates/ecr/src/config.rs similarity index 100% rename from src/config.rs rename to crates/ecr/src/config.rs diff --git a/src/distro.rs b/crates/ecr/src/distro.rs similarity index 100% rename from src/distro.rs rename to crates/ecr/src/distro.rs diff --git a/src/download.rs b/crates/ecr/src/download.rs similarity index 100% rename from src/download.rs rename to crates/ecr/src/download.rs diff --git a/src/extract.rs b/crates/ecr/src/extract.rs similarity index 100% rename from src/extract.rs rename to crates/ecr/src/extract.rs diff --git a/src/kernel.rs b/crates/ecr/src/kernel.rs similarity index 100% rename from src/kernel.rs rename to crates/ecr/src/kernel.rs diff --git a/crates/ecr/src/lib.rs b/crates/ecr/src/lib.rs new file mode 100644 index 0000000..531a15b --- /dev/null +++ b/crates/ecr/src/lib.rs @@ -0,0 +1,30 @@ +//! ecr — ephemeral chroot environments with Linux namespaces. +//! +//! This crate is the library behind the `ecr` CLI. It resolves distro and +//! OCI image references, downloads them through a content cache, extracts +//! the rootfs into a scratch directory, and runs commands inside +//! unprivileged user/PID/mount/UTS namespaces — or boots it in a QEMU VM +//! (see the `qemu_vm` module). + +pub mod chroot; +pub mod config; +pub mod distro; +pub mod download; +pub mod extract; +pub mod kernel; +pub mod mount; +pub mod namespace; +pub mod qemu; +pub mod qemu_vm; +pub mod utils; +pub mod verbose; + +/// Print to stderr only when verbose mode is active (see [`verbose::set`]). +#[macro_export] +macro_rules! veprintln { + ($($arg:tt)*) => { + if $crate::verbose::is_verbose() { + eprintln!($($arg)*); + } + }; +} diff --git a/src/mount.rs b/crates/ecr/src/mount.rs similarity index 99% rename from src/mount.rs rename to crates/ecr/src/mount.rs index dd776b6..3bf396f 100644 --- a/src/mount.rs +++ b/crates/ecr/src/mount.rs @@ -21,15 +21,13 @@ fn escape_overlay_path(path: &Path) -> Result { Ok(s.replace('\\', "\\\\").replace(',', "\\,")) } -use crate::cli::Args; - /// Setup all required mounts inside the chroot /// Returns a TempDir that must be kept alive for the duration of the chroot pub fn setup_mounts( rootfs: &Path, bind_paths: &[std::path::PathBuf], bind_rw_paths: &[std::path::PathBuf], - args: &Args, + no_bind: bool, ) -> Result> { // Keep all overlay temp dirs alive let mut overlay_temps: Vec = Vec::new(); @@ -60,7 +58,7 @@ pub fn setup_mounts( } // Setup overlay mounts for bind paths (read-only via overlay) - if !args.no_bind { + if !no_bind { for bind_path in bind_paths { // Skip if this path is also in bind_rw (bind_rw takes precedence) if !bind_rw_paths.contains(bind_path) { diff --git a/src/namespace.rs b/crates/ecr/src/namespace.rs similarity index 100% rename from src/namespace.rs rename to crates/ecr/src/namespace.rs diff --git a/src/qemu.rs b/crates/ecr/src/qemu.rs similarity index 100% rename from src/qemu.rs rename to crates/ecr/src/qemu.rs diff --git a/src/qemu_vm.rs b/crates/ecr/src/qemu_vm.rs similarity index 100% rename from src/qemu_vm.rs rename to crates/ecr/src/qemu_vm.rs diff --git a/src/utils.rs b/crates/ecr/src/utils.rs similarity index 98% rename from src/utils.rs rename to crates/ecr/src/utils.rs index 60dbca1..9e556a5 100644 --- a/src/utils.rs +++ b/crates/ecr/src/utils.rs @@ -42,6 +42,9 @@ pub enum Arch { impl Arch { /// Get the architecture from a string (any common naming convention) + /// Not a FromStr impl: unrecognized names map to Arch::Unknown rather + /// than an error, by design. + #[allow(clippy::should_implement_trait)] pub fn from_str(s: &str) -> Self { match s { "amd64" | "x86_64" | "x64" => Arch::Amd64, diff --git a/src/verbose.rs b/crates/ecr/src/verbose.rs similarity index 100% rename from src/verbose.rs rename to crates/ecr/src/verbose.rs