use crate::{Error, LinkStats, Linker, apply_multi_file_patch};
use aube_lockfile::{DirectDep, LocalSource, LockfileGraph};
use aube_store::PackageIndex;
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::path::{Path, PathBuf};
#[derive(Debug, Default, Clone)]
pub struct HoistedPlacements {
by_dep_path: BTreeMap<String, Vec<PathBuf>>,
}
impl HoistedPlacements {
pub fn from_graph(root_dir: &Path, graph: &LockfileGraph, modules_dir_name: &str) -> Self {
let mut placements = Self::default();
for (importer_path, deps) in &graph.importers {
if !crate::is_physical_importer(importer_path) {
continue;
}
let importer_dir = if importer_path == "." {
root_dir.to_path_buf()
} else {
root_dir.join(importer_path)
};
let nm = importer_dir.join(modules_dir_name);
let plan = plan_importer(&nm, deps, graph);
for node in &plan.nodes {
let (Some(dep_path), Some(pkg_dir)) = (&node.dep_path, &node.pkg_dir) else {
continue;
};
if pkg_dir.exists() {
placements.record(dep_path, pkg_dir.clone());
}
}
}
placements
}
pub fn package_dir(&self, dep_path: &str) -> Option<&Path> {
self.by_dep_path
.get(dep_path)
.and_then(|v| v.first())
.map(|p| p.as_path())
}
pub fn all_package_dirs(&self, dep_path: &str) -> &[PathBuf] {
self.by_dep_path
.get(dep_path)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &Path)> {
self.by_dep_path
.iter()
.flat_map(|(k, v)| v.iter().map(move |p| (k.as_str(), p.as_path())))
}
pub(crate) fn record(&mut self, dep_path: &str, path: PathBuf) {
self.by_dep_path
.entry(dep_path.to_string())
.or_default()
.push(path);
}
}
struct TreeNode {
pkg_dir: Option<PathBuf>,
nm_dir: PathBuf,
parent: Option<usize>,
children: BTreeMap<String, usize>,
dep_path: Option<String>,
}
pub(crate) struct PlacementPlan {
nodes: Vec<TreeNode>,
root_idx: usize,
}
struct PlaceOutcome {
node_idx: usize,
created: bool,
}
impl PlacementPlan {
fn new(importer_nm: PathBuf) -> Self {
let root = TreeNode {
pkg_dir: None,
nm_dir: importer_nm,
parent: None,
children: BTreeMap::new(),
dep_path: None,
};
Self {
nodes: vec![root],
root_idx: 0,
}
}
fn place(&mut self, requester: usize, name: &str, dep_path: &str) -> PlaceOutcome {
let mut cursor = requester;
let mut candidate = requester;
loop {
if let Some(&existing) = self.nodes[cursor].children.get(name) {
if self.nodes[existing].dep_path.as_deref() == Some(dep_path) {
return PlaceOutcome {
node_idx: existing,
created: false,
};
}
break;
}
candidate = cursor;
match self.nodes[cursor].parent {
Some(p) => cursor = p,
None => break,
}
}
let parent_nm = self.nodes[candidate].nm_dir.clone();
let pkg_dir = parent_nm.join(name);
let nm_dir = pkg_dir.join("node_modules");
let new_idx = self.nodes.len();
self.nodes.push(TreeNode {
pkg_dir: Some(pkg_dir),
nm_dir,
parent: Some(candidate),
children: BTreeMap::new(),
dep_path: Some(dep_path.to_string()),
});
self.nodes[candidate]
.children
.insert(name.to_string(), new_idx);
PlaceOutcome {
node_idx: new_idx,
created: true,
}
}
pub(crate) fn root_names(&self) -> impl Iterator<Item = &str> {
self.nodes[self.root_idx]
.children
.keys()
.map(|s| s.as_str())
}
}
pub(crate) fn plan_importer(
importer_nm: &Path,
root_deps: &[DirectDep],
graph: &LockfileGraph,
) -> PlacementPlan {
let mut plan = PlacementPlan::new(importer_nm.to_path_buf());
let mut queue: VecDeque<(usize, String, String)> = VecDeque::new();
for dep in root_deps {
if !graph.packages.contains_key(&dep.dep_path) {
continue;
}
queue.push_back((plan.root_idx, dep.name.clone(), dep.dep_path.clone()));
}
while let Some((requester, name, dep_path)) = queue.pop_front() {
let outcome = plan.place(requester, &name, &dep_path);
if !outcome.created {
continue;
}
let Some(pkg) = graph.packages.get(&dep_path) else {
continue;
};
if matches!(pkg.local_source.as_ref(), Some(LocalSource::Link(_))) {
continue;
}
for (dep_name, dep_tail) in &pkg.dependencies {
let child_dep_path = format!("{dep_name}@{dep_tail}");
if !graph.packages.contains_key(&child_dep_path) {
continue;
}
queue.push_back((outcome.node_idx, dep_name.clone(), child_dep_path));
}
}
plan
}
pub(crate) fn link_hoisted_importer(
linker: &Linker,
importer_dir: &Path,
root_deps: &[DirectDep],
graph: &LockfileGraph,
package_indices: &BTreeMap<String, PackageIndex>,
stats: &mut LinkStats,
placements: &mut HoistedPlacements,
) -> Result<(), Error> {
let nm = importer_dir.join(linker.modules_dir_name());
crate::mkdirp(&nm)?;
let plan = plan_importer(&nm, root_deps, graph);
let keep_root: std::collections::HashSet<&str> = plan.root_names().collect();
crate::sweep_stale_top_level_entries(&nm, &keep_root, None);
for idx in 0..plan.nodes.len() {
if idx == plan.root_idx {
continue;
}
let (dep_path, pkg_dir) = {
let node = &plan.nodes[idx];
(
node.dep_path.clone().expect("non-root node has dep_path"),
node.pkg_dir.clone().expect("non-root node has pkg_dir"),
)
};
let Some(pkg) = graph.packages.get(&dep_path) else {
continue;
};
if let Some(LocalSource::Link(rel)) = pkg.local_source.as_ref() {
if let Some(parent) = pkg_dir.parent() {
crate::mkdirp(parent)?;
}
crate::try_remove_entry(&pkg_dir);
let abs_target = importer_dir.join(rel);
let link_parent = pkg_dir.parent().unwrap_or(&nm);
let rel_target = pathdiff::diff_paths(&abs_target, link_parent).unwrap_or(abs_target);
crate::sys::create_dir_link(&rel_target, &pkg_dir)
.map_err(|e| Error::Io(pkg_dir.clone(), e))?;
placements.record(&dep_path, pkg_dir);
continue;
}
let owned_index;
let index = match package_indices.get(&dep_path) {
Some(i) => i,
None => {
let loaded = linker
.store
.load_index(pkg.registry_name(), &pkg.version, pkg.integrity.as_deref())
.ok_or_else(|| Error::MissingPackageIndex(dep_path.clone()))?;
owned_index = loaded;
&owned_index
}
};
crate::try_remove_entry(&pkg_dir);
let mut parents: BTreeSet<PathBuf> = BTreeSet::new();
parents.insert(pkg_dir.clone());
for rel_path in index.keys() {
crate::validate_index_key(rel_path)?;
let target = pkg_dir.join(rel_path);
if let Some(parent) = target.parent() {
parents.insert(parent.to_path_buf());
}
}
for parent in &parents {
std::fs::create_dir_all(parent).map_err(|e| Error::Io(parent.clone(), e))?;
}
for (rel_path, stored) in index {
let target = pkg_dir.join(rel_path);
linker.link_file_fresh(stored, &target)?;
stats.files_linked += 1;
if stored.executable {
#[cfg(unix)]
xx::file::make_executable(&target).map_err(|e| Error::Xx(e.to_string()))?;
}
}
let patch_key = pkg.spec_key();
if let Some(patch_text) = linker.patches.get(&patch_key) {
apply_multi_file_patch(&pkg_dir, patch_text)
.map_err(|msg| Error::Patch(patch_key.clone(), msg))?;
}
stats.packages_linked += 1;
placements.record(&dep_path, pkg_dir);
}
stats.top_level_linked += plan.nodes[plan.root_idx].children.len();
Ok(())
}