use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct PlatformSpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub container_platform: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub native: bool,
}
fn is_false(b: &bool) -> bool {
!*b
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Platform {
pub host: String,
pub target: Option<String>,
pub container_platform: Option<String>,
}
impl Platform {
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,
}
}
pub fn is_cross_arch(&self) -> bool {
match &self.target {
None => false,
Some(t) => arch_of(t) != arch_of(&self.host),
}
}
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)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Resolution {
NativeCross,
CrossDocker,
Emulate { docker_platform: String },
Offload { target: String },
Skip { reason: String },
}
impl Resolution {
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})"),
}
}
pub fn is_slow_or_unsatisfiable(&self) -> bool {
matches!(
self,
Resolution::Emulate { .. } | Resolution::Offload { .. } | Resolution::Skip { .. }
)
}
}
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(),
)
}
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);
if let Some(t) = foreign_target {
if host_native_crossable(host, t) {
return Resolution::NativeCross;
}
}
if let Some(p) = container_platform {
if docker_platform_arch(p) != Some(host_arch) {
return Resolution::Emulate {
docker_platform: p.to_string(),
};
}
}
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"
),
};
}
Resolution::NativeCross
}
pub fn resolve_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);
match target.map(str::trim).filter(|t| !t.is_empty()) {
Some(t) if arch_of(t) != host_arch => Resolution::Offload {
target: t.to_string(),
},
_ => Resolution::NativeCross,
}
}
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,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TargetOs {
Linux,
Windows { msvc: bool },
Darwin,
Unknown,
}
fn target_os(triple: &str) -> TargetOs {
if triple.contains("linux") {
TargetOs::Linux
} else if triple.contains("windows") {
TargetOs::Windows {
msvc: triple.ends_with("msvc"),
}
} else if triple.contains("darwin") || triple.contains("apple") {
TargetOs::Darwin
} else {
TargetOs::Unknown
}
}
fn is_known_arch(arch: &str) -> bool {
matches!(
arch,
"x86_64" | "aarch64" | "arm64" | "x86" | "i686" | "arm" | "riscv64" | "powerpc64" | "s390x"
)
}
pub fn docker_platform_arch(platform: &str) -> Option<&'static str> {
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,
}
}
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}")
}
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(),
}
}
pub fn arch_of(triple: &str) -> &str {
triple.split('-').next().unwrap_or(triple)
}
pub fn build_worker_mesh_tags(arch: &str) -> Vec<String> {
let arch_tag = match arch {
"x86_64" | "x86" | "i686" | "amd64" => "tier:x86",
"aarch64" | "arm64" | "arm" => "tier:arm",
_ => return vec!["tag:build-worker".into()],
};
vec!["tag:build-worker".into(), arch_tag.into()]
}
#[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"),
vec!["tag:build-worker".to_string(), "tier:x86".to_string()]
);
}
#[test]
fn arm64_selects_arm_build_worker() {
assert_eq!(
build_worker_mesh_tags("aarch64"),
vec!["tag:build-worker".to_string(), "tier:arm".to_string()]
);
}
#[test]
fn unknown_arch_falls_back_to_pool() {
assert_eq!(
build_worker_mesh_tags("riscv64"),
vec!["tag:build-worker".to_string()]
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_host_triple_is_a_wellformed_triple() {
let t = detect_host_triple();
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() {
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:?}"),
_ => {} }
}
#[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() {
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");
}
#[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() {
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() {
let same_arch = Platform::compose(
"x86_64-unknown-linux-gnu",
None,
Some("x86_64-apple-darwin"),
);
assert!(!same_arch.is_cross_arch());
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() {
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());
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() {
let spec = PlatformSpec::default();
assert_eq!(toml::to_string(&spec).unwrap(), "");
}
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() {
assert_eq!(
resolve(ARM_MAC, Some("aarch64-unknown-linux-musl"), None),
Resolution::NativeCross
);
}
#[test]
fn resolve_foreign_linux_target_cross_compiles_natively() {
assert_eq!(
resolve(ARM_MAC, Some("x86_64-unknown-linux-musl"), None),
Resolution::NativeCross
);
}
#[test]
fn resolve_crossable_target_wins_over_foreign_container() {
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() {
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() {
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() {
assert_eq!(
resolve(ARM_MAC, None, Some("linux/arm64")),
Resolution::NativeCross
);
}
#[test]
fn resolve_darwin_target_from_linux_host_offloads() {
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() {
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() {
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() {
let r = resolve(X64_LINUX, Some("sparc64-unknown-linux-gnu"), None);
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:?}"),
}
}
#[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 {
let _r = resolve(h, t, c);
}
}
}
}
#[test]
fn native_false_defers_to_the_full_decision_table() {
assert_eq!(
resolve_placement(ARM_MAC, Some("x86_64-unknown-linux-musl"), None, false),
resolve(ARM_MAC, Some("x86_64-unknown-linux-musl"), None),
);
assert_eq!(
resolve_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() {
assert_eq!(
resolve_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() {
assert_eq!(
resolve_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_arch_target_builds_locally() {
assert_eq!(
resolve_placement(ARM_MAC, Some("aarch64-unknown-linux-musl"), None, true),
Resolution::NativeCross
);
assert_eq!(
resolve_placement(X64_LINUX, Some("x86_64-unknown-linux-musl"), None, true),
Resolution::NativeCross
);
}
#[test]
fn native_absent_or_empty_target_builds_locally() {
assert_eq!(
resolve_placement(ARM_MAC, None, None, true),
Resolution::NativeCross
);
assert_eq!(
resolve_placement(ARM_MAC, Some(" "), None, true),
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);
}
#[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() {
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() {
assert!(host_native_crossable(ARM_MAC, "x86_64-unknown-linux-musl"));
assert!(host_native_crossable(
X64_LINUX,
"aarch64-unknown-linux-gnu"
));
assert!(host_native_crossable(X64_LINUX, "x86_64-pc-windows-gnu"));
assert!(!host_native_crossable(X64_LINUX, "x86_64-pc-windows-msvc"));
assert!(host_native_crossable(ARM_MAC, "x86_64-apple-darwin"));
assert!(!host_native_crossable(X64_LINUX, "aarch64-apple-darwin"));
assert!(!host_native_crossable(X64_LINUX, "mos-unknown-none"));
}
#[test]
fn host_arch_round_trips_through_gha_vocabulary() {
let host = detect_host_triple();
let gha = gha_runner_arch(arch_of(&host));
assert!(!gha.is_empty(), "host {host:?} → gha arch {gha:?}");
}
}