Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/install/PackageManager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,13 @@ pub type AsyncNetworkTaskQueue = UnboundedQueue<NetworkTask /* , .next */>;
pub(crate) type SuccessFn = fn(&mut PackageManager, DependencyID, PackageID);
pub(crate) type FailFn = fn(&mut PackageManager, &Dependency, PackageID, Error);

/// A peer held back by `find_peer_provider`, with the name and range it resolves under (after aliases, overrides and catalogs).
pub(crate) struct DeferredFolderPeer {
pub(crate) dependency_id: DependencyID,
pub(crate) name_hash: PackageNameHash,
pub(crate) version: crate::dependency::Version,
}

// Default to a maximum of 64 simultaneous HTTP requests for bun install if no proxy is specified
// if a proxy IS specified, default to 64. We have different values because we might change this in the future.
// https://github.com/npm/cli/issues/7072
Expand Down Expand Up @@ -393,6 +400,11 @@ pub struct PackageManager {

pub(crate) peer_dependencies: LinearFifo<DependencyID, DynamicBuffer<DependencyID>>,

/// Peers a root `file:` folder or link could satisfy, held back until nothing else can join the dependency graph.
pub(crate) deferred_folder_peers: Vec<DeferredFolderPeer>,
/// Set while the deferred peers are decided: the graph is complete, so `find_peer_provider` may bind a root folder.
pub(crate) peer_graph_complete: bool,

// name hash from alias package name -> aliased package dependency version info
pub(crate) known_npm_aliases: NpmAliasMap,

Expand Down Expand Up @@ -2122,6 +2134,8 @@ pub fn init(
peer_dependencies,
LinearFifo::<DependencyID, DynamicBuffer<DependencyID>>::init()
);
wr!(deferred_folder_peers, Vec::new());
wr!(peer_graph_complete, false);
wr!(known_npm_aliases, NpmAliasMap::default());
wr!(trusted_deps_to_add_to_package_json, Vec::new());
wr!(any_failed_to_install, false);
Expand Down Expand Up @@ -2581,6 +2595,8 @@ fn init_with_runtime_once(
peer_dependencies,
LinearFifo::<DependencyID, DynamicBuffer<DependencyID>>::init()
);
wr!(deferred_folder_peers, Vec::new());
wr!(peer_graph_complete, false);
wr!(known_npm_aliases, NpmAliasMap::default());
wr!(trusted_deps_to_add_to_package_json, Vec::new());
wr!(any_failed_to_install, false);
Expand Down
150 changes: 147 additions & 3 deletions src/install/PackageManager/PackageManagerEnqueue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
use crate::lockfile::package::Package;
use crate::lockfile_real as Lockfile;
use crate::package_manager_real::{
self, FailFn, PackageManager, SuccessFn, TaskCallbackList, determine_preinstall_state,
get_cache_directory, get_preinstall_state, get_temporary_directory, run_tasks,
set_preinstall_state,
self, DeferredFolderPeer, FailFn, PackageManager, SuccessFn, TaskCallbackList,
determine_preinstall_state, get_cache_directory, get_preinstall_state, get_temporary_directory,
run_tasks, set_preinstall_state,
};
use crate::package_manager_task as Task;
use crate::patch_install::EnqueueAfterState;
Expand Down Expand Up @@ -1101,6 +1101,12 @@
result.package.meta.id,
);
}
} else if this
.deferred_folder_peers
.last()
.is_some_and(|deferred| deferred.dependency_id == id)
{
// Held back by `find_peer_provider`; `resolve_deferred_folder_peers` decides it.
} else if version.tag.is_npm() {
// reshaped for borrowck — `name_str` borrows
// `this.lockfile.buffers.string_bytes`. Route the whole
Expand Down Expand Up @@ -2502,6 +2508,18 @@
..Default::default()
}));
}

if let Some(handled) = bind_peer_provider(
this,
&[existing_id],
dependency,
dependency_id,
name_hash,
version,
success_fn,
) {
return Ok(handled);
}
}
}
PackageIndexEntry::Ids(list) => {
Expand Down Expand Up @@ -2554,6 +2572,19 @@
}));
}
}

let candidates = list.to_vec();

Check failure on line 2576 in src/install/PackageManager/PackageManagerEnqueue.rs

View workflow job for this annotation

GitHub Actions / cargo clippy

implicitly cloning a `Vec` by calling `to_vec` on its dereferenced type
if let Some(handled) = bind_peer_provider(
this,
&candidates,
dependency,
dependency_id,
name_hash,
version,
success_fn,
) {
return Ok(handled);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
}
Expand Down Expand Up @@ -3078,6 +3109,119 @@
.map(|locked| (locked, buf))
}

/// Binds a non-registry provider to the peer, or holds the peer back for `resolve_deferred_folder_peers`; `None` means the callers' registry path takes over.
fn bind_peer_provider(
this: &mut PackageManager,
candidates: &[PackageID],
dependency: &Dependency,
dependency_id: DependencyID,
name_hash: PackageNameHash,
version: &dependency::Version,
success_fn: SuccessFn,
) -> Option<Option<ResolvedPackageResult>> {
match find_peer_provider(this, candidates, dependency, dependency_id, version) {
PeerProvider::Package(provider) => {
success_fn(this, dependency_id, provider);
Some(Some(ResolvedPackageResult {
// we must fetch it from the packages array again, incase the package array mutates the value in the `successFn`
package: *this.lockfile.packages.get(provider as usize),
..Default::default()
}))
}
PeerProvider::Deferred => {
this.deferred_folder_peers.push(DeferredFolderPeer {
dependency_id,
name_hash,
version: version.clone(),
});
Some(None)
}
PeerProvider::None => None,
}
}

pub(crate) enum PeerProvider {
Package(PackageID),
/// A root folder/link could satisfy the peer; decided once the dependency graph is complete.
Deferred,
None,
}

/// Non-registry package satisfying an npm-range peer: the declarer's own same-named dependency, else a tarball/git package, else the root's folder/link when the declarer is installed directly under the root.
pub(crate) fn find_peer_provider(
this: &PackageManager,
candidates: &[PackageID],
dependency: &Dependency,
dependency_id: DependencyID,
version: &dependency::Version,
) -> PeerProvider {
if !matches!(
version.tag,
dependency::version::Tag::Npm | dependency::version::Tag::DistTag
) {
return PeerProvider::None;
}
let lockfile = &this.lockfile;
let resolutions = lockfile.packages.items_resolution();
// Some(true): installed from the cache, works anywhere; Some(false): a folder/link path that only resolves from the right spot; None: registry, left to the callers' version checks.
let usable_anywhere = |package_id: PackageID| {
let resolution = resolutions.get(package_id as usize)?;
match resolution.tag {
ResolutionTag::LocalTarball
| ResolutionTag::RemoteTarball
| ResolutionTag::Git
| ResolutionTag::Github => Some(true),
ResolutionTag::Folder | ResolutionTag::Symlink => Some(false),
_ => None,
}
};
let declarer = lockfile.declaring_package(dependency_id);
// The declarer loads whatever it installs under this name itself, so nothing else can satisfy the peer.
let own = lockfile.resolution_of_dependency_named(
declarer,
dependency.name_hash,
dependency_id,
None,
);
if own != invalid_package_id {
return if candidates.contains(&own) && usable_anywhere(own).is_some() {
PeerProvider::Package(own)
} else {
PeerProvider::None
};
}
if let Some(anywhere) = candidates
.iter()
.copied()
.find(|&candidate| usable_anywhere(candidate) == Some(true))
{
return PeerProvider::Package(anywhere);
}
let root_features = this.options.local_package_features;
let roots = lockfile.resolution_of_dependency_named(
0,
dependency.name_hash,
dependency_id,
Some(root_features),
);
if !candidates.contains(&roots) || usable_anywhere(roots) != Some(false) {
return PeerProvider::None;
}
// A filtered install may leave the root's dependencies out of node_modules altogether.
if !this.options.filter_patterns.is_empty() || this.filtered_link_targets.is_some() {
return PeerProvider::None;
}
// Whether every copy of the declarer sees the root's folder depends on edges that may still be arriving.
if !this.peer_graph_complete {
return PeerProvider::Deferred;
}
if lockfile.dedupes_onto_root_dependency(declarer, root_features) {
PeerProvider::Package(roots)
} else {
PeerProvider::None
}
}

fn resolution_satisfies_dependency(
this: &PackageManager,
resolution: &Resolution,
Expand Down
72 changes: 59 additions & 13 deletions src/install/PackageManager/install_with_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ use bun_install_types::NodeLinker::NodeLinker;

// Free-function "methods" on `PackageManager` hosted in sibling modules
// to avoid one giant `impl PackageManager` block.
use crate::package_manager_real::enqueue::{PeerProvider, find_peer_provider};
use crate::package_manager_real::run_tasks::{RunTasksCallbacks, run_tasks};
use crate::package_manager_real::{
UpdateRequest, enqueue_dependency_list, enqueue_dependency_with_main, enqueue_patch_task_pre,
Expand Down Expand Up @@ -2054,21 +2055,66 @@ fn wait_for_resolution(manager: &mut PackageManager) -> crate::Result<()> {
wait_for_everything_except_peers(manager)?;
}

// Resolving a peer dep can create a NEW package whose own peer deps
// get re-queued to `peer_dependencies` during `drainDependencyList`.
// When all manifests are cached (synchronous resolution), no I/O tasks
// are spawned, so `pendingTaskCount() == 0`. We must drain the peer
// queue iteratively here — entering the event loop (`waitForPeers`)
// with zero pending I/O would block forever.
while manager.peer_dependencies.readable_length() > 0 {
manager.process_peer_dependency_list()?;
manager.drain_dependency_list();
}
loop {
// A peer can add a package whose own peers land back in the queue; with every manifest cached nothing is pending, so drain here (the event loop would block on zero I/O).
while manager.peer_dependencies.readable_length() > 0 {
manager.process_peer_dependency_list()?;
manager.drain_dependency_list();
}

if manager.pending_task_count() > 0 {
wait_for_peers(manager)?;
if manager.pending_task_count() > 0 {
wait_for_peers(manager)?;
}

if !resolve_deferred_folder_peers(manager)? {
return Ok(());
}
}
Ok(())
}

/// Decides the held-back peers now that nothing is in flight: those a root folder cannot serve go to the registry first (what they pull in can change the rest), and the caller settles the graph and calls again when that happened.
fn resolve_deferred_folder_peers(manager: &mut PackageManager) -> crate::Result<bool> {
if manager.deferred_folder_peers.is_empty() {
return Ok(false);
}
let deferred = core::mem::take(&mut manager.deferred_folder_peers);
manager.peer_graph_complete = true;
let (bound, fallback): (Vec<_>, Vec<_>) = deferred.into_iter().partition(|peer| {
let lockfile = &manager.lockfile;
let candidates: &[PackageID] = match lockfile.package_index.get(&peer.name_hash) {
Some(lockfile::PackageIndexEntry::Id(id)) => core::slice::from_ref(id),
Some(lockfile::PackageIndexEntry::Ids(ids)) => ids.as_slice(),
None => &[],
};
let dependency = &lockfile.buffers.dependencies.as_slice()[peer.dependency_id as usize];
matches!(
find_peer_provider(
manager,
candidates,
dependency,
peer.dependency_id,
&peer.version
),
PeerProvider::Package(_)
)
});
let fell_back = !fallback.is_empty();
let to_enqueue = if fell_back {
manager.deferred_folder_peers = bound;
fallback
} else {
bound
};
for peer in to_enqueue {
let dependency =
manager.lockfile.buffers.dependencies.as_slice()[peer.dependency_id as usize].clone();
let resolution =
manager.lockfile.buffers.resolutions.as_slice()[peer.dependency_id as usize];
enqueue_dependency_with_main(manager, peer.dependency_id, &dependency, resolution, true)?;
}
manager.peer_graph_complete = false;
manager.drain_dependency_list();
Ok(fell_back)
}

#[cold]
Expand Down
Loading
Loading