From 57367faa01ea2185d0f3dae3546eefb4ed0d6d06 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Sat, 3 Oct 2026 00:13:34 +0200 Subject: [PATCH] docs: document the library/CLI output split The library is terminal-silent: diagnostics through the log facade, progress through Progress sinks. The CLI owns all rendering. --- README.md | 8 ++++---- SPEC.md | 24 +++++++++++++++++++++--- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5e9f1e9..22a763a 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ ecr fedora # any Docker Hub image ## Library -`ecr` is a cargo workspace: `crates/ecr` is the library crate (package `ecr`), `crates/ecr-cli` the thin command-line front-end. Rust consumers can use the library directly — resolve an image, download it through the content cache, and run commands with a fully caller-controlled environment, bind targets and architecture: +`ecr` is a cargo workspace: `crates/ecr` is the library crate (package `ecr`), `crates/ecr-cli` the thin command-line front-end. The library never writes to the terminal: diagnostics go through the `log` crate, and downloads, extraction and initramfs creation report progress through a caller-supplied `Progress` sink (pass `&NoProgress` for silence, or implement the trait to render your own UI). Rust consumers can use the library directly — resolve an image, download it through the content cache, and run commands with a fully caller-controlled environment, bind targets and architecture: ```toml [dependencies] @@ -26,11 +26,11 @@ ecr = { git = "https://github.com/…" } # or a path / registry source ``` ```rust -use ecr::{chroot, BindTarget, ExecOptions, PrepareRequest, RootfsCache}; +use ecr::{chroot, BindTarget, ExecOptions, NoProgress, PrepareRequest, RootfsCache}; // Download alpine through the cache (~/.cache/ecr) and extract to a scratch dir let cache = RootfsCache::new(RootfsCache::default_dir()?); -let rootfs = cache.prepare(&PrepareRequest::new("alpine:3.23"))?; +let rootfs = cache.prepare(&PrepareRequest::new("alpine:3.23"), &NoProgress)?; // Caller-composed envp, explicit bind targets, target architecture let code = rootfs.exec(&ExecOptions { @@ -53,7 +53,7 @@ rootfs.persist()?; // write the rootfs back into the cache (see below) Prepare once, provision once, amortize every later run — including runs from other processes sharing the same cache directory: ```rust -let rootfs = cache.prepare_provisioned(&PrepareRequest::new("alpine:3.23"), |rootfs| { +let rootfs = cache.prepare_provisioned(&PrepareRequest::new("alpine:3.23"), &NoProgress, |rootfs| { // runs at most once per cache entry rootfs.exec(&ExecOptions { command: vec!["apk".into(), "add".into(), "curl".into()], diff --git a/SPEC.md b/SPEC.md index a15f694..39785ed 100644 --- a/SPEC.md +++ b/SPEC.md @@ -10,7 +10,13 @@ Cargo workspace with two crates: - `crates/ecr-cli` — the CLI front-end (package `ecr-cli`, binary `ecr`). Parses flags, maps them onto the library API, propagates exit codes. -The library must not depend on CLI types (`clap` is a CLI-only dependency). +The library must not depend on CLI types (`clap` is a CLI-only +dependency), and it never writes to the terminal: diagnostics go +through the `log` facade, and long-running operations (downloads, +extraction, initramfs creation) report progress through an +`ecr::progress::Progress` sink (`NoProgress` discards everything). +All terminal output — the logger driven by `-v`, the bars and +spinners (indicatif) — is owned by `crates/ecr-cli`. ## Library API @@ -18,12 +24,13 @@ The library must not depend on CLI types (`clap` is a CLI-only dependency). - `RootfsCache::new(dir)` / `RootfsCache::default_dir()` — the image tarball cache (default `~/.cache/ecr`). -- `cache.prepare(&PrepareRequest)` — parse the image reference, resolve +- `cache.prepare(&req, &progress)` — parse the image reference, resolve floating version aliases ("latest", "lts", "edge") to a concrete version before computing the cache key, run the OCI `:latest` digest freshness check, download through the cache when needed, and extract into a scratch directory. Returns a `PreparedRootfs` (TempDir-backed; dropped on drop). -- `cache.prepare_provisioned(&req, provision)` — hot-cell amortization: on a +- `cache.prepare_provisioned(&req, &progress, provision)` — hot-cell + amortization: on a cache hit with a `.provisioned` sidecar the entry is extracted as-is (no freshness check, no provisioning); otherwise the rootfs is prepared, `provision(&PreparedRootfs)` runs once, and `persist` writes the result @@ -58,6 +65,17 @@ the bind targets; `in_rootfs(rootfs, target)` maps in-chroot paths and rejects escaping targets. Overlay upper/work directories are temp dirs the caller must keep alive (returned by `setup_mounts`). +### Progress and logging (`ecr::progress`) + +- `Progress` — sink trait for sequential operations: `begin(label, unit, + total)` starts the next operation (implicitly ending the previous one), + `advance(n)` counts completed units, `finish` ends it. Consumers + implement it to render UI; `NoProgress` discards every event. +- Diagnostics go through `log`: `debug` for narration (URLs, layer info, + extraction steps), `warn` for recoverable conditions (unreadable + entries, failed best-effort setup). Nothing is emitted unless the + consumer installs a logger. + ## Synopsis ```