//! Native Debian architecture tables and lookups. //! //! Replaces the `dpkg-architecture` satellite tool with pure-Rust lookups //! over the factual architecture data published by dpkg. The embedded data //! tables are factual lists (Debian/GNU name mappings, pointer sizes, //! endianness); attribution comments point at the corresponding upstream //! files in the dpkg repository //! (, files `data/cputable`, //! `data/ostable`, `data/tupletable`, `data/abitable`). //! //! The variable dump produced by [`arch_env`] mirrors the full //! `dpkg-architecture -f` output: `DEB_BUILD_*`, `DEB_HOST_*` and //! `DEB_TARGET_*` × `{ARCH, ARCH_ABI, ARCH_LIBC, ARCH_OS, ARCH_CPU, //! ARCH_BITS, ARCH_ENDIAN, MULTIARCH, GNU_CPU, GNU_SYSTEM, GNU_TYPE}`. use std::collections::BTreeMap; use std::process::Command; use std::sync::OnceLock; use regex::Regex; /// Byte order of a CPU. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Endian { /// Least-significant byte first. Little, /// Most-significant byte first. Big, } impl std::fmt::Display for Endian { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Endian::Little => write!(f, "little"), Endian::Big => write!(f, "big"), } } } /// A Debian architecture tuple `(abi, libc, os, cpu)`, the normalized /// internal representation of an architecture name. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DebTuple { /// ABI attribute (e.g. `base`, `x32`, `gnueabihf`). pub abi: String, /// C library (e.g. `gnu`, `musl`, `uclibc`). pub libc: String, /// Operating system kernel (e.g. `linux`, `hurd`, `freebsd`). pub os: String, /// CPU (e.g. `amd64`, `arm`, `riscv64`). pub cpu: String, } impl DebTuple { /// Render the canonical `abi-libc-os-cpu` form. pub fn to_key(&self) -> String { format!("{}-{}-{}-{}", self.abi, self.libc, self.os, self.cpu) } /// Parse a canonical `abi-libc-os-cpu` key back into a tuple. fn from_key(key: &str) -> Option { let parts: Vec<&str> = key.split('-').collect(); if parts.len() != 4 { return None; } Some(DebTuple { abi: parts[0].to_string(), libc: parts[1].to_string(), os: parts[2].to_string(), cpu: parts[3].to_string(), }) } } /// One row of the CPU table (upstream: `data/cputable`). struct CpuEntry { /// Debian CPU name. name: &'static str, /// GNU config CPU name. gnu: &'static str, /// Anchored regex matching the CPU part of a GNU config.guess triplet. guess: &'static str, /// Pointer size in bits. bits: u32, /// Byte order. endian: Endian, } /// One row of the operating-system table (upstream: `data/ostable`). struct OsEntry { /// Debian system name as `abi-libc-os`. tuple: &'static str, /// GNU config system name. gnu: &'static str, /// Anchored regex matching the system part of a GNU config.guess triplet. guess: &'static str, } // Factual data from dpkg `data/cputable` (columns: debian name, GNU name, // config.guess regex, bits, endianness). static CPU_TABLE: &[CpuEntry] = &[ CpuEntry { name: "alpha", gnu: "alpha", guess: "alpha.*", bits: 64, endian: Endian::Little }, CpuEntry { name: "amd64", gnu: "x86_64", guess: "(amd64|x86_64)", bits: 64, endian: Endian::Little }, CpuEntry { name: "arc", gnu: "arc", guess: "arc", bits: 32, endian: Endian::Little }, CpuEntry { name: "armeb", gnu: "armeb", guess: "arm.*b", bits: 32, endian: Endian::Big }, CpuEntry { name: "arm", gnu: "arm", guess: "arm.*", bits: 32, endian: Endian::Little }, CpuEntry { name: "arm64", gnu: "aarch64", guess: "aarch64", bits: 64, endian: Endian::Little }, CpuEntry { name: "hppa", gnu: "hppa", guess: "hppa.*", bits: 32, endian: Endian::Big }, CpuEntry { name: "loong64", gnu: "loongarch64", guess: "loongarch64", bits: 64, endian: Endian::Little }, CpuEntry { name: "i386", gnu: "i686", guess: "(i[34567]86|pentium)", bits: 32, endian: Endian::Little }, CpuEntry { name: "ia64", gnu: "ia64", guess: "ia64", bits: 64, endian: Endian::Little }, CpuEntry { name: "m68k", gnu: "m68k", guess: "m68k", bits: 32, endian: Endian::Big }, CpuEntry { name: "mips", gnu: "mips", guess: "mips(eb)?", bits: 32, endian: Endian::Big }, CpuEntry { name: "mipsel", gnu: "mipsel", guess: "mipsel", bits: 32, endian: Endian::Little }, CpuEntry { name: "mipsr6", gnu: "mipsisa32r6", guess: "mipsisa32r6", bits: 32, endian: Endian::Big }, CpuEntry { name: "mipsr6el", gnu: "mipsisa32r6el", guess: "mipsisa32r6el", bits: 32, endian: Endian::Little }, CpuEntry { name: "mips64", gnu: "mips64", guess: "mips64", bits: 64, endian: Endian::Big }, CpuEntry { name: "mips64el", gnu: "mips64el", guess: "mips64el", bits: 64, endian: Endian::Little }, CpuEntry { name: "mips64r6", gnu: "mipsisa64r6", guess: "mipsisa64r6", bits: 64, endian: Endian::Big }, CpuEntry { name: "mips64r6el", gnu: "mipsisa64r6el", guess: "mipsisa64r6el", bits: 64, endian: Endian::Little }, CpuEntry { name: "nios2", gnu: "nios2", guess: "nios2", bits: 32, endian: Endian::Little }, CpuEntry { name: "or1k", gnu: "or1k", guess: "or1k", bits: 32, endian: Endian::Big }, CpuEntry { name: "powerpc", gnu: "powerpc", guess: "(powerpc|ppc)", bits: 32, endian: Endian::Big }, CpuEntry { name: "powerpcel", gnu: "powerpcle", guess: "powerpcle", bits: 32, endian: Endian::Little }, CpuEntry { name: "ppc64", gnu: "powerpc64", guess: "(powerpc|ppc)64", bits: 64, endian: Endian::Big }, CpuEntry { name: "ppc64el", gnu: "powerpc64le", guess: "powerpc64le", bits: 64, endian: Endian::Little }, CpuEntry { name: "riscv64", gnu: "riscv64", guess: "riscv64", bits: 64, endian: Endian::Little }, CpuEntry { name: "s390", gnu: "s390", guess: "s390", bits: 32, endian: Endian::Big }, CpuEntry { name: "s390x", gnu: "s390x", guess: "s390x", bits: 64, endian: Endian::Big }, CpuEntry { name: "sh3", gnu: "sh3", guess: "sh3", bits: 32, endian: Endian::Little }, CpuEntry { name: "sh3eb", gnu: "sh3eb", guess: "sh3eb", bits: 32, endian: Endian::Big }, CpuEntry { name: "sh4", gnu: "sh4", guess: "sh4", bits: 32, endian: Endian::Little }, CpuEntry { name: "sh4eb", gnu: "sh4eb", guess: "sh4eb", bits: 32, endian: Endian::Big }, CpuEntry { name: "sparc", gnu: "sparc", guess: "sparc", bits: 32, endian: Endian::Big }, CpuEntry { name: "sparc64", gnu: "sparc64", guess: "sparc(64|v9)", bits: 64, endian: Endian::Big }, ]; // Factual data from dpkg `data/ostable` (columns: debian `abi-libc-os`, // GNU system name, config.guess regex). static OS_TABLE: &[OsEntry] = &[ OsEntry { tuple: "eabi-uclibc-linux", gnu: "linux-uclibceabi", guess: "linux[^-]*-uclibceabi" }, OsEntry { tuple: "base-uclibc-linux", gnu: "linux-uclibc", guess: "linux[^-]*-uclibc" }, OsEntry { tuple: "eabihf-musl-linux", gnu: "linux-musleabihf", guess: "linux[^-]*-musleabihf" }, OsEntry { tuple: "base-musl-linux", gnu: "linux-musl", guess: "linux[^-]*-musl" }, OsEntry { tuple: "eabihf-gnu-linux", gnu: "linux-gnueabihf", guess: "linux[^-]*-gnueabihf" }, OsEntry { tuple: "eabi-gnu-linux", gnu: "linux-gnueabi", guess: "linux[^-]*-gnueabi" }, OsEntry { tuple: "abin32-gnu-linux", gnu: "linux-gnuabin32", guess: "linux[^-]*-gnuabin32" }, OsEntry { tuple: "abi64-gnu-linux", gnu: "linux-gnuabi64", guess: "linux[^-]*-gnuabi64" }, OsEntry { tuple: "spe-gnu-linux", gnu: "linux-gnuspe", guess: "linux[^-]*-gnuspe" }, OsEntry { tuple: "x32-gnu-linux", gnu: "linux-gnux32", guess: "linux[^-]*-gnux32" }, OsEntry { tuple: "base-gnu-linux", gnu: "linux-gnu", guess: "linux[^-]*(-gnu.*)?" }, OsEntry { tuple: "base-gnu-hurd", gnu: "gnu", guess: "gnu[^-]*" }, OsEntry { tuple: "base-bsd-darwin", gnu: "darwin", guess: "darwin[^-]*" }, OsEntry { tuple: "base-bsd-dragonflybsd", gnu: "dragonflybsd", guess: "dragonfly[^-]*" }, OsEntry { tuple: "base-bsd-freebsd", gnu: "freebsd", guess: "freebsd[^-]*" }, OsEntry { tuple: "base-bsd-netbsd", gnu: "netbsd", guess: "netbsd[^-]*" }, OsEntry { tuple: "base-bsd-openbsd", gnu: "openbsd", guess: "openbsd[^-]*" }, OsEntry { tuple: "base-sysv-aix", gnu: "aix", guess: "aix[^-]*" }, OsEntry { tuple: "base-sysv-solaris", gnu: "solaris", guess: "solaris[^-]*" }, OsEntry { tuple: "base-tos-mint", gnu: "mint", guess: "mint[^-]*" }, ]; // Factual data from dpkg `data/tupletable`: bidirectional mapping between a // Debian arch tuple and a Debian arch name. `` expands over every CPU // in [`CPU_TABLE`]; earlier rows take precedence (first-match wins). static TUPLE_TABLE: &[(&str, &str)] = &[ ("eabi-uclibc-linux-arm", "uclibc-linux-armel"), ("base-uclibc-linux-", "uclibc-linux-"), ("eabihf-musl-linux-arm", "musl-linux-armhf"), ("base-musl-linux-", "musl-linux-"), ("eabihf-gnu-linux-arm", "armhf"), ("eabi-gnu-linux-arm", "armel"), ("abin32-gnu-linux-mips64r6el", "mipsn32r6el"), ("abin32-gnu-linux-mips64r6", "mipsn32r6"), ("abin32-gnu-linux-mips64el", "mipsn32el"), ("abin32-gnu-linux-mips64", "mipsn32"), ("abi64-gnu-linux-mips64r6el", "mips64r6el"), ("abi64-gnu-linux-mips64r6", "mips64r6"), ("abi64-gnu-linux-mips64el", "mips64el"), ("abi64-gnu-linux-mips64", "mips64"), ("x32-gnu-linux-amd64", "x32"), ("base-gnu-linux-", ""), ("base-gnu-hurd-amd64", "hurd-amd64"), ("base-gnu-hurd-i386", "hurd-i386"), ("base-bsd-dragonflybsd-amd64", "dragonflybsd-amd64"), ("base-bsd-freebsd-amd64", "freebsd-amd64"), ("base-bsd-freebsd-arm", "freebsd-arm"), ("base-bsd-freebsd-arm64", "freebsd-arm64"), ("base-bsd-freebsd-i386", "freebsd-i386"), ("base-bsd-freebsd-powerpc", "freebsd-powerpc"), ("base-bsd-freebsd-ppc64", "freebsd-ppc64"), ("base-bsd-freebsd-riscv", "freebsd-riscv"), ("base-bsd-openbsd-", "openbsd-"), ("base-bsd-netbsd-", "netbsd-"), ("base-bsd-darwin-amd64", "darwin-amd64"), ("base-bsd-darwin-arm", "darwin-arm"), ("base-bsd-darwin-arm64", "darwin-arm64"), ("base-bsd-darwin-i386", "darwin-i386"), ("base-bsd-darwin-powerpc", "darwin-powerpc"), ("base-bsd-darwin-ppc64", "darwin-ppc64"), ("base-sysv-aix-powerpc", "aix-powerpc"), ("base-sysv-aix-ppc64", "aix-ppc64"), ("base-sysv-solaris-amd64", "solaris-amd64"), ("base-sysv-solaris-i386", "solaris-i386"), ("base-sysv-solaris-sparc", "solaris-sparc"), ("base-sysv-solaris-sparc64", "solaris-sparc64"), ("base-tos-mint-m68k", "mint-m68k"), ]; // Factual data from dpkg `data/abitable`: ABI pointer-size overrides. static ABI_BITS: &[(&str, u32)] = &[("abin32", 32), ("x32", 32)]; fn cpu_by_name(name: &str) -> Option<&'static CpuEntry> { CPU_TABLE.iter().find(|c| c.name == name) } fn os_by_key(key: &str) -> Option<&'static OsEntry> { OS_TABLE.iter().find(|o| o.tuple == key) } /// Map a Debian architecture tuple to its Debian architecture name, using /// the tupletable with `` expansion and first-match precedence. pub fn debtuple_to_debarch(tuple: &DebTuple) -> Option { let key = tuple.to_key(); for (tuple_pattern, arch_pattern) in TUPLE_TABLE { if tuple_pattern.contains("") { for cpu in CPU_TABLE { if tuple_pattern.replace("", cpu.name) == key { return Some(arch_pattern.replace("", cpu.name)); } } } else if *tuple_pattern == key { return Some((*arch_pattern).to_string()); } } None } /// Map a Debian architecture name to its normalized Debian tuple. /// /// Handles the legacy `linux-` spelling by stripping the prefix, like /// dpkg does for historical names that might still circulate. pub fn debarch_to_debtuple(arch: &str) -> Option { // Legacy `linux-` spelling: only the part up to the next dash is // taken, mirroring the historical `/^linux-([^-]*)/` substitution. let legacy; let arch = if let Some(rest) = arch.strip_prefix("linux-") { legacy = rest.split('-').next().unwrap_or("").to_string(); legacy.as_str() } else { arch }; for (tuple_pattern, arch_pattern) in TUPLE_TABLE { if arch_pattern.contains("") { for cpu in CPU_TABLE { if arch_pattern.replace("", cpu.name) == arch { let expanded = tuple_pattern.replace("", cpu.name); return DebTuple::from_key(&expanded); } } } else if *arch_pattern == arch { return DebTuple::from_key(tuple_pattern); } } None } /// Map a Debian architecture to its GNU triplet (`cpu-system`). pub fn debarch_to_gnutriplet(arch: &str) -> Option { let tuple = debarch_to_debtuple(arch)?; let cpu = cpu_by_name(&tuple.cpu)?; let os = os_by_key(&format!("{}-{}-{}", tuple.abi, tuple.libc, tuple.os))?; Some(format!("{}-{}", cpu.gnu, os.gnu)) } /// Map a Debian architecture to its Debian multiarch triplet. /// /// Identical to the GNU triplet except for the i386 family, whose GNU CPU /// names (`i486`...) are normalized to `i386`. pub fn multiarch(arch: &str) -> Option { let gnu = debarch_to_gnutriplet(arch)?; let (gnu_cpu, rest) = gnu.split_once('-')?; let mut chars = gnu_cpu.chars(); let is_i386_family = matches!(chars.next(), Some('i')) && matches!(chars.next(), Some(c) if ('4'..='7').contains(&c)) && chars.as_str() == "86"; if is_i386_family { Some(format!("i386-{rest}")) } else { Some(gnu) } } /// Pointer size (bits) and endianness of a Debian architecture. /// /// The ABI table overrides the CPU pointer size when the architecture tuple /// carries a size-changing ABI (e.g. `x32` is 32-bit pointers on a 64-bit /// CPU). pub fn abi_attrs(arch: &str) -> Option<(u32, Endian)> { let tuple = debarch_to_debtuple(arch)?; let cpu = cpu_by_name(&tuple.cpu)?; let bits = ABI_BITS .iter() .find(|(abi, _)| *abi == tuple.abi) .map(|(_, bits)| *bits) .unwrap_or(cpu.bits); Some((bits, cpu.endian)) } /// Evaluate the equality of two Debian architectures, comparing their /// normalized tuples. No wildcard matching is performed. pub fn eq(a: &str, b: &str) -> bool { if a == b { return true; } match (debarch_to_debtuple(a), debarch_to_debtuple(b)) { (Some(ta), Some(tb)) => ta == tb, _ => false, } } /// Expand an architecture wildcard into a tuple, filling missing leading /// components with `any`. Returns `None` for names that are neither a valid /// wildcard nor a valid architecture. fn wildcard_to_debtuple(wildcard: &str) -> Option { let parts: Vec<&str> = wildcard.split('-').collect(); if parts.contains(&"any") { match parts.len() { 4 => DebTuple::from_key(wildcard), 3 => DebTuple::from_key(&format!("any-{wildcard}")), 2 => DebTuple::from_key(&format!("any-any-{wildcard}")), 1 => DebTuple::from_key(&format!("any-any-any-{wildcard}")), _ => None, } } else { debarch_to_debtuple(wildcard) } } /// Evaluate the identity of a Debian architecture against an architecture /// wildcard (`any`, `linux-any`, `amd64`, ...). pub fn is(real: &str, alias: &str) -> bool { if alias == real || alias == "any" { return true; } let (Some(r), Some(a)) = (debarch_to_debtuple(real), wildcard_to_debtuple(alias)) else { return false; }; [a.abi.as_str(), a.libc.as_str(), a.os.as_str(), a.cpu.as_str()] .iter() .zip([ r.abi.as_str(), r.libc.as_str(), r.os.as_str(), r.cpu.as_str(), ]) .all(|(alias_part, real_part)| *alias_part == "any" || *alias_part == real_part) } /// Evaluate whether a Debian architecture name is an architecture wildcard. pub fn is_wildcard(arch: &str) -> bool { if arch == "all" { return false; } wildcard_to_debtuple(arch).is_some_and(|t| { [ t.abi.as_str(), t.libc.as_str(), t.os.as_str(), t.cpu.as_str(), ] .contains(&"any") }) } /// Validate an architecture name syntax. /// /// With `positive`, negated names (leading `!`) are rejected; otherwise they /// are allowed (as found in bracketed dependency restrictions). pub fn is_invalid(arch: &str, positive: bool) -> bool { let body = if positive { arch } else { arch.strip_prefix('!').unwrap_or(arch) }; let mut chars = body.chars(); match chars.next() { Some(first) if first.is_ascii_alphanumeric() => { !chars.all(|c| c.is_ascii_alphanumeric() || c == '-') } _ => true, } } /// Parse a whitespace-separated architecture list, validating every entry. pub fn list_parse(list: &str) -> Result, String> { let arches: Vec = list.split_whitespace().map(str::to_string).collect(); for arch in &arches { if is_invalid(arch, false) { return Err(format!( "'{arch}' is not a valid architecture in list '{list}'" )); } } Ok(arches) } /// Evaluate whether `host_arch` applies to a bracketed architecture /// restriction list (negations with `!`), as found in dependencies. pub fn is_concerned(host_arch: &str, arches: &[&str]) -> bool { let mut seen_arch = false; for arch in arches { let arch = arch.to_lowercase(); if let Some(negated) = arch.strip_prefix('!') { if is(host_arch, negated) { seen_arch = false; break; } // «!arch» includes by default all other arches unless they also // appear in a «!otherarch». seen_arch = true; } else if is(host_arch, &arch) { seen_arch = true; break; } } seen_arch } /// All currently known Debian architecture names, in table order /// (the equivalent of `dpkg-architecture -L`). pub fn valid_arches() -> Vec { let mut arches = Vec::new(); for os in OS_TABLE { for cpu in CPU_TABLE { let tuple = DebTuple { abi: os.tuple.split('-').next().unwrap_or("").to_string(), libc: os.tuple.split('-').nth(1).unwrap_or("").to_string(), os: os.tuple.split('-').nth(2).unwrap_or("").to_string(), cpu: cpu.name.to_string(), }; if let Some(arch) = debtuple_to_debarch(&tuple) { arches.push(arch); } } } arches } /// Match a GNU config.guess CPU string against the CPU table, in table /// order (first match wins), returning the Debian CPU name. fn cpu_from_config(value: &str) -> Option<&'static str> { static REGEXES: OnceLock> = OnceLock::new(); let regexes = REGEXES.get_or_init(|| { CPU_TABLE .iter() .map(|c| { ( c.name, Regex::new(&format!("^(?:{})$", c.guess)).expect("valid cpu regex"), ) }) .collect() }); regexes .iter() .find(|(_, re)| re.is_match(value)) .map(|(name, _)| *name) } /// Match a GNU config.guess system string against the OS table, in table /// order, returning the Debian `abi-libc-os` key. fn os_from_config(value: &str) -> Option<&'static str> { static REGEXES: OnceLock> = OnceLock::new(); let regexes = REGEXES.get_or_init(|| { OS_TABLE .iter() .map(|o| { ( o.tuple, Regex::new(&format!("^(?:.*-)?(?:{})$", o.guess)).expect("valid os regex"), ) }) .collect() }); regexes .iter() .find(|(_, re)| re.is_match(value)) .map(|(key, _)| *key) } /// Determine the current machine's Debian architecture from `uname`, /// without requiring dpkg. Used as a fallback when the `dpkg` frontend is /// unavailable. fn from_uname() -> Option { let output = Command::new("uname").arg("-m").output().ok()?; if !output.status.success() { return None; } let machine = String::from_utf8_lossy(&output.stdout).trim().to_string(); let cpu = cpu_from_config(&machine)?; let system = std::env::consts::OS; let os_key = os_from_config(system)?; DebTuple::from_key(&format!("{os_key}-{cpu}")).and_then(|t| debtuple_to_debarch(&t)) } /// Determine the native (build) Debian architecture. /// /// Mirrors `dpkg --print-architecture` (what `dpkg-architecture` uses for /// the `DEB_BUILD_*` variables): the authoritative answer comes from the /// dpkg database itself; if the `dpkg` frontend cannot be executed, the /// architecture is derived from `uname` through the same tables. pub fn native() -> Result { if let Ok(output) = Command::new("dpkg").arg("--print-architecture").output() && output.status.success() { let arch = String::from_utf8_lossy(&output.stdout).trim().to_string(); if !arch.is_empty() && debarch_to_debtuple(&arch).is_some() { return Ok(arch); } } from_uname().ok_or_else(|| "cannot determine native Debian architecture".to_string()) } /// Compute the complete architecture environment, the equivalent of /// `dpkg-architecture -f [-a ]`: all `DEB_BUILD_*`, `DEB_HOST_*` /// and `DEB_TARGET_*` variables, recomputed from scratch (force mode). /// /// The target architecture defaults to the host architecture, and the host /// architecture defaults to the native build architecture, exactly like /// dpkg-architecture. pub fn arch_env(host_arch: Option<&str>) -> Result, String> { let build_arch = native()?; let host_arch = host_arch.unwrap_or(&build_arch).to_string(); let target_arch = host_arch.clone(); let mut env = BTreeMap::new(); for (role, arch) in [ ("BUILD", build_arch), ("HOST", host_arch), ("TARGET", target_arch), ] { let tuple = debarch_to_debtuple(&arch) .ok_or_else(|| format!("unknown Debian architecture '{arch}'"))?; env.insert(format!("DEB_{role}_ARCH"), arch.clone()); env.insert(format!("DEB_{role}_ARCH_ABI"), tuple.abi.clone()); env.insert(format!("DEB_{role}_ARCH_LIBC"), tuple.libc.clone()); env.insert(format!("DEB_{role}_ARCH_OS"), tuple.os.clone()); env.insert(format!("DEB_{role}_ARCH_CPU"), tuple.cpu.clone()); let (bits, endian) = abi_attrs(&arch).ok_or_else(|| format!("unknown Debian architecture '{arch}'"))?; env.insert(format!("DEB_{role}_ARCH_BITS"), bits.to_string()); env.insert(format!("DEB_{role}_ARCH_ENDIAN"), endian.to_string()); let multi = multiarch(&arch) .ok_or_else(|| format!("unknown Debian architecture '{arch}'"))?; env.insert(format!("DEB_{role}_MULTIARCH"), multi); let gnu_type = debarch_to_gnutriplet(&arch) .ok_or_else(|| format!("unknown Debian architecture '{arch}'"))?; let (gnu_cpu, gnu_system) = gnu_type .split_once('-') .ok_or_else(|| format!("invalid GNU triplet '{gnu_type}'"))?; env.insert(format!("DEB_{role}_GNU_CPU"), gnu_cpu.to_string()); env.insert(format!("DEB_{role}_GNU_SYSTEM"), gnu_system.to_string()); env.insert(format!("DEB_{role}_GNU_TYPE"), gnu_type); } Ok(env) } #[cfg(test)] mod tests { use super::*; #[test] fn tuple_mapping() { let t = debarch_to_debtuple("amd64").unwrap(); assert_eq!( t, DebTuple { abi: "base".into(), libc: "gnu".into(), os: "linux".into(), cpu: "amd64".into() } ); let t = debarch_to_debtuple("armhf").unwrap(); assert_eq!(t.abi, "eabihf"); assert_eq!(t.cpu, "arm"); assert!(debarch_to_debtuple("not-an-arch").is_none()); // Legacy linux- prefix handling. assert_eq!( debarch_to_debtuple("linux-amd64").map(|t| t.cpu), Some("amd64".to_string()) ); } #[test] fn gnu_triplets_and_multiarch() { assert_eq!( debarch_to_gnutriplet("amd64").as_deref(), Some("x86_64-linux-gnu") ); assert_eq!( debarch_to_gnutriplet("armhf").as_deref(), Some("arm-linux-gnueabihf") ); assert_eq!( debarch_to_gnutriplet("i386").as_deref(), Some("i686-linux-gnu") ); assert_eq!(multiarch("i386").as_deref(), Some("i386-linux-gnu")); assert_eq!( multiarch("amd64").as_deref(), Some("x86_64-linux-gnu") ); assert_eq!( multiarch("arm64").as_deref(), Some("aarch64-linux-gnu") ); } #[test] fn bits_and_endian() { assert_eq!(abi_attrs("amd64"), Some((64, Endian::Little))); assert_eq!(abi_attrs("s390x"), Some((64, Endian::Big))); assert_eq!(abi_attrs("armhf"), Some((32, Endian::Little))); // x32: 32-bit pointers on a 64-bit CPU (abitable override). assert_eq!(abi_attrs("x32"), Some((32, Endian::Little))); assert_eq!(abi_attrs("mipsn32"), Some((32, Endian::Big))); } #[test] fn equality_and_wildcards() { assert!(eq("amd64", "amd64")); assert!(eq("linux-amd64", "amd64")); assert!(!eq("amd64", "i386")); assert!(is("amd64", "amd64")); assert!(is("amd64", "any")); assert!(is("amd64", "linux-any")); // A plain `linux-arm` wildcard pins the default ABI, so it does not // match armhf (whose tuple carries the eabihf ABI). assert!(!is("armhf", "linux-arm")); assert!(!is("amd64", "linux-arm")); assert!(is("hurd-i386", "any-i386")); assert!(is_wildcard("any")); assert!(is_wildcard("linux-any")); assert!(is_wildcard("gnu-any-amd64")); assert!(!is_wildcard("amd64")); assert!(!is_wildcard("all")); } #[test] fn restriction_lists() { assert!(!is_invalid("amd64", true)); assert!(!is_invalid("!amd64", false)); assert!(is_invalid("!amd64", true)); assert!(is_invalid("-bad", false)); assert!(is_invalid("", false)); assert_eq!( list_parse("amd64 arm64 !i386").unwrap(), vec![ "amd64".to_string(), "arm64".to_string(), "!i386".to_string() ] ); assert!(list_parse("amd64 bad$").is_err()); assert!(is_concerned("amd64", &["!i386"])); // Order matters: a positive match short-circuits before a later // negation (verified against Dpkg::Arch). assert!(is_concerned("amd64", &["amd64", "!amd64"])); assert!(!is_concerned("amd64", &["!amd64", "amd64"])); assert!(!is_concerned("i386", &["!i386"])); assert!(is_concerned("amd64", &["any"])); assert!(is_concerned("armhf", &["linux-any"])); } #[test] fn known_arches() { let arches = valid_arches(); for expected in ["amd64", "armhf", "armel", "i386", "riscv64", "x32", "hurd-i386"] { assert!(arches.iter().any(|a| a == expected), "missing {expected}"); } } #[test] fn env_dump_amd64_native() { let env = arch_env(Some("amd64")).unwrap(); assert_eq!(env.get("DEB_BUILD_ARCH").unwrap(), "amd64"); assert_eq!(env.get("DEB_HOST_ARCH").unwrap(), "amd64"); assert_eq!(env.get("DEB_TARGET_ARCH").unwrap(), "amd64"); assert_eq!(env.get("DEB_HOST_GNU_TYPE").unwrap(), "x86_64-linux-gnu"); assert_eq!(env.get("DEB_HOST_MULTIARCH").unwrap(), "x86_64-linux-gnu"); assert_eq!(env.get("DEB_HOST_ARCH_BITS").unwrap(), "64"); assert_eq!(env.get("DEB_HOST_ARCH_ENDIAN").unwrap(), "little"); assert_eq!(env.get("DEB_HOST_ARCH_OS").unwrap(), "linux"); assert_eq!(env.get("DEB_HOST_ARCH_CPU").unwrap(), "amd64"); assert_eq!(env.get("DEB_HOST_ARCH_ABI").unwrap(), "base"); assert_eq!(env.get("DEB_HOST_ARCH_LIBC").unwrap(), "gnu"); assert_eq!(env.get("DEB_HOST_GNU_CPU").unwrap(), "x86_64"); assert_eq!(env.get("DEB_HOST_GNU_SYSTEM").unwrap(), "linux-gnu"); // Exactly 11 variables per role. assert_eq!(env.len(), 33); } #[test] fn env_dump_cross_armhf() { let env = arch_env(Some("armhf")).unwrap(); // Build stays native while host/target follow the requested arch. assert_ne!(env.get("DEB_BUILD_ARCH").unwrap(), "armhf"); assert_eq!(env.get("DEB_HOST_ARCH").unwrap(), "armhf"); assert_eq!(env.get("DEB_HOST_GNU_TYPE").unwrap(), "arm-linux-gnueabihf"); assert_eq!(env.get("DEB_HOST_MULTIARCH").unwrap(), "arm-linux-gnueabihf"); assert_eq!(env.get("DEB_TARGET_ARCH").unwrap(), "armhf"); } #[test] fn env_dump_unknown_arch() { assert!(arch_env(Some("definitely-not-an-arch")).is_err()); } }