fix(install): change semver core numbers to u64 (#22889)

### What does this PR do?
Sometimes packages will use very large numbers exceeding max u32 for
major/minor/patch (usually patch). This pr changes each core number in
bun to u64.

Because we serialize package information to disk for the binary lockfile
and package manifests, this pr bumps the version of each. We don't need
to change anything other than the version for serialized package
manifests because they will invalidate and save the new version. For old
binary lockfiles, this pr adds logic for migrating to the new version.
Even if there are no changes, migrating will always save the new
lockfile. Unfortunately means there will be a one time invisible diff
for binary lockfile users, but this is better than installs failing to
work.

fixes #22881
fixes #21793
fixes #16041
fixes #22891

resolves BUN-7MX, BUN-R4Q, BUN-WRB

### How did you verify your code works?
Manually, and added a test for migrating from an older binary lockfile.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Dylan Conway
2025-09-22 19:28:26 -07:00
committed by GitHub
parent beae53e81b
commit 285143dc66
16 changed files with 3481 additions and 3329 deletions

View File

@@ -782,7 +782,7 @@ pub fn installWithManager(
(did_meta_hash_change or
had_any_diffs or
manager.update_requests.len > 0 or
(load_result == .ok and load_result.ok.serializer_result.packages_need_update) or
(load_result == .ok and (load_result.ok.serializer_result.packages_need_update or load_result.ok.serializer_result.migrated_from_lockb_v2)) or
manager.lockfile.isEmpty() or
manager.options.enable.force_save_lockfile));

View File

@@ -1585,9 +1585,11 @@ pub const FormatVersion = enum(u32) {
// bun v0.1.7+
// This change added tarball URLs to npm-resolved packages
v2 = 2,
// Changed semver major/minor/patch to each use u64 instead of u32
v3 = 3,
_,
pub const current = FormatVersion.v2;
pub const current = FormatVersion.v3;
};
pub const PackageIDSlice = ExternalSlice(PackageID);
@@ -1607,7 +1609,7 @@ pub const Buffers = @import("./lockfile/Buffers.zig");
pub const Serializer = @import("./lockfile/bun.lockb.zig");
pub const CatalogMap = @import("./lockfile/CatalogMap.zig");
pub const OverrideMap = @import("./lockfile/OverrideMap.zig");
pub const Package = @import("./lockfile/Package.zig").Package;
pub const Package = @import("./lockfile/Package.zig").Package(u64);
pub const Tree = @import("./lockfile/Tree.zig");
pub fn deinit(this: *Lockfile) void {

File diff suppressed because it is too large Load Diff

View File

@@ -253,6 +253,7 @@ pub fn save(this: *Lockfile, verbose_log: bool, bytes: *std.ArrayList(u8), total
pub const SerializerLoadResult = struct {
packages_need_update: bool = false,
migrated_from_lockb_v2: bool = false,
};
pub fn load(
@@ -271,9 +272,20 @@ pub fn load(
return error.InvalidLockfile;
}
var migrate_from_v2 = false;
const format = try reader.readInt(u32, .little);
if (format != @intFromEnum(Lockfile.FormatVersion.current)) {
return error.@"Outdated lockfile version";
if (format > @intFromEnum(Lockfile.FormatVersion.current)) {
return error.@"Unexpected lockfile version";
}
if (format < @intFromEnum(Lockfile.FormatVersion.current)) {
// we only allow migrating from v2 to v3 or above
if (format != @intFromEnum(Lockfile.FormatVersion.v2)) {
return error.@"Outdated lockfile version";
}
migrate_from_v2 = true;
}
lockfile.format = Lockfile.FormatVersion.current;
@@ -290,10 +302,13 @@ pub fn load(
stream,
total_buffer_size,
allocator,
migrate_from_v2,
);
lockfile.packages = packages_load_result.list;
res.packages_need_update = packages_load_result.needs_update;
res.migrated_from_lockb_v2 = migrate_from_v2;
lockfile.buffers = try Lockfile.Buffers.load(
stream,

View File

@@ -932,7 +932,8 @@ pub const PackageManifest = struct {
// - v0.0.3: added serialization of registry url. it's used to invalidate when it changes
// - v0.0.4: fixed bug with cpu & os tag not being added correctly
// - v0.0.5: added bundled dependencies
pub const version = "bun-npm-manifest-cache-v0.0.5\n";
// - v0.0.6: changed semver major/minor/patch to each use u64 instead of u32
pub const version = "bun-npm-manifest-cache-v0.0.6\n";
const header_bytes: string = "#!/usr/bin/env bun\n" ++ version;
pub const sizes = blk: {

View File

@@ -1,438 +1,445 @@
pub const Resolution = extern struct {
tag: Tag = .uninitialized,
_padding: [7]u8 = .{0} ** 7,
value: Value = .{ .uninitialized = {} },
pub const Resolution = ResolutionType(u64);
pub const OldV2Resolution = ResolutionType(u32);
/// Use like Resolution.init(.{ .npm = VersionedURL{ ... } })
pub inline fn init(value: bun.meta.Tagged(Value, Tag)) Resolution {
return Resolution{
.tag = std.meta.activeTag(value),
.value = Value.init(value),
};
}
pub fn ResolutionType(comptime SemverIntType: type) type {
return extern struct {
tag: Tag = .uninitialized,
_padding: [7]u8 = .{0} ** 7,
value: Value = .{ .uninitialized = {} },
pub fn isGit(this: *const Resolution) bool {
return this.tag.isGit();
}
const This = @This();
pub fn canEnqueueInstallTask(this: *const Resolution) bool {
return this.tag.canEnqueueInstallTask();
}
const FromTextLockfileError = OOM || error{
UnexpectedResolution,
InvalidSemver,
};
pub fn fromTextLockfile(res_str: string, string_buf: *String.Buf) FromTextLockfileError!Resolution {
if (strings.hasPrefixComptime(res_str, "root:")) {
return Resolution.init(.{ .root = {} });
}
if (strings.withoutPrefixIfPossibleComptime(res_str, "link:")) |link| {
return Resolution.init(.{ .symlink = try string_buf.append(link) });
}
if (strings.withoutPrefixIfPossibleComptime(res_str, "workspace:")) |workspace| {
return Resolution.init(.{ .workspace = try string_buf.append(workspace) });
}
if (strings.withoutPrefixIfPossibleComptime(res_str, "file:")) |folder| {
return Resolution.init(.{ .folder = try string_buf.append(folder) });
}
return switch (Dependency.Version.Tag.infer(res_str)) {
.git => Resolution.init(.{ .git = try Repository.parseAppendGit(res_str, string_buf) }),
.github => Resolution.init(.{ .github = try Repository.parseAppendGithub(res_str, string_buf) }),
.tarball => {
if (Dependency.isRemoteTarball(res_str)) {
return Resolution.init(.{ .remote_tarball = try string_buf.append(res_str) });
}
return Resolution.init(.{ .local_tarball = try string_buf.append(res_str) });
},
.npm => {
const version_literal = try string_buf.append(res_str);
const parsed = Semver.Version.parse(version_literal.sliced(string_buf.bytes.items));
if (!parsed.valid) {
return error.UnexpectedResolution;
}
if (parsed.version.major == null or parsed.version.minor == null or parsed.version.patch == null) {
return error.UnexpectedResolution;
}
return .{
.tag = .npm,
.value = .{
.npm = .{
.version = parsed.version.min(),
// will fill this later
.url = .{},
},
},
};
},
// covered above
.workspace => error.UnexpectedResolution,
.symlink => error.UnexpectedResolution,
.folder => error.UnexpectedResolution,
// even though it's a dependency type, it's not
// possible for 'catalog:' to be written to the
// lockfile for any resolution because the install
// will fail it it's not successfully replaced by
// a version
.catalog => error.UnexpectedResolution,
// should not happen
.dist_tag => error.UnexpectedResolution,
.uninitialized => error.UnexpectedResolution,
};
}
pub fn order(
lhs: *const Resolution,
rhs: *const Resolution,
lhs_buf: []const u8,
rhs_buf: []const u8,
) std.math.Order {
if (lhs.tag != rhs.tag) {
return std.math.order(@intFromEnum(lhs.tag), @intFromEnum(rhs.tag));
}
return switch (lhs.tag) {
.npm => lhs.value.npm.order(rhs.value.npm, lhs_buf, rhs_buf),
.local_tarball => lhs.value.local_tarball.order(&rhs.value.local_tarball, lhs_buf, rhs_buf),
.folder => lhs.value.folder.order(&rhs.value.folder, lhs_buf, rhs_buf),
.remote_tarball => lhs.value.remote_tarball.order(&rhs.value.remote_tarball, lhs_buf, rhs_buf),
.workspace => lhs.value.workspace.order(&rhs.value.workspace, lhs_buf, rhs_buf),
.symlink => lhs.value.symlink.order(&rhs.value.symlink, lhs_buf, rhs_buf),
.single_file_module => lhs.value.single_file_module.order(&rhs.value.single_file_module, lhs_buf, rhs_buf),
.git => lhs.value.git.order(&rhs.value.git, lhs_buf, rhs_buf),
.github => lhs.value.github.order(&rhs.value.github, lhs_buf, rhs_buf),
else => .eq,
};
}
pub fn count(this: *const Resolution, buf: []const u8, comptime Builder: type, builder: Builder) void {
switch (this.tag) {
.npm => this.value.npm.count(buf, Builder, builder),
.local_tarball => builder.count(this.value.local_tarball.slice(buf)),
.folder => builder.count(this.value.folder.slice(buf)),
.remote_tarball => builder.count(this.value.remote_tarball.slice(buf)),
.workspace => builder.count(this.value.workspace.slice(buf)),
.symlink => builder.count(this.value.symlink.slice(buf)),
.single_file_module => builder.count(this.value.single_file_module.slice(buf)),
.git => this.value.git.count(buf, Builder, builder),
.github => this.value.github.count(buf, Builder, builder),
else => {},
}
}
pub fn clone(this: *const Resolution, buf: []const u8, comptime Builder: type, builder: Builder) Resolution {
return .{
.tag = this.tag,
.value = switch (this.tag) {
.npm => Value.init(.{ .npm = this.value.npm.clone(buf, Builder, builder) }),
.local_tarball => Value.init(.{
.local_tarball = builder.append(String, this.value.local_tarball.slice(buf)),
}),
.folder => Value.init(.{
.folder = builder.append(String, this.value.folder.slice(buf)),
}),
.remote_tarball => Value.init(.{
.remote_tarball = builder.append(String, this.value.remote_tarball.slice(buf)),
}),
.workspace => Value.init(.{
.workspace = builder.append(String, this.value.workspace.slice(buf)),
}),
.symlink => Value.init(.{
.symlink = builder.append(String, this.value.symlink.slice(buf)),
}),
.single_file_module => Value.init(.{
.single_file_module = builder.append(String, this.value.single_file_module.slice(buf)),
}),
.git => Value.init(.{
.git = this.value.git.clone(buf, Builder, builder),
}),
.github => Value.init(.{
.github = this.value.github.clone(buf, Builder, builder),
}),
.root => Value.init(.{ .root = {} }),
else => {
std.debug.panic("Internal error: unexpected resolution tag: {}", .{this.tag});
},
},
};
}
pub fn fmt(this: *const Resolution, string_bytes: []const u8, path_sep: bun.fmt.PathFormatOptions.Sep) Formatter {
return Formatter{
.resolution = this,
.buf = string_bytes,
.path_sep = path_sep,
};
}
const StorePathFormatter = struct {
res: *const Resolution,
string_buf: string,
// opts: String.StorePathFormatter.Options,
pub fn format(this: StorePathFormatter, comptime _: string, _: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void {
const string_buf = this.string_buf;
const res = this.res.value;
switch (this.res.tag) {
.root => try writer.writeAll("root"),
.npm => try writer.print("{}", .{res.npm.version.fmt(string_buf)}),
.local_tarball => try writer.print("{}", .{res.local_tarball.fmtStorePath(string_buf)}),
.remote_tarball => try writer.print("{}", .{res.remote_tarball.fmtStorePath(string_buf)}),
.folder => try writer.print("{}", .{res.folder.fmtStorePath(string_buf)}),
.git => try writer.print("{}", .{res.git.fmtStorePath("git+", string_buf)}),
.github => try writer.print("{}", .{res.github.fmtStorePath("github+", string_buf)}),
.workspace => try writer.print("{}", .{res.workspace.fmtStorePath(string_buf)}),
.symlink => try writer.print("{}", .{res.symlink.fmtStorePath(string_buf)}),
.single_file_module => try writer.print("{}", .{res.single_file_module.fmtStorePath(string_buf)}),
else => {},
}
}
};
pub fn fmtStorePath(this: *const Resolution, string_buf: string) StorePathFormatter {
return .{
.res = this,
.string_buf = string_buf,
};
}
pub fn fmtURL(this: *const Resolution, string_bytes: []const u8) URLFormatter {
return URLFormatter{ .resolution = this, .buf = string_bytes };
}
pub fn fmtForDebug(this: *const Resolution, string_bytes: []const u8) DebugFormatter {
return DebugFormatter{ .resolution = this, .buf = string_bytes };
}
pub fn eql(
lhs: *const Resolution,
rhs: *const 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,
),
.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,
),
.git => lhs.value.git.eql(
&rhs.value.git,
lhs_string_buf,
rhs_string_buf,
),
.github => lhs.value.github.eql(
&rhs.value.github,
lhs_string_buf,
rhs_string_buf,
),
else => unreachable,
};
}
pub const URLFormatter = struct {
resolution: *const Resolution,
buf: []const u8,
pub fn format(formatter: URLFormatter, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void {
const buf = formatter.buf;
const value = formatter.resolution.value;
switch (formatter.resolution.tag) {
.npm => try writer.writeAll(value.npm.url.slice(formatter.buf)),
.local_tarball => try bun.fmt.fmtPath(u8, value.local_tarball.slice(buf), .{ .path_sep = .posix }).format("", {}, writer),
.folder => try writer.writeAll(value.folder.slice(formatter.buf)),
.remote_tarball => try writer.writeAll(value.remote_tarball.slice(formatter.buf)),
.git => try value.git.formatAs("git+", formatter.buf, layout, opts, writer),
.github => try value.github.formatAs("github:", formatter.buf, layout, opts, writer),
.workspace => try std.fmt.format(writer, "workspace:{s}", .{value.workspace.slice(formatter.buf)}),
.symlink => try std.fmt.format(writer, "link:{s}", .{value.symlink.slice(formatter.buf)}),
.single_file_module => try std.fmt.format(writer, "module:{s}", .{value.single_file_module.slice(formatter.buf)}),
else => {},
}
}
};
pub const Formatter = struct {
resolution: *const Resolution,
buf: []const u8,
path_sep: bun.fmt.PathFormatOptions.Sep,
pub fn format(formatter: Formatter, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void {
const buf = formatter.buf;
const value = formatter.resolution.value;
switch (formatter.resolution.tag) {
.npm => try value.npm.version.fmt(buf).format(layout, opts, writer),
.local_tarball => try bun.fmt.fmtPath(u8, value.local_tarball.slice(buf), .{ .path_sep = formatter.path_sep }).format("", {}, writer),
.folder => try bun.fmt.fmtPath(u8, value.folder.slice(buf), .{ .path_sep = formatter.path_sep }).format("", {}, writer),
.remote_tarball => try writer.writeAll(value.remote_tarball.slice(buf)),
.git => try value.git.formatAs("git+", buf, layout, opts, writer),
.github => try value.github.formatAs("github:", buf, layout, opts, writer),
.workspace => try std.fmt.format(writer, "workspace:{s}", .{bun.fmt.fmtPath(u8, value.workspace.slice(buf), .{
.path_sep = formatter.path_sep,
})}),
.symlink => try std.fmt.format(writer, "link:{s}", .{bun.fmt.fmtPath(u8, value.symlink.slice(buf), .{
.path_sep = formatter.path_sep,
})}),
.single_file_module => try std.fmt.format(writer, "module:{s}", .{value.single_file_module.slice(buf)}),
else => {},
}
}
};
pub const DebugFormatter = struct {
resolution: *const Resolution,
buf: []const u8,
pub fn format(formatter: DebugFormatter, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
try writer.writeAll("Resolution{ .");
try writer.writeAll(bun.tagName(Tag, formatter.resolution.tag) orelse "invalid");
try writer.writeAll(" = ");
switch (formatter.resolution.tag) {
.npm => try formatter.resolution.value.npm.version.fmt(formatter.buf).format(layout, opts, writer),
.local_tarball => try writer.writeAll(formatter.resolution.value.local_tarball.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)),
.git => try formatter.resolution.value.git.formatAs("git+", formatter.buf, layout, opts, writer),
.github => try formatter.resolution.value.github.formatAs("github:", 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, "module:{s}", .{formatter.resolution.value.single_file_module.slice(formatter.buf)}),
else => try writer.writeAll("{}"),
}
try writer.writeAll(" }");
}
};
pub const Value = extern union {
uninitialized: void,
root: void,
npm: VersionedURL,
folder: String,
/// File path to a tarball relative to the package root
local_tarball: String,
github: Repository,
git: Repository,
/// global link
symlink: String,
workspace: String,
/// URL to a tarball.
remote_tarball: String,
single_file_module: String,
/// To avoid undefined memory between union values, we must zero initialize the union first.
pub fn init(field: bun.meta.Tagged(Value, Tag)) Value {
return switch (field) {
inline else => |v, t| @unionInit(Value, @tagName(t), v),
/// Use like Resolution.init(.{ .npm = VersionedURL{ ... } })
pub inline fn init(value: bun.meta.Tagged(Value, Tag)) This {
return .{
.tag = std.meta.activeTag(value),
.value = Value.init(value),
};
}
};
pub const Tag = enum(u8) {
uninitialized = 0,
root = 1,
npm = 2,
folder = 4,
local_tarball = 8,
github = 16,
git = 32,
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 fn isGit(this: Tag) bool {
return this == .git or this == .github;
pub fn isGit(this: *const This) bool {
return this.tag.isGit();
}
pub fn canEnqueueInstallTask(this: Tag) bool {
return this == .npm or this == .local_tarball or this == .remote_tarball or this == .git or this == .github;
pub fn canEnqueueInstallTask(this: *const This) bool {
return this.tag.canEnqueueInstallTask();
}
const FromTextLockfileError = OOM || error{
UnexpectedResolution,
InvalidSemver,
};
pub fn fromTextLockfile(res_str: string, string_buf: *String.Buf) FromTextLockfileError!This {
if (strings.hasPrefixComptime(res_str, "root:")) {
return This.init(.{ .root = {} });
}
if (strings.withoutPrefixIfPossibleComptime(res_str, "link:")) |link| {
return This.init(.{ .symlink = try string_buf.append(link) });
}
if (strings.withoutPrefixIfPossibleComptime(res_str, "workspace:")) |workspace| {
return This.init(.{ .workspace = try string_buf.append(workspace) });
}
if (strings.withoutPrefixIfPossibleComptime(res_str, "file:")) |folder| {
return This.init(.{ .folder = try string_buf.append(folder) });
}
return switch (Dependency.Version.Tag.infer(res_str)) {
.git => This.init(.{ .git = try Repository.parseAppendGit(res_str, string_buf) }),
.github => This.init(.{ .github = try Repository.parseAppendGithub(res_str, string_buf) }),
.tarball => {
if (Dependency.isRemoteTarball(res_str)) {
return This.init(.{ .remote_tarball = try string_buf.append(res_str) });
}
return This.init(.{ .local_tarball = try string_buf.append(res_str) });
},
.npm => {
const version_literal = try string_buf.append(res_str);
const parsed = Semver.Version.parse(version_literal.sliced(string_buf.bytes.items));
if (!parsed.valid) {
return error.UnexpectedResolution;
}
if (parsed.version.major == null or parsed.version.minor == null or parsed.version.patch == null) {
return error.UnexpectedResolution;
}
return .{
.tag = .npm,
.value = .{
.npm = .{
.version = parsed.version.min(),
// will fill this later
.url = .{},
},
},
};
},
// covered above
.workspace => error.UnexpectedResolution,
.symlink => error.UnexpectedResolution,
.folder => error.UnexpectedResolution,
// even though it's a dependency type, it's not
// possible for 'catalog:' to be written to the
// lockfile for any resolution because the install
// will fail it it's not successfully replaced by
// a version
.catalog => error.UnexpectedResolution,
// should not happen
.dist_tag => error.UnexpectedResolution,
.uninitialized => error.UnexpectedResolution,
};
}
pub fn order(
lhs: *const This,
rhs: *const This,
lhs_buf: []const u8,
rhs_buf: []const u8,
) std.math.Order {
if (lhs.tag != rhs.tag) {
return std.math.order(@intFromEnum(lhs.tag), @intFromEnum(rhs.tag));
}
return switch (lhs.tag) {
.npm => lhs.value.npm.order(rhs.value.npm, lhs_buf, rhs_buf),
.local_tarball => lhs.value.local_tarball.order(&rhs.value.local_tarball, lhs_buf, rhs_buf),
.folder => lhs.value.folder.order(&rhs.value.folder, lhs_buf, rhs_buf),
.remote_tarball => lhs.value.remote_tarball.order(&rhs.value.remote_tarball, lhs_buf, rhs_buf),
.workspace => lhs.value.workspace.order(&rhs.value.workspace, lhs_buf, rhs_buf),
.symlink => lhs.value.symlink.order(&rhs.value.symlink, lhs_buf, rhs_buf),
.single_file_module => lhs.value.single_file_module.order(&rhs.value.single_file_module, lhs_buf, rhs_buf),
.git => lhs.value.git.order(&rhs.value.git, lhs_buf, rhs_buf),
.github => lhs.value.github.order(&rhs.value.github, lhs_buf, rhs_buf),
else => .eq,
};
}
pub fn count(this: *const This, buf: []const u8, comptime Builder: type, builder: Builder) void {
switch (this.tag) {
.npm => this.value.npm.count(buf, Builder, builder),
.local_tarball => builder.count(this.value.local_tarball.slice(buf)),
.folder => builder.count(this.value.folder.slice(buf)),
.remote_tarball => builder.count(this.value.remote_tarball.slice(buf)),
.workspace => builder.count(this.value.workspace.slice(buf)),
.symlink => builder.count(this.value.symlink.slice(buf)),
.single_file_module => builder.count(this.value.single_file_module.slice(buf)),
.git => this.value.git.count(buf, Builder, builder),
.github => this.value.github.count(buf, Builder, builder),
else => {},
}
}
pub fn clone(this: *const This, buf: []const u8, comptime Builder: type, builder: Builder) This {
return .{
.tag = this.tag,
.value = switch (this.tag) {
.npm => Value.init(.{ .npm = this.value.npm.clone(buf, Builder, builder) }),
.local_tarball => Value.init(.{
.local_tarball = builder.append(String, this.value.local_tarball.slice(buf)),
}),
.folder => Value.init(.{
.folder = builder.append(String, this.value.folder.slice(buf)),
}),
.remote_tarball => Value.init(.{
.remote_tarball = builder.append(String, this.value.remote_tarball.slice(buf)),
}),
.workspace => Value.init(.{
.workspace = builder.append(String, this.value.workspace.slice(buf)),
}),
.symlink => Value.init(.{
.symlink = builder.append(String, this.value.symlink.slice(buf)),
}),
.single_file_module => Value.init(.{
.single_file_module = builder.append(String, this.value.single_file_module.slice(buf)),
}),
.git => Value.init(.{
.git = this.value.git.clone(buf, Builder, builder),
}),
.github => Value.init(.{
.github = this.value.github.clone(buf, Builder, builder),
}),
.root => Value.init(.{ .root = {} }),
else => {
std.debug.panic("Internal error: unexpected resolution tag: {}", .{this.tag});
},
},
};
}
pub fn fmt(this: *const This, string_bytes: []const u8, path_sep: bun.fmt.PathFormatOptions.Sep) Formatter {
return Formatter{
.resolution = this,
.buf = string_bytes,
.path_sep = path_sep,
};
}
const StorePathFormatter = struct {
res: *const This,
string_buf: string,
// opts: String.StorePathFormatter.Options,
pub fn format(this: StorePathFormatter, comptime _: string, _: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void {
const string_buf = this.string_buf;
const res = this.res.value;
switch (this.res.tag) {
.root => try writer.writeAll("root"),
.npm => try writer.print("{}", .{res.npm.version.fmt(string_buf)}),
.local_tarball => try writer.print("{}", .{res.local_tarball.fmtStorePath(string_buf)}),
.remote_tarball => try writer.print("{}", .{res.remote_tarball.fmtStorePath(string_buf)}),
.folder => try writer.print("{}", .{res.folder.fmtStorePath(string_buf)}),
.git => try writer.print("{}", .{res.git.fmtStorePath("git+", string_buf)}),
.github => try writer.print("{}", .{res.github.fmtStorePath("github+", string_buf)}),
.workspace => try writer.print("{}", .{res.workspace.fmtStorePath(string_buf)}),
.symlink => try writer.print("{}", .{res.symlink.fmtStorePath(string_buf)}),
.single_file_module => try writer.print("{}", .{res.single_file_module.fmtStorePath(string_buf)}),
else => {},
}
}
};
pub fn fmtStorePath(this: *const This, string_buf: string) StorePathFormatter {
return .{
.res = this,
.string_buf = string_buf,
};
}
pub fn fmtURL(this: *const This, string_bytes: []const u8) URLFormatter {
return URLFormatter{ .resolution = this, .buf = string_bytes };
}
pub fn fmtForDebug(this: *const This, string_bytes: []const u8) DebugFormatter {
return DebugFormatter{ .resolution = this, .buf = string_bytes };
}
pub fn eql(
lhs: *const This,
rhs: *const This,
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,
),
.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,
),
.git => lhs.value.git.eql(
&rhs.value.git,
lhs_string_buf,
rhs_string_buf,
),
.github => lhs.value.github.eql(
&rhs.value.github,
lhs_string_buf,
rhs_string_buf,
),
else => unreachable,
};
}
pub const URLFormatter = struct {
resolution: *const This,
buf: []const u8,
pub fn format(formatter: URLFormatter, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void {
const buf = formatter.buf;
const value = formatter.resolution.value;
switch (formatter.resolution.tag) {
.npm => try writer.writeAll(value.npm.url.slice(formatter.buf)),
.local_tarball => try bun.fmt.fmtPath(u8, value.local_tarball.slice(buf), .{ .path_sep = .posix }).format("", {}, writer),
.folder => try writer.writeAll(value.folder.slice(formatter.buf)),
.remote_tarball => try writer.writeAll(value.remote_tarball.slice(formatter.buf)),
.git => try value.git.formatAs("git+", formatter.buf, layout, opts, writer),
.github => try value.github.formatAs("github:", formatter.buf, layout, opts, writer),
.workspace => try std.fmt.format(writer, "workspace:{s}", .{value.workspace.slice(formatter.buf)}),
.symlink => try std.fmt.format(writer, "link:{s}", .{value.symlink.slice(formatter.buf)}),
.single_file_module => try std.fmt.format(writer, "module:{s}", .{value.single_file_module.slice(formatter.buf)}),
else => {},
}
}
};
pub const Formatter = struct {
resolution: *const This,
buf: []const u8,
path_sep: bun.fmt.PathFormatOptions.Sep,
pub fn format(formatter: Formatter, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void {
const buf = formatter.buf;
const value = formatter.resolution.value;
switch (formatter.resolution.tag) {
.npm => try value.npm.version.fmt(buf).format(layout, opts, writer),
.local_tarball => try bun.fmt.fmtPath(u8, value.local_tarball.slice(buf), .{ .path_sep = formatter.path_sep }).format("", {}, writer),
.folder => try bun.fmt.fmtPath(u8, value.folder.slice(buf), .{ .path_sep = formatter.path_sep }).format("", {}, writer),
.remote_tarball => try writer.writeAll(value.remote_tarball.slice(buf)),
.git => try value.git.formatAs("git+", buf, layout, opts, writer),
.github => try value.github.formatAs("github:", buf, layout, opts, writer),
.workspace => try std.fmt.format(writer, "workspace:{s}", .{bun.fmt.fmtPath(u8, value.workspace.slice(buf), .{
.path_sep = formatter.path_sep,
})}),
.symlink => try std.fmt.format(writer, "link:{s}", .{bun.fmt.fmtPath(u8, value.symlink.slice(buf), .{
.path_sep = formatter.path_sep,
})}),
.single_file_module => try std.fmt.format(writer, "module:{s}", .{value.single_file_module.slice(buf)}),
else => {},
}
}
};
pub const DebugFormatter = struct {
resolution: *const This,
buf: []const u8,
pub fn format(formatter: DebugFormatter, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
try writer.writeAll("Resolution{ .");
try writer.writeAll(bun.tagName(Tag, formatter.resolution.tag) orelse "invalid");
try writer.writeAll(" = ");
switch (formatter.resolution.tag) {
.npm => try formatter.resolution.value.npm.version.fmt(formatter.buf).format(layout, opts, writer),
.local_tarball => try writer.writeAll(formatter.resolution.value.local_tarball.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)),
.git => try formatter.resolution.value.git.formatAs("git+", formatter.buf, layout, opts, writer),
.github => try formatter.resolution.value.github.formatAs("github:", 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, "module:{s}", .{formatter.resolution.value.single_file_module.slice(formatter.buf)}),
else => try writer.writeAll("{}"),
}
try writer.writeAll(" }");
}
};
pub const Value = extern union {
uninitialized: void,
root: void,
npm: VersionedURLType(SemverIntType),
folder: String,
/// File path to a tarball relative to the package root
local_tarball: String,
github: Repository,
git: Repository,
/// global link
symlink: String,
workspace: String,
/// URL to a tarball.
remote_tarball: String,
single_file_module: String,
/// To avoid undefined memory between union values, we must zero initialize the union first.
pub fn init(field: bun.meta.Tagged(Value, Tag)) Value {
return switch (field) {
inline else => |v, t| @unionInit(Value, @tagName(t), v),
};
}
};
pub const Tag = enum(u8) {
uninitialized = 0,
root = 1,
npm = 2,
folder = 4,
local_tarball = 8,
github = 16,
git = 32,
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 fn isGit(this: Tag) bool {
return this == .git or this == .github;
}
pub fn canEnqueueInstallTask(this: Tag) bool {
return this == .npm or this == .local_tarball or this == .remote_tarball or this == .git or this == .github;
}
};
};
};
}
const string = []const u8;
const std = @import("std");
const Repository = @import("./repository.zig").Repository;
const VersionedURL = @import("./versioned_url.zig").VersionedURL;
const VersionedURLType = @import("./versioned_url.zig").VersionedURLType;
const bun = @import("bun");
const OOM = bun.OOM;

View File

@@ -1,27 +1,42 @@
pub const VersionedURL = extern struct {
url: String,
version: Semver.Version,
pub const VersionedURL = VersionedURLType(u64);
pub const OldV2VersionedURL = VersionedURLType(u32);
pub fn eql(this: VersionedURL, other: VersionedURL) bool {
return this.version.eql(other.version);
}
pub fn VersionedURLType(comptime SemverIntType: type) type {
return extern struct {
url: String,
version: Semver.VersionType(SemverIntType),
pub fn order(this: VersionedURL, other: VersionedURL, lhs_buf: []const u8, rhs_buf: []const u8) @import("std").math.Order {
return this.version.order(other.version, lhs_buf, rhs_buf);
}
pub fn eql(this: @This(), other: @This()) bool {
return this.version.eql(other.version);
}
pub fn count(this: VersionedURL, buf: []const u8, comptime Builder: type, builder: Builder) void {
this.version.count(buf, comptime Builder, builder);
builder.count(this.url.slice(buf));
}
pub fn order(this: @This(), other: @This(), lhs_buf: []const u8, rhs_buf: []const u8) @import("std").math.Order {
return this.version.order(other.version, lhs_buf, rhs_buf);
}
pub fn clone(this: VersionedURL, buf: []const u8, comptime Builder: type, builder: Builder) VersionedURL {
return VersionedURL{
.version = this.version.append(buf, Builder, builder),
.url = builder.append(String, this.url.slice(buf)),
};
}
};
pub fn count(this: @This(), buf: []const u8, comptime Builder: type, builder: Builder) void {
this.version.count(buf, comptime Builder, builder);
builder.count(this.url.slice(buf));
}
pub fn clone(this: @This(), buf: []const u8, comptime Builder: type, builder: Builder) @This() {
return @This(){
.version = this.version.append(buf, Builder, builder),
.url = builder.append(String, this.url.slice(buf)),
};
}
pub fn migrate(this: @This()) VersionedURLType(u64) {
if (comptime SemverIntType != u32) {
@compileError("unexpected SemverIntType");
}
return .{
.url = this.url,
.version = this.version.migrate(),
};
}
};
}
const bun = @import("bun");

View File

@@ -2,6 +2,7 @@
pub const String = @import("./semver/SemverString.zig").String;
pub const ExternalString = @import("./semver/ExternalString.zig").ExternalString;
pub const Version = @import("./semver/Version.zig").Version;
pub const VersionType = @import("./semver/Version.zig").VersionType;
pub const SlicedString = @import("./semver/SlicedString.zig");
pub const Range = @import("./semver/SemverRange.zig");

View File

@@ -425,9 +425,9 @@ pub const Token = struct {
.right = .{
.op = .lte,
.version = .{
.major = std.math.maxInt(u32),
.minor = std.math.maxInt(u32),
.patch = std.math.maxInt(u32),
.major = std.math.maxInt(u64),
.minor = std.math.maxInt(u64),
.patch = std.math.maxInt(u64),
},
},
},
@@ -437,8 +437,8 @@ pub const Token = struct {
.op = .lte,
.version = .{
.major = version.major orelse 0,
.minor = std.math.maxInt(u32),
.patch = std.math.maxInt(u32),
.minor = std.math.maxInt(u64),
.patch = std.math.maxInt(u64),
},
},
},
@@ -458,8 +458,8 @@ pub const Token = struct {
.op = .gt,
.version = .{
.major = version.major orelse 0,
.minor = std.math.maxInt(u32),
.patch = std.math.maxInt(u32),
.minor = std.math.maxInt(u64),
.patch = std.math.maxInt(u64),
},
},
},
@@ -483,7 +483,7 @@ pub const Token = struct {
.version = .{
.major = version.major orelse 0,
.minor = version.minor orelse 0,
.patch = std.math.maxInt(u32),
.patch = std.math.maxInt(u64),
},
},
},
@@ -504,7 +504,7 @@ pub const Token = struct {
.version = .{
.major = version.major orelse 0,
.minor = version.minor orelse 0,
.patch = std.math.maxInt(u32),
.patch = std.math.maxInt(u64),
},
},
},

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
exports[`dependency on workspace without version in package.json: version: * 1`] = `
"{
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"no-deps": 2,
@@ -125,7 +125,7 @@ exports[`dependency on workspace without version in package.json: version: * 1`]
exports[`dependency on workspace without version in package.json: version: *.*.* 1`] = `
"{
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"no-deps": 2,
@@ -248,7 +248,7 @@ exports[`dependency on workspace without version in package.json: version: *.*.*
exports[`dependency on workspace without version in package.json: version: =* 1`] = `
"{
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"no-deps": 2,
@@ -371,7 +371,7 @@ exports[`dependency on workspace without version in package.json: version: =* 1`
exports[`dependency on workspace without version in package.json: version: kjwoehcojrgjoj 1`] = `
"{
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"no-deps": 2,
@@ -494,7 +494,7 @@ exports[`dependency on workspace without version in package.json: version: kjwoe
exports[`dependency on workspace without version in package.json: version: *.1.* 1`] = `
"{
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"no-deps": 2,
@@ -617,7 +617,7 @@ exports[`dependency on workspace without version in package.json: version: *.1.*
exports[`dependency on workspace without version in package.json: version: *-pre 1`] = `
"{
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"no-deps": 2,
@@ -740,7 +740,7 @@ exports[`dependency on workspace without version in package.json: version: *-pre
exports[`dependency on workspace without version in package.json: version: 1 1`] = `
"{
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"no-deps": [
@@ -896,7 +896,7 @@ exports[`dependency on workspace without version in package.json: version: 1 1`]
exports[`dependency on workspace without version in package.json: version: 1.* 1`] = `
"{
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"no-deps": [
@@ -1052,7 +1052,7 @@ exports[`dependency on workspace without version in package.json: version: 1.* 1
exports[`dependency on workspace without version in package.json: version: 1.1.* 1`] = `
"{
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"no-deps": [
@@ -1208,7 +1208,7 @@ exports[`dependency on workspace without version in package.json: version: 1.1.*
exports[`dependency on workspace without version in package.json: version: 1.1.0 1`] = `
"{
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"no-deps": [
@@ -1364,7 +1364,7 @@ exports[`dependency on workspace without version in package.json: version: 1.1.0
exports[`dependency on workspace without version in package.json: version: *-pre+build 1`] = `
"{
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"no-deps": [
@@ -1520,7 +1520,7 @@ exports[`dependency on workspace without version in package.json: version: *-pre
exports[`dependency on workspace without version in package.json: version: *+build 1`] = `
"{
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"no-deps": [
@@ -1676,7 +1676,7 @@ exports[`dependency on workspace without version in package.json: version: *+bui
exports[`dependency on workspace without version in package.json: version: latest 1`] = `
"{
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"no-deps": [
@@ -1832,7 +1832,7 @@ exports[`dependency on workspace without version in package.json: version: lates
exports[`dependency on workspace without version in package.json: version: 1`] = `
"{
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"no-deps": [
@@ -1988,7 +1988,7 @@ exports[`dependency on workspace without version in package.json: version: 1`]
exports[`dependency on same name as workspace and dist-tag: with version 1`] = `
"{
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"no-deps": [

Binary file not shown.

View File

@@ -0,0 +1,63 @@
import { file, spawn } from "bun";
import { install_test_helpers } from "bun:internal-for-testing";
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { cp } from "node:fs/promises";
import { join } from "node:path";
const { parseLockfile } = install_test_helpers;
test("old binary lockfile migrates successfully", async () => {
const oldLockfileContents = await file(join(import.meta.dir, "fixtures/bun.lockb.v2")).text();
using testDir = tempDir("migrate-bun-lockb-v2", {
"bunfig.toml": "install.saveTextLockfile = false",
"package.json": JSON.stringify({
name: "migrate-bun-lockb-v2",
dependencies: {
jquery: "~3.7.1",
"is-even": "^1.0.0",
},
}),
});
await cp(join(import.meta.dir, "fixtures/bun.lockb.v2"), join(testDir, "bun.lockb"));
const oldLockfile = parseLockfile(testDir);
let { stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: testDir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
let err = await stderr.text();
expect(await exited).toBe(0);
expect(err).toContain("Saved lockfile");
const newLockfileContents = await file(join(testDir, "bun.lockb")).bytes();
const newLockfile = parseLockfile(testDir);
// contents should be different due to semver numbers changing size
expect(newLockfileContents).not.toEqual(oldLockfileContents);
// but parse result should be the same
expect(newLockfile).toEqual(oldLockfile);
// another install should not change the lockfile
({ stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: testDir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
}));
expect(await exited).toBe(0);
expect(await stderr.text()).not.toContain("Saved lockfile");
const newLockfileContents2 = await file(join(testDir, "bun.lockb")).bytes();
const newLockfile2 = parseLockfile(testDir);
expect(newLockfileContents2).toEqual(newLockfileContents);
expect(newLockfile2).toEqual(newLockfile);
});

View File

@@ -13596,7 +13596,7 @@ exports[`ssr works for 100-ish requests 1`] = `
"package_id": null,
},
],
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"@alloc/quick-lru": 1,

View File

@@ -13596,7 +13596,7 @@ exports[`hot reloading works on the client (+ tailwind hmr) 1`] = `
"package_id": null,
},
],
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"@alloc/quick-lru": 1,

View File

@@ -13596,7 +13596,7 @@ exports[`next build works: bun 1`] = `
"package_id": null,
},
],
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"@alloc/quick-lru": 1,
@@ -39534,7 +39534,7 @@ exports[`next build works: node 1`] = `
"package_id": null,
},
],
"format": "v2",
"format": "v3",
"meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"package_index": {
"@alloc/quick-lru": 1,