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
2 changes: 1 addition & 1 deletion crates/pixi/tests/integration_rust/install_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -930,7 +930,7 @@ setup(
.workspace()
.unwrap()
.default_environment()
.pypi_options()
.pypi_options(None)
.no_build_isolation
.contains(&"my-pkg".parse().unwrap());

Expand Down
8 changes: 5 additions & 3 deletions crates/pixi_api/src/workspace/list/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,11 @@ pub async fn list(
if environment.has_pypi_dependencies() {
uv_context =
UvResolutionContext::from_config(workspace.config(), workspace.client()?.clone())?;
index_locations =
pypi_options_to_index_locations(&environment.pypi_options(), workspace.root())
.into_diagnostic()?;
index_locations = pypi_options_to_index_locations(
&environment.pypi_options(Some(platform)),
workspace.root(),
)
.into_diagnostic()?;
let record = python_record
.record()
.expect("python record should have full metadata");
Expand Down
4 changes: 2 additions & 2 deletions crates/pixi_cli/src/workspace/export/conda_environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ fn build_env_yaml(
}

if !pip_dependencies.is_empty() {
let pypi_options = environment.pypi_options();
let pypi_options = environment.pypi_options(Some(platform));
if let Some(ref find_links) = pypi_options.find_links {
for find_link in find_links {
match find_link {
Expand Down Expand Up @@ -362,7 +362,7 @@ fn build_env_yaml_from_lock_file(
}

if !pip_dependencies.is_empty() {
let pypi_options = environment.pypi_options();
let pypi_options = environment.pypi_options(Some(platform));
if let Some(ref find_links) = pypi_options.find_links {
for find_link in find_links {
match find_link {
Expand Down
7 changes: 5 additions & 2 deletions crates/pixi_core/src/lock_file/satisfiability/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,10 @@ pub fn verify_environment_satisfiability(
// 3. Check that wheel tags still are possible with current system requirements
let pypi_dependencies = environment.pypi_dependencies(None);
if !pypi_dependencies.is_empty() {
let group_pypi_options = grouped_env.pypi_options();
// Whole-environment metadata: `rattler_lock` records one `PypiIndexes`
// per environment, not per platform, so this intentionally ignores any
// per-target `pypi-options` overrides.
let group_pypi_options = grouped_env.pypi_options(None);
let indexes = rattler_lock::PypiIndexes::from(group_pypi_options.clone());

// Check if the indexes in the lock file match our current configuration.
Expand Down Expand Up @@ -233,7 +236,7 @@ pub fn verify_environment_satisfiability(
.pypi_prerelease_mode
.into();
let expected_prerelease_mode = grouped_env
.pypi_options()
.pypi_options(None)
.prerelease_mode
.unwrap_or_default();
if locked_prerelease_mode != expected_prerelease_mode {
Expand Down
2 changes: 1 addition & 1 deletion crates/pixi_core/src/lock_file/satisfiability/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ async fn verify_package_platform_satisfiability(
// map it to (name => requirement) for later matching
let dependency_overrides = ctx
.environment
.pypi_options()
.pypi_options(pixi_platform)
.dependency_overrides
.unwrap_or_default()
.into_iter()
Expand Down
2 changes: 1 addition & 1 deletion crates/pixi_core/src/lock_file/satisfiability/pypi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ async fn read_local_package_metadata(
return Ok(Some(cached_metadata.value().clone()));
}

let pypi_options = ctx.environment.pypi_options();
let pypi_options = ctx.environment.pypi_options(Some(ctx.platform));

// Missing python is a conda-side gap (e.g. `unlock_packages` stripped
// it pre-resolve); raise the non-pypi-only variant so the env is
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
source: crates/pixi_core/src/lock_file/satisfiability/tests.rs
expression: s
snapshot_kind: text
---
environment 'default' does not satisfy the requirements of the project for platform 'variant-b'
Diagnostic severity: error
Caused by: 'my-dep' with specifiers '==1.0.2' does not match the locked version '1.0.1'
21 changes: 14 additions & 7 deletions crates/pixi_core/src/lock_file/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1062,25 +1062,29 @@ impl<'p> LockFileDerivedData<'p> {
.clone()
.set_cache_refresh(uv_reinstall, uv_packages);

let non_isolated_packages = environment.pypi_options().no_build_isolation;
let non_isolated_packages =
environment.pypi_options(Some(platform)).no_build_isolation;
let no_build = environment
.pypi_options()
.pypi_options(Some(platform))
.no_build
.clone()
.unwrap_or_default();
let no_binary = environment
.pypi_options()
.pypi_options(Some(platform))
.no_binary
.clone()
.unwrap_or_default();

// Update the prefix with Pypi records
{
let pypi_indexes = self.locked_env(environment)?.pypi_indexes().cloned();
let index_strategy = environment.pypi_options().index_strategy.clone();
let index_strategy = environment
.pypi_options(Some(platform))
.index_strategy
.clone();
let pypi_exclude_newer = environment.pypi_exclude_newer_config_resolved();
let skip_wheel_filename_check =
environment.pypi_options().skip_wheel_filename_check;
environment.pypi_options(Some(platform)).skip_wheel_filename_check;

let pypi_update_config = PyPIUpdateConfig {
environment_name: environment.name(),
Expand Down Expand Up @@ -2603,7 +2607,10 @@ impl<'p> UpdateContext<'p> {
for environment in project.environments() {
let environment_name = environment.name().to_string();
let grouped_env = GroupedEnvironment::from(environment.clone());
let grouped_pypi_options = grouped_env.pypi_options();
// Whole-environment metadata: `rattler_lock` records one `PypiIndexes`
// per environment, not per platform, so this intentionally ignores
// any per-target `pypi-options` overrides.
let grouped_pypi_options = grouped_env.pypi_options(None);
let pypi_prerelease_mode = grouped_pypi_options.prerelease_mode.unwrap_or_default();

let channel_config = project.channel_config();
Expand Down Expand Up @@ -3368,7 +3375,7 @@ async fn spawn_solve_pypi_task<'p>(
let pixi_solve_records = &repodata_records.records;
let locked_pypi_records = &locked_pypi_packages.records;

let pypi_options = environment.pypi_options();
let pypi_options = environment.pypi_options(Some(pixi_platform));
let platform_for_async = platform.clone();
let (pypi_packages, duration, prefix_task_result) = async move {
let platform = platform_for_async;
Expand Down
4 changes: 3 additions & 1 deletion crates/pixi_core/src/lock_file/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,10 @@ pub fn filter_lock_file<
.set_options(environment_name, environment.solve_options().clone());

let indexes = environment.pypi_indexes().cloned().unwrap_or_else(|| {
// Whole-environment metadata: `rattler_lock` records one `PypiIndexes`
// per environment, not per platform.
GroupedEnvironment::from(project_env.clone())
.pypi_options()
.pypi_options(None)
.into()
});
writer.builder.set_pypi_indexes(environment_name, indexes);
Expand Down
18 changes: 9 additions & 9 deletions crates/pixi_core/src/workspace/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1315,7 +1315,7 @@ mod tests {
)
.unwrap();

let foo_opts = manifest.environment("foo").unwrap().pypi_options();
let foo_opts = manifest.environment("foo").unwrap().pypi_options(None);
assert_eq!(
foo_opts.index_url.unwrap().to_string(),
"https://mypypi.org/simple"
Expand All @@ -1330,7 +1330,7 @@ mod tests {
vec!["https://1.com/"]
);

let bar_opts = manifest.environment("bar").unwrap().pypi_options();
let bar_opts = manifest.environment("bar").unwrap().pypi_options(None);
assert_eq!(
bar_opts
.extra_index_urls
Expand All @@ -1341,7 +1341,7 @@ mod tests {
vec!["https://2.com/"]
);

let foo_bar_opts = manifest.environment("foobar").unwrap().pypi_options();
let foo_bar_opts = manifest.environment("foobar").unwrap().pypi_options(None);

assert_eq!(
foo_bar_opts.index_url.unwrap().to_string(),
Expand Down Expand Up @@ -1383,14 +1383,14 @@ mod tests {
assert_eq!(
manifest
.default_environment()
.pypi_options()
.pypi_options(None)
.extra_index_urls
.unwrap()
.len(),
1
);
let foo_opts = manifest.environment("foo").unwrap().pypi_options();
let bar_opts = manifest.environment("bar").unwrap().pypi_options();
let foo_opts = manifest.environment("foo").unwrap().pypi_options(None);
let bar_opts = manifest.environment("bar").unwrap().pypi_options(None);
// Both `foo` and `bar` see the workspace-level extra-index-url merged
// with the one from feature `foo`. `bar` uses `no-default-feature` but
// the workspace-level options still apply as the always-on base.
Expand Down Expand Up @@ -1422,7 +1422,7 @@ mod tests {
)
.unwrap();

let build_opts = manifest.environment("build").unwrap().pypi_options();
let build_opts = manifest.environment("build").unwrap().pypi_options(None);
assert_eq!(
build_opts.index_url.unwrap().to_string(),
"https://private.example.com/simple/"
Expand Down Expand Up @@ -1452,7 +1452,7 @@ mod tests {
)
.unwrap();

let opts = manifest.environment("custom").unwrap().pypi_options();
let opts = manifest.environment("custom").unwrap().pypi_options(None);
assert_eq!(
opts.index_url.unwrap().to_string(),
"https://feature.example.com/simple/"
Expand Down Expand Up @@ -1482,7 +1482,7 @@ mod tests {
)
.unwrap();

let opts = manifest.environment("ab").unwrap().pypi_options();
let opts = manifest.environment("ab").unwrap().pypi_options(None);
assert_eq!(
opts.index_url.unwrap().to_string(),
"https://shared.example.com/simple/"
Expand Down
2 changes: 1 addition & 1 deletion crates/pixi_core/src/workspace/solve_group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ mod tests {
assert_eq!(default_environment.solve_group(), None);

assert_eq!(
solve_group.pypi_options().index_url.unwrap(),
solve_group.pypi_options(None).index_url.unwrap(),
"https://my-index.com/simple".parse().unwrap()
);

Expand Down
33 changes: 29 additions & 4 deletions crates/pixi_manifest/src/feature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,9 +374,34 @@ impl Feature {
})
}

/// Returns any pypi_options if they are set.
pub fn pypi_options(&self) -> Option<&PypiOptions> {
self.pypi_options.as_ref()
/// Returns the pypi options for this feature and `platform`, combining the
/// feature-level base with any matching per-target overrides.
///
/// Feature-level `pypi-options` act as the base; target-specific
/// `pypi-options` are overlaid on top, most specific last, following the
/// same [`Targets::resolve`] precedence used by `dependencies`.
///
/// If `platform` is `None`, no target-specific pypi-options are considered.
pub fn pypi_options<'a>(&'a self, platform: Option<&'a PixiPlatform>) -> Option<PypiOptions> {
let target_opts = self
.targets
.resolve(platform)
// Get the targets in reverse order, from least specific to most specific.
.rev()
.filter_map(|t| t.pypi_options.as_ref())
.fold(None, |acc: Option<PypiOptions>, opts| {
Some(match acc {
None => opts.clone(),
Some(acc) => acc.overlay(opts),
})
});

match (self.pypi_options.as_ref(), target_opts) {
(None, None) => None,
(Some(base), None) => Some(base.clone()),
(None, Some(target)) => Some(target),
(Some(base), Some(target)) => Some(base.overlay(&target)),
}
}

/// Returns true if the feature supports the given platform.
Expand Down Expand Up @@ -629,7 +654,7 @@ mod tests {
// This behavior has changed from >0.22.0
// and should now be none, previously this was added
// to the default feature
assert!(manifest.default_feature().pypi_options().is_some());
assert!(manifest.default_feature().pypi_options(None).is_some());
assert!(manifest.workspace.pypi_options.is_some());
}
}
13 changes: 8 additions & 5 deletions crates/pixi_manifest/src/features_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,19 +352,22 @@ pub trait FeaturesExt<'source>: HasWorkspaceManifest<'source> + HasFeaturesIter<
DependencyMap::merge_all(constraints.iter().map(|d| d.as_ref()))
}

/// Returns the pypi options for this collection.
/// Returns the pypi options for this collection and `platform`.
///
/// The workspace-level pypi-options act as a base that always applies,
/// regardless of which features the environment includes (notably it still
/// applies when `no-default-feature = true`). Feature-level pypi-options
/// are unioned together and then overlaid on top of the workspace base:
/// (including any per-target overrides that match `platform`) are unioned
/// together and then overlaid on top of the workspace base:
/// single-assignment fields set by a feature override the workspace value,
/// while list-valued and union-like fields are merged.
///
/// If `platform` is `None`, no target-specific pypi-options are considered.
///
/// Multiple features may set the same single-assignment value (e.g. the
/// same `index-url`); conflicting feature-level values for the same
/// single-assignment field should have been rejected at parse time.
fn pypi_options(&self) -> PypiOptions {
fn pypi_options(&self, platform: Option<&PixiPlatform>) -> PypiOptions {
// The workspace-level pypi-options form the base that always applies.
let base = self
.workspace_manifest()
Expand All @@ -378,9 +381,9 @@ pub trait FeaturesExt<'source>: HasWorkspaceManifest<'source> + HasFeaturesIter<
// `expect` is safe here.
let feature_opts = self
.features()
.filter_map(|feature| feature.pypi_options())
.filter_map(|feature| feature.pypi_options(platform))
.fold(PypiOptions::default(), |acc, opts| {
acc.union(opts)
acc.union(&opts)
.expect("merging of pypi-options should already have been checked")
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
---
source: crates/pixi_manifest/src/manifests/workspace.rs
expression: "expect_parse_failure(&format!(\"{PROJECT_BOILERPLATE}\\n[target.win-64.hostdependencies]\"))"
snapshot_kind: text
---
× Unexpected keys, expected only 'dependencies', 'host-dependencies', 'build-dependencies', 'constraints', 'pypi-dependencies', 'dev', 'activation', 'tasks'
× Unexpected keys, expected only 'dependencies', 'host-dependencies', 'build-dependencies', 'constraints', 'pypi-dependencies', 'dev', 'activation', 'pypi-options', 'tasks'
╭─[pixi.toml:8:16]
7 │
8 │ [target.win-64.hostdependencies]
Expand Down
Loading
Loading