Skip to main content

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            name: format!(
512                "__{}",
513                libc.family.to_lowercase().replace(
514                    |c: char| !c.is_ascii_alphanumeric() && c != '-' && c != '_',
515                    "_"
516                )
517            )
518            .try_into()
519            .unwrap(),
520            version: libc.version,
521            build_string: "0".into(),
522        }
523    }
524}
525
526impl From<LibC> for VirtualPackage {
527    fn from(libc: LibC) -> Self {
528        VirtualPackage::LibC(libc)
529    }
530}
531
532impl EnvOverride for LibC {
533    const DEFAULT_ENV_NAME: &'static str = "CONDA_OVERRIDE_GLIBC";
534
535    fn parse_version(env_var_value: &str) -> Result<Self, ParseVersionError> {
536        Version::from_str(env_var_value).map(|version| Self {
537            family: "glibc".into(),
538            version,
539        })
540    }
541
542    fn detect_from_host() -> Result<Option<Self>, DetectVirtualPackageError> {
543        Ok(Self::current()?)
544    }
545}
546
547impl fmt::Display for LibC {
548    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
549        write!(f, "{}={}", self.family, self.version)
550    }
551}
552
553/// Cuda virtual package description
554#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize)]
555pub struct Cuda {
556    /// The maximum supported Cuda version.
557    pub version: Version,
558}
559
560impl Cuda {
561    /// Returns the maximum Cuda version available on the current platform.
562    pub fn current() -> Option<Self> {
563        cuda::cuda_version().map(|version| Self { version })
564    }
565}
566
567impl From<Version> for Cuda {
568    fn from(version: Version) -> Self {
569        Self { version }
570    }
571}
572
573impl EnvOverride for Cuda {
574    fn parse_version(env_var_value: &str) -> Result<Self, ParseVersionError> {
575        Version::from_str(env_var_value).map(|version| Self { version })
576    }
577    fn detect_from_host() -> Result<Option<Self>, DetectVirtualPackageError> {
578        Ok(Self::current())
579    }
580    const DEFAULT_ENV_NAME: &'static str = "CONDA_OVERRIDE_CUDA";
581}
582
583impl From<Cuda> for GenericVirtualPackage {
584    fn from(cuda: Cuda) -> Self {
585        GenericVirtualPackage {
586            name: PackageName::new_unchecked("__cuda"),
587            version: cuda.version,
588            build_string: "0".into(),
589        }
590    }
591}
592
593impl From<Cuda> for VirtualPackage {
594    fn from(cuda: Cuda) -> Self {
595        VirtualPackage::Cuda(cuda)
596    }
597}
598
599/// Archspec describes the CPU architecture
600#[derive(Clone, Debug)]
601pub enum Archspec {
602    /// A micro-architecture from the archspec library.
603    Microarchitecture(Arc<Microarchitecture>),
604
605    /// An unknown micro-architecture
606    Unknown,
607}
608
609impl Serialize for Archspec {
610    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
611    where
612        S: Serializer,
613    {
614        self.as_str().serialize(serializer)
615    }
616}
617
618impl<'de> Deserialize<'de> for Archspec {
619    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
620    where
621        D: Deserializer<'de>,
622    {
623        let name = Cow::<'de, str>::deserialize(deserializer)?;
624        if name == "0" {
625            Ok(Self::Unknown)
626        } else {
627            Ok(Self::from_name(&name))
628        }
629    }
630}
631
632impl Hash for Archspec {
633    fn hash<H: Hasher>(&self, state: &mut H) {
634        self.as_str().hash(state);
635    }
636}
637
638impl PartialEq<Self> for Archspec {
639    fn eq(&self, other: &Self) -> bool {
640        self.as_str() == other.as_str()
641    }
642}
643
644impl Eq for Archspec {}
645
646impl From<Arc<Microarchitecture>> for Archspec {
647    fn from(arch: Arc<Microarchitecture>) -> Self {
648        Self::Microarchitecture(arch)
649    }
650}
651
652impl Display for Archspec {
653    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
654        write!(f, "{}", self.as_str())
655    }
656}
657
658impl Archspec {
659    /// Returns the string representation of the virtual package.
660    pub fn as_str(&self) -> &str {
661        match self {
662            Archspec::Microarchitecture(arch) => arch.name(),
663            Archspec::Unknown => "0",
664        }
665    }
666
667    /// Returns the current CPU architecture or `Archspec::Unknown` if the
668    /// architecture could not be determined.
669    pub fn current() -> Self {
670        archspec::cpu::host()
671            .ok()
672            .map(Into::into)
673            .or_else(|| Self::from_platform(Platform::current()))
674            .unwrap_or(Archspec::Unknown)
675    }
676
677    /// Returns the minimal supported archspec architecture for the given
678    /// platform.
679    #[allow(clippy::match_same_arms)]
680    pub fn from_platform(platform: Platform) -> Option<Self> {
681        // The values are taken from the archspec-json library.
682        // See: https://github.com/archspec/archspec-json/blob/master/cpu/microarchitectures.json
683        let archspec_name = match platform {
684            Platform::NoArch | Platform::Unknown => return None,
685            Platform::EmscriptenWasm32 | Platform::WasiWasm32 => return None,
686            Platform::Win32 | Platform::Linux32 => "x86",
687            Platform::Win64 | Platform::Osx64 | Platform::Linux64 => "x86_64",
688            Platform::LinuxAarch64 | Platform::LinuxArmV6l | Platform::LinuxArmV7l => "aarch64",
689            Platform::LinuxLoongArch64 => "loongarch64",
690            Platform::LinuxPpc64le => "ppc64le",
691            Platform::LinuxPpc64 => "ppc64",
692            Platform::LinuxPpc => "ppc",
693            Platform::LinuxS390X => "s390x",
694            Platform::LinuxRiscv32 => "riscv32",
695            Platform::LinuxRiscv64 => "riscv64",
696            // IBM Zos is a special case. It is not supported by archspec as far as I can see.
697            Platform::ZosZ => return None,
698
699            // TODO: There must be a minimal aarch64 version that windows supports.
700            Platform::WinArm64 => "aarch64",
701
702            // The first every Apple Silicon Macs are based on m1.
703            Platform::OsxArm64 => "m1",
704
705            // Otherwise, we assume that the architecture is unknown.
706            _ => return None,
707        };
708
709        Some(Self::from_name(archspec_name))
710    }
711
712    /// Constructs an `Archspec` from the given `archspec_name`. Creates a
713    /// "generic" architecture if the name is not known.
714    pub fn from_name(archspec_name: &str) -> Self {
715        Microarchitecture::known_targets()
716            .get(archspec_name)
717            .cloned()
718            .unwrap_or_else(|| Arc::new(archspec::cpu::Microarchitecture::generic(archspec_name)))
719            .into()
720    }
721}
722
723impl From<Archspec> for GenericVirtualPackage {
724    fn from(archspec: Archspec) -> Self {
725        GenericVirtualPackage {
726            name: PackageName::new_unchecked("__archspec"),
727            version: Version::major(1),
728            build_string: archspec.to_string(),
729        }
730    }
731}
732
733impl From<Archspec> for VirtualPackage {
734    fn from(archspec: Archspec) -> Self {
735        VirtualPackage::Archspec(archspec)
736    }
737}
738
739impl EnvOverride for Archspec {
740    fn parse_version(value: &str) -> Result<Self, ParseVersionError> {
741        if value == "0" {
742            Ok(Archspec::Unknown)
743        } else {
744            Ok(Self::from_name(value))
745        }
746    }
747
748    const DEFAULT_ENV_NAME: &'static str = "CONDA_OVERRIDE_ARCHSPEC";
749
750    fn detect_from_host() -> Result<Option<Self>, DetectVirtualPackageError> {
751        Ok(Some(Self::current()))
752    }
753}
754
755/// OSX virtual package description
756#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize)]
757pub struct Osx {
758    /// The OSX version
759    pub version: Version,
760}
761
762impl Osx {
763    /// Returns the OSX version of the current platform.
764    ///
765    /// Returns an error if determining the OSX version resulted in an error.
766    /// Returns `None` if the current platform is not an OSX based platform.
767    pub fn current() -> Result<Option<Self>, ParseOsxVersionError> {
768        Ok(osx::osx_version()?.map(|version| Self { version }))
769    }
770}
771
772impl From<Osx> for GenericVirtualPackage {
773    fn from(osx: Osx) -> Self {
774        GenericVirtualPackage {
775            name: PackageName::new_unchecked("__osx"),
776            version: osx.version,
777            build_string: "0".into(),
778        }
779    }
780}
781
782impl From<Osx> for VirtualPackage {
783    fn from(osx: Osx) -> Self {
784        VirtualPackage::Osx(osx)
785    }
786}
787
788impl From<Version> for Osx {
789    fn from(version: Version) -> Self {
790        Self { version }
791    }
792}
793
794impl EnvOverride for Osx {
795    fn parse_version(env_var_value: &str) -> Result<Self, ParseVersionError> {
796        Version::from_str(env_var_value).map(|version| Self { version })
797    }
798    fn detect_from_host() -> Result<Option<Self>, DetectVirtualPackageError> {
799        Ok(Self::current()?)
800    }
801    const DEFAULT_ENV_NAME: &'static str = "CONDA_OVERRIDE_OSX";
802}
803
804/// Windows virtual package description
805#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize)]
806pub struct Windows {
807    /// The version of windows
808    pub version: Option<Version>,
809}
810
811impl Windows {
812    /// Returns the Windows version of the current platform.
813    ///
814    /// Returns `None` if the current platform is not a Windows based platform.
815    pub fn current() -> Option<Self> {
816        if cfg!(target_os = "windows") {
817            Some(Self {
818                version: win::windows_version(),
819            })
820        } else {
821            None
822        }
823    }
824}
825
826impl From<Windows> for GenericVirtualPackage {
827    fn from(windows: Windows) -> Self {
828        GenericVirtualPackage {
829            name: PackageName::new_unchecked("__win"),
830            version: windows.version.unwrap_or_else(|| Version::major(0)),
831            build_string: "0".into(),
832        }
833    }
834}
835
836impl From<Windows> for VirtualPackage {
837    fn from(windows: Windows) -> Self {
838        VirtualPackage::Win(windows)
839    }
840}
841
842impl From<Version> for Windows {
843    fn from(version: Version) -> Self {
844        Self {
845            version: Some(version),
846        }
847    }
848}
849
850impl EnvOverride for Windows {
851    fn parse_version(env_var_value: &str) -> Result<Self, ParseVersionError> {
852        Version::from_str(env_var_value).map(|version| Self {
853            version: Some(version),
854        })
855    }
856    fn detect_from_host() -> Result<Option<Self>, DetectVirtualPackageError> {
857        Ok(Self::current())
858    }
859    const DEFAULT_ENV_NAME: &'static str = "CONDA_OVERRIDE_WIN";
860}
861
862#[cfg(test)]
863mod test {
864    use std::{env, str::FromStr};
865
866    use rattler_conda_types::Version;
867
868    use super::*;
869
870    #[test]
871    fn doesnt_crash() {
872        let virtual_packages =
873            VirtualPackages::detect(&VirtualPackageOverrides::default()).unwrap();
874        println!("{virtual_packages:#?}");
875    }
876    #[test]
877    fn parse_libc() {
878        let v = "1.23";
879        let res = LibC {
880            version: Version::from_str(v).unwrap(),
881            family: "glibc".into(),
882        };
883        let env_var_name = format!("{}_{}", LibC::DEFAULT_ENV_NAME, "12345511231");
884        env::set_var(env_var_name.clone(), v);
885        assert_eq!(
886            LibC::detect(Some(&Override::EnvVar(env_var_name.clone())))
887                .unwrap()
888                .unwrap(),
889            res
890        );
891        env::set_var(env_var_name.clone(), "");
892        assert_eq!(
893            LibC::detect(Some(&Override::EnvVar(env_var_name.clone()))).unwrap(),
894            None
895        );
896        env::remove_var(env_var_name.clone());
897        assert_eq!(
898            LibC::detect_with_fallback(&Override::DefaultEnvVar, || Ok(Some(res.clone())))
899                .unwrap()
900                .unwrap(),
901            res
902        );
903        assert_eq!(
904            LibC::detect_with_fallback(&Override::String(v.to_string()), || Ok(None))
905                .unwrap()
906                .unwrap(),
907            res
908        );
909    }
910
911    #[test]
912    fn parse_libc_invalid_family_chars() {
913        let libc = LibC {
914            family: "glibc 2.34 (Ubuntu)".into(),
915            version: Version::from_str("2.34").unwrap(),
916        };
917        let pkg: GenericVirtualPackage = libc.into();
918        assert_eq!(pkg.name.as_normalized(), "__glibc_2_34__ubuntu_");
919    }
920
921    #[test]
922    fn parse_cuda() {
923        let v = "1.234";
924        let res = Cuda {
925            version: Version::from_str(v).unwrap(),
926        };
927        let env_var_name = format!("{}_{}", Cuda::DEFAULT_ENV_NAME, "12345511231");
928        env::set_var(env_var_name.clone(), v);
929        assert_eq!(
930            Cuda::detect(Some(&Override::EnvVar(env_var_name.clone())))
931                .unwrap()
932                .unwrap(),
933            res
934        );
935        assert_eq!(
936            Cuda::detect(None).map_err(|_x| 1),
937            <Cuda as EnvOverride>::detect_from_host().map_err(|_x| 1)
938        );
939        env::remove_var(env_var_name.clone());
940        assert_eq!(
941            Cuda::detect(Some(&Override::String(v.to_string())))
942                .unwrap()
943                .unwrap(),
944            res
945        );
946    }
947
948    #[test]
949    fn parse_osx() {
950        let v = "2.345";
951        let res = Osx {
952            version: Version::from_str(v).unwrap(),
953        };
954        let env_var_name = format!("{}_{}", Osx::DEFAULT_ENV_NAME, "12345511231");
955        env::set_var(env_var_name.clone(), v);
956        assert_eq!(
957            Osx::detect(Some(&Override::EnvVar(env_var_name.clone())))
958                .unwrap()
959                .unwrap(),
960            res
961        );
962    }
963
964    #[test]
965    fn test_cross_platform_virtual_packages() {
966        // Test that cross-platform detection works for different platforms
967        let overrides = VirtualPackageOverrides::default();
968
969        // Test Linux 64-bit
970        let linux_packages =
971            VirtualPackages::detect_for_platform(Platform::Linux64, &overrides).unwrap();
972        let linux_names: Vec<String> = linux_packages
973            .into_generic_virtual_packages()
974            .map(|pkg| pkg.name.as_normalized().to_string())
975            .collect();
976        assert!(linux_names.contains(&"__linux".to_string()));
977        assert!(linux_names.contains(&"__glibc".to_string()));
978        assert!(linux_names.contains(&"__archspec".to_string()));
979        assert!(linux_names.contains(&"__unix".to_string()));
980
981        // Test macOS ARM64
982        let osx_packages =
983            VirtualPackages::detect_for_platform(Platform::OsxArm64, &overrides).unwrap();
984        let osx_names: Vec<String> = osx_packages
985            .into_generic_virtual_packages()
986            .map(|pkg| pkg.name.as_normalized().to_string())
987            .collect();
988        assert!(osx_names.contains(&"__osx".to_string()));
989        assert!(osx_names.contains(&"__archspec".to_string()));
990        assert!(osx_names.contains(&"__unix".to_string()));
991
992        // Test Windows 64-bit
993        let win_packages =
994            VirtualPackages::detect_for_platform(Platform::Win64, &overrides).unwrap();
995        let win_names: Vec<String> = win_packages
996            .into_generic_virtual_packages()
997            .map(|pkg| pkg.name.as_normalized().to_string())
998            .collect();
999        assert!(win_names.contains(&"__win".to_string()));
1000        assert!(!win_names.contains(&"__unix".to_string()));
1001        assert!(win_names.contains(&"__archspec".to_string()));
1002    }
1003}