src/cli_install.rs
//! THE CLI DOOR — install ONE already-built executable into a bin directory and
//! then **prove the bytes landed**.
//!
//! # Why this module exists
//!
//! Skíðblaðnir already owned three install doors, and every one of them leads to a
//! *service*: [`install_product`](crate::install::install_product) renders a systemd
//! unit, [`install_prefix_payload`](crate::install::install_prefix_payload) unfolds a
//! bundle into a generation layout, and
//! [`refresh_product`](crate::install::refresh_product) is build → install → migrate →
//! restart over that layout. None of them installs a **bare command** into a bin
//! directory, because a command is not a unit: it has no `ExecStart`, no PID to probe,
//! no port to knock on.
//!
//! So the constellation's fat CLIs had no door at all. MEASURED on oden 2026-08-25:
//! `/usr/local/bin/dwarves` was `0.1.4 (2079ba109c22)`, built 2026-08-14, while the
//! schema it validates against had moved on — `RunKind::Tests` (`run = { tests = … }`)
//! landed 2026-08-25 in `ab58b12`. The installed validator therefore refused a pipeline
//! that is *correct*, and refused it with a parse error that names the file rather than
//! the eleven-day gap that actually caused it. Two lanes lost time to that
//! independently. The bug was never in the pipeline; it was that **code had not reached
//! the installed artifact.**
//!
//! # The one thing this door does that a copy does not
//!
//! It **asserts the artifact on disk**, never the exit status of the copy.
//!
//! Two independent things can go wrong and only one of them shows up in a status:
//!
//! - the bytes did not land (a copy that succeeded onto the wrong path, a truncated
//! write, a destination someone else replaced) — [`verify_installed_binary`] re-hashes
//! the **destination** and compares it to the artifact that was handed in;
//! - the bytes landed but are not the build you meant — [`probe_version_line`] runs the
//! installed file and [`CliInstallRequest::expect_marker`] requires the git hash it
//! prints to be the one expected.
//!
//! `probe_version_line` gates on **stdout**, not on the status, because a program that
//! exits 0 and says nothing is entirely ordinary — the module's own test installs one
//! (a real `#!/bin/true` program: status 0, empty stdout) and requires the probe to
//! refuse it. A probe written as `if !status.success() { bail }` passes that binary and
//! would ship a command that cannot name its own build.
//!
//! # Why the write is a rename and not a copy
//!
//! You cannot write onto a running program. MEASURED on oden 2026-08-25: with a live
//! process holding the text segment, `cp` onto its path fails `ETXTBSY` ("Text file
//! busy", `errno` 26); staging beside it and `rename(2)`-ing over it succeeds, because
//! rename unlinks the old inode and the running process keeps it alive. Since the whole
//! point of this door is to update a command that someone may be running *right now* —
//! including the very binary invoking the door — the staging file is not a nicety. It is
//! the only way in.
//!
//! The staging file is created **inside the destination directory** so the rename is
//! same-filesystem and therefore atomic. A reader of `<bin>/dwarves` sees the old bytes
//! or the new bytes, never a torn file.
//!
//! # What is reused
//!
//! - [`UpgradeSource::executable`](crate::install::UpgradeSource::executable) — refuses
//! an absent, non-file or non-executable source. A lifecycle verb installs an artifact
//! that already exists; it never builds one.
//! - [`crate::running::binary_sha256`] — which is `nornir-hash`, the constellation's ONE
//! content-hash primitive. Never a hand-rolled digest.
//! - The "verbs and results as data" shape of [`crate::install::InstallOutcome`] and
//! [`crate::install::PrefixInstallOutcome`]: [`CliInstallOutcome`] is plain serde, so a
//! test asserts exactly what landed rather than that a function returned `Ok`.
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use crate::install::UpgradeSource;
/// Mode a CLI is installed with: readable and executable by all, writable by the owner.
pub const CLI_MODE: u32 = 0o755;
/// The argv a version probe appends to the installed path. Every fat client in the
/// constellation stamps its git hash here — that is what makes a stale install
/// *visible* instead of merely old.
pub const VERSION_ARGV: &[&str] = &["--version"];
/// What to install, where, and what must be true afterwards.
///
/// There is no `Default`: a door with a defaulted destination is how a binary lands
/// somewhere nobody looks.
#[derive(Debug, Clone)]
pub struct CliInstallRequest<'a> {
/// The command name — the file name it takes in `bin_dir` (`dwarves`, `nornir`, …).
pub name: &'a str,
/// The ALREADY-BUILT executable to install. Validated by
/// [`UpgradeSource::executable`]; this door never builds.
pub source: &'a Path,
/// The directory the command lands in — `~/.cargo/bin`, `/usr/local/bin`, or a
/// scratch prefix. This is the bin directory itself, not a prefix with `bin/`
/// beneath it, because both real destinations on oden already ARE bin directories.
pub bin_dir: &'a Path,
/// Run `<installed> --version` after the install and keep the line.
///
/// Off means the outcome carries no `version_line` — never that the probe passed.
pub probe_version: bool,
/// A substring the version output MUST contain — in practice the git hash of the
/// commit that was built. `Some(_)` implies the probe: the whole reason to stamp a
/// hash into `--version` is to assert it from the outside.
pub expect_marker: Option<&'a str>,
}
/// The fully-inspectable result of an install — plain data, so a caller reports and a
/// test asserts the same facts.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CliInstallOutcome {
/// The command name installed.
pub name: String,
/// The artifact that was handed in.
pub source: PathBuf,
/// Where it now is.
pub installed: PathBuf,
/// The digest **re-read from the installed file after the rename** — not the
/// digest of the source, which would make this field a tautology.
pub sha256: String,
/// The digest of the file that occupied `installed` before, when there was one.
/// `None` on a first install.
pub replaced: Option<String>,
/// Did the bytes at `installed` actually change? A reinstall of identical bytes is
/// a legitimate no-op and says so rather than claiming an update.
pub changed: bool,
/// The first non-empty stdout line of `<installed> --version`, when probed.
pub version_line: Option<String>,
/// Mode the installed file carries.
pub mode: u32,
}
impl CliInstallOutcome {
/// One line for an operator: what landed, where, and the proof.
pub fn summary(&self) -> String {
let state = if self.changed { "installed" } else { "unchanged" };
let ver = self
.version_line
.as_deref()
.map(|v| format!(" — {v}"))
.unwrap_or_default();
format!(
"{state} {} at {} (sha256 {}){ver}",
self.name,
self.installed.display(),
&self.sha256[..self.sha256.len().min(12)],
)
}
}
/// Hash a file through the constellation's one primitive, as `anyhow`.
fn digest(path: &Path) -> Result<String> {
crate::running::binary_sha256(path).map_err(|e| anyhow::anyhow!(e))
}
/// **THE GATE** — is the file at `installed` byte-for-byte the artifact `want_sha256`
/// describes?
///
/// Hashes the file **on disk**, right now. Not a flag set by the copy, not the copy's
/// exit status, not a length. The error names both digests and the path, because an
/// installer that says only "mismatch" sends the reader back to compute what it
/// already knew.
///
/// Returns the digest it read, so a caller stores the value it PROVED rather than the
/// value it hoped for.
pub fn verify_installed_binary(installed: &Path, want_sha256: &str) -> Result<String> {
if !installed.exists() {
anyhow::bail!(
"nothing at {} after the install — the install reported no error and produced \
no file, which is exactly the shape an exit-status check cannot see",
installed.display()
);
}
let got = digest(installed).with_context(|| {
format!(
"hash the installed file {} to verify it against the artifact",
installed.display()
)
})?;
if got != want_sha256 {
anyhow::bail!(
"{} is NOT the artifact that was installed: on disk sha256:{got}, built \
sha256:{want_sha256}. The bytes did not land — treat this install as failed, \
not as a warning.",
installed.display()
);
}
Ok(got)
}
/// Run `<installed> --version` and return its first non-empty **stdout** line.
///
/// Zero-shell: an argv `Command`, never `sh -c`. The exit status is deliberately NOT
/// the gate — see the module docs for the libtest binary that prints an error and exits
/// 0. Empty stdout is the failure, whatever the status was.
pub fn probe_version_line(installed: &Path) -> Result<String> {
let out = std::process::Command::new(installed)
.args(VERSION_ARGV)
.output()
.with_context(|| {
format!(
"run `{} --version` — the installed file exists but could not be executed",
installed.display()
)
})?;
let line = String::from_utf8_lossy(&out.stdout)
.lines()
.map(str::trim)
.find(|l| !l.is_empty())
.map(str::to_string);
match line {
Some(l) => Ok(l),
None => {
let stderr = String::from_utf8_lossy(&out.stderr);
let stderr = stderr.lines().find(|l| !l.trim().is_empty()).unwrap_or("");
anyhow::bail!(
"`{} --version` printed NOTHING on stdout (exit status {}{}) — a program \
that cannot name its own build cannot be checked for staleness, and the \
status alone would have called this fine",
installed.display(),
out.status,
if stderr.is_empty() {
String::new()
} else {
format!("; stderr: {stderr}")
},
)
}
}
}
/// **Install one already-built command into a bin directory, then prove it landed.**
///
/// The sequence, and why it is this sequence:
///
/// 1. Validate the source through [`UpgradeSource::executable`] — absent, a directory,
/// or missing its execute bit are all refusals, not warnings.
/// 2. Hash the source. This is the `want` every later step is measured against.
/// 3. Record the digest of whatever already occupies the destination, so the outcome can
/// say *replaced X with Y* instead of *did something*.
/// 4. Copy to a staging file **inside `bin_dir`** and set [`CLI_MODE`] on it.
/// 5. `rename(2)` staging → destination. Atomic (same filesystem) and the only write
/// that survives the destination being a running program (`ETXTBSY`).
/// 6. [`verify_installed_binary`] — re-hash the destination and compare.
/// 7. Optionally probe `--version` and require [`CliInstallRequest::expect_marker`] in
/// the output.
///
/// A failure anywhere after step 4 removes the staging file. A failure at step 6 or 7
/// leaves the destination as it is and reports — the caller must see a hard error, not a
/// silently reverted install it never learns about.
pub fn install_cli_binary(req: &CliInstallRequest<'_>) -> Result<CliInstallOutcome> {
let out = install_cli_binary_impl(req);
match &out {
Ok(o) => crate::surface_status!(
"skidbladnir/cli_install",
"install_cli_binary",
true,
"name={} dest={} sha256={} changed={}",
o.name,
o.installed.display(),
o.sha256,
o.changed,
),
Err(e) => crate::surface_status!(
"skidbladnir/cli_install",
"install_cli_binary",
false,
"install cli `{}` failed: {:#}",
req.name,
e,
),
}
out
}
fn install_cli_binary_impl(req: &CliInstallRequest<'_>) -> Result<CliInstallOutcome> {
if req.name.is_empty() || req.name.contains(std::path::MAIN_SEPARATOR) {
anyhow::bail!(
"`{}` is not a command name — it is the file name the command takes inside \
the bin directory, so it carries no path separators",
req.name
);
}
// 1. The artifact must already exist and be runnable. Reused wholesale.
let source = match UpgradeSource::executable(req.source)? {
UpgradeSource::Executable(p) => p,
// `executable()` cannot return this; matched so a future variant is a compile
// error here rather than a silent wrong install.
UpgradeSource::Bundle(p) => anyhow::bail!(
"{} is a packed bundle, not a command — install it with \
`install_prefix_payload` or `refresh_product`",
p.display()
),
};
// 2. The digest every later assertion is measured against.
let want = digest(&source)
.with_context(|| format!("hash the artifact {} before installing it", source.display()))?;
std::fs::create_dir_all(req.bin_dir)
.with_context(|| format!("create the bin directory {}", req.bin_dir.display()))?;
let installed = req.bin_dir.join(req.name);
if installed.is_dir() {
anyhow::bail!(
"{} is a DIRECTORY — refusing to install a command over it",
installed.display()
);
}
// 3. What is being replaced, by content. A first install reports `None`, which is a
// different fact from "replaced identical bytes".
let replaced = if installed.exists() {
Some(digest(&installed).with_context(|| {
format!(
"hash the file already at {} — an install that cannot say what it \
replaced cannot be audited",
installed.display()
)
})?)
} else {
None
};
// 4. Stage INSIDE the destination directory: same filesystem, so step 5 is atomic.
let staging = req.bin_dir.join(format!(
".{}.new-{}",
req.name,
std::process::id()
));
let staged = stage_then_place(&source, &staging, &installed);
if staged.is_err() {
let _ = std::fs::remove_file(&staging);
}
staged?;
// 6. THE GATE. Hash what is actually on disk.
let sha256 = verify_installed_binary(&installed, &want)?;
// 7. The outside view of the build: what the program says it is.
let version_line = if req.probe_version || req.expect_marker.is_some() {
Some(probe_version_line(&installed)?)
} else {
None
};
if let Some(marker) = req.expect_marker {
let line = version_line.as_deref().unwrap_or("");
if !line.contains(marker) {
anyhow::bail!(
"{} reports `{line}`, which does not contain `{marker}` — the file on disk \
hashes correctly but identifies itself as a different build, so one of the \
two claims is wrong and neither may be trusted",
installed.display()
);
}
}
let changed = replaced.as_deref() != Some(sha256.as_str());
Ok(CliInstallOutcome {
name: req.name.to_string(),
source,
installed,
sha256,
replaced,
changed,
version_line,
mode: CLI_MODE,
})
}
/// Copy `source` to `staging`, mark it executable, and `rename` it over `installed`.
///
/// Split out so the ETXTBSY-defeating order — write elsewhere, then rename — is one
/// reviewable unit rather than something a later edit can quietly reorder into a direct
/// copy.
fn stage_then_place(source: &Path, staging: &Path, installed: &Path) -> Result<()> {
std::fs::copy(source, staging).with_context(|| {
format!(
"stage {} at {}",
source.display(),
staging.display()
)
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(staging, std::fs::Permissions::from_mode(CLI_MODE))
.with_context(|| format!("set mode {CLI_MODE:o} on {}", staging.display()))?;
}
// The rename, not a copy: the destination may be executing right now (`ETXTBSY`),
// and a reader must never observe a half-written command.
std::fs::rename(staging, installed).with_context(|| {
format!(
"place {} at {} — the staging file and the destination must share a \
filesystem for this to be atomic",
staging.display(),
installed.display()
)
})?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
/// A REAL executable whose `--version` output the test chooses, with no shell and no
/// dependency on any particular program being installed: a `#!/bin/echo <line>`
/// shebang. `execve` turns that into `/bin/echo "<line>" <path> --version`, so the
/// chosen text really is printed by a really-executed file.
fn version_stub(dir: &Path, name: &str, line: &str) -> PathBuf {
let p = dir.join(name);
let mut f = std::fs::File::create(&p).unwrap();
writeln!(f, "#!/bin/echo {line}").unwrap();
drop(f);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755)).unwrap();
}
p
}
/// A REAL ELF with a digest unique to `tag`: this very test binary, plus a trailing
/// comment. Used where the fixture must be *executable machine code* — the
/// `ETXTBSY` test needs a process that can hold a text segment, which a shebang
/// script cannot do (the interpreter is what runs, not the script).
fn elf_fixture(dir: &Path, name: &str, tag: &str) -> PathBuf {
let p = dir.join(name);
let mut bytes = std::fs::read(std::env::current_exe().unwrap()).unwrap();
bytes.extend_from_slice(tag.as_bytes());
std::fs::write(&p, &bytes).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755)).unwrap();
}
p
}
fn req<'a>(name: &'a str, source: &'a Path, bin: &'a Path) -> CliInstallRequest<'a> {
CliInstallRequest {
name,
source,
bin_dir: bin,
probe_version: false,
expect_marker: None,
}
}
/// A no-op in an ordinary run; a process that BLOCKS when the ETXTBSY test re-execs
/// this binary at it. That gives the suite a genuine long-lived process holding a
/// text segment without shelling out or depending on `/bin/bash` existing.
#[test]
fn victim_holds_a_text_segment_when_asked() {
if std::env::var_os("SKID_CLI_DOOR_VICTIM").is_none() {
return;
}
// Long enough that a loaded oden (five lanes building at once) cannot let the
// victim expire mid-test and turn a real assertion into a flake. The test kills
// it, so the number is a ceiling, not a wait.
std::thread::sleep(std::time::Duration::from_secs(300));
}
/// ★ RED — the gate must FAIL when the bytes on disk are not the bytes installed.
///
/// Without this the whole module is decoration: every other test installs
/// successfully, so a `verify_installed_binary` that returned `Ok(())`
/// unconditionally would pass all of them. Here the file is corrupted *after* a
/// successful install and the gate must refuse — naming BOTH digests, so the error
/// is usable without re-deriving what the installer already computed.
#[test]
fn the_gate_refuses_a_file_whose_bytes_are_not_the_artifact() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
let src = version_stub(tmp.path(), "src", "tool 1.0 (abcdef)");
let out = install_cli_binary(&req("tool", &src, &bin)).unwrap();
// Green first, so the red below is a change of one thing only.
verify_installed_binary(&out.installed, &out.sha256).expect("the honest file passes");
std::fs::write(&out.installed, b"#!/bin/echo something else entirely\n").unwrap();
let err = verify_installed_binary(&out.installed, &out.sha256)
.expect_err("a file whose bytes changed under us MUST NOT verify");
let msg = format!("{err:#}");
assert!(
msg.contains(&out.sha256),
"the error must name the digest that was built, got: {msg}"
);
let on_disk = crate::running::binary_sha256(&out.installed).unwrap();
assert!(
msg.contains(&on_disk),
"the error must name the digest actually on disk, got: {msg}"
);
assert_ne!(on_disk, out.sha256, "the fixture must really differ");
}
/// ★ RED — the gate must fail when NOTHING landed.
///
/// This is the failure an exit-status check is structurally blind to: a copy that
/// returns success and writes no file looks identical to a copy that worked.
#[test]
fn the_gate_refuses_an_absent_file() {
let tmp = tempfile::tempdir().unwrap();
let err = verify_installed_binary(&tmp.path().join("never-written"), "00ff")
.expect_err("a missing file is a failed install, not a pass");
assert!(
format!("{err:#}").contains("nothing at"),
"the error must say the file is absent"
);
}
/// ★ The outcome's `sha256` is read back FROM DISK, not carried over from the
/// source — otherwise the field is a tautology and the gate is watching itself.
#[test]
fn the_reported_digest_is_the_one_on_disk() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
let src = version_stub(tmp.path(), "src", "tool 1.0 (abcdef)");
let out = install_cli_binary(&req("tool", &src, &bin)).unwrap();
let independently = crate::running::binary_sha256(&out.installed).unwrap();
assert_eq!(out.sha256, independently, "reported digest must be the disk's");
assert_eq!(
out.sha256,
crate::running::binary_sha256(&src).unwrap(),
"and it must equal the artifact that was handed in"
);
assert_eq!(out.mode & 0o777, CLI_MODE, "an installed command is executable");
assert!(out.changed, "a first install changed something");
assert_eq!(out.replaced, None, "there was nothing to replace");
}
/// ★ RED — a `--version` probe MUST NOT be satisfied by the exit status.
///
/// The fixture is a real program that **exits 0 and prints nothing**: a
/// `#!/bin/true` shebang, which `execve` turns into `/bin/true <path> --version`.
/// A probe written as `if !status.success() { bail }` calls that a healthy install
/// of a healthy program. This test requires it to be refused, on the OUTPUT.
///
/// (An earlier draft of this test used a Rust libtest harness as the fixture,
/// believing it exited 0 while printing `error: Unrecognized option: 'version'`.
/// It does not — it exits **101**. That first measurement had read the exit status
/// of a `head` at the end of a pipeline rather than the binary's own. The fixture
/// was replaced with one whose exit status is asserted here, in the test.)
#[test]
fn the_version_probe_refuses_a_binary_that_prints_nothing_though_it_exits_zero() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
let src = tmp.path().join("silent");
{
let mut f = std::fs::File::create(&src).unwrap();
writeln!(f, "#!/bin/true").unwrap();
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&src, std::fs::Permissions::from_mode(0o755)).unwrap();
}
// The premise, asserted rather than assumed: status 0, empty stdout.
let raw = std::process::Command::new(&src)
.args(VERSION_ARGV)
.output()
.unwrap();
assert!(
raw.status.success(),
"the fixture only proves anything if it EXITS ZERO; got {}",
raw.status
);
assert!(
String::from_utf8_lossy(&raw.stdout).trim().is_empty(),
"the fixture must print nothing on stdout"
);
let mut r = req("silent", &src, &bin);
r.probe_version = true;
let err = install_cli_binary(&r)
.expect_err("an install whose program cannot name its build MUST fail");
let msg = format!("{err:#}");
assert!(
msg.contains("printed NOTHING on stdout"),
"the probe must fail on the OUTPUT, got: {msg}"
);
}
/// ★ RED — a version line that does not carry the expected build marker fails, even
/// though the file hashes correctly.
///
/// This is the whole point of stamping a git hash into `--version`: the digest says
/// the bytes arrived, the marker says WHICH source they came from. A door that only
/// hashed would happily install last month's binary from last month's build dir.
#[test]
fn a_version_line_without_the_expected_marker_fails_the_install() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
let src = version_stub(tmp.path(), "src", "dwarves 0.1.4 (2079ba109c22)");
let mut r = req("dwarves", &src, &bin);
r.expect_marker = Some("7681a252956c");
let err = install_cli_binary(&r).expect_err("the stale hash must be refused");
assert!(
format!("{err:#}").contains("7681a252956c"),
"the error must name the marker that was expected"
);
// GREEN, one thing changed: the marker the binary really prints.
let mut ok = req("dwarves", &src, &bin);
ok.expect_marker = Some("2079ba109c22");
let out = install_cli_binary(&ok).expect("the honest marker passes");
assert!(
out.version_line.as_deref().unwrap().contains("2079ba109c22"),
"the kept line is the one that was matched"
);
}
/// ★ The probe runs the INSTALLED file, not the source.
///
/// A probe that ran the source would report a healthy version for an install that
/// never landed — precisely the class of bug this module exists to close. Proven by
/// making the two files disagree after the install and re-probing.
#[test]
fn the_probe_reads_the_installed_file_not_the_source() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
let src = version_stub(tmp.path(), "src", "tool 2.0 (from-source)");
let mut r = req("tool", &src, &bin);
r.probe_version = true;
let out = install_cli_binary(&r).unwrap();
assert!(out.version_line.as_deref().unwrap().contains("from-source"));
// Now make them disagree; the probe must follow the destination.
std::fs::write(&src, "#!/bin/echo tool 9.9 (source-moved-on)\n").unwrap();
let line = probe_version_line(&out.installed).unwrap();
assert!(
line.contains("from-source") && !line.contains("source-moved-on"),
"the probe must run {}, got `{line}`",
out.installed.display()
);
}
/// ★ RED then GREEN — installing over a RUNNING command.
///
/// The red is inside the test: with a live process holding the destination's text
/// segment, a direct `std::fs::copy` onto it fails `ETXTBSY` (errno 26). That is the
/// obvious implementation, and it does not work. The door's stage-then-rename does,
/// and the assertion is on the destination's digest afterwards — not on `Ok(_)`.
///
/// This is not a corner case: `dwarves --install` replacing `dwarves` is the normal
/// use, and on oden the running process may be a warm agent's own client.
#[test]
#[cfg(unix)]
fn a_running_command_can_be_replaced_where_a_plain_copy_cannot() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
let first = elf_fixture(tmp.path(), "v1", "generation-one");
let second = elf_fixture(tmp.path(), "v2", "generation-two");
assert_ne!(
crate::running::binary_sha256(&first).unwrap(),
crate::running::binary_sha256(&second).unwrap(),
"the two generations must really differ"
);
let out = install_cli_binary(&req("victim", &first, &bin)).unwrap();
// Start it, and WAIT until its text segment is genuinely mapped — reusing the
// settle wait that exists because `/proc/<pid>/exe` is unreadable between fork
// and execve.
let mut child = std::process::Command::new(&out.installed)
.args(["--exact", "--nocapture", "cli_install::tests::victim_holds_a_text_segment_when_asked"])
.env("SKID_CLI_DOOR_VICTIM", "1")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn the installed command");
let running = crate::running::settle_running_exe_sha256(
child.id(),
crate::running::EXEC_SETTLE_TIMEOUT,
);
// If the platform will not let us confirm a live text segment, the red below is
// not being demonstrated — say so rather than passing a hollow test.
let running = match running {
Ok(sha) => sha,
Err(e) => {
let _ = child.kill();
panic!("could not confirm the victim is running ({e}) — this test proves nothing without it");
}
};
assert_eq!(running, out.sha256, "the process is running the installed bytes");
// RED: the obvious implementation.
let direct = std::fs::copy(&second, &out.installed);
let err = direct.expect_err("a direct copy onto a running command must fail");
assert_eq!(
err.raw_os_error(),
Some(26),
"expected ETXTBSY (Text file busy), got {err}"
);
// GREEN: the door.
let out2 = install_cli_binary(&req("victim", &second, &bin))
.expect("stage-then-rename replaces a running command");
let on_disk = crate::running::binary_sha256(&out2.installed).unwrap();
assert_eq!(
on_disk,
crate::running::binary_sha256(&second).unwrap(),
"the NEW bytes must be on disk, asserted independently of the return value"
);
assert_eq!(out2.replaced.as_deref(), Some(out.sha256.as_str()));
assert!(out2.changed);
// The old process keeps its own image — that is why the rename was safe.
let still = crate::running::running_exe_sha256(child.id()).unwrap();
assert_eq!(still, out.sha256, "the running process kept the old inode");
let _ = child.kill();
let _ = child.wait();
}
/// ★ A reinstall of identical bytes reports `changed = false`.
///
/// An installer that always claims an update trains its reader to ignore it, and
/// `--refresh` needs to distinguish "already current" from "just replaced".
#[test]
fn reinstalling_identical_bytes_is_reported_as_unchanged() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
let src = version_stub(tmp.path(), "src", "tool 1.0 (abcdef)");
let first = install_cli_binary(&req("tool", &src, &bin)).unwrap();
assert!(first.changed && first.replaced.is_none());
let again = install_cli_binary(&req("tool", &src, &bin)).unwrap();
assert!(!again.changed, "identical bytes are not an update");
assert_eq!(again.replaced.as_deref(), Some(first.sha256.as_str()));
assert_eq!(again.sha256, first.sha256);
}
/// ★ RED — the refusals that keep a bad artifact out, each proven to fire.
#[test]
fn a_source_that_is_not_an_already_built_command_is_refused() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
// absent
let missing = tmp.path().join("was-never-built");
let err = install_cli_binary(&req("tool", &missing, &bin)).unwrap_err();
assert!(
format!("{err:#}").contains("never builds one"),
"an absent artifact must say the door does not build: {err:#}"
);
// present but not executable — the shape of a file that was written, not built
let dud = tmp.path().join("dud");
std::fs::write(&dud, b"not a program").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&dud, std::fs::Permissions::from_mode(0o644)).unwrap();
}
let err = install_cli_binary(&req("tool", &dud, &bin)).unwrap_err();
assert!(
format!("{err:#}").contains("no execute bit"),
"a non-executable artifact must be refused: {err:#}"
);
// a name that is a path
let ok = version_stub(tmp.path(), "src", "tool 1.0 (abcdef)");
let err = install_cli_binary(&req("sub/tool", &ok, &bin)).unwrap_err();
assert!(
format!("{err:#}").contains("no path separators"),
"a name with a separator must be refused: {err:#}"
);
}
/// ★ The staging file never survives — neither on success nor on the failure path.
/// A `.dwarves.new-1234` left in `~/.cargo/bin` is a half-installed command sitting
/// where a shell completion will find it.
#[test]
fn no_staging_file_is_left_behind() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
let src = version_stub(tmp.path(), "src", "tool 1.0 (abcdef)");
install_cli_binary(&req("tool", &src, &bin)).unwrap();
let strays: Vec<_> = std::fs::read_dir(&bin)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().to_string())
.filter(|n| n != "tool")
.collect();
assert!(strays.is_empty(), "bin dir must hold only the command, got {strays:?}");
// And on the failing path (a marker that will not match).
let mut bad = req("tool", &src, &bin);
bad.expect_marker = Some("no-such-build");
assert!(install_cli_binary(&bad).is_err());
let strays: Vec<_> = std::fs::read_dir(&bin)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().to_string())
.filter(|n| n != "tool")
.collect();
assert!(strays.is_empty(), "a failed install must leave no staging file, got {strays:?}");
}
}