anyhow at the public boundary left library consumers unable to match
on failure modes: every mistake was an opaque error, and the ones
worth branching on (host prerequisites, unsupported architectures)
could only be recognized by string matching.
Public functions now return Result<T, ecr::Error> (thiserror) with
semantic variants for those conditions: user namespaces unavailable,
binfmt_misc unregistered, architecture not served by the registry,
missing subuid/subgid entry. anyhow stays the internal error type;
everything else converts to Error::Other, which transparently wraps
the full context chain. Errors raised inside the namespace child
cross the process boundary as formatted chains and also surface as
Other; the error pipe wraps them in anyhow before formatting so the
chain still survives. The CLI maps the error back into anyhow, so
its output is unchanged.
Downloaded content was hashed nowhere: OCI layer digests were only
used to build blob URLs and distro tarballs were trusted as served,
so a truncated or tampered rootfs went straight into the cache and
from there into execution.
Hash sha256 and sha512 while streaming and verify before the atomic
rename, so a bad download never enters the cache:
- OCI architecture manifests requested by digest are checked against
that digest, since the layer digests read from them are trusted
next; layer blobs are checked against the digest the manifest names.
- Direct distro tarballs are checked against the checksums published
next to them (Alpine: per-file .sha256; Ubuntu: SHA256SUMS in the
release directory). A missing or unfetchable checksum artifact
degrades to an unverified download with a warning, mirroring the
freshness-check policy; a mismatch always fails.
- Digest algorithms other than sha256/sha512 cannot be checked: warn
and accept.
The default --bind overlay mounted the current directory at
/root/<basename>, but the command still started in /root: the
library's working-directory fallback only considers read-write
binds. Pass the mapped target as the explicit working_dir so
relative paths keep meaning what they meant on the host.
Bind sources are canonicalized when the targets are built
instead of at mount time. This fixes --bind . and other
basename-less relative paths, which cli_mount_target rejected,
and lets the --bind/--bind-rw dedup match relative and absolute
spellings of the same directory.
Diagnostics now go through the log facade: the library no longer
decides where output goes, and consumers plug in their own sink (the
CLI installs a stderr logger printing warnings always and ecr debug
diagnostics under -v; dependency records stay filtered out). The
kernel 'quiet' flag moves from the global verbosity flag to an
explicit QemuConfig field.
The CLI owns the terminal now: TermProgress turns the library's
Progress events into bars and spinners on stderr (hidden by indicatif
when stderr is not a terminal). Named transfers (OCI layers, kernel,
tarball) render as byte bars, extraction and initramfs as file-count
spinners.
Add ecr::rootfs with the full rootfs lifecycle behind the library:
- RootfsCache::prepare resolves an image reference, downloads through
the tarball cache (with the OCI :latest digest freshness check) and
extracts into a scratch directory tracked by PreparedRootfs.
- PreparedRootfs::persist packs the current rootfs back into its cache
entry (compressed to match the entry's extension, symlinks and
permissions preserved) and marks it with a .provisioned sidecar.
- RootfsCache::prepare_provisioned composes both into the hot-cell
flow: provision once, and every later call sharing the cache skips
the download and the provisioning step.
extract: an "oci-" cache entry without a layers.manifest is a
persisted provisioned rootfs; extract it as a plain archive.
The CLI now drives prepare and drops its inline cache/orchestration
code and the dirs/tempfile dependencies. The binfmt check moves ahead
of the download so foreign-arch runs fail before pulling an image.
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`.
- hard links in the rootfs were written as zero-size entries with no
inode set (all zero), so the kernel could link them to the wrong file;
assign a synthetic inode per (device, inode) group.
- build the cpio archive by streaming entries to disk instead of holding
the whole archive in memory.
- the VM command was joined into a single string and shell-expanded in
the guest, mangling quotes and nested commands; pass the argv
base64-encoded (ECR_ARGV) and exec it verbatim.
- parse ecr parameters from /proc/cmdline in the init script, set the
hostname via procfs (arch has no hostname binary), and give the serial
console a moment to drain before poweroff.
Add --kernel <PATH> option to boot extracted rootfs in a QEMU virtual
machine instead of namespace/chroot mode. The rootfs is converted to an
ext4 disk image using mke2fs and booted with the provided kernel.