Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-xtask-support/src/lib.rs

//! # nornir-xtask-support — the shared, std-only `cargo xtask` primitives
//!
//! The nordisk constellation drives every build/deploy step from a Rust
//! `xtask` crate (no YAML CI). Four of those xtasks — nornir, holger, dwarves and
//! facett — grew a **byte-identical** block of the same plumbing: a hand-rolled
//! `which`, a `run` that maps a non-zero exit to an `Err`, recursive fs helpers,
//! `git rev-parse --short HEAD`, the `$CARGO_TARGET_DIR`-honouring target-dir
//! resolver, and the wasm bootstrap trio that installs the `wasm32` target plus a
//! `wasm-bindgen` CLI **matching `Cargo.lock`** (the fix for the classic
//! blank-canvas version mismatch). This leaf holds that block ONCE (LAW #5).
//!
//! ## Why a leaf (L5a)
//! These are self-contained, dep-light primitives with no domain coupling — a
//! third unrelated Rust xtask wants them *as-is* (facett's `release-streamlit`
//! path already re-uses `which`/`run`/`fresh_dir`/`target_dir`/`ensure_wasm_*`).
//! The *parameterized* gh-pages demo/deploy FLOW lives one crate up in
//! [`nornir-pages`](../nornir_pages/index.html); this crate is only the toolbox.
//!
//! ## Error style — adapt to every caller
//! Everything fallible returns [`Result`], a boxed `Send + Sync` error alias.
//! `anyhow::Error`, `Box<dyn Error>` and `Result<_, String>` callers all `?` or
//! `From`-adapt it, so one implementation serves all four xtasks' error styles.
//!
//! ## Zero deps by default
//! std only. `nornir-testmatrix` is pulled ONLY under the `testmatrix` feature to
//! self-emit the pure primitives' health; the release/default build links neither.

use std::path::{Path, PathBuf};
use std::process::Command;

pub mod ci;
pub mod lockcheck;
pub mod selftest;
pub mod spawn_guard;

/// A boxed dynamic error that is `Send + Sync` — the dependency-free stand-in for
/// `anyhow::Error`. Chosen `Send + Sync` (unlike a bare `Box<dyn Error>`) so an
/// `anyhow` caller's `?` adapts it and it can cross threads. `String`/`&str` both
/// `.into()` it (std provides those `From` impls), so `format!(…).into()` is the
/// idiom for an ad-hoc error here.
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;

/// The wasm target triple every constellation demo builds for.
pub const WASM_TARGET: &str = "wasm32-unknown-unknown";

// ── paths ─────────────────────────────────────────────────────────────────────

/// Expand a leading `~/` against `$HOME` — the ONE implementation.
///
/// `~` is a **shell** convention. Nothing expands it inside a Rust string, so a
/// config literal like `~/.ssh/id_ed25519` becomes a real directory named `~`
/// the moment it is handed to `std::fs`. Every tool in the constellation that
/// reads a path out of a config file, a CLI flag or an env var therefore needs
/// exactly this three-line function — and, until 2026-09-10, wrote its own:
/// gunnar carried **four** copies (`xtask/src/lib.rs`, `bench/src/lib.rs`,
/// `installtest/src/lib.rs`, `deploy/upcloud/src/site.rs`), three of them
/// byte-identical, each with a doc comment claiming to be "the one place that
/// does". Found by `nornir mimir analyze duplicates`; this is the home the
/// keep/kill recommendation named.
///
/// A path that does not start with `~/` is returned unchanged, so a caller may
/// pass an absolute override straight through. A bare `~` (no slash) is also
/// returned unchanged: it is a legal relative filename and expanding it would
/// change the meaning of `./~`.
///
/// `$HOME` unset falls back to `/root`, which is the behaviour every copy had
/// and the one that matters where it bites — a systemd unit or a container that
/// starts with no `HOME` runs as root, and pointing at `/root` beats pointing at
/// a relative path under the process's cwd.
pub fn expand_home(p: &str) -> PathBuf {
    match p.strip_prefix("~/") {
        Some(rest) => {
            PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| "/root".to_string())).join(rest)
        }
        None => PathBuf::from(p),
    }
}

/// The workspace root for an xtask crate: the **parent** of its manifest dir.
///
/// An xtask lives at `<root>/xtask`, so its `CARGO_MANIFEST_DIR` is `<root>/xtask`
/// and the workspace root is one directory up. Pass `env!("CARGO_MANIFEST_DIR")`
/// (as a `Path`) from the calling xtask — the leaf can't read the *caller's*
/// compile-time manifest dir, so the caller supplies it.
pub fn workspace_root_of(manifest_dir: &Path) -> PathBuf {
    manifest_dir
        .parent()
        .expect("xtask manifest dir has a parent (the workspace root)")
        .to_path_buf()
}

/// The cargo **target** directory for `root` — **asked of cargo**, not re-derived.
///
/// Every caller of this uses it for the same thing: to find an artifact `cargo build`
/// has just written. So the only correct answer is the one cargo itself would give,
/// and cargo gives it: `cargo metadata --no-deps` reports `target_directory` with the
/// whole resolution already applied — `--target-dir`, `$CARGO_TARGET_DIR`,
/// `build.target-dir` from every `.cargo/config.toml` up the tree, and the workspace
/// root default, in cargo's own precedence.
///
/// # Why this stopped being a two-line env read
///
/// It honoured `$CARGO_TARGET_DIR` **and nothing else**, so a `build.target-dir` in
/// `.cargo/config.toml` — the documented way, and the one a developer uses to keep
/// builds off a small `/home` — was invisible to it. cargo wrote the artifact to the
/// configured directory; this function looked under `<root>/target`; and the caller
/// reported *"expected wasm artifact missing"* **immediately after building it
/// successfully**. That cost a t14s lane an hour on 2026-09-06, and the symptom does
/// not name the cause: nothing in "artifact missing" points at a config key.
///
/// MEASURED on a throwaway crate before this was written, rather than assumed: with
/// `[build] target-dir` configured, `cargo build` writes to the configured directory
/// and `<root>/target` is **never created**, while `cargo metadata --no-deps` reports
/// the configured path as `target_directory`.
///
/// # The fallback, and why it is not a second answer
///
/// If cargo cannot be run at all (not on `PATH`, no manifest at `root`, a spawn
/// failure) this returns the old heuristic — `$CARGO_TARGET_DIR` else `<root>/target`.
/// That is not a rival implementation: it is what cargo would answer for a default
/// project, kept so a caller in a stripped environment degrades to the previous
/// behaviour instead of panicking. When cargo IS available its answer wins outright.
pub fn target_dir(root: &Path) -> PathBuf {
    cargo_target_directory(root).unwrap_or_else(|| target_dir_fallback(root))
}

/// `cargo metadata --no-deps`'s `target_directory`, or `None` when cargo could not be
/// asked (missing, failed, or an answer without the field).
fn cargo_target_directory(root: &Path) -> Option<PathBuf> {
    let manifest = root.join("Cargo.toml");
    if !manifest.is_file() {
        return None;
    }
    let out = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()))
        .current_dir(root)
        .args([
            "metadata",
            "--no-deps",
            "--format-version",
            "1",
            "--manifest-path",
        ])
        .arg(&manifest)
        .output()
        .ok()?;
    if !out.status.success() {
        return None;
    }
    let text = String::from_utf8(out.stdout).ok()?;
    json_string_field(&text, "target_directory").map(PathBuf::from)
}

/// The pre-2026-09-06 answer: `$CARGO_TARGET_DIR` (absolute verbatim, relative joined
/// onto `root`), else `<root>/target`.
/// Serialises the tests that mutate `CARGO_TARGET_DIR`.
///
/// `std::env::set_var` mutates PROCESS state and `cargo test` runs tests on threads
/// of ONE process, so two tests touching the same variable interleave: each passes
/// when run alone and they flake when run together. THREE tests in this crate set
/// it — `target_dir_falls_back_to_the_env_override_when_cargo_cannot_be_asked`,
/// `target_dir_honours_build_target_dir_from_cargo_config`, and `selftest`'s
/// feature-gated wiring test. It lives at crate level rather than inside
/// `mod tests` because that third one is in a DIFFERENT module, and a lock only
/// some of the mutators take is not a lock.
#[cfg(test)]
pub(crate) static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

fn target_dir_fallback(root: &Path) -> PathBuf {
    match std::env::var_os("CARGO_TARGET_DIR") {
        Some(v) if !v.is_empty() => {
            let p = PathBuf::from(v);
            if p.is_absolute() {
                p
            } else {
                root.join(p)
            }
        }
        _ => root.join("target"),
    }
}

/// Pull one **top-level** JSON string field out of `text`.
///
/// Deliberately not a JSON parser and deliberately not a dependency: this crate is
/// std-only by design (its own description says so, and the `testmatrix` feature
/// exists to keep it that way in the default build), and one string value out of one
/// known document does not justify pulling serde into every xtask in the
/// constellation.
///
/// It does decode the escapes a path can actually carry — `\"`, `\\`, `\/`, the
/// control shorthands and `\uXXXX` — because a directory name may contain any of
/// them and silently truncating at the wrong byte is exactly the class of bug this
/// function was written to remove.
fn json_string_field(text: &str, field: &str) -> Option<String> {
    let key = format!("\"{field}\"");
    let mut from = 0usize;
    loop {
        let at = text[from..].find(&key)? + from;
        let after = &text[at + key.len()..];
        let rest = after.trim_start();
        // Only a `"key":` occurrence is the field; `"key"` as a VALUE is not.
        let Some(rest) = rest.strip_prefix(':') else {
            from = at + key.len();
            continue;
        };
        let rest = rest.trim_start();
        let body = rest.strip_prefix('"')?;
        return decode_json_string(body);
    }
}

/// Decode a JSON string body (everything after the opening quote) up to its closing
/// quote.
fn decode_json_string(body: &str) -> Option<String> {
    let mut out = String::with_capacity(body.len());
    let mut it = body.chars();
    while let Some(c) = it.next() {
        match c {
            '"' => return Some(out),
            '\\' => match it.next()? {
                '"' => out.push('"'),
                '\\' => out.push('\\'),
                '/' => out.push('/'),
                'b' => out.push('\u{8}'),
                'f' => out.push('\u{c}'),
                'n' => out.push('\n'),
                'r' => out.push('\r'),
                't' => out.push('\t'),
                'u' => {
                    let hex: String = (0..4).filter_map(|_| it.next()).collect();
                    if hex.len() != 4 {
                        return None;
                    }
                    let n = u32::from_str_radix(&hex, 16).ok()?;
                    out.push(char::from_u32(n)?);
                }
                _ => return None,
            },
            _ => out.push(c),
        }
    }
    // No closing quote: a truncated document, which must not become a truncated path.
    None
}

// ── process / lookup ──────────────────────────────────────────────────────────

/// Run a command, streaming its output to our stdio; map a non-zero exit to an
/// `Err` tagged with `what` (a human label for the step).
pub fn run(cmd: &mut Command, what: &str) -> Result<()> {
    let status = cmd.status().map_err(|e| format!("spawn {what}: {e}"))?;
    if status.success() {
        Ok(())
    } else {
        Err(format!("{what} exited with {status}").into())
    }
}

/// `which`-style PATH lookup (plus `~/.cargo/bin`), no external dep. Returns the
/// first matching file. `~/.cargo/bin` is appended because a freshly
/// `cargo install`ed tool (e.g. `wasm-bindgen`) lands there and may not be on a
/// bare shell's `PATH`.
pub fn which(bin: &str) -> Option<PathBuf> {
    let mut dirs: Vec<PathBuf> = std::env::var_os("PATH")
        .map(|p| std::env::split_paths(&p).collect())
        .unwrap_or_default();
    if let Some(home) = std::env::var_os("HOME") {
        dirs.push(PathBuf::from(home).join(".cargo/bin"));
    }
    for d in dirs {
        let cand = d.join(bin);
        if cand.is_file() {
            return Some(cand);
        }
    }
    None
}

/// Short `HEAD` revision of `root` — the cache-bust / deploy-message rev. Read
/// in-process via gix through the `nornir-git` leaf (`gitops::short_head_sha`),
/// NEVER the `git` CLI (LAW: every git op goes through nornir-git). `None` (not an
/// `Err`) when HEAD can't be read (no commit yet / not a repo), so a caller can
/// fall back to a placeholder.
pub fn short_rev(root: &Path) -> Option<String> {
    nornir_git::gitops::short_head_sha(root).ok()
}

// ── filesystem ────────────────────────────────────────────────────────────────

/// Remove `dir` (if any) and recreate it empty — the fresh-output-dir idiom.
pub fn fresh_dir(dir: &Path) -> Result<()> {
    if dir.exists() {
        std::fs::remove_dir_all(dir).map_err(|e| format!("clear {}: {e}", dir.display()))?;
    }
    std::fs::create_dir_all(dir).map_err(|e| format!("mkdir {}: {e}", dir.display()))?;
    Ok(())
}

/// Delete every entry in a worktree **except** `.git` (the worktree gitfile/dir),
/// so the tree can be repopulated wholesale (the pages-deploy squash pattern).
pub fn clear_worktree(wt: &Path) -> Result<()> {
    for entry in std::fs::read_dir(wt).map_err(|e| format!("read {}: {e}", wt.display()))? {
        let entry = entry.map_err(|e| format!("dir entry: {e}"))?;
        if entry.file_name() == ".git" {
            continue;
        }
        let p = entry.path();
        let r = if p.is_dir() {
            std::fs::remove_dir_all(&p)
        } else {
            std::fs::remove_file(&p)
        };
        r.map_err(|e| format!("rm {}: {e}", p.display()))?;
    }
    Ok(())
}

/// Recursively copy `src/*` into `dst` (both must exist; `dst`'s missing
/// sub-dirs are created).
pub fn copy_dir(src: &Path, dst: &Path) -> Result<()> {
    for entry in std::fs::read_dir(src).map_err(|e| format!("read {}: {e}", src.display()))? {
        let entry = entry.map_err(|e| format!("dir entry: {e}"))?;
        let from = entry.path();
        let to = dst.join(entry.file_name());
        if from.is_dir() {
            std::fs::create_dir_all(&to).map_err(|e| format!("mkdir {}: {e}", to.display()))?;
            copy_dir(&from, &to)?;
        } else {
            std::fs::copy(&from, &to)
                .map_err(|e| format!("copy {} → {}: {e}", from.display(), to.display()))?;
        }
    }
    Ok(())
}

// ── wasm bootstrap ────────────────────────────────────────────────────────────

/// Ensure the `wasm32-unknown-unknown` rustup target is installed (idempotent).
/// The cheap `rustup target list --installed` check, then `rustup target add` only
/// if missing.
pub fn ensure_wasm_target() -> Result<()> {
    let out = Command::new("rustup")
        .args(["target", "list", "--installed"])
        .output()
        .map_err(|e| format!("run rustup (is rustup installed?): {e}"))?;
    let installed = String::from_utf8_lossy(&out.stdout);
    if installed.lines().any(|l| l.trim() == WASM_TARGET) {
        return Ok(());
    }
    println!("xtask: installing rustup target {WASM_TARGET}…");
    run(
        Command::new("rustup").args(["target", "add", WASM_TARGET]),
        "rustup target add",
    )
}

/// Ensure a `wasm-bindgen` CLI **matching the locked crate version** is available;
/// return the binary to invoke. A version mismatch between the CLI and the
/// `wasm-bindgen` crate is the classic "blank canvas" failure, so pin exactly:
/// if a `wasm-bindgen` on PATH already reports the wanted version use it, else
/// `cargo install wasm-bindgen-cli --version <locked> --locked` (idempotent).
pub fn ensure_wasm_bindgen(root: &Path) -> Result<PathBuf> {
    let want = locked_wasm_bindgen_version(root)?;
    if let Some(path) = which("wasm-bindgen") {
        if let Ok(out) = Command::new(&path).arg("--version").output() {
            let v = String::from_utf8_lossy(&out.stdout);
            // `wasm-bindgen 0.2.123`
            if v.split_whitespace().nth(1) == Some(want.as_str()) {
                return Ok(path);
            }
        }
    }
    println!("xtask: installing wasm-bindgen-cli {want} (matching Cargo.lock)…");
    run(
        Command::new("cargo").args(["install", "wasm-bindgen-cli", "--version", &want, "--locked"]),
        "cargo install wasm-bindgen-cli",
    )?;
    which("wasm-bindgen").ok_or_else(|| {
        "wasm-bindgen installed but not found on PATH — ensure ~/.cargo/bin is on PATH".into()
    })
}

/// Read the resolved `wasm-bindgen` version out of `<root>/Cargo.lock` — the exact
/// version [`ensure_wasm_bindgen`] pins the CLI to. Scans for the
/// `name = "wasm-bindgen"` `[[package]]` block and returns its `version = "…"`.
pub fn locked_wasm_bindgen_version(root: &Path) -> Result<String> {
    let lock = std::fs::read_to_string(root.join("Cargo.lock"))
        .map_err(|e| format!("read Cargo.lock: {e}"))?;
    let mut in_block = false;
    for line in lock.lines() {
        let t = line.trim();
        if t == "name = \"wasm-bindgen\"" {
            in_block = true;
            continue;
        }
        if in_block && t.starts_with("version = ") {
            return Ok(t.trim_start_matches("version = ").trim_matches('"').to_string());
        }
        if in_block && t == "[[package]]" {
            break; // moved past the block without a version
        }
    }
    Err("could not find wasm-bindgen in Cargo.lock (resolve the workspace for wasm first)".into())
}

// ── tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
    use super::*;

    /// `expand_home` replaces a leading `~/` with `$HOME` and touches nothing else.
    ///
    /// RED-when-broken on all four behaviours the four gunnar copies had between
    /// them, because a repoint that quietly changed ANY of them would break a
    /// caller that never read this crate: the `~/` expansion itself; an absolute
    /// path passed through verbatim (every caller's "override" door); a BARE `~`
    /// left alone (a legal relative filename — expanding it would silently
    /// rewrite `./~`); and the `/root` fallback when `$HOME` is unset, which is
    /// what a systemd unit and a container both hit.
    ///
    /// Takes `ENV_LOCK`: `set_var`/`remove_var` are PROCESS state and `cargo
    /// test` threads share one process, so this and the `CARGO_TARGET_DIR` tests
    /// must not interleave.
    #[test]
    fn expand_home_expands_only_a_leading_tilde_slash() {
        let _g = crate::ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
        let prev = std::env::var_os("HOME");

        std::env::set_var("HOME", "/home/someone");
        assert_eq!(
            expand_home("~/.ssh/id_ed25519"),
            PathBuf::from("/home/someone/.ssh/id_ed25519"),
            "a leading `~/` is the one thing that expands"
        );
        assert_eq!(
            expand_home("/etc/gunnar/site.toml"),
            PathBuf::from("/etc/gunnar/site.toml"),
            "an absolute override passes through untouched"
        );
        assert_eq!(
            expand_home("~"),
            PathBuf::from("~"),
            "a bare `~` is a legal relative filename, not a home reference"
        );
        assert_eq!(
            expand_home("a/~/b"),
            PathBuf::from("a/~/b"),
            "a `~` that is not the FIRST segment means nothing"
        );

        std::env::remove_var("HOME");
        assert_eq!(
            expand_home("~/x"),
            PathBuf::from("/root/x"),
            "no $HOME ⇒ /root, the behaviour every copy had"
        );

        match prev {
            Some(v) => std::env::set_var("HOME", v),
            None => std::env::remove_var("HOME"),
        }
    }

    /// `workspace_root_of` returns the parent of the manifest dir (the `<root>/xtask`
    /// → `<root>` climb).
    #[test]
    fn workspace_root_climbs_one_dir() {
        let root = workspace_root_of(Path::new("/home/x/repo/xtask"));
        assert_eq!(root, PathBuf::from("/home/x/repo"));
    }

    /// The FALLBACK honours `$CARGO_TARGET_DIR` (absolute wins verbatim; relative is
    /// joined onto root; unset falls back to `<root>/target`). `/ws` holds no
    /// `Cargo.toml`, so `target_dir` cannot ask cargo and this is exactly the path it
    /// takes — which is the point: a caller in a stripped environment still gets the
    /// old behaviour rather than a panic.
    #[test]
    fn target_dir_falls_back_to_the_env_override_when_cargo_cannot_be_asked() {
        let _g = crate::ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
        let root = Path::new("/ws");
        let prev = std::env::var_os("CARGO_TARGET_DIR");

        std::env::remove_var("CARGO_TARGET_DIR");
        assert_eq!(target_dir(root), PathBuf::from("/ws/target"));

        std::env::set_var("CARGO_TARGET_DIR", "/mnt/work4t/cargo-target/edda");
        assert_eq!(
            target_dir(root),
            PathBuf::from("/mnt/work4t/cargo-target/edda")
        );

        std::env::set_var("CARGO_TARGET_DIR", "rel/dir");
        assert_eq!(target_dir(root), PathBuf::from("/ws/rel/dir"));

        std::env::set_var("CARGO_TARGET_DIR", "");
        assert_eq!(target_dir(root), PathBuf::from("/ws/target"));

        match prev {
            Some(v) => std::env::set_var("CARGO_TARGET_DIR", v),
            None => std::env::remove_var("CARGO_TARGET_DIR"),
        }
    }

    /// **`build.target-dir` in `.cargo/config.toml` is honoured — the defect this
    /// function was rewritten for.**
    ///
    /// Driven against a REAL crate and a REAL cargo, not a fixture: a throwaway
    /// package is written to a temp dir, cargo is asked where its target directory is,
    /// and `target_dir` must agree. Then the config is added and both must MOVE
    /// together.
    ///
    /// RED before the rewrite: the second half answered `<root>/target` while cargo
    /// answered the configured path — and a caller looking for a freshly built
    /// artifact under the first one reported it missing, right after building it.
    /// That is the hour it cost a t14s lane on 2026-09-06.
    ///
    /// Skipped, loudly, if cargo is not on PATH — the assertion is about agreeing with
    /// cargo, and with no cargo there is nothing to agree with.
    #[test]
    fn target_dir_honours_build_target_dir_from_cargo_config() {
        let _g = crate::ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
        let Some(_cargo) = which("cargo") else {
            eprintln!("nornir-xtask-support: no cargo on PATH; skipping the config test");
            return;
        };
        let base = std::env::temp_dir().join(format!("nxs-td-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&base);
        std::fs::create_dir_all(base.join("src")).unwrap();
        std::fs::write(
            base.join("Cargo.toml"),
            b"[package]\nname = \"nxs-td-probe\"\nversion = \"0.0.0\"\nedition = \"2021\"\n",
        )
        .unwrap();
        std::fs::write(base.join("src/main.rs"), b"fn main() {}\n").unwrap();

        // The env var must not decide either arm; this test is about the CONFIG.
        let prev = std::env::var_os("CARGO_TARGET_DIR");
        std::env::remove_var("CARGO_TARGET_DIR");

        // …and neither must the DEVELOPER'S OWN cargo config. `$CARGO_HOME` is
        // pointed at an empty directory for the duration.
        //
        // Not tidiness — without this, arm 1 asserts something about the machine
        // rather than about the code, and it FAILS on any machine that sets
        // `build.target-dir` in `~/.cargo/config.toml`. Measured here, red:
        //
        //     assertion `left == right` failed: with no config, cargo's answer is
        //     <root>/target
        //       left: "~/cargo-target"
        //      right: "/tmp/nxs-td-3769149/target"
        //
        // That is `cargo metadata` being exactly right — it obeys `$CARGO_HOME`'s
        // config, which is the whole point of asking cargo instead of re-deriving —
        // and the test being wrong about what "no config" means. A shared scratch
        // target dir is the normal setup on these boxes, so the arm that proves the
        // fallback still works has to say WHICH configs it means by "none".
        let prev_home = std::env::var_os("CARGO_HOME");
        let empty_home = std::env::temp_dir().join(format!("nxs-td-home-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&empty_home);
        std::fs::create_dir_all(&empty_home).unwrap();
        std::env::set_var("CARGO_HOME", &empty_home);

        // ── arm 1: no config. The default, and the positive control — without it a
        //    `target_dir` that always returned the configured path would pass arm 2.
        assert_eq!(
            target_dir(&base),
            base.join("target"),
            "with no config, cargo's answer is <root>/target"
        );

        // ── arm 2: the config cargo actually obeys.
        let elsewhere = base.join("elsewhere");
        std::fs::create_dir_all(base.join(".cargo")).unwrap();
        std::fs::write(
            base.join(".cargo/config.toml"),
            format!("[build]\ntarget-dir = \"{}\"\n", elsewhere.display()).as_bytes(),
        )
        .unwrap();
        assert_eq!(
            target_dir(&base),
            elsewhere,
            "cargo writes artifacts to `build.target-dir`; this lookup must find them there"
        );

        match prev {
            Some(v) => std::env::set_var("CARGO_TARGET_DIR", v),
            None => std::env::remove_var("CARGO_TARGET_DIR"),
        }
        match prev_home {
            Some(v) => std::env::set_var("CARGO_HOME", v),
            None => std::env::remove_var("CARGO_HOME"),
        }
        let _ = std::fs::remove_dir_all(&empty_home);
        let _ = std::fs::remove_dir_all(&base);
    }

    /// The one-field JSON reader, including the escapes a directory name can carry and
    /// the two shapes that must NOT be mistaken for the field.
    #[test]
    fn the_field_reader_decodes_what_a_path_can_contain_and_refuses_what_it_must() {
        assert_eq!(
            json_string_field(r#"{"a":1,"target_directory":"/tmp/x","b":2}"#, "target_directory"),
            Some("/tmp/x".to_string())
        );
        // A path carrying every escape a directory name can actually produce: an
        // escaped quote, an escaped backslash, an escaped solidus and a \u codepoint.
        // Written as a raw string so what the reader sees is the JSON bytes cargo
        // would emit, not Rust's idea of them.
        assert_eq!(
            json_string_field(
                r#"{"target_directory":"/tmp/a\"b\\c\/då"}"#,
                "target_directory"
            ),
            Some("/tmp/a\"b\\c/då".to_string())
        );
        // The key appearing as a VALUE must not be mistaken for the field — this is
        // why the reader requires a `:` after the key and keeps looking otherwise.
        assert_eq!(
            json_string_field(
                r#"{"note":"target_directory","target_directory":"/real"}"#,
                "target_directory"
            ),
            Some("/real".to_string())
        );
        // Absent, and truncated: both `None`, because a truncated document must never
        // become a truncated path.
        assert_eq!(json_string_field(r#"{"other":"x"}"#, "target_directory"), None);
        assert_eq!(json_string_field(r#"{"target_directory":"/tmp/x"#, "target_directory"), None);
    }

    /// `copy_dir` round-trips a nested tree byte-for-byte, and `fresh_dir` /
    /// `clear_worktree` clear correctly.
    #[test]
    fn copy_dir_round_trips_nested_tree() {
        let base = std::env::temp_dir().join(format!("nxs-copy-{}", std::process::id()));
        let src = base.join("src");
        let dst = base.join("dst");
        let _ = std::fs::remove_dir_all(&base);
        fresh_dir(&src).unwrap();
        fresh_dir(&dst).unwrap();

        std::fs::write(src.join("a.txt"), b"alpha").unwrap();
        std::fs::create_dir_all(src.join("sub")).unwrap();
        std::fs::write(src.join("sub/b.bin"), [0u8, 1, 2, 255]).unwrap();

        copy_dir(&src, &dst).unwrap();
        assert_eq!(std::fs::read(dst.join("a.txt")).unwrap(), b"alpha");
        assert_eq!(
            std::fs::read(dst.join("sub/b.bin")).unwrap(),
            vec![0u8, 1, 2, 255]
        );

        // clear_worktree removes everything but a `.git` marker.
        std::fs::write(dst.join(".git"), b"gitdir: elsewhere").unwrap();
        clear_worktree(&dst).unwrap();
        assert!(dst.join(".git").exists(), ".git must survive clear_worktree");
        assert!(!dst.join("a.txt").exists());
        assert!(!dst.join("sub").exists());

        let _ = std::fs::remove_dir_all(&base);
    }

    /// `which` finds a real, known-present binary somewhere on PATH (`cargo`/`ls`)
    /// and returns `None` for a bogus name.
    #[test]
    fn which_finds_and_misses() {
        assert!(which("definitely-not-a-real-binary-xyzzy").is_none());
        // `cargo` is on PATH in any build env that ran this test; if not, `sh` is.
        assert!(
            which("cargo").is_some() || which("sh").is_some(),
            "expected cargo or sh on PATH"
        );
    }
}