feat: add dependency overrides

This commit is contained in:
daimond113 2024-03-26 23:39:58 +01:00
parent 3a061a9fbe
commit 875379ecbe
No known key found for this signature in database
GPG key ID: 3A8ECE51328B513C
5 changed files with 140 additions and 50 deletions

View file

@ -417,6 +417,7 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
)]), )]),
#[cfg(feature = "wally")] #[cfg(feature = "wally")]
sourcemap_generator: None, sourcemap_generator: None,
overrides: Default::default(),
dependencies: Default::default(), dependencies: Default::default(),
peer_dependencies: Default::default(), peer_dependencies: Default::default(),

View file

@ -1,5 +1,5 @@
use std::{ use std::{
collections::{BTreeMap, BTreeSet, HashSet, VecDeque}, collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
fmt::Display, fmt::Display,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@ -16,7 +16,7 @@ use crate::{
DependencySpecifier, PackageRef, DependencySpecifier, PackageRef,
}, },
index::{Index, IndexFileEntry, IndexPackageError}, index::{Index, IndexFileEntry, IndexPackageError},
manifest::{DependencyType, Manifest, Realm}, manifest::{DependencyType, Manifest, OverrideKey, Realm},
package_name::PackageName, package_name::PackageName,
project::{get_index, get_index_by_url, Project, ReadLockfileError}, project::{get_index, get_index_by_url, Project, ReadLockfileError},
DEV_PACKAGES_FOLDER, INDEX_FOLDER, PACKAGES_FOLDER, SERVER_PACKAGES_FOLDER, DEV_PACKAGES_FOLDER, INDEX_FOLDER, PACKAGES_FOLDER, SERVER_PACKAGES_FOLDER,
@ -29,6 +29,10 @@ pub type PackageMap<T> = BTreeMap<PackageName, BTreeMap<Version, T>>;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, Default)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, Default)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct RootLockfileNode { pub struct RootLockfileNode {
/// Dependency overrides
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub overrides: BTreeMap<OverrideKey, DependencySpecifier>,
/// The specifiers of the root packages /// The specifiers of the root packages
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")] #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub specifiers: PackageMap<DependencySpecifier>, pub specifiers: PackageMap<DependencySpecifier>,
@ -203,17 +207,19 @@ pub enum ResolveError {
} }
impl Manifest { impl Manifest {
/// Resolves the dependency graph for the project fn missing_dependencies(
pub fn dependency_graph(
&self, &self,
project: &mut Project, root: &mut RootLockfileNode,
locked: bool, locked: bool,
) -> Result<RootLockfileNode, ResolveError> { project: &Project,
debug!("resolving dependency graph for project {}", self.name); ) -> Result<Vec<(DependencySpecifier, DependencyType)>, ResolveError> {
// try to reuse versions (according to semver specifiers) to decrease the amount of downloads and storage Ok(if let Some(old_root) = project.lockfile()? {
let mut root = RootLockfileNode::default(); 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"); debug!("lockfile found, resolving dependencies from it");
let mut missing = Vec::new(); let mut missing = Vec::new();
@ -237,10 +243,13 @@ impl Manifest {
.or_default() .or_default()
.insert(version.clone(), specifier.unwrap().clone()); .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() { while let Some((resolved_package, depth)) = queue.pop_front() {
debug!("resolved {resolved_package} from lockfile"); debug!(
"{}resolved {resolved_package} from lockfile",
"\t".repeat(depth)
);
root.children root.children
.entry(name.clone()) .entry(name.clone())
@ -260,7 +269,7 @@ impl Manifest {
.and_then(|v| v.get(dep_version)); .and_then(|v| v.get(dep_version));
match dep { match dep {
Some(dep) => queue.push_back(dep), Some(dep) => queue.push_back((dep, depth + 1)),
// the lockfile is out of date // the lockfile is out of date
None => return Err(ResolveError::OutOfDateLockfile), None => return Err(ResolveError::OutOfDateLockfile),
} }
@ -299,21 +308,45 @@ impl Manifest {
} else { } else {
debug!("no lockfile found, resolving all dependencies"); debug!("no lockfile found, resolving all dependencies");
self.dependencies() self.dependencies()
})
}
/// Resolves the dependency graph for the project
pub fn dependency_graph(
&self,
project: &mut Project,
locked: bool,
) -> Result<RootLockfileNode, ResolveError> {
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..."); debug!("no dependencies left to resolve, finishing...");
return Ok(root); 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::<HashMap<_, _>>();
let mut queue = graph debug!("resolving {} dependencies", missing_dependencies.len());
let mut queue = missing_dependencies
.into_iter() .into_iter()
.map(|(specifier, dep_type)| (specifier, dep_type, None)) .map(|(specifier, dep_type)| (specifier, dep_type, None, vec![]))
.collect::<VecDeque<_>>(); .collect::<VecDeque<_>>();
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 { let (pkg_ref, default_realm, dependencies) = match &specifier {
DependencySpecifier::Registry(registry_dependency) => { DependencySpecifier::Registry(registry_dependency) => {
let index = if dependant.is_none() { let index = if dependant.is_none() {
@ -331,8 +364,10 @@ impl Manifest {
)?; )?;
debug!( debug!(
"resolved registry dependency {} to {}", "{}resolved registry dependency {} to {}",
registry_dependency.name, entry.version "\t".repeat(depth),
registry_dependency.name,
entry.version
); );
( (
@ -350,7 +385,8 @@ impl Manifest {
git_dependency.resolve(project.cache_dir(), project.indices())?; git_dependency.resolve(project.cache_dir(), project.indices())?;
debug!( debug!(
"resolved git dependency {} to {url}#{rev}", "{}resolved git dependency {} to {url}#{rev}",
"\t".repeat(depth),
git_dependency.repo git_dependency.repo
); );
@ -383,8 +419,10 @@ impl Manifest {
)?; )?;
debug!( debug!(
"resolved wally dependency {} to {}", "{}resolved wally dependency {} to {}",
wally_dependency.name, entry.version "\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 { 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(( queue.push_back((
specifier, overridden.cloned().unwrap_or(specifier),
ty, ty,
Some((pkg_ref.name(), pkg_ref.version().clone())), Some((pkg_ref.name(), pkg_ref.version().clone())),
path.clone(),
)); ));
} }
} }

View file

@ -1,6 +1,6 @@
use cfg_if::cfg_if;
use std::{collections::BTreeMap, fmt::Display, fs::read, str::FromStr}; use std::{collections::BTreeMap, fmt::Display, fs::read, str::FromStr};
use cfg_if::cfg_if;
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
use semver::Version; use semver::Version;
use serde::{Deserialize, Serialize}; 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<Vec<String>>);
impl Serialize for OverrideKey {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(
&self
.0
.iter()
.map(|overrides| {
overrides
.iter()
.map(String::to_string)
.collect::<Vec<_>>()
.join(">")
})
.collect::<Vec<_>>()
.join(","),
)
}
}
impl<'de> Deserialize<'de> for OverrideKey {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
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::<Result<Vec<_>, _>>()?,
);
}
Ok(OverrideKey(key))
}
}
/// The manifest of a package /// The manifest of a package
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Manifest { pub struct Manifest {
@ -144,6 +184,9 @@ pub struct Manifest {
#[cfg(feature = "wally")] #[cfg(feature = "wally")]
#[serde(default)] #[serde(default)]
pub sourcemap_generator: Option<String>, pub sourcemap_generator: Option<String>,
/// Dependency overrides
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub overrides: BTreeMap<OverrideKey, DependencySpecifier>,
/// The dependencies of the package /// The dependencies of the package
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
@ -292,6 +335,7 @@ impl Manifest {
"".to_string(), "".to_string(),
)]), )]),
sourcemap_generator: None, sourcemap_generator: None,
overrides: BTreeMap::new(),
dependencies, dependencies,
peer_dependencies: Vec::new(), peer_dependencies: Vec::new(),

View file

@ -130,7 +130,7 @@ const SEPARATOR: char = '/';
const ESCAPED_SEPARATOR: char = '+'; const ESCAPED_SEPARATOR: char = '+';
macro_rules! name_impl { macro_rules! name_impl {
($Name:ident, $Error:ident, $Visitor:ident, $validate:expr, $prefix:expr) => { ($Name:ident, $Error:ident, $validate:expr, $prefix:expr) => {
impl $Name { impl $Name {
/// Creates a new package name /// Creates a new package name
pub fn new(scope: &str, name: &str) -> Result<Self, $Error> { pub fn new(scope: &str, name: &str) -> Result<Self, $Error> {
@ -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<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
v.parse().map_err(|e| E::custom(e))
}
}
impl<'de> Deserialize<'de> for $Name { impl<'de> Deserialize<'de> for $Name {
fn deserialize<D: serde::Deserializer<'de>>( fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D, deserializer: D,
) -> Result<$Name, D::Error> { ) -> 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<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
v.parse().map_err(E::custom)
}
}
deserializer.deserialize_str(NameVisitor)
} }
} }
}; };
} }
struct StandardPackageNameVisitor;
#[cfg(feature = "wally")]
struct WallyPackageNameVisitor;
/// A package name /// A package name
#[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(untagged)] #[serde(untagged)]
@ -323,7 +321,6 @@ impl From<WallyPackageName> for PackageName {
name_impl!( name_impl!(
StandardPackageName, StandardPackageName,
StandardPackageNameValidationError, StandardPackageNameValidationError,
StandardPackageNameVisitor,
validate_part, validate_part,
"" ""
); );
@ -332,7 +329,6 @@ name_impl!(
name_impl!( name_impl!(
WallyPackageName, WallyPackageName,
WallyPackageNameValidationError, WallyPackageNameValidationError,
WallyPackageNameVisitor,
validate_wally_part, validate_wally_part,
"wally#" "wally#"
); );

View file

@ -43,6 +43,7 @@ fn test_resolves_package() {
indices: Default::default(), indices: Default::default(),
#[cfg(feature = "wally")] #[cfg(feature = "wally")]
sourcemap_generator: None, sourcemap_generator: None,
overrides: Default::default(),
dependencies: vec![], dependencies: vec![],
peer_dependencies: vec![], peer_dependencies: vec![],
@ -83,6 +84,7 @@ fn test_resolves_package() {
indices: Default::default(), indices: Default::default(),
#[cfg(feature = "wally")] #[cfg(feature = "wally")]
sourcemap_generator: None, sourcemap_generator: None,
overrides: Default::default(),
dependencies: vec![specifier.clone()], dependencies: vec![specifier.clone()],
peer_dependencies: vec![specifier_2.clone()], peer_dependencies: vec![specifier_2.clone()],