From 875379ecbef3fffbacefd77edbd38d237d2af540 Mon Sep 17 00:00:00 2001 From: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Tue, 26 Mar 2024 23:39:58 +0100 Subject: [PATCH] feat: :sparkles: add dependency overrides --- src/cli/root.rs | 1 + src/dependencies/resolution.rs | 97 +++++++++++++++++++++++++--------- src/manifest.rs | 46 +++++++++++++++- src/package_name.rs | 44 +++++++-------- tests/resolver.rs | 2 + 5 files changed, 140 insertions(+), 50 deletions(-) diff --git a/src/cli/root.rs b/src/cli/root.rs index aa9e116..6256b13 100644 --- a/src/cli/root.rs +++ b/src/cli/root.rs @@ -417,6 +417,7 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> { )]), #[cfg(feature = "wally")] sourcemap_generator: None, + overrides: Default::default(), dependencies: Default::default(), peer_dependencies: Default::default(), diff --git a/src/dependencies/resolution.rs b/src/dependencies/resolution.rs index f07b797..54d6f7d 100644 --- a/src/dependencies/resolution.rs +++ b/src/dependencies/resolution.rs @@ -1,5 +1,5 @@ use std::{ - collections::{BTreeMap, BTreeSet, HashSet, VecDeque}, + collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}, fmt::Display, path::{Path, PathBuf}, }; @@ -16,7 +16,7 @@ use crate::{ DependencySpecifier, PackageRef, }, index::{Index, IndexFileEntry, IndexPackageError}, - manifest::{DependencyType, Manifest, Realm}, + manifest::{DependencyType, Manifest, OverrideKey, Realm}, package_name::PackageName, project::{get_index, get_index_by_url, Project, ReadLockfileError}, DEV_PACKAGES_FOLDER, INDEX_FOLDER, PACKAGES_FOLDER, SERVER_PACKAGES_FOLDER, @@ -29,6 +29,10 @@ pub type PackageMap = BTreeMap>; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, Default)] #[serde(deny_unknown_fields)] pub struct RootLockfileNode { + /// Dependency overrides + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub overrides: BTreeMap, + /// The specifiers of the root packages #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub specifiers: PackageMap, @@ -203,17 +207,19 @@ pub enum ResolveError { } impl Manifest { - /// Resolves the dependency graph for the project - pub fn dependency_graph( + fn missing_dependencies( &self, - project: &mut Project, + root: &mut RootLockfileNode, locked: bool, - ) -> Result { - debug!("resolving dependency graph for project {}", self.name); - // try to reuse versions (according to semver specifiers) to decrease the amount of downloads and storage - let mut root = RootLockfileNode::default(); + project: &Project, + ) -> Result, ResolveError> { + Ok(if let Some(old_root) = project.lockfile()? { + if self.overrides != old_root.overrides { + // TODO: resolve only the changed dependencies (will this be worth it?) + debug!("overrides have changed, resolving all dependencies"); + return Ok(self.dependencies()); + } - let graph = if let Some(old_root) = project.lockfile()? { debug!("lockfile found, resolving dependencies from it"); let mut missing = Vec::new(); @@ -237,10 +243,13 @@ impl Manifest { .or_default() .insert(version.clone(), specifier.unwrap().clone()); - let mut queue = VecDeque::from([resolved_package]); + let mut queue = VecDeque::from([(resolved_package, 0usize)]); - while let Some(resolved_package) = queue.pop_front() { - debug!("resolved {resolved_package} from lockfile"); + while let Some((resolved_package, depth)) = queue.pop_front() { + debug!( + "{}resolved {resolved_package} from lockfile", + "\t".repeat(depth) + ); root.children .entry(name.clone()) @@ -260,7 +269,7 @@ impl Manifest { .and_then(|v| v.get(dep_version)); match dep { - Some(dep) => queue.push_back(dep), + Some(dep) => queue.push_back((dep, depth + 1)), // the lockfile is out of date None => return Err(ResolveError::OutOfDateLockfile), } @@ -299,21 +308,45 @@ impl Manifest { } else { debug!("no lockfile found, resolving all dependencies"); self.dependencies() + }) + } + + /// Resolves the dependency graph for the project + pub fn dependency_graph( + &self, + project: &mut Project, + locked: bool, + ) -> Result { + debug!("resolving dependency graph for project {}", self.name); + // try to reuse versions (according to semver specifiers) to decrease the amount of downloads and storage + let mut root = RootLockfileNode { + overrides: self.overrides.clone(), + ..Default::default() }; - if graph.is_empty() { + let missing_dependencies = self.missing_dependencies(&mut root, locked, project)?; + + if missing_dependencies.is_empty() { debug!("no dependencies left to resolve, finishing..."); return Ok(root); } - debug!("resolving {} dependencies from index", graph.len()); + let overrides = self + .overrides + .iter() + .flat_map(|(k, spec)| k.0.iter().map(|path| (path, spec.clone()))) + .collect::>(); - let mut queue = graph + debug!("resolving {} dependencies", missing_dependencies.len()); + + let mut queue = missing_dependencies .into_iter() - .map(|(specifier, dep_type)| (specifier, dep_type, None)) + .map(|(specifier, dep_type)| (specifier, dep_type, None, vec![])) .collect::>(); - while let Some((specifier, dep_type, dependant)) = queue.pop_front() { + while let Some((specifier, dep_type, dependant, mut path)) = queue.pop_front() { + let depth = path.len(); + let (pkg_ref, default_realm, dependencies) = match &specifier { DependencySpecifier::Registry(registry_dependency) => { let index = if dependant.is_none() { @@ -331,8 +364,10 @@ impl Manifest { )?; debug!( - "resolved registry dependency {} to {}", - registry_dependency.name, entry.version + "{}resolved registry dependency {} to {}", + "\t".repeat(depth), + registry_dependency.name, + entry.version ); ( @@ -350,7 +385,8 @@ impl Manifest { git_dependency.resolve(project.cache_dir(), project.indices())?; debug!( - "resolved git dependency {} to {url}#{rev}", + "{}resolved git dependency {} to {url}#{rev}", + "\t".repeat(depth), git_dependency.repo ); @@ -383,8 +419,10 @@ impl Manifest { )?; debug!( - "resolved wally dependency {} to {}", - wally_dependency.name, entry.version + "{}resolved wally dependency {} to {}", + "\t".repeat(depth), + wally_dependency.name, + entry.version ); ( @@ -473,11 +511,20 @@ impl Manifest { }, ); + path.push(pkg_ref.name().to_string()); + for (specifier, ty) in dependencies { + let overridden = overrides.iter().find_map(|(k_path, spec)| { + (&path == &k_path[..k_path.len() - 1] + && k_path.get(k_path.len() - 1) == Some(&specifier.name())) + .then_some(spec) + }); + queue.push_back(( - specifier, + overridden.cloned().unwrap_or(specifier), ty, Some((pkg_ref.name(), pkg_ref.version().clone())), + path.clone(), )); } } diff --git a/src/manifest.rs b/src/manifest.rs index beab16c..cc376ea 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -1,6 +1,6 @@ -use cfg_if::cfg_if; use std::{collections::BTreeMap, fmt::Display, fs::read, str::FromStr}; +use cfg_if::cfg_if; use relative_path::RelativePathBuf; use semver::Version; use serde::{Deserialize, Serialize}; @@ -108,6 +108,46 @@ impl FromStr for Realm { } } +/// A key to override dependencies +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct OverrideKey(pub Vec>); + +impl Serialize for OverrideKey { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str( + &self + .0 + .iter() + .map(|overrides| { + overrides + .iter() + .map(String::to_string) + .collect::>() + .join(">") + }) + .collect::>() + .join(","), + ) + } +} + +impl<'de> Deserialize<'de> for OverrideKey { + fn deserialize>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + let mut key = Vec::new(); + for overrides in s.split(',') { + key.push( + overrides + .split('>') + .map(|s| String::from_str(s).map_err(serde::de::Error::custom)) + .collect::, _>>()?, + ); + } + + Ok(OverrideKey(key)) + } +} + /// The manifest of a package #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Manifest { @@ -144,6 +184,9 @@ pub struct Manifest { #[cfg(feature = "wally")] #[serde(default)] pub sourcemap_generator: Option, + /// Dependency overrides + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub overrides: BTreeMap, /// The dependencies of the package #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -292,6 +335,7 @@ impl Manifest { "".to_string(), )]), sourcemap_generator: None, + overrides: BTreeMap::new(), dependencies, peer_dependencies: Vec::new(), diff --git a/src/package_name.rs b/src/package_name.rs index 28c1627..76f7a81 100644 --- a/src/package_name.rs +++ b/src/package_name.rs @@ -130,7 +130,7 @@ const SEPARATOR: char = '/'; const ESCAPED_SEPARATOR: char = '+'; macro_rules! name_impl { - ($Name:ident, $Error:ident, $Visitor:ident, $validate:expr, $prefix:expr) => { + ($Name:ident, $Error:ident, $validate:expr, $prefix:expr) => { impl $Name { /// Creates a new package name pub fn new(scope: &str, name: &str) -> Result { @@ -209,36 +209,34 @@ macro_rules! name_impl { } } - impl<'de> Visitor<'de> for $Visitor { - type Value = $Name; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - formatter, - "a string in the format `{}scope{SEPARATOR}name`", - $prefix - ) - } - - fn visit_str(self, v: &str) -> Result { - v.parse().map_err(|e| E::custom(e)) - } - } - impl<'de> Deserialize<'de> for $Name { fn deserialize>( deserializer: D, ) -> Result<$Name, D::Error> { - deserializer.deserialize_str($Visitor) + struct NameVisitor; + + impl<'de> Visitor<'de> for NameVisitor { + type Value = $Name; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + formatter, + "a string in the format `{}scope{SEPARATOR}name`", + $prefix + ) + } + + fn visit_str(self, v: &str) -> Result { + v.parse().map_err(E::custom) + } + } + + deserializer.deserialize_str(NameVisitor) } } }; } -struct StandardPackageNameVisitor; -#[cfg(feature = "wally")] -struct WallyPackageNameVisitor; - /// A package name #[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[serde(untagged)] @@ -323,7 +321,6 @@ impl From for PackageName { name_impl!( StandardPackageName, StandardPackageNameValidationError, - StandardPackageNameVisitor, validate_part, "" ); @@ -332,7 +329,6 @@ name_impl!( name_impl!( WallyPackageName, WallyPackageNameValidationError, - WallyPackageNameVisitor, validate_wally_part, "wally#" ); diff --git a/tests/resolver.rs b/tests/resolver.rs index 6e7fedf..359be44 100644 --- a/tests/resolver.rs +++ b/tests/resolver.rs @@ -43,6 +43,7 @@ fn test_resolves_package() { indices: Default::default(), #[cfg(feature = "wally")] sourcemap_generator: None, + overrides: Default::default(), dependencies: vec![], peer_dependencies: vec![], @@ -83,6 +84,7 @@ fn test_resolves_package() { indices: Default::default(), #[cfg(feature = "wally")] sourcemap_generator: None, + overrides: Default::default(), dependencies: vec![specifier.clone()], peer_dependencies: vec![specifier_2.clone()],