feat(cli): docker-style run flags and local rootfs references

The run interface read like a library's. `ecrou run -it alpine` now
hands the guest a terminal: with no console output file, libkrun wires
the guest console to the process's own stdio and raw-modes the tty.
The parent saves and restores the termios around the run, because
libkrun's own restore lives in the child's exit observers, which a
SIGKILLed machine never runs. A tty run refuses to start without a
terminal on stdin and stdout, and --capture refuses to combine with
-t: the payload's streams cannot land in files and on the terminal at
once.

-i without -t keeps the machine's stdin open through pipes: libkrun
exposes the non-tty streams as virtio console ports (krun-stdin,
krun-stdout, krun-stderr), and the guest init points the payload's
stdio at whichever port nodes exist. `echo hi | ecrou run -i alpine --
cat` crosses the machine; a stream the host left on a tty has no port
node and stays on the console.

-v (--volume HOST:GUEST[:ro]) replaces --share, and -w aliases
--workdir. The global -v/--verbose loses its short flag to make room;
verbose stays reachable by its long form.

A reference starting with ./, ../ or / names a local rootfs archive:
read in place on every run, never downloaded, never cached as a
pristine entry. Provisioning over one persists into a derived cache
entry keyed by size and mtime, so the caller's file is never
overwritten and an edited archive lands in a fresh entry.
This commit is contained in:
vhaudiquet committed 2026-10-29 18:08:30 +01:00
1 parent 719a8209e0
commit 4a0c613ecb
9 files changed
+569 -72

No files matched your search

+14 -3
View File
@@ -10,7 +10,7 @@ use clap::Parser;
use ecrou::vm::RunOptions;
use ecrou::{LogLevel, NoProgress, Progress};
use cli::{parse_env, parse_share, Cli, Command, RunArgs};
use cli::{parse_env, parse_volume, Cli, Command, RunArgs};
use progress::TermProgress;
fn main() -> std::process::ExitCode {
@@ -157,6 +157,15 @@ fn cmd_provision(
}
fn cmd_run(args: &RunArgs, cli_verbose: bool) -> std::process::ExitCode {
// A tty drives the payload's output through the terminal while the
// capture share points the same streams at files: one of them has
// to lose, so the run refuses instead of picking.
if args.tty && args.capture.is_some() {
return fail(&anyhow::anyhow!(
"--capture and --tty cannot be combined: the payload's output \
cannot land in the capture files and on the terminal"
));
}
let mut options = RunOptions {
// Without a layer the image is required by clap; with one the
// field is dead — the layer entry carries the rootfs.
@@ -171,6 +180,8 @@ fn cmd_run(args: &RunArgs, cli_verbose: bool) -> std::process::ExitCode {
} else {
LogLevel::Normal
},
interactive: args.interactive,
tty: args.tty,
no_cache: args.no_cache,
layer: args.layer.clone(),
capture: args.capture.clone(),
@@ -183,8 +194,8 @@ fn cmd_run(args: &RunArgs, cli_verbose: bool) -> std::process::ExitCode {
}
}
let mut shares = Vec::new();
for spec in &args.shares {
match parse_share(spec) {
for spec in &args.volumes {
match parse_volume(spec) {
Ok(share) => shares.push(share),
Err(err) => return fail(&err),
}