diff --git a/crates/pixi_cli/src/workspace/platform.rs b/crates/pixi_cli/src/workspace/platform.rs index f4d95a2e13..e3cafdf6e8 100644 --- a/crates/pixi_cli/src/workspace/platform.rs +++ b/crates/pixi_cli/src/workspace/platform.rs @@ -52,7 +52,7 @@ pub struct VirtualPackageArgs { pub cuda_arch: Option, /// Declare a `__archspec` virtual package with the given microarchitecture - /// string, e.g. `x86-64-v3`. Valid on any subdir. + /// string, e.g. `x86_64_v3`. Valid on any subdir. #[clap(long, value_name = "ARCH")] pub archspec: Option, @@ -128,6 +128,8 @@ impl VirtualPackageArgs { if value.is_empty() { miette::bail!("--archspec requires a non-empty microarchitecture string"); } + pixi_manifest::platform::validate_archspec_name(&value) + .map_err(|message| miette::miette!("{message}"))?; push_unique( &mut specs, &mut seen_names, @@ -258,6 +260,10 @@ fn parse_raw_virtual_package(spec: &str) -> miette::Result = minimum + .virtual_packages() + .iter() + .map(|vp| vp.build_string.as_str()) + .collect(); + assert_eq!(microarchitectures, vec!["skylake", "x86_64_v3"]); + } + /// A linux-64 lock environment with no packages, so the quick-validate /// hash varies only with the platform passed to `from_environment`. fn empty_linux_lock() -> rattler_lock::LockFile { diff --git a/crates/pixi_core/src/lock_file/virtual_packages.rs b/crates/pixi_core/src/lock_file/virtual_packages.rs index 4a4f65c474..e4ca8e7ff0 100644 --- a/crates/pixi_core/src/lock_file/virtual_packages.rs +++ b/crates/pixi_core/src/lock_file/virtual_packages.rs @@ -7,7 +7,8 @@ use pypi_modifiers::pypi_tags::{PyPITagError, get_tags_from_machine, is_python_r use rattler_conda_types::ParseMatchSpecError; use rattler_conda_types::ParseStrictness::Lenient; use rattler_conda_types::{ - GenericVirtualPackage, MatchSpec, Matches, PackageName, Platform, Version, VersionSpec, + GenericVirtualPackage, MatchSpec, Matches, PackageName, Platform, StringMatcher, Version, + VersionSpec, }; use rattler_lock::{CondaPackageData, ConversionError, LockFile, PypiPackageData}; use rattler_virtual_packages::{ @@ -19,7 +20,10 @@ use thiserror::Error; use uv_distribution_filename::WheelFilename; /// Define accepted virtual packages as a constant set -/// These packages will be checked against the system virtual packages +/// These packages will be checked against the system virtual packages. +/// `__archspec` is intentionally excluded: the solver already matches it +/// through the microarchitecture DAG, so re-validating it here would reject +/// hosts the solver accepted (a `skylake` host running `x86_64_v3` packages). const ACCEPTED_VIRTUAL_PACKAGES: &[&str] = &[ "__glibc", "__musl", "__eglibc", "__cuda", "__osx", "__win", "__linux", ]; @@ -41,7 +45,9 @@ impl VirtualPackageNotFoundError { let help = required_package .name .as_exact() - .and_then(|name| conda_override_hint(name.as_normalized(), required_version)) + // `__archspec` is skipped before it reaches here, so no accepted + // virtual package needs a build string for its override hint. + .and_then(|name| conda_override_hint(name.as_normalized(), required_version, None)) .map(|hint| { format!( " You can mock the virtual package by overriding the environment variable, e.g.: '`{hint}`'" @@ -182,10 +188,25 @@ pub fn minimal_required_virtual_packages(depends: &[&str]) -> Vec = HashMap::new(); + // `__archspec` aggregates by distinct microarchitecture instead of by + // version: the machine must satisfy every requirement, and DAG-incomparable + // pairs have no single representative. Bare or pattern-matched + // requirements only assert presence (an empty build string). + let mut archspec_microarchitectures: Vec = Vec::new(); for spec in specs { let Some(name) = spec.name.as_exact() else { continue; }; + if name.as_normalized() == "__archspec" { + let microarchitecture = match &spec.build { + Some(StringMatcher::Exact(microarchitecture)) => microarchitecture.clone(), + Some(_) | None => String::new(), + }; + if !archspec_microarchitectures.contains(µarchitecture) { + archspec_microarchitectures.push(microarchitecture); + } + continue; + } let version = spec .version .as_ref() @@ -207,7 +228,18 @@ pub fn minimal_required_virtual_packages(depends: &[&str]) -> Vec = aggregated.into_values().collect(); - vps.sort_by(|a, b| a.name.as_normalized().cmp(b.name.as_normalized())); + vps.extend( + archspec_microarchitectures + .into_iter() + .map(|microarchitecture| GenericVirtualPackage { + name: PackageName::new_unchecked("__archspec"), + version: Version::major(0), + build_string: microarchitecture, + }), + ); + vps.sort_by(|a, b| { + (a.name.as_normalized(), &a.build_string).cmp(&(b.name.as_normalized(), &b.build_string)) + }); vps } @@ -711,26 +743,34 @@ packages: } } + /// `__archspec` is not validated here: the solver already matched it + /// through the microarchitecture DAG, so lock-file validation skips it + /// even when the host microarchitecture would not match by name. The + /// fixture's package requires `__archspec 1 x86_64`. #[test] - fn test_archspec_skip() { + fn test_archspec_skipped() { let root_dir = Path::new(env!("CARGO_MANIFEST_DIR")); let lock_file_path = root_dir.join("../../tests/data/lock_files/archspec.lock"); let lock_file = LockFile::from_path(&lock_file_path).unwrap(); let platform = pixi_manifest::PixiPlatform::from_subdir(Platform::Linux64); - let overrides = VirtualPackageOverrides { + let overrides = |archspec: &str| VirtualPackageOverrides { libc: Some(Override::String("2.17".to_string())), + archspec: Some(Override::String(archspec.to_string())), ..VirtualPackageOverrides::default() }; - // validate that the archspec is skipped - validate_system_meets_environment_requirements( - &lock_file, - &platform, - &EnvironmentName::default(), - Some(overrides), - ) - .unwrap(); + // A matching host passes, but so does an unknown or unrelated one: + // `__archspec` is never checked here. + for archspec in ["x86_64", "0", "m1"] { + validate_system_meets_environment_requirements( + &lock_file, + &platform, + &EnvironmentName::default(), + Some(overrides(archspec)), + ) + .unwrap_or_else(|error| panic!("archspec {archspec} should be skipped: {error:?}")); + } } #[test] @@ -756,4 +796,33 @@ packages: ) .unwrap(); } + + /// `__archspec` requirements aggregate into the minimal set by distinct + /// microarchitecture -- the machine must satisfy each one -- while other + /// virtual packages keep the highest version seen. + #[test] + fn test_minimal_required_virtual_packages_archspec() { + let depends = [ + "__cuda >=11", + "__cuda >=12", + "__archspec 1 x86_64_v3", + "__archspec 1 skylake", + "__archspec 1 x86_64_v3", + ]; + let minimal = minimal_required_virtual_packages(&depends); + + let archspec: Vec<&str> = minimal + .iter() + .filter(|vp| vp.name.as_normalized() == "__archspec") + .map(|vp| vp.build_string.as_str()) + .collect(); + assert_eq!(archspec, vec!["skylake", "x86_64_v3"]); + + let cuda = minimal + .iter() + .find(|vp| vp.name.as_normalized() == "__cuda") + .expect("__cuda is required"); + assert_eq!(cuda.version, Version::from_str("12").unwrap()); + assert_eq!(minimal.len(), 3); + } } diff --git a/crates/pixi_core/src/workspace/errors.rs b/crates/pixi_core/src/workspace/errors.rs index 0632bb34a2..5e10326f0e 100644 --- a/crates/pixi_core/src/workspace/errors.rs +++ b/crates/pixi_core/src/workspace/errors.rs @@ -74,7 +74,13 @@ impl Diagnostic for UnsupportedPlatformError { let overrides: Vec = self .unsatisfied_requirements .iter() - .filter_map(|req| conda_override_hint(req.name.as_normalized(), Some(&req.version))) + .filter_map(|req| { + conda_override_hint( + req.name.as_normalized(), + Some(&req.version), + Some(&req.build_string), + ) + }) .collect(); let base = if overrides.is_empty() { @@ -105,6 +111,14 @@ impl Diagnostic for UnsupportedPlatformError { fn format_requirements(reqs: &[GenericVirtualPackage]) -> String { reqs.iter() .map(|r| { + // __archspec's requirement is the microarchitecture in its build + // string; its version is a meaningless constant. + if r.name.as_normalized() == "__archspec" + && !r.build_string.is_empty() + && r.build_string != "0" + { + return format!("{} {}", r.name.as_normalized(), r.build_string); + } // Version 0 encodes a version-less requirement (a bare `__cuda` // dependency): the package must be present at any version. if r.version == Version::major(0) { @@ -117,9 +131,14 @@ fn format_requirements(reqs: &[GenericVirtualPackage]) -> String { } /// `CONDA_OVERRIDE_*` hint for a missing virtual package: the required -/// version when known, a realistic example otherwise. `None` for virtual -/// packages without a known override (e.g. `__unix`). -pub(crate) fn conda_override_hint(name: &str, version: Option<&Version>) -> Option { +/// version (or, for `__archspec`, the required microarchitecture) when +/// known, a realistic example otherwise. `None` for virtual packages +/// without a known override (e.g. `__unix`). +pub(crate) fn conda_override_hint( + name: &str, + version: Option<&Version>, + build_string: Option<&str>, +) -> Option { let env_var = match name { "__glibc" => "CONDA_OVERRIDE_GLIBC", "__cuda" => "CONDA_OVERRIDE_CUDA", @@ -129,6 +148,14 @@ pub(crate) fn conda_override_hint(name: &str, version: Option<&Version>) -> Opti "__archspec" => "CONDA_OVERRIDE_ARCHSPEC", _ => return None, }; + // __archspec's payload is the microarchitecture in its build string, + // not its version; suggest overriding to exactly the required one. + if name == "__archspec" { + let example = build_string + .filter(|build| !build.is_empty() && *build != "0") + .unwrap_or("x86_64_v3"); + return Some(format!("{env_var}={example}")); + } // A version-0 requirement means "any version"; "=0" reads like nonsense, // so suggest a realistic value instead. let example = match version.filter(|v| **v != Version::major(0)) { diff --git a/crates/pixi_core/src/workspace/mod.rs b/crates/pixi_core/src/workspace/mod.rs index 0d028e6112..57b9a4840a 100644 --- a/crates/pixi_core/src/workspace/mod.rs +++ b/crates/pixi_core/src/workspace/mod.rs @@ -63,7 +63,8 @@ use crate::lock_file::LockedPackageKind; use rattler_networking::{LazyClient, s3_middleware}; use rattler_repodata_gateway::Gateway; use rattler_virtual_packages::{ - Cuda, EnvOverride, LibC, Linux, Osx, Override, VirtualPackageOverrides, VirtualPackages, + Archspec, Cuda, EnvOverride, LibC, Linux, Osx, Override, VirtualPackageOverrides, + VirtualPackages, }; pub use registry::{WorkspaceRegistry, WorkspaceRegistryError}; pub use solve_group::SolveGroup; @@ -302,6 +303,37 @@ fn apply_environment_variable_overrides(packages: &mut Vec) { + let Ok(value) = std::env::var(Archspec::DEFAULT_ENV_NAME) else { + return; + }; + match Archspec::parse_version_opt(&value) { + Ok(None) => packages.retain(|p| p.name.as_normalized() != "__archspec"), + Ok(Some(archspec)) => { + let overridden = GenericVirtualPackage::from(archspec); + if let Some(existing) = packages + .iter_mut() + .find(|p| p.name.as_normalized() == "__archspec") + { + *existing = overridden; + } else { + packages.push(overridden); + } + } + Err(error) => { + tracing::warn!( + "Ignoring invalid {}='{value}': {error}", + Archspec::DEFAULT_ENV_NAME, + ); + } + } } /// Apply `CONDA_OVERRIDE_GLIBC` (rattler's only libc slot) to `packages`. The diff --git a/crates/pixi_core/src/workspace/virtual_packages.rs b/crates/pixi_core/src/workspace/virtual_packages.rs index c1eb8a1bc2..1f5896b137 100644 --- a/crates/pixi_core/src/workspace/virtual_packages.rs +++ b/crates/pixi_core/src/workspace/virtual_packages.rs @@ -14,7 +14,7 @@ use pixi_manifest::{ }; use rattler_conda_types::{GenericVirtualPackage, Platform}; use rattler_lock::LockFile; -use rattler_virtual_packages::{Archspec, Cuda, CudaArch, LibC, Linux, Osx, VirtualPackage}; +use rattler_virtual_packages::{Cuda, CudaArch, LibC, Linux, Osx, VirtualPackage}; use std::collections::HashSet; use std::path::PathBuf; use std::sync::{LazyLock, Mutex}; @@ -67,17 +67,9 @@ fn generic_to_virtual_package(gvp: &GenericVirtualPackage) -> Option Some(VirtualPackage::CudaArch(CudaArch { version: gvp.version.clone(), })), - "__archspec" => { - // Rattler maps an archspec string through a microarch database - // lookup; an empty/"0" build-string means "unknown microarch" - // and `from_name` returns the generic catch-all in that case. - if gvp.build_string.is_empty() || gvp.build_string == "0" { - return Some(VirtualPackage::Archspec(Archspec::Unknown)); - } - Some(VirtualPackage::Archspec(Archspec::from_name( - gvp.build_string.as_str(), - ))) - } + "__archspec" => Some(VirtualPackage::Archspec( + pixi_manifest::platform::archspec_from_build_string(&gvp.build_string), + )), _ => None, } } @@ -238,7 +230,8 @@ pub fn minimum_compatible_declared_platform<'p>( } /// The declared virtual packages of `platform` that the machine does not -/// provide (missing entirely, or present at a lower version). +/// provide (missing entirely, at a lower version, or with a different +/// build string), per [`pixi_manifest::platform::satisfied_by_system`]. fn unsatisfied_virtual_packages( platform: &PixiPlatform, system: &[GenericVirtualPackage], @@ -246,11 +239,7 @@ fn unsatisfied_virtual_packages( platform .declared_virtual_packages() .iter() - .filter(|required| { - !system - .iter() - .any(|sys| sys.name == required.name && sys.version >= required.version) - }) + .filter(|required| !pixi_manifest::platform::satisfied_by_system(required, system)) .cloned() .collect() } diff --git a/crates/pixi_manifest/src/manifests/workspace.rs b/crates/pixi_manifest/src/manifests/workspace.rs index 86983b28be..37c46d39df 100644 --- a/crates/pixi_manifest/src/manifests/workspace.rs +++ b/crates/pixi_manifest/src/manifests/workspace.rs @@ -5005,7 +5005,7 @@ platforms = [ rattler_conda_types::GenericVirtualPackage { name: rattler_conda_types::PackageName::try_from("__archspec").unwrap(), version: Version::major(0), - build_string: "x86-64-v3".to_string(), + build_string: "x86_64_v3".to_string(), }, ], ..Default::default() @@ -5019,7 +5019,7 @@ platforms = [ name = "named-variants" channels = ["conda-forge"] platforms = [ - { name = "modern", platform = "linux-64", archspec = "x86-64-v3" }, + { name = "modern", platform = "linux-64", archspec = "x86_64_v3" }, "linux-64", ] "#, diff --git a/crates/pixi_manifest/src/platform.rs b/crates/pixi_manifest/src/platform.rs index 5f5673ec9f..181fb16e51 100644 --- a/crates/pixi_manifest/src/platform.rs +++ b/crates/pixi_manifest/src/platform.rs @@ -741,6 +741,56 @@ pub fn is_subdir_default(gvp: &GenericVirtualPackage, subdir: Platform) -> bool }) } +/// Returns true if the system provides `required`: a virtual package of the +/// same name, with a version at least as high, and -- when `required` carries +/// a build string -- exactly that build string. `__archspec` is special: its +/// build strings are microarchitecture names compared through the archspec +/// DAG, and its version is a meaningless constant that is ignored. +pub fn satisfied_by_system( + required: &GenericVirtualPackage, + system: &[GenericVirtualPackage], +) -> bool { + let Some(provided) = system.iter().find(|s| s.name == required.name) else { + return false; + }; + if required.name.as_normalized() == "__archspec" { + return archspec_from_build_string(&provided.build_string) + .is_compatible_with(&archspec_from_build_string(&required.build_string)); + } + provided.version >= required.version + && (required.build_string.is_empty() || provided.build_string == required.build_string) +} + +/// Map an `__archspec` build string to rattler's typed [`Archspec`]. An empty +/// build string or `"0"` means "unknown microarchitecture". +pub fn archspec_from_build_string(build_string: &str) -> Archspec { + if build_string.is_empty() || build_string == "0" { + Archspec::Unknown + } else { + Archspec::from_name(build_string) + } +} + +/// Validate a declared `__archspec` microarchitecture name against the +/// archspec database. An unknown name would produce a platform no real host +/// can ever match, so manifests and the CLI reject it up front; the error +/// suggests the underscore spelling when that is what was meant. +pub fn validate_archspec_name(name: &str) -> Result<(), String> { + if name == "0" || Archspec::from_known_name(name).is_some() { + return Ok(()); + } + let underscored = name.replace('-', "_"); + if Archspec::from_known_name(&underscored).is_some() { + Err(format!( + "'{name}' is not a known archspec microarchitecture, did you mean '{underscored}'?" + )) + } else { + Err(format!( + "'{name}' is not a known archspec microarchitecture" + )) + } +} + /// Parse a virtual-package entry the way it's stored in `pixi.lock` -- either /// `__name=version` or `__name=version=build_string` -- back into a /// [`GenericVirtualPackage`]. The lock-file serializer uses the same shape @@ -924,6 +974,99 @@ mod tests { .any(|gvp| gvp.name.as_normalized() == name) } + fn gvp_with_build(name: &str, version: &str, build_string: &str) -> GenericVirtualPackage { + GenericVirtualPackage { + build_string: build_string.to_string(), + ..gvp(name, version) + } + } + + #[test] + fn satisfied_by_system_compares_name_and_version() { + let system = vec![gvp("__cuda", "12.4")]; + // Same name, system version at least as high. + assert!(satisfied_by_system(&gvp("__cuda", "12"), &system)); + assert!(satisfied_by_system(&gvp("__cuda", "12.4"), &system)); + // A higher requirement or a missing name is unsatisfied. + assert!(!satisfied_by_system(&gvp("__cuda", "13"), &system)); + assert!(!satisfied_by_system(&gvp("__glibc", "2.17"), &system)); + } + + #[test] + fn satisfied_by_system_compares_build_strings() { + let system = vec![gvp_with_build("__foo", "1", "special")]; + // A required build string must be provided exactly. + assert!(satisfied_by_system( + &gvp_with_build("__foo", "1", "special"), + &system + )); + assert!(!satisfied_by_system( + &gvp_with_build("__foo", "1", "other"), + &system + )); + // No required build string accepts any provided one. + assert!(satisfied_by_system(&gvp("__foo", "1"), &system)); + // A required build string is not satisfied by an empty provided one. + assert!(!satisfied_by_system( + &gvp_with_build("__bar", "1", "special"), + &[gvp("__bar", "1")] + )); + } + + #[test] + fn satisfied_by_system_matches_archspec_via_dag() { + let skylake = vec![gvp_with_build("__archspec", "1", "skylake")]; + // A host microarchitecture satisfies any of its DAG ancestors... + assert!(satisfied_by_system( + &gvp_with_build("__archspec", "0", "x86_64_v3"), + &skylake + )); + // ...but not a more specific or cross-family requirement. + assert!(!satisfied_by_system( + &gvp_with_build("__archspec", "0", "cascadelake"), + &skylake + )); + assert!(!satisfied_by_system( + &gvp_with_build("__archspec", "0", "m1"), + &skylake + )); + // The version carried by __archspec entries is a meaningless + // constant (rattler emits 1, pixi defaults use 0) and is ignored. + assert!(satisfied_by_system( + &gvp_with_build("__archspec", "2", "x86_64"), + &skylake + )); + // A bare requirement (no or "0" build string) constrains nothing, + // even for a host whose microarchitecture is unknown. + let unknown = vec![gvp_with_build("__archspec", "1", "0")]; + assert!(satisfied_by_system(&gvp("__archspec", "0"), &unknown)); + assert!(satisfied_by_system( + &gvp_with_build("__archspec", "0", "0"), + &skylake + )); + // An unknown host microarchitecture fails any specific requirement. + assert!(!satisfied_by_system( + &gvp_with_build("__archspec", "0", "x86_64"), + &unknown + )); + } + + #[test] + fn validate_archspec_name_rejects_unknown_names() { + assert_eq!(validate_archspec_name("x86_64_v3"), Ok(())); + assert_eq!(validate_archspec_name("m1"), Ok(())); + assert_eq!(validate_archspec_name("0"), Ok(())); + // The dashed spelling gets a did-you-mean hint. + let error = validate_archspec_name("x86-64-v3").unwrap_err(); + assert!(error.contains("did you mean 'x86_64_v3'"), "{error}"); + // Other unknown names are rejected without a suggestion. + let error = validate_archspec_name("armv8-a").unwrap_err(); + assert!( + error.contains("'armv8-a' is not a known archspec microarchitecture"), + "{error}" + ); + } + #[test] fn from_required_virtual_packages_keeps_exact_vps() { // Exactly the given VPs are declared; subdir defaults are NOT merged in. diff --git a/crates/pixi_manifest/src/toml/platform.rs b/crates/pixi_manifest/src/toml/platform.rs index d55f7976b0..264065f3a5 100644 --- a/crates/pixi_manifest/src/toml/platform.rs +++ b/crates/pixi_manifest/src/toml/platform.rs @@ -169,7 +169,7 @@ const FRIENDLY_VIRTUAL_PACKAGES: &[FriendlyVirtualPackage] = &[ /// platforms = [ /// { platform = "linux-64", cuda = "12.0", glibc = "2.28" }, /// { name = "gpu", platform = "linux-64", cuda = { driver = "12.0", arch = "8.6" } }, -/// { name = "jetson-nano", platform = "linux-aarch64", cuda = "12.8", archspec = "armv8-a" }, +/// { name = "jetson", platform = "linux-aarch64", cuda = "12.8", archspec = "armv8.2a" }, /// ] /// ``` /// @@ -414,6 +414,11 @@ fn build_friendly_virtual_package( line_info: None, }); } + crate::platform::validate_archspec_name(&raw.value).map_err(|message| Error { + kind: ErrorKind::Custom(message.into()), + span: raw.span, + line_info: None, + })?; Ok(GenericVirtualPackage { name: package_name, version: Version::major(0), @@ -532,6 +537,13 @@ fn parse_raw_virtual_package( line_info: None, })?; let build_string = parts.next().unwrap_or("").to_string(); + if name.as_normalized() == "__archspec" && !build_string.is_empty() { + crate::platform::validate_archspec_name(&build_string).map_err(|message| Error { + kind: ErrorKind::Custom(message.into()), + span: value_span, + line_info: None, + })?; + } Ok(GenericVirtualPackage { name, version, @@ -1068,19 +1080,45 @@ mod test { #[test] fn test_workspace_platform_archspec_goes_to_build_string() { let parsed = TopLevel::from_toml_str( - r#"platform = { platform = "linux-64", archspec = "x86-64-v3" }"#, + r#"platform = { platform = "linux-64", archspec = "x86_64_v3" }"#, ) .unwrap(); let package = &parsed.platform.declared_virtual_packages()[0]; assert_eq!(package.name.as_normalized(), "__archspec"); assert_eq!(package.version, Version::major(0)); - assert_eq!(package.build_string, "x86-64-v3"); + assert_eq!(package.build_string, "x86_64_v3"); + // The synthesised name sanitises the underscores away. assert_eq!( parsed.platform.name().as_str(), "linux-64-archspec-x86-64-v3" ); } + #[test] + fn test_workspace_platform_archspec_rejects_unknown_names() { + // The dashed spelling of a known microarchitecture gets a hint. + let input = r#"platform = { platform = "linux-64", archspec = "x86-64-v3" }"#; + let rendered = format_parse_error(input, TopLevel::from_toml_str(input).unwrap_err()); + assert!( + rendered.contains("did you mean 'x86_64_v3'"), + "expected a did-you-mean hint, got: {rendered}", + ); + // Names the archspec database does not know are rejected outright, + // through the friendly key and the raw form alike. + let input = r#"platform = { platform = "linux-aarch64", archspec = "armv8-a" }"#; + let rendered = format_parse_error(input, TopLevel::from_toml_str(input).unwrap_err()); + assert!( + rendered.contains("'armv8-a' is not a known archspec microarchitecture"), + "expected an unknown-name error, got: {rendered}", + ); + let input = r#"platform = { platform = "linux-64", __archspec = "0=nonsense" }"#; + let rendered = format_parse_error(input, TopLevel::from_toml_str(input).unwrap_err()); + assert!( + rendered.contains("'nonsense' is not a known archspec microarchitecture"), + "expected an unknown-name error, got: {rendered}", + ); + } + /// Friendly key order in the TOML source must not affect the auto-derived /// name: it follows the canonical order from `FRIENDLY_VIRTUAL_PACKAGES`. #[test] @@ -1446,14 +1484,14 @@ mod test { let platform = platform_with_packages( "linux-64-archspec-x86-64-v3", Platform::Linux64, - vec![archspec_virtual_package("x86-64-v3")], + vec![archspec_virtual_package("x86_64_v3")], ); let json = serde_json::to_value(TomlPixiPlatform(platform)).unwrap(); assert_eq!( json, serde_json::json!({ "platform": "linux-64", - "archspec": "x86-64-v3", + "archspec": "x86_64_v3", }), ); } diff --git a/crates/pixi_manifest/src/workspace.rs b/crates/pixi_manifest/src/workspace.rs index ce0ad4a8b7..bac0d07c96 100644 --- a/crates/pixi_manifest/src/workspace.rs +++ b/crates/pixi_manifest/src/workspace.rs @@ -17,7 +17,7 @@ use url::Url; use super::pypi::pypi_options::PypiOptions; use crate::{ PixiPlatform, PixiPlatformName, PrioritizedChannel, S3Options, TargetSelector, Targets, - preview::Preview, + platform::satisfied_by_system, preview::Preview, }; use minijinja::{AutoEscape, Environment, UndefinedBehavior}; use once_cell::sync::Lazy; @@ -252,16 +252,6 @@ impl Workspace { } } -/// Returns true if `declared` is provided by the system: the system must list -/// a virtual package of the same name with a version at least as high as the -/// declared one. -fn satisfied_by_system(declared: &GenericVirtualPackage, system: &[GenericVirtualPackage]) -> bool { - system - .iter() - .find(|s| s.name == declared.name) - .is_some_and(|s| s.version >= declared.version) -} - /// A source that contributes additional build variant definitions. #[derive(Debug, Clone, PartialEq, Eq)] pub enum BuildVariantSource { diff --git a/docs/conda_ecosystem.md b/docs/conda_ecosystem.md index 2649477409..1f41118b76 100644 --- a/docs/conda_ecosystem.md +++ b/docs/conda_ecosystem.md @@ -134,7 +134,7 @@ Common virtual packages: | `__glibc` | GNU C Library version | | `__osx` | macOS version | | `__cuda` | NVIDIA driver's CUDA capability | -| `__archspec` | CPU microarchitecture (e.g. `x86-64-v3`) | +| `__archspec` | CPU microarchitecture (e.g. `x86_64_v3`) | When a package depends on `__cuda >= 12`, the solver will only select it if the system provides a `__cuda` virtual package with diff --git a/docs/reference/cli/pixi/workspace/platform/add.md b/docs/reference/cli/pixi/workspace/platform/add.md index b39e68d1a2..5fd01e24bc 100644 --- a/docs/reference/cli/pixi/workspace/platform/add.md +++ b/docs/reference/cli/pixi/workspace/platform/add.md @@ -25,7 +25,7 @@ pixi workspace platform add [OPTIONS] `--cuda-arch ` : Declare a `__cuda_arch` virtual package (GPU compute capability) at the given version, e.g. `8.6`. Requires `--cuda` (or an existing `__cuda`), matching the conda CEP coupling. Serialized as `cuda = { driver, arch }` - `--archspec ` -: Declare a `__archspec` virtual package with the given microarchitecture string, e.g. `x86-64-v3`. Valid on any subdir +: Declare a `__archspec` virtual package with the given microarchitecture string, e.g. `x86_64_v3`. Valid on any subdir - `--glibc ` : Declare a `__glibc` virtual package at the given version, e.g. `2.28`. Only valid on linux subdirs - `--linux ` diff --git a/docs/reference/cli/pixi/workspace/platform/edit.md b/docs/reference/cli/pixi/workspace/platform/edit.md index 18fc642d45..d706de252d 100644 --- a/docs/reference/cli/pixi/workspace/platform/edit.md +++ b/docs/reference/cli/pixi/workspace/platform/edit.md @@ -29,7 +29,7 @@ pixi workspace platform edit [OPTIONS] [__NAME[=VERSION[=BUILD]]]... - `--cuda-arch ` : Declare a `__cuda_arch` virtual package (GPU compute capability) at the given version, e.g. `8.6`. Requires `--cuda` (or an existing `__cuda`), matching the conda CEP coupling. Serialized as `cuda = { driver, arch }` - `--archspec ` -: Declare a `__archspec` virtual package with the given microarchitecture string, e.g. `x86-64-v3`. Valid on any subdir +: Declare a `__archspec` virtual package with the given microarchitecture string, e.g. `x86_64_v3`. Valid on any subdir - `--glibc ` : Declare a `__glibc` virtual package at the given version, e.g. `2.28`. Only valid on linux subdirs - `--linux ` diff --git a/docs/workspace/multi_platform_configuration.md b/docs/workspace/multi_platform_configuration.md index 390d80597a..a2db62191b 100644 --- a/docs/workspace/multi_platform_configuration.md +++ b/docs/workspace/multi_platform_configuration.md @@ -118,6 +118,38 @@ Each inline-table entry has: any other raw `__name` is stored but ignored when checking host compatibility. +### CPU microarchitectures (`archspec`) + +The `archspec` key pins the CPU microarchitecture baseline a platform targets, +e.g. `archspec = "x86_64_v3"`. Names come from the +[archspec database](https://github.com/archspec/archspec-json) (`x86_64_v3`, +`skylake`, `zen4`, `m2`, ...) and use underscores -- conda build strings cannot +contain `-`. Unknown names are rejected when the manifest is parsed. + +`__archspec` is not matched as a plain string: this machine satisfies the +requirement when its detected microarchitecture implements the required +baseline, i.e. it is the same node or a descendant in the archspec graph. A +`skylake` machine satisfies `archspec = "x86_64_v3"`, a `haswell` machine does +not, and an Apple `m2` never satisfies an `x86_64*` requirement -- Pixi +deliberately treats foreign microarchitectures (e.g. under Rosetta emulation) +as unsupported. + +Declare variants from most to least specific: Pixi picks the first declared +platform this machine satisfies, so capable hosts get the optimised variant +and older CPUs fall through to the baseline. + +```toml title="pixi.toml" +[workspace] +platforms = [ + { name = "linux-64-v3", platform = "linux-64", archspec = "x86_64_v3" }, + "linux-64", +] +``` + +`CONDA_OVERRIDE_ARCHSPEC=` overrides detection (the escape hatch when +you know better than Pixi, e.g. a Rosetta setup that does support the +baseline); `CONDA_OVERRIDE_ARCHSPEC=0` marks the microarchitecture as unknown. + A feature's `platforms` array is a list of names that must each resolve to a workspace platform (or be a bare conda subdir, which Pixi treats as an alias for that subdir). This is how you bind a feature to the rich variant: diff --git a/schema/examples/valid/workspace_rich_platform.toml b/schema/examples/valid/workspace_rich_platform.toml index 8bbac63e29..a76a3d8bb6 100644 --- a/schema/examples/valid/workspace_rich_platform.toml +++ b/schema/examples/valid/workspace_rich_platform.toml @@ -5,6 +5,6 @@ name = "rich" platforms = [ "linux-64", { name = "linux-64-cuda", platform = "linux-64", cuda = "12.0", glibc = "2.28" }, - { platform = "linux-aarch64", archspec = "armv8-a" }, + { platform = "linux-aarch64", archspec = "armv8.2a" }, { name = "osx-arm64" }, ] diff --git a/schema/model.py b/schema/model.py index 5d8f9f0129..8bf27abc53 100644 --- a/schema/model.py +++ b/schema/model.py @@ -168,7 +168,7 @@ class WorkspacePlatform(BaseModel): ) archspec: NonEmptyStr | None = Field( None, - description="Declare a `__archspec` virtual package with the given microarchitecture, e.g. `x86-64-v3`.", + description="Declare a `__archspec` virtual package with the given microarchitecture, e.g. `x86_64_v3`.", ) glibc: NonEmptyStr | None = Field( None, diff --git a/schema/pyproject/partial-pixi.json b/schema/pyproject/partial-pixi.json index d3193ffe69..edb021ece3 100644 --- a/schema/pyproject/partial-pixi.json +++ b/schema/pyproject/partial-pixi.json @@ -3391,7 +3391,7 @@ "properties": { "archspec": { "title": "Archspec", - "description": "Declare a `__archspec` virtual package with the given microarchitecture, e.g. `x86-64-v3`.", + "description": "Declare a `__archspec` virtual package with the given microarchitecture, e.g. `x86_64_v3`.", "type": "string", "minLength": 1 }, diff --git a/schema/pyproject/schema.json b/schema/pyproject/schema.json index 4840b1bcbb..cbcbe66706 100644 --- a/schema/pyproject/schema.json +++ b/schema/pyproject/schema.json @@ -3429,7 +3429,7 @@ "properties": { "archspec": { "title": "Archspec", - "description": "Declare a `__archspec` virtual package with the given microarchitecture, e.g. `x86-64-v3`.", + "description": "Declare a `__archspec` virtual package with the given microarchitecture, e.g. `x86_64_v3`.", "type": "string", "minLength": 1 }, diff --git a/schema/schema.json b/schema/schema.json index 5a11d49896..0550a6dd80 100644 --- a/schema/schema.json +++ b/schema/schema.json @@ -3415,7 +3415,7 @@ "properties": { "archspec": { "title": "Archspec", - "description": "Declare a `__archspec` virtual package with the given microarchitecture, e.g. `x86-64-v3`.", + "description": "Declare a `__archspec` virtual package with the given microarchitecture, e.g. `x86_64_v3`.", "type": "string", "minLength": 1 }, diff --git a/tests/data/channels/channels/virtual_packages/.condapackageignore b/tests/data/channels/channels/virtual_packages/.condapackageignore new file mode 100644 index 0000000000..72e8ffc0db --- /dev/null +++ b/tests/data/channels/channels/virtual_packages/.condapackageignore @@ -0,0 +1 @@ +* diff --git a/tests/data/channels/channels/virtual_packages/linux-64/cuda-0.1.0-h055459d_0.conda b/tests/data/channels/channels/virtual_packages/linux-64/cuda-0.1.0-h055459d_0.conda index 2a25558eda..68392f3c69 100644 Binary files a/tests/data/channels/channels/virtual_packages/linux-64/cuda-0.1.0-h055459d_0.conda and b/tests/data/channels/channels/virtual_packages/linux-64/cuda-0.1.0-h055459d_0.conda differ diff --git a/tests/data/channels/channels/virtual_packages/linux-64/cuda-arch-0.1.0-sm100_1.conda b/tests/data/channels/channels/virtual_packages/linux-64/cuda-arch-0.1.0-sm100_1.conda index 65b093b668..6702e93650 100644 Binary files a/tests/data/channels/channels/virtual_packages/linux-64/cuda-arch-0.1.0-sm100_1.conda and b/tests/data/channels/channels/virtual_packages/linux-64/cuda-arch-0.1.0-sm100_1.conda differ diff --git a/tests/data/channels/channels/virtual_packages/linux-64/cuda-arch-0.1.0-sm120_2.conda b/tests/data/channels/channels/virtual_packages/linux-64/cuda-arch-0.1.0-sm120_2.conda index 5d7b53640b..f4626074d4 100644 Binary files a/tests/data/channels/channels/virtual_packages/linux-64/cuda-arch-0.1.0-sm120_2.conda and b/tests/data/channels/channels/virtual_packages/linux-64/cuda-arch-0.1.0-sm120_2.conda differ diff --git a/tests/data/channels/channels/virtual_packages/linux-64/cuda-arch-0.1.0-sm90_0.conda b/tests/data/channels/channels/virtual_packages/linux-64/cuda-arch-0.1.0-sm90_0.conda index 9fe19f2c40..b254ff58d2 100644 Binary files a/tests/data/channels/channels/virtual_packages/linux-64/cuda-arch-0.1.0-sm90_0.conda and b/tests/data/channels/channels/virtual_packages/linux-64/cuda-arch-0.1.0-sm90_0.conda differ diff --git a/tests/data/channels/channels/virtual_packages/linux-64/repodata.json b/tests/data/channels/channels/virtual_packages/linux-64/repodata.json index 615ab8437e..93ffcaa24e 100644 --- a/tests/data/channels/channels/virtual_packages/linux-64/repodata.json +++ b/tests/data/channels/channels/virtual_packages/linux-64/repodata.json @@ -1 +1 @@ -{"info":{"subdir":"linux-64"},"packages":{},"packages.conda":{"cuda-0.1.0-h055459d_0.conda":{"arch":"x86_64","build":"h055459d_0","build_number":0,"depends":["__cuda >=12"],"md5":"76963e2c593ed308ba4c52fb881c6580","name":"cuda","platform":"linux","sha256":"b7c31e25127ee68c99fa34f152a4b738b01aac95555358d07b13bf7ee1e2fda9","size":993,"subdir":"linux-64","timestamp":1756801677814,"version":"0.1.0"},"cuda-arch-0.1.0-sm100_1.conda":{"arch":"x86_64","build":"sm100_1","build_number":1,"depends":["__cuda_arch >=10"],"md5":"c3566740848f2dab9d2fa5af928628e3","name":"cuda-arch","platform":"linux","sha256":"6c8bb8726bf486ad21826f2392e4c7b462de114971e2048e6eab5b64396b6b55","size":999,"subdir":"linux-64","timestamp":1781879513023,"version":"0.1.0"},"cuda-arch-0.1.0-sm120_2.conda":{"arch":"x86_64","build":"sm120_2","build_number":2,"depends":["__cuda_arch >=12"],"md5":"29f6c2c96e3c4e33fe7f8a66ffac853b","name":"cuda-arch","platform":"linux","sha256":"ae1d9de7e44cb14933c11f9483ea55efb52e9aef275f115f3d53ced81a1dc513","size":1000,"subdir":"linux-64","timestamp":1781879513099,"version":"0.1.0"},"cuda-arch-0.1.0-sm90_0.conda":{"arch":"x86_64","build":"sm90_0","build_number":0,"depends":["__cuda_arch >=9"],"md5":"545ee1d4edaf23e6df14330ea8ba21a0","name":"cuda-arch","platform":"linux","sha256":"29f0f02ca342508878af38465973ca865a5d79cc33e6668c54ee9769500e17b1","size":993,"subdir":"linux-64","timestamp":1781879512944,"version":"0.1.0"}},"repodata_version":2} \ No newline at end of file +{"info":{"subdir":"linux-64"},"packages":{},"packages.conda":{"cuda-0.1.0-h055459d_0.conda":{"arch":"x86_64","build":"h055459d_0","build_number":0,"depends":["__cuda >=12"],"md5":"6f5243b6303e6bc3ead553377c9b72e9","name":"cuda","platform":"linux","sha256":"6e2c7d1848130e42a3a4aabd3ffde83742ef24e09d9cc557628cf3dece931eae","size":995,"subdir":"linux-64","timestamp":1783328856382,"version":"0.1.0"},"cuda-arch-0.1.0-sm100_1.conda":{"arch":"x86_64","build":"sm100_1","build_number":1,"depends":["__cuda_arch >=10"],"md5":"e5ffa93fd4ce5927202d67ac954ad9d2","name":"cuda-arch","platform":"linux","sha256":"177c9cb4f386728781ec6cf0c3be03aa0669fc4b774c6155f16c8f62acc52596","size":1001,"subdir":"linux-64","timestamp":1783328856519,"version":"0.1.0"},"cuda-arch-0.1.0-sm120_2.conda":{"arch":"x86_64","build":"sm120_2","build_number":2,"depends":["__cuda_arch >=12"],"md5":"54e607e833eae31fdf785ddd99d27057","name":"cuda-arch","platform":"linux","sha256":"d6fba30b3f0975da05d55cf892aae7a89fef94e9ef2653cf2ca644b5a05a102e","size":1000,"subdir":"linux-64","timestamp":1783328856587,"version":"0.1.0"},"cuda-arch-0.1.0-sm90_0.conda":{"arch":"x86_64","build":"sm90_0","build_number":0,"depends":["__cuda_arch >=9"],"md5":"5d7baed3ecf5c15561402cf9084c2ea2","name":"cuda-arch","platform":"linux","sha256":"1713fe0d676926cc08b3b1536d101cbdfc947587496367e464bc448ee2611f15","size":995,"subdir":"linux-64","timestamp":1783328856658,"version":"0.1.0"}},"repodata_version":2} \ No newline at end of file diff --git a/tests/data/channels/channels/virtual_packages/noarch/microarch-v3-0.1.0-h4caebd9_0.conda b/tests/data/channels/channels/virtual_packages/noarch/microarch-v3-0.1.0-h4caebd9_0.conda new file mode 100644 index 0000000000..2a7b9d3159 Binary files /dev/null and b/tests/data/channels/channels/virtual_packages/noarch/microarch-v3-0.1.0-h4caebd9_0.conda differ diff --git a/tests/data/channels/channels/virtual_packages/noarch/no-deps-0.1.0-h4616a5c_0.conda b/tests/data/channels/channels/virtual_packages/noarch/no-deps-0.1.0-h4616a5c_0.conda index 8ac6735540..11cfd6456c 100644 Binary files a/tests/data/channels/channels/virtual_packages/noarch/no-deps-0.1.0-h4616a5c_0.conda and b/tests/data/channels/channels/virtual_packages/noarch/no-deps-0.1.0-h4616a5c_0.conda differ diff --git a/tests/data/channels/channels/virtual_packages/noarch/noarch_package-0.1.0-ha796245_0.conda b/tests/data/channels/channels/virtual_packages/noarch/noarch_package-0.1.0-ha796245_0.conda index dc05b616e9..1f94d3780f 100644 Binary files a/tests/data/channels/channels/virtual_packages/noarch/noarch_package-0.1.0-ha796245_0.conda and b/tests/data/channels/channels/virtual_packages/noarch/noarch_package-0.1.0-ha796245_0.conda differ diff --git a/tests/data/channels/channels/virtual_packages/noarch/noarch_package-0.1.0-hb7895f7_0.conda b/tests/data/channels/channels/virtual_packages/noarch/noarch_package-0.1.0-hb7895f7_0.conda index 8e78ed614c..eb1d455a61 100644 Binary files a/tests/data/channels/channels/virtual_packages/noarch/noarch_package-0.1.0-hb7895f7_0.conda and b/tests/data/channels/channels/virtual_packages/noarch/noarch_package-0.1.0-hb7895f7_0.conda differ diff --git a/tests/data/channels/channels/virtual_packages/noarch/repodata.json b/tests/data/channels/channels/virtual_packages/noarch/repodata.json index 8e7a7bf422..45c44d1845 100644 --- a/tests/data/channels/channels/virtual_packages/noarch/repodata.json +++ b/tests/data/channels/channels/virtual_packages/noarch/repodata.json @@ -1 +1 @@ -{"info":{"subdir":"noarch"},"packages":{},"packages.conda":{"no-deps-0.1.0-h4616a5c_0.conda":{"build":"h4616a5c_0","build_number":0,"depends":[],"md5":"2ebd6e759b7e9b0e08a7b626795c29ca","name":"no-deps","noarch":"generic","sha256":"0721a385abe51a0c2e2c3618ae5e0831ab29e1e1bd8c6f7d0552fccc6063a420","size":967,"subdir":"noarch","timestamp":1756801678347,"version":"0.1.0"},"noarch_package-0.1.0-ha796245_0.conda":{"build":"ha796245_0","build_number":0,"depends":[],"md5":"7883cc8c36eb3fd814a93696d04a7c05","name":"noarch_package","noarch":"generic","sha256":"1872c535762582b4c74b2e03756a5dfde65393f5bfbf07dd32f7bb98975f33fd","size":1013,"subdir":"noarch","timestamp":1756801678849,"version":"0.1.0"},"noarch_package-0.1.0-hb7895f7_0.conda":{"build":"hb7895f7_0","build_number":0,"depends":[],"md5":"eacff34dda0c42d23be88296e587c67a","name":"noarch_package","noarch":"generic","sha256":"2d6d07d25dc44c1f0f2016750c371d34ba9c3ba5873dd631e4aabb56b29e466b","size":1008,"subdir":"noarch","timestamp":1756801678849,"version":"0.1.0"}},"repodata_version":2} \ No newline at end of file +{"info":{"subdir":"noarch"},"packages":{},"packages.conda":{"microarch-v3-0.1.0-h4caebd9_0.conda":{"build":"h4caebd9_0","build_number":0,"depends":["__archspec ==1 x86_64_v3"],"md5":"f37edf11ecf863d53d6fe49b3c47bae9","name":"microarch-v3","noarch":"generic","sha256":"5928f089bae71fadad4a44dcb3a02a1fdcb99dc04197aa8374d5ef146dd940f0","size":1033,"subdir":"noarch","timestamp":1783328856456,"version":"0.1.0"},"no-deps-0.1.0-h4616a5c_0.conda":{"build":"h4616a5c_0","build_number":0,"depends":[],"md5":"5155d7dad05f50e0cfbde1f8eb0d6799","name":"no-deps","noarch":"generic","sha256":"fa2b855282613249dac61db1d41bb262a045eef4fd77ef52f7a4924fcc30ed48","size":977,"subdir":"noarch","timestamp":1783328856723,"version":"0.1.0"},"noarch_package-0.1.0-ha796245_0.conda":{"build":"ha796245_0","build_number":0,"depends":[],"md5":"98633a7cc1eea409d6e31c3f78dadd0e","name":"noarch_package","noarch":"generic","sha256":"84c318bf7f07b51f34ca8f68fb92abaa8a48697d72ec4ea204760ef076bb698c","size":1022,"subdir":"noarch","timestamp":1783328856780,"version":"0.1.0"},"noarch_package-0.1.0-hb7895f7_0.conda":{"build":"hb7895f7_0","build_number":0,"depends":[],"md5":"0f49af4af314db5d9903aaeed1d305ab","name":"noarch_package","noarch":"generic","sha256":"1d17becdc0bb6bb4e5e89d48ceee1b27c7d01e38b80deb387cd8cd91019db05a","size":1020,"subdir":"noarch","timestamp":1783328856780,"version":"0.1.0"}},"repodata_version":2} \ No newline at end of file diff --git a/tests/data/channels/channels/virtual_packages/rattler-build-log.txt b/tests/data/channels/channels/virtual_packages/rattler-build-log.txt new file mode 100644 index 0000000000..e1d2ca383e --- /dev/null +++ b/tests/data/channels/channels/virtual_packages/rattler-build-log.txt @@ -0,0 +1,9 @@ +{"work_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda_1783328856/work","build_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda_1783328856","host_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda_1783328856/host_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_","build_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda_1783328856/build_env","recipe_dir":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages","recipe_path":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages/cuda.yaml","output_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages","cache_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/build_cache"} +{"work_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda_1783328856/work","build_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda_1783328856","host_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda_1783328856/host_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_","build_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda_1783328856/build_env","recipe_dir":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages","recipe_path":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages/cuda.yaml","output_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages","cache_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/build_cache"} +{"work_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_microarch-v3_1783328856/work","build_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_microarch-v3_1783328856","host_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_microarch-v3_1783328856/host_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_pl","build_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_microarch-v3_1783328856/build_env","recipe_dir":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages","recipe_path":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages/microarch-v3.yaml","output_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages","cache_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/build_cache"} +{"work_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda-arch_1783328856/work","build_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda-arch_1783328856","host_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda-arch_1783328856/host_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_place","build_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda-arch_1783328856/build_env","recipe_dir":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages","recipe_path":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages/cuda-arch-sm100.yaml","output_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages","cache_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/build_cache"} +{"work_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda-arch_1783328856/work","build_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda-arch_1783328856","host_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda-arch_1783328856/host_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_place","build_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda-arch_1783328856/build_env","recipe_dir":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages","recipe_path":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages/cuda-arch-sm120.yaml","output_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages","cache_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/build_cache"} +{"work_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda-arch_1783328856/work","build_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda-arch_1783328856","host_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda-arch_1783328856/host_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_place","build_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_cuda-arch_1783328856/build_env","recipe_dir":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages","recipe_path":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages/cuda-arch-sm90.yaml","output_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages","cache_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/build_cache"} +{"work_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_no-deps_1783328856/work","build_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_no-deps_1783328856","host_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_no-deps_1783328856/host_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placeho","build_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_no-deps_1783328856/build_env","recipe_dir":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages","recipe_path":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages/no-deps.yaml","output_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages","cache_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/build_cache"} +{"work_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_noarch_package_1783328856/work","build_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_noarch_package_1783328856","host_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_noarch_package_1783328856/host_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_","build_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_noarch_package_1783328856/build_env","recipe_dir":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages","recipe_path":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages/noarch_package.yaml","output_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages","cache_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/build_cache"} +{"work_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_noarch_package_1783328856/work","build_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_noarch_package_1783328856","host_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_noarch_package_1783328856/host_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_","build_prefix":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/bld/rattler-build_noarch_package_1783328856/build_env","recipe_dir":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages","recipe_path":"/var/home/tobias/src/pixi/tests/data/channels/recipes/virtual_packages/noarch_package.yaml","output_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages","cache_dir":"/var/home/tobias/src/pixi/tests/data/channels/channels/virtual_packages/build_cache"} diff --git a/tests/data/channels/channels/virtual_packages/win-64/cuda-0.1.0-h28b8efe_0.conda b/tests/data/channels/channels/virtual_packages/win-64/cuda-0.1.0-h28b8efe_0.conda index 43e006e6b5..002daa7eda 100644 Binary files a/tests/data/channels/channels/virtual_packages/win-64/cuda-0.1.0-h28b8efe_0.conda and b/tests/data/channels/channels/virtual_packages/win-64/cuda-0.1.0-h28b8efe_0.conda differ diff --git a/tests/data/channels/channels/virtual_packages/win-64/repodata.json b/tests/data/channels/channels/virtual_packages/win-64/repodata.json index 6b3a8ca7ed..b13d275897 100644 --- a/tests/data/channels/channels/virtual_packages/win-64/repodata.json +++ b/tests/data/channels/channels/virtual_packages/win-64/repodata.json @@ -1 +1 @@ -{"info":{"subdir":"win-64"},"packages":{},"packages.conda":{"cuda-0.1.0-h28b8efe_0.conda":{"arch":"x86_64","build":"h28b8efe_0","build_number":0,"depends":["__cuda >=12"],"md5":"682b32f0087b6678010fe581268d834f","name":"cuda","platform":"win","sha256":"448c83ee3f71a992748cc98ebd297846baeda42288e0c8edc24a7efd543f37e4","size":989,"subdir":"win-64","timestamp":1756801677322,"version":"0.1.0"}},"repodata_version":2} \ No newline at end of file +{"info":{"subdir":"win-64"},"packages":{},"packages.conda":{"cuda-0.1.0-h28b8efe_0.conda":{"arch":"x86_64","build":"h28b8efe_0","build_number":0,"depends":["__cuda >=12"],"md5":"6a28f2f015ce5281856f1bef8e5d61e4","name":"cuda","platform":"win","sha256":"1ab4631af1b1910dd5f75c2f7de1ca1f8d3052434eb63460d1af809918631a59","size":991,"subdir":"win-64","timestamp":1783328856330,"version":"0.1.0"}},"repodata_version":2} \ No newline at end of file diff --git a/tests/data/channels/mappings.toml b/tests/data/channels/mappings.toml index 01991b538e..9efcf798c8 100644 --- a/tests/data/channels/mappings.toml +++ b/tests/data/channels/mappings.toml @@ -19,6 +19,7 @@ "trampoline/trampoline_2.yaml" = "trampoline_channel" "trampoline/trampoline_path.yaml" = "trampoline_path_channel" "virtual_packages/cuda.yaml" = "virtual_packages" +"virtual_packages/microarch-v3.yaml" = "virtual_packages" "virtual_packages/cuda-arch-sm100.yaml" = "virtual_packages" "virtual_packages/cuda-arch-sm120.yaml" = "virtual_packages" "virtual_packages/cuda-arch-sm90.yaml" = "virtual_packages" diff --git a/tests/data/channels/recipes/virtual_packages/microarch-v3.yaml b/tests/data/channels/recipes/virtual_packages/microarch-v3.yaml new file mode 100644 index 0000000000..17ab8da001 --- /dev/null +++ b/tests/data/channels/recipes/virtual_packages/microarch-v3.yaml @@ -0,0 +1,8 @@ +package: + name: microarch-v3 + version: 0.1.0 +build: + noarch: generic +requirements: + run: + - __archspec ==1 x86_64_v3 diff --git a/tests/integration_python/test_archspec_platforms.py b/tests/integration_python/test_archspec_platforms.py new file mode 100644 index 0000000000..c87755a69e --- /dev/null +++ b/tests/integration_python/test_archspec_platforms.py @@ -0,0 +1,164 @@ +"""End-to-end tests for DAG-aware ``__archspec`` handling. + +A workspace declares an ``x86_64_v3`` variant of the host subdir next to the +bare subdir, and ``CONDA_OVERRIDE_ARCHSPEC`` simulates hosts of different +capability so the tests are deterministic on any machine. The +``microarch-v3`` package in the in-repo ``virtual_packages`` channel requires +``__archspec ==1 x86_64_v3``, mirroring conda-forge's microarchitecture +metapackages. All tests stay network-free. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from .common import CURRENT_PLATFORM, ExitCode, verify_cli_command + +# The tests declare an x86_64_v3 variant of the host subdir, which only makes +# sense where the subdir's baseline microarchitecture is x86_64. +X86_64_SUBDIRS = {"linux-64", "win-64", "osx-64"} + +requires_x86_64_subdir = pytest.mark.skipif( + CURRENT_PLATFORM not in X86_64_SUBDIRS, + reason="the workspace declares an x86_64_v3 variant of the host subdir", +) + + +def _write_workspace(workspace: Path, channel: str, *, include_baseline: bool) -> Path: + baseline = f'\n "{CURRENT_PLATFORM}",' if include_baseline else "" + manifest = workspace / "pixi.toml" + manifest.write_text( + f""" +[workspace] +name = "archspec-variants" +channels = ["{channel}"] +platforms = [ + {{ name = "v3", platform = "{CURRENT_PLATFORM}", archspec = "x86_64_v3" }},{baseline} +] + +[tasks] +hi = "echo hi" + +[target.v3.dependencies] +microarch-v3 = "*" +""" + ) + return manifest + + +def _resolved_virtual_packages(workspace: Path) -> list[str]: + marker = workspace / ".pixi" / "envs" / "default" / "conda-meta" / "pixi" + return json.loads(marker.read_text())["resolved_platform"]["virtual_packages"] + + +def _microarch_v3_installed(workspace: Path) -> bool: + conda_meta = workspace / ".pixi" / "envs" / "default" / "conda-meta" + return any(entry.name.startswith("microarch-v3-") for entry in conda_meta.glob("*.json")) + + +@requires_x86_64_subdir +def test_capable_host_selects_archspec_variant( + pixi: Path, tmp_pixi_workspace: Path, virtual_packages_channel: str +) -> None: + """A host whose microarchitecture implements the declared baseline picks + the variant (declared first) and installs its packages.""" + manifest = _write_workspace(tmp_pixi_workspace, virtual_packages_channel, include_baseline=True) + verify_cli_command( + [pixi, "install", "--manifest-path", manifest], + ExitCode.SUCCESS, + env={"CONDA_OVERRIDE_ARCHSPEC": "skylake"}, + ) + assert "__archspec=0=x86_64_v3" in _resolved_virtual_packages(tmp_pixi_workspace) + assert _microarch_v3_installed(tmp_pixi_workspace) + + +@requires_x86_64_subdir +def test_baseline_host_falls_back_to_bare_subdir( + pixi: Path, tmp_pixi_workspace: Path, virtual_packages_channel: str +) -> None: + """A host below the variant's baseline falls through to the bare subdir + declared after it, without the variant's packages.""" + manifest = _write_workspace(tmp_pixi_workspace, virtual_packages_channel, include_baseline=True) + verify_cli_command( + [pixi, "install", "--manifest-path", manifest], + ExitCode.SUCCESS, + env={"CONDA_OVERRIDE_ARCHSPEC": "x86_64"}, + ) + assert "__archspec=0=x86_64_v3" not in _resolved_virtual_packages(tmp_pixi_workspace) + assert not _microarch_v3_installed(tmp_pixi_workspace) + + +@requires_x86_64_subdir +def test_incapable_host_is_refused_with_override_hint( + pixi: Path, tmp_pixi_workspace: Path, virtual_packages_channel: str +) -> None: + """Without a fallback platform, a host below the baseline is refused and + told exactly which override would vouch for the machine.""" + manifest = _write_workspace( + tmp_pixi_workspace, virtual_packages_channel, include_baseline=False + ) + verify_cli_command( + [pixi, "install", "--manifest-path", manifest], + ExitCode.FAILURE, + env={"CONDA_OVERRIDE_ARCHSPEC": "x86_64"}, + stderr_contains="CONDA_OVERRIDE_ARCHSPEC=x86_64_v3", + strip_ansi=True, + ) + + +@requires_x86_64_subdir +def test_run_refuses_weaker_machine_than_installed_for( + pixi: Path, tmp_pixi_workspace: Path, virtual_packages_channel: str +) -> None: + """An environment installed for the x86_64_v3 variant refuses to run on a + machine that only provides the x86_64 baseline: the installed packages + require `__archspec ==1 x86_64_v3`.""" + manifest = _write_workspace( + tmp_pixi_workspace, virtual_packages_channel, include_baseline=False + ) + verify_cli_command( + [pixi, "install", "--manifest-path", manifest], + ExitCode.SUCCESS, + env={"CONDA_OVERRIDE_ARCHSPEC": "x86_64_v3"}, + ) + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "hi"], + ExitCode.FAILURE, + env={"CONDA_OVERRIDE_ARCHSPEC": "x86_64"}, + stderr_contains="__archspec", + strip_ansi=True, + ) + + +def test_add_rejects_unknown_archspec_name(pixi: Path, tmp_pixi_workspace: Path) -> None: + """The CLI rejects archspec names the database does not know, with a + did-you-mean hint for the dashed spelling.""" + manifest = tmp_pixi_workspace / "pixi.toml" + manifest.write_text( + f""" +[workspace] +name = "archspec-reject" +channels = [] +platforms = ["{CURRENT_PLATFORM}"] +""" + ) + verify_cli_command( + [ + pixi, + "workspace", + "--manifest-path", + manifest, + "platform", + "add", + f"v3={CURRENT_PLATFORM}", + "--archspec", + "x86-64-v3", + "--no-install", + ], + ExitCode.FAILURE, + stderr_contains="did you mean 'x86_64_v3'", + strip_ansi=True, + )