mirror of
https://github.com/oven-sh/bun
synced 2026-02-11 19:38:58 +00:00
3095 lines
124 KiB
Zig
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;
|
|
}
|