refactor(resolver): 🎨 improve lockfile format

This commit is contained in:
daimond113 2024-03-25 17:29:31 +01:00
parent 2265aa5b36
commit 3a061a9fbe
No known key found for this signature in database
GPG key ID: 3A8ECE51328B513C
10 changed files with 294 additions and 279 deletions

View file

@ -80,15 +80,10 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
.indices .indices
.clone() .clone()
.into_iter() .into_iter()
.map(|(k, v)| (k, Box::new(clone_index(&v)) as Box<dyn Index>)); .map(|(k, v)| (k, Box::new(clone_index(&v)) as Box<dyn Index>))
.collect::<HashMap<_, _>>();
Project::new( Project::new(CWD.to_path_buf(), CLI_CONFIG.cache_dir(), indices, manifest).unwrap()
CWD.to_path_buf(),
CLI_CONFIG.cache_dir(),
HashMap::from_iter(indices),
manifest,
)
.unwrap()
}); });
match cmd { match cmd {
@ -104,18 +99,18 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
} }
let manifest = project.manifest().clone(); let manifest = project.manifest().clone();
let resolved_versions_map = manifest.dependency_tree(&mut project, locked)?; let lockfile = manifest.dependency_graph(&mut project, locked)?;
let download_job = project.download(resolved_versions_map.clone())?; let download_job = project.download(&lockfile)?;
multithreaded_bar( multithreaded_bar(
download_job, download_job,
resolved_versions_map.len() as u64, lockfile.children.len() as u64,
"Downloading packages".to_string(), "Downloading packages".to_string(),
)?; )?;
#[allow(unused_variables)] #[allow(unused_variables)]
project.convert_manifests(&resolved_versions_map, |path| { project.convert_manifests(&lockfile, |path| {
cfg_if! { cfg_if! {
if #[cfg(feature = "wally")] { if #[cfg(feature = "wally")] {
if let Some(sourcemap_generator) = &manifest.sourcemap_generator { if let Some(sourcemap_generator) = &manifest.sourcemap_generator {
@ -145,7 +140,7 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
InstallOptions::new() InstallOptions::new()
.locked(locked) .locked(locked)
.auto_download(false) .auto_download(false)
.resolved_versions_map(resolved_versions_map), .lockfile(lockfile),
)?; )?;
} }
Command::Run { package, args } => { Command::Run { package, args } => {
@ -153,17 +148,18 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
.lockfile()? .lockfile()?
.ok_or(anyhow::anyhow!("lockfile not found"))?; .ok_or(anyhow::anyhow!("lockfile not found"))?;
let (_, resolved_pkg) = lockfile let resolved_pkg = lockfile
.children
.get(&package.into()) .get(&package.into())
.and_then(|versions| versions.iter().find(|(_, pkg_ref)| pkg_ref.is_root)) .and_then(|versions| {
versions
.values()
.find(|pkg_ref| lockfile.root_specifier(pkg_ref).is_some())
})
.ok_or(anyhow::anyhow!( .ok_or(anyhow::anyhow!(
"package not found in lockfile (or isn't root)" "package not found in lockfile (or isn't root)"
))?; ))?;
if !resolved_pkg.is_root {
anyhow::bail!("package is not a root package");
}
let pkg_path = resolved_pkg.directory(project.path()).1; let pkg_path = resolved_pkg.directory(project.path()).1;
let manifest = Manifest::from_path(&pkg_path)?; let manifest = Manifest::from_path(&pkg_path)?;
@ -278,6 +274,7 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
.ok_or(anyhow::anyhow!("lockfile not found"))?; .ok_or(anyhow::anyhow!("lockfile not found"))?;
let resolved_pkg = lockfile let resolved_pkg = lockfile
.children
.get(&package.0) .get(&package.0)
.and_then(|versions| versions.get(&package.1)) .and_then(|versions| versions.get(&package.1))
.ok_or(anyhow::anyhow!("package not found in lockfile"))?; .ok_or(anyhow::anyhow!("package not found in lockfile"))?;
@ -509,15 +506,15 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
let project = Lazy::force_mut(&mut project); let project = Lazy::force_mut(&mut project);
let manifest = project.manifest().clone(); let manifest = project.manifest().clone();
let dependency_tree = manifest.dependency_tree(project, false)?; let lockfile = manifest.dependency_graph(project, false)?;
for (name, versions) in dependency_tree { for (name, versions) in &lockfile.children {
for (version, resolved_pkg) in versions { for (version, resolved_pkg) in versions {
if !resolved_pkg.is_root { if lockfile.root_specifier(resolved_pkg).is_none() {
continue; continue;
} }
if let PackageRef::Registry(ref registry) = resolved_pkg.pkg_ref { if let PackageRef::Registry(registry) = &resolved_pkg.pkg_ref {
let latest_version = send_request(REQWEST_CLIENT.get(format!( let latest_version = send_request(REQWEST_CLIENT.get(format!(
"{}/v0/packages/{}/{}/versions", "{}/v0/packages/{}/{}/versions",
resolved_pkg.pkg_ref.get_index(project).config()?.api(), resolved_pkg.pkg_ref.get_index(project).config()?.api(),
@ -533,7 +530,7 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
"failed to get latest version of {name}@{version}" "failed to get latest version of {name}@{version}"
))?; ))?;
if latest_version > version { if &latest_version > version {
println!( println!(
"{name}@{version} is outdated. latest version: {latest_version}" "{name}@{version} is outdated. latest version: {latest_version}"
); );

View file

@ -18,7 +18,7 @@ use crate::{
dependencies::{ dependencies::{
git::{GitDependencySpecifier, GitPackageRef}, git::{GitDependencySpecifier, GitPackageRef},
registry::{RegistryDependencySpecifier, RegistryPackageRef}, registry::{RegistryDependencySpecifier, RegistryPackageRef},
resolution::ResolvedVersionsMap, resolution::RootLockfileNode,
}, },
index::{CredentialsFn, Index}, index::{CredentialsFn, Index},
manifest::{Manifest, Realm}, manifest::{Manifest, Realm},
@ -274,11 +274,11 @@ impl Project {
/// Downloads the project's dependencies /// Downloads the project's dependencies
pub fn download( pub fn download(
&mut self, &mut self,
map: ResolvedVersionsMap, lockfile: &RootLockfileNode,
) -> Result<MultithreadedJob<DownloadError>, InstallProjectError> { ) -> Result<MultithreadedJob<DownloadError>, InstallProjectError> {
let (job, tx) = MultithreadedJob::new(); let (job, tx) = MultithreadedJob::new();
for (name, versions) in map.clone() { for (name, versions) in lockfile.children.clone() {
for (version, resolved_package) in versions { for (version, resolved_package) in versions {
let (_, source) = resolved_package.directory(self.path()); let (_, source) = resolved_package.directory(self.path());
@ -319,7 +319,7 @@ impl Project {
#[cfg(feature = "wally")] #[cfg(feature = "wally")]
pub fn convert_manifests<F: Fn(PathBuf)>( pub fn convert_manifests<F: Fn(PathBuf)>(
&self, &self,
map: &ResolvedVersionsMap, lockfile: &RootLockfileNode,
generate_sourcemap: F, generate_sourcemap: F,
) -> Result<(), ConvertManifestsError> { ) -> Result<(), ConvertManifestsError> {
#[derive(Deserialize)] #[derive(Deserialize)]
@ -329,7 +329,7 @@ impl Project {
file_paths: Vec<relative_path::RelativePathBuf>, file_paths: Vec<relative_path::RelativePathBuf>,
} }
for versions in map.values() { for versions in lockfile.children.values() {
for resolved_package in versions.values() { for resolved_package in versions.values() {
let source = match &resolved_package.pkg_ref { let source = match &resolved_package.pkg_ref {
PackageRef::Wally(_) | PackageRef::Git(_) => { PackageRef::Wally(_) | PackageRef::Git(_) => {
@ -373,10 +373,10 @@ impl Project {
#[cfg(not(feature = "wally"))] #[cfg(not(feature = "wally"))]
pub fn convert_manifests<F: Fn(PathBuf)>( pub fn convert_manifests<F: Fn(PathBuf)>(
&self, &self,
map: &ResolvedVersionsMap, lockfile: &RootLockfileNode,
_generate_sourcemap: F, _generate_sourcemap: F,
) -> Result<(), ConvertManifestsError> { ) -> Result<(), ConvertManifestsError> {
for versions in map.values() { for versions in lockfile.children.values() {
for resolved_package in versions.values() { for resolved_package in versions.values() {
let source = match &resolved_package.pkg_ref { let source = match &resolved_package.pkg_ref {
PackageRef::Git(_) => resolved_package.directory(self.path()).1, PackageRef::Git(_) => resolved_package.directory(self.path()).1,

View file

@ -1,45 +1,68 @@
use std::{ use std::{
collections::{BTreeMap, BTreeSet, VecDeque}, collections::{BTreeMap, BTreeSet, HashSet, VecDeque},
fmt::Display, fmt::Display,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use log::debug; use log::debug;
use semver::Version; use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
#[cfg(feature = "wally")]
use crate::index::Index;
use crate::{ use crate::{
dependencies::{ dependencies::{
git::{GitDownloadError, GitPackageRef}, git::{GitDownloadError, GitPackageRef},
registry::RegistryPackageRef, registry::RegistryPackageRef,
DependencySpecifier, PackageRef, DependencySpecifier, PackageRef,
}, },
index::IndexPackageError, index::{Index, IndexFileEntry, IndexPackageError},
manifest::{DependencyType, Manifest, Realm}, manifest::{DependencyType, Manifest, 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,
}; };
/// A node in the dependency tree /// A mapping of packages to something
pub type PackageMap<T> = BTreeMap<PackageName, BTreeMap<Version, T>>;
/// The root node of the dependency graph
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, Default)]
#[serde(deny_unknown_fields)]
pub struct RootLockfileNode {
/// The specifiers of the root packages
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub specifiers: PackageMap<DependencySpecifier>,
/// All nodes in the dependency graph
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub children: PackageMap<ResolvedPackage>,
}
impl RootLockfileNode {
/// Returns the specifier of the root package
pub fn root_specifier(
&self,
resolved_package: &ResolvedPackage,
) -> Option<&DependencySpecifier> {
self.specifiers
.get(&resolved_package.pkg_ref.name())
.and_then(|versions| versions.get(resolved_package.pkg_ref.version()))
}
}
/// A node in the dependency graph
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct ResolvedPackage { pub struct ResolvedPackage {
/// The reference to the package /// The reference to the package
pub pkg_ref: PackageRef, pub pkg_ref: PackageRef,
/// The specifier that resolved to this package
pub specifier: DependencySpecifier,
/// The dependencies of the package /// The dependencies of the package
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")] #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub dependencies: BTreeSet<(PackageName, Version)>, pub dependencies: BTreeSet<(PackageName, Version)>,
/// Whether the package is a root package (top-level dependency)
pub is_root: bool,
/// The realm of the package /// The realm of the package
pub realm: Realm, pub realm: Realm,
/// The type of the dependency /// The type of the dependency
#[serde(default, skip_serializing_if = "crate::is_default")]
pub dep_type: DependencyType, pub dep_type: DependencyType,
} }
@ -49,7 +72,7 @@ impl Display for ResolvedPackage {
} }
} }
pub(crate) fn packages_folder(realm: &Realm) -> &str { pub(crate) fn packages_folder<'a>(realm: Realm) -> &'a str {
match realm { match realm {
Realm::Shared => PACKAGES_FOLDER, Realm::Shared => PACKAGES_FOLDER,
Realm::Server => SERVER_PACKAGES_FOLDER, Realm::Server => SERVER_PACKAGES_FOLDER,
@ -59,7 +82,7 @@ pub(crate) fn packages_folder(realm: &Realm) -> &str {
impl ResolvedPackage { impl ResolvedPackage {
pub(crate) fn packages_folder(&self) -> &str { pub(crate) fn packages_folder(&self) -> &str {
packages_folder(&self.realm) packages_folder(self.realm)
} }
/// Returns the directory of the package in the project, and the parent of the directory /// Returns the directory of the package in the project, and the parent of the directory
@ -76,18 +99,44 @@ impl ResolvedPackage {
} }
} }
/// A flat resolved map, a map of package names to versions to resolved packages
pub type ResolvedVersionsMap = BTreeMap<PackageName, BTreeMap<Version, ResolvedPackage>>;
macro_rules! find_highest { macro_rules! find_highest {
($iter:expr, $dep:expr) => { ($iter:expr, $version:expr) => {
$iter $iter
.filter(|v| $dep.version.matches(v)) .filter(|v| $version.matches(v))
.max_by(|a, b| a.cmp(&b)) .max_by(|a, b| a.cmp(&b))
.cloned() .cloned()
}; };
} }
fn find_version_from_index(
root: &mut RootLockfileNode,
index: &dyn Index,
specifier: &DependencySpecifier,
name: PackageName,
version_req: &VersionReq,
) -> Result<IndexFileEntry, ResolveError> {
let index_entries = index
.package(&name)
.map_err(|e| ResolveError::IndexPackage(e, name.to_string()))?
.ok_or_else(|| ResolveError::PackageNotFound(name.to_string()))?;
let resolved_versions = root.children.entry(name).or_default();
// try to find the highest already downloaded version that satisfies the requirement, otherwise find the highest satisfying version in the index
let Some(version) = find_highest!(resolved_versions.keys(), version_req)
.or_else(|| find_highest!(index_entries.iter().map(|v| &v.version), version_req))
else {
return Err(ResolveError::NoSatisfyingVersion(Box::new(
specifier.clone(),
)));
};
Ok(index_entries
.into_iter()
.find(|e| e.version.eq(&version))
.unwrap())
}
fn find_realm(a: &Realm, b: &Realm) -> Realm { fn find_realm(a: &Realm, b: &Realm) -> Realm {
if a == b { if a == b {
return *a; return *a;
@ -96,38 +145,6 @@ fn find_realm(a: &Realm, b: &Realm) -> Realm {
Realm::Shared Realm::Shared
} }
fn add_to_map(
map: &mut ResolvedVersionsMap,
name: &PackageName,
version: &Version,
resolved_package: &ResolvedPackage,
lockfile: &ResolvedVersionsMap,
depth: usize,
) -> Result<(), ResolveError> {
debug!(
"{}resolved {resolved_package} from lockfile",
"\t".repeat(depth)
);
map.entry(name.clone())
.or_default()
.insert(version.clone(), resolved_package.clone());
for (dep_name, dep_version) in &resolved_package.dependencies {
if map.get(dep_name).and_then(|v| v.get(dep_version)).is_none() {
let dep = lockfile.get(dep_name).and_then(|v| v.get(dep_version));
match dep {
Some(dep) => add_to_map(map, dep_name, dep_version, dep, lockfile, depth + 1)?,
// the lockfile is malformed
None => return Err(ResolveError::OutOfDateLockfile),
}
}
}
Ok(())
}
/// An error that occurred while resolving dependencies /// An error that occurred while resolving dependencies
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ResolveError { pub enum ResolveError {
@ -186,63 +203,95 @@ pub enum ResolveError {
} }
impl Manifest { impl Manifest {
/// Resolves the dependency tree for the project /// Resolves the dependency graph for the project
pub fn dependency_tree( pub fn dependency_graph(
&self, &self,
project: &mut Project, project: &mut Project,
locked: bool, locked: bool,
) -> Result<ResolvedVersionsMap, ResolveError> { ) -> Result<RootLockfileNode, ResolveError> {
debug!("resolving dependency tree for project {}", self.name); debug!("resolving dependency graph for project {}", self.name);
// try to reuse versions (according to semver specifiers) to decrease the amount of downloads and storage // try to reuse versions (according to semver specifiers) to decrease the amount of downloads and storage
let mut resolved_versions_map: ResolvedVersionsMap = BTreeMap::new(); let mut root = RootLockfileNode::default();
let tree = if let Some(lockfile) = project.lockfile()? { 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();
// resolve all root dependencies (and their dependencies) from the lockfile let current_dependencies = self.dependencies();
for (name, versions) in &lockfile { let current_specifiers = current_dependencies
.iter()
.map(|(d, _)| d)
.collect::<HashSet<_>>();
// populate the new lockfile with all root dependencies (and their dependencies) from the old lockfile
for (name, versions) in &old_root.children {
for (version, resolved_package) in versions { for (version, resolved_package) in versions {
if !resolved_package.is_root let specifier = old_root.root_specifier(resolved_package);
|| !self
.dependencies() if !specifier.is_some_and(|specifier| current_specifiers.contains(specifier)) {
.into_iter()
.any(|(spec, _)| spec == resolved_package.specifier)
{
continue; continue;
} }
add_to_map( root.specifiers
&mut resolved_versions_map, .entry(name.clone())
name, .or_default()
version, .insert(version.clone(), specifier.unwrap().clone());
resolved_package,
&lockfile, let mut queue = VecDeque::from([resolved_package]);
1,
)?; while let Some(resolved_package) = queue.pop_front() {
debug!("resolved {resolved_package} from lockfile");
root.children
.entry(name.clone())
.or_default()
.insert(version.clone(), resolved_package.clone());
for (dep_name, dep_version) in &resolved_package.dependencies {
if root
.children
.get(dep_name)
.and_then(|v| v.get(dep_version))
.is_none()
{
let dep = old_root
.children
.get(dep_name)
.and_then(|v| v.get(dep_version));
match dep {
Some(dep) => queue.push_back(dep),
// the lockfile is out of date
None => return Err(ResolveError::OutOfDateLockfile),
}
}
}
}
} }
} }
// resolve new, or modified, dependencies from the lockfile let old_specifiers = old_root
'outer: for (dep, dep_type) in self.dependencies() { .specifiers
for versions in resolved_versions_map.values() { .values()
for resolved_package in versions.values() { .flat_map(|v| v.values())
if resolved_package.specifier == dep && resolved_package.is_root { .collect::<HashSet<_>>();
continue 'outer;
} // resolve new, or modified, dependencies from the manifest
} for (specifier, dep_type) in current_dependencies {
if old_specifiers.contains(&specifier) {
continue;
} }
if locked { if locked {
return Err(ResolveError::OutOfDateLockfile); return Err(ResolveError::OutOfDateLockfile);
} }
missing.push((dep.clone(), dep_type)); missing.push((specifier.clone(), dep_type));
} }
debug!( debug!(
"resolved {} dependencies from lockfile. new dependencies: {}", "resolved {} dependencies from lockfile. new dependencies: {}",
resolved_versions_map.len(), old_root.children.len(),
missing.len() missing.len()
); );
@ -252,16 +301,19 @@ impl Manifest {
self.dependencies() self.dependencies()
}; };
if tree.is_empty() { if graph.is_empty() {
debug!("no dependencies left to resolve, finishing..."); debug!("no dependencies left to resolve, finishing...");
return Ok(resolved_versions_map); return Ok(root);
} }
debug!("resolving {} dependencies from index", tree.len()); debug!("resolving {} dependencies from index", graph.len());
let mut queue = VecDeque::from_iter(self.dependencies().into_iter().map(|d| (d, None))); let mut queue = graph
.into_iter()
.map(|(specifier, dep_type)| (specifier, dep_type, None))
.collect::<VecDeque<_>>();
while let Some(((specifier, dep_type), dependant)) = queue.pop_front() { while let Some((specifier, dep_type, dependant)) = queue.pop_front() {
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() {
@ -269,49 +321,24 @@ impl Manifest {
} else { } else {
get_index_by_url(project.indices(), &registry_dependency.index.parse()?) get_index_by_url(project.indices(), &registry_dependency.index.parse()?)
}; };
let pkg_name: PackageName = registry_dependency.name.clone().into();
let index_entries = index let entry = find_version_from_index(
.package(&pkg_name) &mut root,
.map_err(|e| { index,
ResolveError::IndexPackage(e, registry_dependency.name.to_string()) &specifier,
})? registry_dependency.name.clone().into(),
.ok_or_else(|| { &registry_dependency.version,
ResolveError::PackageNotFound(registry_dependency.name.to_string()) )?;
})?;
let resolved_versions = resolved_versions_map.entry(pkg_name).or_default();
// try to find the highest already downloaded version that satisfies the requirement, otherwise find the highest satisfying version in the index
let Some(version) =
find_highest!(resolved_versions.keys(), registry_dependency).or_else(
|| {
find_highest!(
index_entries.iter().map(|v| &v.version),
registry_dependency
)
},
)
else {
return Err(ResolveError::NoSatisfyingVersion(Box::new(
specifier.clone(),
)));
};
let entry = index_entries
.into_iter()
.find(|e| e.version.eq(&version))
.unwrap();
debug!( debug!(
"resolved registry dependency {} to {}", "resolved registry dependency {} to {}",
registry_dependency.name, version registry_dependency.name, entry.version
); );
( (
PackageRef::Registry(RegistryPackageRef { PackageRef::Registry(RegistryPackageRef {
name: registry_dependency.name.clone(), name: registry_dependency.name.clone(),
version: version.clone(), version: entry.version,
index_url: index.url().clone(), index_url: index.url().clone(),
}), }),
entry.realm, entry.realm,
@ -346,47 +373,24 @@ impl Manifest {
project.indices_mut(), project.indices_mut(),
&wally_dependency.index_url, &wally_dependency.index_url,
)?; )?;
let pkg_name = wally_dependency.name.clone().into();
let index_entries = index let entry = find_version_from_index(
.package(&pkg_name) &mut root,
.map_err(|e| { &index,
ResolveError::IndexPackage(e, wally_dependency.name.to_string()) &specifier,
})? wally_dependency.name.clone().into(),
.ok_or_else(|| { &wally_dependency.version,
ResolveError::PackageNotFound(wally_dependency.name.to_string()) )?;
})?;
let resolved_versions = resolved_versions_map.entry(pkg_name).or_default();
// try to find the highest already downloaded version that satisfies the requirement, otherwise find the highest satisfying version in the index
let Some(version) = find_highest!(resolved_versions.keys(), wally_dependency)
.or_else(|| {
find_highest!(
index_entries.iter().map(|v| &v.version),
wally_dependency
)
})
else {
return Err(ResolveError::NoSatisfyingVersion(Box::new(
specifier.clone(),
)));
};
let entry = index_entries
.into_iter()
.find(|e| e.version.eq(&version))
.unwrap();
debug!( debug!(
"resolved registry dependency {} to {}", "resolved wally dependency {} to {}",
wally_dependency.name, version wally_dependency.name, entry.version
); );
( (
PackageRef::Wally(crate::dependencies::wally::WallyPackageRef { PackageRef::Wally(crate::dependencies::wally::WallyPackageRef {
name: wally_dependency.name.clone(), name: wally_dependency.name.clone(),
version: version.clone(), version: entry.version,
index_url: index.url().clone(), index_url: index.url().clone(),
}), }),
entry.realm, entry.realm,
@ -395,26 +399,30 @@ impl Manifest {
} }
}; };
let is_root = dependant.is_none();
// if the dependency is a root dependency, it can be thought of as a normal dependency // if the dependency is a root dependency, it can be thought of as a normal dependency
let dep_type = if is_root { let dep_type = if dependant.is_some() {
DependencyType::Normal
} else {
dep_type dep_type
} else {
DependencyType::Normal
}; };
let specifier_realm = specifier.realm().copied();
if let Some((dependant_name, dependant_version)) = dependant { if let Some((dependant_name, dependant_version)) = dependant {
resolved_versions_map root.children
.get_mut(&dependant_name) .get_mut(&dependant_name)
.and_then(|v| v.get_mut(&dependant_version)) .and_then(|v| v.get_mut(&dependant_version))
.unwrap() .unwrap()
.dependencies .dependencies
.insert((pkg_ref.name(), pkg_ref.version().clone())); .insert((pkg_ref.name(), pkg_ref.version().clone()));
} else {
root.specifiers
.entry(pkg_ref.name())
.or_default()
.insert(pkg_ref.version().clone(), specifier);
} }
let resolved_versions = resolved_versions_map let resolved_versions = root.children.entry(pkg_ref.name()).or_default();
.entry(pkg_ref.name().clone())
.or_default();
if let Some(previously_resolved) = resolved_versions.get_mut(pkg_ref.version()) { if let Some(previously_resolved) = resolved_versions.get_mut(pkg_ref.version()) {
match (&pkg_ref, &previously_resolved.pkg_ref) { match (&pkg_ref, &previously_resolved.pkg_ref) {
@ -443,15 +451,13 @@ impl Manifest {
continue; continue;
} }
if specifier if specifier_realm.is_some_and(|realm| realm == Realm::Shared)
.realm()
.is_some_and(|realm| realm == &Realm::Shared)
&& default_realm.is_some_and(|realm| realm == Realm::Server) && default_realm.is_some_and(|realm| realm == Realm::Server)
{ {
return Err(ResolveError::IncompatibleRealms( return Err(ResolveError::IncompatibleRealms(
pkg_ref.name().to_string(), pkg_ref.name().to_string(),
default_realm.unwrap(), default_realm.unwrap(),
*specifier.realm().unwrap(), specifier_realm.unwrap(),
)); ));
} }
@ -459,29 +465,26 @@ impl Manifest {
pkg_ref.version().clone(), pkg_ref.version().clone(),
ResolvedPackage { ResolvedPackage {
pkg_ref: pkg_ref.clone(), pkg_ref: pkg_ref.clone(),
specifier: specifier.clone(),
dependencies: BTreeSet::new(), dependencies: BTreeSet::new(),
is_root, realm: specifier_realm
realm: *specifier
.realm()
.copied()
.unwrap_or_default() .unwrap_or_default()
.or(&default_realm.unwrap_or_default()), .or(default_realm.unwrap_or_default()),
dep_type, dep_type,
}, },
); );
for dependency in dependencies { for (specifier, ty) in dependencies {
queue.push_back(( queue.push_back((
dependency, specifier,
Some((pkg_ref.name().clone(), pkg_ref.version().clone())), ty,
Some((pkg_ref.name(), pkg_ref.version().clone())),
)); ));
} }
} }
debug!("resolving realms and peer dependencies..."); debug!("resolving realms and peer dependencies...");
for (name, versions) in resolved_versions_map.clone() { for (name, versions) in root.children.clone() {
for (version, resolved_package) in versions { for (version, resolved_package) in versions {
if resolved_package.dep_type == DependencyType::Peer { if resolved_package.dep_type == DependencyType::Peer {
return Err(ResolveError::PeerNotInstalled( return Err(ResolveError::PeerNotInstalled(
@ -493,16 +496,14 @@ impl Manifest {
let mut realm = resolved_package.realm; let mut realm = resolved_package.realm;
for (dep_name, dep_version) in &resolved_package.dependencies { for (dep_name, dep_version) in &resolved_package.dependencies {
let dep = resolved_versions_map let dep = root.children.get(dep_name).and_then(|v| v.get(dep_version));
.get(dep_name)
.and_then(|v| v.get(dep_version));
if let Some(dep) = dep { if let Some(dep) = dep {
realm = find_realm(&realm, &dep.realm); realm = find_realm(&realm, &dep.realm);
} }
} }
resolved_versions_map root.children
.get_mut(&name) .get_mut(&name)
.and_then(|v| v.get_mut(&version)) .and_then(|v| v.get_mut(&version))
.unwrap() .unwrap()
@ -510,8 +511,8 @@ impl Manifest {
} }
} }
debug!("finished resolving dependency tree"); debug!("finished resolving dependency graph");
Ok(resolved_versions_map) Ok(root)
} }
} }

View file

@ -707,13 +707,13 @@ impl Index for WallyIndex {
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
.map_err(|e| IndexPackageError::Other(Box::new(e)))?; .map_err(|e| IndexPackageError::Other(Box::new(e)))?;
Ok(Some(BTreeSet::from_iter( Ok(Some(
manifest_stream manifest_stream
.into_iter() .into_iter()
.map(|m| m.try_into()) .map(|m| m.try_into())
.collect::<Result<Vec<_>, _>>() .collect::<Result<BTreeSet<_>, _>>()
.map_err(|e| IndexPackageError::Other(Box::new(e)))?, .map_err(|e| IndexPackageError::Other(Box::new(e)))?,
))) ))
} }
fn create_package_version( fn create_package_version(

View file

@ -45,3 +45,7 @@ pub const IGNORED_FOLDERS: &[&str] = &[
SERVER_PACKAGES_FOLDER, SERVER_PACKAGES_FOLDER,
".git", ".git",
]; ];
pub(crate) fn is_default<T: Default + PartialEq>(t: &T) -> bool {
t == &Default::default()
}

View file

@ -14,7 +14,7 @@ use semver::Version;
use thiserror::Error; use thiserror::Error;
use crate::{ use crate::{
dependencies::resolution::{packages_folder, ResolvedPackage, ResolvedVersionsMap}, dependencies::resolution::{packages_folder, ResolvedPackage, RootLockfileNode},
manifest::{Manifest, ManifestReadError, PathStyle, Realm}, manifest::{Manifest, ManifestReadError, PathStyle, Realm},
package_name::PackageName, package_name::PackageName,
project::Project, project::Project,
@ -125,6 +125,7 @@ pub enum LinkingError {
pub(crate) fn link<P: AsRef<Path>, Q: AsRef<Path>>( pub(crate) fn link<P: AsRef<Path>, Q: AsRef<Path>>(
project: &Project, project: &Project,
resolved_pkg: &ResolvedPackage, resolved_pkg: &ResolvedPackage,
lockfile: &RootLockfileNode,
destination_dir: P, destination_dir: P,
parent_dependency_packages_dir: Q, parent_dependency_packages_dir: Q,
only_name: bool, only_name: bool,
@ -146,12 +147,15 @@ pub(crate) fn link<P: AsRef<Path>, Q: AsRef<Path>>(
let pkg_name = resolved_pkg.pkg_ref.name(); let pkg_name = resolved_pkg.pkg_ref.name();
let name = pkg_name.name(); let name = pkg_name.name();
let destination_dir = if resolved_pkg.is_root { let destination_dir = match lockfile
project.path().join(packages_folder( .specifiers
&resolved_pkg.specifier.realm().cloned().unwrap_or_default(), .get(&pkg_name)
)) .and_then(|v| v.get(resolved_pkg.pkg_ref.version()))
} else { {
destination_dir.as_ref().to_path_buf() Some(specifier) => project.path().join(packages_folder(
specifier.realm().copied().unwrap_or_default(),
)),
None => destination_dir.as_ref().to_path_buf(),
}; };
let destination_file = destination_dir.join(format!( let destination_file = destination_dir.join(format!(
@ -230,19 +234,26 @@ pub struct LinkingDependenciesError(
Version, Version,
); );
fn is_duplicate_in<T: PartialEq>(item: T, items: &[T]) -> bool {
let mut count = 0u8;
items.iter().any(|i| {
if i == &item {
count += 1;
}
count > 1
})
}
impl Project { impl Project {
/// Links the dependencies of the project /// Links the dependencies of the project
pub fn link_dependencies( pub fn link_dependencies(
&self, &self,
map: &ResolvedVersionsMap, lockfile: &RootLockfileNode,
) -> Result<(), LinkingDependenciesError> { ) -> Result<(), LinkingDependenciesError> {
let root_deps: HashSet<String> = HashSet::from_iter( let root_deps = lockfile.specifiers.keys().collect::<HashSet<_>>();
map.iter() let root_dep_names = root_deps.iter().map(|n| n.name()).collect::<Vec<_>>();
.flat_map(|(_, v)| v)
.filter_map(|(_, v)| v.is_root.then_some(v.pkg_ref.name().name().to_string())),
);
for (name, versions) in map { for (name, versions) in &lockfile.children {
for (version, resolved_pkg) in versions { for (version, resolved_pkg) in versions {
let (container_dir, _) = resolved_pkg.directory(self.path()); let (container_dir, _) = resolved_pkg.directory(self.path());
@ -251,8 +262,15 @@ impl Project {
container_dir.display() container_dir.display()
); );
let resolved_pkg_dep_names = resolved_pkg
.dependencies
.iter()
.map(|(n, _)| n.name())
.collect::<Vec<_>>();
for (dep_name, dep_version) in &resolved_pkg.dependencies { for (dep_name, dep_version) in &resolved_pkg.dependencies {
let dep = map let dep = lockfile
.children
.get(dep_name) .get(dep_name)
.and_then(|versions| versions.get(dep_version)) .and_then(|versions| versions.get(dep_version))
.unwrap(); .unwrap();
@ -260,12 +278,10 @@ impl Project {
link( link(
self, self,
dep, dep,
lockfile,
&container_dir, &container_dir,
&self.path().join(resolved_pkg.packages_folder()), &self.path().join(resolved_pkg.packages_folder()),
resolved_pkg !is_duplicate_in(dep_name.name(), &resolved_pkg_dep_names),
.dependencies
.iter()
.any(|(n, _)| n.name() == dep_name.name()),
) )
.map_err(|e| { .map_err(|e| {
LinkingDependenciesError( LinkingDependenciesError(
@ -278,7 +294,7 @@ impl Project {
})?; })?;
} }
if resolved_pkg.is_root { if root_deps.contains(&name) {
let linking_dir = &self.path().join(resolved_pkg.packages_folder()); let linking_dir = &self.path().join(resolved_pkg.packages_folder());
debug!( debug!(
@ -289,9 +305,10 @@ impl Project {
link( link(
self, self,
resolved_pkg, resolved_pkg,
lockfile,
linking_dir, linking_dir,
linking_dir, linking_dir,
root_deps.contains(name.name()), !is_duplicate_in(name.name(), &root_dep_names),
) )
.map_err(|e| { .map_err(|e| {
LinkingDependenciesError( LinkingDependenciesError(

View file

@ -72,7 +72,7 @@ pub enum Realm {
impl Realm { impl Realm {
/// Returns the most restrictive realm /// Returns the most restrictive realm
pub fn or<'a>(&'a self, other: &'a Self) -> &'a Self { pub fn or(self, other: Self) -> Self {
match self { match self {
Realm::Shared => other, Realm::Shared => other,
_ => self, _ => self,
@ -115,6 +115,18 @@ pub struct Manifest {
pub name: StandardPackageName, pub name: StandardPackageName,
/// The version of the package. Must be [semver](https://semver.org) compatible. The registry will not accept non-semver versions and the CLI will not handle such packages /// The version of the package. Must be [semver](https://semver.org) compatible. The registry will not accept non-semver versions and the CLI will not handle such packages
pub version: Version, pub version: Version,
/// A short description of the package
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// The license of the package
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
/// The authors of the package
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authors: Option<Vec<String>>,
/// The repository of the package
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repository: Option<String>,
/// The files exported by the package /// The files exported by the package
#[serde(default)] #[serde(default)]
pub exports: Exports, pub exports: Exports,
@ -139,19 +151,6 @@ pub struct Manifest {
/// The peer dependencies of the package /// The peer dependencies of the package
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub peer_dependencies: Vec<DependencySpecifier>, pub peer_dependencies: Vec<DependencySpecifier>,
/// A short description of the package
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// The license of the package
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
/// The authors of the package
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authors: Option<Vec<String>>,
/// The repository of the package
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repository: Option<String>,
} }
/// An error that occurred while reading the manifest /// An error that occurred while reading the manifest

View file

@ -9,7 +9,7 @@ use semver::Version;
use thiserror::Error; use thiserror::Error;
use crate::{ use crate::{
dependencies::resolution::ResolvedVersionsMap, dependencies::resolution::RootLockfileNode,
package_name::{FromEscapedStrPackageNameError, PackageName}, package_name::{FromEscapedStrPackageNameError, PackageName},
project::Project, project::Project,
PATCHES_FOLDER, PATCHES_FOLDER,
@ -141,7 +141,7 @@ pub enum ApplyPatchesError {
impl Project { impl Project {
/// Applies patches for the project /// Applies patches for the project
pub fn apply_patches(&self, map: &ResolvedVersionsMap) -> Result<(), ApplyPatchesError> { pub fn apply_patches(&self, lockfile: &RootLockfileNode) -> Result<(), ApplyPatchesError> {
let patches_dir = self.path().join(PATCHES_FOLDER); let patches_dir = self.path().join(PATCHES_FOLDER);
if !patches_dir.exists() { if !patches_dir.exists() {
return Ok(()); return Ok(());
@ -170,7 +170,8 @@ impl Project {
let version = Version::parse(version)?; let version = Version::parse(version)?;
let resolved_pkg = map let resolved_pkg = lockfile
.children
.get(&package_name) .get(&package_name)
.ok_or_else(|| ApplyPatchesError::PackageNotFound(package_name.clone()))? .ok_or_else(|| ApplyPatchesError::PackageNotFound(package_name.clone()))?
.get(&version) .get(&version)

View file

@ -10,7 +10,7 @@ use thiserror::Error;
use url::Url; use url::Url;
use crate::{ use crate::{
dependencies::{resolution::ResolvedVersionsMap, DownloadError, UrlResolveError}, dependencies::{resolution::RootLockfileNode, DownloadError, UrlResolveError},
index::Index, index::Index,
linking_file::LinkingDependenciesError, linking_file::LinkingDependenciesError,
manifest::{Manifest, ManifestReadError}, manifest::{Manifest, ManifestReadError},
@ -34,7 +34,7 @@ pub struct Project {
pub struct InstallOptions { pub struct InstallOptions {
locked: bool, locked: bool,
auto_download: bool, auto_download: bool,
resolved_versions_map: Option<ResolvedVersionsMap>, lockfile: Option<RootLockfileNode>,
} }
impl Default for InstallOptions { impl Default for InstallOptions {
@ -42,7 +42,7 @@ impl Default for InstallOptions {
Self { Self {
locked: false, locked: false,
auto_download: true, auto_download: true,
resolved_versions_map: None, lockfile: None,
} }
} }
} }
@ -57,7 +57,7 @@ impl InstallOptions {
pub fn locked(&self, locked: bool) -> Self { pub fn locked(&self, locked: bool) -> Self {
Self { Self {
locked, locked,
resolved_versions_map: self.resolved_versions_map.clone(), lockfile: self.lockfile.clone(),
..*self ..*self
} }
} }
@ -67,16 +67,16 @@ impl InstallOptions {
pub fn auto_download(&self, auto_download: bool) -> Self { pub fn auto_download(&self, auto_download: bool) -> Self {
Self { Self {
auto_download, auto_download,
resolved_versions_map: self.resolved_versions_map.clone(), lockfile: self.lockfile.clone(),
..*self ..*self
} }
} }
/// Makes the installation to use the given resolved versions map /// Makes the installation to use the given lockfile
/// Having this set to Some is only useful if you're using auto_download = false /// Having this set to Some is only useful if you're using auto_download = false
pub fn resolved_versions_map(&self, resolved_versions_map: ResolvedVersionsMap) -> Self { pub fn lockfile(&self, lockfile: RootLockfileNode) -> Self {
Self { Self {
resolved_versions_map: Some(resolved_versions_map), lockfile: Some(lockfile),
..*self ..*self
} }
} }
@ -97,9 +97,9 @@ pub enum ReadLockfileError {
/// An error that occurred while downloading a project /// An error that occurred while downloading a project
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum InstallProjectError { pub enum InstallProjectError {
/// An error that occurred while resolving the dependency tree /// An error that occurred while resolving the dependency graph
#[error("failed to resolve dependency tree")] #[error("failed to resolve dependency graph")]
ResolveTree(#[from] crate::dependencies::resolution::ResolveError), ResolveGraph(#[from] crate::dependencies::resolution::ResolveError),
/// An error that occurred while downloading a package /// An error that occurred while downloading a package
#[error("failed to download package")] #[error("failed to download package")]
@ -273,12 +273,12 @@ impl Project {
} }
/// Returns the lockfile of the project /// Returns the lockfile of the project
pub fn lockfile(&self) -> Result<Option<ResolvedVersionsMap>, ReadLockfileError> { pub fn lockfile(&self) -> Result<Option<RootLockfileNode>, ReadLockfileError> {
let lockfile_path = self.path.join(LOCKFILE_FILE_NAME); let lockfile_path = self.path.join(LOCKFILE_FILE_NAME);
Ok(if lockfile_path.exists() { Ok(if lockfile_path.exists() {
let lockfile_contents = read(&lockfile_path)?; let lockfile_contents = read(&lockfile_path)?;
let lockfile: ResolvedVersionsMap = serde_yaml::from_slice(&lockfile_contents) let lockfile: RootLockfileNode = serde_yaml::from_slice(&lockfile_contents)
.map_err(ReadLockfileError::LockfileDeser)?; .map_err(ReadLockfileError::LockfileDeser)?;
Some(lockfile) Some(lockfile)
@ -289,25 +289,25 @@ impl Project {
/// Downloads the project's dependencies, applies patches, and links the dependencies /// Downloads the project's dependencies, applies patches, and links the dependencies
pub fn install(&mut self, install_options: InstallOptions) -> Result<(), InstallProjectError> { pub fn install(&mut self, install_options: InstallOptions) -> Result<(), InstallProjectError> {
let map = match install_options.resolved_versions_map { let lockfile = match install_options.lockfile {
Some(map) => map, Some(map) => map,
None => { None => {
let manifest = self.manifest.clone(); let manifest = self.manifest.clone();
manifest.dependency_tree(self, install_options.locked)? manifest.dependency_graph(self, install_options.locked)?
} }
}; };
if install_options.auto_download { if install_options.auto_download {
self.download(map.clone())?.wait()?; self.download(&lockfile)?.wait()?;
} }
self.apply_patches(&map)?; self.apply_patches(&lockfile)?;
self.link_dependencies(&map)?; self.link_dependencies(&lockfile)?;
if !install_options.locked { if !install_options.locked {
serde_yaml::to_writer(File::create(self.path.join(LOCKFILE_FILE_NAME))?, &map) serde_yaml::to_writer(File::create(self.path.join(LOCKFILE_FILE_NAME))?, &lockfile)
.map_err(InstallProjectError::LockfileSer)?; .map_err(InstallProjectError::LockfileSer)?;
} }

View file

@ -104,9 +104,9 @@ fn test_resolves_package() {
.unwrap(); .unwrap();
let manifest = project.manifest().clone(); let manifest = project.manifest().clone();
let tree = manifest.dependency_tree(&mut project, false).unwrap(); let graph = manifest.dependency_graph(&mut project, false).unwrap();
assert_eq!(tree.len(), 1); assert_eq!(graph.children.len(), 1);
let versions = tree.get(&pkg_name.clone().into()).unwrap(); let versions = graph.children.get(&pkg_name.clone().into()).unwrap();
assert_eq!(versions.len(), 2); assert_eq!(versions.len(), 2);
let resolved_pkg = versions.get(&version).unwrap(); let resolved_pkg = versions.get(&version).unwrap();
assert_eq!( assert_eq!(
@ -117,9 +117,7 @@ fn test_resolves_package() {
version: version.clone(), version: version.clone(),
index_url: index.url().clone(), index_url: index.url().clone(),
}), }),
specifier,
dependencies: Default::default(), dependencies: Default::default(),
is_root: true,
realm: Realm::Shared, realm: Realm::Shared,
dep_type: DependencyType::Normal, dep_type: DependencyType::Normal,
} }
@ -133,9 +131,7 @@ fn test_resolves_package() {
version: version_2.clone(), version: version_2.clone(),
index_url: index.url().clone(), index_url: index.url().clone(),
}), }),
specifier: specifier_2,
dependencies: Default::default(), dependencies: Default::default(),
is_root: true,
realm: Realm::Shared, realm: Realm::Shared,
dep_type: DependencyType::Normal, dep_type: DependencyType::Normal,
} }