-
Notifications
You must be signed in to change notification settings - Fork 5k
install: isolated linker honors active bun link #29615
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
092bf9b
ec74105
721837c
63a8f8e
e30bc30
56d1649
7718c6a
8a88cc7
6a90c13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -910,6 +910,107 @@ pub const PackCommand = struct { | |
| } | ||
| } | ||
|
|
||
| pub const PublishablePaths = struct { | ||
| arena: std.heap.ArenaAllocator, | ||
| /// Relative POSIX subpaths (sentinel-terminated) of files that | ||
| /// `bun pm pack` would include in the published tarball. Includes | ||
| /// `bin` entries and entries reachable via `package.json#files` | ||
| /// (or the entire tree if `files` is absent), and the auto-included | ||
| /// `package.json`. Default ignores plus `.npmignore` / `.gitignore` | ||
| /// at every depth are honored. | ||
| paths: []const [:0]const u8, | ||
|
|
||
| pub fn deinit(this: *@This()) void { | ||
| this.arena.deinit(); | ||
| } | ||
| }; | ||
|
|
||
| /// Single source of truth for "what files would `bun pm pack` ship?". | ||
| /// Used by the isolated linker (when honoring an active `bun link`) so | ||
| /// the contents of `node_modules/.bun/<pkg>/` match what a publish of | ||
| /// the producer would contain — including the auto-included | ||
| /// `package.json`, `bin` entries outside the `files` whitelist, and | ||
| /// the recursive semantics of `files: ["dist/**/*.js"]`-style globs. | ||
| /// Without this, callers were forced to mirror the publish rules by | ||
| /// hand and inevitably drifted (cf. nested same-name directories). | ||
| pub fn collectPublishablePaths( | ||
| parent_allocator: std.mem.Allocator, | ||
| root_dir: std.fs.Dir, | ||
| json_root: Expr, | ||
| ) OOM!PublishablePaths { | ||
| var arena = std.heap.ArenaAllocator.init(parent_allocator); | ||
| errdefer arena.deinit(); | ||
| const allocator = arena.allocator(); | ||
|
|
||
| var pack_queue = PackQueue.init(allocator, {}); | ||
|
|
||
| const bins = try getPackageBins(allocator, json_root); | ||
|
|
||
| for (bins) |bin| { | ||
| switch (bin.type) { | ||
| .file => try pack_queue.add(.{ .path = bin.path, .optional = true }), | ||
| .dir => { | ||
| const bin_dir = root_dir.openDir(bin.path, .{ .iterate = true }) catch continue; | ||
| try iterateProjectTree(allocator, &pack_queue, &.{}, .{ bin_dir, bin.path, 2 }, .silent); | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| if (json_root.get("files")) |files| { | ||
| if (files.asArray()) |_files_array| { | ||
| var includes: std.ArrayListUnmanaged(Pattern) = .{}; | ||
| var excludes: std.ArrayListUnmanaged(Pattern) = .{}; | ||
|
|
||
| var path_buf: PathBuffer = undefined; | ||
| var files_array = _files_array; | ||
| while (files_array.next()) |files_entry| { | ||
| const file_entry_str = files_entry.asString(allocator) orelse continue; | ||
| const normalized = bun.path.normalizeBuf(file_entry_str, &path_buf, .posix); | ||
| const parsed = try Pattern.fromUTF8(allocator, normalized) orelse continue; | ||
| if (parsed.flags.negated) { | ||
| try excludes.append(allocator, parsed); | ||
| } else { | ||
| try includes.append(allocator, parsed); | ||
| } | ||
| } | ||
|
|
||
| try iterateIncludedProjectTree( | ||
| allocator, | ||
| &pack_queue, | ||
| bins, | ||
| includes.items, | ||
| excludes.items, | ||
| root_dir, | ||
| .silent, | ||
| ); | ||
| } else { | ||
| // `files` not an array → malformed manifest. `bun pm pack` | ||
| // crashes here; we can't, so we mirror the no-`files` path | ||
| // (publish-default tree) instead of dropping everything. | ||
| try iterateProjectTree(allocator, &pack_queue, bins, .{ root_dir, "", 1 }, .silent); | ||
| } | ||
| } else { | ||
| try iterateProjectTree(allocator, &pack_queue, bins, .{ root_dir, "", 1 }, .silent); | ||
| } | ||
|
|
||
| // `package.json` is unconditionally included — both iterators skip | ||
| // it explicitly because the pack pipeline writes it from a | ||
| // normalized AST. We don't have that pipeline; just ship the file. | ||
| const pkg_path = try allocator.dupeZ(u8, "package.json"); | ||
|
|
||
| var paths = try allocator.alloc([:0]const u8, pack_queue.count() + 1); | ||
| paths[0] = pkg_path; | ||
| var i: usize = 1; | ||
| while (pack_queue.removeOrNull()) |item| : (i += 1) { | ||
| paths[i] = item.path; | ||
| } | ||
|
|
||
| return .{ | ||
| .arena = arena, | ||
| .paths = paths, | ||
| }; | ||
|
Comment on lines
+928
to
+1011
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This helper is described as the pack-time source of truth, but unlike 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| fn getBundledDeps( | ||
| allocator: std.mem.Allocator, | ||
| json: Expr, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -410,6 +410,123 @@ 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; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Best-effort: for users who have never run `bun link`, the global | ||||||||||||||||||||||||||||||||||
| // link dir may not exist (or may be unreadable). `globalLinkDirPath` | ||||||||||||||||||||||||||||||||||
| // would `Global.exit(1)` on setup failure — treat that same failure | ||||||||||||||||||||||||||||||||||
| // here as "no links on this machine" instead, and leave the cache | ||||||||||||||||||||||||||||||||||
| // empty so `linkedPackagePath` short-circuits to null. If a later | ||||||||||||||||||||||||||||||||||
| // code path really does need the global dir (e.g. `bun link` | ||||||||||||||||||||||||||||||||||
| // itself), `globalLinkDirPath` will be called on that path and | ||||||||||||||||||||||||||||||||||
| // surface the error there. | ||||||||||||||||||||||||||||||||||
| const dir_path = dir_path: { | ||||||||||||||||||||||||||||||||||
| if (this.global_link_dir_path.len != 0) break :dir_path this.global_link_dir_path; | ||||||||||||||||||||||||||||||||||
| var global_dir = Options.openGlobalDir(this.options.explicit_global_directory) catch return; | ||||||||||||||||||||||||||||||||||
| const link_dir = global_dir.makeOpenPath("node_modules", .{}) catch return; | ||||||||||||||||||||||||||||||||||
| this.global_dir = global_dir; | ||||||||||||||||||||||||||||||||||
| this.global_link_dir = link_dir; | ||||||||||||||||||||||||||||||||||
| var buf: bun.PathBuffer = undefined; | ||||||||||||||||||||||||||||||||||
| const path_slice = bun.getFdPath(.fromStdDir(link_dir), &buf) catch return; | ||||||||||||||||||||||||||||||||||
| this.global_link_dir_path = bun.handleOom(Fs.FileSystem.DirnameStore.instance.append([]const u8, path_slice)); | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+434
to
+440
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid caching a half-initialized global link dir. If Suggested fix- var global_dir = Options.openGlobalDir(this.options.explicit_global_directory) catch return;
- const link_dir = global_dir.makeOpenPath("node_modules", .{}) catch return;
- this.global_dir = global_dir;
- this.global_link_dir = link_dir;
+ var global_dir = Options.openGlobalDir(this.options.explicit_global_directory) catch return;
+ errdefer global_dir.close();
+ const link_dir = global_dir.makeOpenPath("node_modules", .{}) catch return;
+ errdefer link_dir.close();
var buf: bun.PathBuffer = undefined;
const path_slice = bun.getFdPath(.fromStdDir(link_dir), &buf) catch return;
+ this.global_dir = global_dir;
+ this.global_link_dir = link_dir;
this.global_link_dir_path = bun.handleOom(Fs.FileSystem.DirnameStore.instance.append([]const u8, path_slice));📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| break :dir_path this.global_link_dir_path; | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| 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, | ||||||||||||||||||||||||||||||||||
|
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 = bun.handleOom(std.fmt.allocPrint(this.allocator, "{s}/{s}", .{ name, sub_name })); | ||||||||||||||||||||||||||||||||||
| bun.handleOom(this.linked_names.put(this.allocator, full, {})); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (comptime bun.Environment.isWindows) continue; | ||||||||||||||||||||||||||||||||||
| const dup = bun.handleOom(this.allocator.dupe(u8, name)); | ||||||||||||||||||||||||||||||||||
| bun.handleOom(this.linked_names.put(this.allocator, dup, {})); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /// 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, | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.