crates/gunnar-recipe/src/emit_recipe.rs
//! **`.nornir/build-variants.toml` is EMITTED from this crate** — code → toml,
//! one-directional, never read back as authority.
//!
//! # What this closes (2026-08-30)
//!
//! Until this module existed gunnar's recipe was the one thing the code-is-the-
//! source law forbids: a **hand-maintained mirror**. Two files said the same
//! thing, `tests/toml_parity.rs` compared them, and everybody believed the
//! comparison. It was not enough, and the measurements are on the record:
//!
//! * An audit found **two whole groups** — `[build.installer.target]` (the
//! machine: SeaBIOS, IDE cdrom, no UART, two NICs) and
//! `[build.installer.data]` (xfs, io_uring) — living in the toml and **not**
//! in `recipe.rs`. Parity passed anyway, because the pinned loader dropped
//! them before the comparison: a mirror compared against a reader that
//! discards what it cannot parse agrees about nothing, loudly.
//! * A later audit measured the same file **refused outright** by the published
//! 0.1.10 reader on one additive `kernel_requires` key, and its schema header
//! had to be **written by hand** — the clearest possible statement that the
//! file had no writer.
//!
//! Both are gone by construction here. There is one declaration
//! ([`GunnarBuildThing::variants`](crate::recipe::GunnarBuildThing)); the file
//! is its projection; `nornir_build_thing::emit` refuses bytes that do not read
//! back EQUAL through the one loader — which carries R1 (the writer computes
//! the schema), R2 (a too-old reader refuses by name) and R3 (a reader that
//! DROPS a key fails). A group the reader would drop can no longer be emitted
//! at all, so `toml_parity` fails for **any** reason the two differ, including
//! that one.
//!
//! # Where the prose went
//!
//! The file this replaced carried ~1 500 lines of reasoning above and between
//! its rows — the atom table, the bin-name history, the linkage argument, the
//! Containerfile the twin used to build. Prose is not data and cannot be
//! emitted from a declaration; it moved to the two homes that keep it beside
//! the code that acts on it: the doc comments in `recipe.rs` / `variants.rs` /
//! `atom.rs`. What remains in the emitted file is what
//! a recipe IS.
use std::path::{Path, PathBuf};
use anyhow::Result;
use nornir_build_thing::emit;
use nornir_build_thing::plugin::BuildThing;
use crate::recipe::GunnarBuildThing;
/// The header every emission starts with — says what the file is, and what it
/// is not.
pub const EMITTED_HEADER: &str = "\
# gunnar — .nornir/build-variants.toml — EMITTED, NOT SOURCE.
#
# Generated by `cargo run -p gunnar-build-thing -- emit` from
# crates/gunnar-build-thing (the Rust `impl BuildThing`: recipe.rs for the rows
# and the media, variants.rs for the cargo build points, atom.rs for the
# delivery matrix). The code is the one source; this file DESCRIBES the build
# for readers and for the tools that discover a repo by this path (the dwarves
# forge, the ⚒ pane, xtask, installtest, the bench harness). Editing it changes
# NOTHING: re-emit after changing the code. Do not edit by hand.
#
# Model: variant → features → settings/sub-settings → typ. gunnar is a git
# client and a git server in ONE binary whose two halves have different
# constraints — the server is worktree-free, spawns nothing and is
# meant to be PID 1 — which is why the server atom and the client atom are two
# build points and not one with a flag.
#
# The ~2 200 lines of reasoning that used to sit between these rows are kept
# VERBATIM in the project's reasoning archive, in file order. New reasoning
# goes beside the code that acts on it — a doc comment in
# crates/gunnar-build-thing/src — and never here: a comment written in this file
# is deleted by the next `emit` without anybody noticing.
#
# The `[nornir-build-thing] schema` header above the rows, when present, is
# COMPUTED from what this set declares (nornir_build_thing::schema::required_by,
# R1) — never chosen, never bumped by an emission that says nothing new.";
/// The emission — what `.nornir/build-variants.toml` must contain.
pub fn emitted_toml() -> Result<String> {
emit::emitted_toml(EMITTED_HEADER, &GunnarBuildThing)
}
/// Regenerate `<repo_root>/.nornir/build-variants.toml` from the code.
pub fn write_emitted(repo_root: &Path) -> Result<PathBuf> {
emit::write_emitted(EMITTED_HEADER, &GunnarBuildThing, repo_root)
}
/// gunnar's repo root, from this crate's manifest directory.
#[must_use]
pub fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("crates/gunnar-build-thing has two ancestors")
.to_path_buf()
}
/// The set this recipe declares — the one source, for a caller that wants it
/// without naming the trait.
#[must_use]
pub fn declared() -> nornir_build_thing::variant::BuildVariantSet {
GunnarBuildThing.variants()
}