Skip to content
Merged
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
15 changes: 13 additions & 2 deletions crates/pixi_core/src/lock_file/resolve/pypi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ use indicatif::ProgressBar;
use itertools::{Either, Itertools};
use miette::{Context, IntoDiagnostic};
use pixi_consts::consts;
use pixi_manifest::{EnvironmentName, SystemRequirements, pypi::pypi_options::PypiOptions};
use pixi_manifest::{
EnvironmentName, SolveStrategy, SystemRequirements, pypi::pypi_options::PypiOptions,
};
use pixi_pypi_spec::PixiPypiSpec;
use pixi_record::PixiRecord;
use pixi_reporters::{UvReporter, UvReporterOptions};
Expand Down Expand Up @@ -50,7 +52,8 @@ use uv_pypi_types::{Conflicts, HashAlgorithm, HashDigests};
use uv_requirements::LookaheadResolver;
use uv_resolver::{
AllowedYanks, DefaultResolverProvider, FlatIndex, InMemoryIndex, Manifest, Options, Preference,
PreferenceError, Preferences, PythonRequirement, ResolveError, Resolver, ResolverEnvironment,
PreferenceError, Preferences, PythonRequirement, ResolutionMode, ResolveError, Resolver,
ResolverEnvironment,
};
use uv_types::EmptyInstalledPackages;

Expand Down Expand Up @@ -281,6 +284,7 @@ pub async fn resolve_pypi(
environment_name: Environment<'_>,
disallow_install_conda_prefix: bool,
exclude_newer: Option<DateTime<Utc>>,
solve_strategy: SolveStrategy,
) -> miette::Result<(LockedPypiPackages, Option<CondaPrefixUpdated>)> {
// Solve python packages
pb.set_message("resolving pypi dependencies");
Expand Down Expand Up @@ -455,11 +459,18 @@ pub async fn resolve_pypi(
&build_options,
);

let resolution_mode = match solve_strategy {
SolveStrategy::Highest => ResolutionMode::Highest,
SolveStrategy::Lowest => ResolutionMode::Lowest,
SolveStrategy::LowestDirect => ResolutionMode::LowestDirect,
};

// Hi maintainers! For anyone coming here, if you expose any additional `uv`
// options, similar to `index_strategy`, make sure to include them in this
// struct as well instead of relying on the default. Otherwise there be
// panics.
let options = Options {
resolution_mode,
index_strategy,
build_options: build_options.clone(),
exclude_newer: exclude_newer.map(to_exclude_newer).unwrap_or_default(),
Expand Down
5 changes: 3 additions & 2 deletions crates/pixi_core/src/lock_file/satisfiability/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -488,10 +488,11 @@ pub fn verify_environment_satisfiability(
}

// Verify solver options
if locked_environment.solve_options().strategy != environment.solve_strategy() {
let expected_solve_strategy = environment.solve_strategy().into();
if locked_environment.solve_options().strategy != expected_solve_strategy {
return Err(EnvironmentUnsat::SolveStrategyMismatch {
locked_strategy: locked_environment.solve_options().strategy,
expected_strategy: environment.solve_strategy(),
expected_strategy: expected_solve_strategy,
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: src/lock_file/satisfiability/mod.rs
expression: s
---
environment 'default' does not satisfy the requirements of the project
Diagnostic severity: error
Caused by: the lock-file was solved with a different strategy (highest) than the one selected (lowest-version)
6 changes: 4 additions & 2 deletions crates/pixi_core/src/lock_file/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1765,7 +1765,7 @@ impl<'p> UpdateContext<'p> {
builder.set_options(
&environment_name,
rattler_lock::SolveOptions {
strategy: grouped_env.solve_strategy(),
strategy: grouped_env.solve_strategy().into(),
channel_priority: grouped_env
.channel_priority()
.unwrap_or_default()
Expand Down Expand Up @@ -1914,7 +1914,7 @@ async fn spawn_solve_conda_environment_task(

// Get solve options
let exclude_newer = group.exclude_newer();
let strategy = group.solve_strategy();
let strategy = group.solve_strategy().into();

// Get the environment name
let group_name = group.name();
Expand Down Expand Up @@ -2241,6 +2241,7 @@ async fn spawn_solve_pypi_task<'p>(
};

let environment_name = grouped_environment.name().clone();
let solve_strategy = grouped_environment.solve_strategy();

let pixi_solve_records = &repodata_records.records;
let locked_pypi_records = &locked_pypi_packages.records;
Expand Down Expand Up @@ -2280,6 +2281,7 @@ async fn spawn_solve_pypi_task<'p>(
environment,
disallow_install_conda_prefix,
exclude_newer,
solve_strategy,
)
.await
.with_context(|| {
Expand Down
86 changes: 86 additions & 0 deletions crates/pixi_core/src/workspace/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -940,4 +940,90 @@ mod tests {
Platform::Win32
);
}

#[test]
pub fn test_solve_strategy() {
let contents = r#"
[project]
name = "foo"
platforms = []
channels = []
solve-strategy = "lowest-direct"

[feature.lowest]
solve-strategy = "lowest"

[feature.highest]
solve-strategy = "highest"

[feature.no_strategy]

[environments]
lowest = ["lowest"]
highest = ["highest"]
combined-declared = ["lowest", "highest"]
combined-undeclared-first = ["no_strategy", "lowest"]
combined-undeclared-last = ["lowest", "no_strategy"]
undeclared-default = ["no_strategy"]
undeclared-no-default = { features = ["no_strategy"], no-default-feature = true }
"#;

let temp_dir = tempfile::tempdir().unwrap();
let workspace = Workspace::from_str(&temp_dir.path().join("pixi.toml"), contents).unwrap();

assert_eq!(
workspace.default_environment().solve_strategy(),
pixi_manifest::SolveStrategy::LowestDirect
);

assert_eq!(
workspace.environment("lowest").unwrap().solve_strategy(),
pixi_manifest::SolveStrategy::Lowest
);

assert_eq!(
workspace.environment("highest").unwrap().solve_strategy(),
pixi_manifest::SolveStrategy::Highest
);

assert_eq!(
workspace
.environment("combined-declared")
.unwrap()
.solve_strategy(),
pixi_manifest::SolveStrategy::Lowest
);

assert_eq!(
workspace
.environment("combined-undeclared-first")
.unwrap()
.solve_strategy(),
pixi_manifest::SolveStrategy::Lowest
);

assert_eq!(
workspace
.environment("combined-undeclared-last")
.unwrap()
.solve_strategy(),
pixi_manifest::SolveStrategy::Lowest
);

assert_eq!(
workspace
.environment("undeclared-default")
.unwrap()
.solve_strategy(),
pixi_manifest::SolveStrategy::LowestDirect
);

assert_eq!(
workspace
.environment("undeclared-no-default")
.unwrap()
.solve_strategy(),
pixi_manifest::SolveStrategy::Highest
);
}
}
8 changes: 8 additions & 0 deletions crates/pixi_manifest/src/feature.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{
SpecType, SystemRequirements, WorkspaceTarget, channel::PrioritizedChannel, consts,
pypi::pypi_options::PypiOptions, target::Targets, workspace::ChannelPriority,
workspace::SolveStrategy,
};
use indexmap::{IndexMap, IndexSet};
use pixi_pypi_spec::{PixiPypiSpec, PypiPackageName};
Expand Down Expand Up @@ -137,6 +138,12 @@ pub struct Feature {
/// it will be seen as unset and overwritten by a set one.
pub channel_priority: Option<ChannelPriority>,

/// Solve strategy specific for this feature.
///
/// If this value is `None` and there are multiple features,
/// it will be seen as unset and overwritten by a set one.
pub solve_strategy: Option<SolveStrategy>,

/// Additional system requirements
pub system_requirements: SystemRequirements,

Expand All @@ -155,6 +162,7 @@ impl Feature {
platforms: None,
channels: None,
channel_priority: None,
solve_strategy: None,
system_requirements: SystemRequirements::default(),
pypi_options: None,
targets: <Targets<WorkspaceTarget> as Default>::default(),
Expand Down
18 changes: 14 additions & 4 deletions crates/pixi_manifest/src/features_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ use rattler_conda_types::{

use crate::{
CondaDependencies, PrioritizedChannel, PyPiDependencies, SpecType, SystemRequirements,
has_features_iter::HasFeaturesIter, has_manifest_ref::HasWorkspaceManifest,
pypi::pypi_options::PypiOptions, workspace::ChannelPriority,
has_features_iter::HasFeaturesIter,
has_manifest_ref::HasWorkspaceManifest,
pypi::pypi_options::PypiOptions,
workspace::{ChannelPriority, SolveStrategy},
};

/// ChannelPriorityCombination error, thrown when multiple channel priorities
Expand Down Expand Up @@ -93,8 +95,16 @@ pub trait FeaturesExt<'source>: HasWorkspaceManifest<'source> + HasFeaturesIter<
}

/// Returns the strategy for solving packages.
fn solve_strategy(&self) -> rattler_solve::SolveStrategy {
rattler_solve::SolveStrategy::default()
///
/// The chosen strategy is the first explicitly declared one in a feature
/// as they are provided by the [`HasFeaturesIter::features`] iterator.
///
/// If no feature declares a strategy, the default value of [`SolveStrategy`] is used.
fn solve_strategy(&self) -> SolveStrategy {
self.features()
.flat_map(|feature| feature.solve_strategy)
.next()
.unwrap_or_default()
}

/// Returns the platforms that this collection is compatible with.
Expand Down
2 changes: 1 addition & 1 deletion crates/pixi_manifest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ pub use target::{PackageTarget, TargetSelector, Targets, WorkspaceTarget};
pub use task::{Task, TaskName};
use thiserror::Error;
pub use warning::{Warning, WarningWithSource, WithWarnings};
pub use workspace::{BuildVariantSource, ChannelPriority, Workspace};
pub use workspace::{BuildVariantSource, ChannelPriority, SolveStrategy, Workspace};

pub use crate::{
environments::Environments,
Expand Down
6 changes: 5 additions & 1 deletion crates/pixi_manifest/src/toml/feature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::{
},
utils::{PixiSpanned, package_map::UniquePackageMap},
warning::Deprecation,
workspace::ChannelPriority,
workspace::{ChannelPriority, SolveStrategy},
};
use pixi_pypi_spec::{PixiPypiSpec, PypiPackageName};

Expand All @@ -27,6 +27,7 @@ pub struct TomlFeature {
pub channel_priority: Option<ChannelPriority>,
pub system_requirements: SystemRequirements,
pub target: IndexMap<PixiSpanned<TargetSelector>, TomlTarget>,
pub solve_strategy: Option<SolveStrategy>,
pub dependencies: Option<PixiSpanned<UniquePackageMap>>,
pub host_dependencies: Option<PixiSpanned<UniquePackageMap>>,
pub build_dependencies: Option<PixiSpanned<UniquePackageMap>>,
Expand Down Expand Up @@ -115,6 +116,7 @@ impl TomlFeature {
.channels
.map(|channels| channels.into_iter().map(|channel| channel.into()).collect()),
channel_priority: self.channel_priority,
solve_strategy: self.solve_strategy,
system_requirements: self.system_requirements,
pypi_options: self.pypi_options,
targets: Targets::from_default_and_user_defined(default_target, targets),
Expand All @@ -133,6 +135,7 @@ impl<'de> toml_span::Deserialize<'de> for TomlFeature {
.map(TomlWith::into_inner);
let channels = th.optional("channels");
let channel_priority = th.optional("channel-priority");
let solve_strategy = th.optional("solve-strategy");
let target = th
.optional::<TomlIndexMap<_, _>>("target")
.map(TomlIndexMap::into_inner)
Expand Down Expand Up @@ -192,6 +195,7 @@ impl<'de> toml_span::Deserialize<'de> for TomlFeature {
platforms,
channels,
channel_priority,
solve_strategy,
system_requirements,
target,
dependencies,
Expand Down
1 change: 1 addition & 0 deletions crates/pixi_manifest/src/toml/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ impl TomlManifest {
channels: None,

channel_priority: workspace.value.channel_priority,
solve_strategy: workspace.value.solve_strategy,

system_requirements: self
.system_requirements
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
source: crates/pixi_manifest/src/toml/manifest.rs
expression: "expect_parse_failure(r#\"\n [workspace]\n channels = []\n platforms = []\n\n [feature.foobar.run-dependencies]\n \"#,)"
---
× Unexpected keys, expected only 'platforms', 'channels', 'channel-priority', 'target', 'dependencies', 'host-dependencies', 'build-dependencies', 'pypi-dependencies', 'activation', 'tasks',
│ 'pypi-options', 'system-requirements'
× Unexpected keys, expected only 'platforms', 'channels', 'channel-priority', 'solve-strategy', 'target', 'dependencies', 'host-dependencies', 'build-dependencies', 'pypi-dependencies',
│ 'activation', 'tasks', 'pypi-options', 'system-requirements'
╭─[pixi.toml:6:25]
5 │
6 │ [feature.foobar.run-dependencies]
Expand Down
8 changes: 7 additions & 1 deletion crates/pixi_manifest/src/toml/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::{
pypi::pypi_options::PypiOptions,
toml::{manifest::ExternalWorkspaceProperties, platform::TomlPlatform, preview::TomlPreview},
utils::PixiSpanned,
workspace::{BuildVariantSource, ChannelPriority},
workspace::{BuildVariantSource, ChannelPriority, SolveStrategy},
};

#[derive(Debug, Clone)]
Expand All @@ -37,6 +37,7 @@ pub struct TomlWorkspace {
pub authors: Option<Vec<String>>,
pub channels: IndexSet<PrioritizedChannel>,
pub channel_priority: Option<ChannelPriority>,
pub solve_strategy: Option<SolveStrategy>,
pub platforms: Spanned<IndexSet<Platform>>,
pub license: Option<Spanned<String>>,
pub license_file: Option<Spanned<PathBuf>>,
Expand Down Expand Up @@ -130,6 +131,7 @@ impl TomlWorkspace {
documentation: self.documentation.or(external.documentation),
channels: self.channels,
channel_priority: self.channel_priority,
solve_strategy: self.solve_strategy,
platforms: self.platforms.value,
conda_pypi_map: self.conda_pypi_map,
pypi_options: self.pypi_options,
Expand Down Expand Up @@ -201,6 +203,9 @@ impl<'de> toml_span::Deserialize<'de> for TomlWorkspace {
.required::<TomlIndexSet<_>>("channels")
.map(TomlIndexSet::into_inner)?;
let channel_priority = th.optional("channel-priority");
let solve_strategy = th
.optional::<TomlWith<_, TomlFromStr<_>>>("solve-strategy")
.map(TomlWith::into_inner);
let platforms = th
.optional::<TomlWith<_, Spanned<TomlIndexSet<TomlPlatform>>>>("platforms")
.map(TomlWith::into_inner);
Expand Down Expand Up @@ -252,6 +257,7 @@ impl<'de> toml_span::Deserialize<'de> for TomlWorkspace {
authors,
channels,
channel_priority,
solve_strategy,
platforms: platforms.unwrap_or_default(),
license,
license_file,
Expand Down
Loading
Loading