//! Host platform self-detection (R531-T1, W222).
//!
//! QED today models *where* (Local vs Remote) and *runtime* (Native vs
//! Container) but has no concept of **architecture**. W222 introduces three
//! triples per step — `host` (where commands actually execute), `target`
//! (what the step produces), and `container_platform` (the arch of the base
//! image it pulls) — and a `resolve(host, target, container_platform)`
//! decision table that picks cross-compile over emulation.
//!
//! This module is the foundation that lands first: the **host** triple is
//! cheap and reliable to self-detect at runner start, so the planner can
//! reason about portability instead of discovering an arch mismatch three
//! waves into a run (the mesofact `x86_64-unknown-linux-musl`-on-arm64
//! faceplant in W222's frame).
//!
//! The host triple is derived from the compiled binary's own
//! [`std::env::consts`] — `ARCH` (`uname -m`) plus `OS` mapped to the Rust
//! vendor/os/env convention. This is exactly the "uname -m + OS →
//! `aarch64-apple-darwin`" detection W222 calls for, and it needs no
//! subprocess: the QED runner *is* a host-native binary, so its own build
//! target is the host.
//!
//! F2 builds the structured `Platform { host, target, container_platform }`
//! field on steps atop [`detect_host_triple`]; F3 builds the `resolve(...)`
//! decision table that consumes the host triple this module produces.
//!
//! @arch:see(.yah/docs/working/W235-remote-qed.md)
//!
//! @yah:relay(R631, "Placement mesh-tags carry no OS dimension — a darwin target routes to Linux build-workers")
//! @yah:status(review)
//! @yah:assignee(agent:bundle-anthropic-miravel)
//! @yah:at(2026-07-24T05:11:43Z)
//! @yah:gotcha("The failure is silent and picks the WRONG node rather than none. An aarch64-apple-darwin offload requests exactly the tag set the Raspberry Pi 5s (us-west-011/013/014) already carry; candidates are filtered by tag superset and ties break on declaration order, so a Linux Pi wins and then cannot emit Mach-O.")
//! @yah:gotcha("qed already knows darwin cannot be cross-built from Linux — platform.rs resolve() sends such a target to Offload (see resolve_darwin_target_from_linux_host_offloads). So the placement decision is correct in isolation; it is only the TAG DERIVATION that loses the OS, which is why this survived.")
//! @yah:gotcha("us-west-015 already declares os:darwin and tag:mac-builder, but nothing selects on them — they are descriptive until this lands. Its inventory file says so explicitly; update that note when the gap closes.")
//! @arch:see(.yah/infra/machines/us-west-015.toml)
//! @yah:next("Verify on live infra once R626-F5 (kamaji deploy route) lands: a real rusty-v8-musl-shaped arm64/darwin offload should route to us-west-015 and no other node.")
//! @yah:handoff("Fixed the tag-derivation gap: build_worker_mesh_tags(arch, os) now takes an os token and emits an os:<os> requirement tag alongside tier:<arch> (oss/qed/crates/qed/src/platform.rs, new os_tag_of() helper reusing the existing target_os() triple classifier).")
//! @yah:handoff("Updated all 6 call sites (oss/qed/crates/qed/src/runner.rs:471,3444,4303,4472,6390 and app/yah/cli/src/qed_images.rs:457) to pass the OS — derived from the full target triple where one is available, hardcoded \"linux\" where the call site only ever builds docker container images (which are always Linux).")
//! @yah:handoff("Added os:linux to the mesh_tags of the four existing Linux build-workers (us-west-002/011/013/014) so they stay selectable now that build-worker placement requests an OS dimension; us-west-015 already declared os:darwin.")
//! @yah:handoff("Updated us-west-015.toml's header commentary: the declaration-order/capacity-floor contingency it described is gone now that os:darwin no longer tag-matches the Pi5s at all — R626-F5 (deploy route stub) is the only remaining blocker for real work landing there.")
//! @yah:handoff("All qed platform/runner unit tests updated and green (cargo test -p yah-qed --lib platform:: / runner::mesh_tags — new arm64_darwin_does_not_collide_with_arm64_linux regression test added); cargo check -p yah --bin yah is clean.")
//! @yah:gotcha("SIBLING GAP, closed separately under R577-F2 (2026-08-04) -- flagged here because R631 alone did NOT make darwin routing work, and a reviewer signing this off could reasonably assume it did. The same arch-only blindness existed one layer UP, in platform::resolve_placement, which decides local-vs-offload before any mesh tag is derived. It compared arch_of(target) vs arch_of(host) only (with a test asserting 'different OS is irrelevant'), so on an arm64 Linux coordinator an aarch64-apple-darwin native=true step resolved NativeCross and never offloaded at all -- meaning R631's os:darwin tags were unreachable on that path. resolve_placement now compares (arch, OS), exempting steps that declare a container_platform. Same file; R577-F2's diff sits just above build_worker_mesh_tags. Nothing in R631's own change needed altering.")
use serde::{Deserialize, Serialize};
/// The TOML-declared portion of a step's platform intent (R531-F2, W222).
///
/// `host` is deliberately *not* here — it's self-detected per runner
/// (R531-T1) and composed in at plan time, so a pipeline file never hard-codes
/// the machine it runs on. A step declares only what it *produces* (`target`)
/// and, when it pulls a foreign-arch base image, that image's docker platform
/// (`container_platform`). Both default to `None`, so the overwhelming
/// majority of steps (host-native builds, checks, typechecks) need no
/// `[platform]` block at all.
///
/// On the TOML side this is an inline table on a step:
///
/// ```toml
/// [[steps]]
/// name = "build-musl"
/// platform = { target = "x86_64-unknown-linux-musl" }
/// ```
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct PlatformSpec {
/// Rust target triple this step produces, e.g.
/// `x86_64-unknown-linux-musl`. `None` = host-native build / nothing
/// cross-compiled.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
/// Docker platform of the toolchain / base image this step pulls, e.g.
/// `linux/amd64`. `None` = no container, or the host-platform default.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub container_platform: Option<String>,
/// R590-F4: when `true`, this step's target MUST be built on a real machine
/// of that target's **platform** — its arch *and* its OS (R577-F2; a
/// declared `container_platform` exempts the OS half, since the container
/// brings its own userland). QED disables the cross-compile / emulate tiers
/// for the step and routes it to a matching build-worker (`Offload`)
/// instead. Defaults `false`, so ordinary steps keep the cross-first ladder.
/// Set it only for builds that genuinely can't cross or emulate here — e.g.
/// `rusty-v8-musl`, a gn/ninja C++ build that OOMs under QEMU on an arm64
/// host, or a Tauri `.dmg`, which needs a live macOS userland. On the TOML
/// side:
/// `platform = { target = "x86_64-unknown-linux-musl", native = true }`.
#[serde(default, skip_serializing_if = "is_false")]
pub native: bool,
}
/// `skip_serializing_if` predicate for a `bool` field that defaults to `false`
/// — so an all-default [`PlatformSpec`] still emits no TOML keys.
fn is_false(b: &bool) -> bool {
!*b
}
/// A step's fully-composed platform triple-set (R531-F2, W222): where it runs
/// (`host`), what it produces (`target`), and the arch of the image it pulls
/// (`container_platform`).
///
/// Built at plan time by [`Platform::compose`] from the runner's self-detected
/// host (R531-T1) plus the step's declared [`PlatformSpec`] — falling back to
/// the legacy per-kind `triple` field so existing `package-native-tarball`
/// TOML keeps producing the right target without a `[platform]` block. F3's
/// `resolve(host, target, container_platform)` decision table consumes this
/// directly.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Platform {
/// Where the step's commands actually execute — the runner host triple.
pub host: String,
/// What the step produces. `None` = host-native, nothing cross-built.
pub target: Option<String>,
/// Arch of the base image the step pulls (`linux/amd64`). `None` = no
/// container, or the host-platform default.
pub container_platform: Option<String>,
}
impl Platform {
/// Compose the triple-set for a step.
///
/// - `host` — the runner's self-detected triple (R531-T1).
/// - `declared` — the step's `[platform]` block, if any.
/// - `triple_field` — the legacy per-kind `triple`
/// (`package-native-tarball` / `sign-native-tarball`), used as the
/// `target` fallback so existing TOML keeps working: an explicit
/// `[platform].target` always wins over it.
pub fn compose(
host: impl Into<String>,
declared: Option<&PlatformSpec>,
triple_field: Option<&str>,
) -> Self {
let target = declared
.and_then(|d| d.target.clone())
.or_else(|| triple_field.map(str::to_string));
let container_platform = declared.and_then(|d| d.container_platform.clone());
Platform {
host: host.into(),
target,
container_platform,
}
}
/// True when the step builds for an arch other than the host's. A
/// `target` of `None` (host-native) is never cross. Compared on the arch
/// segment only — `x86_64-apple-darwin` on an `x86_64-unknown-linux-gnu`
/// host is *not* a cross *arch* even though the full triples differ (the
/// OS/cross distinction is F3's resolution concern, not this predicate's).
pub fn is_cross_arch(&self) -> bool {
match &self.target {
None => false,
Some(t) => arch_of(t) != arch_of(&self.host),
}
}
/// True when the step pulls a container image whose arch differs from the
/// host's — the exact host ≠ container_platform mismatch that produced the
/// mesofact `no matching manifest for linux/arm64` faceplant in W222. The
/// `linux/amd64` docker-platform vocabulary is normalized to a bare arch
/// for the comparison.
pub fn container_is_foreign_arch(&self) -> bool {
match &self.container_platform {
None => false,
Some(p) => docker_platform_arch(p) != Some(arch_of(&self.host)),
}
}
}
/// The verdict of [`resolve`] for one step's platform triple-set (R531-F3,
/// W222): *how* QED should satisfy a "build for target T" / "pull image P"
/// step on the host it actually runs on.
///
/// The ordering encodes W222's **cross-compile first, emulate last** ladder:
/// a target that can be built with a host-native linker always is; emulation
/// is an explicit, named fallback for the residue, never the silent default.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Resolution {
/// Tier 1 — host-native cross-compile (`cargo-zigbuild` / musl-cross), no
/// container, no emulation. The default path for the overwhelming
/// majority of Rust targets (W222: ~99%). Also covers a plain host-native
/// build (target absent or host-arch).
NativeCross,
/// Tier 2 — `cross` via a **host-arch** toolchain container. The toolchain
/// runs in a container, but the container's arch matches the host so this
/// is genuinely emulation-free (unlike a foreign-arch `cross` image, which
/// resolves to [`Emulate`](Self::Emulate)). Chosen when the target isn't
/// host-native crossable but a host-arch cross image can build it.
CrossDocker,
/// Tier 3 — QEMU / platform virtualization. The step pulls a foreign-arch
/// image (`docker_platform`) and runs it under emulation. This is the
/// W222 mesofact case (`linux/amd64` cross-rs image on an arm64 host) and
/// multi-arch `buildx` image builds, where there is no cross-compile for
/// an image. Slow and explicit — the preflight (T4) flags it so the
/// operator sees the cost before the run.
Emulate { docker_platform: String },
/// No local path: the target can't be host-native crossed and no container
/// can build it here (e.g. `*-apple-darwin` from a Linux host) — it needs
/// a real runner capable of building `target`. The scheduler picks the
/// concrete remote (P2+); the verdict just names what's needed.
Offload { target: String },
/// Nothing resolvable: a foreign target with an unrecognized arch and no
/// container — QED can neither cross it, emulate it, nor name a runner for
/// it. Carries a human-readable reason for the preflight.
Skip { reason: String },
}
impl Resolution {
/// Short human label with the cost/mechanism parenthetical, for the T4
/// portability preflight and the QED detail pane.
pub fn label(&self) -> String {
match self {
Resolution::NativeCross => "NativeCross (cargo-zigbuild)".into(),
Resolution::CrossDocker => "CrossDocker (cross-rs container)".into(),
Resolution::Emulate { docker_platform } => {
format!("Emulate (QEMU {docker_platform}, slow)")
}
Resolution::Offload { target } => format!("Offload (needs {target} runner)"),
Resolution::Skip { reason } => format!("Skip ({reason})"),
}
}
/// True for the emulation / offload tiers — the verdicts that mean "this
/// step will *not* run fast (or at all) on this host". The preflight uses
/// this to flag divergence; tier 1/2 (NativeCross / CrossDocker) are the
/// emulation-free happy path.
pub fn is_slow_or_unsatisfiable(&self) -> bool {
matches!(
self,
Resolution::Emulate { .. } | Resolution::Offload { .. } | Resolution::Skip { .. }
)
}
}
/// Render one portability-preflight line for a step (R531-T4, W222): its name,
/// what it targets/builds, the host it runs on, and the resolution verdict —
/// so an operator sees where mac and linux diverge (and what it costs) *before*
/// a run, not after a faceplant. Format mirrors W222's example:
///
/// ```text
/// mesofact-dev-build · targets x86_64-unknown-linux-musl · host aarch64-apple-darwin · resolution = NativeCross (cargo-zigbuild)
/// ```
pub fn preflight_line(name: &str, platform: &Platform, resolution: &Resolution) -> String {
let what = match (&platform.target, &platform.container_platform) {
(Some(t), _) => format!("targets {t}"),
(None, Some(c)) => format!("builds {c} image"),
(None, None) => "host-native".to_string(),
};
format!(
"{name} · {what} · host {host} · resolution = {res}",
host = platform.host,
res = resolution.label(),
)
}
/// Total resolution function (R531-F3, W222) — a decision-table-as-spec over
/// (host-arch × target × container-arch). Pure and total: every input maps to
/// exactly one [`Resolution`], so the mac-vs-linux behaviour is *specified and
/// tested* rather than emergent.
///
/// It is **toolchain-blind on purpose** (W235 §4): branch 1 asks whether a pair
/// is *crossable*, never whether this box has the cross toolchain installed.
/// That Capability question is an input to Derivation one layer up, in
/// [`resolve_placement`] — putting a filesystem probe in here would cost the
/// purity that makes this a spec.
///
/// The decision order (cross-first):
/// 1. **Host-native crossable target → [`NativeCross`](Resolution::NativeCross).**
/// Wins even if the recipe declares a foreign container — that container is
/// the slow path P2/T6 should replace, not what *should* happen.
/// 2. **Foreign-arch container → [`Emulate`](Resolution::Emulate).** A
/// foreign image can't be cross-compiled away; it's pulled and run under
/// QEMU.
/// 3. **Foreign non-crossable target** → [`CrossDocker`](Resolution::CrossDocker)
/// if a host-arch toolchain container is present, else
/// [`Offload`](Resolution::Offload) (known arch) or
/// [`Skip`](Resolution::Skip) (unknown arch).
/// 4. **No target / host-arch target** → host-native
/// [`NativeCross`](Resolution::NativeCross).
pub fn resolve(host: &str, target: Option<&str>, container_platform: Option<&str>) -> Resolution {
let host_arch = arch_of(host);
let foreign_target = target
.map(str::trim)
.filter(|t| !t.is_empty())
.filter(|t| arch_of(t) != host_arch);
// 1. Cross-first: a host-native crossable target always wins.
if let Some(t) = foreign_target {
if host_native_crossable(host, t) {
return Resolution::NativeCross;
}
}
// 2. A foreign-arch container forces emulation (mesofact case + buildx).
if let Some(p) = container_platform {
if docker_platform_arch(p) != Some(host_arch) {
return Resolution::Emulate {
docker_platform: p.to_string(),
};
}
}
// 3. Foreign target we can't host-native cross. Any container that reaches
// here is host-arch (a foreign one returned Emulate above), so it's a
// cross-rs-style toolchain image that can build the target.
if let Some(t) = foreign_target {
if container_platform.is_some() {
return Resolution::CrossDocker;
}
if is_known_arch(arch_of(t)) {
return Resolution::Offload {
target: t.to_string(),
};
}
return Resolution::Skip {
reason: format!(
"cannot build `{t}` on host `{host}`: target arch is unrecognized, \
no host-native cross path, and no toolchain container declared"
),
};
}
// 4. No target, or a host-arch target → plain native build on the host.
Resolution::NativeCross
}
/// R590-F4 native-placement policy — the entry point the runner resolves each
/// step through. Placement is *derived from what the step declares*, not an
/// imperative `--where` flag.
///
/// When `native` is `false` this defers entirely to the cross-first decision
/// table [`resolve`] (the ~99% path: cross-compile beats emulation).
///
/// When `native` is `true` the step demands a real machine of its target
/// *platform*, so the cross-compile and emulate tiers are *disabled* and the
/// decision collapses to a binary:
/// - host-platform (or absent) target → build locally ([`NativeCross`]);
/// - foreign-platform target → [`Offload`] to a matched build-worker.
///
/// "Platform" here is **arch and OS**, not arch alone (R577-F2). Arch alone was
/// the same blindness R631 fixed one layer up in [`build_worker_mesh_tags`]:
/// there it picked the wrong *node*, here it picks the wrong *machine class
/// entirely*. `native = true` means "no cross-compiling, run it on real
/// silicon", and a Mach-O `.app`/`.dmg` cannot be produced by a Linux userland
/// any more than an AppImage can be produced by macOS — even when both are
/// `aarch64`. Concretely, `desktop-release`'s pipeline-level matrix declares
/// `aarch64-apple-darwin` *and* `aarch64-unknown-linux-gnu` with
/// `native = true`: on this camp's arm64 Mac the Linux row used to match on
/// arch and resolve [`NativeCross`], i.e. run `cargo tauri build` for a Linux
/// bundle against macOS, instead of offloading to a Pi5. The mirror case is the
/// darwin row from an arm64 Linux coordinator.
///
/// The OS dimension applies only when the step has **no** `container_platform`.
/// A container supplies its own userland — docker on macOS runs a Linux VM — so
/// a `linux/arm64` image build on an arm64 Mac genuinely is native and must
/// keep resolving [`NativeCross`]. Only a bare host-userland build is
/// OS-constrained.
///
/// Unknown OS tokens (bare-metal triples like `mos-unknown-none`, for which
/// [`os_tag_of`] yields `"unknown"`) never force an offload: there is no node
/// to name for them, so they keep the pre-R577 arch-only verdict rather than
/// routing to a build-worker that cannot exist.
///
/// `native = true` deliberately overrides even a declared foreign
/// `container_platform` (which [`resolve`] would send to [`Emulate`] at its
/// branch 2): the whole point is "no emulation — put it on real silicon." This
/// is the `rusty-v8-musl` forcing case — a gn/ninja C++ build that OOMs under
/// QEMU, so it must land on the x86 build-worker (`us-west-002`) rather than
/// emulate on an arm64 host.
///
/// # Capability folds in here, not in [`resolve`]
///
/// R555-B10 / W235 §6: Derivation answers *what does physics allow*, and it is
/// host-relative but toolchain-blind — `host_native_crossable` says a pair is
/// crossable, not that this box has the cross toolchain installed. The
/// **Capability** axis (W235 §4) is an *input* to Derivation, not a peer of it,
/// so this entry point takes the probed `capability` and demotes a
/// [`NativeCross`](Resolution::NativeCross) verdict this host cannot actually
/// carry to [`Offload`](Resolution::Offload) — see [`capability_demotion`]. [`resolve`]
/// and [`derive_placement`] stay pure decision tables over the declaration; the
/// probing (and its caching) belongs to the caller.
pub fn resolve_placement(
host: &str,
target: Option<&str>,
container_platform: Option<&str>,
native: bool,
capability: &crate::nativecross::ToolAvailability,
) -> Resolution {
let derived = derive_placement(host, target, container_platform, native);
match capability_demotion(host, target, container_platform, &derived, capability) {
Some(gap) => Resolution::Offload { target: gap.target },
None => derived,
}
}
/// The Derivation half of [`resolve_placement`] — pure over the *declaration*
/// (host, target, container, `native`), with no tool-availability input.
///
/// Split out under R555-B10 so the two W235 §4 axes are separately nameable:
/// this is "what does physics allow on a host of this shape", and it is the
/// verdict the NativeCross-tier machinery must gate on (a step whose derivation
/// is NativeCross still routes its argv through
/// [`plan_native_cross`](crate::nativecross::plan_native_cross) when forced
/// local, so it fails with the toolchain's install hint rather than a raw linker
/// error). Callers deciding *where a step runs* want [`resolve_placement`],
/// which folds Capability in on top of this.
pub fn derive_placement(
host: &str,
target: Option<&str>,
container_platform: Option<&str>,
native: bool,
) -> Resolution {
if !native {
return resolve(host, target, container_platform);
}
let host_arch = arch_of(host);
// A container carries its own OS, so only a bare host-userland build is
// constrained by the host's OS.
let os_constrained = container_platform.is_none();
match target.map(str::trim).filter(|t| !t.is_empty()) {
Some(t) if arch_of(t) != host_arch || (os_constrained && foreign_os(host, t)) => {
Resolution::Offload {
target: t.to_string(),
}
}
// Host-platform target or no target: a plain native build on this host.
_ => Resolution::NativeCross,
}
}
/// The Capability gap in a Derivation verdict (R555-B10, W235 §4/§6):
/// `Some(gap)` when `resolution` is [`NativeCross`](Resolution::NativeCross) for
/// a target that needs a *foreign* host toolchain which `capability` says is not
/// installed. `None` means this host can carry the verdict as derived.
///
/// Pure — the probe that produced `capability` is the caller's. This is exactly
/// the predicate `execute_step_local` already applies at *execution* time (it
/// selects the tool and hard-fails on
/// [`CrossToolUnavailable`](crate::nativecross::CrossToolUnavailable)); asking it
/// at resolution time is the whole of the fix, because by execution time the
/// step has already been placed on this box.
///
/// The returned error carries the tool it would have used and its install hint,
/// which is the payload a demotion warning must name — W235 §6's deliberate
/// non-goal is a *silent* demotion.
///
/// A gap is necessary but not sufficient for a demotion: see
/// [`capability_demotion`], which is what callers should ask.
fn capability_gap(
host: &str,
target: Option<&str>,
resolution: &Resolution,
capability: &crate::nativecross::ToolAvailability,
) -> Option<crate::nativecross::CrossToolUnavailable> {
if !matches!(resolution, Resolution::NativeCross) {
return None;
}
let target = target.map(str::trim).filter(|t| !t.is_empty())?;
// A host-platform target needs no foreign toolchain at all (plain `cargo
// build`), so there is nothing for a probe to be missing.
if !crate::nativecross::is_native_cross_target(host, target) {
return None;
}
crate::nativecross::select_cross_tool(host, Some(target), capability).err()
}
/// Whether [`resolve_placement`] demotes this
/// [`NativeCross`](Resolution::NativeCross) verdict to
/// [`Offload`](Resolution::Offload), and why (R555-B10, W235 §6).
///
/// `Some(gap)` means the demotion fires and `gap` names the missing tool and its
/// install command — the payload every warning about it must carry. Callers that
/// need to *report* a demotion (the runner's preflight, the CLI's `--where=auto`
/// notice) ask this rather than diffing two resolutions, so the report and the
/// routing can never disagree.
///
/// The measured failure this exists for: `qed run 690455c1` (2026-08-19) lost 2
/// of `mesofact-build`'s 4 matrix legs because the coordinator Mac had no
/// `cargo-zigbuild`, while an idle x86 build-worker sat in the same camp. An
/// under-provisioned coordinator should ship the build to a box that can do it,
/// not hard-fail the release.
///
/// Two deliberate non-demotions, both of which would otherwise trade a clear
/// error for a wrong one:
///
/// - **A declared `container_platform` is left alone.** The container supplies
/// its own toolchain, and the host probe knows nothing about what is inside
/// it, so "this box lacks zig" is not evidence the step cannot build here —
/// `mesofact-musl`'s arm64 leg (`native = true`, `container_platform =
/// "linux/arm64"`) builds through Colima's Linux VM on this Mac and must keep
/// resolving NativeCross. A step on the container path that *does* need a host
/// toolchain keeps the pre-R555-B10 behaviour: an install-hint failure at
/// execution time.
/// - **An unrecognized target arch is left alone**, matching [`resolve`]'s
/// branch 3: there is no build-worker tag to name for it (see
/// [`build_worker_mesh_tags`]), so offloading would route into the void.
pub fn capability_demotion(
host: &str,
target: Option<&str>,
container_platform: Option<&str>,
derived: &Resolution,
capability: &crate::nativecross::ToolAvailability,
) -> Option<crate::nativecross::CrossToolUnavailable> {
if container_platform.is_some() {
return None;
}
capability_gap(host, target, derived, capability)
.filter(|gap| is_known_arch(arch_of(&gap.target)))
}
/// True when `target`'s OS differs from `host`'s *and both are recognized*.
///
/// The both-known guard is what keeps a bare-metal / unrecognized triple from
/// being routed to a build-worker that could never be tagged for it — see
/// [`derive_placement`], the only caller.
fn foreign_os(host: &str, target: &str) -> bool {
let (host_os, target_os) = (os_tag_of(host), os_tag_of(target));
host_os != "unknown" && target_os != "unknown" && host_os != target_os
}
/// Can `target` be built on `host` with a host-native linker
/// (`cargo-zigbuild` / musl-cross), i.e. no container and no emulation?
///
/// The spec (refinable as real cross builds surface, the same discipline as
/// [`crate::preflight::KNOWN_GLIBC_ONLY_CRATES`]):
/// - **Linux** (`-gnu` / `-musl`, any arch): yes from any host — zig provides
/// the sysroot + linker for both libc flavors.
/// - **Windows `-gnu`**: yes from any host (zig). **Windows `-msvc`**: no —
/// needs the MSVC toolchain.
/// - **Apple/Darwin**: yes only from a macOS host (the SDK + codesign aren't
/// redistributable), no from Linux/Windows.
/// - **Unknown OS**: no.
pub fn host_native_crossable(host: &str, target: &str) -> bool {
match target_os(target) {
TargetOs::Linux => true,
TargetOs::Windows { msvc } => !msvc,
TargetOs::Darwin => matches!(target_os(host), TargetOs::Darwin),
TargetOs::Unknown => false,
}
}
/// Coarse OS classification of a target triple, for [`host_native_crossable`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TargetOs {
Linux,
Windows { msvc: bool },
Darwin,
Unknown,
}
/// Is `triple` a Windows target on the **MSVC** ABI? The ABI split is the whole
/// reason Windows appears twice in W352's four-layer table: `-gnu` cross-builds
/// from any host under zig, `-msvc` cross-builds from nowhere and needs a real
/// Windows execution surface. One definition, shared by [`target_os`] and
/// [`crate::buildcap`], so the two can't drift.
pub fn target_is_msvc(triple: &str) -> bool {
triple.contains("windows") && triple.ends_with("msvc")
}
fn target_os(triple: &str) -> TargetOs {
if triple.contains("linux") {
TargetOs::Linux
} else if triple.contains("windows") {
TargetOs::Windows {
msvc: target_is_msvc(triple),
}
} else if triple.contains("darwin") || triple.contains("apple") {
TargetOs::Darwin
} else {
TargetOs::Unknown
}
}
/// The OS segment of a target triple, normalized to the `os:<os>` mesh-tag
/// vocabulary (R631). Reuses [`target_os`]'s triple classification so this
/// stays in lockstep with [`host_native_crossable`]'s notion of OS.
pub fn os_tag_of(triple: &str) -> &'static str {
match target_os(triple) {
TargetOs::Linux => "linux",
TargetOs::Windows { .. } => "windows",
TargetOs::Darwin => "darwin",
TargetOs::Unknown => "unknown",
}
}
/// Recognized CPU arch tokens — the set [`resolve`] can name a runner for when
/// it has to [`Offload`](Resolution::Offload). An unrecognized arch resolves
/// to [`Skip`](Resolution::Skip) instead.
fn is_known_arch(arch: &str) -> bool {
matches!(
arch,
"x86_64" | "aarch64" | "arm64" | "x86" | "i686" | "arm" | "riscv64" | "powerpc64" | "s390x"
)
}
/// Map a docker `--platform` value (`linux/amd64`, `linux/arm64/v8`, or a bare
/// `arm64`) to the Rust arch token used in target triples (`x86_64`,
/// `aarch64`). Returns `None` for an unrecognized arch so callers can decide
/// how to treat the unknown rather than silently matching.
pub fn docker_platform_arch(platform: &str) -> Option<&'static str> {
// `os/arch[/variant]` — the arch is the middle (or only) segment.
let arch = platform.split('/').nth(1).unwrap_or(platform);
match arch {
"amd64" | "x86_64" => Some("x86_64"),
"arm64" | "aarch64" => Some("aarch64"),
"386" | "x86" => Some("x86"),
"arm" => Some("arm"),
_ => None,
}
}
/// Detect the host's Rust target triple (e.g. `aarch64-apple-darwin`,
/// `x86_64-unknown-linux-gnu`).
///
/// Composed from [`std::env::consts::ARCH`] and [`std::env::consts::OS`] —
/// the arch is taken verbatim (it already matches the Rust triple's first
/// segment) and the OS is mapped to the canonical `<vendor>-<os>[-<env>]`
/// tail. A Linux host is reported as `-gnu`: a musl *host* is vanishingly
/// rare for our runners, and `target` (what a step builds) — where musl
/// actually matters — is a separate triple F2 carries per step.
///
/// Unknown OSes fall back to `unknown-<os>` so the output is still a
/// well-formed, greppable triple rather than a panic.
pub fn detect_host_triple() -> String {
let arch = std::env::consts::ARCH;
let tail = match std::env::consts::OS {
"macos" => "apple-darwin",
"linux" => "unknown-linux-gnu",
"windows" => "pc-windows-msvc",
other => return format!("{arch}-unknown-{other}"),
};
format!("{arch}-{tail}")
}
/// Normalize a Rust arch token (the first segment of a target triple) to the
/// GitHub Actions `runner.arch` vocabulary (`X86` / `X64` / `ARM` / `ARM64`).
///
/// GHA workflows gate on `runner.arch`, so when QED threads the detected host
/// into the GHA expression context (see `yah_qed_gha::Executor::runner_arch`) it
/// must speak that vocabulary, not Rust's. An unrecognized arch is upcased
/// verbatim — a forward-compatible, debuggable default rather than a wrong
/// guess.
pub fn gha_runner_arch(arch: &str) -> String {
match arch {
"x86_64" => "X64".into(),
"aarch64" | "arm64" => "ARM64".into(),
"x86" | "i686" => "X86".into(),
"arm" => "ARM".into(),
other => other.to_ascii_uppercase(),
}
}
/// The arch segment (first `-`-delimited token) of a target triple.
/// `arch_of("aarch64-apple-darwin") == "aarch64"`.
pub fn arch_of(triple: &str) -> &str {
triple.split('-').next().unwrap_or(triple)
}
/// R594/R631: mesh tags that select an arch-and-OS-matched build-worker for a
/// remote image build. `arch` is an arch token (as from [`arch_of`]); `os` is
/// an OS token (as from [`os_tag_of`]). The returned tags are a *superset
/// requirement* — a candidate node must carry all of them.
///
/// The fleet nodes are tagged in `.yah/infra/machines/*.toml` with
/// `mesh_tags = ["tag:build-worker", "tag:qed", "arch:x86" | "arch:arm",
/// "os:linux" | "os:darwin"]`, so an amd64 image build routes to
/// `us-west-002` (x86) and an arm64 *Linux* build to the Pi5s — not to
/// `us-west-015`, the fleet's only arm64 *Darwin* node, even though it also
/// carries `arch:arm`. Before R631 this function derived the tag set from
/// arch alone, so an `aarch64-apple-darwin` offload requested exactly the tag
/// set the Pi5s already carry and a Linux node silently won a job it could
/// never satisfy — the OS dimension is what tells the Pi5s and the Mac apart.
/// yubaba admission consumes this set (see
/// `velveteen_exec::remote::NODE_SELECTOR_MESH_TAGS_ANNOTATION`).
pub fn build_worker_mesh_tags(arch: &str, os: &str) -> Vec<String> {
let os_tag = format!("os:{os}");
let arch_tag = match arch {
"x86_64" | "x86" | "i686" | "amd64" => "arch:x86",
"aarch64" | "arm64" | "arm" => "arch:arm",
// Unknown arch: fall back to the build-worker pool without an arch pin
// (still OS-pinned — a foreign OS can't be emulated the way an
// unrecognized arch tier can); yubaba admission picks any build-worker
// of the right OS (may emulate the arch).
_ => return vec!["tag:build-worker".into(), os_tag],
};
vec!["tag:build-worker".into(), arch_tag.into(), os_tag]
}
#[cfg(test)]
mod build_worker_tag_tests {
use super::build_worker_mesh_tags;
#[test]
fn amd64_selects_x86_build_worker() {
assert_eq!(
build_worker_mesh_tags("x86_64", "linux"),
vec![
"tag:build-worker".to_string(),
"arch:x86".to_string(),
"os:linux".to_string()
]
);
}
#[test]
fn arm64_selects_arm_build_worker() {
assert_eq!(
build_worker_mesh_tags("aarch64", "linux"),
vec![
"tag:build-worker".to_string(),
"arch:arm".to_string(),
"os:linux".to_string()
]
);
}
#[test]
fn arm64_darwin_does_not_collide_with_arm64_linux() {
// R631: same tier, different OS — the Pi5s (arm/linux) must not be a
// candidate for an arm64/darwin request, and vice versa.
let darwin = build_worker_mesh_tags("aarch64", "darwin");
let linux = build_worker_mesh_tags("aarch64", "linux");
assert_ne!(darwin, linux);
assert!(darwin.contains(&"os:darwin".to_string()));
assert!(linux.contains(&"os:linux".to_string()));
}
#[test]
fn unknown_arch_falls_back_to_os_pinned_pool() {
assert_eq!(
build_worker_mesh_tags("riscv64", "linux"),
vec!["tag:build-worker".to_string(), "os:linux".to_string()]
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_host_triple_is_a_wellformed_triple() {
let t = detect_host_triple();
// Always at least arch + vendor + os (3 segments), arch first.
let segs: Vec<&str> = t.split('-').collect();
assert!(
segs.len() >= 3,
"host triple should have >=3 segments: {t:?}"
);
assert_eq!(segs[0], std::env::consts::ARCH, "arch segment leads: {t:?}");
}
#[test]
fn detect_host_triple_matches_known_os_tails() {
// The detected triple's tail must match the running OS's convention,
// so this test pins the mapping on whatever host CI/dev runs it.
let t = detect_host_triple();
match std::env::consts::OS {
"macos" => assert!(t.ends_with("-apple-darwin"), "{t:?}"),
"linux" => assert!(t.ends_with("-unknown-linux-gnu"), "{t:?}"),
"windows" => assert!(t.ends_with("-pc-windows-msvc"), "{t:?}"),
_ => {} // unknown-OS fallback covered by the wellformed test
}
}
#[test]
fn gha_runner_arch_maps_the_rust_vocabulary() {
assert_eq!(gha_runner_arch("x86_64"), "X64");
assert_eq!(gha_runner_arch("aarch64"), "ARM64");
assert_eq!(gha_runner_arch("arm64"), "ARM64");
assert_eq!(gha_runner_arch("x86"), "X86");
assert_eq!(gha_runner_arch("i686"), "X86");
assert_eq!(gha_runner_arch("arm"), "ARM");
}
#[test]
fn gha_runner_arch_upcases_unknown_arch() {
// Forward-compatible: a new arch we haven't mapped is upcased, not
// mis-guessed.
assert_eq!(gha_runner_arch("riscv64"), "RISCV64");
}
#[test]
fn arch_of_takes_the_first_segment() {
assert_eq!(arch_of("aarch64-apple-darwin"), "aarch64");
assert_eq!(arch_of("x86_64-unknown-linux-musl"), "x86_64");
assert_eq!(arch_of("nodashes"), "nodashes");
}
// ── Platform / PlatformSpec composition (R531-F2) ───────────────────────
#[test]
fn compose_prefers_declared_target_over_triple_field() {
let spec = PlatformSpec {
target: Some("x86_64-unknown-linux-musl".into()),
container_platform: None,
native: false,
};
let p = Platform::compose("aarch64-apple-darwin", Some(&spec), Some("legacy-triple"));
assert_eq!(p.target.as_deref(), Some("x86_64-unknown-linux-musl"));
assert_eq!(p.host, "aarch64-apple-darwin");
}
#[test]
fn compose_falls_back_to_triple_field_when_undeclared() {
// package-native-tarball back-compat: a step with no `[platform]`
// block still lifts its legacy `triple` into the target.
let p = Platform::compose(
"aarch64-apple-darwin",
None,
Some("x86_64-unknown-linux-musl"),
);
assert_eq!(p.target.as_deref(), Some("x86_64-unknown-linux-musl"));
assert!(p.container_platform.is_none());
}
#[test]
fn compose_host_native_when_nothing_declared() {
let p = Platform::compose("aarch64-apple-darwin", None, None);
assert!(p.target.is_none());
assert!(!p.is_cross_arch(), "no target is never cross-arch");
}
#[test]
fn is_cross_arch_compares_arch_segment_only() {
// Same arch, different OS → not a cross *arch*.
let same_arch = Platform::compose(
"x86_64-unknown-linux-gnu",
None,
Some("x86_64-apple-darwin"),
);
assert!(!same_arch.is_cross_arch());
// Different arch → cross.
let cross = Platform::compose(
"aarch64-apple-darwin",
None,
Some("x86_64-unknown-linux-musl"),
);
assert!(cross.is_cross_arch());
}
#[test]
fn container_foreign_arch_catches_the_mesofact_case() {
// The W222 motivating failure: arm64 host pulling a linux/amd64
// toolchain image → foreign-arch container.
let spec = PlatformSpec {
target: Some("x86_64-unknown-linux-musl".into()),
container_platform: Some("linux/amd64".into()),
native: false,
};
let p = Platform::compose("aarch64-apple-darwin", Some(&spec), None);
assert!(p.container_is_foreign_arch());
// Same arch image on an amd64 host → not foreign.
let native = Platform::compose("x86_64-unknown-linux-gnu", Some(&spec), None);
assert!(!native.container_is_foreign_arch());
}
#[test]
fn docker_platform_arch_normalizes_os_arch_variant() {
assert_eq!(docker_platform_arch("linux/amd64"), Some("x86_64"));
assert_eq!(docker_platform_arch("linux/arm64/v8"), Some("aarch64"));
assert_eq!(docker_platform_arch("arm64"), Some("aarch64"));
assert_eq!(docker_platform_arch("linux/riscv64"), None);
}
#[test]
fn platform_spec_round_trips_through_toml() {
let spec = PlatformSpec {
target: Some("x86_64-unknown-linux-musl".into()),
container_platform: Some("linux/amd64".into()),
native: false,
};
let toml = toml::to_string(&spec).unwrap();
let back: PlatformSpec = toml::from_str(&toml).unwrap();
assert_eq!(spec, back);
}
#[test]
fn empty_platform_spec_serializes_to_nothing() {
// Both fields skip_serializing_if None → an all-default spec emits no
// keys, so a step that declares `platform = {}` stays inert.
let spec = PlatformSpec::default();
assert_eq!(toml::to_string(&spec).unwrap(), "");
}
// ── resolve() decision table (R531-F3) ──────────────────────────────────
const ARM_MAC: &str = "aarch64-apple-darwin";
const X64_LINUX: &str = "x86_64-unknown-linux-gnu";
#[test]
fn resolve_no_target_is_native() {
assert_eq!(resolve(ARM_MAC, None, None), Resolution::NativeCross);
}
#[test]
fn resolve_host_arch_target_is_native() {
// Same arch (different OS doesn't matter to this tier) → native.
assert_eq!(
resolve(ARM_MAC, Some("aarch64-unknown-linux-musl"), None),
Resolution::NativeCross
);
}
#[test]
fn resolve_foreign_linux_target_cross_compiles_natively() {
// The mesofact target itself, sans foreign container: zig cross-builds
// x86_64 musl from an arm64 mac with no emulation.
assert_eq!(
resolve(ARM_MAC, Some("x86_64-unknown-linux-musl"), None),
Resolution::NativeCross
);
}
#[test]
fn resolve_crossable_target_wins_over_foreign_container() {
// Cross-first: even though the recipe declares a foreign cross-rs
// image, a host-native crossable target resolves to NativeCross (the
// slow container is what P2/T6 should replace).
assert_eq!(
resolve(
ARM_MAC,
Some("x86_64-unknown-linux-musl"),
Some("linux/amd64")
),
Resolution::NativeCross
);
}
#[test]
fn resolve_foreign_container_with_non_crossable_emulates() {
// A non-crossable foreign target (windows-msvc) pulling a foreign-arch
// image on an arm64 host → Emulate, carrying the platform to pull.
// (For the *crossable* mesofact target the verdict is NativeCross —
// "use zigbuild, not the container" — see the test above.)
let r = resolve(ARM_MAC, Some("x86_64-pc-windows-msvc"), Some("linux/amd64"));
assert_eq!(
r,
Resolution::Emulate {
docker_platform: "linux/amd64".into()
}
);
}
#[test]
fn resolve_multiarch_image_build_with_no_target_emulates() {
// A buildx image job (no Rust target) pulling a foreign-arch image:
// no cross-compile for an image → Emulate.
assert_eq!(
resolve(ARM_MAC, None, Some("linux/amd64")),
Resolution::Emulate {
docker_platform: "linux/amd64".into()
}
);
}
#[test]
fn resolve_host_arch_container_is_not_emulation() {
// A host-arch image (no target) is just a native containerized build.
assert_eq!(
resolve(ARM_MAC, None, Some("linux/arm64")),
Resolution::NativeCross
);
}
#[test]
fn resolve_darwin_target_from_linux_host_offloads() {
// Can't host-native cross macOS off Linux, no container → needs a mac.
assert_eq!(
resolve(X64_LINUX, Some("aarch64-apple-darwin"), None),
Resolution::Offload {
target: "aarch64-apple-darwin".into()
}
);
}
#[test]
fn resolve_non_crossable_with_host_arch_container_uses_cross_docker() {
// macOS target off Linux, but a host-arch (linux/amd64) cross toolchain
// image is declared → CrossDocker (genuinely emulation-free).
assert_eq!(
resolve(X64_LINUX, Some("aarch64-apple-darwin"), Some("linux/amd64")),
Resolution::CrossDocker
);
}
#[test]
fn resolve_windows_msvc_off_linux_offloads_but_gnu_cross_compiles() {
// -msvc needs MSVC → offload; -gnu zig-cross-compiles → native.
assert_eq!(
resolve(X64_LINUX, Some("aarch64-pc-windows-msvc"), None),
Resolution::Offload {
target: "aarch64-pc-windows-msvc".into()
}
);
assert_eq!(
resolve(X64_LINUX, Some("aarch64-pc-windows-gnu"), None),
Resolution::NativeCross
);
}
#[test]
fn resolve_unknown_foreign_arch_with_no_container_skips() {
// An unrecognizable arch we can't name a runner for → Skip with reason.
let r = resolve(X64_LINUX, Some("sparc64-unknown-linux-gnu"), None);
// sparc64 linux is technically zig-crossable per our coarse rule
// (linux ⇒ true), so this resolves NativeCross — assert that, and use a
// genuinely unknown-OS triple for the Skip path below.
assert_eq!(r, Resolution::NativeCross);
let skip = resolve(X64_LINUX, Some("mos-unknown-none"), None);
match skip {
Resolution::Skip { reason } => {
assert!(reason.contains("mos-unknown-none"), "reason: {reason}");
}
other => panic!("expected Skip, got {other:?}"),
}
}
/// Exhaustive sweep: every (host, target, container) class combination maps
/// to exactly one Resolution and the function never panics. This is the
/// decision-table-as-spec guarantee — totality over the input space.
#[test]
fn resolve_is_total_over_the_class_space() {
let hosts = [ARM_MAC, X64_LINUX, "x86_64-pc-windows-msvc"];
let targets = [
None,
Some("x86_64-unknown-linux-musl"),
Some("aarch64-unknown-linux-gnu"),
Some("aarch64-apple-darwin"),
Some("x86_64-pc-windows-msvc"),
Some("mos-unknown-none"),
Some(""),
];
let containers = [
None,
Some("linux/amd64"),
Some("linux/arm64"),
Some("linux/riscv64"),
];
for h in hosts {
for t in targets {
for c in containers {
// Just exercising every cell — the assertion is "doesn't
// panic and returns a value"; specific cells are pinned by
// the named tests above.
let _r = resolve(h, t, c);
}
}
}
}
// ── derive_placement() native policy (R590-F4) ─────────────────────────
#[test]
fn native_false_defers_to_the_full_decision_table() {
// native=false must be byte-identical to resolve() across the board:
// the mesofact target still cross-compiles, a foreign container still
// emulates a non-crossable target.
assert_eq!(
derive_placement(ARM_MAC, Some("x86_64-unknown-linux-musl"), None, false),
resolve(ARM_MAC, Some("x86_64-unknown-linux-musl"), None),
);
assert_eq!(
derive_placement(ARM_MAC, Some("x86_64-pc-windows-msvc"), Some("linux/amd64"), false),
resolve(ARM_MAC, Some("x86_64-pc-windows-msvc"), Some("linux/amd64")),
);
}
#[test]
fn native_foreign_arch_offloads_instead_of_cross_compiling() {
// The rusty-v8-musl forcing case: x86_64 musl from an arm64 mac. Without
// native this is NativeCross (zig); WITH native it MUST Offload to the
// x86 build-worker (the C++ build can't cross/emulate here).
assert_eq!(
derive_placement(ARM_MAC, Some("x86_64-unknown-linux-musl"), None, true),
Resolution::Offload {
target: "x86_64-unknown-linux-musl".into()
}
);
}
#[test]
fn native_forces_past_the_foreign_container_emulate_branch() {
// A foreign container_platform would send resolve() to Emulate (branch
// 2). native=true overrides that — no emulation, offload to real silicon.
assert_eq!(
derive_placement(
ARM_MAC,
Some("x86_64-unknown-linux-musl"),
Some("linux/amd64"),
true
),
Resolution::Offload {
target: "x86_64-unknown-linux-musl".into()
}
);
}
#[test]
fn native_host_platform_target_builds_locally() {
// Same arch AND same OS → the native build runs right here; no offload
// even under native=true. (`-musl` vs `-gnu` is a libc tail, not an OS.)
assert_eq!(
derive_placement(ARM_MAC, Some("aarch64-apple-darwin"), None, true),
Resolution::NativeCross
);
// The x86 build-worker running its own x86 musl build: local native.
assert_eq!(
derive_placement(X64_LINUX, Some("x86_64-unknown-linux-musl"), None, true),
Resolution::NativeCross
);
}
/// R577-F2: `native = true` means "real machine of the target platform",
/// and a platform is (arch, OS). Same-arch/different-OS used to resolve
/// NativeCross, which put `desktop-release`'s `aarch64-unknown-linux-gnu`
/// row on this camp's arm64 Mac — a `cargo tauri build` for a Linux bundle
/// against macOS instead of an offload to a Pi5.
#[test]
fn native_same_arch_foreign_os_offloads() {
// The live case: the Linux rows of desktop-release, from an arm64 Mac.
assert_eq!(
derive_placement(ARM_MAC, Some("aarch64-unknown-linux-gnu"), None, true),
Resolution::Offload {
target: "aarch64-unknown-linux-gnu".into()
}
);
assert_eq!(
derive_placement(ARM_MAC, Some("aarch64-unknown-linux-musl"), None, true),
Resolution::Offload {
target: "aarch64-unknown-linux-musl".into()
}
);
// The mirror: the darwin row from an arm64 Linux coordinator, which is
// exactly what must reach us-west-015 (R631 tags it `os:darwin`).
assert_eq!(
derive_placement(
"aarch64-unknown-linux-gnu",
Some("aarch64-apple-darwin"),
None,
true
),
Resolution::Offload {
target: "aarch64-apple-darwin".into()
}
);
}
/// A container brings its own userland, so the OS dimension must NOT apply
/// to an image build: `yah qed images build --platform linux/arm64` on an
/// arm64 Mac builds locally through Colima's Linux VM. Only the arch has to
/// match there.
#[test]
fn native_container_step_is_not_os_constrained() {
assert_eq!(
derive_placement(
ARM_MAC,
Some("aarch64-unknown-linux-musl"),
Some("linux/arm64"),
true
),
Resolution::NativeCross
);
}
/// An unrecognized-OS triple has no build-worker tag to route to, so it
/// keeps the arch-only verdict rather than offloading into the void.
#[test]
fn native_unknown_os_target_does_not_offload_on_os_alone() {
assert_eq!(
derive_placement(X64_LINUX, Some("x86_64-unknown-none"), None, true),
Resolution::NativeCross
);
}
#[test]
fn native_absent_or_empty_target_builds_locally() {
assert_eq!(
derive_placement(ARM_MAC, None, None, true),
Resolution::NativeCross
);
assert_eq!(
derive_placement(ARM_MAC, Some(" "), None, true),
Resolution::NativeCross
);
}
// ── resolve_placement() = Derivation ∘ Capability (R555-B10, W235 §6) ────
//
// Every case below is driven by a hand-built ToolAvailability, never a real
// probe: the point of taking Capability as an argument is that the whole
// table is specified without touching a toolchain.
use crate::nativecross::ToolAvailability;
const NO_ZIG: ToolAvailability = ToolAvailability {
zigbuild: false,
musl_cross: false,
};
#[test]
fn capability_full_is_pure_derivation() {
// With every toolchain present, the composition must be byte-identical
// to the derivation table — no demotion anywhere in the space the
// native-policy tests above cover.
for (host, target, container, native) in [
(ARM_MAC, Some("x86_64-unknown-linux-musl"), None, false),
(ARM_MAC, Some("x86_64-unknown-linux-gnu"), None, false),
(ARM_MAC, Some("aarch64-unknown-linux-gnu"), None, false),
(ARM_MAC, Some("x86_64-unknown-linux-musl"), None, true),
(ARM_MAC, Some("aarch64-apple-darwin"), None, true),
(X64_LINUX, Some("aarch64-apple-darwin"), None, false),
(ARM_MAC, None, None, false),
(
ARM_MAC,
Some("x86_64-pc-windows-msvc"),
Some("linux/amd64"),
false,
),
] {
assert_eq!(
resolve_placement(host, target, container, native, &ToolAvailability::FULL),
derive_placement(host, target, container, native),
"FULL capability must not perturb {host} → {target:?}"
);
}
}
/// The measured incident (qed run 690455c1, 2026-08-19): `mesofact-build`'s
/// gnu legs derive NativeCross from this arm64 Mac — the foreign-arch one
/// through `resolve` branch 1, the same-arch/foreign-OS one through branch 4
/// — and both need `cargo-zigbuild`. Without it they must reach the idle
/// build-worker, not hard-fail the release.
#[test]
fn missing_zigbuild_demotes_native_cross_to_offload() {
for target in ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] {
assert_eq!(
derive_placement(ARM_MAC, Some(target), None, false),
Resolution::NativeCross,
"{target} derives NativeCross (that is the defect's premise)"
);
assert_eq!(
resolve_placement(ARM_MAC, Some(target), None, false, &NO_ZIG),
Resolution::Offload {
target: target.into()
}
);
}
}
/// The demotion asks the same question the *execution* path asks, so it
/// honors the same fallback ladder: a musl target still builds here off a
/// musl-cross toolchain when zig is absent, and only a target no installed
/// tool can carry leaves the box.
#[test]
fn musl_cross_fallback_is_not_a_capability_gap() {
let musl_only = ToolAvailability {
zigbuild: false,
musl_cross: true,
};
assert_eq!(
resolve_placement(
ARM_MAC,
Some("x86_64-unknown-linux-musl"),
None,
false,
&musl_only
),
Resolution::NativeCross
);
// …while the gnu leg of the same pipeline has no such fallback.
assert_eq!(
resolve_placement(
ARM_MAC,
Some("x86_64-unknown-linux-gnu"),
None,
false,
&musl_only
),
Resolution::Offload {
target: "x86_64-unknown-linux-gnu".into()
}
);
}
#[test]
fn host_platform_target_never_demotes() {
// A plain host build needs no foreign toolchain, so an empty
// ToolAvailability is irrelevant to it — demoting here would ship a
// five-second local build to the fleet for no reason at all.
for (host, target) in [
(ARM_MAC, Some("aarch64-apple-darwin")),
(ARM_MAC, None),
(ARM_MAC, Some(" ")),
(X64_LINUX, Some("x86_64-unknown-linux-gnu")),
// darwin↔darwin cross-arch: Apple's SDK ships both slices, so
// `select_cross_tool` answers CargoNative without zig.
(ARM_MAC, Some("x86_64-apple-darwin")),
] {
assert_eq!(
resolve_placement(host, target, None, false, &NO_ZIG),
Resolution::NativeCross,
"{host} → {target:?} must not demote"
);
}
}
/// A declared container carries its own toolchain and the host probe can see
/// nothing inside it, so a missing host tool is not evidence the step cannot
/// build here. `mesofact-musl`'s arm64 leg is the live case.
#[test]
fn container_step_is_never_demoted() {
assert_eq!(
resolve_placement(
ARM_MAC,
Some("aarch64-unknown-linux-musl"),
Some("linux/arm64"),
true,
&NO_ZIG
),
Resolution::NativeCross
);
}
/// Only NativeCross is demotable: the other tiers are not claims about a
/// host cross toolchain, so a capability gap must leave them exactly alone
/// (an Emulate turned Offload would route around R560's emulation gate).
#[test]
fn other_tiers_are_not_demotable() {
// Emulate: foreign-arch container, non-crossable target.
assert!(matches!(
resolve_placement(
ARM_MAC,
Some("x86_64-pc-windows-msvc"),
Some("linux/amd64"),
false,
&NO_ZIG
),
Resolution::Emulate { .. }
));
// Offload stays Offload (the derived kind, with its own target).
assert_eq!(
resolve_placement(X64_LINUX, Some("aarch64-apple-darwin"), None, false, &NO_ZIG),
Resolution::Offload {
target: "aarch64-apple-darwin".into()
}
);
// Skip stays Skip — no toolchain question was ever asked of it.
assert!(matches!(
resolve_placement(ARM_MAC, Some("mos-unknown-none"), None, false, &NO_ZIG),
Resolution::Skip { .. }
));
}
/// `capability_demotion` is what the CLI notice and the runner preflight
/// read, so it must agree with `resolve_placement` exactly — and it must
/// carry the missing tool's install hint, because W235 §6's non-goal is a
/// silent demotion.
#[test]
fn capability_demotion_names_the_missing_tool_and_matches_the_verdict() {
let target = "x86_64-unknown-linux-gnu";
let derived = derive_placement(ARM_MAC, Some(target), None, false);
let gap = capability_demotion(ARM_MAC, Some(target), None, &derived, &NO_ZIG)
.expect("a zigbuild-less mac cannot carry the gnu leg");
assert_eq!(gap.target, target);
assert_eq!(gap.preferred, crate::nativecross::CrossTool::CargoZigbuild);
let rendered = gap.to_string();
assert!(
rendered.contains("cargo install cargo-zigbuild"),
"the demotion must name the install command, got: {rendered}"
);
// Agreement with the routing verdict, in both directions.
assert_eq!(
resolve_placement(ARM_MAC, Some(target), None, false, &NO_ZIG),
Resolution::Offload {
target: target.into()
}
);
assert!(
capability_demotion(
ARM_MAC,
Some(target),
None,
&derived,
&ToolAvailability::FULL
)
.is_none()
);
}
/// An unrecognized target arch has no build-worker tag to name, so it keeps
/// the derived verdict rather than offloading into the void — the same
/// discipline as `resolve`'s branch 3. `powerpc64` is known and crossable
/// (linux), so it demotes; a made-up arch does not.
#[test]
fn unknown_arch_is_not_offloaded_into_the_void() {
assert_eq!(
resolve_placement(ARM_MAC, Some("powerpc64-unknown-linux-gnu"), None, false, &NO_ZIG),
Resolution::Offload {
target: "powerpc64-unknown-linux-gnu".into()
}
);
assert_eq!(
resolve_placement(ARM_MAC, Some("fictional-unknown-linux-gnu"), None, false, &NO_ZIG),
Resolution::NativeCross
);
}
#[test]
fn native_spec_round_trips_through_toml() {
let spec = PlatformSpec {
target: Some("x86_64-unknown-linux-musl".into()),
container_platform: None,
native: true,
};
let toml = toml::to_string(&spec).unwrap();
assert!(toml.contains("native = true"), "toml: {toml:?}");
let back: PlatformSpec = toml::from_str(&toml).unwrap();
assert_eq!(spec, back);
assert!(back.native);
}
// ── preflight rendering (R531-T4) ────────────────────────────────────────
#[test]
fn resolution_labels_carry_the_cost_parenthetical() {
assert_eq!(
Resolution::NativeCross.label(),
"NativeCross (cargo-zigbuild)"
);
assert_eq!(
Resolution::CrossDocker.label(),
"CrossDocker (cross-rs container)"
);
assert_eq!(
Resolution::Emulate {
docker_platform: "linux/amd64".into()
}
.label(),
"Emulate (QEMU linux/amd64, slow)"
);
assert_eq!(
Resolution::Offload {
target: "aarch64-apple-darwin".into()
}
.label(),
"Offload (needs aarch64-apple-darwin runner)"
);
}
#[test]
fn is_slow_or_unsatisfiable_partitions_the_tiers() {
assert!(!Resolution::NativeCross.is_slow_or_unsatisfiable());
assert!(!Resolution::CrossDocker.is_slow_or_unsatisfiable());
assert!(Resolution::Emulate {
docker_platform: "linux/amd64".into()
}
.is_slow_or_unsatisfiable());
assert!(Resolution::Offload { target: "x".into() }.is_slow_or_unsatisfiable());
assert!(Resolution::Skip { reason: "x".into() }.is_slow_or_unsatisfiable());
}
#[test]
fn preflight_line_matches_w222_format() {
// The motivating example from W222, verbatim shape.
let p = Platform::compose(ARM_MAC, None, Some("x86_64-unknown-linux-musl"));
let r = resolve(
&p.host,
p.target.as_deref(),
p.container_platform.as_deref(),
);
let line = preflight_line("mesofact-dev-build", &p, &r);
assert_eq!(
line,
"mesofact-dev-build · targets x86_64-unknown-linux-musl · \
host aarch64-apple-darwin · resolution = NativeCross (cargo-zigbuild)"
);
}
#[test]
fn preflight_line_describes_image_builds_and_host_native() {
let img = Platform {
host: ARM_MAC.into(),
target: None,
container_platform: Some("linux/amd64".into()),
};
let r = resolve(&img.host, None, img.container_platform.as_deref());
assert!(preflight_line("image-yah-base", &img, &r).contains("builds linux/amd64 image"),);
let native = Platform::compose(ARM_MAC, None, None);
let rn = resolve(&native.host, None, None);
assert!(preflight_line("check", &native, &rn).contains("· host-native ·"));
}
#[test]
fn host_native_crossable_spec() {
// Linux from anywhere.
assert!(host_native_crossable(ARM_MAC, "x86_64-unknown-linux-musl"));
assert!(host_native_crossable(
X64_LINUX,
"aarch64-unknown-linux-gnu"
));
// Windows gnu yes, msvc no.
assert!(host_native_crossable(X64_LINUX, "x86_64-pc-windows-gnu"));
assert!(!host_native_crossable(X64_LINUX, "x86_64-pc-windows-msvc"));
// Darwin only from a darwin host.
assert!(host_native_crossable(ARM_MAC, "x86_64-apple-darwin"));
assert!(!host_native_crossable(X64_LINUX, "aarch64-apple-darwin"));
// Unknown OS never.
assert!(!host_native_crossable(X64_LINUX, "mos-unknown-none"));
}
#[test]
fn host_arch_round_trips_through_gha_vocabulary() {
// The host we detect must always normalize to a non-empty GHA arch.
let host = detect_host_triple();
let gha = gha_runner_arch(arch_of(&host));
assert!(!gha.is_empty(), "host {host:?} → gha arch {gha:?}");
}
}