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

nordisk/edda

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)
}