diff --git a/crates/pixi/tests/integration_rust/add_tests.rs b/crates/pixi/tests/integration_rust/add_tests.rs index d8337c8ba6..77d6c7b00a 100644 --- a/crates/pixi/tests/integration_rust/add_tests.rs +++ b/crates/pixi/tests/integration_rust/add_tests.rs @@ -544,7 +544,8 @@ index-url = "{index_url}" source: PixiPypiSource::Registry { version: VersionOrStar::from_str("==24.8.0").unwrap(), index: None, - } + }, + env_markers: pep508_rs::MarkerTree::default(), } ); } diff --git a/crates/pixi/tests/integration_rust/pypi_tests.rs b/crates/pixi/tests/integration_rust/pypi_tests.rs index 6a9b6275c5..ea297cd1d2 100644 --- a/crates/pixi/tests/integration_rust/pypi_tests.rs +++ b/crates/pixi/tests/integration_rust/pypi_tests.rs @@ -150,6 +150,83 @@ test = {{features = ["test"]}} ); } +#[tokio::test] +async fn pyproject_environment_markers_resolved() { + setup_tracing(); + + // Add a dependency that's present only on linux-64 + let simple = PyPIDatabase::new() + .with(PyPIPackage::new("nvidia-nccl-cu12", "1.0.0").with_tag( + "cp311", + "cp311", + "manylinux1_x86_64", + )) + .into_simple_index() + .unwrap(); + + // Create a TOML with two platforms + let platform1 = Platform::Linux64; + let platform2 = Platform::OsxArm64; + let platform_str = format!("\"{}\", \"{}\"", platform1, platform2); + + let mut package_db = MockRepoData::default(); + package_db.add_package( + Package::build("python", "3.11.0") + .with_subdir(platform1) + .finish(), + ); + package_db.add_package( + Package::build("python", "3.11.0") + .with_subdir(platform2) + .finish(), + ); + let channel = package_db.into_channel().await.unwrap(); + let channel_url = channel.url(); + let index_url = simple.index_url(); + + // Make sure that the TOML contains an env marker to allow linux-64. + let pyproject = format!( + r#" +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "environment-markers" +dependencies = [ + "nvidia-nccl-cu12; sys_platform == 'linux'" +] + +[tool.pixi.workspace] +channels = ["{channel_url}"] +platforms = [{platform_str}] +conda-pypi-map = {{}} + +[tool.pixi.dependencies] +python = "==3.11.0" + +[tool.pixi.pypi-options] +index-url = "{index_url}" +"#, + ); + + let pixi = PixiControl::from_pyproject_manifest(&pyproject).unwrap(); + + let lock = pixi.update_lock_file().await.unwrap(); + + let nccl_req = Requirement::from_str("nvidia-nccl-cu12; sys_platform == 'linux'").unwrap(); + // Check that the requirement is present in the lockfile for linux-64 + assert!( + lock.contains_pep508_requirement("default", platform1, nccl_req.clone()), + "default environment should include nccl for linux-64" + ); + // But not for osx-arm64 + assert!( + !lock.contains_pep508_requirement("default", platform2, nccl_req.clone()), + "default environment shouldn't include nccl for osx-arm64" + ); +} + #[tokio::test] async fn test_flat_links_based_index_returns_path() { setup_tracing(); diff --git a/crates/pixi_cli/src/upgrade.rs b/crates/pixi_cli/src/upgrade.rs index b323b0ae54..b88976a67b 100644 --- a/crates/pixi_cli/src/upgrade.rs +++ b/crates/pixi_cli/src/upgrade.rs @@ -5,7 +5,7 @@ use fancy_display::FancyDisplay; use indexmap::{IndexMap, IndexSet}; use itertools::Itertools; use miette::{IntoDiagnostic, MietteDiagnostic, WrapErr}; -use pep508_rs::{MarkerTree, Requirement}; +use pep508_rs::Requirement; use pixi_config::ConfigCli; use pixi_core::{ WorkspaceLocator, @@ -473,8 +473,7 @@ pub fn parse_specs_for_platform( Requirement { name: name.as_normalized().clone(), extras: req.extras.clone(), - // TODO: Add marker support here to avoid overwriting existing markers - marker: MarkerTree::default(), + marker: req.env_markers.clone(), origin: None, version_or_url: None, }, diff --git a/crates/pixi_cli/src/workspace/export/conda_environment.rs b/crates/pixi_cli/src/workspace/export/conda_environment.rs index c8ccd5e10b..d334d4f461 100644 --- a/crates/pixi_cli/src/workspace/export/conda_environment.rs +++ b/crates/pixi_cli/src/workspace/export/conda_environment.rs @@ -50,8 +50,9 @@ fn format_pip_extras(extras: &[ExtraName]) -> String { fn format_pip_dependency(name: &PypiPackageName, requirement: &PixiPypiSpec) -> String { let extras = &requirement.extras; + let markers = &requirement.env_markers; - match &requirement.source { + let mut dependency = match &requirement.source { PixiPypiSource::Git { git: git_url } => { let mut git_string = format!( "{name}{extras} @ git+{url}", @@ -115,7 +116,14 @@ fn format_pip_dependency(name: &PypiPackageName, requirement: &PixiPypiSpec) -> extras = format_pip_extras(extras) ), }, + }; + + let marker_str = markers.try_to_string(); + if let Some(marker_str) = marker_str { + dependency.push_str(&format!("; {marker_str}")); } + + dependency } fn build_env_yaml( diff --git a/crates/pixi_core/src/lock_file/resolve/pypi.rs b/crates/pixi_core/src/lock_file/resolve/pypi.rs index 26fd90f5a1..5f15506787 100644 --- a/crates/pixi_core/src/lock_file/resolve/pypi.rs +++ b/crates/pixi_core/src/lock_file/resolve/pypi.rs @@ -390,15 +390,6 @@ pub async fn resolve_pypi( } } - let requirements = dependencies - .into_iter() - .flat_map(|(name, req)| { - req.into_iter() - .map(move |r| as_uv_req(&r, name.as_ref(), project_root)) - }) - .collect::, _>>() - .into_diagnostic()?; - // Determine the python interpreter that is installed as part of the conda // packages. let python_record = locked_pixi_records @@ -414,6 +405,16 @@ pub async fn resolve_pypi( // Construct the marker environment for the target platform let marker_environment = determine_marker_environment(platform, python_record.as_ref())?; + let requirements = dependencies + .into_iter() + .flat_map(|(name, req)| { + req.into_iter() + .map(move |r| as_uv_req(&r, name.as_ref(), project_root)) + }) + .filter_ok(|uv_req| uv_req.evaluate_markers(Some(&marker_environment), &uv_req.extras)) + .collect::, _>>() + .into_diagnostic()?; + // Determine the tags for this particular solve. let tags = get_pypi_tags(platform, &system_requirements, python_record.as_ref())?; diff --git a/crates/pixi_core/src/lock_file/satisfiability/mod.rs b/crates/pixi_core/src/lock_file/satisfiability/mod.rs index 2e3352432d..5530364ee0 100644 --- a/crates/pixi_core/src/lock_file/satisfiability/mod.rs +++ b/crates/pixi_core/src/lock_file/satisfiability/mod.rs @@ -1604,22 +1604,56 @@ pub(crate) async fn verify_package_platform_satisfiability( }) .collect::, _>>()?; + // Find the python interpreter from the list of conda packages. Note that this + // refers to the locked python interpreter, it might not match the specs + // from the environment. That is ok because we will find that out when we + // check all the records. + let python_interpreter_record = locked_pixi_records.python_interpreter_record(); + + // Determine the marker environment from the python interpreter package. + let marker_environment = python_interpreter_record + .map(|interpreter| determine_marker_environment(platform, &interpreter.package_record)) + .transpose() + .map_err(|err| { + Box::new(PlatformUnsat::FailedToDetermineMarkerEnvironment( + err.into(), + )) + }); + + let pypi_dependencies = environment.pypi_dependencies(Some(platform)); + + // We cannot determine the marker environment, for example if installing + // `wasm32` dependencies. However, it also doesn't really matter if we don't + // have any pypi requirements. + let marker_environment = match marker_environment { + Err(err) => { + if !pypi_dependencies.is_empty() { + return Err(err); + } else { + None + } + } + Ok(marker_environment) => marker_environment, + }; + // Transform from PyPiPackage name into UV Requirement type - let pypi_requirements = environment - .pypi_dependencies(Some(platform)) + let pypi_requirements = pypi_dependencies .iter() .flat_map(|(name, reqs)| { - reqs.iter().map(move |req| { - Ok::>(Dependency::PyPi( - as_uv_req(req, name.as_source(), project_root).map_err(|e| { - Box::new(PlatformUnsat::AsPep508Error( - name.as_normalized().clone(), - e, - )) - })?, - "".into(), - )) - }) + reqs.iter() + .map(|req| as_uv_req(req, name.as_source(), project_root)) + .filter_ok(|req| req.evaluate_markers(marker_environment.as_ref(), &req.extras)) + .map(move |req| { + Ok::>(Dependency::PyPi( + req.map_err(|e| { + Box::new(PlatformUnsat::AsPep508Error( + name.as_normalized().clone(), + e, + )) + })?, + "".into(), + )) + }) }) .collect::, _>>()?; @@ -1703,36 +1737,6 @@ pub(crate) async fn verify_package_platform_satisfiability( return Err(Box::new(PlatformUnsat::TooManyCondaPackages(Vec::new()))); } - // Find the python interpreter from the list of conda packages. Note that this - // refers to the locked python interpreter, it might not match the specs - // from the environment. That is ok because we will find that out when we - // check all the records. - let python_interpreter_record = locked_pixi_records.python_interpreter_record(); - - // Determine the marker environment from the python interpreter package. - let marker_environment = python_interpreter_record - .map(|interpreter| determine_marker_environment(platform, &interpreter.package_record)) - .transpose() - .map_err(|err| { - Box::new(PlatformUnsat::FailedToDetermineMarkerEnvironment( - err.into(), - )) - }); - - // We cannot determine the marker environment, for example if installing - // `wasm32` dependencies. However, it also doesn't really matter if we don't - // have any pypi requirements. - let marker_environment = match marker_environment { - Err(err) => { - if !pypi_requirements.is_empty() { - return Err(err); - } else { - None - } - } - Ok(marker_environment) => marker_environment, - }; - // Determine the pypi packages provided by the locked conda packages. let locked_conda_pypi_packages = locked_pixi_records .by_pypi_name() diff --git a/crates/pixi_core/src/lock_file/satisfiability/snapshots/pixi_core__lock_file__satisfiability__tests__failing_satisfiability@changed-env-marker.snap b/crates/pixi_core/src/lock_file/satisfiability/snapshots/pixi_core__lock_file__satisfiability__tests__failing_satisfiability@changed-env-marker.snap new file mode 100644 index 0000000000..2229ce068c --- /dev/null +++ b/crates/pixi_core/src/lock_file/satisfiability/snapshots/pixi_core__lock_file__satisfiability__tests__failing_satisfiability@changed-env-marker.snap @@ -0,0 +1,8 @@ +--- +source: crates/pixi_core/src/lock_file/satisfiability/mod.rs +expression: s +snapshot_kind: text +--- +environment 'default' does not satisfy the requirements of the project for platform 'osx-arm64' + Diagnostic severity: error + Caused by: the requirement 'numpy==2.* ; sys_platform == 'darwin'' could not be satisfied (required by '') diff --git a/crates/pixi_pypi_spec/src/lib.rs b/crates/pixi_pypi_spec/src/lib.rs index f9550a829d..40532668d1 100644 --- a/crates/pixi_pypi_spec/src/lib.rs +++ b/crates/pixi_pypi_spec/src/lib.rs @@ -10,7 +10,7 @@ use std::{ }; use pep440_rs::VersionSpecifiers; -use pep508_rs::ExtraName; +use pep508_rs::{ExtraName, MarkerTree}; use pixi_spec::GitSpec; use serde::Serialize; use thiserror::Error; @@ -119,6 +119,16 @@ impl Default for PixiPypiSource { } } +/// Serialize a `pep508_rs::MarkerTree` into a string representation +fn serialize_markertree(value: &MarkerTree, s: S) -> Result +where + S: serde::Serializer, +{ + // `.expect()` succeeds because we don't serialize when + // `value.is_true()`, which is the default. + value.contents().expect("contents were null").serialize(s) +} + /// A complete PyPI dependency specification. /// /// This is the main type used throughout pixi for PyPI dependencies. It combines @@ -131,6 +141,14 @@ pub struct PixiPypiSpec { /// Optional package extras to install. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub extras: Vec, + /// The environment markers that decide if/when this package gets installed + #[serde( + default, + // Needed because `pep508_rs::MarkerTree` doesn't implement `serde::Serialize` + serialize_with = "serialize_markertree", + skip_serializing_if = "MarkerTree::is_true" + )] + pub env_markers: MarkerTree, /// The source for this package. #[serde(flatten)] pub source: PixiPypiSource, @@ -181,6 +199,7 @@ impl From for PixiPypiSpec { PixiPypiSpec { extras: Vec::new(), source, + env_markers: MarkerTree::default(), } } } @@ -188,15 +207,20 @@ impl From for PixiPypiSpec { impl PixiPypiSpec { /// Creates a new spec with the given source and no extras. pub fn new(source: PixiPypiSource) -> Self { - PixiPypiSpec { - extras: Vec::new(), - source, - } + source.into() } /// Creates a new spec with the given source and extras. - pub fn with_extras(source: PixiPypiSource, extras: Vec) -> Self { - PixiPypiSpec { extras, source } + pub fn with_extras_and_markers( + source: PixiPypiSource, + extras: Vec, + env_markers: MarkerTree, + ) -> Self { + PixiPypiSpec { + extras, + source, + env_markers, + } } /// Returns a reference to the source. @@ -253,6 +277,11 @@ impl PixiPypiSpec { &self.extras } + /// Returns the environment markers for this spec. + pub fn env_markers(&self) -> &MarkerTree { + &self.env_markers + } + /// Returns the editability setting from the manifest. /// Only `Path` specs can be editable. Returns `None` for non-path specs /// or if editability is not explicitly specified. @@ -293,6 +322,8 @@ impl PixiPypiSpec { updated.extras = self.extras.clone(); } + updated.env_markers.or(requirement.marker.clone()); + Ok(updated) } } @@ -359,7 +390,7 @@ mod tests { let extra = ExtraName::new("test".to_string()).unwrap(); // Spec with extras - let spec = PixiPypiSpec::with_extras( + let spec = PixiPypiSpec::with_extras_and_markers( PixiPypiSource::Git { git: GitSpec { git: Url::parse("https://github.com/example/repo").unwrap(), @@ -368,10 +399,11 @@ mod tests { }, }, vec![extra.clone()], + MarkerTree::default(), ); assert_eq!(spec.extras(), std::slice::from_ref(&extra)); - // Spec without extras + // Spec without extras and markers let spec = PixiPypiSpec::new(PixiPypiSource::Registry { version: VersionOrStar::Star, index: None, @@ -379,6 +411,31 @@ mod tests { assert!(spec.extras().is_empty()); } + #[test] + fn test_env_markers_accessor() { + let markers = MarkerTree::from_str("python_version >= '3.12'").unwrap(); + // Spec with markers + let spec = PixiPypiSpec::with_extras_and_markers( + PixiPypiSource::Git { + git: GitSpec { + git: Url::parse("https://github.com/example/repo").unwrap(), + rev: None, + subdirectory: None, + }, + }, + vec![], + markers.clone(), + ); + assert_eq!(spec.env_markers(), &markers); + + // Spec without extras and markers + let spec = PixiPypiSpec::new(PixiPypiSource::Registry { + version: VersionOrStar::Star, + index: None, + }); + assert!(spec.env_markers().is_true()); + } + #[test] fn test_source_accessor() { let spec = PixiPypiSpec::new(PixiPypiSource::Path { @@ -447,12 +504,14 @@ mod tests { let spec: PixiPypiSpec = source.clone().into(); assert_eq!(spec.source, source); assert!(spec.extras.is_empty()); + assert!(spec.env_markers.is_true()); } #[test] fn test_default_spec() { let spec = PixiPypiSpec::default(); assert!(spec.extras.is_empty()); + assert!(spec.env_markers.is_true()); assert!(matches!( spec.source, PixiPypiSource::Registry { @@ -469,7 +528,7 @@ mod tests { let pypi = PixiPypiSpec::try_from(req).unwrap(); assert_eq!( pypi.to_string(), - "{ version = \"==1.0.0\", extras = [\"testing\"] }" + "{ version = \"==1.0.0\", extras = [\"testing\"], env-markers = \"os_name == 'posix'\" }" ); let req = pep508_rs::Requirement::from_str("numpy").unwrap(); @@ -525,7 +584,11 @@ mod tests { let pypi: Requirement = "boltons[nichita] @ https://files.pythonhosted.org/packages/46/35/e50d4a115f93e2a3fbf52438435bb2efcf14c11d4fcd6bdcd77a6fc399c9/boltons-24.0.0-py3-none-any.whl".parse().unwrap(); let as_pypi_req: PixiPypiSpec = pypi.try_into().unwrap(); - assert_eq!(as_pypi_req, PixiPypiSpec::with_extras(PixiPypiSource::Url { url: Url::parse("https://files.pythonhosted.org/packages/46/35/e50d4a115f93e2a3fbf52438435bb2efcf14c11d4fcd6bdcd77a6fc399c9/boltons-24.0.0-py3-none-any.whl").unwrap(), subdirectory: None }, vec![ExtraName::new("nichita".to_string()).unwrap()])); + assert_eq!(as_pypi_req, PixiPypiSpec::with_extras_and_markers(PixiPypiSource::Url { url: Url::parse("https://files.pythonhosted.org/packages/46/35/e50d4a115f93e2a3fbf52438435bb2efcf14c11d4fcd6bdcd77a6fc399c9/boltons-24.0.0-py3-none-any.whl").unwrap(), subdirectory: None }, vec![ExtraName::new("nichita".to_string()).unwrap()], MarkerTree::default())); + + let pypi: Requirement = "potato[habbasi]; sys_platform == 'linux'".parse().unwrap(); + let as_pypi_req: PixiPypiSpec = pypi.try_into().unwrap(); + assert_snapshot!(as_pypi_req); #[cfg(target_os = "windows")] let pypi: Requirement = "boltons @ file:///C:/path/to/boltons".parse().unwrap(); @@ -601,6 +664,8 @@ mod tests { r#"pkg = { git = "https://github.com/prefix-dev/rattler-build", "rev" = "123456" }"#, r#"pkg = { git = "https://github.com/prefix-dev/rattler-build", "subdirectory" = "pyrattler" }"#, r#"pkg = { git = "https://github.com/prefix-dev/rattler-build", "extras" = ["test"] }"#, + r#"pkg = { version = "*", "env-markers" = "sys_platform == 'win32'" }"#, + r#"pkg = { git = "https://github.com/prefix-dev/rattler-build", "extras" = ["test"], "env-markers" = "sys_platform == 'linux'" }"#, ]; #[derive(Serialize)] @@ -639,6 +704,7 @@ mod tests { r#"pkg = "~/path/style""#, r#"pkg = "https://example.com""#, r#"pkg = "https://github.com/conda-forge/21cmfast-feedstock""#, + r#"pkg = { version = "*", "env-markers" = "potato == 'potato'" }"#, ]; struct Snapshot { diff --git a/crates/pixi_pypi_spec/src/pep508.rs b/crates/pixi_pypi_spec/src/pep508.rs index f3ab232bbd..c47776bab3 100644 --- a/crates/pixi_pypi_spec/src/pep508.rs +++ b/crates/pixi_pypi_spec/src/pep508.rs @@ -10,13 +10,16 @@ impl TryFrom for PixiPypiSpec { fn try_from(req: pep508_rs::Requirement) -> Result { let converted = if let Some(version_or_url) = req.version_or_url { match version_or_url { - pep508_rs::VersionOrUrl::VersionSpecifier(v) => PixiPypiSpec::with_extras( - PixiPypiSource::Registry { - version: v.into(), - index: None, - }, - req.extras, - ), + pep508_rs::VersionOrUrl::VersionSpecifier(v) => { + PixiPypiSpec::with_extras_and_markers( + PixiPypiSource::Registry { + version: v.into(), + index: None, + }, + req.extras, + req.marker, + ) + } pep508_rs::VersionOrUrl::Url(u) => { let url = u.to_url(); if let Some((prefix, ..)) = url.scheme().split_once('+') { @@ -30,9 +33,10 @@ impl TryFrom for PixiPypiSpec { subdirectory, }; - PixiPypiSpec::with_extras( + PixiPypiSpec::with_extras_and_markers( PixiPypiSource::Git { git: git_spec }, req.extras, + req.marker, ) } "bzr" => { @@ -75,35 +79,42 @@ impl TryFrom for PixiPypiSpec { rev: Some(git_url.reference().clone().into()), subdirectory, }; - PixiPypiSpec::with_extras(PixiPypiSource::Git { git: git_spec }, req.extras) + PixiPypiSpec::with_extras_and_markers( + PixiPypiSource::Git { git: git_spec }, + req.extras, + req.marker, + ) } else if url.scheme().eq_ignore_ascii_case("file") { // Convert the file url to a path. let file = url.to_file_path().map_err(|_| { Pep508ToPyPiRequirementError::PathUrlIntoPath(url.clone()) })?; - PixiPypiSpec::with_extras( + PixiPypiSpec::with_extras_and_markers( PixiPypiSource::Path { path: file, editable: None, }, req.extras, + req.marker, ) } else { let subdirectory = extract_directory_from_url(&url); - PixiPypiSpec::with_extras( + PixiPypiSpec::with_extras_and_markers( PixiPypiSource::Url { url, subdirectory }, req.extras, + req.marker, ) } } } - } else if !req.extras.is_empty() { - PixiPypiSpec::with_extras( + } else if !req.extras.is_empty() || !req.marker.is_true() { + PixiPypiSpec::with_extras_and_markers( PixiPypiSource::Registry { version: VersionOrStar::Star, index: None, }, req.extras, + req.marker, ) } else { PixiPypiSpec::new(PixiPypiSource::Registry { diff --git a/crates/pixi_pypi_spec/src/snapshots/pixi_pypi_spec__tests__deserialize_failing.snap b/crates/pixi_pypi_spec/src/snapshots/pixi_pypi_spec__tests__deserialize_failing.snap index 78e608f28e..b6a906096a 100644 --- a/crates/pixi_pypi_spec/src/snapshots/pixi_pypi_spec__tests__deserialize_failing.snap +++ b/crates/pixi_pypi_spec/src/snapshots/pixi_pypi_spec__tests__deserialize_failing.snap @@ -3,7 +3,7 @@ source: crates/pixi_pypi_spec/src/lib.rs expression: "snapshot.into_iter().map(|Snapshot { input, result }|\nformat!(\"input: {input}\\nresult: {} \",\nresult.as_object().unwrap().get(\"error\").unwrap().as_str().unwrap())).join(\"\\n\")" --- input: pkg = { ver = "1.2.3" } -result: × Unexpected keys, expected only 'version', 'extras', 'path', 'editable', 'git', 'branch', 'tag', 'rev', 'url', 'subdirectory', 'index' +result: × Unexpected keys, expected only 'version', 'extras', 'path', 'editable', 'git', 'branch', 'tag', 'rev', 'url', 'subdirectory', 'index', 'env-markers' ╭─[pixi.toml:1:9] 1 │ pkg = { ver = "1.2.3" } · ─┬─ @@ -96,4 +96,12 @@ result: × it seems you're trying to add a git dependency, please specify as a ╭─[pixi.toml:1:8] 1 │ pkg = "https://github.com/conda-forge/21cmfast-feedstock" · ───────────────────────────────────────────────── + ╰──── +input: pkg = { version = "*", "env-markers" = "potato == 'potato'" } +result: × Expected a quoted string or a valid marker name, found `potato` + │ potato == 'potato' + │ ^^^^^^ + ╭─[pixi.toml:1:41] + 1 │ pkg = { version = "*", "env-markers" = "potato == 'potato'" } + · ────────────────── ╰──── diff --git a/crates/pixi_pypi_spec/src/snapshots/pixi_pypi_spec__tests__deserialize_succeeding.snap b/crates/pixi_pypi_spec/src/snapshots/pixi_pypi_spec__tests__deserialize_succeeding.snap index 4a9a1f1688..c07db0c7a6 100644 --- a/crates/pixi_pypi_spec/src/snapshots/pixi_pypi_spec__tests__deserialize_succeeding.snap +++ b/crates/pixi_pypi_spec/src/snapshots/pixi_pypi_spec__tests__deserialize_succeeding.snap @@ -44,3 +44,13 @@ expression: snapshot extras: - test git: "https://github.com/prefix-dev/rattler-build" +- input: "pkg = { version = \"*\", \"env-markers\" = \"sys_platform == 'win32'\" }" + result: + env_markers: "sys_platform == 'win32'" + version: "*" +- input: "pkg = { git = \"https://github.com/prefix-dev/rattler-build\", \"extras\" = [\"test\"], \"env-markers\" = \"sys_platform == 'linux'\" }" + result: + extras: + - test + env_markers: "sys_platform == 'linux'" + git: "https://github.com/prefix-dev/rattler-build" diff --git a/crates/pixi_pypi_spec/src/snapshots/pixi_pypi_spec__tests__from_args-3.snap b/crates/pixi_pypi_spec/src/snapshots/pixi_pypi_spec__tests__from_args-3.snap new file mode 100644 index 0000000000..d4e7684e37 --- /dev/null +++ b/crates/pixi_pypi_spec/src/snapshots/pixi_pypi_spec__tests__from_args-3.snap @@ -0,0 +1,6 @@ +--- +source: crates/pixi_pypi_spec/src/lib.rs +expression: as_pypi_req +snapshot_kind: text +--- +{ version = "*", extras = ["habbasi"], env-markers = "sys_platform == 'linux'" } diff --git a/crates/pixi_pypi_spec/src/toml.rs b/crates/pixi_pypi_spec/src/toml.rs index ff47dd0131..ac6914febd 100644 --- a/crates/pixi_pypi_spec/src/toml.rs +++ b/crates/pixi_pypi_spec/src/toml.rs @@ -1,6 +1,6 @@ use crate::{PixiPypiSource, PixiPypiSpec, VersionOrStar}; use itertools::Itertools; -use pep508_rs::ExtraName; +use pep508_rs::{ExtraName, MarkerTree}; use pixi_spec::{GitReference, GitSpec}; use pixi_toml::{TomlFromStr, TomlWith}; use std::fmt::Display; @@ -58,6 +58,8 @@ struct RawPyPiRequirement { extras: Vec, + marker: MarkerTree, + // Path Only pub path: Option, pub editable: Option, @@ -111,19 +113,21 @@ impl RawPyPiRequirement { } let req = match (self.url, self.path, self.git, self.index) { - (Some(url), None, None, None) => PixiPypiSpec::with_extras( + (Some(url), None, None, None) => PixiPypiSpec::with_extras_and_markers( PixiPypiSource::Url { url, subdirectory: self.subdirectory, }, self.extras, + self.marker, ), - (None, Some(path), None, None) => PixiPypiSpec::with_extras( + (None, Some(path), None, None) => PixiPypiSpec::with_extras_and_markers( PixiPypiSource::Path { path, editable: self.editable, }, self.extras, + self.marker, ), (None, None, Some(git), None) => { let rev = match (self.branch, self.rev, self.tag) { @@ -135,7 +139,7 @@ impl RawPyPiRequirement { return Err(SpecConversion::MultipleGitSpecifiers); } }; - PixiPypiSpec::with_extras( + PixiPypiSpec::with_extras_and_markers( PixiPypiSource::Git { git: GitSpec { git, @@ -144,14 +148,16 @@ impl RawPyPiRequirement { }, }, self.extras, + self.marker, ) } - (None, None, None, index) => PixiPypiSpec::with_extras( + (None, None, None, index) => PixiPypiSpec::with_extras_and_markers( PixiPypiSource::Registry { version: self.version.unwrap_or(VersionOrStar::Star), index, }, self.extras, + self.marker, ), _ => { return Err(SpecConversion::MultipleVersionSpecifiers); @@ -196,6 +202,11 @@ impl<'de> toml_span::Deserialize<'de> for RawPyPiRequirement { .optional::>("index") .map(TomlFromStr::into_inner); + let marker = th + .optional::>("env-markers") + .map(TomlFromStr::into_inner) + .unwrap_or_default(); + th.finalize(None)?; Ok(RawPyPiRequirement { @@ -210,6 +221,7 @@ impl<'de> toml_span::Deserialize<'de> for RawPyPiRequirement { url, subdirectory, index, + marker, }) } } @@ -264,6 +276,16 @@ impl From for toml_edit::Value { } } + fn insert_markers(table: &mut toml_edit::InlineTable, markers: &MarkerTree) { + let markers_str = markers.try_to_string(); + if let Some(markers_str) = markers_str { + table.insert( + "env-markers", + toml_edit::Value::String(toml_edit::Formatted::new(markers_str)), + ); + } + } + fn insert_index(table: &mut toml_edit::InlineTable, index: &Option) { if let Some(index) = index { table.insert( @@ -274,10 +296,13 @@ impl From for toml_edit::Value { } let extras = &val.extras; + let markers = &val.env_markers; match &val.source { // Simple version string (no extras, no index) - PixiPypiSource::Registry { version, index } if extras.is_empty() && index.is_none() => { + PixiPypiSource::Registry { version, index } + if extras.is_empty() && index.is_none() && markers.is_true() => + { toml_edit::Value::from(version.to_string()) } // Registry with extras or index @@ -289,6 +314,7 @@ impl From for toml_edit::Value { ); insert_extras(&mut table, extras); insert_index(&mut table, index); + insert_markers(&mut table, markers); toml_edit::Value::InlineTable(table.to_owned()) } PixiPypiSource::Git { @@ -338,6 +364,7 @@ impl From for toml_edit::Value { ); } insert_extras(&mut table, extras); + insert_markers(&mut table, markers); toml_edit::Value::InlineTable(table.to_owned()) } PixiPypiSource::Path { path, editable } => { @@ -355,6 +382,7 @@ impl From for toml_edit::Value { ); } insert_extras(&mut table, extras); + insert_markers(&mut table, markers); toml_edit::Value::InlineTable(table.to_owned()) } PixiPypiSource::Url { url, subdirectory } => { @@ -372,6 +400,7 @@ impl From for toml_edit::Value { ); } insert_extras(&mut table, extras); + insert_markers(&mut table, markers); toml_edit::Value::InlineTable(table.to_owned()) } } @@ -472,12 +501,13 @@ mod test { ); assert_eq!( requirement.first().unwrap().1, - &PixiPypiSpec::with_extras( + &PixiPypiSpec::with_extras_and_markers( PixiPypiSource::Registry { version: ">=3.12".parse().unwrap(), index: None, }, vec![ExtraName::from_str("bar").unwrap()], + pep508_rs::MarkerTree::default(), ) ); @@ -492,7 +522,7 @@ mod test { ); assert_eq!( requirement.first().unwrap().1, - &PixiPypiSpec::with_extras( + &PixiPypiSpec::with_extras_and_markers( PixiPypiSource::Registry { version: ">=3.12,<3.13.0".parse().unwrap(), index: None, @@ -501,6 +531,7 @@ mod test { ExtraName::from_str("bar").unwrap(), ExtraName::from_str("foo").unwrap(), ], + pep508_rs::MarkerTree::default(), ) ); } @@ -511,13 +542,14 @@ mod test { r#" version = "==1.2.3" extras = ["feature1", "feature2"] + env-markers = "python_version >= '3.11'" "#, ) .unwrap(); assert_eq!( pypi_requirement, - PixiPypiSpec::with_extras( + PixiPypiSpec::with_extras_and_markers( PixiPypiSource::Registry { version: "==1.2.3".parse().unwrap(), index: None, @@ -526,6 +558,7 @@ mod test { ExtraName::from_str("feature1").unwrap(), ExtraName::from_str("feature2").unwrap() ], + pep508_rs::MarkerTree::from_str("python_version >= '3.11'").unwrap(), ) ); } @@ -587,14 +620,14 @@ mod test { #[test] fn test_deserialize_fail_on_unknown() { let input = r#"foo = { borked = "bork"}"#; - assert_snapshot!(format_parse_error(input, from_toml_str::>(input).unwrap_err()), @r###" - × Unexpected keys, expected only 'version', 'extras', 'path', 'editable', 'git', 'branch', 'tag', 'rev', 'url', 'subdirectory', 'index' + assert_snapshot!(format_parse_error(input, from_toml_str::>(input).unwrap_err()), @r#" + × Unexpected keys, expected only 'version', 'extras', 'path', 'editable', 'git', 'branch', 'tag', 'rev', 'url', 'subdirectory', 'index', 'env-markers' ╭─[pixi.toml:1:9] 1 │ foo = { borked = "bork"} · ───┬── · ╰── 'borked' was not expected here ╰──── - "###); + "#); } #[test] diff --git a/crates/pixi_uv_conversions/src/requirements.rs b/crates/pixi_uv_conversions/src/requirements.rs index 893a5704d7..2a99ee50f0 100644 --- a/crates/pixi_uv_conversions/src/requirements.rs +++ b/crates/pixi_uv_conversions/src/requirements.rs @@ -220,7 +220,7 @@ pub fn as_uv_req( .iter() .map(|e| uv_normalize::ExtraName::from_str(e.as_ref()).expect("conversion failed")) .collect(), - marker: Default::default(), + marker: to_uv_marker_tree(req.env_markers()).expect("marker conversion failed"), groups: Default::default(), source, origin: None, @@ -309,10 +309,47 @@ pub fn pep508_requirement_to_uv_requirement( #[cfg(test)] mod tests { + use pep508_rs::MarkerTree; use uv_redacted::DisplaySafeUrl; use super::*; + #[test] + fn test_markers() { + let pypi_req = PixiPypiSpec::with_extras_and_markers( + PixiPypiSource::Registry { + version: VersionOrStar::Star, + index: None, + }, + vec![], + MarkerTree::from_str("sys_platform == 'linux'").unwrap(), + ); + let uv_req = as_uv_req(&pypi_req, "test", Path::new("")).unwrap(); + + let expected_uv_source = RequirementSource::Registry { + specifier: VersionSpecifiers::empty(), + index: None, + conflict: None, + }; + + assert_eq!( + uv_req.source, expected_uv_source, + "Expected {} but got {}", + expected_uv_source, uv_req.source + ); + + let expected_uv_markers = + uv_pep508::MarkerTree::from_str("sys_platform == 'linux'").unwrap(); + + assert_eq!( + uv_req.marker, + expected_uv_markers, + "Expected {:?} but got {:?}", + expected_uv_markers.try_to_string(), + uv_req.marker.try_to_string(), + ); + } + #[test] fn test_git_url() { let pypi_req = PixiPypiSpec::new(PixiPypiSource::Git { diff --git a/docs/source_files/pixi_tomls/package_specifications.toml b/docs/source_files/pixi_tomls/package_specifications.toml index c99a3f08a2..dfa2e5f57f 100644 --- a/docs/source_files/pixi_tomls/package_specifications.toml +++ b/docs/source_files/pixi_tomls/package_specifications.toml @@ -89,4 +89,7 @@ requests3 = { git = "https://github.com/psf/requests.git", subdirectory = "reque local_package = { path = "../local_package" } local_package2 = { path = "../local_package2", extras = ["extra_feature"] } local_package3 = { path = "../local_package3", editable = true } + +# With environment markers +nvidia-nccl-cu12 = { version = "==2.27.3", env-markers = "sys_platform == 'linux'" } # --8<-- [end:pypi-fields] diff --git a/schema/model.py b/schema/model.py index fe656d724c..8c87639071 100644 --- a/schema/model.py +++ b/schema/model.py @@ -267,6 +267,7 @@ class SourceSpecTable(StrictBaseModel): MatchSpec = NonEmptyStr | MatchspecTable CondaPackageName = NonEmptyStr +EnvironmentMarkers = NonEmptyStr class _PyPIRequirement(StrictBaseModel): @@ -274,6 +275,10 @@ class _PyPIRequirement(StrictBaseModel): None, description="The [PEP 508 extras](https://peps.python.org/pep-0508/#extras) of the package", ) + env_markers: EnvironmentMarkers | None = Field( + None, + description="The [PEP 508 markers](https://peps.python.org/pep-0508/#environment-markers) of the package", + ) class _PyPiGitRequirement(_PyPIRequirement): diff --git a/schema/schema.json b/schema/schema.json index 070ee910f4..b7ded3429b 100644 --- a/schema/schema.json +++ b/schema/schema.json @@ -1352,6 +1352,12 @@ "type": "string", "minLength": 1 }, + "env-markers": { + "title": "Env-Markers", + "description": "The [PEP 508 markers](https://peps.python.org/pep-0508/#environment-markers) of the package", + "type": "string", + "minLength": 1 + }, "extras": { "title": "Extras", "description": "The [PEP 508 extras](https://peps.python.org/pep-0508/#extras) of the package", @@ -1380,6 +1386,12 @@ "type": "object", "additionalProperties": false, "properties": { + "env-markers": { + "title": "Env-Markers", + "description": "The [PEP 508 markers](https://peps.python.org/pep-0508/#environment-markers) of the package", + "type": "string", + "minLength": 1 + }, "extras": { "title": "Extras", "description": "The [PEP 508 extras](https://peps.python.org/pep-0508/#extras) of the package", @@ -1414,6 +1426,12 @@ "type": "object", "additionalProperties": false, "properties": { + "env-markers": { + "title": "Env-Markers", + "description": "The [PEP 508 markers](https://peps.python.org/pep-0508/#environment-markers) of the package", + "type": "string", + "minLength": 1 + }, "extras": { "title": "Extras", "description": "The [PEP 508 extras](https://peps.python.org/pep-0508/#extras) of the package", @@ -1661,6 +1679,12 @@ "description": "If `true` the package will be installed as editable", "type": "boolean" }, + "env-markers": { + "title": "Env-Markers", + "description": "The [PEP 508 markers](https://peps.python.org/pep-0508/#environment-markers) of the package", + "type": "string", + "minLength": 1 + }, "extras": { "title": "Extras", "description": "The [PEP 508 extras](https://peps.python.org/pep-0508/#extras) of the package", @@ -1689,6 +1713,12 @@ "type": "object", "additionalProperties": false, "properties": { + "env-markers": { + "title": "Env-Markers", + "description": "The [PEP 508 markers](https://peps.python.org/pep-0508/#environment-markers) of the package", + "type": "string", + "minLength": 1 + }, "extras": { "title": "Extras", "description": "The [PEP 508 extras](https://peps.python.org/pep-0508/#extras) of the package", @@ -1711,6 +1741,12 @@ "type": "object", "additionalProperties": false, "properties": { + "env-markers": { + "title": "Env-Markers", + "description": "The [PEP 508 markers](https://peps.python.org/pep-0508/#environment-markers) of the package", + "type": "string", + "minLength": 1 + }, "extras": { "title": "Extras", "description": "The [PEP 508 extras](https://peps.python.org/pep-0508/#extras) of the package", diff --git a/tests/data/non-satisfiability/changed-env-marker/pixi.lock b/tests/data/non-satisfiability/changed-env-marker/pixi.lock new file mode 100644 index 0000000000..e853640b19 --- /dev/null +++ b/tests/data/non-satisfiability/changed-env-marker/pixi.lock @@ -0,0 +1,171 @@ +version: 6 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit + packages: + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.3-haf25636_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.1-h1b79a29_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.14-h18782d2_2_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda +packages: +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda + sha256: b456200636bd5fecb2bec63f7e0985ad2097cf1b83d60ce0b6968dffa6d02aa1 + md5: 58fd217444c2a5701a44244faf518206 + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 125061 + timestamp: 1757437486465 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + sha256: b5974ec9b50e3c514a382335efa81ed02b05906849827a34061c496f4defa0b2 + md5: bddacf101bb4dd0e51811cb69c7790e2 + depends: + - __unix + license: ISC + purls: [] + size: 146519 + timestamp: 1767500828366 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.3-haf25636_0.conda + sha256: fce22610ecc95e6d149e42a42fbc3cc9d9179bd4eb6232639a60f06e080eec98 + md5: b79875dbb5b1db9a4a22a4520f918e1a + depends: + - __osx >=11.0 + constrains: + - expat 2.7.3.* + license: MIT + license_family: MIT + purls: [] + size: 67800 + timestamp: 1763549994166 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda + sha256: 9b8acdf42df61b7bfe8bdc545c016c29e61985e79748c64ad66df47dbc2e295f + md5: 411ff7cd5d1472bba0f55c0faf04453b + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 40251 + timestamp: 1760295839166 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + sha256: 0cb92a9e026e7bd4842f410a5c5c665c89b2eb97794ffddba519a626b8ce7285 + md5: d6df911d4564d77c4374b02552cb17d1 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.1.* + license: 0BSD + purls: [] + size: 92286 + timestamp: 1749230283517 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.1-h1b79a29_1.conda + sha256: f2c3cbf2ca7d697098964a748fbf19d6e4adcefa23844ec49f0166f1d36af83c + md5: 8c3951797658e10b610929c3e57e9ad9 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 905861 + timestamp: 1766319901587 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b + md5: 369964e85dc26bfe78f41399b366c435 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 46438 + timestamp: 1727963202283 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + sha256: 2827ada40e8d9ca69a153a45f7fd14f32b2ead7045d3bbb5d10964898fe65733 + md5: 068d497125e4bf8a66bf707254fff5ae + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + purls: [] + size: 797030 + timestamp: 1738196177597 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda + sha256: ebe93dafcc09e099782fe3907485d4e1671296bc14f8c383cb6f3dfebb773988 + md5: b34dc4172653c13dcf453862f251af2b + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3108371 + timestamp: 1762839712322 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.14-h18782d2_2_cpython.conda + build_number: 2 + sha256: 64a2bc6be8582fae75f1f2da7bdc49afd81c2793f65bb843fc37f53c99734063 + md5: da948e6cd735249ab4cfbb3fdede785e + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.1,<6.0a0 + - libsqlite >=3.50.4,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.4,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.11.* *_cp311 + license: Python-2.0 + purls: [] + size: 14788204 + timestamp: 1761174033541 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + sha256: a77010528efb4b548ac2a4484eaf7e1c3907f2aec86123ed9c5212ae44502477 + md5: f8381319127120ce51e081dce4865cf4 + depends: + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 313930 + timestamp: 1765813902568 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda + sha256: ad0c67cb03c163a109820dc9ecf77faf6ec7150e942d1e8bb13e5d39dc058ab7 + md5: a73d54a5abba6543cb2f0af1bfbd6851 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3125484 + timestamp: 1763055028377 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c + md5: ad659d0a2b3e47e38d829aa8cad2d610 + license: LicenseRef-Public-Domain + purls: [] + size: 119135 + timestamp: 1767016325805 diff --git a/tests/data/non-satisfiability/changed-env-marker/pixi.toml b/tests/data/non-satisfiability/changed-env-marker/pixi.toml new file mode 100644 index 0000000000..1ba99e4ecd --- /dev/null +++ b/tests/data/non-satisfiability/changed-env-marker/pixi.toml @@ -0,0 +1,14 @@ +[workspace] +authors = ["Hameer Abbasi <2190658+hameerabbasi@users.noreply.github.com>"] +channels = ["conda-forge"] +name = "sattest" +platforms = ["osx-arm64"] +version = "0.1.0" + +[tasks] + +[dependencies] +python = "3.11.*" + +[pypi-dependencies] +numpy = { version = "==2.*", env-markers = "sys_platform == 'darwin'" } diff --git a/tests/data/satisfiability/added-env-marker/pixi.lock b/tests/data/satisfiability/added-env-marker/pixi.lock new file mode 100644 index 0000000000..e853640b19 --- /dev/null +++ b/tests/data/satisfiability/added-env-marker/pixi.lock @@ -0,0 +1,171 @@ +version: 6 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit + packages: + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.3-haf25636_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.1-h1b79a29_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.14-h18782d2_2_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda +packages: +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda + sha256: b456200636bd5fecb2bec63f7e0985ad2097cf1b83d60ce0b6968dffa6d02aa1 + md5: 58fd217444c2a5701a44244faf518206 + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 125061 + timestamp: 1757437486465 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + sha256: b5974ec9b50e3c514a382335efa81ed02b05906849827a34061c496f4defa0b2 + md5: bddacf101bb4dd0e51811cb69c7790e2 + depends: + - __unix + license: ISC + purls: [] + size: 146519 + timestamp: 1767500828366 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.3-haf25636_0.conda + sha256: fce22610ecc95e6d149e42a42fbc3cc9d9179bd4eb6232639a60f06e080eec98 + md5: b79875dbb5b1db9a4a22a4520f918e1a + depends: + - __osx >=11.0 + constrains: + - expat 2.7.3.* + license: MIT + license_family: MIT + purls: [] + size: 67800 + timestamp: 1763549994166 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda + sha256: 9b8acdf42df61b7bfe8bdc545c016c29e61985e79748c64ad66df47dbc2e295f + md5: 411ff7cd5d1472bba0f55c0faf04453b + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 40251 + timestamp: 1760295839166 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + sha256: 0cb92a9e026e7bd4842f410a5c5c665c89b2eb97794ffddba519a626b8ce7285 + md5: d6df911d4564d77c4374b02552cb17d1 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.1.* + license: 0BSD + purls: [] + size: 92286 + timestamp: 1749230283517 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.1-h1b79a29_1.conda + sha256: f2c3cbf2ca7d697098964a748fbf19d6e4adcefa23844ec49f0166f1d36af83c + md5: 8c3951797658e10b610929c3e57e9ad9 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 905861 + timestamp: 1766319901587 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b + md5: 369964e85dc26bfe78f41399b366c435 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 46438 + timestamp: 1727963202283 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + sha256: 2827ada40e8d9ca69a153a45f7fd14f32b2ead7045d3bbb5d10964898fe65733 + md5: 068d497125e4bf8a66bf707254fff5ae + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + purls: [] + size: 797030 + timestamp: 1738196177597 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda + sha256: ebe93dafcc09e099782fe3907485d4e1671296bc14f8c383cb6f3dfebb773988 + md5: b34dc4172653c13dcf453862f251af2b + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3108371 + timestamp: 1762839712322 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.14-h18782d2_2_cpython.conda + build_number: 2 + sha256: 64a2bc6be8582fae75f1f2da7bdc49afd81c2793f65bb843fc37f53c99734063 + md5: da948e6cd735249ab4cfbb3fdede785e + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.1,<6.0a0 + - libsqlite >=3.50.4,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.4,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.11.* *_cp311 + license: Python-2.0 + purls: [] + size: 14788204 + timestamp: 1761174033541 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + sha256: a77010528efb4b548ac2a4484eaf7e1c3907f2aec86123ed9c5212ae44502477 + md5: f8381319127120ce51e081dce4865cf4 + depends: + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 313930 + timestamp: 1765813902568 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda + sha256: ad0c67cb03c163a109820dc9ecf77faf6ec7150e942d1e8bb13e5d39dc058ab7 + md5: a73d54a5abba6543cb2f0af1bfbd6851 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3125484 + timestamp: 1763055028377 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c + md5: ad659d0a2b3e47e38d829aa8cad2d610 + license: LicenseRef-Public-Domain + purls: [] + size: 119135 + timestamp: 1767016325805 diff --git a/tests/data/satisfiability/added-env-marker/pixi.toml b/tests/data/satisfiability/added-env-marker/pixi.toml new file mode 100644 index 0000000000..7bfecf17d5 --- /dev/null +++ b/tests/data/satisfiability/added-env-marker/pixi.toml @@ -0,0 +1,14 @@ +[workspace] +authors = ["Hameer Abbasi <2190658+hameerabbasi@users.noreply.github.com>"] +channels = ["conda-forge"] +name = "sattest" +platforms = ["osx-arm64"] +version = "0.1.0" + +[tasks] + +[dependencies] +python = "3.11.*" + +[pypi-dependencies] +numpy = { version = "==2.*", env-markers = "sys_platform == 'linux'" }