//! 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)
//! @arch:see(.yah/infra/machines/us-west-015.toml)
//!
//! @yah:relay(R969, "QED opportunistic offload: use a fleet build worker when one answers, else run locally (noisetable release parallelism)")
//! @yah:at(2026-10-10T23:04:01Z)
//! @yah:assignee()
//! @yah:next("Filed from noisetable (operator call 2026-10-10, Ashguard session:8c2f4d3c). Today a step either stays local (cross-first ladder) or MUST offload (`platform.native = true`, or `node = \"<machine>\"`), and the must-forms fail when no candidate answers. `elect_node` (R605-T14) already walks the interchangeable pool probing /node/usage then /health, so 002→003 failover works; what is missing is (a) a soft form that falls back to local and (b) a way to target the MSVC-over-WSL capability by tag rather than by node pin. Consumer: noisetable `.yah/qed/release.toml` + desktop.build-{linux,windows}.toml, where the Linux and Windows legs should land on us-west-003 / us-west-002 when up and still run (Mac docker / cargo-xwin) when not.")
//! @arch:see(.yah/docs/working/W325-isolated-x86-build-capacity.md)
//! @arch:see(.yah/docs/working/W352-windows-and-macos-build-targets.md)
//! @yah:gotcha("DILIGENCE 2026-10-10 (Glimmerstone leader, at operator request; relay filed from noisetable). Verdict: needs-revision; children revised in place. Hard: (1) noisetable desktop.build-linux needs docker and us-west-003 has none (us-west-003.toml:52-54), so the motivating Linux leg cannot land on 003 whatever F1 does; (2) F2's tag-only plan does not compose: offload tags derive only from platform.target, and an msvc target emits os:windows, which no node carries. Soft: F1's election is private behind WardenClient::deploy and never reports none-answered; effective_placement is pure and read at 6+ sites; F3 reverses elect_node's documented warm-affinity rule and has no reservation substrate or step resource ask. DAG serialized F1 -> F2 -> F3 -> F4 (all edit qed runner.rs/types.rs).")
//! @yah:next("S5, F6 and F7 were filed from noisetable AFTER the 2026-10-10 diligence pass and have NOT been diligenced. The operator asked for diligence before ANY dispatch on noisetable-filed R969 tickets. Next leader: run /diligence on them first. S5 comes first by ID; it is a spike that may reshape F6. Known overlaps: F6's retry worker-affinity must extend F3's PlacementLedger warm affinity (last_node, AFFINITY_MARGIN in app/yah/cli/src/yubaba_client.rs), not add a second mechanism; F6 cites F4's land_produced_artifact helper (runner.rs); F7 reuses qed.await's needs_agent terminal kit. F6/F7 soft-depend on R971-F5, a different relay live under @Glimmerstone (session:49c96966); read it before cutting.")
//! @yah:next("Open operator calls, asked in chat 2026-10-10 and unanswered: (1) install docker on us-west-003 so noisetable's desktop.build-linux can offload; (2) provision cargo-xwin + clang-cl on us-west-003 for F2's fleet fallback row; (3) whether yah-cli-release-windows (still node = us-west-002) moves to target + prefer_native + an xwin fallback, which ships a cross-built release artifact whenever 002 sleeps. Live verification of F1-F4 needs the operator's desktop build + relaunch and an awake 002/003. F3 carries one @yah:cleanup residual (release the election hold on a pre-deploy failure).")
//! @yah:handoff("LEADER Glimmerstone (session:898072a5), 2026-10-10: F1-F4 delivered to review. All uncommitted (git policy defer), each re-verified by a leader re-run (counts are on each child's @yah:verify). The diligence verdict and the revisions it drove are recorded on R969's gotcha and on each child's next/gotcha. Leader stopped at context wind-down (292k fill), not on a blocker.")
//! @yah:gotcha("DILIGENCE 2026-10-13 on S5/F6/F7 (Ashguard leader session:30775128, done at operator request before dispatch). Verdict: needs-revision, all three revised in place. S5 (Hard): a reattach path already exists (R603, camp.rs reconcile_inflight_qed_runs :9689), so the spike is rescoped to six concrete gaps, including cancel leaking the forge and a mid-pipeline reattach failing the run. F6 (Hard): delta shipping already exists (R971-F5 git snapshots), as do the warm target (forge_cache bind) and soft affinity, so it is reduced to a qed.retry camp verb, hard same-node affinity, and a check that CARGO_TARGET_DIR lands in $YAH_CACHE_DIR. F7 (Hard): needs_agent is a manual-gate kit, failed forges are reaped, and yubaba has no exec API, so it was redesigned as a debug-forge re-deploy, a new yubaba exec endpoint, and a git return ref; it depends on F6.")
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" }
/// ```
///
/// @yah:ticket(R969-F1, "PlatformSpec soft offload: prefer a matching build worker, fall back to the local ladder when none answers")
/// @yah:status(review)
/// @yah:at(2026-10-11T00:03:47Z)
/// @yah:assignee(agent:bundle-anthropic-glimmerstone)
/// @yah:parent(R969)
/// @yah:next("Add a declarative preference beside `native` (e.g. an offload mode enum: never | prefer | require; `native = true` == require) so a step that CAN cross/emulate locally still goes to an arch+os-matched build worker when one is live. Election reuses `admit_workload_candidates` + `elect_node`'s /node/usage→/health probe; if every candidate refuses, the step runs on the coordinator via the existing cross-first ladder instead of failing. Record where it actually ran in the step result so `qed.status` shows offloaded vs fell-back.")
/// @yah:next("Placement is decided at step start, not run start, so a node that wakes mid-run is used by later steps.")
/// @yah:next("Wire `--where=local` to suppress it and `--where=remote` to treat it as require, consistent with today's flag semantics.")
/// @yah:next("DILIGENCE 2026-10-10 (leader): seam the ticket did not name. `elect_node` (app/yah/cli/src/yubaba_client.rs:243) is private to MeshYubabaClient and reached only through `WardenClient::deploy` (oss/qed/crates/velveteen-exec/src/remote.rs). It never reports 'none answered': by documented design it returns the unprobed head (yubaba_client.rs:238-242, 297-303). Prefer needs an explicit election verb on WardenClient (e.g. `elect(&WorkloadSpec) -> Result<Option<node>>`) that the runner calls at step start; require keeps today's attempt-the-head behaviour. Update the test_support Scripted/Hanging clients.")
/// @yah:next("`effective_placement` (oss/qed/crates/qed/src/runner.rs:2912) is pure and read at >=6 sites (2455 resolve_runtime, 3134, 3472, 3748, 4153, 7738) plus pre-run `pipeline_needs_offload` (runner.rs:448). Elect ONCE per step at step start, memoize it, and make every site read the memo; never probe inside effective_placement. The runtime default follows the memo (Remote -> Container / native-userland rule at runner.rs:2455-2460; Local -> Native).")
/// @yah:next("Dispatcher wiring: `pipeline_needs_offload` must count prefer steps so Auto runs stand up a mesh client (camp.rs:11286/11367/11402, qed.rs:1475/1998), but a failed `build_mesh_yubaba_client` or missing cloud config must degrade prefer steps to local, not fail the run. `--where=node:<name>` (CliPlacement::Node, qed.rs:822) still pins.")
/// @yah:next("Record placement on StepStatus (oss/qed/crates/qed/src/types.rs `pub struct StepStatus`, no such field today): e.g. `placement: Option<StepPlacement>` = Offloaded{node} | Local | FellBack{tried}, serde(default), rendered by qed.status.")
/// @yah:verify("Unit (qed runner, ScriptedWardenClient): prefer-step with zero live candidates runs locally and records FellBack; with one live candidate dispatches there and records Offloaded{node}; require-step (native = true) with zero candidates still fails; --where=local keeps a prefer step local; --where=remote forces it remote.")
/// @yah:verify("Unit (yah cli, mock yubaba as in yubaba_client.rs tests ~L976): the election verb returns None when no candidate answers, while deploy_attempts_the_head_when_no_candidate_answers stays green.")
/// @yah:verify("Consumer: NOT noisetable desktop.build-linux (see gotcha). A yah-side prefer step with target x86_64-unknown-linux-gnu lands on an x86 build worker when one answers (002 sorts first, then 003) and runs locally when none does.")
/// @yah:gotcha("DILIGENCE 2026-10-10: the noisetable consumer cannot land on us-west-003. Its build step runs scripts/build-desktop-linux.sh, which requires docker (`docker run --platform linux/amd64`, `docker volume create`), and .yah/infra/machines/us-west-003.toml:52-54 records docker NOT installed (docker LV removed 2026-08-29). With 002 and 003 both live, first-fit elects 002, not 003. Putting that leg on the fleet is an operator infra call (docker on a build worker) or a non-docker mode in noisetable's script, not F1 code.")
/// @yah:handoff("LANDED (uncommitted, @Ashguard:eclipse session:6845fbb6, 2026-10-10). DESIGN: new `PlatformSpec.prefer_native: bool` (platform.rs, beside `native`) + `PlatformSpec::prefers_offload(host,target)` = prefer_native && !native && derive_placement(..,native=true) is Offload. Kept a second bool instead of a never/prefer/require enum on purpose: `native` also carries the real-userland meaning remote_step_needs_native_userland reads, which a preference must not inherit; `native = true` stays the require form, so no recipe migration. resolve_placement is untouched for prefer steps - the local fallback IS the ordinary ladder.")
/// @yah:handoff("ELECTION SEAM: `WardenClient::elect(&self, mesh_tags:&[String]) -> Result<Election>` (velveteen-exec remote.rs) with default impl = never elects (a client that can't probe leaves prefer steps local; this is why test_support Scripted/Hanging clients needed no change). New `Election{node:Option<String>, tried:Vec<String>}`, `selector_workload_spec(mesh_tags)` (one builder for the selector annotation), `RemoteForgeDriver::elect`; all re-exported from velveteen_exec. MeshYubabaClient (app/yah/cli/src/yubaba_client.rs): probe walk extracted into `probe_pool` -> PoolProbe; elect_node keeps its unprobed-head fallback, the new `elect` impl reports None + every candidate tried (incl. unroutable).")
/// @yah:handoff("RUNNER (oss/qed/crates/qed/src/runner.rs): memo field `step_elections: Mutex<HashMap<step name, StepPlacement>>` on PipelineRunner (4 constructors). `elect_step_placement(step).await` runs ONCE at step start in the main loop and the finally loop, right before resolve_runtime; skipped for non-prefer steps and when step.node / --where=node: pin / node-bound participant already place it. Auto+driver -> elect -> Offloaded{node} (pinned via remote_location, after step pin and run pin) or FellBack{tried}; Auto w/o driver -> FellBack{[]}; Local -> records Local; Remote -> no election. effective_placement's Auto arm reads the memo (never probes), so resolve_runtime/dispatch/fleet sites all agree. pipeline_needs_offload now counts prefer steps (soft flag threaded through needs_offload_walk); new `pipeline_requires_offload` (hard-only), exported from lib.rs.")
/// @yah:handoff("RECORD: types.rs `StepStatus.placement: Option<StepPlacement>` (serde default/skip none) + `enum StepPlacement { Offloaded{node:Option<String>}, Local, FellBack{tried} }` (serde tag=kind, snake_case) with Display; set via `recorded_placement` on executed main + finally rows (memo, else Offloaded for any Remote step naming the pin when known; None for ordinary local). Rendered: rpc QedStepWire.placement: Option<String> (crates/yah/rpc/src/lib.rs) filled in camp.rs qed_step_to_wire -> qed.status; both CLI step reports in qed.rs print `(offloaded to X)` / `(fell back to local (...))`.")
/// @yah:handoff("DISPATCHER DEGRADE: qed.rs run_one_in_process, camp.rs single-run (needs_fleet arm) and matrix fanout: a failed build_mesh_yubaba_client / build_fleet_client is propagated ONLY when forced remote or pipeline_requires_offload; otherwise warn + driverless arm, prefer steps run local. CliPlacement::Node still pins (forced arm). Regenerated .yah/schema/qed-pipeline.toml.schema.json (+4 lines, prefer_native). 30 StepStatus/PlatformSpec literals across qed + camp.rs + qed_images.rs gained the new field.")
/// @yah:handoff("TESTS: yah-qed --lib 1060 passed/0 failed/1 ignored (new: runner::tests::{a_prefer_step_wants_a_dispatcher_but_does_not_require_one, prefer_step_with_no_live_candidate_runs_locally_and_records_fell_back, prefer_step_with_a_live_candidate_dispatches_there_and_records_offloaded, require_step_with_no_live_candidate_still_fails, where_local_keeps_a_prefer_step_local, where_remote_forces_a_prefer_step_remote}, platform::tests::prefer_native_round_trips_and_only_prefers_a_foreign_target). velveteen-exec --lib 136 passed/0 failed/2 ignored (new selector_spec_carries_mesh_tags_and_default_election_is_none). yah --lib yubaba_client 18/0 (new elect_returns_none_when_no_candidate_answers, elect_names_the_first_live_candidate; deploy_attempts_the_head_when_no_candidate_answers green). yah --lib qed:: 16/0. CAVEAT: no pre-edit baseline was recorded (courier miss) - counts are after-only; no failures anywhere.")
/// @yah:handoff("LEFT / GOTCHAS: (1) the election .await runs in the admission loop, so a prefer step's probe (per-node probe timeout x pool size) briefly stalls admission of sibling steps - acceptable for the 2-node x86 pool, revisit if pools grow. (2) memo keyed by step name; matrix-expanded rows are separate runners so fine. (3) Consumer verify (a real x86 prefer step landing on 002/003) not run - needs a live fleet + a yah-side recipe declaring prefer_native; no in-tree recipe uses it yet. (4) Needs desktop build + relaunch for the daemon-side degrade/qed.status rendering to reach live binaries. (5) build-image remote path uses remote_location too, so an elected pin applies there, but its own mesh tags (remote_build_image_arch) differ from remote_subprocess_mesh_tags used for election - untested for build-image prefer steps.")
/// @yah:handoff("LEADER SIGN-OFF REQUEST (Glimmerstone, 2026-10-10): courier @Ashguard:eclipse (session:6845fbb6) landed prefer_native + WardenClient::elect + per-step election memo + StepStatus.placement + prefer-only dispatcher degrade, uncommitted (git policy defer). Design accepted as shipped: `prefer_native` is a second bool beside `native`, not a never/prefer/require enum, because `native` also carries the real-userland meaning (runner.rs remote_step_needs_native_userland) that a preference must not inherit. Full account in the courier handoff above.")
/// @yah:verify("Leader re-run 2026-10-10: cargo test --manifest-path oss/qed/Cargo.toml -p yah-qed --lib 1060 pass/0 fail/1 ignored; -p velveteen-exec --lib 136/0/2 ignored; cargo test -p yah --lib yubaba_client 18/0; -p yah --lib qed:: 16/0. Matches the courier's counts. Skew advisory: a peer modified oss/qed/crates/qed/src/build_context.rs (not an F1 file) during the run. Not yet run: live-fleet consumer check (a yah-side prefer_native recipe landing on 002/003), plus a desktop build + relaunch to reach the live daemon.")
#[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,
/// R969-F1: the soft form of [`native`](Self::native). When `true`, a step
/// whose target is foreign to this host (arch, or OS for a bare-userland
/// build — the same test `native` applies) *prefers* a matching build
/// worker: at step start the runner asks the dispatcher to elect a live
/// one, and dispatches there if one answers. If none does — or no
/// dispatcher is wired — the step keeps its ordinary cross-first ladder
/// here instead of failing. Placement resolution is otherwise untouched:
/// [`resolve_placement`] still answers the local verdict, which is exactly
/// what the fallback runs. `native = true` wins when both are set (it is
/// the hard form). `--where=local` suppresses the preference;
/// `--where=remote` forces every step remote anyway. On the TOML side:
/// `platform = { target = "x86_64-unknown-linux-gnu", prefer_native = true }`.
///
/// Kept a second bool rather than folding `native` into a
/// never/prefer/require enum: `native` also carries the "real userland"
/// meaning `remote_step_needs_native_userland` reads, which a preference
/// must not inherit (a preferred step falls back to a local *cross*).
#[serde(default, skip_serializing_if = "is_false")]
pub prefer_native: bool,
}
impl PlatformSpec {
/// R969-F1: does this declaration ask for a soft offload on `host`? True
/// only for a `prefer_native` step that is not already hard-`native` and
/// whose target the hard form would send off-box — a host-platform target
/// has nothing to gain from a worker.
pub fn prefers_offload(&self, host: &str, target: Option<&str>) -> bool {
self.prefer_native
&& !self.native
&& matches!(
derive_placement(host, target, self.container_platform.as_deref(), true),
Resolution::Offload { .. }
)
}
}
/// `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/R969-F2: mesh tags that select a build-worker able to produce
/// `triple` natively. 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", "cap:…"]`, so an amd64 build routes to
/// `us-west-002` / `us-west-003` (x86) and an arm64 *Linux* build to an
/// arm/linux worker — 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 carried and a Linux node silently
/// won a job it could never satisfy — the OS dimension is what tells them apart.
///
/// **MSVC is a capability, not an OS (R969-F2).** A `*-pc-windows-msvc` target
/// derives `[tag:build-worker, arch:<arch>, cap:msvc]` instead of `os:windows`.
/// No node runs a Windows kernel *as a worker*: the MSVC box (`us-west-002`) is
/// a WSL2 Linux VM that drives `cargo.exe` across interop, so it is truthfully
/// `os:linux`, and painting `os:windows` on it would lie to every other
/// consumer of mesh_tags. `cap:msvc` states the fact placement needs — "this
/// node produces an MSVC PE natively" — independent of the host OS. A
/// `-windows-gnu` target keeps `os:windows` (it crosses from any host under
/// zig, so it never offloads in practice).
///
/// Takes the whole triple, not an `(arch, os)` pair, because the ABI suffix is
/// what decides it. Callers that only know an arch (catalog build-image steps,
/// which always produce Linux images) pass [`linux_triple`]`(arch)`.
/// yubaba admission consumes this set (see
/// `velveteen_exec::remote::NODE_SELECTOR_MESH_TAGS_ANNOTATION`).
pub fn build_worker_mesh_tags(triple: &str) -> Vec<String> {
let arch_tag = match arch_of(triple) {
"x86_64" | "x86" | "i686" | "amd64" => Some("arch:x86"),
"aarch64" | "arm64" | "arm" => Some("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).
_ => None,
};
let os_or_cap = if target_is_msvc(triple) {
MSVC_CAP_TAG.to_string()
} else {
format!("os:{}", os_tag_of(triple))
};
let mut tags = vec!["tag:build-worker".to_string()];
tags.extend(arch_tag.map(String::from));
tags.push(os_or_cap);
tags
}
/// R969-F2: the mesh tag a node carries when it produces MSVC PEs natively.
/// See [`build_worker_mesh_tags`].
pub const MSVC_CAP_TAG: &str = "cap:msvc";
/// The mesh tag a node carries when its kamaji runs native-exec workloads.
/// Mirrors `cloud::config::NATIVE_EXEC_MESH_TAG`, which yubaba admission
/// requires of any workload marked native-exec; qed cannot depend on `cloud`
/// (separate exported workspace), so the string is restated here and pinned by
/// `xtask/tests/fleet_build_placement.rs`, which runs both sides together.
pub const NATIVE_EXEC_CAP_TAG: &str = "cap:native-exec";
/// A Linux GNU triple for a bare arch token — the target a catalog
/// build-image step implicitly has (buildx only produces Linux images), so the
/// arch-only callers of [`build_worker_mesh_tags`] can speak its vocabulary.
pub fn linux_triple(arch: &str) -> String {
format!("{arch}-unknown-linux-gnu")
}
#[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-unknown-linux-gnu"),
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-unknown-linux-musl"),
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-apple-darwin");
let linux = build_worker_mesh_tags("aarch64-unknown-linux-gnu");
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("riscv64gc-unknown-linux-gnu"),
vec!["tag:build-worker".to_string(), "os:linux".to_string()]
);
}
#[test]
fn msvc_selects_by_capability_not_os() {
// R969-F2: no worker is os:windows; the MSVC box is a WSL2 Linux VM
// tagged cap:msvc.
assert_eq!(
build_worker_mesh_tags("x86_64-pc-windows-msvc"),
vec![
"tag:build-worker".to_string(),
"arch:x86".to_string(),
"cap:msvc".to_string()
]
);
// -gnu keeps the OS dimension: it is not an MSVC PE.
assert!(build_worker_mesh_tags("x86_64-pc-windows-gnu").contains(&"os:windows".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,
prefer_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,
prefer_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,
prefer_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,
prefer_native: false,
};
let toml = toml::to_string(&spec).unwrap();
assert!(toml.contains("native = true"), "toml: {toml:?}");
assert!(!toml.contains("prefer_native"), "toml: {toml:?}");
let back: PlatformSpec = toml::from_str(&toml).unwrap();
assert_eq!(spec, back);
assert!(back.native);
}
#[test]
fn prefer_native_round_trips_and_only_prefers_a_foreign_target() {
let back: PlatformSpec =
toml::from_str("target = \"x86_64-unknown-linux-gnu\"\nprefer_native = true\n").unwrap();
assert!(back.prefer_native && !back.native);
assert!(back.prefers_offload(ARM_MAC, back.target.as_deref()));
// A host-platform target gains nothing from a worker.
assert!(!back.prefers_offload(X64_LINUX, back.target.as_deref()));
// The hard form wins; a hard-native step is not a *preference*.
let hard = PlatformSpec { native: true, ..back.clone() };
assert!(!hard.prefers_offload(ARM_MAC, hard.target.as_deref()));
// Preference never changes the local verdict — the fallback IS it.
assert_eq!(
resolve_placement(ARM_MAC, back.target.as_deref(), None, back.native, &ToolAvailability::FULL),
resolve(ARM_MAC, back.target.as_deref(), None)
);
}
// ── 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:?}");
}
}