crates/nornir-build-thing/src/consequence.rs
//! **The consequence of a choice, derived bijectively — and per `typ`.**
//!
//! The owner's own words for what Build Thing IS: *"koden som bygger och visar hur
//! bygget påverkas bijektivt"* — it is not a catalogue that LISTS variants, it is
//! the code that COMPUTES, from the declaration, exactly what appears and what
//! disappears when a setting is toggled. Add a feature and the code derives what
//! it drags in; remove it and the code derives what falls away.
//!
//! # The consequence's SPACE depends on the `typ`, and the two are not one space
//!
//! A build thing's [`ArtifactKind`](crate::variant::ArtifactKind) — the `typ` —
//! decides what "the consequence of a choice" even MEANS, and forcing both
//! through one closure would be a category error:
//!
//! * a **cargo / app** variant ([`Exe`](crate::variant::ArtifactKind::Exe),
//! [`Lib`](crate::variant::ArtifactKind::Lib), `Service`, …) — the consequence
//! is the **dependency / feature CLOSURE** delta. Turning `wireguard` off does
//! not merely unset a flag; it removes `boringtun` and `wg_appliance_core` from
//! what compiles (the symbol families measured `70 → 0` on gunnar the day this
//! was written). Computed by reusing [`FeatureTable::closure`] — the same pure
//! closure walk the suite guard uses — never a second one.
//! * a **kernel** variant ([`Kernel`](crate::variant::ArtifactKind::Kernel)) —
//! the consequence is the **Kconfig MODULE** delta: which `CONFIG_*` symbols
//! enter and leave the merged `.config`, and therefore the `bzImage`. Computed
//! by reusing [`crate::kernel::merge_kconfig`] — the same merge the kernel
//! build runs — and diffing the enabled symbols.
//!
//! # Why it is bijective, and what that buys
//!
//! The forward map is `toggle → Consequence`. The inverse
//! ([`toggle_of_features`], [`group_of_symbol`]) recovers the TOGGLE from the
//! result — the delta names its own cause. That is the same shape as law D
//! (decision ↔ fragment): a receipt that carries the consequence carries enough
//! to name the choice that produced it, so a consequence can never be an orphan
//! nobody can trace back to a setting.
use std::collections::BTreeSet;
use crate::kernel::merge_kconfig;
use crate::variant::FeatureTable;
/// What a toggle brings IN and takes OUT — the two halves of one delta, in the
/// space its `typ` decides (cargo features/deps, or `CONFIG_*` symbols).
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Consequence {
/// Tokens present AFTER the toggle that were absent before.
pub enters: BTreeSet<String>,
/// Tokens present BEFORE the toggle that are absent after.
pub leaves: BTreeSet<String>,
}
impl Consequence {
/// The delta from a `before`/`after` pair of resolved sets. This is the ONE
/// place a consequence is computed from two states, so the cargo and kernel
/// halves below share the diff rather than each writing `after - before`.
pub fn between(before: &BTreeSet<String>, after: &BTreeSet<String>) -> Self {
Consequence {
enters: after.difference(before).cloned().collect(),
leaves: before.difference(after).cloned().collect(),
}
}
/// Empty ⇒ the toggle changed nothing that reaches the build. A real red
/// flag for a setting that claims to matter — a choice with no consequence is
/// a choice the build cannot see.
pub fn is_empty(&self) -> bool {
self.enters.is_empty() && self.leaves.is_empty()
}
}
// ── cargo / app typ: the feature-closure delta ────────────────────────────────
/// **The cargo consequence of toggling `feature` on the base selection.**
///
/// `base` is the variant's declared feature selection; `with_defaults` mirrors
/// `!no_default_features`. The result is the delta between the closure WITHOUT
/// `feature` and the closure WITH it (or the reverse for `on = false`), so
/// `enters`/`leaves` are exactly the tokens that reach — or stop reaching —
/// cargo. Reuses [`FeatureTable::closure`]; no second closure exists.
pub fn feature_consequence(
ft: &FeatureTable,
base: &[String],
feature: &str,
on: bool,
with_defaults: bool,
) -> Consequence {
let mut without: Vec<String> = base.iter().filter(|f| *f != feature).cloned().collect();
let mut with = without.clone();
with.push(feature.to_string());
let closed_without = ft.closure(&without, with_defaults);
let closed_with = ft.closure(&with, with_defaults);
// `without` and `with` are the base sans/with the toggle; consume them so a
// careless later edit cannot read a stale copy.
without.clear();
with.clear();
if on {
// going off→on: what the presence of `feature` DRAGS IN
Consequence::between(&closed_without, &closed_with)
} else {
// going on→off: what the absence of `feature` TAKES OUT
Consequence::between(&closed_with, &closed_without)
}
}
/// **The inverse: name the feature whose toggle produced this delta.**
///
/// Given the same graph and base, tries each candidate and returns the one whose
/// `feature_consequence` equals `delta`. `None` when no single candidate explains
/// it (an ambiguous or invented delta) — the bijection refusing to lie, exactly
/// as the flag/decision laws refuse an alias they cannot resolve.
pub fn toggle_of_features(
ft: &FeatureTable,
base: &[String],
candidates: &[String],
on: bool,
delta: &Consequence,
) -> Option<String> {
let mut found = None;
for c in candidates {
if &feature_consequence(ft, base, c, on, true) == delta {
if found.is_some() {
return None; // two toggles explain it: not a bijection, refuse
}
found = Some(c.clone());
}
}
found
}
// ── kernel typ: the Kconfig module delta ──────────────────────────────────────
/// The `CONFIG_*` symbols a merged `.config` ENABLES (`=y` or `=m`). A
/// `# CONFIG_X is not set` line and a bare comment enable nothing and are not
/// counted — the same reading the kernel build's own merge produces.
pub fn enabled_symbols(merged: &str) -> BTreeSet<String> {
merged
.lines()
.filter_map(|l| {
let l = l.trim();
if l.starts_with('#') || l.is_empty() {
return None;
}
let (sym, val) = l.split_once('=')?;
let val = val.trim();
(val == "y" || val == "m").then(|| sym.trim().to_string())
})
.collect()
}
/// **The kernel consequence of a fragment choice.** `with`/`without` are two
/// fragment stacks (base, hardened overlay, a group's lines); the result is the
/// `CONFIG_*` symbols that enter/leave the merged `.config` — and therefore the
/// `bzImage`. Reuses [`merge_kconfig`], the build's own merge, so a symbol this
/// reports as entering is a symbol the built kernel actually gains.
pub fn kconfig_consequence(with: &[&str], without: &[&str]) -> Consequence {
let on = enabled_symbols(&merge_kconfig(with));
let off = enabled_symbols(&merge_kconfig(without));
Consequence::between(&off, &on)
}
/// The six groups that made the thin appliance kernel RUNNABLE — each a
/// named toggle over `CONFIG_*` symbols. The value is the symbols the group
/// turns on; the key is what the operator would ask for. This is the kernel
/// `typ`'s vocabulary, the counterpart of a cargo feature's name.
pub fn thin_kernel_groups() -> &'static [(&'static str, &'static [&'static str])] {
&[
("kernel.proc-fs", &["CONFIG_PROC_FS"]),
("kernel.memfd-create", &["CONFIG_MEMFD_CREATE"]),
("kernel.shmem-tmpfs", &["CONFIG_SHMEM", "CONFIG_TMPFS"]),
("kernel.file-locking", &["CONFIG_FILE_LOCKING"]),
(
"kernel.event-syscalls",
&[
"CONFIG_EPOLL",
"CONFIG_EVENTFD",
"CONFIG_TIMERFD",
"CONFIG_SIGNALFD",
"CONFIG_FUTEX",
"CONFIG_POSIX_TIMERS",
],
),
("kernel.devtmpfs", &["CONFIG_DEVTMPFS", "CONFIG_DEVTMPFS_MOUNT"]),
]
}
/// **The inverse: name the thin-kernel group a symbol belongs to.** Recovers the
/// toggle (the group handle) from a symbol in a consequence's `enters`/`leaves`,
/// so a kernel delta traces back to the choice that caused it. `None` for a
/// symbol no declared group owns.
pub fn group_of_symbol(symbol: &str) -> Option<&'static str> {
thin_kernel_groups()
.iter()
.find(|(_, syms)| syms.contains(&symbol))
.map(|(handle, _)| *handle)
}