diff options
Diffstat (limited to 'src/install/install.zig')
| -rw-r--r-- | src/install/install.zig | 6707 |
1 files changed, 6707 insertions, 0 deletions
diff --git a/src/install/install.zig b/src/install/install.zig new file mode 100644 index 000000000..075a249a0 --- /dev/null +++ b/src/install/install.zig @@ -0,0 +1,6707 @@ +usingnamespace @import("../global.zig"); +const std = @import("std"); + +const JSLexer = @import("../js_lexer.zig"); +const logger = @import("../logger.zig"); +const alloc = @import("../alloc.zig"); +const js_parser = @import("../js_parser.zig"); +const json_parser = @import("../json_parser.zig"); +const JSPrinter = @import("../js_printer.zig"); + +const linker = @import("../linker.zig"); +usingnamespace @import("../ast/base.zig"); +usingnamespace @import("../defines.zig"); +const panicky = @import("../panic_handler.zig"); +const sync = @import("../sync.zig"); +const Api = @import("../api/schema.zig").Api; +const resolve_path = @import("../resolver/resolve_path.zig"); +const configureTransformOptionsForBun = @import("../javascript/jsc/config.zig").configureTransformOptionsForBun; +const Command = @import("../cli.zig").Command; +const bundler = @import("../bundler.zig"); +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 NewBunQueue = @import("../bun_queue.zig").NewBunQueue; +const HeaderBuilder = @import("http").HeaderBuilder; +const Fs = @import("../fs.zig"); +const FileSystem = Fs.FileSystem; +const Lock = @import("../lock.zig").Lock; +var path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; +var path_buf2: [std.fs.MAX_PATH_BYTES]u8 = undefined; +const URL = @import("../query_string_map.zig").URL; +const NetworkThread = @import("network_thread"); +const AsyncHTTP = @import("http").AsyncHTTP; +const HTTPChannel = @import("http").HTTPChannel; +const Integrity = @import("./integrity.zig").Integrity; +const clap = @import("clap"); +const ExtractTarball = @import("./extract_tarball.zig"); +const Npm = @import("./npm.zig"); +const Bitset = @import("./bit_set.zig").DynamicBitSetUnmanaged; + +threadlocal var initialized_store = false; + +// these bytes are skipped +// so we just make it repeat bun bun bun bun bun bun bun bun bun +// because why not +const alignment_bytes_to_repeat = "bun"; +const alignment_bytes_to_repeat_buffer = "bunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbunbun"; + +const JSAst = @import("../js_ast.zig"); + +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; + +pub const ExternalStringBuilder = StructBuilder.Builder(ExternalString); +pub const SmallExternalStringList = ExternalSlice(String); + +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 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..@minimum(in.len, this.off + this.len)]; + } + + pub inline fn mut(this: Slice, in: []Type) []Type { + return in.ptr[this.off..@minimum(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 Iterator = NewIterator(ExternalStringList); + + pub const Small = extern struct { + name: SmallExternalStringList = SmallExternalStringList{}, + value: SmallExternalStringList = SmallExternalStringList{}, + + pub const Iterator = NewIterator(SmallExternalStringList); + + pub inline fn iterator(this: Small, buf: []const String) Small.Iterator { + return Small.Iterator.init(buf, this.name, this.value); + } + }; + + pub inline fn iterator(this: ExternalStringMap, buf: []const String) Iterator { + return Iterator.init(buf, this.name, this.value); + } + + fn NewIterator(comptime Type: type) type { + return struct { + const ThisIterator = @This(); + + i: usize = 0, + names: []const Type.Child, + values: []const Type.Child, + + pub fn init(all: []const Type.Child, names: Type, values: Type) ThisIterator { + this.names = names.get(all); + this.values = values.get(all); + return this; + } + + pub fn next(this: *ThisIterator) ?[2]Type.Child { + if (this.i < this.names.len) { + const ret = [2]Type.Child{ this.names[this.i], this.values[this.i] }; + this.i += 1; + } + + return null; + } + }; + } +}; + +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 = std.mem.alignForward(pos, @alignOf(Type)) - pos; + var i: usize = 0; + + var remainder: string = alignment_bytes_to_repeat_buffer[0..@minimum(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, + callback: union(Task.Tag) { + package_manifest: struct { + loaded_manifest: ?Npm.PackageManifest = null, + name: strings.StringOrTinyString, + }, + extract: ExtractTarball, + binlink: void, + }, + + pub fn notify(http: *AsyncHTTP, sender: *AsyncHTTP.HTTPSender) void { + PackageManager.instance.network_channel.writeItem(@fieldParentPtr(NetworkTask, "http", http)) catch {}; + sender.onFinish(); + } + + const default_headers_buf: string = "Acceptapplication/vnd.npm.install-v1+json"; + pub fn forManifest( + this: *NetworkTask, + name: string, + allocator: *std.mem.Allocator, + registry_url: URL, + loaded_manifest: ?Npm.PackageManifest, + ) !void { + this.url_buf = try std.fmt.allocPrint(allocator, "{s}://{s}/{s}", .{ registry_url.displayProtocol(), registry_url.hostname, 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{}; + + 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", "application/vnd.npm.install-v1+json"); + if (last_modified.len > 0 and etag.len > 0) { + header_builder.content.count(last_modified); + } + try header_builder.allocate(allocator); + + 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", "application/vnd.npm.install-v1+json"); + + 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.request_buffer = try MutableString.init(allocator, 0); + this.response_buffer = try MutableString.init(allocator, 0); + this.allocator = allocator; + this.http = try AsyncHTTP.init( + allocator, + .GET, + URL.parse(this.url_buf), + header_builder.entries, + header_builder.content.ptr.?[0..header_builder.content.len], + &this.response_buffer, + &this.request_buffer, + 0, + ); + 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; + } + + this.http.callback = notify; + } + + 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, + ) !void { + this.url_buf = try ExtractTarball.buildURL( + tarball.registry, + tarball.name, + tarball.resolution.value.npm, + PackageManager.instance.lockfile.buffers.string_bytes.items, + ); + + this.request_buffer = try MutableString.init(allocator, 0); + this.response_buffer = try MutableString.init(allocator, 0); + this.allocator = allocator; + + this.http = try AsyncHTTP.init( + allocator, + .GET, + URL.parse(this.url_buf), + .{}, + "", + &this.response_buffer, + &this.request_buffer, + 0, + ); + this.http.callback = notify; + this.callback = .{ .extract = tarball }; + } +}; + +pub const Origin = enum(u8) { + local = 0, + npm = 1, + tarball = 2, +}; + +pub const Features = struct { + optional_dependencies: bool = false, + dev_dependencies: bool = false, + scripts: bool = false, + peer_dependencies: bool = true, + is_main: bool = false, + + check_for_duplicate_dependencies: bool = false, + + pub const npm = Features{ + .optional_dependencies = true, + }; + + pub const npm_manifest = Features{ + .optional_dependencies = true, + }; +}; + +pub const PreinstallState = enum(u8) { + unknown = 0, + done = 1, + extract = 2, + extracting = 3, +}; + +pub const Lockfile = struct { + + // Serialized data + /// The version of the lockfile format, intended to prevent data corruption for format changes. + format: FormatVersion = .v0, + + /// + packages: Lockfile.Package.List = Lockfile.Package.List{}, + buffers: Buffers = Buffers{}, + + /// name -> PackageID || [*]PackageID + /// Not for iterating. + package_index: PackageIndex.Map, + unique_packages: Bitset, + string_pool: StringPool, + allocator: *std.mem.Allocator, + scratch: Scratch = Scratch{}, + + const Stream = std.io.FixedBufferStream([]u8); + pub const default_filename = "bun.lockb"; + + pub const LoadFromDiskResult = union(Tag) { + not_found: void, + err: struct { + step: Step, + value: anyerror, + }, + ok: *Lockfile, + + pub const Step = enum { open_file, read_file, parse_file }; + + pub const Tag = enum { + not_found, + err, + ok, + }; + }; + + pub fn loadFromDisk(this: *Lockfile, allocator: *std.mem.Allocator, log: *logger.Log, filename: stringZ) LoadFromDiskResult { + std.debug.assert(FileSystem.instance_loaded); + var file = std.fs.cwd().openFileZ(filename, .{ .read = true }) catch |err| { + return switch (err) { + error.AccessDenied, error.BadPathName, error.FileNotFound => LoadFromDiskResult{ .not_found = .{} }, + else => LoadFromDiskResult{ .err = .{ .step = .open_file, .value = err } }, + }; + }; + defer file.close(); + var buf = file.readToEndAlloc(allocator, std.math.maxInt(usize)) catch |err| { + return LoadFromDiskResult{ .err = .{ .step = .read_file, .value = err } }; + }; + + var stream = Stream{ .buffer = buf, .pos = 0 }; + Lockfile.Serializer.load(this, &stream, allocator, log) catch |err| { + return LoadFromDiskResult{ .err = .{ .step = .parse_file, .value = err } }; + }; + + return LoadFromDiskResult{ .ok = this }; + } + + const PackageIDQueue = std.fifo.LinearFifo(PackageID, .Dynamic); + + pub const InstallResult = struct { + lockfile: *Lockfile, + summary: PackageInstall.Summary, + }; + + pub const Tree = extern struct { + id: Id = invalid_id, + package_id: PackageID = invalid_package_id, + + parent: Id = invalid_id, + packages: Lockfile.PackageIDSlice = Lockfile.PackageIDSlice{}, + + pub const Slice = ExternalSlice(Tree); + pub const List = std.ArrayListUnmanaged(Tree); + pub const Id = u32; + const invalid_id: Id = std.math.maxInt(Id); + const dependency_loop = invalid_id - 1; + const hoisted = invalid_id - 2; + const error_id = hoisted; + + const SubtreeError = error{ OutOfMemory, DependencyLoop }; + + const NodeModulesFolder = struct { + relative_path: stringZ, + in: PackageID, + packages: []const PackageID, + }; + + pub const Iterator = struct { + trees: []const Tree, + package_ids: []const PackageID, + names: []const String, + tree_id: Id = 0, + path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined, + path_buf_len: usize = 0, + last_parent: Id = invalid_id, + string_buf: string, + + // max number of node_modules folders + depth_stack: [(std.fs.MAX_PATH_BYTES / "node_modules".len) + 1]Id = undefined, + + pub fn init( + trees: []const Tree, + package_ids: []const PackageID, + names: []const String, + string_buf: string, + ) Iterator { + return Tree.Iterator{ + .trees = trees, + .package_ids = package_ids, + .names = names, + .tree_id = 0, + .path_buf = undefined, + .path_buf_len = 0, + .last_parent = invalid_id, + .string_buf = string_buf, + }; + } + + pub fn nextNodeModulesFolder(this: *Iterator) ?NodeModulesFolder { + if (this.tree_id >= this.trees.len) return null; + + while (this.trees[this.tree_id].packages.len == 0) { + this.tree_id += 1; + if (this.tree_id >= this.trees.len) return null; + } + + const tree = this.trees[this.tree_id]; + const string_buf = this.string_buf; + + { + + // For now, the dumb way + // (the smart way is avoiding this copy) + this.path_buf[0.."node_modules".len].* = "node_modules".*; + var parent_id = tree.id; + var path_written: usize = "node_modules".len; + this.depth_stack[0] = 0; + + if (tree.id > 0) { + var depth_buf_len: usize = 1; + while (parent_id > 0 and parent_id < @intCast(Id, this.trees.len)) { + this.depth_stack[depth_buf_len] = parent_id; + parent_id = this.trees[parent_id].parent; + depth_buf_len += 1; + } + depth_buf_len -= 1; + while (depth_buf_len > 0) : (depth_buf_len -= 1) { + this.path_buf[path_written] = std.fs.path.sep; + path_written += 1; + + const tree_id = this.depth_stack[depth_buf_len]; + + const name = this.names[this.trees[tree_id].package_id].slice(string_buf); + std.mem.copy(u8, this.path_buf[path_written..], name); + path_written += name.len; + + this.path_buf[path_written..][0.."/node_modules".len].* = (std.fs.path.sep_str ++ "node_modules").*; + path_written += "/node_modules".len; + } + } + this.path_buf[path_written] = 0; + this.path_buf_len = path_written; + } + + this.tree_id += 1; + var relative_path: [:0]u8 = this.path_buf[0..this.path_buf_len :0]; + return NodeModulesFolder{ + .relative_path = relative_path, + .in = tree.package_id, + .packages = tree.packages.get(this.package_ids), + }; + } + }; + + const Builder = struct { + allocator: *std.mem.Allocator, + name_hashes: []const PackageNameHash, + list: ArrayList = ArrayList{}, + resolutions: []const PackageID, + dependencies: []const Dependency, + resolution_lists: []const Lockfile.PackageIDSlice, + queue: Lockfile.TreeFiller, + + pub const Entry = struct { + tree: Tree, + packages: Lockfile.PackageIDList, + }; + + pub const ArrayList = std.MultiArrayList(Entry); + + /// Flatten the multi-dimensional ArrayList of package IDs into a single easily serializable array + pub fn clean(this: *Builder) ![]PackageID { + const end = @truncate(Id, this.list.len); + var i: Id = 0; + var total_packages_count: u32 = 0; + + var slice = this.list.slice(); + var trees = this.list.items(.tree); + var packages = this.list.items(.packages); + + // TODO: can we cull empty trees here? + while (i < end) : (i += 1) { + total_packages_count += trees[i].packages.len; + } + + var package_ids = try this.allocator.alloc(PackageID, total_packages_count); + var next = PackageIDSlice{}; + + for (trees) |tree, id| { + if (tree.packages.len > 0) { + var child = packages[id]; + const len = @truncate(PackageID, child.items.len); + next.off += next.len; + next.len = len; + trees[id].packages = next; + std.mem.copy(PackageID, package_ids[next.off..][0..next.len], child.items); + child.deinit(this.allocator); + } + } + this.queue.deinit(); + + return package_ids; + } + }; + + pub fn processSubtree( + this: *Tree, + package_id: PackageID, + builder: *Builder, + ) SubtreeError!void { + try builder.list.append(builder.allocator, .{ + .tree = Tree{ + .parent = this.id, + .id = @truncate(Id, builder.list.len), + .package_id = package_id, + }, + .packages = .{}, + }); + + var list_slice = builder.list.slice(); + var trees = list_slice.items(.tree); + var package_lists = list_slice.items(.packages); + var next: *Tree = &trees[builder.list.len - 1]; + + const resolution_list = builder.resolution_lists[package_id]; + const resolutions: []const PackageID = resolution_list.get(builder.resolutions); + if (resolutions.len == 0) { + return; + } + const dependencies: []const Dependency = builder.dependencies[resolution_list.off .. resolution_list.off + resolution_list.len]; + + const max_package_id = builder.name_hashes.len; + + for (resolutions) |pid, j| { + if (pid >= max_package_id or dependencies[j].behavior.isPeer()) continue; + + const destination = next.addDependency(pid, builder.name_hashes, package_lists, trees, builder.allocator); + switch (destination) { + Tree.dependency_loop => return error.DependencyLoop, + Tree.hoisted => continue, + else => {}, + } + + if (builder.resolution_lists[pid].len > 0) { + try builder.queue.writeItem([2]PackageID{ pid, destination }); + } + } + } + + // todo: use error type when it no longer takes up extra stack space + pub fn addDependency( + this: *Tree, + package_id: PackageID, + name_hashes: []const PackageNameHash, + lists: []Lockfile.PackageIDList, + trees: []Tree, + allocator: *std.mem.Allocator, + ) Id { + const this_packages = this.packages.get(lists[this.id].items); + const name_hash = name_hashes[package_id]; + + for (this_packages) |pid, slot| { + if (name_hashes[pid] == name_hash) { + if (pid != package_id) { + return dependency_loop; + } + + return hoisted; + } + } + + if (this.parent < error_id) { + const id = trees[this.parent].addDependency( + package_id, + name_hashes, + lists, + trees, + allocator, + ); + switch (id) { + // If there is a dependency loop, we've reached the highest point + // Therefore, we resolve the dependency loop by appending to ourself + Tree.dependency_loop => {}, + Tree.hoisted => return hoisted, + else => return id, + } + } + + lists[this.id].append(allocator, package_id) catch unreachable; + this.packages.len += 1; + return this.id; + } + }; + + pub fn clean(old: *Lockfile, deduped: *u32, updates: []PackageManager.UpdateRequest, options: *const PackageManager.Options) !*Lockfile { + + // We will only shrink the number of packages here. + // never grow + const max_package_id = old.packages.len; + + if (updates.len > 0) { + var root_deps: []Dependency = old.packages.items(.dependencies)[0].mut(old.buffers.dependencies.items); + const old_resolutions: []const PackageID = old.packages.items(.resolutions)[0].get(old.buffers.resolutions.items); + const resolutions_of_yore: []const Resolution = old.packages.items(.resolution); + const old_names = old.packages.items(.name); + var string_builder = old.stringBuilder(); + for (updates) |update| { + if (update.version.tag == .uninitialized) { + for (root_deps) |dep, i| { + if (dep.name_hash == String.Builder.stringHash(update.name)) { + const old_resolution = old_resolutions[i]; + if (old_resolution > old.packages.len) continue; + const res = resolutions_of_yore[old_resolution]; + const len = std.fmt.count("^{}", .{res.value.npm.fmt(old.buffers.string_bytes.items)}); + if (len >= String.max_inline_len) { + string_builder.cap += len; + } + } + } + } + } + + try string_builder.allocate(); + defer string_builder.clamp(); + var full_buf = string_builder.ptr.?[0 .. string_builder.cap + old.buffers.string_bytes.items.len]; + var temp_buf: [513]u8 = undefined; + + for (updates) |update, update_i| { + if (update.version.tag == .uninitialized) { + // prevent the deduping logic + updates[update_i].e_string = null; + + for (root_deps) |dep, i| { + if (dep.name_hash == String.Builder.stringHash(update.name)) { + const old_resolution = old_resolutions[i]; + if (old_resolution > old.packages.len) continue; + const res = resolutions_of_yore[old_resolution]; + var buf = std.fmt.bufPrint(&temp_buf, "^{}", .{res.value.npm.fmt(old.buffers.string_bytes.items)}) catch break; + const external_version = string_builder.append(ExternalString, buf); + const sliced = external_version.value.sliced( + old.buffers.string_bytes.items, + ); + root_deps[i].version = Dependency.parse( + old.allocator, + sliced.slice, + &sliced, + null, + ) orelse Dependency.Version{}; + } + } + } + } + } + + // Deduplication works like this + // Go through *already* resolved package versions + // Ask, do any of those versions happen to match a lower version? + // If yes, choose that version instead. + // Why lower? + // + // Normally, the problem is looks like this: + // Package A: "react@^17" + // Package B: "react@17.0.1 + // + // Now you have two copies of React. + // When you really only wanted one. + // Since _typically_ the issue is that Semver ranges with "^" or "~" say "choose latest", we end up with latest + // if (options.enable.deduplicate_packages) { + // var resolutions: []PackageID = old.buffers.resolutions.items; + // const dependencies: []const Dependency = old.buffers.dependencies.items; + // const package_resolutions: []const Resolution = old.packages.items(.resolution); + // const string_buf = old.buffers.string_bytes.items; + + // const root_resolution = @as(usize, old.packages.items(.resolutions)[0].len); + + // const DedupeMap = std.ArrayHashMap(PackageNameHash, std.ArrayListUnmanaged([2]PackageID), ArrayIdentityContext(PackageNameHash), false); + // var dedupe_map = DedupeMap.initContext(allocator, .{}); + // try dedupe_map.ensureTotalCapacity(old.unique_packages.count()); + + // for (resolutions) |resolved_package_id, dep_i| { + // if (resolved_package_id < max_package_id and !old.unique_packages.isSet(resolved_package_id)) { + // const dependency = dependencies[dep_i]; + // if (dependency.version.tag == .npm) { + // var dedupe_entry = try dedupe_map.getOrPut(dependency.name_hash); + // if (!dedupe_entry.found_existing) dedupe_entry.value_ptr.* = .{}; + // try dedupe_entry.value_ptr.append(allocator, [2]PackageID{ dep_i, resolved_package_id }); + // } + // } + // } + // } + + var new = try old.allocator.create(Lockfile); + try new.initEmpty( + old.allocator, + ); + try new.string_pool.ensureTotalCapacity(old.string_pool.capacity()); + try new.package_index.ensureTotalCapacity(old.package_index.capacity()); + try new.packages.ensureTotalCapacity(old.allocator, old.packages.len); + try new.buffers.preallocate(old.buffers, old.allocator); + + const InstallOrder = struct { + parent: PackageID, + children: PackageIDSlice, + }; + + old.scratch.dependency_list_queue.head = 0; + + // Step 1. Recreate the lockfile with only the packages that are still alive + const root = old.rootPackage() orelse return error.NoPackage; + + var package_id_mapping = try old.allocator.alloc(PackageID, old.packages.len); + std.mem.set( + PackageID, + package_id_mapping, + invalid_package_id, + ); + var clone_queue_ = PendingResolutions.init(old.allocator); + new.unique_packages = try Bitset.initEmpty(old.unique_packages.bit_length, old.allocator); + var cloner = Cloner{ + .old = old, + .lockfile = new, + .mapping = package_id_mapping, + .clone_queue = clone_queue_, + }; + // try clone_queue.ensureUnusedCapacity(root.dependencies.len); + _ = try root.clone(old, new, package_id_mapping, &cloner); + + // When you run `"bun add react" + // This is where we update it in the lockfile from "latest" to "^17.0.2" + + try cloner.flush(); + + // Don't allow invalid memory to happen + if (updates.len > 0) { + const dep_list = new.packages.items(.dependencies)[0]; + const res_list = new.packages.items(.resolutions)[0]; + const root_deps: []const Dependency = dep_list.get(new.buffers.dependencies.items); + const new_resolutions: []const PackageID = res_list.get(new.buffers.resolutions.items); + + for (updates) |update, update_i| { + if (update.version.tag == .uninitialized) { + for (root_deps) |dep, i| { + if (dep.name_hash == String.Builder.stringHash(update.name)) { + if (new_resolutions[i] > new.packages.len) continue; + updates[update_i].version_buf = new.buffers.string_bytes.items; + updates[update_i].version = dep.version; + updates[update_i].resolved_version_buf = new.buffers.string_bytes.items; + updates[update_i].missing_version = true; + } + } + } + } + } + + return new; + } + + pub const TreeFiller = std.fifo.LinearFifo([2]PackageID, .Dynamic); + + const Cloner = struct { + clone_queue: PendingResolutions, + lockfile: *Lockfile, + old: *Lockfile, + mapping: []PackageID, + trees: Tree.List = Tree.List{}, + trees_count: u32 = 1, + + pub fn flush(this: *Cloner) anyerror!void { + const max_package_id = this.old.packages.len; + while (this.clone_queue.popOrNull()) |to_clone_| { + const to_clone: PendingResolution = to_clone_; + + const mapping = this.mapping[to_clone.old_resolution]; + if (mapping < max_package_id) { + this.lockfile.buffers.resolutions.items[to_clone.resolve_id] = this.mapping[to_clone.old_resolution]; + continue; + } + + const old_package = this.old.packages.get(to_clone.old_resolution); + + this.lockfile.buffers.resolutions.items[to_clone.resolve_id] = try old_package.clone( + this.old, + this.lockfile, + this.mapping, + this, + ); + } + + try this.hoist(); + } + + fn hoist(this: *Cloner) anyerror!void { + const max = @truncate(PackageID, this.lockfile.packages.len); + if (max == 0) return; + var allocator = this.lockfile.allocator; + + var tree_list = Tree.Builder.ArrayList{}; + + var slice = this.lockfile.packages.slice(); + const unique_packages = this.lockfile.unique_packages; + + var resolutions_lists: []const PackageIDSlice = slice.items(.resolutions); + const name_hashes: []const PackageNameHash = slice.items(.name_hash); + const resolutions_buffer: []const PackageID = this.lockfile.buffers.resolutions.items; + // populate the root of the tree with: + // - packages where only one version exists in the tree and they have no dependencies + // - dependencies from package.json + // Dependencies from package.json must always be put into the tree + + var root_packages_count: u32 = resolutions_lists[0].len; + for (resolutions_lists[1..]) |list, package_id| { + if (list.len > 0 or !unique_packages.isSet(package_id + 1)) continue; + root_packages_count += 1; + } + + var root_package_list = try PackageIDList.initCapacity(allocator, root_packages_count); + const root_resolutions: []const PackageID = resolutions_lists[0].get(resolutions_buffer); + + try tree_list.ensureTotalCapacity(allocator, root_packages_count); + tree_list.len = root_packages_count; + + for (resolutions_lists[1..]) |list, package_id_| { + const package_id = @intCast(PackageID, package_id_ + 1); + if (list.len > 0 or + !unique_packages.isSet(package_id) or + std.mem.indexOfScalar(PackageID, root_package_list.items, package_id) != null) + continue; + root_package_list.appendAssumeCapacity(package_id); + } + + var tree_filler_queue: TreeFiller = TreeFiller.init(allocator); + try tree_filler_queue.ensureUnusedCapacity(root_resolutions.len); + + var possible_duplicates_len = root_package_list.items.len; + for (root_resolutions) |package_id| { + if (package_id >= max) continue; + if (std.mem.indexOfScalar(PackageID, root_package_list.items[0..possible_duplicates_len], package_id) != null) continue; + + root_package_list.appendAssumeCapacity(package_id); + } + { + var sliced = tree_list.slice(); + var trees = sliced.items(.tree); + var packages = sliced.items(.packages); + trees[0] = .{ + .parent = Tree.invalid_id, + .id = 0, + .packages = .{ + .len = @truncate(PackageID, root_package_list.items.len), + }, + }; + packages[0] = root_package_list; + + std.mem.set(PackageIDList, packages[1..], PackageIDList{}); + std.mem.set(Tree, trees[1..], Tree{}); + } + + var builder = Tree.Builder{ + .name_hashes = name_hashes, + .list = tree_list, + .queue = tree_filler_queue, + .resolution_lists = resolutions_lists, + .resolutions = resolutions_buffer, + .allocator = allocator, + .dependencies = this.lockfile.buffers.dependencies.items, + }; + var builder_ = &builder; + + for (root_resolutions) |package_id| { + if (package_id >= max) continue; + + try builder.list.items(.tree)[0].processSubtree( + package_id, + builder_, + ); + } + + // This goes breadth-first + while (builder.queue.readItem()) |pids| { + try builder.list.items(.tree)[pids[1]].processSubtree(pids[0], builder_); + } + + var tree_packages = try builder.clean(); + this.lockfile.buffers.hoisted_packages = Lockfile.PackageIDList{ + .items = tree_packages, + .capacity = tree_packages.len, + }; + { + const final = builder.list.items(.tree); + this.lockfile.buffers.trees = Tree.List{ + .items = final, + .capacity = final.len, + }; + } + } + }; + + const PendingResolution = struct { + old_resolution: PackageID, + resolve_id: PackageID, + parent: PackageID, + }; + + const PendingResolutions = std.ArrayList(PendingResolution); + + pub const Printer = struct { + lockfile: *Lockfile, + options: PackageManager.Options, + successfully_installed: ?std.DynamicBitSetUnmanaged = null, + + pub const Format = enum { yarn }; + + var lockfile_path_buf1: [std.fs.MAX_PATH_BYTES]u8 = undefined; + var lockfile_path_buf2: [std.fs.MAX_PATH_BYTES]u8 = undefined; + + pub fn print( + allocator: *std.mem.Allocator, + log: *logger.Log, + lockfile_path_: string, + format: Format, + ) !void { + var lockfile_path: stringZ = undefined; + + if (!std.fs.path.isAbsolute(lockfile_path_)) { + var cwd = try std.os.getcwd(&lockfile_path_buf1); + var parts = [_]string{lockfile_path_}; + var lockfile_path__ = resolve_path.joinAbsStringBuf(cwd, &lockfile_path_buf2, &parts, .auto); + lockfile_path_buf2[lockfile_path__.len] = 0; + lockfile_path = lockfile_path_buf2[0..lockfile_path__.len :0]; + } else { + std.mem.copy(u8, &lockfile_path_buf1, lockfile_path); + lockfile_path_buf1[lockfile_path_.len] = 0; + lockfile_path = lockfile_path_buf1[0..lockfile_path_.len :0]; + } + + std.os.chdir(std.fs.path.dirname(lockfile_path) orelse "/") catch {}; + + _ = try FileSystem.init1(allocator, null); + + var lockfile = try allocator.create(Lockfile); + + const load_from_disk = lockfile.loadFromDisk(allocator, log, lockfile_path); + switch (load_from_disk) { + .err => |cause| { + switch (cause.step) { + .open_file => Output.prettyErrorln("<r><red>error opening lockfile:<r> {s}.", .{ + @errorName(cause.value), + }), + .parse_file => Output.prettyErrorln("<r><red>error parsing lockfile:<r> {s}", .{ + @errorName(cause.value), + }), + .read_file => Output.prettyErrorln("<r><red>error reading lockfile:<r> {s}", .{ + @errorName(cause.value), + }), + } + if (log.errors > 0) { + if (Output.enable_ansi_colors) { + try log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true); + } else { + try log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false); + } + } + Output.flush(); + std.os.exit(1); + return; + }, + .not_found => { + Output.prettyErrorln("<r><red>lockfile not found:<r> {s}", .{ + std.mem.span(lockfile_path), + }); + Output.flush(); + std.os.exit(1); + return; + }, + + .ok => {}, + } + + var writer = Output.writer(); + try printWithLockfile(allocator, lockfile, format, @TypeOf(writer), writer); + Output.flush(); + } + + pub fn printWithLockfile( + allocator: *std.mem.Allocator, + lockfile: *Lockfile, + format: Format, + comptime Writer: type, + writer: Writer, + ) !void { + var fs = &FileSystem.instance; + var options = PackageManager.Options{}; + + var entries_option = try fs.fs.readDirectory(fs.top_level_dir, null); + + var env_loader: *DotEnv.Loader = brk: { + var map = try allocator.create(DotEnv.Map); + map.* = DotEnv.Map.init(allocator); + + var loader = try allocator.create(DotEnv.Loader); + loader.* = DotEnv.Loader.init(map, allocator); + break :brk loader; + }; + + env_loader.loadProcess(); + try env_loader.load(&fs.fs, &entries_option.entries, false); + var log = logger.Log.init(allocator); + try options.load( + allocator, + &log, + env_loader, + null, + ); + + var printer = Printer{ + .lockfile = lockfile, + .options = options, + }; + + switch (format) { + .yarn => { + try Yarn.print(&printer, Writer, writer); + }, + } + } + + pub const Tree = struct { + pub fn print( + this: *Printer, + comptime Writer: type, + writer: Writer, + comptime enable_ansi_colors: bool, + ) !void { + var lockfile = this.lockfile; + + const IDDepthPair = struct { + depth: u16 = 0, + id: PackageID, + }; + + var visited = try Bitset.initEmpty(this.lockfile.packages.len, this.lockfile.allocator); + + var slice = this.lockfile.packages.slice(); + const names: []const String = slice.items(.name); + const resolved: []const Resolution = slice.items(.resolution); + const metas: []const Lockfile.Package.Meta = slice.items(.meta); + if (names.len == 0) return; + const dependency_lists = slice.items(.dependencies); + const resolutions_list = slice.items(.resolutions); + const resolutions_buffer = this.lockfile.buffers.resolutions.items; + const dependencies_buffer = this.lockfile.buffers.dependencies.items; + const package_count = @truncate(PackageID, names.len); + const string_buf = this.lockfile.buffers.string_bytes.items; + + const root = this.lockfile.rootPackage() orelse return; + visited.set(0); + const end = @truncate(PackageID, names.len); + + if (this.successfully_installed) |installed| { + for (resolutions_list[0].get(resolutions_buffer)) |package_id| { + if (package_id > end or !installed.isSet(package_id)) continue; + + const package_name = names[package_id].slice(string_buf); + + const dependency_list = dependency_lists[package_id]; + + const fmt = comptime brk: { + if (enable_ansi_colors) { + break :brk Output.prettyFmt("<r> <green>+<r> <b>{s}<r><d>@{}<r>\n", enable_ansi_colors); + } else { + break :brk Output.prettyFmt("<r> + {s}<r><d>@{}<r>\n", enable_ansi_colors); + } + }; + + try writer.print( + fmt, + .{ + package_name, + resolved[package_id].fmt(string_buf), + }, + ); + } + } else { + for (names) |name, package_id| { + const package_name = name.slice(string_buf); + + const dependency_list = dependency_lists[package_id]; + + try writer.print( + comptime Output.prettyFmt(" <r><b>{s}<r><d>@<b>{}<r>\n", enable_ansi_colors), + .{ + package_name, + resolved[package_id].fmt(string_buf), + }, + ); + } + } + + for (resolutions_list) |list, parent_id| { + for (list.get(resolutions_buffer)) |package_id, j| { + if (package_id >= end) { + const failed_dep: Dependency = dependency_lists[parent_id].get(dependencies_buffer)[j]; + if (failed_dep.behavior.isOptional() or failed_dep.behavior.isPeer() or (failed_dep.behavior.isDev() and parent_id > 0)) continue; + + try writer.print( + comptime Output.prettyFmt( + "<r><red>ERROR<r><d>:<r> <b>{s}<r><d>@<b>{}<r><d> failed to resolve\n", + enable_ansi_colors, + ), + .{ + failed_dep.name.slice(string_buf), + failed_dep.version.literal.fmt(string_buf), + }, + ); + continue; + } + } + } + } + }; + + pub const Yarn = struct { + pub fn print( + this: *Printer, + comptime Writer: type, + writer: Writer, + ) !void { + try writer.writeAll( + \\# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. + \\# yarn lockfile v1 + \\ + \\ + ); + + try Yarn.packages(this, Writer, writer); + } + + pub fn packages( + this: *Printer, + comptime Writer: type, + writer: Writer, + ) !void { + var slice = this.lockfile.packages.slice(); + const names: []const String = slice.items(.name); + const resolved: []const Resolution = slice.items(.resolution); + const metas: []const Lockfile.Package.Meta = slice.items(.meta); + if (names.len == 0) return; + const dependency_lists = slice.items(.dependencies); + const resolutions_list = slice.items(.resolutions); + const resolutions_buffer = this.lockfile.buffers.resolutions.items; + const dependencies_buffer = this.lockfile.buffers.dependencies.items; + const RequestedVersion = std.HashMap(PackageID, []Dependency.Version, IdentityContext(PackageID), 80); + var requested_versions = RequestedVersion.init(this.lockfile.allocator); + var all_requested_versions = try this.lockfile.allocator.alloc(Dependency.Version, resolutions_buffer.len); + defer this.lockfile.allocator.free(all_requested_versions); + const package_count = @truncate(PackageID, names.len); + var alphabetized_names = try this.lockfile.allocator.alloc(PackageID, package_count - 1); + defer this.lockfile.allocator.free(alphabetized_names); + + const string_buf = this.lockfile.buffers.string_bytes.items; + + // First, we need to build a map of all requested versions + // This is so we can print requested versions + { + var i: PackageID = 1; + while (i < package_count) : (i += 1) { + alphabetized_names[i - 1] = @truncate(PackageID, i); + + var resolutions = resolutions_buffer; + var dependencies = dependencies_buffer; + + var j: PackageID = 0; + var requested_version_start = all_requested_versions; + while (std.mem.indexOfScalar(PackageID, resolutions, i)) |k| { + j += 1; + + all_requested_versions[0] = dependencies[k].version; + all_requested_versions = all_requested_versions[1..]; + + dependencies = dependencies[k + 1 ..]; + resolutions = resolutions[k + 1 ..]; + } + + var dependency_versions = requested_version_start[0..j]; + if (dependency_versions.len > 1) std.sort.insertionSort(Dependency.Version, dependency_versions, string_buf, Dependency.Version.isLessThan); + try requested_versions.put(i, dependency_versions); + } + } + + std.sort.sort( + PackageID, + alphabetized_names, + Lockfile.Package.Alphabetizer{ + .names = names, + .buf = string_buf, + .resolutions = resolved, + }, + Lockfile.Package.Alphabetizer.isAlphabetical, + ); + + // When printing, we start at 1 + for (alphabetized_names) |i| { + const name = names[i].slice(string_buf); + const resolution = resolved[i]; + const meta = metas[i]; + const dependencies: []const Dependency = dependency_lists[i].get(dependencies_buffer); + + // This prints: + // "@babel/core@7.9.0": + { + try writer.writeAll("\n"); + + const dependency_versions = requested_versions.get(i).?; + const always_needs_quote = name[0] == '@'; + + for (dependency_versions) |dependency_version, j| { + if (j > 0) { + try writer.writeAll(", "); + } + const version_name = dependency_version.literal.slice(string_buf); + const needs_quote = always_needs_quote or std.mem.indexOfAny(u8, version_name, " |\t-/!") != null; + + if (needs_quote) { + try writer.writeByte('"'); + } + + try writer.writeAll(name); + try writer.writeByte('@'); + try writer.writeAll(version_name); + + if (needs_quote) { + try writer.writeByte('"'); + } + } + + try writer.writeAll(":\n"); + } + + { + try writer.writeAll(" version "); + + const version_formatter = resolution.fmt(string_buf); + + // Version is always quoted + try std.fmt.format(writer, "\"{any}\"\n", .{version_formatter}); + + try writer.writeAll(" resolved "); + + const url_formatter = resolution.fmtURL(&this.options, name, string_buf); + + // Resolved URL is always quoted + try std.fmt.format(writer, "\"{any}\"\n", .{url_formatter}); + + if (meta.integrity.tag != .unknown) { + // Integrity is...never quoted? + try std.fmt.format(writer, " integrity {any}\n", .{&meta.integrity}); + } + + if (dependencies.len > 0) { + var behavior = Behavior.uninitialized; + var dependency_behavior_change_count: u8 = 0; + for (dependencies) |dep, j| { + if (dep.behavior != behavior) { + if (dep.behavior.isOptional()) { + try writer.writeAll(" optionalDependencies:\n"); + if (comptime Environment.isDebug or Environment.isTest) dependency_behavior_change_count += 1; + } else if (dep.behavior.isNormal()) { + try writer.writeAll(" dependencies:\n"); + if (comptime Environment.isDebug or Environment.isTest) dependency_behavior_change_count += 1; + } else if (dep.behavior.isDev()) { + try writer.writeAll(" devDependencies:\n"); + if (comptime Environment.isDebug or Environment.isTest) dependency_behavior_change_count += 1; + } else { + continue; + } + behavior = dep.behavior; + + // assert its sorted + if (comptime Environment.isDebug or Environment.isTest) std.debug.assert(dependency_behavior_change_count < 3); + } + + try writer.writeAll(" "); + const dependency_name = dep.name.slice(string_buf); + const needs_quote = dependency_name[0] == '@'; + if (needs_quote) { + try writer.writeByte('"'); + } + try writer.writeAll(dependency_name); + if (needs_quote) { + try writer.writeByte('"'); + } + try writer.writeAll(" \""); + try writer.writeAll(dep.version.literal.slice(string_buf)); + try writer.writeAll("\"\n"); + } + } + } + } + } + }; + }; + + pub fn verify(this: *Lockfile) !void { + std.debug.assert(this.format == .v0); + { + var i: usize = 0; + while (i < this.packages.len) : (i += 1) { + const package: Lockfile.Package = this.packages.get(i); + std.debug.assert(this.str(package.name).len == @as(usize, package.name.len())); + std.debug.assert(stringHash(this.str(package.name)) == @as(usize, package.name_hash)); + std.debug.assert(package.dependencies.get(this.buffers.dependencies.items).len == @as(usize, package.dependencies.len)); + std.debug.assert(package.resolutions.get(this.buffers.resolutions.items).len == @as(usize, package.resolutions.len)); + std.debug.assert(package.resolutions.get(this.buffers.resolutions.items).len == @as(usize, package.dependencies.len)); + const dependencies = package.dependencies.get(this.buffers.dependencies.items); + for (dependencies) |dependency| { + std.debug.assert(this.str(dependency.name).len == @as(usize, dependency.name.len())); + std.debug.assert(stringHash(this.str(dependency.name)) == dependency.name_hash); + } + } + } + } + + pub fn saveToDisk(this: *Lockfile, filename: stringZ) void { + if (comptime Environment.isDebug) { + this.verify() catch |err| { + Output.prettyErrorln("<r><red>error:<r> failed to verify lockfile: {s}", .{@errorName(err)}); + Output.flush(); + Global.crash(); + }; + } + std.debug.assert(FileSystem.instance_loaded); + var tmpname_buf: [512]u8 = undefined; + tmpname_buf[0..8].* = "bunlock-".*; + var tmpfile = FileSystem.RealFS.Tmpfile{}; + var secret: [32]u8 = undefined; + std.mem.writeIntNative(u64, secret[0..8], @intCast(u64, std.time.milliTimestamp())); + var rng = std.rand.Gimli.init(secret); + var base64_bytes: [64]u8 = undefined; + rng.random.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("<r><red>error:<r> failed to open lockfile: {s}", .{@errorName(err)}); + Output.flush(); + Global.crash(); + }; + + var file = tmpfile.file(); + + Lockfile.Serializer.save(this, std.fs.File, file) catch |err| { + tmpfile.dir().deleteFileZ(tmpname) catch {}; + Output.prettyErrorln("<r><red>error:<r> failed to serialize lockfile: {s}", .{@errorName(err)}); + Output.flush(); + Global.crash(); + }; + + _ = C.fchmod( + tmpfile.fd, + // chmod 777 + 0000010 | 0000100 | 0000001 | 0001000 | 0000040 | 0000004 | 0000002 | 0000400 | 0000200 | 0000020, + ); + + tmpfile.promote(tmpname, std.fs.cwd().fd, filename) catch |err| { + tmpfile.dir().deleteFileZ(tmpname) catch {}; + Output.prettyErrorln("<r><red>error:<r> failed to save lockfile: {s}", .{@errorName(err)}); + Output.flush(); + Global.crash(); + }; + } + + pub fn rootPackage(this: *Lockfile) ?Lockfile.Package { + if (this.packages.len == 0) { + return null; + } + + return this.packages.get(0); + } + + pub inline fn str(this: *Lockfile, slicable: anytype) string { + return slicable.slice(this.buffers.string_bytes.items); + } + + pub inline fn cloneString(this: *Lockfile, slicable: anytype, from: *Lockfile) string { + // const slice = from.str(slicable); + // if (this.string_pool) { + + // } + } + + pub fn initEmpty(this: *Lockfile, allocator: *std.mem.Allocator) !void { + this.* = Lockfile{ + .format = .v0, + .packages = Lockfile.Package.List{}, + .buffers = Buffers{}, + .package_index = PackageIndex.Map.initContext(allocator, .{}), + .unique_packages = try Bitset.initFull(0, allocator), + .string_pool = StringPool.init(allocator), + .allocator = allocator, + .scratch = Scratch.init(allocator), + }; + } + + pub fn getPackageID( + this: *Lockfile, + name_hash: u64, + // if it's a peer dependency + version: ?Dependency.Version, + resolution: Resolution, + ) ?PackageID { + const entry = this.package_index.get(name_hash) orelse return null; + const resolutions: []const Resolution = this.packages.items(.resolution); + switch (entry) { + .PackageID => |id| { + if (comptime Environment.isDebug or Environment.isTest) { + std.debug.assert(id != invalid_package_id); + std.debug.assert(id != invalid_package_id - 1); + } + + if (resolutions[id].eql( + resolution, + this.buffers.string_bytes.items, + this.buffers.string_bytes.items, + )) { + return id; + } else if (version) |version_| { + switch (version_.tag) { + .npm => { + // is it a peerDependency satisfied by a parent package? + if (version_.value.npm.satisfies(resolutions[id].value.npm)) { + return id; + } + }, + else => return null, + } + } + }, + .PackageIDMultiple => |multi_| { + const multi = std.mem.span(multi_); + + const can_satisfy = version != null and version.?.tag == .npm; + + for (multi) |id| { + if (comptime Environment.isDebug or Environment.isTest) { + std.debug.assert(id != invalid_package_id); + } + + if (id == invalid_package_id - 1) return null; + + if (resolutions[id].eql(resolution, this.buffers.string_bytes.items, this.buffers.string_bytes.items)) { + return id; + } + + if (can_satisfy and version.?.value.npm.satisfies(resolutions[id].value.npm)) { + return id; + } + } + }, + } + + return null; + } + + pub fn getOrPutID(this: *Lockfile, id: PackageID, name_hash: PackageNameHash) !void { + if (this.unique_packages.capacity() < this.packages.len) try this.unique_packages.resize(this.packages.len, true, this.allocator); + var gpe = try this.package_index.getOrPut(name_hash); + + if (gpe.found_existing) { + var index: *PackageIndex.Entry = gpe.value_ptr; + + this.unique_packages.unset(id); + + switch (index.*) { + .PackageID => |single_| { + var ids = try this.allocator.alloc(PackageID, 8); + ids[0] = single_; + ids[1] = id; + this.unique_packages.unset(single_); + for (ids[2..7]) |_, i| { + ids[i + 2] = invalid_package_id - 1; + } + ids[7] = invalid_package_id; + // stage1 compiler doesn't like this + var ids_sentinel = ids.ptr[0 .. ids.len - 1 :invalid_package_id]; + index.* = .{ + .PackageIDMultiple = ids_sentinel, + }; + }, + .PackageIDMultiple => |ids_| { + var ids = std.mem.span(ids_); + for (ids) |id2, i| { + if (id2 == invalid_package_id - 1) { + ids[i] = id; + return; + } + } + + var new_ids = try this.allocator.alloc(PackageID, ids.len + 8); + defer this.allocator.free(ids); + std.mem.set(PackageID, new_ids, invalid_package_id - 1); + for (ids) |id2, i| { + new_ids[i] = id2; + } + new_ids[new_ids.len - 1] = invalid_package_id; + + // stage1 compiler doesn't like this + var new_ids_sentinel = new_ids.ptr[0 .. new_ids.len - 1 :invalid_package_id]; + index.* = .{ + .PackageIDMultiple = new_ids_sentinel, + }; + }, + } + } else { + gpe.value_ptr.* = .{ .PackageID = id }; + this.unique_packages.set(id); + } + } + + pub fn appendPackage(this: *Lockfile, package_: Lockfile.Package) !Lockfile.Package { + const id = @truncate(PackageID, this.packages.len); + return try appendPackageWithID(this, package_, id); + } + + pub fn appendPackageWithID(this: *Lockfile, package_: Lockfile.Package, id: PackageID) !Lockfile.Package { + defer { + if (comptime Environment.isDebug) { + std.debug.assert(this.getPackageID(package_.name_hash, null, package_.resolution) != null); + std.debug.assert(this.getPackageID(package_.name_hash, null, package_.resolution).? == id); + } + } + var package = package_; + package.meta.id = id; + try this.packages.append(this.allocator, package); + try this.getOrPutID(id, package.name_hash); + + return package; + } + + const StringPool = String.Builder.StringPool; + + pub inline fn stringHash(in: []const u8) u64 { + return std.hash.Wyhash.hash(0, in); + } + + pub inline fn stringBuilder(this: *Lockfile) Lockfile.StringBuilder { + return Lockfile.StringBuilder{ + .lockfile = this, + }; + } + + pub const Scratch = struct { + pub const DuplicateCheckerMap = std.HashMap(PackageNameHash, logger.Loc, IdentityContext(PackageNameHash), 80); + pub const DependencyQueue = std.fifo.LinearFifo(DependencySlice, .Dynamic); + + duplicate_checker_map: DuplicateCheckerMap = undefined, + dependency_list_queue: DependencyQueue = undefined, + + pub fn init(allocator: *std.mem.Allocator) Scratch { + return Scratch{ + .dependency_list_queue = DependencyQueue.init(allocator), + .duplicate_checker_map = DuplicateCheckerMap.init(allocator), + }; + } + }; + + pub const StringBuilder = struct { + const Allocator = @import("std").mem.Allocator; + const assert = @import("std").debug.assert; + const copy = @import("std").mem.copy; + + len: usize = 0, + cap: usize = 0, + off: usize = 0, + ptr: ?[*]u8 = null, + lockfile: *Lockfile = undefined, + + pub inline fn count(this: *StringBuilder, slice: string) void { + if (String.canInline(slice)) return; + return countWithHash(this, slice, stringHash(slice)); + } + + pub inline fn countWithHash(this: *StringBuilder, slice: string, hash: u64) void { + if (String.canInline(slice)) return; + if (!this.lockfile.string_pool.contains(hash)) { + this.cap += slice.len; + } + } + + pub fn allocatedSlice(this: *StringBuilder) ![]u8 { + return this.ptr.?[0..this.cap]; + } + + pub fn clamp(this: *StringBuilder) void { + std.debug.assert(this.cap >= this.len); + + const excess = this.cap - this.len; + + if (excess > 0) + this.lockfile.buffers.string_bytes.items = this.lockfile.buffers.string_bytes.items[0 .. this.lockfile.buffers.string_bytes.items.len - excess]; + } + + pub fn allocate(this: *StringBuilder) !void { + var string_bytes = &this.lockfile.buffers.string_bytes; + try string_bytes.ensureUnusedCapacity(this.lockfile.allocator, this.cap); + const prev_len = string_bytes.items.len; + this.off = prev_len; + string_bytes.items = string_bytes.items.ptr[0 .. string_bytes.items.len + this.cap]; + this.ptr = string_bytes.items.ptr[prev_len .. prev_len + this.cap].ptr; + this.len = 0; + } + + pub fn append(this: *StringBuilder, comptime Type: type, slice: string) Type { + return @call(.{ .modifier = .always_inline }, appendWithHash, .{ this, Type, slice, stringHash(slice) }); + } + + // SlicedString is not supported due to inline strings. + pub fn appendWithoutPool(this: *StringBuilder, comptime Type: type, slice: string, hash: u64) Type { + if (String.canInline(slice)) { + switch (Type) { + String => { + return String.init(this.lockfile.buffers.string_bytes.items, slice); + }, + ExternalString => { + return ExternalString.init(this.lockfile.buffers.string_bytes.items, slice, hash); + }, + else => @compileError("Invalid type passed to StringBuilder"), + } + } + assert(this.len <= this.cap); // didn't count everything + assert(this.ptr != null); // must call allocate first + + copy(u8, this.ptr.?[this.len..this.cap], slice); + const final_slice = this.ptr.?[this.len..this.cap][0..slice.len]; + this.len += slice.len; + + assert(this.len <= this.cap); + + switch (Type) { + String => { + return String.init(this.lockfile.buffers.string_bytes.items, final_slice); + }, + ExternalString => { + return ExternalString.init(this.lockfile.buffers.string_bytes.items, final_slice, hash); + }, + else => @compileError("Invalid type passed to StringBuilder"), + } + } + + pub fn appendWithHash(this: *StringBuilder, comptime Type: type, slice: string, hash: u64) Type { + if (String.canInline(slice)) { + switch (Type) { + String => { + return String.init(this.lockfile.buffers.string_bytes.items, slice); + }, + ExternalString => { + return ExternalString.init(this.lockfile.buffers.string_bytes.items, slice, hash); + }, + else => @compileError("Invalid type passed to StringBuilder"), + } + } + + assert(this.len <= this.cap); // didn't count everything + assert(this.ptr != null); // must call allocate first + + var string_entry = this.lockfile.string_pool.getOrPut(hash) catch unreachable; + if (!string_entry.found_existing) { + copy(u8, this.ptr.?[this.len..this.cap], slice); + const final_slice = this.ptr.?[this.len..this.cap][0..slice.len]; + this.len += slice.len; + + string_entry.value_ptr.* = String.init(this.lockfile.buffers.string_bytes.items, final_slice); + } + + assert(this.len <= this.cap); + + switch (Type) { + String => { + return string_entry.value_ptr.*; + }, + ExternalString => { + return ExternalString{ + .value = string_entry.value_ptr.*, + .hash = hash, + }; + }, + else => @compileError("Invalid type passed to StringBuilder"), + } + } + }; + + pub const PackageIndex = struct { + pub const Map = std.HashMap(PackageNameHash, PackageIndex.Entry, IdentityContext(PackageNameHash), 80); + pub const Entry = union(Tag) { + PackageID: PackageID, + PackageIDMultiple: PackageIDMultiple, + + pub const Tag = enum(u8) { + PackageID = 0, + PackageIDMultiple = 1, + }; + }; + }; + + pub const FormatVersion = enum(u32) { + v0, + _, + }; + + pub const DependencySlice = ExternalSlice(Dependency); + pub const PackageIDSlice = ExternalSlice(PackageID); + pub const NodeModulesFolderSlice = ExternalSlice(NodeModulesFolder); + + pub const PackageIDList = std.ArrayListUnmanaged(PackageID); + pub const DependencyList = std.ArrayListUnmanaged(Dependency); + pub const StringBuffer = std.ArrayListUnmanaged(u8); + pub const SmallExternalStringBuffer = std.ArrayListUnmanaged(String); + + pub const Package = extern struct { + const DependencyGroup = struct { + prop: string, + field: string, + behavior: Behavior, + + pub const dependencies = DependencyGroup{ .prop = "dependencies", .field = "dependencies", .behavior = @intToEnum(Behavior, Behavior.normal) }; + pub const dev = DependencyGroup{ .prop = "devDependencies", .field = "dev_dependencies", .behavior = @intToEnum(Behavior, Behavior.dev) }; + pub const optional = DependencyGroup{ .prop = "optionalDependencies", .field = "optional_dependencies", .behavior = @intToEnum(Behavior, Behavior.optional) }; + pub const peer = DependencyGroup{ .prop = "peerDependencies", .field = "peer_dependencies", .behavior = @intToEnum(Behavior, Behavior.peer) }; + }; + + pub inline fn isDisabled(this: *const Lockfile.Package) bool { + return this.meta.isDisabled(); + } + + const Alphabetizer = struct { + names: []const String, + buf: []const u8, + resolutions: []const Resolution, + + pub fn isAlphabetical(ctx: Alphabetizer, lhs: PackageID, rhs: PackageID) bool { + return switch (ctx.names[lhs].order(&ctx.names[rhs], ctx.buf, ctx.buf)) { + .eq => ctx.resolutions[lhs].order(&ctx.resolutions[rhs], ctx.buf, ctx.buf) == .lt, + .lt => true, + .gt => false, + }; + } + }; + + pub fn clone( + this: *const Lockfile.Package, + old: *Lockfile, + new: *Lockfile, + package_id_mapping: []PackageID, + cloner: *Cloner, + ) !PackageID { + const old_string_buf = old.buffers.string_bytes.items; + var builder_ = new.stringBuilder(); + var builder = &builder_; + + builder.count(this.name.slice(old_string_buf)); + this.resolution.count(old_string_buf, *Lockfile.StringBuilder, builder); + this.meta.count(old_string_buf, *Lockfile.StringBuilder, builder); + this.bin.count(old_string_buf, *Lockfile.StringBuilder, builder); + + const old_dependencies: []const Dependency = this.dependencies.get(old.buffers.dependencies.items); + const old_resolutions: []const PackageID = this.resolutions.get(old.buffers.resolutions.items); + + for (old_dependencies) |dependency, i| { + dependency.count(old_string_buf, *Lockfile.StringBuilder, builder); + } + + try builder.allocate(); + + // should be unnecessary, but Just In Case + try new.buffers.dependencies.ensureUnusedCapacity(new.allocator, old_dependencies.len); + try new.buffers.resolutions.ensureUnusedCapacity(new.allocator, old_dependencies.len); + + const prev_len = @truncate(u32, new.buffers.dependencies.items.len); + const end = prev_len + @truncate(u32, old_dependencies.len); + const max_package_id = @truncate(PackageID, old.packages.len); + + new.buffers.dependencies.items = new.buffers.dependencies.items.ptr[0..end]; + new.buffers.resolutions.items = new.buffers.resolutions.items.ptr[0..end]; + + var dependencies: []Dependency = new.buffers.dependencies.items[prev_len..end]; + var resolutions: []PackageID = new.buffers.resolutions.items[prev_len..end]; + + const id = @truncate(PackageID, new.packages.len); + const new_package = try new.appendPackageWithID( + Lockfile.Package{ + .name = builder.appendWithHash( + String, + this.name.slice(old_string_buf), + this.name_hash, + ), + .bin = this.bin.clone(old_string_buf, *Lockfile.StringBuilder, builder), + .name_hash = this.name_hash, + .meta = this.meta.clone( + old_string_buf, + *Lockfile.StringBuilder, + builder, + ), + .resolution = this.resolution.clone( + old_string_buf, + *Lockfile.StringBuilder, + builder, + ), + .dependencies = .{ .off = prev_len, .len = end - prev_len }, + .resolutions = .{ .off = prev_len, .len = end - prev_len }, + }, + id, + ); + + package_id_mapping[this.meta.id] = new_package.meta.id; + + for (old_dependencies) |dependency, i| { + dependencies[i] = try dependency.clone( + old_string_buf, + *Lockfile.StringBuilder, + builder, + ); + } + + builder.clamp(); + + cloner.trees_count += @as(u32, @boolToInt(old_resolutions.len > 0)); + + for (old_resolutions) |old_resolution, i| { + if (old_resolution >= max_package_id) continue; + + const mapped = package_id_mapping[old_resolution]; + const resolve_id = new_package.resolutions.off + @intCast(PackageID, i); + + if (mapped < max_package_id) { + resolutions[i] = mapped; + } else { + try cloner.clone_queue.append( + PendingResolution{ + .old_resolution = old_resolution, + .parent = new_package.meta.id, + .resolve_id = resolve_id, + }, + ); + } + } + + return new_package.meta.id; + } + + pub fn fromNPM( + allocator: *std.mem.Allocator, + lockfile: *Lockfile, + log: *logger.Log, + manifest: *const Npm.PackageManifest, + version: Semver.Version, + package_version_ptr: *const Npm.PackageVersion, + string_buf: []const u8, + comptime features: Features, + ) !Lockfile.Package { + var npm_count: u32 = 0; + var package = Lockfile.Package{}; + + const package_version = package_version_ptr.*; + + const dependency_groups = comptime brk: { + var out_groups: [ + 1 + + @as(usize, @boolToInt(features.dev_dependencies)) + + @as(usize, @boolToInt(features.optional_dependencies)) + + @as(usize, @boolToInt(features.peer_dependencies)) + ]DependencyGroup = undefined; + var out_group_i: usize = 0; + + out_groups[out_group_i] = DependencyGroup.dependencies; + out_group_i += 1; + + if (features.dev_dependencies) { + out_groups[out_group_i] = DependencyGroup.dev; + out_group_i += 1; + } + + if (features.optional_dependencies) { + out_groups[out_group_i] = DependencyGroup.optional; + out_group_i += 1; + } + + if (features.peer_dependencies) { + out_groups[out_group_i] = DependencyGroup.peer; + out_group_i += 1; + } + + break :brk out_groups; + }; + + var string_builder = lockfile.stringBuilder(); + + var total_dependencies_count: u32 = 0; + + // --- Counting + { + string_builder.count(manifest.name()); + version.count(string_buf, @TypeOf(&string_builder), &string_builder); + + inline for (dependency_groups) |group| { + const map: ExternalStringMap = @field(package_version, group.field); + const keys = map.name.get(manifest.external_strings); + const version_strings = map.value.get(manifest.external_strings_for_versions); + total_dependencies_count += map.value.len; + + if (comptime Environment.isDebug) std.debug.assert(keys.len == version_strings.len); + + for (keys) |key, i| { + string_builder.count(key.slice(string_buf)); + string_builder.count(version_strings[i].slice(string_buf)); + } + } + + package_version.bin.count(string_buf, @TypeOf(&string_builder), &string_builder); + } + + try string_builder.allocate(); + defer string_builder.clamp(); + + var dependencies_list = &lockfile.buffers.dependencies; + var resolutions_list = &lockfile.buffers.resolutions; + try dependencies_list.ensureUnusedCapacity(lockfile.allocator, total_dependencies_count); + try resolutions_list.ensureUnusedCapacity(lockfile.allocator, total_dependencies_count); + + // -- Cloning + { + const package_name: ExternalString = string_builder.appendWithHash(ExternalString, manifest.name(), manifest.pkg.name.hash); + package.name_hash = package_name.hash; + package.name = package_name.value; + package.resolution = Resolution{ + .value = .{ + .npm = version.clone( + manifest.string_buf, + @TypeOf(&string_builder), + &string_builder, + ), + }, + .tag = .npm, + }; + + const total_len = dependencies_list.items.len + total_dependencies_count; + std.debug.assert(dependencies_list.items.len == resolutions_list.items.len); + + var dependencies: []Dependency = dependencies_list.items.ptr[dependencies_list.items.len..total_len]; + std.mem.set(Dependency, dependencies, Dependency{}); + + var start_dependencies = dependencies; + + const off = @truncate(u32, dependencies_list.items.len); + + inline for (dependency_groups) |group| { + const map: ExternalStringMap = @field(package_version, group.field); + const keys = map.name.get(manifest.external_strings); + const version_strings = map.value.get(manifest.external_strings_for_versions); + + if (comptime Environment.isDebug) std.debug.assert(keys.len == version_strings.len); + const is_peer = comptime strings.eqlComptime(group.field, "peer_dependencies"); + var i: usize = 0; + + list: while (i < keys.len) { + const key = keys[i]; + const version_string_ = version_strings[i]; + + // Duplicate peer & dev dependencies are promoted to whichever appeared first + // In practice, npm validates this so it shouldn't happen + if (comptime group.behavior.isPeer() or group.behavior.isDev()) { + for (start_dependencies[0 .. total_dependencies_count - dependencies.len]) |dependency, j| { + if (dependency.name_hash == key.hash) { + i += 1; + continue :list; + } + } + } + + const name: ExternalString = string_builder.appendWithHash(ExternalString, key.slice(string_buf), key.hash); + const dep_version = string_builder.appendWithHash(String, version_string_.slice(string_buf), version_string_.hash); + const sliced = dep_version.sliced(lockfile.buffers.string_bytes.items); + + const dependency = Dependency{ + .name = name.value, + .name_hash = name.hash, + .behavior = if (comptime is_peer) + group.behavior.setOptional(package_version.optional_peer_dependencies_len > i) + else + group.behavior, + .version = Dependency.parse( + allocator, + sliced.slice, + &sliced, + log, + ) orelse Dependency.Version{}, + }; + + // If a dependency appears in both "dependencies" and "optionalDependencies", it is considered optional! + if (comptime group.behavior.isOptional()) { + for (start_dependencies[0 .. total_dependencies_count - dependencies.len]) |dep, j| { + if (dep.name_hash == key.hash) { + // https://docs.npmjs.com/cli/v8/configuring-npm/package-json#optionaldependencies + // > Entries in optionalDependencies will override entries of the same name in dependencies, so it's usually best to only put in one place. + start_dependencies[j] = dep; + + i += 1; + continue :list; + } + } + } + + package.meta.npm_dependency_count += @as(u32, @boolToInt(dependency.version.tag.isNPM())); + + dependencies[0] = dependency; + dependencies = dependencies[1..]; + i += 1; + } + } + + package.bin = package_version.bin.clone(string_buf, @TypeOf(&string_builder), &string_builder); + + package.meta.arch = package_version.cpu; + package.meta.os = package_version.os; + package.meta.unpacked_size = package_version.unpacked_size; + package.meta.file_count = package_version.file_count; + + package.meta.integrity = package_version.integrity; + + package.dependencies.off = @truncate(u32, dependencies_list.items.len); + package.dependencies.len = total_dependencies_count - @truncate(u32, dependencies.len); + package.resolutions.off = package.dependencies.off; + package.resolutions.len = package.dependencies.len; + + const new_length = package.dependencies.len + dependencies_list.items.len; + + std.mem.set(PackageID, resolutions_list.items.ptr[package.dependencies.off .. package.dependencies.off + package.dependencies.len], invalid_package_id); + + dependencies_list.items = dependencies_list.items.ptr[0..new_length]; + resolutions_list.items = resolutions_list.items.ptr[0..new_length]; + + return package; + } + } + + pub const Diff = union(Op) { + add: Lockfile.Package.Diff.Entry, + remove: Lockfile.Package.Diff.Entry, + update: struct { in: PackageID, from: Dependency, to: Dependency, from_resolution: PackageID, to_resolution: PackageID }, + + pub const Entry = struct { in: PackageID, dependency: Dependency, resolution: PackageID }; + pub const Op = enum { + add, + remove, + update, + }; + + pub const List = std.fifo.LinearFifo(Diff, .Dynamic); + + pub const Summary = struct { + add: u32 = 0, + remove: u32 = 0, + update: u32 = 0, + deduped: u32 = 0, + + pub inline fn sum(this: *Summary, that: Summary) void { + this.add += that.add; + this.remove += that.remove; + this.update += that.update; + this.deduped += that.deduped; + } + }; + + pub fn generate( + allocator: *std.mem.Allocator, + from_lockfile: *Lockfile, + to_lockfile: *Lockfile, + from: *Lockfile.Package, + to: *Lockfile.Package, + mapping: []PackageID, + ) !Summary { + var summary = Summary{}; + const to_deps = to.dependencies.get(to_lockfile.buffers.dependencies.items); + const to_res = to.resolutions.get(to_lockfile.buffers.resolutions.items); + const from_res = from.resolutions.get(from_lockfile.buffers.resolutions.items); + const from_deps = from.dependencies.get(from_lockfile.buffers.dependencies.items); + + for (from_deps) |from_dep, i| { + // common case: dependency is present in both versions and in the same position + const to_i = if (to_deps.len > i and to_deps[i].name_hash == from_dep.name_hash) + i + else brk: { + // less common, o(n^2) case + for (to_deps) |to_dep, j| { + if (from_dep.name_hash == to_dep.name_hash) break :brk j; + } + + // We found a removed dependency! + // We don't need to remove it + // It will be cleaned up later + summary.remove += 1; + continue; + }; + + if (to_deps[to_i].eql(from_dep, from_lockfile.buffers.string_bytes.items, to_lockfile.buffers.string_bytes.items)) { + mapping[to_i] = @truncate(PackageID, i); + continue; + } + + // We found a changed dependency! + summary.update += 1; + } + + outer: for (to_deps) |to_dep, i| { + if (from_deps.len > i and from_deps[i].name_hash == to_dep.name_hash) continue; + + for (from_deps) |from_dep, j| { + if (from_dep.name_hash == to_dep.name_hash) continue :outer; + } + + summary.add += 1; + } + + return summary; + } + }; + + pub fn determinePreinstallState(this: *Lockfile.Package, lockfile: *Lockfile, manager: *PackageManager) PreinstallState { + switch (this.meta.preinstall_state) { + .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()) { + this.meta.preinstall_state = .done; + return .done; + } + + const folder_path = PackageManager.cachedNPMPackageFolderName(this.name.slice(lockfile.buffers.string_bytes.items), this.resolution.value.npm); + if (manager.isFolderInCache(folder_path)) { + this.meta.preinstall_state = .done; + return this.meta.preinstall_state; + } + + this.meta.preinstall_state = .extract; + return this.meta.preinstall_state; + }, + else => return this.meta.preinstall_state, + } + } + + pub fn hash(name: string, version: Semver.Version) u64 { + var hasher = std.hash.Wyhash.init(0); + hasher.update(name); + hasher.update(std.mem.asBytes(&version)); + return hasher.final(); + } + + pub fn parse( + lockfile: *Lockfile, + package: *Lockfile.Package, + allocator: *std.mem.Allocator, + log: *logger.Log, + source: logger.Source, + comptime features: Features, + ) !void { + initializeStore(); + + var json = json_parser.ParseJSON(&source, log, allocator) catch |err| { + if (Output.enable_ansi_colors) { + log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true) catch {}; + } else { + log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false) catch {}; + } + + Output.panic("<r><red>{s}<r> parsing package.json for <b>\"{s}\"<r>", .{ @errorName(err), source.path.prettyDir() }); + }; + + var string_builder = lockfile.stringBuilder(); + var total_dependencies_count: u32 = 0; + + package.meta.origin = if (features.is_main) .local else .npm; + + // -- Count the sizes + if (json.asProperty("name")) |name_q| { + if (name_q.expr.asString(allocator)) |name| { + string_builder.count(name); + } + } + + if (comptime !features.is_main) { + if (json.asProperty("version")) |version_q| { + if (version_q.expr.asString(allocator)) |version_str| { + string_builder.count(version_str); + } + } + } + + const dependency_groups = comptime brk: { + var out_groups: [ + 2 + + @as(usize, @boolToInt(features.optional_dependencies)) + + @as(usize, @boolToInt(features.peer_dependencies)) + ]DependencyGroup = undefined; + var out_group_i: usize = 0; + + out_groups[out_group_i] = DependencyGroup.dependencies; + out_group_i += 1; + + out_groups[out_group_i] = DependencyGroup.dev; + out_group_i += 1; + + if (features.optional_dependencies) { + out_groups[out_group_i] = DependencyGroup.optional; + out_group_i += 1; + } + + if (features.peer_dependencies) { + out_groups[out_group_i] = DependencyGroup.peer; + out_group_i += 1; + } + + break :brk out_groups; + }; + + inline for (dependency_groups) |group| { + if (json.asProperty(group.prop)) |dependencies_q| { + if (dependencies_q.expr.data == .e_object) { + for (dependencies_q.expr.data.e_object.properties) |item| { + string_builder.count(item.key.?.asString(allocator) orelse ""); + string_builder.count(item.value.?.asString(allocator) orelse ""); + } + total_dependencies_count += @truncate(u32, dependencies_q.expr.data.e_object.properties.len); + } + } + } + + try string_builder.allocate(); + try lockfile.buffers.dependencies.ensureUnusedCapacity(lockfile.allocator, total_dependencies_count); + try lockfile.buffers.resolutions.ensureUnusedCapacity(lockfile.allocator, total_dependencies_count); + + const total_len = lockfile.buffers.dependencies.items.len + total_dependencies_count; + std.debug.assert(lockfile.buffers.dependencies.items.len == lockfile.buffers.resolutions.items.len); + const off = lockfile.buffers.dependencies.items.len; + + var package_dependencies = lockfile.buffers.dependencies.items.ptr[off..total_len]; + var dependencies = package_dependencies; + + if (json.asProperty("name")) |name_q| { + if (name_q.expr.asString(allocator)) |name| { + const external_string = string_builder.append(ExternalString, name); + + package.name = external_string.value; + package.name_hash = external_string.hash; + } + } + + if (comptime !features.is_main) { + if (json.asProperty("version")) |version_q| { + if (version_q.expr.asString(allocator)) |version_str_| { + const version_str: String = string_builder.append(String, version_str_); + const sliced_string: SlicedString = version_str.sliced(string_buf.allocatedSlice()); + + const semver_version = Semver.Version.parse(sliced_string, allocator); + + if (semver_version.valid) { + package.resolution = .{ + .tag = .npm, + .value = .{ .npm = semver_version.version }, + }; + } else { + log.addErrorFmt(null, logger.Loc.Empty, allocator, "invalid version \"{s}\"", .{version_str}) catch unreachable; + } + } + } + } else { + package.resolution = .{ + .tag = .root, + .value = .{ .root = .{} }, + }; + } + + // It is allowed for duplicate dependencies to exist in optionalDependencies and regular dependencies + if (comptime features.check_for_duplicate_dependencies) { + lockfile.scratch.duplicate_checker_map.clearRetainingCapacity(); + try lockfile.scratch.duplicate_checker_map.ensureTotalCapacity(total_dependencies_count); + } + + inline for (dependency_groups) |group| { + if (json.asProperty(group.prop)) |dependencies_q| { + if (dependencies_q.expr.data == .e_object) { + const dependency_props: []const JSAst.G.Property = dependencies_q.expr.data.e_object.properties; + var i: usize = 0; + outer: while (i < dependency_props.len) { + const item = dependency_props[i]; + + const name_ = item.key.?.asString(allocator) orelse ""; + const version_ = item.value.?.asString(allocator) orelse ""; + + const external_name = string_builder.append(ExternalString, name_); + + const external_version = string_builder.append(String, version_); + + const sliced = external_version.sliced( + lockfile.buffers.string_bytes.items, + ); + + const dependency_version = Dependency.parse( + allocator, + sliced.slice, + &sliced, + log, + ) orelse Dependency.Version{}; + const this_dep = Dependency{ + .behavior = group.behavior, + .name = external_name.value, + .name_hash = external_name.hash, + .version = dependency_version, + }; + + if (comptime features.check_for_duplicate_dependencies) { + var entry = lockfile.scratch.duplicate_checker_map.getOrPutAssumeCapacity(external_name.hash); + if (entry.found_existing) { + // duplicate dependencies are allowed in optionalDependencies + if (comptime group.behavior.isOptional()) { + for (package_dependencies[0 .. package_dependencies.len - dependencies.len]) |package_dep, j| { + if (package_dep.name_hash == this_dep.name_hash) { + package_dependencies[j] = this_dep; + break; + } + } + + i += 1; + continue :outer; + } else { + var notes = try allocator.alloc(logger.Data, 1); + + notes[0] = logger.Data{ + .text = try std.fmt.allocPrint(lockfile.allocator, "\"{s}\" originally specified here", .{name_}), + .location = logger.Location.init_or_nil(&source, source.rangeOfString(entry.value_ptr.*)), + }; + + try log.addRangeErrorFmtWithNotes( + &source, + source.rangeOfString(item.key.?.loc), + lockfile.allocator, + notes, + "Duplicate dependency: \"{s}\" specified in package.json", + .{name_}, + ); + } + } + + entry.value_ptr.* = item.value.?.loc; + } + + dependencies[0] = this_dep; + package.meta.npm_dependency_count += @as(u32, @boolToInt(dependency_version.tag.isNPM())); + dependencies = dependencies[1..]; + i += 1; + } + } + } + } + + std.sort.sort( + Dependency, + package_dependencies[0 .. package_dependencies.len - dependencies.len], + lockfile.buffers.string_bytes.items, + Dependency.isLessThan, + ); + + total_dependencies_count -= @truncate(u32, dependencies.len); + package.dependencies.off = @truncate(u32, off); + package.dependencies.len = @truncate(u32, total_dependencies_count); + + package.resolutions = @bitCast(@TypeOf(package.resolutions), package.dependencies); + + std.mem.set(PackageID, lockfile.buffers.resolutions.items.ptr[off..total_len], invalid_package_id); + + lockfile.buffers.dependencies.items = lockfile.buffers.dependencies.items.ptr[0 .. lockfile.buffers.dependencies.items.len + total_dependencies_count]; + lockfile.buffers.resolutions.items = lockfile.buffers.resolutions.items.ptr[0..lockfile.buffers.dependencies.items.len]; + } + + pub const List = std.MultiArrayList(Lockfile.Package); + + pub const Meta = extern struct { + preinstall_state: PreinstallState = PreinstallState.unknown, + + origin: Origin = Origin.npm, + arch: Npm.Architecture = Npm.Architecture.all, + os: Npm.OperatingSystem = Npm.OperatingSystem.all, + + file_count: u32 = 0, + npm_dependency_count: u32 = 0, + id: PackageID = invalid_package_id, + + man_dir: String = String{}, + unpacked_size: u64 = 0, + integrity: Integrity = Integrity{}, + + pub fn isDisabled(this: *const Meta) bool { + return !this.arch.isMatch() or !this.os.isMatch(); + } + + pub fn count(this: *const Meta, buf: []const u8, comptime StringBuilderType: type, builder: StringBuilderType) void { + builder.count(this.man_dir.slice(buf)); + } + + pub fn clone(this: *const Meta, buf: []const u8, comptime StringBuilderType: type, builder: StringBuilderType) Meta { + var new = this.*; + new.id = invalid_package_id; + new.man_dir = builder.append(String, this.man_dir.slice(buf)); + + return new; + } + }; + + name: String = String{}, + name_hash: PackageNameHash = 0, + resolution: Resolution = Resolution{}, + dependencies: DependencySlice = DependencySlice{}, + resolutions: PackageIDSlice = PackageIDSlice{}, + meta: Meta = Meta{}, + bin: Bin = Bin{}, + + pub const Serializer = struct { + pub const sizes = blk: { + const fields = std.meta.fields(Lockfile.Package); + const Data = struct { + size: usize, + size_index: usize, + alignment: usize, + Type: type, + }; + var data: [fields.len]Data = undefined; + for (fields) |field_info, i| { + data[i] = .{ + .size = @sizeOf(field_info.field_type), + .size_index = i, + .Type = field_info.field_type, + .alignment = if (@sizeOf(field_info.field_type) == 0) 1 else field_info.alignment, + }; + } + const Sort = struct { + fn lessThan(trash: *i32, comptime lhs: Data, comptime rhs: Data) bool { + _ = trash; + return lhs.alignment > rhs.alignment; + } + }; + var trash: i32 = undefined; // workaround for stage1 compiler bug + std.sort.sort(Data, &data, &trash, Sort.lessThan); + var sizes_bytes: [fields.len]usize = undefined; + var field_indexes: [fields.len]usize = undefined; + var Types: [fields.len]type = undefined; + for (data) |elem, i| { + sizes_bytes[i] = elem.size; + field_indexes[i] = elem.size_index; + Types[i] = elem.Type; + } + break :blk .{ + .bytes = sizes_bytes, + .fields = field_indexes, + .Types = Types, + }; + }; + + const FieldsEnum = @typeInfo(Lockfile.Package.List.Field).Enum; + + pub fn byteSize(list: Lockfile.Package.List) usize { + const sizes_vector: std.meta.Vector(sizes.bytes.len, usize) = sizes.bytes; + const capacity_vector = @splat(sizes.bytes.len, list.len); + return @reduce(.Add, capacity_vector * sizes_vector); + } + + const AlignmentType = sizes.Types[sizes.fields[0]]; + + pub fn save(list: Lockfile.Package.List, comptime StreamType: type, stream: StreamType, comptime Writer: type, writer: Writer) !void { + const bytes = list.bytes[0..byteSize(list)]; + try writer.writeIntLittle(u64, bytes.len); + try writer.writeIntLittle(u64, list.len); + // _ = try Aligner.write(AlignmentType, Writer, writer, try stream.getPos()); + var slice = list.slice(); + inline for (sizes.fields) |field_index| { + const Type = sizes.Types[field_index]; + _ = try Aligner.write(Type, Writer, writer, try stream.getPos()); + try writer.writeAll(std.mem.sliceAsBytes( + slice.items( + @intToEnum(Lockfile.Package.List.Field, FieldsEnum.fields[field_index].value), + ), + )); + } + } + + pub fn load( + stream: *Stream, + allocator: *std.mem.Allocator, + ) !Lockfile.Package.List { + var reader = stream.reader(); + + const byte_len = try reader.readIntLittle(u64); + + if (byte_len == 0) { + return Lockfile.Package.List{ + .len = 0, + .capacity = 0, + }; + } + + // Count of items in the list + const list_len = try reader.readIntLittle(u64); + + var list = Lockfile.Package.List{}; + try list.ensureTotalCapacity(allocator, list_len); + list.len = list_len; + var slice = list.slice(); + + inline for (sizes.fields) |field_index| { + const Type = sizes.Types[field_index]; + stream.pos += Aligner.skipAmount(Type, try stream.getPos()); + var bytes = std.mem.sliceAsBytes( + slice.items( + @intToEnum(Lockfile.Package.List.Field, FieldsEnum.fields[field_index].value), + ), + ); + @memcpy(bytes.ptr, @ptrCast([*]u8, &stream.buffer[stream.pos]), bytes.len); + stream.pos += bytes.len; + } + + // Alignment bytes to skip + // stream.pos += Aligner.skipAmount(AlignmentType, try stream.getPos()); + + return list; + } + }; + }; + + const Buffers = struct { + trees: Tree.List = Tree.List{}, + hoisted_packages: PackageIDList = PackageIDList{}, + resolutions: PackageIDList = PackageIDList{}, + dependencies: DependencyList = DependencyList{}, + extern_strings: SmallExternalStringBuffer = SmallExternalStringBuffer{}, + // node_modules_folders: NodeModulesFolderList = NodeModulesFolderList{}, + // node_modules_package_ids: PackageIDList = PackageIDList{}, + string_bytes: StringBuffer = StringBuffer{}, + + pub fn preallocate(this: *Buffers, that: Buffers, allocator: *std.mem.Allocator) !void { + try this.trees.ensureTotalCapacity(allocator, that.trees.items.len); + try this.resolutions.ensureTotalCapacity(allocator, that.resolutions.items.len); + try this.dependencies.ensureTotalCapacity(allocator, that.dependencies.items.len); + try this.extern_strings.ensureTotalCapacity(allocator, that.extern_strings.items.len); + try this.string_bytes.ensureTotalCapacity(allocator, that.string_bytes.items.len); + } + + pub fn readArray(stream: *Stream, comptime ArrayList: type) !ArrayList { + const arraylist: ArrayList = undefined; + + const PointerType = std.meta.Child(@TypeOf(arraylist.items.ptr)); + const alignment = @alignOf([*]PointerType); + + var reader = stream.reader(); + const byte_len = try reader.readIntLittle(u64); + + if (byte_len == 0) return ArrayList{ + .items = &[_]PointerType{}, + .capacity = 0, + }; + + stream.pos += Aligner.skipAmount([*]PointerType, stream.pos); + const start = stream.pos; + stream.pos += byte_len; + + if (stream.pos > stream.buffer.len) { + return error.BufferOverflow; + } + + return ArrayList{ + .items = @ptrCast([*]PointerType, @alignCast(alignment, &stream.buffer[start]))[0 .. byte_len / @sizeOf(PointerType)], + .capacity = byte_len, + }; + } + + const sizes = blk: { + const fields = std.meta.fields(Lockfile.Buffers); + const Data = struct { + size: usize, + name: []const u8, + field_type: type, + alignment: usize, + }; + var data: [fields.len]Data = undefined; + for (fields) |field_info, i| { + data[i] = .{ + .size = @sizeOf(field_info.field_type), + .name = field_info.name, + .alignment = if (@sizeOf(field_info.field_type) == 0) 1 else field_info.alignment, + .field_type = field_info.field_type.Slice, + }; + } + const Sort = struct { + fn lessThan(trash: *i32, comptime lhs: Data, comptime rhs: Data) bool { + _ = trash; + return lhs.alignment > rhs.alignment; + } + }; + var trash: i32 = undefined; // workaround for stage1 compiler bug + std.sort.sort(Data, &data, &trash, Sort.lessThan); + var sizes_bytes: [fields.len]usize = undefined; + var names: [fields.len][]const u8 = undefined; + var types: [fields.len]type = undefined; + for (data) |elem, i| { + sizes_bytes[i] = elem.size; + names[i] = elem.name; + types[i] = elem.field_type; + } + break :blk .{ + .bytes = sizes_bytes, + .names = names, + .types = types, + }; + }; + + pub fn writeArray(comptime StreamType: type, stream: StreamType, comptime Writer: type, writer: Writer, comptime ArrayList: type, array: ArrayList) !void { + const bytes = std.mem.sliceAsBytes(array); + try writer.writeIntLittle(u64, bytes.len); + + if (bytes.len > 0) { + _ = try Aligner.write([*]std.meta.Child(ArrayList), Writer, writer, try stream.getPos()); + + try writer.writeAll(bytes); + } + } + + pub fn save(this: Buffers, allocator: *std.mem.Allocator, comptime StreamType: type, stream: StreamType, comptime Writer: type, writer: Writer) !void { + inline for (sizes.names) |name, i| { + var pos: usize = 0; + if (comptime Environment.isDebug) { + pos = try stream.getPos(); + } + + // Dependencies have to be converted to .toExternal first + // We store pointers in Version.Value, so we can't just write it directly + if (comptime strings.eqlComptime(name, "dependencies")) { + var remaining = this.dependencies.items; + + var buf: [128]Dependency.External = undefined; + + switch (remaining.len) { + 0 => { + try writer.writeIntLittle(u64, 0); + }, + 1...127 => { + for (remaining) |dep, j| { + buf[j] = dep.toExternal(); + } + const to_write = std.mem.sliceAsBytes(buf[0..remaining.len]); + try writer.writeIntLittle(u64, to_write.len); + _ = try Aligner.write( + [*]Dependency.External, + Writer, + writer, + try stream.getPos(), + ); + try writer.writeAll(to_write); + }, + else => { + try writer.writeIntLittle(u64, @sizeOf(Dependency.External) * remaining.len); + _ = try Aligner.write( + [*]Dependency.External, + Writer, + writer, + try stream.getPos(), + ); + + var buf_i: usize = 0; + for (remaining) |dep| { + if (buf_i >= buf.len) { + try writer.writeAll(std.mem.sliceAsBytes(&buf)); + buf_i = 0; + } + buf[buf_i] = dep.toExternal(); + buf_i += 1; + } + if (buf_i > 0) { + const to_write = std.mem.sliceAsBytes(buf[0..buf_i]); + try writer.writeAll(to_write); + } + }, + } + } else { + const list = @field(this, name).items; + const Type = @TypeOf(list); + + try writeArray(StreamType, stream, Writer, writer, Type, list); + } + + if (comptime Environment.isDebug) { + // Output.prettyErrorln("Field {s}: {d} - {d}", .{ name, pos, try stream.getPos() }); + } + } + } + + pub fn load(stream: *Stream, allocator: *std.mem.Allocator, log: *logger.Log) !Buffers { + var this = Buffers{}; + var external_dependency_list: []Dependency.External = &[_]Dependency.External{}; + inline for (sizes.names) |name, i| { + const Type = @TypeOf(@field(this, name)); + + var pos: usize = 0; + if (comptime Environment.isDebug) { + pos = try stream.getPos(); + } + + if (comptime Type == @TypeOf(this.dependencies)) { + var reader = stream.reader(); + const len = try reader.readIntLittle(u64); + if (len > 0) { + stream.pos += Aligner.skipAmount([*]Dependency.External, stream.pos); + const start = stream.pos; + stream.pos += len; + if (stream.pos > stream.buffer.len) { + return error.BufferOverflow; + } + var bytes = stream.buffer[start..][0..len]; + external_dependency_list = @alignCast( + @alignOf([]Dependency.External), + std.mem.bytesAsSlice( + Dependency.External, + bytes, + ), + ); + } + } else { + @field(this, sizes.names[i]) = try readArray(stream, Type); + } + + if (comptime Environment.isDebug) { + // Output.prettyErrorln("Field {s}: {d} - {d}", .{ sizes.names[i], pos, try stream.getPos() }); + } + } + + // Dependencies are serialized separately. + // This is unfortunate. However, not using pointers for Semver Range's make the code a lot more complex. + this.dependencies = try DependencyList.initCapacity(allocator, external_dependency_list.len); + const extern_context = Dependency.External.Context{ + .log = log, + .allocator = allocator, + .buffer = this.string_bytes.items, + }; + + this.dependencies.expandToCapacity(); + this.dependencies.items.len = external_dependency_list.len; + for (external_dependency_list) |dep, i| { + this.dependencies.items[i] = dep.toDependency(extern_context); + } + + return this; + } + }; + + pub const Serializer = struct { + pub const version = "bun-lockfile-format-v0\n"; + const header_bytes: string = "#!/usr/bin/env bun\n" ++ version; + + pub fn save(this: *Lockfile, comptime StreamType: type, stream: StreamType) !void { + var writer = stream.writer(); + try writer.writeAll(header_bytes); + try writer.writeIntLittle(u32, @enumToInt(this.format)); + const pos = try stream.getPos(); + try writer.writeIntLittle(u64, 0); + + try Lockfile.Package.Serializer.save(this.packages, StreamType, stream, @TypeOf(&writer), &writer); + try Lockfile.Buffers.save(this.buffers, this.allocator, StreamType, stream, @TypeOf(&writer), &writer); + + try writer.writeIntLittle(u64, 0); + const end = try stream.getPos(); + try writer.writeAll(alignment_bytes_to_repeat_buffer); + + _ = try std.os.pwrite(stream.handle, std.mem.asBytes(&end), pos); + } + pub fn load( + lockfile: *Lockfile, + stream: *Stream, + allocator: *std.mem.Allocator, + log: *logger.Log, + ) !void { + var reader = stream.reader(); + var header_buf_: [header_bytes.len]u8 = undefined; + var header_buf = header_buf_[0..try reader.readAll(&header_buf_)]; + + if (!strings.eqlComptime(header_buf, header_bytes)) { + return error.InvalidLockfile; + } + + var format = try reader.readIntLittle(u32); + if (format != @enumToInt(Lockfile.FormatVersion.v0)) { + return error.InvalidLockfileVersion; + } + lockfile.format = .v0; + lockfile.allocator = allocator; + const byte_len = try reader.readIntLittle(u64); + + lockfile.packages = try Lockfile.Package.Serializer.load( + stream, + allocator, + ); + lockfile.buffers = try Lockfile.Buffers.load(stream, allocator, log); + lockfile.scratch = Lockfile.Scratch.init(allocator); + + { + lockfile.package_index = PackageIndex.Map.initContext(allocator, .{}); + lockfile.unique_packages = try Bitset.initFull(lockfile.packages.len, allocator); + lockfile.string_pool = StringPool.initContext(allocator, .{}); + try lockfile.package_index.ensureTotalCapacity(@truncate(u32, lockfile.packages.len)); + var slice = lockfile.packages.slice(); + var name_hashes = slice.items(.name_hash); + for (name_hashes) |name_hash, id| { + try lockfile.getOrPutID(@truncate(PackageID, id), name_hash); + } + } + + // const end = try reader.readIntLittle(u64); + } + }; +}; +/// 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, + + /// An ID that lets us register a callback without keeping the same pointer around + pub const Id = struct { + pub fn forNPMPackage(tag: 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( + tag: 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); + + switch (this.tag) { + .package_manifest => { + var allocator = PackageManager.instance.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, + ) catch |err| { + this.status = Status.fail; + PackageManager.instance.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; + PackageManager.instance.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; + PackageManager.instance.resolve_tasks.writeItem(this.*) catch unreachable; + return; + }, + } + }, + .extract => { + const result = this.request.extract.tarball.run( + this.request.extract.network.response_buffer.toOwnedSliceLeaky(), + ) catch |err| { + this.status = Status.fail; + this.data = .{ .extract = "" }; + PackageManager.instance.resolve_tasks.writeItem(this.*) catch unreachable; + return; + }; + + this.data = .{ .extract = result }; + this.status = Status.success; + PackageManager.instance.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.Dir, + destination_dir: std.fs.Dir, + 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, + + threadlocal var package_json_checker: json_parser.PackageJSONVersionChecker = undefined; + + pub const Context = struct { + metas: []const Lockfile.Package.Meta, + names: []const String, + resolutions: []const Resolution, + string_buf: []const u8, + channel: PackageInstall.Task.Channel = undefined, + skip_verify: bool = false, + progress: *Progress = undefined, + cache_dir: std.fs.Dir = undefined, + allocator: *std.mem.Allocator, + }; + + pub const Task = struct { + task: ThreadPool.Task = .{ .callback = callback }, + result: Result = Result{ .pending = void{} }, + package_install: PackageInstall = undefined, + package_id: PackageID, + ctx: *PackageInstall.Context, + destination_dir: std.fs.Dir, + + pub const Channel = sync.Channel(*PackageInstall.Task, .{ .Static = 1024 }); + + pub fn callback(task: *ThreadPool.Task) void { + Output.Source.configureThread(); + defer Output.flush(); + + var this: *PackageInstall.Task = @fieldParentPtr(PackageInstall.Task, "task", task); + var ctx = this.ctx; + + var destination_dir_subpath_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + var cache_dir_subpath_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const name = ctx.names[this.package_id].slice(ctx.string_buf); + const meta = ctx.metas[this.package_id]; + const resolution = ctx.resolutions[this.package_id]; + std.mem.copy(u8, &destination_dir_subpath_buf, name); + destination_dir_subpath_buf[name.len] = 0; + var destination_dir_subpath: [:0]u8 = destination_dir_subpath_buf[0..name.len :0]; + var resolution_buf: [512]u8 = undefined; + var resolution_label = std.fmt.bufPrint(&resolution_buf, "{}", .{resolution.fmt(ctx.string_buf)}) catch unreachable; + + switch (resolution.tag) { + .npm => { + this.package_install = PackageInstall{ + .cache_dir = ctx.cache_dir, + .progress = ctx.progress, + .cache_dir_subpath = PackageManager.cachedNPMPackageFolderNamePrint(&cache_dir_subpath_buf, name, resolution.value.npm), + .destination_dir = this.destination_dir, + .destination_dir_subpath = destination_dir_subpath, + .destination_dir_subpath_buf = &destination_dir_subpath_buf, + .allocator = ctx.allocator, + .package_name = name, + .package_version = resolution_label, + }; + + const needs_install = ctx.skip_verify or !this.package_install.verify(); + + if (needs_install) { + this.result = this.package_install.install(ctx.skip_verify); + } else { + this.result = .{ .skip = .{} }; + } + }, + else => {}, + } + + ctx.channel.writeItem(this) catch unreachable; + } + }; + + pub const Summary = struct { + fail: u32 = 0, + success: u32 = 0, + skipped: u32 = 0, + successfully_installed: ?std.DynamicBitSetUnmanaged = null, + }; + + pub const Method = enum { + clonefile, + + // Slower than clonefile + clonefile_each_dir, + + // On macOS, slow. + // On Linux, fast. + hardlink, + + // Slowest if single-threaded + copyfile, + + const BackendSupport = std.EnumArray(Method, bool); + + pub const macOS = BackendSupport.initDefault(false, .{ + .clonefile = true, + .clonefile_each_dir = true, + .hardlink = true, + .copyfile = true, + }); + + pub const linux = BackendSupport.initDefault(false, .{ + .hardlink = true, + .copyfile = 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.openFileZ(package_json_path, .{ .read = true }) 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[@minimum(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(); + + 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 = Step.clone, + + 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, + }; + + const CloneFileError = error{ + NotSupported, + Unexpected, + FileNotFound, + }; + + var supported_method: Method = if (Environment.isMac) + Method.clonefile + else + Method.copyfile; + + fn installWithClonefileEachDir(this: *PackageInstall) !Result { + const Walker = @import("../walker_skippable.zig"); + + var cached_package_dir = this.cache_dir.openDirZ(this.cache_dir_subpath, .{ + .iterate = true, + }) 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, + ) !u32 { + var real_file_count: u32 = 0; + var stackpath: [std.fs.MAX_PATH_BYTES]u8 = undefined; + while (try walker.next()) |entry| { + switch (entry.kind) { + .Directory => std.os.mkdirat(destination_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.fd, + basename, + destination_dir_.fd, + path, + 0, + )) { + 0 => void{}, + else => |errno| switch (std.os.errno(errno)) { + .OPNOTSUPP => return error.NotSupported, + .NOENT => return error.FileNotFound, + else => return error.Unexpected, + }, + } + + real_file_count += 1; + }, + else => {}, + } + } + + return real_file_count; + } + }; + + var subdir = this.destination_dir.makeOpenPath(std.mem.span(this.destination_dir_subpath), .{ .iterate = true }) 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.makeDirZ(subdir) catch {}; + this.destination_dir_subpath_buf[slash] = std.fs.path.sep; + } + } + + return switch (C.clonefileat( + this.cache_dir.fd, + this.cache_dir_subpath, + this.destination_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"); + const CopyFile = @import("../copy_file.zig"); + + var cached_package_dir = this.cache_dir.openDirZ(this.cache_dir_subpath, .{ + .iterate = true, + }) 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("<r><red>{s}<r>: copying file {s}", .{ @errorName(err), entry.path }); + Output.flush(); + std.os.exit(1); + }; + }; + defer outfile.close(); + + var infile = try entry.dir.openFile(entry.basename, .{ .read = true }); + defer infile.close(); + + const stat = infile.stat() catch continue; + _ = C.fchmod(outfile.handle, stat.mode); + + CopyFile.copy(infile.handle, outfile.handle) catch { + entry.dir.copyFile(entry.basename, destination_dir_, entry.path, .{}) catch |err| { + progress_.root.end(); + + progress_.refresh(); + + Output.prettyErrorln("<r><red>{s}<r>: copying file {s}", .{ @errorName(err), entry.path }); + Output.flush(); + std.os.exit(1); + }; + }; + } + + return real_file_count; + } + }; + + var subdir = this.destination_dir.makeOpenPath(std.mem.span(this.destination_dir_subpath), .{ .iterate = true }) catch |err| return Result{ + .fail = .{ .err = err, .step = .opening_cache_dir }, + }; + + defer subdir.close(); + + this.file_count = FileCopier.copy(subdir, &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 = this.cache_dir.openDirZ(this.cache_dir_subpath, .{ + .iterate = true, + }) 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, + ) !u32 { + var real_file_count: u32 = 0; + while (try walker.next()) |entry| { + switch (entry.kind) { + .Directory => std.os.mkdirat(destination_dir_.fd, entry.path, 0o755) catch {}, + .File => { + try std.os.linkat(entry.dir.fd, entry.basename, destination_dir_.fd, entry.path, 0); + real_file_count += 1; + }, + else => {}, + } + } + + return real_file_count; + } + }; + + var subdir = this.destination_dir.makeOpenPath(std.mem.span(this.destination_dir_subpath), .{ .iterate = true }) 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{}, + }; + } + + 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) this.destination_dir.deleteTree(std.mem.span(this.destination_dir_subpath)) catch {}; + + switch (supported_method) { + .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; + }, + 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; + }, + 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.NotSupported => { + supported_method = .copyfile; + }, + 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 != .copyfile) return Result{ + .success = void{}, + }; + + // TODO: linux io_uring + return this.installWithCopyfile(); + } +}; + +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, + node_modules_folder: u32, // Really, this is a file descriptor + pub const Tag = enum { + dependency, + request_id, + 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("thread_pool"); +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, +}; + +// We can't know all the package s we need until we've downloaded all the packages +// The easy way wouild be: +// 1. Download all packages, parsing their dependencies and enqueuing all dependnecies for resolution +// 2. +pub const PackageManager = struct { + cache_directory_path: string = "", + cache_directory: std.fs.Dir = undefined, + root_dir: *Fs.FileSystem.DirEntry, + env_loader: *DotEnv.Loader, + allocator: *std.mem.Allocator, + log: *logger.Log, + resolve_tasks: TaskChannel, + timestamp: 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{}, + + to_remove: []const UpdateRequest = &[_]UpdateRequest{}, + + root_package_json_file: std.fs.File, + root_dependency_list: Lockfile.DependencySlice = .{}, + + registry: Npm.Registry = Npm.Registry{}, + + thread_pool: ThreadPool, + + manifests: PackageManifestMap = PackageManifestMap{}, + resolved_package_index: PackageIndex = PackageIndex{}, + + 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{}, + + const PreallocatedNetworkTasks = std.BoundedArray(NetworkTask, 1024); + const NetworkTaskQueue = std.HashMapUnmanaged(u64, void, IdentityContext(u64), 80); + const PackageIndex = std.AutoHashMapUnmanaged(u64, *Package); + pub var verbose_install = false; + + const PackageDedupeList = std.HashMapUnmanaged( + u32, + void, + IdentityContext(u32), + 80, + ); + + 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: [std.fs.MAX_PATH_BYTES]u8 = undefined; + + 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 cachedNPMPackageFolderName(name: string, version: Semver.Version) stringZ { + return cachedNPMPackageFolderNamePrint(&cached_package_folder_name_buf, name, version); + } + + // TODO: normalize to alphanumeric + pub fn cachedNPMPackageFolderNamePrint(buf: []u8, name: string, version: Semver.Version) stringZ { + 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}-{x}+{X}", + .{ name, version.major, version.minor, version.patch, version.tag.pre.hash, version.tag.build.hash }, + ) catch unreachable; + } else if (version.tag.hasPre()) { + return std.fmt.bufPrintZ( + buf, + "{s}@{d}.{d}.{d}-{x}", + .{ name, version.major, version.minor, version.patch, version.tag.pre.hash }, + ) catch unreachable; + } else if (version.tag.hasBuild()) { + return std.fmt.bufPrintZ( + buf, + "{s}@{d}.{d}.{d}+{X}", + .{ name, version.major, version.minor, version.patch, version.tag.build.hash }, + ) catch unreachable; + } else { + unreachable; + } + + unreachable; + } + + pub fn isFolderInCache(this: *PackageManager, folder_path: stringZ) bool { + // TODO: is this slow? + var dir = this.cache_directory.openDirZ(folder_path, .{ .iterate = true }) catch return false; + dir.close(); + return true; + } + + 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, + }; + + pub fn getOrPutResolvedPackageWithFindResult( + this: *PackageManager, + name_hash: PackageNameHash, + name: String, + version: Dependency.Version, + dependency_id: PackageID, + behavior: Behavior, + manifest: *const Npm.PackageManifest, + find_result: Npm.PackageManifest.FindResult, + ) !?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 = find_result.version }, + }, + )) |id| { + this.lockfile.buffers.resolutions.items[dependency_id] = id; + return ResolvedPackageResult{ + .package = this.lockfile.packages.get(id), + .is_first_time = false, + }; + } + + var package = + try Lockfile.Package.fromNPM( + this.allocator, + this.lockfile, + this.log, + manifest, + find_result.version, + find_result.package, + manifest.string_buf, + Features.npm, + ); + + if (!behavior.isEnabled(this.options.omit.toFeatures())) { + package.meta.preinstall_state = .done; + } + + const preinstall = package.determinePreinstallState(this.lockfile, this); + + // appendPackage sets the PackageID on the package + package = try this.lockfile.appendPackage(package); + this.lockfile.buffers.resolutions.items[dependency_id] = package.meta.id; + if (comptime Environment.isDebug or Environment.isTest) std.debug.assert(package.meta.id != invalid_package_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, + name.slice(this.lockfile.buffers.string_bytes.items), + package.resolution.value.npm, + ); + + var network_task = (try this.generateNetworkTaskForTarball(task_id, package)).?; + + return ResolvedPackageResult{ + .package = package, + .is_first_time = true, + .network_task = network_task, + }; + }, + else => unreachable, + } + + return ResolvedPackageResult{ .package = package }; + } + + pub fn generateNetworkTaskForTarball(this: *PackageManager, task_id: u64, 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, + }; + + try network_task.forTarball( + this.allocator, + ExtractTarball{ + .name = if (package.name.len() >= strings.StringOrTinyString.Max) + strings.StringOrTinyString.init( + try FileSystem.FilenameStore.instance.append( + @TypeOf(this.lockfile.str(package.name)), + this.lockfile.str(package.name), + ), + ) + else + strings.StringOrTinyString.init(this.lockfile.str(package.name)), + + .resolution = package.resolution, + .cache_dir = this.cache_directory_path, + .registry = this.registry.url.href, + .package_id = package.meta.id, + .extracted_file_count = package.meta.file_count, + .integrity = package.meta.integrity, + }, + ); + + return network_task; + } + + pub fn fetchVersionsForPackageName( + this: *PackageManager, + name: string, + version: Dependency.Version, + id: PackageID, + ) !?Npm.PackageManifest { + const task_id = Task.Id.forManifest(Task.Tag.package_manifest, name); + var network_entry = try this.network_dedupe_map.getOrPutContext(this.allocator, task_id, .{}); + var loaded_manifest: ?Npm.PackageManifest = null; + if (!network_entry.found_existing) { + if (this.options.enable.manifest_cache) { + if (this.manifests.get(std.hash.Wyhash.hash(0, name)) orelse (Npm.PackageManifest.Serializer.load(this.allocator, this.cache_directory, name) 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) { + 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 (version.tag == .npm and version.value.npm.isExact()) { + if (loaded_manifest.?.findByVersion(version.value.npm.head.head.range.left.version) != null) { + return manifest; + } + } + + // 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) { + return manifest; + } + } + } + + if (PackageManager.verbose_install) { + Output.prettyErrorln("Enqueue package manifest for download: {s}", .{name}); + } + + var network_task = this.getNetworkTask(); + network_task.* = NetworkTask{ + .callback = undefined, + .task_id = task_id, + .allocator = this.allocator, + }; + try network_task.forManifest(name, this.allocator, this.registry.url, loaded_manifest); + this.enqueueNetworkTask(network_task); + } + + 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{}; + } + + try manifest_entry_parse.value_ptr.append(this.allocator, TaskCallbackContext{ .request_id = id }); + return null; + } + + fn enqueueNetworkTask(this: *PackageManager, task: *NetworkTask) void { + if (this.network_task_fifo.writableLength() == 0) { + this.flushNetworkQueue(); + } + + this.network_task_fifo.writeItemAssumeCapacity(task); + } + + pub fn getOrPutResolvedPackage( + this: *PackageManager, + name_hash: PackageNameHash, + name: String, + version: Dependency.Version, + behavior: Behavior, + dependency_id: PackageID, + resolution: PackageID, + ) !?ResolvedPackageResult { + 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)), + .npm => manifest.findBestVersion(version.value.npm), + else => unreachable, + } orelse return switch (version.tag) { + .npm => error.NoMatchingVersion, + .dist_tag => error.DistTagNotFound, + else => unreachable, + }; + + return try getOrPutResolvedPackageWithFindResult(this, name_hash, name, version, dependency_id, behavior, manifest, find_result); + }, + + else => return null, + } + } + + pub fn resolvePackageFromManifest( + this: *PackageManager, + semver: Semver.Version, + version: *const Npm.PackageVersion, + manifest: *const Npm.PackageManifest, + ) !void {} + + 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{ + .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{ + .log = logger.Log.init(this.allocator), + .tag = Task.Tag.extract, + .request = .{ + .extract = .{ + .network = network_task, + .tarball = tarball, + }, + }, + .id = network_task.task_id, + .data = undefined, + }; + return &task.threadpool_task; + } + + inline fn enqueueDependency(this: *PackageManager, id: u32, dependency: Dependency, resolution: PackageID) !void { + return try this.enqueueDependencyWithMain(id, dependency, resolution, false); + } + + 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 rng = std.rand.Gimli.init(secret); + var base64_bytes: [64]u8 = undefined; + rng.random.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("<r><red>error:<r> failed to create tmpfile: {s}", .{@errorName(err)}); + Output.flush(); + 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, + 0000040 | 0000004 | 0000002 | 0000400 | 0000200 | 0000020, + ); + + try tmpfile.promote(tmpname, std.fs.cwd().fd, "yarn.lock"); + } + + fn enqueueDependencyWithMain( + this: *PackageManager, + id: u32, + dependency: Dependency, + resolution: PackageID, + comptime is_main: bool, + ) !void { + const name = dependency.name; + const name_hash = dependency.name_hash; + const version: Dependency.Version = dependency.version; + var loaded_manifest: ?Npm.PackageManifest = null; + + if (comptime !is_main) { + // it might really be main + if (!(id >= this.root_dependency_list.off and id < this.root_dependency_list.len + this.root_dependency_list.off)) { + if (!dependency.behavior.isEnabled(Features.npm)) + return; + } + } + + switch (dependency.version.tag) { + .npm, .dist_tag => { + retry_from_manifests_ptr: while (true) { + var resolve_result_ = this.getOrPutResolvedPackage( + name_hash, + name, + version, + dependency.behavior, + id, + resolution, + ); + + retry_with_new_resolve_result: while (true) { + const resolve_result = resolve_result_ catch |err| { + switch (err) { + error.DistTagNotFound => { + if (dependency.behavior.isRequired()) { + 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), + }, + ) catch unreachable; + } + + return; + }, + error.NoMatchingVersion => { + if (dependency.behavior.isRequired()) { + 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 => 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| { + var meta: *Lockfile.Package.Meta = &this.lockfile.packages.items(.meta)[result.package.meta.id]; + if (meta.preinstall_state == .extract) { + meta.preinstall_state = .extracting; + this.enqueueNetworkTask(network_task); + } + } + } else if (!dependency.behavior.isPeer()) { + 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.cache_directory, 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) { + 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.isExact()) { + if (loaded_manifest.?.findByVersion(dependency.version.value.npm.head.head.range.left.version)) |find_result| { + if (this.getOrPutResolvedPackageWithFindResult( + name_hash, + name, + version, + id, + dependency.behavior, + &loaded_manifest.?, + find_result, + ) 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) { + _ = this.network_dedupe_map.remove(task_id); + continue :retry_from_manifests_ptr; + } + } + } + + if (PackageManager.verbose_install) { + Output.prettyErrorln("Enqueue package manifest for download: {s}", .{this.lockfile.str(name)}); + } + + var network_task = this.getNetworkTask(); + network_task.* = NetworkTask{ + .callback = undefined, + .task_id = task_id, + .allocator = this.allocator, + }; + try network_task.forManifest(this.lockfile.str(name), this.allocator, this.registry.url, loaded_manifest); + this.enqueueNetworkTask(network_task); + } + + 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{}; + } + + try manifest_entry_parse.value_ptr.append(this.allocator, TaskCallbackContext{ .dependency = id }); + } + return; + } + } + return; + }, + 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) { + this.enqueueDependencyWithMain( + i, + lockfile.buffers.dependencies.items[i], + lockfile.buffers.resolutions.items[i], + false, + ) catch {}; + } + + this.flushNetworkQueue(); + } + } + pub fn flushDependencyQueue(this: *PackageManager) void { + this.flushNetworkQueue(); + this.doFlushDependencyQueue(); + this.doFlushDependencyQueue(); + this.doFlushDependencyQueue(); + this.flushNetworkQueue(); + } + + 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); + NetworkThread.global.pool.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) { + this.enqueueDependencyWithMain( + i, + lockfile.buffers.dependencies.items[i], + lockfile.buffers.resolutions.items[i], + is_main, + ) catch {}; + } + } + + // 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); + NetworkThread.global.pool.schedule(this.network_resolve_batch); + this.network_tarball_batch = .{}; + this.network_resolve_batch = .{}; + } + + pub fn hoist(this: *PackageManager) !void {} + pub fn link(this: *PackageManager) !void {} + + pub fn fetchCacheDirectoryPath( + allocator: *std.mem.Allocator, + env_loader: *DotEnv.Loader, + root_dir: *Fs.FileSystem.DirEntry, + ) ?string { + if (env_loader.map.get("BUN_INSTALL_CACHE_DIR")) |dir| { + return dir; + } + + if (env_loader.map.get("BUN_INSTALL")) |dir| { + var parts = [_]string{ dir, "install/", "cache/" }; + return Fs.FileSystem.instance.abs(&parts); + } + + if (env_loader.map.get("HOME")) |dir| { + var parts = [_]string{ dir, ".bun/", "install/", "cache/" }; + return Fs.FileSystem.instance.abs(&parts); + } + + if (env_loader.map.get("XDG_CACHE_HOME")) |dir| { + var parts = [_]string{ dir, ".bun/", "install/", "cache/" }; + return Fs.FileSystem.instance.abs(&parts); + } + + if (env_loader.map.get("TMPDIR")) |dir| { + var parts = [_]string{ dir, ".bun-cache" }; + return Fs.FileSystem.instance.abs(&parts); + } + + return null; + } + + fn runTasks( + manager: *PackageManager, + comptime ExtractCompletionContext: type, + extract_ctx: ExtractCompletionContext, + comptime callback_fn: anytype, + comptime log_level: Options.LogLevel, + ) anyerror!void { + var batch = ThreadPool.Batch{}; + var has_updated_this_run = false; + 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 { + if (comptime log_level != .silent) { + Output.prettyErrorln("Failed to download package manifest for package {s}", .{name}); + Output.flush(); + } + continue; + }; + + if (response.status_code > 399) { + if (comptime log_level != .silent) { + const fmt = "<r><red><b>GET<r><red> {s}<d> - {d}<r>\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(f128, task.http.elapsed) / std.time.ns_per_ms)); + Output.prettyError(" <d>Downloaded <r><green>{s}<r> 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; + entry.value_ptr.*.pkg.public_max_age = @truncate(u32, @intCast(u64, @maximum(0, std.time.timestamp()))) + 300; + { + var tmpdir = Fs.FileSystem.instance.tmpdir(); + Npm.PackageManifest.Serializer.save(entry.value_ptr, tmpdir, PackageManager.instance.cache_directory) 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.* = .{}; + + if (dependency_list.items.len > 0) { + for (dependency_list.items) |item| { + var dependency = manager.lockfile.buffers.dependencies.items[item.dependency]; + var resolution = manager.lockfile.buffers.resolutions.items[item.dependency]; + + try manager.enqueueDependency( + item.dependency, + dependency, + resolution, + ); + } + + dependency_list.deinit(manager.allocator); + } + + manager.flushDependencyQueue(); + continue; + } + } + + batch.push(ThreadPool.Batch.from(manager.enqueueParseNPMPackage(task.task_id, name, task))); + }, + .extract => |extract| { + const response = task.http.response orelse { + const fmt = "Failed to download package tarball for package {s}\n"; + const args = .{extract.name}; + + 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 (comptime log_level != .silent) { + const fmt = "<r><red><b>GET<r><red> {s}<d> - {d}<r>\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(f128, task.http.elapsed) / std.time.ns_per_ms)); + Output.prettyError(" <d>Downloaded <r><green>{s}<r> 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) { + if (comptime log_level != .silent) { + Output.prettyErrorln("Failed to parse package manifest for {s}", .{task.request.package_manifest.name.slice()}); + Output.flush(); + } + continue; + } + const manifest = task.data.package_manifest; + var entry = 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.* = .{}; + + if (dependency_list.items.len > 0) { + for (dependency_list.items) |item| { + var dependency = manager.lockfile.buffers.dependencies.items[item.dependency]; + var resolution = manager.lockfile.buffers.resolutions.items[item.dependency]; + + try manager.enqueueDependency( + item.dependency, + dependency, + resolution, + ); + } + + dependency_list.deinit(manager.allocator); + } + + 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) { + Output.prettyErrorln("Failed to extract tarball for {s}", .{ + task.request.extract.tarball.name, + }); + Output.flush(); + continue; + } + const package_id = task.request.extract.tarball.package_id; + manager.extracted_count += 1; + manager.lockfile.packages.items(.meta)[package_id].preinstall_state = .done; + + if (comptime ExtractCompletionContext != void) { + callback_fn(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 prev_total = manager.total_tasks; + { + 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); + NetworkThread.global.pool.schedule(manager.network_resolve_batch); + manager.network_tarball_batch = .{}; + manager.network_resolve_batch = .{}; + + if (comptime log_level.showProgress()) { + if (comptime ExtractCompletionContext == void) { + 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, + + lockfile_path: stringZ = Lockfile.default_filename, + save_lockfile_path: stringZ = Lockfile.default_filename, + scope: Npm.Registry.Scope = .{ + .name = "", + .token = "", + .url = URL.parse("https://registry.npmjs.org/"), + }, + + 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, + omit: CommandLineArguments.Omit = .{}, + + allowed_install_scripts: []const PackageNameHash = &default_allowed_install_scripts, + + // 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 optionalDepenencies + // 2. Has a platform and/or os specified, which evaluates to not disabled + native_bin_link_allowlist: []const PackageNameHash = &default_native_bin_link_allowlist, + + const default_native_bin_link_allowlist = [_]PackageNameHash{ + String.Builder.stringHash("esbuild"), + String.Builder.stringHash("turbo"), + }; + + const install_scripts_package_count = 5; + const default_allowed_install_scripts: [install_scripts_package_count]PackageNameHash = brk: { + const names = std.mem.span(@embedFile("install-scripts-allowlist.txt")); + var hashes: [install_scripts_package_count]PackageNameHash = undefined; + var splitter = std.mem.split(u8, names, "\n"); + var i: usize = 0; + while (splitter.next()) |item| { + hashes[i] = String.Builder.stringHash(item); + i += 1; + } + break :brk hashes; + }; + + pub const LogLevel = enum { + default, + verbose, + silent, + default_no_progress, + verbose_no_progress, + + pub inline fn isVerbose(this: LogLevel) bool { + return 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 load( + this: *Options, + allocator: *std.mem.Allocator, + log: *logger.Log, + env_loader: *DotEnv.Loader, + cli_: ?CommandLineArguments, + ) !void { + + // 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://"))) + { + this.scope.url = URL.parse(registry_); + 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); + } + } + + this.save_lockfile_path = this.lockfile_path; + + 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_NO_CLONEFILE") != null) { + PackageInstall.supported_method = .copyfile; + } + + if (env_loader.map.get("BUN_CONFIG_YARN_LOCKFILE") != null) { + this.do.save_yarn_lock = true; + } + + 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; + // } + + this.do.save_lockfile = strings.eqlComptime((env_loader.map.get("BUN_CONFIG_SKIP_SAVE_LOCKFILE") orelse "0"), "0"); + this.do.load_lockfile = strings.eqlComptime((env_loader.map.get("BUN_CONFIG_SKIP_LOAD_LOCKFILE") orelse "0"), "0"); + this.do.install_packages = strings.eqlComptime((env_loader.map.get("BUN_CONFIG_SKIP_INSTALL_PACKAGES") orelse "0"), "0"); + + if (cli_) |cli| { + if (cli.no_save) { + this.do.save_lockfile = false; + } + + if (cli.dry_run) { + this.do.install_packages = false; + this.dry_run = true; + } + + if (cli.no_cache) { + this.enable.manifest_cache = false; + this.enable.manifest_cache_control = false; + } + + // if (cli.no_dedupe) { + // this.enable.deduplicate_packages = false; + // } + + this.omit.dev = cli.omit.dev or this.omit.dev; + this.omit.optional = cli.omit.optional or this.omit.optional; + this.omit.peer = cli.omit.peer or this.omit.peer; + + if (cli.verbose) { + this.log_level = .verbose; + PackageManager.verbose_install = true; + } else if (cli.silent) { + this.log_level = .silent; + PackageManager.verbose_install = 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.enable.install_dev_dependencies = false; + } + + if (cli.force) { + this.enable.manifest_cache_control = false; + this.enable.force_install = true; + } + + this.update.development = cli.development; + if (!this.update.development) this.update.optional = cli.optional; + } + } + + pub const Do = struct { + save_lockfile: bool = true, + load_lockfile: bool = true, + install_packages: bool = true, + save_yarn_lock: bool = false, + }; + + pub const Enable = struct { + manifest_cache: bool = true, + manifest_cache_control: bool = true, + cache: bool = true, + + /// 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, + + install_dev_dependencies: bool = true, + 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; + } + } + + 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{ + .utf8 = update.name, + }, + logger.Loc.Empty, + ); + + new_dependencies[k].value = JSAst.Expr.init( + JSAst.E.String, + JSAst.E.String{ + // we set it later + .utf8 = "", + }, + 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; + var dependencies_object: JSAst.Expr = undefined; + if (current_package_json.asProperty(dependency_list)) |query| { + if (query.expr.data == .e_object) { + needs_new_dependency_list = false; + + dependencies_object = query.expr; + } + } + + if (needs_new_dependency_list) { + dependencies_object = JSAst.Expr.init( + JSAst.E.Object, + JSAst.E.Object{ + .properties = new_dependencies, + }, + logger.Loc.Empty, + ); + } + + 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{ + .utf8 = dependency_list, + }, + logger.Loc.Empty, + ), + .value = dependencies_object, + }; + current_package_json.* = JSAst.Expr.init(JSAst.E.Object, JSAst.E.Object{ .properties = 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); + root_properties[root_properties.len - 1].key = JSAst.Expr.init( + JSAst.E.String, + JSAst.E.String{ + .utf8 = dependency_list, + }, + logger.Loc.Empty, + ); + root_properties[root_properties.len - 1].value = dependencies_object; + current_package_json.* = JSAst.Expr.init(JSAst.E.Object, JSAst.E.Object{ .properties = root_properties }, logger.Loc.Empty); + } + + dependencies_object.data.e_object.properties = new_dependencies; + dependencies_object.data.e_object.packageJSONSort(); + } + + for (updates) |*update, j| { + var str = update.e_string.?; + + if (update.version.tag == .uninitialized) { + str.utf8 = latest; + } else { + str.utf8 = update.version.literal.slice(update.version_buf); + } + } + } + }; + + fn init( + ctx: Command.Context, + package_json_file_: ?std.fs.File, + comptime params: []const ParamType, + ) !*PackageManager { + // assume that spawning a thread will take a lil so we do that asap + try NetworkThread.init(); + + var cli = try CommandLineArguments.parse(ctx.allocator, params); + + 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", .{ .read = true, .write = true }) catch |err2| 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", .{ .read = true, .write = true }) catch |err| { + 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{}; + var cache_directory: std.fs.Dir = undefined; + + 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.fetchCacheDirectoryPath(ctx.allocator, env_loader, &entries_option.entries)) |cache_dir_path| { + options.cache_directory = try fs.dirname_store.append(@TypeOf(cache_dir_path), cache_dir_path); + cache_directory = std.fs.cwd().makeOpenPath(options.cache_directory, .{ .iterate = true }) catch |err| brk: { + options.enable.cache = false; + options.enable.manifest_cache = false; + options.enable.manifest_cache_control = false; + Output.prettyErrorln("Cache is disabled due to error: {s}", .{@errorName(err)}); + break :brk undefined; + }; + } else {} + + 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 = @minimum(cpu_count, cpu_count_); + } else |err| {} + } + + 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(), + .cache_directory = cache_directory, + .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, + // .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, + ); + manager.registry.url = manager.options.scope.url; + manager.registry.scopes = manager.options.registries; + manager.registry.token = manager.options.scope.token; + manager.registry.auth = manager.options.scope.auth; + + manager.timestamp = @truncate(u32, @intCast(u64, @maximum(std.time.timestamp(), 0))); + 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); + } + + const ParamType = clap.Param(clap.Help); + pub const install_params_ = [_]ParamType{ + clap.parseParam("--registry <STR> Change default registry (default: $BUN_CONFIG_REGISTRY || $npm_config_registry)") catch unreachable, + clap.parseParam("--token <STR> Authentication token used for npm registry requests (default: $npm_config_token)") 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 <STR> Store & load a lockfile at a specific filepath") catch unreachable, + clap.parseParam("-f, --force Always request the latest versions from the registry & reinstall all dependenices") catch unreachable, + clap.parseParam("--cache-dir <STR> 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("--cwd <STR> Set a specific cwd") catch unreachable, + clap.parseParam("--backend <STR> Platform-specific optimizations for installing dependencies. For macOS, \"clonefile\" (default), \"copyfile\"") catch unreachable, + clap.parseParam("--link-native-bins <STR>... Link \"bin\" from a matching platform-specific \"optionalDependencies\" instead. Default: esbuild, turbo") catch unreachable, + + // clap.parseParam("--omit <STR>... 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("<POS> ... ") catch unreachable, + }; + + pub const add_params = install_params_ ++ [_]ParamType{ + clap.parseParam("-d, --development Add depenedency to \"devDependencies\"") catch unreachable, + clap.parseParam("--optional Add depenedency to \"optionalDependencies\"") catch unreachable, + clap.parseParam("<POS> ... \"name\" or \"name@version\" of packages to install") catch unreachable, + }; + + pub const remove_params = install_params_ ++ [_]ParamType{ + clap.parseParam("<POS> ... \"name\" of packages to remove from package.json") catch unreachable, + }; + + const CommandLineArguments = struct { + registry: string = "", + cache_dir: string = "", + lockfile: string = "", + token: string = "", + + 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, + + 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 = false, + 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, + ) !CommandLineArguments { + var diag = clap.Diagnostic{}; + + var args = clap.parse(clap.Help, params, .{ + .diagnostic = &diag, + .allocator = allocator, + }) catch |err| { + // Report useful error and exit + diag.report(Output.errorWriter(), err) catch {}; + return err; + }; + + if (args.flag("--help")) { + Output.prettyln("\n<b><magenta>bun<r> (package manager) flags:<r>\n\n", .{}); + Output.flush(); + + clap.help(Output.writer(), params) catch {}; + + Output.flush(); + std.os.exit(0); + } + + var cli = CommandLineArguments{}; + cli.yarn = args.flag("--yarn"); + cli.production = args.flag("--production"); + cli.no_save = args.flag("--no-save"); + cli.dry_run = args.flag("--dry-run"); + cli.force = args.flag("--force"); + // 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.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("<b>error<r><d>:<r> Invalid argument <b>\"--omit\"<r> must be one of <cyan>\"dev\"<r>, <cyan>\"optional\"<r>, or <cyan>\"peer\"<r>. ", .{}); + // Output.flush(); + // std.os.exit(1); + // } + // } + + if (args.option("--token")) |token| { + cli.token = token; + } + + if (args.option("--lockfile")) |lockfile| { + cli.lockfile = lockfile; + } + + if (args.option("--cwd")) |cwd_| { + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + var buf2: [std.fs.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_ = resolve_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); + } + + if (args.option("--registry")) |registry_| { + cli.registry = registry_; + } + + const specified_backend: ?PackageInstall.Method = brk: { + if (args.option("--backend")) |backend_| { + if (strings.eqlComptime(backend_, "clonefile")) { + break :brk PackageInstall.Method.clonefile; + } else if (strings.eqlComptime(backend_, "clonefile_each_dir")) { + break :brk PackageInstall.Method.clonefile_each_dir; + } else if (strings.eqlComptime(backend_, "hardlink")) { + break :brk PackageInstall.Method.hardlink; + } else if (strings.eqlComptime(backend_, "copyfile")) { + break :brk PackageInstall.Method.copyfile; + } + } + break :brk null; + }; + + if (specified_backend) |backend| { + if (backend.isSupported()) { + cli.backend = backend; + } + } + + cli.positionals = args.positionals(); + + return cli; + } + }; + const latest: string = "latest"; + + const UpdateRequest = struct { + name: string = "", + resolved_version_buf: string = "", + version: Dependency.Version = Dependency.Version{}, + version_buf: []const u8 = "", + resolved_version: Resolution = Resolution{}, + resolution_string_buf: []const u8 = "", + missing_version: bool = false, + e_string: ?*JSAst.E.String = null, + }; + + 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("<r><red>error:<r> {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("<r>No package.json, so nothing to remove\n", .{}); + Global.crash(); + }, + else => return err, + } + + unreachable; + }; + + if (manager.options.log_level != .silent) { + Output.prettyErrorln("<r><b>bun " ++ @tagName(op) ++ " <r><d>v" ++ Global.package_json_version ++ "<r>\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 std.BoundedArray(UpdateRequest, 64).init(0); + var need_to_get_versions_from_npm = false; + + // first one is always either: + // add + // remove + for (manager.options.positionals[1..]) |positional| { + var request = UpdateRequest{ + .name = positional, + }; + var unscoped_name = positional; + if (unscoped_name.len > 0 and unscoped_name[0] == '@') { + request.name = unscoped_name[1..]; + } + if (std.mem.indexOfScalar(u8, unscoped_name, '@')) |i| { + request.name = unscoped_name[0..i]; + + if (unscoped_name.ptr != positional.ptr) { + request.name = request.name[0 .. i + 1]; + } + + if (positional.len > i + 1) request.version_buf = positional[i + 1 ..]; + } + + if (strings.hasPrefix("http://", request.name) or + strings.hasPrefix("https://", request.name)) + { + if (Output.isEmojiEnabled()) { + Output.prettyErrorln("<r>😢 <red>error<r><d>:<r> bun {s} http://url is not implemented yet.", .{ + @tagName(op), + }); + } else { + Output.prettyErrorln("<r><red>error<r><d>:<r> bun {s} http://url is not implemented yet.", .{ + @tagName(op), + }); + } + Output.flush(); + + std.os.exit(1); + } + + request.name = std.mem.trim(u8, request.name, "\n\r\t"); + if (request.name.len == 0) continue; + + 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("<r>😢 <red>error<r><d>:<r> bun {s} http://url is not implemented yet.", .{ + @tagName(op), + }); + } else { + Output.prettyErrorln("<r><red>error<r><d>:<r> bun {s} http://url is not implemented yet.", .{ + @tagName(op), + }); + } + Output.flush(); + + std.os.exit(1); + } + + if (request.version_buf.len == 0) { + need_to_get_versions_from_npm = true; + request.missing_version = true; + } else { + const sliced = SlicedString.init(request.version_buf, request.version_buf); + request.version = Dependency.parse(ctx.allocator, request.version_buf, &sliced, ctx.log) orelse Dependency.Version{}; + } + + update_requests.append(request) catch break; + } + + var updates: []UpdateRequest = update_requests.slice(); + + 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 {}; + } + } + + Output.flush(); + 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], + ); + + initializeStore(); + var current_package_json = json_parser.ParseJSON(&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("<r><red>{s}<r> parsing package.json<r>", .{ + @errorName(err), + }); + }; + + if (op == .remove) { + if (current_package_json.data != .e_object) { + Output.prettyErrorln("<red>error<r><d>:<r> package.json is not an Object {{}}, so there's nothing to remove!", .{}); + Output.flush(); + std.os.exit(1); + return; + } else if (current_package_json.data.e_object.properties.len == 0) { + Output.prettyErrorln("<red>error<r><d>:<r> package.json is empty {{}}, so there's nothing to remove!", .{}); + Output.flush(); + std.os.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("<red>error<r><d>:<r> package.json doesn't have dependencies, there's nothing to remove!", .{}); + Output.flush(); + std.os.exit(1); + 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; + 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 = query.expr.data.e_object.properties[0..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 = std.ArrayListUnmanaged(JSAst.G.Property){ + .items = current_package_json.data.e_object.properties, + .capacity = current_package_json.data.e_object.properties.len, + }; + _ = arraylist.swapRemove(query.i); + current_package_json.data.e_object.properties = arraylist.items; + current_package_json.data.e_object.packageJSONSort(); + } else { + var obj = query.expr.data.e_object; + obj.alphabetizeProperties(); + } + } + } + } + } + } + + if (!any_changes) { + Output.prettyErrorln("\n<red>error<r><d>:<r> \"<b>{s}<r>\" is not in a package.json file", .{updates[0].name}); + Output.flush(); + std.os.exit(1); + return; + } + manager.to_remove = updates; + }, + .add, .update => { + try PackageJSONEditor.edit(ctx.allocator, updates, ¤t_package_json, dependency_list); + manager.package_json_updates = updates; + }, + } + + 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 = package_json_writer.ctx.buffer.toOwnedSliceLeaky().ptr[0 .. written + 1]; + + try installWithManager(ctx, manager, new_package_json_source, log_level); + + if (op == .update or op == .add) { + 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.ParseJSON(&source, ctx.log, manager.allocator) catch |err| { + Output.prettyErrorln("<red>error<r><d>:<r> package.json failed to parse due to error {s}", .{@errorName(err)}); + Output.flush(); + std.os.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, current_package_json_buf.len + 1); + 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 = package_json_writer_two.ctx.buffer.toOwnedSliceLeaky().ptr[0 .. written + 1]; + } + + if (!manager.options.dry_run) { + // 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, written + 1) catch {}; + manager.root_package_json_file.close(); + + if (op == .remove) { + var cwd = std.fs.cwd(); + // This is not exactly correct + var node_modules_buf: [std.fs.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.openDirZ("node_modules/.bin", .{ + .iterate = true, + })) |node_modules_bin_| { + var node_modules_bin: std.fs.Dir = node_modules_bin_; + var iter: std.fs.Dir.Iterator = node_modules_bin.iterate(); + iterator: while (iter.next() catch null) |entry| { + switch (entry.kind) { + std.fs.Dir.Entry.Kind.SymLink => { + if (std.fs.path.extension(entry.name).len == 0) { + // 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.openFileZ(buf, .{ .read = true }) catch |err| { + node_modules_bin.deleteFileZ(buf) catch {}; + continue :iterator; + }; + + file.close(); + } + }, + else => {}, + } + } + } else |_| {} + } + } + } + + var cwd_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + var package_json_cwd_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + + pub inline fn install( + ctx: Command.Context, + ) !void { + var manager = try PackageManager.init(ctx, null, &install_params); + + if (manager.options.log_level != .silent) { + Output.prettyErrorln("<r><b>bun install <r><d>v" ++ Global.package_json_version ++ "<r>\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("<r><red>{s} reading package.json<r> :(", .{@errorName(err)}); + Output.flush(); + } + return; + }; + + switch (manager.options.log_level) { + .default => try installWithManager(ctx, manager, package_json_contents, .default), + .verbose => try installWithManager(ctx, manager, package_json_contents, .verbose), + .silent => try installWithManager(ctx, manager, package_json_contents, .silent), + .default_no_progress => try installWithManager(ctx, manager, package_json_contents, .default_no_progress), + .verbose_no_progress => try installWithManager(ctx, manager, package_json_contents, .verbose_no_progress), + } + } + + const PackageInstaller = struct { + manager: *PackageManager, + lockfile: *Lockfile, + progress: *std.Progress, + node_modules_folder: std.fs.Dir, + skip_verify: bool, + skip_delete: bool, + force_install: bool, + root_node_modules_folder: std.fs.Dir, + 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, + destination_dir_subpath_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined, + install_count: usize = 0, + successfully_installed: std.DynamicBitSetUnmanaged, + + // 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.Dir, + }; + + /// 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 buf = this.lockfile.buffers.string_bytes.items; + + const name = this.names[package_id].slice(buf); + const resolution = this.resolutions[package_id]; + + var callbacks = this.manager.task_queue.fetchRemove(Task.Id.forNPMPackage( + Task.Tag.extract, + name, + resolution.value.npm, + )).?.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 = std.fs.Dir{ .fd = @intCast(std.os.fd_t, 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 { + std.mem.copy(u8, &this.destination_dir_subpath_buf, name); + this.destination_dir_subpath_buf[name.len] = 0; + var destination_dir_subpath: [:0]u8 = this.destination_dir_subpath_buf[0..name.len :0]; + var resolution_buf: [512]u8 = undefined; + const buf = this.lockfile.buffers.string_bytes.items; + var resolution_label = std.fmt.bufPrint(&resolution_buf, "{}", .{resolution.fmt(buf)}) catch unreachable; + switch (resolution.tag) { + .npm => { + var installer = PackageInstall{ + .cache_dir = this.manager.cache_directory, + .progress = this.progress, + .cache_dir_subpath = PackageManager.cachedNPMPackageFolderName(name, resolution.value.npm), + .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, + }; + + const needs_install = this.force_install or this.skip_verify or !installer.verify(); + this.summary.skipped += @as(u32, @boolToInt(!needs_install)); + + if (needs_install) { + const result = 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) { + this.node_modules_folder.makeDirZ(".bin") catch {}; + Bin.Linker.umask = C.umask(0); + 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.fd, + .root_node_modules_folder = this.root_node_modules_folder.fd, + .package_name = strings.StringOrTinyString.init(name), + .string_buf = buf, + }; + + bin_linker.link(); + + if (comptime log_level != .silent) { + if (bin_linker.err) |err| { + const fmt = "\n<r><red>error:<r> linking <b>{s}<r>: {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); + } + } + } + } + } + } + }, + .fail => |cause| { + if (cause.isPackageMissingFromCache()) { + const task_id = Task.Id.forNPMPackage(Task.Tag.extract, name, resolution.value.npm); + var task_queue = this.manager.task_queue.getOrPut(this.manager.allocator, task_id) catch unreachable; + if (!task_queue.found_existing) { + task_queue.value_ptr.* = .{}; + } + + task_queue.value_ptr.append( + this.manager.allocator, + .{ + .node_modules_folder = @intCast(u32, this.node_modules_folder.fd), + }, + ) catch unreachable; + + if (this.manager.generateNetworkTaskForTarball(task_id, this.lockfile.packages.get(package_id)) catch unreachable) |task| { + task.schedule(&this.manager.network_tarball_batch); + if (this.manager.network_tarball_batch.len > 6) { + _ = this.manager.scheduleNetworkTasks(); + } + } + } else { + Output.prettyErrorln( + "<r><red>error<r>: <b><red>{s}<r> installing <b>{s}<r>", + .{ @errorName(cause.err), this.names[package_id].slice(buf) }, + ); + this.summary.fail += 1; + } + }, + else => {}, + } + } + }, + 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 buf = this.lockfile.buffers.string_bytes.items; + const name = this.names[package_id].slice(buf); + const resolution = this.resolutions[package_id]; + + this.installPackageWithNameAndResolution(package_id, log_level, name, resolution); + } + }; + + pub fn installPackages( + this: *PackageManager, + lockfile: *Lockfile, + comptime log_level: PackageManager.Options.LogLevel, + ) !PackageInstall.Summary { + 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 = try progress.start("", 0); + 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.* = .{}; + } + } + const cache_dir = this.cache_directory; + + lockfile.unique_packages.unset(0); + + // 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 = false; + var node_modules_folder = std.fs.cwd().openDirZ("node_modules", .{ .iterate = true }) catch brk: { + skip_verify = true; + std.fs.cwd().makeDirZ("node_modules") catch |err| { + Output.prettyErrorln("<r><red>error<r>: <b><red>{s}<r> creating <b>node_modules<r> folder", .{@errorName(err)}); + Output.flush(); + Global.crash(); + }; + break :brk std.fs.cwd().openDirZ("node_modules", .{ .iterate = true }) catch |err| { + Output.prettyErrorln("<r><red>error<r>: <b><red>{s}<r> opening <b>node_modules<r> folder", .{@errorName(err)}); + Output.flush(); + Global.crash(); + }; + }; + var skip_delete = skip_verify; + const force_install = options.enable.force_install; + if (options.enable.force_install) { + skip_verify = 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); + const end = @truncate(PackageID, names.len); + const pending_task_offset = this.total_tasks; + 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 = skip_verify, + .skip_delete = skip_delete, + .summary = &summary, + .force_install = force_install, + .install_count = lockfile.buffers.hoisted_packages.items.len, + .successfully_installed = try std.DynamicBitSetUnmanaged.initEmpty(lockfile.packages.len, this.allocator), + }; + + 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.openDirZ(node_modules.relative_path, .{ + .iterate = true, + }); + + 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, + PackageInstaller.installEnqueuedPackages, + log_level, + ); + } + } + + for (remaining) |package_id| { + installer.installPackage(@truncate(PackageID, package_id), log_level); + } + + try this.runTasks( + *PackageInstaller, + &installer, + PackageInstaller.installEnqueuedPackages, + log_level, + ); + } + + while (this.pending_tasks > 0) { + try this.runTasks( + *PackageInstaller, + &installer, + PackageInstaller.installEnqueuedPackages, + log_level, + ); + } + + 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) |dependency, 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 = installer.names[resolved_id].slice(lockfile.buffers.string_bytes.items); + + if (!installer.has_created_bin) { + node_modules_folder.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.fd, + .root_node_modules_folder = node_modules_folder.fd, + .package_name = strings.StringOrTinyString.init(name), + .string_buf = lockfile.buffers.string_bytes.items, + }; + + bin_linker.link(); + + if (comptime log_level != .silent) { + if (bin_linker.err) |err| { + const fmt = "\n<r><red>error:<r> linking <b>{s}<r>: {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); + } + } + } + + continue :outer; + } + + if (comptime log_level != .silent) { + const fmt = "\n<r><yellow>warn:<r> no compatible binaries found for <b>{s}<r>\n"; + const args = .{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; + } + + fn installWithManager( + ctx: Command.Context, + manager: *PackageManager, + package_json_contents: string, + comptime log_level: Options.LogLevel, + ) !void { + 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 needs_new_lockfile = load_lockfile_result != .ok; + + // 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| { + switch (cause.step) { + .open_file => Output.prettyErrorln("<r><red>error opening lockfile:<r> {s}. <b>Ignoring lockfile<r>.", .{ + @errorName(cause.value), + }), + .parse_file => Output.prettyErrorln("<r><red>error parsing lockfile:<r> {s}. <b>Ignoring lockfile<r>.", .{ + @errorName(cause.value), + }), + .read_file => Output.prettyErrorln("<r><red>error reading lockfile:<r> {s}. <b>Ignoring lockfile<r>.", .{ + @errorName(cause.value), + }), + } + 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(); + }, + .ok => { + differ: { + root = load_lockfile_result.ok.rootPackage() orelse { + needs_new_lockfile = true; + break :differ; + }; + + if (root.dependencies.len == 0) { + needs_new_lockfile = true; + break :differ; + } + + var lockfile: Lockfile = undefined; + try lockfile.initEmpty(ctx.allocator); + var new_root: Lockfile.Package = undefined; + if (manager.options.enable.install_dev_dependencies) { + try Lockfile.Package.parse( + &lockfile, + &new_root, + ctx.allocator, + ctx.log, + package_json_source, + Features{ + .optional_dependencies = true, + .dev_dependencies = true, + .is_main = true, + .check_for_duplicate_dependencies = true, + .peer_dependencies = false, + }, + ); + } else { + try Lockfile.Package.parse( + &lockfile, + &new_root, + ctx.allocator, + ctx.log, + package_json_source, + Features{ + .optional_dependencies = true, + .dev_dependencies = false, + .is_main = true, + .check_for_duplicate_dependencies = true, + .peer_dependencies = false, + }, + ); + } + var mapping = try manager.lockfile.allocator.alloc(PackageID, new_root.dependencies.len); + std.mem.set(PackageID, mapping, invalid_package_id); + + manager.summary = try Package.Diff.generate( + ctx.allocator, + manager.lockfile, + &lockfile, + &root, + &new_root, + mapping, + ); + + const sum = manager.summary.add + manager.summary.remove + manager.summary.update; + had_any_diffs = had_any_diffs or sum > 0; + + // If you changed packages, we will copy over the new package from the new lockfile + const new_dependencies = new_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, i| { + 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_dependencies_list = dep_lists[0]; + 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; + while (std.mem.indexOfScalar(PackageID, remaining, invalid_package_id)) |next_i_| { + remaining = remaining[next_i_ + 1 ..]; + + dependency_i += @intCast(PackageID, next_i_); + try manager.enqueueDependencyWithMain( + dependency_i, + manager.lockfile.buffers.dependencies.items[dependency_i], + 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.install_dev_dependencies) { + try Lockfile.Package.parse( + manager.lockfile, + &root, + ctx.allocator, + ctx.log, + package_json_source, + Features{ + .optional_dependencies = true, + .dev_dependencies = true, + .is_main = true, + .check_for_duplicate_dependencies = true, + .peer_dependencies = false, + }, + ); + } else { + try Lockfile.Package.parse( + manager.lockfile, + &root, + ctx.allocator, + ctx.log, + package_json_source, + Features{ + .optional_dependencies = true, + .dev_dependencies = false, + .is_main = true, + .check_for_duplicate_dependencies = true, + .peer_dependencies = false, + }, + ); + } + + root = try manager.lockfile.appendPackage(root); + + manager.root_dependency_list = root.dependencies; + 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 (comptime log_level.showProgress()) { + manager.downloads_node = try manager.progress.start(ProgressStrings.download(), 0); + 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(); + } + + while (manager.pending_tasks > 0) { + try manager.runTasks(void, void{}, null, log_level); + } + + if (comptime log_level.showProgress()) { + manager.downloads_node.?.setEstimatedTotalItems(manager.downloads_node.?.unprotected_estimated_total_items); + manager.downloads_node.?.setCompletedItems(manager.downloads_node.?.unprotected_estimated_total_items); + manager.progress.refresh(); + manager.progress.root.end(); + manager.progress = .{}; + manager.downloads_node = null; + } + } + + 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) { + Output.flush(); + std.os.exit(1); + } + + if (had_any_diffs or needs_new_lockfile or manager.package_json_updates.len > 0) { + manager.lockfile = try manager.lockfile.clean(&manager.summary.deduped, manager.package_json_updates, &manager.options); + } + + if (manager.options.do.save_lockfile) { + var node: *Progress.Node = undefined; + + if (comptime log_level.showProgress()) { + node = try manager.progress.start(ProgressStrings.save(), 0); + 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 = .{}; + } + } + + 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 = try manager.progress.start("Saving yarn.lock", 0); + manager.progress.refresh(); + } + + try manager.writeYarnLock(); + if (comptime log_level.showProgress()) { + node.completeOne(); + manager.progress.refresh(); + manager.progress.root.end(); + manager.progress = .{}; + } + } + + if (comptime log_level != .silent) { + var printer = Lockfile.Printer{ + .lockfile = manager.lockfile, + .options = manager.options, + .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 (install_summary.success > 0) { + Output.pretty("\n <green>{d}<r> packages<r> installed ", .{install_summary.success}); + Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + Output.pretty("<r>\n", .{}); + + if (manager.summary.update > 0) { + Output.pretty(" Updated: <cyan>{d}<r>\n", .{manager.summary.update}); + } + + if (manager.summary.remove > 0) { + Output.pretty(" Removed: <cyan>{d}<r>\n", .{manager.summary.remove}); + } + } else if (manager.summary.remove > 0) { + if (manager.to_remove.len > 0) { + for (manager.to_remove) |update| { + Output.prettyln(" <r><red>-<r> {s}", .{update.name}); + } + } + + Output.pretty("\n <r><b>{d}<r> packages removed ", .{manager.summary.remove}); + Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + Output.pretty("<r>\n", .{}); + } else if (install_summary.skipped > 0 and install_summary.fail == 0) { + Output.pretty("\n", .{}); + + const count = @truncate(PackageID, manager.lockfile.packages.len); + if (count != install_summary.skipped) { + Output.pretty("Checked <green>{d} installs<r> across {d} packages <d>(no changes)<r> ", .{ + install_summary.skipped, + count, + }); + Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + Output.pretty("<r>\n", .{}); + } else { + Output.pretty("<r> Done! Checked <green>{d} packages<r> <d>(no changes)<r> ", .{ + install_summary.skipped, + }); + Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); + Output.pretty("<r>\n", .{}); + } + } else if (manager.summary.update > 0) { + Output.prettyln(" Updated: <cyan>{d}<r>\n", .{manager.summary.update}); + } + + if (install_summary.fail > 0) { + Output.prettyln("<r> Failed to install <red><b>{d}<r> packages", .{install_summary.fail}); + } + } + Output.flush(); + } +}; + +const Package = Lockfile.Package; |
