Files
bun.sh/src/install/lockfile.zig
2022-08-07 23:23:34 -07:00

3095 lines
124 KiB
Zig

const bun = @import("../global.zig");
const string = bun.string;
const Output = bun.Output;
const Global = bun.Global;
const Environment = bun.Environment;
const strings = bun.strings;
const MutableString = bun.MutableString;
const stringZ = bun.stringZ;
const default_allocator = bun.default_allocator;
const C = bun.C;
const std = @import("std");
const JSLexer = @import("../js_lexer.zig");
const logger = @import("../logger.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");
const panicky = @import("../panic_handler.zig");
const sync = @import("../sync.zig");
const Api = @import("../api/schema.zig").Api;
const Path = @import("../resolver/resolve_path.zig");
const configureTransformOptionsForBun = @import("../bun.js/config.zig").configureTransformOptionsForBun;
const Command = @import("../cli.zig").Command;
const BunArguments = @import("../cli.zig").Arguments;
const bundler = @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 HeaderBuilder = @import("http").HeaderBuilder;
const Fs = @import("../fs.zig");
const FileSystem = Fs.FileSystem;
const Lock = @import("../lock.zig").Lock;
var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
var path_buf2: [bun.MAX_PATH_BYTES]u8 = undefined;
const URL = @import("../url.zig").URL;
const AsyncHTTP = @import("http").AsyncHTTP;
const HTTPChannel = @import("http").HTTPChannel;
const NetworkThread = @import("http").NetworkThread;
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;
const z_allocator = @import("../memory_allocator.zig").z_allocator;
const Lockfile = @This();
const IdentityContext = @import("../identity_context.zig").IdentityContext;
const ArrayIdentityContext = @import("../identity_context.zig").ArrayIdentityContext;
const Semver = @import("./semver.zig");
const ExternalString = Semver.ExternalString;
const String = Semver.String;
const GlobalStringBuilder = @import("../string_builder.zig");
const SlicedString = Semver.SlicedString;
const Repository = @import("./repository.zig").Repository;
const StructBuilder = @import("../builder.zig");
const Bin = @import("./bin.zig").Bin;
const Dependency = @import("./dependency.zig");
const Behavior = @import("./dependency.zig").Behavior;
const FolderResolution = @import("./resolvers/folder_resolver.zig").FolderResolution;
const PackageManager = @import("./install.zig").PackageManager;
const ExternalSlice = @import("./install.zig").ExternalSlice;
const ExternalSliceAligned = @import("./install.zig").ExternalSliceAligned;
const PackageID = @import("./install.zig").PackageID;
const Features = @import("./install.zig").Features;
const PackageInstall = @import("./install.zig").PackageInstall;
const PackageNameHash = @import("./install.zig").PackageNameHash;
const Aligner = @import("./install.zig").Aligner;
const ExternalStringMap = @import("./install.zig").ExternalStringMap;
const alignment_bytes_to_repeat_buffer = @import("./install.zig").alignment_bytes_to_repeat_buffer;
const Resolution = @import("./resolution.zig").Resolution;
const initializeStore = @import("./install.zig").initializeStore;
const invalid_package_id = @import("./install.zig").invalid_package_id;
const JSAst = @import("../js_ast.zig");
const Origin = @import("./install.zig").Origin;
const PackageIDMultiple = @import("./install.zig").PackageIDMultiple;
const Crypto = @import("../sha.zig").Hashers;
pub const MetaHash = [std.crypto.hash.sha2.Sha512256.digest_length]u8;
const zero_hash = std.mem.zeroes(MetaHash);
pub const ExternalStringBuilder = StructBuilder.Builder(ExternalString);
pub const SmallExternalStringList = ExternalSlice(String);
// Serialized data
/// The version of the lockfile format, intended to prevent data corruption for format changes.
format: FormatVersion = .v1,
/// Not used yet.
/// Eventually, this will be a relative path to a parent lockfile
workspace_path: string = "",
meta_hash: MetaHash = zero_hash,
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{},
scripts: Scripts = .{},
const Stream = std.io.FixedBufferStream([]u8);
pub const default_filename = "bun.lockb";
pub const Scripts = struct {
const StringArrayList = std.ArrayListUnmanaged(string);
const RunCommand = @import("../cli/run_command.zig").RunCommand;
preinstall: StringArrayList = .{},
install: StringArrayList = .{},
postinstall: StringArrayList = .{},
preprepare: StringArrayList = .{},
prepare: StringArrayList = .{},
postprepare: StringArrayList = .{},
pub fn hasAny(this: Scripts) bool {
return (this.preinstall.items.len +
this.install.items.len +
this.postinstall.items.len +
this.preprepare.items.len +
this.prepare.items.len +
this.postprepare.items.len) > 0;
}
pub fn run(this: Scripts, allocator: std.mem.Allocator, env: *DotEnv.Loader, silent: bool, comptime hook: []const u8) !void {
for (@field(this, hook).items) |script| {
std.debug.assert(Fs.FileSystem.instance_loaded);
const cwd = Fs.FileSystem.instance.top_level_dir;
_ = try RunCommand.runPackageScript(allocator, script, hook, cwd, env, &.{}, silent);
}
}
};
pub fn isEmpty(this: *const Lockfile) bool {
return this.packages.len == 0 or this.packages.len == 1 or this.packages.get(0).resolutions.len == 0;
}
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.io.getStdIn();
if (filename.len > 0)
file = std.fs.cwd().openFileZ(filename, .{ .mode = .read_only }) catch |err| {
return switch (err) {
error.FileNotFound, error.AccessDenied, error.BadPathName => 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 } };
};
return this.loadFromBytes(buf, allocator, log);
}
pub fn loadFromBytes(this: *Lockfile, buf: []u8, allocator: std.mem.Allocator, log: *logger.Log) LoadFromDiskResult {
var stream = Stream{ .buffer = buf, .pos = 0 };
this.workspace_path = "";
this.format = FormatVersion.current;
this.scripts = .{};
Lockfile.Serializer.load(this, &stream, allocator, log) catch |err| {
return LoadFromDiskResult{ .err = .{ .step = .parse_file, .value = err } };
};
return LoadFromDiskResult{ .ok = this };
}
pub const InstallResult = struct {
lockfile: *Lockfile,
summary: PackageInstall.Summary,
};
pub const Tree = struct {
id: Id = invalid_id,
package_id: PackageID = invalid_package_id,
parent: Id = invalid_id,
packages: Lockfile.PackageIDSlice = Lockfile.PackageIDSlice{},
pub const external_size = @sizeOf(Id) + @sizeOf(PackageID) + @sizeOf(Id) + @sizeOf(Lockfile.PackageIDSlice);
pub const External = [external_size]u8;
pub const Slice = ExternalSlice(Tree);
pub const List = std.ArrayListUnmanaged(Tree);
pub const Id = u32;
pub fn toExternal(this: Tree) External {
var out = External{};
out[0..4].* = @bitCast(Id, this.id);
out[4..8].* = @bitCast(Id, this.package_id);
out[8..12].* = @bitCast(Id, this.parent);
out[12..16].* = @bitCast(u32, this.packages.off);
out[16..20].* = @bitCast(u32, this.packages.len);
if (out.len != 20) @compileError("Tree.External is not 20 bytes");
return out;
}
pub fn toTree(out: External) Tree {
return Tree{
.id = @bitCast(Id, out[0..4].*),
.package_id = @bitCast(Id, out[4..8].*),
.parent = @bitCast(Id, out[8..12].*),
.packages = .{
.off = @bitCast(u32, out[12..16].*),
.len = @bitCast(u32, out[16..20].*),
},
};
}
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: [bun.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: [(bun.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 {
var end = @truncate(Id, this.list.len);
var i: Id = 0;
var total_packages_count: u32 = 0;
var trees = this.list.items(.tree);
var packages = this.list.items(.packages);
// var real_end: Id = 0;
// TODO: can we cull empty trees here?
while (i < end) : (i += 1) {
total_packages_count += trees[i].packages.len;
// if (!(prev == total_packages_count and trees[i].package_id >= max_package_id)) {
// trees[real_end] = trees[i];
// packages[real_end] = packages[i];
// real_end += 1;
// }
}
// this.list.len = real_end;
// trees = trees[0..real_end];
// packages = packages[0..real_end];
var package_ids = try z_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| {
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;
}
};
/// This conditonally clones the lockfile with root packages marked as non-resolved that do not satisfy `Features`. The package may still end up installed even if it was e.g. in "devDependencies" and its a production install. In that case, it would be installed because another dependency or transient dependency needed it
///
/// Warning: This potentially modifies the existing lockfile in-place. That is safe to do because at this stage, the lockfile has already been saved to disk. Our in-memory representation is all that's left.
pub fn maybeCloneFilteringRootPackages(
old: *Lockfile,
features: Features,
) !*Lockfile {
const old_root_dependenices_list: DependencySlice = old.packages.items(.dependencies)[0];
var old_root_resolutions: PackageIDSlice = old.packages.items(.resolutions)[0];
const root_dependencies = old_root_dependenices_list.get(old.buffers.dependencies.items);
var resolutions = old_root_resolutions.mut(old.buffers.resolutions.items);
var any_changes = false;
const end = @truncate(PackageID, old.packages.len);
for (root_dependencies) |dependency, i| {
if (!dependency.behavior.isEnabled(features) and resolutions[i] < end) {
resolutions[i] = invalid_package_id;
any_changes = true;
}
}
if (!any_changes) return old;
return try old.clean(&.{});
}
fn preprocessUpdateRequests(old: *Lockfile, updates: []PackageManager.UpdateRequest) !void {
const root_deps_list: Lockfile.DependencySlice = old.packages.items(.dependencies)[0];
if (@as(usize, root_deps_list.off) < old.buffers.dependencies.items.len) {
var string_builder = old.stringBuilder();
{
const root_deps: []const Dependency = root_deps_list.get(old.buffers.dependencies.items);
const old_resolutions_list = old.packages.items(.resolutions)[0];
const old_resolutions: []const PackageID = old_resolutions_list.get(old.buffers.resolutions.items);
const resolutions_of_yore: []const Resolution = old.packages.items(.resolution);
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 temp_buf: [513]u8 = undefined;
var root_deps: []Dependency = root_deps_list.mut(old.buffers.dependencies.items);
const old_resolutions_list_lists = old.packages.items(.resolutions);
const old_resolutions_list = old_resolutions_list_lists[0];
const old_resolutions: []const PackageID = old_resolutions_list.get(old.buffers.resolutions.items);
const resolutions_of_yore: []const Resolution = old.packages.items(.resolution);
for (updates) |update, update_i| {
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];
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,
);
dep.version = Dependency.parse(
old.allocator,
sliced.slice,
&sliced,
null,
) orelse Dependency.Version{};
}
}
}
updates[update_i].e_string = null;
}
}
}
}
pub fn clean(old: *Lockfile, updates: []PackageManager.UpdateRequest) !*Lockfile {
const old_scripts = old.scripts;
// We will only shrink the number of packages here.
// never grow
if (updates.len > 0) {
try old.preprocessUpdateRequests(updates);
}
// 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);
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;
}
}
}
}
}
new.scripts = old_scripts;
return new;
}
pub const MetaHashFormatter = struct {
meta_hash: *const MetaHash,
pub fn format(this: MetaHashFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
var remain: []const u8 = this.meta_hash[0..];
try std.fmt.format(
writer,
"{}-{}-{}-{}",
.{
std.fmt.fmtSliceHexUpper(remain[0..8]),
std.fmt.fmtSliceHexLower(remain[8..16]),
std.fmt.fmtSliceHexUpper(remain[16..24]),
std.fmt.fmtSliceHexLower(remain[24..32]),
},
);
}
};
pub fn fmtMetaHash(this: *const Lockfile) MetaHashFormatter {
return .{
.meta_hash = &this.meta_hash,
};
}
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,
);
}
if (this.lockfile.buffers.dependencies.items.len > 0)
try this.hoist();
// capacity is used for calculating byte size
// so we need to make sure it's exact
if (this.lockfile.packages.capacity != this.lockfile.packages.len and this.lockfile.packages.len > 0)
this.lockfile.packages.shrinkAndFree(this.lockfile.allocator, this.lockfile.packages.len);
}
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: ?Bitset = null,
updates: []const PackageManager.UpdateRequest = &[_]PackageManager.UpdateRequest{},
pub const Format = enum { yarn };
var lockfile_path_buf1: [bun.MAX_PATH_BYTES]u8 = undefined;
var lockfile_path_buf2: [bun.MAX_PATH_BYTES]u8 = undefined;
pub fn print(
allocator: std.mem.Allocator,
log: *logger.Log,
lockfile_path_: string,
format: Format,
) !void {
@setCold(true);
var lockfile_path: stringZ = "";
if (!std.fs.path.isAbsolute(lockfile_path_)) {
var cwd = try std.os.getcwd(&lockfile_path_buf1);
var parts = [_]string{lockfile_path_};
var lockfile_path__ = 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];
}
if (lockfile_path.len > 0 and lockfile_path[0] == std.fs.path.sep)
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<r> opening lockfile:<r> {s}.", .{
@errorName(cause.value),
}),
.parse_file => Output.prettyErrorln("<r><red>error<r> parsing lockfile:<r> {s}", .{
@errorName(cause.value),
}),
.read_file => Output.prettyErrorln("<r><red>error<r> 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);
}
}
Global.exit(1);
return;
},
.not_found => {
Output.prettyErrorln("<r><red>lockfile not found:<r> {s}", .{
std.mem.span(lockfile_path),
});
Global.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,
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 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 names_hashes: []const PackageNameHash = slice.items(.name_hash);
const bins: []const Bin = slice.items(.bin);
const resolved: []const Resolution = slice.items(.resolution);
if (names.len == 0) return;
const resolutions_list = slice.items(.resolutions);
const resolutions_buffer = this.lockfile.buffers.resolutions.items;
const string_buf = this.lockfile.buffers.string_bytes.items;
var id_map = try default_allocator.alloc(PackageID, this.updates.len);
std.mem.set(PackageID, id_map, std.math.maxInt(PackageID));
defer if (id_map.len > 0) default_allocator.free(id_map);
visited.set(0);
const end = @truncate(PackageID, names.len);
if (this.successfully_installed) |installed| {
outer: for (resolutions_list[0].get(resolutions_buffer)) |package_id| {
if (package_id > end) continue;
const is_new = installed.isSet(package_id);
const package_name = names[package_id].slice(string_buf);
if (this.updates.len > 0) {
const name_hash = names_hashes[package_id];
for (this.updates) |update, update_id| {
if (update.failed) return;
if (update.name.len == package_name.len and name_hash == update.name_hash) {
if (id_map[update_id] == std.math.maxInt(PackageID)) {
id_map[update_id] = @truncate(PackageID, package_id);
}
continue :outer;
}
}
}
if (!is_new) continue;
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 {
outer: for (names) |name, package_id| {
const package_name = name.slice(string_buf);
if (this.updates.len > 0) {
const name_hash = names_hashes[package_id];
for (this.updates) |update, update_id| {
if (update.failed) return;
if (update.name.len == package_name.len and name_hash == update.name_hash) {
if (id_map[update_id] == std.math.maxInt(PackageID)) {
id_map[update_id] = @truncate(PackageID, package_id);
}
continue :outer;
}
}
}
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),
},
);
}
}
if (this.updates.len > 0) {
try writer.writeAll("\n");
}
for (this.updates) |_, update_id| {
const package_id = id_map[update_id];
if (package_id == std.math.maxInt(PackageID)) continue;
const name = names[package_id];
const bin = bins[package_id];
const package_name = name.slice(string_buf);
switch (bin.tag) {
.none, .dir => {
const fmt = comptime brk: {
if (enable_ansi_colors) {
break :brk Output.prettyFmt("<r> <green>installed<r> <b>{s}<r><d>@{}<r>\n", enable_ansi_colors);
} else {
break :brk Output.prettyFmt("<r> installed {s}<r><d>@{}<r>\n", enable_ansi_colors);
}
};
try writer.print(
comptime Output.prettyFmt(fmt, enable_ansi_colors),
.{
package_name,
resolved[package_id].fmt(string_buf),
},
);
},
.map, .file, .named_file => {
var iterator = Bin.NamesIterator{
.bin = bin,
.package_name = name,
.string_buffer = string_buf,
.extern_string_buf = this.lockfile.buffers.extern_strings.items,
};
const fmt = comptime brk: {
if (enable_ansi_colors) {
break :brk Output.prettyFmt("<r> <green>installed<r> {s}<r><d>@{}<r> with binaries:\n", enable_ansi_colors);
} else {
break :brk Output.prettyFmt("<r> installed {s}<r><d>@{}<r> with binaries:\n", enable_ansi_colors);
}
};
try writer.print(
comptime Output.prettyFmt(fmt, enable_ansi_colors),
.{
package_name,
resolved[package_id].fmt(string_buf),
},
);
while (iterator.next() catch null) |bin_name| {
try writer.print(
comptime Output.prettyFmt("<r> <d>- <r><b>{s}<r>\n", enable_ansi_colors),
.{
bin_name,
},
);
}
},
}
}
if (this.updates.len > 0) {
try writer.writeAll("\n");
}
}
};
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
\\# bun ./bun.lockb --hash:
);
try writer.print(
"{}\n\n",
.{this.lockfile.fmtMetaHash()},
);
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_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] == '@';
var prev_dependency_version: ?Dependency.Version = null;
var needs_comma = false;
for (dependency_versions) |dependency_version| {
if (needs_comma) {
if (prev_dependency_version) |prev| {
if (prev.eql(dependency_version, string_buf, string_buf)) {
continue;
}
}
try writer.writeAll(", ");
needs_comma = false;
}
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('"');
}
prev_dependency_version = dependency_version;
needs_comma = true;
}
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| {
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 verifyData(this: *Lockfile) !void {
std.debug.assert(this.format == Lockfile.FormatVersion.current);
{
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 verifyResolutions(this: *Lockfile, local_features: Features, remote_features: Features, comptime log_level: PackageManager.Options.LogLevel) void {
const resolutions_list: []const PackageIDSlice = this.packages.items(.resolutions);
const dependency_lists: []const DependencySlice = this.packages.items(.dependencies);
const dependencies_buffer = this.buffers.dependencies.items;
const resolutions_buffer = this.buffers.resolutions.items;
const end = @truncate(PackageID, this.packages.len);
var any_failed = false;
const string_buf = this.buffers.string_bytes.items;
const root_list = resolutions_list[0];
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.isEnabled(if (root_list.contains(@truncate(PackageID, parent_id)))
local_features
else
remote_features)) continue;
if (log_level != .silent)
Output.prettyErrorln(
"<r><red>error<r><d>:<r> <b>{s}<r><d>@<b>{}<r><d> failed to resolve<r>\n",
.{
failed_dep.name.slice(string_buf),
failed_dep.version.literal.fmt(string_buf),
},
);
// track this so we can log each failure instead of just the first
any_failed = true;
}
}
}
if (any_failed) {
Global.exit(1);
}
}
pub fn saveToDisk(this: *Lockfile, filename: stringZ) void {
if (comptime Environment.allow_assert) {
this.verifyData() catch |err| {
Output.prettyErrorln("<r><red>error:<r> failed to verify lockfile: {s}", .{@errorName(err)});
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.Xoodoo.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)});
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)});
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)});
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 fn initEmpty(this: *Lockfile, allocator: std.mem.Allocator) !void {
this.* = Lockfile{
.format = Lockfile.FormatVersion.current,
.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),
.scripts = .{},
};
}
pub fn getPackageID(
this: *Lockfile,
name_hash: u64,
// if it's a peer dependency, a folder, or a symlink
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.version)) {
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.version)) {
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);
}
}
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,
// bun v0.0.x - bun v0.1.6
v1,
// bun v0.1.7+
// This change added tarball URLs to npm-resolved packages
v2,
_,
pub const current = FormatVersion.v2;
};
pub const DependencySlice = ExternalSlice(Dependency);
pub const PackageIDSlice = ExternalSlice(PackageID);
pub const PackageIDList = std.ArrayListUnmanaged(PackageID);
pub const DependencyList = std.ArrayListUnmanaged(Dependency);
pub const StringBuffer = std.ArrayListUnmanaged(u8);
pub const ExternalStringBuffer = std.ArrayListUnmanaged(ExternalString);
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 this = this_.*;
const old_string_buf = old.buffers.string_bytes.items;
const old_extern_string_buf = old.buffers.extern_strings.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);
const new_extern_string_count = this.bin.count(old_string_buf, old_extern_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| {
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);
try new.buffers.extern_strings.ensureUnusedCapacity(new.allocator, new_extern_string_count);
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];
new.buffers.extern_strings.items.len += new_extern_string_count;
var new_extern_strings = new.buffers.extern_strings.items[new.buffers.extern_strings.items.len - new_extern_string_count ..];
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, old_extern_string_buf, new.buffers.extern_strings.items, new_extern_strings, *Lockfile.StringBuilder, builder),
.name_hash = this.name_hash,
.meta = this.meta.clone(
id,
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 package = Lockfile.Package{};
const package_version = package_version_ptr.*;
const dependency_groups = comptime brk: {
var out_groups: [
@as(usize, @boolToInt(features.dependencies)) +
@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;
if (features.dependencies) {
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;
var bin_extern_strings_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));
}
}
bin_extern_strings_count = package_version.bin.count(string_buf, manifest.extern_strings_bin_entries, @TypeOf(&string_builder), &string_builder);
}
string_builder.count(manifest.str(package_version_ptr.tarball_url));
try string_builder.allocate();
defer string_builder.clamp();
var extern_strings_list = &lockfile.buffers.extern_strings;
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);
try extern_strings_list.ensureUnusedCapacity(lockfile.allocator, bin_extern_strings_count);
extern_strings_list.items.len += bin_extern_strings_count;
var extern_strings_slice = extern_strings_list.items[extern_strings_list.items.len - bin_extern_strings_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 = version.clone(
manifest.string_buf,
@TypeOf(&string_builder),
&string_builder,
),
.url = string_builder.append(String, manifest.str(package_version_ptr.tarball_url)),
},
},
.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;
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| {
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;
}
}
}
dependencies[0] = dependency;
dependencies = dependencies[1..];
i += 1;
}
}
package.bin = package_version.bin.clone(string_buf, manifest.extern_strings_bin_entries, extern_strings_list.items, extern_strings_slice, @TypeOf(&string_builder), &string_builder);
package.meta.arch = package_version.cpu;
package.meta.os = package_version.os;
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 = struct {
pub const Op = enum {
add,
remove,
update,
unlink,
link,
};
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(
_: 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 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| {
if (from_dep.name_hash == to_dep.name_hash) continue :outer;
}
summary.add += 1;
}
return summary;
}
};
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 parseMain(
lockfile: *Lockfile,
package: *Lockfile.Package,
allocator: std.mem.Allocator,
log: *logger.Log,
source: logger.Source,
comptime features: Features,
) !void {
return try parse(lockfile, package, allocator, log, source, void, void{}, features);
}
pub fn parse(
lockfile: *Lockfile,
package: *Lockfile.Package,
allocator: std.mem.Allocator,
log: *logger.Log,
source: logger.Source,
comptime ResolverContext: type,
resolver: ResolverContext,
comptime features: Features,
) !void {
initializeStore();
// A valid package.json always has "{}" characters
if (source.contents.len < 2) return error.InvalidPackageJSON;
var json = json_parser.ParseJSONUTF8(&source, log, allocator) catch |err| {
if (Output.enable_ansi_colors) {
log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true) catch {};
} else {
log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false) catch {};
}
Output.prettyErrorln("<r><red>{s}<r> parsing package.json in <b>\"{s}\"<r>", .{ @errorName(err), source.path.prettyDir() });
Global.exit(1);
};
var string_builder = lockfile.stringBuilder();
var total_dependencies_count: u32 = 0;
package.meta.origin = if (features.is_main) .local else .npm;
package.name = String{};
package.name_hash = 0;
// -- 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);
}
}
}
if (comptime features.scripts) {
if (json.asProperty("scripts")) |scripts_prop| {
if (scripts_prop.expr.data == .e_object) {
const scripts = .{
"install",
"postinstall",
"postprepare",
"preinstall",
"prepare",
"preprepare",
};
inline for (scripts) |script_name| {
if (scripts_prop.expr.get(script_name)) |script| {
if (script.asString(allocator)) |input| {
var list = @field(lockfile.scripts, script_name);
if (list.capacity == 0) {
list.capacity = 1;
list.items = try allocator.alloc(string, 1);
list.items[0] = input;
} else {
try list.append(allocator, input);
}
@field(lockfile.scripts, script_name) = list;
}
}
}
}
}
}
if (comptime ResolverContext != void) {
resolver.count(*Lockfile.StringBuilder, &string_builder, json);
}
const dependency_groups = comptime brk: {
var out_groups: [
@as(usize, @boolToInt(features.dependencies)) +
@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;
if (features.dependencies) {
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;
};
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.slice()) |item| {
const key = item.key.?.asString(allocator) orelse "";
const value = item.value.?.asString(allocator) orelse "";
string_builder.count(key);
string_builder.count(value);
// If it's a folder, pessimistically assume we will need a maximum path
if (Dependency.Version.Tag.infer(value) == .folder) {
string_builder.cap += bun.MAX_PATH_BYTES;
}
}
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 (comptime ResolverContext != void) {
package.resolution = try resolver.resolve(*Lockfile.StringBuilder, &string_builder, json);
}
} 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.slice();
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,
);
var dependency_version = Dependency.parse(
allocator,
sliced.slice,
&sliced,
log,
) orelse Dependency.Version{};
if (dependency_version.tag == .folder) {
const folder_path = dependency_version.value.folder.slice(lockfile.buffers.string_bytes.items);
dependency_version.value.folder = string_builder.append(
String,
Path.relative(
FileSystem.instance.top_level_dir,
Path.joinAbsString(
FileSystem.instance.top_level_dir,
&[_]string{
source.path.name.dir,
folder_path,
},
.posix,
),
),
);
}
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;
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];
string_builder.clamp();
}
pub const List = std.MultiArrayList(Lockfile.Package);
pub const Meta = extern struct {
origin: Origin = Origin.npm,
arch: Npm.Architecture = Npm.Architecture.all,
os: Npm.OperatingSystem = Npm.OperatingSystem.all,
id: PackageID = invalid_package_id,
man_dir: String = String{},
integrity: Integrity = Integrity{},
/// Does the `cpu` arch and `os` match the requirements listed in the package?
/// This is completely unrelated to "devDependencies", "peerDependencies", "optionalDependencies" etc
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, id: PackageID, buf: []const u8, comptime StringBuilderType: type, builder: StringBuilderType) Meta {
var new = this.*;
new.id = 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 {
try writer.writeIntLittle(u64, list.len);
try writer.writeIntLittle(u64, @alignOf(@TypeOf(list.bytes)));
try writer.writeIntLittle(u64, sizes.Types.len);
const begin_at = try stream.getPos();
try writer.writeIntLittle(u64, 0);
const end_at = try stream.getPos();
try writer.writeIntLittle(u64, 0);
_ = try Aligner.write(@TypeOf(list.bytes), Writer, writer, try stream.getPos());
const really_begin_at = try stream.getPos();
var sliced = list.slice();
inline for (FieldsEnum.fields) |field| {
try writer.writeAll(std.mem.sliceAsBytes(sliced.items(@field(Lockfile.Package.List.Field, field.name))));
}
const really_end_at = try stream.getPos();
_ = try std.os.pwrite(stream.handle, std.mem.asBytes(&really_begin_at), begin_at);
_ = try std.os.pwrite(stream.handle, std.mem.asBytes(&really_end_at), end_at);
}
pub fn load(
stream: *Stream,
end: usize,
allocator: std.mem.Allocator,
) !Lockfile.Package.List {
var reader = stream.reader();
const list_len = try reader.readIntLittle(u64);
if (list_len > std.math.maxInt(u32) - 1)
return error.@"Lockfile validation failed: list is impossibly long";
const input_alignment = try reader.readIntLittle(u64);
var list = Lockfile.Package.List{};
const Alingee = @TypeOf(list.bytes);
const expected_alignment = @alignOf(Alingee);
if (expected_alignment != input_alignment) {
return error.@"Lockfile validation failed: alignment mismatch";
}
const field_count = try reader.readIntLittle(u64);
if (field_count != sizes.Types.len) {
return error.@"Lockfile validation failed: unexpected number of package fields";
}
const begin_at = try reader.readIntLittle(u64);
const end_at = try reader.readIntLittle(u64);
if (begin_at > end or end_at > end or begin_at > end_at) {
return error.@"Lockfile validation failed: invalid package list range";
}
stream.pos = begin_at;
try list.ensureTotalCapacity(allocator, list_len);
list.len = list_len;
var sliced = list.slice();
inline for (FieldsEnum.fields) |field| {
var bytes = std.mem.sliceAsBytes(sliced.items(@field(Lockfile.Package.List.Field, field.name)));
@memcpy(bytes.ptr, stream.buffer[stream.pos..].ptr, bytes.len);
stream.pos += bytes.len;
}
return list;
}
};
};
pub fn deinit(this: *Lockfile) void {
this.buffers.deinit(this.allocator);
this.packages.deinit(this.allocator);
this.unique_packages.deinit(this.allocator);
this.string_pool.deinit();
this.* = undefined;
}
const Buffers = struct {
trees: Tree.List = Tree.List{},
hoisted_packages: PackageIDList = PackageIDList{},
resolutions: PackageIDList = PackageIDList{},
dependencies: DependencyList = DependencyList{},
extern_strings: ExternalStringBuffer = ExternalStringBuffer{},
// node_modules_folders: NodeModulesFolderList = NodeModulesFolderList{},
// node_modules_package_ids: PackageIDList = PackageIDList{},
string_bytes: StringBuffer = StringBuffer{},
pub fn deinit(this: *Buffers, allocator: std.mem.Allocator) void {
try this.trees.deinit(allocator);
try this.resolutions.deinit(allocator);
try this.dependencies.deinit(allocator);
try this.extern_strings.deinit(allocator);
try this.string_bytes.deinit(allocator);
}
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);
}
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 readArray(stream: *Stream, allocator: std.mem.Allocator, comptime ArrayList: type) !ArrayList {
const arraylist: ArrayList = undefined;
const PointerType = std.meta.Child(@TypeOf(arraylist.items.ptr));
var reader = stream.reader();
const start_pos = try reader.readIntLittle(u64);
const end_pos = try reader.readIntLittle(u64);
stream.pos = end_pos;
const byte_len = end_pos - start_pos;
if (byte_len == 0) return ArrayList{
.items = &[_]PointerType{},
.capacity = 0,
};
if (stream.pos > stream.buffer.len) {
return error.BufferOverflow;
}
const misaligned = std.mem.bytesAsSlice(PointerType, stream.buffer[start_pos..end_pos]);
return ArrayList{
.items = try allocator.dupe(PointerType, @alignCast(@alignOf([*]PointerType), misaligned.ptr)[0..misaligned.len]),
.capacity = misaligned.len,
};
}
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);
const start_pos = try stream.getPos();
try writer.writeIntLittle(u64, 0xDEADBEEF);
try writer.writeIntLittle(u64, 0xDEADBEEF);
const prefix = comptime std.fmt.comptimePrint(
"\n<{s}> {d} sizeof, {d} alignof\n",
.{
@typeName(std.meta.Child(ArrayList)),
@sizeOf(std.meta.Child(ArrayList)),
@alignOf(std.meta.Child(ArrayList)),
},
);
try writer.writeAll(prefix);
if (bytes.len > 0) {
_ = try Aligner.write(sizes.types[0], Writer, writer, try stream.getPos());
const real_start_pos = try stream.getPos();
try writer.writeAll(bytes);
const real_end_pos = try stream.getPos();
const positioned = [2]u64{ real_start_pos, real_end_pos };
var written: usize = 0;
while (written < 16) {
written += try std.os.pwrite(stream.handle, std.mem.asBytes(&positioned)[written..], start_pos + written);
}
} else {
const real_end_pos = try stream.getPos();
const positioned = [2]u64{ real_end_pos, real_end_pos };
var written: usize = 0;
while (written < 16) {
written += try std.os.pwrite(stream.handle, std.mem.asBytes(&positioned)[written..], start_pos + written);
}
}
}
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| {
if (PackageManager.instance.options.log_level.isVerbose()) {
Output.prettyErrorln("Saving {d} {s}", .{ @field(this, name).items.len, name });
}
// 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;
// It would be faster to buffer these instead of one big allocation
var to_clone = try std.ArrayListUnmanaged(Dependency.External).initCapacity(allocator, remaining.len);
defer to_clone.deinit(allocator);
for (remaining) |dep| {
to_clone.appendAssumeCapacity(Dependency.toExternal(dep));
}
try writeArray(StreamType, stream, Writer, writer, []Dependency.External, to_clone.items);
} else {
var list = @field(this, name);
const items = list.items;
const Type = @TypeOf(items);
if (comptime Type == Tree) {
// We duplicate it here so that alignment bytes are zeroed out
var clone = try std.ArrayListUnmanaged(Tree.External).initCapacity(allocator, list.items.len);
for (list.items) |item| {
clone.appendAssumeCapacity(Tree.toExternal(item));
}
defer clone.deinit(allocator);
try writeArray(StreamType, stream, Writer, writer, Tree.External, clone.items);
} else {
// We duplicate it here so that alignment bytes are zeroed out
var clone = try std.ArrayListUnmanaged(std.meta.Child(Type)).initCapacity(allocator, list.items.len);
clone.appendSliceAssumeCapacity(items);
defer clone.deinit(allocator);
try writeArray(StreamType, stream, Writer, writer, Type, clone.items);
}
}
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_: std.ArrayListUnmanaged(Dependency.External) = std.ArrayListUnmanaged(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)) {
external_dependency_list_ = try readArray(stream, allocator, std.ArrayListUnmanaged(Dependency.External));
if (PackageManager.instance.options.log_level.isVerbose()) {
Output.prettyErrorln("Loaded {d} {s}", .{ external_dependency_list_.items.len, name });
}
} else if (comptime Type == @TypeOf(this.trees)) {
var tree_list = try readArray(stream, allocator, std.ArrayListUnmanaged(Tree.External));
defer tree_list.deinit(allocator);
this.trees = try Tree.List.initCapacity(allocator, tree_list.items.len);
this.trees.items.len = tree_list.items.len;
for (tree_list.items) |tree, j| {
this.trees.items[j] = Tree.toTree(tree);
}
} else {
@field(this, sizes.names[i]) = try readArray(stream, allocator, Type);
if (PackageManager.instance.options.log_level.isVerbose()) {
Output.prettyErrorln("Loaded {d} {s}", .{ @field(this, sizes.names[i]).items.len, name });
}
}
if (comptime Environment.isDebug) {
// Output.prettyErrorln("Field {s}: {d} - {d}", .{ sizes.names[i], pos, try stream.getPos() });
}
}
var external_dependency_list = external_dependency_list_.items;
// 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.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] = Dependency.toDependency(dep, 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 old_package_list = this.packages;
this.packages = try this.packages.clone(z_allocator);
old_package_list.deinit(this.allocator);
var writer = stream.writer();
try writer.writeAll(header_bytes);
try writer.writeIntLittle(u32, @enumToInt(this.format));
try writer.writeAll(&this.meta_hash);
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, z_allocator, StreamType, stream, @TypeOf(&writer), &writer);
try writer.writeIntLittle(u64, 0);
const end = try stream.getPos();
try writer.writeIntLittle(u64, this.workspace_path.len);
if (this.workspace_path.len > 0)
try writer.writeAll(this.workspace_path);
try writer.writeAll(&alignment_bytes_to_repeat_buffer);
_ = try std.os.pwrite(stream.handle, std.mem.asBytes(&end), pos);
try std.os.ftruncate(stream.handle, try stream.getPos());
}
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.current)) {
return error.@"Outdated lockfile version";
}
lockfile.format = Lockfile.FormatVersion.current;
lockfile.allocator = allocator;
_ = try reader.readAll(&lockfile.meta_hash);
const total_buffer_size = try reader.readIntLittle(u64);
if (total_buffer_size > stream.buffer.len) {
return error.@"Lockfile is missing data";
}
lockfile.packages = try Lockfile.Package.Serializer.load(
stream,
total_buffer_size,
allocator,
);
lockfile.buffers = try Lockfile.Buffers.load(stream, allocator, log);
if ((try stream.reader().readIntLittle(u64)) != 0) {
return error.@"Lockfile is malformed (expected 0 at the end)";
}
std.debug.assert(stream.pos == total_buffer_size);
load_workspace: {
const workspace_path_len = reader.readIntLittle(u64) catch break :load_workspace;
if (workspace_path_len > 0 and workspace_path_len < bun.MAX_PATH_BYTES) {
var workspace_path = try allocator.alloc(u8, workspace_path_len);
const len = reader.readAll(workspace_path) catch break :load_workspace;
lockfile.workspace_path = workspace_path[0..len];
}
}
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);
}
};
pub fn hasMetaHashChanged(this: *Lockfile, print_name_version_string: bool) !bool {
const previous_meta_hash = this.meta_hash;
this.meta_hash = try this.generateMetaHash(print_name_version_string);
return !strings.eqlLong(&previous_meta_hash, &this.meta_hash, false);
}
pub fn generateMetaHash(this: *Lockfile, print_name_version_string: bool) !MetaHash {
if (this.packages.len <= 1)
return zero_hash;
var string_builder = GlobalStringBuilder{};
defer string_builder.deinit(this.allocator);
const names: []const String = this.packages.items(.name);
const resolutions: []const Resolution = this.packages.items(.resolution);
const bytes = this.buffers.string_bytes.items;
var alphabetized_names = try this.allocator.alloc(PackageID, this.packages.len -| 1);
defer this.allocator.free(alphabetized_names);
const hash_prefix = "\n-- BEGIN SHA512/256(`${alphabetize(name)}@${order(version)}`) --\n";
const hash_suffix = "-- END HASH--\n";
string_builder.cap += hash_prefix.len + hash_suffix.len;
{
var i: usize = 1;
while (i + 16 < this.packages.len) : (i += 16) {
comptime var j: usize = 0;
inline while (j < 16) : (j += 1) {
alphabetized_names[(i + j) - 1] = @truncate(PackageID, (i + j));
string_builder.fmtCount("{s}@{}\n", .{ names[i + j].slice(bytes), resolutions[i + j].fmt(bytes) });
}
}
while (i < this.packages.len) : (i += 1) {
alphabetized_names[i - 1] = @truncate(PackageID, i);
string_builder.fmtCount("{s}@{}\n", .{ names[i].slice(bytes), resolutions[i].fmt(bytes) });
}
}
std.sort.sort(
PackageID,
alphabetized_names,
Lockfile.Package.Alphabetizer{
.names = names,
.buf = bytes,
.resolutions = resolutions,
},
Lockfile.Package.Alphabetizer.isAlphabetical,
);
string_builder.allocate(this.allocator) catch unreachable;
string_builder.ptr.?[0..hash_prefix.len].* = hash_prefix.*;
string_builder.len += hash_prefix.len;
for (alphabetized_names) |i| {
_ = string_builder.fmt("{s}@{}\n", .{ names[i].slice(bytes), resolutions[i].fmt(bytes) });
}
string_builder.ptr.?[string_builder.len..string_builder.cap][0..hash_suffix.len].* = hash_suffix.*;
string_builder.len += hash_suffix.len;
const alphabetized_name_version_string = string_builder.ptr.?[0..string_builder.len];
if (print_name_version_string) {
Output.flush();
Output.disableBuffering();
Output.writer().writeAll(alphabetized_name_version_string) catch unreachable;
Output.enableBuffering();
}
var digest = zero_hash;
Crypto.SHA512_256.hash(alphabetized_name_version_string, &digest);
return digest;
}