diff --git a/crates/rattler_installs_packages/src/artifacts/sdist.rs b/crates/rattler_installs_packages/src/artifacts/sdist.rs index 472707cf..429971e8 100644 --- a/crates/rattler_installs_packages/src/artifacts/sdist.rs +++ b/crates/rattler_installs_packages/src/artifacts/sdist.rs @@ -261,16 +261,16 @@ fn generic_archive_reader( #[cfg(test)] mod tests { use crate::artifacts::SDist; + use crate::index::PackageDb; use crate::index::{ArtifactRequest, PackageSourcesBuilder}; use crate::python_env::Pep508EnvMakers; + use crate::resolve::solve_options::{ResolveOptions, SDistResolution}; use crate::resolve::PypiVersion; - use crate::resolve::SDistResolution; use crate::types::{ArtifactFromSource, PackageName}; use crate::types::{ ArtifactInfo, ArtifactName, DistInfoMetadata, Extra, STreeFilename, Yanked, }; use crate::wheel_builder::WheelBuilder; - use crate::{index::PackageDb, resolve::ResolveOptions}; use insta::{assert_debug_snapshot, assert_ron_snapshot}; use pep440_rs::Version; use reqwest::Client; diff --git a/crates/rattler_installs_packages/src/resolve/dependency_provider.rs b/crates/rattler_installs_packages/src/resolve/dependency_provider.rs index a47aa6bb..5912764a 100644 --- a/crates/rattler_installs_packages/src/resolve/dependency_provider.rs +++ b/crates/rattler_installs_packages/src/resolve/dependency_provider.rs @@ -1,10 +1,10 @@ -use super::solve::PreReleaseResolution; -use super::SDistResolution; use crate::artifacts::SDist; use crate::artifacts::Wheel; use crate::index::{ArtifactRequest, PackageDb}; use crate::python_env::WheelTags; -use crate::resolve::{PinnedPackage, ResolveOptions}; +use crate::resolve::solve_options::SDistResolution; +use crate::resolve::solve_options::{PreReleaseResolution, ResolveOptions}; +use crate::resolve::PinnedPackage; use crate::types::{ ArtifactFromBytes, ArtifactInfo, ArtifactName, Extra, NormalizedPackageName, PackageName, }; @@ -13,19 +13,18 @@ use elsa::FrozenMap; use itertools::Itertools; use miette::{Diagnostic, IntoDiagnostic, MietteDiagnostic}; use parking_lot::Mutex; -use pep440_rs::{Operator, Version, VersionSpecifier, VersionSpecifiers}; +use pep440_rs::{Operator, VersionSpecifier, VersionSpecifiers}; use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl}; use resolvo::{ Candidates, Dependencies, DependencyProvider, KnownDependencies, NameId, Pool, SolvableId, - SolverCache, VersionSet, + SolverCache, }; -use serde::Deserialize; -use serde::Serialize; use std::any::Any; use std::borrow::Borrow; use std::cmp::Ordering; use std::collections::HashMap; -use std::fmt::{Display, Formatter}; + +use crate::resolve::pypi_version_types::{PypiPackageName, PypiVersion, PypiVersionSet}; use std::str::FromStr; use std::sync::Arc; use thiserror::Error; @@ -33,172 +32,6 @@ use tokio::runtime::Handle; use tokio::task; use url::Url; -#[derive(Clone, Debug, Hash, Eq, PartialEq)] -/// This is a wrapper around Specifiers that implements [`VersionSet`] -pub struct PypiVersionSet { - /// The spec to match against - spec: Option, - /// If the VersionOrUrl is a Version specifier and any of the specifiers contains a - /// prerelease, then pre-releases are allowed. For example, - /// `jupyterlab==3.0.0a1` allows pre-releases, but `jupyterlab==3.0.0` does not. - /// - /// We pre-compute if any of the items in the specifiers contains a pre-release and store - /// this as a boolean which is later used during matching. - allows_prerelease: bool, -} - -impl PypiVersionSet { - /// Create a PyPiVersionSeet from VersionOrUrl specifier - pub fn from_spec(spec: Option, prerelease_option: &PreReleaseResolution) -> Self { - let allows_prerelease = match prerelease_option { - PreReleaseResolution::Disallow => false, - PreReleaseResolution::AllowIfNoOtherVersionsOrEnabled { .. } => match spec.as_ref() { - Some(VersionOrUrl::VersionSpecifier(v)) => { - v.iter().any(|s| s.version().any_prerelease()) - } - _ => false, - }, - PreReleaseResolution::Allow => true, - }; - - Self { - spec, - allows_prerelease, - } - } -} - -impl Display for PypiVersionSet { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match &self.spec { - None => write!(f, "*"), - Some(VersionOrUrl::Url(url)) => write!(f, "{url}"), - Some(VersionOrUrl::VersionSpecifier(spec)) => write!(f, "{spec}"), - } - } -} - -/// This is a wrapper around [`Version`] that serves a version -/// within the [`PypiVersionSet`] version set. -#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub enum PypiVersion { - /// Version of artifact - Version { - /// Version of Artifact - version: Version, - - /// Given that the [`PreReleaseResolution`] is - /// AllowIfNoOtherVersionsOrEnabled, this field is true if there are - /// only pre-releases available for this package or if a spec explicitly - /// enabled pre-releases for this package. For example, if the package - /// `foo` has only versions `foo-1.0.0a1` and `foo-1.0.0a2` then this - /// will be true. This allows us later to match against this version and - /// allow the selection of pre-releases. Additionally, this is also true - /// if any of the explicitly mentioned specs (by the user) contains a - /// prerelease (for example c>0.0.0b0) contains the `b0` which signifies - /// a pre-release. - package_allows_prerelease: bool, - }, - /// Direct reference for artifact - Url(Url), -} - -impl PypiVersion { - /// Return if there are any prereleases for version - pub fn any_prerelease(&self) -> bool { - match self { - PypiVersion::Url(_) => false, - PypiVersion::Version { version, .. } => version.any_prerelease(), - } - } - - /// Return if pypi version is git url version - pub fn is_git(&self) -> bool { - match self { - PypiVersion::Version { .. } => false, - PypiVersion::Url(url) => url.scheme().contains("git"), - } - } -} - -impl VersionSet for PypiVersionSet { - type V = PypiVersion; - - fn contains(&self, v: &Self::V) -> bool { - match (self.spec.as_ref(), v) { - (Some(VersionOrUrl::Url(a)), PypiVersion::Url(b)) => a == b, - ( - Some(VersionOrUrl::VersionSpecifier(spec)), - PypiVersion::Version { - version, - package_allows_prerelease, - }, - ) => { - spec.contains(version) - // pre-releases are allowed only when the versionset allows them (jupyterlab==3.0.0a1) - // or there are no other versions available (foo-1.0.0a1, foo-1.0.0a2) - // or alternatively if the user has enabled all pre-releases or this specific (this is encoded in the allows_prerelease field) - && (self.allows_prerelease || *package_allows_prerelease || !version.any_prerelease()) - } - ( - None, - PypiVersion::Version { - version, - package_allows_prerelease, - }, - ) => self.allows_prerelease || *package_allows_prerelease || !version.any_prerelease(), - (None, PypiVersion::Url(_)) => true, - _ => false, - } - } -} - -impl Display for PypiVersion { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - PypiVersion::Version { version, .. } => write!(f, "{version}"), - PypiVersion::Url(u) => write!(f, "{u}"), - } - } -} - -#[derive(PartialEq, Eq, Hash, Clone, Debug)] -/// This can either be a base package name or with an extra -/// this is used to support optional dependencies -pub(crate) enum PypiPackageName { - /// Regular dependency - Base(NormalizedPackageName), - /// Optional dependency - Extra(NormalizedPackageName, Extra), -} - -impl PypiPackageName { - /// Returns the actual package (normalized) name without the extra - pub fn base(&self) -> &NormalizedPackageName { - match self { - PypiPackageName::Base(normalized) => normalized, - PypiPackageName::Extra(normalized, _) => normalized, - } - } - - /// Retrieves the extra if it is available - pub fn extra(&self) -> Option<&Extra> { - match self { - PypiPackageName::Base(_) => None, - PypiPackageName::Extra(_, e) => Some(e), - } - } -} - -impl Display for PypiPackageName { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - PypiPackageName::Base(name) => write!(f, "{}", name), - PypiPackageName::Extra(name, extra) => write!(f, "{}[{}]", name, extra.as_str()), - } - } -} - /// This is a [`DependencyProvider`] for PyPI packages pub(crate) struct PypiDependencyProvider { pub pool: Pool, diff --git a/crates/rattler_installs_packages/src/resolve/mod.rs b/crates/rattler_installs_packages/src/resolve/mod.rs index b5dde58b..8bd8a96c 100644 --- a/crates/rattler_installs_packages/src/resolve/mod.rs +++ b/crates/rattler_installs_packages/src/resolve/mod.rs @@ -9,10 +9,11 @@ //! mod dependency_provider; +mod pypi_version_types; mod solve; +pub mod solve_options; +mod solve_types; -pub use dependency_provider::{PypiVersion, PypiVersionSet}; -pub use solve::{ - resolve, OnWheelBuildFailure, PinnedPackage, PreReleaseResolution, ResolveOptions, - SDistResolution, -}; +pub use pypi_version_types::PypiVersion; +pub use pypi_version_types::PypiVersionSet; +pub use solve::{resolve, PinnedPackage}; diff --git a/crates/rattler_installs_packages/src/resolve/pypi_version_types.rs b/crates/rattler_installs_packages/src/resolve/pypi_version_types.rs new file mode 100644 index 00000000..238c6e04 --- /dev/null +++ b/crates/rattler_installs_packages/src/resolve/pypi_version_types.rs @@ -0,0 +1,178 @@ +//! This module contains types that are used to represent versions and version sets +//! these are used by the [`resolvo`] crate to resolve dependencies. +//! This module, in combination with the [`super::dependency_provider`] modules is used to make the PyPI ecosystem compatible with the [`resolvo`] crate. + +use crate::resolve::solve_options::PreReleaseResolution; +use crate::types::{Extra, NormalizedPackageName}; +use pep440_rs::Version; +use pep508_rs::VersionOrUrl; +use resolvo::VersionSet; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use url::Url; + +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +/// This is a wrapper around Specifiers that implements [`VersionSet`] +pub struct PypiVersionSet { + /// The spec to match against + spec: Option, + /// If the VersionOrUrl is a Version specifier and any of the specifiers contains a + /// prerelease, then pre-releases are allowed. For example, + /// `jupyterlab==3.0.0a1` allows pre-releases, but `jupyterlab==3.0.0` does not. + /// + /// We pre-compute if any of the items in the specifiers contains a pre-release and store + /// this as a boolean which is later used during matching. + allows_prerelease: bool, +} + +impl PypiVersionSet { + /// Create a PyPiVersionSeet from VersionOrUrl specifier + pub fn from_spec(spec: Option, prerelease_option: &PreReleaseResolution) -> Self { + let allows_prerelease = match prerelease_option { + PreReleaseResolution::Disallow => false, + PreReleaseResolution::AllowIfNoOtherVersionsOrEnabled { .. } => match spec.as_ref() { + Some(VersionOrUrl::VersionSpecifier(v)) => { + v.iter().any(|s| s.version().any_prerelease()) + } + _ => false, + }, + PreReleaseResolution::Allow => true, + }; + + Self { + spec, + allows_prerelease, + } + } +} + +impl Display for PypiVersionSet { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self.spec { + None => write!(f, "*"), + Some(VersionOrUrl::Url(url)) => write!(f, "{url}"), + Some(VersionOrUrl::VersionSpecifier(spec)) => write!(f, "{spec}"), + } + } +} + +/// This is a wrapper around [`Version`] that serves a version +/// within the [`PypiVersionSet`] version set. +#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum PypiVersion { + /// Version of artifact + Version { + /// Version of Artifact + version: Version, + + /// Given that the [`PreReleaseResolution`] is + /// AllowIfNoOtherVersionsOrEnabled, this field is true if there are + /// only pre-releases available for this package or if a spec explicitly + /// enabled pre-releases for this package. For example, if the package + /// `foo` has only versions `foo-1.0.0a1` and `foo-1.0.0a2` then this + /// will be true. This allows us later to match against this version and + /// allow the selection of pre-releases. Additionally, this is also true + /// if any of the explicitly mentioned specs (by the user) contains a + /// prerelease (for example c>0.0.0b0) contains the `b0` which signifies + /// a pre-release. + package_allows_prerelease: bool, + }, + /// Direct reference for artifact + Url(Url), +} + +impl PypiVersion { + /// Return if there are any prereleases for version + pub fn any_prerelease(&self) -> bool { + match self { + PypiVersion::Url(_) => false, + PypiVersion::Version { version, .. } => version.any_prerelease(), + } + } + + /// Return if pypi version is git url version + pub fn is_git(&self) -> bool { + match self { + PypiVersion::Version { .. } => false, + PypiVersion::Url(url) => url.scheme().contains("git"), + } + } +} + +impl VersionSet for PypiVersionSet { + type V = PypiVersion; + + fn contains(&self, v: &Self::V) -> bool { + match (self.spec.as_ref(), v) { + (Some(VersionOrUrl::Url(a)), PypiVersion::Url(b)) => a == b, + ( + Some(VersionOrUrl::VersionSpecifier(spec)), + PypiVersion::Version { + version, + package_allows_prerelease, + }, + ) => { + spec.contains(version) + // pre-releases are allowed only when the versionset allows them (jupyterlab==3.0.0a1) + // or there are no other versions available (foo-1.0.0a1, foo-1.0.0a2) + // or alternatively if the user has enabled all pre-releases or this specific (this is encoded in the allows_prerelease field) + && (self.allows_prerelease || *package_allows_prerelease || !version.any_prerelease()) + } + ( + None, + PypiVersion::Version { + version, + package_allows_prerelease, + }, + ) => self.allows_prerelease || *package_allows_prerelease || !version.any_prerelease(), + (None, PypiVersion::Url(_)) => true, + _ => false, + } + } +} + +impl Display for PypiVersion { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PypiVersion::Version { version, .. } => write!(f, "{version}"), + PypiVersion::Url(u) => write!(f, "{u}"), + } + } +} + +#[derive(PartialEq, Eq, Hash, Clone, Debug)] +/// This can either be a base package name or with an extra +/// this is used to support optional dependencies +pub(crate) enum PypiPackageName { + /// Regular dependency + Base(NormalizedPackageName), + /// Optional dependency + Extra(NormalizedPackageName, Extra), +} + +impl PypiPackageName { + /// Returns the actual package (normalized) name without the extra + pub fn base(&self) -> &NormalizedPackageName { + match self { + PypiPackageName::Base(normalized) => normalized, + PypiPackageName::Extra(normalized, _) => normalized, + } + } + + /// Retrieves the extra if it is available + pub fn extra(&self) -> Option<&Extra> { + match self { + PypiPackageName::Base(_) => None, + PypiPackageName::Extra(_, e) => Some(e), + } + } +} + +impl Display for PypiPackageName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PypiPackageName::Base(name) => write!(f, "{}", name), + PypiPackageName::Extra(name, extra) => write!(f, "{}[{}]", name, extra.as_str()), + } + } +} diff --git a/crates/rattler_installs_packages/src/resolve/solve.rs b/crates/rattler_installs_packages/src/resolve/solve.rs index ec925858..d8b75b87 100644 --- a/crates/rattler_installs_packages/src/resolve/solve.rs +++ b/crates/rattler_installs_packages/src/resolve/solve.rs @@ -1,8 +1,7 @@ -use super::dependency_provider::{PypiPackageName, PypiVersionSet}; use crate::index::PackageDb; -use crate::python_env::{PythonLocation, WheelTags}; +use crate::python_env::WheelTags; use crate::resolve::dependency_provider::PypiDependencyProvider; -use crate::resolve::PypiVersion; +use crate::resolve::pypi_version_types::PypiVersion; use crate::types::PackageName; use crate::{types::ArtifactInfo, types::Extra, types::NormalizedPackageName}; use elsa::FrozenMap; @@ -13,6 +12,8 @@ use std::collections::HashMap; use std::str::FromStr; use url::Url; +use crate::resolve::pypi_version_types::{PypiPackageName, PypiVersionSet}; +use crate::resolve::solve_options::ResolveOptions; use std::collections::HashSet; use std::ops::Deref; use std::sync::Arc; @@ -39,210 +40,6 @@ pub struct PinnedPackage { pub artifacts: Vec>, } -/// Defines how to handle sdists during resolution. -#[derive(Default, Debug, Clone, Copy, Eq, PartialOrd, PartialEq)] -pub enum SDistResolution { - /// Both versions with wheels and/or sdists are allowed to be selected during resolution. But - /// during resolution the metadata from wheels is preferred over sdists. - /// - /// If we have the following scenario: - /// - /// ```txt - /// Version@1 - /// - WheelA - /// - WheelB - /// Version@2 - /// - SDist - /// - WheelA - /// - WheelB - /// Version@3 - /// - SDist - /// ``` - /// - /// Then the Version@3 will be selected because it has the highest version. This option makes no - /// distinction between whether the version has wheels or sdist. - #[default] - Normal, - - /// Allow sdists to be selected during resolution but only if all versions with wheels cannot - /// be selected. This means that even if a higher version is technically available it might not - /// be selected if it only has an available sdist. - /// - /// If we have the following scenario: - /// - /// ```txt - /// Version@1 - /// - SDist - /// - WheelA - /// - WheelB - /// Version@2 - /// - SDist - /// ``` - /// - /// Then the Version@1 will be selected even though the highest version is 2. This is because - /// version 2 has no available wheels. If version 1 would not exist though then version 2 is - /// selected because there are no other versions with a wheel. - PreferWheels, - - /// Allow sdists to be selected during resolution and prefer them over wheels. This means that - /// even if a higher version is available but it only includes wheels it might not be selected. - /// - /// If we have the following scenario: - /// - /// ```txt - /// Version@1 - /// - SDist - /// - WheelA - /// Version@2 - /// - WheelA - /// ``` - /// - /// Then the version@1 will be selected even though the highest version is 2. This is because - /// version 2 has no sdists available. If version 1 would not exist though then version 2 is - /// selected because there are no other versions with an sdist. - PreferSDists, - - /// Don't select sdists during resolution - /// - /// If we have the following scenario: - /// - /// ```txt - /// Version@1 - /// - SDist - /// - WheelA - /// - WheelB - /// Version@2 - /// - SDist - /// ``` - /// - /// Then version 1 will be selected because it has wheels and version 2 does not. If version 1 - /// would not exist there would be no solution because none of the versions have wheels. - OnlyWheels, - - /// Only select sdists during resolution - /// - /// If we have the following scenario: - /// - /// ```txt - /// Version@1 - /// - SDist - /// Version@2 - /// - WheelA - /// ``` - /// - /// Then version 1 will be selected because it has an sdist and version 2 does not. If version 1 - /// would not exist there would be no solution because none of the versions have sdists. - OnlySDists, -} - -/// Defines how to pre-releases are handled during package resolution. -#[derive(Debug, Clone, Eq, PartialOrd, PartialEq)] -pub enum PreReleaseResolution { - /// Don't allow pre-releases to be selected during resolution - Disallow, - - /// Conditionally allow pre-releases to be selected during resolution. This - /// behavior emulates `pip`'s pre-release resolution, which is not according - /// to "spec" but the most widely used logic. - /// - /// It works as follows: - /// - /// - if a version specifier mentions a pre-release, then we allow - /// pre-releases to be selected, for example `jupyterlab==4.1.0b0` will - /// allow the selection of the `jupyterlab-4.1.0b0` beta release during - /// resolution. - /// - if a package _only_ contains pre-release versions then we allow - /// pre-releases to be selected for any version specifier. For example, if - /// the package `supernew` only contains `supernew-1.0.0b0` and - /// `supernew-1.0.0b1` then we allow `supernew==1.0.0` to select - /// `supernew-1.0.0b1` during resolution. - /// - Any name that is mentioned in the `allow` list will allow pre-releases (this - /// is usually derived from the specs given by the user). For example, if the user - /// asks for `foo>0.0.0b0`, pre-releases are globally enabled for package foo (also as - /// transitive dependency). - AllowIfNoOtherVersionsOrEnabled { - /// A list of package names that will allow pre-releases to be selected - allow_names: Vec, - }, - - /// Allow any pre-releases to be selected during resolution - Allow, -} - -impl Default for PreReleaseResolution { - fn default() -> Self { - PreReleaseResolution::AllowIfNoOtherVersionsOrEnabled { - allow_names: Vec::new(), - } - } -} - -impl PreReleaseResolution { - /// Return a AllowIfNoOtherVersionsOrEnabled variant from a list of requirements - pub fn from_specs(specs: &[Requirement]) -> Self { - let mut allow_names = Vec::new(); - for spec in specs { - match &spec.version_or_url { - Some(VersionOrUrl::VersionSpecifier(v)) => { - if v.iter().any(|s| s.version().any_prerelease()) { - let name = PackageName::from_str(&spec.name).expect("invalid package name"); - allow_names.push(name.as_str().to_string()); - } - } - _ => continue, - }; - } - PreReleaseResolution::AllowIfNoOtherVersionsOrEnabled { allow_names } - } -} - -impl SDistResolution { - /// Returns true if sdists are allowed to be selected during resolution - pub fn allow_sdists(&self) -> bool { - !matches!(self, SDistResolution::OnlyWheels) - } - - /// Returns true if sdists are allowed to be selected during resolution - pub fn allow_wheels(&self) -> bool { - !matches!(self, SDistResolution::OnlySDists) - } -} - -/// Specifies what to do with failed build environments -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub enum OnWheelBuildFailure { - /// Save failed build environments to temporary directory - SaveBuildEnv, - /// Delete failed build environments - #[default] - DeleteBuildEnv, -} - -/// Additional options that may influence the solver. In general passing [`Default::default`] to -/// the [`resolve`] function should provide sane defaults, however if you want to fine tune the -/// resolver you can do so via this struct. -#[derive(Default, Clone)] -pub struct ResolveOptions { - /// Defines how to handle sdists during resolution. By default sdists will be treated the same - /// as wheels. - pub sdist_resolution: SDistResolution, - - /// Defines what python interpreter to use for resolution. By default the python interpreter - /// from the system is used. This is only used during resolution and building of wheel files - pub python_location: PythonLocation, - - /// Defines if we should inherit env variables during build process of wheel files - pub clean_env: bool, - - /// Defines what to do with failed build environments - /// by default these are deleted but can also be saved for debugging purposes - pub on_wheel_build_failure: OnWheelBuildFailure, - - /// Defines whether pre-releases are allowed to be selected during resolution. By default - /// pre-releases are not allowed (only if there are no other versions available for a given dependency). - pub pre_release_resolution: PreReleaseResolution, -} - /// Resolves an environment that contains the given requirements and all dependencies of those /// requirements. /// diff --git a/crates/rattler_installs_packages/src/resolve/solve_options.rs b/crates/rattler_installs_packages/src/resolve/solve_options.rs new file mode 100644 index 00000000..69549712 --- /dev/null +++ b/crates/rattler_installs_packages/src/resolve/solve_options.rs @@ -0,0 +1,211 @@ +//! Contains the options that can be passed to the [`super::solve::resolve`] function. + +use crate::python_env::PythonLocation; +use pep508_rs::{Requirement, VersionOrUrl}; +use std::str::FromStr; + +use crate::types::PackageName; + +/// Defines how to handle sdists during resolution. +#[derive(Default, Debug, Clone, Copy, Eq, PartialOrd, PartialEq)] +pub enum SDistResolution { + /// Both versions with wheels and/or sdists are allowed to be selected during resolution. But + /// during resolution the metadata from wheels is preferred over sdists. + /// + /// If we have the following scenario: + /// + /// ```txt + /// Version@1 + /// - WheelA + /// - WheelB + /// Version@2 + /// - SDist + /// - WheelA + /// - WheelB + /// Version@3 + /// - SDist + /// ``` + /// + /// Then the Version@3 will be selected because it has the highest version. This option makes no + /// distinction between whether the version has wheels or sdist. + #[default] + Normal, + + /// Allow sdists to be selected during resolution but only if all versions with wheels cannot + /// be selected. This means that even if a higher version is technically available it might not + /// be selected if it only has an available sdist. + /// + /// If we have the following scenario: + /// + /// ```txt + /// Version@1 + /// - SDist + /// - WheelA + /// - WheelB + /// Version@2 + /// - SDist + /// ``` + /// + /// Then the Version@1 will be selected even though the highest version is 2. This is because + /// version 2 has no available wheels. If version 1 would not exist though then version 2 is + /// selected because there are no other versions with a wheel. + PreferWheels, + + /// Allow sdists to be selected during resolution and prefer them over wheels. This means that + /// even if a higher version is available but it only includes wheels it might not be selected. + /// + /// If we have the following scenario: + /// + /// ```txt + /// Version@1 + /// - SDist + /// - WheelA + /// Version@2 + /// - WheelA + /// ``` + /// + /// Then the version@1 will be selected even though the highest version is 2. This is because + /// version 2 has no sdists available. If version 1 would not exist though then version 2 is + /// selected because there are no other versions with an sdist. + PreferSDists, + + /// Don't select sdists during resolution + /// + /// If we have the following scenario: + /// + /// ```txt + /// Version@1 + /// - SDist + /// - WheelA + /// - WheelB + /// Version@2 + /// - SDist + /// ``` + /// + /// Then version 1 will be selected because it has wheels and version 2 does not. If version 1 + /// would not exist there would be no solution because none of the versions have wheels. + OnlyWheels, + + /// Only select sdists during resolution + /// + /// If we have the following scenario: + /// + /// ```txt + /// Version@1 + /// - SDist + /// Version@2 + /// - WheelA + /// ``` + /// + /// Then version 1 will be selected because it has an sdist and version 2 does not. If version 1 + /// would not exist there would be no solution because none of the versions have sdists. + OnlySDists, +} + +/// Defines how to pre-releases are handled during package resolution. +#[derive(Debug, Clone, Eq, PartialOrd, PartialEq)] +pub enum PreReleaseResolution { + /// Don't allow pre-releases to be selected during resolution + Disallow, + + /// Conditionally allow pre-releases to be selected during resolution. This + /// behavior emulates `pip`'s pre-release resolution, which is not according + /// to "spec" but the most widely used logic. + /// + /// It works as follows: + /// + /// - if a version specifier mentions a pre-release, then we allow + /// pre-releases to be selected, for example `jupyterlab==4.1.0b0` will + /// allow the selection of the `jupyterlab-4.1.0b0` beta release during + /// resolution. + /// - if a package _only_ contains pre-release versions then we allow + /// pre-releases to be selected for any version specifier. For example, if + /// the package `supernew` only contains `supernew-1.0.0b0` and + /// `supernew-1.0.0b1` then we allow `supernew==1.0.0` to select + /// `supernew-1.0.0b1` during resolution. + /// - Any name that is mentioned in the `allow` list will allow pre-releases (this + /// is usually derived from the specs given by the user). For example, if the user + /// asks for `foo>0.0.0b0`, pre-releases are globally enabled for package foo (also as + /// transitive dependency). + AllowIfNoOtherVersionsOrEnabled { + /// A list of package names that will allow pre-releases to be selected + allow_names: Vec, + }, + + /// Allow any pre-releases to be selected during resolution + Allow, +} + +impl Default for PreReleaseResolution { + fn default() -> Self { + PreReleaseResolution::AllowIfNoOtherVersionsOrEnabled { + allow_names: Vec::new(), + } + } +} + +impl PreReleaseResolution { + /// Return a AllowIfNoOtherVersionsOrEnabled variant from a list of requirements + pub fn from_specs(specs: &[Requirement]) -> Self { + let mut allow_names = Vec::new(); + for spec in specs { + match &spec.version_or_url { + Some(VersionOrUrl::VersionSpecifier(v)) => { + if v.iter().any(|s| s.version().any_prerelease()) { + let name = PackageName::from_str(&spec.name).expect("invalid package name"); + allow_names.push(name.as_str().to_string()); + } + } + _ => continue, + }; + } + PreReleaseResolution::AllowIfNoOtherVersionsOrEnabled { allow_names } + } +} + +impl SDistResolution { + /// Returns true if sdists are allowed to be selected during resolution + pub fn allow_sdists(&self) -> bool { + !matches!(self, SDistResolution::OnlyWheels) + } + + /// Returns true if sdists are allowed to be selected during resolution + pub fn allow_wheels(&self) -> bool { + !matches!(self, SDistResolution::OnlySDists) + } +} + +/// Specifies what to do with failed build environments +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum OnWheelBuildFailure { + /// Save failed build environments to temporary directory + SaveBuildEnv, + /// Delete failed build environments + #[default] + DeleteBuildEnv, +} + +/// Additional options that may influence the solver. In general passing [`Default::default`] to +/// the [`resolve::resolve`] function should provide sane defaults, however if you want to fine tune the +/// resolver you can do so via this struct. +#[derive(Default, Clone)] +pub struct ResolveOptions { + /// Defines how to handle sdists during resolution. By default sdists will be treated the same + /// as wheels. + pub sdist_resolution: SDistResolution, + + /// Defines what python interpreter to use for resolution. By default the python interpreter + /// from the system is used. This is only used during resolution and building of wheel files + pub python_location: PythonLocation, + + /// Defines if we should inherit env variables during build process of wheel files + pub clean_env: bool, + + /// Defines what to do with failed build environments + /// by default these are deleted but can also be saved for debugging purposes + pub on_wheel_build_failure: OnWheelBuildFailure, + + /// Defines whether pre-releases are allowed to be selected during resolution. By default + /// pre-releases are not allowed (only if there are no other versions available for a given dependency). + pub pre_release_resolution: PreReleaseResolution, +} diff --git a/crates/rattler_installs_packages/src/resolve/solve_types.rs b/crates/rattler_installs_packages/src/resolve/solve_types.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/rattler_installs_packages/src/resolve/solve_types.rs @@ -0,0 +1 @@ + diff --git a/crates/rattler_installs_packages/src/wheel_builder/mod.rs b/crates/rattler_installs_packages/src/wheel_builder/mod.rs index 299db35c..22f68b9f 100644 --- a/crates/rattler_installs_packages/src/wheel_builder/mod.rs +++ b/crates/rattler_installs_packages/src/wheel_builder/mod.rs @@ -16,7 +16,7 @@ use parking_lot::Mutex; use pep508_rs::MarkerEnvironment; use crate::python_env::{ParsePythonInterpreterVersionError, PythonInterpreterVersion}; -use crate::resolve::{OnWheelBuildFailure, ResolveOptions}; +use crate::resolve::solve_options::{OnWheelBuildFailure, ResolveOptions}; use crate::types::ArtifactFromSource; use crate::types::{NormalizedPackageName, PackageName, SourceArtifactName, WheelFilename}; use crate::wheel_builder::build_environment::BuildEnvironment; @@ -291,7 +291,7 @@ mod tests { use crate::artifacts::SDist; use crate::index::{PackageDb, PackageSourcesBuilder}; use crate::python_env::{Pep508EnvMakers, PythonInterpreterVersion}; - use crate::resolve::ResolveOptions; + use crate::resolve::solve_options::ResolveOptions; use crate::wheel_builder::wheel_cache::WheelCacheKey; use crate::wheel_builder::WheelBuilder; use reqwest::Client; @@ -365,7 +365,8 @@ mod tests { let package_db = get_package_db(); let env_markers = Arc::new(Pep508EnvMakers::from_env().await.unwrap().0); let resolve_options = ResolveOptions { - on_wheel_build_failure: crate::resolve::OnWheelBuildFailure::SaveBuildEnv, + on_wheel_build_failure: + crate::resolve::solve_options::OnWheelBuildFailure::SaveBuildEnv, ..Default::default() }; diff --git a/crates/rattler_installs_packages/tests/resolver.rs b/crates/rattler_installs_packages/tests/resolver.rs index 1a179848..f8883624 100644 --- a/crates/rattler_installs_packages/tests/resolver.rs +++ b/crates/rattler_installs_packages/tests/resolver.rs @@ -1,13 +1,12 @@ #![cfg(feature = "resolvo")] use pep508_rs::{MarkerEnvironment, Requirement}; +use rattler_installs_packages::resolve::solve_options::{ResolveOptions, SDistResolution}; use rattler_installs_packages::{ index::PackageDb, python_env::{WheelTag, WheelTags}, resolve::resolve, resolve::PinnedPackage, - resolve::ResolveOptions, - resolve::SDistResolution, types::NormalizedPackageName, }; use std::{collections::HashMap, path::Path, str::FromStr, sync::OnceLock}; diff --git a/crates/rip_bin/src/main.rs b/crates/rip_bin/src/main.rs index c8e30f5b..fed018a9 100644 --- a/crates/rip_bin/src/main.rs +++ b/crates/rip_bin/src/main.rs @@ -1,5 +1,5 @@ use fs_err as fs; -use rattler_installs_packages::resolve::PreReleaseResolution; +use rattler_installs_packages::resolve::solve_options::{PreReleaseResolution, ResolveOptions}; use rip_bin::{global_multi_progress, IndicatifWriter}; use serde::Serialize; use std::collections::HashMap; @@ -19,11 +19,10 @@ use url::Url; use rattler_installs_packages::artifacts::wheel::UnpackWheelOptions; use rattler_installs_packages::index::PackageSourcesBuilder; use rattler_installs_packages::python_env::{PythonLocation, WheelTags}; -use rattler_installs_packages::resolve::OnWheelBuildFailure; +use rattler_installs_packages::resolve::solve_options::OnWheelBuildFailure; use rattler_installs_packages::wheel_builder::WheelBuilder; use rattler_installs_packages::{ - normalize_index_url, python_env::Pep508EnvMakers, resolve, resolve::resolve, - resolve::ResolveOptions, types::Requirement, + normalize_index_url, python_env::Pep508EnvMakers, resolve, resolve::resolve, types::Requirement, }; #[derive(Serialize, Debug)] @@ -55,7 +54,7 @@ struct Args { /// How to handle sidsts #[clap(flatten)] - sdist_resolution: SDistResolution, + sdist_resolution: SDistResolutionArgs, /// Path to the python interpreter to use for resolving environment markers and creating venvs #[clap(long, short)] @@ -79,7 +78,7 @@ struct Args { #[derive(Parser)] #[group(multiple = false)] -struct SDistResolution { +struct SDistResolutionArgs { /// Prefer any version with wheels over any version with sdists #[clap(long)] prefer_wheels: bool, @@ -97,18 +96,19 @@ struct SDistResolution { only_sdists: bool, } -impl From for resolve::SDistResolution { - fn from(value: SDistResolution) -> Self { +use resolve::solve_options::SDistResolution; +impl From for SDistResolution { + fn from(value: SDistResolutionArgs) -> Self { if value.only_sdists { - resolve::SDistResolution::OnlySDists + SDistResolution::OnlySDists } else if value.only_wheels { - resolve::SDistResolution::OnlyWheels + SDistResolution::OnlyWheels } else if value.prefer_sdists { - resolve::SDistResolution::PreferSDists + SDistResolution::PreferSDists } else if value.prefer_wheels { - resolve::SDistResolution::PreferWheels + SDistResolution::PreferWheels } else { - resolve::SDistResolution::Normal + SDistResolution::Normal } } }