xtask/src/benchguard.rs
//! **The bench-target guard**: does `bench/` still compile?
//!
//! Two independent write-ups of the same break end with the same sentence —
//! *"add a guard that builds the bench target"* — and this is it.
//!
//! # The hole it closes, stated exactly
//!
//! `bench/` is a **detached workspace**. Its manifest carries its own
//! `[workspace]` table and the root `Cargo.toml`'s `members` list does not name
//! it, deliberately (`bench/Cargo.toml`'s header argues the case at length: a
//! bench dev-dependency inside a workspace member takes part in resolver-2
//! feature unification and can turn features on inside the appliance build).
//!
//! The price of that detachment is that **no command anyone runs in the normal
//! loop compiles it**:
//!
//! | command | reaches `bench/`? |
//! |---|---|
//! | `cargo build` / `cargo check --workspace` | no — not a member |
//! | `cargo test --workspace` | no — not a member |
//! | `cargo build -p gunnar-bench` | no — "package ID specification did not match any packages" |
//! | `cargo clippy --workspace --all-targets` | no — not a member |
//!
//! **Twice now that has been paid for**, and the second time is what caused
//! this file to exist:
//!
//! 1. **2026-08-08, `a38a236`** ("Amputate the gix engine") removed
//! `crates/gunnar-store/src/fs.rs`. `bench/` stopped compiling that day and
//! **stayed broken for two days across six commits**, unnoticed.
//! 2. **2026-08-10**, the old znippy engine crate was deleted. `bench/` named
//! `gunnar_znippy_engine::{ZnippyRepo, GitStore, GitStorePackIngest}` at 23 code
//! sites and stopped resolving again.
//!
//! Both are ordinary compile errors. Both were invisible to every command in
//! the loop. That is the class this guard stops — not either particular break.
//!
//! # Why `--all-targets` is the load-bearing flag
//!
//! MEASURED 2026-08-10 against a three-file fixture whose *library* is fine and
//! whose *example* is missing one struct field (the same `E0063` shape that
//! broke znippy's own harness):
//!
//! ```text
//! cargo build -> exit 0 ← blind
//! cargo build --all-targets -> exit 101 ← sees it
//! cargo test --no-run -> exit 101 ← sees it
//! ```
//!
//! Every bench arm in this repository lives in an **example** target
//! (`bench/examples/nornir-bench.rs` and the fourteen probe examples beside
//! it). A guard that ran a plain `cargo build` would have compiled the tiny
//! `bench/src/` library, printed nothing, and passed — the exact shape of green
//! this tree keeps finding. `xtask/tests/bench_guard.rs` pins that measurement
//! as an assertion so the flag cannot be dropped as noise.
//!
//! # Why this is a task of its own and not only a `ci` step
//!
//! `xtask ci` already ran `cargo test --manifest-path bench/Cargo.toml --no-run`
//! as its `bench-compile` step, added `2026-08-04` in `a30837c` — **four days
//! before the first break, and it still did not catch it.** Not because the
//! command was wrong; because nothing invoked it. `xtask ci` is `fmt` +
//! `clippy --workspace --all-targets` + the whole 517-test suite + appliance +
//! guards, it is tens of minutes, and the bench step sits near the end of it.
//! A guard whose only entry point costs half an hour is a guard that is not run.
//!
//! So the fix is reachability, not a new command: `cargo xtask bench-build` is
//! seconds-to-minutes and does exactly one thing, and `ci` calls the *same
//! function* (one writer, not two) as its second step, **before** `preflight`,
//! so a machine missing an interop binary cannot take the bench guard down with
//! it.
use std::path::{Path, PathBuf};
use std::process::Command;
/// The detached manifests that hold bench targets and that no workspace command
/// reaches. Relative to the repository root.
///
/// One entry today. It is a list rather than a constant because
/// the deleted engine crate’s nested bench manifest was a second one until
/// 2026-08-10, and the next detached bench tree should be one line here rather
/// than a second copy of this module.
pub const BENCH_MANIFESTS: &[&str] = &["bench/Cargo.toml"];
/// How hard to lean on the compiler.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Depth {
/// `cargo check --all-targets`. Catches every error either break was made
/// of (`E0432` unresolved import, `E0063` missing field) and is the form a
/// human or a tool runs between edits.
Check,
/// `cargo build --all-targets`. What `ci` runs. Codegen and link, so it
/// also sees the failures a `check` cannot: a missing `#[no_mangle]`
/// symbol, an `inventory` registration that does not link.
Build,
}
impl Depth {
fn subcommand(self) -> &'static str {
match self {
Depth::Check => "check",
Depth::Build => "build",
}
}
}
/// The outcome of guarding one manifest. `Err` carries the compiler's own words
/// — never a paraphrase, because the whole value of this guard is that the next
/// person reads the real error instead of "the bench guard failed".
pub struct Failure {
pub manifest: PathBuf,
pub depth: Depth,
/// Everything cargo said on stderr.
pub output: String,
}
/// Guard one manifest. `root` is the directory cargo is invoked from; `manifest`
/// is interpreted relative to it, exactly as `--manifest-path` does.
///
/// Deliberately **not** `--locked`. `bench/Cargo.lock` is a detached lockfile
/// that no workspace command updates, so `--locked` would turn "a path
/// dependency changed its own dependencies" into a lockfile error that reads
/// nothing like the compile break this guard is about. Let cargo update it.
pub fn guard_one(root: &Path, manifest: &str, depth: Depth, cargo: &str) -> Result<(), Failure> {
let out = Command::new(cargo)
.current_dir(root)
.args([
depth.subcommand(),
"--manifest-path",
manifest,
// The flag the measurement above is about. Every arm is an example.
"--all-targets",
])
.output();
let out = match out {
Ok(out) => out,
Err(err) => {
return Err(Failure {
manifest: root.join(manifest),
depth,
output: format!("could not spawn `{cargo}`: {err}"),
})
}
};
if out.status.success() {
return Ok(());
}
let mut text = String::from_utf8_lossy(&out.stderr).into_owned();
if text.trim().is_empty() {
// A non-zero exit with nothing on stderr is not a compile error, it is a
// broken invocation, and saying so is better than printing a blank.
text = format!("cargo exited {:?} and said nothing", out.status.code());
}
Err(Failure {
manifest: root.join(manifest),
depth,
output: text,
})
}
/// Guard every manifest in [`BENCH_MANIFESTS`], print the verdict, and say
/// **what was covered** rather than the word "ok".
///
/// Returns `true` on a clean sweep. Every manifest is attempted even after one
/// fails, for the reason `xtask ci` runs every step: the first failure must not
/// be able to hide the second.
pub fn run(root: &Path, depth: Depth, cargo: &str) -> bool {
let mut failed = Vec::new();
for manifest in BENCH_MANIFESTS {
eprintln!(
"bench guard: cargo {} --manifest-path {manifest} --all-targets",
depth.subcommand()
);
if let Err(f) = guard_one(root, manifest, depth, cargo) {
eprintln!("{}", f.output);
failed.push(f);
}
}
if failed.is_empty() {
eprintln!(
"bench guard: PASS, {} detached bench manifest(s) compiled:",
BENCH_MANIFESTS.len()
);
for m in BENCH_MANIFESTS {
eprintln!(" {m}");
}
return true;
}
eprintln!(
"\nbench guard: FAILED, {} of {} detached bench manifest(s):",
failed.len(),
BENCH_MANIFESTS.len()
);
for f in &failed {
eprintln!(" {}", f.manifest.display());
}
eprintln!(
"\n`bench/` is a DETACHED workspace: `cargo build`, `cargo check --workspace`,\n\
`cargo test --workspace` and `cargo clippy --workspace` all skip it, and\n\
`-p gunnar-bench` does not match it. Nothing else in this repository will\n\
tell you about the error above. It has already gone unnoticed twice\n\
(2026-08-08 `a38a236`, two days and six commits; 2026-08-10, the\n\
the old znippy engine crate deletion)."
);
false
}