mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
### What does this PR do? fixes #22679 * includes a better error if a package cant be met because of the age (but would normally) * logs the resolved one in --verbose (which can be helpful in debugging to show it does know latest but couldn't use) * makes bun outdated show in the table when the package isn't true latest * includes a rudimentary "stability" check if a later version is in blacked out time (but only up to 7 days as it goes back to latest with min age) For extended security we could also Last-Modified header of the tgz download and then abort if too new (just like the hash) | install error with no recent version | bun outdated respecting the rule | | --- | --- | <img width="838" height="119" alt="image" src="https://github.com/user-attachments/assets/b60916a8-27f6-4405-bfb6-57f9fa8bb0d6" /> | <img width="609" height="314" alt="image" src="https://github.com/user-attachments/assets/d8869ff4-8e16-492c-8e4c-9ac1dfa302ba" /> | For stable release we will make it use `3d` type syntax instead of magic second numbers. ### How did you verify your code works? tests & manual --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
380 lines
12 KiB
Zig
380 lines
12 KiB
Zig
//! Schedule long-running callbacks for a task
|
|
//! Slow stuff is broken into tasks, each can run independently without locks
|
|
|
|
tag: Tag,
|
|
request: Request,
|
|
data: Data,
|
|
status: Status = Status.waiting,
|
|
threadpool_task: ThreadPool.Task = ThreadPool.Task{ .callback = &callback },
|
|
log: logger.Log,
|
|
id: Id,
|
|
err: ?anyerror = null,
|
|
package_manager: *PackageManager,
|
|
apply_patch_task: ?*PatchTask = null,
|
|
next: ?*Task = null,
|
|
|
|
/// An ID that lets us register a callback without keeping the same pointer around
|
|
pub const Id = enum(u64) {
|
|
_,
|
|
|
|
pub fn get(this: @This()) u64 {
|
|
return @intFromEnum(this);
|
|
}
|
|
|
|
pub fn forNPMPackage(package_name: string, package_version: Semver.Version) Id {
|
|
var hasher = bun.Wyhash11.init(0);
|
|
hasher.update("npm-package:");
|
|
hasher.update(package_name);
|
|
hasher.update("@");
|
|
hasher.update(std.mem.asBytes(&package_version));
|
|
return @enumFromInt(hasher.final());
|
|
}
|
|
|
|
pub fn forBinLink(package_id: PackageID) Id {
|
|
var hasher = bun.Wyhash11.init(0);
|
|
hasher.update("bin-link:");
|
|
hasher.update(std.mem.asBytes(&package_id));
|
|
return @enumFromInt(hasher.final());
|
|
}
|
|
|
|
pub fn forManifest(name: string) Id {
|
|
var hasher = bun.Wyhash11.init(0);
|
|
hasher.update("manifest:");
|
|
hasher.update(name);
|
|
return @enumFromInt(hasher.final());
|
|
}
|
|
|
|
pub fn forTarball(url: string) Id {
|
|
var hasher = bun.Wyhash11.init(0);
|
|
hasher.update("tarball:");
|
|
hasher.update(url);
|
|
return @enumFromInt(hasher.final());
|
|
}
|
|
|
|
// These cannot change:
|
|
// We persist them to the filesystem.
|
|
pub fn forGitClone(url: string) Id {
|
|
var hasher = bun.Wyhash11.init(0);
|
|
hasher.update(url);
|
|
return @enumFromInt(@as(u64, 4 << 61) | @as(u64, @as(u61, @truncate(hasher.final()))));
|
|
}
|
|
|
|
pub fn forGitCheckout(url: string, resolved: string) Id {
|
|
var hasher = bun.Wyhash11.init(0);
|
|
hasher.update(url);
|
|
hasher.update("@");
|
|
hasher.update(resolved);
|
|
return @enumFromInt(@as(u64, 5 << 61) | @as(u64, @as(u61, @truncate(hasher.final()))));
|
|
}
|
|
};
|
|
|
|
pub fn callback(task: *ThreadPool.Task) void {
|
|
Output.Source.configureThread();
|
|
defer Output.flush();
|
|
|
|
var this: *Task = @fieldParentPtr("threadpool_task", task);
|
|
const manager = this.package_manager;
|
|
defer {
|
|
if (this.status == .success) {
|
|
if (this.apply_patch_task) |pt| {
|
|
defer pt.deinit();
|
|
bun.handleOom(pt.apply());
|
|
if (pt.callback.apply.logger.errors > 0) {
|
|
defer pt.callback.apply.logger.deinit();
|
|
// this.log.addErrorFmt(null, logger.Loc.Empty, bun.default_allocator, "failed to apply patch: {}", .{e}) catch unreachable;
|
|
pt.callback.apply.logger.print(Output.errorWriter()) catch {};
|
|
}
|
|
}
|
|
}
|
|
manager.resolve_tasks.push(this);
|
|
manager.wake();
|
|
}
|
|
|
|
switch (this.tag) {
|
|
.package_manifest => {
|
|
const allocator = bun.default_allocator;
|
|
var manifest = &this.request.package_manifest;
|
|
|
|
const body = &manifest.network.response_buffer;
|
|
defer body.deinit();
|
|
|
|
const package_manifest = Npm.Registry.getPackageMetadata(
|
|
allocator,
|
|
manager.scopeForPackageName(manifest.name.slice()),
|
|
(manifest.network.response.metadata orelse @panic("Assertion failure: Expected metadata to be set")).response,
|
|
body.slice(),
|
|
&this.log,
|
|
manifest.name.slice(),
|
|
manifest.network.callback.package_manifest.loaded_manifest,
|
|
manager,
|
|
manifest.network.callback.package_manifest.is_extended_manifest,
|
|
) catch |err| {
|
|
bun.handleErrorReturnTrace(err, @errorReturnTrace());
|
|
|
|
this.err = err;
|
|
this.status = Status.fail;
|
|
this.data = .{ .package_manifest = .{} };
|
|
return;
|
|
};
|
|
|
|
switch (package_manifest) {
|
|
.fresh, .cached => |result| {
|
|
this.status = Status.success;
|
|
this.data = .{ .package_manifest = result };
|
|
return;
|
|
},
|
|
.not_found => {
|
|
this.log.addErrorFmt(null, logger.Loc.Empty, allocator, "404 - GET {s}", .{
|
|
this.request.package_manifest.name.slice(),
|
|
}) catch unreachable;
|
|
this.status = Status.fail;
|
|
this.data = .{ .package_manifest = .{} };
|
|
return;
|
|
},
|
|
}
|
|
},
|
|
.extract => {
|
|
const buffer = &this.request.extract.network.response_buffer;
|
|
defer buffer.deinit();
|
|
|
|
const result = this.request.extract.tarball.run(
|
|
&this.log,
|
|
buffer.slice(),
|
|
) catch |err| {
|
|
bun.handleErrorReturnTrace(err, @errorReturnTrace());
|
|
|
|
this.err = err;
|
|
this.status = Status.fail;
|
|
this.data = .{ .extract = .{} };
|
|
return;
|
|
};
|
|
|
|
this.data = .{ .extract = result };
|
|
this.status = Status.success;
|
|
},
|
|
.git_clone => {
|
|
const name = this.request.git_clone.name.slice();
|
|
const url = this.request.git_clone.url.slice();
|
|
var attempt: u8 = 1;
|
|
const dir = brk: {
|
|
if (Repository.tryHTTPS(url)) |https| break :brk Repository.download(
|
|
manager.allocator,
|
|
this.request.git_clone.env,
|
|
&this.log,
|
|
manager.getCacheDirectory(),
|
|
this.id,
|
|
name,
|
|
https,
|
|
attempt,
|
|
) catch |err| {
|
|
// Exit early if git checked and could
|
|
// not find the repository, skip ssh
|
|
if (err == error.RepositoryNotFound) {
|
|
this.err = err;
|
|
this.status = Status.fail;
|
|
this.data = .{ .git_clone = bun.invalid_fd };
|
|
|
|
return;
|
|
}
|
|
|
|
this.err = err;
|
|
this.status = Status.fail;
|
|
this.data = .{ .git_clone = bun.invalid_fd };
|
|
attempt += 1;
|
|
break :brk null;
|
|
};
|
|
break :brk null;
|
|
} orelse if (Repository.trySSH(url)) |ssh| Repository.download(
|
|
manager.allocator,
|
|
this.request.git_clone.env,
|
|
&this.log,
|
|
manager.getCacheDirectory(),
|
|
this.id,
|
|
name,
|
|
ssh,
|
|
attempt,
|
|
) catch |err| {
|
|
this.err = err;
|
|
this.status = Status.fail;
|
|
this.data = .{ .git_clone = bun.invalid_fd };
|
|
return;
|
|
} else {
|
|
return;
|
|
};
|
|
|
|
this.err = null;
|
|
this.data = .{ .git_clone = .fromStdDir(dir) };
|
|
this.status = Status.success;
|
|
},
|
|
.git_checkout => {
|
|
const git_checkout = &this.request.git_checkout;
|
|
const data = Repository.checkout(
|
|
manager.allocator,
|
|
this.request.git_checkout.env,
|
|
&this.log,
|
|
manager.getCacheDirectory(),
|
|
git_checkout.repo_dir.stdDir(),
|
|
git_checkout.name.slice(),
|
|
git_checkout.url.slice(),
|
|
git_checkout.resolved.slice(),
|
|
) catch |err| {
|
|
this.err = err;
|
|
this.status = Status.fail;
|
|
this.data = .{ .git_checkout = .{} };
|
|
|
|
return;
|
|
};
|
|
|
|
this.data = .{
|
|
.git_checkout = data,
|
|
};
|
|
this.status = Status.success;
|
|
},
|
|
.local_tarball => {
|
|
const workspace_pkg_id = manager.lockfile.getWorkspacePkgIfWorkspaceDep(this.request.local_tarball.tarball.dependency_id);
|
|
|
|
var abs_buf: bun.PathBuffer = undefined;
|
|
const tarball_path, const normalize = if (workspace_pkg_id != invalid_package_id) tarball_path: {
|
|
const workspace_res = manager.lockfile.packages.items(.resolution)[workspace_pkg_id];
|
|
|
|
if (workspace_res.tag != .workspace) break :tarball_path .{ this.request.local_tarball.tarball.url.slice(), true };
|
|
|
|
// Construct an absolute path to the tarball.
|
|
// Normally tarball paths are always relative to the root directory, but if a
|
|
// workspace depends on a tarball path, it should be relative to the workspace.
|
|
const workspace_path = workspace_res.value.workspace.slice(manager.lockfile.buffers.string_bytes.items);
|
|
break :tarball_path .{
|
|
Path.joinAbsStringBuf(
|
|
FileSystem.instance.top_level_dir,
|
|
&abs_buf,
|
|
&[_][]const u8{
|
|
workspace_path,
|
|
this.request.local_tarball.tarball.url.slice(),
|
|
},
|
|
.auto,
|
|
),
|
|
false,
|
|
};
|
|
} else .{ this.request.local_tarball.tarball.url.slice(), true };
|
|
|
|
const result = readAndExtract(
|
|
manager.allocator,
|
|
&this.request.local_tarball.tarball,
|
|
tarball_path,
|
|
normalize,
|
|
&this.log,
|
|
) catch |err| {
|
|
bun.handleErrorReturnTrace(err, @errorReturnTrace());
|
|
|
|
this.err = err;
|
|
this.status = Status.fail;
|
|
this.data = .{ .extract = .{} };
|
|
|
|
return;
|
|
};
|
|
|
|
this.data = .{ .extract = result };
|
|
this.status = Status.success;
|
|
},
|
|
}
|
|
}
|
|
|
|
fn readAndExtract(
|
|
allocator: std.mem.Allocator,
|
|
tarball: *const ExtractTarball,
|
|
tarball_path: string,
|
|
normalize: bool,
|
|
log: *logger.Log,
|
|
) !ExtractData {
|
|
const bytes = if (normalize)
|
|
try File.readFromUserInput(std.fs.cwd(), tarball_path, allocator).unwrap()
|
|
else
|
|
try File.readFrom(bun.FD.cwd(), tarball_path, allocator).unwrap();
|
|
defer allocator.free(bytes);
|
|
return tarball.run(log, bytes);
|
|
}
|
|
|
|
pub const Tag = enum(u3) {
|
|
package_manifest = 0,
|
|
extract = 1,
|
|
git_clone = 2,
|
|
git_checkout = 3,
|
|
local_tarball = 4,
|
|
};
|
|
|
|
pub const Status = enum {
|
|
waiting,
|
|
success,
|
|
fail,
|
|
};
|
|
|
|
pub const Data = union {
|
|
package_manifest: Npm.PackageManifest,
|
|
extract: ExtractData,
|
|
git_clone: bun.FileDescriptor,
|
|
git_checkout: ExtractData,
|
|
};
|
|
|
|
pub const Request = union {
|
|
/// package name
|
|
// todo: Registry URL
|
|
package_manifest: struct {
|
|
name: strings.StringOrTinyString,
|
|
network: *NetworkTask,
|
|
},
|
|
extract: struct {
|
|
network: *NetworkTask,
|
|
tarball: ExtractTarball,
|
|
},
|
|
git_clone: struct {
|
|
name: strings.StringOrTinyString,
|
|
url: strings.StringOrTinyString,
|
|
env: DotEnv.Map,
|
|
dep_id: DependencyID,
|
|
res: Resolution,
|
|
},
|
|
git_checkout: struct {
|
|
repo_dir: bun.FileDescriptor,
|
|
dependency_id: DependencyID,
|
|
name: strings.StringOrTinyString,
|
|
url: strings.StringOrTinyString,
|
|
resolved: strings.StringOrTinyString,
|
|
resolution: Resolution,
|
|
env: DotEnv.Map,
|
|
},
|
|
local_tarball: struct {
|
|
tarball: ExtractTarball,
|
|
},
|
|
};
|
|
|
|
const string = []const u8;
|
|
|
|
const std = @import("std");
|
|
|
|
const install = @import("./install.zig");
|
|
const DependencyID = install.DependencyID;
|
|
const ExtractData = install.ExtractData;
|
|
const ExtractTarball = install.ExtractTarball;
|
|
const NetworkTask = install.NetworkTask;
|
|
const Npm = install.Npm;
|
|
const PackageID = install.PackageID;
|
|
const PackageManager = install.PackageManager;
|
|
const PatchTask = install.PatchTask;
|
|
const Repository = install.Repository;
|
|
const Resolution = install.Resolution;
|
|
const Task = install.Task;
|
|
const invalid_package_id = install.invalid_package_id;
|
|
|
|
const bun = @import("bun");
|
|
const DotEnv = bun.DotEnv;
|
|
const Output = bun.Output;
|
|
const Path = bun.path;
|
|
const Semver = bun.Semver;
|
|
const ThreadPool = bun.ThreadPool;
|
|
const logger = bun.logger;
|
|
const strings = bun.strings;
|
|
const File = bun.sys.File;
|
|
|
|
const Fs = bun.fs;
|
|
const FileSystem = Fs.FileSystem;
|