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