diff --git a/crates/rattler_conda_types/src/match_spec/mod.rs b/crates/rattler_conda_types/src/match_spec/mod.rs index 14274f155b..261c2bd7e0 100644 --- a/crates/rattler_conda_types/src/match_spec/mod.rs +++ b/crates/rattler_conda_types/src/match_spec/mod.rs @@ -6,8 +6,10 @@ use itertools::Itertools; use rattler_digest::{serde::SerializableHash, Md5Hash, Sha256Hash}; use serde::{Deserialize, Deserializer, Serialize}; use serde_with::{serde_as, skip_serializing_none}; +use std::collections::BTreeSet; use std::fmt::{Debug, Display, Formatter}; use std::hash::Hash; +use std::str::FromStr; use std::sync::Arc; use url::Url; @@ -138,6 +140,8 @@ pub struct MatchSpec { pub file_name: Option, /// The selected optional features of the package pub extras: Option>, + /// The selected build flags + pub flags: Option>, /// The channel of the package pub channel: Option>, /// The subdir of the channel @@ -192,6 +196,10 @@ impl Display for MatchSpec { keys.push(format!("extras=[{}]", extras.iter().format(", "))); } + if let Some(flags) = &self.flags { + keys.push(format!("flags=[{:?}]", flags.iter().format(", "))); + } + if let Some(md5) = &self.md5 { keys.push(format!("md5=\"{md5:x}\"")); } @@ -224,6 +232,80 @@ impl Display for MatchSpec { } } +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum FlagMatcher { + /// Match if flag exists + Required(String), + /// Match if flag doesn't exist + Negated(String), + /// Match if flag exists, but don't fail if it doesn't + Optional(String), +} + +impl FlagMatcher { + pub fn matches(&self, flags: &BTreeSet) -> bool { + match self { + FlagMatcher::Required(flag) => flags.contains(flag), + FlagMatcher::Negated(flag) => !flags.contains(flag), + FlagMatcher::Optional(flag) => !flags.contains(flag) || flags.contains(flag), + } + } +} + +impl Display for FlagMatcher { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + FlagMatcher::Required(flag) => write!(f, "{flag}"), + FlagMatcher::Negated(flag) => write!(f, "~{flag}"), + FlagMatcher::Optional(flag) => write!(f, "?{flag}"), + } + } +} + +impl FromStr for FlagMatcher { + type Err = (); + + fn from_str(s: &str) -> Result { + if s.starts_with('~') { + Ok(FlagMatcher::Negated(s[1..].to_string())) + } else if s.starts_with('?') { + Ok(FlagMatcher::Optional(s[1..].to_string())) + } else { + Ok(FlagMatcher::Required(s.to_string())) + } + } +} + +impl Serialize for FlagMatcher { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + FlagMatcher::Required(flag) => serializer.serialize_str(flag), + FlagMatcher::Negated(flag) => serializer.serialize_str(&format!("~{}", flag)), + FlagMatcher::Optional(flag) => serializer.serialize_str(&format!("?{}", flag)), + } + } +} + +// Add deserialization implementation +impl<'de> Deserialize<'de> for FlagMatcher { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + if s.starts_with('~') || s.starts_with('!') { + Ok(FlagMatcher::Negated(s[1..].to_string())) + } else if s.starts_with('?') { + Ok(FlagMatcher::Optional(s[1..].to_string())) + } else { + Ok(FlagMatcher::Required(s)) + } + } +} + impl MatchSpec { /// Decomposes this instance into a [`NamelessMatchSpec`] and a name. pub fn into_nameless(self) -> (Option, NamelessMatchSpec) { @@ -235,6 +317,7 @@ impl MatchSpec { build_number: self.build_number, file_name: self.file_name, extras: self.extras, + flags: self.flags, channel: self.channel, subdir: self.subdir, namespace: self.namespace, @@ -282,6 +365,8 @@ pub struct NamelessMatchSpec { pub file_name: Option, /// Optional extra dependencies to select for the package pub extras: Option>, + /// Optional build time flags to select for the package + pub flags: Option>, /// The channel of the package #[serde(deserialize_with = "deserialize_channel", default)] pub channel: Option>, @@ -338,6 +423,7 @@ impl From for NamelessMatchSpec { build_number: spec.build_number, file_name: spec.file_name, extras: spec.extras, + flags: spec.flags, channel: spec.channel, subdir: spec.subdir, namespace: spec.namespace, @@ -359,6 +445,7 @@ impl MatchSpec { build_number: spec.build_number, file_name: spec.file_name, extras: spec.extras, + flags: spec.flags, channel: spec.channel, subdir: spec.subdir, namespace: spec.namespace, @@ -439,6 +526,12 @@ impl Matches for NamelessMatchSpec { } } + if let Some(flags) = self.flags.as_ref() { + if !flags.iter().all(|flag| flag.matches(&other.flags)) { + return false; + } + } + true } } @@ -559,8 +652,10 @@ mod tests { use rattler_digest::{parse_digest_from_hex, Md5, Sha256}; use crate::{ - match_spec::Matches, MatchSpec, NamelessMatchSpec, PackageName, PackageRecord, - ParseStrictness::*, RepoDataRecord, StringMatcher, Version, VersionSpec, + match_spec::{FlagMatcher, Matches}, + MatchSpec, NamelessMatchSpec, PackageName, PackageRecord, + ParseStrictness::*, + RepoDataRecord, StringMatcher, Version, VersionSpec, }; use insta::assert_snapshot; use std::hash::{Hash, Hasher}; @@ -906,4 +1001,127 @@ mod tests { MatchSpec::from_nameless(NamelessMatchSpec::from_str(">=12", Strict).unwrap(), None); assert!(!spec.is_virtual()); } + + #[test] + fn test_flagmatcher() { + use std::collections::BTreeSet; + + // Create a set of flags to test against + let mut flags = BTreeSet::new(); + flags.insert("mkl".to_string()); + flags.insert("cuda".to_string()); + + // Test Required flag matcher + let matcher = FlagMatcher::Required("mkl".to_string()); + assert!(matcher.matches(&flags)); + + let matcher = FlagMatcher::Required("nomkl".to_string()); + assert!(!matcher.matches(&flags)); + + // Test Negated flag matcher + let matcher = FlagMatcher::Negated("nomkl".to_string()); + assert!(matcher.matches(&flags)); + + let matcher = FlagMatcher::Negated("mkl".to_string()); + assert!(!matcher.matches(&flags)); + + // Test Optional flag matcher + let matcher = FlagMatcher::Optional("mkl".to_string()); + assert!(matcher.matches(&flags)); + + let matcher = FlagMatcher::Optional("nomkl".to_string()); + assert!(matcher.matches(&flags)); + } + + #[test] + fn test_flagmatcher_parsing() { + // Test parsing standard flag + let matcher = FlagMatcher::from_str("mkl").unwrap(); + assert!(matches!(matcher, FlagMatcher::Required(_))); + + // Test parsing negated flag + let matcher = FlagMatcher::from_str("~mkl").unwrap(); + assert!(matches!(matcher, FlagMatcher::Negated(_))); + + // Test parsing optional flag + let matcher = FlagMatcher::from_str("?mkl").unwrap(); + assert!(matches!(matcher, FlagMatcher::Optional(_))); + } + + #[test] + fn test_flagmatcher_display() { + // Test display formatting for Required flag + let matcher = FlagMatcher::Required("mkl".to_string()); + assert_eq!(matcher.to_string(), "mkl"); + + // Test display formatting for Negated flag + let matcher = FlagMatcher::Negated("mkl".to_string()); + assert_eq!(matcher.to_string(), "~mkl"); + + // Test display formatting for Optional flag + let matcher = FlagMatcher::Optional("mkl".to_string()); + assert_eq!(matcher.to_string(), "?mkl"); + } + + #[test] + fn test_flagmatcher_serde() { + use serde_json; + + // Test serialization + let matcher = FlagMatcher::Required("mkl".to_string()); + assert_eq!(serde_json::to_string(&matcher).unwrap(), "\"mkl\""); + + let matcher = FlagMatcher::Negated("mkl".to_string()); + assert_eq!(serde_json::to_string(&matcher).unwrap(), "\"~mkl\""); + + let matcher = FlagMatcher::Optional("mkl".to_string()); + assert_eq!(serde_json::to_string(&matcher).unwrap(), "\"?mkl\""); + + // Test deserialization + let matcher: FlagMatcher = serde_json::from_str("\"mkl\"").unwrap(); + assert!(matches!(matcher, FlagMatcher::Required(_))); + + let matcher: FlagMatcher = serde_json::from_str("\"~mkl\"").unwrap(); + assert!(matches!(matcher, FlagMatcher::Negated(_))); + + let matcher: FlagMatcher = serde_json::from_str("\"?mkl\"").unwrap(); + assert!(matches!(matcher, FlagMatcher::Optional(_))); + } + + #[test] + fn test_matchspec_with_flags() { + use std::collections::BTreeSet; + + // Create a package record with flags + let mut flags = BTreeSet::new(); + flags.insert("mkl".to_string()); + flags.insert("cuda".to_string()); + + let mut package = PackageRecord::new( + PackageName::new_unchecked("numpy"), + Version::from_str("1.0").unwrap(), + String::from("py37_0"), + ); + package.flags = flags; + + // Test match with required flag + let spec = MatchSpec::from_str("numpy[flags=['mkl']]", Strict).unwrap(); + assert!(spec.matches(&package)); + + // Test match with negated flag + let spec = MatchSpec::from_str("numpy[flags=['~nomkl']]", Strict).unwrap(); + assert!(spec.matches(&package)); + + // Test match with optional flag + let spec = MatchSpec::from_str("numpy[flags=['?mkl']]", Strict).unwrap(); + assert!(spec.matches(&package)); + + // Test match with multiple flags + let spec = MatchSpec::from_str("numpy[flags=['mkl', 'cuda']]", Strict).unwrap(); + assert!(spec.matches(&package)); + + // Test non-match with missing required flag + let spec = MatchSpec::from_str("numpy[flags=['nomkl']]", Strict).unwrap(); + assert!(!spec.matches(&package)); + } } diff --git a/crates/rattler_conda_types/src/match_spec/parse.rs b/crates/rattler_conda_types/src/match_spec/parse.rs index 3f899134be..d6a2953ae7 100644 --- a/crates/rattler_conda_types/src/match_spec/parse.rs +++ b/crates/rattler_conda_types/src/match_spec/parse.rs @@ -19,7 +19,7 @@ use url::Url; use super::{ matcher::{StringMatcher, StringMatcherParseError}, - MatchSpec, + FlagMatcher, MatchSpec, }; use crate::{ build_spec::{BuildNumberSpec, ParseBuildNumberSpecError}, @@ -227,9 +227,8 @@ 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> { +pub fn parse_list(input: &str) -> Result, ParseMatchSpecError> { use nom::{ combinator::{all_consuming, map}, multi::separated_list1, @@ -282,7 +281,7 @@ fn parse_bracket_vec_into_components( // Optional features are still experimental #[cfg(feature = "experimental_extras")] { - match_spec.extras = Some(parse_extras(value)?); + match_spec.extras = Some(parse_list(value)?); } #[cfg(not(feature = "experimental_extras"))] { @@ -325,6 +324,15 @@ fn parse_bracket_vec_into_components( match_spec.subdir = match_spec.subdir.or(subdir); } "license" => match_spec.license = Some(value.to_string()), + "flags" => { + match_spec.flags = Some( + parse_list(value) + .unwrap() + .iter() + .map(|s| FlagMatcher::from_str(s).unwrap()) + .collect(), + ); + } // TODO: Still need to add `track_features`, `features`, and `license_family` // to the match spec. _ => Err(ParseMatchSpecError::InvalidBracketKey(key.to_owned()))?, @@ -1424,6 +1432,7 @@ mod tests { ) .unwrap(), ), + flags: None, license: Some("MIT".into()), }); @@ -1462,15 +1471,15 @@ mod tests { #[test] fn test_parse_extras() { assert_eq!( - parse_extras("bar,baz").unwrap(), + parse_list("bar,baz").unwrap(), vec!["bar".to_string(), "baz".to_string()] ); - assert_eq!(parse_extras("bar").unwrap(), vec!["bar".to_string()]); + assert_eq!(parse_list("bar").unwrap(), vec!["bar".to_string()]); assert_eq!( - parse_extras("bar, baz").unwrap(), + parse_list("bar, baz").unwrap(), vec!["bar".to_string(), "baz".to_string()] ); - assert!(parse_extras("[bar,baz]").is_err()); + assert!(parse_list("[bar,baz]").is_err()); } #[cfg(feature = "experimental_extras")] diff --git a/crates/rattler_conda_types/src/package/index.rs b/crates/rattler_conda_types/src/package/index.rs index e7bfac055c..64a56562d4 100644 --- a/crates/rattler_conda_types/src/package/index.rs +++ b/crates/rattler_conda_types/src/package/index.rs @@ -41,6 +41,10 @@ pub struct IndexJson { /// mutually exclusive features. pub features: Option, + /// Flags that can be matched by the solver. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub flags: Vec, + /// Optionally, the license pub license: Option, diff --git a/crates/rattler_conda_types/src/repo_data/mod.rs b/crates/rattler_conda_types/src/repo_data/mod.rs index 38800101ec..9ff12bb5dd 100644 --- a/crates/rattler_conda_types/src/repo_data/mod.rs +++ b/crates/rattler_conda_types/src/repo_data/mod.rs @@ -129,6 +129,11 @@ pub struct PackageRecord { /// mutually exclusive features. pub features: Option, + /// Flags are a way to define build-time features. This has been traditionally + /// done by changing the build string, but flags are much nicer. + #[serde(default, skip_serializing_if = "BTreeSet::is_empty")] + pub flags: BTreeSet, + /// A deprecated md5 hash #[serde_as(as = "Option>")] pub legacy_bz2_md5: Option, @@ -335,6 +340,7 @@ impl PackageRecord { platform: None, python_site_packages_path: None, extra_depends: BTreeMap::new(), + flags: BTreeSet::new(), sha256: None, size: None, subdir: Platform::current().to_string(), @@ -509,6 +515,7 @@ impl PackageRecord { platform: index.platform, python_site_packages_path: index.python_site_packages_path, extra_depends: BTreeMap::new(), + flags: BTreeSet::new(), sha256, size, subdir, diff --git a/crates/rattler_index/src/lib.rs b/crates/rattler_index/src/lib.rs index c781d0a387..a9e8d44da6 100644 --- a/crates/rattler_index/src/lib.rs +++ b/crates/rattler_index/src/lib.rs @@ -18,6 +18,7 @@ use rattler_package_streaming::{ seek::{self, stream_conda_content}, }; use std::{ + collections::{BTreeMap, BTreeSet}, collections::{HashMap, HashSet}, io::{Cursor, Read, Seek}, path::{Path, PathBuf}, @@ -57,7 +58,8 @@ pub fn package_record_from_index_json( arch: index.arch, platform: index.platform, depends: index.depends, - extra_depends: std::collections::BTreeMap::new(), + extra_depends: BTreeMap::new(), + flags: BTreeSet::from_iter(index.flags.into_iter()), constrains: index.constrains, track_features: index.track_features, features: index.features, diff --git a/crates/rattler_libsolv_c/libsolv b/crates/rattler_libsolv_c/libsolv index 068739980a..2512204a58 160000 --- a/crates/rattler_libsolv_c/libsolv +++ b/crates/rattler_libsolv_c/libsolv @@ -1 +1 @@ -Subproject commit 068739980a909f39b647b9a96146e58e80741d1e +Subproject commit 2512204a58dde5a5869384cbddde801835a33ab4 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 f7a3c7bcfd..099b572ae7 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 @@ -1,4 +1,7 @@ -use std::{borrow::Cow, collections::BTreeSet}; +use std::{ + borrow::Cow, + collections::{BTreeMap, BTreeSet}, +}; use rattler_conda_types::{ BuildNumber, ChannelUrl, NoArchType, PackageName, PackageRecord, PackageUrl, VersionWithSource, @@ -117,7 +120,8 @@ 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(), + extra_depends: BTreeMap::new(), + flags: BTreeSet::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 7d82964532..9ccee16fd0 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 @@ -80,6 +80,8 @@ pub(crate) struct CondaPackageDataModel<'a> { pub constrains: Cow<'a, Vec>, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub extra_depends: Cow<'a, BTreeMap>>, + #[serde(default, skip_serializing_if = "BTreeSet::is_empty")] + pub flags: Cow<'a, BTreeSet>, // Additional properties (in semi alphabetic order but grouped by commonality) #[serde(default, skip_serializing_if = "Option::is_none")] @@ -164,6 +166,7 @@ impl<'a> TryFrom> for CondaPackageData { constrains: value.constrains.into_owned(), depends: value.depends.into_owned(), extra_depends: value.extra_depends.into_owned(), + flags: value.flags.into_owned(), features: value.features.into_owned(), legacy_bz2_md5: value.legacy_bz2_md5, legacy_bz2_size: value.legacy_bz2_size.into_owned(), @@ -280,6 +283,7 @@ impl<'a> From<&'a CondaPackageData> for CondaPackageDataModel<'a> { depends: Cow::Borrowed(&package_record.depends), constrains: Cow::Borrowed(&package_record.constrains), extra_depends: Cow::Borrowed(&package_record.extra_depends), + flags: Cow::Borrowed(&package_record.flags), md5: package_record.md5, legacy_bz2_md5: package_record.legacy_bz2_md5, sha256: package_record.sha256, diff --git a/crates/rattler_lock/src/parse/v3.rs b/crates/rattler_lock/src/parse/v3.rs index 832612d165..f866eb6cd0 100644 --- a/crates/rattler_lock/src/parse/v3.rs +++ b/crates/rattler_lock/src/parse/v3.rs @@ -1,6 +1,11 @@ //! A module that enables parsing of lock files version 3 or lower. -use std::{collections::BTreeSet, ops::Not, str::FromStr, sync::Arc}; +use std::{ + collections::{BTreeMap, BTreeSet}, + ops::Not, + str::FromStr, + sync::Arc, +}; use fxhash::FxHashMap; use indexmap::IndexSet; @@ -202,7 +207,8 @@ pub fn parse_v3_or_lower( build_number, constrains: value.constrains, depends: value.dependencies, - extra_depends: std::collections::BTreeMap::new(), + extra_depends: BTreeMap::new(), + flags: BTreeSet::new(), features: value.features, legacy_bz2_md5: None, legacy_bz2_size: None, diff --git a/crates/rattler_solve/src/resolvo/conda_sorting.rs b/crates/rattler_solve/src/resolvo/conda_sorting.rs index d8da82ac13..e8f7fb0a60 100644 --- a/crates/rattler_solve/src/resolvo/conda_sorting.rs +++ b/crates/rattler_solve/src/resolvo/conda_sorting.rs @@ -93,6 +93,13 @@ impl<'a, 'repo> SolvableSorter<'a, 'repo> { _ => {} }; + // The one with more _flags_ is sorted lower + match a_record.flags_len().cmp(&b_record.flags_len()) { + Ordering::Greater => return Ordering::Less, + Ordering::Less => return Ordering::Greater, + Ordering::Equal => {} + } + // Otherwise, select the variant with the highest version match (self.strategy, a_record.version().cmp(b_record.version())) { (CompareStrategy::Default, Ordering::Greater) diff --git a/crates/rattler_solve/src/resolvo/mod.rs b/crates/rattler_solve/src/resolvo/mod.rs index 353ba678f7..11b1d7d33c 100644 --- a/crates/rattler_solve/src/resolvo/mod.rs +++ b/crates/rattler_solve/src/resolvo/mod.rs @@ -151,6 +151,15 @@ impl SolverPackageRecord<'_> { } } + fn flags_len(&self) -> usize { + match self { + SolverPackageRecord::Record(rec) | SolverPackageRecord::RecordWithFeature(rec, _) => { + rec.package_record.flags.len() + } + SolverPackageRecord::VirtualPackage(_rec) => 0, + } + } + fn track_features(&self) -> &[String] { const EMPTY: [String; 0] = []; match self {