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/target.rs

//! **The `target` setting group** — the machine shape an installer medium is
//! built FOR: which firmware starts it, which consoles exist, which UARTs are
//! really there, which buses carry the medium and the target disk, which NIC
//! model and CPU the guest sees.
//!
//! Declared once, here in the ENGINE, on
//! [`InstallerBuild::target`](crate::installer::InstallerBuild)
//! (`[build.installer.target]` in the emitted toml) — never hardcoded in tunnr
//! or gunnar. Consumed by: the installer composer (El-Torito entries, the
//! `console=` order), the kernel recipe through the generic requirement closure
//! ([`Target::kconfig_needs`] → `crate::requires::RequirementSource`, one
//! mechanism, one why-index), and the proof door (the qemu machine). The design
//! note is `dwarves/.nornir/build-thing-target-facts.md`; the closure mechanism
//! is `dwarves/.nornir/build-thing-kernel-closure.md`.
//!
//! # Why this is a declared setting and not folklore
//!
//! UpCloud boots SeaBIOS only, attaches an ISO as an IDE cdrom, has **no UART at
//! 0x3f8** (ttyS1/ttyS2 exist at 0x2f8/0x3e8), VGA, two NICs, EPYC. Not one of
//! those facts was declared anywhere, and each cost a blind lane against the
//! real host (2026-08-28/29). A medium proven under OVMF with a captured ttyS0
//! had been proven for a machine the customer does not have.
//!
//! # Presets are data — ONE roster
//!
//! A named machine (`upcloud`, `qemu-local`, …) is a [`presets::TargetPreset`]
//! const in [`presets::PRESETS`] — the rich cloud matrix (firmware, secure
//! boot, console reach, disk bus, medium, NIC, CPU, chipset, metadata, mimic
//! verdict). The `target` group a preset implies is DERIVED from it by
//! [`presets::TargetPreset::target`]; the roster is emitted into the toml by
//! [`presets_toml`] so both doors (the CLI and the ⚒ pane) show the same
//! machines from the same evaluator; and the test matrix's
//! `appliance-boot/<preset>` rows are floored by the same list. A row spells
//! either `preset = "upcloud"` OR the fields; both at once is refused by name
//! (two writers of one fact), and a preset the group cannot spell yet (NVMe,
//! ENA, Hyper-V, …) is refused with the device named.
//!
//! Every field's default is what the medium did before the group existed, so
//! an undeclared row changes nothing and emits nothing.

use serde::{Deserialize, Serialize};

/// **Named machine shapes** — one const per cloud (UpCloud, GCE, AWS Nitro,
/// Azure, …) with the facts above plus the ones a proof door needs (disk bus,
/// NIC model, CPU model, chipset, the real COM ports, metadata service) and the
/// verdict on whether QEMU can mimic it. See [`presets::PRESETS`] — THE roster.
pub mod presets;

pub use presets::{names as preset_names, preset, PresetStatus, TargetPreset, PRESETS};

/// Which firmware starts the medium.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Firmware {
    /// EFI El-Torito section only. The BIOS default entry is the honest
    /// refusal stub (`BIOSREF.BIN`): SeaBIOS prints one sentence and halts.
    Uefi,
    /// The real legacy-BIOS stage (`korp-boot-bios`) as the El-Torito default
    /// entry; no EFI section on the medium. The ESP is still composed — the
    /// installed disk's `korpbios.bin` lives on it.
    SeaBios,
    /// One medium, two firmware paths: EFI section AND the real BIOS stage.
    /// Today's behaviour (edda 39cb702).
    #[default]
    Both,
}

impl Firmware {
    pub fn as_str(self) -> &'static str {
        match self {
            Firmware::Uefi => "uefi",
            Firmware::SeaBios => "seabios",
            Firmware::Both => "both",
        }
    }

    /// Whether the composer lays the real BIOS stage (else the refusal stub).
    pub fn lays_bios(self) -> bool {
        matches!(self, Firmware::SeaBios | Firmware::Both)
    }

    /// Whether the medium carries an EFI El-Torito section.
    pub fn lays_uefi(self) -> bool {
        matches!(self, Firmware::Uefi | Firmware::Both)
    }

    /// The masterer's format for a medium built for this firmware. `Both` is
    /// `Uefi` on purpose: the writer's UEFI format is what carried the BIOS
    /// default entry all along (the stage is declared beside it, not by it).
    pub fn iso_format(self) -> crate::iso::IsoFormat {
        match self {
            Firmware::SeaBios => crate::iso::IsoFormat::Bios,
            Firmware::Uefi | Firmware::Both => crate::iso::IsoFormat::Uefi,
        }
    }
}

/// A console the target machine HAS. Order in [`Target::consoles`] is the
/// `console=` order: the kernel replays printk to every one, and the LAST is
/// `/dev/console` — where PID 1's own lines go.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Console {
    /// A 16550 UART — the FIRST present [`UartPort`] (`console=ttyS<n>,115200`);
    /// kernel `SERIAL_8250(_CONSOLE)`.
    Serial,
    /// A VGA text console: `console=tty0`; kernel `VT VT_CONSOLE VGA_CONSOLE`.
    Vga,
}

impl Console {
    pub fn as_str(self) -> &'static str {
        match self {
            Console::Serial => "serial",
            Console::Vga => "vga",
        }
    }
}

/// **A legacy COM port the machine really has.** `console=ttyS0` on a machine
/// with no COM1 is the UpCloud bug spelled as a cmdline: the kernel opens no
/// `/dev/console`, PID 1's first `print!` is a panic nobody sees.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum UartPort {
    /// `0x3f8`, `ttyS0`.
    Com1,
    /// `0x2f8`, `ttyS1`.
    Com2,
    /// `0x3e8`, `ttyS2`.
    Com3,
    /// `0x2e8`, `ttyS3`.
    Com4,
}

impl UartPort {
    pub const ALL: [UartPort; 4] = [UartPort::Com1, UartPort::Com2, UartPort::Com3, UartPort::Com4];

    pub fn as_str(self) -> &'static str {
        match self {
            UartPort::Com1 => "com1",
            UartPort::Com2 => "com2",
            UartPort::Com3 => "com3",
            UartPort::Com4 => "com4",
        }
    }

    /// The I/O base — the fact a datasheet or `dmesg` names.
    pub fn io_base(self) -> u16 {
        match self {
            UartPort::Com1 => 0x3f8,
            UartPort::Com2 => 0x2f8,
            UartPort::Com3 => 0x3e8,
            UartPort::Com4 => 0x2e8,
        }
    }

    /// The Linux name (`ttyS0` … `ttyS3`).
    pub fn ttys(self) -> &'static str {
        match self {
            UartPort::Com1 => "ttyS0",
            UartPort::Com2 => "ttyS1",
            UartPort::Com3 => "ttyS2",
            UartPort::Com4 => "ttyS3",
        }
    }

    /// The zero-based index qemu's n-th `-serial` maps onto.
    pub fn index(self) -> usize {
        match self {
            UartPort::Com1 => 0,
            UartPort::Com2 => 1,
            UartPort::Com3 => 2,
            UartPort::Com4 => 3,
        }
    }
}

/// **The Kconfig menu chain every `drivers/net/ethernet/<vendor>` driver sits
/// at the bottom of** — one writer for the rows ENA and gVNIC both need, so the
/// two cannot drift apart (they already had: the ENA arm omitted
/// `CONFIG_NETDEVICES`, the menu `ETHERNET` itself lives inside).
///
/// Read off linux-6.12.104, the tree tunnr builds:
/// * `NETDEVICES` — `menuconfig`, `depends on NET`, drivers/net/Kconfig:6. NOT
///   `default y` outside UML, and `drivers/net/ethernet/Kconfig` is sourced at
///   :513, inside `if NETDEVICES` (27..671).
/// * `ETHERNET` — `menuconfig`, `depends on NET`, `default y`,
///   drivers/net/ethernet/Kconfig:6; every vendor menu is inside `if ETHERNET`.
///
/// A symbol whose menu parent is missing is not refused by `olddefconfig`, it is
/// silently dropped — the failure this whole family of rows exists to prevent
/// (MEASURED 2026-09-02 on tunnr-kernel-nvme for ENA; again 2026-09-05 on
/// tunnr-kernel-hyperv for GVE, which `depends on PCI_MSI`).
fn ethernet_menu_rows(nic: &str) -> Vec<(&'static str, bool, String)> {
    vec![
        ("CONFIG_NET", true, format!("target.nic = {nic}: an Ethernet driver — the whole net stack hangs off CONFIG_NET")),
        ("CONFIG_NETDEVICES", true, format!("target.nic = {nic}: the ETHERNET menuconfig is sourced inside `if NETDEVICES` (drivers/net/Kconfig)")),
        ("CONFIG_ETHERNET", true, format!("target.nic = {nic}: the driver lives under the ETHERNET menuconfig (drivers/net/ethernet/Kconfig)")),
    ]
}

/// A bus the medium or the target disk rides.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Bus {
    /// **A cdrom the KERNEL itself must open** (ATAPI): `ATA ATA_PIIX
    /// BLK_DEV_SR`. NOT "the platform attached the medium as a cdrom" — that is
    /// [`crate::target::presets::MediumBring`], the firmware reads it, and no
    /// roster preset derives this bus (2026-09-07).
    ///
    /// That the platform's cdrom is not a consumer REQUIREMENT does not mean the
    /// kernel cannot read it. Since tunnr `3f7c2ff` the gunnar appliance donors
    /// carry `ATA_PIIX`, `SATA_AHCI` and `BLK_DEV_SR` as a donor choice, in their
    /// own fragment, and korp-installer ≥ 0.1.14 reads the medium off `sr0` first.
    /// This bus is still what a consumer names when it must be able to COUNT on it.
    ///
    /// `ISO9660_FS` is deliberately NOT here: a filesystem is not a bus driver,
    /// and the medium is ISO-9660 whichever device it arrives on — the appliance
    /// boot contract owns that row, which is why an ISO read off a VIRTIO disk
    /// still mounts.
    IdeCdrom,
    /// virtio-blk — the target disk, and the medium's second face: the fallback
    /// for a kernel with no ATAPI stack, and for media composed before the
    /// appliance kernels had one; kernel `VIRTIO_BLK VIRTIO_PCI VIRTIO_PCI_LEGACY`.
    VirtioBlk,
    /// virtio-scsi; kernel `SCSI_VIRTIO SCSI BLK_DEV_SD`.
    VirtioScsi,
    /// NVMe — how AWS Nitro presents every block device (the imported AMI root
    /// and any EBS volume are `/dev/nvme*`, never virtio/IDE); kernel
    /// `NVME_CORE BLK_DEV_NVME`. Built-in, not a module: the appliance carries
    /// no initramfs, so the driver that mounts root must already be in the
    /// bzImage.
    Nvme,
    /// Hyper-V synthetic SCSI (`hv_storvsc` over VMBus) — how Azure presents
    /// every disk: the OS disk of a Gen2 VM and every data disk ride it, and
    /// on a Gen1 VM the "IDE" OS disk is taken over by the same driver once it
    /// is present. Kernel `HYPERVISOR_GUEST HYPERV HYPERV_STORAGE SCSI
    /// BLK_DEV_SD`, all built-in for [`Bus::Nvme`]'s no-initramfs reason.
    HvStorvsc,
    /// Xen blkfront (`/dev/xvda`) — how the legacy AWS EC2 Xen HVM families
    /// (m4, c4, t2, …) present the root EBS volume: a PVHVM guest boots on the
    /// emulated IDE disk and the PV frontend takes it over as `xvda`. Kernel
    /// `HYPERVISOR_GUEST PARAVIRT XEN XEN_BLKDEV_FRONTEND`, built-in for
    /// [`Bus::Nvme`]'s no-initramfs reason.
    ///
    /// QEMU on THIS box cannot present it — the `xen-disk` model exists but
    /// there is no `xen-bus` to hang it on. See
    /// [`presets::DiskBus::qemu_mimic`], measured 2026-09-05.
    XenBlk,
}

impl Bus {
    pub fn as_str(self) -> &'static str {
        match self {
            Bus::IdeCdrom => "ide-cdrom",
            Bus::VirtioBlk => "virtio-blk",
            Bus::VirtioScsi => "virtio-scsi",
            Bus::Nvme => "nvme",
            Bus::HvStorvsc => "hv-storvsc",
            Bus::XenBlk => "xen-blk",
        }
    }
}

/// The NIC model the guest sees.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Nic {
    /// virtio-net; kernel `VIRTIO_NET`. The only NIC an appliance initramfs
    /// drives today.
    #[default]
    Virtio,
    /// Intel e1000; kernel `E1000`.
    E1000,
    /// AWS Elastic Network Adapter — the only NIC an EC2 Nitro instance
    /// presents; kernel `ENA_ETHERNET`. Built-in, not a module, for the same
    /// no-initramfs reason as [`Bus::Nvme`]: without the driver in the bzImage
    /// the appliance boots with no network at all.
    Ena,
    /// Hyper-V synthetic NIC (`hv_netvsc` over VMBus) — the only NIC an Azure
    /// VM presents to a guest without SR-IOV; with Accelerated Networking the
    /// synthetic path is still the one that comes up first and stays as the
    /// fallback. Kernel `HYPERVISOR_GUEST HYPERV HYPERV_NET`, built-in.
    #[serde(rename = "hv-netvsc")]
    HvNetvsc,
    /// Google Virtual NIC — the only NIC a gen3+ GCE machine series (C3, N4,
    /// C4, …) offers, and required there; kernel `GVE`, built-in for
    /// [`Nic::Ena`]'s no-initramfs reason. QEMU has no model at all — `gve`,
    /// `gvnic` and `gve-pci` are all absent from `-device help` (oden
    /// 2026-09-05, QEMU 10.2.1).
    Gvnic,
    /// Xen netfront — the NIC an AWS EC2 Xen HVM instance presents once the PV
    /// drivers bind; kernel `HYPERVISOR_GUEST PARAVIRT XEN XEN_NETDEV_FRONTEND`,
    /// built-in. QEMU *lists* `xen-net-device` but cannot instantiate it on
    /// this box — see [`presets::NicModel::qemu_mimic`].
    #[serde(rename = "xen-netfront")]
    XenNetfront,
}

impl Nic {
    pub fn as_str(self) -> &'static str {
        match self {
            Nic::Virtio => "virtio",
            Nic::E1000 => "e1000",
            Nic::Ena => "ena",
            Nic::HvNetvsc => "hv-netvsc",
            Nic::Gvnic => "gvnic",
            Nic::XenNetfront => "xen-netfront",
        }
    }

    /// The qemu `-device` model, or `None` when QEMU has no model for this NIC.
    /// ENA is Nitro's own device and QEMU cannot present it — mirrors
    /// [`crate::target::presets::NicModel::qemu_mimic`], and is why a preset
    /// whose nic is `ena` is `Mimic::NeedsRealCloud`.
    pub fn qemu_device(self) -> Option<&'static str> {
        match self {
            Nic::Virtio => Some("virtio-net-pci"),
            Nic::E1000 => Some("e1000"),
            // `xen-netfront` is here for the reason the presets table spells
            // out: `xen-net-device` is in `-device help` but has no bus to hang
            // on, and a listing is not a device.
            Nic::Ena | Nic::HvNetvsc | Nic::Gvnic | Nic::XenNetfront => None,
        }
    }
}

/// The CPU the guest sees — the fact that picks the vendor half of the kernel.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Cpu {
    /// Whatever the hypervisor gives (qemu's default model); no vendor claim.
    #[default]
    Generic,
    /// AMD EPYC (UpCloud); kernel `CPU_SUP_AMD`, qemu `-cpu EPYC`.
    Epyc,
}

impl Cpu {
    pub fn as_str(self) -> &'static str {
        match self {
            Cpu::Generic => "generic",
            Cpu::Epyc => "epyc",
        }
    }

    /// The qemu `-cpu` model, when the fact names one.
    pub fn qemu_model(self) -> Option<&'static str> {
        match self {
            Cpu::Generic => None,
            Cpu::Epyc => Some("EPYC"),
        }
    }
}

/// **The group.** `[build.installer.target]`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Target {
    /// A named machine from [`presets::PRESETS`] (`preset = "upcloud"`). When
    /// set, every other field must stay at its default — the preset is the one
    /// writer — and [`Target::effective`] is the preset's derived shape.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub preset: Option<String>,
    pub firmware: Firmware,
    /// In `console=` order; the last one is `/dev/console`. Never empty — a
    /// boot nobody can see is refused at declaration time.
    pub consoles: Vec<Console>,
    /// The COM ports that are really there. `serial` in `consoles` names the
    /// first of these; a serial console with no UART is refused by name.
    pub uarts: Vec<UartPort>,
    pub buses: Vec<Bus>,
    pub nic: Nic,
    /// How many NICs the guest has (UpCloud: public + utility = 2).
    pub nics: u8,
    pub cpu: Cpu,
}

impl Default for Target {
    /// Exactly what the medium did before the group existed: both firmwares,
    /// `console=tty0 console=ttyS0,115200`, COM1 present, IDE cdrom +
    /// virtio-blk, one virtio-net, no CPU claim.
    ///
    /// **A COMPAT TOKEN, not a kernel claim, and the `ide-cdrom` here is the
    /// proof.** This value's whole job is [`Target::is_default`]: an undeclared
    /// group is SILENT — `crate::requires::TargetFacts` places no row for it and
    /// [`canonical_fragment`](Target::canonical_fragment) is empty — so nothing
    /// in it ever reaches a Kconfig edge. A DERIVED shape
    /// ([`crate::target::presets::TargetPreset::target`]) does reach one, and
    /// since 2026-09-07 it names only buses the KERNEL must drive, which for
    /// every ISO-attach machine excludes the cdrom the FIRMWARE reads. The two
    /// therefore differ by that one bus, on purpose. ("Must drive" is a
    /// requirement. The gunnar appliance donors have read that cdrom too since
    /// tunnr `3f7c2ff`, on their own initiative and not because a preset asked;
    /// see [`Bus::IdeCdrom`].)
    ///
    /// It is FROZEN: `gunnar-recipe` hand-spells this exact shape so its
    /// installer row equals the default and stays silent. Editing this value
    /// would make that row non-default, light up its entire closure and refuse
    /// the build on `CONFIG_ATA` against the `nuked` donor.
    fn default() -> Self {
        Target {
            preset: None,
            firmware: Firmware::Both,
            consoles: vec![Console::Vga, Console::Serial],
            uarts: vec![UartPort::Com1],
            buses: vec![Bus::IdeCdrom, Bus::VirtioBlk],
            nic: Nic::Virtio,
            nics: 1,
            cpu: Cpu::Generic,
        }
    }
}

impl Target {
    /// For `skip_serializing_if`: an undeclared group emits no row.
    pub fn is_default(&self) -> bool {
        *self == Target::default()
    }

    /// The shape to CONSUME: the preset's derived group when one is named,
    /// else this row. An unknown or unspellable preset resolves to the row
    /// itself — [`validate`](Self::validate) is what refuses it, by name,
    /// before anything is built.
    pub fn effective(&self) -> Target {
        match self.preset.as_deref().and_then(preset).and_then(|p| p.target().ok()) {
            Some(t) => t,
            None => self.clone(),
        }
    }

    pub fn has_console(&self, c: Console) -> bool {
        self.consoles.contains(&c)
    }

    pub fn has_bus(&self, b: Bus) -> bool {
        self.buses.contains(&b)
    }

    /// The UART a `serial` console means: the first present port.
    pub fn serial_port(&self) -> Option<UartPort> {
        self.uarts.iter().copied().min()
    }

    /// The `console=` tokens, in declared order — `serial` spelled as the
    /// first present UART (`ttyS1` on UpCloud, never a `ttyS0` that is not there).
    pub fn console_tokens(&self) -> Vec<String> {
        self.consoles
            .iter()
            .map(|c| match c {
                Console::Vga => "console=tty0".to_string(),
                Console::Serial => format!(
                    "console={},115200",
                    self.serial_port().map(UartPort::ttys).unwrap_or("ttyS0")
                ),
            })
            .collect()
    }

    /// Refuse by name what no machine can boot, and a row with two writers.
    pub fn validate(&self, who: &str) -> anyhow::Result<()> {
        if let Some(name) = &self.preset {
            let Some(p) = preset(name) else {
                anyhow::bail!(
                    "{who}: target.preset = {name:?} names no declared machine — the roster is: {}",
                    presets::names().join(", ")
                );
            };
            let mut bare = self.clone();
            bare.preset = None;
            if !bare.is_default() {
                anyhow::bail!(
                    "{who}: target.preset = {name:?} AND explicit target fields — a preset is the one \
                     writer of the machine shape; spell the preset or the fields, not both"
                );
            }
            // The derived shape must itself be a machine the group can spell
            // and boot — the preset's facts are validated, not trusted.
            let derived = p.target().map_err(|why| {
                anyhow::anyhow!(
                    "{who}: target.preset = {name:?} ({}) is a machine the target group cannot spell yet: {why} — \
                     the group grows when the kernel recipe learns that device's Kconfig closure; \
                     mimic: {}",
                    p.cloud,
                    p.gap().unwrap_or("qemu")
                )
            })?;
            return derived.validate(&format!("{who} (preset {name})"));
        }
        if self.consoles.is_empty() {
            anyhow::bail!(
                "{who}: target.consoles is empty — a boot with no console is a boot nobody \
                 can see; name at least one of serial | vga"
            );
        }
        let mut seen = Vec::new();
        for c in &self.consoles {
            if seen.contains(c) {
                anyhow::bail!("{who}: target.consoles names `{}` twice", c.as_str());
            }
            seen.push(*c);
        }
        if self.has_console(Console::Serial) && self.uarts.is_empty() {
            anyhow::bail!(
                "{who}: target.consoles names `serial` but target.uarts is empty — a serial console \
                 on a machine with no COM port is the UpCloud dark boot (console=ttyS0 on a host \
                 with nothing at 0x3f8); drop `serial` or name the UART that exists (com2 = 0x2f8)"
            );
        }
        if self.buses.is_empty() {
            anyhow::bail!(
                "{who}: target.buses is empty — the medium and the target disk ride SOME bus; \
                 name at least one of ide-cdrom | virtio-blk | virtio-scsi"
            );
        }
        if self.nics == 0 {
            anyhow::bail!("{who}: target.nics = 0 — an appliance with no NIC serves nobody");
        }
        Ok(())
    }

    /// The fragment of the content id this group contributes — only when
    /// declared, so every pre-`target` medium keeps its id.
    pub fn canonical_fragment(&self) -> String {
        if self.is_default() {
            return String::new();
        }
        let e = self.effective();
        format!(
            "#target:{}fw={},consoles={},uarts={},buses={},nic={}x{},cpu={}",
            self.preset.as_deref().map(|p| format!("preset={p},")).unwrap_or_default(),
            e.firmware.as_str(),
            e.consoles.iter().map(|c| c.as_str()).collect::<Vec<_>>().join("+"),
            e.uarts.iter().map(|u| u.as_str()).collect::<Vec<_>>().join("+"),
            e.buses.iter().map(|b| b.as_str()).collect::<Vec<_>>().join("+"),
            e.nic.as_str(),
            e.nics,
            e.cpu.as_str()
        )
    }

    /// **What this machine needs of the kernel** — `(CONFIG_*, y|n, why)`
    /// rows, the data the requirement closure (`crate::requires`) places on
    /// the consumer's kernel edge. Positive rows for what the machine HAS,
    /// negative rows for what it demonstrably has NOT, and no row at all where
    /// this group has no measured claim either way.
    ///
    /// **Each symbol is owned by the fact that decides it, which is not always
    /// the fact that reads it.** `target.uarts` owns `SERIAL_8250` and
    /// `SERIAL_EARLYCON` (a COM port is hardware: the driver is needed iff the
    /// port exists); `target.consoles` owns `SERIAL_8250_CONSOLE` and the VGA
    /// rows (a console is where the OPERATOR reads the boot). Until 2026-09-07
    /// `target.consoles` owned the driver too, so a machine with real ports
    /// that nobody watches — UpCloud, `ttyS1`/`ttyS2` at 0x2f8/0x3e8, VNC the
    /// only console — had its 8250 switched OFF against a boot contract that
    /// asks for it and against the donor kernel that was already serving that
    /// machine. That is the same category error as deriving a kernel bus from
    /// the way a platform hands a medium over
    /// ([`crate::target::presets::MediumBring::kernel_medium_bus`]), one group
    /// over.
    ///
    /// A `console=` on a UART that is not there is still the UpCloud dark boot,
    /// and the negative row is still what lets the closure refuse that pair by
    /// name (the appliance-boot contract demands SOME console). What changed is
    /// which fact triggers it: an empty `uarts`, not an unwatched port.
    pub fn kconfig_needs(&self) -> Vec<(&'static str, bool, String)> {
        let e = self.effective();
        let mut v: Vec<(&'static str, bool, String)> = Vec::new();
        // ── the 16550, and WHO owns it ──────────────────────────────────────
        //
        // `CONFIG_SERIAL_8250` is the DRIVER, so the fact that decides it is
        // `target.uarts` — the COM ports the machine really has. It was owned
        // by `target.consoles` until 2026-09-07, and that is a category error
        // of exactly the same shape as `MediumBring::IsoCdrom` deriving
        // `ide-cdrom` one file over: `target.consoles` says where the OPERATOR
        // reads the boot; a Kconfig symbol says which driver the KERNEL
        // carries. They differ on a real machine.
        //
        // **The machine they differ on is the one that cost the lanes.**
        // UpCloud has NO ttyS0 — `console=ttyS0` there opens no `/dev/console`
        // and PID 1's first `print!` is a panic nobody sees, which is the whole
        // reason this group exists — but `ttyS1`/`ttyS2` at 0x2f8/0x3e8 REALLY
        // EXIST (`Uarts::Known([Com2, Com3])`, and `QemuShape::uarts` is
        // `[false, true, true, false]` precisely so a proof rig CAPTURES them).
        // Gating the driver on the CONSOLE turned `SERIAL_8250` and
        // `SERIAL_EARLYCON` OFF for that machine, against an appliance-boot
        // contract that asks for both — and against the SHIPPED donor
        // (`tunnr/.kernel-out/tunnr-kernel/kernel.config`: `CONFIG_SERIAL_8250=y`),
        // which had been serving gunnar.rs off that exact machine for a month.
        // A kernel with no 8250 writes to neither port, and `earlycon=uart,io,0x2f8`
        // — the only printk between the firmware handing over and the driver
        // registering, the window every dark boot this constellation has
        // debugged sits in — would stop being available on the platform that
        // taught us to want it.
        //
        // So: the PORTS decide the driver and the early console; the CONSOLE
        // decides only whether printk is pointed AT one
        // (`SERIAL_8250_CONSOLE`). `validate` already refuses `serial` in
        // `consoles` with an empty `uarts`, so "a console on a port that is not
        // there" cannot reach this writer at all.
        match e.uarts.first().copied() {
            Some(p) => {
                let why = format!(
                    "target.uarts names {} at {:#x}: the machine has a 16550, and a kernel that cannot drive it cannot print to it — whether or not a console= points at one",
                    p.ttys(),
                    p.io_base()
                );
                v.push(("CONFIG_SERIAL_8250", true, why.clone()));
                v.push((
                    "CONFIG_SERIAL_EARLYCON",
                    true,
                    format!("{why}; and `earlycon` is the ONLY printk between the firmware handing over and the 8250 registering"),
                ));
            }
            None => {
                // The veto, and the ONE case in which it is a fact: no COM port
                // at all. Then the driver has nothing to bind, the contract's
                // `=y` really is a contradiction, and the closure refuses the
                // pair BY NAME instead of building a kernel that prints to
                // nobody.
                v.push((
                    "CONFIG_SERIAL_8250",
                    false,
                    "target.uarts is empty: this machine has no UART, so there is no 16550 to drive and no console= could point at one".into(),
                ));
                v.push((
                    "CONFIG_SERIAL_EARLYCON",
                    false,
                    "target.uarts is empty: an early console on a port that is not there prints to nobody".into(),
                ));
            }
        }
        if e.has_console(Console::Serial) {
            v.push((
                "CONFIG_SERIAL_8250_CONSOLE",
                true,
                match e.serial_port() {
                    Some(p) => format!("target.consoles names serial: printk goes to {} at {:#x}", p.ttys(), p.io_base()),
                    None => "target.consoles names serial".to_string(),
                },
            ));
        }
        // No `=n` twin for `SERIAL_8250_CONSOLE`: a machine nobody points a
        // `console=` at has no need for one and no argument against it, and the
        // symbol is inert without a `console=ttyS*` on the cmdline. Silence is
        // the third answer here — the one `CONFIG_UNIX` already gets in
        // [`crate::kernel_requires`] — and it is what keeps a VGA-only machine
        // from asking for a console on a driver it just switched off.
        if e.has_console(Console::Vga) {
            for s in ["CONFIG_VT", "CONFIG_VT_CONSOLE", "CONFIG_VGA_CONSOLE", "CONFIG_PRINTK"] {
                v.push((s, true, "target.consoles names vga: tty0 is a console".into()));
            }
        } else {
            for s in ["CONFIG_VT", "CONFIG_VGA_CONSOLE"] {
                v.push((s, false, "target.consoles has no vga: no text console to draw".into()));
            }
        }
        // The PCI root complex, once, whatever the buses are: every device this
        // group can spell is a PCI device — the IDE cdrom hangs off the PIIX,
        // virtio-blk and virtio-net off virtio-pci. Lane AE (2026-08-30) gave
        // `CONFIG_PCI` an owner here; before that it was a
        // `deps-tinyconfig.config` closure line the why-index called
        // preset-only, and the appliance-boot contract carries the same row for
        // a consumer that declares no `target` group at all.
        v.push(("CONFIG_PCI", true, "target.buses: every bus this machine can spell hangs off the PCI root complex".into()));
        // The menu symbol virtio itself lives behind — named when any declared
        // fact is a virtio one, so a hypothetical all-IDE machine does not drag
        // the virtio menu in.
        if e.buses.iter().any(|b| matches!(b, Bus::VirtioBlk | Bus::VirtioScsi)) {
            v.push(("CONFIG_VIRTIO_MENU", true, "target.buses names a virtio bus: VIRTIO/VIRTIO_PCI/VIRTIO_BLK all live inside the menu symbol".into()));
        } else if e.nic == Nic::Virtio {
            v.push(("CONFIG_VIRTIO_MENU", true, "target.nic = virtio: VIRTIO_NET lives inside the menu symbol".into()));
        }
        for b in &e.buses {
            let (syms, why): (&[&'static str], &str) = match b {
                Bus::IdeCdrom => (&["CONFIG_ATA", "CONFIG_ATA_PIIX", "CONFIG_BLK_DEV_SR"], "target.buses has ide-cdrom: the kernel itself must open an ATAPI cdrom on this machine"),
                Bus::VirtioBlk => (&["CONFIG_VIRTIO_BLK", "CONFIG_VIRTIO_PCI", "CONFIG_VIRTIO_PCI_LEGACY"], "target.buses has virtio-blk: the target disk and the medium's virtio face"),
                Bus::VirtioScsi => (&["CONFIG_SCSI", "CONFIG_BLK_DEV_SD", "CONFIG_SCSI_VIRTIO"], "target.buses has virtio-scsi"),
                Bus::Nvme => (&["CONFIG_NVME_CORE", "CONFIG_BLK_DEV_NVME"], "target.buses has nvme: AWS Nitro presents the boot disk (and every EBS volume) as an NVMe controller"),
                // storvsc is a SCSI LLDD over VMBus: HYPERV is the VMBus core
                // (x86: `depends on X86_LOCAL_APIC && HYPERVISOR_GUEST`, so the
                // guest menu must be open), HYPERV_STORAGE `depends on SCSI &&
                // HYPERV`. Read off linux-6.12.104 drivers/hv/Kconfig and
                // drivers/scsi/Kconfig.
                Bus::HvStorvsc => (&["CONFIG_HYPERVISOR_GUEST", "CONFIG_HYPERV", "CONFIG_SCSI", "CONFIG_BLK_DEV_SD", "CONFIG_HYPERV_STORAGE"], "target.buses has hv-storvsc: Azure presents every disk as Hyper-V synthetic SCSI over VMBus"),
                // blkfront is a `drivers/block` driver behind the Xen guest
                // menu, and the menu is what `olddefconfig` drops the driver
                // for. The chain, read off linux-6.12.104 (the tree tunnr
                // builds): `HYPERVISOR_GUEST` (menuconfig, arch/x86/Kconfig:783)
                // → `PARAVIRT` (:795, inside `if HYPERVISOR_GUEST` 793..910) →
                // `XEN` (arch/x86/xen/Kconfig:6, `depends on PARAVIRT`) →
                // `XEN_BLKDEV_FRONTEND` (drivers/block/Kconfig:318, `depends on
                // XEN`, and it `select`s XEN_XENBUS_FRONTEND so that one is not
                // asked for here). `BLK_DEV` (the menuconfig it sits inside) is
                // `default y` and comes out `kconfig-derived` in every built
                // tunnr kernel, so it is not named either.
                Bus::XenBlk => (&["CONFIG_HYPERVISOR_GUEST", "CONFIG_PARAVIRT", "CONFIG_XEN", "CONFIG_XEN_BLKDEV_FRONTEND"], "target.buses has xen-blk: an EC2 Xen HVM instance's root volume is taken over by blkfront as /dev/xvda"),
            };
            for s in syms {
                v.push((s, true, why.to_string()));
            }
        }
        match e.nic {
            Nic::Virtio => v.push(("CONFIG_VIRTIO_NET", true, "target.nic = virtio".into())),
            Nic::E1000 => v.push(("CONFIG_E1000", true, "target.nic = e1000".into())),
            Nic::Ena => {
                // ENA is not a bare driver symbol: it sits at the bottom of a
                // Kconfig menu chain, and every gate above it must be `y` or
                // `make olddefconfig` turns ENA back off (MEASURED 2026-09-02 —
                // the tunnr-kernel-nvme build refused because only
                // CONFIG_ENA_ETHERNET was asked for). The chain, read off
                // linux-6.12.104 drivers/net/ethernet/{Kconfig,amazon/Kconfig}:
                // ETHERNET (menuconfig, depends on NET) → NET_VENDOR_AMAZON →
                // ENA_ETHERNET (depends on PCI_MSI && !CPU_BIG_ENDIAN). x86 is
                // little-endian, and PCI is already forced by the bus rule.
                v.extend(ethernet_menu_rows("ena"));
                v.push(("CONFIG_NET_VENDOR_AMAZON", true, "target.nic = ena: ENA_ETHERNET sits inside `if NET_VENDOR_AMAZON`".into()));
                v.push(("CONFIG_PCI_MSI", true, "target.nic = ena: CONFIG_ENA_ETHERNET `depends on PCI_MSI` — without it olddefconfig drops ENA".into()));
                v.push(("CONFIG_ENA_ETHERNET", true, "target.nic = ena (AWS Nitro Elastic Network Adapter)".into()));
            }
            Nic::HvNetvsc => {
                // netvsc lives in drivers/net/hyperv, not under the ETHERNET
                // vendor menu: `HYPERV_NET depends on HYPERV` and selects
                // UCS2_STRING/NLS itself. NET is the stack it hangs off;
                // HYPERVISOR_GUEST + HYPERV are the VMBus it rides (the same
                // pair the storvsc bus names — a machine with one has both).
                v.push(("CONFIG_NET", true, "target.nic = hv-netvsc: a network driver — the whole net stack hangs off CONFIG_NET".into()));
                v.push(("CONFIG_HYPERVISOR_GUEST", true, "target.nic = hv-netvsc: CONFIG_HYPERV `depends on HYPERVISOR_GUEST`".into()));
                v.push(("CONFIG_HYPERV", true, "target.nic = hv-netvsc: the VMBus core the synthetic NIC rides".into()));
                v.push(("CONFIG_HYPERV_NET", true, "target.nic = hv-netvsc (Hyper-V synthetic NIC, Azure)".into()));
            }
            Nic::Gvnic => {
                // gVNIC is the same menu-chain shape as ENA and fails the same
                // way. MEASURED, not argued: `tunnr-kernel-hyperv`'s shipped
                // `kernel.config` has `# CONFIG_PCI_MSI is not set` and
                // `CONFIG_NET_VENDOR_GOOGLE=y` — and CONFIG_GVE is not in that
                // file AT ALL, not even as `# … is not set`, because
                // `depends on PCI_MSI` left it unoffered. That is exactly the
                // silent drop the ENA rows below were written for.
                //
                // The chain, read off linux-6.12.104
                // drivers/net/ethernet/google/Kconfig: `NET_VENDOR_GOOGLE`
                // (`default y`, the `if` GVE sits inside) → `GVE`
                // (`depends on (PCI_MSI && (X86 || CPU_LITTLE_ENDIAN))`). x86_64
                // satisfies the second half; PCI_MSI is the half that bites.
                v.extend(ethernet_menu_rows("gvnic"));
                v.push(("CONFIG_NET_VENDOR_GOOGLE", true, "target.nic = gvnic: CONFIG_GVE sits inside `if NET_VENDOR_GOOGLE`".into()));
                v.push(("CONFIG_PCI_MSI", true, "target.nic = gvnic: CONFIG_GVE `depends on PCI_MSI` — MEASURED on tunnr-kernel-hyperv, where PCI_MSI=n left GVE out of the built .config entirely".into()));
                v.push(("CONFIG_GVE", true, "target.nic = gvnic (Google Virtual NIC; required on GCE gen3+ machine series)".into()));
            }
            Nic::XenNetfront => {
                // netfront is NOT under the ETHERNET vendor menu — it lives at
                // drivers/net/xen-netfront.c, sourced directly from
                // drivers/net/Kconfig:575, inside `if NETDEVICES` (27..671) and
                // OUTSIDE `if NET_CORE` (39..500). `depends on XEN`, and it
                // `select`s XEN_XENBUS_FRONTEND and PAGE_POOL, so neither is
                // asked for here. The Xen guest menu above it is the same chain
                // `Bus::XenBlk` names — a machine with one frontend has both.
                v.push(("CONFIG_NET", true, "target.nic = xen-netfront: a network driver — the whole net stack hangs off CONFIG_NET".into()));
                v.push(("CONFIG_NETDEVICES", true, "target.nic = xen-netfront: XEN_NETDEV_FRONTEND lives inside `if NETDEVICES` (drivers/net/Kconfig)".into()));
                v.push(("CONFIG_HYPERVISOR_GUEST", true, "target.nic = xen-netfront: CONFIG_PARAVIRT lives inside `if HYPERVISOR_GUEST` (arch/x86/Kconfig)".into()));
                v.push(("CONFIG_PARAVIRT", true, "target.nic = xen-netfront: CONFIG_XEN `depends on PARAVIRT`".into()));
                v.push(("CONFIG_XEN", true, "target.nic = xen-netfront: the Xen guest port the frontend rides".into()));
                v.push(("CONFIG_XEN_NETDEV_FRONTEND", true, "target.nic = xen-netfront (AWS EC2 Xen HVM paravirtual NIC)".into()));
            }
        }
        if e.cpu == Cpu::Epyc {
            v.push(("CONFIG_CPU_SUP_AMD", true, "target.cpu = epyc (AMD)".into()));
        }
        // The firmware's own two symbols. Both spellings of `firmware` that lay
        // an ESP need EFI, and on x86 `CONFIG_EFI` depends on `CONFIG_ACPI` —
        // the firmware tables are how an EFI boot finds the machine. A
        // `seabios` medium still carries the ESP (the installed disk's
        // `korpbios.bin` lives on it) and is still started from a bzImage whose
        // EFI stub the boot contract requires, so the rows are unconditional
        // today; the day a firmware word means NO ESP at all, it says `=n` here
        // and the closure refuses the pair by name.
        let fw_why = format!("target.firmware = {}: the medium carries an ESP and the bzImage is started through it", e.firmware.as_str());
        v.push(("CONFIG_EFI", true, fw_why.clone()));
        v.push(("CONFIG_ACPI", true, format!("{fw_why} — on x86 CONFIG_EFI depends on CONFIG_ACPI")));
        v
    }
}

// ── the roster, emitted as data ──────────────────────────────────────────────

/// One emitted row of the roster (`[[target-preset]]`): the name, the cloud,
/// the DERIVED status, the QEMU gap when there is one, and the derived
/// `target` group when the group can spell the machine (`[target-preset.target]`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PresetRow {
    pub name: String,
    pub cloud: String,
    pub status: PresetStatus,
    /// Where/when it was measured, when it was.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub measured: Option<String>,
    /// Why QEMU cannot mimic it, when it cannot.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub needs_real_cloud: Option<String>,
    /// Why the `target` group cannot spell it, when it cannot.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub unspellable: Option<String>,
    pub notes: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target: Option<Target>,
}

impl PresetRow {
    /// The row FROM the const — the only way one is made.
    pub fn of(p: &TargetPreset) -> PresetRow {
        let (target, unspellable) = match p.target() {
            Ok(t) => (Some(t), None),
            Err(why) => (None, Some(why)),
        };
        PresetRow {
            name: p.name.into(),
            cloud: p.cloud.into(),
            status: p.status(),
            // The label, not the bare note: an emitted row says `real cloud: …`
            // or `qemu rig: …` so a reader of the toml never has to guess which
            // kind of evidence a date is (the `status` word says it too).
            measured: p.measured.map(|e| e.label()),
            needs_real_cloud: p.gap().map(str::to_string),
            unspellable,
            notes: p.notes.into(),
            target,
        }
    }
}

#[derive(Serialize, Deserialize)]
struct PresetRoster {
    #[serde(rename = "target-preset")]
    rows: Vec<PresetRow>,
}

/// The roster as data — what `info --format toml` prints and what the ⚒ pane
/// lists. Code → toml, one-directional; nothing reads it back as authority.
pub fn presets_toml() -> String {
    let rows = PRESETS.iter().map(PresetRow::of).collect();
    toml::to_string(&PresetRoster { rows }).expect("the preset roster serializes")
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The default is today's medium, and it emits nothing.
    #[test]
    fn the_default_is_todays_medium_and_is_silent() {
        let t = Target::default();
        assert!(t.is_default());
        assert!(t.firmware.lays_bios() && t.firmware.lays_uefi());
        assert_eq!(t.firmware.iso_format(), crate::iso::IsoFormat::Uefi);
        assert_eq!(t.console_tokens(), vec!["console=tty0", "console=ttyS0,115200"]);
        assert_eq!(t.canonical_fragment(), "");
        assert_eq!(t.effective(), t);
        t.validate("t").unwrap();
        // And it IS the qemu-local preset's derived shape — undeclared means
        // "this box", by construction and not by a second table — with ONE
        // deliberate difference, `buses`.
        //
        // The default is a COMPAT TOKEN, not a kernel claim: it is what the
        // medium did before this group existed, and its only job is
        // `is_default()`, which makes an undeclared group SILENT (`TargetFacts`
        // places no row for it, `canonical_fragment` is empty). Its `ide-cdrom`
        // therefore never reaches a Kconfig edge and never has. A DERIVED shape
        // does reach one, so it may only name buses the kernel must really
        // drive — see `MediumBring::kernel_medium_bus`, and the month four
        // gunnar rows spent unable to name their machine because these two
        // were kept identical instead of kept honest.
        //
        // Freezing the default is not cosmetic: `gunnar-recipe` hand-spells
        // `buses = [ide-cdrom, virtio-blk]` precisely so its row EQUALS the
        // default and stays silent. Changing the default would make that row
        // non-default, light up its whole closure and refuse the build on
        // `CONFIG_ATA` — the very failure this lane removed.
        let qemu_local = presets::QEMU_LOCAL.target().unwrap();
        assert_eq!(qemu_local, Target { buses: vec![Bus::VirtioBlk], ..t.clone() });
        assert_eq!(t.buses, vec![Bus::IdeCdrom, Bus::VirtioBlk], "the pre-group shape, frozen");
    }

    /// Each firmware word has its typed consequence on the medium.
    #[test]
    fn firmware_decides_which_el_torito_entries_are_laid() {
        assert!(!Firmware::Uefi.lays_bios() && Firmware::Uefi.lays_uefi());
        assert!(Firmware::SeaBios.lays_bios() && !Firmware::SeaBios.lays_uefi());
        assert_eq!(Firmware::SeaBios.iso_format(), crate::iso::IsoFormat::Bios);
        assert_eq!(Firmware::Uefi.iso_format(), crate::iso::IsoFormat::Uefi);
    }

    /// The toml spelling round-trips, and an unknown word is refused.
    #[test]
    fn the_toml_row_reads_and_writes_the_same_words() {
        let t: Target = toml::from_str(
            "firmware = \"seabios\"\nconsoles = [\"vga\"]\nuarts = []\nbuses = [\"ide-cdrom\", \"virtio-blk\"]\nnic = \"virtio\"\ncpu = \"epyc\"\n",
        )
        .unwrap();
        assert_eq!(t.firmware, Firmware::SeaBios);
        assert_eq!(t.consoles, vec![Console::Vga]);
        assert_eq!(t.cpu, Cpu::Epyc);
        assert!(!t.is_default());
        let back = toml::to_string(&t).unwrap();
        assert!(back.contains("firmware = \"seabios\""), "{back}");
        assert!(back.contains("\"ide-cdrom\""), "{back}");
        let again: Target = toml::from_str(&back).unwrap();
        assert_eq!(again, t);
        // A partial row keeps the defaults for what it does not spell.
        let partial: Target = toml::from_str("firmware = \"uefi\"").unwrap();
        assert_eq!(partial.firmware, Firmware::Uefi);
        assert_eq!(partial.consoles, Target::default().consoles);
        assert!(toml::from_str::<Target>("firmware = \"coreboot\"").is_err());
    }

    /// A boot nobody can see, a serial console with no UART, two writers of
    /// one fact, an unknown preset — each refused by name.
    #[test]
    fn unbootable_and_two_writer_rows_are_refused_by_name() {
        let mut t = Target::default();
        t.consoles.clear();
        assert!(t.validate("x").unwrap_err().to_string().contains("nobody can see"));
        t.consoles = vec![Console::Vga, Console::Vga];
        assert!(t.validate("x").unwrap_err().to_string().contains("twice"));
        t.consoles = vec![Console::Serial];
        t.uarts.clear();
        let err = t.validate("x").unwrap_err().to_string();
        assert!(err.contains("0x3f8") && err.contains("com2"), "{err}");
        t.consoles = vec![Console::Vga];
        t.buses.clear();
        assert!(t.validate("x").unwrap_err().to_string().contains("target.buses"));
        t.buses = vec![Bus::VirtioBlk];
        t.nics = 0;
        assert!(t.validate("x").unwrap_err().to_string().contains("nics = 0"));

        let p: Target = toml::from_str("preset = \"upcloud\"\nfirmware = \"uefi\"").unwrap();
        assert!(p.validate("x").unwrap_err().to_string().contains("not both"));
        let u: Target = toml::from_str("preset = \"hetzner\"").unwrap();
        let err = u.validate("x").unwrap_err().to_string();
        assert!(err.contains("hetzner") && err.contains("upcloud") && err.contains("qemu-local"), "{err}");
        // A machine the group cannot spell yet is refused with the device named.
        // aws-nitro WAS that machine until `d48e77c` (2026-09-02) taught the
        // group NVMe + ENA, and this assertion kept refusing it for three days
        // after — a red nobody ran. gVNIC and the Xen frontends left the list on
        // the W2 device-words lane (2026-09-05); `azure-gen1` is the one still
        // unspelt, and on its OS disk (a real IDE hard disk, which the group
        // spells only as a cdrom).
        let nitro: Target = toml::from_str("preset = \"aws-nitro\"").unwrap();
        nitro.validate("x").expect("nvme + ena are spelt since d48e77c");
        let gvnic: Target = toml::from_str("preset = \"gce-gvnic\"").unwrap();
        gvnic.validate("x").expect("gvnic is spelt since the W2 lane");
        let xen: Target = toml::from_str("preset = \"aws-xen-hvm\"").unwrap();
        xen.validate("x").expect("xen blkfront + netfront are spelt since the W2 lane");
        let n: Target = toml::from_str("preset = \"azure-gen1\"").unwrap();
        let err = n.validate("x").unwrap_err().to_string();
        assert!(err.contains("azure-gen1") && err.contains("ide"), "{err}");
        assert_eq!(n.effective(), n, "an unspellable preset resolves to the row itself; validate is the gate");
    }

    /// **UpCloud, as data**: SeaBIOS, no COM1 (ttyS1/ttyS2 exist), VGA is the
    /// ONLY console so `console=tty0` alone and NO `SERIAL_8250_CONSOLE` — the
    /// facts that cost the blind lanes. The 8250 DRIVER stays `y`: the ports
    /// are real, and it is only the console that is not pointed at them
    /// (2026-09-07). The preset is consumed through `effective()`; a row that
    /// names `serial` on a real port still resolves it to the first port that
    /// exists.
    #[test]
    fn the_upcloud_preset_is_the_measured_shape_and_its_kconfig_needs_follow() {
        let row: Target = toml::from_str("preset = \"upcloud\"").unwrap();
        row.validate("x").unwrap();
        assert!(!row.is_default());
        let e = row.effective();
        assert_eq!(e.firmware, Firmware::SeaBios);
        assert_eq!(e.uarts, vec![UartPort::Com2, UartPort::Com3]);
        assert_eq!(e.serial_port(), Some(UartPort::Com2));
        assert_eq!(e.console_tokens(), vec!["console=tty0"], "VNC is the only console anyone reads");
        assert_eq!(e.nics, 2);
        assert_eq!(e.cpu.qemu_model(), Some("EPYC"));
        assert!(row.canonical_fragment().contains("preset=upcloud") && row.canonical_fragment().contains("fw=seabios"));
        // An explicit row that DOES name serial on UpCloud's ports spells ttyS1, never ttyS0.
        let mut with_serial = e.clone();
        with_serial.consoles.push(Console::Serial);
        with_serial.validate("x").unwrap();
        assert_eq!(with_serial.console_tokens(), vec!["console=tty0", "console=ttyS1,115200"]);
        let needs = row.kconfig_needs();
        let find = |s: &str| needs.iter().find(|(sym, _, _)| *sym == s).map(|(_, on, _)| *on);
        assert_eq!(
            find("CONFIG_SERIAL_8250"),
            Some(true),
            "the PORTS decide the driver, and UpCloud has ttyS1/ttyS2 — only the ttyS0 nobody has is missing (2026-09-07)"
        );
        assert_eq!(find("CONFIG_SERIAL_8250_CONSOLE"), None, "and no `console=` is pointed at either of them");
        assert_eq!(find("CONFIG_VGA_CONSOLE"), Some(true));
        assert_eq!(
            find("CONFIG_ATA_PIIX"),
            None,
            "the UpCloud PRESET places no ATA requirement: the ISO arrives as an IDE cdrom the firmware boots, and the \
             kernel reading it is the DONOR's choice (tunnr 3f7c2ff carries PIIX/AHCI+SR in its own fragment), not \
             a consumer row that the `nuked` veto would refuse"
        );
        assert_eq!(find("CONFIG_VIRTIO_BLK"), Some(true));
        assert_eq!(find("CONFIG_VIRTIO_NET"), Some(true));
        assert_eq!(find("CONFIG_CPU_SUP_AMD"), Some(true));
        assert_eq!(find("CONFIG_E1000"), None);
        // A machine with NO port at all says SERIAL_8250 = n, by name.
        let mut vga_only = Target::default();
        vga_only.consoles = vec![Console::Vga];
        vga_only.uarts.clear();
        let needs = vga_only.kconfig_needs();
        let (_, on, why) = needs.iter().find(|(s, _, _)| *s == "CONFIG_SERIAL_8250").unwrap();
        assert!(!on && why.contains("no UART"), "{why}");
    }

    /// **A machine with real UARTs and no serial console keeps its 8250 — the
    /// same platform-vs-kernel split, one group over.**
    ///
    /// `target.consoles` says where the OPERATOR reads the boot; `SERIAL_8250`
    /// says whether the KERNEL carries the 16550 driver. UpCloud is the shape
    /// where those two answers differ: there is no COM1, so `console=ttyS0` is
    /// the dark boot this crate exists to prevent — but `ttyS1`/`ttyS2` at
    /// `0x2f8`/`0x3e8` REALLY EXIST (`Uarts::Known([Com2, Com3])`, and
    /// `QemuShape::uarts` is `[false, true, true, false]` precisely so a rig
    /// CAPTURES them). A kernel with no 8250 writes to neither.
    ///
    /// RED until 2026-09-07: the driver was owned by `target.consoles`, so
    /// naming `upcloud` flipped `CONFIG_SERIAL_8250` and `CONFIG_SERIAL_EARLYCON`
    /// from `y` to `n` and met the appliance-boot contract's `y` head-on — on a
    /// donor whose SHIPPED config says `CONFIG_SERIAL_8250=y` and which had been
    /// serving gunnar.rs on UpCloud for a month. The veto is right for a machine
    /// with NO port at all; it was never right for a machine with ports nobody
    /// points a `console=` at. The port decides the driver; the console decides
    /// only `SERIAL_8250_CONSOLE`.
    #[test]
    fn a_machine_with_ports_but_no_serial_console_keeps_the_8250_and_the_earlycon() {
        let row: Target = toml::from_str("preset = \"upcloud\"").unwrap();
        let needs = row.kconfig_needs();
        let find = |s: &str| needs.iter().find(|(sym, _, _)| *sym == s).map(|(_, on, _)| *on);
        let why = |s: &str| needs.iter().find(|(sym, _, _)| *sym == s).map(|(_, _, w)| w.clone()).unwrap();
        assert_eq!(find("CONFIG_SERIAL_8250"), Some(true), "ttyS1/ttyS2 exist: the driver is what makes them usable");
        assert!(why("CONFIG_SERIAL_8250").starts_with("target.uarts"), "owned by the PORTS, not the console: {}", why("CONFIG_SERIAL_8250"));
        assert!(why("CONFIG_SERIAL_8250").contains("ttyS1") && why("CONFIG_SERIAL_8250").contains("0x2f8"), "{}", why("CONFIG_SERIAL_8250"));
        assert_eq!(find("CONFIG_SERIAL_EARLYCON"), Some(true), "`earlycon=uart,io,0x2f8` is the door out of the next dark boot");
        assert_eq!(find("CONFIG_SERIAL_8250_CONSOLE"), None, "nothing asks for a console on a port no `console=` names");
        assert_eq!(find("CONFIG_VGA_CONSOLE"), Some(true), "what the operator DOES read is still required");
        assert_eq!(row.effective().console_tokens(), vec!["console=tty0"], "and no `console=` points at a UART");

        // The veto is intact where it is a fact: NO port at all.
        let mut none_at_all = Target::default();
        none_at_all.consoles = vec![Console::Vga];
        none_at_all.uarts.clear();
        let needs = none_at_all.kconfig_needs();
        let (_, on, why) = needs.iter().find(|(s, _, _)| *s == "CONFIG_SERIAL_8250").unwrap();
        assert!(!on && why.starts_with("target.uarts") && why.contains("no UART"), "{why}");
    }

    /// The roster is emitted as data — every const, its derived status, its
    /// gap, its derived `target` — and round-trips: the toml reads back to
    /// exactly the rows the consts produce, and every spellable preset's
    /// row validates as a `preset = "<name>"` reference.
    #[test]
    fn the_preset_roster_is_emitted_and_round_trips() {
        let text = presets_toml();
        for p in PRESETS {
            assert!(text.contains(&format!("name = \"{}\"", p.name)), "{text}");
        }
        // All FOUR status words appear, and the two measured ones are distinct:
        // the roster can now say "one cloud actually booted, two rigs" instead
        // of "three measured".
        for want in ["status = \"stub\"", "status = \"mimic-measured\"", "status = \"cloud-measured\"", "status = \"needs-real-cloud\""] {
            assert!(text.contains(want), "{want} missing from:\n{text}");
        }
        assert!(!text.contains("status = \"measured\""), "the conflated word is gone:\n{text}");
        // And the evidence itself says which kind it is, wherever it appears.
        assert!(text.contains("real cloud: 2026-08-28/29"), "the UpCloud row names its cloud evidence:\n{text}");
        assert!(text.contains("qemu rig: "), "the rig rows name theirs:\n{text}");
        let back: PresetRoster = toml::from_str(&text).unwrap();
        assert_eq!(back.rows.len(), PRESETS.len());
        for (row, p) in back.rows.iter().zip(PRESETS) {
            assert_eq!(*row, PresetRow::of(p), "{}: the toml is the const, byte for byte", p.name);
            assert_eq!(row.target.as_ref().map(|t| t.validate(p.name).is_ok()), p.target().ok().map(|_| true), "{}", p.name);
            let reference: Target = toml::from_str(&format!("preset = \"{}\"", p.name)).unwrap();
            assert_eq!(reference.validate("x").is_ok(), row.target.is_some(), "{}: spellable ⇔ a preset row validates", p.name);
            if let Some(t) = &row.target {
                assert_eq!(&reference.effective(), t, "{}: effective() IS the emitted target", p.name);
            }
        }
        assert_eq!(back.rows[2].name, "upcloud");
        assert_eq!(preset("upcloud").unwrap().nics, 2);
        assert!(preset("nope").is_none());
        assert_eq!(preset_names().len(), PRESETS.len());
    }
}