Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
12 changes: 12 additions & 0 deletions src/install/PackageManager.zig
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ global_link_dir: ?std.fs.Dir = null,
global_dir: ?std.fs.Dir = null,
global_link_dir_path: string = "",

/// Names of packages registered via `bun link`, read from
/// `<globalLinkDir>/` once per install. Populated on the main thread
/// before any install worker starts; after that it's read-only and can
/// be accessed lock-free from worker threads. Lookups return early
/// when the set is empty (no active links) — the common case on dev
/// machines without `bun link` configured, and unconditional on CI.
/// See `populateLinkedNamesCache` / `linkedPackagePath`.
linked_names: bun.StringHashMapUnmanaged(void) = .{},
linked_names_populated: bool = false,

onWake: WakeHandler = .{},
ci_mode: bun.LazyBool(computeIsContinuousIntegration, @This(), "ci_mode") = .{},

Expand Down Expand Up @@ -1177,6 +1187,8 @@ pub const getTemporaryDirectory = directories.getTemporaryDirectory;
pub const globalLinkDir = directories.globalLinkDir;
pub const globalLinkDirAndPath = directories.globalLinkDirAndPath;
pub const globalLinkDirPath = directories.globalLinkDirPath;
pub const linkedPackagePath = directories.linkedPackagePath;
pub const populateLinkedNamesCache = directories.populateLinkedNamesCache;
pub const isFolderInCache = directories.isFolderInCache;
pub const pathForCachedNPMPath = directories.pathForCachedNPMPath;
pub const pathForResolution = directories.pathForResolution;
Expand Down
99 changes: 99 additions & 0 deletions src/install/PackageManager/PackageManagerDirectories.zig
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,105 @@ pub fn globalLinkDirAndPath(this: *PackageManager) struct { std.fs.Dir, []const
return .{ dir, this.global_link_dir_path };
}

/// Read the global link dir once and populate
/// `this.linked_names` with every registered package name (including
/// scoped names as `@scope/name`). Must be called on the main thread
/// before any install worker touches `linkedPackagePath`; after that
/// the map is read-only and lock-free.
///
/// Safe to call repeatedly; subsequent calls are no-ops.
pub fn populateLinkedNamesCache(this: *PackageManager) void {
if (this.linked_names_populated) return;
this.linked_names_populated = true;

const dir_path = this.globalLinkDirPath();
const root_fd = switch (bun.openDirForIteration(bun.FD.cwd(), dir_path)) {
.result => |fd| fd,
// Dir missing / unreadable → empty set. Every linkedPackagePath
// lookup will short-circuit to null with no further syscalls.
.err => return,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};
defer root_fd.close();

var iter = bun.DirIterator.iterate(root_fd, if (bun.Environment.isWindows) .u16 else .u8);
while (iter.next().unwrap() catch null) |entry| {
const name = entry.name.slice();
if (name.len == 0) continue;

// Scope dirs (`@scope`) contain the actual links nested one
// level deeper; flatten to `@scope/name` in the cache.
if (name[0] == '@' and entry.kind == .directory) {
if (comptime bun.Environment.isWindows) {
// WTF-16 name; skip scope flattening on Windows for now.
// Falls through to the lstat path in linkedPackagePath.
continue;
}
const scope_fd = switch (bun.openDirForIteration(root_fd, name)) {
.result => |fd| fd,
.err => continue,
};
defer scope_fd.close();

var scope_iter = bun.DirIterator.iterate(scope_fd, .u8);
while (scope_iter.next().unwrap() catch null) |scope_entry| {
const sub_name = scope_entry.name.slice();
if (sub_name.len == 0) continue;
const full = std.fmt.allocPrint(this.allocator, "{s}/{s}", .{ name, sub_name }) catch continue;
this.linked_names.put(this.allocator, full, {}) catch continue;
}
continue;
}

if (comptime bun.Environment.isWindows) continue;
const dup = this.allocator.dupe(u8, name) catch continue;
this.linked_names.put(this.allocator, dup, {}) catch continue;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}

/// If `<globalLinkDir>/<pkg_name>` exists (typically a symlink created by
/// `bun link` from the producer dir), write its absolute path into `buf` and
/// return it. Otherwise `null`. Scoped names (`@scope/name`) are handled
/// because `joinAbsStringBufZ` preserves the `/`.
///
/// Performance: when `linked_names` has been populated (via
/// `populateLinkedNamesCache` at install start), this is a single
/// hashmap check with no syscalls. When the cache is empty (no active
/// links on this machine), it returns immediately. Falls back to the
/// per-call `lstat` when the cache has not been populated (e.g. on
/// Windows, or if a caller runs outside the isolated-install flow).
pub fn linkedPackagePath(
this: *PackageManager,
pkg_name: []const u8,
buf: *bun.PathBuffer,
) ?[:0]const u8 {
if (pkg_name.len == 0) return null;

const use_cache = this.linked_names_populated and !bun.Environment.isWindows;
if (use_cache) {
if (this.linked_names.count() == 0) return null;
if (!this.linked_names.contains(pkg_name)) return null;
const dir_path = this.globalLinkDirPath();
return bun.path.joinAbsStringBufZ(dir_path, buf, &.{pkg_name}, .auto);
}

const dir_path = this.globalLinkDirPath();
const joined = bun.path.joinAbsStringBufZ(dir_path, buf, &.{pkg_name}, .auto);
if (comptime bun.Environment.isWindows) {
const attrs = bun.sys.getFileAttributes(joined) orelse return null;
return if (attrs.is_directory or attrs.is_reparse_point) joined else null;
}
return switch (bun.sys.lstat(joined)) {
.result => |st| brk: {
const mode: u32 = @intCast(st.mode);
if (std.posix.S.ISDIR(mode) or std.posix.S.ISLNK(mode)) {
break :brk joined;
}
break :brk null;
},
.err => null,
};
}

pub fn pathForCachedNPMPath(
this: *PackageManager,
buf: *bun.PathBuffer,
Expand Down
1 change: 1 addition & 0 deletions src/install/PackageManager/patchPackage.zig
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,7 @@ fn overwritePackageInNodeModulesFolder(
.fromStdDir(cached_package_folder),
src_path,
dest_subpath,
&.{},
ignore_directories,
);
defer copier.deinit();
Expand Down
30 changes: 30 additions & 0 deletions src/install/isolated_install.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ pub fn installIsolatedPackages(
) OOM!PackageInstall.Summary {
bun.analytics.Features.isolated_bun_install += 1;

// Populate the linked-names cache once, on the main thread, before
// any install worker calls `manager.linkedPackagePath()`. Replaces a
// per-dependency `lstat` with a single readdir of the global link
// dir; short-circuits every subsequent lookup when nothing is
// linked (the common case on CI and dev machines without active
// links). See populateLinkedNamesCache in
// PackageManager/PackageManagerDirectories.zig.
manager.populateLinkedNamesCache();

const lockfile = manager.lockfile;

const store: Store = store: {
Expand Down Expand Up @@ -1577,6 +1586,17 @@ pub fn installIsolatedPackages(
// write the new project-local tree through the link into
// the shared cache). Treat the stale link as
// needs-install so `link_package` detaches and rebuilds.
// An active `bun link` for this package name means the
// producer dir is the source of truth — override the
// cache-based materialization regardless of whether the
// store dir already exists. The existence check below
// would otherwise short-circuit and leave the body stale.
// Only fires when the user didn't opt into the
// symlink-only backend.
var link_buf: bun.PathBuffer = undefined;
const has_active_link = PackageInstall.supported_method != .symlink and
manager.linkedPackagePath(pkg_name.slice(string_buf), &link_buf) != null;

const has_stale_gvs_link = !uses_global_store and stale: {
if (installer.global_store_path == null) break :stale false;
var local: bun.Path(.{ .sep = .auto }) = .initTopLevelDir();
Expand All @@ -1599,6 +1619,7 @@ pub fn installIsolatedPackages(
// should still take the cheap symlink-only path.
(is_new_bun_modules and !uses_global_store) or
has_stale_gvs_link or
has_active_link or
patch_info == .remove or
needs_install: {
var store_path: bun.AbsPath(.{}) = .initTopLevelDir();
Expand Down Expand Up @@ -1657,6 +1678,15 @@ pub fn installIsolatedPackages(
continue;
}

// `link_package` will source from the producer dir via
// `linkedPackagePath`; skip the cache-fetch dance entirely
// (mirrors how `.folder` is handled — no registry traffic
// needed when the body comes from an on-disk producer).
if (has_active_link) {
installer.startTask(entry_id);
continue;
}

var pkg_cache_dir_subpath: bun.RelPath(.{ .sep = .auto }) = .from(switch (pkg_res_tag) {
.npm => manager.cachedNPMPackageFolderName(pkg_name.slice(string_buf), pkg_res.value.npm.version, patch_info.contentsHash()),
.git => manager.cachedGitFolderName(&pkg_res.value.git, patch_info.contentsHash()),
Expand Down
3 changes: 2 additions & 1 deletion src/install/isolated_install/FileCopier.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub const FileCopier = struct {
src_dir: FD,
src_path: bun.AbsPath(.{ .sep = .auto, .unit = .os }),
dest_subpath: bun.Path(.{ .sep = .auto, .unit = .os }),
skip_filenames: []const bun.OSPathSlice,
skip_dirnames: []const bun.OSPathSlice,
) OOM!FileCopier {
return .{
Expand All @@ -16,7 +17,7 @@ pub const FileCopier = struct {
var w = try Walker.walk(
src_dir,
bun.default_allocator,
&.{},
skip_filenames,
skip_dirnames,
);
w.resolve_unknown_entry_types = true;
Expand Down
3 changes: 2 additions & 1 deletion src/install/isolated_install/Hardlinker.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub fn init(
folder_dir: FD,
src: bun.AbsPath(.{ .sep = .auto, .unit = .os }),
dest: bun.Path(.{ .sep = .auto, .unit = .os }),
skip_filenames: []const bun.OSPathSlice,
skip_dirnames: []const bun.OSPathSlice,
) OOM!Hardlinker {
return .{
Expand All @@ -19,7 +20,7 @@ pub fn init(
var w = try Walker.walk(
folder_dir,
bun.default_allocator,
&.{},
skip_filenames,
skip_dirnames,
);
w.resolve_unknown_entry_types = true;
Expand Down
Loading