Skip to content
Closed
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
166 changes: 153 additions & 13 deletions src/install/isolated_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,98 @@ impl<'a, 'b> Wait<'a, 'b> {
}
}

/// `dep` is a `file:` dependency whose path is relative to the package declaring it
/// (resolved as `declarer`): `Package::from_npm` stores these paths as declared,
/// `Package::parse` and `overrides` store top-level relative ones. Such a folder is
/// linked from inside the package and is not a peer provider for the packages below.
Comment thread
robobun marked this conversation as resolved.
Resolved
fn dependency_is_contained_folder(
lockfile: &Lockfile,
declarer: &Resolution,
dep: &install::Dependency,
) -> bool {
if matches!(
declarer.tag,
ResolutionTag::Root | ResolutionTag::Workspace | ResolutionTag::Folder
) || dep.version.tag != VersionTag::Folder
{
return false;
}
let string_buf = lockfile.buffers.string_bytes.as_slice();
!lockfile
.overrides
.contains_name(dep.name_hash, dep.name.slice(string_buf), string_buf)
}

/// `dependency_is_contained_folder` evaluated once for the whole lockfile.
struct ContainedFolders {
/// Indexed by `DependencyID`.
dependencies: DynamicBitSet,
/// Indexed by `PackageID`: folders only such dependencies resolve to. They get
/// no store entry (`store::entry::Entry::nested_folder`).
Comment thread
robobun marked this conversation as resolved.
Resolved
packages: DynamicBitSet,
}

fn contained_folders(lockfile: &Lockfile) -> Result<ContainedFolders, AllocError> {
let pkgs = lockfile.packages.slice();
let pkg_resolutions = pkgs.items_resolution();
let resolutions = &lockfile.buffers.resolutions[..];
let dependencies = &lockfile.buffers.dependencies[..];

let mut contained = ContainedFolders {
dependencies: DynamicBitSet::init_empty(dependencies.len())?,
packages: DynamicBitSet::init_empty(pkg_resolutions.len())?,
};
for (pkg_id, pkg_res) in pkg_resolutions.iter().enumerate() {
if pkg_res.tag == ResolutionTag::Folder {
contained.packages.set(pkg_id);
}
}
if contained.packages.count() == 0 {
return Ok(contained);
}

for (pkg_id, pkg_deps) in pkgs.items_dependencies().iter().enumerate() {
for dep_id in pkg_deps.begin()..pkg_deps.end() {
let target = resolutions[dep_id as usize];
if target == invalid_package_id
|| target as usize >= pkg_resolutions.len()
|| pkg_resolutions[target as usize].tag != ResolutionTag::Folder
{
continue;
}
if dependency_is_contained_folder(
lockfile,
&pkg_resolutions[pkg_id],
&dependencies[dep_id as usize],
) {
contained.dependencies.set(dep_id as usize);
} else {
contained.packages.unset(target as usize);
}
}
}

Ok(contained)
}

/// `pkg_id` has a `dependency_is_contained_folder` dependency resolving to `folder_pkg_id`.
pub(crate) fn folder_is_inside_package(
lockfile: &Lockfile,
pkg_id: PackageID,
folder_pkg_id: PackageID,
) -> bool {
let pkgs = lockfile.packages.slice();
let declarer = &pkgs.items_resolution()[pkg_id as usize];
let pkg_deps = pkgs.items_dependencies()[pkg_id as usize];
let resolutions = &lockfile.buffers.resolutions[..];
let dependencies = &lockfile.buffers.dependencies[..];

(pkg_deps.begin()..pkg_deps.end()).any(|dep_id| {
resolutions[dep_id as usize] == folder_pkg_id
&& dependency_is_contained_folder(lockfile, declarer, &dependencies[dep_id as usize])
})
}

pub(crate) fn build_store(
manager: &PackageManager,
lockfile: &Lockfile,
Expand All @@ -231,6 +323,9 @@ pub(crate) fn build_store(
let dependencies = &lockfile.buffers.dependencies[..];
let string_buf = &lockfile.buffers.string_bytes[..];

let contained = contained_folders(lockfile)?;
let is_contained_folder = |dep_id: DependencyID| contained.dependencies.is_set(dep_id as usize);

let mut nodes: store::node::List = store::node::List::default();

// DFS so a deduplicated node's full subtree (and therefore its `peers`)
Expand Down Expand Up @@ -289,7 +384,8 @@ pub(crate) fn build_store(

// Per-package bits computed once: own peer-dep names, and non-peer
// dependency names that will appear in `node_dependencies` (i.e., not
// filtered out by bundled/disabled/unresolved).
// filtered out by bundled/disabled/unresolved) and are peer providers for
// the packages below (contained folders are not, see the walk below).
Comment thread
robobun marked this conversation as resolved.
Resolved
let own_peers: DynamicBitSetList =
DynamicBitSetList::init_empty(lockfile.packages.len(), peer_name_count as usize)?;
let provides: DynamicBitSetList =
Expand All @@ -305,15 +401,17 @@ pub(crate) fn build_store(
};
if dep.behavior.is_peer() {
own_peers.set(pkg_id as usize, bit);
} else if !is_filtered_dependency_or_workspace(
dep_id,
pkg_id,
workspace_filters,
install_root_dependencies,
manager,
lockfile,
resolutions,
) {
} else if !is_contained_folder(dep_id)
&& !is_filtered_dependency_or_workspace(
dep_id,
pkg_id,
workspace_filters,
install_root_dependencies,
manager,
lockfile,
resolutions,
)
{
provides.set(pkg_id as usize, bit);
}
}
Expand Down Expand Up @@ -504,6 +602,7 @@ pub(crate) fn build_store(
for ids in &node_dependencies[curr_id.get() as usize] {
if dependencies[ids.dep_id as usize].name_hash
== peer_name_hash
&& !is_contained_folder(ids.dep_id)
{
break 'resolved ids.pkg_id;
}
Expand Down Expand Up @@ -581,7 +680,9 @@ pub(crate) fn build_store(
let mut curr_id = entry.parent_id;
'walk: while curr_id != store::node::Id::INVALID {
for ids in &node_dependencies[curr_id.get() as usize] {
if dependencies[ids.dep_id as usize].name_hash == peer_name_hash {
if dependencies[ids.dep_id as usize].name_hash == peer_name_hash
&& !is_contained_folder(ids.dep_id)
{
break 'walk;
}
}
Expand Down Expand Up @@ -746,6 +847,11 @@ pub(crate) fn build_store(
continue;
}

// private to the ancestor; a package's own one still satisfies its own peer
if curr_id != node_id && is_contained_folder(ids.dep_id) {
continue;
}

let res = &pkg_resolutions[ids.pkg_id as usize];

if peer_dep.version.tag != VersionTag::Npm || res.tag != ResolutionTag::Npm
Expand Down Expand Up @@ -1007,8 +1113,11 @@ pub(crate) fn build_store(

let new_entry_parents: Vec<store::entry::Id> = vec![entry.entry_parent_id];

// never hoisted, so it must not claim the hoist slots for its name below
let new_entry_nested_folder = contained.packages.is_set(pkg_id as usize);

let hoisted = 'hoisted: {
if !manager.options.hoist {
if !manager.options.hoist || new_entry_nested_folder {
break 'hoisted false;
}

Expand Down Expand Up @@ -1039,6 +1148,7 @@ pub(crate) fn build_store(
parents: new_entry_parents,
peer_hash: new_entry_peer_hash,
hoisted,
nested_folder: new_entry_nested_folder,
step: core::sync::atomic::AtomicU32::new(0),
entry_hash: 0,
scripts: core::cell::Cell::new(None),
Expand Down Expand Up @@ -1080,7 +1190,7 @@ pub(crate) fn build_store(
.name
.slice(string_buf);
public_hoisted.put(dep_name, ())?;
} else {
} else if !new_entry_nested_folder {
// transitive dependencies (also direct dependencies of workspaces!)
let dep_name = dependencies[new_entry_dep_id as usize]
.name
Expand Down Expand Up @@ -1110,6 +1220,10 @@ pub(crate) fn build_store(
peers: node_peers[entry.node_id.get() as usize].clone(),
});

debug_assert!(
!new_entry_nested_folder || node_nodes[entry.node_id.get() as usize].is_empty(),
"a folder declared by a remote package is resolved without dependencies"
);
for &child_node_id in &node_nodes[entry.node_id.get() as usize] {
entry_queue.write_item(QueuedEntry {
node_id: child_node_id,
Expand Down Expand Up @@ -1968,6 +2082,7 @@ pub(crate) fn install_isolated_packages(
let entry_steps = entries.items_step();
let entry_dependencies = entries.items_dependencies();
let entry_hoisted = entries.items_hoisted();
let entry_nested_folder = entries.items_nested_folder();

// Reborrow through a
// `BackRef` so `string_buf` / `pkgs` don't tie up `&mut lockfile` for
Expand Down Expand Up @@ -2182,6 +2297,31 @@ pub(crate) fn install_isolated_packages(
continue;
}
ResolutionTag::Folder => {
if entry_nested_folder[entry_id.get() as usize] {
// linked by the packages containing it (`Installer::symlink_dependencies`)
debug_assert!(entry_dependencies[entry_id.get() as usize].list.is_empty());
let folder = pkg_res.folder().slice(string_buf);
let state = if crate::bin::bin_target_escapes_package_dir(folder) {
// the resolver rejects these; only an edited or old lockfile gets here
Output::err_generic(
"refusing to install dependency <b>{}<r> with unsafe folder path \"{}\"",
(BStr::new(pkg_name.slice(string_buf)), BStr::new(folder)),
);
Output::flush();
if installer.manager().options.enable.fail_early() {
Global::exit(1);
}
installer::CompleteState::Fail
} else {
installer::CompleteState::Skipped
};
// .monotonic is okay because the task isn't running on another thread.
entry_steps[entry_id.get() as usize]
.store(installer::Step::Done as u32, Ordering::Relaxed);
installer.on_task_complete(entry_id, state);
continue;
}

// folders are always hardlinked to keep them up-to-date
installer.start_task(entry_id);
continue;
Expand Down
57 changes: 56 additions & 1 deletion src/install/isolated_install/Installer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2277,8 +2277,32 @@ impl<'a> Installer<'a> {
let _ = dest.append(dep_name); // OOM/capacity: fire-and-forget
}

let dep_node_id = self.store.entries.items_node_id()[dep.entry_id.get() as usize];
let dep_pkg_id = self.store.nodes.items_pkg_id()[dep_node_id.get() as usize];
let dep_res = &pkg_resolutions[dep_pkg_id as usize];
let folder_is_inside_this_package = dep_res.tag == ResolutionTag::Folder
&& super::folder_is_inside_package(lockfile, pkg_id, dep_pkg_id);
let dep_is_nested_folder =
self.store.entries.items_nested_folder()[dep.entry_id.get() as usize];
// `build_store` only binds a nested folder to the packages containing it.
debug_assert!(folder_is_inside_this_package || !dep_is_nested_folder);

let mut dep_store_path = AutoAbsPath::init_top_level_dir();
if uses_global_store {
if folder_is_inside_this_package {
let Some(package_dir_name) = entry_node_modules_name else {
continue;
};
if !self.append_nested_folder_path(
&mut dep_store_path,
entry_id,
package_dir_name,
dep_res.folder().slice(string_buf),
)? {
continue;
}
} else if dep_is_nested_folder {
continue;
} else if uses_global_store {
debug_assert!(self.entry_uses_global_store(dep.entry_id));
self.append_real_store_path(&mut dep_store_path, dep.entry_id, Which::Final);
} else {
Expand Down Expand Up @@ -2791,6 +2815,37 @@ impl<'a> Installer<'a> {
}
}

/// Appends `<entry's store node_modules>/<package_dir_name>/<folder>` to `buf`.
/// `Ok(false)`: nothing to link (the path leaves the package, or no such directory).
Comment thread
robobun marked this conversation as resolved.
Resolved
fn append_nested_folder_path(
&self,
buf: &mut AutoAbsPath,
entry_id: StoreEntryId,
package_dir_name: &[u8],
folder: &[u8],
) -> sys::Result<bool> {
if bin_real::bin_target_escapes_package_dir(folder) {
return Ok(false);
}

self.append_real_store_node_modules_path(buf, entry_id, Which::Staging);
buf.append(package_dir_name).assume_ok();
// `folder` comes from a third-party manifest, so its length is unchecked.
if buf.len() + 1 + folder.len() >= paths::MAX_PATH_BYTES {
return Err(sys::Error::from_code(
sys::Errno::ENAMETOOLONG,
sys::Tag::fstatat,
));
}
buf.append_join(folder).assume_ok();

match sys::directory_exists_at(Fd::cwd(), buf.slice_z()) {
Ok(is_dir) => Ok(is_dir),
Err(err) if err.get_errno() == sys::Errno::ENOTDIR => Ok(false),
Err(err) => Err(err),
}
}

/// The directory name for the entry store node_modules install
/// folder.
/// ./node_modules/.bun/jquery@3.7.1/node_modules/jquery
Expand Down
6 changes: 6 additions & 0 deletions src/install/isolated_install/Store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,10 @@ pub mod entry {
// if true this entry gets symlinked to `node_modules/.bun/node_modules`
pub hoisted: bool,

/// Exists only inside the packages declaring it (`isolated_install::ContainedFolders`):
/// no store directory, task or hoist slot; linked in `Installer::symlink_dependencies`.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub nested_folder: bool,

pub peer_hash: PeerHash,

/// Content hash of (package + sorted resolved dependency global-store keys),
Expand Down Expand Up @@ -313,6 +317,7 @@ pub mod entry {
parents: Vec<Id>,
step: core::sync::atomic::AtomicU32,
hoisted: bool,
nested_folder: bool,
peer_hash: PeerHash,
entry_hash: u64,
scripts: core::cell::Cell<Option<*mut package::scripts::List>>,
Expand All @@ -328,6 +333,7 @@ pub mod entry {
// `Step::LinkPackage as u32 == 0`.
step: core::sync::atomic::AtomicU32::new(0),
hoisted: false,
nested_folder: false,
peer_hash: PeerHash::NONE,
entry_hash: 0,
scripts: core::cell::Cell::new(None),
Expand Down
6 changes: 5 additions & 1 deletion src/install/prune.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1491,11 +1491,15 @@ fn push_store_entry_names(
) {
let pkg_res = lockfile.packages.items_resolution();
let node_pkg_ids = store.nodes.items_pkg_id();
let entry_nested_folder = store.entries.items_nested_folder();
names.reserve(store.entries.len());
let mut name: Vec<u8> = Vec::new();
for (entry_idx, node_id) in store.entries.items_node_id().iter().enumerate() {
let pkg_id = node_pkg_ids[node_id.get() as usize] as usize;
if pkg_res[pkg_id].tag == ResolutionTag::Workspace || !wanted.is_set(pkg_id) {
if pkg_res[pkg_id].tag == ResolutionTag::Workspace
|| entry_nested_folder[entry_idx]
|| !wanted.is_set(pkg_id)
{
continue;
}
name.clear();
Expand Down
Loading
Loading