diff --git a/src/cli/package_manager_command.zig b/src/cli/package_manager_command.zig index a7d3939a93..ca83c40805 100644 --- a/src/cli/package_manager_command.zig +++ b/src/cli/package_manager_command.zig @@ -20,24 +20,11 @@ const Path = @import("../resolver/resolve_path.zig"); const String = @import("../install/semver.zig").String; const ArrayIdentityContext = bun.ArrayIdentityContext; const DepIdSet = std.ArrayHashMapUnmanaged(DependencyID, void, ArrayIdentityContext, false); +const UntrustedCommand = @import("./pm_trusted_command.zig").UntrustedCommand; +const TrustCommand = @import("./pm_trusted_command.zig").TrustCommand; +const DefaultTrustedCommand = @import("./pm_trusted_command.zig").DefaultTrustedCommand; const Environment = bun.Environment; -fn handleLoadLockfileErrors(load_lockfile: Lockfile.LoadFromDiskResult, pm: *PackageManager) void { - if (load_lockfile == .not_found) { - if (pm.options.log_level != .silent) { - Output.errGeneric("Lockfile not found", .{}); - } - Global.exit(1); - } - - if (load_lockfile == .err) { - if (pm.options.log_level != .silent) { - Output.errGeneric("Error loading lockfile: {s}", .{@errorName(load_lockfile.err.value)}); - } - Global.exit(1); - } -} - const ByName = struct { dependencies: []const Dependency, buf: []const u8, @@ -52,6 +39,22 @@ const ByName = struct { }; pub const PackageManagerCommand = struct { + pub fn handleLoadLockfileErrors(load_lockfile: Lockfile.LoadFromDiskResult, pm: *PackageManager) void { + if (load_lockfile == .not_found) { + if (pm.options.log_level != .silent) { + Output.errGeneric("Lockfile not found", .{}); + } + Global.exit(1); + } + + if (load_lockfile == .err) { + if (pm.options.log_level != .silent) { + Output.errGeneric("Error loading lockfile: {s}", .{@errorName(load_lockfile.err.value)}); + } + Global.exit(1); + } + } + pub fn printHash(ctx: Command.Context, lockfile_: []const u8) !void { @setCold(true); var lockfile_buffer: [bun.MAX_PATH_BYTES]u8 = undefined; @@ -94,20 +97,20 @@ pub const PackageManagerCommand = struct { Output.prettyln( \\bun pm: Package manager utilities \\ - \\ bun pm bin print the path to bin folder - \\ -g bin print the global path to bin folder - \\ bun pm ls list the dependency tree according to the current lockfile - \\ --all list the entire dependency tree according to the current lockfile - \\ bun pm hash generate & print the hash of the current lockfile - \\ bun pm hash-string print the string used to hash the lockfile - \\ bun pm hash-print print the hash stored in the current lockfile - \\ bun pm cache print the path to the cache folder - \\ bun pm cache rm clear the cache - \\ bun pm migrate migrate another package manager's lockfile without installing anything - \\ bun pm trust(ed) print current trusted and untrusted dependencies with scripts - \\ \ trust dependencies and run scripts - \\ --all trust all untrusted dependencies and run their scripts - \\ --default print the list of default trusted dependencies + \\ bun pm bin print the path to bin folder + \\ -g print the global path to bin folder + \\ bun pm ls list the dependency tree according to the current lockfile + \\ --all list the entire dependency tree according to the current lockfile + \\ bun pm hash generate & print the hash of the current lockfile + \\ bun pm hash-string print the string used to hash the lockfile + \\ bun pm hash-print print the hash stored in the current lockfile + \\ bun pm cache print the path to the cache folder + \\ bun pm cache rm clear the cache + \\ bun pm migrate migrate another package manager's lockfile without installing anything + \\ bun pm untrusted print current untrusted dependencies with scripts + \\ bun pm trust names ... run scripts for untrusted dependencies and add to `trustedDependencies` + \\ --all trust all untrusted dependencies + \\ bun pm default-trusted print the default trusted dependencies list \\ \\Learn more about these at https://bun.sh/docs/cli/pm \\ @@ -247,379 +250,14 @@ pub const PackageManagerCommand = struct { Output.writer().writeAll(outpath) catch {}; Global.exit(0); - } else if (strings.eqlComptime(subcommand, "trusted") or (strings.eqlComptime(subcommand, "trust"))) { - - // do this before loading lockfile because you don't need a lockfile - // to see the default trusted dependencies - if (strings.leftHasAnyInRight(args, &.{"--default"})) { - Output.print("Default trusted dependencies ({d}):\n", .{Lockfile.default_trusted_dependencies_list.len}); - for (Lockfile.default_trusted_dependencies_list) |name| { - Output.pretty(" - {s}\n", .{name}); - } - - Global.exit(0); - } - - const load_lockfile = pm.lockfile.loadFromDisk(ctx.allocator, ctx.log, "bun.lockb"); - handleLoadLockfileErrors(load_lockfile, pm); - try pm.updateLockfileIfNeeded(load_lockfile); - const buf = pm.lockfile.buffers.string_bytes.items; - - if (args.len == 2) { - // no args, print information for trusted and untrusted dependencies with scripts. - const packages = pm.lockfile.packages.slice(); - const metas: []Lockfile.Package.Meta = packages.items(.meta); - const scripts: []Lockfile.Package.Scripts = packages.items(.scripts); - const resolutions: []Install.Resolution = packages.items(.resolution); - - var trusted_set: std.AutoArrayHashMapUnmanaged(u64, String) = .{}; - var untrusted_dep_ids: std.AutoArrayHashMapUnmanaged(DependencyID, void) = .{}; - defer untrusted_dep_ids.deinit(ctx.allocator); - - // loop through all dependencies, print all the trusted packages, and collect - // untrusted packages with lifecycle scripts - for (pm.lockfile.buffers.dependencies.items, 0..) |dep, i| { - const dep_id: DependencyID = @intCast(i); - const package_id = pm.lockfile.buffers.resolutions.items[dep_id]; - if (package_id == Install.invalid_package_id) continue; - - // called alias because a dependency name is not always the package name - const alias = dep.name.slice(buf); - - if (metas[package_id].hasInstallScript()) { - if (pm.lockfile.hasTrustedDependency(alias)) { - // can't put alias directly because it might be inline - try trusted_set.put(ctx.allocator, dep.name_hash, dep.name); - } else { - try untrusted_dep_ids.put(ctx.allocator, dep_id, {}); - } - } - } - - { - const Sorter = struct { - buf: string, - pub fn lessThan(this: @This(), rhs: String, lhs: String) bool { - return rhs.order(&lhs, this.buf, this.buf) == .lt; - } - }; - const aliases = trusted_set.values(); - std.sort.pdq(String, aliases, Sorter{ .buf = buf }, Sorter.lessThan); - - Output.pretty("Trusted dependencies ({d}):\n", .{aliases.len}); - for (aliases) |alias| { - Output.pretty(" - {s}\n", .{alias.slice(buf)}); - } else { - Output.pretty("\n", .{}); - } - - trusted_set.deinit(ctx.allocator); - } - - if (untrusted_dep_ids.count() == 0) { - Output.print("Untrusted dependencies (0):\n", .{}); - Global.exit(0); - } - - var untrusted_with_scripts: std.StringArrayHashMapUnmanaged(std.ArrayListUnmanaged(struct { - dep_id: DependencyID, - scripts_list: Lockfile.Package.Scripts.List, - })) = .{}; - defer untrusted_with_scripts.deinit(ctx.allocator); - - var tree_iterator = Lockfile.Tree.Iterator.init(pm.lockfile); - - const top_level_without_trailing_slash = strings.withoutTrailingSlash(Fs.FileSystem.instance.top_level_dir); - var abs_node_modules_path: std.ArrayListUnmanaged(u8) = .{}; - defer abs_node_modules_path.deinit(ctx.allocator); - try abs_node_modules_path.appendSlice(ctx.allocator, top_level_without_trailing_slash); - try abs_node_modules_path.append(ctx.allocator, std.fs.path.sep); - - while (tree_iterator.nextNodeModulesFolder(null)) |node_modules| { - // + 1 because we want to keep the path separator - abs_node_modules_path.items.len = top_level_without_trailing_slash.len + 1; - try abs_node_modules_path.appendSlice(ctx.allocator, node_modules.relative_path); - - var node_modules_dir = bun.openDir(std.fs.cwd(), node_modules.relative_path) catch |err| { - if (err == error.ENOENT) continue; - return err; - }; - defer node_modules_dir.close(); - - for (node_modules.dependencies) |dep_id| { - if (untrusted_dep_ids.contains(dep_id)) { - const dep = pm.lockfile.buffers.dependencies.items[dep_id]; - const alias = dep.name.slice(buf); - const package_id = pm.lockfile.buffers.resolutions.items[dep_id]; - const resolution = &resolutions[package_id]; - var package_scripts = scripts[package_id]; - - if (try package_scripts.getList( - pm.log, - pm.lockfile, - node_modules_dir, - abs_node_modules_path.items, - alias, - resolution, - )) |scripts_list| { - if (scripts_list.items.len == 0) continue; - const key = try ctx.allocator.dupe(u8, alias); - const gop = try untrusted_with_scripts.getOrPut(ctx.allocator, key); - if (!gop.found_existing) { - gop.value_ptr.* = .{}; - } else { - ctx.allocator.free(key); - } - - try gop.value_ptr.append(ctx.allocator, .{ .dep_id = dep_id, .scripts_list = scripts_list }); - } - } - } - } - - if (untrusted_with_scripts.count() == 0) { - Output.print("Untrusted dependencies (0):\n", .{}); - Global.exit(0); - } - - const Sorter = struct { - pub fn lessThan(_: void, rhs: string, lhs: string) bool { - return std.mem.order(u8, rhs, lhs) == .lt; - } - }; - - const aliases = untrusted_with_scripts.keys(); - std.sort.pdq(string, aliases, {}, Sorter.lessThan); - try untrusted_with_scripts.reIndex(ctx.allocator); - - Output.print("Untrusted dependencies ({d}):\n", .{aliases.len}); - - for (aliases) |alias| { - const _entry = untrusted_with_scripts.get(alias); - - if (comptime bun.Environment.allow_assert) { - std.debug.assert(_entry != null); - } - - if (_entry) |entry| { - if (comptime bun.Environment.allow_assert) { - std.debug.assert(entry.items.len > 0); - } - - Output.pretty(" - {s}\n", .{alias}); - } - } - - Global.exit(0); - } - - // this isn't great, flags could be in this slice, but it works - const packages_to_trust = args[2..]; - const trust_all = strings.leftHasAnyInRight(args, &.{ "-a", "--all" }); - - const packages = pm.lockfile.packages.slice(); - const metas: []Lockfile.Package.Meta = packages.items(.meta); - const resolutions: []Install.Resolution = packages.items(.resolution); - const scripts: []Lockfile.Package.Scripts = packages.items(.scripts); - - var untrusted_dep_ids: DepIdSet = .{}; - defer untrusted_dep_ids.deinit(ctx.allocator); - - // .1 go through all installed dependencies and find untrusted ones with scripts - // from packages through cli, or all if --all. - // .2 iterate through node_modules folder and spawn lifecycle scripts for each - // untrusted dependency from step 1. - // .3 add the untrusted dependencies to package.json and lockfile.trusted_dependencies. - - for (pm.lockfile.buffers.dependencies.items, 0..) |dep, i| { - const dep_id: u32 = @intCast(i); - const package_id = pm.lockfile.buffers.resolutions.items[dep_id]; - if (package_id == Install.invalid_package_id) continue; - - const alias = dep.name.slice(buf); - - if (metas[package_id].hasInstallScript()) { - if (trust_all and !pm.lockfile.hasTrustedDependency(alias)) { - try untrusted_dep_ids.put(ctx.allocator, dep_id, {}); - continue; - } - - for (packages_to_trust) |package_name_from_cli| { - if (strings.eqlLong(package_name_from_cli, alias, true) and !pm.lockfile.hasTrustedDependency(alias)) { - try untrusted_dep_ids.put(ctx.allocator, dep_id, {}); - continue; - } - } - } - } - - if (untrusted_dep_ids.count() == 0) Global.exit(0); - - // instead of running them right away, we group scripts by depth in the node_modules - // file structure, then run in descending order. this ensures lifecycle scripts are run - // in the correct order as they would during a normal install - var tree_iter = Lockfile.Tree.Iterator.init(pm.lockfile); - - const top_level_without_trailing_slash = strings.withoutTrailingSlash(Fs.FileSystem.instance.top_level_dir); - var abs_node_modules_path: std.ArrayListUnmanaged(u8) = .{}; - defer abs_node_modules_path.deinit(ctx.allocator); - try abs_node_modules_path.appendSlice(ctx.allocator, top_level_without_trailing_slash); - try abs_node_modules_path.append(ctx.allocator, std.fs.path.sep); - - var package_names_to_add: std.StringArrayHashMapUnmanaged(void) = .{}; - var scripts_at_depth: std.AutoArrayHashMapUnmanaged(usize, std.ArrayListUnmanaged(Lockfile.Package.Scripts.List)) = .{}; - defer { - var iter = scripts_at_depth.iterator(); - while (iter.next()) |entry| { - for (entry.value_ptr.items) |item| { - item.deinit(ctx.allocator); - } - entry.value_ptr.deinit(ctx.allocator); - } - scripts_at_depth.deinit(ctx.allocator); - package_names_to_add.deinit(ctx.allocator); - } - - var scripts_count: usize = 0; - - while (tree_iter.nextNodeModulesFolder(null)) |node_modules| { - abs_node_modules_path.items.len = top_level_without_trailing_slash.len + 1; - try abs_node_modules_path.appendSlice(ctx.allocator, node_modules.relative_path); - - var node_modules_dir = bun.openDir(std.fs.cwd(), node_modules.relative_path) catch |err| { - if (err == error.ENOENT) continue; - return err; - }; - defer node_modules_dir.close(); - - for (node_modules.dependencies) |dep_id| { - if (untrusted_dep_ids.contains(dep_id)) { - const dep = pm.lockfile.buffers.dependencies.items[dep_id]; - const alias = dep.name.slice(buf); - const package_id = pm.lockfile.buffers.resolutions.items[dep_id]; - const resolution = &resolutions[package_id]; - var package_scripts = scripts[package_id]; - - if (try package_scripts.getList( - pm.log, - pm.lockfile, - node_modules_dir, - abs_node_modules_path.items, - alias, - resolution, - )) |scripts_list| { - const entry = try scripts_at_depth.getOrPut(ctx.allocator, node_modules.depth); - if (!entry.found_existing) { - entry.value_ptr.* = .{}; - } - scripts_count += scripts_list.total; - try entry.value_ptr.append(ctx.allocator, scripts_list); - try package_names_to_add.put(ctx.allocator, try ctx.allocator.dupe(u8, alias), {}); - } - } - } - } - - if (scripts_at_depth.count() == 0) Global.exit(0); - - var root_node: *Progress.Node = undefined; - var scripts_node: Progress.Node = undefined; - var progress = &pm.progress; - - if (pm.options.log_level.showProgress()) { - root_node = progress.start("", 0); - progress.supports_ansi_escape_codes = Output.enable_ansi_colors_stderr; - - scripts_node = root_node.start(PackageManager.ProgressStrings.script(), scripts_count); - pm.scripts_node = &scripts_node; - } - - var depth = scripts_at_depth.count(); - while (depth > 0) { - depth -= 1; - const _entry = scripts_at_depth.get(depth); - if (comptime bun.Environment.allow_assert) { - std.debug.assert(_entry != null); - } - if (_entry) |entry| { - for (entry.items) |scripts_list| { - switch (pm.options.log_level) { - inline else => |log_level| try pm.spawnPackageLifecycleScripts(ctx, scripts_list, log_level), - } - - if (pm.options.log_level.showProgress()) { - scripts_node.activate(); - progress.refresh(); - } - } - - const loop = pm.event_loop.loop(); - while (pm.pending_lifecycle_script_tasks.load(.Monotonic) > 0) { - loop.tick(); - } - } - } - - if (pm.options.log_level.showProgress()) { - progress.root.end(); - progress.* = .{}; - } - - const package_json_contents = try pm.root_package_json_file.readToEndAlloc(ctx.allocator, try pm.root_package_json_file.getEndPos()); - defer ctx.allocator.free(package_json_contents); - - const package_json_source = logger.Source.initPathString(PackageManager.package_json_cwd, package_json_contents); - - var package_json = bun.JSON.ParseJSONUTF8(&package_json_source, ctx.log, ctx.allocator) catch |err| { - switch (Output.enable_ansi_colors) { - inline else => |enable_ansi_colors| ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), enable_ansi_colors) catch {}, - } - - if (err == error.ParserError and ctx.log.errors > 0) { - Output.prettyErrorln("error: Failed to parse package.json", .{}); - Global.crash(); - } - - Output.panic("{s} parsing package.json", .{ - @errorName(err), - }); - }; - - // now add the package names to lockfile.trustedDependencies and package.json `trustedDependencies` - const names_count = package_names_to_add.count(); - if (comptime Environment.allow_assert) { - std.debug.assert(names_count > 0); - } - - // could be null if these are the first packages to be trusted - if (names_count > 0 and pm.lockfile.trusted_dependencies == null) pm.lockfile.trusted_dependencies = .{}; - - const names = package_names_to_add.keys(); - - try Install.PackageManager.PackageJSONEditor.editTrustedDependencies(ctx.allocator, &package_json, names); - - for (names) |name| { - try pm.lockfile.trusted_dependencies.?.put(ctx.allocator, @truncate(String.Builder.stringHash(name)), {}); - } - - pm.lockfile.saveToDisk(pm.options.lockfile_path); - - var buffer_writer = try bun.js_printer.BufferWriter.init(ctx.allocator); - try buffer_writer.buffer.list.ensureTotalCapacity(ctx.allocator, package_json_contents.len + 1); - buffer_writer.append_newline = package_json_contents.len > 0 and package_json_contents[package_json_contents.len - 1] == '\n'; - var package_json_writer = bun.js_printer.BufferPrinter.init(buffer_writer); - - _ = bun.js_printer.printJSON(@TypeOf(&package_json_writer), &package_json_writer, package_json, &package_json_source) catch |err| { - Output.prettyErrorln("package.json failed to write due to error {s}", .{@errorName(err)}); - Global.crash(); - }; - - const new_package_json_contents = package_json_writer.ctx.writtenWithoutTrailingZero(); - - try pm.root_package_json_file.pwriteAll(new_package_json_contents, 0); - std.os.ftruncate(pm.root_package_json_file.handle, new_package_json_contents.len) catch {}; - pm.root_package_json_file.close(); - + } else if (strings.eqlComptime(subcommand, "default-trusted")) { + try DefaultTrustedCommand.exec(); + Global.exit(0); + } else if (strings.eqlComptime(subcommand, "untrusted")) { + try UntrustedCommand.exec(ctx, pm, args); + Global.exit(0); + } else if (strings.eqlComptime(subcommand, "trust")) { + try TrustCommand.exec(ctx, pm, args); Global.exit(0); } else if (strings.eqlComptime(subcommand, "ls")) { const load_lockfile = pm.lockfile.loadFromDisk(ctx.allocator, ctx.log, "bun.lockb"); @@ -746,10 +384,9 @@ fn printNodeModulesFolderStructure( depth: usize, directories: *std.ArrayList(NodeModulesFolder), lockfile: *Lockfile, - more_packages_: []bool, + more_packages: []bool, ) !void { const allocator = lockfile.allocator; - var more_packages = more_packages_; const resolutions = lockfile.packages.items(.resolution); const string_bytes = lockfile.buffers.string_bytes.items; diff --git a/src/cli/pm_trusted_command.zig b/src/cli/pm_trusted_command.zig new file mode 100644 index 0000000000..2093032425 --- /dev/null +++ b/src/cli/pm_trusted_command.zig @@ -0,0 +1,447 @@ +const std = @import("std"); +const Progress = std.Progress; +const bun = @import("root").bun; +const logger = bun.logger; +const Environment = bun.Environment; +const Command = @import("../cli.zig").Command; +const Install = @import("../install/install.zig"); +const PackageID = Install.PackageID; +const String = @import("../install/semver.zig").String; +const PackageManager = Install.PackageManager; +const PackageManagerCommand = @import("./package_manager_command.zig").PackageManagerCommand; +const Lockfile = Install.Lockfile; +const Fs = @import("../fs.zig"); +const Global = bun.Global; +const DependencyID = Install.DependencyID; +const ArrayIdentityContext = bun.ArrayIdentityContext; +const DepIdSet = std.ArrayHashMapUnmanaged(DependencyID, void, ArrayIdentityContext, false); +const strings = bun.strings; +const string = bun.string; +const Output = bun.Output; + +pub const DefaultTrustedCommand = struct { + pub fn exec() !void { + Output.print("Default trusted dependencies ({d}):\n", .{Lockfile.default_trusted_dependencies_list.len}); + for (Lockfile.default_trusted_dependencies_list) |name| { + Output.pretty(" - {s}\n", .{name}); + } + + return; + } +}; + +pub const UntrustedCommand = struct { + pub fn exec(ctx: Command.Context, pm: *PackageManager, args: [][:0]u8) !void { + _ = args; + Output.prettyError("bun pm untrusted v" ++ Global.package_json_version_with_sha ++ "\n\n", .{}); + Output.flush(); + + const load_lockfile = pm.lockfile.loadFromDisk(ctx.allocator, ctx.log, "bun.lockb"); + PackageManagerCommand.handleLoadLockfileErrors(load_lockfile, pm); + try pm.updateLockfileIfNeeded(load_lockfile); + + const packages = pm.lockfile.packages.slice(); + const metas: []Lockfile.Package.Meta = packages.items(.meta); + const scripts: []Lockfile.Package.Scripts = packages.items(.scripts); + const resolutions: []Install.Resolution = packages.items(.resolution); + const buf = pm.lockfile.buffers.string_bytes.items; + + var untrusted_dep_ids: std.AutoArrayHashMapUnmanaged(DependencyID, void) = .{}; + defer untrusted_dep_ids.deinit(ctx.allocator); + + // loop through dependencies and get trusted and untrusted deps with lifecycle scripts + for (pm.lockfile.buffers.dependencies.items, 0..) |dep, i| { + const dep_id: DependencyID = @intCast(i); + const package_id = pm.lockfile.buffers.resolutions.items[dep_id]; + if (package_id == Install.invalid_package_id) continue; + + // called alias because a dependency name is not always the package name + const alias = dep.name.slice(buf); + + if (metas[package_id].hasInstallScript()) { + if (!pm.lockfile.hasTrustedDependency(alias)) { + try untrusted_dep_ids.put(ctx.allocator, dep_id, {}); + } + } + } + + if (untrusted_dep_ids.count() == 0) { + printZeroUntrustedDependenciesFound(); + return; + } + + var untrusted_deps: std.AutoArrayHashMapUnmanaged(DependencyID, Lockfile.Package.Scripts.List) = .{}; + defer untrusted_deps.deinit(ctx.allocator); + + var tree_iterator = Lockfile.Tree.Iterator.init(pm.lockfile); + + const top_level_without_trailing_slash = strings.withoutTrailingSlash(Fs.FileSystem.instance.top_level_dir); + var abs_node_modules_path: std.ArrayListUnmanaged(u8) = .{}; + defer abs_node_modules_path.deinit(ctx.allocator); + try abs_node_modules_path.appendSlice(ctx.allocator, top_level_without_trailing_slash); + try abs_node_modules_path.append(ctx.allocator, std.fs.path.sep); + + while (tree_iterator.nextNodeModulesFolder(null)) |node_modules| { + // + 1 because we want to keep the path separator + abs_node_modules_path.items.len = top_level_without_trailing_slash.len + 1; + try abs_node_modules_path.appendSlice(ctx.allocator, node_modules.relative_path); + + var node_modules_dir = bun.openDir(std.fs.cwd(), node_modules.relative_path) catch |err| { + if (err == error.ENOENT) continue; + return err; + }; + defer node_modules_dir.close(); + + for (node_modules.dependencies) |dep_id| { + if (untrusted_dep_ids.contains(dep_id)) { + const dep = pm.lockfile.buffers.dependencies.items[dep_id]; + const alias = dep.name.slice(buf); + const package_id = pm.lockfile.buffers.resolutions.items[dep_id]; + const resolution = &resolutions[package_id]; + var package_scripts = scripts[package_id]; + + if (try package_scripts.getList( + pm.log, + pm.lockfile, + node_modules_dir, + abs_node_modules_path.items, + alias, + resolution, + )) |scripts_list| { + if (scripts_list.total == 0 or scripts_list.items.len == 0) continue; + try untrusted_deps.put(ctx.allocator, dep_id, scripts_list); + } + } + } + } + + if (untrusted_deps.count() == 0) { + printZeroUntrustedDependenciesFound(); + return; + } + + var iter = untrusted_deps.iterator(); + while (iter.next()) |entry| { + const dep_id = entry.key_ptr.*; + const scripts_list = entry.value_ptr.*; + const package_id = pm.lockfile.buffers.resolutions.items[dep_id]; + const resolution = pm.lockfile.packages.items(.resolution)[package_id]; + + scripts_list.printScripts(&resolution, buf, .untrusted); + Output.pretty("\n", .{}); + } + + Output.pretty( + \\These dependencies had their lifecycle scripts blocked during install. + \\ + \\If you trust them and wish to run their scripts, use `bun pm trust`. + \\ + , .{}); + } + + fn printZeroUntrustedDependenciesFound() void { + Output.pretty( + \\Found 0 untrusted dependencies with scripts. + \\ + \\This means all packages with scripts are in "trustedDependencies" or none of your dependencies have scripts. + \\ + \\For more information, visit https://bun.sh/docs/install/lifecycle#trusteddependencies + \\ + , .{}); + } +}; + +pub const TrustCommand = struct { + pub const Sorter = struct { + pub fn lessThan(_: void, rhs: string, lhs: string) bool { + return std.mem.order(u8, rhs, lhs) == .lt; + } + }; + + fn errorExpectedArgs() noreturn { + Output.errGeneric("expected package names(s) or --all", .{}); + Global.crash(); + } + + fn printErrorZeroUntrustedDependenciesFound(trust_all: bool, packages_to_trust: []const string) void { + Output.print("\n", .{}); + if (trust_all) { + Output.errGeneric("0 scripts ran. This means all dependencies are already trusted or non have scripts.", .{}); + } else { + Output.errGeneric("0 scripts ran. The following packages are already trusted, don't have scripts to run, or don't exist:\n\n", .{}); + for (packages_to_trust) |arg| { + Output.prettyError(" - {s}\n", .{arg}); + } + } + } + + pub fn exec(ctx: Command.Context, pm: *PackageManager, args: [][:0]u8) !void { + Output.prettyError("bun pm trust v" ++ Global.package_json_version_with_sha ++ "\n", .{}); + Output.flush(); + + if (args.len == 2) errorExpectedArgs(); + + const load_lockfile = pm.lockfile.loadFromDisk(ctx.allocator, ctx.log, "bun.lockb"); + PackageManagerCommand.handleLoadLockfileErrors(load_lockfile, pm); + try pm.updateLockfileIfNeeded(load_lockfile); + + var packages_to_trust: std.ArrayListUnmanaged(string) = .{}; + defer packages_to_trust.deinit(ctx.allocator); + try packages_to_trust.ensureUnusedCapacity(ctx.allocator, args[2..].len); + for (args[2..]) |arg| { + if (arg.len > 0 and arg[0] != '-') packages_to_trust.appendAssumeCapacity(arg); + } + const trust_all = strings.leftHasAnyInRight(args, &.{ "-a", "--all" }); + + if (!trust_all and packages_to_trust.items.len == 0) errorExpectedArgs(); + + const buf = pm.lockfile.buffers.string_bytes.items; + const packages = pm.lockfile.packages.slice(); + const metas: []Lockfile.Package.Meta = packages.items(.meta); + const resolutions: []Install.Resolution = packages.items(.resolution); + const scripts: []Lockfile.Package.Scripts = packages.items(.scripts); + + var untrusted_dep_ids: DepIdSet = .{}; + defer untrusted_dep_ids.deinit(ctx.allocator); + + for (pm.lockfile.buffers.dependencies.items, pm.lockfile.buffers.resolutions.items, 0..) |dep, package_id, i| { + const dep_id: u32 = @intCast(i); + if (package_id == Install.invalid_package_id) continue; + + const alias = dep.name.slice(buf); + + if (metas[package_id].hasInstallScript()) { + if (!pm.lockfile.hasTrustedDependency(alias)) { + try untrusted_dep_ids.put(ctx.allocator, dep_id, {}); + } + } + } + + if (untrusted_dep_ids.count() == 0) { + printErrorZeroUntrustedDependenciesFound(trust_all, packages_to_trust.items); + Global.crash(); + } + + // Instead of running them right away, we group scripts by depth in the node_modules + // file structure, then run them starting at max depth. This ensures lifecycle scripts are run + // in the correct order as they would during a normal install + var tree_iter = Lockfile.Tree.Iterator.init(pm.lockfile); + + const top_level_without_trailing_slash = strings.withoutTrailingSlash(Fs.FileSystem.instance.top_level_dir); + var abs_node_modules_path: std.ArrayListUnmanaged(u8) = .{}; + defer abs_node_modules_path.deinit(ctx.allocator); + try abs_node_modules_path.appendSlice(ctx.allocator, top_level_without_trailing_slash); + try abs_node_modules_path.append(ctx.allocator, std.fs.path.sep); + + var package_names_to_add: std.StringArrayHashMapUnmanaged(void) = .{}; + var scripts_at_depth: std.AutoArrayHashMapUnmanaged(usize, std.ArrayListUnmanaged(struct { + package_id: PackageID, + scripts_list: Lockfile.Package.Scripts.List, + skip: bool, + })) = .{}; + + var scripts_count: usize = 0; + + while (tree_iter.nextNodeModulesFolder(null)) |node_modules| { + abs_node_modules_path.items.len = top_level_without_trailing_slash.len + 1; + try abs_node_modules_path.appendSlice(ctx.allocator, node_modules.relative_path); + + var node_modules_dir = bun.openDir(std.fs.cwd(), node_modules.relative_path) catch |err| { + if (err == error.ENOENT) continue; + return err; + }; + defer node_modules_dir.close(); + + for (node_modules.dependencies) |dep_id| { + if (untrusted_dep_ids.contains(dep_id)) { + const dep = pm.lockfile.buffers.dependencies.items[dep_id]; + const alias = dep.name.slice(buf); + const package_id = pm.lockfile.buffers.resolutions.items[dep_id]; + if (comptime Environment.allow_assert) { + std.debug.assert(package_id != Install.invalid_package_id); + } + const resolution = &resolutions[package_id]; + var package_scripts = scripts[package_id]; + + if (try package_scripts.getList( + pm.log, + pm.lockfile, + node_modules_dir, + abs_node_modules_path.items, + alias, + resolution, + )) |scripts_list| { + const skip = brk: { + if (trust_all) break :brk false; + + for (packages_to_trust.items) |package_name_from_cli| { + if (strings.eqlLong(package_name_from_cli, alias, true) and !pm.lockfile.hasTrustedDependency(alias)) { + break :brk false; + } + } + + break :brk true; + }; + + // even if it is skipped we still add to scripts_at_depth for logging later + const entry = try scripts_at_depth.getOrPut(ctx.allocator, node_modules.depth); + if (!entry.found_existing) entry.value_ptr.* = .{}; + try entry.value_ptr.append(ctx.allocator, .{ + .package_id = package_id, + .scripts_list = scripts_list, + .skip = skip, + }); + + if (!skip) { + try package_names_to_add.put(ctx.allocator, try ctx.allocator.dupe(u8, alias), {}); + scripts_count += scripts_list.total; + } + } + } + } + } + + if (scripts_at_depth.count() == 0 or package_names_to_add.count() == 0) { + printErrorZeroUntrustedDependenciesFound(trust_all, packages_to_trust.items); + Global.crash(); + } + + var root_node: *Progress.Node = undefined; + var scripts_node: Progress.Node = undefined; + var progress = &pm.progress; + + if (pm.options.log_level.showProgress()) { + root_node = progress.start("", 0); + progress.supports_ansi_escape_codes = Output.enable_ansi_colors_stderr; + + scripts_node = root_node.start(PackageManager.ProgressStrings.script(), scripts_count); + pm.scripts_node = &scripts_node; + } + + var depth = scripts_at_depth.count(); + while (depth > 0) { + depth -= 1; + const _entry = scripts_at_depth.get(depth); + if (comptime bun.Environment.allow_assert) { + std.debug.assert(_entry != null); + } + if (_entry) |entry| { + for (entry.items) |info| { + if (info.skip) continue; + + switch (pm.options.log_level) { + inline else => |log_level| try pm.spawnPackageLifecycleScripts(ctx, info.scripts_list, log_level), + } + + if (pm.options.log_level.showProgress()) { + scripts_node.activate(); + progress.refresh(); + } + } + + while (pm.pending_lifecycle_script_tasks.load(.Monotonic) > 0) { + pm.event_loop.loop().tick(); + } + } + } + + if (pm.options.log_level.showProgress()) { + progress.root.end(); + progress.* = .{}; + } + + const package_json_contents = try pm.root_package_json_file.readToEndAlloc(ctx.allocator, try pm.root_package_json_file.getEndPos()); + defer ctx.allocator.free(package_json_contents); + + const package_json_source = logger.Source.initPathString(PackageManager.package_json_cwd, package_json_contents); + + var package_json = bun.JSON.ParseJSONUTF8(&package_json_source, ctx.log, ctx.allocator) catch |err| { + switch (Output.enable_ansi_colors) { + inline else => |enable_ansi_colors| ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), enable_ansi_colors) catch {}, + } + + Output.errGeneric("failed to parse package.json: {s}", .{@errorName(err)}); + Global.crash(); + }; + + // now add the package names to lockfile.trustedDependencies and package.json `trustedDependencies` + const names = package_names_to_add.keys(); + if (comptime Environment.allow_assert) { + std.debug.assert(names.len > 0); + } + + // could be null if these are the first packages to be trusted + if (pm.lockfile.trusted_dependencies == null) pm.lockfile.trusted_dependencies = .{}; + + var total_scripts_ran: usize = 0; + var total_packages_with_scripts: usize = 0; + var total_skipped_packages: usize = 0; + + Output.print("\n", .{}); + + depth = scripts_at_depth.count(); + while (depth > 0) { + depth -= 1; + if (scripts_at_depth.get(depth)) |entry| { + for (entry.items) |info| { + const resolution = pm.lockfile.packages.items(.resolution)[info.package_id]; + if (info.skip) { + info.scripts_list.printScripts(&resolution, buf, .untrusted); + total_skipped_packages += 1; + } else { + total_packages_with_scripts += 1; + total_scripts_ran += info.scripts_list.total; + info.scripts_list.printScripts(&resolution, buf, .completed); + } + Output.print("\n", .{}); + } + } + } + + try Install.PackageManager.PackageJSONEditor.editTrustedDependencies(ctx.allocator, &package_json, names); + + for (names) |name| { + try pm.lockfile.trusted_dependencies.?.put(ctx.allocator, @truncate(String.Builder.stringHash(name)), {}); + } + + pm.lockfile.saveToDisk(pm.options.lockfile_path); + + var buffer_writer = try bun.js_printer.BufferWriter.init(ctx.allocator); + try buffer_writer.buffer.list.ensureTotalCapacity(ctx.allocator, package_json_contents.len + 1); + buffer_writer.append_newline = package_json_contents.len > 0 and package_json_contents[package_json_contents.len - 1] == '\n'; + var package_json_writer = bun.js_printer.BufferPrinter.init(buffer_writer); + + _ = bun.js_printer.printJSON(@TypeOf(&package_json_writer), &package_json_writer, package_json, &package_json_source) catch |err| { + Output.errGeneric("failed to print package.json: {s}", .{@errorName(err)}); + Global.crash(); + }; + + const new_package_json_contents = package_json_writer.ctx.writtenWithoutTrailingZero(); + + try pm.root_package_json_file.pwriteAll(new_package_json_contents, 0); + std.os.ftruncate(pm.root_package_json_file.handle, new_package_json_contents.len) catch {}; + pm.root_package_json_file.close(); + + if (comptime Environment.allow_assert) { + std.debug.assert(total_scripts_ran > 0); + } + + Output.pretty(" {d} script{s} ran across {d} package{s} ", .{ + total_scripts_ran, + if (total_scripts_ran > 1) "s" else "", + total_packages_with_scripts, + if (total_packages_with_scripts > 1) "s" else "", + }); + + Output.printStartEndStdout(bun.start_time, std.time.nanoTimestamp()); + Output.print("\n", .{}); + + if (total_skipped_packages > 0) { + Output.print("\n", .{}); + Output.prettyln(" {d} package{s} with blocked scripts", .{ + total_skipped_packages, + if (total_skipped_packages > 1) "s" else "", + }); + } + } +}; diff --git a/src/install/bin.zig b/src/install/bin.zig index 1d752d9be2..0e53e20b30 100644 --- a/src/install/bin.zig +++ b/src/install/bin.zig @@ -341,10 +341,14 @@ pub const Bin = extern struct { target_path_trim = target_path_trim[3..]; } setPermissions(node_modules.fd, target_path_trim); - this.err = err; + return; } + + this.err = err; + return; }; setPermissions(node_modules.fd, dest_path); + return; } else { const WinBinLinkingShim = @import("./windows-shim/BinLinkingShim.zig"); diff --git a/src/install/install.zig b/src/install/install.zig index 5c8bb484de..d06224d5e9 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -857,8 +857,10 @@ pub const PackageInstall = struct { skipped: u32 = 0, successfully_installed: ?Bitset = null, - // deduplicated - packages_with_skipped_scripts_set: std.AutoArrayHashMapUnmanaged(TruncatedPackageNameHash, void) = .{}, + /// Package name hash -> number of scripts skipped. + /// Multiple versions of the same package might add to the count, and each version + /// might have a different number of scripts + packages_with_blocked_scripts: std.AutoArrayHashMapUnmanaged(TruncatedPackageNameHash, usize) = .{}, }; pub const Method = enum { @@ -6847,10 +6849,10 @@ pub const PackageManager = struct { lockfile_path_z, )) { .ok => |load| manager.lockfile = load.lockfile, - else => try manager.lockfile.initEmpty(allocator), + else => manager.lockfile.initEmpty(allocator), } } else { - try manager.lockfile.initEmpty(allocator); + manager.lockfile.initEmpty(allocator); } return manager; @@ -6920,7 +6922,7 @@ pub const PackageManager = struct { package_json_cwd, current_package_json_buf[0..current_package_json_contents_len], ); - try lockfile.initEmpty(ctx.allocator); + lockfile.initEmpty(ctx.allocator); try package.parseMain(&lockfile, ctx.allocator, manager.log, package_json_source, Features.folder); name = lockfile.str(&package.name); @@ -7096,7 +7098,7 @@ pub const PackageManager = struct { package_json_cwd, current_package_json_buf[0..current_package_json_contents_len], ); - try lockfile.initEmpty(ctx.allocator); + lockfile.initEmpty(ctx.allocator); try package.parseMain(&lockfile, ctx.allocator, manager.log, package_json_source, Features.folder); name = lockfile.str(&package.name); @@ -8351,9 +8353,90 @@ pub const PackageManager = struct { } } + fn getInstalledPackageScriptsCount( + this: *PackageInstaller, + alias: string, + package_id: PackageID, + resolution_tag: Resolution.Tag, + comptime log_level: Options.LogLevel, + ) usize { + if (comptime Environment.allow_assert) { + std.debug.assert(resolution_tag != .root); + std.debug.assert(package_id != 0); + } + var count: usize = 0; + const scripts = brk: { + const scripts = this.lockfile.packages.items(.scripts)[package_id]; + if (scripts.filled) break :brk scripts; + + var temp: Package.Scripts = .{}; + var temp_lockfile: Lockfile = undefined; + temp_lockfile.initEmpty(this.lockfile.allocator); + defer temp_lockfile.deinit(); + var string_builder = temp_lockfile.stringBuilder(); + temp.fillFromPackageJSON( + this.lockfile.allocator, + &string_builder, + this.manager.log, + this.node_modules_folder, + alias, + ) catch |err| { + if (comptime log_level != .silent) { + Output.errGeneric("failed to fill lifecycle scripts for {s}: {s}", .{ + alias, + @errorName(err), + }); + } + + if (this.manager.options.enable.fail_early) { + Global.crash(); + } + + return 0; + }; + break :brk temp; + }; + + if (comptime Environment.allow_assert) { + std.debug.assert(scripts.filled); + } + + switch (resolution_tag) { + .git, .github, .gitlab, .root => { + inline for (Lockfile.Scripts.names) |script_name| { + count += @intFromBool(!@field(scripts, script_name).isEmpty()); + } + }, + else => { + const install_script_names = .{ + "preinstall", + "install", + "postinstall", + }; + inline for (install_script_names) |script_name| { + count += @intFromBool(!@field(scripts, script_name).isEmpty()); + } + }, + } + + if (scripts.preinstall.isEmpty() and scripts.install.isEmpty()) { + const binding_dot_gyp_path = Path.joinAbsStringZ( + this.node_modules_folder_path.items, + &[_]string{ + alias, + "binding.gyp", + }, + .auto, + ); + count += @intFromBool(Syscall.exists(binding_dot_gyp_path)); + } + + return count; + } + fn installPackageWithNameAndResolution( this: *PackageInstaller, - dependency_id: PackageID, + dependency_id: DependencyID, package_id: PackageID, comptime log_level: Options.LogLevel, name: string, @@ -8577,11 +8660,23 @@ pub const PackageManager = struct { } } - if (resolution.tag != .workspace and !is_trusted and this.lockfile.packages.get(package_id).meta.hasInstallScript()) { - if (comptime log_level.isVerbose()) { - Output.prettyError("Blocked scripts for: {s}@{}\n", .{ alias, resolution.fmt(this.lockfile.buffers.string_bytes.items) }); + if (resolution.tag != .workspace and !is_trusted and this.lockfile.packages.items(.meta)[package_id].hasInstallScript()) { + // Check if the package actually has scripts. `hasInstallScript` can be false positive if a package is published with + // an auto binding.gyp rebuild script but binding.gyp is excluded from the published files. + const count = this.getInstalledPackageScriptsCount(alias, package_id, resolution.tag, log_level); + if (count > 0) { + if (comptime log_level.isVerbose()) { + Output.prettyError("Blocked {d} scripts for: {s}@{}\n", .{ + count, + alias, + resolution.fmt(this.lockfile.buffers.string_bytes.items), + }); + } + + const entry = this.summary.packages_with_blocked_scripts.getOrPut(this.manager.allocator, name_hash) catch bun.outOfMemory(); + if (!entry.found_existing) entry.value_ptr.* = 0; + entry.value_ptr.* += count; } - this.summary.packages_with_skipped_scripts_set.put(this.manager.allocator, name_hash, {}) catch bun.outOfMemory(); } this.incrementTreeInstallCount(this.current_tree_id, log_level); @@ -8950,25 +9045,24 @@ pub const PackageManager = struct { } } - fn addDependencyAndDependenciesToSet( - name_hash_set: *std.AutoArrayHashMapUnmanaged(TruncatedPackageNameHash, void), + fn addDependenciesToSet( + names: *std.AutoArrayHashMapUnmanaged(TruncatedPackageNameHash, void), lockfile: *Lockfile, - dep_name_hash: PackageNameHash, dependencies_slice: Lockfile.DependencySlice, ) void { - name_hash_set.put(lockfile.allocator, @truncate(dep_name_hash), {}) catch bun.outOfMemory(); - const begin = dependencies_slice.off; const end = begin +| dependencies_slice.len; var dep_id = begin; while (dep_id < end) : (dep_id += 1) { - const dep = lockfile.buffers.dependencies.items[dep_id]; - const package_id = lockfile.buffers.resolutions.items[dep_id]; if (package_id == invalid_package_id) continue; - const dependency_slice = lockfile.packages.items(.dependencies)[package_id]; - addDependencyAndDependenciesToSet(name_hash_set, lockfile, dep.name_hash, dependency_slice); + const dep = lockfile.buffers.dependencies.items[dep_id]; + const entry = names.getOrPut(lockfile.allocator, @truncate(dep.name_hash)) catch bun.outOfMemory(); + if (!entry.found_existing) { + const dependency_slice = lockfile.packages.items(.dependencies)[package_id]; + addDependenciesToSet(names, lockfile, dependency_slice); + } } } @@ -9172,8 +9266,11 @@ pub const PackageManager = struct { const package_id = this.lockfile.buffers.resolutions.items[dep_id]; if (package_id == invalid_package_id) continue; - const dependency_slice = this.lockfile.packages.items(.dependencies)[package_id]; - addDependencyAndDependenciesToSet(&set, this.lockfile, root_dep.name_hash, dependency_slice); + const entry = set.getOrPut(this.lockfile.allocator, @truncate(root_dep.name_hash)) catch bun.outOfMemory(); + if (!entry.found_existing) { + const dependency_slice = this.lockfile.packages.items(.dependencies)[package_id]; + addDependenciesToSet(&set, this.lockfile, dependency_slice); + } break; } } @@ -9400,9 +9497,9 @@ pub const PackageManager = struct { &[_]string{"binding.gyp"}, .auto, ); - if (comptime Environment.allow_assert) { - std.debug.assert(root_package.scripts.filled); - } + + // might need to read scripts from disk if we are migrating from package-lock.json + if (root_package.scripts.hasAny()) { const add_node_gyp_rebuild_script = root_package.scripts.install.isEmpty() and root_package.scripts.preinstall.isEmpty() and Syscall.exists(binding_dot_gyp_path); @@ -9512,7 +9609,7 @@ pub const PackageManager = struct { if (needs_new_lockfile) break :differ; var lockfile: Lockfile = undefined; - try lockfile.initEmpty(ctx.allocator); + lockfile.initEmpty(ctx.allocator); var maybe_root = Lockfile.Package{}; try maybe_root.parseMain( @@ -9660,7 +9757,7 @@ pub const PackageManager = struct { if (needs_new_lockfile) { root = .{}; - try manager.lockfile.initEmpty(ctx.allocator); + manager.lockfile.initEmpty(ctx.allocator); if (manager.options.enable.frozen_lockfile) { if (comptime log_level != .silent) { @@ -10005,7 +10102,7 @@ pub const PackageManager = struct { Output.pretty(" {d} package{s} installed ", .{ pkgs_installed, if (pkgs_installed == 1) "" else "s" }); Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); printed_timestamp = true; - printSkippedScripts(install_summary); + printBlockedPackagesInfo(install_summary); if (manager.summary.remove > 0) { Output.pretty(" Removed: {d}\n", .{manager.summary.remove}); @@ -10020,7 +10117,7 @@ pub const PackageManager = struct { Output.pretty(" {d} package{s} removed ", .{ manager.summary.remove, if (manager.summary.remove == 1) "" else "s" }); Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); printed_timestamp = true; - printSkippedScripts(install_summary); + printBlockedPackagesInfo(install_summary); } else if (install_summary.skipped > 0 and install_summary.fail == 0 and manager.package_json_updates.len == 0) { const count = @as(PackageID, @truncate(manager.lockfile.packages.len)); if (count != install_summary.skipped) { @@ -10032,7 +10129,7 @@ pub const PackageManager = struct { }); Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); printed_timestamp = true; - printSkippedScripts(install_summary); + printBlockedPackagesInfo(install_summary); } else { Output.pretty(" Done! Checked {d} package{s} (no changes) ", .{ install_summary.skipped, @@ -10040,7 +10137,7 @@ pub const PackageManager = struct { }); Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); printed_timestamp = true; - printSkippedScripts(install_summary); + printBlockedPackagesInfo(install_summary); } } @@ -10064,12 +10161,22 @@ pub const PackageManager = struct { Output.flush(); } - fn printSkippedScripts(summary: PackageInstall.Summary) void { - const count = summary.packages_with_skipped_scripts_set.count(); - if (count > 0) { - Output.prettyln("\n\n Skipped ~{d} script{s}. Run `bun pm trusted` for details.\n", .{ - count, - if (count > 1) "s" else "", + fn printBlockedPackagesInfo(summary: PackageInstall.Summary) void { + const packages_count = summary.packages_with_blocked_scripts.count(); + var scripts_count: usize = 0; + for (summary.packages_with_blocked_scripts.values()) |count| scripts_count += count; + + if (comptime Environment.allow_assert) { + // if packages_count is greater than 0, scripts_count must also be greater than 0. + std.debug.assert(packages_count == 0 or scripts_count > 0); + // if scripts_count is 1, it's only possible for packages_count to be 1. + std.debug.assert(scripts_count != 1 or packages_count == 1); + } + + if (packages_count > 0) { + Output.prettyln("\n\n Blocked {d} postinstall{s}. Run `bun pm untrusted` for details.\n", .{ + scripts_count, + if (scripts_count > 1) "s" else "", }); } else { Output.pretty("\n", .{}); diff --git a/src/install/lifecycle_script_runner.zig b/src/install/lifecycle_script_runner.zig index eda35f1a77..fa5dc007d6 100644 --- a/src/install/lifecycle_script_runner.zig +++ b/src/install/lifecycle_script_runner.zig @@ -77,11 +77,7 @@ pub const LifecycleScriptSubprocess = struct { return; const process = this.process orelse return; - this.process = null; - const status = process.status; - process.detach(); - process.deref(); - this.handleExit(status); + this.handleExit(process.status); } // This is only used on the main thread. @@ -328,13 +324,20 @@ pub const LifecycleScriptSubprocess = struct { } pub fn resetPolls(this: *LifecycleScriptSubprocess) void { - std.debug.assert(this.remaining_fds == 0); + if (comptime Environment.allow_assert) { + std.debug.assert(this.remaining_fds == 0); + } if (this.process) |process| { this.process = null; process.close(); process.deref(); } + + this.stdout.deinit(); + this.stderr.deinit(); + this.stdout = OutputReader.init(@This()); + this.stderr = OutputReader.init(@This()); } pub fn deinit(this: *LifecycleScriptSubprocess) void { diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index 361b516ce8..861e174b8e 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -767,7 +767,7 @@ pub fn cleanWithLogger( // } var new: *Lockfile = try old.allocator.create(Lockfile); - try new.initEmpty( + new.initEmpty( old.allocator, ); try new.string_pool.ensureTotalCapacity(old.string_pool.capacity()); @@ -1722,7 +1722,7 @@ inline fn strWithType(this: *const Lockfile, comptime Type: type, slicable: Type return slicable.slice(this.buffers.string_bytes.items); } -pub fn initEmpty(this: *Lockfile, allocator: Allocator) !void { +pub fn initEmpty(this: *Lockfile, allocator: Allocator) void { this.* = .{ .format = Lockfile.FormatVersion.current, .packages = .{}, @@ -2384,6 +2384,40 @@ pub const Package = extern struct { cwd: string, package_name: string, + pub fn printScripts( + this: Package.Scripts.List, + resolution: *const Resolution, + resolution_buf: []const u8, + comptime format_type: enum { completed, info, untrusted }, + ) void { + if (std.mem.indexOf(u8, this.cwd, std.fs.path.sep_str ++ "node_modules" ++ std.fs.path.sep_str)) |i| { + Output.pretty(".{s}{s} @{}\n", .{ + std.fs.path.sep_str, + strings.withoutTrailingSlash(this.cwd[i + 1 ..]), + resolution.fmt(resolution_buf), + }); + } else { + Output.pretty("{s} @{}\n", .{ + strings.withoutTrailingSlash(this.cwd), + resolution.fmt(resolution_buf), + }); + } + + const fmt = switch (comptime format_type) { + .completed => " [{s}]: {s}\n", + .untrusted => " » [{s}]: {s}\n", + .info => " [{s}]: {s}\n", + }; + for (this.items, 0..) |maybe_script, script_index| { + if (maybe_script) |script| { + Output.pretty(fmt, .{ + Lockfile.Scripts.names[script_index], + script.script, + }); + } + } + } + pub fn first(this: Package.Scripts.List) Lockfile.Scripts.Entry { if (comptime Environment.allow_assert) { std.debug.assert(this.items[this.first_index] != null); @@ -2592,7 +2626,7 @@ pub const Package = extern struct { folder_name: string, resolution: *const Resolution, ) !?Package.Scripts.List { - var path_buf_to_use: [bun.MAX_PATH_BYTES * 2]u8 = undefined; + var path_buf: [bun.MAX_PATH_BYTES * 2]u8 = undefined; if (this.hasAny()) { const add_node_gyp_rebuild_script = if (lockfile.hasTrustedDependency(folder_name) and this.install.isEmpty() and @@ -2609,7 +2643,7 @@ pub const Package = extern struct { const cwd = Path.joinAbsStringBufZTrailingSlash( abs_node_modules_path, - &path_buf_to_use, + &path_buf, &[_]string{folder_name}, .auto, ); @@ -2623,37 +2657,33 @@ pub const Package = extern struct { add_node_gyp_rebuild_script, ); } else if (!this.filled) { + const abs_folder_path = Path.joinAbsStringBufZTrailingSlash( + abs_node_modules_path, + &path_buf, + &[_]string{folder_name}, + .auto, + ); return this.createFromPackageJSON( log, lockfile, node_modules, - abs_node_modules_path, + abs_folder_path, folder_name, - resolution, + resolution.tag, ); } return null; } - pub fn createFromPackageJSON( + pub fn fillFromPackageJSON( this: *Package.Scripts, + allocator: std.mem.Allocator, + string_builder: *Lockfile.StringBuilder, log: *logger.Log, - lockfile: *Lockfile, node_modules: std.fs.Dir, - node_modules_path: string, folder_name: string, - resolution: *const Resolution, - ) !?Package.Scripts.List { - var path_buf: bun.PathBuffer = undefined; - - const cwd = Path.joinAbsStringBufZTrailingSlash( - node_modules_path, - &path_buf, - &[_]string{folder_name}, - .auto, - ); - + ) !void { const json = brk: { const json_src = brk2: { const json_path = bun.path.joinZ([_]string{ folder_name, "package.json" }, .auto); @@ -2666,8 +2696,8 @@ pub const Package = extern struct { const json_file = json_file_fd.asFile(); defer json_file.close(); const json_stat_size = try json_file.getEndPos(); - const json_buf = try lockfile.allocator.alloc(u8, json_stat_size + 64); - errdefer lockfile.allocator.free(json_buf); + const json_buf = try allocator.alloc(u8, json_stat_size + 64); + errdefer allocator.free(json_buf); const json_len = try json_file.preadAll(json_buf, 0); break :brk2 logger.Source.initPathString(json_path, json_buf[0..json_len]); }; @@ -2676,22 +2706,34 @@ pub const Package = extern struct { break :brk try json_parser.ParseJSONUTF8( &json_src, log, - lockfile.allocator, + allocator, ); }; + Lockfile.Package.Scripts.parseCount(allocator, string_builder, json); + try string_builder.allocate(); + this.parseAlloc(allocator, string_builder, json); + this.filled = true; + } + + pub fn createFromPackageJSON( + this: *Package.Scripts, + log: *logger.Log, + lockfile: *Lockfile, + node_modules: std.fs.Dir, + abs_folder_path: string, + folder_name: string, + resolution_tag: Resolution.Tag, + ) !?Package.Scripts.List { var tmp: Lockfile = undefined; - try tmp.initEmpty(lockfile.allocator); + tmp.initEmpty(lockfile.allocator); defer tmp.deinit(); var builder = tmp.stringBuilder(); - Lockfile.Package.Scripts.parseCount(lockfile.allocator, &builder, json); - try builder.allocate(); - this.parseAlloc(lockfile.allocator, &builder, json); - this.filled = true; + try this.fillFromPackageJSON(lockfile.allocator, &builder, log, node_modules, folder_name); const add_node_gyp_rebuild_script = if (this.install.isEmpty() and this.preinstall.isEmpty()) brk: { const binding_dot_gyp_path = Path.joinAbsStringZ( - cwd, + abs_folder_path, &[_]string{"binding.gyp"}, .auto, ); @@ -2702,9 +2744,9 @@ pub const Package = extern struct { return this.createList( lockfile, tmp.buffers.string_bytes.items, - cwd, + abs_folder_path, folder_name, - resolution.tag, + resolution_tag, add_node_gyp_rebuild_script, ); } diff --git a/src/install/migration.zig b/src/install/migration.zig index 8b021a8969..811dd180c0 100644 --- a/src/install/migration.zig +++ b/src/install/migration.zig @@ -120,7 +120,7 @@ const dependency_keys = .{ pub fn migrateNPMLockfile(this: *Lockfile, allocator: Allocator, log: *logger.Log, data: string, path: string) !LoadFromDiskResult { debug("begin lockfile migration", .{}); - try this.initEmpty(allocator); + this.initEmpty(allocator); Install.initializeStore(); const json_src = logger.Source.initPathString(path, data); diff --git a/src/install/resolution.zig b/src/install/resolution.zig index e5bc2ad25b..bc8f14eed4 100644 --- a/src/install/resolution.zig +++ b/src/install/resolution.zig @@ -28,10 +28,6 @@ pub const Resolution = extern struct { return this.tag.isGit(); } - pub fn isLocal(this: *const Resolution) bool { - return this.tag.isLocal(); - } - pub fn order( lhs: *const Resolution, rhs: *const Resolution, @@ -328,9 +324,5 @@ pub const Resolution = extern struct { pub fn isGit(this: Tag) bool { return this == .git or this == .github or this == .gitlab; } - - pub fn isLocal(this: Tag) bool { - return this == .local_tarball or this == .folder or this == .symlink or this == .workspace or this == .root; - } }; }; diff --git a/src/install/semver.zig b/src/install/semver.zig index 5754d46f37..dff0c9ec97 100644 --- a/src/install/semver.zig +++ b/src/install/semver.zig @@ -79,6 +79,16 @@ pub const String = extern struct { } }; + pub fn Sorter(comptime direction: enum { asc, desc }) type { + return struct { + lhs_buf: []const u8, + rhs_buf: []const u8, + pub fn lessThan(this: @This(), lhs: String, rhs: String) bool { + return lhs.order(&rhs, this.lhs_buf, this.rhs_buf) == if (comptime direction == .asc) .lt else .gt; + } + }; + } + pub inline fn order( lhs: *const String, rhs: *const String, diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index 781de0cfc8..3324ff7cc8 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -6289,7 +6289,7 @@ it("should handle trustedDependencies", async () => { "", expect.stringContaining(" 2 packages installed"), "", - " Skipped ~1 script. Run `bun pm trusted` for details.", + " Blocked 3 postinstalls. Run `bun pm untrusted` for details.", "", ]); expect(await exited).toBe(0); diff --git a/test/cli/install/registry/bun-install-registry.test.ts b/test/cli/install/registry/bun-install-registry.test.ts index 33ab192ab3..79cc66f6de 100644 --- a/test/cli/install/registry/bun-install-registry.test.ts +++ b/test/cli/install/registry/bun-install-registry.test.ts @@ -410,7 +410,7 @@ test("it should correctly link binaries after deleting node_modules", async () = "", expect.stringContaining("3 packages installed"), "", - " Skipped ~1 script. Run `bun pm trusted` for details.", + " Blocked 1 postinstall. Run `bun pm untrusted` for details.", "", ]); expect(await exited).toBe(0); @@ -438,7 +438,7 @@ test("it should correctly link binaries after deleting node_modules", async () = "", expect.stringContaining("3 packages installed"), "", - " Skipped ~1 script. Run `bun pm trusted` for details.", + " Blocked 1 postinstall. Run `bun pm untrusted` for details.", "", ]); expect(await exited).toBe(0); @@ -581,7 +581,7 @@ test("it should install with missing bun.lockb, node_modules, and/or cache", asy "", expect.stringContaining("19 packages installed"), "", - " Skipped ~1 script. Run `bun pm trusted` for details.", + " Blocked 1 postinstall. Run `bun pm untrusted` for details.", "", ]); expect(await exited).toBe(0); @@ -620,7 +620,7 @@ test("it should install with missing bun.lockb, node_modules, and/or cache", asy "", expect.stringContaining("19 packages installed"), "", - " Skipped ~1 script. Run `bun pm trusted` for details.", + " Blocked 1 postinstall. Run `bun pm untrusted` for details.", "", ]); expect(await exited).toBe(0); @@ -1648,7 +1648,7 @@ test("missing package on reinstall, some with binaries", async () => { "", expect.stringContaining("19 packages installed"), "", - " Skipped ~1 script. Run `bun pm trusted` for details.", + " Blocked 1 postinstall. Run `bun pm untrusted` for details.", "", ]); expect(await exited).toBe(0); @@ -2071,7 +2071,7 @@ for (const forceWaiterThread of [false, true]) { "", expect.stringContaining("1 package installed"), "", - " Skipped ~1 script. Run `bun pm trusted` for details.", + " Blocked 3 postinstalls. Run `bun pm untrusted` for details.", "", ]); @@ -2533,7 +2533,7 @@ for (const forceWaiterThread of [false, true]) { "", expect.stringContaining("2 packages installed"), "", - " Skipped ~1 script. Run `bun pm trusted` for details.", + " Blocked 1 postinstall. Run `bun pm untrusted` for details.", "", ]); expect(await exited).toBe(0); @@ -2722,7 +2722,7 @@ for (const forceWaiterThread of [false, true]) { "", expect.stringContaining("1 package installed"), "", - " Skipped ~1 script. Run `bun pm trusted` for details.", + " Blocked 6 postinstalls. Run `bun pm untrusted` for details.", "", ]); expect(await exited).toBe(0); @@ -2888,7 +2888,7 @@ for (const forceWaiterThread of [false, true]) { "", expect.stringContaining("3 packages installed"), "", - " Skipped ~1 script. Run `bun pm trusted` for details.", + " Blocked 1 postinstall. Run `bun pm untrusted` for details.", "", ]); expect(await exited).toBe(0); @@ -3723,7 +3723,7 @@ describe("pm trust", async () => { ); let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "trusted", "--default"], + cmd: [bunExe(), "pm", "default-trusted"], cwd: packageDir, stdout: "pipe", stderr: "pipe", @@ -3749,7 +3749,7 @@ describe("pm trust", async () => { ); let { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "pm", "trusted", "--all"], + cmd: [bunExe(), "pm", "trust", "--all"], cwd: packageDir, stdout: "pipe", stderr: "pipe", @@ -3792,7 +3792,7 @@ describe("pm trust", async () => { "", expect.stringContaining("2 packages installed"), "", - " Skipped ~1 script. Run `bun pm trusted` for details.", + " Blocked 1 postinstall. Run `bun pm untrusted` for details.", "", ]); expect(await exited).toBe(0); @@ -3808,10 +3808,12 @@ describe("pm trust", async () => { })); err = await Bun.readableStreamToText(stderr); - expect(err).toBeEmpty(); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); out = await Bun.readableStreamToText(stdout); - expect(err).toBeEmpty(); + expect(out).toContain("1 script ran across 1 package"); expect(await exited).toBe(0); expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue();