Skip to main content

uv_platform/
arch.rs

1use crate::{Error, Os};
2use std::str::FromStr;
3use target_lexicon::Architecture;
4
5/// Architecture variants, e.g., with support for different instruction sets
6#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, Ord, PartialOrd)]
7pub enum ArchVariant {
8    /// Targets 64-bit Intel/AMD CPUs newer than Nehalem (2008).
9    /// Includes SSE3, SSE4 and other post-2003 CPU instructions.
10    V2,
11    /// Targets 64-bit Intel/AMD CPUs newer than Haswell (2013) and Excavator (2015).
12    /// Includes AVX, AVX2, MOVBE and other newer CPU instructions.
13    V3,
14    /// Targets 64-bit Intel/AMD CPUs with AVX-512 instructions (post-2017 Intel CPUs).
15    /// Many post-2017 Intel CPUs do not support AVX-512.
16    V4,
17}
18
19#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
20pub struct Arch {
21    family: target_lexicon::Architecture,
22    variant: Option<ArchVariant>,
23}
24
25impl Ord for Arch {
26    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
27        self.cmp_for_os(*other, Os::from_env(), Self::from_env())
28    }
29}
30
31impl Arch {
32    /// Compare architectures by their preference for the given OS and native architecture.
33    pub(crate) fn cmp_for_os(self, other: Self, os: Os, native: Self) -> std::cmp::Ordering {
34        if self.family == other.family {
35            return self.variant.cmp(&other.variant);
36        }
37
38        self.preference(os, native)
39            .cmp(&other.preference(os, native))
40            // Fall back to lexicographic order for equally preferred architecture families.
41            .then_with(|| self.family.to_string().cmp(&other.family.to_string()))
42    }
43
44    /// Return this architecture's preference rank for the given OS and native architecture.
45    /// Lower ranks are preferred when sorting architecture candidates.
46    fn preference(self, os: Os, native: Self) -> u8 {
47        // Prefer native architectures.
48        if self.family == native.family {
49            return 0;
50        }
51
52        if !os.is_windows() {
53            return 3;
54        }
55
56        // Windows ARM64 can emulate both x86-64 and 32-bit x86, while x86-64 can run
57        // 32-bit x86. Prefer these compatible architectures over other families, with
58        // 64-bit Python ahead of 32-bit Python when both emulated builds are available.
59        match (native.family, self.family) {
60            (Architecture::Aarch64(_), Architecture::X86_64) => 1,
61            (Architecture::Aarch64(_) | Architecture::X86_64, Architecture::X86_32(_)) => 2,
62            _ => 3,
63        }
64    }
65}
66
67impl PartialOrd for Arch {
68    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
69        Some(self.cmp(other))
70    }
71}
72impl Arch {
73    pub fn new(family: target_lexicon::Architecture, variant: Option<ArchVariant>) -> Self {
74        Self { family, variant }
75    }
76
77    pub fn from_env() -> Self {
78        #[cfg(test)]
79        {
80            if let Some(arch) = test_support::get_mock_arch() {
81                return arch;
82            }
83        }
84
85        Self {
86            family: target_lexicon::HOST.architecture,
87            variant: None,
88        }
89    }
90
91    pub fn family(&self) -> target_lexicon::Architecture {
92        self.family
93    }
94
95    pub(crate) fn is_wasm(self) -> bool {
96        matches!(self.family, target_lexicon::Architecture::Wasm32)
97    }
98}
99
100impl std::fmt::Display for Arch {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        match self.family {
103            target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686) => {
104                write!(f, "x86")?;
105            }
106            inner => write!(f, "{inner}")?,
107        }
108        if let Some(variant) = self.variant {
109            write!(f, "_{variant}")?;
110        }
111        Ok(())
112    }
113}
114
115impl FromStr for Arch {
116    type Err = Error;
117
118    fn from_str(s: &str) -> Result<Self, Self::Err> {
119        fn parse_family(s: &str) -> Result<target_lexicon::Architecture, Error> {
120            let inner = match s {
121                // Allow users to specify "x86" as a shorthand for the "i686" variant, they should not need
122                // to specify the exact architecture and this variant is what we have downloads for.
123                "x86" => {
124                    target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686)
125                }
126                _ => target_lexicon::Architecture::from_str(s)
127                    .map_err(|()| Error::UnknownArch(s.to_string()))?,
128            };
129            if matches!(inner, target_lexicon::Architecture::Unknown) {
130                return Err(Error::UnknownArch(s.to_string()));
131            }
132            Ok(inner)
133        }
134
135        // First check for a variant
136        if let Some((Ok(family), Ok(variant))) = s
137            .rsplit_once('_')
138            .map(|(family, variant)| (parse_family(family), ArchVariant::from_str(variant)))
139        {
140            // We only support variants for `x86_64` right now
141            if !matches!(family, target_lexicon::Architecture::X86_64) {
142                return Err(Error::UnsupportedVariant(
143                    variant.to_string(),
144                    family.to_string(),
145                ));
146            }
147            return Ok(Self {
148                family,
149                variant: Some(variant),
150            });
151        }
152
153        let family = parse_family(s)?;
154
155        Ok(Self {
156            family,
157            variant: None,
158        })
159    }
160}
161
162impl FromStr for ArchVariant {
163    type Err = ();
164
165    fn from_str(s: &str) -> Result<Self, Self::Err> {
166        match s {
167            "v2" => Ok(Self::V2),
168            "v3" => Ok(Self::V3),
169            "v4" => Ok(Self::V4),
170            _ => Err(()),
171        }
172    }
173}
174
175impl std::fmt::Display for ArchVariant {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        match self {
178            Self::V2 => write!(f, "v2"),
179            Self::V3 => write!(f, "v3"),
180            Self::V4 => write!(f, "v4"),
181        }
182    }
183}
184
185impl From<&uv_platform_tags::Arch> for Arch {
186    fn from(value: &uv_platform_tags::Arch) -> Self {
187        match value {
188            uv_platform_tags::Arch::Aarch64 => Self::new(
189                target_lexicon::Architecture::Aarch64(target_lexicon::Aarch64Architecture::Aarch64),
190                None,
191            ),
192            uv_platform_tags::Arch::Armv5TEL => Self::new(
193                target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv5te),
194                None,
195            ),
196            uv_platform_tags::Arch::Armv6L => Self::new(
197                target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv6),
198                None,
199            ),
200            uv_platform_tags::Arch::Armv7L => Self::new(
201                target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv7),
202                None,
203            ),
204            uv_platform_tags::Arch::S390X => Self::new(target_lexicon::Architecture::S390x, None),
205            uv_platform_tags::Arch::Powerpc => {
206                Self::new(target_lexicon::Architecture::Powerpc, None)
207            }
208            uv_platform_tags::Arch::Powerpc64 => {
209                Self::new(target_lexicon::Architecture::Powerpc64, None)
210            }
211            uv_platform_tags::Arch::Powerpc64Le => {
212                Self::new(target_lexicon::Architecture::Powerpc64le, None)
213            }
214            uv_platform_tags::Arch::X86 => Self::new(
215                target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686),
216                None,
217            ),
218            uv_platform_tags::Arch::X86_64 => Self::new(target_lexicon::Architecture::X86_64, None),
219            uv_platform_tags::Arch::LoongArch64 => {
220                Self::new(target_lexicon::Architecture::LoongArch64, None)
221            }
222            uv_platform_tags::Arch::Riscv64 => Self::new(
223                target_lexicon::Architecture::Riscv64(target_lexicon::Riscv64Architecture::Riscv64),
224                None,
225            ),
226            uv_platform_tags::Arch::Wasm32 => Self::new(target_lexicon::Architecture::Wasm32, None),
227        }
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use std::str::FromStr;
234
235    use super::test_support::{aarch64, run_with_arch, x86_64};
236    use super::{Arch, Os};
237
238    #[test]
239    fn test_arch_sorting_prefers_native() {
240        run_with_arch(aarch64(), || {
241            assert!(aarch64() < x86_64());
242        });
243        run_with_arch(x86_64(), || {
244            assert!(x86_64() < aarch64());
245        });
246    }
247
248    #[test]
249    fn test_arch_sorting_for_os() {
250        let x86 = Arch::from_str("x86").expect("valid architecture");
251        let windows = Os::from_str("windows").expect("valid operating system");
252        let linux = Os::from_str("linux").expect("valid operating system");
253
254        for (os, native, expected) in [
255            (windows, aarch64(), [aarch64(), x86_64(), x86]),
256            (windows, x86_64(), [x86_64(), x86, aarch64()]),
257            (windows, x86, [x86, aarch64(), x86_64()]),
258            (linux, aarch64(), [aarch64(), x86, x86_64()]),
259            (linux, x86_64(), [x86_64(), aarch64(), x86]),
260            (linux, x86, [x86, aarch64(), x86_64()]),
261        ] {
262            let mut architectures = [x86, x86_64(), aarch64()];
263            architectures.sort_by(|left, right| left.cmp_for_os(*right, os, native));
264            assert_eq!(architectures, expected, "{os} on {native}");
265        }
266    }
267}
268
269#[cfg(test)]
270pub(crate) mod test_support {
271    use super::*;
272    use std::cell::RefCell;
273
274    thread_local! {
275        static MOCK_ARCH: RefCell<Option<Arch>> = const { RefCell::new(None) };
276    }
277
278    pub(super) fn get_mock_arch() -> Option<Arch> {
279        MOCK_ARCH.with(|arch| *arch.borrow())
280    }
281
282    fn set_mock_arch(arch: Option<Arch>) {
283        MOCK_ARCH.with(|mock| *mock.borrow_mut() = arch);
284    }
285
286    struct MockArchGuard {
287        previous: Option<Arch>,
288    }
289
290    impl MockArchGuard {
291        fn new(arch: Arch) -> Self {
292            let previous = get_mock_arch();
293            set_mock_arch(Some(arch));
294            Self { previous }
295        }
296    }
297
298    impl Drop for MockArchGuard {
299        fn drop(&mut self) {
300            set_mock_arch(self.previous);
301        }
302    }
303
304    /// Run a function with a mocked architecture.
305    /// The mock is automatically cleaned up after the function returns.
306    pub(crate) fn run_with_arch<F, R>(arch: Arch, f: F) -> R
307    where
308        F: FnOnce() -> R,
309    {
310        let _guard = MockArchGuard::new(arch);
311        f()
312    }
313
314    pub(crate) fn x86_64() -> Arch {
315        Arch::new(target_lexicon::Architecture::X86_64, None)
316    }
317
318    pub(crate) fn aarch64() -> Arch {
319        Arch::new(
320            target_lexicon::Architecture::Aarch64(target_lexicon::Aarch64Architecture::Aarch64),
321            None,
322        )
323    }
324}