use std::{borrow::Cow, sync::Arc}; use uv_normalize::{DEV_DEPENDENCIES, DefaultGroups, GroupName}; /// Manager of all dependency-group decisions and settings history. /// /// This is an Arc mostly just to avoid size bloat on things that contain these. #[derive(Debug, Default, Clone)] pub struct DependencyGroups(Arc); /// Manager of all dependency-group decisions and settings history. #[derive(Debug, Default, Clone)] pub struct DependencyGroupsInner { /// Groups to include. include: IncludeGroups, /// Groups to exclude (always wins over include). exclude: Vec, /// Whether an `--only` flag was passed. /// /// If true, users of this API should refrain from looking at packages /// that *aren't* specified by the dependency-groups. This is exposed /// via [`DependencyGroupsInner::prod`][]. only_groups: bool, /// The "raw" flags/settings we were passed for diagnostics. history: DependencyGroupsHistory, } impl DependencyGroups { /// Create from history. /// /// This is the "real" constructor, it's basically taking raw CLI flags but in /// a way that's a bit nicer for other constructors to use. fn from_history(history: DependencyGroupsHistory) -> Self { let DependencyGroupsHistory { dev_mode, mut group, mut only_group, mut no_group, all_groups, no_default_groups, mut defaults, } = history.clone(); // First desugar --dev flags match dev_mode { Some(DevMode::Include) => group.push(DEV_DEPENDENCIES.clone()), Some(DevMode::Only) => only_group.push(DEV_DEPENDENCIES.clone()), Some(DevMode::Exclude) => no_group.push(DEV_DEPENDENCIES.clone()), None => {} } // `group` and `only_group` actually have the same meanings: packages to include. // But if `only_group` is non-empty then *other* packages should be excluded. // So we just record whether it was and then treat the two lists as equivalent. let only_groups = !only_group.is_empty(); // --only flags imply --no-default-groups let default_groups = !no_default_groups && !only_groups; let include = if all_groups { // If this is set we can ignore group/only_group/defaults as irrelevant // (`--all-groups --only-*` is rejected at the CLI level, don't worry about it). IncludeGroups::All } else { // Merge all these lists, they're equivalent now group.append(&mut only_group); // Resolve default groups potentially also setting All if default_groups { match &mut defaults { DefaultGroups::All => IncludeGroups::All, DefaultGroups::List(defaults) => { group.append(defaults); IncludeGroups::Some(group) } } } else { IncludeGroups::Some(group) } }; Self(Arc::new(DependencyGroupsInner { include, exclude: no_group, only_groups, history, })) } /// Create from raw CLI args #[allow(clippy::fn_params_excessive_bools)] pub fn from_args( dev: bool, no_dev: bool, only_dev: bool, group: Vec, no_group: Vec, no_default_groups: bool, only_group: Vec, all_groups: bool, ) -> Self { // Lower the --dev flags into a single dev mode. // // In theory only one of these 3 flags should be set (enforced by CLI), // but we explicitly allow `--dev` and `--only-dev` to both be set, // and "saturate" that to `--only-dev`. let dev_mode = if only_dev { Some(DevMode::Only) } else if no_dev { Some(DevMode::Exclude) } else if dev { Some(DevMode::Include) } else { None }; Self::from_history(DependencyGroupsHistory { dev_mode, group, only_group, no_group, all_groups, no_default_groups, // This is unknown at CLI-time, use `.with_defaults(...)` to apply this later! defaults: DefaultGroups::default(), }) } /// Helper to make a spec from just a --dev flag pub fn from_dev_mode(dev_mode: DevMode) -> Self { Self::from_history(DependencyGroupsHistory { dev_mode: Some(dev_mode), ..Default::default() }) } /// Helper to make a spec from just a --group pub fn from_group(group: GroupName) -> Self { Self::from_history(DependencyGroupsHistory { group: vec![group], ..Default::default() }) } /// Apply defaults to a base [`DependencyGroups`]. /// /// This is appropriate in projects, where the `dev` group is synced by default. pub fn with_defaults(&self, defaults: DefaultGroups) -> DependencyGroupsWithDefaults { // Explicitly clone the inner history and set the defaults, then remake the result. let mut history = self.0.history.clone(); history.defaults = defaults; DependencyGroupsWithDefaults { cur: Self::from_history(history), prev: self.clone(), } } } impl std::ops::Deref for DependencyGroups { type Target = DependencyGroupsInner; fn deref(&self) -> &Self::Target { &self.0 } } impl DependencyGroupsInner { /// Returns `true` if packages other than the ones referenced by these /// dependency-groups should be considered. /// /// That is, if I tell you to install a project and this is false, /// you should ignore the project itself and all its dependencies, /// and instead just install the dependency-groups. /// /// (This is really just asking if an --only flag was passed.) pub fn prod(&self) -> bool { !self.only_groups } /// Returns `true` if the specification includes the given group. pub fn contains(&self, group: &GroupName) -> bool { // exclude always trumps include !self.exclude.contains(group) && self.include.contains(group) } /// Iterate over all groups that we think should exist. pub fn desugarred_names(&self) -> impl Iterator { self.include.names().chain(&self.exclude) } /// Returns an iterator over all groups that are included in the specification, /// assuming `all_names` is an iterator over all groups. pub fn group_names<'a, Names>( &'a self, all_names: Names, ) -> impl Iterator + 'a where Names: Iterator + 'a, { all_names.filter(move |name| self.contains(name)) } /// Iterate over all groups the user explicitly asked for on the CLI pub fn explicit_names(&self) -> impl Iterator { let DependencyGroupsHistory { // Strictly speaking this is an explicit reference to "dev" // but we're currently tolerant of dev not existing when referenced with // these flags, since it kinda implicitly always exists even if // it's not properly defined in a config file. dev_mode: _, group, only_group, no_group, // These reference no groups explicitly all_groups: _, no_default_groups: _, // This doesn't include defaults because the `dev` group may not be defined // but gets implicitly added as a default sometimes! defaults: _, } = self.history(); group.iter().chain(no_group).chain(only_group) } /// Returns `true` if the specification will have no effect. pub fn is_empty(&self) -> bool { self.prod() && self.exclude.is_empty() && self.include.is_empty() } /// Get the raw history for diagnostics pub fn history(&self) -> &DependencyGroupsHistory { &self.history } } /// Context about a [`DependencyGroups`][] that we've preserved for diagnostics #[derive(Debug, Default, Clone)] pub struct DependencyGroupsHistory { pub dev_mode: Option, pub group: Vec, pub only_group: Vec, pub no_group: Vec, pub all_groups: bool, pub no_default_groups: bool, pub defaults: DefaultGroups, } impl DependencyGroupsHistory { /// Returns all the CLI flags that this represents. /// /// If a flag was provided multiple times (e.g. `--group A --group B`) this will /// elide the arguments and just show the flag once (e.g. just yield "--group"). /// /// Conceptually this being an empty list should be equivalent to /// [`DependencyGroups::is_empty`][] when there aren't any defaults set. /// When there are defaults the two will disagree, and rightfully so! pub fn as_flags_pretty(&self) -> Vec> { let Self { dev_mode, group, only_group, no_group, all_groups, no_default_groups, // defaults aren't CLI flags! defaults: _, } = self; let mut flags = vec![]; if *all_groups { flags.push(Cow::Borrowed("--all-groups")); } if *no_default_groups { flags.push(Cow::Borrowed("--no-default-groups")); } if let Some(dev_mode) = dev_mode { flags.push(Cow::Borrowed(dev_mode.as_flag())); } match &**group { [] => {} [group] => flags.push(Cow::Owned(format!("--group {group}"))), [..] => flags.push(Cow::Borrowed("--group")), } match &**only_group { [] => {} [group] => flags.push(Cow::Owned(format!("--only-group {group}"))), [..] => flags.push(Cow::Borrowed("--only-group")), } match &**no_group { [] => {} [group] => flags.push(Cow::Owned(format!("--no-group {group}"))), [..] => flags.push(Cow::Borrowed("--no-group")), } flags } } /// A trivial newtype wrapped around [`DependencyGroups`][] that signifies "defaults applied" /// /// It includes a copy of the previous semantics to provide info on if /// the group being a default actually affected it being enabled, because it's obviously "correct". /// (These are Arcs so it's ~free to hold onto the previous semantics) #[derive(Debug, Clone)] pub struct DependencyGroupsWithDefaults { /// The active semantics cur: DependencyGroups, /// The semantics before defaults were applied prev: DependencyGroups, } impl DependencyGroupsWithDefaults { /// Do not enable any groups /// /// Many places in the code need to know what dependency-groups are active, /// but various commands or subsystems never enable any dependency-groups, /// in which case they want this. pub fn none() -> Self { DependencyGroups::default().with_defaults(DefaultGroups::default()) } /// Returns `true` if the specification was enabled, and *only* because it was a default pub fn contains_because_default(&self, group: &GroupName) -> bool { self.cur.contains(group) && !self.prev.contains(group) } } impl std::ops::Deref for DependencyGroupsWithDefaults { type Target = DependencyGroups; fn deref(&self) -> &Self::Target { &self.cur } } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum DevMode { /// Include development dependencies. #[default] Include, /// Exclude development dependencies. Exclude, /// Only include development dependencies, excluding all other dependencies. Only, } impl DevMode { /// Returns the flag that was used to request development dependencies. pub fn as_flag(&self) -> &'static str { match self { Self::Exclude => "--no-dev", Self::Include => "--dev", Self::Only => "--only-dev", } } } #[derive(Debug, Clone)] pub enum IncludeGroups { /// Include dependencies from the specified groups. Some(Vec), /// A marker indicates including dependencies from all groups. All, } impl IncludeGroups { /// Returns `true` if the specification includes the given group. pub fn contains(&self, group: &GroupName) -> bool { match self { Self::Some(groups) => groups.contains(group), Self::All => true, } } /// Returns `true` if the specification will have no effect. pub fn is_empty(&self) -> bool { match self { Self::Some(groups) => groups.is_empty(), // Although technically this is a noop if they have no groups, // conceptually they're *trying* to have an effect, so treat it as one. Self::All => false, } } /// Iterate over all groups referenced in the [`IncludeGroups`]. pub fn names(&self) -> std::slice::Iter<'_, GroupName> { match self { Self::Some(groups) => groups.iter(), Self::All => [].iter(), } } } impl Default for IncludeGroups { fn default() -> Self { Self::Some(Vec::new()) } }