Files
pkh/src/debian/arch.rs
T
vhaudiquet 01c05f04a3 debian/arch: native architecture tables replacing dpkg-architecture
Embed dpkg's factual cputable/ostable/tupletable/abitable data and
implement tuple/triplet/multiarch lookups, wildcard matching, arch
restriction evaluation and the full DEB_BUILD_*/DEB_HOST_*/DEB_TARGET_*
environment dump natively.

build/env.rs::arch_env now delegates to the native implementation
instead of shelling out to 'dpkg-architecture -f'.

Differential gate (build/mod.rs): arch_env(Some(a)) must equal real
'dpkg-architecture -f -a a' key-for-key for every architecture listed by
'dpkg-architecture -L', plus the native case. Data tables carry upstream
attribution comments; no dpkg code was transliterated.
2026-08-23 21:53:09 +02:00

734 lines
29 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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
//! (<https://salsa.debian.org/dpkg-team/dpkg>, 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<DebTuple> {
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. `<cpu>` 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-<cpu>", "uclibc-linux-<cpu>"),
("eabihf-musl-linux-arm", "musl-linux-armhf"),
("base-musl-linux-<cpu>", "musl-linux-<cpu>"),
("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-<cpu>", "<cpu>"),
("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-<cpu>", "openbsd-<cpu>"),
("base-bsd-netbsd-<cpu>", "netbsd-<cpu>"),
("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 `<cpu>` expansion and first-match precedence.
pub fn debtuple_to_debarch(tuple: &DebTuple) -> Option<String> {
let key = tuple.to_key();
for (tuple_pattern, arch_pattern) in TUPLE_TABLE {
if tuple_pattern.contains("<cpu>") {
for cpu in CPU_TABLE {
if tuple_pattern.replace("<cpu>", cpu.name) == key {
return Some(arch_pattern.replace("<cpu>", 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-<cpu>` spelling by stripping the prefix, like
/// dpkg does for historical names that might still circulate.
pub fn debarch_to_debtuple(arch: &str) -> Option<DebTuple> {
// Legacy `linux-<cpu>` 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("<cpu>") {
for cpu in CPU_TABLE {
if arch_pattern.replace("<cpu>", cpu.name) == arch {
let expanded = tuple_pattern.replace("<cpu>", 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<String> {
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<String> {
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<DebTuple> {
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<Vec<String>, String> {
let arches: Vec<String> = 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<String> {
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<Vec<(&'static str, Regex)>> = 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<Vec<(&'static str, Regex)>> = 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<String> {
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<String, String> {
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 <host-arch>]`: 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<BTreeMap<String, String>, 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());
}
}