From b6cca2bb5da4fa372265ccdbc9b71446597c27b0 Mon Sep 17 00:00:00 2001 From: Tobias Hunger Date: Tue, 9 Jun 2026 13:15:36 +0000 Subject: [PATCH] feat(build): derive c_stdlib variants from system requirements Inject conda-forge c_stdlib/c_stdlib_version build variants from a platform's declared virtual packages (__osx, __glibc) in Workspace::variants(), so build backends pin the minimum OS/libc target and stop emitting wheels tagged for the host OS version (e.g. macosx_26_0). Explicit [workspace.build-variants] entries still win; derivation only fills absent keys. osx-* derives macosx_deployment_target, linux-* derives sysroot; windows and unmatched platforms are skipped. The providers are conda-forge packages, so derivation is gated on the workspace resolving at least one conda-forge channel; the macosx_deployment_target package's activation script sets MACOSX_DEPLOYMENT_TARGET during the build. musl and __cuda are left for a follow-up (TODO marker). Refs: #6175 --- crates/pixi_core/src/workspace/mod.rs | 73 +++++- .../src/workspace/stdlib_variants.rs | 228 ++++++++++++++++++ 2 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 crates/pixi_core/src/workspace/stdlib_variants.rs diff --git a/crates/pixi_core/src/workspace/mod.rs b/crates/pixi_core/src/workspace/mod.rs index ab08564323..90279312ab 100644 --- a/crates/pixi_core/src/workspace/mod.rs +++ b/crates/pixi_core/src/workspace/mod.rs @@ -6,6 +6,7 @@ mod has_project_ref; pub mod registry; mod repodata; mod solve_group; +mod stdlib_variants; pub mod virtual_packages; mod workspace_mut; @@ -56,7 +57,8 @@ use pypi_mapping::{ ChannelName, ProjectDefinedMapping, ProjectDefinedMappingLocation, PurlDerivationMode, }; use rattler_conda_types::{ - Channel, ChannelConfig, GenericVirtualPackage, MatchSpec, PackageName, Platform, Version, + Channel, ChannelConfig, ChannelUrl, GenericVirtualPackage, MatchSpec, PackageName, Platform, + Version, }; use rattler_lock::LockFile; @@ -724,6 +726,34 @@ impl Workspace { } } + // Derive `c_stdlib` variants from the platform's system requirements, + // filling only keys an explicit `[workspace.build-variants]` entry + // didn't already set -- a hand-written variant always wins. The derived + // providers are conda-forge packages, so this resolves the workspace's + // channels and only applies when one of them is conda-forge. + let channel_config = self.channel_config(); + let manifest = &self.workspace.value; + let channel_urls: Vec = manifest + .workspace + .channels + .iter() + .map(|prioritized| &prioritized.channel) + .chain( + manifest + .features + .values() + .filter_map(|feature| feature.channels.as_ref()) + .flatten() + .map(|prioritized| &prioritized.channel), + ) + .filter_map(|channel| channel.clone().into_base_url(&channel_config).ok()) + .collect(); + for (key, value) in stdlib_variants::derive_stdlib_variants(platform, &channel_urls) { + variant_configuration + .entry(key) + .or_insert_with(|| vec![value]); + } + // Collect absolute variant file paths without reading their content. let variant_files = self .workspace @@ -1581,6 +1611,47 @@ mod tests { ); } + /// An explicit `[workspace.build-variants]` entry wins over the value + /// derived from the platform's system requirements, while a key the user + /// did not set (`c_stdlib`) is still filled in from the platform. + #[test] + fn explicit_build_variant_overrides_derived_stdlib() { + let file_contents = r#" + [workspace] + name = "foo" + channels = [] + platforms = ["osx-arm64"] + build-variants = { c_stdlib_version = ["99.0"] } + "#; + let workspace = Workspace::from_str(Path::new("pixi.toml"), file_contents).unwrap(); + + let platform = pixi_manifest::PixiPlatform::new( + pixi_manifest::PixiPlatformName::from_str("mac").unwrap(), + Platform::OsxArm64, + vec![GenericVirtualPackage { + name: "__osx".parse().unwrap(), + version: Version::from_str("13.5").unwrap(), + build_string: "0".to_string(), + }], + ) + .unwrap(); + + let variants = workspace.variants(&platform).unwrap().variant_configuration; + + // Explicit override is kept verbatim, not replaced by the derived 13.5. + assert_eq!( + variants.get("c_stdlib_version"), + Some(&vec![VariantValue::String("99.0".to_string())]) + ); + // The provider key the user didn't set is derived from the platform. + assert_eq!( + variants.get("c_stdlib"), + Some(&vec![VariantValue::String( + "macosx_deployment_target".to_string() + )]) + ); + } + #[test] fn test_mapping_location() { let file_contents = r#" diff --git a/crates/pixi_core/src/workspace/stdlib_variants.rs b/crates/pixi_core/src/workspace/stdlib_variants.rs new file mode 100644 index 0000000000..dd8ad7801e --- /dev/null +++ b/crates/pixi_core/src/workspace/stdlib_variants.rs @@ -0,0 +1,228 @@ +//! Derive conda `c_stdlib` build variants from a platform's system +//! requirements (recorded as declared virtual packages). + +use pixi_manifest::PixiPlatform; +use pixi_utils::variants::VariantValue; +use rattler_conda_types::ChannelUrl; + +/// conda build-variant key naming the C stdlib provider for a build. +const C_STDLIB: &str = "c_stdlib"; +/// conda build-variant key carrying the minimum C stdlib version. +const C_STDLIB_VERSION: &str = "c_stdlib_version"; +/// conda-forge channel name. The providers below are conda-forge packages, so +/// the derivation only applies when the workspace builds against this channel. +const CONDA_FORGE: &str = "conda-forge"; + +/// Whether any resolved channel is conda-forge. +/// +/// Matched on the final non-empty path segment of the resolved channel URL, so +/// both the bare name `conda-forge` and full forms like +/// `https://prefix.dev/conda-forge` count. +fn channels_target_conda_forge<'a>(channels: impl IntoIterator) -> bool { + channels.into_iter().any(|channel| { + channel + .url() + .path_segments() + .and_then(|mut segments| segments.rfind(|segment| !segment.is_empty())) + == Some(CONDA_FORGE) + }) +} + +/// Translate a platform's declared system requirements into the +/// `c_stdlib`/`c_stdlib_version` build-variant pair. +/// +/// The variant *keys* are generic conda build-variant keys; only the providers +/// they map to (`macosx_deployment_target`, `sysroot`) are conda-forge +/// conventions. Build backends pin the minimum OS/libc target through the +/// `stdlib('c')` recipe function, which resolves against these two variants. +/// Pixi already records the target as virtual packages on the [`PixiPlatform`] +/// (`__osx` / `__glibc`), so we derive the variant pair from those instead of +/// making users spell it out by hand in `[workspace.build-variants]`: +/// +/// | subdir | virtual package | `c_stdlib` | `c_stdlib_version` | +/// |-----------|-----------------|----------------------------|-----------------------| +/// | `osx-*` | `__osx` | `macosx_deployment_target` | the `__osx` version | +/// | `linux-*` | `__glibc` | `sysroot` | the `__glibc` version | +/// +/// Because the providers are conda-forge packages, this returns an empty vec +/// unless one of `channels` is conda-forge. It also returns empty on Windows +/// (no meaningful stdlib version) and when the platform declares no matching +/// virtual package. +/// +// TODO(#6175): handle `__musl` (musl libc) and `__cuda` -> `cuda_compiler_version`. +pub(crate) fn derive_stdlib_variants<'a>( + platform: &PixiPlatform, + channels: impl IntoIterator, +) -> Vec<(String, VariantValue)> { + if !channels_target_conda_forge(channels) { + return Vec::new(); + } + + let subdir = platform.subdir(); + let (vp_name, provider) = if subdir.is_osx() { + ("__osx", "macosx_deployment_target") + } else if subdir.is_linux() { + ("__glibc", "sysroot") + } else { + return Vec::new(); + }; + + let Some(declared) = platform + .declared_virtual_packages() + .iter() + .find(|vp| vp.name.as_normalized() == vp_name) + else { + return Vec::new(); + }; + + vec![ + ( + C_STDLIB.to_string(), + VariantValue::String(provider.to_string()), + ), + ( + C_STDLIB_VERSION.to_string(), + VariantValue::String(declared.version.to_string()), + ), + ] +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use pixi_manifest::{PixiPlatform, PixiPlatformName}; + use rattler_conda_types::{GenericVirtualPackage, Platform, Version}; + use url::Url; + + use super::*; + + fn vp(name: &str, version: &str) -> GenericVirtualPackage { + GenericVirtualPackage { + name: name.parse().unwrap(), + version: Version::from_str(version).unwrap(), + build_string: "0".to_string(), + } + } + + /// Build a rich (named) platform declaring exactly `packages`. + fn rich(name: &str, subdir: Platform, packages: Vec) -> PixiPlatform { + PixiPlatform::new(PixiPlatformName::from_str(name).unwrap(), subdir, packages).unwrap() + } + + fn channel(url: &str) -> Vec { + vec![ChannelUrl::from(Url::parse(url).unwrap())] + } + + fn conda_forge() -> Vec { + channel("https://conda.anaconda.org/conda-forge") + } + + fn into_map( + pairs: Vec<(String, VariantValue)>, + ) -> std::collections::BTreeMap { + pairs.into_iter().collect() + } + + #[test] + fn osx_derives_macosx_deployment_target() { + let platform = rich("mac", Platform::OsxArm64, vec![vp("__osx", "13.5")]); + let variants = into_map(derive_stdlib_variants(&platform, &conda_forge())); + + assert_eq!( + variants.get("c_stdlib"), + Some(&VariantValue::String( + "macosx_deployment_target".to_string() + )) + ); + assert_eq!( + variants.get("c_stdlib_version"), + Some(&VariantValue::String("13.5".to_string())) + ); + } + + #[test] + fn linux_derives_sysroot_from_glibc() { + let platform = rich("lin", Platform::Linux64, vec![vp("__glibc", "2.28")]); + let variants = into_map(derive_stdlib_variants(&platform, &conda_forge())); + + assert_eq!( + variants.get("c_stdlib"), + Some(&VariantValue::String("sysroot".to_string())) + ); + assert_eq!( + variants.get("c_stdlib_version"), + Some(&VariantValue::String("2.28".to_string())) + ); + } + + #[test] + fn windows_skips() { + let platform = rich("windows-rich", Platform::Win64, vec![vp("__win", "10.0")]); + assert!(derive_stdlib_variants(&platform, &conda_forge()).is_empty()); + } + + /// A linux platform that declares no `__glibc` (only an unrelated VP) + /// produces nothing rather than inventing a version. + #[test] + fn linux_without_glibc_skips() { + let platform = rich("lin-cuda", Platform::Linux64, vec![vp("__cuda", "12.0")]); + assert!(derive_stdlib_variants(&platform, &conda_forge()).is_empty()); + } + + /// Bare subdir platforms carry pixi's portable defaults (`__glibc=2.28`, + /// `__osx=13.0`), so derivation works off them too -- no rich platform + /// required. + #[test] + fn bare_subdir_derives_from_defaults() { + let linux = into_map(derive_stdlib_variants( + &PixiPlatform::from_subdir(Platform::Linux64), + &conda_forge(), + )); + assert_eq!( + linux.get("c_stdlib_version"), + Some(&VariantValue::String("2.28".to_string())) + ); + + let osx = into_map(derive_stdlib_variants( + &PixiPlatform::from_subdir(Platform::OsxArm64), + &conda_forge(), + )); + assert_eq!( + osx.get("c_stdlib_version"), + Some(&VariantValue::String("13.0".to_string())) + ); + } + + /// conda-forge referenced by full URL still gates the derivation on. + #[test] + fn conda_forge_by_url_derives() { + let platform = rich("mac", Platform::OsxArm64, vec![vp("__osx", "13.5")]); + let variants = into_map(derive_stdlib_variants( + &platform, + &channel("https://prefix.dev/conda-forge"), + )); + assert_eq!( + variants.get("c_stdlib"), + Some(&VariantValue::String( + "macosx_deployment_target".to_string() + )) + ); + } + + /// The providers are conda-forge packages, so a workspace that doesn't build + /// against conda-forge derives nothing. + #[test] + fn non_conda_forge_channel_skips() { + let platform = rich("mac", Platform::OsxArm64, vec![vp("__osx", "13.5")]); + assert!( + derive_stdlib_variants(&platform, &channel("https://prefix.dev/my-channel")).is_empty() + ); + } + + #[test] + fn no_channels_skips() { + let platform = rich("mac", Platform::OsxArm64, vec![vp("__osx", "13.5")]); + assert!(derive_stdlib_variants(&platform, &[]).is_empty()); + } +}