rattler_virtual_packages/
lib.rs

1#![deny(missing_docs)]
2
3//! A library to detect Conda virtual packages present on a system.
4//!
5//! A virtual package represents a package that is injected into the solver to
6//! provide system information to packages. This allows packages to add
7//! dependencies on specific system features, like the platform version, the
8//! machines architecture, or the availability of a Cuda driver with a specific
9//! version.
10//!
11//! This library provides both a low- and high level API to detect versions of
12//! virtual packages for the host system.
13//!
14//! To detect all virtual packages for the host system use the
15//! [`VirtualPackage::detect`] method which will return a memoized slice of all
16//! detected virtual packages. The `VirtualPackage` enum represents all
17//! available virtual package types. Using it provides some flexibility to the
18//! user to not care about which exact virtual packages exist but still allows
19//! users to override specific virtual package behavior. Say for instance you
20//! just want to detect the capabilities of the host system but you still want
21//! to restrict the targeted linux version. You can convert an instance of
22//! `VirtualPackage` to `GenericVirtualPackage` which erases any typing for
23//! specific virtual packages.
24//!
25//! Each virtual package is also represented by a struct which can be used to
26//! detect the specifics of one virtual package. For instance the
27//! [`Linux::current`] method returns an instance of `Linux` which contains the
28//! current Linux version. It also provides conversions to the higher level API.
29//!
30//! Finally at the core of the library are detection functions to perform
31//! specific capability detections that are not tied to anything related to
32//! virtual packages. See [`cuda::detect_cuda_version_via_libcuda`] as an
33//! example.
34
35pub mod cuda;
36pub mod libc;
37pub mod linux;
38pub mod osx;
39pub mod win;
40
41use std::{
42    borrow::Cow,
43    env, fmt,
44    fmt::Display,
45    hash::{Hash, Hasher},
46    str::FromStr,
47    sync::Arc,
48};
49
50use archspec::cpu::Microarchitecture;
51use libc::DetectLibCError;
52use linux::ParseLinuxVersionError;
53use rattler_conda_types::{
54    GenericVirtualPackage, PackageName, ParseVersionError, Platform, Version,
55};
56use serde::{Deserialize, Deserializer, Serialize, Serializer};
57
58use crate::osx::ParseOsxVersionError;
59
60/// Configure the overrides used in in this crate.
61#[derive(Clone, Debug, PartialEq, Default)]
62pub enum Override {
63    /// Use the default override env var name
64    #[default]
65    DefaultEnvVar,
66    /// Use custom env var name
67    EnvVar(String),
68    /// Use a custom override directly
69    String(String),
70}
71
72/// Traits for overridable virtual packages
73/// Use as `Cuda::detect(override)`
74pub trait EnvOverride: Sized {
75    /// Parse `env_var_value`
76    fn parse_version(value: &str) -> Result<Self, ParseVersionError>;
77
78    /// Helper to convert the output of `parse_version` and handling empty
79    /// strings.
80    fn parse_version_opt(value: &str) -> Result<Option<Self>, DetectVirtualPackageError> {
81        if value.is_empty() {
82            Ok(None)
83        } else {
84            Ok(Some(Self::parse_version(value)?))
85        }
86    }
87
88    /// Read the environment variable and if it exists, try to parse it with
89    /// [`EnvOverride::parse_version`] If the output is:
90    /// - `None`, then the environment variable did not exist,
91    /// - `Some(Err(None))`, then the environment variable exist but was set to
92    ///   zero, so the package should be disabled
93    /// - `Some(Ok(pkg))`, then the override was for the package.
94    fn from_env_var_name_or<F>(
95        env_var_name: &str,
96        fallback: F,
97    ) -> Result<Option<Self>, DetectVirtualPackageError>
98    where
99        F: FnOnce() -> Result<Option<Self>, DetectVirtualPackageError>,
100    {
101        match env::var(env_var_name) {
102            Ok(var) => Self::parse_version_opt(&var),
103            Err(env::VarError::NotPresent) => fallback(),
104            Err(e) => Err(DetectVirtualPackageError::VarError(e)),
105        }
106    }
107
108    /// Default name of the environment variable that overrides the virtual
109    /// package.
110    const DEFAULT_ENV_NAME: &'static str;
111
112    /// Detect the virtual package for the current system.
113    /// This method is here so that `<Self as EnvOverride>::current` always
114    /// returns the same error type. `current` may return different types of
115    /// errors depending on the virtual package. This one always returns
116    /// `DetectVirtualPackageError`.
117    fn detect_from_host() -> Result<Option<Self>, DetectVirtualPackageError>;
118
119    /// Apply the override to the current virtual package. If the override is
120    /// `None` then use the fallback
121    fn detect_with_fallback<F>(
122        ov: &Override,
123        fallback: F,
124    ) -> Result<Option<Self>, DetectVirtualPackageError>
125    where
126        F: FnOnce() -> Result<Option<Self>, DetectVirtualPackageError>,
127    {
128        match ov {
129            Override::String(str) => Self::parse_version_opt(str),
130            Override::DefaultEnvVar => Self::from_env_var_name_or(Self::DEFAULT_ENV_NAME, fallback),
131            Override::EnvVar(name) => Self::from_env_var_name_or(name, fallback),
132        }
133    }
134
135    /// Shortcut for `Self::detect_with_fallback` with `Self::detect_from_host`
136    /// as fallback
137    fn detect(ov: Option<&Override>) -> Result<Option<Self>, DetectVirtualPackageError> {
138        ov.map_or_else(Self::detect_from_host, |ov| {
139            Self::detect_with_fallback(ov, Self::detect_from_host)
140        })
141    }
142}
143
144/// An enum that represents all virtual package types provided by this library.
145#[derive(Clone, Eq, PartialEq, Hash, Debug)]
146pub enum VirtualPackage {
147    /// Available on windows
148    Win(Windows),
149
150    /// Available on `Unix` based platforms
151    Unix,
152
153    /// Available when running on `Linux`
154    Linux(Linux),
155
156    /// Available when running on `OSX`
157    Osx(Osx),
158
159    /// Available `LibC` family and version
160    LibC(LibC),
161
162    /// Available `Cuda` version
163    Cuda(Cuda),
164
165    /// The CPU architecture
166    Archspec(Archspec),
167}
168
169/// A struct that represents all virtual packages provided by this library.
170#[derive(Debug, Clone, Default)]
171pub struct VirtualPackages {
172    /// Available on windows
173    pub win: Option<Windows>,
174
175    /// Available on `Unix` based platforms
176    pub unix: bool,
177
178    /// Available when running on `Linux`
179    pub linux: Option<Linux>,
180
181    /// Available when running on `OSX`
182    pub osx: Option<Osx>,
183
184    /// Available `LibC` family and version
185    pub libc: Option<LibC>,
186
187    /// Available `Cuda` version
188    pub cuda: Option<Cuda>,
189
190    /// The CPU architecture
191    pub archspec: Option<Archspec>,
192}
193
194impl VirtualPackages {
195    /// Convert this struct into an iterator of [`VirtualPackage`].
196    pub fn into_virtual_packages(self) -> impl Iterator<Item = VirtualPackage> {
197        let Self {
198            win,
199            unix,
200            linux,
201            osx,
202            libc,
203            cuda,
204            archspec,
205        } = self;
206
207        [
208            win.map(VirtualPackage::Win),
209            unix.then_some(VirtualPackage::Unix),
210            linux.map(VirtualPackage::Linux),
211            osx.map(VirtualPackage::Osx),
212            libc.map(VirtualPackage::LibC),
213            cuda.map(VirtualPackage::Cuda),
214            archspec.map(VirtualPackage::Archspec),
215        ]
216        .into_iter()
217        .flatten()
218    }
219
220    /// Convert this struct into an iterator of [`GenericVirtualPackage`].
221    pub fn into_generic_virtual_packages(self) -> impl Iterator<Item = GenericVirtualPackage> {
222        self.into_virtual_packages().map(Into::into)
223    }
224
225    /// Detect the virtual packages of the current system with the given
226    /// overrides.
227    pub fn detect(overrides: &VirtualPackageOverrides) -> Result<Self, DetectVirtualPackageError> {
228        Ok(Self {
229            win: Windows::detect(overrides.win.as_ref())?,
230            unix: Platform::current().is_unix(),
231            linux: Linux::detect(overrides.linux.as_ref())?,
232            osx: Osx::detect(overrides.osx.as_ref())?,
233            libc: LibC::detect(overrides.libc.as_ref())?,
234            cuda: Cuda::detect(overrides.cuda.as_ref())?,
235            archspec: Archspec::detect(overrides.archspec.as_ref())?,
236        })
237    }
238
239    /// Detect the virtual packages for a given platform. This will detect the
240    /// "native" packages for the platform if possible, and otherwise fall back to
241    /// some reasonable defaults when cross-compiling.
242    ///
243    /// Overrides are always respected, even when cross-compiling to a different platform.
244    /// This matches the behavior of conda, where environment variables like `CONDA_OVERRIDE_OSX`
245    /// are used even when targeting osx-* from a non-macOS machine.
246    ///
247    /// # Cross-compilation defaults
248    ///
249    /// When cross-compiling (targeting a platform different from the current one), the following
250    /// defaults are used if no override is provided:
251    ///
252    /// - **Windows** (`__win`): No version specified
253    /// - **Linux** (`__linux`): Version 0
254    /// - **OSX** (`__osx`): Version 0
255    /// - **`LibC`** (`__glibc`): `glibc` with version 0 (only for Linux platforms)
256    /// - **CUDA** (`__cuda`): Not included (None)
257    /// - **Archspec**: Platform-specific minimal architecture (e.g., `x86_64` for `osx-64`)
258    pub fn detect_for_platform(
259        platform: Platform,
260        overrides: &VirtualPackageOverrides,
261    ) -> Result<Self, DetectVirtualPackageError> {
262        let virtual_packages = Self::detect(overrides)?;
263        if platform == Platform::current() {
264            // If we're targeting the current platform, just return the detected packages
265            Ok(virtual_packages)
266        } else {
267            // When cross-compiling, respect overrides but fall back to defaults
268            let win = if platform.is_windows() {
269                // Check override first, fall back to default (no version)
270                virtual_packages
271                    .win
272                    .or_else(|| Some(Windows { version: None }))
273            } else {
274                None
275            };
276
277            let linux = if platform.is_linux() {
278                virtual_packages.linux.or_else(|| {
279                    Some(Linux {
280                        version: Version::major(0),
281                    })
282                })
283            } else {
284                None
285            };
286
287            let osx = if platform.is_osx() {
288                // Check override first, fall back to version 0
289                virtual_packages.osx.or_else(|| {
290                    Some(Osx {
291                        version: Version::major(0),
292                    })
293                })
294            } else {
295                None
296            };
297
298            let libc = if platform.is_linux() {
299                // Check override first, fall back to glibc 0
300                virtual_packages.libc.or_else(|| {
301                    Some(LibC {
302                        family: "glibc".into(),
303                        version: Version::major(0),
304                    })
305                })
306            } else {
307                None
308            };
309
310            let archspec = Archspec::detect_with_fallback(
311                overrides
312                    .archspec
313                    .as_ref()
314                    .unwrap_or(&Override::DefaultEnvVar),
315                || Ok(Archspec::from_platform(platform)),
316            )?;
317
318            Ok(Self {
319                win,
320                unix: platform.is_unix(),
321                linux,
322                osx,
323                libc,
324                cuda: virtual_packages.cuda,
325                archspec,
326            })
327        }
328    }
329}
330
331impl From<VirtualPackage> for GenericVirtualPackage {
332    fn from(package: VirtualPackage) -> Self {
333        match package {
334            VirtualPackage::Unix => GenericVirtualPackage {
335                name: PackageName::new_unchecked("__unix"),
336                version: Version::major(0),
337                build_string: "0".into(),
338            },
339            VirtualPackage::Win(windows) => windows.into(),
340            VirtualPackage::Linux(linux) => linux.into(),
341            VirtualPackage::Osx(osx) => osx.into(),
342            VirtualPackage::LibC(libc) => libc.into(),
343            VirtualPackage::Cuda(cuda) => cuda.into(),
344            VirtualPackage::Archspec(spec) => spec.into(),
345        }
346    }
347}
348
349impl VirtualPackage {
350    /// Returns virtual packages detected for the current system or an error if
351    /// the versions could not be properly detected.
352    #[deprecated(
353        since = "1.1.0",
354        note = "Use `VirtualPackage::detect(&VirtualPackageOverrides::default())` instead."
355    )]
356    pub fn current() -> Result<Vec<Self>, DetectVirtualPackageError> {
357        Self::detect(&VirtualPackageOverrides::default())
358    }
359
360    /// Detect the virtual packages of the current system with the given
361    /// overrides.
362    pub fn detect(
363        overrides: &VirtualPackageOverrides,
364    ) -> Result<Vec<Self>, DetectVirtualPackageError> {
365        Ok(VirtualPackages::detect(overrides)?
366            .into_virtual_packages()
367            .collect())
368    }
369}
370
371/// An error that might be returned by [`VirtualPackage::current`].
372#[derive(Debug, thiserror::Error)]
373#[allow(missing_docs)]
374pub enum DetectVirtualPackageError {
375    #[error(transparent)]
376    ParseLinuxVersion(#[from] ParseLinuxVersionError),
377
378    #[error(transparent)]
379    ParseMacOsVersion(#[from] ParseOsxVersionError),
380
381    #[error(transparent)]
382    DetectLibC(#[from] DetectLibCError),
383
384    #[error(transparent)]
385    VarError(#[from] env::VarError),
386
387    #[error(transparent)]
388    VersionParseError(#[from] ParseVersionError),
389}
390/// Configure the overrides used in this crate.
391///
392/// The default value is `None` for all overrides which means that by default
393/// none of the virtual packages are overridden.
394///
395/// Use `VirtualPackageOverrides::from_env()` to create an instance of this
396/// struct with all overrides set to the default environment variables.
397#[derive(Default, Clone, Debug)]
398pub struct VirtualPackageOverrides {
399    /// The override for the win virtual package
400    pub win: Option<Override>,
401    /// The override for the osx virtual package
402    pub osx: Option<Override>,
403    /// The override for the linux virtual package
404    pub linux: Option<Override>,
405    /// The override for the libc virtual package
406    pub libc: Option<Override>,
407    /// The override for the cuda virtual package
408    pub cuda: Option<Override>,
409    /// The override for the archspec virtual package
410    pub archspec: Option<Override>,
411}
412
413impl VirtualPackageOverrides {
414    /// Returns an instance of `VirtualPackageOverrides` with all overrides set
415    /// to a given value.
416    pub fn all(ov: Override) -> Self {
417        Self {
418            win: Some(ov.clone()),
419            osx: Some(ov.clone()),
420            linux: Some(ov.clone()),
421            libc: Some(ov.clone()),
422            cuda: Some(ov.clone()),
423            archspec: Some(ov),
424        }
425    }
426
427    /// Returns an instance of `VirtualPackageOverrides` where all overrides are
428    /// taken from default environment variables.
429    pub fn from_env() -> Self {
430        Self::all(Override::DefaultEnvVar)
431    }
432}
433
434/// Linux virtual package description
435#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize)]
436pub struct Linux {
437    /// The version of linux
438    pub version: Version,
439}
440
441impl Linux {
442    /// Returns the Linux version of the current platform.
443    ///
444    /// Returns an error if determining the Linux version resulted in an error.
445    /// Returns `None` if the current platform is not a Linux based
446    /// platform.
447    pub fn current() -> Result<Option<Self>, ParseLinuxVersionError> {
448        Ok(linux::linux_version()?.map(|version| Self { version }))
449    }
450}
451
452impl From<Linux> for GenericVirtualPackage {
453    fn from(linux: Linux) -> Self {
454        GenericVirtualPackage {
455            name: PackageName::new_unchecked("__linux"),
456            version: linux.version,
457            build_string: "0".into(),
458        }
459    }
460}
461
462impl From<Linux> for VirtualPackage {
463    fn from(linux: Linux) -> Self {
464        VirtualPackage::Linux(linux)
465    }
466}
467
468impl From<Version> for Linux {
469    fn from(version: Version) -> Self {
470        Linux { version }
471    }
472}
473
474impl EnvOverride for Linux {
475    const DEFAULT_ENV_NAME: &'static str = "CONDA_OVERRIDE_LINUX";
476
477    fn parse_version(env_var_value: &str) -> Result<Self, ParseVersionError> {
478        Version::from_str(env_var_value).map(Self::from)
479    }
480
481    fn detect_from_host() -> Result<Option<Self>, DetectVirtualPackageError> {
482        Ok(Self::current()?)
483    }
484}
485
486/// `LibC` virtual package description
487#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize)]
488pub struct LibC {
489    /// The family of `LibC`. This could be glibc for instance.
490    pub family: String,
491
492    /// The version of the libc distribution.
493    pub version: Version,
494}
495
496impl LibC {
497    /// Returns the `LibC` family and version of the current platform.
498    ///
499    /// Returns an error if determining the `LibC` family and version resulted
500    /// in an error. Returns `None` if the current platform does not have an
501    /// available version of `LibC`.
502    pub fn current() -> Result<Option<Self>, DetectLibCError> {
503        Ok(libc::libc_family_and_version()?.map(|(family, version)| Self { family, version }))
504    }
505}
506
507#[allow(clippy::fallible_impl_from)]
508impl From<LibC> for GenericVirtualPackage {
509    fn from(libc: LibC) -> Self {
510        GenericVirtualPackage {
511            // TODO: Convert the family to a valid package name. We can simply replace invalid
512            // characters.
513            name: format!("__{}", libc.family.to_lowercase())
514                .try_into()
515                .unwrap(),
516            version: libc.version,
517            build_string: "0".into(),
518        }
519    }
520}
521
522impl From<LibC> for VirtualPackage {
523    fn from(libc: LibC) -> Self {
524        VirtualPackage::LibC(libc)
525    }
526}
527
528impl EnvOverride for LibC {
529    const DEFAULT_ENV_NAME: &'static str = "CONDA_OVERRIDE_GLIBC";
530
531    fn parse_version(env_var_value: &str) -> Result<Self, ParseVersionError> {
532        Version::from_str(env_var_value).map(|version| Self {
533            family: "glibc".into(),
534            version,
535        })
536    }
537
538    fn detect_from_host() -> Result<Option<Self>, DetectVirtualPackageError> {
539        Ok(Self::current()?)
540    }
541}
542
543impl fmt::Display for LibC {
544    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
545        write!(f, "{}={}", self.family, self.version)
546    }
547}
548
549/// Cuda virtual package description
550#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize)]
551pub struct Cuda {
552    /// The maximum supported Cuda version.
553    pub version: Version,
554}
555
556impl Cuda {
557    /// Returns the maximum Cuda version available on the current platform.
558    pub fn current() -> Option<Self> {
559        cuda::cuda_version().map(|version| Self { version })
560    }
561}
562
563impl From<Version> for Cuda {
564    fn from(version: Version) -> Self {
565        Self { version }
566    }
567}
568
569impl EnvOverride for Cuda {
570    fn parse_version(env_var_value: &str) -> Result<Self, ParseVersionError> {
571        Version::from_str(env_var_value).map(|version| Self { version })
572    }
573    fn detect_from_host() -> Result<Option<Self>, DetectVirtualPackageError> {
574        Ok(Self::current())
575    }
576    const DEFAULT_ENV_NAME: &'static str = "CONDA_OVERRIDE_CUDA";
577}
578
579impl From<Cuda> for GenericVirtualPackage {
580    fn from(cuda: Cuda) -> Self {
581        GenericVirtualPackage {
582            name: PackageName::new_unchecked("__cuda"),
583            version: cuda.version,
584            build_string: "0".into(),
585        }
586    }
587}
588
589impl From<Cuda> for VirtualPackage {
590    fn from(cuda: Cuda) -> Self {
591        VirtualPackage::Cuda(cuda)
592    }
593}
594
595/// Archspec describes the CPU architecture
596#[derive(Clone, Debug)]
597pub enum Archspec {
598    /// A microarchitecture from the archspec library.
599    Microarchitecture(Arc<Microarchitecture>),
600
601    /// An unknown microarchitecture
602    Unknown,
603}
604
605impl Serialize for Archspec {
606    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
607    where
608        S: Serializer,
609    {
610        self.as_str().serialize(serializer)
611    }
612}
613
614impl<'de> Deserialize<'de> for Archspec {
615    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
616    where
617        D: Deserializer<'de>,
618    {
619        let name = Cow::<'de, str>::deserialize(deserializer)?;
620        if name == "0" {
621            Ok(Self::Unknown)
622        } else {
623            Ok(Self::from_name(&name))
624        }
625    }
626}
627
628impl Hash for Archspec {
629    fn hash<H: Hasher>(&self, state: &mut H) {
630        self.as_str().hash(state);
631    }
632}
633
634impl PartialEq<Self> for Archspec {
635    fn eq(&self, other: &Self) -> bool {
636        self.as_str() == other.as_str()
637    }
638}
639
640impl Eq for Archspec {}
641
642impl From<Arc<Microarchitecture>> for Archspec {
643    fn from(arch: Arc<Microarchitecture>) -> Self {
644        Self::Microarchitecture(arch)
645    }
646}
647
648impl Display for Archspec {
649    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
650        write!(f, "{}", self.as_str())
651    }
652}
653
654impl Archspec {
655    /// Returns the string representation of the virtual package.
656    pub fn as_str(&self) -> &str {
657        match self {
658            Archspec::Microarchitecture(arch) => arch.name(),
659            Archspec::Unknown => "0",
660        }
661    }
662
663    /// Returns the current CPU architecture or `Archspec::Unknown` if the
664    /// architecture could not be determined.
665    pub fn current() -> Self {
666        archspec::cpu::host()
667            .ok()
668            .map(Into::into)
669            .or_else(|| Self::from_platform(Platform::current()))
670            .unwrap_or(Archspec::Unknown)
671    }
672
673    /// Returns the minimal supported archspec architecture for the given
674    /// platform.
675    #[allow(clippy::match_same_arms)]
676    pub fn from_platform(platform: Platform) -> Option<Self> {
677        // The values are taken from the archspec-json library.
678        // See: https://github.com/archspec/archspec-json/blob/master/cpu/microarchitectures.json
679        let archspec_name = match platform {
680            Platform::NoArch | Platform::Unknown => return None,
681            Platform::EmscriptenWasm32 | Platform::WasiWasm32 => return None,
682            Platform::Win32 | Platform::Linux32 => "x86",
683            Platform::Win64 | Platform::Osx64 | Platform::Linux64 => "x86_64",
684            Platform::LinuxAarch64 | Platform::LinuxArmV6l | Platform::LinuxArmV7l => "aarch64",
685            Platform::LinuxLoong64 => "loong64",
686            Platform::LinuxPpc64le => "ppc64le",
687            Platform::LinuxPpc64 => "ppc64",
688            Platform::LinuxPpc => "ppc",
689            Platform::LinuxS390X => "s390x",
690            Platform::LinuxRiscv32 => "riscv32",
691            Platform::LinuxRiscv64 => "riscv64",
692            // IBM Zos is a special case. It is not supported by archspec as far as I can see.
693            Platform::ZosZ => return None,
694
695            // TODO: There must be a minimal aarch64 version that windows supports.
696            Platform::WinArm64 => "aarch64",
697
698            // The first every Apple Silicon Macs are based on m1.
699            Platform::OsxArm64 => "m1",
700
701            // Otherwise, we assume that the architecture is unknown.
702            _ => return None,
703        };
704
705        Some(Self::from_name(archspec_name))
706    }
707
708    /// Constructs an `Archspec` from the given `archspec_name`. Creates a
709    /// "generic" architecture if the name is not known.
710    pub fn from_name(archspec_name: &str) -> Self {
711        Microarchitecture::known_targets()
712            .get(archspec_name)
713            .cloned()
714            .unwrap_or_else(|| Arc::new(archspec::cpu::Microarchitecture::generic(archspec_name)))
715            .into()
716    }
717}
718
719impl From<Archspec> for GenericVirtualPackage {
720    fn from(archspec: Archspec) -> Self {
721        GenericVirtualPackage {
722            name: PackageName::new_unchecked("__archspec"),
723            version: Version::major(1),
724            build_string: archspec.to_string(),
725        }
726    }
727}
728
729impl From<Archspec> for VirtualPackage {
730    fn from(archspec: Archspec) -> Self {
731        VirtualPackage::Archspec(archspec)
732    }
733}
734
735impl EnvOverride for Archspec {
736    fn parse_version(value: &str) -> Result<Self, ParseVersionError> {
737        if value == "0" {
738            Ok(Archspec::Unknown)
739        } else {
740            Ok(Self::from_name(value))
741        }
742    }
743
744    const DEFAULT_ENV_NAME: &'static str = "CONDA_OVERRIDE_ARCHSPEC";
745
746    fn detect_from_host() -> Result<Option<Self>, DetectVirtualPackageError> {
747        Ok(Some(Self::current()))
748    }
749}
750
751/// OSX virtual package description
752#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize)]
753pub struct Osx {
754    /// The OSX version
755    pub version: Version,
756}
757
758impl Osx {
759    /// Returns the OSX version of the current platform.
760    ///
761    /// Returns an error if determining the OSX version resulted in an error.
762    /// Returns `None` if the current platform is not an OSX based platform.
763    pub fn current() -> Result<Option<Self>, ParseOsxVersionError> {
764        Ok(osx::osx_version()?.map(|version| Self { version }))
765    }
766}
767
768impl From<Osx> for GenericVirtualPackage {
769    fn from(osx: Osx) -> Self {
770        GenericVirtualPackage {
771            name: PackageName::new_unchecked("__osx"),
772            version: osx.version,
773            build_string: "0".into(),
774        }
775    }
776}
777
778impl From<Osx> for VirtualPackage {
779    fn from(osx: Osx) -> Self {
780        VirtualPackage::Osx(osx)
781    }
782}
783
784impl From<Version> for Osx {
785    fn from(version: Version) -> Self {
786        Self { version }
787    }
788}
789
790impl EnvOverride for Osx {
791    fn parse_version(env_var_value: &str) -> Result<Self, ParseVersionError> {
792        Version::from_str(env_var_value).map(|version| Self { version })
793    }
794    fn detect_from_host() -> Result<Option<Self>, DetectVirtualPackageError> {
795        Ok(Self::current()?)
796    }
797    const DEFAULT_ENV_NAME: &'static str = "CONDA_OVERRIDE_OSX";
798}
799
800/// Windows virtual package description
801#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize)]
802pub struct Windows {
803    /// The version of windows
804    pub version: Option<Version>,
805}
806
807impl Windows {
808    /// Returns the Windows version of the current platform.
809    ///
810    /// Returns `None` if the current platform is not a Windows based platform.
811    pub fn current() -> Option<Self> {
812        if cfg!(target_os = "windows") {
813            Some(Self {
814                version: win::windows_version(),
815            })
816        } else {
817            None
818        }
819    }
820}
821
822impl From<Windows> for GenericVirtualPackage {
823    fn from(windows: Windows) -> Self {
824        GenericVirtualPackage {
825            name: PackageName::new_unchecked("__win"),
826            version: windows.version.unwrap_or_else(|| Version::major(0)),
827            build_string: "0".into(),
828        }
829    }
830}
831
832impl From<Windows> for VirtualPackage {
833    fn from(windows: Windows) -> Self {
834        VirtualPackage::Win(windows)
835    }
836}
837
838impl From<Version> for Windows {
839    fn from(version: Version) -> Self {
840        Self {
841            version: Some(version),
842        }
843    }
844}
845
846impl EnvOverride for Windows {
847    fn parse_version(env_var_value: &str) -> Result<Self, ParseVersionError> {
848        Version::from_str(env_var_value).map(|version| Self {
849            version: Some(version),
850        })
851    }
852    fn detect_from_host() -> Result<Option<Self>, DetectVirtualPackageError> {
853        Ok(Self::current())
854    }
855    const DEFAULT_ENV_NAME: &'static str = "CONDA_OVERRIDE_WIN";
856}
857
858#[cfg(test)]
859mod test {
860    use std::{env, str::FromStr};
861
862    use rattler_conda_types::Version;
863
864    use super::*;
865
866    #[test]
867    fn doesnt_crash() {
868        let virtual_packages =
869            VirtualPackages::detect(&VirtualPackageOverrides::default()).unwrap();
870        println!("{virtual_packages:#?}");
871    }
872    #[test]
873    fn parse_libc() {
874        let v = "1.23";
875        let res = LibC {
876            version: Version::from_str(v).unwrap(),
877            family: "glibc".into(),
878        };
879        let env_var_name = format!("{}_{}", LibC::DEFAULT_ENV_NAME, "12345511231");
880        env::set_var(env_var_name.clone(), v);
881        assert_eq!(
882            LibC::detect(Some(&Override::EnvVar(env_var_name.clone())))
883                .unwrap()
884                .unwrap(),
885            res
886        );
887        env::set_var(env_var_name.clone(), "");
888        assert_eq!(
889            LibC::detect(Some(&Override::EnvVar(env_var_name.clone()))).unwrap(),
890            None
891        );
892        env::remove_var(env_var_name.clone());
893        assert_eq!(
894            LibC::detect_with_fallback(&Override::DefaultEnvVar, || Ok(Some(res.clone())))
895                .unwrap()
896                .unwrap(),
897            res
898        );
899        assert_eq!(
900            LibC::detect_with_fallback(&Override::String(v.to_string()), || Ok(None))
901                .unwrap()
902                .unwrap(),
903            res
904        );
905    }
906
907    #[test]
908    fn parse_cuda() {
909        let v = "1.234";
910        let res = Cuda {
911            version: Version::from_str(v).unwrap(),
912        };
913        let env_var_name = format!("{}_{}", Cuda::DEFAULT_ENV_NAME, "12345511231");
914        env::set_var(env_var_name.clone(), v);
915        assert_eq!(
916            Cuda::detect(Some(&Override::EnvVar(env_var_name.clone())))
917                .unwrap()
918                .unwrap(),
919            res
920        );
921        assert_eq!(
922            Cuda::detect(None).map_err(|_x| 1),
923            <Cuda as EnvOverride>::detect_from_host().map_err(|_x| 1)
924        );
925        env::remove_var(env_var_name.clone());
926        assert_eq!(
927            Cuda::detect(Some(&Override::String(v.to_string())))
928                .unwrap()
929                .unwrap(),
930            res
931        );
932    }
933
934    #[test]
935    fn parse_osx() {
936        let v = "2.345";
937        let res = Osx {
938            version: Version::from_str(v).unwrap(),
939        };
940        let env_var_name = format!("{}_{}", Osx::DEFAULT_ENV_NAME, "12345511231");
941        env::set_var(env_var_name.clone(), v);
942        assert_eq!(
943            Osx::detect(Some(&Override::EnvVar(env_var_name.clone())))
944                .unwrap()
945                .unwrap(),
946            res
947        );
948    }
949
950    #[test]
951    fn test_cross_platform_virtual_packages() {
952        // Test that cross-platform detection works for different platforms
953        let overrides = VirtualPackageOverrides::default();
954
955        // Test Linux 64-bit
956        let linux_packages =
957            VirtualPackages::detect_for_platform(Platform::Linux64, &overrides).unwrap();
958        let linux_names: Vec<String> = linux_packages
959            .into_generic_virtual_packages()
960            .map(|pkg| pkg.name.as_normalized().to_string())
961            .collect();
962        assert!(linux_names.contains(&"__linux".to_string()));
963        assert!(linux_names.contains(&"__glibc".to_string()));
964        assert!(linux_names.contains(&"__archspec".to_string()));
965        assert!(linux_names.contains(&"__unix".to_string()));
966
967        // Test macOS ARM64
968        let osx_packages =
969            VirtualPackages::detect_for_platform(Platform::OsxArm64, &overrides).unwrap();
970        let osx_names: Vec<String> = osx_packages
971            .into_generic_virtual_packages()
972            .map(|pkg| pkg.name.as_normalized().to_string())
973            .collect();
974        assert!(osx_names.contains(&"__osx".to_string()));
975        assert!(osx_names.contains(&"__archspec".to_string()));
976        assert!(osx_names.contains(&"__unix".to_string()));
977
978        // Test Windows 64-bit
979        let win_packages =
980            VirtualPackages::detect_for_platform(Platform::Win64, &overrides).unwrap();
981        let win_names: Vec<String> = win_packages
982            .into_generic_virtual_packages()
983            .map(|pkg| pkg.name.as_normalized().to_string())
984            .collect();
985        assert!(win_names.contains(&"__win".to_string()));
986        assert!(!win_names.contains(&"__unix".to_string()));
987        assert!(win_names.contains(&"__archspec".to_string()));
988    }
989}