diff --git a/.github/workflows/rust-compile.yml b/.github/workflows/rust-compile.yml index e60144ae2..cb07c179a 100644 --- a/.github/workflows/rust-compile.yml +++ b/.github/workflows/rust-compile.yml @@ -146,7 +146,7 @@ jobs: run: > cargo nextest run --workspace - --features ${{ env.DEFAULT_FEATURES }} + --features ${{ env.DEFAULT_FEATURES }},experimental_extras --target ${{ matrix.target }} ${{ steps.build-options.outputs.CARGO_BUILD_OPTIONS}} ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}} diff --git a/crates/rattler-bin/src/commands/create.rs b/crates/rattler-bin/src/commands/create.rs index 7ab8e7fd0..72a8171f9 100644 --- a/crates/rattler-bin/src/commands/create.rs +++ b/crates/rattler-bin/src/commands/create.rs @@ -1,5 +1,6 @@ use std::{ borrow::Cow, + collections::HashMap, env, future::IntoFuture, path::PathBuf, @@ -18,8 +19,8 @@ use rattler::{ package_cache::PackageCache, }; use rattler_conda_types::{ - Channel, ChannelConfig, GenericVirtualPackage, MatchSpec, ParseStrictness, Platform, - PrefixRecord, RepoDataRecord, Version, + Channel, ChannelConfig, GenericVirtualPackage, MatchSpec, PackageName, ParseStrictness, + Platform, PrefixRecord, RepoDataRecord, Version, }; use rattler_networking::AuthenticationMiddleware; use rattler_repodata_gateway::{Gateway, RepoData, SourceConfig}; @@ -258,12 +259,14 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { // Next, use a solver to solve this specific problem. This provides us with all // the operations we need to apply to our environment to bring it up to // date. - let required_packages = + let solver_result = wrap_in_progress("solving", move || match opt.solver.unwrap_or_default() { Solver::Resolvo => resolvo::Solver.solve(solver_task), Solver::LibSolv => libsolv_c::Solver.solve(solver_task), })?; + let required_packages: Vec = solver_result.records; + if opt.dry_run { // Construct a transaction to let transaction = Transaction::from_current_and_desired( @@ -275,7 +278,7 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { if transaction.operations.is_empty() { println!("No operations necessary"); } else { - print_transaction(&transaction); + print_transaction(&transaction, solver_result.features); } return Ok(()); @@ -306,14 +309,17 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { console::style(console::Emoji("✔", "")).green(), install_start.elapsed() ); - print_transaction(&result.transaction); + print_transaction(&result.transaction, solver_result.features); } Ok(()) } /// Prints the operations of the transaction to the console. -fn print_transaction(transaction: &Transaction) { +fn print_transaction( + transaction: &Transaction, + features: HashMap>, +) { let format_record = |r: &RepoDataRecord| { let direct_url_print = if let Some(channel) = &r.channel { channel.clone() @@ -321,13 +327,24 @@ fn print_transaction(transaction: &Transaction) { String::new() }; - format!( - "{} {} {} {}", - r.package_record.name.as_normalized(), - r.package_record.version, - r.package_record.build, - direct_url_print, - ) + if let Some(features) = features.get(&r.package_record.name) { + format!( + "{}[{}] {} {} {}", + r.package_record.name.as_normalized(), + features.join(", "), + r.package_record.version, + r.package_record.build, + direct_url_print, + ) + } else { + format!( + "{} {} {} {}", + r.package_record.name.as_normalized(), + r.package_record.version, + r.package_record.build, + direct_url_print, + ) + } }; for operation in &transaction.operations { diff --git a/crates/rattler_conda_types/Cargo.toml b/crates/rattler_conda_types/Cargo.toml index 7868ce32b..5b103a6e2 100644 --- a/crates/rattler_conda_types/Cargo.toml +++ b/crates/rattler_conda_types/Cargo.toml @@ -12,6 +12,7 @@ readme.workspace = true [features] default = ["rayon"] +experimental_extras = [] [dependencies] chrono = { workspace = true } diff --git a/crates/rattler_conda_types/src/lib.rs b/crates/rattler_conda_types/src/lib.rs index 4afa9d58e..c305b5ed4 100644 --- a/crates/rattler_conda_types/src/lib.rs +++ b/crates/rattler_conda_types/src/lib.rs @@ -27,7 +27,7 @@ pub mod prefix_record; #[cfg(test)] use std::path::{Path, PathBuf}; -pub use build_spec::{BuildNumber, BuildNumberSpec, ParseBuildNumberSpecError}; +pub use build_spec::{BuildNumber, BuildNumberSpec, OrdOperator, ParseBuildNumberSpecError}; pub use channel::{Channel, ChannelConfig, ChannelUrl, NamedChannelOrUrl, ParseChannelError}; pub use channel_data::{ChannelData, ChannelDataPackage}; pub use environment_yaml::{EnvironmentYaml, MatchSpecOrSubSection}; @@ -53,7 +53,7 @@ pub use repo_data::{ ChannelInfo, ConvertSubdirError, PackageRecord, RecordFromPath, RepoData, ValidatePackageRecordsError, }; -pub use repo_data_record::RepoDataRecord; +pub use repo_data_record::{RepoDataRecord, SolverResult}; pub use run_export::RunExportKind; pub use version::{ Component, ParseVersionError, ParseVersionErrorKind, StrictVersion, Version, VersionBumpError, diff --git a/crates/rattler_conda_types/src/match_spec/mod.rs b/crates/rattler_conda_types/src/match_spec/mod.rs index 24bf7d68a..e1cd2a40e 100644 --- a/crates/rattler_conda_types/src/match_spec/mod.rs +++ b/crates/rattler_conda_types/src/match_spec/mod.rs @@ -2,6 +2,7 @@ use crate::{ build_spec::BuildNumberSpec, GenericVirtualPackage, PackageName, PackageRecord, RepoDataRecord, VersionSpec, }; +use itertools::Itertools; use rattler_digest::{serde::SerializableHash, Md5Hash, Sha256Hash}; use serde::{Deserialize, Deserializer, Serialize}; use serde_with::{serde_as, skip_serializing_none}; @@ -135,6 +136,8 @@ pub struct MatchSpec { pub build_number: Option, /// Match the specific filename of the package pub file_name: Option, + /// The selected optional features of the package + pub extras: Option>, /// The channel of the package pub channel: Option>, /// The subdir of the channel @@ -183,6 +186,10 @@ impl Display for MatchSpec { let mut keys = Vec::new(); + if let Some(extras) = &self.extras { + keys.push(format!("extras=[{}]", extras.iter().format(", "))); + } + if let Some(md5) = &self.md5 { keys.push(format!("md5=\"{md5:x}\"")); } @@ -221,6 +228,7 @@ impl MatchSpec { build: self.build, build_number: self.build_number, file_name: self.file_name, + extras: self.extras, channel: self.channel, subdir: self.subdir, namespace: self.namespace, @@ -265,6 +273,8 @@ pub struct NamelessMatchSpec { pub build_number: Option, /// Match the specific filename of the package pub file_name: Option, + /// Optional extra dependencies to select for the package + pub extras: Option>, /// The channel of the package #[serde(deserialize_with = "deserialize_channel", default)] pub channel: Option>, @@ -318,6 +328,7 @@ impl From for NamelessMatchSpec { build: spec.build, build_number: spec.build_number, file_name: spec.file_name, + extras: spec.extras, channel: spec.channel, subdir: spec.subdir, namespace: spec.namespace, @@ -337,6 +348,7 @@ impl MatchSpec { build: spec.build, build_number: spec.build_number, file_name: spec.file_name, + extras: spec.extras, channel: spec.channel, subdir: spec.subdir, namespace: spec.namespace, diff --git a/crates/rattler_conda_types/src/match_spec/parse.rs b/crates/rattler_conda_types/src/match_spec/parse.rs index f27b06788..959ce5327 100644 --- a/crates/rattler_conda_types/src/match_spec/parse.rs +++ b/crates/rattler_conda_types/src/match_spec/parse.rs @@ -10,6 +10,7 @@ use nom::{ sequence::{delimited, preceded, separated_pair, terminated}, Finish, IResult, }; + use rattler_digest::{parse_digest_from_hex, Md5, Sha256}; use smallvec::SmallVec; use thiserror::Error; @@ -178,6 +179,7 @@ fn parse_bracket_list(input: &str) -> Result, ParseMatchSpecError alt(( delimited(char('"'), take_until("\""), char('"')), delimited(char('\''), take_until("'"), char('\'')), + delimited(char('['), take_until("]"), char(']')), take_till1(|c| c == ',' || c == ']' || c == '\'' || c == '"'), )), ))(input) @@ -207,7 +209,9 @@ fn parse_bracket_list(input: &str) -> Result, ParseMatchSpecError /// Strips the brackets part of the matchspec returning the rest of the /// matchspec and the contents of the brackets as a `Vec<&str>`. fn strip_brackets(input: &str) -> Result<(Cow<'_, str>, BracketVec<'_>), ParseMatchSpecError> { - if let Some(matches) = lazy_regex::regex!(r#".*(?:(\[.*\]))$"#).captures(input) { + if let Some(matches) = + lazy_regex::regex!(r#".*(\[(?:[^\[\]]|\[(?:[^\[\]]|\[.*\])*\])*\])$"#).captures(input) + { let bracket_str = matches.get(1).unwrap().as_str(); let bracket_contents = parse_bracket_list(bracket_str)?; @@ -223,6 +227,32 @@ fn strip_brackets(input: &str) -> Result<(Cow<'_, str>, BracketVec<'_>), ParseMa } } +#[cfg(feature = "experimental_extras")] +/// Parses a list of optional dependencies from a string `feat1, feat2, feat3]` -> `vec![feat1, feat2, feat3]`. +pub fn parse_extras(input: &str) -> Result, ParseMatchSpecError> { + use nom::{ + combinator::{all_consuming, map}, + multi::separated_list1, + }; + + fn parse_feature_name(i: &str) -> IResult<&str, &str> { + delimited( + multispace0, + take_while1(|c: char| c.is_alphanumeric() || c == '_' || c == '-'), + multispace0, + )(i) + } + + fn parse_features(i: &str) -> IResult<&str, Vec> { + separated_list1(char(','), map(parse_feature_name, |s: &str| s.to_string()))(i) + } + + match all_consuming(parse_features)(input).finish() { + Ok((_remaining, features)) => Ok(features), + Err(_e) => Err(ParseMatchSpecError::InvalidBracket), + } +} + /// Parses a [`BracketVec`] into precise components fn parse_bracket_vec_into_components( bracket: BracketVec<'_>, @@ -248,6 +278,17 @@ fn parse_bracket_vec_into_components( "version" => match_spec.version = Some(VersionSpec::from_str(value, strictness)?), "build" => match_spec.build = Some(StringMatcher::from_str(value)?), "build_number" => match_spec.build_number = Some(BuildNumberSpec::from_str(value)?), + "extras" => { + // Optional features are still experimental + #[cfg(feature = "experimental_extras")] + { + match_spec.extras = Some(parse_extras(value)?); + } + #[cfg(not(feature = "experimental_extras"))] + { + return Err(ParseMatchSpecError::InvalidBracketKey("extras".to_string())); + } + } "sha256" => { match_spec.sha256 = Some( parse_digest_from_hex::(value) @@ -699,6 +740,9 @@ mod tests { NamelessMatchSpec, ParseChannelError, ParseStrictness, ParseStrictness::*, VersionSpec, }; + #[cfg(feature = "experimental_extras")] + use crate::match_spec::parse::parse_extras; + fn channel_config() -> ChannelConfig { ChannelConfig::default_with_root_dir( std::env::current_dir().expect("Could not get current directory"), @@ -1342,6 +1386,7 @@ mod tests { build: "py27_0*".parse().ok(), build_number: Some(BuildNumberSpec::from_str(">=6").unwrap()), file_name: Some("foo-1.0-py27_0.tar.bz2".to_string()), + extras: None, channel: Some( Channel::from_str("conda-forge", &channel_config()) .map(Arc::new) @@ -1375,4 +1420,70 @@ mod tests { .collect::>(); assert_eq!(specs, parsed_specs); } + + #[cfg(feature = "experimental_extras")] + #[test] + fn test_simple_extras() { + let spec = MatchSpec::from_str("foo[extras=[bar]]", Strict).unwrap(); + + assert_eq!(spec.extras, Some(vec!["bar".to_string()])); + assert!(MatchSpec::from_str("foo[extras=[bar,baz]", Strict).is_err()); + } + + #[cfg(feature = "experimental_extras")] + #[test] + fn test_multiple_extras() { + let spec = MatchSpec::from_str("foo[extras=[bar,baz]]", Strict).unwrap(); + assert_eq!( + spec.extras, + Some(vec!["bar".to_string(), "baz".to_string()]) + ); + } + + #[cfg(feature = "experimental_extras")] + #[test] + fn test_parse_extras() { + assert_eq!( + parse_extras("bar,baz").unwrap(), + vec!["bar".to_string(), "baz".to_string()] + ); + assert_eq!(parse_extras("bar").unwrap(), vec!["bar".to_string()]); + assert_eq!( + parse_extras("bar, baz").unwrap(), + vec!["bar".to_string(), "baz".to_string()] + ); + assert!(parse_extras("[bar,baz]").is_err()); + } + + #[cfg(feature = "experimental_extras")] + #[test] + fn test_invalid_extras() { + // Empty extras value + assert!(MatchSpec::from_str("foo[extras=]", Strict).is_err()); + + // Missing brackets around extras list + assert!(MatchSpec::from_str("foo[extras=bar,baz]", Strict).is_err()); + + // Trailing comma in extras list + assert!(MatchSpec::from_str("foo[extras=[bar,]]", Strict).is_err()); + + // Invalid characters in extras name + assert!(MatchSpec::from_str("foo[extras=[bar!,baz]]", Strict).is_err()); + + // Invalid characters in extras name + println!( + "{:?}", + MatchSpec::from_str("foo[extras=[bar!,baz]]", Strict) + ); + assert!(MatchSpec::from_str("foo[extras=[bar!,baz]]", Strict).is_err()); + + // Empty extras item + assert!(MatchSpec::from_str("foo[extras=[bar,,baz]]", Strict).is_err()); + + // Missing closing bracket + assert!(MatchSpec::from_str("foo[extras=[bar,baz", Strict).is_err()); + + // Missing opening bracket + assert!(MatchSpec::from_str("foo[extras=bar,baz]]", Strict).is_err()); + } } diff --git a/crates/rattler_conda_types/src/repo_data/mod.rs b/crates/rattler_conda_types/src/repo_data/mod.rs index 7291e5f5e..683b880e7 100644 --- a/crates/rattler_conda_types/src/repo_data/mod.rs +++ b/crates/rattler_conda_types/src/repo_data/mod.rs @@ -6,7 +6,7 @@ pub mod sharded; mod topological_sort; use std::{ - collections::BTreeSet, + collections::{BTreeMap, BTreeSet}, fmt::{Display, Formatter}, path::Path, }; @@ -114,6 +114,11 @@ pub struct PackageRecord { #[serde(default)] pub depends: Vec, + /// Specifications of optional or dependencies. These are dependencies that are + /// only required if certain features are enabled or if certain conditions are met. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub extra_depends: BTreeMap>, + /// Features are a deprecated way to specify different feature sets for the /// conda solver. This is not supported anymore and should not be used. /// Instead, `mutex` packages should be used to specify @@ -323,6 +328,7 @@ impl PackageRecord { noarch: NoArchType::default(), platform: None, python_site_packages_path: None, + extra_depends: BTreeMap::new(), sha256: None, size: None, subdir: Platform::current().to_string(), @@ -511,6 +517,7 @@ impl PackageRecord { noarch: index.noarch, platform: index.platform, python_site_packages_path: index.python_site_packages_path, + extra_depends: BTreeMap::new(), sha256, size, subdir, diff --git a/crates/rattler_conda_types/src/repo_data_record.rs b/crates/rattler_conda_types/src/repo_data_record.rs index 9d12937f5..82ef88010 100644 --- a/crates/rattler_conda_types/src/repo_data_record.rs +++ b/crates/rattler_conda_types/src/repo_data_record.rs @@ -1,6 +1,8 @@ //! Defines the `[RepoDataRecord]` struct. -use crate::PackageRecord; +use std::{collections::HashMap, vec::Vec}; + +use crate::{PackageName, PackageRecord}; use serde::{Deserialize, Serialize}; use url::Url; @@ -31,3 +33,12 @@ impl AsRef for RepoDataRecord { &self.package_record } } + +/// Struct for the solver result containing records and their features +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct SolverResult { + /// The records that are part of the solution to the solver task. + pub records: Vec, + /// The features of the records that are part of the solution to the solver task. + pub features: HashMap>, +} diff --git a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__libsqlite-3_40_0-hcfcfb64_0_json.snap b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__libsqlite-3_40_0-hcfcfb64_0_json.snap index 3d95b6ede..aa8fa7d65 100644 --- a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__libsqlite-3_40_0-hcfcfb64_0_json.snap +++ b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__libsqlite-3_40_0-hcfcfb64_0_json.snap @@ -55,4 +55,3 @@ link: source: "C:\\Users\\bas\\micromamba\\envs\\conda\\pkgs\\libsqlite-3.40.0-hcfcfb64_0" type: 1 requested_spec: "conda-forge/win-64::libsqlite==3.40.0=hcfcfb64_0[md5=5e5a97795de72f8cc3baf3d9ea6327a2]" - diff --git a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__menuinst-1_4_19-py311h1ea47a8_1_json.snap b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__menuinst-1_4_19-py311h1ea47a8_1_json.snap index daa89c8e6..a2c522b17 100644 --- a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__menuinst-1_4_19-py311h1ea47a8_1_json.snap +++ b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__menuinst-1_4_19-py311h1ea47a8_1_json.snap @@ -1,6 +1,5 @@ --- source: crates/rattler_conda_types/src/prefix_record.rs -assertion_line: 266 expression: prefix_record --- arch: x86_64 @@ -229,4 +228,3 @@ link: source: "C:\\Users\\bas\\micromamba\\pkgs\\menuinst-1.4.19-py311h1ea47a8_1" type: 1 requested_spec: "" - diff --git a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__pip-23_0-pyhd8ed1ab_0_json.snap b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__pip-23_0-pyhd8ed1ab_0_json.snap index e546761d0..3da10bf5d 100644 --- a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__pip-23_0-pyhd8ed1ab_0_json.snap +++ b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__pip-23_0-pyhd8ed1ab_0_json.snap @@ -4552,4 +4552,3 @@ link: source: "C:\\Users\\bas\\micromamba\\envs\\conda\\pkgs\\pip-23.0-pyhd8ed1ab_0" type: 1 requested_spec: "conda-forge/noarch::pip==23.0=pyhd8ed1ab_0[md5=85b35999162ec95f9f999bac15279c02]" - diff --git a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__pysocks-1_7_1-pyh0701188_6_json.snap b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__pysocks-1_7_1-pyh0701188_6_json.snap index 6c3a63de3..3deb88c87 100644 --- a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__pysocks-1_7_1-pyh0701188_6_json.snap +++ b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__pysocks-1_7_1-pyh0701188_6_json.snap @@ -1,6 +1,5 @@ --- source: crates/rattler_conda_types/src/prefix_record.rs -assertion_line: 266 expression: prefix_record --- build: pyh0701188_6 @@ -98,4 +97,3 @@ link: source: "C:\\Users\\bas\\micromamba\\pkgs\\pysocks-1.7.1-pyh0701188_6" type: 1 requested_spec: "" - diff --git a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__requests-2_28_2-pyhd8ed1ab_0_json.snap b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__requests-2_28_2-pyhd8ed1ab_0_json.snap index fecc9a8e7..5b6988c25 100644 --- a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__requests-2_28_2-pyhd8ed1ab_0_json.snap +++ b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__requests-2_28_2-pyhd8ed1ab_0_json.snap @@ -244,4 +244,3 @@ link: source: "C:\\Users\\bas\\micromamba\\pkgs\\requests-2.28.2-pyhd8ed1ab_0" type: 1 requested_spec: "" - diff --git a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__tk-8_6_12-h8ffe710_0_json.snap b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__tk-8_6_12-h8ffe710_0_json.snap index 782161e34..f336c9104 100644 --- a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__tk-8_6_12-h8ffe710_0_json.snap +++ b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__tk-8_6_12-h8ffe710_0_json.snap @@ -1,6 +1,5 @@ --- source: crates/rattler_conda_types/src/prefix_record.rs -assertion_line: 344 expression: prefix_record --- arch: x86_64 diff --git a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__urllib3-1_26_14-pyhd8ed1ab_0_json.snap b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__urllib3-1_26_14-pyhd8ed1ab_0_json.snap index e4f213e83..c8ad26016 100644 --- a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__urllib3-1_26_14-pyhd8ed1ab_0_json.snap +++ b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__urllib3-1_26_14-pyhd8ed1ab_0_json.snap @@ -421,4 +421,3 @@ link: source: "C:\\Users\\bas\\micromamba\\pkgs\\urllib3-1.26.14-pyhd8ed1ab_0" type: 1 requested_spec: "" - diff --git a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__vc-14_3-hb6edc58_10_json.snap b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__vc-14_3-hb6edc58_10_json.snap index 6f9fcc087..ebafbc65c 100644 --- a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__vc-14_3-hb6edc58_10_json.snap +++ b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__vc-14_3-hb6edc58_10_json.snap @@ -31,4 +31,3 @@ link: source: "C:\\Users\\bas\\micromamba\\envs\\conda\\pkgs\\vc-14.3-hb6edc58_10" type: 1 requested_spec: "conda-forge/win-64::vc==14.3=hb6edc58_10[md5=52d246d8d14b83c516229be5bb03a163]" - diff --git a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__wheel-0_38_4-pyhd8ed1ab_0_json.snap b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__wheel-0_38_4-pyhd8ed1ab_0_json.snap index 81a5e69ab..771009a6f 100644 --- a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__wheel-0_38_4-pyhd8ed1ab_0_json.snap +++ b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__wheel-0_38_4-pyhd8ed1ab_0_json.snap @@ -230,4 +230,3 @@ link: source: "C:\\Users\\bas\\micromamba\\envs\\conda\\pkgs\\wheel-0.38.4-pyhd8ed1ab_0" type: 1 requested_spec: "conda-forge/noarch::wheel==0.38.4=pyhd8ed1ab_0[md5=c829cfb8cb826acb9de0ac1a2df0a940]" - diff --git a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__xz-5_2_6-h8d14728_0_json.snap b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__xz-5_2_6-h8d14728_0_json.snap index d29d458d8..cd8aa4a25 100644 --- a/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__xz-5_2_6-h8d14728_0_json.snap +++ b/crates/rattler_conda_types/src/snapshots/rattler_conda_types__prefix_record__test__xz-5_2_6-h8d14728_0_json.snap @@ -157,4 +157,3 @@ link: source: "C:\\Users\\bas\\micromamba\\pkgs\\xz-5.2.6-h8d14728_0" type: 1 requested_spec: "" - diff --git a/crates/rattler_index/src/lib.rs b/crates/rattler_index/src/lib.rs index 5605b0a68..37bdac722 100644 --- a/crates/rattler_index/src/lib.rs +++ b/crates/rattler_index/src/lib.rs @@ -40,6 +40,7 @@ pub fn package_record_from_index_json( arch: index.arch, platform: index.platform, depends: index.depends, + extra_depends: std::collections::BTreeMap::new(), constrains: index.constrains, track_features: index.track_features, features: index.features, diff --git a/crates/rattler_lock/src/parse/models/v5/conda_package_data.rs b/crates/rattler_lock/src/parse/models/v5/conda_package_data.rs index 6b3f7b27d..f7a3c7bcf 100644 --- a/crates/rattler_lock/src/parse/models/v5/conda_package_data.rs +++ b/crates/rattler_lock/src/parse/models/v5/conda_package_data.rs @@ -117,6 +117,7 @@ impl<'a> From> for CondaPackageData { build_number: value.build_number, constrains: value.constrains.into_owned(), depends: value.depends.into_owned(), + extra_depends: std::collections::BTreeMap::new(), features: value.features.into_owned(), legacy_bz2_md5: value.legacy_bz2_md5, legacy_bz2_size: value.legacy_bz2_size.into_owned(), diff --git a/crates/rattler_lock/src/parse/models/v6/conda_package_data.rs b/crates/rattler_lock/src/parse/models/v6/conda_package_data.rs index 78a4d5f05..73d6eb2fd 100644 --- a/crates/rattler_lock/src/parse/models/v6/conda_package_data.rs +++ b/crates/rattler_lock/src/parse/models/v6/conda_package_data.rs @@ -1,4 +1,7 @@ -use std::{borrow::Cow, collections::BTreeSet}; +use std::{ + borrow::Cow, + collections::{BTreeMap, BTreeSet}, +}; use rattler_conda_types::{ package::ArchiveIdentifier, BuildNumber, ChannelUrl, NoArchType, PackageName, PackageRecord, @@ -73,6 +76,8 @@ pub(crate) struct CondaPackageDataModel<'a> { pub depends: Cow<'a, Vec>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub constrains: Cow<'a, Vec>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub extra_depends: Cow<'a, BTreeMap>>, // Additional properties (in semi alphabetic order but grouped by commonality) #[serde(default, skip_serializing_if = "Option::is_none")] @@ -157,6 +162,7 @@ impl<'a> TryFrom> for CondaPackageData { build_number, constrains: value.constrains.into_owned(), depends: value.depends.into_owned(), + extra_depends: value.extra_depends.into_owned(), features: value.features.into_owned(), legacy_bz2_md5: value.legacy_bz2_md5, legacy_bz2_size: value.legacy_bz2_size.into_owned(), @@ -273,6 +279,7 @@ impl<'a> From<&'a CondaPackageData> for CondaPackageDataModel<'a> { purls: Cow::Borrowed(&package_record.purls), depends: Cow::Borrowed(&package_record.depends), constrains: Cow::Borrowed(&package_record.constrains), + extra_depends: Cow::Borrowed(&package_record.extra_depends), arch: (package_record.arch != arch).then_some(Cow::Owned(arch)), platform: (package_record.platform != platform).then_some(Cow::Owned(platform)), md5: package_record.md5, diff --git a/crates/rattler_lock/src/parse/v3.rs b/crates/rattler_lock/src/parse/v3.rs index 2e05225bd..ab35768ab 100644 --- a/crates/rattler_lock/src/parse/v3.rs +++ b/crates/rattler_lock/src/parse/v3.rs @@ -202,6 +202,7 @@ pub fn parse_v3_or_lower( build_number, constrains: value.constrains, depends: value.dependencies, + extra_depends: std::collections::BTreeMap::new(), features: value.features, legacy_bz2_md5: None, legacy_bz2_size: None, diff --git a/crates/rattler_repodata_gateway/src/gateway/query.rs b/crates/rattler_repodata_gateway/src/gateway/query.rs index f759069ae..3b600bd5a 100644 --- a/crates/rattler_repodata_gateway/src/gateway/query.rs +++ b/crates/rattler_repodata_gateway/src/gateway/query.rs @@ -265,6 +265,15 @@ impl RepoDataQuery { pending_package_specs.insert(dependency_name.clone(), SourceSpecs::Transitive); } } + + for (_, dependencies) in record.package_record.extra_depends.iter() { + for dependency in dependencies { + let dependency_name = PackageName::new_unchecked(dependency.split_once(' ').unwrap_or((dependency, "")).0); + if seen.insert(dependency_name.clone()) { + pending_package_specs.insert(dependency_name.clone(), SourceSpecs::Transitive); + } + } + } } } diff --git a/crates/rattler_solve/Cargo.toml b/crates/rattler_solve/Cargo.toml index 65cc4c21a..8b0112794 100644 --- a/crates/rattler_solve/Cargo.toml +++ b/crates/rattler_solve/Cargo.toml @@ -24,6 +24,7 @@ rattler_libsolv_c = { path = "../rattler_libsolv_c", version = "1.1.1", default- resolvo = { workspace = true, optional = true } futures = { workspace = true, optional = true } serde = { workspace = true, optional = true } +indexmap = { workspace = true} [dev-dependencies] criterion = { workspace = true } @@ -43,6 +44,7 @@ url = { workspace = true } default = ["resolvo"] libsolv_c = ["rattler_libsolv_c", "libc"] resolvo = ["dep:resolvo", "dep:futures"] +experimental_extras = [] [[bench]] name = "bench" diff --git a/crates/rattler_solve/src/lib.rs b/crates/rattler_solve/src/lib.rs index ec21eb093..1f9510f50 100644 --- a/crates/rattler_solve/src/lib.rs +++ b/crates/rattler_solve/src/lib.rs @@ -12,7 +12,7 @@ pub mod resolvo; use std::fmt; use chrono::{DateTime, Utc}; -use rattler_conda_types::{GenericVirtualPackage, MatchSpec, RepoDataRecord}; +use rattler_conda_types::{GenericVirtualPackage, MatchSpec, RepoDataRecord, SolverResult}; /// Represents a solver implementation, capable of solving [`SolverTask`]s pub trait SolverImpl { @@ -28,7 +28,7 @@ pub trait SolverImpl { >( &mut self, task: SolverTask, - ) -> Result, SolveError>; + ) -> Result; } /// Represents an error when solving the dependencies for a given environment @@ -97,7 +97,7 @@ pub enum ChannelPriority { /// precedence. Disabled, } - +#[derive(Debug, Clone, PartialEq, Eq)] /// Represents a dependency resolution task, to be solved by one of the backends pub struct SolverTask { /// An iterator over all available packages diff --git a/crates/rattler_solve/src/libsolv_c/mod.rs b/crates/rattler_solve/src/libsolv_c/mod.rs index 997fc3faa..b643b2e0e 100644 --- a/crates/rattler_solve/src/libsolv_c/mod.rs +++ b/crates/rattler_solve/src/libsolv_c/mod.rs @@ -10,7 +10,7 @@ pub use input::cache_repodata; use input::{add_repodata_records, add_solv_file, add_virtual_packages}; pub use libc_byte_slice::LibcByteSlice; use output::get_required_packages; -use rattler_conda_types::{MatchSpec, NamelessMatchSpec, RepoDataRecord}; +use rattler_conda_types::{MatchSpec, NamelessMatchSpec, RepoDataRecord, SolverResult}; use wrapper::{ flags::SolverFlag, pool::{Pool, Verbosity}, @@ -94,7 +94,7 @@ impl super::SolverImpl for Solver { >( &mut self, task: SolverTask, - ) -> Result, SolveError> { + ) -> Result { if task.timeout.is_some() { return Err(SolveError::UnsupportedOperations(vec![ "timeout".to_string() @@ -107,6 +107,12 @@ impl super::SolverImpl for Solver { ])); } + if task.specs.iter().any(|spec| spec.extras.is_some()) { + return Err(SolveError::UnsupportedOperations( + vec!["extras".to_string()], + )); + } + // Construct a default libsolv pool let pool = Pool::default(); @@ -279,7 +285,10 @@ impl super::SolverImpl for Solver { ) })?; - Ok(required_records) + Ok(SolverResult { + records: required_records, + features: HashMap::new(), + }) } } diff --git a/crates/rattler_solve/src/resolvo/conda_sorting.rs b/crates/rattler_solve/src/resolvo/conda_sorting.rs index 0c68e87ea..d8da82ac1 100644 --- a/crates/rattler_solve/src/resolvo/conda_sorting.rs +++ b/crates/rattler_solve/src/resolvo/conda_sorting.rs @@ -10,7 +10,7 @@ use resolvo::{ utils::Pool, Dependencies, NameId, Requirement, SolvableId, SolverCache, VersionSetId, }; -use super::{SolverMatchSpec, SolverPackageRecord}; +use super::{NameType, SolverMatchSpec, SolverPackageRecord}; use crate::resolvo::CondaDependencyProvider; #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -52,7 +52,7 @@ impl<'a, 'repo> SolvableSorter<'a, 'repo> { } /// Referece to the pool - fn pool(&self) -> &Pool> { + fn pool(&self) -> &Pool, NameType> { &self.solver.provider().pool } diff --git a/crates/rattler_solve/src/resolvo/mod.rs b/crates/rattler_solve/src/resolvo/mod.rs index 413d599de..421975691 100644 --- a/crates/rattler_solve/src/resolvo/mod.rs +++ b/crates/rattler_solve/src/resolvo/mod.rs @@ -13,8 +13,9 @@ use chrono::{DateTime, Utc}; use conda_sorting::SolvableSorter; use itertools::Itertools; use rattler_conda_types::{ - package::ArchiveType, GenericVirtualPackage, MatchSpec, Matches, NamelessMatchSpec, - PackageName, PackageRecord, ParseMatchSpecError, ParseStrictness, RepoDataRecord, + package::ArchiveType, version_spec::EqualityOperator, BuildNumberSpec, GenericVirtualPackage, + MatchSpec, Matches, NamelessMatchSpec, OrdOperator, PackageName, PackageRecord, + ParseMatchSpecError, ParseStrictness, RepoDataRecord, SolverResult, StringMatcher, VersionSpec, }; use resolvo::{ utils::{Pool, VersionSet}, @@ -49,17 +50,35 @@ impl<'a> FromIterator<&'a RepoDataRecord> for RepoData<'a> { impl<'a> SolverRepoData<'a> for RepoData<'a> {} /// Wrapper around `MatchSpec` so that we can use it in the `resolvo` pool -#[repr(transparent)] #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct SolverMatchSpec<'a> { inner: NamelessMatchSpec, + feature: Option, _marker: PhantomData<&'a PackageRecord>, } +impl<'a> SolverMatchSpec<'a> { + /// Returns a reference to this match spec with the given feature enabled + pub fn with_feature(&self, feature: String) -> SolverMatchSpec<'a> { + Self { + inner: self.inner.clone(), + feature: Some(feature), + _marker: self._marker, + } + } + + /// Returns a mutable reference to this match spec after enabling the given feature + pub fn set_feature(&mut self, feature: String) -> &SolverMatchSpec<'a> { + self.feature = Some(feature); + self + } +} + impl<'a> From for SolverMatchSpec<'a> { fn from(value: NamelessMatchSpec) -> Self { Self { inner: value, + feature: None, _marker: PhantomData, } } @@ -89,6 +108,9 @@ pub enum SolverPackageRecord<'a> { /// Represents a record from the repodata Record(&'a RepoDataRecord), + /// Represents a record with a specific feature enabled + RecordWithFeature(&'a RepoDataRecord, String), + /// Represents a virtual package. VirtualPackage(&'a GenericVirtualPackage), } @@ -112,14 +134,18 @@ impl<'a> Ord for SolverPackageRecord<'a> { impl<'a> SolverPackageRecord<'a> { fn name(&self) -> &PackageName { match self { - SolverPackageRecord::Record(rec) => &rec.package_record.name, + SolverPackageRecord::Record(rec) | SolverPackageRecord::RecordWithFeature(rec, _) => { + &rec.package_record.name + } SolverPackageRecord::VirtualPackage(rec) => &rec.name, } } fn version(&self) -> &rattler_conda_types::Version { match self { - SolverPackageRecord::Record(rec) => rec.package_record.version.version(), + SolverPackageRecord::Record(rec) | SolverPackageRecord::RecordWithFeature(rec, _) => { + rec.package_record.version.version() + } SolverPackageRecord::VirtualPackage(rec) => &rec.version, } } @@ -127,21 +153,27 @@ impl<'a> SolverPackageRecord<'a> { fn track_features(&self) -> &[String] { const EMPTY: [String; 0] = []; match self { - SolverPackageRecord::Record(rec) => &rec.package_record.track_features, + SolverPackageRecord::Record(rec) | SolverPackageRecord::RecordWithFeature(rec, _) => { + &rec.package_record.track_features + } SolverPackageRecord::VirtualPackage(_rec) => &EMPTY, } } fn build_number(&self) -> u64 { match self { - SolverPackageRecord::Record(rec) => rec.package_record.build_number, + SolverPackageRecord::Record(rec) | SolverPackageRecord::RecordWithFeature(rec, _) => { + rec.package_record.build_number + } SolverPackageRecord::VirtualPackage(_rec) => 0, } } fn timestamp(&self) -> Option<&chrono::DateTime> { match self { - SolverPackageRecord::Record(rec) => rec.package_record.timestamp.as_ref(), + SolverPackageRecord::Record(rec) | SolverPackageRecord::RecordWithFeature(rec, _) => { + rec.package_record.timestamp.as_ref() + } SolverPackageRecord::VirtualPackage(_rec) => None, } } @@ -153,6 +185,9 @@ impl<'a> Display for SolverPackageRecord<'a> { SolverPackageRecord::Record(rec) => { write!(f, "{}", &rec.package_record) } + SolverPackageRecord::RecordWithFeature(rec, feature) => { + write!(f, "{}[{}]", &rec.package_record, feature) + } SolverPackageRecord::VirtualPackage(rec) => { write!(f, "{rec}") } @@ -160,20 +195,66 @@ impl<'a> Display for SolverPackageRecord<'a> { } } +/// Represents the type of name that is being used in the pool. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum NameType { + /// A package name without a feature. + Base(String), + + /// A package name with a feature. + BaseWithFeature(String, String), +} + +impl Display for NameType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + NameType::Base(name) => write!(f, "{name}"), + NameType::BaseWithFeature(name, feature) => write!(f, "{name}[{feature}]"), + } + } +} + +impl Ord for NameType { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match (self, other) { + // Compare names first + (NameType::Base(name1), NameType::Base(name2)) + | (NameType::BaseWithFeature(name1, _), NameType::BaseWithFeature(name2, _)) => { + name1.cmp(name2) + } + // WithoutFeature comes before WithFeature + (NameType::Base(_), NameType::BaseWithFeature(_, _)) => std::cmp::Ordering::Greater, + (NameType::BaseWithFeature(_, _), NameType::Base(_)) => std::cmp::Ordering::Less, + } + } +} + +impl PartialOrd for NameType { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl From<&str> for NameType { + fn from(value: &str) -> Self { + NameType::Base(value.to_owned()) + } +} + /// An implement of [`resolvo::DependencyProvider`] that implements the /// ecosystem behavior for conda. This allows resolvo to solve for conda /// packages. #[derive(Default)] pub struct CondaDependencyProvider<'a> { /// The pool that deduplicates data used by the provider. - pub pool: Pool, String>, + pub pool: Pool, NameType>, records: HashMap, matchspec_to_highest_version: RefCell>>, - parse_match_spec_cache: RefCell>, + parse_match_spec_cache: RefCell>>, stop_time: Option, @@ -297,94 +378,120 @@ impl<'a> CondaDependencyProvider<'a> { pool.intern_package_name(record.package_record.name.as_normalized()); let solvable_id = pool.intern_solvable(package_name, SolverPackageRecord::Record(record)); - let candidates = records.entry(package_name).or_default(); - candidates.candidates.push(solvable_id); - // Filter out any records that are newer than a specific date. - match (&exclude_newer, &record.package_record.timestamp) { - (Some(exclude_newer), Some(record_timestamp)) - if record_timestamp > exclude_newer => - { - let reason = pool.intern_string(format!( - "the package is uploaded after the cutoff date of {exclude_newer}" + // Collect all candidates first + let mut all_entries = vec![(package_name, solvable_id)]; + + // Add feature-enabled solvables + for feature in record.package_record.extra_depends.keys() { + let package_name_with_feature = + pool.intern_package_name(NameType::BaseWithFeature( + record.package_record.name.as_normalized().to_owned(), + feature.clone(), )); - candidates.excluded.push((solvable_id, reason)); - } - _ => {} + let feature_solvable = pool.intern_solvable( + package_name_with_feature, + SolverPackageRecord::RecordWithFeature(record, feature.clone()), + ); + let package_name_with_feature = + pool.intern_package_name(NameType::BaseWithFeature( + record.package_record.name.as_normalized().to_owned(), + feature.clone(), + )); + + all_entries.push((package_name_with_feature, feature_solvable)); } - // Add to excluded when package is not in the specified channel. - if !channel_specific_specs.is_empty() { - if let Some(spec) = channel_specific_specs.iter().find(|&&spec| { - spec.name - .as_ref() - .expect("expecting a name") - .as_normalized() - == record.package_record.name.as_normalized() - }) { - // Check if the spec has a channel, and compare it to the repodata channel - if let Some(spec_channel) = &spec.channel { - if record.channel.as_ref() != Some(&spec_channel.canonical_name()) { - tracing::debug!("Ignoring {} {} because it was not requested from that channel.", &record.package_record.name.as_normalized(), match &record.channel { - Some(channel) => format!("from {}", &channel), - None => "without a channel".to_string(), - }); - // Add record to the excluded with reason of being in the non - // requested channel. - let message = format!( - "candidate not in requested channel: '{}'", - spec_channel - .name - .clone() - .unwrap_or(spec_channel.base_url.to_string()) - ); - candidates - .excluded - .push((solvable_id, pool.intern_string(message))); - continue; + // Update records with all entries in a single mutable borrow + for (pkg_name, solvable) in all_entries { + let candidates = records.entry(pkg_name).or_default(); + candidates.candidates.push(solvable); + + // Filter out any records that are newer than a specific date. + match (&exclude_newer, &record.package_record.timestamp) { + (Some(exclude_newer), Some(record_timestamp)) + if record_timestamp > exclude_newer => + { + let reason = pool.intern_string(format!( + "the package is uploaded after the cutoff date of {exclude_newer}" + )); + candidates.excluded.push((solvable, reason)); + } + _ => {} + } + + // Add to excluded when package is not in the specified channel. + if !channel_specific_specs.is_empty() { + if let Some(spec) = channel_specific_specs.iter().find(|&&spec| { + spec.name + .as_ref() + .expect("expecting a name") + .as_normalized() + == record.package_record.name.as_normalized() + }) { + // Check if the spec has a channel, and compare it to the repodata channel + if let Some(spec_channel) = &spec.channel { + if record.channel.as_ref() != Some(&spec_channel.canonical_name()) { + tracing::debug!("Ignoring {} {} because it was not requested from that channel.", &record.package_record.name.as_normalized(), match &record.channel { + Some(channel) => format!("from {}", &channel), + None => "without a channel".to_string(), + }); + // Add record to the excluded with reason of being in the non + // requested channel. + let message = format!( + "candidate not in requested channel: '{}'", + spec_channel + .name + .clone() + .unwrap_or(spec_channel.base_url.to_string()) + ); + candidates + .excluded + .push((solvable, pool.intern_string(message))); + continue; + } } } } - } - // Enforce channel priority - // This function makes the assumption that the records are given in order of the - // channels. - if let (Some(first_channel), ChannelPriority::Strict) = ( - package_name_found_in_channel.get(record.package_record.name.as_normalized()), - channel_priority, - ) { - // Add the record to the excluded list when it is from a different channel. - if first_channel != &&record.channel { - if let Some(channel) = &record.channel { - tracing::debug!( - "Ignoring '{}' from '{}' because of strict channel priority.", - &record.package_record.name.as_normalized(), - channel - ); - candidates.excluded.push(( - solvable_id, - pool.intern_string(format!( - "due to strict channel priority not using this option from: '{channel}'", - )), - )); - } else { - tracing::debug!( - "Ignoring '{}' without a channel because of strict channel priority.", - &record.package_record.name.as_normalized(), - ); - candidates.excluded.push(( - solvable_id, - pool.intern_string("due to strict channel priority not using from an unknown channel".to_string()), - )); + // Enforce channel priority + if let (Some(first_channel), ChannelPriority::Strict) = ( + package_name_found_in_channel + .get(record.package_record.name.as_normalized()), + channel_priority, + ) { + // Add the record to the excluded list when it is from a different channel. + if first_channel != &&record.channel { + if let Some(channel) = &record.channel { + tracing::debug!( + "Ignoring '{}' from '{}' because of strict channel priority.", + &record.package_record.name.as_normalized(), + channel + ); + candidates.excluded.push(( + solvable, + pool.intern_string(format!( + "due to strict channel priority not using this option from: '{channel}'", + )), + )); + } else { + tracing::debug!( + "Ignoring '{}' without a channel because of strict channel priority.", + &record.package_record.name.as_normalized(), + ); + candidates.excluded.push(( + solvable, + pool.intern_string("due to strict channel priority not using from an unknown channel".to_string()), + )); + } + continue; } - continue; + } else { + package_name_found_in_channel.insert( + record.package_record.name.as_normalized().to_string(), + &record.channel, + ); } - } else { - package_name_found_in_channel.insert( - record.package_record.name.as_normalized().to_string(), - &record.channel, - ); } } } @@ -522,40 +629,104 @@ impl<'a> DependencyProvider for CondaDependencyProvider<'a> { async fn get_dependencies(&self, solvable: SolvableId) -> Dependencies { let mut dependencies = KnownDependencies::default(); - let SolverPackageRecord::Record(rec) = self.pool.resolve_solvable(solvable).record else { - return Dependencies::Known(dependencies); + + // Get the record and any feature that might be enabled + let (record, feature) = match &self.pool.resolve_solvable(solvable).record { + SolverPackageRecord::Record(rec) => (rec, None), + SolverPackageRecord::RecordWithFeature(rec, feature) => (rec, Some(feature)), + SolverPackageRecord::VirtualPackage(_) => return Dependencies::Known(dependencies), }; let mut parse_match_spec_cache = self.parse_match_spec_cache.borrow_mut(); - for depends in rec.package_record.depends.iter() { - let version_set_id = - match parse_match_spec(&self.pool, depends, &mut parse_match_spec_cache) { - Ok(version_set_id) => version_set_id, - Err(e) => { - let reason = self.pool.intern_string(format!( - "the dependency '{depends}' failed to parse: {e}", - )); - return Dependencies::Unknown(reason); - } + // If this is a feature-enabled package, add its feature dependencies + if let Some(feature_name) = feature { + // Find the feature's dependencies + if let Some(deps) = record.package_record.extra_depends.get(feature_name) { + // Add each dependency for this feature + for req in deps { + let version_set_id = match parse_match_spec( + &self.pool, + req, + &mut parse_match_spec_cache, + ) { + Ok(version_set_id) => version_set_id, + Err(e) => { + let reason = self.pool.intern_string(format!( + "the optional dependency '{req}' for feature '{feature_name}' failed to parse: {e}" + )); + return Dependencies::Unknown(reason); + } + }; + + dependencies + .requirements + .extend(version_set_id.into_iter().map(Requirement::from)); + } + + // Add a dependency back to the base package with exact version + let base_spec = MatchSpec { + name: Some(record.package_record.name.clone()), + version: Some(VersionSpec::Exact( + EqualityOperator::Equals, + record.package_record.version.version().clone(), + )), + build: Some(StringMatcher::Exact(record.package_record.build.clone())), + build_number: Some(BuildNumberSpec::new( + OrdOperator::Eq, + record.package_record.build_number, + )), + subdir: Some(record.package_record.subdir.clone()), + md5: record.package_record.md5, + sha256: record.package_record.sha256, + extras: None, + ..Default::default() }; - dependencies.requirements.push(version_set_id.into()); - } + let (name, nameless_spec) = base_spec.into_nameless(); + let name_id = self.pool.intern_package_name( + name.expect("cannot use matchspec without a name") + .as_normalized(), + ); + let version_set_id = self.pool.intern_version_set(name_id, nameless_spec.into()); + dependencies.requirements.push(version_set_id.into()); + } + } else { + // Add regular dependencies + for depends in record.package_record.depends.iter() { + let version_set_id = + match parse_match_spec(&self.pool, depends, &mut parse_match_spec_cache) { + Ok(version_set_id) => version_set_id, + Err(e) => { + let reason = self.pool.intern_string(format!( + "the dependency '{depends}' failed to parse: {e}", + )); - for constrains in rec.package_record.constrains.iter() { - let version_set_id = - match parse_match_spec(&self.pool, constrains, &mut parse_match_spec_cache) { - Ok(version_set_id) => version_set_id, - Err(e) => { - let reason = self.pool.intern_string(format!( - "the constrains '{constrains}' failed to parse: {e}", - )); + return Dependencies::Unknown(reason); + } + }; - return Dependencies::Unknown(reason); - } - }; - dependencies.constrains.push(version_set_id); + dependencies + .requirements + .extend(version_set_id.into_iter().map(Requirement::from)); + } + + for constrains in record.package_record.constrains.iter() { + let version_set_id = + match parse_match_spec(&self.pool, constrains, &mut parse_match_spec_cache) { + Ok(version_set_id) => version_set_id, + Err(e) => { + let reason = self.pool.intern_string(format!( + "the constrains '{constrains}' failed to parse: {e}", + )); + + return Dependencies::Unknown(reason); + } + }; + for version_set_id in version_set_id { + dependencies.constrains.push(version_set_id); + } + } } Dependencies::Known(dependencies) @@ -575,7 +746,24 @@ impl<'a> DependencyProvider for CondaDependencyProvider<'a> { .filter(|c| { let record = &self.pool.resolve_solvable(*c).record; match record { - SolverPackageRecord::Record(rec) => spec.matches(*rec) != inverse, + SolverPackageRecord::Record(rec) => { + // Base package matches if spec matches and no features are required + + spec.matches(*rec) != inverse + } + SolverPackageRecord::RecordWithFeature(rec, feature) => { + // Feature-enabled package matches if spec matches and feature is required + + if spec.matches(*rec) { + if let Some(spec_feature) = &spec.feature { + (*spec_feature == *feature) != inverse + } else { + inverse + } + } else { + inverse + } + } SolverPackageRecord::VirtualPackage(GenericVirtualPackage { version, build_string, @@ -625,7 +813,7 @@ impl super::SolverImpl for Solver { >( &mut self, task: SolverTask, - ) -> Result, SolveError> { + ) -> Result { let stop_time = task .timeout .map(|timeout| std::time::SystemTime::now() + timeout); @@ -651,16 +839,39 @@ impl super::SolverImpl for Solver { .intern_version_set(name_id, NamelessMatchSpec::default().into()) }); - let root_requirements = task.specs.iter().map(|spec| { + let root_requirements = task.specs.iter().flat_map(|spec| { let (name, nameless_spec) = spec.clone().into_nameless(); + let features = &spec.extras; let name = name.expect("cannot use matchspec without a name"); let name_id = provider.pool.intern_package_name(name.as_normalized()); - provider + let mut reqs = vec![provider .pool - .intern_version_set(name_id, nameless_spec.into()) + .intern_version_set(name_id, nameless_spec.clone().into())]; + + // Add requirements for optional features if specified + if let Some(features) = features { + for feature in features { + // Create a version set that matches the feature-enabled package + let package_name_with_feature = NameType::BaseWithFeature( + name.as_normalized().to_owned(), + feature.to_string(), + ); + let feature_name_id = + provider.pool.intern_package_name(package_name_with_feature); + + let mut solver_match_spec: SolverMatchSpec<'_> = nameless_spec.clone().into(); + let _ = solver_match_spec.set_feature(feature.to_string()); + + let feature_version_set = provider + .pool + .intern_version_set(feature_name_id, solver_match_spec); + reqs.push(feature_version_set); + } + } + reqs }); - let all_requirements = virtual_package_requirements + let all_requirements: Vec = virtual_package_requirements .chain(root_requirements) .map(Requirement::from) .collect(); @@ -677,7 +888,7 @@ impl super::SolverImpl for Solver { .collect(); let problem = Problem::new() - .requirements(all_requirements) + .requirements(all_requirements.clone()) .constraints(root_constraints); // Construct a solver and solve the problems in the queue @@ -694,37 +905,70 @@ impl super::SolverImpl for Solver { })?; // Get the resulting packages from the solver. - let required_records = solvables - .into_iter() - .filter_map( - |id| match solver.provider().pool.resolve_solvable(id).record { - SolverPackageRecord::Record(rec) => Some(rec.clone()), - SolverPackageRecord::VirtualPackage(_) => None, - }, - ) - .collect(); + let mut features: HashMap> = HashMap::new(); + let mut records = Vec::new(); + + for id in solvables { + match &solver.provider().pool.resolve_solvable(id).record { + SolverPackageRecord::Record(rec) => { + records.push((*rec).clone()); + } + SolverPackageRecord::RecordWithFeature(rec, feature) => { + features + .entry(rec.package_record.name.clone()) + .or_default() + .push(feature.clone()); + } + SolverPackageRecord::VirtualPackage(_) => {} + } + } - Ok(required_records) + Ok(SolverResult { records, features }) } } -fn parse_match_spec<'a>( - pool: &Pool>, - spec_str: &'a str, - parse_match_spec_cache: &mut HashMap<&'a str, VersionSetId>, -) -> Result { +fn parse_match_spec( + pool: &Pool, NameType>, + spec_str: &str, + parse_match_spec_cache: &mut HashMap>, +) -> Result, ParseMatchSpecError> { if let Some(spec_id) = parse_match_spec_cache.get(spec_str) { - Ok(*spec_id) + return Ok(spec_id.clone()); + } + + let match_spec = MatchSpec::from_str(spec_str, ParseStrictness::Lenient)?; + let (name, spec) = match_spec.into_nameless(); + + let mut version_set_ids = vec![]; + if let Some(ref features) = spec.extras { + let spec_with_feature: SolverMatchSpec<'_> = spec.clone().into(); + + for feature in features { + let name_with_feature = NameType::BaseWithFeature( + name.as_ref() + .expect("Packages with no name are not supported") + .as_normalized() + .to_owned(), + feature.to_string(), + ); + let dependency_name = pool.intern_package_name(name_with_feature); + + let version_set_id = pool.intern_version_set( + dependency_name, + spec_with_feature.with_feature(feature.to_string()), + ); + version_set_ids.push(version_set_id); + } } else { - let match_spec = MatchSpec::from_str(spec_str, ParseStrictness::Lenient)?; - let (name, spec) = match_spec.into_nameless(); let dependency_name = pool.intern_package_name( name.as_ref() - .expect("match specs without names are not supported") + .expect("Packages with no name are not supported") .as_normalized(), ); let version_set_id = pool.intern_version_set(dependency_name, spec.into()); - parse_match_spec_cache.insert(spec_str, version_set_id); - Ok(version_set_id) + version_set_ids.push(version_set_id); } + parse_match_spec_cache.insert(spec_str.to_string(), version_set_ids.clone()); + + Ok(version_set_ids) } diff --git a/crates/rattler_solve/tests/backends.rs b/crates/rattler_solve/tests/backends.rs index e64ce51cb..d453901fc 100644 --- a/crates/rattler_solve/tests/backends.rs +++ b/crates/rattler_solve/tests/backends.rs @@ -1,10 +1,10 @@ -use std::{str::FromStr, time::Instant}; +use std::{collections::BTreeMap, str::FromStr, time::Instant}; use chrono::{DateTime, Utc}; use once_cell::sync::Lazy; use rattler_conda_types::{ Channel, ChannelConfig, GenericVirtualPackage, MatchSpec, NoArchType, PackageRecord, - ParseStrictness, RepoData, RepoDataRecord, Version, + ParseStrictness, RepoData, RepoDataRecord, SolverResult, Version, }; use rattler_repodata_gateway::sparse::SparseRepoData; use rattler_solve::{ChannelPriority, SolveError, SolveStrategy, SolverImpl, SolverTask}; @@ -46,6 +46,15 @@ fn dummy_channel_json_path() -> String { ) } +#[cfg(feature = "experimental_extras")] +fn dummy_channel_with_optional_dependencies_json_path() -> String { + format!( + "{}/{}", + env!("CARGO_MANIFEST_DIR"), + "../../test-data/channels/dummy-optional-dependencies/noarch/repodata.json" + ) +} + fn dummy_md5_hash() -> rattler_digest::Md5Hash { rattler_digest::parse_digest_from_hex::("b3af409bb8423187c75e6c7f5b683908") .unwrap() @@ -96,6 +105,7 @@ fn installed_package( sha256: Some(dummy_sha256_hash()), size: None, arch: None, + extra_depends: BTreeMap::new(), platform: None, depends: Vec::new(), constrains: Vec::new(), @@ -132,7 +142,7 @@ fn solve_real_world(specs: Vec<&str>) -> Vec { }; let pkgs1 = match T::default().solve(solver_task) { - Ok(result) => result, + Ok(result) => result.records, Err(e) => panic!("{e}"), }; @@ -261,8 +271,8 @@ macro_rules! solver_backend_tests { ) .unwrap(); - assert_eq!(result.len(), 1); - assert_eq!(result[0].package_record.to_string(), "bors=1.0=bla_1"); + assert_eq!(result.records.len(), 1); + assert_eq!(result.records[0].package_record.to_string(), "bors=1.0=bla_1"); } #[test] @@ -326,9 +336,9 @@ macro_rules! solver_backend_tests { ) .unwrap(); - assert_eq!(pkgs.len(), 1); + assert_eq!(pkgs.records.len(), 1); - let info = &pkgs[0]; + let info = &pkgs.records[0]; assert_eq!("bar", info.package_record.name.as_normalized()); assert_eq!("1.2.3", &info.package_record.version.to_string()); } @@ -344,8 +354,8 @@ macro_rules! solver_backend_tests { ) .unwrap(); - assert_eq!(1, pkgs.len()); - let info = &pkgs[0]; + assert_eq!(1, pkgs.records.len()); + let info = &pkgs.records[0]; assert_eq!("foo-3.0.2-py36h1af98f8_3.conda", info.file_name); assert_eq!( @@ -391,8 +401,8 @@ macro_rules! solver_backend_tests { .unwrap(); // The .conda entry is selected for installing - assert_eq!(operations.len(), 1); - assert_eq!(operations[0].file_name, "foo-3.0.2-py36h1af98f8_1.conda"); + assert_eq!(operations.records.len(), 1); + assert_eq!(operations.records[0].file_name, "foo-3.0.2-py36h1af98f8_1.conda"); } #[test] @@ -416,10 +426,10 @@ macro_rules! solver_backend_tests { ) .unwrap(); - assert_eq!(1, pkgs.len()); + assert_eq!(1, pkgs.records.len()); // Install - let info = &pkgs[0]; + let info = &pkgs.records[0]; assert_eq!("foo", info.package_record.name.as_normalized()); assert_eq!("3.0.2", &info.package_record.version.to_string()); } @@ -446,7 +456,7 @@ macro_rules! solver_backend_tests { .unwrap(); // Install - let info = &pkgs[0]; + let info = &pkgs.records[0]; assert_eq!("foo", info.package_record.name.as_normalized()); assert_eq!("4.0.2", &info.package_record.version.to_string()); } @@ -472,10 +482,10 @@ macro_rules! solver_backend_tests { ) .unwrap(); - assert_eq!(pkgs.len(), 1); + assert_eq!(pkgs.records.len(), 1); // Uninstall - let info = &pkgs[0]; + let info = &pkgs.records[0]; assert_eq!("foo", info.package_record.name.as_normalized()); assert_eq!("3.0.2", &info.package_record.version.to_string()); } @@ -501,7 +511,7 @@ macro_rules! solver_backend_tests { .unwrap(); // Should be no packages! - assert_eq!(0, pkgs.len()); + assert_eq!(0, pkgs.records.len()); } #[test] @@ -518,9 +528,9 @@ macro_rules! solver_backend_tests { ) .unwrap(); - assert_eq!(1, pkgs.len()); + assert_eq!(1, pkgs.records.len()); - let info = &pkgs[0]; + let info = &pkgs.records[0]; assert_eq!("foo", info.package_record.name.as_normalized()); assert_eq!("3.0.2", &info.package_record.version.to_string(), "although there is a newer version available we expect an older version of foo because we exclude the newer version based on the timestamp"); @@ -557,11 +567,11 @@ macro_rules! solver_backend_tests { .unwrap(); // Sort operations by file name to make the test deterministic - operations.sort_by(|a, b| a.file_name.cmp(&b.file_name)); + operations.records.sort_by(|a, b| a.file_name.cmp(&b.file_name)); - assert_eq!(operations.len(), 2); - assert_eq!(operations[0].file_name, "bors-1.0-bla_1.tar.bz2"); - assert_eq!(operations[1].file_name, "foobar-2.1-bla_1.tar.bz2"); + assert_eq!(operations.records.len(), 2); + assert_eq!(operations.records[0].file_name, "bors-1.0-bla_1.tar.bz2"); + assert_eq!(operations.records[1].file_name, "foobar-2.1-bla_1.tar.bz2"); } #[test] @@ -583,6 +593,7 @@ macro_rules! solver_backend_tests { let output = match result { Ok(pkgs) => pkgs + .records .iter() .format_with("\n", |pkg, f| { f(&format_args!( @@ -617,7 +628,7 @@ mod libsolv_c { #[test] #[cfg(target_family = "unix")] fn test_solve_with_cached_solv_file_install_new() { - use rattler_conda_types::{Channel, ChannelConfig, MatchSpec}; + use rattler_conda_types::{Channel, ChannelConfig, MatchSpec, RepoDataRecord}; use rattler_solve::{SolverImpl, SolverTask}; use super::read_repodata; @@ -644,7 +655,7 @@ mod libsolv_c { let specs: Vec = vec!["foo<4".parse().unwrap()]; - let pkgs = rattler_solve::libsolv_c::Solver + let pkgs: Vec = rattler_solve::libsolv_c::Solver .solve(SolverTask { locked_packages: Vec::new(), virtual_packages: Vec::new(), @@ -657,7 +668,8 @@ mod libsolv_c { exclude_newer: None, strategy: SolveStrategy::default(), }) - .unwrap(); + .unwrap() + .records; if pkgs.is_empty() { println!("No packages in the environment!"); @@ -712,6 +724,9 @@ mod resolvo { GenericVirtualPackage, SimpleSolveTask, SolveError, Version, }; + #[cfg(feature = "experimental_extras")] + use super::dummy_channel_with_optional_dependencies_json_path; + solver_backend_tests!(rattler_solve::resolvo::Solver); #[test] @@ -779,13 +794,13 @@ mod resolvo { ) .unwrap(); - assert_eq!(result.len(), 1); + assert_eq!(result.records.len(), 1); assert_eq!( - result[0].package_record.version, + result.records[0].package_record.version, Version::from_str("3.0.2").unwrap() ); assert_eq!( - result[0].package_record.build_number, 3, + result.records[0].package_record.build_number, 3, "expected the highest build number" ); } @@ -802,17 +817,23 @@ mod resolvo { ) .unwrap(); - assert_eq!(result.len(), 2); - assert_eq!(result[0].package_record.name.as_normalized(), "foobar"); + assert_eq!(result.records.len(), 2); + assert_eq!( + result.records[0].package_record.name.as_normalized(), + "foobar" + ); assert_eq!( - result[0].package_record.version, + result.records[0].package_record.version, Version::from_str("2.0").unwrap(), "expected lowest version of foobar" ); - assert_eq!(result[1].package_record.name.as_normalized(), "bors"); assert_eq!( - result[1].package_record.version, + result.records[1].package_record.name.as_normalized(), + "bors" + ); + assert_eq!( + result.records[1].package_record.version, Version::from_str("1.0").unwrap(), "expected lowest version of bors" ); @@ -830,17 +851,23 @@ mod resolvo { ) .unwrap(); - assert_eq!(result.len(), 2); - assert_eq!(result[0].package_record.name.as_normalized(), "foobar"); + assert_eq!(result.records.len(), 2); assert_eq!( - result[0].package_record.version, + result.records[0].package_record.name.as_normalized(), + "foobar" + ); + assert_eq!( + result.records[0].package_record.version, Version::from_str("2.0").unwrap(), "expected lowest version of foobar" ); - assert_eq!(result[1].package_record.name.as_normalized(), "bors"); assert_eq!( - result[1].package_record.version, + result.records[1].package_record.name.as_normalized(), + "bors" + ); + assert_eq!( + result.records[1].package_record.version, Version::from_str("1.2.1").unwrap(), "expected highest compatible version of bors" ); @@ -885,7 +912,7 @@ mod resolvo { ..SolverTask::from_iter([&repo_data]) }; - let pkgs = rattler_solve::resolvo::Solver.solve(task).unwrap(); + let pkgs: Vec = rattler_solve::resolvo::Solver.solve(task).unwrap().records; assert_eq!(pkgs.len(), 1); assert_eq!(pkgs[0].package_record.name.as_normalized(), "_libgcc_mutex"); @@ -942,6 +969,362 @@ mod resolvo { }, ); + insta::assert_snapshot!(result.unwrap_err()); + } + #[cfg(feature = "experimental_extras")] + /// Installs `foo` while enabling a single optional dependency `[with-latest-bors]`. + /// This should pull in `bors >=2.0`. + #[test] + fn test_solve_dummy_repo_extra_depends_foo_latest_bors_resolvo() { + let mut result = solve::( + &[dummy_channel_with_optional_dependencies_json_path()], + SimpleSolveTask { + specs: &["foo[extras=[with-latest-bors]]"], + ..SimpleSolveTask::default() + }, + ) + .unwrap(); + + result + .records + .sort_by(|a, b| a.package_record.name.cmp(&b.package_record.name)); + + assert_eq!(result.records.len(), 2); + assert_eq!(result.records[1].package_record.name.as_normalized(), "foo"); + assert_eq!( + result.features.get("foo"), + Some(&vec!["with-latest-bors".to_string()]) + ); + assert_eq!( + result.records[1].package_record.version, + Version::from_str("2.0.2").unwrap(), + "expected lowest version of foobar" + ); + + assert_eq!( + result.records[0].package_record.name.as_normalized(), + "bors" + ); + assert_eq!( + result.records[0].package_record.version, + Version::from_str("2.1").unwrap(), + "expected highest compatible version of bors" + ); + } + + #[cfg(feature = "experimental_extras")] + /// Installs `cuda-version` with `[with-cudadev]` which depends on `"foo >=4.0.2", "bar >=1.2.3"`. + #[test] + fn test_solve_dummy_repo_extra_depends_cuda_dev_resolvo() { + let mut result = solve::( + &[dummy_channel_with_optional_dependencies_json_path()], + SimpleSolveTask { + specs: &["cuda-version[extras=[with-cudadev]]"], + ..SimpleSolveTask::default() + }, + ) + .unwrap(); + + result + .records + .sort_by(|a, b| a.package_record.name.cmp(&b.package_record.name)); + + assert_eq!(result.records.len(), 3); + assert_eq!(result.records[0].package_record.name.as_normalized(), "bar"); + + assert_eq!( + result.records[0].package_record.version, + Version::from_str("1.2.3").unwrap(), + "expected version 1.2.3 of bar" + ); + + // The cuda-version with feature `with-cudadev`: + assert_eq!( + result.records[1].package_record.name.as_normalized(), + "cuda-version" + ); + assert_eq!( + result.records[1].package_record.version, + Version::from_str("12.5").unwrap(), + "expected version 12.5 of cuda-version" + ); + + assert_eq!( + result.features.get("cuda-version"), + Some(&vec!["with-cudadev".to_string()]) + ); + + assert_eq!(result.records[2].package_record.name.as_normalized(), "foo"); + assert_eq!( + result.records[2].package_record.version, + Version::from_str("4.0.2").unwrap(), + "expected version 4.0.2 of foo" + ); + } + + #[cfg(feature = "experimental_extras")] + /// Attempts to enable two optional features that conflict: `[with-oldbors,with-latest-bors]`. + /// This should fail because one requests `bors <2.0` and the other requests `bors >=2.0`. + #[test] + fn test_solve_dummy_repo_extra_depends_conflict_resolvo() { + let result = solve::( + &[dummy_channel_with_optional_dependencies_json_path()], + SimpleSolveTask { + specs: &["foo[extras=[with-oldbors,with-latest-bors]]"], + ..SimpleSolveTask::default() + }, + ); + + insta::assert_snapshot!(result.unwrap_err()); + } + + #[cfg(feature = "experimental_extras")] + /// Enables multiple optional dependencies in the same spec (like `[with-baz2,with-bar]`). + /// This should pull in `baz >=2.0` and `bar >=1.2.3` if both can coexist. + #[test] + fn test_solve_dummy_repo_extra_depends_foo_multi_resolvo() { + let mut result = solve::( + &[dummy_channel_with_optional_dependencies_json_path()], + SimpleSolveTask { + specs: &["foo[extras=[with-baz2,with-bar]]"], + ..SimpleSolveTask::default() + }, + ) + .unwrap(); + + result + .records + .sort_by(|a, b| a.package_record.name.cmp(&b.package_record.name)); + + assert_eq!(result.records.len(), 3); + + assert_eq!(result.records[0].package_record.name.as_normalized(), "bar"); + assert_eq!( + result.records[0].package_record.version, + Version::from_str("1.2.3").unwrap(), + "expected version 1.2.3 of bar" + ); + + assert_eq!(result.records[1].package_record.name.as_normalized(), "baz"); + assert_eq!( + result.records[1].package_record.version, + Version::from_str("2.0").unwrap(), + "expected version 2.0 of baz" + ); + + assert_eq!(result.records[2].package_record.name.as_normalized(), "foo"); + assert_eq!( + result.records[2].package_record.version, + Version::from_str("3.0.2").unwrap(), + "expected version 3.0.2 of foo" + ); + let mut features = result.features.get("foo").unwrap().clone(); + features.sort(); + result.features.insert("foo".parse().unwrap(), features); + assert_eq!( + result.features.get("foo"), + Some(&vec!["with-bar".to_string(), "with-baz2".to_string()]) + ); + } + + #[cfg(feature = "experimental_extras")] + /// Should install xfoo with the feature with-issue717 which requires `with-issue717[with-bors21]` hence pulling in bors 2.1 as well + #[test] + fn test_solve_dummy_repo_extra_depends_xfoo_extra_depends_with_features() { + let mut result = solve::( + &[dummy_channel_with_optional_dependencies_json_path()], + SimpleSolveTask { + specs: &["xfoo[extras=[with-issue717]]"], + ..SimpleSolveTask::default() + }, + ) + .unwrap(); + + result + .records + .sort_by(|a, b| a.package_record.name.cmp(&b.package_record.name)); + + assert_eq!(result.records.len(), 3); + assert_eq!( + result.records[0].package_record.name.as_normalized(), + "bors" + ); + assert_eq!( + result.records[0].package_record.version, + Version::from_str("2.1").unwrap(), + "expected version 2.1 of bors" + ); + + assert_eq!( + result.records[1].package_record.name.as_normalized(), + "issue_717" + ); + assert_eq!( + result.records[1].package_record.version, + Version::from_str("2.1").unwrap(), + "expected version 2.1 of issue_717" + ); + + assert_eq!( + result.records[2].package_record.name.as_normalized(), + "xfoo" + ); + assert_eq!( + result.records[2].package_record.version, + Version::from_str("2.0").unwrap(), + "expected version 2.0 of xfoo" + ); + assert_eq!( + result.features.get("xfoo"), + Some(&vec!["with-issue717".to_string()]) + ); + } + + #[cfg(feature = "experimental_extras")] + /// Tests what happens when a feature depends on the base package but with another feature enabled + #[test] + fn test_solve_dummy_repo_extra_depends_recursive_feature() { + let result = solve::( + &[dummy_channel_with_optional_dependencies_json_path()], + SimpleSolveTask { + specs: &["foo[extras=[with-recursive]]"], + ..SimpleSolveTask::default() + }, + ); + + // Sort records by name for stable test results + insta::assert_snapshot!(result.unwrap_err()); + } + + #[cfg(feature = "experimental_extras")] + /// Tests that an optional dependency can restrict the highest version of a base dependency + #[test] + fn test_solve_dummy_repo_extra_depends_version_restriction() { + let result = solve::( + &[dummy_channel_with_optional_dependencies_json_path()], + SimpleSolveTask { + specs: &["foo[extras=[with-version-restrict]]"], + ..SimpleSolveTask::default() + }, + ) + .unwrap(); + + assert_eq!(result.records.len(), 1); + // Both records should be foo + assert_eq!(result.records[0].package_record.name.as_normalized(), "foo"); + assert_eq!( + result.records[0].package_record.version, + Version::from_str("3.0.2").unwrap(), + "expected version 3.0.2 of foo due to version restriction from feature" + ); + assert_eq!( + result.features.get("foo"), + Some(&vec!["with-version-restrict".to_string()]) + ); + } + + #[cfg(feature = "experimental_extras")] + /// Tests what happens if a feature introduces a dependency on the base package itself + #[test] + fn test_solve_dummy_repo_extra_depends_self_dependency() { + let mut result = solve::( + &[dummy_channel_with_optional_dependencies_json_path()], + SimpleSolveTask { + specs: &["foo[extras=[with-self]]"], + ..SimpleSolveTask::default() + }, + ) + .unwrap(); + + // Sort records by name for stable test results + result + .records + .sort_by(|a, b| a.package_record.name.cmp(&b.package_record.name)); + + assert_eq!(result.records.len(), 1); + // Both records should be foo + assert_eq!(result.records[0].package_record.name.as_normalized(), "foo"); + assert_eq!( + result.records[0].package_record.version, + Version::from_str("2.0.2").unwrap(), + "expected version 2.0.2 of foo" + ); + assert_eq!( + result.features.get("foo"), + Some(&vec!["with-self".to_string()]) + ); + } + + #[cfg(feature = "experimental_extras")] + /// Tests what happens if there are two packages for foo but only the package with the lower version has the package that is requested + #[test] + fn test_solve_dummy_repo_extra_depends_feature_only_in_older() { + let mut result = solve::( + &[dummy_channel_with_optional_dependencies_json_path()], + SimpleSolveTask { + specs: &["foo[extras=[legacy-only]]"], + ..SimpleSolveTask::default() + }, + ) + .unwrap(); + + // Sort records by name for stable test results + result + .records + .sort_by(|a, b| a.package_record.name.cmp(&b.package_record.name)); + + assert_eq!(result.records.len(), 2); + + // Both records should be foo + assert_eq!(result.records[1].package_record.name.as_normalized(), "foo"); + assert_eq!( + result.records[1].package_record.version, + Version::from_str("2.0.2").unwrap(), + "expected older version 2.0.2 of foo since it has the required feature" + ); + assert_eq!( + result.features.get("foo"), + Some(&vec!["legacy-only".to_string()]) + ); + + assert_eq!(result.records[0].package_record.name.as_normalized(), "bar"); + assert_eq!( + result.records[0].package_record.version, + Version::from_str("1.2.3").unwrap(), + "expected version 1.2.3 of bar" + ); + } + + #[cfg(feature = "experimental_extras")] + /// Test what happens if a feature is requested that doesn't exist + #[test] + fn test_solve_dummy_repo_extra_depends_nonexistent_feature() { + let result = solve::( + &[dummy_channel_with_optional_dependencies_json_path()], + SimpleSolveTask { + specs: &["foo[extras=[does-not-exist]]"], + ..SimpleSolveTask::default() + }, + ); + + assert_eq!( + result.unwrap_err().to_string(), + "Cannot solve the request because of: No candidates were found for foo[does-not-exist] *.\n" + ); + } + + #[cfg(feature = "experimental_extras")] + /// Test what happens when the only package that provides a certain feature cannot be selected due to a conflict + #[test] + fn test_solve_dummy_repo_extra_depends_feature_conflict() { + let result = solve::( + &[dummy_channel_with_optional_dependencies_json_path()], + SimpleSolveTask { + specs: &["foo[extras=[with-bar]]", "foo>=4.0"], + ..SimpleSolveTask::default() + }, + ); + insta::assert_snapshot!(result.unwrap_err()); } } @@ -960,7 +1343,7 @@ struct SimpleSolveTask<'a> { fn solve( repo_path: &[String], task: SimpleSolveTask<'_>, -) -> Result, SolveError> { +) -> Result { let repo_data = repo_path .iter() .map(|path| read_repodata(path)) @@ -991,7 +1374,7 @@ fn solve( let pkgs = T::default().solve(task)?; - if pkgs.is_empty() { + if pkgs.records.is_empty() { println!("No packages in the environment!"); } @@ -1050,7 +1433,8 @@ fn compare_solve(task: CompareTask<'_>) { exclude_newer: task.exclude_newer, ..SolverTask::from_iter(&available_packages) }) - .unwrap(), + .unwrap() + .records, ), )); let end_solve = Instant::now(); @@ -1069,7 +1453,8 @@ fn compare_solve(task: CompareTask<'_>) { exclude_newer: task.exclude_newer, ..SolverTask::from_iter(&available_packages) }) - .unwrap(), + .unwrap() + .records, ), )); let end_solve = Instant::now(); @@ -1153,7 +1538,7 @@ fn solve_to_get_channel_of_spec( ..SolverTask::from_iter(&available_packages) }; - let result = T::default().solve(task).unwrap(); + let result: Vec = T::default().solve(task).unwrap().records; let record = result.iter().find(|record| { record.package_record.name.as_normalized() == spec.name.as_ref().unwrap().as_normalized() diff --git a/crates/rattler_solve/tests/snapshots/backends__resolvo__solve_dummy_repo_extra_depends_conflict_resolvo.snap b/crates/rattler_solve/tests/snapshots/backends__resolvo__solve_dummy_repo_extra_depends_conflict_resolvo.snap new file mode 100644 index 000000000..5513f5086 --- /dev/null +++ b/crates/rattler_solve/tests/snapshots/backends__resolvo__solve_dummy_repo_extra_depends_conflict_resolvo.snap @@ -0,0 +1,16 @@ +--- +source: crates/rattler_solve/tests/backends.rs +expression: result.unwrap_err() +--- +Cannot solve the request because of: The following packages are incompatible +├─ foo[with-oldbors] * cannot be installed because there are no viable options: +│ └─ foo[with-oldbors] 2.0.2 would require +│ └─ bors <2.0, which cannot be installed because there are no viable options: +│ ├─ bors 1.2.1, which conflicts with the versions reported above. +│ ├─ bors 1.1, which conflicts with the versions reported above. +│ └─ bors 1.0, which conflicts with the versions reported above. +└─ foo[with-latest-bors] * cannot be installed because there are no viable options: + └─ foo[with-latest-bors] 2.0.2 would require + └─ bors >=2.0, which cannot be installed because there are no viable options: + ├─ bors 2.1, which conflicts with the versions reported above. + └─ bors 2.0, which conflicts with the versions reported above. diff --git a/crates/rattler_solve/tests/snapshots/backends__resolvo__solve_dummy_repo_extra_depends_feature_conflict.snap b/crates/rattler_solve/tests/snapshots/backends__resolvo__solve_dummy_repo_extra_depends_feature_conflict.snap new file mode 100644 index 000000000..2e6a3aa0b --- /dev/null +++ b/crates/rattler_solve/tests/snapshots/backends__resolvo__solve_dummy_repo_extra_depends_feature_conflict.snap @@ -0,0 +1,26 @@ +--- +source: crates/rattler_solve/tests/backends.rs +expression: result.unwrap_err() +--- +Cannot solve the request because of: The following packages are incompatible +├─ foo[with-bar] * cannot be installed because there are no viable options: +│ ├─ foo[with-bar] 3.5.0 would require +│ │ └─ foo ==3.5.0 py36h1af98f8_1[md5=fb731d9290f0bcbf3a054665f33ec94f, sha256=67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4], which cannot be installed because there are no viable options: +│ │ └─ foo 3.5.0, which conflicts with the versions reported above. +│ ├─ foo[with-bar] 3.4.0 would require +│ │ └─ foo ==3.4.0 py36h1af98f8_1[md5=fb731d9290f0bcbf3a054665f33ec94f, sha256=67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4], which cannot be installed because there are no viable options: +│ │ └─ foo 3.4.0, which conflicts with the versions reported above. +│ ├─ foo[with-bar] 3.3.0 would require +│ │ └─ foo ==3.3.0 py36h1af98f8_1[md5=fb731d9290f0bcbf3a054665f33ec94f, sha256=67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4], which cannot be installed because there are no viable options: +│ │ └─ foo 3.3.0, which conflicts with the versions reported above. +│ ├─ foo[with-bar] 3.2.0 would require +│ │ └─ foo ==3.2.0 py36h1af98f8_1[md5=fb731d9290f0bcbf3a054665f33ec94f, sha256=67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4], which cannot be installed because there are no viable options: +│ │ └─ foo 3.2.0, which conflicts with the versions reported above. +│ ├─ foo[with-bar] 3.1.0 would require +│ │ └─ foo ==3.1.0 py36h1af98f8_1[md5=fb731d9290f0bcbf3a054665f33ec94f, sha256=67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4], which cannot be installed because there are no viable options: +│ │ └─ foo 3.1.0, which conflicts with the versions reported above. +│ └─ foo[with-bar] 3.0.2 would require +│ └─ foo ==3.0.2 py36h1af98f8_1[md5=fb731d9290f0bcbf3a054665f33ec94f, sha256=67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4], which cannot be installed because there are no viable options: +│ └─ foo 3.0.2, which conflicts with the versions reported above. +└─ foo >=4.0 cannot be installed because there are no viable options: + └─ foo 4.0.2, which conflicts with the versions reported above. diff --git a/crates/rattler_solve/tests/snapshots/backends__resolvo__solve_dummy_repo_extra_depends_recursive_feature.snap b/crates/rattler_solve/tests/snapshots/backends__resolvo__solve_dummy_repo_extra_depends_recursive_feature.snap new file mode 100644 index 000000000..125b88902 --- /dev/null +++ b/crates/rattler_solve/tests/snapshots/backends__resolvo__solve_dummy_repo_extra_depends_recursive_feature.snap @@ -0,0 +1,28 @@ +--- +source: crates/rattler_solve/tests/backends.rs +expression: result.unwrap_err() +--- +Cannot solve the request because of: The following packages are incompatible +└─ foo[with-recursive] * cannot be installed because there are no viable options: + └─ foo[with-recursive] 2.0.2 would require + ├─ foo[with-bar] *, which cannot be installed because there are no viable options: + │ ├─ foo[with-bar] 3.5.0 would require + │ │ └─ foo ==3.5.0 py36h1af98f8_1[md5=fb731d9290f0bcbf3a054665f33ec94f, sha256=67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4], which cannot be installed because there are no viable options: + │ │ └─ foo 3.5.0, which conflicts with the versions reported above. + │ ├─ foo[with-bar] 3.4.0 would require + │ │ └─ foo ==3.4.0 py36h1af98f8_1[md5=fb731d9290f0bcbf3a054665f33ec94f, sha256=67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4], which cannot be installed because there are no viable options: + │ │ └─ foo 3.4.0, which conflicts with the versions reported above. + │ ├─ foo[with-bar] 3.3.0 would require + │ │ └─ foo ==3.3.0 py36h1af98f8_1[md5=fb731d9290f0bcbf3a054665f33ec94f, sha256=67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4], which cannot be installed because there are no viable options: + │ │ └─ foo 3.3.0, which conflicts with the versions reported above. + │ ├─ foo[with-bar] 3.2.0 would require + │ │ └─ foo ==3.2.0 py36h1af98f8_1[md5=fb731d9290f0bcbf3a054665f33ec94f, sha256=67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4], which cannot be installed because there are no viable options: + │ │ └─ foo 3.2.0, which conflicts with the versions reported above. + │ ├─ foo[with-bar] 3.1.0 would require + │ │ └─ foo ==3.1.0 py36h1af98f8_1[md5=fb731d9290f0bcbf3a054665f33ec94f, sha256=67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4], which cannot be installed because there are no viable options: + │ │ └─ foo 3.1.0, which conflicts with the versions reported above. + │ └─ foo[with-bar] 3.0.2 would require + │ └─ foo ==3.0.2 py36h1af98f8_1[md5=fb731d9290f0bcbf3a054665f33ec94f, sha256=67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4], which cannot be installed because there are no viable options: + │ └─ foo 3.0.2, which conflicts with the versions reported above. + └─ foo ==2.0.2 py36h1af98f8_1[md5=d65ab674acf3b7294ebacaec05fc5b54, sha256=1154fceeb5c4ee9bb97d245713ac21eb1910237c724d2b7103747215663273c2], which cannot be installed because there are no viable options: + └─ foo 2.0.2, which conflicts with the versions reported above. diff --git a/crates/rattler_solve/tests/snapshots/backends__resolvo__solve_dummy_repo_install_non_existent.snap b/crates/rattler_solve/tests/snapshots/backends__resolvo__solve_dummy_repo_install_non_existent.snap index 05b2f9a23..fd9c3542a 100644 --- a/crates/rattler_solve/tests/snapshots/backends__resolvo__solve_dummy_repo_install_non_existent.snap +++ b/crates/rattler_solve/tests/snapshots/backends__resolvo__solve_dummy_repo_install_non_existent.snap @@ -1,6 +1,5 @@ --- source: crates/rattler_solve/tests/backends.rs -assertion_line: 530 expression: err --- Unsolvable( diff --git a/py-rattler/Cargo.lock b/py-rattler/Cargo.lock index ed0f80124..b086a9776 100644 --- a/py-rattler/Cargo.lock +++ b/py-rattler/Cargo.lock @@ -79,9 +79,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "arbitrary" @@ -271,9 +271,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" dependencies = [ "proc-macro2", "quote", @@ -847,9 +847,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", @@ -874,9 +874,9 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "file_url" -version = "0.2.1" +version = "0.2.2" dependencies = [ - "itertools 0.13.0", + "itertools 0.14.0", "percent-encoding", "thiserror 2.0.9", "typed-path", @@ -897,9 +897,9 @@ dependencies = [ [[package]] name = "fixedbitset" -version = "0.4.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" @@ -1131,9 +1131,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "google-cloud-auth" @@ -1689,6 +1689,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -1768,9 +1777,9 @@ dependencies = [ [[package]] name = "lazy-regex" -version = "3.3.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d8e41c97e6bc7ecb552016274b99fbb5d035e8de288c582d9b933af6677bfda" +checksum = "60c7310b93682b36b98fa7ea4de998d3463ccbebd94d935d6b48ba5b6ffa7126" dependencies = [ "lazy-regex-proc_macros", "once_cell", @@ -1779,9 +1788,9 @@ dependencies = [ [[package]] name = "lazy-regex-proc_macros" -version = "3.3.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76e1d8b05d672c53cb9c7b920bbba8783845ae4f0b076e02a3db1d02c81b4163" +checksum = "4ba01db5ef81e17eb10a5e0f2109d1b3a3e29bac3070fdbd7d156bf7dbd206a1" dependencies = [ "proc-macro2", "quote", @@ -2252,9 +2261,9 @@ dependencies = [ [[package]] name = "pep508_rs" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2feee999fa547bacab06a4881bacc74688858b92fa8ef1e206c748b0a76048" +checksum = "faee7227064121fcadcd2ff788ea26f0d8f2bd23a0574da11eca23bc935bcc05" dependencies = [ "boxcar", "indexmap 2.7.0", @@ -2280,9 +2289,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" -version = "0.6.5" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", "indexmap 2.7.0", @@ -2463,9 +2472,9 @@ dependencies = [ [[package]] name = "purl" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c14fe28c8495f7eaf77a6e6106966f95211c0a2404b9da50d248fc32af3a3f14" +checksum = "f112b0e2a9bca03924c39166775b74fec9a831f7d4d8fa539dee0e565f403a0e" dependencies = [ "hex", "percent-encoding", @@ -2655,9 +2664,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -2700,7 +2709,7 @@ dependencies = [ [[package]] name = "rattler" -version = "0.28.8" +version = "0.28.12" dependencies = [ "anyhow", "console", @@ -2711,7 +2720,7 @@ dependencies = [ "humantime", "indexmap 2.7.0", "indicatif", - "itertools 0.13.0", + "itertools 0.14.0", "memchr", "memmap2", "once_cell", @@ -2739,7 +2748,7 @@ dependencies = [ [[package]] name = "rattler_cache" -version = "0.3.0" +version = "0.3.4" dependencies = [ "anyhow", "dashmap", @@ -2749,7 +2758,7 @@ dependencies = [ "fs4", "futures", "fxhash", - "itertools 0.13.0", + "itertools 0.14.0", "parking_lot", "rattler_conda_types", "rattler_digest", @@ -2767,7 +2776,7 @@ dependencies = [ [[package]] name = "rattler_conda_types" -version = "0.29.6" +version = "0.29.10" dependencies = [ "chrono", "dirs", @@ -2777,7 +2786,7 @@ dependencies = [ "glob", "hex", "indexmap 2.7.0", - "itertools 0.13.0", + "itertools 0.14.0", "lazy-regex", "nom", "purl", @@ -2802,7 +2811,7 @@ dependencies = [ [[package]] name = "rattler_digest" -version = "1.0.4" +version = "1.0.5" dependencies = [ "blake2", "digest", @@ -2817,7 +2826,7 @@ dependencies = [ [[package]] name = "rattler_index" -version = "0.20.3" +version = "0.20.7" dependencies = [ "fs-err", "rattler_conda_types", @@ -2830,13 +2839,13 @@ dependencies = [ [[package]] name = "rattler_lock" -version = "0.22.35" +version = "0.22.39" dependencies = [ "chrono", "file_url", "fxhash", "indexmap 2.7.0", - "itertools 0.13.0", + "itertools 0.14.0", "pep440_rs", "pep508_rs", "rattler_conda_types", @@ -2853,7 +2862,7 @@ dependencies = [ [[package]] name = "rattler_macros" -version = "1.0.4" +version = "1.0.5" dependencies = [ "quote", "syn", @@ -2861,7 +2870,7 @@ dependencies = [ [[package]] name = "rattler_networking" -version = "0.21.9" +version = "0.21.10" dependencies = [ "anyhow", "async-trait", @@ -2873,7 +2882,7 @@ dependencies = [ "google-cloud-auth", "google-cloud-token", "http", - "itertools 0.13.0", + "itertools 0.14.0", "keyring", "netrc-rs", "reqwest", @@ -2888,7 +2897,7 @@ dependencies = [ [[package]] name = "rattler_package_streaming" -version = "0.22.19" +version = "0.22.23" dependencies = [ "bzip2 0.5.0", "chrono", @@ -2915,7 +2924,7 @@ dependencies = [ [[package]] name = "rattler_redaction" -version = "0.1.5" +version = "0.1.6" dependencies = [ "reqwest", "reqwest-middleware", @@ -2924,7 +2933,7 @@ dependencies = [ [[package]] name = "rattler_repodata_gateway" -version = "0.21.28" +version = "0.21.32" dependencies = [ "anyhow", "async-compression", @@ -2944,7 +2953,7 @@ dependencies = [ "http-cache-semantics", "humansize", "humantime", - "itertools 0.13.0", + "itertools 0.14.0", "json-patch", "libc", "md-5", @@ -2959,6 +2968,7 @@ dependencies = [ "rattler_redaction", "reqwest", "reqwest-middleware", + "retry-policies", "rmp-serde", "serde", "serde_json", @@ -2977,12 +2987,12 @@ dependencies = [ [[package]] name = "rattler_shell" -version = "0.22.11" +version = "0.22.15" dependencies = [ "enum_dispatch", "fs-err", "indexmap 2.7.0", - "itertools 0.13.0", + "itertools 0.14.0", "rattler_conda_types", "serde_json", "shlex", @@ -2993,11 +3003,12 @@ dependencies = [ [[package]] name = "rattler_solve" -version = "1.3.0" +version = "1.3.4" dependencies = [ "chrono", "futures", - "itertools 0.13.0", + "indexmap 2.7.0", + "itertools 0.14.0", "rattler_conda_types", "rattler_digest", "resolvo", @@ -3009,7 +3020,7 @@ dependencies = [ [[package]] name = "rattler_virtual_packages" -version = "1.1.14" +version = "1.2.0" dependencies = [ "archspec", "libloading", @@ -3125,9 +3136,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.9" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ "async-compression", "base64 0.22.1", @@ -3164,6 +3175,7 @@ dependencies = [ "tokio-native-tls", "tokio-rustls", "tokio-util", + "tower", "tower-service", "url", "wasm-bindgen", @@ -3191,9 +3203,9 @@ dependencies = [ [[package]] name = "resolvo" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03fdd3aa47ae0816ce4ec203eba1330e7c96a6760cbfbee5f1d2ca6e768b50f7" +checksum = "5314eb4b865d39acd1b3cd05eb91b87031bb49fd1278a1bdf8d6680f1389ec29" dependencies = [ "ahash", "bitvec", @@ -3201,7 +3213,7 @@ dependencies = [ "event-listener", "futures", "indexmap 2.7.0", - "itertools 0.13.0", + "itertools 0.14.0", "petgraph", "tracing", ] @@ -3422,9 +3434,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] @@ -3452,9 +3464,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -3499,9 +3511,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.11.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" dependencies = [ "base64 0.22.1", "chrono", @@ -3517,9 +3529,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.11.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ "darling", "proc-macro2", @@ -3728,9 +3740,9 @@ checksum = "ab16ced94dbd8a46c82fd81e3ed9a8727dac2977ea869d217bcc4ea1f122e81f" [[package]] name = "syn" -version = "2.0.90" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -3796,12 +3808,13 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.14.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", + "getrandom", "once_cell", "rustix", "windows-sys 0.59.0", @@ -3982,6 +3995,27 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" diff --git a/py-rattler/src/record.rs b/py-rattler/src/record.rs index f1a0902b6..da2884e0d 100644 --- a/py-rattler/src/record.rs +++ b/py-rattler/src/record.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::path::PathBuf; use pyo3::prelude::PyAnyMethods; @@ -164,6 +165,7 @@ impl PyRecord { subdir, constrains: Vec::new(), depends: Vec::new(), + extra_depends: BTreeMap::new(), features: None, legacy_bz2_md5: None, legacy_bz2_size: None, diff --git a/py-rattler/src/solver.rs b/py-rattler/src/solver.rs index e75e86125..5f8071dc4 100644 --- a/py-rattler/src/solver.rs +++ b/py-rattler/src/solver.rs @@ -95,7 +95,12 @@ pub fn py_solve( Ok::<_, PyErr>( Solver .solve(task) - .map(|res| res.into_iter().map(Into::into).collect::>()) + .map(|res| { + res.records + .into_iter() + .map(Into::into) + .collect::>() + }) .map_err(PyRattlerError::from)?, ) }) @@ -173,7 +178,12 @@ pub fn py_solve_with_sparse_repodata( Ok::<_, PyErr>( Solver .solve(task) - .map(|res| res.into_iter().map(Into::into).collect::>()) + .map(|res| { + res.records + .into_iter() + .map(Into::into) + .collect::>() + }) .map_err(PyRattlerError::from)?, ) }) diff --git a/test-data/channels/dummy-optional-dependencies/noarch/repodata.json b/test-data/channels/dummy-optional-dependencies/noarch/repodata.json new file mode 100644 index 000000000..8ea5d85bb --- /dev/null +++ b/test-data/channels/dummy-optional-dependencies/noarch/repodata.json @@ -0,0 +1,528 @@ +{ + "info": { + "subdir": "noarch", + "base_url": "../linux-64" + }, + "packages": { + "cuda-version-12.5-hd4f0392_3.conda": { + "build": "hd4f0392_3", + "build_number": 3, + "depends": [], + "constrains": [ + "__cuda >=12.1" + ], + "license": "LicenseRef-NVIDIA-End-User-License-Agreement", + "license_family": "LicenseRef-NVIDIA-End-User-License-Agreement", + "md5": "6ae1a563a4aa61e55e8ae8260f0d021b", + "name": "cuda-version", + "sha256": "e45a5d14909296abd0784a073da9ee5c420fa58671fbc999f8a9ec898cf3486b", + "size": 21151, + "subdir": "noarch", + "timestamp": 1716314536803, + "version": "12.5", + "extra_depends": { + "with-cudadev": [ + "foo >=4.0.2", + "bar >=1.2.3" + ] + } + }, + "foo-2.0.2-py36h1af98f8_1.tar.bz2": { + "build": "py36h1af98f8_1", + "build_number": 1, + "depends": [], + "license": "MIT", + "license_family": "MIT", + "md5": "d65ab674acf3b7294ebacaec05fc5b54", + "name": "foo", + "sha256": "1154fceeb5c4ee9bb97d245713ac21eb1910237c724d2b7103747215663273c2", + "size": 414494, + "subdir": "noarch", + "timestamp": 1605110689658, + "version": "2.0.2", + "extra_depends": { + "with-oldbors": [ + "bors <2.0" + ], + "with-latest-bors": [ + "bors >=2.0" + ], + "legacy-only": [ + "bar >=1.0" + ], + "with-recursive": [ + "foo[extras=[with-bar]]" + ], + "with-self": [ + "foo >=2.0" + ] + } + }, + "foo-3.0.2-py36h1af98f8_1.conda": { + "build": "py36h1af98f8_1", + "build_number": 1, + "depends": [], + "license": "MIT", + "license_family": "MIT", + "md5": "fb731d9290f0bcbf3a054665f33ec94f", + "name": "foo", + "sha256": "67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4", + "size": 414494, + "subdir": "noarch", + "timestamp": 1715610974000, + "version": "3.0.2", + "extra_depends": { + "with-baz2": [ + "baz >=2.0" + ], + "with-bar": [ + "bar >=1.2.3" + ], + "with-version-restrict": [ + "foo <3.0.3" + ] + } + }, + "foo-3.0.3-py36h1af98f8_2.conda": { + "build": "py36h1af98f8_2", + "build_number": 2, + "depends": [], + "license": "MIT", + "license_family": "MIT", + "md5": "fb731d9290f0bcbf3a054665f33ec94f", + "name": "foo", + "sha256": "67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4", + "size": 414494, + "subdir": "noarch", + "timestamp": 1715610974000, + "version": "3.0.3", + "extra_depends": { + "with-foobar21": [ + "foobar >=2.1" + ], + "with-bors2": [ + "bors >=1.2.1" + ] + } + }, + "foo-3.0.4-py36h1af98f8_3.conda": { + "build": "py36h1af98f8_3", + "build_number": 3, + "depends": [], + "constrains": [ + "bors <2.0" + ], + "license": "MIT", + "license_family": "MIT", + "md5": "fb731d9290f0bcbf3a054665f33ec94f", + "name": "foo", + "sha256": "67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4", + "size": 414494, + "subdir": "noarch", + "timestamp": 1715610974000, + "version": "3.0.4", + "extra_depends": { + "with-issue717": [ + "issue_717 >=2.1" + ], + "with-xfoo2": [ + "xfoo >=2" + ] + } + }, + "foo-3.1.0-py36h1af98f8_1.conda": { + "build": "py36h1af98f8_1", + "build_number": 1, + "depends": [], + "license": "MIT", + "license_family": "MIT", + "md5": "fb731d9290f0bcbf3a054665f33ec94f", + "name": "foo", + "sha256": "67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4", + "size": 414494, + "subdir": "noarch", + "timestamp": 1715610974000, + "version": "3.1.0", + "extra_depends": { + "with-bar": [ + "bar >=1.2.3" + ] + } + }, + "foo-3.2.0-py36h1af98f8_1.conda": { + "build": "py36h1af98f8_1", + "build_number": 1, + "depends": [], + "license": "MIT", + "license_family": "MIT", + "md5": "fb731d9290f0bcbf3a054665f33ec94f", + "name": "foo", + "sha256": "67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4", + "size": 414494, + "subdir": "noarch", + "timestamp": 1715610974000, + "version": "3.2.0", + "extra_depends": { + "with-bar": [ + "bar >=1.2.3" + ] + } + }, + "foo-3.3.0-py36h1af98f8_1.conda": { + "build": "py36h1af98f8_1", + "build_number": 1, + "depends": [], + "license": "MIT", + "license_family": "MIT", + "md5": "fb731d9290f0bcbf3a054665f33ec94f", + "name": "foo", + "sha256": "67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4", + "size": 414494, + "subdir": "noarch", + "timestamp": 1715610974000, + "version": "3.3.0", + "extra_depends": { + "with-bar": [ + "bar >=1.2.3" + ] + } + }, + "foo-3.4.0-py36h1af98f8_1.conda": { + "build": "py36h1af98f8_1", + "build_number": 1, + "depends": [], + "license": "MIT", + "license_family": "MIT", + "md5": "fb731d9290f0bcbf3a054665f33ec94f", + "name": "foo", + "sha256": "67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4", + "size": 414494, + "subdir": "noarch", + "timestamp": 1715610974000, + "version": "3.4.0", + "extra_depends": { + "with-bar": [ + "bar >=1.2.3" + ] + } + }, + "foo-3.5.0-py36h1af98f8_1.conda": { + "build": "py36h1af98f8_1", + "build_number": 1, + "depends": [], + "license": "MIT", + "license_family": "MIT", + "md5": "fb731d9290f0bcbf3a054665f33ec94f", + "name": "foo", + "sha256": "67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4", + "size": 414494, + "subdir": "noarch", + "timestamp": 1715610974000, + "version": "3.5.0", + "extra_depends": { + "with-bar": [ + "bar >=1.2.3" + ] + } + }, + "foo-4.0.2-py36h1af98f8_2.tar.bz2": { + "build": "py36h1af98f8_2", + "build_number": 1, + "depends": [], + "license": "MIT", + "license_family": "MIT", + "md5": "bc13aa58e2092bcb0b97c561373d3905", + "name": "foo", + "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", + "size": 414494, + "subdir": "noarch", + "timestamp": 1715610974000, + "version": "4.0.2", + "extra_depends": { + "with-xbar": [ + "xbar >=1" + ] + } + }, + "bar-1.0-unix_py36h1af98f8_2.tar.bz2": { + "build": "unix_py36h1af98f8_2", + "build_number": 1, + "depends": [ + ], + "license": "MIT", + "license_family": "MIT", + "md5": "bc13aa58e2092bcb0b97c561373d3905", + "name": "bar", + "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", + "size": 414494, + "subdir": "noarch", + "timestamp": 1605110689658, + "version": "1.2.3", + "extra_depends": { + "with-xfoo": [ + "xfoo >=1" + ] + } + }, + "baz-1.0-unix_py36h1af98f8_2.tar.bz2": { + "build": "unix_py36h1af98f8_2", + "build_number": 1, + "depends": [ + "__unix" + ], + "license": "MIT", + "license_family": "MIT", + "md5": "bc13aa58e2092bcb0b97c561373d3905", + "name": "baz", + "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", + "size": 414494, + "subdir": "noarch", + "timestamp": 1605110689658, + "version": "1.2.3", + "extra_depends": { + "with-foobar": [ + "foobar >=2.0" + ] + } + }, + "baz-2.0-unix_py36h1af98f8_2.tar.bz2": { + "build": "unix_py36h1af98f8_2", + "build_number": 1, + "depends": [], + "license": "MIT", + "license_family": "MIT", + "md5": "bc13aa58e2092bcb0b97c561373d3905", + "name": "baz", + "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", + "size": 414494, + "subdir": "noarch", + "timestamp": 1715610974000, + "version": "2.0", + "extra_depends": { + "with-bors": [ + "bors >=2.0" + ] + } + }, + "bors-1.0-bla_1.tar.bz2": { + "build": "bla_1", + "build_number": 1, + "depends": [], + "license": "MIT", + "license_family": "MIT", + "md5": "bc13aa58e2092bcb0b97c561373d3905", + "name": "bors", + "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", + "size": 414494, + "subdir": "noarch", + "timestamp": 1605110689658, + "version": "1.0", + "extra_depends": { + "with-baz10": [ + "baz >=1.0" + ] + } + }, + "bors-1.1-bla_1.tar.bz2": { + "build": "bla_1", + "build_number": 1, + "depends": [], + "license": "MIT", + "license_family": "MIT", + "md5": "bc13aa58e2092bcb0b97c561373d3905", + "name": "bors", + "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", + "size": 414494, + "subdir": "noarch", + "timestamp": 1605110689658, + "version": "1.1", + "extra_depends": { + "with-foobar20": [ + "foobar >=2.0" + ] + } + }, + "bors-1.2.1-bla_1.tar.bz2": { + "build": "bla_1", + "build_number": 1, + "depends": [], + "license": "MIT", + "license_family": "MIT", + "md5": "bc13aa58e2092bcb0b97c561373d3905", + "name": "bors", + "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", + "size": 414494, + "subdir": "noarch", + "timestamp": 1605110689658, + "version": "1.2.1", + "extra_depends": { + "with-baz2": [ + "baz >=2.0" + ] + } + }, + "bors-2.0-bla_1.tar.bz2": { + "build": "bla_1", + "build_number": 1, + "depends": [], + "license": "MIT", + "license_family": "MIT", + "md5": "bc13aa58e2092bcb0b97c561373d3905", + "name": "bors", + "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", + "size": 414494, + "subdir": "noarch", + "timestamp": 1715610974000, + "version": "2.0", + "extra_depends": { + "with-xbar": [ + "xbar >=1" + ] + } + }, + "bors-2.1-bla_1.tar.bz2": { + "build": "bla_1", + "build_number": 1, + "depends": [], + "license": "MIT", + "license_family": "MIT", + "md5": "bc13aa58e2092bcb0b97c561373d3905", + "name": "bors", + "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", + "size": 414494, + "subdir": "noarch", + "timestamp": 1715610974000, + "version": "2.1", + "extra_depends": { + "with-cuda": [ + "cuda-version >=12.5" + ] + } + }, + "foobar-2.0-bla_1.tar.bz2": { + "build": "bla_1", + "build_number": 1, + "depends": [ + "bors <2.0" + ], + "license": "MIT", + "license_family": "MIT", + "md5": "bc13aa58e2092bcb0b97c561373d3905", + "name": "foobar", + "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", + "size": 414494, + "subdir": "noarch", + "timestamp": 1605110689658, + "version": "2.0", + "extra_depends": { + "with-baz10": [ + "baz >=1.0" + ] + } + }, + "foobar-2.1-bla_1.tar.bz2": { + "build": "bla_1", + "build_number": 1, + "depends": [ + "bors <2.0" + ], + "license": "MIT", + "license_family": "MIT", + "md5": "bc13aa58e2092bcb0b97c561373d3905", + "name": "foobar", + "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", + "size": 414494, + "subdir": "noarch", + "timestamp": 1715610974000, + "version": "2.1", + "extra_depends": { + "with-baz2": [ + "baz >=2.0" + ] + } + }, + "issue_717-2.1-bla_1.tar.bz2": { + "build": "issue_717", + "build_number": 0, + "depends": [], + "constrains": [ + "ray[version=>=2.9.0, extras=[default,data]]" + ], + "license": "MIT", + "license_family": "MIT", + "md5": "bc13aa58e2092bcb0b97c561373d3905", + "name": "issue_717", + "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", + "size": 414494, + "subdir": "noarch", + "timestamp": 1715610974000, + "version": "2.1", + "extra_depends": { + "with-bors21": [ + "bors >=2.1" + ] + } + }, + "xfoo-1-xxx.tar.bz2": { + "build": "xxx", + "build_number": 0, + "depends": [], + "license": "MIT", + "license_family": "MIT", + "md5": "bc13aa58e2092bcb0b97c561373d3905", + "name": "xfoo", + "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", + "size": 414494, + "subdir": "noarch", + "timestamp": 1715610974000, + "version": "1", + "extra_depends": { + "with-cuda": [ + "cuda-version >=12.5" + ] + } + }, + "xfoo-2-xxx.tar.bz2": { + "build": "xxx", + "build_number": 0, + "depends": [], + "license": "MIT", + "license_family": "MIT", + "md5": "bc13aa58e2092bcb0b97c561373d3905", + "name": "xfoo", + "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", + "size": 414494, + "subdir": "noarch", + "timestamp": 1715610974000, + "version": "2", + "extra_depends": { + "with-issue717": [ + "issue_717[version=>=2.1, extras=[with-bors21]]" + ] + } + }, + "xbar-1-xxx.tar.bz2": { + "build": "xxx", + "build_number": 0, + "depends": [ + "xfoo >=2" + ], + "license": "MIT", + "license_family": "MIT", + "md5": "bc13aa58e2092bcb0b97c561373d3905", + "name": "xbar", + "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", + "size": 414494, + "subdir": "noarch", + "timestamp": 1715610974000, + "version": "1", + "extra_depends": { + "with-foo4": [ + "foo >=4.0.2" + ] + } + } + }, + "packages.conda": {}, + "repodata_version": 2 + } \ No newline at end of file