[bun install] Support printing yarn.lock

This commit is contained in:
Jarred Sumner
2021-12-04 22:16:51 -08:00
parent 196f7eb101
commit d83c610002
5 changed files with 742 additions and 91 deletions

13
.vscode/launch.json vendored
View File

@@ -365,6 +365,19 @@
},
"console": "internalConsole"
},
{
"type": "lldb",
"request": "launch",
"name": "Print Lockfile",
"program": "bun-debug",
"args": ["./bun.lockb"],
"cwd": "/tmp/wow-such-npm",
"env": {
"BUN_CONFIG_SKIP_SAVE_LOCKFILE": "1"
},
"console": "internalConsole"
},
{
"type": "lldb",
"request": "launch",

View File

@@ -285,12 +285,16 @@ pub fn BSSStringList(comptime _count: usize, comptime _item_length: usize) type
return try self.appendMutable(EmptyType, EmptyType{ .len = len });
}
pub fn print(self: *Self, comptime fmt: []const u8, args: anytype) ![]const u8 {
pub fn printWithType(self: *Self, comptime fmt: []const u8, comptime Args: type, args: Args) ![]const u8 {
var buf = try self.appendMutable(EmptyType, EmptyType{ .len = std.fmt.count(fmt, args) + 1 });
buf[buf.len - 1] = 0;
return std.fmt.bufPrint(buf.ptr[0 .. buf.len - 1], fmt, args) catch unreachable;
}
pub fn print(self: *Self, comptime fmt: []const u8, args: anytype) ![]const u8 {
return try printWithType(self, fmt, @TypeOf(args), args);
}
pub fn append(self: *Self, comptime AppendType: type, _value: AppendType) ![]const u8 {
mutex.lock();
defer mutex.unlock();

View File

@@ -20,6 +20,7 @@ const resolve_path = @import("./resolver/resolve_path.zig");
const configureTransformOptionsForBun = @import("./javascript/jsc/config.zig").configureTransformOptionsForBun;
const clap = @import("clap");
const Install = @import("./install/install.zig");
const bundler = @import("bundler.zig");
const DotEnv = @import("./env_loader.zig");
@@ -39,6 +40,7 @@ const UpgradeCommand = @import("./cli/upgrade_command.zig").UpgradeCommand;
const InstallCommand = @import("./cli/install_command.zig").InstallCommand;
const InstallCompletionsCommand = @import("./cli/install_completions_command.zig").InstallCompletionsCommand;
const ShellCompletions = @import("./cli/shell_completions.zig");
var start_time: i128 = undefined;
pub const Cli = struct {
@@ -757,11 +759,24 @@ pub const Command = struct {
}
};
if (ctx.args.entry_points.len == 1 and
std.mem.endsWith(u8, ctx.args.entry_points[0], ".bun"))
{
try PrintBundleCommand.exec(ctx);
return;
// KEYWORDS: open file argv argv0
if (ctx.args.entry_points.len == 1) {
const extension = std.fs.path.extension(ctx.args.entry_points[0]);
if (strings.eqlComptime(extension, ".bun")) {
try PrintBundleCommand.exec(ctx);
return;
}
if (strings.eqlComptime(extension, ".lockb")) {
try Install.Lockfile.Printer.print(
ctx.allocator,
ctx.log,
ctx.args.entry_points[0],
.yarn,
);
return;
}
}
if (ctx.positionals.len > 0 and (std.fs.path.extension(ctx.positionals[0]).len == 0)) {

View File

@@ -85,7 +85,7 @@ const ExternalString = Semver.ExternalString;
const String = Semver.String;
const GlobalStringBuilder = @import("../string_builder.zig");
const SlicedString = Semver.SlicedString;
const GitSHA = String;
const StructBuilder = @import("../builder.zig");
const ExternalStringBuilder = StructBuilder.Builder(ExternalString);
@@ -200,6 +200,43 @@ pub const Aligner = struct {
}
};
const Repository = extern struct {
owner: String = String{},
repo: String = String{},
committish: GitSHA = GitSHA{},
pub fn eql(lhs: Repository, rhs: Repository, lhs_buf: []const u8, rhs_buf: []const u8) bool {
return lhs.owner.eql(rhs.owner, lhs_buf, rhs_buf) and
lhs.repo.eql(rhs.repo, lhs_buf, rhs_buf) and
lhs.committish.eql(rhs.committish, lhs_buf, rhs_buf);
}
pub fn formatAs(this: Repository, label: string, buf: []const u8, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
const formatter = Formatter{ .label = label, .repository = this, .buf = buf };
return try formatter.format(layout, opts, writer);
}
pub const Formatter = struct {
label: []const u8 = "",
buf: []const u8,
repository: Repository,
pub fn format(formatter: Formatter, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
std.debug.assert(formatter.label.len > 0);
try writer.writeAll(formatter.label);
try writer.writeAll(":");
try writer.writeAll(formatter.repository.owner.slice(formatter.buf));
try writer.writeAll(formatter.repository.repo.slice(formatter.buf));
if (!formatter.repository.committish.isEmpty()) {
try writer.writeAll("#");
try writer.writeAll(formatter.repository.committish.slice(formatter.buf));
}
}
};
};
const NetworkTask = struct {
http: AsyncHTTP = undefined,
task_id: u64,
@@ -317,10 +354,9 @@ const NetworkTask = struct {
tarball: ExtractTarball,
) !void {
this.url_buf = try ExtractTarball.buildURL(
allocator,
tarball.registry,
tarball.name,
tarball.version,
tarball.resolution.value.npm,
PackageManager.instance.lockfile.buffers.string_bytes.items,
);
@@ -446,7 +482,7 @@ pub const Bin = extern struct {
};
};
const Lockfile = struct {
pub const Lockfile = struct {
// Serialized data
/// The version of the lockfile format, intended to prevent data corruption for format changes.
@@ -469,7 +505,6 @@ const Lockfile = struct {
pub const LoadFromDiskResult = union(Tag) {
not_found: void,
invalid_format: void,
err: struct {
step: Step,
value: anyerror,
@@ -480,7 +515,6 @@ const Lockfile = struct {
pub const Tag = enum {
not_found,
invalid_format,
err,
ok,
};
@@ -540,6 +574,283 @@ const Lockfile = struct {
const PackageIDFifo = std.fifo.LinearFifo(PackageID, .Dynamic);
pub const Printer = struct {
lockfile: *Lockfile,
options: PackageManager.Options,
pub const Format = enum { yarn };
var lockfile_path_buf1: [std.fs.MAX_PATH_BYTES]u8 = undefined;
var lockfile_path_buf2: [std.fs.MAX_PATH_BYTES]u8 = undefined;
pub fn print(
allocator: *std.mem.Allocator,
log: *logger.Log,
lockfile_path_: string,
format: Format,
) !void {
var lockfile_path: stringZ = undefined;
if (!std.fs.path.isAbsolute(lockfile_path_)) {
var cwd = try std.os.getcwd(&lockfile_path_buf1);
var parts = [_]string{lockfile_path_};
var lockfile_path__ = resolve_path.joinAbsStringBuf(cwd, &lockfile_path_buf2, &parts, .auto);
lockfile_path_buf2[lockfile_path__.len] = 0;
lockfile_path = lockfile_path_buf2[0..lockfile_path__.len :0];
} else {
std.mem.copy(u8, &lockfile_path_buf1, lockfile_path);
lockfile_path_buf1[lockfile_path_.len] = 0;
lockfile_path = lockfile_path_buf1[0..lockfile_path_.len :0];
}
std.os.chdir(std.fs.path.dirname(lockfile_path) orelse "/") catch {};
_ = try FileSystem.init1(allocator, null);
const load_from_disk = Lockfile.loadFromDisk(allocator, log, lockfile_path);
switch (load_from_disk) {
.err => |cause| {
switch (cause.step) {
.open_file => Output.prettyErrorln("<r><red>error opening lockfile:<r> {s}.", .{
@errorName(cause.value),
}),
.parse_file => Output.prettyErrorln("<r><red>error parsing lockfile:<r> {s}", .{
@errorName(cause.value),
}),
.read_file => Output.prettyErrorln("<r><red>error reading lockfile:<r> {s}", .{
@errorName(cause.value),
}),
}
if (log.errors > 0) {
if (Output.enable_ansi_colors) {
try log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true);
} else {
try log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false);
}
}
Output.flush();
std.os.exit(1);
return;
},
.not_found => {
Output.prettyErrorln("<r><red>lockfile not found:<r> {s}", .{
std.mem.span(lockfile_path),
});
Output.flush();
std.os.exit(1);
return;
},
.ok => {},
}
var lockfile = load_from_disk.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,
);
var printer = Printer{
.lockfile = lockfile,
.options = options,
};
switch (format) {
.yarn => {
try Yarn.print(&printer, Writer, writer);
},
}
}
pub const Yarn = struct {
pub fn print(
this: *Printer,
comptime Writer: type,
writer: Writer,
) !void {
try writer.writeAll(
\\# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
\\# yarn lockfile v1
\\
\\
);
try Yarn.packages(this, Writer, writer);
}
pub fn packages(
this: *Printer,
comptime Writer: type,
writer: Writer,
) !void {
var slice = this.lockfile.packages.slice();
const names: []const String = slice.items(.name);
const resolved: []const Lockfile.Package.Resolution = slice.items(.resolution);
const metas: []const Lockfile.Package.Meta = slice.items(.meta);
if (names.len == 0) return;
const dependency_lists = slice.items(.dependencies);
const resolutions_list = slice.items(.resolutions);
const resolutions_buffer = this.lockfile.buffers.resolutions.items;
const dependencies_buffer = this.lockfile.buffers.dependencies.items;
const RequestedVersion = std.HashMap(PackageID, []Dependency.Version, IdentityContext(PackageID), 80);
var requested_versions = RequestedVersion.init(this.lockfile.allocator);
var all_requested_versions = try this.lockfile.allocator.alloc(Dependency.Version, resolutions_buffer.len);
defer this.lockfile.allocator.free(all_requested_versions);
const package_count = @truncate(PackageID, names.len);
var alphabetized_names = try this.lockfile.allocator.alloc(PackageID, package_count - 1);
defer this.lockfile.allocator.free(alphabetized_names);
const string_buf = this.lockfile.buffers.string_bytes.items;
// First, we need to build a map of all requested versions
// This is so we can print requested versions
{
var i: PackageID = 1;
while (i < package_count) : (i += 1) {
alphabetized_names[i - 1] = @truncate(PackageID, i);
var resolutions = resolutions_buffer;
var dependencies = dependencies_buffer;
var j: PackageID = 0;
var requested_version_start = all_requested_versions;
while (std.mem.indexOfScalar(PackageID, resolutions, i)) |k| {
j += 1;
all_requested_versions[0] = dependencies[k].version;
all_requested_versions = all_requested_versions[1..];
dependencies = dependencies[k + 1 ..];
resolutions = resolutions[k + 1 ..];
}
var dependency_versions = requested_version_start[0..j];
if (dependency_versions.len > 1) std.sort.insertionSort(Dependency.Version, dependency_versions, string_buf, Dependency.Version.isLessThan);
try requested_versions.put(i, dependency_versions);
}
}
std.sort.sort(
PackageID,
alphabetized_names,
Lockfile.Package.Alphabetizer{
.names = names,
.buf = string_buf,
.resolutions = resolved,
},
Lockfile.Package.Alphabetizer.isAlphabetical,
);
// When printing, we start at 1
for (alphabetized_names) |i| {
const name = names[i].slice(string_buf);
const resolution = resolved[i];
const meta = metas[i];
const dependencies: []const Dependency = dependency_lists[i].get(dependencies_buffer);
// This prints:
// "@babel/core@7.9.0":
{
try writer.writeAll("\n");
const dependency_versions = requested_versions.get(i).?;
const always_needs_quote = name[0] == '@';
for (dependency_versions) |dependency_version, j| {
if (j > 0) {
try writer.writeAll(", ");
}
const version_name = dependency_version.literal.slice(string_buf);
const needs_quote = always_needs_quote or std.mem.indexOfAny(u8, version_name, " |\t-/!") != null;
if (needs_quote) {
try writer.writeByte('"');
}
try writer.writeAll(name);
try writer.writeByte('@');
try writer.writeAll(version_name);
if (needs_quote) {
try writer.writeByte('"');
}
}
try writer.writeAll(":\n");
}
{
try writer.writeAll(" version ");
// Version is always quoted
try std.fmt.format(writer, "\"{}\"\n", .{resolution.fmt(string_buf)});
try writer.writeAll(" resolved ");
// Resolved URL is always quoted
try std.fmt.format(writer, "\"{}\"\n", .{resolution.fmtURL(&this.options, name, string_buf)});
if (meta.integrity.tag != .unknown) {
// Integrity is...never quoted?
try std.fmt.format(writer, " integrity {}\n", .{meta.integrity});
}
if (dependencies.len > 0) {
try writer.writeAll(" dependencies:\n");
for (dependencies) |dep, j| {
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");
}
}
}
}
}
};
};
const CleanVisitor = struct {
visited: std.DynamicBitSetUnmanaged,
sorted_ids: std.ArrayListUnmanaged(PackageID),
@@ -703,9 +1014,13 @@ const Lockfile = struct {
}
}
pub fn getPackageID(this: *Lockfile, name_hash: u64, version: Semver.Version) ?PackageID {
pub fn getPackageID(
this: *Lockfile,
name_hash: u64,
resolution: Lockfile.Package.Resolution,
) ?PackageID {
const entry = this.package_index.get(name_hash) orelse return null;
const versions = this.packages.items(.version);
const resolutions = this.packages.items(.resolution);
switch (entry) {
.PackageID => |id| {
if (comptime Environment.isDebug or Environment.isTest) {
@@ -713,7 +1028,11 @@ const Lockfile = struct {
std.debug.assert(id != invalid_package_id - 1);
}
if (versions[id].eql(version)) {
if (resolutions[id].eql(
resolution,
this.buffers.string_bytes.items,
this.buffers.string_bytes.items,
)) {
return id;
}
},
@@ -726,7 +1045,7 @@ const Lockfile = struct {
if (id == invalid_package_id - 1) return null;
if (versions[id].eql(version)) {
if (resolutions[id].eql(resolution, this.buffers.string_bytes.items, this.buffers.string_bytes.items)) {
return id;
}
}
@@ -794,8 +1113,8 @@ const Lockfile = struct {
const id = @truncate(u32, this.packages.len);
defer {
if (comptime Environment.isDebug) {
std.debug.assert(this.getPackageID(package_.name_hash, package_.version) != null);
std.debug.assert(this.getPackageID(package_.name_hash, package_.version).? == id);
std.debug.assert(this.getPackageID(package_.name_hash, package_.resolution) != null);
std.debug.assert(this.getPackageID(package_.name_hash, package_.resolution).? == id);
}
}
var package = package_;
@@ -977,7 +1296,6 @@ const Lockfile = struct {
const SmallExternalStringBuffer = std.ArrayListUnmanaged(String);
pub const Package = extern struct {
const Version = Dependency.Version;
const DependencyGroup = struct {
prop: string,
field: string,
@@ -993,6 +1311,20 @@ const Lockfile = struct {
return !this.meta.arch.isMatch() or !this.meta.os.isMatch();
}
const Alphabetizer = struct {
names: []const String,
buf: []const u8,
resolutions: []const Lockfile.Package.Resolution,
pub fn isAlphabetical(ctx: Alphabetizer, lhs: PackageID, rhs: PackageID) bool {
return switch (std.mem.order(u8, ctx.names[lhs].slice(ctx.buf), ctx.names[rhs].slice(ctx.buf))) {
.eq => lhs < rhs,
.lt => true,
.gt => false,
};
}
};
pub fn fromNPM(
allocator: *std.mem.Allocator,
lockfile: *Lockfile,
@@ -1072,7 +1404,16 @@ const Lockfile = struct {
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.version = version.clone(manifest.string_buf, @TypeOf(&string_builder), &string_builder);
package.resolution = Resolution{
.value = .{
.npm = version.clone(
manifest.string_buf,
@TypeOf(&string_builder),
&string_builder,
),
},
.tag = .npm,
};
const total_len = dependencies_list.items.len + total_dependencies_count;
std.debug.assert(dependencies_list.items.len == resolutions_list.items.len);
@@ -1245,7 +1586,7 @@ const Lockfile = struct {
pub fn determinePreinstallState(this: *Lockfile.Package, lockfile: *Lockfile, manager: *PackageManager) PreinstallState {
switch (this.meta.preinstall_state) {
.unknown => {
const folder_path = PackageManager.cachedNPMPackageFolderName(this.name.slice(lockfile.buffers.string_bytes.items), this.version);
const folder_path = PackageManager.cachedNPMPackageFolderName(this.name.slice(lockfile.buffers.string_bytes.items), this.resolution.value.npm);
if (manager.isFolderInCache(folder_path)) {
this.meta.preinstall_state = .done;
return this.meta.preinstall_state;
@@ -1376,12 +1717,20 @@ const Lockfile = struct {
const semver_version = Semver.Version.parse(sliced_string, allocator);
if (semver_version.valid) {
package.version = semver_version.version;
package.resolution = .{
.tag = .npm,
.value = .{ .npm = semver_version.version },
};
} else {
log.addErrorFmt(null, logger.Loc.Empty, allocator, "invalid version \"{s}\"", .{version_str}) catch unreachable;
}
}
}
} else {
package.resolution = .{
.tag = .root,
.value = .{ .root = .{} },
};
}
if (comptime features.check_for_duplicate_dependencies) {
@@ -1466,6 +1815,207 @@ const Lockfile = struct {
pub const List = std.MultiArrayList(Lockfile.Package);
pub const Resolution = extern struct {
tag: Tag = Tag.uninitialized,
value: Value = Value{ .uninitialized = .{} },
pub fn fmt(this: Resolution, buf: []const u8) Formatter {
return Formatter{ .resolution = this, .buf = buf };
}
pub fn fmtURL(this: Resolution, options: *const PackageManager.Options, name: string, buf: []const u8) URLFormatter {
return URLFormatter{ .resolution = this, .buf = buf, .package_name = name, .options = options };
}
pub fn eql(
lhs: Resolution,
rhs: Resolution,
lhs_string_buf: []const u8,
rhs_string_buf: []const u8,
) bool {
if (lhs.tag != rhs.tag) return false;
return switch (lhs.tag) {
.root => true,
.npm => lhs.value.npm.eql(rhs.value.npm),
.local_tarball => lhs.value.local_tarball.eql(
rhs.value.local_tarball,
lhs_string_buf,
rhs_string_buf,
),
.git_ssh => lhs.value.git_ssh.eql(
rhs.value.git_ssh,
lhs_string_buf,
rhs_string_buf,
),
.git_http => lhs.value.git_http.eql(
rhs.value.git_http,
lhs_string_buf,
rhs_string_buf,
),
.folder => lhs.value.folder.eql(
rhs.value.folder,
lhs_string_buf,
rhs_string_buf,
),
.remote_tarball => lhs.value.remote_tarball.eql(
rhs.value.remote_tarball,
lhs_string_buf,
rhs_string_buf,
),
.workspace => lhs.value.workspace.eql(
rhs.value.workspace,
lhs_string_buf,
rhs_string_buf,
),
.symlink => lhs.value.symlink.eql(
rhs.value.symlink,
lhs_string_buf,
rhs_string_buf,
),
.single_file_module => lhs.value.single_file_module.eql(
rhs.value.single_file_module,
lhs_string_buf,
rhs_string_buf,
),
.github => lhs.value.github.eql(
rhs.value.github,
lhs_string_buf,
rhs_string_buf,
),
.gitlab => lhs.value.gitlab.eql(
rhs.value.gitlab,
lhs_string_buf,
rhs_string_buf,
),
else => unreachable,
};
}
pub const URLFormatter = struct {
resolution: Resolution,
options: *const PackageManager.Options,
package_name: string,
buf: []const u8,
pub fn format(formatter: URLFormatter, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
switch (formatter.resolution.tag) {
.npm => try ExtractTarball.buildURLWithWriter(
@TypeOf(writer),
writer,
formatter.options.registry_url.href,
strings.StringOrTinyString.init(formatter.package_name),
formatter.resolution.value.npm,
formatter.buf,
),
.local_tarball => try writer.writeAll(formatter.resolution.value.local_tarball.slice(formatter.buf)),
.git_ssh => try std.fmt.format(writer, "git+ssh://{s}", .{formatter.resolution.value.git_ssh.slice(formatter.buf)}),
.git_http => try std.fmt.format(writer, "https://{s}", .{formatter.resolution.value.git_http.slice(formatter.buf)}),
.folder => try writer.writeAll(formatter.resolution.value.folder.slice(formatter.buf)),
.remote_tarball => try writer.writeAll(formatter.resolution.value.remote_tarball.slice(formatter.buf)),
.github => try formatter.resolution.value.github.formatAs("github", formatter.buf, layout, opts, writer),
.gitlab => try formatter.resolution.value.gitlab.formatAs("gitlab", formatter.buf, layout, opts, writer),
.workspace => try std.fmt.format(writer, "workspace://{s}", .{formatter.resolution.value.workspace.slice(formatter.buf)}),
.symlink => try std.fmt.format(writer, "link://{s}", .{formatter.resolution.value.symlink.slice(formatter.buf)}),
.single_file_module => try std.fmt.format(writer, "link://{s}", .{formatter.resolution.value.symlink.slice(formatter.buf)}),
else => {},
}
}
};
pub const Formatter = struct {
resolution: Resolution,
buf: []const u8,
pub fn format(formatter: Formatter, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
switch (formatter.resolution.tag) {
.npm => try formatter.resolution.value.npm.fmt(formatter.buf).format(layout, opts, writer),
.local_tarball => try writer.writeAll(formatter.resolution.value.local_tarball.slice(formatter.buf)),
.git_ssh => try std.fmt.format(writer, "git+ssh://{s}", .{formatter.resolution.value.git_ssh.slice(formatter.buf)}),
.git_http => try std.fmt.format(writer, "https://{s}", .{formatter.resolution.value.git_http.slice(formatter.buf)}),
.folder => try writer.writeAll(formatter.resolution.value.folder.slice(formatter.buf)),
.remote_tarball => try writer.writeAll(formatter.resolution.value.remote_tarball.slice(formatter.buf)),
.github => try formatter.resolution.value.github.formatAs("github", formatter.buf, layout, opts, writer),
.gitlab => try formatter.resolution.value.gitlab.formatAs("gitlab", formatter.buf, layout, opts, writer),
.workspace => try std.fmt.format(writer, "workspace://{s}", .{formatter.resolution.value.workspace.slice(formatter.buf)}),
.symlink => try std.fmt.format(writer, "link://{s}", .{formatter.resolution.value.symlink.slice(formatter.buf)}),
.single_file_module => try std.fmt.format(writer, "link://{s}", .{formatter.resolution.value.symlink.slice(formatter.buf)}),
else => {},
}
}
};
pub const Value = extern union {
uninitialized: void,
root: void,
npm: Semver.Version,
/// File path to a tarball relative to the package root
local_tarball: String,
git_ssh: String,
git_http: String,
folder: String,
/// URL to a tarball.
remote_tarball: String,
github: Repository,
gitlab: Repository,
workspace: String,
symlink: String,
single_file_module: String,
};
pub const Tag = enum(u8) {
uninitialized = 0,
root = 1,
npm = 2,
folder = 4,
local_tarball = 8,
github = 16,
gitlab = 24,
git_ssh = 32,
git_http = 33,
symlink = 64,
workspace = 72,
remote_tarball = 80,
// This is a placeholder for now.
// But the intent is to eventually support URL imports at the package manager level.
//
// There are many ways to do it, but perhaps one way to be maximally compatible is just removing the protocol part of the URL.
//
// For example, Bun would transform this input:
//
// import _ from "https://github.com/lodash/lodash/lodash.min.js";
//
// Into:
//
// import _ from "github.com/lodash/lodash/lodash.min.js";
//
// github.com would become a package, with it's own package.json
// This is similar to how Go does it, except it wouldn't clone the whole repo.
// There are more efficient ways to do this, e.g. generate a .bun file just for all URL imports.
// There are questions of determinism, but perhaps that's what Integrity would do.
single_file_module = 100,
_,
};
};
pub const Meta = extern struct {
preinstall_state: PreinstallState = PreinstallState.unknown,
@@ -1485,7 +2035,7 @@ const Lockfile = struct {
name: String = String{},
name_hash: PackageNameHash = 0,
version: Semver.Version = Semver.Version{},
resolution: Resolution = Resolution{},
dependencies: DependencySlice = DependencySlice{},
resolutions: PackageIDSlice = PackageIDSlice{},
meta: Meta = Meta{},
@@ -1984,7 +2534,7 @@ pub const Dependency = struct {
const lhs_name = lhs.name.slice(string_buf);
const rhs_name = rhs.name.slice(string_buf);
return strings.cmpStringsDesc(void{}, lhs_name, rhs_name);
return strings.cmpStringsAsc(void{}, lhs_name, rhs_name);
}
pub fn clone(this: Dependency, from: *Lockfile, to: *Lockfile) Dependency {
@@ -2035,6 +2585,11 @@ pub const Dependency = struct {
literal: String = String{},
value: Value = Value{ .uninitialized = void{} },
pub fn isLessThan(string_buf: []const u8, lhs: Dependency.Version, rhs: Dependency.Version) bool {
std.debug.assert(lhs.tag == rhs.tag);
return strings.cmpStringsAsc(.{}, lhs.literal.slice(string_buf), rhs.literal.slice(string_buf));
}
pub const External = extern struct {
tag: Dependency.Version.Tag,
literal: String,
@@ -3425,7 +3980,7 @@ const Npm = struct {
if (dist.expr.asProperty("integrity")) |shasum| {
if (shasum.expr.asString(allocator)) |shasum_str| {
package_version.integrity = Integrity.parse(shasum_str) catch Integrity{};
break :integrity;
if (package_version.integrity.tag.isSupported()) break :integrity;
}
}
@@ -3633,7 +4188,7 @@ const Npm = struct {
const ExtractTarball = struct {
name: strings.StringOrTinyString,
version: Semver.Version,
resolution: Package.Resolution,
registry: string,
cache_dir: string,
package_id: PackageID,
@@ -3653,13 +4208,67 @@ const ExtractTarball = struct {
return this.extract(bytes);
}
fn buildURL(
allocator: *std.mem.Allocator,
pub fn buildURL(
registry_: string,
full_name_: strings.StringOrTinyString,
version: Semver.Version,
string_buf: []const u8,
) !string {
return try buildURLWithPrinter(
registry_,
full_name_,
version,
string_buf,
@TypeOf(FileSystem.instance.dirname_store),
string,
anyerror,
FileSystem.instance.dirname_store,
FileSystem.DirnameStore.print,
);
}
pub fn buildURLWithWriter(
comptime Writer: type,
writer: Writer,
registry_: string,
full_name_: strings.StringOrTinyString,
version: Semver.Version,
string_buf: []const u8,
) !void {
const Printer = struct {
writer: Writer,
pub fn print(this: @This(), comptime fmt: string, args: anytype) Writer.Error!void {
return try std.fmt.format(this.writer, fmt, args);
}
};
return try buildURLWithPrinter(
registry_,
full_name_,
version,
string_buf,
Printer,
void,
Writer.Error,
Printer{
.writer = writer,
},
Printer.print,
);
}
pub fn buildURLWithPrinter(
registry_: string,
full_name_: strings.StringOrTinyString,
version: Semver.Version,
string_buf: []const u8,
comptime PrinterContext: type,
comptime ReturnType: type,
comptime ErrorType: type,
printer: PrinterContext,
comptime print: fn (ctx: PrinterContext, comptime str: string, args: anytype) ErrorType!ReturnType,
) ErrorType!ReturnType {
const registry = std.mem.trimRight(u8, registry_, "/");
const full_name = full_name_.slice();
@@ -3673,27 +4282,32 @@ const ExtractTarball = struct {
const default_format = "{s}/{s}/-/";
if (!version.tag.hasPre() and !version.tag.hasBuild()) {
return try FileSystem.DirnameStore.instance.print(
const args = .{ registry, full_name, name, version.major, version.minor, version.patch };
return try print(
printer,
default_format ++ "{s}-{d}.{d}.{d}.tgz",
.{ registry, full_name, name, version.major, version.minor, version.patch },
args,
);
// TODO: tarball URLs for build/pre
} else if (version.tag.hasPre() and version.tag.hasBuild()) {
return try FileSystem.DirnameStore.instance.print(
const args = .{ registry, full_name, name, version.major, version.minor, version.patch, version.tag.pre.slice(string_buf), version.tag.build.slice(string_buf) };
return try print(
printer,
default_format ++ "{s}-{d}.{d}.{d}-{s}+{s}.tgz",
.{ registry, full_name, name, version.major, version.minor, version.patch, version.tag.pre.slice(string_buf), version.tag.build.slice(string_buf) },
args,
);
// TODO: tarball URLs for build/pre
} else if (version.tag.hasPre()) {
return try FileSystem.DirnameStore.instance.print(
const args = .{ registry, full_name, name, version.major, version.minor, version.patch, version.tag.pre.slice(string_buf) };
return try print(
printer,
default_format ++ "{s}-{d}.{d}.{d}-{s}.tgz",
.{ registry, full_name, name, version.major, version.minor, version.patch, version.tag.pre.slice(string_buf) },
args,
);
// TODO: tarball URLs for build/pre
} else if (version.tag.hasBuild()) {
return try FileSystem.DirnameStore.instance.print(
const args = .{ registry, full_name, name, version.major, version.minor, version.patch, version.tag.build.slice(string_buf) };
return try print(
printer,
default_format ++ "{s}-{d}.{d}.{d}+{s}.tgz",
.{ registry, full_name, name, version.major, version.minor, version.patch, version.tag.build.slice(string_buf) },
args,
);
} else {
unreachable;
@@ -3783,7 +4397,7 @@ const ExtractTarball = struct {
Output.flush();
}
var folder_name = PackageManager.cachedNPMPackageFolderNamePrint(&abs_buf2, name, this.version);
var folder_name = PackageManager.cachedNPMPackageFolderNamePrint(&abs_buf2, name, this.resolution.value.npm);
if (folder_name.len == 0 or (folder_name.len == 1 and folder_name[0] == '/')) @panic("Tried to delete root and stopped it");
PackageManager.instance.cache_directory.deleteTree(folder_name) catch {};
@@ -3857,7 +4471,7 @@ const Task = struct {
tag: Task.Tag,
bytes: u60 = 0,
pub fn forPackage(tag: Task.Tag, package_name: string, package_version: Semver.Version) u64 {
pub fn forNPMPackage(tag: Task.Tag, package_name: string, package_version: Semver.Version) u64 {
var hasher = std.hash.Wyhash.init(0);
hasher.update(package_name);
hasher.update("@");
@@ -4110,7 +4724,10 @@ pub const PackageManager = struct {
) !?ResolvedPackageResult {
// Was this package already allocated? Let's reuse the existing one.
if (this.lockfile.getPackageID(name_hash, find_result.version)) |id| {
if (this.lockfile.getPackageID(name_hash, .{
.tag = .npm,
.value = .{ .npm = find_result.version },
})) |id| {
const package = this.lockfile.packages.get(id);
return ResolvedPackageResult{
.package = package,
@@ -4144,7 +4761,7 @@ pub const PackageManager = struct {
// Do we need to download the tarball?
.extract => {
const task_id = Task.Id.forPackage(Task.Tag.extract, this.lockfile.str(package.name), package.version);
const task_id = Task.Id.forNPMPackage(Task.Tag.extract, this.lockfile.str(package.name), package.resolution.value.npm);
const dedupe_entry = try this.network_task_queue.getOrPut(this.allocator, task_id);
// Assert that we don't end up downloading the tarball twice.
@@ -4163,7 +4780,7 @@ pub const PackageManager = struct {
strings.StringOrTinyString.init(try FileSystem.FilenameStore.instance.append(@TypeOf(this.lockfile.str(name)), this.lockfile.str(name)))
else
strings.StringOrTinyString.init(this.lockfile.str(name)),
.version = package.version,
.resolution = package.resolution,
.cache_dir = this.cache_directory_path,
.registry = this.registry.url.href,
.package_id = package.meta.id,
@@ -4343,7 +4960,7 @@ pub const PackageManager = struct {
this.lockfile.str(result.package.name),
label,
this.lockfile.str(result.package.name),
result.package.version.fmt(this.lockfile.buffers.string_bytes.items),
result.package.resolution.fmt(this.lockfile.buffers.string_bytes.items),
});
}
// Resolve dependencies first
@@ -5151,45 +5768,4 @@ pub const PackageManager = struct {
var verbose_install = false;
test "getPackageMetadata" {
Output.initTest();
var registry = Npm.Registry{};
var log = logger.Log.init(default_allocator);
var response = try registry.getPackageMetadata(default_allocator, &log, "react", "", "");
switch (response) {
.cached, .not_found => unreachable,
.fresh => |package| {
package.reportSize();
const react = package.findByString("beta") orelse return try std.testing.expect(false);
try std.testing.expect(react.package.file_count > 0);
try std.testing.expect(react.package.unpacked_size > 0);
// try std.testing.expectEqualStrings("loose-envify", entry.slice(package.string_buf));
},
}
}
test "dumb wyhash" {
var i: usize = 0;
var j: usize = 0;
var z: usize = 0;
while (i < 100) {
j = 0;
while (j < 100) {
while (z < 100) {
try std.testing.expectEqual(
std.hash.Wyhash.hash(0, try std.fmt.allocPrint(default_allocator, "{d}.{d}.{d}", .{ i, j, z })),
std.hash.Wyhash.hash(0, try std.fmt.allocPrint(default_allocator, "{d}.{d}.{d}", .{ i, j, z })),
);
z += 1;
}
j += 1;
}
i += 1;
}
}
const Package = Lockfile.Package;

View File

@@ -7,6 +7,8 @@ pub const Integrity = extern struct {
/// We transform it though.
value: [digest_buf_len]u8 = undefined,
const Base64 = std.base64.standard_no_pad;
pub const digest_buf_len: usize = brk: {
const values = [_]usize{
std.crypto.hash.Sha1.digest_length,
@@ -86,7 +88,7 @@ pub const Integrity = extern struct {
};
}
std.base64.url_safe.Decoder.decode(&out, buf["sha256-".len..]) catch {
Base64.Decoder.decode(&out, std.mem.trimRight(u8, buf["sha256-".len..], "=")) catch {
return Integrity{
.tag = Tag.unknown,
.value = undefined,
@@ -115,15 +117,56 @@ pub const Integrity = extern struct {
pub fn parse(buf: []const u8) Tag {
const Matcher = strings.ExactSizeMatcher(8);
return switch (Matcher.match(buf[0..@minimum(buf.len, 8)])) {
Matcher.case("sha256-") => Tag.sha256,
Matcher.case("sha384-") => Tag.sha384,
Matcher.case("sha512-") => Tag.sha512,
const i = std.mem.indexOfScalar(u8, buf[0..@minimum(buf.len, 7)], '-') orelse return Tag.unknown;
return switch (Matcher.match(buf[0..i])) {
Matcher.case("sha1") => Tag.sha1,
Matcher.case("sha256") => Tag.sha256,
Matcher.case("sha384") => Tag.sha384,
Matcher.case("sha512") => Tag.sha512,
else => .unknown,
};
}
pub inline fn digestLen(this: Tag) usize {
return switch (this) {
.sha1 => std.crypto.hash.Sha1.digest_length,
.sha512 => std.crypto.hash.sha2.Sha512.digest_length,
.sha256 => std.crypto.hash.sha2.Sha256.digest_length,
.sha384 => std.crypto.hash.sha2.Sha384.digest_length,
else => 0,
};
}
};
pub fn slice(
this: *const Integrity,
) []const u8 {
return this.value[0..this.tag.digestLen()];
}
pub fn format(this: *const Integrity, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
switch (this.tag) {
.sha1 => try writer.writeAll("sha1-"),
.sha256 => try writer.writeAll("sha256-"),
.sha384 => try writer.writeAll("sha384-"),
.sha512 => try writer.writeAll("sha512-"),
else => return,
}
var base64_buf: [512]u8 = undefined;
const bytes = this.slice();
try writer.writeAll(Base64.Encoder.encode(&base64_buf, bytes));
// consistentcy with yarn.lock
switch (this.tag) {
.sha1 => try writer.writeAll("="),
else => try writer.writeAll("=="),
}
}
pub fn verify(this: *const Integrity, bytes: []const u8) bool {
return @call(.{ .modifier = .always_inline }, verifyByTag, .{ this.tag, bytes, &this.value });
}