Skip to main content

gentoo_core/
arch.rs

1//! Gentoo architecture types.
2//!
3//! This module provides types for representing CPU architectures as they
4//! appear in Gentoo's keyword system:
5//!
6//! - [`KnownArch`]: The 18 architectures officially supported by Gentoo Linux.
7//!   Provides zero-cost representation and metadata like bitness.
8//! - [`Arch`]: A generic architecture type that can be either a known Gentoo
9//!   architecture or an overlay-defined keyword string.
10//!
11//! # Keywords vs Architecture Names
12//!
13//! Gentoo uses specific keyword strings in ebuilds (e.g., `~amd64`, `arm64`).
14//! `KnownArch` maps to these canonical keywords via [`KnownArch::as_keyword`].
15//! Some architectures share keywords (e.g., `riscv32` and `riscv64` both map
16//! to `"riscv"`), reflecting how Gentoo keywords group related architectures.
17
18use std::fmt;
19use std::hash::Hash;
20use std::str::FromStr;
21
22use crate::Error;
23use gentoo_interner::{DefaultInterner, Interned, Interner};
24
25/// A CPU architecture officially supported by Gentoo Linux.
26///
27/// Represents the 18 architectures with stable or testing keywords in the
28/// Gentoo ebuild repository. Each variant maps to a canonical Gentoo keyword
29/// string via [`KnownArch::as_keyword`].
30///
31/// # Keyword Grouping
32///
33/// Some architectures share keywords due to Gentoo's keyword conventions:
34///
35/// | Architecture | Keyword | Notes |
36/// |--------------|---------|-------|
37/// | `Riscv32`, `Riscv64` | `"riscv"` | RISC-V variants share a keyword |
38/// | `Mips`, `Mips64` | `"mips"` | MIPS variants share a keyword |
39/// | `Sparc`, `Sparc64` | `"sparc"` | SPARC variants share a keyword |
40///
41/// # Examples
42///
43/// ```
44/// use gentoo_core::KnownArch;
45///
46/// let arch: KnownArch = "amd64".parse().unwrap();
47/// assert_eq!(arch.as_keyword(), "amd64");
48/// assert_eq!(arch.bitness(), 64);
49/// ```
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub enum KnownArch {
53    Arm,
54    AArch64,
55    X86,
56    X86_64,
57    Riscv32,
58    Riscv64,
59    Powerpc,
60    Powerpc64,
61    Mips,
62    Mips64,
63    Sparc,
64    Sparc64,
65    S390x,
66    M68k,
67    LoongArch64,
68    Alpha,
69    Hppa,
70    Ia64,
71}
72
73impl KnownArch {
74    /// Gentoo keyword string for this architecture (e.g. `"amd64"`).
75    pub fn as_keyword(&self) -> &'static str {
76        match self {
77            KnownArch::Arm => "arm",
78            KnownArch::AArch64 => "arm64",
79            KnownArch::X86 => "x86",
80            KnownArch::X86_64 => "amd64",
81            KnownArch::Riscv32 | KnownArch::Riscv64 => "riscv",
82            KnownArch::Powerpc => "ppc",
83            KnownArch::Powerpc64 => "ppc64",
84            KnownArch::Mips | KnownArch::Mips64 => "mips",
85            KnownArch::Sparc | KnownArch::Sparc64 => "sparc",
86            KnownArch::S390x => "s390",
87            KnownArch::M68k => "m68k",
88            KnownArch::LoongArch64 => "loong",
89            KnownArch::Alpha => "alpha",
90            KnownArch::Hppa => "hppa",
91            KnownArch::Ia64 => "ia64",
92        }
93    }
94
95    /// Parse from a keyword or common alias string (case-insensitive).
96    pub fn parse(arch: &str) -> Result<Self, Error> {
97        match arch.to_lowercase().as_str() {
98            "arm" | "armv7" | "armv7a" | "armv7l" | "armv7hl" => Ok(KnownArch::Arm),
99            "aarch64" | "arm64" | "armv8" | "armv8a" => Ok(KnownArch::AArch64),
100            "x86" | "i386" | "i486" | "i586" | "i686" => Ok(KnownArch::X86),
101            "x86_64" | "amd64" => Ok(KnownArch::X86_64),
102            "riscv32" => Ok(KnownArch::Riscv32),
103            "riscv64" | "riscv" => Ok(KnownArch::Riscv64),
104            "powerpc" | "ppc" => Ok(KnownArch::Powerpc),
105            "powerpc64" | "ppc64" => Ok(KnownArch::Powerpc64),
106            "mips" => Ok(KnownArch::Mips),
107            "mips64" => Ok(KnownArch::Mips64),
108            "sparc" => Ok(KnownArch::Sparc),
109            "sparc64" => Ok(KnownArch::Sparc64),
110            "s390" | "s390x" => Ok(KnownArch::S390x),
111            "m68k" => Ok(KnownArch::M68k),
112            "loong" | "loongarch64" => Ok(KnownArch::LoongArch64),
113            "alpha" => Ok(KnownArch::Alpha),
114            "hppa" => Ok(KnownArch::Hppa),
115            "ia64" => Ok(KnownArch::Ia64),
116            _ => Err(Error::ParseError(format!("Unknown architecture: {arch}"))),
117        }
118    }
119
120    /// Bitness (32 or 64) of this architecture.
121    pub fn bitness(&self) -> u32 {
122        match self {
123            KnownArch::Arm
124            | KnownArch::X86
125            | KnownArch::Riscv32
126            | KnownArch::Powerpc
127            | KnownArch::Mips
128            | KnownArch::Sparc
129            | KnownArch::M68k
130            | KnownArch::Hppa => 32,
131            KnownArch::AArch64
132            | KnownArch::X86_64
133            | KnownArch::Riscv64
134            | KnownArch::Powerpc64
135            | KnownArch::Mips64
136            | KnownArch::Sparc64
137            | KnownArch::S390x
138            | KnownArch::LoongArch64
139            | KnownArch::Alpha
140            | KnownArch::Ia64 => 64,
141        }
142    }
143
144    /// Current system architecture from [`std::env::consts::ARCH`].
145    pub fn current() -> Result<Self, Error> {
146        Self::parse(std::env::consts::ARCH)
147    }
148}
149
150impl fmt::Display for KnownArch {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        let name = match self {
153            KnownArch::Arm => "arm",
154            KnownArch::AArch64 => "aarch64",
155            KnownArch::X86 => "x86",
156            KnownArch::X86_64 => "x86_64",
157            KnownArch::Riscv32 => "riscv32",
158            KnownArch::Riscv64 => "riscv64",
159            KnownArch::Powerpc => "powerpc",
160            KnownArch::Powerpc64 => "powerpc64",
161            KnownArch::Mips => "mips",
162            KnownArch::Mips64 => "mips64",
163            KnownArch::Sparc => "sparc",
164            KnownArch::Sparc64 => "sparc64",
165            KnownArch::S390x => "s390x",
166            KnownArch::M68k => "m68k",
167            KnownArch::LoongArch64 => "loongarch64",
168            KnownArch::Alpha => "alpha",
169            KnownArch::Hppa => "hppa",
170            KnownArch::Ia64 => "ia64",
171        };
172        write!(f, "{name}")
173    }
174}
175
176impl FromStr for KnownArch {
177    type Err = Error;
178    fn from_str(s: &str) -> Result<Self, Self::Err> {
179        Self::parse(s)
180    }
181}
182
183// ── Arch<I> ───────────────────────────────────────────────────────────────────
184
185/// Opaque key for an overlay-defined keyword string.
186///
187/// Type alias for [`Interned<I>`](crate::interner::Interned).
188/// See that type for the full API and serde behaviour.
189pub type ExoticKey<I> = Interned<I>;
190
191/// A Gentoo architecture keyword.
192///
193/// Represents either a well-known Gentoo architecture or an overlay-specific
194/// keyword string. This type is used when parsing ebuild `KEYWORDS` or other
195/// architecture references that may include non-standard values.
196///
197/// # Variants
198///
199/// - `Known(KnownArch)`: A recognized Gentoo architecture. Zero-cost and `Copy`.
200/// - `Exotic(ExoticKey<I>)`: An overlay-defined keyword stored via interning.
201///
202/// # Memory Efficiency
203///
204/// With the default `interner` feature, `Arch<GlobalInterner>` is `Copy` (4 bytes)
205/// and identical exotic strings share a single allocation. This is useful when
206/// processing large numbers of ebuilds.
207///
208/// # Examples
209///
210/// ```
211/// use gentoo_core::Arch;
212///
213/// // Known architectures are recognized automatically
214/// let known = Arch::intern("amd64");
215/// assert_eq!(known.as_str(), "amd64");
216///
217/// // Unknown strings become exotic keys
218/// let exotic = Arch::intern("my-custom-board");
219/// assert_eq!(exotic.as_str(), "my-custom-board");
220/// ```
221#[derive(Debug, PartialEq, Eq, Hash)]
222pub enum Arch<I = DefaultInterner>
223where
224    I: Interner,
225{
226    /// A well-known Gentoo architecture keyword.
227    Known(KnownArch),
228    /// An overlay-defined keyword string interned via `I`.
229    Exotic(ExoticKey<I>),
230}
231
232impl<I: Interner> Clone for Arch<I> {
233    fn clone(&self) -> Self {
234        match self {
235            Self::Known(arch) => Self::Known(*arch),
236            Self::Exotic(key) => Self::Exotic(key.clone()),
237        }
238    }
239}
240
241impl<I: Interner> Copy for Arch<I> where Interned<I>: Copy {}
242
243impl<I: Interner> Arch<I> {
244    /// Intern `keyword` using the interner `I`.
245    pub fn intern(keyword: &str) -> Self {
246        if let Ok(known) = KnownArch::parse(keyword) {
247            Self::Known(known)
248        } else {
249            Self::Exotic(ExoticKey::intern(keyword))
250        }
251    }
252
253    /// Current system architecture from [`std::env::consts::ARCH`].
254    ///
255    /// Returns `Known` for recognized architectures, `Exotic` otherwise.
256    pub fn current() -> Self {
257        Self::intern(std::env::consts::ARCH)
258    }
259
260    /// Extract the CPU arch from a GNU CHOST triple using the interner `I`.
261    ///
262    /// Returns `None` only when `chost` is empty.
263    pub fn from_chost(chost: &str) -> Option<Self> {
264        let cpu = chost.split('-').next().filter(|s| !s.is_empty())?;
265        Some(Self::intern(&normalize_chost_cpu(cpu)))
266    }
267
268    /// Resolve to the Gentoo keyword string using the interner `I`.
269    pub fn as_str(&self) -> &str {
270        match self {
271            Self::Known(arch) => arch.as_keyword(),
272            Self::Exotic(key) => key.resolve(),
273        }
274    }
275
276    /// The Gentoo keyword for this architecture.
277    ///
278    /// For known architectures, returns the canonical keyword (e.g., `"amd64"`).
279    /// For exotic architectures, returns the interned string directly.
280    pub fn as_keyword(&self) -> &str {
281        self.as_str()
282    }
283}
284
285impl<I: Interner> fmt::Display for Arch<I> {
286    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287        f.write_str(self.as_str())
288    }
289}
290
291impl<I: Interner> PartialEq<str> for Arch<I> {
292    fn eq(&self, other: &str) -> bool {
293        self.as_str() == other
294    }
295}
296
297impl<I: Interner> PartialEq<&str> for Arch<I> {
298    fn eq(&self, other: &&str) -> bool {
299        self.as_str() == *other
300    }
301}
302
303impl<I: Interner> PartialEq<String> for Arch<I> {
304    fn eq(&self, other: &String) -> bool {
305        self.as_str() == other.as_str()
306    }
307}
308
309impl<I: Interner> FromStr for Arch<I> {
310    type Err = std::convert::Infallible;
311
312    fn from_str(s: &str) -> Result<Self, Self::Err> {
313        match KnownArch::from_str(s) {
314            Ok(known) => Ok(Self::Known(known)),
315            Err(_) => Ok(Self::Exotic(ExoticKey::intern(s))),
316        }
317    }
318}
319
320/// Normalise the CPU field of a GNU CHOST triple before matching known arches.
321fn normalize_chost_cpu(cpu: &str) -> String {
322    let s = cpu.to_lowercase();
323
324    // powerpc64le / powerpc64be → powerpc64
325    for suffix in &["le", "be"] {
326        if let Some(base) = s.strip_suffix(suffix)
327            && base == "powerpc64"
328        {
329            return base.to_string();
330        }
331    }
332
333    // mipsel / mipseb → mips;  mips64el / mips64eb → mips64
334    for suffix in &["el", "eb"] {
335        if let Some(base) = s.strip_suffix(suffix)
336            && (base == "mips" || base == "mips64")
337        {
338            return base.to_string();
339        }
340    }
341
342    // riscv64gc, riscv64imac → riscv64;  riscv32gc → riscv32
343    if let Some(after_riscv) = s.strip_prefix("riscv") {
344        if let Some(end) = after_riscv.find(|c: char| !c.is_ascii_digit())
345            && end > 0
346        {
347            return format!("riscv{}", &after_riscv[..end]);
348        }
349        return s;
350    }
351
352    // hppa2.0w, hppa1.1 → hppa
353    if s.starts_with("hppa") && s.len() > "hppa".len() {
354        return "hppa".to_string();
355    }
356
357    s
358}
359
360// ── Serde impls ──────────────────────────────────────────────────────────────
361
362/// `Arch<I>` serializes as its keyword string (e.g. `"amd64"`, `"mymachine"`),
363/// regardless of how the underlying interner key is stored.
364#[cfg(feature = "serde")]
365impl<I: Interner> serde::Serialize for Arch<I> {
366    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
367        serializer.serialize_str(self.as_str())
368    }
369}
370
371/// Deserializes from the keyword string, interning via `I`.
372#[cfg(feature = "serde")]
373impl<'de, I: Interner> serde::Deserialize<'de> for Arch<I> {
374    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
375        let s = <String as serde::Deserialize<'de>>::deserialize(deserializer)?;
376        Ok(Self::intern(&s))
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    // ── KnownArch ────────────────────────────────────────────────────────────
385
386    #[test]
387    fn known_arch_keywords() {
388        assert_eq!(KnownArch::Arm.as_keyword(), "arm");
389        assert_eq!(KnownArch::AArch64.as_keyword(), "arm64");
390        assert_eq!(KnownArch::X86.as_keyword(), "x86");
391        assert_eq!(KnownArch::X86_64.as_keyword(), "amd64");
392        assert_eq!(KnownArch::Riscv32.as_keyword(), "riscv");
393        assert_eq!(KnownArch::Riscv64.as_keyword(), "riscv");
394        assert_eq!(KnownArch::Powerpc.as_keyword(), "ppc");
395        assert_eq!(KnownArch::Powerpc64.as_keyword(), "ppc64");
396        assert_eq!(KnownArch::LoongArch64.as_keyword(), "loong");
397        assert_eq!(KnownArch::Hppa.as_keyword(), "hppa");
398    }
399
400    #[test]
401    fn known_arch_parsing() {
402        assert!(KnownArch::parse("arm").is_ok());
403        assert!(KnownArch::parse("amd64").is_ok());
404        assert!(KnownArch::parse("AMD64").is_ok());
405        assert!(KnownArch::parse("invalid").is_err());
406    }
407
408    #[test]
409    fn known_arch_from_str() {
410        assert_eq!("amd64".parse::<KnownArch>().unwrap(), KnownArch::X86_64);
411        assert!("invalid".parse::<KnownArch>().is_err());
412    }
413
414    // ── Arch convenience methods (DefaultInterner) ───────────────────────────
415
416    #[test]
417    fn arch_intern_known() {
418        assert!(matches!(<Arch>::intern("amd64"), Arch::Known(_)));
419        assert!(matches!(<Arch>::intern("arm64"), Arch::Known(_)));
420        assert!(matches!(<Arch>::intern("loong"), Arch::Known(_)));
421        assert!(matches!(<Arch>::intern("hppa"), Arch::Known(_)));
422    }
423
424    #[test]
425    fn arch_intern_exotic() {
426        let a1: Arch = Arch::intern("mymachine");
427        assert!(matches!(a1, Arch::Exotic(_)));
428        assert_eq!(Arch::intern("mymachine"), a1); // same key
429        assert_eq!(a1.as_str(), "mymachine");
430    }
431
432    #[test]
433    fn arch_from_chost_known() {
434        let cases = [
435            ("x86_64-pc-linux-gnu", "amd64"),
436            ("aarch64-unknown-linux-gnu", "arm64"),
437            ("i686-pc-linux-gnu", "x86"),
438            ("powerpc-unknown-linux-gnu", "ppc"),
439            ("s390x-linux-gnu", "s390"),
440        ];
441        for (chost, expected) in cases {
442            let arch: Arch = Arch::from_chost(chost).unwrap();
443            assert_eq!(arch.as_str(), expected, "chost={chost}");
444            assert!(
445                matches!(arch, Arch::Known(_)),
446                "chost={chost} should be Known"
447            );
448        }
449    }
450
451    #[test]
452    fn arch_chost_normalization() {
453        let cases = [
454            ("powerpc64le-unknown-linux-gnu", "ppc64"),
455            ("riscv64gc-unknown-linux-gnu", "riscv"),
456            ("mipsel-unknown-linux-gnu", "mips"),
457            ("mips64el-unknown-linux-gnu", "mips"),
458            ("hppa2.0w-hp-linux-gnu", "hppa"),
459        ];
460        for (chost, expected) in cases {
461            assert_eq!(
462                <Arch>::from_chost(chost).unwrap().as_str(),
463                expected,
464                "chost={chost}"
465            );
466        }
467    }
468
469    #[test]
470    fn arch_empty_chost() {
471        assert!(<Arch>::from_chost("").is_none());
472    }
473
474    #[test]
475    fn arch_from_str_known() {
476        let arch: Arch = "amd64".parse().unwrap();
477        assert!(matches!(arch, Arch::Known(KnownArch::X86_64)));
478        assert_eq!(arch.as_str(), "amd64");
479
480        let arch: Arch = "arm64".parse().unwrap();
481        assert!(matches!(arch, Arch::Known(KnownArch::AArch64)));
482        assert_eq!(arch.as_str(), "arm64");
483    }
484
485    #[test]
486    fn arch_from_str_exotic() {
487        let arch: Arch = "mymachine".parse().unwrap();
488        assert!(matches!(arch, Arch::Exotic(_)));
489        assert_eq!(arch.as_str(), "mymachine");
490    }
491
492    // ── Serde roundtrip ──────────────────────────────────────────────────────
493
494    #[cfg(feature = "serde")]
495    mod serde {
496        use super::*;
497
498        #[test]
499        fn arch_known_serializes_as_keyword() {
500            let arch: Arch = Arch::intern("amd64");
501            let json = serde_json::to_string(&arch).unwrap();
502            assert_eq!(json, r#""amd64""#);
503        }
504
505        #[test]
506        fn arch_exotic_serializes_as_string() {
507            let arch: Arch = Arch::intern("mymachine");
508            let json = serde_json::to_string(&arch).unwrap();
509            assert_eq!(json, r#""mymachine""#);
510        }
511
512        #[test]
513        fn arch_known_roundtrip() {
514            let original = Arch::intern("arm64");
515            let json = serde_json::to_string(&original).unwrap();
516            let restored: Arch = serde_json::from_str(&json).unwrap();
517            assert_eq!(original, restored);
518            assert!(matches!(restored, Arch::Known(KnownArch::AArch64)));
519        }
520
521        #[test]
522        fn arch_exotic_roundtrip() {
523            let original = Arch::intern("mymachine");
524            let json = serde_json::to_string(&original).unwrap();
525            let restored: Arch = serde_json::from_str(&json).unwrap();
526            assert_eq!(original, restored);
527            assert!(matches!(restored, Arch::Exotic(_)));
528        }
529
530        #[test]
531        fn arch_deserialize_known_from_alias() {
532            let restored: Arch = serde_json::from_str(r#""x86_64""#).unwrap();
533            assert_eq!(restored, Arch::intern("amd64"));
534        }
535    }
536}