diff --git a/crates/pixi_cli/src/global/install.rs b/crates/pixi_cli/src/global/install.rs index a77ca9a5a2..0ea1ff386e 100644 --- a/crates/pixi_cli/src/global/install.rs +++ b/crates/pixi_cli/src/global/install.rs @@ -5,7 +5,7 @@ use indexmap::IndexMap; use clap::Parser; use fancy_display::FancyDisplay; use itertools::Itertools; -use miette::Report; +use miette::{IntoDiagnostic, Report}; use rattler_conda_types::{MatchSpec, NamedChannelOrUrl, Platform}; use crate::global::{global_specs::GlobalSpecs, revert_environment_after_error}; @@ -14,6 +14,7 @@ use pixi_global::{ self, EnvChanges, EnvState, EnvironmentName, Mapping, Project, StateChange, StateChanges, common::{NotChangedReason, contains_menuinst_document}, list::list_all_global_environments, + overrides_from_env, project::{ExposedType, GlobalSpec}, }; @@ -203,6 +204,14 @@ async fn setup_environment( project.manifest.set_platform(env_name, platform)?; } + // Record any `CONDA_OVERRIDE_*` environment variables as overrides in the + // manifest, so that operations that re-solve the environment later (like + // `pixi global update`) use the same values. + let env_overrides = overrides_from_env().into_diagnostic()?; + if !env_overrides.is_empty() { + project.manifest.set_overrides(env_name, &env_overrides)?; + } + let converted_with_inclusions = args .with .iter() diff --git a/crates/pixi_command_dispatcher/src/command_dispatcher/builder.rs b/crates/pixi_command_dispatcher/src/command_dispatcher/builder.rs index a87f7ff009..a021a9e493 100644 --- a/crates/pixi_command_dispatcher/src/command_dispatcher/builder.rs +++ b/crates/pixi_command_dispatcher/src/command_dispatcher/builder.rs @@ -386,7 +386,7 @@ impl CommandDispatcherBuilder { let tool_platform = self.tool_platform.unwrap_or_else(|| { let platform = Platform::current(); let virtual_packages = - VirtualPackages::detect(&VirtualPackageOverrides::default()).unwrap_or_default(); + VirtualPackages::detect(&VirtualPackageOverrides::from_env()).unwrap_or_default(); ( platform, virtual_packages.into_generic_virtual_packages().collect(), diff --git a/crates/pixi_global/src/lib.rs b/crates/pixi_global/src/lib.rs index df705e5a72..4a24c6afbf 100644 --- a/crates/pixi_global/src/lib.rs +++ b/crates/pixi_global/src/lib.rs @@ -8,7 +8,7 @@ pub mod trampoline; pub use common::{BinDir, EnvChanges, EnvDir, EnvRoot, EnvState, StateChange, StateChanges}; use pixi_utils::executable_from_path; -pub use project::{EnvironmentName, ExposedName, Mapping, Project}; +pub use project::{EnvironmentName, ExposedName, Mapping, Project, overrides_from_env}; use pixi_utils::prefix::{Executable, Prefix}; use rattler_conda_types::PrefixRecord; diff --git a/crates/pixi_global/src/project/manifest.rs b/crates/pixi_global/src/project/manifest.rs index 46a5dd3e90..311cc9e3db 100644 --- a/crates/pixi_global/src/project/manifest.rs +++ b/crates/pixi_global/src/project/manifest.rs @@ -9,7 +9,9 @@ use indexmap::IndexSet; use miette::IntoDiagnostic; use pixi_config::Config; use pixi_consts::consts; -use pixi_manifest::{PrioritizedChannel, toml::TomlDocument}; +use pixi_manifest::{ + LibCSystemRequirement, PrioritizedChannel, SystemRequirements, toml::TomlDocument, +}; use pixi_toml::TomlIndexMap; use pixi_utils::{executable_from_path, strip_executable_extension}; use rattler_conda_types::{NamedChannelOrUrl, PackageName, Platform}; @@ -242,6 +244,73 @@ impl Manifest { Ok(()) } + /// Records virtual package overrides for a specific environment in the + /// manifest. Only the overrides that are specified are written, overrides + /// that were recorded earlier are kept. + pub fn set_overrides( + &mut self, + env_name: &EnvironmentName, + overrides: &SystemRequirements, + ) -> miette::Result<()> { + // Ensure the environment exists + let env = self.parsed.envs.get_mut(env_name).ok_or_else(|| { + miette::miette!("Environment {} doesn't exist", env_name.fancy_display()) + })?; + + if overrides.is_empty() { + return Ok(()); + } + + // Update self.parsed, overrides that are specified overwrite + // previously recorded values. + if let Some(cuda) = &overrides.cuda { + env.overrides.cuda = Some(cuda.clone()); + } + if let Some(linux) = &overrides.linux { + env.overrides.linux = Some(linux.clone()); + } + if let Some(macos) = &overrides.macos { + env.overrides.macos = Some(macos.clone()); + } + if let Some(libc) = &overrides.libc { + env.overrides.libc = Some(libc.clone()); + } + + // Update self.document + let table = + self.document + .get_or_insert_nested_table(&["envs", env_name.as_str(), "overrides"])?; + if let Some(cuda) = &overrides.cuda { + table.insert("cuda", toml_edit::value(cuda.to_string())); + } + if let Some(linux) = &overrides.linux { + table.insert("linux", toml_edit::value(linux.to_string())); + } + if let Some(macos) = &overrides.macos { + table.insert("macos", toml_edit::value(macos.to_string())); + } + if let Some(libc) = &overrides.libc { + let value = match libc { + LibCSystemRequirement::GlibC(version) => toml_edit::value(version.to_string()), + LibCSystemRequirement::OtherFamily(family_and_version) => { + let mut libc_table = toml_edit::InlineTable::new(); + if let Some(family) = &family_and_version.family { + libc_table.insert("family", family.as_str().into()); + } + libc_table.insert("version", family_and_version.version.to_string().into()); + Item::Value(toml_edit::Value::InlineTable(libc_table)) + } + }; + table.insert("libc", value); + } + + tracing::debug!( + "Set virtual package overrides for environment {} in toml document", + env_name.fancy_display() + ); + Ok(()) + } + #[allow(dead_code)] /// Adds a channel to the manifest pub fn add_channel( @@ -1092,6 +1161,47 @@ mod tests { assert_eq!(actual_platform, platform); } + #[test] + fn test_set_overrides() { + let mut manifest = Manifest::default(); + let env_name = EnvironmentName::from_str("test-env").unwrap(); + manifest.add_environment(&env_name, None).unwrap(); + + let overrides = SystemRequirements { + cuda: Some("12.0".parse().unwrap()), + macos: Some("13.0".parse().unwrap()), + ..Default::default() + }; + manifest.set_overrides(&env_name, &overrides).unwrap(); + + // Recording again only overwrites the specified fields. + let overrides = SystemRequirements { + cuda: Some("11.0".parse().unwrap()), + libc: Some(LibCSystemRequirement::GlibC("2.17".parse().unwrap())), + ..Default::default() + }; + manifest.set_overrides(&env_name, &overrides).unwrap(); + + // Check parsed + let parsed = &manifest.parsed.envs.get(&env_name).unwrap().overrides; + assert_eq!(parsed.cuda, Some("11.0".parse().unwrap())); + assert_eq!(parsed.macos, Some("13.0".parse().unwrap())); + assert_eq!( + parsed.libc, + Some(LibCSystemRequirement::GlibC("2.17".parse().unwrap())) + ); + + // Check that the document round-trips to the same requirements. + let reparsed = + Manifest::from_str(Path::new("global.toml"), manifest.document.to_string()).unwrap(); + assert_eq!( + &reparsed.parsed.envs.get(&env_name).unwrap().overrides, + parsed + ); + + assert_snapshot!(manifest.document.to_string()); + } + #[test] fn test_add_channel() { let mut manifest = Manifest::default(); diff --git a/crates/pixi_global/src/project/mod.rs b/crates/pixi_global/src/project/mod.rs index 879320b4a1..2f19ec4031 100644 --- a/crates/pixi_global/src/project/mod.rs +++ b/crates/pixi_global/src/project/mod.rs @@ -29,7 +29,10 @@ use pixi_command_dispatcher::{ use pixi_config::{Config, RunPostLinkScripts, default_channel_config, pixi_home}; use pixi_consts::consts::{self}; use pixi_core::repodata::Repodata; -use pixi_manifest::PrioritizedChannel; +use pixi_manifest::{ + GLIBC_FAMILY, LibCFamilyAndVersion, LibCSystemRequirement, PrioritizedChannel, + SystemRequirements, +}; use pixi_path::AbsPathBuf; use pixi_reporters::TopLevelProgress; use pixi_spec::{BinarySpec, PathBinarySpec}; @@ -48,7 +51,8 @@ use rattler_networking::LazyClient; use rattler_repodata_gateway::Gateway; // Removed unused rattler_solve imports use rattler_virtual_packages::{ - DetectVirtualPackageError, VirtualPackage, VirtualPackageOverrides, + Archspec, Cuda, DetectVirtualPackageError, EnvOverride, LibC, Linux, Osx, VirtualPackage, + VirtualPackageOverrides, Windows, }; use tokio::sync::Semaphore; use toml_edit::DocumentMut; @@ -561,25 +565,57 @@ impl Project { self.config.global_channel_config() } - /// Check if the platform matches the current platform (OS) - /// We only need to detect virtual packages if the platform is the current - /// one. Otherwise, we use an empty list + /// Returns the virtual packages to use when solving an environment for + /// the given platform. + /// + /// The base set is detected from the current machine, but only if the + /// platform matches the current platform (OS). Otherwise, nothing can be + /// detected. On top of that, the overrides recorded in the manifest take + /// precedence over the detected values, and the `CONDA_OVERRIDE_*` + /// environment variables take precedence over both, so users can always + /// override the recorded values ephemerally. pub(crate) fn virtual_packages_for( platform: &Platform, + overrides: &SystemRequirements, ) -> Result, DetectVirtualPackageError> { - if platform + let is_current_os = platform .only_platform() .map(|p| p == Platform::current().only_platform().unwrap_or("")) - .unwrap_or(false) - { - Ok(VirtualPackage::detect(&VirtualPackageOverrides::default())? - .iter() - .cloned() - .map(GenericVirtualPackage::from) - .collect()) + .unwrap_or(false); + + let mut virtual_packages = if is_current_os { + VirtualPackage::detect(&VirtualPackageOverrides::default())? } else { - Ok(vec![]) + Vec::new() + }; + + for requirement in overrides.virtual_packages() { + upsert_virtual_package(&mut virtual_packages, requirement); } + + apply_env_override::(&mut virtual_packages, |vp| { + matches!(vp, VirtualPackage::Win(_)) + })?; + apply_env_override::(&mut virtual_packages, |vp| { + matches!(vp, VirtualPackage::Linux(_)) + })?; + apply_env_override::(&mut virtual_packages, |vp| { + matches!(vp, VirtualPackage::Osx(_)) + })?; + apply_env_override::(&mut virtual_packages, |vp| { + matches!(vp, VirtualPackage::LibC(_)) + })?; + apply_env_override::(&mut virtual_packages, |vp| { + matches!(vp, VirtualPackage::Cuda(_)) + })?; + apply_env_override::(&mut virtual_packages, |vp| { + matches!(vp, VirtualPackage::Archspec(_)) + })?; + + Ok(virtual_packages + .into_iter() + .map(GenericVirtualPackage::from) + .collect()) } pub async fn install_environment( @@ -628,7 +664,7 @@ impl Project { let build_environment = BuildEnvironment::simple( platform, - Self::virtual_packages_for(&platform).into_diagnostic()?, + Self::virtual_packages_for(&platform, &environment.overrides).into_diagnostic()?, ); // Create solve spec (compute-engine keys path). let solve_spec = SolvePixiEnvironmentSpec { @@ -1571,6 +1607,64 @@ impl Project { } } +/// Inserts the given virtual package into the list, replacing an existing +/// virtual package of the same kind. +fn upsert_virtual_package(virtual_packages: &mut Vec, package: VirtualPackage) { + if let Some(existing) = virtual_packages + .iter_mut() + .find(|vp| std::mem::discriminant(&**vp) == std::mem::discriminant(&package)) + { + *existing = package; + } else { + virtual_packages.push(package); + } +} + +/// Applies the `CONDA_OVERRIDE_*` environment variable of the given virtual +/// package kind on top of the given virtual packages. Setting the variable to +/// an empty string removes the virtual package entirely. +fn apply_env_override>( + virtual_packages: &mut Vec, + is_kind: fn(&VirtualPackage) -> bool, +) -> Result<(), DetectVirtualPackageError> { + match std::env::var(T::DEFAULT_ENV_NAME) { + Ok(value) => { + virtual_packages.retain(|vp| !is_kind(vp)); + if let Some(package) = T::parse_version_opt(&value)? { + virtual_packages.push(package.into()); + } + Ok(()) + } + Err(std::env::VarError::NotPresent) => Ok(()), + Err(err) => Err(err.into()), + } +} + +/// Constructs [`SystemRequirements`] from the `CONDA_OVERRIDE_*` environment +/// variables that are currently set. This is used to record the overrides in +/// the manifest so subsequent solves use the same values. +pub fn overrides_from_env() -> Result { + Ok(SystemRequirements { + cuda: Cuda::from_env_var_name_or(Cuda::DEFAULT_ENV_NAME, || Ok(None))? + .map(|cuda| cuda.version), + linux: Linux::from_env_var_name_or(Linux::DEFAULT_ENV_NAME, || Ok(None))? + .map(|linux| linux.version), + macos: Osx::from_env_var_name_or(Osx::DEFAULT_ENV_NAME, || Ok(None))? + .map(|osx| osx.version), + libc: LibC::from_env_var_name_or(LibC::DEFAULT_ENV_NAME, || Ok(None))?.map(|libc| { + if libc.family == GLIBC_FAMILY { + LibCSystemRequirement::GlibC(libc.version) + } else { + LibCSystemRequirement::OtherFamily(LibCFamilyAndVersion { + family: Some(libc.family), + version: libc.version, + }) + } + }), + archspec: None, + }) +} + impl Repodata for Project { /// Returns the [`Gateway`] used by this project. fn repodata_gateway(&self) -> miette::Result<&Gateway> { @@ -1611,6 +1705,38 @@ mod tests { dummy = "dummy" "#; + #[test] + fn test_virtual_packages_for_overrides() { + let overrides = SystemRequirements { + cuda: Some("12.0".parse().unwrap()), + ..Default::default() + }; + + // A recorded override replaces the detected value (or adds it when + // nothing was detected). + let virtual_packages = + Project::virtual_packages_for(&Platform::current(), &overrides).unwrap(); + let cuda = virtual_packages + .iter() + .find(|vp| vp.name.as_normalized() == "__cuda") + .expect("cuda virtual package should be present"); + assert_eq!(cuda.version.to_string(), "12.0"); + + // For a platform with a different OS nothing is detected, but the + // recorded overrides are still used. + let other_platform = if Platform::current().is_windows() { + Platform::Linux64 + } else { + Platform::Win64 + }; + let virtual_packages = Project::virtual_packages_for(&other_platform, &overrides).unwrap(); + let names = virtual_packages + .iter() + .map(|vp| vp.name.as_normalized()) + .collect_vec(); + assert_eq!(names, vec!["__cuda"]); + } + #[tokio::test] async fn test_project_from_str() { // Use a deterministic path with a parent. `FilePath().fake()` from diff --git a/crates/pixi_global/src/project/parsed_manifest.rs b/crates/pixi_global/src/project/parsed_manifest.rs index 40fe65768f..e4a3b56bc3 100644 --- a/crates/pixi_global/src/project/parsed_manifest.rs +++ b/crates/pixi_global/src/project/parsed_manifest.rs @@ -6,7 +6,10 @@ use indexmap::{IndexMap, IndexSet}; use itertools::{Either, Itertools}; use miette::{Context, Diagnostic, IntoDiagnostic, LabeledSpan, NamedSource, Report}; use pixi_consts::consts; -use pixi_manifest::{PrioritizedChannel, toml::TomlPlatform, utils::package_map::UniquePackageMap}; +use pixi_manifest::{ + PrioritizedChannel, SystemRequirements, toml::TomlPlatform, + utils::package_map::UniquePackageMap, +}; use pixi_spec::PixiSpec; use pixi_toml::{TomlFromStr, TomlIndexMap, TomlIndexSet, TomlWith}; use rattler_conda_types::{NamedChannelOrUrl, PackageName, Platform}; @@ -297,6 +300,10 @@ pub struct ParsedEnvironment { /// Platform used by the environment. pub platform: Option, pub dependencies: UniquePackageMap, + /// Overrides for the virtual packages detected on the machine, used when + /// solving the environment. + #[serde(skip_serializing_if = "SystemRequirements::is_empty")] + pub overrides: SystemRequirements, #[serde(default, serialize_with = "serialize_expose_mappings")] pub exposed: IndexSet, pub shortcuts: Option>, @@ -312,6 +319,7 @@ impl<'de> toml_span::Deserialize<'de> for ParsedEnvironment { .unwrap_or_default(); let platform = th.optional::("platform").map(Platform::from); let dependencies = th.optional("dependencies").unwrap_or_default(); + let overrides = th.optional("overrides").unwrap_or_default(); let exposed = th .optional::("exposed") .map(TomlMapping::into_inner) @@ -326,6 +334,7 @@ impl<'de> toml_span::Deserialize<'de> for ParsedEnvironment { channels, platform, dependencies, + overrides, exposed, shortcuts, }) @@ -513,6 +522,25 @@ mod tests { ); } + #[test] + fn test_overrides_deserialization() { + let contents = r#" + [envs.cuda] + channels = ["conda-forge"] + [envs.cuda.dependencies] + cuda-version = "13.*" + [envs.cuda.overrides] + cuda = "13.0" + libc = { family = "glibc", version = "2.17" } + "#; + let manifest = ParsedManifest::from_toml_str(contents).unwrap(); + let env = manifest.envs.values().next().unwrap(); + assert_eq!(env.overrides.cuda, Some("13.0".parse().unwrap())); + let (family, version) = env.overrides.libc.as_ref().unwrap().family_and_version(); + assert_eq!(family, "glibc"); + assert_eq!(version.to_string(), "2.17"); + } + #[test] fn test_tool_deserialization() { let contents = r#" diff --git a/crates/pixi_global/src/project/snapshots/pixi_global__project__manifest__tests__set_overrides.snap b/crates/pixi_global/src/project/snapshots/pixi_global__project__manifest__tests__set_overrides.snap new file mode 100644 index 0000000000..086b366332 --- /dev/null +++ b/crates/pixi_global/src/project/snapshots/pixi_global__project__manifest__tests__set_overrides.snap @@ -0,0 +1,12 @@ +--- +source: crates/pixi_global/src/project/manifest.rs +assertion_line: 1202 +expression: manifest.document.to_string() +--- +[envs.test-env] +channels = ["conda-forge"] + +[envs.test-env.overrides] +cuda = "11.0" +macos = "13.0" +libc = "2.17" diff --git a/crates/pixi_global/src/project/snapshots/pixi_global__project__parsed_manifest__tests__invalid_key.snap b/crates/pixi_global/src/project/snapshots/pixi_global__project__parsed_manifest__tests__invalid_key.snap index c7676070e8..1e7b579efd 100644 --- a/crates/pixi_global/src/project/snapshots/pixi_global__project__parsed_manifest__tests__invalid_key.snap +++ b/crates/pixi_global/src/project/snapshots/pixi_global__project__parsed_manifest__tests__invalid_key.snap @@ -1,9 +1,10 @@ --- -source: src/global/project/parsed_manifest.rs +source: crates/pixi_global/src/project/parsed_manifest.rs +assertion_line: 436 expression: "examples.into_iter().map(|example|\nParsedManifest::from_toml_str(example).unwrap_err().to_string()).collect::>().join(\"\\n\")" --- unexpected keys in table: `[("invalid", Span { start: 1, end: 8 })]` expected: ["version", "envs"] unexpected keys in table: `[("invalid", Span { start: 14, end: 21 })]` -expected: ["channels", "platform", "dependencies", "exposed", "shortcuts"] +expected: ["channels", "platform", "dependencies", "overrides", "exposed", "shortcuts"] Failed to parse environment name 'python;3', please use only lowercase letters, numbers, dashes, underscores and dots diff --git a/docs/global_tools/manifest.md b/docs/global_tools/manifest.md index 2ab2751d6b..56049e303e 100644 --- a/docs/global_tools/manifest.md +++ b/docs/global_tools/manifest.md @@ -178,6 +178,39 @@ dependencies = { dotnet = "*" } exposed = { dotnet = 'dotnet\dotnet' } ``` +## Virtual package overrides + +Some packages depend on or are constrained by [virtual packages](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-virtual.html) like `__cuda`. +When solving an environment, these virtual packages are detected on your machine. +The detected values can be overridden with the `CONDA_OVERRIDE_*` environment variables, for example `CONDA_OVERRIDE_CUDA=12.0`. + +Overrides that are set while running `pixi global install` are recorded in the manifest: + +```shell +CONDA_OVERRIDE_CUDA=12.0 pixi global install cuda-tool +``` + +results in: + +```toml +[envs.cuda-tool] +channels = ["conda-forge"] +dependencies = { cuda-tool = "*" } +exposed = { cuda-tool = "cuda-tool" } + +[envs.cuda-tool.overrides] +cuda = "12.0" +``` + +This way commands that solve the environment again later, like `pixi global update` or `pixi global sync`, keep using the same values. +The table can also be edited by hand and supports the same fields as the workspace [system requirements](../workspace/system_requirements.md): `linux`, `cuda`, `macos` and `libc`. + +When an environment is solved the following precedence applies: + +1. `CONDA_OVERRIDE_*` environment variables, so the recorded values can always be overridden ephemerally. +2. The values recorded under `overrides`. +3. The virtual packages detected on your machine. + ## Shortcuts Especially for graphical user interfaces it is useful to add shortcuts. diff --git a/tests/integration_python/pixi_global/test_global.py b/tests/integration_python/pixi_global/test_global.py index f65f4ad8ae..34294a10ac 100644 --- a/tests/integration_python/pixi_global/test_global.py +++ b/tests/integration_python/pixi_global/test_global.py @@ -2481,3 +2481,75 @@ def test_install_nonexistent_package_no_empty_dir( assert not (envs_dir / "this-package-does-not-exist").exists(), ( "Empty directory was left behind for failed package installation" ) + + +@pytest.mark.skipif( + platform.system() == "Darwin", + reason="The virtual packages channel doesn't contain a cuda package for macOS", +) +def test_install_records_virtual_package_overrides( + pixi: Path, tmp_path: Path, virtual_packages_channel: str +) -> None: + """The `cuda` package in the channel depends on `__cuda >= 12`.""" + env = {"PIXI_HOME": str(tmp_path)} + manifest = tmp_path / "manifests" / "pixi-global.toml" + + # Installing with an override that doesn't satisfy `__cuda >= 12` fails. + verify_cli_command( + [pixi, "global", "install", "--channel", virtual_packages_channel, "cuda"], + ExitCode.FAILURE, + env=env | {"CONDA_OVERRIDE_CUDA": "11.0"}, + ) + + # With a satisfying override the install succeeds, and the override is + # recorded in the manifest. + verify_cli_command( + [pixi, "global", "install", "--channel", virtual_packages_channel, "cuda"], + env=env | {"CONDA_OVERRIDE_CUDA": "12.0"}, + ) + parsed_manifest = tomllib.loads(manifest.read_text()) + assert parsed_manifest["envs"]["cuda"]["overrides"]["cuda"] == "12.0" + + # Because the override is recorded, re-creating the environment without + # the environment variable still uses the recorded value. + shutil.rmtree(tmp_path / "envs") + verify_cli_command([pixi, "global", "sync"], env=env) + + # The environment variable takes precedence over the recorded value. + shutil.rmtree(tmp_path / "envs") + verify_cli_command( + [pixi, "global", "sync"], + ExitCode.FAILURE, + env=env | {"CONDA_OVERRIDE_CUDA": "11.0"}, + ) + + +@pytest.mark.skipif( + platform.system() == "Darwin", + reason="The virtual packages channel doesn't contain a cuda package for macOS", +) +def test_sync_uses_overrides_from_manifest( + pixi: Path, tmp_path: Path, virtual_packages_channel: str +) -> None: + """The `cuda` package in the channel depends on `__cuda >= 12`.""" + env = {"PIXI_HOME": str(tmp_path)} + manifests = tmp_path.joinpath("manifests") + manifests.mkdir() + manifest = manifests.joinpath("pixi-global.toml") + toml = f""" + [envs.cuda] + channels = ["{virtual_packages_channel}"] + dependencies = {{ cuda = "*" }} + + [envs.cuda.overrides] + cuda = "12.0" + """ + manifest.write_text(toml) + + # The system requirement satisfies `__cuda >= 12`. + verify_cli_command([pixi, "global", "sync"], env=env) + + # Lowering the requirement makes the solve fail. + shutil.rmtree(tmp_path / "envs") + manifest.write_text(toml.replace('cuda = "12.0"', 'cuda = "11.0"')) + verify_cli_command([pixi, "global", "sync"], ExitCode.FAILURE, env=env)