const bun = @import("bun"); const string = bun.string; const Output = bun.Output; const Global = bun.Global; const Environment = bun.Environment; const strings = bun.strings; const MutableString = bun.MutableString; const stringZ = bun.stringZ; const default_allocator = bun.default_allocator; const C = bun.C; const std = @import("std"); const JSLexer = bun.js_lexer; const logger = @import("bun").logger; const js_parser = bun.js_parser; const json_parser = bun.JSON; const JSPrinter = bun.js_printer; const linker = @import("../linker.zig"); const sync = @import("../sync.zig"); const Api = @import("../api/schema.zig").Api; const Path = @import("../resolver/resolve_path.zig"); const configureTransformOptionsForBun = @import("../bun.js/config.zig").configureTransformOptionsForBun; const Command = @import("../cli.zig").Command; const BunArguments = @import("../cli.zig").Arguments; const bundler = bun.bundler; const NodeModuleBundle = @import("../node_module_bundle.zig").NodeModuleBundle; const DotEnv = @import("../env_loader.zig"); const which = @import("../which.zig").which; const Run = @import("../bun_js.zig").Run; const HeaderBuilder = @import("bun").HTTP.HeaderBuilder; const Fs = @import("../fs.zig"); const FileSystem = Fs.FileSystem; const Lock = @import("../lock.zig").Lock; var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; var path_buf2: [bun.MAX_PATH_BYTES]u8 = undefined; const URL = @import("../url.zig").URL; const AsyncHTTP = @import("bun").HTTP.AsyncHTTP; const HTTPChannel = @import("bun").HTTP.HTTPChannel; const NetworkThread = @import("bun").HTTP.NetworkThread; const HTTP = @import("bun").HTTP; const Integrity = @import("./integrity.zig").Integrity; const clap = @import("bun").clap; const ExtractTarball = @import("./extract_tarball.zig"); const Npm = @import("./npm.zig"); const Bitset = @import("./bit_set.zig").DynamicBitSetUnmanaged; const z_allocator = @import("../memory_allocator.zig").z_allocator; const Syscall = @import("bun").JSC.Node.Syscall; const RunCommand = @import("../cli/run_command.zig").RunCommand; threadlocal var initialized_store = false; const Futex = @import("../futex.zig"); pub const Lockfile = @import("./lockfile.zig"); // these bytes are skipped // so we just make it repeat bun bun bun bun bun bun bun bun bun // because why not pub const alignment_bytes_to_repeat_buffer = [_]u8{0} ** 144; const JSAst = bun.JSAst; pub fn initializeStore() void { if (initialized_store) { JSAst.Expr.Data.Store.reset(); JSAst.Stmt.Data.Store.reset(); return; } initialized_store = true; JSAst.Expr.Data.Store.create(default_allocator); JSAst.Stmt.Data.Store.create(default_allocator); } const IdentityContext = @import("../identity_context.zig").IdentityContext; const ArrayIdentityContext = @import("../identity_context.zig").ArrayIdentityContext; const NetworkQueue = std.fifo.LinearFifo(*NetworkTask, .{ .Static = 32 }); const Semver = @import("./semver.zig"); const ExternalString = Semver.ExternalString; const String = Semver.String; const GlobalStringBuilder = @import("../string_builder.zig"); const SlicedString = Semver.SlicedString; const Repository = @import("./repository.zig").Repository; const StructBuilder = @import("../builder.zig"); const Bin = @import("./bin.zig").Bin; const Dependency = @import("./dependency.zig"); const Behavior = @import("./dependency.zig").Behavior; const FolderResolution = @import("./resolvers/folder_resolver.zig").FolderResolution; pub fn ExternalSlice(comptime Type: type) type { return ExternalSliceAligned(Type, null); } pub fn ExternalSliceAligned(comptime Type: type, comptime alignment_: ?u29) type { return extern struct { const alignment = alignment_ orelse @alignOf(*Type); const Slice = @This(); pub const Child: type = Type; off: u32 = 0, len: u32 = 0, pub inline fn contains(this: Slice, id: u32) bool { return id >= this.off and id < (this.len + this.off); } pub inline fn get(this: Slice, in: []const Type) []const Type { // it should be impossible to address this out of bounds due to the minimum here return in.ptr[this.off..@min(in.len, this.off + this.len)]; } pub inline fn mut(this: Slice, in: []Type) []Type { return in.ptr[this.off..@min(in.len, this.off + this.len)]; } pub fn init(buf: []const Type, in: []const Type) Slice { // if (comptime isDebug or isTest) { // std.debug.assert(@ptrToInt(buf.ptr) <= @ptrToInt(in.ptr)); // std.debug.assert((@ptrToInt(in.ptr) + in.len) <= (@ptrToInt(buf.ptr) + buf.len)); // } return Slice{ .off = @truncate(u32, (@ptrToInt(in.ptr) - @ptrToInt(buf.ptr)) / @sizeOf(Type)), .len = @truncate(u32, in.len), }; } }; } pub const PackageID = u32; pub const DependencyID = u32; pub const PackageIDMultiple = [*:invalid_package_id]PackageID; pub const invalid_package_id = std.math.maxInt(PackageID); pub const ExternalStringList = ExternalSlice(ExternalString); pub const VersionSlice = ExternalSlice(Semver.Version); pub const ExternalStringMap = extern struct { name: ExternalStringList = ExternalStringList{}, value: ExternalStringList = ExternalStringList{}, }; pub const PackageNameHash = u64; pub const Aligner = struct { pub fn write(comptime Type: type, comptime Writer: type, writer: Writer, pos: usize) !usize { const to_write = skipAmount(Type, pos); var remainder: string = alignment_bytes_to_repeat_buffer[0..@min(to_write, alignment_bytes_to_repeat_buffer.len)]; try writer.writeAll(remainder); return to_write; } pub inline fn skipAmount(comptime Type: type, pos: usize) usize { return std.mem.alignForward(pos, @alignOf(Type)) - pos; } }; const NetworkTask = struct { http: AsyncHTTP = undefined, task_id: u64, url_buf: []const u8 = &[_]u8{}, allocator: std.mem.Allocator, request_buffer: MutableString = undefined, response_buffer: MutableString = undefined, package_manager: *PackageManager, callback: union(Task.Tag) { package_manifest: struct { loaded_manifest: ?Npm.PackageManifest = null, name: strings.StringOrTinyString, }, extract: ExtractTarball, binlink: void, }, pub fn notify(this: *NetworkTask, _: anytype) void { defer this.package_manager.wake(); this.package_manager.network_channel.writeItem(this) catch {}; } // We must use a less restrictive Accept header value // https://github.com/oven-sh/bun/issues/341 // https://www.jfrog.com/jira/browse/RTFACT-18398 const accept_header_value = "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*"; const default_headers_buf: string = "Accept" ++ accept_header_value; fn appendAuth(header_builder: *HeaderBuilder, scope: *const Npm.Registry.Scope) void { if (scope.token.len > 0) { header_builder.appendFmt("Authorization", "Bearer {s}", .{scope.token}); } else if (scope.auth.len > 0) { header_builder.appendFmt("Authorization", "Basic {s}", .{scope.auth}); } else { return; } header_builder.append("npm-auth-type", "legacy"); } fn countAuth(header_builder: *HeaderBuilder, scope: *const Npm.Registry.Scope) void { if (scope.token.len > 0) { header_builder.count("Authorization", ""); header_builder.content.cap += "Bearer ".len + scope.token.len; } else if (scope.auth.len > 0) { header_builder.count("Authorization", ""); header_builder.content.cap += "Basic ".len + scope.auth.len; } else { return; } header_builder.count("npm-auth-type", "legacy"); } pub fn forManifest( this: *NetworkTask, name: string, allocator: std.mem.Allocator, scope: *const Npm.Registry.Scope, loaded_manifest: ?Npm.PackageManifest, ) !void { const pathname: string = if (!strings.eqlComptime(scope.url.pathname, "/")) scope.url.pathname else @as(string, ""); if (pathname.len > 0) { if (scope.url.getPort()) |port_number| { this.url_buf = try std.fmt.allocPrint( allocator, "{s}://{s}:{d}/{s}/{s}", .{ scope.url.displayProtocol(), scope.url.displayHostname(), port_number, pathname, name, }, ); } else { this.url_buf = try std.fmt.allocPrint( allocator, "{s}://{s}/{s}/{s}", .{ scope.url.displayProtocol(), scope.url.displayHostname(), pathname, name, }, ); } } else { if (scope.url.getPort()) |port_number| { this.url_buf = try std.fmt.allocPrint( allocator, "{s}://{s}:{d}/{s}", .{ scope.url.displayProtocol(), scope.url.displayHostname(), port_number, name, }, ); } else { this.url_buf = try std.fmt.allocPrint( allocator, "{s}://{s}/{s}", .{ scope.url.displayProtocol(), scope.url.displayHostname(), name, }, ); } } var last_modified: string = ""; var etag: string = ""; if (loaded_manifest) |manifest| { last_modified = manifest.pkg.last_modified.slice(manifest.string_buf); etag = manifest.pkg.etag.slice(manifest.string_buf); } var header_builder = HeaderBuilder{}; countAuth(&header_builder, scope); if (etag.len != 0) { header_builder.count("If-None-Match", etag); } else if (last_modified.len != 0) { header_builder.count("If-Modified-Since", last_modified); } if (header_builder.header_count > 0) { header_builder.count("Accept", accept_header_value); if (last_modified.len > 0 and etag.len > 0) { header_builder.content.count(last_modified); } try header_builder.allocate(allocator); appendAuth(&header_builder, scope); if (etag.len != 0) { header_builder.append("If-None-Match", etag); } else if (last_modified.len != 0) { header_builder.append("If-Modified-Since", last_modified); } header_builder.append("Accept", accept_header_value); if (last_modified.len > 0 and etag.len > 0) { last_modified = header_builder.content.append(last_modified); } } else { try header_builder.entries.append( allocator, .{ .name = .{ .offset = 0, .length = @truncate(u32, "Accept".len) }, .value = .{ .offset = "Accept".len, .length = @truncate(u32, default_headers_buf.len - "Accept".len) }, }, ); header_builder.header_count = 1; header_builder.content = GlobalStringBuilder{ .ptr = @intToPtr([*]u8, @ptrToInt(std.mem.span(default_headers_buf).ptr)), .len = default_headers_buf.len, .cap = default_headers_buf.len }; } this.response_buffer = try MutableString.init(allocator, 0); this.allocator = allocator; const env_loader = this.package_manager.env_loader; var url = URL.parse(this.url_buf); var http_proxy: ?URL = env_loader.getHttpProxy(url); this.http = AsyncHTTP.init(allocator, .GET, url, header_builder.entries, header_builder.content.ptr.?[0..header_builder.content.len], &this.response_buffer, "", 0, this.getCompletionCallback(), http_proxy); this.http.max_retry_count = this.package_manager.options.max_retry_count; this.callback = .{ .package_manifest = .{ .name = try strings.StringOrTinyString.initAppendIfNeeded(name, *FileSystem.FilenameStore, &FileSystem.FilenameStore.instance), .loaded_manifest = loaded_manifest, }, }; if (PackageManager.verbose_install) { this.http.verbose = true; this.http.client.verbose = true; } // Incase the ETag causes invalidation, we fallback to the last modified date. if (last_modified.len != 0) { this.http.client.force_last_modified = true; this.http.client.if_modified_since = last_modified; } } pub fn getCompletionCallback(this: *NetworkTask) HTTP.HTTPClientResult.Callback { return HTTP.HTTPClientResult.Callback.New(*NetworkTask, notify).init(this); } pub fn schedule(this: *NetworkTask, batch: *ThreadPool.Batch) void { this.http.schedule(this.allocator, batch); } pub fn forTarball( this: *NetworkTask, allocator: std.mem.Allocator, tarball: ExtractTarball, scope: *const Npm.Registry.Scope, ) !void { if (tarball.url.len == 0) { this.url_buf = try ExtractTarball.buildURL( scope.url.href, tarball.name, tarball.resolution.value.npm.version, this.package_manager.lockfile.buffers.string_bytes.items, ); } else { this.url_buf = tarball.url; } this.response_buffer = try MutableString.init(allocator, 0); this.allocator = allocator; var header_builder = HeaderBuilder{}; countAuth(&header_builder, scope); var header_buf: string = ""; if (header_builder.header_count > 0) { try header_builder.allocate(allocator); appendAuth(&header_builder, scope); header_buf = header_builder.content.ptr.?[0..header_builder.content.len]; } const env_loader = this.package_manager.env_loader; var url = URL.parse(this.url_buf); var http_proxy: ?URL = env_loader.getHttpProxy(url); this.http = AsyncHTTP.init(allocator, .GET, url, header_builder.entries, header_buf, &this.response_buffer, "", 0, this.getCompletionCallback(), http_proxy); this.http.max_retry_count = this.package_manager.options.max_retry_count; this.callback = .{ .extract = tarball }; } }; pub const Origin = enum(u8) { local = 0, npm = 1, tarball = 2, }; pub const Features = struct { dependencies: bool = true, dev_dependencies: bool = false, is_main: bool = false, optional_dependencies: bool = false, peer_dependencies: bool = true, scripts: bool = false, workspaces: bool = false, check_for_duplicate_dependencies: bool = false, pub fn behavior(this: Features) Behavior { var out: u8 = 0; out |= @as(u8, @boolToInt(this.dependencies)) << 1; out |= @as(u8, @boolToInt(this.optional_dependencies)) << 2; out |= @as(u8, @boolToInt(this.dev_dependencies)) << 3; out |= @as(u8, @boolToInt(this.peer_dependencies)) << 4; out |= @as(u8, @boolToInt(this.workspaces)) << 5; return @intToEnum(Behavior, out); } pub const folder = Features{ .dev_dependencies = true, .optional_dependencies = true, }; pub const workspace = Features{ .dev_dependencies = true, .optional_dependencies = true, .scripts = true, .workspaces = true, }; pub const link = Features{ .dependencies = false, .peer_dependencies = false, }; pub const npm = Features{ .optional_dependencies = true, }; pub const tarball = npm; pub const npm_manifest = Features{ .optional_dependencies = true, }; }; pub const PreinstallState = enum(u2) { unknown = 0, done = 1, extract = 2, extracting = 3, }; /// Schedule long-running callbacks for a task /// Slow stuff is broken into tasks, each can run independently without locks const Task = struct { tag: Tag, request: Request, data: Data, status: Status = Status.waiting, threadpool_task: ThreadPool.Task = ThreadPool.Task{ .callback = &callback }, log: logger.Log, id: u64, err: ?anyerror = null, package_manager: *PackageManager, /// An ID that lets us register a callback without keeping the same pointer around pub const Id = struct { pub fn forNPMPackage(_: Task.Tag, package_name: string, package_version: Semver.Version) u64 { var hasher = std.hash.Wyhash.init(0); hasher.update(package_name); hasher.update("@"); hasher.update(std.mem.asBytes(&package_version)); return @as(u64, @truncate(u63, hasher.final())) | @as(u64, 1 << 63); } pub fn forBinLink(package_id: PackageID) u64 { const hash = std.hash.Wyhash.hash(0, std.mem.asBytes(&package_id)); return @as(u64, @truncate(u62, hash)) | @as(u64, 1 << 62) | @as(u64, 1 << 63); } pub fn forManifest( _: Task.Tag, name: string, ) u64 { return @as(u64, @truncate(u63, std.hash.Wyhash.hash(0, name))); } }; pub fn callback(task: *ThreadPool.Task) void { Output.Source.configureThread(); defer Output.flush(); var this = @fieldParentPtr(Task, "threadpool_task", task); defer this.package_manager.wake(); switch (this.tag) { .package_manifest => { var allocator = bun.default_allocator; const package_manifest = Npm.Registry.getPackageMetadata( allocator, this.request.package_manifest.network.http.response.?, this.request.package_manifest.network.response_buffer.toOwnedSliceLeaky(), &this.log, this.request.package_manifest.name.slice(), this.request.package_manifest.network.callback.package_manifest.loaded_manifest, this.package_manager, ) catch |err| { if (comptime Environment.isDebug) { if (@errorReturnTrace()) |trace| { std.debug.dumpStackTrace(trace.*); } } this.err = err; this.status = Status.fail; this.package_manager.resolve_tasks.writeItem(this.*) catch unreachable; return; }; this.data = .{ .package_manifest = .{} }; switch (package_manifest) { .cached => unreachable, .fresh => |manifest| { this.data = .{ .package_manifest = manifest }; this.status = Status.success; this.package_manager.resolve_tasks.writeItem(this.*) catch unreachable; return; }, .not_found => { this.log.addErrorFmt(null, logger.Loc.Empty, allocator, "404 - GET {s}", .{ this.request.package_manifest.name.slice(), }) catch unreachable; this.status = Status.fail; this.package_manager.resolve_tasks.writeItem(this.*) catch unreachable; return; }, } }, .extract => { const result = this.request.extract.tarball.run( this.request.extract.network.response_buffer.toOwnedSliceLeaky(), ) catch |err| { if (comptime Environment.isDebug) { if (@errorReturnTrace()) |trace| { std.debug.dumpStackTrace(trace.*); } } this.err = err; this.status = Status.fail; this.data = .{ .extract = "" }; this.package_manager.resolve_tasks.writeItem(this.*) catch unreachable; return; }; this.data = .{ .extract = result }; this.status = Status.success; this.package_manager.resolve_tasks.writeItem(this.*) catch unreachable; }, .binlink => {}, } } pub const Tag = enum(u2) { package_manifest = 1, extract = 2, binlink = 3, // install = 3, }; pub const Status = enum { waiting, success, fail, }; pub const Data = union { package_manifest: Npm.PackageManifest, extract: string, binlink: bool, }; pub const Request = union { /// package name // todo: Registry URL package_manifest: struct { name: strings.StringOrTinyString, network: *NetworkTask, }, extract: struct { network: *NetworkTask, tarball: ExtractTarball, }, binlink: Bin.Linker, // install: PackageInstall, }; }; const PackageInstall = struct { cache_dir: std.fs.IterableDir, destination_dir: std.fs.IterableDir, cache_dir_subpath: stringZ = "", destination_dir_subpath: stringZ = "", destination_dir_subpath_buf: []u8, allocator: std.mem.Allocator, progress: *Progress, package_name: string, package_version: string, file_count: u32 = 0, pub const Summary = struct { fail: u32 = 0, success: u32 = 0, skipped: u32 = 0, successfully_installed: ?Bitset = null, }; pub const Method = enum { clonefile, /// Slower than clonefile clonefile_each_dir, /// On macOS, slow. /// On Linux, fast. hardlink, /// Slowest if single-threaded /// Note that copyfile does technically support recursion /// But I suspect it is slower in practice than manually doing it because: /// - it adds syscalls /// - it runs in userspace /// - it reads each dir twice incase the first pass modifies it copyfile, /// Used for file: when file: points to a parent directory /// example: "file:../" symlink, const BackendSupport = std.EnumArray(Method, bool); pub const map = std.ComptimeStringMap(Method, .{ .{ "clonefile", Method.clonefile }, .{ "clonefile_each_dir", Method.clonefile_each_dir }, .{ "hardlink", Method.hardlink }, .{ "copyfile", Method.copyfile }, .{ "symlink", Method.symlink }, }); pub const macOS = BackendSupport.initDefault(false, .{ .clonefile = true, .clonefile_each_dir = true, .hardlink = true, .copyfile = true, .symlink = true, }); pub const linux = BackendSupport.initDefault(false, .{ .hardlink = true, .copyfile = true, .symlink = true, }); pub inline fn isSupported(this: Method) bool { if (comptime Environment.isMac) return macOS.get(this); if (comptime Environment.isLinux) return linux.get(this); return false; } }; pub fn verify( this: *PackageInstall, ) bool { var allocator = this.allocator; std.mem.copy(u8, this.destination_dir_subpath_buf[this.destination_dir_subpath.len..], std.fs.path.sep_str ++ "package.json"); this.destination_dir_subpath_buf[this.destination_dir_subpath.len + std.fs.path.sep_str.len + "package.json".len] = 0; var package_json_path: [:0]u8 = this.destination_dir_subpath_buf[0 .. this.destination_dir_subpath.len + std.fs.path.sep_str.len + "package.json".len :0]; defer this.destination_dir_subpath_buf[this.destination_dir_subpath.len] = 0; var package_json_file = this.destination_dir.dir.openFileZ(package_json_path, .{ .mode = .read_only }) catch return false; defer package_json_file.close(); var body_pool = Npm.Registry.BodyPool.get(allocator); var mutable: MutableString = body_pool.data; defer { body_pool.data = mutable; Npm.Registry.BodyPool.release(body_pool); } mutable.reset(); var total: usize = 0; var read: usize = 0; mutable.list.expandToCapacity(); // Heuristic: most package.jsons will be less than 2048 bytes. read = package_json_file.read(mutable.list.items[total..]) catch return false; var remain = mutable.list.items[@min(total, read)..]; if (read > 0 and remain.len < 1024) { mutable.growBy(4096) catch return false; mutable.list.expandToCapacity(); } while (read > 0) : (read = package_json_file.read(remain) catch return false) { total += read; mutable.list.expandToCapacity(); remain = mutable.list.items[total..]; if (remain.len < 1024) { mutable.growBy(4096) catch return false; } mutable.list.expandToCapacity(); remain = mutable.list.items[total..]; } // If it's not long enough to have {"name": "foo", "version": "1.2.0"}, there's no way it's valid if (total < "{\"name\":\"\",\"version\":\"\"}".len + this.package_name.len + this.package_version.len) return false; const source = logger.Source.initPathString(std.mem.span(package_json_path), mutable.list.items[0..total]); var log = logger.Log.init(allocator); defer log.deinit(); initializeStore(); var package_json_checker = json_parser.PackageJSONVersionChecker.init(allocator, &source, &log) catch return false; _ = package_json_checker.parseExpr() catch return false; if (!package_json_checker.has_found_name or !package_json_checker.has_found_version or log.errors > 0) return false; // Version is more likely to not match than name, so we check it first. return strings.eql(package_json_checker.found_version, this.package_version) and strings.eql(package_json_checker.found_name, this.package_name); } pub const Result = union(Tag) { pending: void, success: void, skip: void, fail: struct { err: anyerror, step: Step, pub inline fn isPackageMissingFromCache(this: @This()) bool { return this.err == error.FileNotFound and this.step == .opening_cache_dir; } }, pub const Tag = enum { success, fail, pending, skip, }; }; pub const Step = enum { copyfile, opening_cache_dir, copying_files, linking, }; const CloneFileError = error{ NotSupported, Unexpected, FileNotFound, }; var supported_method: Method = if (Environment.isMac) Method.clonefile else Method.hardlink; fn installWithClonefileEachDir(this: *PackageInstall) !Result { const Walker = @import("../walker_skippable.zig"); var cached_package_dir = bun.openDir(this.cache_dir.dir, this.cache_dir_subpath) catch |err| return Result{ .fail = .{ .err = err, .step = .opening_cache_dir }, }; defer cached_package_dir.close(); var walker_ = Walker.walk( cached_package_dir, this.allocator, &[_]string{}, &[_]string{}, ) catch |err| return Result{ .fail = .{ .err = err, .step = .opening_cache_dir }, }; defer walker_.deinit(); const FileCopier = struct { pub fn copy( destination_dir_: std.fs.IterableDir, walker: *Walker, ) !u32 { var real_file_count: u32 = 0; var stackpath: [bun.MAX_PATH_BYTES]u8 = undefined; while (try walker.next()) |entry| { switch (entry.kind) { .Directory => { std.os.mkdirat(destination_dir_.dir.fd, entry.path, 0o755) catch {}; }, .File => { std.mem.copy(u8, &stackpath, entry.path); stackpath[entry.path.len] = 0; var path: [:0]u8 = stackpath[0..entry.path.len :0]; var basename: [:0]u8 = stackpath[entry.path.len - entry.basename.len .. entry.path.len :0]; switch (C.clonefileat( entry.dir.dir.fd, basename, destination_dir_.dir.fd, path, 0, )) { 0 => void{}, else => |errno| switch (std.os.errno(errno)) { .OPNOTSUPP => return error.NotSupported, .NOENT => return error.FileNotFound, // sometimes the downlowded npm package has already node_modules with it, so just ignore exist error here .EXIST => {}, else => return error.Unexpected, }, } real_file_count += 1; }, else => {}, } } return real_file_count; } }; var subdir = this.destination_dir.dir.makeOpenPathIterable(std.mem.span(this.destination_dir_subpath), .{}) catch |err| return Result{ .fail = .{ .err = err, .step = .opening_cache_dir }, }; defer subdir.close(); this.file_count = FileCopier.copy( subdir, &walker_, ) catch |err| return Result{ .fail = .{ .err = err, .step = .copying_files }, }; return Result{ .success = void{}, }; } // https://www.unix.com/man-page/mojave/2/fclonefileat/ fn installWithClonefile(this: *PackageInstall) CloneFileError!Result { if (comptime !Environment.isMac) @compileError("clonefileat() is macOS only."); if (this.package_name[0] == '@') { const current = std.mem.span(this.destination_dir_subpath); if (strings.indexOfChar(current, std.fs.path.sep)) |slash| { this.destination_dir_subpath_buf[slash] = 0; var subdir = this.destination_dir_subpath_buf[0..slash :0]; this.destination_dir.dir.makeDirZ(subdir) catch {}; this.destination_dir_subpath_buf[slash] = std.fs.path.sep; } } return switch (C.clonefileat( this.cache_dir.dir.fd, this.cache_dir_subpath, this.destination_dir.dir.fd, this.destination_dir_subpath, 0, )) { 0 => .{ .success = void{} }, else => |errno| switch (std.os.errno(errno)) { .OPNOTSUPP => error.NotSupported, .NOENT => error.FileNotFound, // We first try to delete the directory // But, this can happen if this package contains a node_modules folder // We want to continue installing as many packages as we can, so we shouldn't block while downloading // We use the slow path in this case .EXIST => try this.installWithClonefileEachDir(), else => error.Unexpected, }, }; } fn installWithCopyfile(this: *PackageInstall) Result { const Walker = @import("../walker_skippable.zig"); var cached_package_dir = bun.openDir(this.cache_dir.dir, this.cache_dir_subpath) catch |err| return Result{ .fail = .{ .err = err, .step = .opening_cache_dir }, }; defer cached_package_dir.close(); var walker_ = Walker.walk( cached_package_dir, this.allocator, &[_]string{}, &[_]string{}, ) catch |err| return Result{ .fail = .{ .err = err, .step = .opening_cache_dir }, }; defer walker_.deinit(); const FileCopier = struct { pub fn copy( destination_dir_: std.fs.Dir, walker: *Walker, progress_: *Progress, ) !u32 { var real_file_count: u32 = 0; while (try walker.next()) |entry| { if (entry.kind != .File) continue; real_file_count += 1; var outfile = destination_dir_.createFile(entry.path, .{}) catch brk: { if (std.fs.path.dirname(entry.path)) |entry_dirname| { destination_dir_.makePath(entry_dirname) catch {}; } break :brk destination_dir_.createFile(entry.path, .{}) catch |err| { progress_.root.end(); progress_.refresh(); Output.prettyErrorln("{s}: copying file {s}", .{ @errorName(err), entry.path }); Global.exit(1); }; }; defer outfile.close(); var infile = try entry.dir.dir.openFile(entry.basename, .{ .mode = .read_only }); defer infile.close(); const stat = infile.stat() catch continue; _ = C.fchmod(outfile.handle, stat.mode); bun.copyFile(infile.handle, outfile.handle) catch |err| { progress_.root.end(); progress_.refresh(); Output.prettyErrorln("{s}: copying file {s}", .{ @errorName(err), entry.path }); Global.exit(1); }; } return real_file_count; } }; var subdir = this.destination_dir.dir.makeOpenPathIterable(std.mem.span(this.destination_dir_subpath), .{}) catch |err| return Result{ .fail = .{ .err = err, .step = .opening_cache_dir }, }; defer subdir.dir.close(); this.file_count = FileCopier.copy(subdir.dir, &walker_, this.progress) catch |err| return Result{ .fail = .{ .err = err, .step = .copying_files }, }; return Result{ .success = void{}, }; } fn installWithHardlink(this: *PackageInstall) !Result { const Walker = @import("../walker_skippable.zig"); var cached_package_dir = bun.openDir(this.cache_dir.dir, this.cache_dir_subpath) catch |err| return Result{ .fail = .{ .err = err, .step = .opening_cache_dir }, }; defer cached_package_dir.close(); var walker_ = Walker.walk( cached_package_dir, this.allocator, &[_]string{}, &[_]string{"node_modules"}, ) catch |err| return Result{ .fail = .{ .err = err, .step = .opening_cache_dir }, }; defer walker_.deinit(); const FileCopier = struct { pub fn copy( destination_dir_: std.fs.IterableDir, walker: *Walker, ) !u32 { var real_file_count: u32 = 0; while (try walker.next()) |entry| { switch (entry.kind) { .Directory => { std.os.mkdirat(destination_dir_.dir.fd, entry.path, 0o755) catch {}; }, .File => { try std.os.linkat(entry.dir.dir.fd, entry.basename, destination_dir_.dir.fd, entry.path, 0); real_file_count += 1; }, else => {}, } } return real_file_count; } }; var subdir = this.destination_dir.dir.makeOpenPathIterable(std.mem.span(this.destination_dir_subpath), .{}) catch |err| return Result{ .fail = .{ .err = err, .step = .opening_cache_dir }, }; defer subdir.close(); this.file_count = FileCopier.copy( subdir, &walker_, ) catch |err| switch (err) { error.NotSameFileSystem => return err, else => return Result{ .fail = .{ .err = err, .step = .copying_files }, }, }; return Result{ .success = void{}, }; } fn installWithSymlink(this: *PackageInstall) !Result { const Walker = @import("../walker_skippable.zig"); var cached_package_dir = bun.openDir(this.cache_dir.dir, this.cache_dir_subpath) catch |err| return Result{ .fail = .{ .err = err, .step = .opening_cache_dir }, }; defer cached_package_dir.close(); var walker_ = Walker.walk( cached_package_dir, this.allocator, &[_]string{}, &[_]string{ "node_modules", ".git" }, ) catch |err| return Result{ .fail = .{ .err = err, .step = .opening_cache_dir }, }; defer walker_.deinit(); const FileCopier = struct { pub fn copy( dest_dir_fd: std.os.fd_t, cache_dir_fd: std.os.fd_t, walker: *Walker, ) !u32 { var real_file_count: u32 = 0; var buf: [bun.MAX_PATH_BYTES]u8 = undefined; var cache_dir_path = try bun.getFdPath(cache_dir_fd, &buf); var remain = buf[cache_dir_path.len..]; var cache_dir_offset = cache_dir_path.len; if (cache_dir_path.len > 0 and cache_dir_path[cache_dir_path.len - 1] != std.fs.path.sep) { remain[0] = std.fs.path.sep; cache_dir_offset += 1; remain = remain[1..]; } var dest_buf: [bun.MAX_PATH_BYTES]u8 = undefined; var dest_base = try bun.getFdPath(dest_dir_fd, &dest_buf); var dest_remaining = dest_buf[dest_base.len..]; var dest_dir_offset = dest_base.len; if (dest_base.len > 0 and dest_buf[dest_base.len - 1] != std.fs.path.sep) { dest_remaining[0] = std.fs.path.sep; dest_remaining = dest_remaining[1..]; dest_dir_offset += 1; } while (try walker.next()) |entry| { switch (entry.kind) { // directories are created .Directory => { std.os.mkdirat(dest_dir_fd, entry.path, 0o755) catch {}; }, // but each file in the directory is a symlink .File => { @memcpy(remain.ptr, entry.path.ptr, entry.path.len); remain[entry.path.len] = 0; var from_path = buf[0 .. cache_dir_offset + entry.path.len :0]; @memcpy(dest_remaining.ptr, entry.path.ptr, entry.path.len); dest_remaining[entry.path.len] = 0; var to_path = dest_buf[0 .. dest_dir_offset + entry.path.len :0]; try std.os.symlinkZ(from_path, to_path); real_file_count += 1; }, else => {}, } } return real_file_count; } }; var subdir = this.destination_dir.dir.makeOpenPathIterable(std.mem.span(this.destination_dir_subpath), .{}) catch |err| return Result{ .fail = .{ .err = err, .step = .opening_cache_dir }, }; defer subdir.close(); this.file_count = FileCopier.copy( subdir.dir.fd, cached_package_dir.dir.fd, &walker_, ) catch |err| return Result{ .fail = .{ .err = err, .step = .copying_files, }, }; return Result{ .success = void{}, }; } pub fn uninstall(this: *PackageInstall) !void { try this.destination_dir.dir.deleteTree(std.mem.span(this.destination_dir_subpath)); } fn isDanglingSymlink(path: [:0]const u8) bool { if (comptime Environment.isLinux) { const rc = Syscall.system.open(path, @as(u32, std.os.O.PATH | 0), @as(u32, 0)); switch (Syscall.getErrno(rc)) { .SUCCESS => { const fd = @intCast(std.os.fd_t, rc); _ = Syscall.system.close(fd); return false; }, else => return true, } } else { const rc = Syscall.system.open(path, @as(u32, 0), @as(u32, 0)); switch (Syscall.getErrno(rc)) { .SUCCESS => { _ = Syscall.system.close(rc); return false; }, else => return true, } } } pub fn installFromLink(this: *PackageInstall, skip_delete: bool) Result { const dest_path = this.destination_dir_subpath; // If this fails, we don't care. // we'll catch it the next error if (!skip_delete and !strings.eqlComptime(dest_path, ".")) this.uninstall() catch {}; const subdir = std.fs.path.dirname(dest_path); var dest_dir = if (subdir) |dir| brk: { break :brk this.destination_dir.dir.makeOpenPath(dir, .{}) catch |err| return Result{ .fail = .{ .err = err, .step = .linking, }, }; } else this.destination_dir.dir; defer { if (subdir != null) dest_dir.close(); } var dest_buf: [bun.MAX_PATH_BYTES]u8 = undefined; const dest_dir_path = dest_dir.realpath(".", &dest_buf) catch |err| return Result{ .fail = .{ .err = err, .step = .linking, }, }; // cache_dir_subpath in here is actually the full path to the symlink pointing to the linked package const symlinked_path = this.cache_dir_subpath; var to_buf: [bun.MAX_PATH_BYTES]u8 = undefined; const to_path = this.cache_dir.dir.realpath(symlinked_path, &to_buf) catch |err| return Result{ .fail = .{ .err = err, .step = .linking, }, }; const target = Path.relative(dest_dir_path, to_path); std.os.symlinkat(target, dest_dir.fd, std.fs.path.basename(dest_path)) catch |err| return Result{ .fail = .{ .err = err, .step = .linking, }, }; if (isDanglingSymlink(symlinked_path)) return Result{ .fail = .{ .err = error.DanglingSymlink, .step = .linking, }, }; return Result{ .success = {}, }; } pub fn install(this: *PackageInstall, skip_delete: bool) Result { // If this fails, we don't care. // we'll catch it the next error if (!skip_delete and !strings.eqlComptime(this.destination_dir_subpath, ".")) this.uninstall() catch {}; var supported_method_to_use = if (strings.eqlComptime(this.cache_dir_subpath, ".") or strings.hasPrefixComptime(this.cache_dir_subpath, "..")) Method.symlink else supported_method; switch (supported_method_to_use) { .clonefile => { if (comptime Environment.isMac) { // First, attempt to use clonefile // if that fails due to ENOTSUP, mark it as unsupported and then fall back to copyfile if (this.installWithClonefile()) |result| { return result; } else |err| { switch (err) { error.NotSupported => { supported_method = .copyfile; supported_method_to_use = .copyfile; }, error.FileNotFound => return Result{ .fail = .{ .err = error.FileNotFound, .step = .opening_cache_dir }, }, else => return Result{ .fail = .{ .err = err, .step = .copying_files }, }, } } } }, .clonefile_each_dir => { if (comptime Environment.isMac) { if (this.installWithClonefileEachDir()) |result| { return result; } else |err| { switch (err) { error.NotSupported => { supported_method = .copyfile; supported_method_to_use = .copyfile; }, error.FileNotFound => return Result{ .fail = .{ .err = error.FileNotFound, .step = .opening_cache_dir }, }, else => return Result{ .fail = .{ .err = err, .step = .copying_files }, }, } } } }, .hardlink => { if (this.installWithHardlink()) |result| { return result; } else |err| { switch (err) { error.NotSameFileSystem => { supported_method = .copyfile; supported_method_to_use = .copyfile; }, error.FileNotFound => return Result{ .fail = .{ .err = error.FileNotFound, .step = .opening_cache_dir }, }, else => return Result{ .fail = .{ .err = err, .step = .copying_files }, }, } } }, .symlink => { if (this.installWithSymlink()) |result| { return result; } else |err| { switch (err) { error.FileNotFound => return Result{ .fail = .{ .err = error.FileNotFound, .step = .opening_cache_dir }, }, else => return Result{ .fail = .{ .err = err, .step = .copying_files }, }, } } }, else => {}, } if (supported_method_to_use != .copyfile) return Result{ .success = void{}, }; // TODO: linux io_uring return this.installWithCopyfile(); } }; pub const Resolution = @import("./resolution.zig").Resolution; const Progress = std.Progress; const TaggedPointer = @import("../tagged_pointer.zig"); const TaskCallbackContext = union(Tag) { dependency: PackageID, request_id: PackageID, root_dependency: PackageID, root_request_id: PackageID, node_modules_folder: u32, // Really, this is a file descriptor root_node_modules_folder: u32, // Really, this is a file descriptor pub const Tag = enum { dependency, request_id, node_modules_folder, root_dependency, root_request_id, root_node_modules_folder, }; }; const TaskCallbackList = std.ArrayListUnmanaged(TaskCallbackContext); const TaskDependencyQueue = std.HashMapUnmanaged(u64, TaskCallbackList, IdentityContext(u64), 80); const TaskChannel = sync.Channel(Task, .{ .Static = 4096 }); const NetworkChannel = sync.Channel(*NetworkTask, .{ .Static = 8192 }); const ThreadPool = @import("bun").ThreadPool; const PackageManifestMap = std.HashMapUnmanaged(PackageNameHash, Npm.PackageManifest, IdentityContext(PackageNameHash), 80); pub const CacheLevel = struct { use_cache_control_headers: bool, use_etag: bool, use_last_modified: bool, }; const AsyncIO = @import("bun").AsyncIO; const Waker = AsyncIO.Waker; // We can't know all the packages we need until we've downloaded all the packages // The easy way would be: // 1. Download all packages, parsing their dependencies and enqueuing all dependencies for resolution // 2. pub const PackageManager = struct { cache_directory_: ?std.fs.IterableDir = null, temp_dir_: ?std.fs.IterableDir = null, root_dir: *Fs.FileSystem.DirEntry, env_loader: *DotEnv.Loader, allocator: std.mem.Allocator, log: *logger.Log, resolve_tasks: TaskChannel, timestamp_for_manifest_cache_control: u32 = 0, extracted_count: u32 = 0, default_features: Features = Features{}, summary: Lockfile.Package.Diff.Summary = Lockfile.Package.Diff.Summary{}, env: *DotEnv.Loader, progress: Progress = .{}, downloads_node: ?*Progress.Node = null, progress_name_buf: [768]u8 = undefined, progress_name_buf_dynamic: []u8 = &[_]u8{}, cpu_count: u32 = 0, package_json_updates: []UpdateRequest = &[_]UpdateRequest{}, // progress bar stuff when not stack allocated root_progress_node: *std.Progress.Node = undefined, root_download_node: std.Progress.Node = undefined, to_remove: []const UpdateRequest = &[_]UpdateRequest{}, root_package_json_file: std.fs.File, root_dependency_list: Lockfile.DependencySlice = .{}, /// Used to make "dependencies" optional in the main package /// Depended on packages have to explicitly list their dependencies dynamic_root_dependencies: ?std.ArrayList(Dependency.Pair) = null, // remote_dependencies: RemoteDependency.List = .{}, thread_pool: ThreadPool, manifests: PackageManifestMap = PackageManifestMap{}, folders: FolderResolution.Map = FolderResolution.Map{}, task_queue: TaskDependencyQueue = .{}, network_dedupe_map: NetworkTaskQueue = .{}, network_channel: NetworkChannel = NetworkChannel.init(), network_tarball_batch: ThreadPool.Batch = ThreadPool.Batch{}, network_resolve_batch: ThreadPool.Batch = ThreadPool.Batch{}, network_task_fifo: NetworkQueue = undefined, preallocated_network_tasks: PreallocatedNetworkTasks = PreallocatedNetworkTasks{ .buffer = undefined, .len = 0 }, pending_tasks: u32 = 0, total_tasks: u32 = 0, lockfile: *Lockfile = undefined, options: Options = Options{}, preinstall_state: std.ArrayListUnmanaged(PreinstallState) = std.ArrayListUnmanaged(PreinstallState){}, global_link_dir: ?std.fs.IterableDir = null, global_dir: ?std.fs.IterableDir = null, global_link_dir_path: string = "", waiter: Waker = undefined, wait_count: std.atomic.Atomic(usize) = std.atomic.Atomic(usize).init(0), onWake: WakeHandler = .{}, const PreallocatedNetworkTasks = std.BoundedArray(NetworkTask, 1024); const NetworkTaskQueue = std.HashMapUnmanaged(u64, void, IdentityContext(u64), 80); pub var verbose_install = false; const PackageDedupeList = std.HashMapUnmanaged( u32, void, IdentityContext(u32), 80, ); pub const WakeHandler = struct { // handler: fn (ctx: *anyopaque, pm: *PackageManager) void = undefined, // onDependencyError: fn (ctx: *anyopaque, Dependency, PackageID, anyerror) void = undefined, handler: *const anyopaque = undefined, onDependencyError: *const anyopaque = undefined, context: ?*anyopaque = null, pub inline fn getHandler(t: @This()) *const fn (ctx: *anyopaque, pm: *PackageManager) void { return bun.cast(*const fn (ctx: *anyopaque, pm: *PackageManager) void, t.handler); } pub inline fn getonDependencyError(t: @This()) *const fn (ctx: *anyopaque, Dependency, PackageID, anyerror) void { return bun.cast(*const fn (ctx: *anyopaque, Dependency, PackageID, anyerror) void, t.handler); } }; pub fn failRootResolution(this: *PackageManager, dependency: *const Dependency, dependency_id: PackageID, err: anyerror) void { if (this.dynamic_root_dependencies) |*dynamic| { dynamic.items[dependency_id].failed = err; if (this.onWake.context) |ctx| { this.onWake.getonDependencyError()( ctx, dependency.*, dependency_id, err, ); } } else { // this means a bug bun.unreachablePanic("assignRootResolution: dependency_id: {d} out of bounds", .{dependency_id}); } } pub fn wake(this: *PackageManager) void { if (this.onWake.context != null) { this.onWake.getHandler()(this.onWake.context.?, this); return; } _ = this.wait_count.fetchAdd(1, .Monotonic); this.waiter.wake() catch {}; } pub fn sleep(this: *PackageManager) void { if (this.wait_count.swap(0, .Monotonic) > 0) return; _ = this.waiter.wait() catch 0; } const DependencyToEnqueue = union(enum) { pending: PackageID, resolution: struct { package_id: PackageID, resolution: Resolution }, not_found: void, failure: anyerror, }; pub fn enqueueDependencyToRoot( this: *PackageManager, name: []const u8, version_buf: []const u8, version: Dependency.Version, behavior: Dependency.Behavior, is_main: bool, ) DependencyToEnqueue { var root_deps = this.dynamicRootDependencies(); const existing: []const Dependency.Pair = root_deps.items; var str_buf = this.lockfile.buffers.string_bytes.items; for (existing) |pair, i| { if (strings.eqlLong(this.lockfile.str(&pair.dependency.name), name, true)) { if (pair.dependency.version.eql(version, str_buf, version_buf)) { if (pair.resolution_id != invalid_package_id) { return .{ .resolution = .{ .resolution = this.lockfile.packages.items(.resolution)[pair.resolution_id], .package_id = pair.resolution_id, }, }; } return .{ .pending = @truncate(u32, i) }; } } } var builder = this.lockfile.stringBuilder(); const dependency = Dependency{ .name = String.init(name, name), .name_hash = String.Builder.stringHash(name), .version = version, .behavior = behavior, }; dependency.countWithDifferentBuffers(name, version_buf, @TypeOf(&builder), &builder); builder.allocate() catch |err| { return .{ .failure = err }; }; const cloned_dependency = dependency.cloneWithDifferentBuffers(name, version_buf, @TypeOf(&builder), &builder) catch unreachable; builder.clamp(); const index = @truncate(u32, root_deps.items.len); root_deps.append( .{ .dependency = cloned_dependency, }, ) catch unreachable; if (is_main) { this.enqueueDependencyWithMainAndSuccessFn( index, &cloned_dependency, invalid_package_id, true, assignRootResolution, failRootResolution, ) catch |err| { root_deps.items.len = index; return .{ .failure = err }; }; } else { this.enqueueDependencyWithMainAndSuccessFn( index, &cloned_dependency, invalid_package_id, false, assignRootResolution, failRootResolution, ) catch |err| { root_deps.items.len = index; return .{ .failure = err }; }; } if (root_deps.items[index].failed) |fail| { root_deps.items.len = index; return .{ .failure = fail }; } const resolution_id = root_deps.items[index].resolution_id; // check if we managed to synchronously resolve the dependency if (resolution_id != invalid_package_id) { this.drainDependencyList(); return .{ .resolution = .{ .resolution = this.lockfile.packages.items(.resolution)[resolution_id], .package_id = resolution_id, }, }; } return .{ .pending = index }; } pub fn globalLinkDir(this: *PackageManager) !std.fs.IterableDir { return this.global_link_dir orelse brk: { var global_dir = try Options.openGlobalDir(this.options.explicit_global_directory); this.global_dir = global_dir; this.global_link_dir = try global_dir.dir.makeOpenPathIterable("node_modules", .{}); var buf: [bun.MAX_PATH_BYTES]u8 = undefined; const _path = try bun.getFdPath(this.global_link_dir.?.dir.fd, &buf); this.global_link_dir_path = try Fs.FileSystem.DirnameStore.instance.append([]const u8, _path); break :brk this.global_link_dir.?; }; } pub fn globalLinkDirPath(this: *PackageManager) ![]const u8 { _ = try this.globalLinkDir(); return this.global_link_dir_path; } fn ensurePreinstallStateListCapacity(this: *PackageManager, count: usize) !void { if (this.preinstall_state.items.len >= count) { return; } const offset = this.preinstall_state.items.len; try this.preinstall_state.ensureTotalCapacity(this.allocator, count); this.preinstall_state.expandToCapacity(); std.mem.set(PreinstallState, this.preinstall_state.items[offset..], PreinstallState.unknown); } pub fn setPreinstallState(this: *PackageManager, package_id: PackageID, lockfile: *Lockfile, value: PreinstallState) void { this.ensurePreinstallStateListCapacity(lockfile.packages.len) catch return; this.preinstall_state.items[package_id] = value; } pub fn getPreinstallState(this: *PackageManager, package_id: PackageID, _: *Lockfile) PreinstallState { if (package_id >= this.preinstall_state.items.len) { return PreinstallState.unknown; } return this.preinstall_state.items[package_id]; } pub fn determinePreinstallState(manager: *PackageManager, this: Package, lockfile: *Lockfile) PreinstallState { switch (manager.getPreinstallState(this.meta.id, lockfile)) { .unknown => { // Do not automatically start downloading packages which are disabled // i.e. don't download all of esbuild's versions or SWCs if (this.isDisabled()) { manager.setPreinstallState(this.meta.id, lockfile, .done); return .done; } const folder_path = manager.cachedNPMPackageFolderName(lockfile.str(&this.name), this.resolution.value.npm.version); if (manager.isFolderInCache(folder_path)) { manager.setPreinstallState(this.meta.id, lockfile, .done); return .done; } manager.setPreinstallState(this.meta.id, lockfile, .extract); return .extract; }, else => |val| return val, } } pub fn scopeForPackageName(this: *const PackageManager, name: string) *const Npm.Registry.Scope { if (name.len == 0 or name[0] != '@') return &this.options.scope; return this.options.registries.getPtr( Npm.Registry.Scope.hash( Npm.Registry.Scope.getName(name), ), ) orelse &this.options.scope; } pub fn setNodeName( this: *PackageManager, node: *Progress.Node, name: string, emoji: string, comptime is_first: bool, ) void { if (Output.isEmojiEnabled()) { if (is_first) { std.mem.copy(u8, &this.progress_name_buf, emoji); std.mem.copy(u8, this.progress_name_buf[emoji.len..], name); node.name = this.progress_name_buf[0 .. emoji.len + name.len]; } else { std.mem.copy(u8, this.progress_name_buf[emoji.len..], name); node.name = this.progress_name_buf[0 .. emoji.len + name.len]; } } else { std.mem.copy(u8, &this.progress_name_buf, name); node.name = this.progress_name_buf[0..name.len]; } } var cached_package_folder_name_buf: [bun.MAX_PATH_BYTES]u8 = undefined; pub inline fn getCacheDirectory(this: *PackageManager) std.fs.IterableDir { return this.cache_directory_ orelse brk: { this.cache_directory_ = this.ensureCacheDirectory(); break :brk this.cache_directory_.?; }; } pub inline fn getTemporaryDirectory(this: *PackageManager) std.fs.IterableDir { return this.temp_dir_ orelse brk: { this.temp_dir_ = this.ensureTemporaryDirectory(); break :brk this.temp_dir_.?; }; } noinline fn ensureCacheDirectory(this: *PackageManager) std.fs.IterableDir { loop: while (true) { if (this.options.enable.cache) { const cache_dir = fetchCacheDirectoryPath(this.env_loader); return std.fs.cwd().makeOpenPathIterable(cache_dir.path, .{}) catch { this.options.enable.cache = false; continue :loop; }; } return std.fs.cwd().makeOpenPathIterable("node_modules/.cache", .{}) catch |err| { Output.prettyErrorln("error: bun is unable to write files: {s}", .{@errorName(err)}); Global.crash(); }; } unreachable; } // We need a temporary directory that can be rename() // This is important for extracting files. // // However, we want it to be reused! Otherwise a cache is silly. // Error RenameAcrossMountPoints moving react-is to cache dir: noinline fn ensureTemporaryDirectory(this: *PackageManager) std.fs.IterableDir { var cache_directory = this.getCacheDirectory(); // The chosen tempdir must be on the same filesystem as the cache directory // This makes renameat() work const default_tempdir = Fs.FileSystem.RealFS.getDefaultTempDir(); var tried_dot_tmp = false; var tempdir: std.fs.IterableDir = std.fs.cwd().makeOpenPathIterable(default_tempdir, .{}) catch brk: { tried_dot_tmp = true; break :brk cache_directory.dir.makeOpenPathIterable(".tmp", .{}) catch |err| { Output.prettyErrorln("error: bun is unable to access tempdir: {s}", .{@errorName(err)}); Global.crash(); }; }; var tmpbuf: ["18446744073709551615".len + 8]u8 = undefined; const tmpname = Fs.FileSystem.instance.tmpname("hm", &tmpbuf, 999) catch unreachable; var timer: std.time.Timer = if (this.options.log_level != .silent) std.time.Timer.start() catch unreachable else undefined; brk: while (true) { _ = tempdir.dir.createFileZ(tmpname, .{ .truncate = true }) catch |err2| { if (!tried_dot_tmp) { tried_dot_tmp = true; tempdir = cache_directory.dir.makeOpenPathIterable(".tmp", .{}) catch |err| { Output.prettyErrorln("error: bun is unable to access tempdir: {s}", .{@errorName(err)}); Global.crash(); }; continue :brk; } Output.prettyErrorln("error: {s} accessing temporary directory. Please set $BUN_TMPDIR or $BUN_INSTALL", .{ @errorName(err2), }); Global.crash(); }; std.os.renameatZ(tempdir.dir.fd, tmpname, cache_directory.dir.fd, tmpname) catch |err| { if (!tried_dot_tmp) { tried_dot_tmp = true; tempdir = cache_directory.dir.makeOpenPathIterable(".tmp", .{}) catch |err2| { Output.prettyErrorln("error: bun is unable to write files to tempdir: {s}", .{@errorName(err2)}); Global.crash(); }; continue :brk; } Output.prettyErrorln("error: {s} accessing temporary directory. Please set $BUN_TMPDIR or $BUN_INSTALL", .{ @errorName(err), }); Global.crash(); }; cache_directory.dir.deleteFileZ(tmpname) catch {}; break; } if (this.options.log_level != .silent) { const elapsed = timer.read(); if (elapsed > std.time.ns_per_ms * 100) { var cache_dir_path = bun.getFdPath(cache_directory.dir.fd, &path_buf) catch "it's"; Output.prettyErrorln( "warn: Slow filesystem detected. If {s} is a network drive, consider setting $BUN_INSTALL_CACHE_DIR to a local folder.", .{cache_dir_path}, ); } } return tempdir; } pub var instance: PackageManager = undefined; pub fn getNetworkTask(this: *PackageManager) *NetworkTask { if (this.preallocated_network_tasks.len + 1 < this.preallocated_network_tasks.buffer.len) { const len = this.preallocated_network_tasks.len; this.preallocated_network_tasks.len += 1; return &this.preallocated_network_tasks.buffer[len]; } return this.allocator.create(NetworkTask) catch @panic("Memory allocation failure creating NetworkTask!"); } // TODO: normalize to alphanumeric pub fn cachedNPMPackageFolderNamePrint(this: *const PackageManager, buf: []u8, name: string, version: Semver.Version) stringZ { const scope = this.scopeForPackageName(name); const basename = cachedNPMPackageFolderPrintBasename(buf, name, version); if (scope.name.len == 0 and !this.options.did_override_default_scope) { return basename; } const spanned = std.mem.span(basename); var available = buf[spanned.len..]; var end: []u8 = undefined; if (scope.url.hostname.len > 32 or available.len < 64) { const visible_hostname = scope.url.hostname[0..@min(scope.url.hostname.len, 12)]; end = std.fmt.bufPrint(available, "@@{s}__{any}", .{ visible_hostname, bun.fmt.hexIntLower(String.Builder.stringHash(scope.url.href)) }) catch unreachable; } else { end = std.fmt.bufPrint(available, "@@{s}", .{scope.url.hostname}) catch unreachable; } buf[spanned.len + end.len] = 0; var result: [:0]u8 = buf[0 .. spanned.len + end.len :0]; return result; } pub fn cachedNPMPackageFolderBasename(name: string, version: Semver.Version) stringZ { return cachedNPMPackageFolderPrintBasename(&cached_package_folder_name_buf, name, version); } pub fn cachedNPMPackageFolderName(this: *const PackageManager, name: string, version: Semver.Version) stringZ { return this.cachedNPMPackageFolderNamePrint(&cached_package_folder_name_buf, name, version); } // TODO: normalize to alphanumeric pub fn cachedNPMPackageFolderPrintBasename(buf: []u8, name: string, version: Semver.Version) stringZ { const pre_hex_int = version.tag.pre.hash; const build_hex_int = version.tag.build.hash; if (!version.tag.hasPre() and !version.tag.hasBuild()) { return std.fmt.bufPrintZ(buf, "{s}@{d}.{d}.{d}", .{ name, version.major, version.minor, version.patch }) catch unreachable; } else if (version.tag.hasPre() and version.tag.hasBuild()) { return std.fmt.bufPrintZ( buf, "{s}@{d}.{d}.{d}-{any}+{any}", .{ name, version.major, version.minor, version.patch, bun.fmt.hexIntLower(pre_hex_int), bun.fmt.hexIntUpper(build_hex_int) }, ) catch unreachable; } else if (version.tag.hasPre()) { return std.fmt.bufPrintZ( buf, "{s}@{d}.{d}.{d}-{any}", .{ name, version.major, version.minor, version.patch, bun.fmt.hexIntLower(pre_hex_int) }, ) catch unreachable; } else if (version.tag.hasBuild()) { return std.fmt.bufPrintZ( buf, "{s}@{d}.{d}.{d}+{any}", .{ name, version.major, version.minor, version.patch, bun.fmt.hexIntUpper(build_hex_int) }, ) catch unreachable; } else { unreachable; } unreachable; } pub fn isFolderInCache(this: *PackageManager, folder_path: stringZ) bool { // TODO: is this slow? var dir = this.getCacheDirectory().dir.openDirZ(folder_path, .{}, true) catch return false; dir.close(); return true; } pub fn pathForCachedNPMPath( this: *PackageManager, buf: *[bun.MAX_PATH_BYTES]u8, package_name: []const u8, npm: Semver.Version, ) ![]u8 { var package_name_version_buf: [bun.MAX_PATH_BYTES]u8 = undefined; var subpath = std.fmt.bufPrintZ( &package_name_version_buf, "{s}" ++ std.fs.path.sep_str ++ "{any}", .{ package_name, npm.fmt(this.lockfile.buffers.string_bytes.items), }, ) catch unreachable; return this.getCacheDirectory().dir.readLink( subpath, buf, ) catch |err| { // if we run into an error, delete the symlink // so that we don't repeatedly try to read it std.os.unlinkat(this.getCacheDirectory().dir.fd, subpath, 0) catch {}; return err; }; } pub fn pathForResolution( this: *PackageManager, package_id: PackageID, resolution: Resolution, buf: *[bun.MAX_PATH_BYTES]u8, ) ![]u8 { // const folder_name = this.cachedNPMPackageFolderName(name, version); switch (resolution.tag) { .npm => { const npm = resolution.value.npm; const package_name_ = this.lockfile.packages.items(.name)[package_id]; const package_name = this.lockfile.str(&package_name_); return this.pathForCachedNPMPath(buf, package_name, npm.version); }, else => return "", } } pub fn getInstalledVersionsFromDiskCache(this: *PackageManager, tags_buf: *std.ArrayList(u8), package_name: []const u8, allocator: std.mem.Allocator) !std.ArrayList(Semver.Version) { var list = std.ArrayList(Semver.Version).init(allocator); var dir = this.getCacheDirectory().dir.openIterableDir(package_name, .{}) catch |err| { switch (err) { error.FileNotFound, error.NotDir, error.AccessDenied, error.DeviceBusy => { return list; }, else => return err, } }; defer dir.close(); var iter = dir.iterate(); while (try iter.next()) |entry| { if (entry.kind != .Directory and entry.kind != .SymLink) continue; const name = entry.name; const sliced = SlicedString.init(name, name); const parsed = Semver.Version.parse(sliced, allocator); if (!parsed.valid or parsed.wildcard != .none) continue; // not handling OOM // TODO: wildcard var version = parsed.version.fill(); const total = version.tag.build.len() + version.tag.pre.len(); if (total > 0) { tags_buf.ensureUnusedCapacity(total) catch unreachable; var available = tags_buf.items.ptr[tags_buf.items.len..tags_buf.capacity]; const new_version = version.cloneInto(name, &available); tags_buf.items.len += total; version = new_version; } list.append(version) catch unreachable; } return list; } pub fn resolveFromDiskCache(this: *PackageManager, package_name: []const u8, version: Dependency.Version) ?PackageID { if (version.tag != .npm) { // only npm supported right now // tags are more ambiguous return null; } var arena = std.heap.ArenaAllocator.init(this.allocator); defer arena.deinit(); var arena_alloc = arena.allocator(); var stack_fallback = std.heap.stackFallback(4096, arena_alloc); var allocator = stack_fallback.get(); var tags_buf = std.ArrayList(u8).init(allocator); var installed_versions = this.getInstalledVersionsFromDiskCache(&tags_buf, package_name, allocator) catch |err| { Output.debug("error getting installed versions from disk cache: {s}", .{std.mem.span(@errorName(err))}); return null; }; // TODO: make this fewer passes std.sort.sort( Semver.Version, installed_versions.items, @as([]const u8, tags_buf.items), Semver.Version.sortGt, ); for (installed_versions.items) |installed_version| { if (version.value.npm.version.satisfies(installed_version)) { var buf: [bun.MAX_PATH_BYTES]u8 = undefined; var npm_package_path = this.pathForCachedNPMPath(&buf, package_name, installed_version) catch |err| { Output.debug("error getting path for cached npm path: {s}", .{std.mem.span(@errorName(err))}); return null; }; const dependency = Dependency.Version{ .tag = .npm, .value = .{ .npm = .{ .name = String.init(package_name, package_name), .version = Semver.Query.Group.from(installed_version), }, }, }; switch (FolderResolution.getOrPut(.{ .cache_folder = npm_package_path }, dependency, ".", this)) { .new_package_id => |id| { this.enqueueDependencyList(this.lockfile.packages.items(.dependencies)[id], false); return id; }, .package_id => |id| { this.enqueueDependencyList(this.lockfile.packages.items(.dependencies)[id], false); return id; }, .err => |err| { Output.debug("error getting or putting folder resolution: {s}", .{std.mem.span(@errorName(err))}); return null; }, } } } return null; } const ResolvedPackageResult = struct { package: Lockfile.Package, /// Is this the first time we've seen this package? is_first_time: bool = false, /// Pending network task to schedule network_task: ?*NetworkTask = null, }; fn getOrPutResolvedPackageWithFindResult( this: *PackageManager, alias: String, name_hash: PackageNameHash, name: String, version: Dependency.Version, dependency_id: PackageID, behavior: Behavior, manifest: *const Npm.PackageManifest, find_result: Npm.PackageManifest.FindResult, comptime successFn: SuccessFn, ) !?ResolvedPackageResult { // Was this package already allocated? Let's reuse the existing one. if (this.lockfile.getPackageID( name_hash, if (behavior.isPeer()) version else null, .{ .tag = .npm, .value = .{ .npm = .{ .version = find_result.version, .url = find_result.package.tarball_url.value, }, }, }, )) |id| { successFn(this, dependency_id, id); return ResolvedPackageResult{ .package = this.lockfile.packages.get(id), .is_first_time = false, }; } // appendPackage sets the PackageID on the package const package = try this.lockfile.appendPackage(try Lockfile.Package.fromNPM( this.allocator, this.lockfile, this.log, manifest, find_result.version, find_result.package, manifest.string_buf, Features.npm, )); const buf = this.lockfile.buffers.string_bytes.items; if (!alias.eql(name, buf, buf)) { try this.lockfile.alias_map.put(this.allocator, package.meta.id, alias); } if (!behavior.isEnabled(if (this.isRootDependency(dependency_id)) this.options.local_package_features else this.options.remote_package_features)) { this.setPreinstallState(package.meta.id, this.lockfile, .done); } const preinstall = this.determinePreinstallState(package, this.lockfile); if (comptime Environment.isDebug or Environment.isTest) std.debug.assert(package.meta.id != invalid_package_id); defer successFn(this, dependency_id, package.meta.id); switch (preinstall) { // Is this package already in the cache? // We don't need to download the tarball, but we should enqueue dependencies .done => { return ResolvedPackageResult{ .package = package, .is_first_time = true }; }, // Do we need to download the tarball? .extract => { const task_id = Task.Id.forNPMPackage( Task.Tag.extract, this.lockfile.str(&name), package.resolution.value.npm.version, ); if (try this.generateNetworkTaskForTarball(task_id, manifest.str(&find_result.package.tarball_url), package)) |network_task| { return ResolvedPackageResult{ .package = package, .is_first_time = true, .network_task = network_task, }; } // if we are in the middle of extracting this package, we should wait for it to finish return ResolvedPackageResult{ .package = package }; }, else => unreachable, } return ResolvedPackageResult{ .package = package }; } pub fn generateNetworkTaskForTarball(this: *PackageManager, task_id: u64, url: string, package: Lockfile.Package) !?*NetworkTask { const dedupe_entry = try this.network_dedupe_map.getOrPut(this.allocator, task_id); if (dedupe_entry.found_existing) { return null; } var network_task = this.getNetworkTask(); network_task.* = NetworkTask{ .task_id = task_id, .callback = undefined, .allocator = this.allocator, .package_manager = this, }; const scope = this.scopeForPackageName(this.lockfile.str(&package.name)); try network_task.forTarball( this.allocator, ExtractTarball{ .package_manager = &PackageManager.instance, // https://github.com/ziglang/zig/issues/14005 .name = if (package.name.len() >= strings.StringOrTinyString.Max) strings.StringOrTinyString.init( try FileSystem.FilenameStore.instance.append( string, this.lockfile.str(&package.name), ), ) else strings.StringOrTinyString.init(this.lockfile.str(&package.name)), .resolution = package.resolution, .cache_dir = this.getCacheDirectory().dir, .temp_dir = this.getTemporaryDirectory().dir, .registry = scope.url.href, .package_id = package.meta.id, .integrity = package.meta.integrity, .url = url, }, scope, ); return network_task; } fn enqueueNetworkTask(this: *PackageManager, task: *NetworkTask) void { if (this.network_task_fifo.writableLength() == 0) { this.flushNetworkQueue(); } this.network_task_fifo.writeItemAssumeCapacity(task); } const SuccessFn = *const fn (*PackageManager, PackageID, PackageID) void; const FailFn = *const fn (*PackageManager, *const Dependency, PackageID, anyerror) void; fn assignResolution(this: *PackageManager, dependency_id: PackageID, package_id: PackageID) void { this.lockfile.buffers.resolutions.items[dependency_id] = package_id; } fn assignRootResolution(this: *PackageManager, dependency_id: PackageID, package_id: PackageID) void { if (this.dynamic_root_dependencies) |*dynamic| { dynamic.items[dependency_id].resolution_id = package_id; } else { if (this.lockfile.buffers.resolutions.items.len > dependency_id) { this.lockfile.buffers.resolutions.items[dependency_id] = package_id; } else { // this means a bug bun.unreachablePanic("assignRootResolution: dependency_id: {d} out of bounds (package_id: {d})", .{ dependency_id, package_id }); } } } fn getOrPutResolvedPackage( this: *PackageManager, alias: String, name_hash: PackageNameHash, name: String, version: Dependency.Version, behavior: Behavior, dependency_id: PackageID, resolution: PackageID, comptime successFn: SuccessFn, ) !?ResolvedPackageResult { name.assertDefined(); alias.assertDefined(); if (resolution < this.lockfile.packages.len) { return ResolvedPackageResult{ .package = this.lockfile.packages.get(resolution) }; } switch (version.tag) { .npm, .dist_tag => { // Resolve the version from the loaded NPM manifest const manifest = this.manifests.getPtr(name_hash) orelse return null; // manifest might still be downloading. This feels unreliable. const find_result: Npm.PackageManifest.FindResult = switch (version.tag) { .dist_tag => manifest.findByDistTag(this.lockfile.str(&version.value.dist_tag.tag)), .npm => manifest.findBestVersion(version.value.npm.version), else => unreachable, } orelse return switch (version.tag) { .npm => error.NoMatchingVersion, .dist_tag => error.DistTagNotFound, else => unreachable, }; return try getOrPutResolvedPackageWithFindResult( this, alias, name_hash, name, version, dependency_id, behavior, manifest, find_result, successFn, ); }, .folder => { // relative to cwd const res = FolderResolution.getOrPut(.{ .relative = .folder }, version, this.lockfile.str(&version.value.folder), this); switch (res) { .err => |err| return err, .package_id => |package_id| { successFn(this, dependency_id, package_id); return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id) }; }, .new_package_id => |package_id| { successFn(this, dependency_id, package_id); return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id), .is_first_time = true }; }, } }, .workspace => { // relative to cwd const res = FolderResolution.getOrPut(.{ .relative = .workspace }, version, this.lockfile.str(&version.value.workspace), this); switch (res) { .err => |err| return err, .package_id => |package_id| { successFn(this, dependency_id, package_id); return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id) }; }, .new_package_id => |package_id| { successFn(this, dependency_id, package_id); return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id), .is_first_time = true }; }, } }, .symlink => { const res = FolderResolution.getOrPut(.{ .global = try this.globalLinkDirPath() }, version, this.lockfile.str(&version.value.symlink), this); switch (res) { .err => |err| return err, .package_id => |package_id| { successFn(this, dependency_id, package_id); return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id) }; }, .new_package_id => |package_id| { successFn(this, dependency_id, package_id); return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id), .is_first_time = true }; }, } }, else => return null, } } fn enqueueParseNPMPackage( this: *PackageManager, task_id: u64, name: strings.StringOrTinyString, network_task: *NetworkTask, ) *ThreadPool.Task { var task = this.allocator.create(Task) catch unreachable; task.* = Task{ .package_manager = &PackageManager.instance, // https://github.com/ziglang/zig/issues/14005 .log = logger.Log.init(this.allocator), .tag = Task.Tag.package_manifest, .request = .{ .package_manifest = .{ .network = network_task, .name = name, }, }, .id = task_id, .data = undefined, }; return &task.threadpool_task; } fn enqueueExtractNPMPackage( this: *PackageManager, tarball: ExtractTarball, network_task: *NetworkTask, ) *ThreadPool.Task { var task = this.allocator.create(Task) catch unreachable; task.* = Task{ .package_manager = &PackageManager.instance, // https://github.com/ziglang/zig/issues/14005 .log = logger.Log.init(this.allocator), .tag = Task.Tag.extract, .request = .{ .extract = .{ .network = network_task, .tarball = tarball, }, }, .id = network_task.task_id, .data = undefined, }; task.request.extract.tarball.skip_verify = !this.options.do.verify_integrity; return &task.threadpool_task; } pub fn dynamicRootDependencies(this: *PackageManager) *std.ArrayList(Dependency.Pair) { if (this.dynamic_root_dependencies == null) { const root_deps = this.lockfile.rootPackage().?.dependencies.get(this.lockfile.buffers.dependencies.items); this.dynamic_root_dependencies = std.ArrayList(Dependency.Pair).initCapacity(this.allocator, root_deps.len) catch unreachable; this.dynamic_root_dependencies.?.items.len = root_deps.len; for (root_deps) |dep, i| { this.dynamic_root_dependencies.?.items[i] = .{ .dependency = dep, .resolution_id = invalid_package_id, }; } } return &this.dynamic_root_dependencies.?; } pub fn writeYarnLock(this: *PackageManager) !void { var printer = Lockfile.Printer{ .lockfile = this.lockfile, .options = this.options, }; var tmpname_buf: [512]u8 = undefined; tmpname_buf[0..8].* = "tmplock-".*; var tmpfile = FileSystem.RealFS.Tmpfile{}; var secret: [32]u8 = undefined; std.mem.writeIntNative(u64, secret[0..8], @intCast(u64, std.time.milliTimestamp())); var state = std.rand.Xoodoo.init(secret); const rng = state.random(); var base64_bytes: [64]u8 = undefined; rng.bytes(&base64_bytes); const tmpname__ = std.fmt.bufPrint(tmpname_buf[8..], "{s}", .{std.fmt.fmtSliceHexLower(&base64_bytes)}) catch unreachable; tmpname_buf[tmpname__.len + 8] = 0; const tmpname = tmpname_buf[0 .. tmpname__.len + 8 :0]; tmpfile.create(&FileSystem.instance.fs, tmpname) catch |err| { Output.prettyErrorln("error: failed to create tmpfile: {s}", .{@errorName(err)}); Global.crash(); }; var file = tmpfile.file(); var file_writer = file.writer(); var buffered_writer = std.io.BufferedWriter(std.mem.page_size, @TypeOf(file_writer)){ .unbuffered_writer = file_writer, }; var writer = buffered_writer.writer(); try Lockfile.Printer.Yarn.print(&printer, @TypeOf(writer), writer); try buffered_writer.flush(); _ = C.fchmod( tmpfile.fd, // chmod 666, 0o0000040 | 0o0000004 | 0o0000002 | 0o0000400 | 0o0000200 | 0o0000020, ); try tmpfile.promote(tmpname, std.fs.cwd().fd, "yarn.lock"); } pub fn isRootDependency(this: *const PackageManager, id: PackageID) bool { if (this.dynamic_root_dependencies != null) { return false; } return this.root_dependency_list.contains(id); } fn enqueueDependencyWithMain( this: *PackageManager, id: u32, /// This must be a *const to prevent UB dependency: *const Dependency, resolution: PackageID, comptime is_main: bool, ) !void { return this.enqueueDependencyWithMainAndSuccessFn( id, dependency, resolution, is_main, assignResolution, null, ); } /// Q: "What do we do with a dependency in a package.json?" /// A: "We enqueue it!" pub fn enqueueDependencyWithMainAndSuccessFn( this: *PackageManager, id: u32, /// This must be a *const to prevent UB dependency: *const Dependency, resolution: PackageID, comptime is_main: bool, comptime successFn: SuccessFn, comptime failFn: ?FailFn, ) !void { const alias = dependency.name; const name = dependency.realname(); const name_hash = switch (dependency.version.tag) { .dist_tag, .npm => Lockfile.stringHash(this.lockfile.str(&name)), else => dependency.name_hash, }; const version = dependency.version; var loaded_manifest: ?Npm.PackageManifest = null; if (comptime !is_main) { // it might really be main if (!this.isRootDependency(id)) if (!dependency.behavior.isEnabled(switch (dependency.version.tag) { .dist_tag, .folder, .npm => this.options.remote_package_features, else => Features{}, })) return; } switch (dependency.version.tag) { .dist_tag, .folder, .npm => { retry_from_manifests_ptr: while (true) { var resolve_result_ = this.getOrPutResolvedPackage( alias, name_hash, name, version, dependency.behavior, id, resolution, successFn, ); retry_with_new_resolve_result: while (true) { const resolve_result = resolve_result_ catch |err| { switch (err) { error.DistTagNotFound => { if (dependency.behavior.isRequired()) { if (failFn) |fail| { fail( this, dependency, id, err, ); } else { this.log.addErrorFmt( null, logger.Loc.Empty, this.allocator, "package \"{s}\" with tag \"{s}\" not found, but package exists", .{ this.lockfile.str(&name), this.lockfile.str(&version.value.dist_tag.tag), }, ) catch unreachable; } } return; }, error.NoMatchingVersion => { if (dependency.behavior.isRequired()) { if (failFn) |fail| { fail( this, dependency, id, err, ); } else { this.log.addErrorFmt( null, logger.Loc.Empty, this.allocator, "No version matching \"{s}\" found for specifier \"{s}\" (but package exists)", .{ this.lockfile.str(&version.literal), this.lockfile.str(&name), }, ) catch unreachable; } } return; }, else => { if (failFn) |fail| { fail( this, dependency, id, err, ); return; } return err; }, } }; if (resolve_result) |result| { // First time? if (result.is_first_time) { if (PackageManager.verbose_install) { const label: string = this.lockfile.str(&version.literal); Output.prettyErrorln(" -> \"{s}\": \"{s}\" -> {s}@{}", .{ this.lockfile.str(&result.package.name), label, this.lockfile.str(&result.package.name), result.package.resolution.fmt(this.lockfile.buffers.string_bytes.items), }); } // Resolve dependencies first if (result.package.dependencies.len > 0) { try this.lockfile.scratch.dependency_list_queue.writeItem(result.package.dependencies); } } if (result.network_task) |network_task| { if (this.getPreinstallState(result.package.meta.id, this.lockfile) == .extract) { this.setPreinstallState(result.package.meta.id, this.lockfile, .extracting); this.enqueueNetworkTask(network_task); } } } else if (!dependency.behavior.isPeer() and dependency.version.tag.isNPM()) { const name_str = this.lockfile.str(&name); const task_id = Task.Id.forManifest(Task.Tag.package_manifest, name_str); var network_entry = try this.network_dedupe_map.getOrPutContext(this.allocator, task_id, .{}); if (!network_entry.found_existing) { if (this.options.enable.manifest_cache) { if (Npm.PackageManifest.Serializer.load(this.allocator, this.getCacheDirectory(), name_str) catch null) |manifest_| { const manifest: Npm.PackageManifest = manifest_; loaded_manifest = manifest; if (this.options.enable.manifest_cache_control and manifest.pkg.public_max_age > this.timestamp_for_manifest_cache_control) { try this.manifests.put(this.allocator, @truncate(PackageNameHash, manifest.pkg.name.hash), manifest); } // If it's an exact package version already living in the cache // We can skip the network request, even if it's beyond the caching period if (dependency.version.tag == .npm and dependency.version.value.npm.version.isExact()) { if (loaded_manifest.?.findByVersion(dependency.version.value.npm.version.head.head.range.left.version)) |find_result| { if (this.getOrPutResolvedPackageWithFindResult( alias, name_hash, name, version, id, dependency.behavior, &loaded_manifest.?, find_result, successFn, ) catch null) |new_resolve_result| { resolve_result_ = new_resolve_result; _ = this.network_dedupe_map.remove(task_id); continue :retry_with_new_resolve_result; } } } // Was it recent enough to just load it without the network call? if (this.options.enable.manifest_cache_control and manifest.pkg.public_max_age > this.timestamp_for_manifest_cache_control) { _ = this.network_dedupe_map.remove(task_id); continue :retry_from_manifests_ptr; } } } if (PackageManager.verbose_install) { Output.prettyErrorln("Enqueue package manifest for download: {s}", .{name_str}); } var network_task = this.getNetworkTask(); network_task.* = NetworkTask{ .package_manager = &PackageManager.instance, // https://github.com/ziglang/zig/issues/14005 .callback = undefined, .task_id = task_id, .allocator = this.allocator, }; try network_task.forManifest( name_str, this.allocator, this.scopeForPackageName(name_str), loaded_manifest, ); this.enqueueNetworkTask(network_task); } std.debug.assert(task_id != 0); var manifest_entry_parse = try this.task_queue.getOrPutContext(this.allocator, task_id, .{}); if (!manifest_entry_parse.found_existing) { manifest_entry_parse.value_ptr.* = TaskCallbackList{}; } const callback_tag = comptime if (successFn == assignRootResolution) "root_dependency" else "dependency"; try manifest_entry_parse.value_ptr.append(this.allocator, @unionInit(TaskCallbackContext, callback_tag, id)); } return; } } return; }, .symlink, .workspace => { const _result = this.getOrPutResolvedPackage( alias, name_hash, name, version, dependency.behavior, id, resolution, successFn, ) catch |err| brk: { if (err == error.MissingPackageJSON) { break :brk @as(?ResolvedPackageResult, null); } return err; }; const not_found_fmt = \\package \"{[name]s}\" is not linked \\ \\To install a linked package: \\ bun link my-pkg-name-from-package-json \\ \\Tip: the package name is from package.json, which can differ from the folder name. \\ ; if (_result) |result| { // First time? if (result.is_first_time) { if (PackageManager.verbose_install) { const label: string = this.lockfile.str(&version.literal); Output.prettyErrorln(" -> \"{s}\": \"{s}\" -> {s}@{}", .{ this.lockfile.str(&result.package.name), label, this.lockfile.str(&result.package.name), result.package.resolution.fmt(this.lockfile.buffers.string_bytes.items), }); } // We shouldn't see any dependencies if (result.package.dependencies.len > 0) { try this.lockfile.scratch.dependency_list_queue.writeItem(result.package.dependencies); } } // should not trigger a network call std.debug.assert(result.network_task == null); } else if (dependency.behavior.isRequired()) { this.log.addErrorFmt( null, logger.Loc.Empty, this.allocator, not_found_fmt, .{ .name = this.lockfile.str(&name), }, ) catch unreachable; } else if (this.options.log_level.isVerbose()) { this.log.addWarningFmt( null, logger.Loc.Empty, this.allocator, not_found_fmt, .{ .name = this.lockfile.str(&name), }, ) catch unreachable; } }, else => {}, } } fn flushNetworkQueue(this: *PackageManager) void { var network = &this.network_task_fifo; while (network.readItem()) |network_task| { network_task.schedule(if (network_task.callback == .extract) &this.network_tarball_batch else &this.network_resolve_batch); } } fn doFlushDependencyQueue(this: *PackageManager) void { var lockfile = this.lockfile; var dependency_queue = &lockfile.scratch.dependency_list_queue; while (dependency_queue.readItem()) |dependencies_list| { var i: u32 = dependencies_list.off; const end = dependencies_list.off + dependencies_list.len; while (i < end) : (i += 1) { const dependency = lockfile.buffers.dependencies.items[i]; this.enqueueDependencyWithMain( i, &dependency, lockfile.buffers.resolutions.items[i], false, ) catch {}; } } this.flushNetworkQueue(); } pub fn flushDependencyQueue(this: *PackageManager) void { var last_count = this.total_tasks; while (true) : (last_count = this.total_tasks) { this.flushNetworkQueue(); this.doFlushDependencyQueue(); this.flushNetworkQueue(); if (this.total_tasks == last_count) break; } } pub fn scheduleNetworkTasks(manager: *PackageManager) usize { const count = manager.network_resolve_batch.len + manager.network_tarball_batch.len; manager.pending_tasks += @truncate(u32, count); manager.total_tasks += @truncate(u32, count); manager.network_resolve_batch.push(manager.network_tarball_batch); HTTP.http_thread.schedule(manager.network_resolve_batch); manager.network_tarball_batch = .{}; manager.network_resolve_batch = .{}; return count; } pub fn enqueueDependencyList( this: *PackageManager, dependencies_list: Lockfile.DependencySlice, comptime is_main: bool, ) void { this.task_queue.ensureUnusedCapacity(this.allocator, dependencies_list.len) catch unreachable; var lockfile = this.lockfile; // Step 1. Go through main dependencies { var i: u32 = dependencies_list.off; const end = dependencies_list.off + dependencies_list.len; // we have to be very careful with pointers here while (i < end) : (i += 1) { const dependency = lockfile.buffers.dependencies.items[i]; const resolution = lockfile.buffers.resolutions.items[i]; this.enqueueDependencyWithMain( i, &dependency, resolution, is_main, ) catch {}; } } this.drainDependencyList(); } pub fn drainDependencyList(this: *PackageManager) void { // Step 2. If there were cached dependencies, go through all of those but don't download the devDependencies for them. this.flushDependencyQueue(); if (PackageManager.verbose_install) Output.flush(); // It's only network requests here because we don't store tarballs. const count = this.network_resolve_batch.len + this.network_tarball_batch.len; this.pending_tasks += @truncate(u32, count); this.total_tasks += @truncate(u32, count); this.network_resolve_batch.push(this.network_tarball_batch); HTTP.http_thread.schedule(this.network_resolve_batch); this.network_tarball_batch = .{}; this.network_resolve_batch = .{}; } fn processDependencyList( this: *PackageManager, dep_list: TaskCallbackList, comptime Context: type, ctx: Context, comptime callbacks: anytype, ) !void { if (dep_list.items.len > 0) { var dependency_list = dep_list; var any_root = false; for (dependency_list.items) |item| { switch (item) { .dependency => |dependency_id| { const dependency = this.lockfile.buffers.dependencies.items[dependency_id]; const resolution = this.lockfile.buffers.resolutions.items[dependency_id]; try this.enqueueDependencyWithMain( dependency_id, &dependency, resolution, false, ); }, .root_dependency => |dependency_id| { const pair = this.dynamicRootDependencies().items[dependency_id]; const dependency = pair.dependency; const resolution = pair.resolution_id; try this.enqueueDependencyWithMainAndSuccessFn( dependency_id, &dependency, resolution, true, assignRootResolution, failRootResolution, ); const new_resolution_id = this.dynamicRootDependencies().items[dependency_id].resolution_id; if (new_resolution_id != pair.resolution_id) { any_root = true; } }, else => unreachable, } } if (comptime @TypeOf(callbacks.onResolve) != void) { if (any_root) { callbacks.onResolve(ctx); } } dependency_list.deinit(this.allocator); } } const CacheDir = struct { path: string, is_node_modules: bool }; pub fn fetchCacheDirectoryPath( env_loader: *DotEnv.Loader, ) CacheDir { if (env_loader.map.get("BUN_INSTALL_CACHE_DIR")) |dir| { return CacheDir{ .path = dir, .is_node_modules = false }; } if (env_loader.map.get("BUN_INSTALL")) |dir| { var parts = [_]string{ dir, "install/", "cache/" }; return CacheDir{ .path = Fs.FileSystem.instance.abs(&parts), .is_node_modules = false }; } if (env_loader.map.get("XDG_CACHE_HOME")) |dir| { var parts = [_]string{ dir, ".bun/", "install/", "cache/" }; return CacheDir{ .path = Fs.FileSystem.instance.abs(&parts), .is_node_modules = false }; } if (env_loader.map.get("HOME")) |dir| { var parts = [_]string{ dir, ".bun/", "install/", "cache/" }; return CacheDir{ .path = Fs.FileSystem.instance.abs(&parts), .is_node_modules = false }; } var fallback_parts = [_]string{"node_modules/.bun-cache"}; return CacheDir{ .is_node_modules = true, .path = Fs.FileSystem.instance.abs(&fallback_parts) }; } pub fn runTasks( manager: *PackageManager, comptime ExtractCompletionContext: type, extract_ctx: ExtractCompletionContext, comptime callbacks: anytype, comptime log_level: Options.LogLevel, ) anyerror!void { var batch = ThreadPool.Batch{}; var has_updated_this_run = false; var timestamp_this_tick: ?u32 = null; while (manager.network_channel.tryReadItem() catch null) |task_| { var task: *NetworkTask = task_; manager.pending_tasks -|= 1; switch (task.callback) { .package_manifest => |manifest_req| { const name = manifest_req.name; if (comptime log_level.showProgress()) { if (!has_updated_this_run) { manager.setNodeName(manager.downloads_node.?, name.slice(), ProgressStrings.download_emoji, true); has_updated_this_run = true; } } const response = task.http.response orelse { const err = task.http.err orelse error.HTTPError; if (@TypeOf(callbacks.onPackageManifestError) != void) { if (manager.dynamic_root_dependencies) |*root_deps| { var deps: []Dependency.Pair = root_deps.items; for (deps) |*dep| { if (strings.eqlLong(manager.lockfile.str(&dep.dependency.name), name.slice(), true)) { dep.failed = dep.failed orelse err; } } } callbacks.onPackageManifestError( extract_ctx, name.slice(), err, task.url_buf, ); } else if (comptime log_level != .silent) { const fmt = "\nerror: {s} downloading package manifest {s}\n"; const error_name: string = std.mem.span(@errorName(err)); const args = .{ error_name, name.slice() }; if (comptime log_level.showProgress()) { Output.prettyWithPrinterFn(fmt, args, Progress.log, &manager.progress); } else { Output.prettyErrorln( fmt, args, ); Output.flush(); } } continue; }; if (response.status_code > 399) { if (@TypeOf(callbacks.onPackageManifestError) != void) { const err: PackageManifestError = switch (response.status_code) { 400 => error.PackageManifestHTTP400, 401 => error.PackageManifestHTTP401, 402 => error.PackageManifestHTTP402, 403 => error.PackageManifestHTTP403, 404 => error.PackageManifestHTTP404, 405...499 => error.PackageManifestHTTP4xx, else => error.PackageManifestHTTP5xx, }; if (manager.dynamic_root_dependencies) |*root_deps| { var deps: []Dependency.Pair = root_deps.items; for (deps) |*dep| { if (strings.eql(manager.lockfile.str(&dep.dependency.name), name.slice())) { dep.failed = dep.failed orelse err; } } } callbacks.onPackageManifestError( extract_ctx, name.slice(), err, task.url_buf, ); } else { switch (response.status_code) { 404 => { if (comptime log_level != .silent) { const fmt = "\nerror: package \"{s}\" not found {s}{s} 404\n"; const args = .{ name.slice(), task.http.url.displayHostname(), task.http.url.pathname, }; if (comptime log_level.showProgress()) { Output.prettyWithPrinterFn(fmt, args, Progress.log, &manager.progress); } else { Output.prettyErrorln(fmt, args); Output.flush(); } } }, 401 => { if (comptime log_level != .silent) { const fmt = "\nerror: unauthorized \"{s}\" {s}{s} 401\n"; const args = .{ name.slice(), task.http.url.displayHostname(), task.http.url.pathname, }; if (comptime log_level.showProgress()) { Output.prettyWithPrinterFn(fmt, args, Progress.log, &manager.progress); } else { Output.prettyErrorln(fmt, args); Output.flush(); } } }, 403 => { if (comptime log_level != .silent) { const fmt = "\nerror: forbidden while loading \"{s}\" 403\n"; const args = .{ name.slice(), }; if (comptime log_level.showProgress()) { Output.prettyWithPrinterFn(fmt, args, Progress.log, &manager.progress); } else { Output.prettyErrorln(fmt, args); Output.flush(); } } }, else => { if (comptime log_level != .silent) { const fmt = "\nGET {s} - {d}\n"; const args = .{ task.http.client.url.href, response.status_code, }; if (comptime log_level.showProgress()) { Output.prettyWithPrinterFn(fmt, args, Progress.log, &manager.progress); } else { Output.prettyErrorln(fmt, args); Output.flush(); } } }, } } for (manager.package_json_updates) |*update| { if (strings.eql(update.name, name.slice())) { update.failed = true; manager.options.do.save_lockfile = false; manager.options.do.save_yarn_lock = false; manager.options.do.install_packages = false; } } continue; } if (comptime log_level.isVerbose()) { Output.prettyError(" ", .{}); Output.printElapsed(@intToFloat(f64, task.http.elapsed) / std.time.ns_per_ms); Output.prettyError("\n Downloaded {s} versions\n", .{name.slice()}); Output.flush(); } if (response.status_code == 304) { // The HTTP request was cached if (manifest_req.loaded_manifest) |manifest| { var entry = try manager.manifests.getOrPut(manager.allocator, manifest.pkg.name.hash); entry.value_ptr.* = manifest; if (timestamp_this_tick == null) { timestamp_this_tick = @truncate(u32, @intCast(u64, @max(0, std.time.timestamp()))) +| 300; } entry.value_ptr.*.pkg.public_max_age = timestamp_this_tick.?; { Npm.PackageManifest.Serializer.save(entry.value_ptr, manager.getTemporaryDirectory(), manager.getCacheDirectory()) catch {}; } var dependency_list_entry = manager.task_queue.getEntry(task.task_id).?; var dependency_list = dependency_list_entry.value_ptr.*; dependency_list_entry.value_ptr.* = .{}; try manager.processDependencyList(dependency_list, ExtractCompletionContext, extract_ctx, callbacks); continue; } } batch.push(ThreadPool.Batch.from(manager.enqueueParseNPMPackage(task.task_id, name, task))); }, .extract => |extract| { const response = task.http.response orelse { const err = task.http.err orelse error.TarballFailedToDownload; if (@TypeOf(callbacks.onPackageDownloadError) != void) { if (manager.dynamic_root_dependencies) |*root_deps| { for (root_deps.items) |*dep| { if (dep.resolution_id == extract.package_id) { dep.failed = err; } } } callbacks.onPackageDownloadError( extract_ctx, extract.package_id, extract.name.slice(), extract.resolution, err, task.url_buf, ); } else { const fmt = "\nerror: {s} downloading tarball {s}@{s}\n"; const error_name: string = std.mem.span(@errorName(err)); const args = .{ error_name, extract.name.slice(), extract.resolution.fmt(manager.lockfile.buffers.string_bytes.items) }; if (comptime log_level != .silent) { if (comptime log_level.showProgress()) { Output.prettyWithPrinterFn(fmt, args, Progress.log, &manager.progress); } else { Output.prettyErrorln(fmt, args); Output.flush(); } } } continue; }; if (response.status_code > 399) { if (@TypeOf(callbacks.onPackageDownloadError) != void) { const err = switch (response.status_code) { 400 => error.TarballHTTP400, 401 => error.TarballHTTP401, 402 => error.TarballHTTP402, 403 => error.TarballHTTP403, 404 => error.TarballHTTP404, 405...499 => error.TarballHTTP4xx, else => error.TarballHTTP5xx, }; if (manager.dynamic_root_dependencies) |*root_deps| { for (root_deps.items) |*dep| { if (dep.resolution_id == extract.package_id) { dep.failed = err; } } } callbacks.onPackageDownloadError( extract_ctx, extract.package_id, extract.name.slice(), extract.resolution, err, task.url_buf, ); } else if (comptime log_level != .silent) { const fmt = "\nGET {s} - {d}\n"; const args = .{ task.http.client.url.href, response.status_code, }; if (comptime log_level.showProgress()) { Output.prettyWithPrinterFn(fmt, args, Progress.log, &manager.progress); } else { Output.prettyErrorln( fmt, args, ); Output.flush(); } } continue; } if (comptime log_level.isVerbose()) { Output.prettyError(" ", .{}); Output.printElapsed(@floatCast(f64, @intToFloat(f64, task.http.elapsed) / std.time.ns_per_ms)); Output.prettyError(" Downloaded {s} tarball\n", .{extract.name.slice()}); Output.flush(); } if (comptime log_level.showProgress()) { if (!has_updated_this_run) { manager.setNodeName(manager.downloads_node.?, extract.name.slice(), ProgressStrings.extract_emoji, true); has_updated_this_run = true; } } batch.push(ThreadPool.Batch.from(manager.enqueueExtractNPMPackage(extract, task))); }, .binlink => {}, } } while (manager.resolve_tasks.tryReadItem() catch null) |task_| { manager.pending_tasks -= 1; var task: Task = task_; if (task.log.msgs.items.len > 0) { if (Output.enable_ansi_colors) { try task.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true); } else { try task.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false); } } switch (task.tag) { .package_manifest => { if (task.status == .fail) { const name = task.request.package_manifest.name; const err = task.err orelse error.Failed; if (@TypeOf(callbacks.onPackageManifestError) != void) { if (manager.dynamic_root_dependencies) |*root_deps| { var deps: []Dependency.Pair = root_deps.items; for (deps) |*dep| { if (strings.eql(manager.lockfile.str(&dep.dependency.name), name.slice())) { dep.failed = dep.failed orelse err; } } } callbacks.onPackageManifestError( extract_ctx, name.slice(), err, task.request.package_manifest.network.url_buf, ); } else if (comptime log_level != .silent) { const fmt = "\nrerror: {s} parsing package manifest for {s}"; const error_name: string = @errorName(err); const args = .{ error_name, name.slice() }; if (comptime log_level.showProgress()) { Output.prettyWithPrinterFn(fmt, args, Progress.log, &manager.progress); } else { Output.prettyErrorln( fmt, args, ); Output.flush(); } } continue; } const manifest = task.data.package_manifest; _ = try manager.manifests.getOrPutValue(manager.allocator, @truncate(PackageNameHash, manifest.pkg.name.hash), manifest); var dependency_list_entry = manager.task_queue.getEntry(task.id).?; var dependency_list = dependency_list_entry.value_ptr.*; dependency_list_entry.value_ptr.* = .{}; try manager.processDependencyList(dependency_list, ExtractCompletionContext, extract_ctx, callbacks); if (comptime log_level.showProgress()) { if (!has_updated_this_run) { manager.setNodeName(manager.downloads_node.?, manifest.name(), ProgressStrings.download_emoji, true); has_updated_this_run = true; } } }, .extract => { if (task.status == .fail) { const err = task.err orelse error.TarballFailedToExtract; if (@TypeOf(callbacks.onPackageDownloadError) != void) { if (manager.dynamic_root_dependencies) |*root_deps| { var deps: []Dependency.Pair = root_deps.items; for (deps) |*dep| { if (dep.resolution_id == task.request.extract.tarball.package_id) { dep.failed = dep.failed orelse err; } } } callbacks.onPackageDownloadError( extract_ctx, task.request.extract.tarball.package_id, task.request.extract.tarball.name.slice(), task.request.extract.tarball.resolution, err, task.request.extract.network.url_buf, ); } else if (comptime log_level != .silent) { const fmt = "error: {s} extracting tarball for {s}"; const error_name: string = @errorName(err); const args = .{ error_name, task.request.extract.tarball.name.slice(), }; if (comptime log_level.showProgress()) { Output.prettyWithPrinterFn(fmt, args, Progress.log, &manager.progress); } else { Output.prettyErrorln( fmt, args, ); Output.flush(); } } continue; } const package_id = task.request.extract.tarball.package_id; manager.extracted_count += 1; bun.Analytics.Features.extracted_packages = true; manager.setPreinstallState(package_id, manager.lockfile, .done); if (comptime @TypeOf(callbacks.onExtract) != void) { callbacks.onExtract(extract_ctx, package_id, comptime log_level); } if (comptime log_level.showProgress()) { if (!has_updated_this_run) { manager.setNodeName(manager.downloads_node.?, task.request.extract.tarball.name.slice(), ProgressStrings.extract_emoji, true); has_updated_this_run = true; } } }, .binlink => {}, } } manager.flushDependencyQueue(); { const count = batch.len + manager.network_resolve_batch.len + manager.network_tarball_batch.len; manager.pending_tasks += @truncate(u32, count); manager.total_tasks += @truncate(u32, count); manager.thread_pool.schedule(batch); manager.network_resolve_batch.push(manager.network_tarball_batch); HTTP.http_thread.schedule(manager.network_resolve_batch); manager.network_tarball_batch = .{}; manager.network_resolve_batch = .{}; if (comptime log_level.showProgress()) { if (comptime ExtractCompletionContext == void or (@hasField(@TypeOf(callbacks), "progress_bar") and callbacks.progress_bar == true)) { const completed_items = manager.total_tasks - manager.pending_tasks; if (completed_items != manager.downloads_node.?.unprotected_completed_items or has_updated_this_run) { manager.downloads_node.?.setCompletedItems(completed_items); manager.downloads_node.?.setEstimatedTotalItems(manager.total_tasks); } } manager.downloads_node.?.activate(); manager.progress.maybeRefresh(); } } } pub const Options = struct { log_level: LogLevel = LogLevel.default, global: bool = false, global_bin_dir: std.fs.IterableDir = std.fs.IterableDir{ .dir = .{ .fd = std.math.maxInt(std.os.fd_t) } }, explicit_global_directory: string = "", /// destination directory to link bins into // must be a variable due to global installs and bunx bin_path: stringZ = "node_modules/.bin", lockfile_path: stringZ = Lockfile.default_filename, save_lockfile_path: stringZ = Lockfile.default_filename, did_override_default_scope: bool = false, scope: Npm.Registry.Scope = undefined, registries: Npm.Registry.Map = Npm.Registry.Map{}, cache_directory: string = "", enable: Enable = .{}, do: Do = .{}, positionals: []const string = &[_]string{}, update: Update = Update{}, dry_run: bool = false, remote_package_features: Features = Features{ .optional_dependencies = true, .peer_dependencies = false, }, local_package_features: Features = Features{ .dev_dependencies = true, .peer_dependencies = false, }, // The idea here is: // 1. package has a platform-specific binary to install // 2. To prevent downloading & installing incompatible versions, they stick the "real" one in optionalDependencies // 3. The real one we want to link is in another package // 4. Therefore, we remap the "bin" specified in the real package // to the target package which is the one which is: // 1. In optionalDependencies // 2. Has a platform and/or os specified, which evaluates to not disabled native_bin_link_allowlist: []const PackageNameHash = &default_native_bin_link_allowlist, max_retry_count: u16 = 5, pub fn shouldPrintCommandName(this: *const Options) bool { return this.log_level != .silent and this.do.summary; } pub fn isBinPathInPATH(this: *const Options) bool { // must be absolute if (this.bin_path[0] != std.fs.path.sep) return false; var tokenizer = std.mem.split(bun.getenvZ("PATH") orelse "", ":"); const spanned = std.mem.span(this.bin_path); while (tokenizer.next()) |token| { if (strings.eql(token, spanned)) return true; } return false; } const default_native_bin_link_allowlist = [_]PackageNameHash{ String.Builder.stringHash("esbuild"), String.Builder.stringHash("turbo"), String.Builder.stringHash("bun"), String.Builder.stringHash("rome"), }; pub const LogLevel = enum { default, verbose, silent, default_no_progress, verbose_no_progress, pub inline fn isVerbose(this: LogLevel) bool { return switch (this) { .verbose_no_progress, .verbose => true, else => false, }; } pub inline fn showProgress(this: LogLevel) bool { return switch (this) { .default, .verbose => true, else => false, }; } }; pub const Update = struct { development: bool = false, optional: bool = false, }; pub fn openGlobalDir(explicit_global_dir: string) !std.fs.IterableDir { if (bun.getenvZ("BUN_INSTALL_GLOBAL_DIR")) |home_dir| { return try std.fs.cwd().makeOpenPathIterable(home_dir, .{}); } if (explicit_global_dir.len > 0) { return try std.fs.cwd().makeOpenPathIterable(explicit_global_dir, .{}); } if (bun.getenvZ("BUN_INSTALL")) |home_dir| { var buf: [bun.MAX_PATH_BYTES]u8 = undefined; var parts = [_]string{ "install", "global" }; var path = Path.joinAbsStringBuf(home_dir, &buf, &parts, .auto); return try std.fs.cwd().makeOpenPathIterable(path, .{}); } if (bun.getenvZ("XDG_CACHE_HOME") orelse bun.getenvZ("HOME")) |home_dir| { var buf: [bun.MAX_PATH_BYTES]u8 = undefined; var parts = [_]string{ ".bun", "install", "global" }; var path = Path.joinAbsStringBuf(home_dir, &buf, &parts, .auto); return try std.fs.cwd().makeOpenPathIterable(path, .{}); } return error.@"No global directory found"; } pub fn openGlobalBinDir(opts_: ?*const Api.BunInstall) !std.fs.IterableDir { if (bun.getenvZ("BUN_INSTALL_BIN")) |home_dir| { return try std.fs.cwd().makeOpenPathIterable(home_dir, .{}); } if (opts_) |opts| { if (opts.global_bin_dir) |home_dir| { if (home_dir.len > 0) { return try std.fs.cwd().makeOpenPathIterable(home_dir, .{}); } } } if (bun.getenvZ("BUN_INSTALL")) |home_dir| { var buf: [bun.MAX_PATH_BYTES]u8 = undefined; var parts = [_]string{ "bin", }; var path = Path.joinAbsStringBuf(home_dir, &buf, &parts, .auto); return try std.fs.cwd().makeOpenPathIterable(path, .{}); } if (bun.getenvZ("XDG_CACHE_HOME") orelse bun.getenvZ("HOME")) |home_dir| { var buf: [bun.MAX_PATH_BYTES]u8 = undefined; var parts = [_]string{ ".bun", "bin", }; var path = Path.joinAbsStringBuf(home_dir, &buf, &parts, .auto); return try std.fs.cwd().makeOpenPathIterable(path, .{}); } return error.@"Missing global bin directory: try setting $BUN_INSTALL"; } pub fn load( this: *Options, allocator: std.mem.Allocator, log: *logger.Log, env_loader: *DotEnv.Loader, cli_: ?CommandLineArguments, bun_install_: ?*Api.BunInstall, ) !void { this.save_lockfile_path = this.lockfile_path; var base = Api.NpmRegistry{ .url = "", .username = "", .password = "", .token = "", }; if (bun_install_) |bun_install| { if (bun_install.default_registry) |registry| { base = registry; } } if (base.url.len == 0) base.url = Npm.Registry.default_url; this.scope = try Npm.Registry.Scope.fromAPI("", base, allocator, env_loader); defer { this.did_override_default_scope = !strings.eqlComptime(this.scope.url.href, Npm.Registry.default_url); } if (bun_install_) |bun_install| { if (bun_install.scoped) |scoped| { for (scoped.scopes) |name, i| { var registry = scoped.registries[i]; if (registry.url.len == 0) registry.url = base.url; try this.registries.put(allocator, Npm.Registry.Scope.hash(name), try Npm.Registry.Scope.fromAPI(name, registry, allocator, env_loader)); } } if (bun_install.disable_cache orelse false) { this.enable.cache = false; } if (bun_install.disable_manifest_cache orelse false) { this.enable.manifest_cache = false; } if (bun_install.force orelse false) { this.enable.manifest_cache_control = false; this.enable.force_install = true; } if (bun_install.native_bin_links.len > 0) { var buf = try allocator.alloc(u64, bun_install.native_bin_links.len); for (bun_install.native_bin_links) |name, i| { buf[i] = String.Builder.stringHash(name); } this.native_bin_link_allowlist = buf; } if (bun_install.save_yarn_lockfile orelse false) { this.do.save_yarn_lock = true; } if (bun_install.save_lockfile) |save_lockfile| { this.do.save_lockfile = save_lockfile; this.enable.force_save_lockfile = true; } if (bun_install.save_dev) |save| { this.local_package_features.dev_dependencies = save; } if (bun_install.save_peer) |save| { this.remote_package_features.peer_dependencies = save; } if (bun_install.production) |production| { if (production) { this.local_package_features.dev_dependencies = false; this.enable.fail_early = true; this.enable.frozen_lockfile = true; this.enable.force_save_lockfile = false; } } if (bun_install.save_optional) |save| { this.remote_package_features.optional_dependencies = save; this.local_package_features.optional_dependencies = save; } if (bun_install.lockfile_path) |save| { if (save.len > 0) { this.lockfile_path = try allocator.dupeZ(u8, save); this.save_lockfile_path = this.lockfile_path; } } if (bun_install.save_lockfile_path) |save| { if (save.len > 0) { this.save_lockfile_path = try allocator.dupeZ(u8, save); } } this.explicit_global_directory = bun_install.global_dir orelse this.explicit_global_directory; } const default_disable_progress_bar: bool = brk: { if (env_loader.get("BUN_INSTALL_PROGRESS")) |prog| { break :brk strings.eqlComptime(prog, "0"); } if (env_loader.isCI()) { break :brk true; } break :brk Output.stderr_descriptor_type != .terminal; }; // technically, npm_config is case in-sensitive // load_registry: { const registry_keys = [_]string{ "BUN_CONFIG_REGISTRY", "NPM_CONFIG_REGISTRY", "npm_config_registry", }; var did_set = false; inline for (registry_keys) |registry_key| { if (!did_set) { if (env_loader.map.get(registry_key)) |registry_| { if (registry_.len > 0 and (strings.startsWith(registry_, "https://") or strings.startsWith(registry_, "http://"))) { const prev_scope = this.scope; var api_registry = std.mem.zeroes(Api.NpmRegistry); api_registry.url = registry_; api_registry.token = prev_scope.token; this.scope = try Npm.Registry.Scope.fromAPI("", api_registry, allocator, env_loader); did_set = true; // stage1 bug: break inside inline is broken // break :load_registry; } } } } } { const token_keys = [_]string{ "BUN_CONFIG_TOKEN", "NPM_CONFIG_token", "npm_config_token", }; var did_set = false; inline for (token_keys) |registry_key| { if (!did_set) { if (env_loader.map.get(registry_key)) |registry_| { if (registry_.len > 0) { this.scope.token = registry_; did_set = true; // stage1 bug: break inside inline is broken // break :load_registry; } } } } } if (cli_) |cli| { if (cli.registry.len > 0 and strings.startsWith(cli.registry, "https://") or strings.startsWith(cli.registry, "http://")) { this.scope.url = URL.parse(cli.registry); } if (cli.token.len > 0) { this.scope.token = cli.token; } if (cli.lockfile.len > 0) { this.lockfile_path = try allocator.dupeZ(u8, cli.lockfile); } } if (env_loader.map.get("BUN_CONFIG_LOCKFILE_SAVE_PATH")) |save_lockfile_path| { this.save_lockfile_path = try allocator.dupeZ(u8, save_lockfile_path); } if (env_loader.map.get("BUN_CONFIG_YARN_LOCKFILE") != null) { this.do.save_yarn_lock = true; } if (env_loader.map.get("BUN_CONFIG_HTTP_RETRY_COUNT")) |retry_count| { if (std.fmt.parseInt(i32, retry_count, 10)) |int| { this.max_retry_count = @intCast(u16, @min(@max(int, 0), 65355)); } else |_| {} } if (env_loader.map.get("BUN_CONFIG_LINK_NATIVE_BINS")) |native_packages| { const len = std.mem.count(u8, native_packages, " "); if (len > 0) { var all = try allocator.alloc(PackageNameHash, this.native_bin_link_allowlist.len + len); std.mem.copy(PackageNameHash, all, this.native_bin_link_allowlist); var remain = all[this.native_bin_link_allowlist.len..]; var splitter = std.mem.split(u8, native_packages, " "); var i: usize = 0; while (splitter.next()) |name| { remain[i] = String.Builder.stringHash(name); i += 1; } this.native_bin_link_allowlist = all; } } // if (env_loader.map.get("BUN_CONFIG_NO_DEDUPLICATE") != null) { // this.enable.deduplicate_packages = false; // } if (env_loader.map.get("BUN_CONFIG_MAX_HTTP_REQUESTS")) |max_http_requests| { load: { AsyncHTTP.max_simultaneous_requests = std.fmt.parseInt(u16, max_http_requests, 10) catch { log.addErrorFmt( null, logger.Loc.Empty, allocator, "BUN_CONFIG_MAX_HTTP_REQUESTS value \"{s}\" is not a valid integer between 1 and 65535", .{max_http_requests}, ) catch unreachable; break :load; }; if (AsyncHTTP.max_simultaneous_requests == 0) { log.addWarningFmt( null, logger.Loc.Empty, allocator, "BUN_CONFIG_MAX_HTTP_REQUESTS value must be a number between 1 and 65535", .{}, ) catch unreachable; AsyncHTTP.max_simultaneous_requests = 255; } } } if (env_loader.map.get("BUN_CONFIG_SKIP_SAVE_LOCKFILE")) |check_bool| { this.do.save_lockfile = strings.eqlComptime(check_bool, "0"); } if (env_loader.map.get("BUN_CONFIG_SKIP_LOAD_LOCKFILE")) |check_bool| { this.do.load_lockfile = strings.eqlComptime(check_bool, "0"); } if (env_loader.map.get("BUN_CONFIG_SKIP_INSTALL_PACKAGES")) |check_bool| { this.do.install_packages = strings.eqlComptime(check_bool, "0"); } if (env_loader.map.get("BUN_CONFIG_NO_VERIFY")) |check_bool| { this.do.verify_integrity = !strings.eqlComptime(check_bool, "0"); } if (cli_) |cli| { if (cli.no_save) { this.do.save_lockfile = false; this.do.write_package_json = false; } if (cli.dry_run) { this.do.install_packages = false; this.dry_run = true; this.do.write_package_json = false; this.do.save_lockfile = false; } if (cli.no_summary) { this.do.summary = false; } if (cli.no_cache) { this.enable.manifest_cache = false; this.enable.manifest_cache_control = false; } // if (cli.no_dedupe) { // this.enable.deduplicate_packages = false; // } if (cli.omit.dev) { this.local_package_features.dev_dependencies = false; } if (cli.global or cli.ignore_scripts) { this.do.run_scripts = false; } this.local_package_features.optional_dependencies = !cli.omit.optional; const disable_progress_bar = default_disable_progress_bar or cli.no_progress; if (cli.verbose) { this.log_level = if (disable_progress_bar) LogLevel.verbose_no_progress else LogLevel.verbose; PackageManager.verbose_install = true; } else if (cli.silent) { this.log_level = .silent; PackageManager.verbose_install = false; } else { this.log_level = if (disable_progress_bar) LogLevel.default_no_progress else LogLevel.default; PackageManager.verbose_install = false; } if (cli.no_verify) { this.do.verify_integrity = false; } if (cli.yarn) { this.do.save_yarn_lock = true; } if (cli.link_native_bins.len > 0) { var all = try allocator.alloc(PackageNameHash, this.native_bin_link_allowlist.len + cli.link_native_bins.len); std.mem.copy(PackageNameHash, all, this.native_bin_link_allowlist); var remain = all[this.native_bin_link_allowlist.len..]; for (cli.link_native_bins) |name, i| { remain[i] = String.Builder.stringHash(name); } this.native_bin_link_allowlist = all; } if (cli.backend) |backend| { PackageInstall.supported_method = backend; } if (cli.positionals.len > 0) { this.positionals = cli.positionals; } if (cli.production) { this.local_package_features.dev_dependencies = false; this.enable.fail_early = true; this.enable.frozen_lockfile = true; } if (cli.force) { this.enable.manifest_cache_control = false; this.enable.force_install = true; this.enable.force_save_lockfile = true; } this.update.development = cli.development; if (!this.update.development) this.update.optional = cli.optional; } else { this.log_level = if (default_disable_progress_bar) LogLevel.default_no_progress else LogLevel.default; PackageManager.verbose_install = false; } } pub const Do = struct { save_lockfile: bool = true, load_lockfile: bool = true, install_packages: bool = true, write_package_json: bool = true, run_scripts: bool = true, save_yarn_lock: bool = false, print_meta_hash_string: bool = false, verify_integrity: bool = true, summary: bool = true, }; pub const Enable = struct { manifest_cache: bool = true, manifest_cache_control: bool = true, cache: bool = true, fail_early: bool = false, frozen_lockfile: bool = false, /// Disabled because it doesn't actually reduce the number of packages we end up installing /// Probably need to be a little smarter deduplicate_packages: bool = false, // Don't save the lockfile unless there were actual changes // unless... force_save_lockfile: bool = false, force_install: bool = false, }; }; const ProgressStrings = struct { pub const download_no_emoji_ = "Resolving"; const download_no_emoji: string = download_no_emoji_ ++ "\n"; const download_with_emoji: string = download_emoji ++ download_no_emoji_; pub const download_emoji: string = " 🔍 "; pub const extract_no_emoji_ = "Resolving & extracting"; const extract_no_emoji: string = extract_no_emoji_ ++ "\n"; const extract_with_emoji: string = extract_emoji ++ extract_no_emoji_; pub const extract_emoji: string = " 🚚 "; pub const install_no_emoji_ = "Installing"; const install_no_emoji: string = install_no_emoji_ ++ "\n"; const install_with_emoji: string = install_emoji ++ install_no_emoji_; pub const install_emoji: string = " 📦 "; pub const save_no_emoji_ = "Saving lockfile"; const save_no_emoji: string = save_no_emoji_; const save_with_emoji: string = save_emoji ++ save_no_emoji_; pub const save_emoji: string = " 🔒 "; pub inline fn download() string { return if (Output.isEmojiEnabled()) download_with_emoji else download_no_emoji; } pub inline fn save() string { return if (Output.isEmojiEnabled()) save_with_emoji else save_no_emoji; } pub inline fn extract() string { return if (Output.isEmojiEnabled()) extract_with_emoji else extract_no_emoji; } pub inline fn install() string { return if (Output.isEmojiEnabled()) install_with_emoji else install_no_emoji; } }; const PackageJSONEditor = struct { pub fn edit( allocator: std.mem.Allocator, updates: []UpdateRequest, current_package_json: *JSAst.Expr, dependency_list: string, ) !void { const G = JSAst.G; var remaining: usize = updates.len; // There are three possible scenarios here // 1. There is no "dependencies" (or equivalent list) or it is empty // 2. There is a "dependencies" (or equivalent list), but the package name already exists in a separate list // 3. There is a "dependencies" (or equivalent list), and the package name exists in multiple lists ast_modifier: { // Try to use the existing spot in the dependencies list if possible for (updates) |update, i| { outer: for (dependency_lists_to_check) |list| { if (current_package_json.asProperty(list)) |query| { if (query.expr.data == .e_object) { if (query.expr.asProperty(update.name)) |value| { if (value.expr.data == .e_string) { updates[i].e_string = value.expr.data.e_string; remaining -= 1; } break :outer; } } } } } if (remaining == 0) break :ast_modifier; var dependencies: []G.Property = &[_]G.Property{}; if (current_package_json.asProperty(dependency_list)) |query| { if (query.expr.data == .e_object) { dependencies = query.expr.data.e_object.properties.slice(); } } var new_dependencies = try allocator.alloc(G.Property, dependencies.len + remaining); std.mem.copy(G.Property, new_dependencies, dependencies); std.mem.set(G.Property, new_dependencies[dependencies.len..], G.Property{}); outer: for (updates) |update, j| { if (update.e_string != null) continue; var k: usize = 0; while (k < new_dependencies.len) : (k += 1) { if (new_dependencies[k].key == null) { new_dependencies[k].key = JSAst.Expr.init( JSAst.E.String, JSAst.E.String{ .data = update.name, }, logger.Loc.Empty, ); new_dependencies[k].value = JSAst.Expr.init( JSAst.E.String, JSAst.E.String{ // we set it later .data = "", }, logger.Loc.Empty, ); updates[j].e_string = new_dependencies[k].value.?.data.e_string; continue :outer; } // This actually is a duplicate // like "react" appearing in both "dependencies" and "optionalDependencies" // For this case, we'll just swap remove it if (new_dependencies[k].key.?.data.e_string.eql(string, update.name)) { if (new_dependencies.len > 1) { new_dependencies[k] = new_dependencies[new_dependencies.len - 1]; new_dependencies = new_dependencies[0 .. new_dependencies.len - 1]; } else { new_dependencies = &[_]G.Property{}; } } } } var needs_new_dependency_list = true; const dependencies_object: JSAst.Expr = brk: { if (current_package_json.asProperty(dependency_list)) |query| { if (query.expr.data == .e_object) { needs_new_dependency_list = false; break :brk query.expr; } } break :brk JSAst.Expr.init( JSAst.E.Object, JSAst.E.Object{ .properties = JSAst.G.Property.List.init(new_dependencies), }, logger.Loc.Empty, ); }; dependencies_object.data.e_object.properties = JSAst.G.Property.List.init(new_dependencies); if (new_dependencies.len > 1) dependencies_object.data.e_object.alphabetizeProperties(); if (current_package_json.data != .e_object or current_package_json.data.e_object.properties.len == 0) { var root_properties = try allocator.alloc(JSAst.G.Property, 1); root_properties[0] = JSAst.G.Property{ .key = JSAst.Expr.init( JSAst.E.String, JSAst.E.String{ .data = dependency_list, }, logger.Loc.Empty, ), .value = dependencies_object, }; current_package_json.* = JSAst.Expr.init(JSAst.E.Object, JSAst.E.Object{ .properties = JSAst.G.Property.List.init(root_properties) }, logger.Loc.Empty); } else if (needs_new_dependency_list) { var root_properties = try allocator.alloc(JSAst.G.Property, current_package_json.data.e_object.properties.len + 1); std.mem.copy(JSAst.G.Property, root_properties, current_package_json.data.e_object.properties.slice()); root_properties[root_properties.len - 1] = .{ .key = JSAst.Expr.init( JSAst.E.String, JSAst.E.String{ .data = dependency_list, }, logger.Loc.Empty, ), .value = dependencies_object, }; current_package_json.* = JSAst.Expr.init( JSAst.E.Object, JSAst.E.Object{ .properties = JSAst.G.Property.List.init(root_properties), }, logger.Loc.Empty, ); } } for (updates) |*update| { var str = update.e_string.?; if (update.version.tag == .uninitialized) { str.data = latest; } else { str.data = update.version.literal.slice(update.version_buf); } } } }; pub fn init( ctx: Command.Context, package_json_file_: ?std.fs.File, comptime params: []const ParamType, ) !*PackageManager { return initMaybeInstall(ctx, package_json_file_, params, false); } pub fn initMaybeInstall( ctx: Command.Context, package_json_file_: ?std.fs.File, comptime params: []const ParamType, comptime is_install: bool, ) !*PackageManager { var _ctx = ctx; var cli = try CommandLineArguments.parse(ctx.allocator, params, &_ctx); if (comptime is_install) { if (cli.positionals.len > 1) { return error.SwitchToBunAdd; } } return try initWithCLI(_ctx, package_json_file_, cli); } pub fn initWithCLI( ctx: Command.Context, package_json_file_: ?std.fs.File, cli: CommandLineArguments, ) !*PackageManager { // assume that spawning a thread will take a lil so we do that asap try HTTP.HTTPThread.init(); if (cli.global) { var explicit_global_dir: string = ""; if (ctx.install) |opts| { explicit_global_dir = opts.global_dir orelse explicit_global_dir; } var global_dir = try Options.openGlobalDir(explicit_global_dir); try global_dir.dir.setAsCwd(); } var fs = try Fs.FileSystem.init1(ctx.allocator, null); var original_cwd = std.mem.trimRight(u8, fs.top_level_dir, "/"); std.mem.copy(u8, &cwd_buf, original_cwd); // Step 1. Find the nearest package.json directory // // We will walk up from the cwd, calling chdir on each directory until we find a package.json // If we fail to find one, we will report an error saying no packages to install var package_json_file: std.fs.File = undefined; if (package_json_file_) |file| { package_json_file = file; } else { // can't use orelse due to a stage1 bug package_json_file = std.fs.cwd().openFileZ("package.json", .{ .mode = .read_write }) catch brk: { var this_cwd = original_cwd; outer: while (std.fs.path.dirname(this_cwd)) |parent| { cwd_buf[parent.len] = 0; var chdir = cwd_buf[0..parent.len :0]; std.os.chdirZ(chdir) catch |err| { Output.prettyErrorln("Error {s} while chdir - {s}", .{ @errorName(err), std.mem.span(chdir) }); Output.flush(); return err; }; break :brk std.fs.cwd().openFileZ("package.json", .{ .mode = .read_write }) catch { this_cwd = parent; continue :outer; }; } std.mem.copy(u8, &cwd_buf, original_cwd); cwd_buf[original_cwd.len] = 0; var real_cwd: [:0]u8 = cwd_buf[0..original_cwd.len :0]; std.os.chdirZ(real_cwd) catch {}; return error.MissingPackageJSON; }; } fs.top_level_dir = try std.os.getcwd(&cwd_buf); cwd_buf[fs.top_level_dir.len] = '/'; cwd_buf[fs.top_level_dir.len + 1] = 0; fs.top_level_dir = cwd_buf[0 .. fs.top_level_dir.len + 1]; std.mem.copy(u8, &package_json_cwd_buf, fs.top_level_dir); std.mem.copy(u8, package_json_cwd_buf[fs.top_level_dir.len..], "package.json"); var entries_option = try fs.fs.readDirectory(fs.top_level_dir, null); var options = Options{ .global = cli.global, }; var env_loader: *DotEnv.Loader = brk: { var map = try ctx.allocator.create(DotEnv.Map); map.* = DotEnv.Map.init(ctx.allocator); var loader = try ctx.allocator.create(DotEnv.Loader); loader.* = DotEnv.Loader.init(map, ctx.allocator); break :brk loader; }; env_loader.loadProcess(); try env_loader.load(&fs.fs, &entries_option.entries, false); if (env_loader.map.get("BUN_INSTALL_VERBOSE") != null) { PackageManager.verbose_install = true; } if (PackageManager.verbose_install) { Output.prettyErrorln("Cache Dir: {s}", .{options.cache_directory}); Output.flush(); } var cpu_count = @truncate(u32, ((try std.Thread.getCpuCount()) + 1)); if (env_loader.map.get("GOMAXPROCS")) |max_procs| { if (std.fmt.parseInt(u32, max_procs, 10)) |cpu_count_| { cpu_count = @min(cpu_count, cpu_count_); } else |_| {} } var manager = &instance; // var progress = Progress{}; // var node = progress.start(name: []const u8, estimated_total_items: usize) manager.* = PackageManager{ .options = options, .network_task_fifo = NetworkQueue.init(), .env_loader = env_loader, .allocator = ctx.allocator, .log = ctx.log, .root_dir = &entries_option.entries, .env = env_loader, .cpu_count = cpu_count, .thread_pool = ThreadPool.init(.{ .max_threads = cpu_count, }), .resolve_tasks = TaskChannel.init(), .lockfile = undefined, .root_package_json_file = package_json_file, .waiter = try Waker.init(ctx.allocator), // .progress }; manager.lockfile = try ctx.allocator.create(Lockfile); if (!manager.options.enable.cache) { manager.options.enable.manifest_cache = false; manager.options.enable.manifest_cache_control = false; } if (env_loader.map.get("BUN_MANIFEST_CACHE")) |manifest_cache| { if (strings.eqlComptime(manifest_cache, "1")) { manager.options.enable.manifest_cache = true; manager.options.enable.manifest_cache_control = false; } else if (strings.eqlComptime(manifest_cache, "2")) { manager.options.enable.manifest_cache = true; manager.options.enable.manifest_cache_control = true; } else { manager.options.enable.manifest_cache = false; manager.options.enable.manifest_cache_control = false; } } try manager.options.load( ctx.allocator, ctx.log, env_loader, cli, ctx.install, ); manager.timestamp_for_manifest_cache_control = @truncate(u32, @intCast(u64, @max(std.time.timestamp(), 0))); return manager; } pub fn initWithRuntime( log: *logger.Log, bun_install: ?*Api.BunInstall, allocator: std.mem.Allocator, cli: CommandLineArguments, env_loader: *DotEnv.Loader, ) !*PackageManager { if (env_loader.map.get("BUN_INSTALL_VERBOSE") != null) { PackageManager.verbose_install = true; } var cpu_count = @truncate(u32, ((try std.Thread.getCpuCount()) + 1)); if (env_loader.map.get("GOMAXPROCS")) |max_procs| { if (std.fmt.parseInt(u32, max_procs, 10)) |cpu_count_| { cpu_count = @min(cpu_count, cpu_count_); } else |_| {} } var manager = &instance; var root_dir = try Fs.FileSystem.instance.fs.readDirectory( Fs.FileSystem.instance.top_level_dir, null, ); // var progress = Progress{}; // var node = progress.start(name: []const u8, estimated_total_items: usize) manager.* = PackageManager{ .options = .{}, .network_task_fifo = NetworkQueue.init(), .env_loader = env_loader, .allocator = allocator, .log = log, .root_dir = &root_dir.entries, .env = env_loader, .cpu_count = cpu_count, .thread_pool = ThreadPool.init(.{ .max_threads = cpu_count, }), .resolve_tasks = TaskChannel.init(), .lockfile = undefined, .root_package_json_file = undefined, .waiter = try Waker.init(allocator), }; manager.lockfile = try allocator.create(Lockfile); if (Output.enable_ansi_colors_stderr) { manager.progress = Progress{}; manager.progress.supports_ansi_escape_codes = Output.enable_ansi_colors_stderr; manager.root_progress_node = manager.progress.start("", 0); manager.root_download_node = manager.root_progress_node.start(ProgressStrings.download(), 0); } if (!manager.options.enable.cache) { manager.options.enable.manifest_cache = false; manager.options.enable.manifest_cache_control = false; } if (env_loader.map.get("BUN_MANIFEST_CACHE")) |manifest_cache| { if (strings.eqlComptime(manifest_cache, "1")) { manager.options.enable.manifest_cache = true; manager.options.enable.manifest_cache_control = false; } else if (strings.eqlComptime(manifest_cache, "2")) { manager.options.enable.manifest_cache = true; manager.options.enable.manifest_cache_control = true; } else { manager.options.enable.manifest_cache = false; manager.options.enable.manifest_cache_control = false; } } try manager.options.load( allocator, log, env_loader, cli, bun_install, ); manager.timestamp_for_manifest_cache_control = @truncate( u32, @intCast( u64, @max( std.time.timestamp(), 0, ), ), // When using "bun install", we check for updates with a 300 second cache. // When using bun, we only do staleness checks once per day ) -| std.time.s_per_day; manager.lockfile = brk: { var buf: [bun.MAX_PATH_BYTES]u8 = undefined; if (root_dir.entries.hasComptimeQuery("bun.lockb")) { var parts = [_]string{ "./bun.lockb", }; var lockfile_path = Path.joinAbsStringBuf( Fs.FileSystem.instance.top_level_dir, &buf, &parts, .auto, ); buf[lockfile_path.len] = 0; var lockfile_path_z = std.meta.assumeSentinel(buf[0..lockfile_path.len], 0); const result = manager.lockfile.loadFromDisk( allocator, log, lockfile_path_z, ); if (result == .ok) { break :brk result.ok; } } try manager.lockfile.initEmpty(allocator); break :brk manager.lockfile; }; return manager; } pub inline fn add( ctx: Command.Context, ) !void { try updatePackageJSONAndInstall(ctx, .add, &add_params); } pub inline fn remove( ctx: Command.Context, ) !void { try updatePackageJSONAndInstall(ctx, .remove, &remove_params); } pub inline fn link( ctx: Command.Context, ) !void { var manager = PackageManager.init(ctx, null, &link_params) catch |err| brk: { switch (err) { error.MissingPackageJSON => { var package_json_file = std.fs.cwd().createFileZ("package.json", .{ .read = true }) catch |err2| { Output.prettyErrorln("error: {s} create package.json", .{@errorName(err2)}); Global.crash(); }; try package_json_file.pwriteAll("{\"dependencies\": {}}", 0); break :brk try PackageManager.init(ctx, package_json_file, &link_params); }, else => return err, } unreachable; }; if (manager.options.shouldPrintCommandName()) { Output.prettyErrorln("bun link v" ++ Global.package_json_version_with_sha ++ "\n", .{}); Output.flush(); } if (manager.options.positionals.len == 1) { // bun link var lockfile: Lockfile = undefined; var name: string = ""; var package: Lockfile.Package = Lockfile.Package{}; // Step 1. parse the nearest package.json file { var current_package_json_stat = try manager.root_package_json_file.stat(); var current_package_json_buf = try ctx.allocator.alloc(u8, current_package_json_stat.size + 64); const current_package_json_contents_len = try manager.root_package_json_file.preadAll( current_package_json_buf, 0, ); const package_json_source = logger.Source.initPathString( package_json_cwd_buf[0 .. FileSystem.instance.top_level_dir.len + "package.json".len], current_package_json_buf[0..current_package_json_contents_len], ); try lockfile.initEmpty(ctx.allocator); try Lockfile.Package.parseMain(&lockfile, &package, ctx.allocator, manager.log, package_json_source, Features.folder); name = lockfile.str(&package.name); if (name.len == 0) { if (manager.options.log_level != .silent) Output.prettyErrorln("error: package.json missing \"name\" in \"{s}\"", .{package_json_source.path.text}); Global.crash(); } else if (!strings.isNPMPackageName(name)) { if (manager.options.log_level != .silent) Output.prettyErrorln("error: invalid package.json name \"{s}\" in \"{s}\"", .{ name, package_json_source.path.text, }); Global.crash(); } } // Step 2. Setup the global directory var node_modules: std.fs.IterableDir = brk: { Bin.Linker.umask = C.umask(0); var explicit_global_dir: string = ""; if (ctx.install) |install_| { explicit_global_dir = install_.global_dir orelse explicit_global_dir; } manager.global_dir = try Options.openGlobalDir(explicit_global_dir); try manager.setupGlobalDir(&ctx); break :brk manager.global_dir.?.dir.makeOpenPathIterable("node_modules", .{}) catch |err| { if (manager.options.log_level != .silent) Output.prettyErrorln("error: failed to create node_modules in global dir due to error {s}", .{@errorName(err)}); Global.crash(); }; }; // Step 3a. symlink to the node_modules folder { // delete it if it exists node_modules.dir.deleteTree(name) catch {}; // create the symlink node_modules.dir.symLink(Fs.FileSystem.instance.topLevelDirWithoutTrailingSlash(), name, .{ .is_directory = true }) catch |err| { if (manager.options.log_level != .silent) Output.prettyErrorln("error: failed to create symlink to node_modules in global dir due to error {s}", .{@errorName(err)}); Global.crash(); }; } // Step 3b. Link any global bins if (package.bin.tag != .none) { var bin_linker = Bin.Linker{ .bin = package.bin, .package_installed_node_modules = node_modules.dir.fd, .global_bin_path = manager.options.bin_path, .global_bin_dir = manager.options.global_bin_dir.dir, // .destination_dir_subpath = destination_dir_subpath, .root_node_modules_folder = node_modules.dir.fd, .package_name = strings.StringOrTinyString.init(name), .string_buf = lockfile.buffers.string_bytes.items, .extern_string_buf = lockfile.buffers.extern_strings.items, }; bin_linker.link(true); if (bin_linker.err) |err| { if (manager.options.log_level != .silent) Output.prettyErrorln("error: failed to link bin due to error {s}", .{@errorName(err)}); Global.crash(); } } Output.flush(); // Done if (manager.options.log_level != .silent) Output.prettyln( \\Success! Registered \"{[name]s}\" \\ \\To use {[name]s} in a project, run: \\ bun link {[name]s} \\ \\Or add it in dependencies in your package.json file: \\ "{[name]s}": "link:{[name]s}" \\ , .{ .name = name, }, ); Output.flush(); Global.exit(0); } else { // bun link lodash switch (manager.options.log_level) { .default => try updatePackageJSONAndInstallWithManager(ctx, manager, .link, .default), .verbose => try updatePackageJSONAndInstallWithManager(ctx, manager, .link, .verbose), .silent => try updatePackageJSONAndInstallWithManager(ctx, manager, .link, .silent), .default_no_progress => try updatePackageJSONAndInstallWithManager(ctx, manager, .link, .default_no_progress), .verbose_no_progress => try updatePackageJSONAndInstallWithManager(ctx, manager, .link, .verbose_no_progress), } } } pub inline fn unlink( ctx: Command.Context, ) !void { var manager = PackageManager.init(ctx, null, &unlink_params) catch |err| brk: { switch (err) { error.MissingPackageJSON => { var package_json_file = std.fs.cwd().createFileZ("package.json", .{ .read = true }) catch |err2| { Output.prettyErrorln("error: {s} create package.json", .{@errorName(err2)}); Global.crash(); }; try package_json_file.pwriteAll("{\"dependencies\": {}}", 0); break :brk try PackageManager.init(ctx, package_json_file, &unlink_params); }, else => return err, } unreachable; }; if (manager.options.shouldPrintCommandName()) { Output.prettyErrorln("bun unlink v" ++ Global.package_json_version_with_sha ++ "\n", .{}); Output.flush(); } if (manager.options.positionals.len == 1) { // bun unlink var lockfile: Lockfile = undefined; var name: string = ""; var package: Lockfile.Package = Lockfile.Package{}; // Step 1. parse the nearest package.json file { var current_package_json_stat = try manager.root_package_json_file.stat(); var current_package_json_buf = try ctx.allocator.alloc(u8, current_package_json_stat.size + 64); const current_package_json_contents_len = try manager.root_package_json_file.preadAll( current_package_json_buf, 0, ); const package_json_source = logger.Source.initPathString( package_json_cwd_buf[0 .. FileSystem.instance.top_level_dir.len + "package.json".len], current_package_json_buf[0..current_package_json_contents_len], ); try lockfile.initEmpty(ctx.allocator); try Lockfile.Package.parseMain(&lockfile, &package, ctx.allocator, manager.log, package_json_source, Features.folder); name = lockfile.str(&package.name); if (name.len == 0) { if (manager.options.log_level != .silent) Output.prettyErrorln("error: package.json missing \"name\" in \"{s}\"", .{package_json_source.path.text}); Global.crash(); } else if (!strings.isNPMPackageName(name)) { if (manager.options.log_level != .silent) Output.prettyErrorln("error: invalid package.json name \"{s}\" in \"{s}\"", .{ name, package_json_source.path.text, }); Global.crash(); } } switch (Syscall.lstat(Path.joinAbsStringZ(try manager.globalLinkDirPath(), &.{name}, .auto))) { .result => |stat| { if (!std.os.S.ISLNK(stat.mode)) { Output.prettyErrorln("success: package \"{s}\" is not globally linked, so there's nothing to do.", .{name}); Global.exit(0); } }, .err => { Output.prettyErrorln("success: package \"{s}\" is not globally linked, so there's nothing to do.", .{name}); Global.exit(0); }, } // Step 2. Setup the global directory var node_modules: std.fs.IterableDir = brk: { Bin.Linker.umask = C.umask(0); var explicit_global_dir: string = ""; if (ctx.install) |install_| { explicit_global_dir = install_.global_dir orelse explicit_global_dir; } manager.global_dir = try Options.openGlobalDir(explicit_global_dir); try manager.setupGlobalDir(&ctx); break :brk manager.global_dir.?.dir.makeOpenPathIterable("node_modules", .{}) catch |err| { if (manager.options.log_level != .silent) Output.prettyErrorln("error: failed to create node_modules in global dir due to error {s}", .{@errorName(err)}); Global.crash(); }; }; // Step 3b. Link any global bins if (package.bin.tag != .none) { var bin_linker = Bin.Linker{ .bin = package.bin, .package_installed_node_modules = node_modules.dir.fd, .global_bin_path = manager.options.bin_path, .global_bin_dir = manager.options.global_bin_dir.dir, // .destination_dir_subpath = destination_dir_subpath, .root_node_modules_folder = node_modules.dir.fd, .package_name = strings.StringOrTinyString.init(name), .string_buf = lockfile.buffers.string_bytes.items, .extern_string_buf = lockfile.buffers.extern_strings.items, }; bin_linker.unlink(true); } // delete it if it exists node_modules.dir.deleteTree(name) catch |err| { if (manager.options.log_level != .silent) Output.prettyErrorln("error: failed to unlink package in global dir due to error {s}", .{@errorName(err)}); Global.crash(); }; Output.prettyln("success: unlinked package \"{s}\"", .{name}); Global.exit(0); } else { Output.prettyln("error: bun unlink {{packageName}} not implemented yet", .{}); Global.exit(1); } } const ParamType = clap.Param(clap.Help); const platform_specific_backend_label = if (Environment.isMac) "Possible values: \"clonefile\" (default), \"hardlink\", \"symlink\", \"copyfile\"" else "Possible values: \"hardlink\" (default), \"symlink\", \"copyfile\""; pub const install_params_ = [_]ParamType{ clap.parseParam("-c, --config ? Load config (bunfig.toml)") catch unreachable, clap.parseParam("-y, --yarn Write a yarn.lock file (yarn v1)") catch unreachable, clap.parseParam("-p, --production Don't install devDependencies") catch unreachable, clap.parseParam("--no-save Don't save a lockfile") catch unreachable, clap.parseParam("--dry-run Don't install anything") catch unreachable, clap.parseParam("--lockfile Store & load a lockfile at a specific filepath") catch unreachable, clap.parseParam("-f, --force Always request the latest versions from the registry & reinstall all dependencies") catch unreachable, clap.parseParam("--cache-dir Store & load cached data from a specific directory path") catch unreachable, clap.parseParam("--no-cache Ignore manifest cache entirely") catch unreachable, clap.parseParam("--silent Don't log anything") catch unreachable, clap.parseParam("--verbose Excessively verbose logging") catch unreachable, clap.parseParam("--no-progress Disable the progress bar") catch unreachable, clap.parseParam("--no-summary Don't print a summary") catch unreachable, clap.parseParam("--no-verify Skip verifying integrity of newly downloaded packages") catch unreachable, clap.parseParam("--ignore-scripts Skip lifecycle scripts in the project's package.json (dependency scripts are never run)") catch unreachable, clap.parseParam("-g, --global Install globally") catch unreachable, clap.parseParam("--cwd Set a specific cwd") catch unreachable, clap.parseParam("--backend Platform-specific optimizations for installing dependencies. " ++ platform_specific_backend_label) catch unreachable, clap.parseParam("--link-native-bins ... Link \"bin\" from a matching platform-specific \"optionalDependencies\" instead. Default: esbuild, turbo") catch unreachable, // clap.parseParam("--omit ... Skip installing dependencies of a certain type. \"dev\", \"optional\", or \"peer\"") catch unreachable, // clap.parseParam("--no-dedupe Disable automatic downgrading of dependencies that would otherwise cause unnecessary duplicate package versions ($BUN_CONFIG_NO_DEDUPLICATE)") catch unreachable, clap.parseParam("--help Print this help menu") catch unreachable, }; pub const install_params = install_params_ ++ [_]ParamType{ clap.parseParam(" ... ") catch unreachable, }; pub const add_params = install_params_ ++ [_]ParamType{ clap.parseParam("-d, --development Add dependency to \"devDependencies\"") catch unreachable, clap.parseParam("--optional Add dependency to \"optionalDependencies\"") catch unreachable, clap.parseParam(" ... \"name\" or \"name@version\" of packages to install") catch unreachable, }; pub const remove_params = install_params_ ++ [_]ParamType{ clap.parseParam(" ... \"name\" of packages to remove from package.json") catch unreachable, }; pub const link_params = install_params_ ++ [_]ParamType{ clap.parseParam("--save Save to package.json") catch unreachable, clap.parseParam(" ... \"name\" install package as a link") catch unreachable, }; pub const unlink_params = install_params_ ++ [_]ParamType{ clap.parseParam("--save Save to package.json") catch unreachable, clap.parseParam(" ... \"name\" uninstall package as a link") catch unreachable, }; pub const CommandLineArguments = struct { registry: string = "", cache_dir: string = "", lockfile: string = "", token: string = "", global: bool = false, config: ?string = null, backend: ?PackageInstall.Method = null, positionals: []const string = &[_]string{}, yarn: bool = false, production: bool = false, no_save: bool = false, dry_run: bool = false, force: bool = false, no_dedupe: bool = false, no_cache: bool = false, silent: bool = false, verbose: bool = false, no_progress: bool = false, no_verify: bool = false, ignore_scripts: bool = false, no_summary: bool = false, link_native_bins: []const string = &[_]string{}, development: bool = false, optional: bool = false, no_optional: bool = false, omit: Omit = Omit{}, const Omit = struct { dev: bool = false, optional: bool = true, peer: bool = false, pub inline fn toFeatures(this: Omit) Features { return Features{ .dev_dependencies = this.dev, .optional_dependencies = this.optional, .peer_dependencies = this.peer, }; } }; pub fn parse( allocator: std.mem.Allocator, comptime params: []const ParamType, ctx: *Command.Context, ) !CommandLineArguments { var diag = clap.Diagnostic{}; var args = clap.parse(clap.Help, params, .{ .diagnostic = &diag, .allocator = allocator, }) catch |err| { clap.help(Output.errorWriter(), params) catch {}; Output.errorWriter().writeAll("\n") catch {}; diag.report(Output.errorWriter(), err) catch {}; return err; }; if (args.flag("--help")) { Output.prettyln("\nbun (package manager) flags:\n\n", .{}); Output.flush(); clap.help(Output.writer(), params) catch {}; Global.exit(0); } var cli = CommandLineArguments{}; cli.yarn = args.flag("--yarn"); cli.production = args.flag("--production"); cli.no_save = args.flag("--no-save"); cli.no_progress = args.flag("--no-progress"); cli.dry_run = args.flag("--dry-run"); cli.global = args.flag("--global"); cli.force = args.flag("--force"); cli.no_verify = args.flag("--no-verify"); // cli.no_dedupe = args.flag("--no-dedupe"); cli.no_cache = args.flag("--no-cache"); cli.silent = args.flag("--silent"); cli.verbose = args.flag("--verbose"); cli.ignore_scripts = args.flag("--ignore-scripts"); cli.no_summary = args.flag("--no-summary"); if (comptime @TypeOf(args).hasFlag("--save")) { cli.no_save = true; if (args.flag("--save")) { cli.no_save = false; } } if (args.option("--config")) |opt| { cli.config = opt; } try BunArguments.loadConfig(allocator, cli.config, ctx, .InstallCommand); cli.link_native_bins = args.options("--link-native-bins"); if (comptime params.len == add_params.len) { cli.development = args.flag("--development"); cli.optional = args.flag("--optional"); } // for (args.options("--omit")) |omit| { // if (strings.eqlComptime(omit, "dev")) { // cli.omit.dev = true; // } else if (strings.eqlComptime(omit, "optional")) { // cli.omit.optional = true; // } else if (strings.eqlComptime(omit, "peer")) { // cli.omit.peer = true; // } else { // Output.prettyErrorln("error: Invalid argument \"--omit\" must be one of \"dev\", \"optional\", or \"peer\". ", .{}); // Global.exit(1); // } // } if (args.option("--lockfile")) |lockfile| { cli.lockfile = lockfile; } if (args.option("--cwd")) |cwd_| { var buf: [bun.MAX_PATH_BYTES]u8 = undefined; var buf2: [bun.MAX_PATH_BYTES]u8 = undefined; var final_path: [:0]u8 = undefined; if (cwd_.len > 0 and cwd_[0] == '.') { var cwd = try std.os.getcwd(&buf); var parts = [_]string{cwd_}; var path_ = Path.joinAbsStringBuf(cwd, &buf2, &parts, .auto); buf2[path_.len] = 0; final_path = buf2[0..path_.len :0]; } else { std.mem.copy(u8, &buf, cwd_); buf[cwd_.len] = 0; final_path = buf[0..cwd_.len :0]; } try std.os.chdirZ(final_path); } const specified_backend: ?PackageInstall.Method = brk: { if (args.option("--backend")) |backend_| { break :brk PackageInstall.Method.map.get(backend_); } break :brk null; }; if (specified_backend) |backend| { if (backend.isSupported()) { cli.backend = backend; } } cli.positionals = args.positionals(); return cli; } }; const latest: string = "latest"; pub const UpdateRequest = struct { name: string = "", name_hash: PackageNameHash = 0, resolved_version_buf: string = "", version: Dependency.Version = Dependency.Version{}, version_buf: []const u8 = "", missing_version: bool = false, failed: bool = false, // This must be cloned to handle when the AST store resets e_string: ?*JSAst.E.String = null, pub const Array = std.BoundedArray(UpdateRequest, 64); pub fn parse( allocator: std.mem.Allocator, log: *logger.Log, positionals: []const string, update_requests: *Array, op: Lockfile.Package.Diff.Op, ) []UpdateRequest { // first one is always either: // add // remove outer: for (positionals) |positional| { var request = UpdateRequest{ .name = positional, }; var unscoped_name = positional; request.name = unscoped_name; // request.name = "@package..." => unscoped_name = "package..." if (unscoped_name.len > 0 and unscoped_name[0] == '@') { unscoped_name = unscoped_name[1..]; } // if there is a semver in package name... if (std.mem.indexOfScalar(u8, unscoped_name, '@')) |i| { // unscoped_name = "package@1.0.0" => request.name = "package" request.name = unscoped_name[0..i]; // if package was scoped, put "@" back in request.name if (unscoped_name.ptr != positional.ptr) { request.name = positional[0 .. i + 1]; } // unscoped_name = "package@1.0.0" => request.version_buf = "1.0.0" if (unscoped_name.len > i + 1) request.version_buf = unscoped_name[i + 1 ..]; } if (strings.hasPrefix("http://", request.name) or strings.hasPrefix("https://", request.name)) { if (Output.isEmojiEnabled()) { Output.prettyErrorln("😢 error: bun {s} http://url is not implemented yet.", .{ @tagName(op), }); } else { Output.prettyErrorln("error: bun {s} http://url is not implemented yet.", .{ @tagName(op), }); } Global.exit(1); } request.name = std.mem.trim(u8, request.name, "\n\r\t"); if (request.name.len == 0) continue; request.name_hash = String.Builder.stringHash(request.name); for (update_requests.constSlice()) |*prev| { if (prev.name_hash == request.name_hash and request.name.len == prev.name.len) continue :outer; } request.version_buf = std.mem.trim(u8, request.version_buf, "\n\r\t"); // https://github.com/npm/npm-package-arg/blob/fbaf2fd0b72a0f38e7c24260fd4504f4724c9466/npa.js#L330 if (strings.hasPrefix("https://", request.version_buf) or strings.hasPrefix("http://", request.version_buf)) { if (Output.isEmojiEnabled()) { Output.prettyErrorln("😢 error: bun {s} http://url is not implemented yet.", .{ @tagName(op), }); } else { Output.prettyErrorln("error: bun {s} http://url is not implemented yet.", .{ @tagName(op), }); } Global.exit(1); } if ((op == .link or op == .unlink) and !strings.hasPrefixComptime(request.version_buf, "link:")) { request.version_buf = std.fmt.allocPrint(allocator, "link:{s}", .{request.name}) catch unreachable; } if (request.version_buf.len == 0) { request.missing_version = true; } else { const sliced = SlicedString.init(request.version_buf, request.version_buf); request.version = Dependency.parse(allocator, String.init(request.name, request.name), request.version_buf, &sliced, log) orelse Dependency.Version{}; } update_requests.append(request) catch break; } return update_requests.slice(); } }; fn updatePackageJSONAndInstall( ctx: Command.Context, comptime op: Lockfile.Package.Diff.Op, comptime params: []const ParamType, ) !void { var manager = PackageManager.init(ctx, null, params) catch |err| brk: { switch (err) { error.MissingPackageJSON => { if (op == .add or op == .update) { var package_json_file = std.fs.cwd().createFileZ("package.json", .{ .read = true }) catch |err2| { Output.prettyErrorln("error: {s} create package.json", .{@errorName(err2)}); Global.crash(); }; try package_json_file.pwriteAll("{\"dependencies\": {}}", 0); break :brk try PackageManager.init(ctx, package_json_file, params); } Output.prettyErrorln("No package.json, so nothing to remove\n", .{}); Global.crash(); }, else => return err, } unreachable; }; if (manager.options.shouldPrintCommandName()) { Output.prettyErrorln("bun " ++ @tagName(op) ++ " v" ++ Global.package_json_version_with_sha ++ "\n", .{}); Output.flush(); } switch (manager.options.log_level) { .default => try updatePackageJSONAndInstallWithManager(ctx, manager, op, .default), .verbose => try updatePackageJSONAndInstallWithManager(ctx, manager, op, .verbose), .silent => try updatePackageJSONAndInstallWithManager(ctx, manager, op, .silent), .default_no_progress => try updatePackageJSONAndInstallWithManager(ctx, manager, op, .default_no_progress), .verbose_no_progress => try updatePackageJSONAndInstallWithManager(ctx, manager, op, .verbose_no_progress), } } const dependency_lists_to_check = [_]string{ "dependencies", "devDependencies", "optionalDependencies", "peerDependencies", }; fn updatePackageJSONAndInstallWithManager( ctx: Command.Context, manager: *PackageManager, comptime op: Lockfile.Package.Diff.Op, comptime log_level: Options.LogLevel, ) !void { var update_requests = try UpdateRequest.Array.init(0); if (manager.options.positionals.len == 1) { var examples_to_print: [3]string = undefined; const off = @intCast(u64, std.time.milliTimestamp()); switch (op) { .update, .add => { const filler = @import("../cli.zig").HelpCommand.packages_to_add_filler; examples_to_print[0] = filler[@intCast(usize, (off) % filler.len)]; examples_to_print[1] = filler[@intCast(usize, (off + 1) % filler.len)]; examples_to_print[2] = filler[@intCast(usize, (off + 2) % filler.len)]; Output.prettyErrorln( \\ \\Usage: \\ \\ bun add package-name@version \\ bun add package-name \\ bun add package-name a-second-package \\ \\Examples: \\ \\ bun add -g {s} \\ bun add {s} \\ bun add {s} \\ , .{ examples_to_print[0], examples_to_print[1], examples_to_print[2] }); if (manager.options.global) { Output.prettyErrorln( \\ \\Shorthand: bun a -g \\ , .{}); } else { Output.prettyErrorln( \\ \\Shorthand: bun a \\ , .{}); } Global.exit(0); }, .remove => { const filler = @import("../cli.zig").HelpCommand.packages_to_remove_filler; examples_to_print[0] = filler[@intCast(usize, (off) % filler.len)]; examples_to_print[1] = filler[@intCast(usize, (off + 1) % filler.len)]; examples_to_print[2] = filler[@intCast(usize, (off + 2) % filler.len)]; Output.prettyErrorln( \\ \\Usage: \\ \\ bun remove package-name \\ bun remove package-name a-second-package \\ \\Examples: \\ \\ bun remove {s} {s} \\ bun remove {s} \\ , .{ examples_to_print[0], examples_to_print[1], examples_to_print[2], }); if (manager.options.global) { Output.prettyErrorln( \\ \\Shorthand: bun rm -g \\ , .{}); } else { Output.prettyErrorln( \\ \\Shorthand: bun rm \\ , .{}); } Output.flush(); Global.exit(0); }, else => {}, } } var updates = UpdateRequest.parse(ctx.allocator, ctx.log, manager.options.positionals[1..], &update_requests, op); try updatePackageJSONAndInstallWithManagerWithUpdates( ctx, manager, updates, false, op, log_level, ); } fn updatePackageJSONAndInstallWithManagerWithUpdates( ctx: Command.Context, manager: *PackageManager, updates: []UpdateRequest, auto_free: bool, comptime op: Lockfile.Package.Diff.Op, comptime log_level: Options.LogLevel, ) !void { if (ctx.log.errors > 0) { if (comptime log_level != .silent) { if (Output.enable_ansi_colors) { ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true) catch {}; } else { ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false) catch {}; } } Global.crash(); } var current_package_json_stat = try manager.root_package_json_file.stat(); var current_package_json_buf = try ctx.allocator.alloc(u8, current_package_json_stat.size + 64); const current_package_json_contents_len = try manager.root_package_json_file.preadAll( current_package_json_buf, 0, ); const package_json_source = logger.Source.initPathString( package_json_cwd_buf[0 .. FileSystem.instance.top_level_dir.len + "package.json".len], current_package_json_buf[0..current_package_json_contents_len], ); // If there originally was a newline at the end of their package.json, preserve it // so that we don't cause unnecessary diffs in their git history. // https://github.com/oven-sh/bun/issues/1375 const preserve_trailing_newline_at_eof_for_package_json = current_package_json_contents_len > 0 and current_package_json_buf[current_package_json_contents_len - 1] == '\n'; initializeStore(); var current_package_json = json_parser.ParseJSONUTF8(&package_json_source, ctx.log, manager.allocator) catch |err| { if (Output.enable_ansi_colors) { ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true) catch {}; } else { ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false) catch {}; } Output.panic("{s} parsing package.json", .{ @errorName(err), }); }; if (op == .remove) { if (current_package_json.data != .e_object) { Output.prettyErrorln("error: package.json is not an Object {{}}, so there's nothing to remove!", .{}); Global.exit(1); return; } else if (current_package_json.data.e_object.properties.len == 0) { Output.prettyErrorln("error: package.json is empty {{}}, so there's nothing to remove!", .{}); Global.exit(1); return; } else if (current_package_json.asProperty("devDependencies") == null and current_package_json.asProperty("dependencies") == null and current_package_json.asProperty("optionalDependencies") == null and current_package_json.asProperty("peerDependencies") == null) { Output.prettyErrorln("package.json doesn't have dependencies, there's nothing to remove!", .{}); Global.exit(0); return; } } var any_changes = false; var dependency_list: string = "dependencies"; if (manager.options.update.development) { dependency_list = "devDependencies"; } else if (manager.options.update.optional) { dependency_list = "optionalDependencies"; } switch (op) { .remove => { // if we're removing, they don't have to specify where it is installed in the dependencies list // they can even put it multiple times and we will just remove all of them for (updates) |update| { inline for (dependency_lists_to_check) |list| { if (current_package_json.asProperty(list)) |query| { if (query.expr.data == .e_object) { var dependencies = query.expr.data.e_object.properties.slice(); var i: usize = 0; var new_len = dependencies.len; while (i < dependencies.len) : (i += 1) { if (dependencies[i].key.?.data == .e_string) { if (dependencies[i].key.?.data.e_string.eql(string, update.name)) { if (new_len > 1) { dependencies[i] = dependencies[new_len - 1]; new_len -= 1; } else { new_len = 0; } any_changes = true; } } } const changed = new_len != dependencies.len; if (changed) { query.expr.data.e_object.properties.len = @truncate(u32, new_len); // If the dependencies list is now empty, remove it from the package.json // since we're swapRemove, we have to re-sort it if (query.expr.data.e_object.properties.len == 0) { var arraylist = current_package_json.data.e_object.properties.list(); _ = arraylist.swapRemove(query.i); current_package_json.data.e_object.properties.update(arraylist); current_package_json.data.e_object.packageJSONSort(); } else { var obj = query.expr.data.e_object; obj.alphabetizeProperties(); } } } } } } if (!any_changes) { Output.prettyErrorln("\nerror: \"{s}\" is not in a package.json file", .{updates[0].name}); Global.exit(1); return; } manager.to_remove = updates; }, .link, .add, .update => { try PackageJSONEditor.edit(ctx.allocator, updates, ¤t_package_json, dependency_list); manager.package_json_updates = updates; }, else => {}, } var buffer_writer = try JSPrinter.BufferWriter.init(ctx.allocator); try buffer_writer.buffer.list.ensureTotalCapacity(ctx.allocator, current_package_json_buf.len + 1); var package_json_writer = JSPrinter.BufferPrinter.init(buffer_writer); var written = JSPrinter.printJSON(@TypeOf(&package_json_writer), &package_json_writer, current_package_json, &package_json_source) catch |err| { Output.prettyErrorln("package.json failed to write due to error {s}", .{@errorName(err)}); Global.crash(); }; // There are various tradeoffs with how we commit updates when you run `bun add` or `bun remove` // The one we chose here is to effectively pretend a human did: // 1. "bun add react@latest" // 2. open lockfile, find what react resolved to // 3. open package.json // 4. replace "react" : "latest" with "react" : "^16.2.0" // 5. save package.json // The Smarter™ approach is you resolve ahead of time and write to disk once! // But, turns out that's slower in any case where more than one package has to be resolved (most of the time!) // Concurrent network requests are faster than doing one and then waiting until the next batch var new_package_json_source = try ctx.allocator.dupe(u8, package_json_writer.ctx.writtenWithoutTrailingZero()); // Do not free the old package.json AST nodes var old_ast_nodes = JSAst.Expr.Data.Store.toOwnedSlice(); // haha unless defer if (auto_free) bun.default_allocator.free(old_ast_nodes); try installWithManager(ctx, manager, new_package_json_source, log_level); if (op == .update or op == .add or op == .link) { for (manager.package_json_updates) |update| { if (update.failed) { Global.exit(1); return; } } const source = logger.Source.initPathString("package.json", new_package_json_source); // Now, we _re_ parse our in-memory edited package.json // so we can commit the version we changed from the lockfile current_package_json = json_parser.ParseJSONUTF8(&source, ctx.log, manager.allocator) catch |err| { Output.prettyErrorln("error: package.json failed to parse due to error {s}", .{@errorName(err)}); Global.exit(1); return; }; try PackageJSONEditor.edit(ctx.allocator, updates, ¤t_package_json, dependency_list); var buffer_writer_two = try JSPrinter.BufferWriter.init(ctx.allocator); try buffer_writer_two.buffer.list.ensureTotalCapacity(ctx.allocator, new_package_json_source.len + 1); buffer_writer_two.append_newline = preserve_trailing_newline_at_eof_for_package_json; var package_json_writer_two = JSPrinter.BufferPrinter.init(buffer_writer_two); written = JSPrinter.printJSON( @TypeOf(&package_json_writer_two), &package_json_writer_two, current_package_json, &source, ) catch |err| { Output.prettyErrorln("package.json failed to write due to error {s}", .{@errorName(err)}); Global.crash(); }; new_package_json_source = try ctx.allocator.dupe(u8, package_json_writer_two.ctx.writtenWithoutTrailingZero()); } if (manager.options.do.write_package_json) { // Now that we've run the install step // We can save our in-memory package.json to disk try manager.root_package_json_file.pwriteAll(new_package_json_source, 0); std.os.ftruncate(manager.root_package_json_file.handle, new_package_json_source.len) catch {}; manager.root_package_json_file.close(); if (op == .remove) { var cwd = std.fs.cwd(); // This is not exactly correct var node_modules_buf: [bun.MAX_PATH_BYTES]u8 = undefined; std.mem.copy(u8, &node_modules_buf, "node_modules" ++ std.fs.path.sep_str); var offset_buf: []u8 = node_modules_buf["node_modules/".len..]; const name_hashes = manager.lockfile.packages.items(.name_hash); for (updates) |update| { // If the package no longer exists in the updated lockfile, delete the directory // This is not thorough. // It does not handle nested dependencies // This is a quick & dirty cleanup intended for when deleting top-level dependencies if (std.mem.indexOfScalar(PackageNameHash, name_hashes, String.Builder.stringHash(update.name)) == null) { std.mem.copy(u8, offset_buf, update.name); cwd.deleteTree(node_modules_buf[0 .. "node_modules/".len + update.name.len]) catch {}; } } // This is where we clean dangling symlinks // This could be slow if there are a lot of symlinks if (cwd.openIterableDir(manager.options.bin_path, .{})) |node_modules_bin_| { var node_modules_bin: std.fs.IterableDir = node_modules_bin_; var iter: std.fs.IterableDir.Iterator = node_modules_bin.iterate(); iterator: while (iter.next() catch null) |entry| { switch (entry.kind) { std.fs.IterableDir.Entry.Kind.SymLink => { // any symlinks which we are unable to open are assumed to be dangling // note that using access won't work here, because access doesn't resolve symlinks std.mem.copy(u8, &node_modules_buf, entry.name); node_modules_buf[entry.name.len] = 0; var buf: [:0]u8 = node_modules_buf[0..entry.name.len :0]; var file = node_modules_bin.dir.openFileZ(buf, .{ .mode = .read_only }) catch { node_modules_bin.dir.deleteFileZ(buf) catch {}; continue :iterator; }; file.close(); }, else => {}, } } } else |_| {} } } } var cwd_buf: [bun.MAX_PATH_BYTES]u8 = undefined; var package_json_cwd_buf: [bun.MAX_PATH_BYTES]u8 = undefined; pub inline fn install( ctx: Command.Context, ) !void { var manager = PackageManager.initMaybeInstall(ctx, null, &install_params, true) catch |err| { if (err == error.SwitchToBunAdd) { return add(ctx); } return err; }; if (manager.options.shouldPrintCommandName()) { Output.prettyErrorln("bun install v" ++ Global.package_json_version_with_sha ++ "\n", .{}); Output.flush(); } var package_json_contents = manager.root_package_json_file.readToEndAlloc(ctx.allocator, std.math.maxInt(usize)) catch |err| { if (manager.options.log_level != .silent) { Output.prettyErrorln("{s} reading package.json :(", .{@errorName(err)}); Output.flush(); } return; }; try switch (manager.options.log_level) { .default => installWithManager(ctx, manager, package_json_contents, .default), .verbose => installWithManager(ctx, manager, package_json_contents, .verbose), .silent => installWithManager(ctx, manager, package_json_contents, .silent), .default_no_progress => installWithManager(ctx, manager, package_json_contents, .default_no_progress), .verbose_no_progress => installWithManager(ctx, manager, package_json_contents, .verbose_no_progress), }; } pub const PackageInstaller = struct { manager: *PackageManager, lockfile: *Lockfile, progress: *std.Progress, node_modules_folder: std.fs.IterableDir, skip_verify_installed_version_number: bool, skip_delete: bool, force_install: bool, root_node_modules_folder: std.fs.IterableDir, summary: *PackageInstall.Summary, options: *const PackageManager.Options, metas: []const Lockfile.Package.Meta, names: []const String, bins: []const Bin, resolutions: []Resolution, node: *Progress.Node, has_created_bin: bool = false, global_bin_dir: std.fs.IterableDir, destination_dir_subpath_buf: [bun.MAX_PATH_BYTES]u8 = undefined, folder_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined, install_count: usize = 0, successfully_installed: Bitset, // For linking native binaries, we only want to link after we've installed the companion dependencies // We don't want to introduce dependent callbacks like that for every single package // Since this will only be a handful, it's fine to just say "run this at the end" platform_binlinks: std.ArrayListUnmanaged(DeferredBinLink) = std.ArrayListUnmanaged(DeferredBinLink){}, pub const DeferredBinLink = struct { package_id: PackageID, node_modules_folder: std.fs.IterableDir, }; /// Install versions of a package which are waiting on a network request pub fn installEnqueuedPackages( this: *PackageInstaller, package_id: PackageID, comptime log_level: Options.LogLevel, ) void { const name = this.lockfile.str(&this.names[package_id]); const resolution = this.resolutions[package_id]; if (this.manager.task_queue.fetchRemove(Task.Id.forNPMPackage( Task.Tag.extract, name, resolution.value.npm.version, ))) |removed| { var callbacks = removed.value; defer callbacks.deinit(this.manager.allocator); const prev_node_modules_folder = this.node_modules_folder; defer this.node_modules_folder = prev_node_modules_folder; for (callbacks.items) |cb| { const node_modules_folder = cb.node_modules_folder; this.node_modules_folder = .{ .dir = .{ .fd = @intCast(bun.FileDescriptor, node_modules_folder) } }; this.installPackageWithNameAndResolution(package_id, log_level, name, resolution); } } } fn installPackageWithNameAndResolution( this: *PackageInstaller, package_id: PackageID, comptime log_level: Options.LogLevel, name: string, resolution: Resolution, ) void { const buf = this.lockfile.buffers.string_bytes.items; const alias = if (this.lockfile.alias_map.get(package_id)) |str| str.slice(buf) else name; std.mem.copy(u8, &this.destination_dir_subpath_buf, alias); this.destination_dir_subpath_buf[alias.len] = 0; var destination_dir_subpath: [:0]u8 = this.destination_dir_subpath_buf[0..alias.len :0]; var resolution_buf: [512]u8 = undefined; const extern_string_buf = this.lockfile.buffers.extern_strings.items; var resolution_label = std.fmt.bufPrint(&resolution_buf, "{}", .{resolution.fmt(buf)}) catch unreachable; var installer = PackageInstall{ .progress = this.progress, .cache_dir = undefined, .cache_dir_subpath = undefined, .destination_dir = this.node_modules_folder, .destination_dir_subpath = destination_dir_subpath, .destination_dir_subpath_buf = &this.destination_dir_subpath_buf, .allocator = this.lockfile.allocator, .package_name = name, .package_version = resolution_label, }; switch (resolution.tag) { .npm => { installer.cache_dir_subpath = this.manager.cachedNPMPackageFolderName(name, resolution.value.npm.version); installer.cache_dir = this.manager.getCacheDirectory(); }, .folder => { const folder = resolution.value.folder.slice(buf); // Handle when a package depends on itself via file: // example: // "mineflayer": "file:." if (folder.len == 0 or (folder.len == 1 and folder[0] == '.')) { installer.cache_dir_subpath = "."; } else { @memcpy(&this.folder_path_buf, folder.ptr, folder.len); this.folder_path_buf[folder.len] = 0; installer.cache_dir_subpath = std.meta.assumeSentinel(this.folder_path_buf[0..folder.len], 0); } installer.cache_dir = .{ .dir = std.fs.cwd() }; }, .workspace => { const folder = resolution.value.workspace.slice(buf); // Handle when a package depends on itself if (folder.len == 0 or (folder.len == 1 and folder[0] == '.')) { installer.cache_dir_subpath = "."; } else { @memcpy(&this.folder_path_buf, folder.ptr, folder.len); this.folder_path_buf[folder.len] = 0; installer.cache_dir_subpath = std.meta.assumeSentinel(this.folder_path_buf[0..folder.len], 0); } installer.cache_dir = .{ .dir = std.fs.cwd() }; }, .symlink => { const directory = this.manager.globalLinkDir() catch |err| { if (comptime log_level != .silent) { const fmt = "\nerror: unable to access global directory while installing {s}: {s}\n"; const args = .{ name, @errorName(err) }; if (comptime log_level.showProgress()) { if (Output.enable_ansi_colors) { this.progress.log(comptime Output.prettyFmt(fmt, true), args); } else { this.progress.log(comptime Output.prettyFmt(fmt, false), args); } } else { Output.prettyErrorln(fmt, args); } } if (this.manager.options.enable.fail_early) { Global.exit(1); } Output.flush(); this.summary.fail += 1; return; }; const folder = resolution.value.symlink.slice(buf); if (folder.len == 0 or (folder.len == 1 and folder[0] == '.')) { installer.cache_dir_subpath = "."; installer.cache_dir = .{ .dir = std.fs.cwd() }; } else { const global_link_dir = this.manager.globalLinkDirPath() catch unreachable; var ptr = &this.folder_path_buf; var remain: []u8 = this.folder_path_buf[0..]; @memcpy(ptr, global_link_dir.ptr, global_link_dir.len); remain = remain[global_link_dir.len..]; if (global_link_dir[global_link_dir.len - 1] != std.fs.path.sep) { remain[0] = std.fs.path.sep; remain = remain[1..]; } @memcpy(remain.ptr, folder.ptr, folder.len); remain = remain[folder.len..]; remain[0] = 0; const len = @ptrToInt(remain.ptr) - @ptrToInt(ptr); installer.cache_dir_subpath = std.meta.assumeSentinel( this.folder_path_buf[0..len :0], 0, ); installer.cache_dir = directory; } }, else => return, } const needs_install = this.force_install or this.skip_verify_installed_version_number or !installer.verify(); this.summary.skipped += @as(u32, @boolToInt(!needs_install)); if (needs_install) { const result: PackageInstall.Result = switch (resolution.tag) { .symlink, .workspace => installer.installFromLink(this.skip_delete), else => installer.install(this.skip_delete), }; switch (result) { .success => { const is_duplicate = this.successfully_installed.isSet(package_id); this.summary.success += @as(u32, @boolToInt(!is_duplicate)); this.successfully_installed.set(package_id); if (comptime log_level.showProgress()) { this.node.completeOne(); } const bin = this.bins[package_id]; if (bin.tag != .none) { if (!this.has_created_bin) { Bin.Linker.umask = C.umask(0); if (!this.options.global) this.node_modules_folder.dir.makeDirZ(".bin") catch {}; this.has_created_bin = true; } const bin_task_id = Task.Id.forBinLink(package_id); var task_queue = this.manager.task_queue.getOrPut(this.manager.allocator, bin_task_id) catch unreachable; if (!task_queue.found_existing) { run_bin_link: { if (std.mem.indexOfScalar(PackageNameHash, this.options.native_bin_link_allowlist, String.Builder.stringHash(name)) != null) { this.platform_binlinks.append(this.lockfile.allocator, .{ .package_id = package_id, .node_modules_folder = this.node_modules_folder, }) catch unreachable; break :run_bin_link; } var bin_linker = Bin.Linker{ .bin = bin, .package_installed_node_modules = this.node_modules_folder.dir.fd, .global_bin_path = this.options.bin_path, .global_bin_dir = this.options.global_bin_dir.dir, // .destination_dir_subpath = destination_dir_subpath, .root_node_modules_folder = this.root_node_modules_folder.dir.fd, .package_name = strings.StringOrTinyString.init(name), .string_buf = buf, .extern_string_buf = extern_string_buf, }; bin_linker.link(this.manager.options.global); if (bin_linker.err) |err| { if (comptime log_level != .silent) { const fmt = "\nerror: linking {s}: {s}\n"; const args = .{ name, @errorName(err) }; if (comptime log_level.showProgress()) { if (Output.enable_ansi_colors) { this.progress.log(comptime Output.prettyFmt(fmt, true), args); } else { this.progress.log(comptime Output.prettyFmt(fmt, false), args); } } else { Output.prettyErrorln(fmt, args); } } if (this.manager.options.enable.fail_early) { installer.uninstall() catch {}; Global.exit(1); } } } } } }, .fail => |cause| { if (cause.isPackageMissingFromCache()) { switch (resolution.tag) { .npm => { std.debug.assert(resolution.value.npm.url.len() > 0); this.manager.enqueuePackageForDownload( name, package_id, resolution.value.npm.version, resolution.value.npm.url.slice(buf), .{ .node_modules_folder = @intCast(u32, this.node_modules_folder.dir.fd), }, ); }, else => { Output.prettyErrorln( "error: {s} installing {s}", .{ @errorName(cause.err), this.names[package_id].slice(buf) }, ); this.summary.fail += 1; }, } } else if (cause.err == error.DanglingSymlink) { Output.prettyErrorln( "error: {s} \"link:{s}\" not found (try running 'bun link' in the intended package's folder)", .{ @errorName(cause.err), this.names[package_id].slice(buf) }, ); this.summary.fail += 1; } else { Output.prettyErrorln( "error: {s} installing {s}", .{ @errorName(cause.err), this.names[package_id].slice(buf) }, ); this.summary.fail += 1; } }, else => {}, } } } pub fn installPackage( this: *PackageInstaller, package_id: PackageID, comptime log_level: Options.LogLevel, ) void { // const package_id = ctx.package_id; // const tree = ctx.trees[ctx.tree_id]; const meta = &this.metas[package_id]; if (meta.isDisabled()) { if (comptime log_level.showProgress()) { this.node.completeOne(); } return; } const name = this.lockfile.str(&this.names[package_id]); const resolution = this.resolutions[package_id]; this.installPackageWithNameAndResolution(package_id, log_level, name, resolution); } }; pub fn enqueuePackageForDownload( this: *PackageManager, name: []const u8, package_id: PackageID, version: Semver.Version, url: []const u8, task_context: TaskCallbackContext, ) void { const task_id = Task.Id.forNPMPackage(Task.Tag.extract, name, version); var task_queue = this.task_queue.getOrPut(this.allocator, task_id) catch unreachable; if (!task_queue.found_existing) { task_queue.value_ptr.* = .{}; } task_queue.value_ptr.append( this.allocator, task_context, ) catch unreachable; if (!task_queue.found_existing) { if (this.generateNetworkTaskForTarball(task_id, url, this.lockfile.packages.get(package_id)) catch unreachable) |task| { task.schedule(&this.network_tarball_batch); if (this.network_tarball_batch.len > 0) { _ = this.scheduleNetworkTasks(); } } } } pub fn installPackages( this: *PackageManager, lockfile_: *Lockfile, comptime log_level: PackageManager.Options.LogLevel, ) !PackageInstall.Summary { var lockfile = lockfile_; if (!this.options.local_package_features.dev_dependencies) { lockfile = try lockfile.maybeCloneFilteringRootPackages(this.options.local_package_features); } var root_node: *Progress.Node = undefined; var download_node: Progress.Node = undefined; var install_node: Progress.Node = undefined; const options = &this.options; var progress = &this.progress; if (comptime log_level.showProgress()) { root_node = progress.start("", 0); progress.supports_ansi_escape_codes = Output.enable_ansi_colors_stderr; download_node = root_node.start(ProgressStrings.download(), 0); install_node = root_node.start(ProgressStrings.install(), lockfile.packages.len); this.downloads_node = &download_node; } defer { if (comptime log_level.showProgress()) { progress.root.end(); progress.* = .{}; } } // If there was already a valid lockfile and so we did not resolve, i.e. there was zero network activity // the packages could still not be in the cache dir // this would be a common scenario in a CI environment // or if you just cloned a repo // we want to check lazily though // no need to download packages you've already installed!! var skip_verify_installed_version_number = false; var node_modules_folder = std.fs.cwd().openIterableDir("node_modules", .{}) catch brk: { skip_verify_installed_version_number = true; std.fs.cwd().makeDirZ("node_modules") catch |err| { Output.prettyErrorln("error: {s} creating node_modules folder", .{@errorName(err)}); Global.crash(); }; break :brk std.fs.cwd().openIterableDir("node_modules", .{}) catch |err| { Output.prettyErrorln("error: {s} opening node_modules folder", .{@errorName(err)}); Global.crash(); }; }; var skip_delete = skip_verify_installed_version_number; const force_install = options.enable.force_install; if (options.enable.force_install) { skip_verify_installed_version_number = true; skip_delete = false; } var summary = PackageInstall.Summary{}; { var parts = lockfile.packages.slice(); var metas = parts.items(.meta); var names = parts.items(.name); var dependency_lists: []const Lockfile.DependencySlice = parts.items(.dependencies); var dependencies = lockfile.buffers.dependencies.items; const resolutions_buffer: []const PackageID = lockfile.buffers.resolutions.items; const resolution_lists: []const Lockfile.PackageIDSlice = parts.items(.resolutions); var resolutions = parts.items(.resolution); var iterator = Lockfile.Tree.Iterator.init( lockfile.buffers.trees.items, lockfile.buffers.hoisted_packages.items, names, lockfile.buffers.string_bytes.items, ); var installer = PackageInstaller{ .manager = this, .options = &this.options, .metas = metas, .bins = parts.items(.bin), .root_node_modules_folder = node_modules_folder, .names = names, .resolutions = resolutions, .lockfile = lockfile, .node = &install_node, .node_modules_folder = node_modules_folder, .progress = progress, .skip_verify_installed_version_number = skip_verify_installed_version_number, .skip_delete = skip_delete, .summary = &summary, .global_bin_dir = this.options.global_bin_dir, .force_install = force_install, .install_count = lockfile.buffers.hoisted_packages.items.len, .successfully_installed = try Bitset.initEmpty( this.allocator, lockfile.packages.len, ), }; const cwd = std.fs.cwd(); while (iterator.nextNodeModulesFolder()) |node_modules| { try cwd.makePath(std.mem.span(node_modules.relative_path)); // We deliberately do not close this folder. // If the package hasn't been downloaded, we will need to install it later // We use this file descriptor to know where to put it. var folder = try cwd.openIterableDir(node_modules.relative_path, .{}); installer.node_modules_folder = folder; var remaining = node_modules.packages; // cache line is 64 bytes on ARM64 and x64 // PackageIDs are 4 bytes // Hence, we can fit up to 64 / 4 = 16 package IDs in a cache line const unroll_count = comptime 64 / @sizeOf(PackageID); while (remaining.len > unroll_count) { comptime var i: usize = 0; inline while (i < unroll_count) : (i += 1) { installer.installPackage(remaining[i], comptime log_level); } remaining = remaining[unroll_count..]; // We want to minimize how often we call this function // That's part of why we unroll this loop if (this.pending_tasks > 0) { try this.runTasks( *PackageInstaller, &installer, .{ .onExtract = PackageInstaller.installEnqueuedPackages, .onResolve = void{}, .onPackageManifestError = void{}, .onPackageDownloadError = void{}, }, log_level, ); if (!installer.options.do.install_packages) return error.InstallFailed; } } for (remaining) |package_id| { installer.installPackage(@truncate(PackageID, package_id), log_level); } try this.runTasks( *PackageInstaller, &installer, .{ .onExtract = PackageInstaller.installEnqueuedPackages, .onResolve = void{}, .onPackageManifestError = void{}, .onPackageDownloadError = void{}, }, log_level, ); if (!installer.options.do.install_packages) return error.InstallFailed; } while (this.pending_tasks > 0 and installer.options.do.install_packages) : (this.sleep()) { try this.runTasks( *PackageInstaller, &installer, .{ .onExtract = PackageInstaller.installEnqueuedPackages, .onResolve = void{}, .onPackageManifestError = void{}, .onPackageDownloadError = void{}, }, log_level, ); } if (!installer.options.do.install_packages) return error.InstallFailed; summary.successfully_installed = installer.successfully_installed; outer: for (installer.platform_binlinks.items) |deferred| { const package_id = deferred.package_id; const folder = deferred.node_modules_folder; const package_dependencies: []const Dependency = dependency_lists[package_id].get(dependencies); const package_resolutions: []const PackageID = resolution_lists[package_id].get(resolutions_buffer); const original_bin: Bin = installer.bins[package_id]; for (package_dependencies) |_, i| { const resolved_id = package_resolutions[i]; if (resolved_id >= names.len) continue; const meta: Lockfile.Package.Meta = metas[resolved_id]; // This is specifically for platform-specific binaries if (meta.os == .all and meta.arch == .all) continue; // Don't attempt to link incompatible binaries if (meta.isDisabled()) continue; const name: string = lockfile.str(&installer.names[resolved_id]); if (!installer.has_created_bin) { if (!this.options.global) { node_modules_folder.dir.makeDirZ(".bin") catch {}; } Bin.Linker.umask = C.umask(0); installer.has_created_bin = true; } var bin_linker = Bin.Linker{ .bin = original_bin, .package_installed_node_modules = folder.dir.fd, .root_node_modules_folder = node_modules_folder.dir.fd, .global_bin_path = this.options.bin_path, .global_bin_dir = this.options.global_bin_dir.dir, .package_name = strings.StringOrTinyString.init(name), .string_buf = lockfile.buffers.string_bytes.items, .extern_string_buf = lockfile.buffers.extern_strings.items, }; bin_linker.link(this.options.global); if (bin_linker.err) |err| { if (comptime log_level != .silent) { const fmt = "\nerror: linking {s}: {s}\n"; const args = .{ name, @errorName(err) }; if (comptime log_level.showProgress()) { if (Output.enable_ansi_colors) { this.progress.log(comptime Output.prettyFmt(fmt, true), args); } else { this.progress.log(comptime Output.prettyFmt(fmt, false), args); } } else { Output.prettyErrorln(fmt, args); } } if (this.options.enable.fail_early) { Global.exit(1); } } continue :outer; } if (comptime log_level != .silent) { const fmt = "\nwarn: no compatible binaries found for {s}\n"; const args = .{lockfile.str(&names[package_id])}; if (comptime log_level.showProgress()) { if (Output.enable_ansi_colors) { this.progress.log(comptime Output.prettyFmt(fmt, true), args); } else { this.progress.log(comptime Output.prettyFmt(fmt, false), args); } } else { Output.prettyErrorln(fmt, args); } } } } return summary; } pub fn setupGlobalDir(manager: *PackageManager, ctx: *const Command.Context) !void { manager.options.global_bin_dir = try Options.openGlobalBinDir(ctx.install); var out_buffer: [bun.MAX_PATH_BYTES]u8 = undefined; var result = try bun.getFdPath(manager.options.global_bin_dir.dir.fd, &out_buffer); out_buffer[result.len] = 0; var result_: [:0]u8 = out_buffer[0..result.len :0]; manager.options.bin_path = std.meta.assumeSentinel(try FileSystem.instance.dirname_store.append([:0]u8, result_), 0); } pub fn startProgressBarIfNone(manager: *PackageManager) void { if (manager.downloads_node == null) { manager.startProgressBar(); } } pub fn startProgressBar(manager: *PackageManager) void { manager.downloads_node = manager.progress.start(ProgressStrings.download(), 0); manager.progress.supports_ansi_escape_codes = Output.enable_ansi_colors_stderr; manager.setNodeName(manager.downloads_node.?, ProgressStrings.download_no_emoji_, ProgressStrings.download_emoji, true); manager.downloads_node.?.setEstimatedTotalItems(manager.total_tasks + manager.extracted_count); manager.downloads_node.?.setCompletedItems(manager.total_tasks - manager.pending_tasks); manager.downloads_node.?.activate(); manager.progress.refresh(); } pub fn endProgressBar(manager: *PackageManager) void { var downloads_node = manager.downloads_node orelse return; downloads_node.setEstimatedTotalItems(downloads_node.unprotected_estimated_total_items); downloads_node.setCompletedItems(downloads_node.unprotected_estimated_total_items); manager.progress.refresh(); manager.progress.root.end(); manager.progress = .{}; manager.downloads_node = null; } fn installWithManager( ctx: Command.Context, manager: *PackageManager, package_json_contents: string, comptime log_level: Options.LogLevel, ) !void { // sleep off for maximum network throughput var load_lockfile_result: Lockfile.LoadFromDiskResult = if (manager.options.do.load_lockfile) manager.lockfile.loadFromDisk( ctx.allocator, ctx.log, manager.options.lockfile_path, ) else Lockfile.LoadFromDiskResult{ .not_found = {} }; var root = Lockfile.Package{}; var maybe_root: Lockfile.Package = undefined; var needs_new_lockfile = load_lockfile_result != .ok or (load_lockfile_result.ok.buffers.dependencies.items.len == 0 and manager.package_json_updates.len > 0); // this defaults to false // but we force allowing updates to the lockfile when you do bun add var had_any_diffs = false; manager.progress = .{}; // Step 2. Parse the package.json file // var package_json_source = logger.Source.initPathString( package_json_cwd_buf[0 .. FileSystem.instance.top_level_dir.len + "package.json".len], package_json_contents, ); switch (load_lockfile_result) { .err => |cause| { if (log_level != .silent) { switch (cause.step) { .open_file => Output.prettyError("error opening lockfile: {s}\n", .{ @errorName(cause.value), }), .parse_file => Output.prettyError("error parsing lockfile: {s}\n", .{ @errorName(cause.value), }), .read_file => Output.prettyError("error reading lockfile: {s}\n", .{ @errorName(cause.value), }), } if (manager.options.enable.fail_early) { Output.prettyError("failed to load lockfile\n", .{}); } else { Output.prettyError("ignoring lockfile\n", .{}); } if (ctx.log.errors > 0) { if (Output.enable_ansi_colors) { try manager.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true); } else { try manager.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false); } } Output.flush(); } if (manager.options.enable.fail_early) Global.exit(1); }, .ok => { differ: { root = load_lockfile_result.ok.rootPackage() orelse { needs_new_lockfile = true; break :differ; }; if (root.dependencies.len == 0) { needs_new_lockfile = true; } if (needs_new_lockfile) break :differ; var lockfile: Lockfile = undefined; try lockfile.initEmpty(ctx.allocator); maybe_root = Lockfile.Package{}; try Lockfile.Package.parseMain( &lockfile, &maybe_root, ctx.allocator, ctx.log, package_json_source, Features{ .check_for_duplicate_dependencies = true, .dev_dependencies = true, .is_main = true, .optional_dependencies = true, .peer_dependencies = false, .scripts = true, .workspaces = true, }, ); manager.lockfile.scripts = lockfile.scripts; var mapping = try manager.lockfile.allocator.alloc(PackageID, maybe_root.dependencies.len); std.mem.set(PackageID, mapping, invalid_package_id); manager.summary = try Package.Diff.generate( ctx.allocator, manager.lockfile, &lockfile, &root, &maybe_root, mapping, ); const sum = manager.summary.add + manager.summary.remove + manager.summary.update; had_any_diffs = had_any_diffs or sum > 0; if (manager.options.enable.frozen_lockfile and had_any_diffs) { if (log_level != .silent) { Output.prettyErrorln("error: lockfile had changes, but lockfile is frozen", .{}); } Global.exit(1); } // If you changed packages, we will copy over the new package from the new lockfile const new_dependencies = maybe_root.dependencies.get(lockfile.buffers.dependencies.items); if (had_any_diffs) { var builder_ = manager.lockfile.stringBuilder(); // ensure we use one pointer to reference it instead of creating new ones and potentially aliasing var builder = &builder_; for (new_dependencies) |new_dep| { new_dep.count(lockfile.buffers.string_bytes.items, *Lockfile.StringBuilder, builder); } const off = @truncate(u32, manager.lockfile.buffers.dependencies.items.len); const len = @truncate(u32, new_dependencies.len); var packages = manager.lockfile.packages.slice(); var dep_lists = packages.items(.dependencies); var resolution_lists = packages.items(.resolutions); const old_resolutions_list = resolution_lists[0]; dep_lists[0] = .{ .off = off, .len = len }; resolution_lists[0] = .{ .off = off, .len = len }; manager.root_dependency_list = dep_lists[0]; try builder.allocate(); try manager.lockfile.buffers.dependencies.ensureUnusedCapacity(manager.lockfile.allocator, len); try manager.lockfile.buffers.resolutions.ensureUnusedCapacity(manager.lockfile.allocator, len); var old_resolutions = old_resolutions_list.get(manager.lockfile.buffers.resolutions.items); var dependencies = manager.lockfile.buffers.dependencies.items.ptr[off .. off + len]; var resolutions = manager.lockfile.buffers.resolutions.items.ptr[off .. off + len]; // It is too easy to accidentally undefined memory std.mem.set(PackageID, resolutions, invalid_package_id); std.mem.set(Dependency, dependencies, Dependency{}); manager.lockfile.buffers.dependencies.items = manager.lockfile.buffers.dependencies.items.ptr[0 .. off + len]; manager.lockfile.buffers.resolutions.items = manager.lockfile.buffers.resolutions.items.ptr[0 .. off + len]; for (new_dependencies) |new_dep, i| { dependencies[i] = try new_dep.clone(lockfile.buffers.string_bytes.items, *Lockfile.StringBuilder, builder); if (mapping[i] != invalid_package_id) { resolutions[i] = old_resolutions[mapping[i]]; } } builder.clamp(); // Split this into two passes because the below may allocate memory or invalidate pointers if (manager.summary.add > 0 or manager.summary.update > 0) { var remaining = mapping; var dependency_i: PackageID = off; const changes = @truncate(PackageID, mapping.len); _ = manager.getCacheDirectory(); _ = manager.getTemporaryDirectory(); var counter_i: PackageID = 0; while (counter_i < changes) : (counter_i += 1) { if (remaining[counter_i] == invalid_package_id) { dependency_i = counter_i + off; const dependency = manager.lockfile.buffers.dependencies.items[dependency_i]; try manager.enqueueDependencyWithMain( dependency_i, &dependency, manager.lockfile.buffers.resolutions.items[dependency_i], true, ); } } } } } }, else => {}, } if (needs_new_lockfile) { root = Lockfile.Package{}; try manager.lockfile.initEmpty(ctx.allocator); if (manager.options.enable.frozen_lockfile) { if (log_level != .silent) { Output.prettyErrorln("error: lockfile had changes, but lockfile is frozen", .{}); } Global.exit(1); } try Lockfile.Package.parseMain( manager.lockfile, &root, ctx.allocator, ctx.log, package_json_source, Features{ .check_for_duplicate_dependencies = true, .dev_dependencies = true, .is_main = true, .optional_dependencies = true, .peer_dependencies = false, .scripts = true, .workspaces = true, }, ); root = try manager.lockfile.appendPackage(root); manager.root_dependency_list = root.dependencies; if (root.dependencies.len > 0) { _ = manager.getCacheDirectory(); _ = manager.getTemporaryDirectory(); } manager.enqueueDependencyList( root.dependencies, true, ); } manager.flushDependencyQueue(); // Anything that needs to be downloaded from an update needs to be scheduled here _ = manager.scheduleNetworkTasks(); if (manager.pending_tasks > 0) { if (root.dependencies.len > 0) { _ = manager.getCacheDirectory(); _ = manager.getTemporaryDirectory(); } if (comptime log_level.showProgress()) { manager.startProgressBar(); } else if (comptime log_level != .silent) { Output.prettyErrorln(" Resolving dependencies", .{}); Output.flush(); } { while (manager.pending_tasks > 0) : (manager.sleep()) { try manager.runTasks(void, void{}, .{ .onExtract = void{}, .onResolve = void{}, .onPackageManifestError = void{}, .onPackageDownloadError = void{}, }, log_level); } } if (comptime log_level.showProgress()) { manager.endProgressBar(); } else if (comptime log_level != .silent) { Output.prettyErrorln(" Resolved, downloaded and extracted [{d}]", .{manager.total_tasks}); Output.flush(); } } if (Output.enable_ansi_colors) { try manager.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true); } else { try manager.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false); } if (manager.log.errors > 0) { Global.exit(1); } const needs_clean_lockfile = had_any_diffs or needs_new_lockfile or manager.package_json_updates.len > 0; var did_meta_hash_change = needs_clean_lockfile; if (needs_clean_lockfile) { manager.lockfile = try manager.lockfile.clean(manager.package_json_updates); } if (manager.lockfile.packages.len > 0) { for (manager.package_json_updates) |update| { // prevent redundant errors if (update.failed) { return error.InstallFailed; } } manager.root_dependency_list = manager.lockfile.packages.items(.dependencies)[0]; manager.lockfile.verifyResolutions(manager.options.local_package_features, manager.options.remote_package_features, log_level); } if (needs_clean_lockfile or manager.options.enable.force_save_lockfile) { did_meta_hash_change = try manager.lockfile.hasMetaHashChanged( PackageManager.verbose_install or manager.options.do.print_meta_hash_string, ); } if (manager.options.global) { try manager.setupGlobalDir(&ctx); } // We don't always save the lockfile. // This is for two reasons. // 1. It's unnecessary work if there are no changes // 2. There is a determinism issue in the file where alignment bytes might be garbage data // This is a bug that needs to be fixed, however we can work around it for now // by avoiding saving the lockfile if (manager.options.do.save_lockfile and (did_meta_hash_change or manager.lockfile.isEmpty() or manager.options.enable.force_save_lockfile)) { save: { if (manager.lockfile.isEmpty()) { if (!manager.options.dry_run) { std.fs.cwd().deleteFileZ(manager.options.save_lockfile_path) catch |err| brk: { // we don't care if (err == error.FileNotFound) { if (had_any_diffs) break :save; break :brk; } if (log_level != .silent) Output.prettyErrorln("\n error: {s} deleting empty lockfile", .{@errorName(err)}); break :save; }; } if (!manager.options.global) { if (log_level != .silent) Output.prettyErrorln("No packages! Deleted empty lockfile", .{}); } break :save; } var node: *Progress.Node = undefined; if (comptime log_level.showProgress()) { node = manager.progress.start(ProgressStrings.save(), 0); manager.progress.supports_ansi_escape_codes = Output.enable_ansi_colors_stderr; node.activate(); manager.progress.refresh(); } manager.lockfile.saveToDisk(manager.options.save_lockfile_path); if (comptime log_level.showProgress()) { node.end(); manager.progress.refresh(); manager.progress.root.end(); manager.progress = .{}; } else if (comptime log_level != .silent) { Output.prettyErrorln(" Saved lockfile", .{}); Output.flush(); } } } // Install script order for npm 8.3.0: // 1. preinstall // 2. install // 3. postinstall // 4. preprepare // 5. prepare // 6. postprepare const run_lifecycle_scripts = manager.options.do.run_scripts and manager.lockfile.scripts.hasAny() and manager.options.do.install_packages; const has_pre_lifecycle_scripts = manager.lockfile.scripts.preinstall.items.len > 0; const needs_configure_bundler_for_run = run_lifecycle_scripts and !has_pre_lifecycle_scripts; if (run_lifecycle_scripts and has_pre_lifecycle_scripts) { // We need to figure out the PATH and other environment variables // to do that, we re-use the code from bun run // this is expensive, it traverses the entire directory tree going up to the root // so we really only want to do it when strictly necessary { var this_bundler: bundler.Bundler = undefined; var ORIGINAL_PATH: string = ""; _ = try RunCommand.configureEnvForRun( ctx, &this_bundler, manager.env, &ORIGINAL_PATH, log_level != .silent, false, ); } try manager.lockfile.scripts.run(manager.allocator, manager.env, log_level != .silent, "preinstall"); } var install_summary = PackageInstall.Summary{}; if (manager.options.do.install_packages) { install_summary = try manager.installPackages( manager.lockfile, log_level, ); } if (needs_new_lockfile) { manager.summary.add = @truncate(u32, manager.lockfile.packages.len); } if (manager.options.do.save_yarn_lock) { var node: *Progress.Node = undefined; if (comptime log_level.showProgress()) { node = manager.progress.start("Saving yarn.lock", 0); manager.progress.supports_ansi_escape_codes = Output.enable_ansi_colors_stderr; manager.progress.refresh(); } else if (comptime log_level != .silent) { Output.prettyErrorln(" Saved yarn.lock", .{}); Output.flush(); } try manager.writeYarnLock(); if (comptime log_level.showProgress()) { node.completeOne(); manager.progress.refresh(); manager.progress.root.end(); manager.progress = .{}; } } var printed_timestamp = false; if (comptime log_level != .silent) { if (manager.options.do.summary) { var printer = Lockfile.Printer{ .lockfile = manager.lockfile, .options = manager.options, .updates = manager.package_json_updates, .successfully_installed = install_summary.successfully_installed, }; if (Output.enable_ansi_colors) { try Lockfile.Printer.Tree.print(&printer, Output.WriterType, Output.writer(), true); } else { try Lockfile.Printer.Tree.print(&printer, Output.WriterType, Output.writer(), false); } if (!did_meta_hash_change) { manager.summary.remove = 0; manager.summary.add = 0; manager.summary.update = 0; } if (install_summary.success > 0) { // it's confusing when it shows 3 packages and says it installed 1 Output.pretty("\n {d} packages installed ", .{@max( install_summary.success, @truncate( u32, manager.package_json_updates.len, ), )}); Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); printed_timestamp = true; Output.pretty("\n", .{}); if (manager.summary.remove > 0) { Output.pretty(" Removed: {d}\n", .{manager.summary.remove}); } } else if (manager.summary.remove > 0) { if (manager.to_remove.len > 0) { for (manager.to_remove) |update| { Output.prettyln(" - {s}", .{update.name}); } } Output.pretty("\n {d} packages removed ", .{manager.summary.remove}); Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); printed_timestamp = true; Output.pretty("\n", .{}); } else if (install_summary.skipped > 0 and install_summary.fail == 0 and manager.package_json_updates.len == 0) { Output.pretty("\n", .{}); const count = @truncate(PackageID, manager.lockfile.packages.len); if (count != install_summary.skipped) { Output.pretty("Checked {d} installs across {d} packages (no changes) ", .{ install_summary.skipped, count, }); Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); printed_timestamp = true; Output.pretty("\n", .{}); } else { Output.pretty(" Done! Checked {d} packages (no changes) ", .{ install_summary.skipped, }); Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); printed_timestamp = true; Output.pretty("\n", .{}); } } if (install_summary.fail > 0) { Output.prettyln("Failed to install {d} packages\n", .{install_summary.fail}); Output.flush(); } } } if (run_lifecycle_scripts and install_summary.fail == 0) { // We need to figure out the PATH and other environment variables // to do that, we re-use the code from bun run // this is expensive, it traverses the entire directory tree going up to the root // so we really only want to do it when strictly necessary if (needs_configure_bundler_for_run) { var this_bundler: bundler.Bundler = undefined; var ORIGINAL_PATH: string = ""; _ = try RunCommand.configureEnvForRun( ctx, &this_bundler, manager.env, &ORIGINAL_PATH, log_level != .silent, false, ); } else { // bun install may have installed new bins, so we need to update the PATH // this can happen if node_modules/.bin didn't previously exist // note: it is harmless to have the same directory in the PATH multiple times const current_path = manager.env.map.get("PATH") orelse ""; // TODO: windows const cwd_without_trailing_slash = if (Fs.FileSystem.instance.top_level_dir.len > 1 and Fs.FileSystem.instance.top_level_dir[Fs.FileSystem.instance.top_level_dir.len - 1] == '/') Fs.FileSystem.instance.top_level_dir[0 .. Fs.FileSystem.instance.top_level_dir.len - 1] else Fs.FileSystem.instance.top_level_dir; try manager.env.map.put("PATH", try std.fmt.allocPrint( ctx.allocator, "{s}:{s}/node_modules/.bin", .{ current_path, cwd_without_trailing_slash, }, )); } // 1. preinstall // 2. install // 3. postinstall try manager.lockfile.scripts.run(manager.allocator, manager.env, log_level != .silent, "install"); try manager.lockfile.scripts.run(manager.allocator, manager.env, log_level != .silent, "postinstall"); // 4. preprepare // 5. prepare // 6. postprepare try manager.lockfile.scripts.run(manager.allocator, manager.env, log_level != .silent, "preprepare"); try manager.lockfile.scripts.run(manager.allocator, manager.env, log_level != .silent, "prepare"); try manager.lockfile.scripts.run(manager.allocator, manager.env, log_level != .silent, "postprepare"); } if (comptime log_level != .silent) { if (manager.options.do.summary) { if (!printed_timestamp) { Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); Output.prettyln(" done", .{}); printed_timestamp = true; } } } Output.flush(); } }; const Package = Lockfile.Package; pub const PackageManifestError = error{ PackageManifestHTTP400, PackageManifestHTTP401, PackageManifestHTTP402, PackageManifestHTTP403, PackageManifestHTTP404, PackageManifestHTTP4xx, PackageManifestHTTP5xx, }; test "UpdateRequests.parse" { var log = logger.Log.init(default_allocator); var array = PackageManager.UpdateRequest.Array.init(0) catch unreachable; const updates: []const []const u8 = &.{ "@bacon/name", "foo", "bar", "baz", "boo@1.0.0", "bing@latest", }; var reqs = PackageManager.UpdateRequest.parse(default_allocator, &log, updates, &array, .add); try std.testing.expectEqualStrings(reqs[0].name, "@bacon/name"); try std.testing.expectEqualStrings(reqs[1].name, "foo"); try std.testing.expectEqualStrings(reqs[2].name, "bar"); try std.testing.expectEqualStrings(reqs[3].name, "baz"); try std.testing.expectEqualStrings(reqs[4].name, "boo"); try std.testing.expectEqual(reqs[4].version.tag, Dependency.Version.Tag.npm); try std.testing.expectEqualStrings(reqs[4].version.literal.slice("boo@1.0.0"), "1.0.0"); try std.testing.expectEqual(reqs[5].version.tag, Dependency.Version.Tag.dist_tag); try std.testing.expectEqualStrings(reqs[5].version.literal.slice("bing@1.0.0"), "latest"); try std.testing.expectEqual(updates.len, 6); } test "PackageManager.Options - default registry, default values" { var allocator = default_allocator; var log = logger.Log.init(allocator); defer log.deinit(); var env_loader = DotEnv.Loader.init(&DotEnv.Map.init(allocator), allocator); var options = PackageManager.Options{}; try options.load(allocator, &log, &env_loader, null, null); try std.testing.expectEqualStrings("", options.scope.name); try std.testing.expectEqualStrings("", options.scope.auth); try std.testing.expectEqualStrings(Npm.Registry.default_url, options.scope.url.href); try std.testing.expectEqualStrings("", options.scope.token); } test "PackageManager.Options - default registry, custom token" { var allocator = default_allocator; var log = logger.Log.init(allocator); defer log.deinit(); var env_loader = DotEnv.Loader.init(&DotEnv.Map.init(allocator), allocator); var install = Api.BunInstall{ .default_registry = Api.NpmRegistry{ .url = "", .username = "foo", .password = "bar", .token = "baz", }, .native_bin_links = &.{}, }; var options = PackageManager.Options{}; try options.load(allocator, &log, &env_loader, null, &install); try std.testing.expectEqualStrings("", options.scope.name); try std.testing.expectEqualStrings("", options.scope.auth); try std.testing.expectEqualStrings(Npm.Registry.default_url, options.scope.url.href); try std.testing.expectEqualStrings("baz", options.scope.token); } test "PackageManager.Options - default registry, custom URL" { var allocator = default_allocator; var log = logger.Log.init(allocator); defer log.deinit(); var env_loader = DotEnv.Loader.init(&DotEnv.Map.init(allocator), allocator); var install = Api.BunInstall{ .default_registry = Api.NpmRegistry{ .url = "https://example.com/", .username = "foo", .password = "bar", .token = "", }, .native_bin_links = &.{}, }; var options = PackageManager.Options{}; try options.load(allocator, &log, &env_loader, null, &install); try std.testing.expectEqualStrings("", options.scope.name); try std.testing.expectEqualStrings("Zm9vOmJhcg==", options.scope.auth); try std.testing.expectEqualStrings("https://example.com/", options.scope.url.href); try std.testing.expectEqualStrings("", options.scope.token); } test "PackageManager.Options - scoped registry" { var allocator = default_allocator; var log = logger.Log.init(allocator); defer log.deinit(); var env_loader = DotEnv.Loader.init(&DotEnv.Map.init(allocator), allocator); var install = Api.BunInstall{ .scoped = Api.NpmRegistryMap{ .scopes = &.{ "foo", }, .registries = &.{ Api.NpmRegistry{ .url = "", .username = "", .password = "", .token = "bar", }, }, }, .native_bin_links = &.{}, }; var options = PackageManager.Options{}; try options.load(allocator, &log, &env_loader, null, &install); try std.testing.expectEqualStrings("", options.scope.name); try std.testing.expectEqualStrings("", options.scope.auth); try std.testing.expectEqualStrings(Npm.Registry.default_url, options.scope.url.href); try std.testing.expectEqualStrings("", options.scope.token); var scoped = options.registries.getPtr(Npm.Registry.Scope.hash(Npm.Registry.Scope.getName("foo"))); try std.testing.expect(scoped != null); if (scoped) |scope| { try std.testing.expectEqualStrings("foo", scope.name); try std.testing.expectEqualStrings("", scope.auth); try std.testing.expectEqualStrings(Npm.Registry.default_url, scope.url.href); try std.testing.expectEqualStrings("bar", scope.token); } } test "PackageManager.Options - mixed default/scoped registry" { var allocator = default_allocator; var log = logger.Log.init(allocator); defer log.deinit(); var env_loader = DotEnv.Loader.init(&DotEnv.Map.init(allocator), allocator); var install = Api.BunInstall{ .default_registry = Api.NpmRegistry{ .url = "https://example.com/", .username = "", .password = "", .token = "foo", }, .scoped = Api.NpmRegistryMap{ .scopes = &.{ "bar", }, .registries = &.{ Api.NpmRegistry{ .url = "", .username = "baz", .password = "moo", .token = "", }, }, }, .native_bin_links = &.{}, }; var options = PackageManager.Options{}; try options.load(allocator, &log, &env_loader, null, &install); try std.testing.expectEqualStrings("", options.scope.name); try std.testing.expectEqualStrings("", options.scope.auth); try std.testing.expectEqualStrings("https://example.com/", options.scope.url.href); try std.testing.expectEqualStrings("foo", options.scope.token); var scoped = options.registries.getPtr(Npm.Registry.Scope.hash(Npm.Registry.Scope.getName("bar"))); try std.testing.expect(scoped != null); if (scoped) |scope| { try std.testing.expectEqualStrings("bar", scope.name); try std.testing.expectEqualStrings("YmF6Om1vbw==", scope.auth); try std.testing.expectEqualStrings("https://example.com/", scope.url.href); try std.testing.expectEqualStrings("", scope.token); } }