crates/nornir-build-thing/src/options.rs
//! **Declared build OPTIONS** — the opt-out axis of an installer variant: one
//! checkbox an operator ticks off, and the code behind it is **NOT COMPILED
//! IN**.
//!
//! ```toml
//! [[build.installer.options]]
//! name = "dhcp"
//! label = "DHCP (eth0)"
//! default = true # ticked unless the operator unticks it
//! feature = "dhcp-client" # the cargo feature of the APP binary
//! params_when_off = ["static_ip", "gateway", "dns"]
//! param_kinds = { static_ip = "ipv4-cidr", gateway = "ipv4", dns = "ipv4" }
//! forbid_symbols = ["dhcp"] # what must be ABSENT when it is off
//! bakes = { boot_param = "korp.env.GUNNAR_NET", when_on = "eth0:dhcp", when_off = "eth0:static:{static_ip}:{gateway}:{dns}" }
//! ```
//!
//! # Why this is a feature and not a runtime flag
//!
//! A runtime flag leaves the DHCP client and the WireGuard client in the packed
//! binary and merely declines to call them. That is not what an appliance
//! customer who says "no WireGuard" is buying: the attack surface, the crypto
//! and the 3pp are still shipped, and the only thing standing between them and
//! the wire is an `if`. An option resolves to the cargo feature set the app
//! binary is BUILT with — off means the module never enters codegen, which is a
//! property of the artifact rather than a promise about its behaviour, and
//! [`OptionAudit`] reads it back out of the symbol table.
//!
//! # The rows are the SPELLING; the tree is the model
//!
//! An `OptionSpec` / `ParamSpec` row is the toml spelling of one node of the
//! variant's settings tree ([`crate::tree`]) — nothing here decides anything
//! by walking the rows: [`resolve_options`], [`ResolvedOptions::effective_specs`]
//! and [`ResolvedOptions::render_bakes`] build the tree
//! ([`SettingsTree::of_rows`]) and read the nodes, and [`parse_cli_options`]
//! parses through the one flag grammar ([`crate::flags`]). Law A proves the
//! rows render back from the tree; laws B/C/D quantify over the tree. The owner,
//! 2026-08-26: *"kaka på kaka"* — never two models.
//!
//! # The declaration is the ONE source — CLI, pane and receipt all read it
//!
//! `dwarves buildthing <repo> --variant <name> --opt dhcp=off --param
//! static_ip=…` and the Build Thing pane's checkboxes render the SAME
//! [`OptionSpec`] list, exactly as `--param` and the pane's input rows already
//! render the same [`ParamSpec`](crate::params::ParamSpec) list. One
//! declaration cannot ask the CLI and the panel for different things.
//!
//! # The four things one toggle decides
//!
//! | | on | off |
//! |---|---|---|
//! | the app build's feature set | `feature` present | `feature` **subtracted** |
//! | `params_when_on` | active (each param's own `required` applies) | **refused by name** |
//! | `params_when_off` | **refused by name** | active |
//! | `bakes` | `when_on` rendered onto the cmdline | `when_off` rendered |
//!
//! "Refused by name" is the leg that matters: with WireGuard off, supplying
//! `hub_conf` is not quietly ignored — the build stops and says which option
//! forbids it. A staged hub declaration riding a medium whose binary cannot
//! read it is exactly the two-files-deep lie this crate keeps refusing.
//!
//! # Why `default` must agree with the app variant's declaration
//!
//! [`validate_against_variant`] is the by-construction half of the LAW-2 proof,
//! and it is the reason a green audit means something:
//!
//! * `default = true` ⇒ `feature` must sit in the app variant's `features`. ON
//! is then literally the row that already builds, and OFF is a real
//! SUBTRACTION from it.
//! * `default = false` ⇒ `feature` must sit in the app variant's `open` (the
//! row's own switchable axis, [`BuildVariant::open`]). OFF is then the row
//! that already builds and ON is a real ADDITION.
//! * either way the app variant must carry `no_default_features = true`, or the
//! crate's own `default = [...]` can pull the feature back in behind the
//! toggle's back and every OFF build would silently contain the code.
//!
//! Without that third rule the whole axis is decorative, and it would be
//! decorative in the way that reads as working.
use std::collections::{BTreeMap, BTreeSet};
use anyhow::{Result, anyhow, bail};
use serde::{Deserialize, Serialize};
use crate::flags::Flag;
use crate::params::{AppliedParam, ParamKind, ParamSpec, names_a_secret};
use crate::tree::{NodeKind, SettingsTree};
use crate::variant::{ArtifactKind, BuildVariant};
/// What the cmdline selector for one option looks like in each state.
///
/// Rendered AFTER the parameters are applied, because `when_off` normally
/// interpolates them: `"eth0:static:{static_ip}:{gateway}:{dns}"`. A `{name}`
/// placeholder must name a parameter that is ACTIVE in that state — a template
/// that reaches for a parameter the same state forbids is refused at
/// declaration time, not left to produce a literal `{gateway}` on a kernel
/// command line nobody reads until the appliance is on a customer's floor.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct OptionBake {
/// The cmdline key, e.g. `korp.env.GUNNAR_NET`. `korp.`-prefixed so the
/// installer's `carry_site_tokens` delivers it to the INSTALLED disk.
pub boot_param: String,
/// The value when the option is ON. Empty ⇒ nothing is baked in that state.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub when_on: String,
/// The value when the option is OFF. Empty ⇒ nothing is baked in that state.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub when_off: String,
}
impl OptionBake {
/// The template for `on`/`off`, or `None` when that state bakes nothing.
fn template(&self, on: bool) -> Option<&str> {
let t = if on { &self.when_on } else { &self.when_off };
(!t.trim().is_empty()).then_some(t.as_str())
}
}
/// Every `{name}` placeholder in `template`, in order, without duplicates.
///
/// A deliberately small scanner rather than a template engine: the grammar is
/// one placeholder form and nothing else, and an unmatched `{` is a REFUSAL
/// (see [`validate_template`]) rather than a literal — a cmdline that shipped a
/// stray brace would be read by nobody and noticed by nobody.
fn placeholders(template: &str) -> Result<Vec<String>> {
let mut out: Vec<String> = Vec::new();
let mut rest = template;
while let Some(open) = rest.find('{') {
let after = &rest[open + 1..];
let close = after.find('}').ok_or_else(|| {
anyhow!("template '{template}' has a '{{' with no closing '}}'")
})?;
let name = &after[..close];
if name.trim().is_empty() {
bail!("template '{template}' has an empty '{{}}' placeholder");
}
if name.contains('{') {
bail!("template '{template}' has a nested '{{' inside a placeholder");
}
if !out.iter().any(|n| n == name) {
out.push(name.to_string());
}
rest = &after[close + 1..];
}
if rest.contains('}') {
bail!("template '{template}' has a '}}' with no opening '{{'");
}
Ok(out)
}
/// Substitute `{name}` from `values`. Every placeholder must be present — a
/// missing one is a refusal, never an empty string on a kernel command line.
fn render_template(template: &str, values: &BTreeMap<String, String>) -> Result<String> {
let mut out = template.to_string();
for name in placeholders(template)? {
let v = values.get(&name).ok_or_else(|| {
anyhow!("template '{template}': no value for '{{{name}}}'")
})?;
out = out.replace(&format!("{{{name}}}"), v);
}
Ok(out)
}
fn default_on() -> bool {
true
}
/// **One declared opt-out toggle of an installer variant.**
///
/// Serialized as a `[[build.installer.options]]` row. Deliberately a row on the
/// INSTALLER rather than a field on [`ParamSpec`](crate::params::ParamSpec):
/// `ParamSpec` is `deny_unknown_fields`, so a new key there makes the recipe
/// unloadable for every consumer still reading through a published 0.1.x
/// loader, while an unknown key on [`InstallerBuild`](crate::InstallerBuild) is
/// ignored benignly — the same reasoning that put
/// `[build.installer.fixtures]` at cell level. Its own contents (including the
/// [`param_kinds`](Self::param_kinds) spellings this crate is adding) are
/// therefore never parsed by an older reader at all, so a parity mirror stays
/// green across the whole feature.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OptionSpec {
/// The `--opt <name>=on|off` handle. Refused if it names a secret.
pub name: String,
/// What the checkbox says. Falls back to `name` when empty.
#[serde(default)]
pub label: String,
/// The state when nobody says otherwise. **`true` is the deliberate
/// default**: an appliance option is an OPT-OUT, so a recipe that forgets
/// the key ships the capability rather than silently dropping it.
#[serde(default = "default_on")]
pub default: bool,
/// The cargo feature of the APP binary this toggle decides. Off ⇒ absent
/// from `--features` ⇒ NOT COMPILED IN.
pub feature: String,
/// Parameters that are active when this option is ON, and refused when it
/// is OFF (WireGuard on ⇒ `hub_conf` may be supplied).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub params_when_on: Vec<String>,
/// Parameters that are active when this option is OFF, and refused when it
/// is ON (DHCP off ⇒ `static_ip` / `gateway` / `dns` are the static
/// address, and each one's own `required` then applies).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub params_when_off: Vec<String>,
/// **Shape overrides for this option's conditional parameters**, by name.
///
/// The `ParamSpec` rows themselves stay at spellings every published
/// loader knows, and the sharper shape (`ipv4`, `ipv4-cidr`) is declared
/// HERE, inside a table older readers skip whole. One validator either way
/// — [`ParamKind::validate_value`] — so there is no second opinion about
/// what an IPv4 address is.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub param_kinds: BTreeMap<String, ParamKind>,
/// Symbol families that must be **absent** from the packed app binary when
/// this option is OFF — the declared half of the LAW-2 proof, read back by
/// [`OptionAudit::of`]. Matched as substrings against demangled symbols,
/// exactly like [`THIN_FORBIDDEN_SYMBOLS`](crate::linkage::THIN_FORBIDDEN_SYMBOLS).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub forbid_symbols: Vec<String>,
/// The cmdline selector this option bakes, per state.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bakes: Option<OptionBake>,
/// **What this feature needs of the KERNEL** — typed `symbol = value,
/// because` rows ([`crate::kernel_requires::KconfigReq`]), declared next to
/// the feature they belong to. ON ⇒ the planner unions them into the donor
/// kernel step's requirements (the forward index) and the why-index names
/// this option as the requirer; OFF ⇒ the feature is not compiled in and
/// asks nothing. Added 2026-08-29: until then `--opt dhcp=on` reached the
/// app's cargo features and the cmdline and NEVER the kernel step.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub kernel_requires: Vec<crate::kernel_requires::KconfigReq>,
/// **What this feature needs of ANY sub build-thing**, addressed by edge —
/// the general form ([`crate::requires::EdgeRequirement`]); `kernel_requires`
/// above is its sugar for the one kernel edge. Same union, same refusal,
/// same why-index ([`crate::requires`]).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires: Vec<crate::requires::EdgeRequirement>,
}
impl Default for OptionSpec {
fn default() -> Self {
OptionSpec {
name: String::new(),
label: String::new(),
default: default_on(),
feature: String::new(),
params_when_on: Vec::new(),
params_when_off: Vec::new(),
param_kinds: BTreeMap::new(),
forbid_symbols: Vec::new(),
bakes: None,
kernel_requires: Vec::new(),
requires: Vec::new(),
}
}
}
impl OptionSpec {
/// What a human is shown — the label, or the name when there is none.
pub fn display_label(&self) -> &str {
if self.label.trim().is_empty() { &self.name } else { &self.label }
}
/// The parameters this option makes active in state `on`.
pub fn params_active(&self, on: bool) -> &[String] {
if on { &self.params_when_on } else { &self.params_when_off }
}
/// The parameters this option REFUSES in state `on`.
pub fn params_refused(&self, on: bool) -> &[String] {
if on { &self.params_when_off } else { &self.params_when_on }
}
/// Declaration-time validation against the cell's declared parameters.
/// Every refusal names the option AND what to do about it.
pub fn validate(&self, params: &[ParamSpec]) -> Result<()> {
if self.name.trim().is_empty() {
bail!("an option with an empty name");
}
if names_a_secret(&self.name) {
bail!(
"option '{}' NAMES A SECRET — an option is a checkbox whose state rides the \
build record and the kernel cmdline, both readable",
self.name
);
}
if self.feature.trim().is_empty() {
bail!(
"option '{}' declares no `feature` — an option that decides no cargo feature \
cannot leave anything out of the binary, which is the whole point of one",
self.name
);
}
let mut spelled: BTreeSet<&str> = BTreeSet::new();
for r in &self.kernel_requires {
r.validate(&format!("option '{}'", self.name))?;
// ONE WRITER PER SYMBOL, inside the one row that can write it.
// Kept from the `option_requires` table this key replaced (lane
// AE/AF, 2026-08-30): there the refusal had to police two tables,
// here there is only one, so a repeated symbol is a copy-paste of a
// reason rather than a disagreement between writers — and a symbol
// with two reasons makes `kernel.why.toml` say two things about one
// line. The DIFFERENT-value case is a conflict the closure names on
// its own ([`crate::kernel_requires::KernelRequirements::conflicts`]);
// this catches the same-value one, which nothing else would.
if !spelled.insert(r.symbol.as_str()) {
bail!(
"option '{}': {} is declared twice in its own `kernel_requires` — one option, \
one writer per symbol; keep the row whose reason is the measured one",
self.name,
r.symbol
);
}
}
for r in &self.requires {
r.what.validate(&format!("option '{}' (edge '{}')", self.name, r.edge))?;
}
let declared: Vec<&str> = params.iter().map(|s| s.name.as_str()).collect();
let known = |n: &str| params.iter().any(|s| s.name == n);
let mut seen: BTreeSet<&str> = BTreeSet::new();
for (branch, list) in
[("params_when_on", &self.params_when_on), ("params_when_off", &self.params_when_off)]
{
for p in list {
if !known(p) {
bail!(
"option '{}': {branch} names '{p}', which is not a declared parameter \
of this variant (declared: {})",
self.name,
if declared.is_empty() {
"none".to_string()
} else {
declared.join(", ")
}
);
}
if !seen.insert(p.as_str()) {
bail!(
"option '{}': parameter '{p}' is named in BOTH params_when_on and \
params_when_off — a parameter belongs to exactly one state of a \
toggle, or the toggle decides nothing about it",
self.name
);
}
}
}
for name in self.param_kinds.keys() {
if !known(name) {
bail!(
"option '{}': param_kinds names '{name}', which is not a declared \
parameter of this variant (declared: {})",
self.name,
if declared.is_empty() { "none".to_string() } else { declared.join(", ") }
);
}
if !seen.contains(name.as_str()) {
bail!(
"option '{}': param_kinds sharpens '{name}', which this option does not \
make conditional — name it in params_when_on or params_when_off, or \
declare the kind on the parameter row",
self.name
);
}
}
for fam in &self.forbid_symbols {
if fam.trim().is_empty() {
bail!("option '{}': an empty forbid_symbols family", self.name);
}
}
if let Some(b) = &self.bakes {
if b.boot_param.trim().is_empty() {
bail!(
"option '{}': `bakes` declares no boot_param — say which cmdline key the \
selector lands under",
self.name
);
}
if b.when_on.trim().is_empty() && b.when_off.trim().is_empty() {
bail!(
"option '{}': `bakes` renders nothing in either state — drop it, or give \
it a when_on / when_off",
self.name
);
}
for on in [true, false] {
let Some(t) = b.template(on) else { continue };
self.validate_template(t, on, params)?;
}
}
Ok(())
}
/// A bake template must interpolate only parameters that are ACTIVE in the
/// state it renders in — anything else is a value that cannot exist.
fn validate_template(&self, template: &str, on: bool, params: &[ParamSpec]) -> Result<()> {
let names = placeholders(template)
.map_err(|e| anyhow!("option '{}': bakes.{}: {e:#}", self.name, if on { "when_on" } else { "when_off" }))?;
let active: BTreeSet<&str> = self.params_active(on).iter().map(String::as_str).collect();
let refused: BTreeSet<&str> = self.params_refused(on).iter().map(String::as_str).collect();
for n in &names {
if refused.contains(n.as_str()) {
bail!(
"option '{}': bakes.{} interpolates '{{{n}}}', but this option REFUSES \
'{n}' when it is {} — the template asks for a value that cannot exist",
self.name,
if on { "when_on" } else { "when_off" },
if on { "on" } else { "off" }
);
}
if !active.contains(n.as_str()) && !params.iter().any(|p| p.name == *n) {
bail!(
"option '{}': bakes.{} interpolates '{{{n}}}', which is not a declared \
parameter of this variant",
self.name,
if on { "when_on" } else { "when_off" }
);
}
}
Ok(())
}
/// **The by-construction half of the LAW-2 proof** — see the module docs.
///
/// Checked against the APP variant this installer lays down (the one whose
/// packed binary the audit reads), never against the installer's own PID-1
/// row.
pub fn validate_against_variant(&self, app: &BuildVariant) -> Result<()> {
if !app.no_default_features {
bail!(
"option '{}' decides feature '{}' of variant '{}', but that variant does not \
set `no_default_features = true` — the crate's own `default` list could then \
enable '{}' behind the toggle's back and every OFF build would still contain \
the code, silently",
self.name,
self.feature,
app.name,
self.feature
);
}
let in_features = app.features.iter().any(|f| f == &self.feature);
let in_open = app.open.contains(&self.feature);
match (self.default, in_features, in_open) {
(true, true, _) => Ok(()),
(false, _, true) => Ok(()),
(true, false, true) => bail!(
"option '{}' defaults ON but variant '{}' leaves '{}' merely `open` (default \
off) — put it in that variant's `features` so ON is the row that already \
builds and OFF is a real subtraction",
self.name,
app.name,
self.feature
),
(false, true, _) => bail!(
"option '{}' defaults OFF but variant '{}' has '{}' in `features`, which is \
always on — move it to that variant's `open`",
self.name,
app.name,
self.feature
),
(_, false, false) => bail!(
"option '{}' decides feature '{}', which variant '{}' neither enables nor \
declares `open` — the toggle would change nothing about the build (variant \
features: {})",
self.name,
self.feature,
app.name,
if app.features.is_empty() {
"none".to_string()
} else {
app.features.join(", ")
}
),
}
}
}
/// Validate a variant's whole option roster: each option, unique names, unique
/// features, and no two options fighting over one parameter.
pub fn validate_options(options: &[OptionSpec], params: &[ParamSpec]) -> Result<()> {
let mut names = BTreeSet::new();
let mut features = BTreeSet::new();
let mut owned: BTreeMap<&str, &str> = BTreeMap::new();
for o in options {
o.validate(params)?;
if !names.insert(o.name.as_str()) {
bail!("option '{}' is declared twice", o.name);
}
if !features.insert(o.feature.as_str()) {
bail!(
"option '{}': feature '{}' is decided by two options — one feature, one \
checkbox, or the two toggles disagree about the same build",
o.name,
o.feature
);
}
for p in o.params_when_on.iter().chain(o.params_when_off.iter()) {
if let Some(other) = owned.insert(p.as_str(), o.name.as_str()) {
bail!(
"parameter '{p}' is made conditional by TWO options ('{other}' and '{}') \
— a parameter answers to one toggle",
o.name
);
}
}
}
Ok(())
}
/// Parse repeatable `--opt name=on|off` pairs (already split off the argv).
///
/// The spellings are the ones a human types: `on`/`off`, `true`/`false`,
/// `yes`/`no`, `1`/`0`, case-insensitive. Anything else is refused by name
/// rather than read as false — a typo that silently disabled WireGuard would
/// ship an appliance that cannot phone home.
pub fn parse_cli_options<S: AsRef<str>>(pairs: &[S]) -> Result<BTreeMap<String, bool>> {
let mut out = BTreeMap::new();
for p in pairs {
let p = p.as_ref();
if !p.contains('=') {
bail!("--opt takes name=on|off, got '{p}'");
}
let flag = Flag::parse(&format!("--opt {p}")).map_err(|e| anyhow!("--opt {p}: {e:#}"))?;
let Flag::Opt { path, value } = flag else { unreachable!("parsed as --opt") };
let Some(on) = Flag::Opt { path: path.clone(), value: value.clone() }.feature_state() else {
bail!("--opt {path}={value}: an option is on or off (on|off, true|false, yes|no, 1|0)");
};
if out.insert(path.clone(), on).is_some() {
bail!("--opt '{path}' was supplied twice");
}
}
Ok(out)
}
/// One option's resolved state, recorded ON the cell so the serialized
/// descriptor — and therefore the attestation and the content id — carries
/// exactly which toggles produced the artifact.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppliedOption {
/// The declared option name.
pub name: String,
/// Its resolved state.
pub on: bool,
/// The cargo feature it decided.
pub feature: String,
/// Whether nobody said — i.e. the recipe's `default` stands. Recorded
/// because "the operator chose DHCP" and "nobody touched it" are different
/// facts about a customer's appliance.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub from_default: bool,
}
impl AppliedOption {
/// One line for a human — the CLI's stderr and the pane's receipt print the
/// SAME sentence.
pub fn describe(&self) -> String {
format!(
"{} = {} ({}{})",
self.name,
if self.on { "on" } else { "off" },
if self.on { "+" } else { "−" },
self.feature
)
}
}
/// What resolving a variant's options produced.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ResolvedOptions {
/// Every option's state, in declaration order.
pub record: Vec<AppliedOption>,
/// Features the app build must be built WITH, in declaration order.
pub features_on: Vec<String>,
/// Features the app build must be built WITHOUT — and whose code the audit
/// requires to be absent from the packed binary.
pub features_off: Vec<String>,
/// Parameters that are active in this resolution.
pub active_params: BTreeSet<String>,
/// Parameters that are REFUSED in this resolution, each mapped to the
/// option (and the state) that refuses it, so the message can say why.
pub refused_params: BTreeMap<String, String>,
/// `(option, families)` that must be absent from the packed binary.
pub forbidden_symbols: Vec<(String, Vec<String>)>,
}
/// **Resolve declared options against the toggles somebody supplied.**
///
/// An unknown `--opt` name is refused with the declared list — never applied to
/// nothing, which is how a customer ends up with a WireGuard client they asked
/// not to have.
pub fn resolve_options(
options: &[OptionSpec],
params: &[ParamSpec],
supplied: &BTreeMap<String, bool>,
) -> Result<ResolvedOptions> {
validate_options(options, params)?;
let tree = SettingsTree::of_rows("", ArtifactKind::Installer, options, params, &BTreeMap::new());
// Every supplied spelling resolves THROUGH THE TREE to a root feature —
// a full path or an unambiguous alias — or is refused with the declared list.
let mut chosen: BTreeMap<String, bool> = BTreeMap::new();
for (name, on) in supplied {
let declared = || {
let d: Vec<&str> = tree.roots.iter().map(|n| n.path.as_str()).collect();
if d.is_empty() { "none".to_string() } else { d.join(", ") }
};
let path = match tree.resolve(name) {
Ok(r) if matches!(r.node.kind, NodeKind::Feature(_)) => r.node.path.clone(),
Ok(r) => bail!(
"--opt '{name}' names '{}', which is a {} and not an option of this variant \
(declared: {})",
r.node.path,
r.node.kind.word(),
declared()
),
Err(_) => bail!("--opt '{name}' is not a declared option of this variant (declared: {})", declared()),
};
chosen.insert(path, *on);
}
let mut out = ResolvedOptions::default();
for node in &tree.roots {
let NodeKind::Feature(f) = &node.kind else { continue };
let supplied_state = chosen.get(&node.path).copied();
let on = supplied_state.unwrap_or(f.default);
out.record.push(AppliedOption {
name: node.name().to_string(),
on,
feature: f.feature.clone(),
from_default: supplied_state.is_none(),
});
if on {
out.features_on.push(f.feature.clone());
} else {
out.features_off.push(f.feature.clone());
if !f.forbid_symbols.is_empty() {
out.forbidden_symbols.push((node.name().to_string(), f.forbid_symbols.clone()));
}
}
for c in &node.children {
match c.when {
Some(w) if w == on => {
out.active_params.insert(c.name().to_string());
}
Some(_) => {
out.refused_params.insert(
c.name().to_string(),
format!("option '{}' is {}", node.name(), if on { "on" } else { "off" }),
);
}
None => {}
}
}
}
Ok(out)
}
impl ResolvedOptions {
/// Whether the option named resolved ON. `None` when it is not declared.
pub fn is_on(&self, name: &str) -> Option<bool> {
self.record.iter().find(|r| r.name == name).map(|r| r.on)
}
/// **The parameter specs this resolution actually accepts** — the declared
/// roster minus everything a toggle refuses, with the sharper
/// [`OptionSpec::param_kinds`] shape applied to the conditional ones.
///
/// The `required` flag is untouched: a parameter that is active and
/// declares `required = true` is still required, which is how DHCP-off
/// makes the static address mandatory without a second requiredness rule.
pub fn effective_specs(&self, options: &[OptionSpec], params: &[ParamSpec]) -> Vec<ParamSpec> {
// The sharper shape is read off the tree's setting nodes — the same
// node the pane draws — not off the rows again.
let tree = SettingsTree::of_rows("", ArtifactKind::Installer, options, params, &BTreeMap::new());
let sharpen: BTreeMap<String, ParamKind> = tree
.walk()
.into_iter()
.filter_map(|n| match &n.kind {
NodeKind::Setting(s) => Some((n.name().to_string(), s.param_kind)),
_ => None,
})
.collect();
params
.iter()
.filter(|p| !self.refused_params.contains_key(&p.name))
.map(|p| {
let mut p = p.clone();
if let Some(k) = sharpen.get(&p.name) {
p.kind = *k;
}
p
})
.collect()
}
/// **The fixtures this resolution can still mint** — the declared map
/// minus every parameter a toggle refuses.
///
/// Without this the whole feature is unusable on gunnar's real recipe:
/// `validate_fixtures` holds every fixture key against the spec list it is
/// given, so passing the EFFECTIVE list (WireGuard off ⇒ no `hub_conf`)
/// while passing the FULL fixture map refuses the build with "fixture
/// 'wg-hub' is declared for 'hub_conf', which is not a declared parameter"
/// — a refusal about the recipe, on a recipe that is correct.
pub fn effective_fixtures(
&self,
fixtures: &BTreeMap<String, crate::params::FixtureKind>,
) -> BTreeMap<String, crate::params::FixtureKind> {
fixtures
.iter()
.filter(|(n, _)| !self.refused_params.contains_key(n.as_str()))
.map(|(n, k)| (n.clone(), *k))
.collect()
}
/// **Refuse a supplied value for a parameter this resolution forbids**, by
/// name, saying which toggle forbids it. Without this the value would land
/// on a medium whose binary cannot read it.
pub fn refuse_forbidden(&self, supplied: &BTreeMap<String, String>) -> Result<()> {
for name in supplied.keys() {
if let Some(why) = self.refused_params.get(name) {
bail!(
"parameter '{name}' was supplied, but {why} — that parameter has no \
meaning in this build and nothing on the medium would read it"
);
}
}
Ok(())
}
/// The cmdline selectors the resolved options bake, given the values that
/// were actually applied. Deterministic: declaration order.
pub fn render_bakes(
&self,
options: &[OptionSpec],
applied: &[AppliedParam],
) -> Result<Vec<String>> {
let values: BTreeMap<String, String> =
applied.iter().map(|a| (a.name.clone(), a.value.clone())).collect();
let tree = SettingsTree::of_rows("", ArtifactKind::Installer, options, &[], &BTreeMap::new());
let mut out = Vec::new();
for rec in &self.record {
let Some(NodeKind::Feature(f)) = tree.get(&rec.name).map(|n| &n.kind) else { continue };
let Some(b) = &f.bakes else { continue };
let Some(t) = b.template(rec.on) else { continue };
let rendered = render_template(t, &values)
.map_err(|e| anyhow!("option '{}': {e:#}", rec.name))?;
if rendered.split_whitespace().count() != 1 {
bail!(
"option '{}': the baked selector must stay ONE cmdline token, got \
'{rendered}'",
rec.name
);
}
out.push(format!("{}={}", b.boot_param.trim(), rendered));
}
Ok(out)
}
/// **The feature delta, for the receipt and the pane's result row** —
/// `features: +wg-client −dhcp-client`. Empty when nothing is declared.
pub fn feature_delta(&self) -> String {
if self.record.is_empty() {
return String::new();
}
let mut parts = Vec::new();
for r in &self.record {
parts.push(format!("{}{}", if r.on { '+' } else { '−' }, r.feature));
}
format!("features: {}", parts.join(" "))
}
/// The cargo `--features` list for the APP build: the variant's declared
/// features MINUS every OFF feature, PLUS every ON feature that the variant
/// only left `open`. Declaration order is preserved and additions append,
/// so the argv is stable across runs.
pub fn app_features(&self, app: &BuildVariant) -> Vec<String> {
let off: BTreeSet<&str> = self.features_off.iter().map(String::as_str).collect();
let mut out: Vec<String> =
app.features.iter().filter(|f| !off.contains(f.as_str())).cloned().collect();
for f in &self.features_on {
if !out.iter().any(|h| h == f) && app.open.contains(f) {
out.push(f.clone());
}
}
out
}
}
impl ResolvedOptions {
/// **Push this resolution into the build space**, so the APP variant the
/// installer packs is built with exactly the features the toggles decided.
///
/// This is the step that makes "not compiled in" true rather than
/// promised, and it is deliberately a mutation of a CLONED
/// [`BuildVariantSet`] rather than a second argv builder: the one thing
/// that turns a variant into a cargo command line is
/// [`BuildVariant::cargo_args`], and a parallel path would drift from it
/// the first time a flag was added there.
///
/// Every option is held against the variant by
/// [`OptionSpec::validate_against_variant`] first, so a recipe whose
/// toggle could not actually change the build is refused HERE — before a
/// binary exists to audit — rather than passing an audit it was never
/// able to fail.
pub fn apply_to_set(
&self,
options: &[OptionSpec],
set: &mut crate::variant::BuildVariantSet,
app_variant: &str,
) -> Result<()> {
let v = set.variants.iter().find(|v| v.name == app_variant).ok_or_else(|| {
anyhow!(
"installer options decide features of variant '{app_variant}', which this build space does not declare (declared: {})",
set.variants.iter().map(|v| v.name.as_str()).collect::<Vec<_>>().join(", ")
)
})?;
for o in options {
o.validate_against_variant(v)?;
}
let features = self.app_features(v);
let declared = self.declared_row(set, app_variant).map(|r| r.name.clone());
let v = set
.variants
.iter_mut()
.find(|v| v.name == app_variant)
.expect("just found above");
// A feature an ON option ADDED from the row's `open` axis is now a
// DECIDED value of this build, not a switch: it leaves `open`, or the
// set's own validator refuses the row as "both enabled and open"
// (MEASURED release-3 test2 mock 2026-09-30, `--opt dev-narrate=on`).
v.open.retain(|f| !features.contains(f));
v.features = features;
// **The declared build point** (lane P1, 2026-10-20): when another declared row
// carries exactly these features, the cells that pack `app_variant` name THAT row,
// so the medium gets the recipe's own row — its name, its profile (holger's and
// gunnar's `…-narrate` rows build with `dev-media`), its artifact. The payload row
// above keeps the decided features too, so a reader still holding its name builds
// what the toggles said.
if let Some(row) = declared {
for i in set.installers.iter_mut().filter(|i| i.variant == app_variant) {
i.variant = row.clone();
}
for i in set.isos.iter_mut().filter(|i| i.from_variant.as_deref() == Some(app_variant)) {
i.from_variant = Some(row.clone());
}
}
Ok(())
}
/// **The declared row this resolution arrives at**, when it is not `app_variant`
/// itself: another row of the same package, bin, example, target, manifest dir, kind and
/// default-feature switches whose feature SET is exactly [`Self::app_features`] — Gunnar's
/// `xtask payload` rule (package, bin, target, features; never the profile, the one
/// field a `…-narrate` row differs in besides its name). `None` when the toggles leave
/// the payload row's own features, or no other row carries them. Read it on the
/// DECLARED set, before [`Self::apply_to_set`].
pub fn declared_row<'a>(
&self,
set: &'a crate::variant::BuildVariantSet,
app_variant: &str,
) -> Option<&'a BuildVariant> {
let app = set.variants.iter().find(|v| v.name == app_variant)?;
let want: BTreeSet<String> = self.app_features(app).into_iter().collect();
if want == app.features.iter().cloned().collect() {
return None;
}
set.variants.iter().find(|v| {
v.name != app.name
&& v.package == app.package
&& v.bin == app.bin
&& v.example == app.example
&& v.target == app.target
&& v.manifest_dir == app.manifest_dir
&& v.kind == app.kind
&& v.no_default_features == app.no_default_features
&& v.all_features == app.all_features
&& v.features.iter().cloned().collect::<BTreeSet<String>>() == want
})
}
}
/// **The read-back half of the LAW-2 proof**: what a packed binary actually
/// contains for the families an OFF option forbids.
///
/// Reuses [`crate::linkage::demangled_symbols`] and
/// [`crate::linkage::count_families`] rather than walking the symbol table a
/// second time — two readers of one binary would disagree the first time one
/// of them learned about a mangling the other did not, and this one exists to
/// be believed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OptionAudit {
/// The binary read.
pub artifact: std::path::PathBuf,
/// Whether the symbol table could be read at all. `false` ⇒ UNMEASURED,
/// **never** a pass: a stripped or absent binary has nothing to say, and
/// "nothing found" from a reader that saw nothing is the silent green this
/// whole crate keeps refusing.
pub measured: bool,
/// `(option, family, non-glue symbol count)` for every family that is
/// PRESENT although its option is off. Empty on a clean OFF build.
pub violations: Vec<(String, String, usize)>,
/// `(option, family, glue count)` — families whose only evidence is
/// compiler destructor glue. Reported, not counted, exactly as
/// [`crate::linkage::LinkageAudit`] does.
pub glue_only: Vec<(String, String, usize)>,
}
impl OptionAudit {
/// Audit `artifact` against everything `resolved` says must be absent.
///
/// Returns an audit even when nothing is forbidden — a resolution with all
/// options ON still reads the binary, so `measured` reports honestly and a
/// caller cannot mistake "nothing to check" for "checked and clean".
pub fn of(artifact: &std::path::Path, resolved: &ResolvedOptions) -> OptionAudit {
let symbols = crate::linkage::demangled_symbols(artifact);
let measured = symbols.is_some();
let symbols = symbols.unwrap_or_default();
let mut violations = Vec::new();
let mut glue_only = Vec::new();
if measured {
for (opt, families) in &resolved.forbidden_symbols {
let fams: Vec<&str> = families.iter().map(String::as_str).collect();
let counts = crate::linkage::count_families(&symbols, &fams);
for (fam, n) in counts.found {
violations.push((opt.clone(), fam, n));
}
for (fam, n) in counts.glue_only {
glue_only.push((opt.clone(), fam, n));
}
}
}
OptionAudit { artifact: artifact.to_path_buf(), measured, violations, glue_only }
}
/// Does the build hold? Only when the binary WAS read and no forbidden
/// family is present. Nothing forbidden + measured ⇒ holds.
pub fn holds(&self) -> bool {
self.measured && self.violations.is_empty()
}
/// One line naming why it does not hold, or `None` when it does.
pub fn refusal(&self) -> Option<String> {
if self.holds() {
return None;
}
if !self.measured {
return Some(format!(
"option audit UNMEASURED: {} has no readable symbol table (absent, not an \
object file, or STRIPPED) — an unchecked guarantee is not a guarantee",
self.artifact.display()
));
}
let hits: Vec<String> = self
.violations
.iter()
.map(|(o, f, n)| format!("{o} is off but '{f}' is present ×{n}"))
.collect();
Some(format!(
"option audit RED on {}: {}",
self.artifact.display(),
hits.join("; ")
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::params::ParamKind;
fn p(name: &str, required: bool) -> ParamSpec {
ParamSpec {
name: name.to_string(),
label: name.to_string(),
kind: ParamKind::Text,
required,
boot_param: String::new(),
staged_file: format!("{name}.txt"),
lands: crate::params::Landing::Data,
}
}
fn boot_p(name: &str, required: bool) -> ParamSpec {
ParamSpec {
name: name.to_string(),
label: name.to_string(),
kind: ParamKind::Text,
required,
boot_param: format!("korp.{name}"),
staged_file: String::new(),
lands: crate::params::Landing::Data,
}
}
fn dhcp_option() -> OptionSpec {
OptionSpec {
name: "dhcp".into(),
label: "DHCP (eth0)".into(),
default: true,
feature: "dhcp-client".into(),
params_when_off: vec!["static_ip".into(), "gateway".into(), "dns".into()],
param_kinds: [
("static_ip".to_string(), ParamKind::Ipv4Cidr),
("gateway".to_string(), ParamKind::Ipv4),
("dns".to_string(), ParamKind::Ipv4),
]
.into_iter()
.collect(),
forbid_symbols: vec!["dhcp".into()],
bakes: Some(OptionBake {
boot_param: "korp.env.GUNNAR_NET".into(),
when_on: "eth0:dhcp".into(),
when_off: "eth0:static:{static_ip}:{gateway}:{dns}".into(),
}),
..Default::default()
}
}
fn wg_option() -> OptionSpec {
OptionSpec {
name: "wireguard".into(),
label: "WireGuard-klient".into(),
default: true,
feature: "wg-client".into(),
params_when_on: vec!["hub_conf".into()],
forbid_symbols: vec!["wg_appliance_core::client".into(), "x25519".into()],
..Default::default()
}
}
fn net_params() -> Vec<ParamSpec> {
vec![
boot_p("static_ip", true),
boot_p("gateway", true),
boot_p("dns", true),
p("hub_conf", false),
]
}
#[test]
fn a_declared_roster_validates() {
validate_options(&[dhcp_option(), wg_option()], &net_params()).unwrap();
}
#[test]
fn an_option_naming_an_undeclared_parameter_is_refused_by_name() {
let mut o = dhcp_option();
o.params_when_off.push("netmask".into());
let e = format!("{:#}", validate_options(&[o], &net_params()).unwrap_err());
assert!(e.contains("netmask"), "{e}");
assert!(e.contains("not a declared parameter"), "{e}");
assert!(e.contains("static_ip"), "the refusal must list what IS declared: {e}");
}
#[test]
fn an_option_with_no_feature_is_refused() {
let mut o = dhcp_option();
o.feature = String::new();
let e = format!("{:#}", validate_options(&[o], &net_params()).unwrap_err());
assert!(e.contains("declares no `feature`"), "{e}");
}
#[test]
fn a_parameter_in_both_branches_is_refused() {
let mut o = dhcp_option();
o.params_when_on.push("static_ip".into());
let e = format!("{:#}", validate_options(&[o], &net_params()).unwrap_err());
assert!(e.contains("BOTH params_when_on and params_when_off"), "{e}");
}
#[test]
fn two_options_may_not_own_one_parameter_or_one_feature() {
let mut a = wg_option();
let mut b = wg_option();
b.name = "wireguard2".into();
let e = format!("{:#}", validate_options(&[a.clone(), b.clone()], &net_params()).unwrap_err());
assert!(e.contains("decided by two options"), "{e}");
a.feature = "wg-a".into();
b.feature = "wg-b".into();
let e = format!("{:#}", validate_options(&[a, b], &net_params()).unwrap_err());
assert!(e.contains("conditional by TWO options"), "{e}");
}
#[test]
fn a_secret_named_option_is_refused() {
let mut o = wg_option();
o.name = "wg_private".into();
let e = format!("{:#}", validate_options(&[o], &net_params()).unwrap_err());
assert!(e.contains("NAMES A SECRET"), "{e}");
}
#[test]
fn a_bake_template_may_not_reach_for_a_parameter_its_own_state_refuses() {
let mut o = dhcp_option();
// `static_ip` is refused when dhcp is ON, so when_on may not use it.
o.bakes.as_mut().unwrap().when_on = "eth0:{static_ip}".into();
let e = format!("{:#}", validate_options(&[o], &net_params()).unwrap_err());
assert!(e.contains("REFUSES"), "{e}");
assert!(e.contains("cannot exist"), "{e}");
}
#[test]
fn an_unclosed_placeholder_is_refused_not_shipped_literally() {
let mut o = dhcp_option();
o.bakes.as_mut().unwrap().when_off = "eth0:static:{static_ip".into();
let e = format!("{:#}", validate_options(&[o], &net_params()).unwrap_err());
assert!(e.contains("no closing"), "{e}");
}
#[test]
fn param_kinds_may_only_sharpen_a_parameter_this_option_makes_conditional() {
let mut o = wg_option();
o.param_kinds.insert("hub_conf".into(), ParamKind::Ipv4);
// hub_conf IS conditional on wireguard, so this validates …
validate_options(&[o.clone()], &net_params()).unwrap();
// … but a parameter no branch names does not.
o.param_kinds.insert("gateway".into(), ParamKind::Ipv4);
let e = format!("{:#}", validate_options(&[o], &net_params()).unwrap_err());
assert!(e.contains("does not make conditional"), "{e}");
}
#[test]
fn defaults_stand_when_nobody_says_and_the_record_says_so() {
let opts = [dhcp_option(), wg_option()];
let r = resolve_options(&opts, &net_params(), &BTreeMap::new()).unwrap();
assert_eq!(r.features_on, vec!["dhcp-client", "wg-client"]);
assert!(r.features_off.is_empty());
assert!(r.record.iter().all(|a| a.on && a.from_default));
// dhcp ON refuses the static address; wireguard ON allows hub_conf.
assert!(r.refused_params.contains_key("static_ip"));
assert!(r.active_params.contains("hub_conf"));
assert!(r.forbidden_symbols.is_empty(), "nothing is off, so nothing is forbidden");
}
#[test]
fn turning_dhcp_off_subtracts_the_feature_and_makes_the_static_address_required() {
let opts = [dhcp_option(), wg_option()];
let supplied = [("dhcp".to_string(), false)].into_iter().collect();
let r = resolve_options(&opts, &net_params(), &supplied).unwrap();
assert_eq!(r.features_on, vec!["wg-client"]);
assert_eq!(r.features_off, vec!["dhcp-client"]);
assert!(!r.record[0].from_default, "the operator said so");
let eff = r.effective_specs(&opts, &net_params());
let ip = eff.iter().find(|s| s.name == "static_ip").expect("static_ip is active");
assert!(ip.required);
assert_eq!(ip.kind, ParamKind::Ipv4Cidr, "param_kinds sharpened the shape");
assert_eq!(
eff.iter().find(|s| s.name == "gateway").unwrap().kind,
ParamKind::Ipv4
);
assert_eq!(r.forbidden_symbols, vec![("dhcp".to_string(), vec!["dhcp".to_string()])]);
}
#[test]
fn turning_wireguard_off_refuses_hub_conf_by_name() {
let opts = [dhcp_option(), wg_option()];
let supplied = [("wireguard".to_string(), false)].into_iter().collect();
let r = resolve_options(&opts, &net_params(), &supplied).unwrap();
assert!(!r.effective_specs(&opts, &net_params()).iter().any(|s| s.name == "hub_conf"));
let e = format!(
"{:#}",
r.refuse_forbidden(&[("hub_conf".to_string(), "x".to_string())].into_iter().collect())
.unwrap_err()
);
assert!(e.contains("hub_conf"), "{e}");
assert!(e.contains("option 'wireguard' is off"), "{e}");
}
/// The resolver answers THROUGH THE TREE: a full path spells the same
/// toggle, a setting's name is refused as "not an option" by kind, and a
/// typo lists the declared roots.
#[test]
fn the_resolver_speaks_tree_paths_and_refuses_a_setting_by_kind() {
let opts = [dhcp_option(), wg_option()];
let supplied = [("dhcp".to_string(), false)].into_iter().collect();
let r = resolve_options(&opts, &net_params(), &supplied).unwrap();
assert_eq!(r.features_off, vec!["dhcp-client"]);
let e = format!(
"{:#}",
resolve_options(&opts, &net_params(), &[("static_ip".to_string(), false)].into_iter().collect()).unwrap_err()
);
assert!(e.contains("is a setting") && e.contains("dhcp.static_ip"), "{e}");
}
#[test]
fn an_unknown_toggle_is_refused_with_the_declared_list() {
let supplied = [("dchp".to_string(), false)].into_iter().collect();
let e = format!(
"{:#}",
resolve_options(&[dhcp_option(), wg_option()], &net_params(), &supplied).unwrap_err()
);
assert!(e.contains("'dchp' is not a declared option"), "{e}");
assert!(e.contains("dhcp, wireguard"), "{e}");
}
#[test]
fn the_bake_renders_the_selector_for_the_resolved_state() {
let opts = [dhcp_option()];
let on = resolve_options(&opts, &net_params(), &BTreeMap::new()).unwrap();
assert_eq!(
on.render_bakes(&opts, &[]).unwrap(),
vec!["korp.env.GUNNAR_NET=eth0:dhcp"]
);
let supplied = [("dhcp".to_string(), false)].into_iter().collect();
let off = resolve_options(&opts, &net_params(), &supplied).unwrap();
let applied = vec![
AppliedParam::supplied("static_ip", "192.168.1.50/24"),
AppliedParam::supplied("gateway", "192.168.1.1"),
AppliedParam::supplied("dns", "9.9.9.9"),
];
assert_eq!(
off.render_bakes(&opts, &applied).unwrap(),
vec!["korp.env.GUNNAR_NET=eth0:static:192.168.1.50/24:192.168.1.1:9.9.9.9"]
);
}
#[test]
fn a_bake_with_a_missing_value_refuses_rather_than_shipping_a_brace() {
let opts = [dhcp_option()];
let supplied = [("dhcp".to_string(), false)].into_iter().collect();
let off = resolve_options(&opts, &net_params(), &supplied).unwrap();
let e = format!("{:#}", off.render_bakes(&opts, &[]).unwrap_err());
assert!(e.contains("no value for '{static_ip}'"), "{e}");
}
#[test]
fn the_cli_spellings_parse_and_a_typo_is_refused() {
let m = parse_cli_options(&["dhcp=off", "wireguard=TRUE"]).unwrap();
assert_eq!(m.get("dhcp"), Some(&false));
assert_eq!(m.get("wireguard"), Some(&true));
let e = format!("{:#}", parse_cli_options(&["dhcp=of"]).unwrap_err());
assert!(e.contains("an option is on or off"), "{e}");
let e = format!("{:#}", parse_cli_options(&["dhcp"]).unwrap_err());
assert!(e.contains("name=on|off"), "{e}");
}
fn app_variant() -> BuildVariant {
BuildVariant {
name: "gunnar-server".into(),
features: vec![
"server".into(),
"engine-znippy".into(),
"dhcp-client".into(),
"wg-client".into(),
],
no_default_features: true,
..Default::default()
}
}
#[test]
fn the_variant_check_holds_for_a_correct_declaration() {
let app = app_variant();
dhcp_option().validate_against_variant(&app).unwrap();
wg_option().validate_against_variant(&app).unwrap();
}
#[test]
fn a_variant_without_no_default_features_makes_every_off_build_a_lie() {
let mut app = app_variant();
app.no_default_features = false;
let e = format!("{:#}", dhcp_option().validate_against_variant(&app).unwrap_err());
assert!(e.contains("no_default_features"), "{e}");
assert!(e.contains("behind the toggle's back"), "{e}");
}
#[test]
fn a_default_on_option_whose_feature_the_variant_never_enables_is_refused() {
let mut app = app_variant();
app.features.retain(|f| f != "dhcp-client");
let e = format!("{:#}", dhcp_option().validate_against_variant(&app).unwrap_err());
assert!(e.contains("neither enables nor declares `open`"), "{e}");
}
#[test]
fn a_default_off_option_must_ride_the_variants_open_axis() {
let mut app = app_variant();
let mut o = wg_option();
o.default = false;
// still in `features` ⇒ always on ⇒ refused
let e = format!("{:#}", o.validate_against_variant(&app).unwrap_err());
assert!(e.contains("move it to that variant's `open`"), "{e}");
app.features.retain(|f| f != "wg-client");
app.open.insert("wg-client".to_string());
o.validate_against_variant(&app).unwrap();
}
#[test]
fn app_features_subtract_the_off_features_and_keep_declaration_order() {
let opts = [dhcp_option(), wg_option()];
let app = app_variant();
let supplied = [("wireguard".to_string(), false)].into_iter().collect();
let r = resolve_options(&opts, &net_params(), &supplied).unwrap();
assert_eq!(r.app_features(&app), vec!["server", "engine-znippy", "dhcp-client"]);
assert_eq!(r.feature_delta(), "features: +dhcp-client −wg-client");
}
#[test]
fn an_open_feature_is_added_when_its_option_is_switched_on() {
let mut app = app_variant();
app.features.retain(|f| f != "wg-client");
app.open.insert("wg-client".to_string());
let mut o = wg_option();
o.default = false;
let opts = [o];
let supplied = [("wireguard".to_string(), true)].into_iter().collect();
let r = resolve_options(&opts, &net_params(), &supplied).unwrap();
assert_eq!(r.app_features(&app), vec!["server", "engine-znippy", "dhcp-client", "wg-client"]);
}
/// **An OPEN feature switched ON is a DECIDED feature of the build, and no
/// longer a switch** (DEV-NARRATE, 2026-09-30). MEASURED on release-3's test2
/// mock (gunnar 3e374eb, `--opt dev-narrate=on`): `apply_to_set` added
/// `dev-narrate` to `gunnar-server-metered`'s `features` and LEFT it in
/// `open`, and the set's own validator then refused the build — `feature
/// 'dev-narrate' is both enabled and open`. ON and OFF must both build.
#[test]
fn a_default_off_option_switched_on_builds_and_its_feature_leaves_the_open_axis() {
let mut app = app_variant();
app.features.retain(|f| f != "wg-client");
app.open.insert("wg-client".to_string());
let mut o = wg_option();
o.default = false;
let opts = [o];
for on in [true, false] {
let mut set = crate::variant::BuildVariantSet { variants: vec![app.clone()], ..Default::default() };
let supplied = [("wireguard".to_string(), on)].into_iter().collect();
let r = resolve_options(&opts, &net_params(), &supplied).unwrap();
r.apply_to_set(&opts, &mut set, "gunnar-server").unwrap();
let v = set.get("gunnar-server").unwrap().clone();
set.validate(&v).unwrap_or_else(|e| panic!("wireguard={on}: {e:#}"));
assert_eq!(v.features.contains(&"wg-client".to_string()), on, "{v:?}");
assert_eq!(v.open.contains("wg-client"), !on, "ON: decided, not a switch; OFF: still the row's switch: {v:?}");
}
}
/// **An ON option ARRIVES at the declared row that carries exactly its features**
/// (lane P1, 2026-10-20). MEASURED on holger (R6): `--opt dev-narrate=on` built
/// `mvp-server-musl-metered` + `dev-narrate` under `release/`, beside the declared
/// `mvp-server-musl-metered-narrate` row and its `dev-media` profile. The cell now
/// names the declared row (Gunnar's `xtask payload` rule: same package, bin, target,
/// features); OFF keeps the payload row.
#[test]
fn an_on_option_retargets_the_cell_to_the_declared_row_with_exactly_its_features() {
let mut app = app_variant();
app.open.insert("dev-narrate".to_string());
app.profile = crate::variant::Profile::Release;
let mut dev = app.clone();
dev.name = "gunnar-server-narrate".to_string();
dev.open.remove("dev-narrate");
dev.features.push("dev-narrate".to_string());
dev.profile = crate::variant::Profile::Custom("dev-media".to_string());
let o = OptionSpec { name: "dev-narrate".into(), label: "DEV-NARRATE".into(), feature: "dev-narrate".into(), default: false, forbid_symbols: vec!["nordisk_narrate".into()], ..Default::default() };
let opts = [o];
for (on, want, profile) in [(true, "gunnar-server-narrate", "dev-media"), (false, "gunnar-server", "release")] {
let mut set = crate::variant::BuildVariantSet {
variants: vec![app.clone(), dev.clone()],
installers: vec![crate::installer::InstallerBuild { name: "medium".into(), variant: "gunnar-server".into(), ..Default::default() }],
..Default::default()
};
let supplied = [("dev-narrate".to_string(), on)].into_iter().collect();
let r = resolve_options(&opts, &[], &supplied).unwrap();
assert_eq!(r.declared_row(&set, "gunnar-server").map(|v| v.name.as_str()), on.then_some("gunnar-server-narrate"));
r.apply_to_set(&opts, &mut set, "gunnar-server").unwrap();
let cell = set.installers[0].variant.clone();
assert_eq!(cell, want, "dev-narrate={on}");
let v = set.get(&cell).unwrap();
assert_eq!(v.profile.label(), profile, "dev-narrate={on}: {v:?}");
assert_eq!(v.features.contains(&"dev-narrate".to_string()), on);
assert_eq!(set.get("gunnar-server-narrate").unwrap(), &dev, "the declared row is built as declared");
}
}
// ── the audit ────────────────────────────────────────────────────────────
#[test]
fn an_unreadable_binary_is_unmeasured_and_never_a_pass() {
let opts = [wg_option()];
let supplied = [("wireguard".to_string(), false)].into_iter().collect();
let r = resolve_options(&opts, &net_params(), &supplied).unwrap();
let a = OptionAudit::of(std::path::Path::new("/nonexistent/binary"), &r);
assert!(!a.measured);
assert!(!a.holds(), "an audit that read nothing must not pass");
assert!(a.refusal().unwrap().contains("UNMEASURED"));
}
/// LAW 2 on the audit itself: build the two symbol tables by hand and prove
/// the guard reports RED on the one that carries the forbidden family and
/// GREEN on the one that does not. Without both arms a guard that always
/// said "clean" would look identical.
#[test]
fn the_audit_counts_a_forbidden_family_and_ignores_destructor_glue() {
let symbols_off: Vec<String> = vec![
"gunnar_server::serve".into(),
"core::ptr::drop_in_place::<x25519_dalek::StaticSecret>".into(),
];
let symbols_on: Vec<String> = vec![
"gunnar_server::serve".into(),
"x25519_dalek::StaticSecret::new".into(),
"wg_appliance_core::client::handshake".into(),
];
let fams = ["x25519", "wg_appliance_core::client"];
let clean = crate::linkage::count_families(&symbols_off, &fams);
assert!(clean.found.is_empty(), "glue alone is not linkage: {clean:?}");
assert_eq!(clean.glue_only, vec![("x25519".to_string(), 1)]);
let dirty = crate::linkage::count_families(&symbols_on, &fams);
assert_eq!(
dirty.found,
vec![("x25519".to_string(), 1), ("wg_appliance_core::client".to_string(), 1)]
);
}
}