Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/pixi/tests/integration_rust/add_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
);
}
Expand Down
77 changes: 77 additions & 0 deletions crates/pixi/tests/integration_rust/pypi_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,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-64"
);
}

#[tokio::test]
async fn test_flat_links_based_index_returns_path() {
setup_tracing();
Expand Down
5 changes: 2 additions & 3 deletions crates/pixi_cli/src/upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
},
Expand Down
10 changes: 9 additions & 1 deletion crates/pixi_cli/src/workspace/export/conda_environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down Expand Up @@ -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(
Expand Down
19 changes: 10 additions & 9 deletions crates/pixi_core/src/lock_file/resolve/pypi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,15 +347,6 @@ pub async fn resolve_pypi(
tracing::info!("there are no python packages installed by conda");
}

let mut requirements = dependencies
.into_iter()
.flat_map(|(name, req)| {
req.into_iter()
.map(move |r| as_uv_req(&r, name.as_ref(), project_root))
})
.collect::<Result<Vec<_>, _>>()
.into_diagnostic()?;

// Determine the python interpreter that is installed as part of the conda
// packages.
let python_record = locked_pixi_records
Expand All @@ -371,6 +362,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 mut 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::<Result<Vec<_>, _>>()
.into_diagnostic()?;

// Determine the tags for this particular solve.
let tags = get_pypi_tags(platform, &system_requirements, python_record.as_ref())?;

Expand Down
90 changes: 47 additions & 43 deletions crates/pixi_core/src/lock_file/satisfiability/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1506,22 +1506,56 @@ pub(crate) async fn verify_package_platform_satisfiability(
})
.collect::<Result<indexmap::IndexMap<_, _>, _>>()?;

// 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, Box<PlatformUnsat>>(Dependency::PyPi(
as_uv_req(req, name.as_source(), project_root).map_err(|e| {
Box::new(PlatformUnsat::AsPep508Error(
name.as_normalized().clone(),
e,
))
})?,
"<environment>".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, Box<PlatformUnsat>>(Dependency::PyPi(
req.map_err(|e| {
Box::new(PlatformUnsat::AsPep508Error(
name.as_normalized().clone(),
e,
))
})?,
"<environment>".into(),
))
})
})
.collect::<Result<Vec<_>, _>>()?;

Expand Down Expand Up @@ -1605,36 +1639,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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: crates/pixi_core/src/lock_file/satisfiability/mod.rs
expression: s
---
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 '<environment>')
Loading
Loading