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.
This commit is contained in:
+6
-33
@@ -1,10 +1,9 @@
|
||||
//! Build environment setup: `SOURCE_DATE_EPOCH`, `DEB_BUILD_OPTIONS`,
|
||||
//! architecture variables (via `dpkg-architecture`) and the sanitized
|
||||
//! environment recorded in `.buildinfo` files.
|
||||
//! architecture variables (native `dpkg-architecture` equivalent) and the
|
||||
//! sanitized environment recorded in `.buildinfo` files.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
/// Number of parallel jobs to advertise in `DEB_BUILD_OPTIONS`.
|
||||
pub fn num_parallel() -> usize {
|
||||
@@ -41,41 +40,15 @@ pub fn build_env(
|
||||
env
|
||||
}
|
||||
|
||||
/// Import the full architecture variable set by running
|
||||
/// `dpkg-architecture -f [-a <host-arch>]` and parsing its `KEY=VALUE` dump.
|
||||
/// Import the full architecture variable set, computed natively by
|
||||
/// [`crate::debian::arch`] (the equivalent of `dpkg-architecture -f
|
||||
/// [-a <host-arch>]`).
|
||||
///
|
||||
/// This exports all `DEB_BUILD_*`, `DEB_HOST_*` and `DEB_TARGET_*` variables
|
||||
/// (`*_ARCH`, `*_OS`, `*_CPU`, `*_MULTIARCH`, `*_GNU_TYPE`, ...), exactly as
|
||||
/// `dpkg-buildpackage` does.
|
||||
pub fn arch_env(host_arch: Option<&str>) -> Result<BTreeMap<String, String>, String> {
|
||||
let mut cmd = Command::new("dpkg-architecture");
|
||||
cmd.arg("-f");
|
||||
if let Some(arch) = host_arch {
|
||||
cmd.args(["--host-arch", arch]);
|
||||
}
|
||||
|
||||
let output = cmd.output().map_err(|e| {
|
||||
format!(
|
||||
"failed to run 'dpkg-architecture': {}. Is 'dpkg-dev' installed?",
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"dpkg-architecture failed with status {}: {}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
));
|
||||
}
|
||||
|
||||
let mut env = BTreeMap::new();
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
env.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
Ok(env)
|
||||
crate::debian::arch::arch_env(host_arch)
|
||||
}
|
||||
|
||||
/// Read the current vendor name from `/etc/dpkg/origins/default`
|
||||
|
||||
@@ -859,6 +859,56 @@ mod differential_tests {
|
||||
differential_on_tree(&tree);
|
||||
}
|
||||
|
||||
/// Differential check of [`crate::debian::arch::arch_env`] against real
|
||||
/// `dpkg-architecture -f -a <arch>` for one architecture.
|
||||
fn diff_arch_env_one(arch: Option<&str>) {
|
||||
let mut cmd = Command::new("dpkg-architecture");
|
||||
cmd.arg("-f");
|
||||
if let Some(a) = arch {
|
||||
cmd.args(["-a", a]);
|
||||
}
|
||||
let output = cmd.output().expect("run dpkg-architecture (is dpkg-dev installed?)");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"dpkg-architecture -f {arch:?} failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let mut expected = BTreeMap::new();
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
expected.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let ours = crate::debian::arch::arch_env(arch)
|
||||
.unwrap_or_else(|e| panic!("native arch_env({arch:?}) failed: {e}"));
|
||||
assert_eq!(ours, expected, "arch_env({arch:?}) differs from dpkg-architecture");
|
||||
}
|
||||
|
||||
/// Every architecture known to the local dpkg must produce an identical
|
||||
/// environment dump (`dpkg-architecture -L`).
|
||||
#[test]
|
||||
fn diff_arch_env_all_known_arches() {
|
||||
let output = Command::new("dpkg-architecture")
|
||||
.arg("-L")
|
||||
.output()
|
||||
.expect("run dpkg-architecture -L (is dpkg-dev installed?)");
|
||||
assert!(output.status.success());
|
||||
for arch in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
let arch = arch.trim();
|
||||
if arch.is_empty() {
|
||||
continue;
|
||||
}
|
||||
diff_arch_env_one(Some(arch));
|
||||
}
|
||||
}
|
||||
|
||||
/// Native (no explicit host architecture) must match too.
|
||||
#[test]
|
||||
fn diff_arch_env_native() {
|
||||
diff_arch_env_one(None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_native_minimal() {
|
||||
differential_case(&FixtureSpec::new("pkh-diff-a", "1.0-1", "unstable"));
|
||||
|
||||
@@ -0,0 +1,733 @@
|
||||
//! 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());
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -3,12 +3,15 @@
|
||||
//! These components are independent from any build orchestration and can be
|
||||
//! used by any pkh submodule (or external consumers of the library):
|
||||
//!
|
||||
//! - [`arch`]: Debian architecture tables and lookups (dpkg-architecture)
|
||||
//! - [`control`]: deb822 paragraph parsing/writing and `debian/control`
|
||||
//! - [`checksums`]: file checksum registry (`Dpkg::Checksums` equivalent)
|
||||
//! - [`deps`]: dependency grammar and evaluation (dpkg-checkbuilddeps)
|
||||
//! - [`files`]: `debian/files` artifact registry (`Dpkg::Dist::Files`)
|
||||
//! - [`version`]: Debian version splitting/validation
|
||||
//! - [`version`]: Debian version splitting/validation/comparison
|
||||
//! - [`changelog`]: `debian/changelog` entry parsing
|
||||
|
||||
pub mod arch;
|
||||
pub mod changelog;
|
||||
pub mod checksums;
|
||||
pub mod control;
|
||||
|
||||
Reference in New Issue
Block a user