crates/nornir-build-thing/src/requires.rs
//! **The requirement CLOSURE — generic over sub build-things.** (the owner,
//! 2026-08-29: *"the kernel is just ONE sub build-thing; any feature of any
//! consumer may declare requirements on any sub build-thing; put the mechanism
//! in the ENGINE, the repos only declare and consume."*)
//!
//! A consumer's tree (`variant → features → settings`) carries EDGES to the sub
//! build-things it consumes (`kernel → tunnr/tunnr-kernel : kernel`; later a
//! toolchain image, a container base, a firmware). A feature of the consumer
//! may place a [`Requirement`] on the thing behind an edge; the requirement is
//! typed by the donor's SURFACE — a `typ = kernel` donor exposes Kconfig
//! symbols ([`Requirement::Kconfig`]), and a future `typ = container` donor
//! would expose its own. The engine:
//!
//! 1. **collects** — [`closure_of_consumer`] asks every [`RequirementSource`]
//! (the declared option rows, the row-level list, the appliance-boot
//! contract, the `target.*` facts when that lane plugs in) what the
//! consumer's ENABLED features require, per edge;
//! 2. **unions recursively** — the planner ([`crate::build_dag::BuildDag`])
//! walks variant → edges → the donor's own edges and joins every consumer's
//! closure for one coordinate into that step's requirements;
//! 3. **refuses by name** — two requirers disagreeing, or a donor preset that
//! vetoes a requirement, is a refusal naming symbol · consumer · feature ·
//! reason ([`SurfaceClosure`] dispatches to the surface's own checks);
//! 4. **inverts** — the surface emits its why-index beside the artifact
//! (`kernel.why.toml` for Kconfig: [`crate::kernel_requires::WhyIndex`]).
//!
//! Both thin doors — `dwarves buildthing` and the ⚒ pane — call the same
//! derivation and the same compose path, so there is ONE evaluator and no
//! CLI-only road. Nothing here reads an emitted toml back as authority: the
//! sources read the [`BuildVariantSet`] the impl produced.
//!
//! # Plugging in a source (the `target.*` lane)
//!
//! Implement [`RequirementSource`] over the consumer view and add it to
//! [`default_sources`]. A `target.consoles ∌ serial` fact then yields
//! `Requirement::Kconfig(SERIAL_8250 = n)` on edge `kernel`, joins the same
//! union, is refused by the same rule and appears in the same why-index — no
//! second mechanism.
use std::collections::BTreeMap;
use anyhow::{anyhow, bail, Result};
use serde::{Deserialize, Serialize};
use crate::kernel_requires::{KconfigReq, KernelRequirements};
use crate::options::{OptionSpec, ResolvedOptions};
use crate::toolchain_requires::{ToolRequirements, ToolReq};
use crate::variant::{ArtifactKind, BuildVariantSet};
/// **One requirement, typed by the SURFACE of the sub build-thing it is placed
/// on.** The tag is the surface; a donor whose typ has no surface variant here
/// cannot be required of yet, and saying so is a named refusal.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "surface", rename_all = "kebab-case")]
pub enum Requirement {
/// A Kconfig symbol on a `typ = kernel` donor.
Kconfig(KconfigReq),
/// A TOOL on a `typ = container` donor — the toolchain-image surface
/// ([`crate::toolchain_requires`]). The second surface, and the one that
/// made the word "generic" in this module's header true.
Tool(ToolReq),
}
impl Requirement {
/// The donor typ this requirement can be placed on.
pub fn typ(&self) -> ArtifactKind {
match self {
Requirement::Kconfig(_) => ArtifactKind::Kernel,
Requirement::Tool(_) => ArtifactKind::Container,
}
}
/// Declaration-time check, by the surface's own rule.
pub fn validate(&self, who: &str) -> Result<()> {
match self {
Requirement::Kconfig(r) => r.validate(who),
Requirement::Tool(r) => r.validate(who),
}
}
}
/// **A requirement addressed to an edge** — `edge` is the feature path on the
/// consumer that carries the consumption edge (`kernel`, `b.c` for a chained one).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EdgeRequirement {
/// The consumer's edge path.
pub edge: String,
/// What is required of the thing behind it.
#[serde(flatten)]
pub what: Requirement,
}
/// **The union for ONE edge**, per surface — the data a step carries.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "surface", rename_all = "kebab-case")]
pub enum SurfaceClosure {
/// Kconfig symbols → requirers ([`KernelRequirements`]).
Kconfig(KernelRequirements),
/// Tools → requirers ([`ToolRequirements`]) — the toolchain-image surface.
Tool(ToolRequirements),
}
impl SurfaceClosure {
/// The donor typ this closure is about.
pub fn typ(&self) -> ArtifactKind {
match self {
SurfaceClosure::Kconfig(_) => ArtifactKind::Kernel,
SurfaceClosure::Tool(_) => ArtifactKind::Container,
}
}
/// Nothing required.
pub fn is_empty(&self) -> bool {
match self {
SurfaceClosure::Kconfig(k) => k.is_empty(),
SurfaceClosure::Tool(t) => t.is_empty(),
}
}
/// Join another consumer's closure over the same edge (same surface, else refused).
pub fn union(&mut self, other: &SurfaceClosure) -> Result<()> {
match (self, other) {
(SurfaceClosure::Kconfig(a), SurfaceClosure::Kconfig(b)) => {
a.union(b);
Ok(())
}
(SurfaceClosure::Tool(a), SurfaceClosure::Tool(b)) => {
a.union(b);
Ok(())
}
// One edge, two surfaces: refused by name rather than resolved by a
// precedence nobody remembers. Reachable only through a hand-built
// closure — the edge's typ is checked before a requirement lands.
(a, b) => bail!(
"one edge cannot carry two surfaces — a {} closure joined with a {} one",
a.typ().as_str(),
b.typ().as_str()
),
}
}
/// Requirer against requirer, as refusal sentences (empty = consistent).
pub fn conflicts(&self) -> Vec<String> {
match self {
SurfaceClosure::Kconfig(k) => k.conflicts().iter().map(ToString::to_string).collect(),
SurfaceClosure::Tool(t) => t.conflicts().iter().map(ToString::to_string).collect(),
}
}
/// The step-log sentence.
pub fn summary(&self) -> String {
match self {
SurfaceClosure::Kconfig(k) => k.summary(),
SurfaceClosure::Tool(t) => t.summary(),
}
}
/// The Kconfig half, when this is one.
pub fn as_kconfig(&self) -> Option<&KernelRequirements> {
match self {
SurfaceClosure::Kconfig(k) => Some(k),
_ => None,
}
}
/// The toolchain half, when this is one.
pub fn as_tools(&self) -> Option<&ToolRequirements> {
match self {
SurfaceClosure::Tool(t) => Some(t),
_ => None,
}
}
}
/// **Everything one consumer requires of its sub build-things**, by edge path.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Closure {
/// Edge path → the union over that edge.
pub by_edge: BTreeMap<String, SurfaceClosure>,
}
impl Closure {
/// Place one requirement from `feature` of `consumer` (`repo/variant`).
pub fn require(&mut self, consumer: &str, feature: &str, r: &EdgeRequirement) {
match &r.what {
Requirement::Kconfig(k) => {
let slot = self.by_edge.entry(r.edge.clone()).or_insert_with(|| SurfaceClosure::Kconfig(KernelRequirements::default()));
if let SurfaceClosure::Kconfig(kr) = slot {
kr.require(consumer, feature, k);
}
}
Requirement::Tool(t) => {
let slot = self.by_edge.entry(r.edge.clone()).or_insert_with(|| SurfaceClosure::Tool(ToolRequirements::default()));
if let SurfaceClosure::Tool(tr) = slot {
tr.require(consumer, feature, t);
}
}
}
}
/// The closure over one edge, if any feature required anything of it.
pub fn on_edge(&self, edge: &str) -> Option<&SurfaceClosure> {
self.by_edge.get(edge).filter(|c| !c.is_empty())
}
/// Nothing required of anything.
pub fn is_empty(&self) -> bool {
self.by_edge.values().all(SurfaceClosure::is_empty)
}
}
/// **What a [`RequirementSource`] is shown** — the consumer, resolved.
pub struct ConsumerView<'a> {
/// `repo/variant`.
pub consumer: String,
/// The repo name.
pub repo: &'a str,
/// The variant name.
pub variant: &'a str,
/// The consumer's whole declaration (the impl's answer, never a toml read back).
pub set: &'a BuildVariantSet,
/// The option rows of this variant (installer/iso), empty for other kinds.
pub options: &'a [OptionSpec],
/// The row-level requirements of this variant.
pub row_requires: &'a [EdgeRequirement],
/// The resolved toggles (`--opt name=on|off` over the declared defaults).
pub resolved: &'a ResolvedOptions,
/// The consumer's edges: `(path, donor typ)`.
pub edges: Vec<(String, ArtifactKind)>,
}
impl ConsumerView<'_> {
/// The consumer's DECLARED `target` group — `Some` only for an installer
/// row whose group is not at its default (the effective shape, a preset
/// resolved). `None` for an undeclared group and for every other kind:
/// the group is spelled on `[[build.installer]]` alone.
pub fn declared_target(&self) -> Option<crate::target::Target> {
let row = self.set.installers.iter().find(|i| i.name == self.variant)?;
(!row.target.is_default()).then(|| row.target.effective())
}
/// The path of the ONE edge of typ `typ`, when exactly one exists — the
/// implicit address of a `kernel_requires` row (a `kernel_donor` edge is
/// always at path `kernel`).
/// The consumer's declared `data` group (`[build.installer.data]`), `None`
/// when the row is undeclared — an undeclared group must be inert.
pub fn declared_data(&self) -> Option<crate::data::Data> {
let row = self.set.installers.iter().find(|i| i.name == self.variant)?;
(!row.data.is_default()).then(|| row.data.clone())
}
pub fn edge_of_typ(&self, typ: ArtifactKind) -> Option<&str> {
let mut it = self.edges.iter().filter(|(_, t)| *t == typ);
match (it.next(), it.next()) {
(Some((p, _)), None) => Some(p.as_str()),
_ => None,
}
}
}
/// **A plug-in that says what a consumer requires** — the declared rows are
/// one, the appliance-boot contract is another, the `target.*` facts are the
/// next. Every source answers `(feature, requirement)` rows; the engine unions.
pub trait RequirementSource {
/// The source's name (for a refusal).
fn name(&self) -> &str;
/// The rows this source places for `view` — ENABLED features only.
fn requirements(&self, view: &ConsumerView<'_>) -> Result<Vec<(String, EdgeRequirement)>>;
}
/// The declared `kernel_requires` / `requires` rows of the consumer's ENABLED
/// options (an option that is off is not compiled in and asks nothing), plus
/// the row-level list.
pub struct DeclaredRows;
impl RequirementSource for DeclaredRows {
fn name(&self) -> &str {
"declared rows"
}
fn requirements(&self, view: &ConsumerView<'_>) -> Result<Vec<(String, EdgeRequirement)>> {
let mut out = Vec::new();
let kernel_edge = view.edge_of_typ(ArtifactKind::Kernel).map(str::to_string);
for r in view.row_requires {
out.push(("variant".to_string(), r.clone()));
}
for o in view.options {
if view.resolved.is_on(&o.name) != Some(true) {
continue;
}
for r in &o.requires {
out.push((o.name.clone(), r.clone()));
}
if !o.kernel_requires.is_empty() {
let Some(edge) = &kernel_edge else {
bail!(
"{}: option '{}' declares kernel_requires but the variant consumes no kernel (no kernel_donor / no edge of typ kernel) — a requirement on nothing",
view.consumer,
o.name
);
};
for k in &o.kernel_requires {
out.push((o.name.clone(), EdgeRequirement { edge: edge.clone(), what: Requirement::Kconfig(k.clone()) }));
}
}
}
Ok(out)
}
}
/// The PID-1 boot contract this crate owns ([`crate::kernel_requires::appliance_boot_requirements`])
/// — placed on the kernel edge of every consumer that has one, under the
/// feature [`crate::kernel_requires::APPLIANCE_BOOT_FEATURE`].
pub struct ApplianceBootContract;
impl RequirementSource for ApplianceBootContract {
fn name(&self) -> &str {
crate::kernel_requires::APPLIANCE_BOOT_FEATURE
}
fn requirements(&self, view: &ConsumerView<'_>) -> Result<Vec<(String, EdgeRequirement)>> {
let Some(edge) = view.edge_of_typ(ArtifactKind::Kernel) else { return Ok(Vec::new()) };
// Once the consumer DECLARES its `target` group, that group is the one
// writer of every symbol it CAN name (uarts, consoles, buses, nic,
// cpu): the contract's measured defaults for those step aside, so a
// target with no UART can say `SERIAL_8250=n` without contradicting the
// contract's `=y` — and the contract's `SERIAL_8250_CONSOLE=y` goes
// with it, a console on a driver the group switched off. "Can name" is
// derived from the writer itself: the symbols of the default shape's
// rows (everything present) plus the declared shape's (the absences).
// No hand list here; `target.rs` stays the only place a fact's symbols
// are spelled. Undeclared: the contract stands whole — byte-identical
// to before the group existed.
//
// ⚠ THE VOCABULARY IS WHAT STEPS THE CONTRACT ASIDE, so a group that
// declines to speak inside it leaves the symbol to `kconfig/base.config`
// rather than to the contract. That is why `Target::kconfig_needs` says
// `SERIAL_8250=y` for a machine with real ports and no serial console
// (UpCloud) instead of going quiet: a fact this group owns must be
// ANSWERED by it, and the honest answer for a machine with a ttyS1 is
// that the driver belongs in the kernel. Silence is only ever right for
// a symbol NOT in the vocabulary — `SERIAL_8250_CONSOLE` on a machine
// no `console=` points at a UART on, where base.config's `=y` is inert.
let owned: Vec<String> = view
.declared_target()
.map(|t| {
crate::target::Target::default()
.kconfig_needs()
.into_iter()
.chain(t.kconfig_needs())
.map(|(s, _, _)| s.to_string())
.collect()
})
.unwrap_or_default();
// ONE user is the contract's DEFAULT, not a law (lane HOLGER-MEDIA,
// 2026-10-23): a consumer whose own rows require `CONFIG_MULTIUSER=y`
// (Holger's main and twin: every scan in a container, which needs user
// namespaces and cgroups, both `depends on MULTIUSER`) lifts the
// contract's `=n` by saying so; every other consumer keeps it.
//
// IPv4 only is lifted the same way and only by a ROW (owner
// 2026-10-29): a Kubernetes node requires `CONFIG_IPV6=y` because
// Cilium's socket-LB programs read `bpf_sock->src_ip6`, and boots with
// `ipv6.disable=1`. A feature asking for IPv6 is still refused by name.
//
// The framebuffer console likewise (owner 2026-10-30, option A): the
// k8s node's row requires FB, FB_EFI and FRAMEBUFFER_CONSOLE so its
// console dashboard shows under UEFI; DRM and LOGO are never lifted.
let lifted: Vec<&str> = crate::kernel_requires::ROW_LIFTABLE_SYMBOLS
.iter()
.copied()
.filter(|sym| {
view.row_requires.iter().any(|r| {
matches!(&r.what, Requirement::Kconfig(k) if k.symbol == *sym && k.value == crate::kernel_requires::KconfigValue::Y)
})
})
.collect();
Ok(crate::kernel_requires::appliance_boot_requirements()
.into_iter()
.filter(|k| !owned.contains(&k.symbol))
.filter(|k| !lifted.contains(&k.symbol.as_str()))
.map(|k| (self.name().to_string(), EdgeRequirement { edge: edge.to_string(), what: Requirement::Kconfig(k) }))
.collect())
}
}
/// **The `target` facts as kernel requirements** — the consumer's declared
/// machine shape ([`crate::target::Target`], `[build.installer.target]`) placed
/// on its kernel edge through the SAME closure every other feature uses. The
/// rows are [`crate::target::Target::kconfig_needs`]'s — `SERIAL_8250`
/// + `SERIAL_EARLYCON` iff the machine has a COM PORT (`target.uarts`, not the
/// console: the ports decide the driver), `SERIAL_8250_CONSOLE` iff a serial
/// console, `VT VT_CONSOLE VGA_CONSOLE PRINTK` iff vga, `ATA ATA_PIIX
/// BLK_DEV_SR ISO9660_FS` iff `ide-cdrom` is a bus the KERNEL drives (which the
/// way a platform hands a medium over never decides — see
/// [`crate::target::presets::MediumBring::kernel_medium_bus`]),
/// `VIRTIO_BLK VIRTIO_PCI(_LEGACY)`
/// iff virtio-blk, `SCSI BLK_DEV_SD SCSI_VIRTIO` iff virtio-scsi, `VIRTIO_NET`
/// iff nic virtio, `E1000` iff e1000, `CPU_SUP_AMD` iff epyc — consumed here,
/// never restated: `target.rs` is the one writer of what a fact needs, this
/// source only carries it. The feature each row lands under is the group the
/// row's own reason names (`target.uarts`, `target.consoles`, `target.buses`,
/// `target.nic`, `target.cpu`), so the why-index says which fact asked.
///
/// Only a DECLARED group speaks (`!Target::is_default()`): an undeclared row
/// changes nothing anywhere in the group, and here that means the boot
/// contract's measured defaults stand as they did. A declared fact the donor's
/// preset vetoes — `ide-cdrom` on a `nuked` kernel, which says `# CONFIG_ATA is
/// not set` — is refused BY NAME by [`crate::kernel::KernelBuild::apply_requirements`],
/// not merged over the veto: pick a preset that grants it, or drop the bus.
/// That refusal is the mechanism working: it is the UpCloud IDE-cdrom lane
/// (2026-08-28/29) stated as a build fact instead of a dark boot.
pub struct TargetFacts;
/// The feature a `kconfig_needs` row lands under: the leading `target.<group>`
/// word of its reason (`"target.consoles names serial: …"` → `target.consoles`).
fn target_feature_of(why: &str) -> String {
why.split_whitespace()
.next()
.unwrap_or("target")
.trim_end_matches(':')
.to_string()
}
impl RequirementSource for TargetFacts {
fn name(&self) -> &str {
"target facts"
}
fn requirements(&self, view: &ConsumerView<'_>) -> Result<Vec<(String, EdgeRequirement)>> {
let Some(edge) = view.edge_of_typ(ArtifactKind::Kernel) else { return Ok(Vec::new()) };
let Some(target) = view.declared_target() else { return Ok(Vec::new()) };
Ok(target
.kconfig_needs()
.into_iter()
.map(|(symbol, on, why)| {
let req = if on { KconfigReq::y(symbol, &why) } else { KconfigReq::n(symbol, &why) };
(target_feature_of(&why), EdgeRequirement { edge: edge.to_string(), what: Requirement::Kconfig(req) })
})
.collect())
}
}
/// **The sources the engine asks by default** — the one list both doors and
/// the planner use. A new fact family joins HERE.
/// **The `data` facts as kernel requirements** — the consumer's declared
/// `/data` filesystem ([`crate::data::Data`], `[build.installer.data]`) on
/// its kernel edge, every row under the feature `data.fs` (the leading word
/// of its reason, the same rule as [`TargetFacts`]). `xfs` lands
/// `CONFIG_XFS_FS=y` + CRC32C and the one-user `=n` narrowings; an undeclared
/// group lands nothing, so every pre-group closure is byte-identical.
pub struct DataFacts;
impl RequirementSource for DataFacts {
fn name(&self) -> &str {
"data facts"
}
fn requirements(&self, view: &ConsumerView<'_>) -> Result<Vec<(String, EdgeRequirement)>> {
let Some(edge) = view.edge_of_typ(ArtifactKind::Kernel) else { return Ok(Vec::new()) };
let Some(data) = view.declared_data() else { return Ok(Vec::new()) };
Ok(data
.kconfig_needs()
.into_iter()
.map(|(symbol, on, why)| {
let req = if on { KconfigReq::y(symbol, &why) } else { KconfigReq::n(symbol, &why) };
(target_feature_of(&why), EdgeRequirement { edge: edge.to_string(), what: Requirement::Kconfig(req) })
})
.collect())
}
}
/// **What a KERNEL row needs of the toolchain image behind its `toolchain`
/// edge** — the SECOND surface, and the first source that speaks to a donor
/// that is not a kernel ([`crate::toolchain_requires`]).
///
/// It is the sentence the generic engine exists to make sayable: the kernel is a
/// CONSUMER as well as a donor. `tunnr-kernel` consumes
/// `tunnr/nordisk-kernel-toolchain : container`, and its own axes decide which
/// tools the image owes it — `toolchain = "clang"` or `lto` pulls `clang`/`lld`
/// under the feature `kernel.lto`, a named `solver` pulls `kmax` under
/// `kernel.minify`, a probe pulls `qemu-system-x86` under `kernel.probe-boot`.
///
/// A kernel row that declares no container edge asks nothing, so every existing
/// declaration is byte-identical — exactly the shape [`DataFacts`] and
/// [`TargetFacts`] take for an undeclared group.
///
/// The consumer here is a `[[build.kernel]]` row, which carries no option rows
/// at all: the closure's `options` are empty for it and the toggles are empty
/// with them. That is not a gap — a kernel's axes are FIELDS (`toolchain`,
/// `lto`, `solver`, `probe_timeout_secs`), and this source reads them off the
/// row the way `DataFacts` reads the `data` group off an installer's.
pub struct ToolchainTools;
impl RequirementSource for ToolchainTools {
fn name(&self) -> &str {
"toolchain tools"
}
fn requirements(&self, view: &ConsumerView<'_>) -> Result<Vec<(String, EdgeRequirement)>> {
let Some(k) = view.set.kernels.iter().find(|k| k.name == view.variant) else { return Ok(Vec::new()) };
let Some(edge) = view.edge_of_typ(ArtifactKind::Container) else { return Ok(Vec::new()) };
Ok(crate::toolchain_requires::kernel_tool_requirements(k)
.into_iter()
.map(|(feature, req)| (feature, EdgeRequirement { edge: edge.to_string(), what: Requirement::Tool(req) }))
.collect())
}
}
pub fn default_sources() -> Vec<Box<dyn RequirementSource>> {
vec![Box::new(DeclaredRows), Box::new(ApplianceBootContract), Box::new(TargetFacts), Box::new(DataFacts), Box::new(ToolchainTools)]
}
/// **The closure of one consumer variant** through [`default_sources`].
/// `toggles` are the consumer's root-scope `--opt name=on|off`. `Ok(None)`
/// when the variant declares no edges (nothing to require of nobody).
pub fn closure_of_consumer(set: &BuildVariantSet, repo: &str, variant: &str, toggles: &BTreeMap<String, bool>) -> Result<Option<Closure>> {
closure_of_consumer_with(set, repo, variant, toggles, &default_sources())
}
/// [`closure_of_consumer`] over explicit sources (a test's, or a door that adds one).
pub fn closure_of_consumer_with(
set: &BuildVariantSet,
repo: &str,
variant: &str,
toggles: &BTreeMap<String, bool>,
sources: &[Box<dyn RequirementSource>],
) -> Result<Option<Closure>> {
let Some(tree) = crate::tree::SettingsTree::of_set(set, variant) else {
return Ok(None);
};
let edges: Vec<(String, ArtifactKind)> = tree.walk().into_iter().filter_map(|n| n.edge.as_ref().map(|e| (n.path.clone(), e.typ))).collect();
if edges.is_empty() {
return Ok(None);
}
let empty_opts: Vec<OptionSpec> = Vec::new();
let empty_reqs: Vec<EdgeRequirement> = Vec::new();
let (options, params, row_requires): (&[OptionSpec], &[crate::params::ParamSpec], &[EdgeRequirement]) =
if let Some(i) = set.installers.iter().find(|i| i.name == variant) {
(&i.options, &i.params, &i.requires)
} else if let Some(i) = set.isos.iter().find(|i| i.name == variant) {
(&i.options, &i.params, &i.requires)
} else {
(&empty_opts, &[], &empty_reqs)
};
// Only the toggles that name a DECLARED option are the closure's business;
// a root toggle on a cargo variant's feature (`--opt fast=on`) is judged by
// the variant's own door, not here.
let toggles: BTreeMap<String, bool> = toggles.iter().filter(|(n, _)| options.iter().any(|o| o.name == **n)).map(|(n, v)| (n.clone(), *v)).collect();
let resolved = crate::options::resolve_options(options, params, &toggles)?;
let view = ConsumerView { consumer: format!("{repo}/{variant}"), repo, variant, set, options, row_requires, resolved: &resolved, edges };
let mut out = Closure::default();
for s in sources {
for (feature, r) in s.requirements(&view).map_err(|e| anyhow!("{}: source '{}': {e:#}", view.consumer, s.name()))? {
// The edge must exist and the surface must be the donor's typ.
match view.edges.iter().find(|(p, _)| *p == r.edge) {
None => bail!(
"{}: feature '{feature}' requires something of edge '{}', which the variant does not declare (edges: {})",
view.consumer,
r.edge,
view.edges.iter().map(|(p, t)| format!("{p} : {}", t.as_str())).collect::<Vec<_>>().join(", ")
),
Some((_, t)) if *t != r.what.typ() => bail!(
"{}: feature '{feature}' places a {} requirement on edge '{}', whose donor is typ {} — the surface does not match",
view.consumer,
r.what.typ().as_str(),
r.edge,
t.as_str()
),
Some(_) => out.require(&view.consumer, &feature, &r),
}
}
}
Ok(Some(out))
}
/// The consumer's root-scope toggles out of a flag list: `--opt name=on|off`
/// with no `.` in the path (a dotted path is scoped into an edge). `prefix` is
/// the consumer's own scope for a chained consumer (`""` for the target).
pub fn toggles_in_scope(flags: &[crate::flags::Flag], prefix: &str) -> Result<BTreeMap<String, bool>> {
let mut pairs: Vec<String> = Vec::new();
for f in flags {
if let crate::flags::Flag::Opt { path, value } = f {
let Some(inner) = path.strip_prefix(prefix) else { continue };
if inner.contains(crate::tree::PATH_SEP) || inner == "preset" || inner == "profile" || inner == crate::kernel_trim::coverage::COVERAGE_OPT {
continue;
}
pairs.push(format!("{inner}={value}"));
}
}
crate::options::parse_cli_options(&pairs)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::installer::{InstallerBuild, KernelDonor};
use crate::kernel_requires::KconfigValue;
fn set() -> BuildVariantSet {
let mut set = BuildVariantSet::default();
set.installers.push(InstallerBuild {
name: "inst".into(),
options: vec![
OptionSpec {
name: "dhcp".into(),
feature: "dhcp-client".into(),
default: true,
kernel_requires: vec![KconfigReq::y("CONFIG_INET", "UDP 0.0.0.0:68")],
..Default::default()
},
OptionSpec {
name: "wireguard".into(),
feature: "wg-client".into(),
default: true,
kernel_requires: vec![KconfigReq::y("CONFIG_TUN", "boringtun")],
..Default::default()
},
],
kernel_donor: Some(KernelDonor { repo: "tunnr".into(), kernel: "tunnr-kernel".into() }),
..Default::default()
});
set
}
#[test]
fn the_closure_follows_the_toggles_and_lands_on_the_kernel_edge() {
let c = closure_of_consumer(&set(), "gunnar", "inst", &BTreeMap::new()).unwrap().unwrap();
let k = c.on_edge("kernel").unwrap().as_kconfig().unwrap();
assert!(k.rows.contains_key("CONFIG_TUN") && k.rows.contains_key("CONFIG_INET") && k.rows.contains_key("CONFIG_PROC_FS"));
assert_eq!(k.rows["CONFIG_IPV6"][0].0, KconfigValue::N, "the negative law rides the boot contract");
let mut t = BTreeMap::new();
t.insert("wireguard".to_string(), false);
let c = closure_of_consumer(&set(), "gunnar", "inst", &t).unwrap().unwrap();
assert!(!c.on_edge("kernel").unwrap().as_kconfig().unwrap().rows.contains_key("CONFIG_TUN"));
// A variant with no edges has no closure.
let mut plain = set();
plain.installers[0].kernel_donor = None;
assert!(closure_of_consumer(&plain, "gunnar", "inst", &BTreeMap::new()).unwrap().is_none());
}
/// **An option's own `kernel_requires` follows the option's toggle** — the
/// OPTION is the requirer (not the row, not the variant), ON places the
/// rows and OFF places none, and a second reason for one symbol is refused
/// at declaration time.
///
/// Lane AE (2026-08-30) had to park gunnar's rows on a temporary
/// `[[build.installer.option_requires]]` table, because gunnar's own reader
/// was a published `nornir-build-thing 0.1.10` whose `OptionSpec` is
/// `deny_unknown_fields` and refused the whole recipe on the key. Lane AF
/// moved gunnar onto a path dep and the rows onto the option rows, and this
/// test is AE's, restated over the key that survives: same feature name,
/// same ON/OFF rule, same union. Two of AE's three refusals are gone
/// because they became IMPOSSIBLE rather than unchecked — a row addressed
/// to an option nobody declares cannot be written inside that option, and
/// neither can a symbol spelled in two places for one option. The third
/// (one option, one writer per symbol) is the assertion below.
#[test]
fn an_options_kernel_requires_follows_the_same_toggle_and_a_second_reason_for_one_symbol_is_refused() {
let mut s = set();
s.installers[0].options[1].kernel_requires.push(KconfigReq::y("CONFIG_NETDEVICES", "TUN is a netdevice"));
s.installers[0].validate().unwrap();
let on = closure_of_consumer(&s, "gunnar", "inst", &BTreeMap::new()).unwrap().unwrap();
let k = on.on_edge("kernel").unwrap().as_kconfig().unwrap();
let who: Vec<&str> = k.rows["CONFIG_NETDEVICES"].iter().map(|(_, q)| q.feature.as_str()).collect();
assert!(who.contains(&"wireguard"), "the OPTION is the requirer, not the variant: {who:?}");
// OFF ⇒ the requirer is gone. (The symbol itself stays, required by the
// boot contract — that is the union, not a declaration that cannot fail.)
let mut t = BTreeMap::new();
t.insert("wireguard".to_string(), false);
let off = closure_of_consumer(&s, "gunnar", "inst", &t).unwrap().unwrap();
let k = off.on_edge("kernel").unwrap().as_kconfig().unwrap();
assert!(
!k.rows["CONFIG_NETDEVICES"].iter().any(|(_, q)| q.feature == "wireguard"),
"wireguard=off asks nothing: {:?}",
k.rows["CONFIG_NETDEVICES"]
);
// RED: one option, two reasons for one symbol.
let mut twice = set();
twice.installers[0].options[1].kernel_requires.push(KconfigReq::y("CONFIG_TUN", "again"));
let e = format!("{:#}", twice.installers[0].validate().unwrap_err());
assert!(e.contains("CONFIG_TUN") && e.contains("one writer per symbol"), "{e}");
// RED: a symbol that is not a CONFIG_* symbol at all.
let mut typo = set();
typo.installers[0].options[0].kernel_requires.push(KconfigReq::y("TUN", "no prefix"));
let e = format!("{:#}", typo.installers[0].validate().unwrap_err());
assert!(e.contains("not a CONFIG_* symbol"), "{e}");
}
#[test]
fn a_requirement_on_an_undeclared_edge_or_the_wrong_surface_is_refused_by_name() {
let mut s = set();
s.installers[0].requires.push(EdgeRequirement { edge: "firmware".into(), what: Requirement::Kconfig(KconfigReq::y("CONFIG_X", "x")) });
let e = format!("{:#}", closure_of_consumer(&s, "gunnar", "inst", &BTreeMap::new()).unwrap_err());
assert!(e.contains("edge 'firmware'") && e.contains("does not declare"), "{e}");
// kernel_requires on a variant with no kernel edge.
let mut s = set();
s.installers[0].kernel_donor = None;
s.installers[0].edges.insert("base".into(), crate::tree::EdgeDecl { repo: "x".into(), variant: "y".into(), typ: ArtifactKind::Container, lazy: false });
let e = format!("{:#}", closure_of_consumer(&s, "gunnar", "inst", &BTreeMap::new()).unwrap_err());
assert!(e.contains("consumes no kernel"), "{e}");
}
#[test]
fn a_plugged_in_source_joins_the_same_union() {
struct Target;
impl RequirementSource for Target {
fn name(&self) -> &str {
"target.consoles"
}
fn requirements(&self, view: &ConsumerView<'_>) -> Result<Vec<(String, EdgeRequirement)>> {
let edge = view.edge_of_typ(ArtifactKind::Kernel).unwrap().to_string();
Ok(vec![("target.consoles".into(), EdgeRequirement { edge, what: Requirement::Kconfig(KconfigReq::n("CONFIG_SERIAL_8250", "no UART on this host")) })])
}
}
let mut sources = default_sources();
sources.push(Box::new(Target));
let c = closure_of_consumer_with(&set(), "gunnar", "inst", &BTreeMap::new(), &sources).unwrap().unwrap();
let k = c.on_edge("kernel").unwrap();
// The contract says SERIAL_8250=y, the target fact says n: ONE refusal, naming both.
let conflicts = k.conflicts();
assert_eq!(conflicts.len(), 1, "{conflicts:?}");
assert!(conflicts[0].contains("CONFIG_SERIAL_8250") && conflicts[0].contains("target.consoles") && conflicts[0].contains("appliance-boot"), "{}", conflicts[0]);
}
/// The `data` group the same way: undeclared = byte-identical closure;
/// declared xfs = `CONFIG_XFS_FS=y` under feature `data.fs` on the
/// kernel edge and `CONFIG_XFS_QUOTA=n` beside it, named by the requirer.
#[test]
fn declared_data_facts_land_on_the_kernel_edge_and_an_undeclared_group_is_inert() {
use crate::data::{Data, DataFs, DataIo};
let with = closure_of_consumer(&set(), "gunnar", "inst", &BTreeMap::new()).unwrap().unwrap();
let without_src: Vec<Box<dyn RequirementSource>> = vec![Box::new(DeclaredRows), Box::new(ApplianceBootContract), Box::new(TargetFacts)];
let without = closure_of_consumer_with(&set(), "gunnar", "inst", &BTreeMap::new(), &without_src).unwrap().unwrap();
assert_eq!(with, without, "an undeclared data group changes nothing");
assert!(!with.on_edge("kernel").unwrap().as_kconfig().unwrap().rows.contains_key("CONFIG_XFS_FS"));
let mut s = set();
s.installers[0].data = Data { fs: DataFs::Xfs, io: DataIo::Safe };
let c = closure_of_consumer(&s, "gunnar", "inst", &BTreeMap::new()).unwrap().unwrap();
let rows = &c.on_edge("kernel").unwrap().as_kconfig().unwrap().rows;
assert_eq!(rows["CONFIG_XFS_FS"][0].0, KconfigValue::Y, "{rows:?}");
assert_eq!(rows["CONFIG_LIBCRC32C"][0].0, KconfigValue::Y);
assert_eq!(rows["CONFIG_XFS_QUOTA"][0].0, KconfigValue::N);
assert_eq!(rows["CONFIG_FS_POSIX_ACL"][0].0, KconfigValue::N);
let feature = &rows["CONFIG_XFS_FS"][0].1;
assert_eq!(feature.feature, "data.fs", "the requirer is named: {feature:?}");
assert!(!rows.contains_key("CONFIG_IO_URING"), "safe asks for no ring");
// The writer arm: `io = uring` lands CONFIG_IO_URING=y under ITS OWN
// feature `data.io`, so kernel.why.toml names the arm as the requirer.
s.installers[0].data.io = DataIo::Uring;
let c = closure_of_consumer(&s, "gunnar", "inst", &BTreeMap::new()).unwrap().unwrap();
let k = c.on_edge("kernel").unwrap();
assert!(k.conflicts().is_empty(), "{:?}", k.conflicts());
let rows = &k.as_kconfig().unwrap().rows;
assert_eq!(rows["CONFIG_IO_URING"][0].0, KconfigValue::Y, "{rows:?}");
assert_eq!(rows["CONFIG_IO_URING"][0].1.feature, "data.io");
assert_eq!(rows["CONFIG_XFS_FS"][0].1.feature, "data.fs", "the fs rows keep their own feature");
}
/// **The `target` group's Kconfig rows ride the closure, one writer.** A
/// declared VGA-only machine (UpCloud's console shape) lands
/// `SERIAL_8250=n` and `VT=y` on the kernel edge with NO contradiction —
/// the boot contract yields the symbols the group names; e1000/epyc land
/// their rows; an undeclared group leaves the closure byte-identical to a
/// closure computed without the source at all.
///
/// ⚠ This function LOST its `#[test]` and ran nowhere between lane L/S and
/// lane AF (2026-08-30): the `data` test was inserted between this one's doc
/// comment + attribute and its own body, so the attribute landed on the new
/// function (twice — `warning: duplicated attribute` was the only trace) and
/// this one compiled as dead code. A test that does not run is the purest
/// form of the green nobody has seen red. Restored, and green as written.
/// ONE user is the contract's default, lifted by name (lane HOLGER-MEDIA,
/// 2026-10-23): a consumer whose own row requires `CONFIG_MULTIUSER=y` (it runs
/// containers) gets no `=n` from the contract — one row, the consumer's, no
/// contradiction; every other consumer keeps the contract's `=n` under the
/// contract's feature, byte for byte as before.
#[test]
fn a_consumer_that_requires_multiuser_lifts_the_contracts_single_user_row() {
let kept = closure_of_consumer(&set(), "gunnar", "inst", &BTreeMap::new()).unwrap().unwrap();
let k = kept.on_edge("kernel").unwrap().as_kconfig().unwrap();
let row = &k.rows[crate::kernel_requires::SINGLE_USER_SYMBOL];
assert_eq!(row.len(), 1);
assert_eq!(row[0].0, KconfigValue::N, "one user by default");
assert_eq!(row[0].1.feature, crate::kernel_requires::APPLIANCE_BOOT_FEATURE);
let mut s = set();
s.installers[0].requires.push(EdgeRequirement {
edge: "kernel".into(),
what: Requirement::Kconfig(KconfigReq::y(crate::kernel_requires::SINGLE_USER_SYMBOL, "every scan runs in a container: user namespaces and cgroups depend on it")),
});
let lifted = closure_of_consumer(&s, "gunnar", "inst", &BTreeMap::new()).unwrap().unwrap();
let edge = lifted.on_edge("kernel").unwrap();
assert!(edge.conflicts().is_empty(), "the lift leaves no contradiction: {:?}", edge.conflicts());
let k = edge.as_kconfig().unwrap();
let row = &k.rows[crate::kernel_requires::SINGLE_USER_SYMBOL];
assert_eq!(row.len(), 1, "the contract's =n is gone, the row's =y stands alone: {row:?}");
assert_eq!(row[0].0, KconfigValue::Y);
assert_ne!(row[0].1.feature, crate::kernel_requires::APPLIANCE_BOOT_FEATURE);
assert_eq!(k.rows["CONFIG_PROC_FS"][0].1.feature, crate::kernel_requires::APPLIANCE_BOOT_FEATURE, "the rest of the contract stands");
}
/// IPv4 only is the contract's law for the appliance, lifted the same way
/// as the one-user default and ONLY by a row that requires `CONFIG_IPV6=y`
/// itself (owner 2026-10-29: a Kubernetes node, whose Cilium socket-LB
/// programs read `bpf_sock->src_ip6`; the node boots `ipv6.disable=1`).
/// A FEATURE asking for IPv6 is still refused by name; every other
/// consumer keeps the contract's `=n`.
#[test]
fn a_consumer_row_that_requires_ipv6_lifts_the_contracts_ipv4_only_row() {
let kept = closure_of_consumer(&set(), "gunnar", "inst", &BTreeMap::new()).unwrap().unwrap();
let k = kept.on_edge("kernel").unwrap().as_kconfig().unwrap();
assert_eq!(k.rows["CONFIG_IPV6"][0].0, KconfigValue::N, "IPv4 only by default");
let mut s = set();
s.installers[0].requires.push(EdgeRequirement {
edge: "kernel".into(),
what: Requirement::Kconfig(KconfigReq::y("CONFIG_IPV6", "a k8s node: Cilium's socket-LB programs read bpf_sock->src_ip6")),
});
let lifted = closure_of_consumer(&s, "gunnar", "inst", &BTreeMap::new()).unwrap().unwrap();
let edge = lifted.on_edge("kernel").unwrap();
assert!(edge.conflicts().is_empty(), "the lift leaves no contradiction: {:?}", edge.conflicts());
let row = &edge.as_kconfig().unwrap().rows["CONFIG_IPV6"];
assert_eq!(row.len(), 1, "the contract's =n is gone, the row's =y stands alone: {row:?}");
assert_eq!(row[0].0, KconfigValue::Y);
let k = edge.as_kconfig().unwrap();
assert_eq!(k.rows["CONFIG_WIREGUARD"][0].0, KconfigValue::N, "the rest of the negative law stands");
assert_eq!(k.rows[crate::kernel_requires::SINGLE_USER_SYMBOL][0].0, KconfigValue::N, "lifting IPv6 lifts nothing else");
}
/// **The framebuffer console is the contract's `=n`, lifted only by a ROW**
/// (owner 2026-10-30, option A: the k8s node's console dashboard must show
/// under UEFI, where there is no VGA text mode). Every consumer gets
/// `FB`, `FB_EFI` and `FRAMEBUFFER_CONSOLE` `=n`; a row that requires the
/// three `=y` lifts exactly those three. `DRM` and `LOGO` stay vetoed for
/// everyone: a row asking for either is a contradiction, not a lift.
#[test]
fn a_consumer_row_that_requires_the_framebuffer_console_lifts_fb_and_drm_logo_stay_vetoed() {
const FB: [&str; 3] = ["CONFIG_FB", "CONFIG_FB_EFI", "CONFIG_FRAMEBUFFER_CONSOLE"];
const NEVER: [&str; 2] = ["CONFIG_DRM", "CONFIG_LOGO"];
let kept = closure_of_consumer(&set(), "gunnar", "inst", &BTreeMap::new()).unwrap().unwrap();
let k = kept.on_edge("kernel").unwrap().as_kconfig().unwrap();
// FB_EFI and FRAMEBUFFER_CONSOLE depend on FB: the contract's FB=n is their veto.
for sym in ["CONFIG_FB"].iter().chain(NEVER.iter()) {
let row = k.rows.get(*sym).unwrap_or_else(|| panic!("{sym}: the contract vetoes it for every consumer"));
assert_eq!(row.len(), 1, "{sym}: {row:?}");
assert_eq!(row[0].0, KconfigValue::N, "{sym}: vetoed by default");
assert_eq!(row[0].1.feature, crate::kernel_requires::APPLIANCE_BOOT_FEATURE, "{sym}");
}
let mut s = set();
for sym in FB {
s.installers[0].requires.push(EdgeRequirement {
edge: "kernel".into(),
what: Requirement::Kconfig(KconfigReq::y(sym, "a k8s node: the console dashboard on the UEFI framebuffer")),
});
}
let lifted = closure_of_consumer(&s, "gunnar", "inst", &BTreeMap::new()).unwrap().unwrap();
let edge = lifted.on_edge("kernel").unwrap();
assert!(edge.conflicts().is_empty(), "the lift leaves no contradiction: {:?}", edge.conflicts());
let k = edge.as_kconfig().unwrap();
for sym in FB {
let row = &k.rows[sym];
assert_eq!(row.len(), 1, "{sym}: the contract's =n is gone, the row's =y stands alone: {row:?}");
assert_eq!(row[0].0, KconfigValue::Y, "{sym}");
}
for sym in NEVER {
assert_eq!(k.rows[sym][0].0, KconfigValue::N, "{sym}: lifting the framebuffer console lifts no DRM and no logo");
}
assert_eq!(k.rows["CONFIG_IPV6"][0].0, KconfigValue::N, "lifting the framebuffer lifts nothing else");
// DRM and LOGO are not liftable: a row asking for one meets the contract.
for sym in NEVER {
let mut s = set();
s.installers[0].requires.push(EdgeRequirement { edge: "kernel".into(), what: Requirement::Kconfig(KconfigReq::y(sym, "wants it")) });
let c = closure_of_consumer(&s, "gunnar", "inst", &BTreeMap::new()).unwrap().unwrap();
let conflicts = c.on_edge("kernel").unwrap().conflicts();
assert_eq!(conflicts.len(), 1, "{sym}: {conflicts:?}");
assert!(conflicts[0].to_string().contains(sym), "{}", conflicts[0]);
}
}
#[test]
fn declared_target_facts_land_on_the_kernel_edge_and_an_undeclared_group_is_inert() {
use crate::target::{Console, Cpu, Nic, UartPort};
// Undeclared: the default sources and the sources WITHOUT TargetFacts agree byte for byte.
let with = closure_of_consumer(&set(), "gunnar", "inst", &BTreeMap::new()).unwrap().unwrap();
let without_src: Vec<Box<dyn RequirementSource>> = vec![Box::new(DeclaredRows), Box::new(ApplianceBootContract)];
let without = closure_of_consumer_with(&set(), "gunnar", "inst", &BTreeMap::new(), &without_src).unwrap().unwrap();
assert_eq!(with, without, "an undeclared target changes nothing");
assert_eq!(with.on_edge("kernel").unwrap().as_kconfig().unwrap().rows["CONFIG_SERIAL_8250"][0].0, KconfigValue::Y);
// Declared VGA only, no UART: serial OUT, vga IN, no contradiction.
let mut s = set();
s.installers[0].target.consoles = vec![Console::Vga];
s.installers[0].target.uarts = Vec::new();
let c = closure_of_consumer(&s, "gunnar", "inst", &BTreeMap::new()).unwrap().unwrap();
let k = c.on_edge("kernel").unwrap();
assert!(k.conflicts().is_empty(), "{:?}", k.conflicts());
let k = k.as_kconfig().unwrap();
assert_eq!(k.rows["CONFIG_SERIAL_8250"][0].0, KconfigValue::N);
assert_eq!(k.rows["CONFIG_SERIAL_8250"][0].1.feature, "target.uarts", "the PORTS own the driver, not the console (2026-09-07)");
assert!(!k.rows.contains_key("CONFIG_SERIAL_8250_CONSOLE"), "nothing asks for a console on a UART that is not there");
assert_eq!(k.rows["CONFIG_VT"][0].0, KconfigValue::Y);
assert_eq!(k.rows["CONFIG_VGA_CONSOLE"][0].0, KconfigValue::Y);
assert_eq!(k.rows["CONFIG_PROC_FS"][0].1.feature, crate::kernel_requires::APPLIANCE_BOOT_FEATURE, "the rest of the contract stands");
assert!(k.rows["CONFIG_VT"].iter().all(|(_, q)| q.feature == "target.consoles"), "the group is the ONE writer of a symbol it names: {:?}", k.rows["CONFIG_VT"]);
// Declared serial on COM2 (UpCloud's real UART), e1000, EPYC.
let mut s = set();
s.installers[0].target.consoles = vec![Console::Serial];
s.installers[0].target.uarts = vec![UartPort::Com2];
s.installers[0].target.buses = vec![crate::target::Bus::VirtioBlk];
s.installers[0].target.nic = Nic::E1000;
s.installers[0].target.cpu = Cpu::Epyc;
let c = closure_of_consumer(&s, "gunnar", "inst", &BTreeMap::new()).unwrap().unwrap();
let k = c.on_edge("kernel").unwrap().as_kconfig().unwrap().clone();
assert_eq!(k.rows["CONFIG_SERIAL_8250_CONSOLE"][0].0, KconfigValue::Y);
assert!(k.rows["CONFIG_SERIAL_8250"][0].1.reason.contains("ttyS1"), "{:?}", k.rows["CONFIG_SERIAL_8250"]);
assert_eq!(k.rows["CONFIG_VT"][0].0, KconfigValue::N);
assert_eq!((k.rows["CONFIG_E1000"][0].0.clone(), k.rows["CONFIG_E1000"][0].1.feature.as_str()), (KconfigValue::Y, "target.nic"));
assert!(!k.rows.contains_key("CONFIG_VIRTIO_NET"), "nic = e1000 asks nothing of virtio-net: {:?}", k.rows.keys());
assert_eq!((k.rows["CONFIG_CPU_SUP_AMD"][0].0.clone(), k.rows["CONFIG_CPU_SUP_AMD"][0].1.feature.as_str()), (KconfigValue::Y, "target.cpu"));
assert_eq!(target_feature_of("target.buses has ide-cdrom: the medium is an IDE cdrom"), "target.buses");
// The absences NARROW a nuked kernel (its base fragment has VT=y): a
// `=n` row over a preset `=y` lands and renders as the veto directive;
// only a POSITIVE row over a preset veto is refused.
let mut nuked = crate::kernel::KernelBuild { name: "k".into(), version: "6.12.104".into(), tarball_sha256: "0".repeat(64), preset: "nuked".into(), ..Default::default() };
nuked.apply_requirements(&k).expect("a narrowing is not a veto met");
let frag = nuked.requirements.as_ref().unwrap().fragment();
assert!(frag.contains("# CONFIG_VT is not set") && frag.contains("CONFIG_E1000=y"), "{frag}");
}
/// **The closure half of the same ruling.** A declared machine with REAL
/// ports and no serial console keeps `CONFIG_SERIAL_8250=y` on its kernel
/// edge with no conflict — written by `target.uarts`, the fact that
/// actually decides a driver, while `target.consoles` keeps only
/// `SERIAL_8250_CONSOLE` and the VGA rows.
///
/// RED until 2026-09-07: the group vetoed the DRIVER whenever no `console=`
/// named a UART, which is UpCloud exactly — `Uarts::Known([Com2, Com3])`,
/// VNC the only console — and the refusal that produced named a symbol the
/// shipped `tunnr-kernel` has had `=y` all month while serving off that very
/// machine.
#[test]
fn a_declared_machine_with_ports_but_no_serial_console_leaves_the_contract_standing() {
use crate::target::{Console, UartPort};
let mut s = set();
s.installers[0].target.consoles = vec![Console::Vga];
s.installers[0].target.uarts = vec![UartPort::Com2, UartPort::Com3];
let c = closure_of_consumer(&s, "gunnar", "inst", &BTreeMap::new()).unwrap().unwrap();
let k = c.on_edge("kernel").unwrap();
assert!(k.conflicts().is_empty(), "a machine with ports vetoes nothing: {:?}", k.conflicts());
let k = k.as_kconfig().unwrap();
assert_eq!(k.rows["CONFIG_SERIAL_8250"][0].0, KconfigValue::Y);
assert_eq!(
k.rows["CONFIG_SERIAL_8250"][0].1.feature,
"target.uarts",
"the PORTS own the driver — the contract steps aside for a symbol the group names, and here it names it `y`: {:?}",
k.rows["CONFIG_SERIAL_8250"]
);
assert_eq!(k.rows["CONFIG_SERIAL_EARLYCON"][0].0, KconfigValue::Y, "and the earlycon door stays open");
assert_eq!(k.rows["CONFIG_VGA_CONSOLE"][0].1.feature, "target.consoles", "what the operator DOES read is still the group's");
}
/// **A declared bus the donor's preset vetoes is refused BY NAME, never
/// merged over the veto.** UpCloud attaches the ISO as an IDE cdrom; the
/// `nuked` preset says `# CONFIG_ATA is not set`; the refusal names the
/// symbol, the consumer, the feature and the preset — the dark UpCloud boot
/// as a build-time sentence.
#[test]
fn an_ide_cdrom_target_on_a_nuked_kernel_is_refused_by_name() {
use crate::target::{Bus, Console, UartPort};
let mut s = set();
// UpCloud's buses on a COM1 machine: the ide-cdrom is the fact under
// test, spelled as fields so the assertion does not ride the preset
// roster (which another lane owns).
s.installers[0].target.buses = vec![Bus::IdeCdrom, Bus::VirtioBlk];
s.installers[0].target.consoles = vec![Console::Vga, Console::Serial];
s.installers[0].target.uarts = vec![UartPort::Com1];
s.installers[0].target.nics = 2;
let c = closure_of_consumer(&s, "gunnar", "inst", &BTreeMap::new()).unwrap().unwrap();
let k = c.on_edge("kernel").unwrap().as_kconfig().unwrap().clone();
assert_eq!(k.rows["CONFIG_ATA"][0].0, KconfigValue::Y);
let mut nuked = crate::kernel::KernelBuild { name: "tunnr-kernel".into(), version: "6.12.104".into(), tarball_sha256: "0".repeat(64), preset: "nuked".into(), ..Default::default() };
let err = format!("{:#}", nuked.apply_requirements(&k).unwrap_err());
assert!(err.contains("CONFIG_ATA") && err.contains("gunnar/inst:target.buses") && err.contains("nuked"), "{err}");
// A preset that grants it lands the same rows.
let mut normal = crate::kernel::KernelBuild { preset: "normal".into(), ..nuked.clone() };
normal.apply_requirements(&k).unwrap();
assert!(normal.requirements.as_ref().unwrap().rows.contains_key("CONFIG_ATA_PIIX"));
}
#[test]
fn toggles_in_scope_take_root_features_only() {
use crate::flags::Flag;
let flags = vec![Flag::opt("dhcp", true), Flag::choice("kernel.preset", "nuked"), Flag::opt("wireguard", false), Flag::opt("a.b", true)];
let t = toggles_in_scope(&flags, "").unwrap();
assert_eq!(t.len(), 2);
assert_eq!(t["dhcp"], true);
assert_eq!(t["wireguard"], false);
let t = toggles_in_scope(&flags, "a.").unwrap();
assert_eq!(t.len(), 1);
assert_eq!(t["b"], true);
}
}