Compare commits

...

11 Commits

Author SHA1 Message Date
Jarred Sumner
7988a54155 WIP 2025-06-19 21:44:00 -07:00
Jarred Sumner
ddd4305227 w 2025-06-18 23:37:11 -07:00
Jarred Sumner
548c9a943c more 2025-06-18 18:32:19 -07:00
Jarred-Sumner
c1ccfe9d5f bun run zig-format 2025-06-18 21:50:12 +00:00
Jarred Sumner
f34a04d80a here 2025-06-18 14:48:20 -07:00
Jarred Sumner
1ac56afd65 Merge branch 'main' into jarred/gitrunner 2025-06-18 12:30:14 -07:00
Jarred Sumner
d302a67517 Merge branch 'main' into jarred/gitrunner 2025-06-18 02:29:48 -07:00
Jarred Sumner
38469e743b Use pool 2025-06-18 00:27:29 -07:00
Jarred-Sumner
fae579e26f bun run zig-format 2025-06-18 06:52:41 +00:00
Jarred-Sumner
c9bf8c39f5 bun scripts/glob-sources.mjs 2025-06-18 06:52:22 +00:00
Jarred Sumner
4c6f57efff Refactor bun install git repositories to use the event loop instead of the thread pool 2025-06-18 08:46:57 +02:00
8 changed files with 1262 additions and 648 deletions

View File

@@ -518,6 +518,7 @@ src/ini.zig
src/install/bin.zig
src/install/dependency.zig
src/install/extract_tarball.zig
src/install/GitRunner.zig
src/install/hoisted_install.zig
src/install/install_binding.zig
src/install/install.zig

View File

@@ -80,6 +80,7 @@ pub const Rusage = if (Environment.isWindows) win_rusage else std.posix.rusage;
const Subprocess = JSC.Subprocess;
const LifecycleScriptSubprocess = bun.install.LifecycleScriptSubprocess;
const GitRunner = bun.install.GitRunner;
const ShellSubprocess = bun.shell.ShellSubprocess;
const ProcessHandle = @import("../../../cli/filter_run.zig").ProcessHandle;
// const ShellSubprocessMini = bun.shell.ShellSubprocessMini;
@@ -93,6 +94,7 @@ pub const ProcessExitHandler = struct {
.{
Subprocess,
LifecycleScriptSubprocess,
GitRunner,
ShellSubprocess,
ProcessHandle,
@@ -118,6 +120,10 @@ pub const ProcessExitHandler = struct {
const subprocess = this.ptr.as(LifecycleScriptSubprocess);
subprocess.onProcessExit(process, status, rusage);
},
@field(TaggedPointer.Tag, @typeName(GitRunner)) => {
const subprocess = this.ptr.as(GitRunner);
subprocess.onProcessExit(process, status, rusage);
},
@field(TaggedPointer.Tag, @typeName(ProcessHandle)) => {
const subprocess = this.ptr.as(ProcessHandle);
subprocess.onProcessExit(process, status, rusage);

View File

@@ -14,6 +14,12 @@ const errno = std.posix.errno;
const mode_t = std.posix.mode_t;
const unexpectedErrno = std.posix.unexpectedErrno;
const NullTerminatedCStringSlice = [*:null]?[*:0]const u8;
fn allocateSpawnArguments(allocator: std.mem.Allocator, argv: anytype) !struct { NullTerminatedCStringSlice, []u8 } {
return try bun.StringBuilder.createNullDelimited(allocator, argv);
}
pub const BunSpawn = struct {
pub const Action = extern struct {
pub const FileActionType = enum(u8) {
@@ -133,6 +139,9 @@ pub const BunSpawn = struct {
_ = this;
}
};
pub const allocateArguments = allocateSpawnArguments;
pub const Argv = NullTerminatedCStringSlice;
};
// mostly taken from zig's posix_spawn.zig
@@ -292,15 +301,15 @@ pub const PosixSpawn = struct {
pid: *c_int,
path: [*:0]const u8,
request: *const BunSpawnRequest,
argv: [*:null]?[*:0]const u8,
envp: [*:null]?[*:0]const u8,
argv: Argv,
envp: Argv,
) isize;
pub fn spawn(
path: [*:0]const u8,
req_: BunSpawnRequest,
argv: [*:null]?[*:0]const u8,
envp: [*:null]?[*:0]const u8,
argv: Argv,
envp: Argv,
) Maybe(pid_t) {
var req = req_;
var pid: c_int = 0;
@@ -331,8 +340,8 @@ pub const PosixSpawn = struct {
path: [*:0]const u8,
actions: ?Actions,
attr: ?Attr,
argv: [*:null]?[*:0]const u8,
envp: [*:null]?[*:0]const u8,
argv: Argv,
envp: Argv,
) Maybe(pid_t) {
if (comptime Environment.isLinux) {
return BunSpawnRequest.spawn(
@@ -440,4 +449,7 @@ pub const PosixSpawn = struct {
pub const Rusage = process.Rusage;
pub const Stdio = @import("spawn/stdio.zig").Stdio;
pub const allocateArguments = allocateSpawnArguments;
pub const Argv = NullTerminatedCStringSlice;
};

View File

@@ -13,6 +13,8 @@ const Api = @import("./api/schema.zig").Api;
const which = @import("./which.zig").which;
const s3 = bun.S3;
pub const NullDelimitedEnvMap = [:null]?[*:0]const u8;
const DotEnvFileSuffix = enum {
development,
production,
@@ -1146,13 +1148,11 @@ pub const Map = struct {
map: HashTable,
pub fn createNullDelimitedEnvMap(this: *Map, arena: std.mem.Allocator) ![:null]?[*:0]const u8 {
var env_map = &this.map;
const envp_count = env_map.count();
pub fn createNullDelimitedEnvMap(this: *const Map, arena: std.mem.Allocator) !NullDelimitedEnvMap {
const envp_count = this.map.count();
const envp_buf = try arena.allocSentinel(?[*:0]const u8, envp_count, null);
{
var it = env_map.iterator();
var it = this.map.iterator();
var i: usize = 0;
while (it.next()) |pair| : (i += 1) {
const env_buf = try arena.allocSentinel(u8, pair.key_ptr.len + pair.value_ptr.value.len + 1, 0);

513
src/install/GitRunner.zig Normal file
View File

@@ -0,0 +1,513 @@
const bun = @import("bun");
const std = @import("std");
const strings = @import("../string_immutable.zig");
const PackageManager = @import("./install.zig").PackageManager;
const DotEnv = @import("../env_loader.zig");
const Environment = @import("../env.zig");
const Process = bun.spawn.Process;
const Output = bun.Output;
const JSC = bun.JSC;
const ExtractData = @import("./install.zig").ExtractData;
const Path = bun.path;
threadlocal var folder_name_buf: bun.PathBuffer = undefined;
pub const GitRunner = struct {
process: ?*Process = null,
stdout: bun.io.BufferedReader = bun.io.BufferedReader.init(@This()),
stderr: bun.io.BufferedReader = bun.io.BufferedReader.init(@This()),
manager: *PackageManager,
remaining_fds: i8 = 0,
has_called_process_exit: bool = false,
completion_context: CompletionContext,
envp: ?DotEnv.NullDelimitedEnvMap = null,
arena: bun.ArenaAllocator,
/// Allocator for this runner
allocator: std.mem.Allocator,
pub const CompletionContext = union(enum) {
git_clone: struct {
name: []const u8,
url: []const u8,
task_id: u64,
attempt: u8,
dir: union(enum) {
/// Not yet created. Check it worked by opening the directory.
cache: std.fs.Dir,
/// Already downloaded. Exit code of 0 says it worked.
repo: std.fs.Dir,
},
},
git_find_commit: struct {
name: []const u8,
committish: []const u8,
task_id: u64,
repo_dir: std.fs.Dir,
},
git_checkout: struct {
name: []const u8,
url: []const u8,
resolved: []const u8,
task_id: u64,
cache_dir: std.fs.Dir,
repo_dir: std.fs.Dir,
},
pub fn needsStdout(this: *const CompletionContext) bool {
return switch (this.*) {
.git_find_commit => true,
else => false,
};
}
};
pub const Result = struct {
task_id: u64,
pending: bool = false,
err: ?anyerror = null,
// The original context is passed back with the result.
context: CompletionContext,
// The success payload. Only valid if err is null.
result: union(enum) {
git_clone: std.fs.Dir,
git_find_commit: []const u8,
git_checkout: ExtractData,
},
};
// Note: The `.Queue` definition needs to be updated to use this new struct.
pub const Queue = std.fifo.LinearFifo(Result, .Dynamic);
pub fn gitExecutable() [:0]const u8 {
const GitExecutableOnce = struct {
pub var once = std.once(get);
pub var executable: [:0]const u8 = "";
fn get() void {
// First clone without checkout
const gitpath = bun.PathBufferPool.get();
defer bun.PathBufferPool.put(gitpath);
const git = bun.which(gitpath, bun.getenvZ("PATH") orelse "", bun.fs.FileSystem.instance.top_level_dir, "git") orelse "git";
executable = bun.default_allocator.dupeZ(u8, git) catch bun.outOfMemory();
}
};
GitExecutableOnce.once.call();
return GitExecutableOnce.executable;
}
pub fn init(
allocator: std.mem.Allocator,
manager: *PackageManager,
context: CompletionContext,
) !*GitRunner {
const runner = try allocator.create(GitRunner);
runner.* = .{
.manager = manager,
.completion_context = context,
.arena = bun.ArenaAllocator.init(allocator),
.allocator = allocator,
};
runner.stdout.setParent(runner);
runner.stderr.setParent(runner);
return runner;
}
pub fn spawn(this: *GitRunner, argv_slice: []const []const u8, env: ?*const DotEnv.Map) !void {
if (this.manager.options.log_level.isVerbose()) {
Output.prettyError("<r><d>$ git", .{});
for (argv_slice[1..]) |arg| {
Output.prettyError(" {s}", .{arg});
}
Output.prettyErrorln("<r>\n", .{});
Output.flush();
}
const spawn_options = bun.spawn.SpawnOptions{
.stdin = .ignore,
.stdout = if (this.manager.options.log_level == .silent and !this.completion_context.needsStdout())
.ignore
else if (this.manager.options.log_level.isVerbose() and !this.completion_context.needsStdout())
.inherit
else if (Environment.isPosix)
.buffer
else
.{
.buffer = this.stdout.source.?.pipe,
},
.stderr = if (this.manager.options.log_level == .silent)
.ignore
else if (this.manager.options.log_level.isVerbose())
.inherit
else if (Environment.isPosix)
.buffer
else
.{
.buffer = this.stderr.source.?.pipe,
},
.windows = if (Environment.isWindows) .{
.loop = JSC.EventLoopHandle.init(&this.manager.event_loop),
},
.stream = false,
.cwd = this.manager.cache_directory_path,
};
const argv, _ = bun.spawn.allocateArguments(this.arena.allocator(), argv_slice) catch bun.outOfMemory();
const envp = if (env) |env_map| try env_map.createNullDelimitedEnvMap(this.arena.allocator()) else this.envp.?;
var spawned = try (try bun.spawn.spawnProcess(&spawn_options, argv, envp)).unwrap();
this.envp = envp;
this.remaining_fds = 0;
if (spawned.stdout) |stdout| {
this.stdout.setParent(this);
_ = bun.sys.setNonblocking(stdout);
this.remaining_fds += 1;
this.stdout.flags.nonblocking = true;
this.stdout.flags.socket = true;
try this.stdout.start(stdout, true).unwrap();
}
if (spawned.stderr) |stderr| {
this.stderr.setParent(this);
_ = bun.sys.setNonblocking(stderr);
this.remaining_fds += 1;
this.stderr.flags.nonblocking = true;
this.stderr.flags.socket = true;
try this.stderr.start(stderr, true).unwrap();
}
const event_loop = &this.manager.event_loop;
var process = spawned.toProcess(event_loop, false);
this.process = process;
process.setExitHandler(this);
switch (process.watchOrReap()) {
.err => |err| {
if (!process.hasExited())
process.onExit(.{ .err = err }, &std.mem.zeroes(bun.spawn.Rusage));
},
.result => {},
}
}
pub fn eventLoop(this: *const GitRunner) *JSC.AnyEventLoop {
return &this.manager.event_loop;
}
pub fn loop(this: *const GitRunner) *bun.uws.Loop {
return this.manager.event_loop.loop();
}
pub fn onReaderDone(this: *GitRunner) void {
bun.assert(this.remaining_fds > 0);
this.remaining_fds -= 1;
this.maybeFinished();
}
pub fn onReaderError(this: *GitRunner, err: bun.sys.Error) void {
bun.assert(this.remaining_fds > 0);
this.remaining_fds -= 1;
Output.prettyErrorln("<r><red>error<r>: Failed to read git output due to error <b>{d} {s}<r>", .{
err.errno,
@tagName(err.getErrno()),
});
Output.flush();
this.maybeFinished();
}
pub fn maybeFinished(this: *GitRunner) void {
if (!this.has_called_process_exit or this.remaining_fds != 0)
return;
const process = this.process orelse return;
this.handleExit(process.status);
}
pub fn onProcessExit(this: *GitRunner, proc: *Process, _: bun.spawn.Status, _: *const bun.spawn.Rusage) void {
if (this.process != proc) {
Output.debugWarn("<d>[GitRunner]<r> onProcessExit called with wrong process", .{});
return;
}
this.has_called_process_exit = true;
this.maybeFinished();
}
pub fn handleExit(this: *GitRunner, status: bun.spawn.Status) void {
defer this.deinit();
switch (status) {
.exited => |exit| {
if (exit.code == 0) {
// Success case
const stdout_data = this.stdout.finalBuffer();
switch (this.completion_context) {
.git_clone => |*ctx| {
switch (ctx.dir) {
.cache => |cache_dir| {
const buf = bun.PathBufferPool.get();
defer bun.PathBufferPool.put(buf);
const path = Path.joinAbsStringBufZ(PackageManager.get().cache_directory_path, buf, &.{PackageManager.cachedGitFolderNamePrint(&folder_name_buf, ctx.name, null)}, .auto);
if (bun.openDir(cache_dir, path)) |repo_dir| {
this.manager.git_tasks.writeItem(.{
.task_id = ctx.task_id,
.context = .{ .git_clone = ctx.* },
.result = .{ .git_clone = repo_dir },
.pending = true,
}) catch {};
} else |err| {
this.manager.git_tasks.writeItem(.{
.task_id = ctx.task_id,
.context = .{ .git_clone = ctx.* },
.err = err,
.result = undefined,
.pending = true,
}) catch {};
}
},
.repo => |repo_dir| {
this.manager.git_tasks.writeItem(.{
.task_id = ctx.task_id,
.context = .{ .git_clone = ctx.* },
.result = .{ .git_clone = repo_dir },
.pending = true,
}) catch {};
},
}
},
.git_find_commit => |*ctx| {
// For find_commit, we need to parse the commit hash from stdout
const commit_hash = std.mem.trim(u8, stdout_data.items, " \t\r\n");
if (commit_hash.len > 0) {
const duped = this.allocator.dupe(u8, commit_hash) catch bun.outOfMemory();
this.manager.git_tasks.writeItem(.{
.task_id = ctx.task_id,
.context = .{ .git_find_commit = ctx.* },
.result = .{ .git_find_commit = duped },
.pending = true,
}) catch {};
} else {
this.manager.git_tasks.writeItem(.{
.task_id = ctx.task_id,
.context = .{ .git_find_commit = ctx.* },
.err = error.InstallFailed,
.result = undefined,
.pending = true,
}) catch {};
}
},
.git_checkout => |*ctx| {
// Checkout completed, clean up and read package.json
const folder_name = PackageManager.cachedGitFolderNamePrint(&folder_name_buf, ctx.resolved, null);
if (bun.openDir(ctx.cache_dir, folder_name)) |dir_const| {
var dir = dir_const;
defer dir.close();
dir.deleteTree(".git") catch {};
if (ctx.resolved.len > 0) {
switch (bun.sys.File.writeFile(bun.FD.fromStdDir(dir), ".bun-tag", ctx.resolved)) {
.err => {
_ = bun.sys.unlinkat(.fromStdDir(dir), ".bun-tag");
},
.result => {},
}
}
const extract_data = this.readPackageJson(dir, ctx.url, ctx.resolved) catch |err| {
this.manager.git_tasks.writeItem(.{
.task_id = ctx.task_id,
.context = .{ .git_checkout = ctx.* },
.err = err,
.result = undefined,
.pending = true,
}) catch {};
return;
};
this.manager.git_tasks.writeItem(.{
.task_id = ctx.task_id,
.context = .{ .git_checkout = ctx.* },
.result = .{ .git_checkout = extract_data },
.pending = true,
}) catch {};
} else |err| {
this.manager.git_tasks.writeItem(.{
.task_id = ctx.task_id,
.context = .{ .git_checkout = ctx.* },
.err = err,
.result = undefined,
.pending = true,
}) catch {};
}
},
}
} else {
// Error case - check stderr for specific errors
const stderr_data = this.stderr.finalBuffer();
if (this.manager.options.log_level.isVerbose() and stderr_data.items.len > 0) {
Output.printErrorln("<r>{s}<r>\n", .{stderr_data.items});
}
const err = if ((strings.containsComptime(stderr_data.items, "remote:") and
strings.containsComptime(stderr_data.items, "not") and
strings.containsComptime(stderr_data.items, "found")) or
strings.containsComptime(stderr_data.items, "does not exist"))
error.RepositoryNotFound
else
error.InstallFailed;
switch (this.completion_context) {
.git_clone => |ctx| {
this.manager.git_tasks.writeItem(.{
.task_id = ctx.task_id,
.context = .{ .git_clone = ctx },
.err = err,
.result = undefined,
.pending = true,
}) catch {};
},
.git_find_commit => |ctx| {
this.manager.git_tasks.writeItem(.{
.task_id = ctx.task_id,
.context = .{ .git_find_commit = ctx },
.err = err,
.result = undefined,
.pending = true,
}) catch {};
},
.git_checkout => |ctx| {
this.manager.git_tasks.writeItem(.{
.task_id = ctx.task_id,
.context = .{ .git_checkout = ctx },
.err = err,
.result = undefined,
.pending = true,
}) catch {};
},
}
}
},
.err => |_| {
switch (this.completion_context) {
.git_clone => |ctx| {
this.manager.git_tasks.writeItem(.{
.task_id = ctx.task_id,
.context = .{ .git_clone = ctx },
.err = error.InstallFailed,
.result = undefined,
.pending = true,
}) catch {};
},
.git_checkout => |ctx| {
this.manager.git_tasks.writeItem(.{
.task_id = ctx.task_id,
.context = .{ .git_checkout = ctx },
.err = error.InstallFailed,
.result = undefined,
.pending = true,
}) catch {};
},
.git_find_commit => |ctx| {
this.manager.git_tasks.writeItem(.{
.task_id = ctx.task_id,
.context = .{ .git_find_commit = ctx },
.err = error.InstallFailed,
.result = undefined,
.pending = true,
}) catch {};
},
}
},
else => {
switch (this.completion_context) {
.git_clone => |ctx| {
this.manager.git_tasks.writeItem(.{
.task_id = ctx.task_id,
.context = .{ .git_clone = ctx },
.err = error.InstallFailed,
.result = undefined,
.pending = true,
}) catch {};
},
.git_find_commit => |ctx| {
this.manager.git_tasks.writeItem(.{
.task_id = ctx.task_id,
.context = .{ .git_find_commit = ctx },
.err = error.InstallFailed,
.result = undefined,
.pending = true,
}) catch {};
},
.git_checkout => |ctx| {
this.manager.git_tasks.writeItem(.{
.task_id = ctx.task_id,
.context = .{ .git_checkout = ctx },
.err = error.InstallFailed,
.result = undefined,
.pending = true,
}) catch {};
},
}
},
}
}
fn readPackageJson(this: *GitRunner, package_dir: std.fs.Dir, url: []const u8, resolved: []const u8) !ExtractData {
var json_path_buf: bun.PathBuffer = undefined;
const json_file, const json_buf = bun.sys.File.readFileFrom(package_dir, "package.json", this.allocator).unwrap() catch |err| {
if (err == error.ENOENT) {
// allow git dependencies without package.json
return .{
.url = url,
.resolved = resolved,
};
}
return error.InstallFailed;
};
defer json_file.close();
const json_path = json_file.getPath(&json_path_buf).unwrap() catch {
return error.InstallFailed;
};
const ret_json_path = try @import("../fs.zig").FileSystem.instance.dirname_store.append(@TypeOf(json_path), json_path);
return .{
.url = url,
.resolved = resolved,
.json = .{
.path = ret_json_path,
.buf = json_buf,
},
};
}
pub fn deinit(this: *GitRunner) void {
if (this.process) |proc| {
this.process = null;
proc.close();
proc.deref();
}
this.stdout.deinit();
this.stderr.deinit();
this.arena.deinit();
this.allocator.destroy(this);
}
pub const ScheduleResult = enum {
scheduled,
completed,
};
};

View File

@@ -253,8 +253,6 @@ pub const NetworkTask = struct {
name: strings.StringOrTinyString,
},
extract: ExtractTarball,
git_clone: void,
git_checkout: void,
local_tarball: void,
},
/// Key in patchedDependencies in package.json
@@ -774,81 +772,7 @@ pub const Task = struct {
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;
}
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.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;
},
// git_clone and git_checkout are now handled asynchronously via GitRunner
.local_tarball => {
const workspace_pkg_id = manager.lockfile.getWorkspacePkgIfWorkspaceDep(this.request.local_tarball.tarball.dependency_id);
@@ -916,9 +840,7 @@ pub const Task = struct {
pub const Tag = enum(u3) {
package_manifest = 0,
extract = 1,
git_clone = 2,
git_checkout = 3,
local_tarball = 4,
local_tarball = 2,
};
pub const Status = enum {
@@ -930,8 +852,6 @@ pub const Task = struct {
pub const Data = union {
package_manifest: Npm.PackageManifest,
extract: ExtractData,
git_clone: bun.FileDescriptor,
git_checkout: ExtractData,
};
pub const Request = union {
@@ -945,22 +865,6 @@ pub const Task = 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,
},
@@ -1129,6 +1033,7 @@ pub const PackageManager = struct {
progress_name_buf: [768]u8 = undefined,
progress_name_buf_dynamic: []u8 = &[_]u8{},
cpu_count: u32 = 0,
git_tasks: GitRunner.Queue = .init(bun.default_allocator),
track_installed_bin: TrackInstalledBin = .{
.none = {},
@@ -1166,6 +1071,8 @@ pub const PackageManager = struct {
manifests: PackageManifestMap = .{},
folders: FolderResolution.Map = .{},
git_repositories: RepositoryMap = .{},
git_find_commits_dedupe_map: std.AutoHashMapUnmanaged(u64, void) = .{},
git_checkout_dedupe_map: std.AutoHashMapUnmanaged(u64, void) = .{},
network_dedupe_map: NetworkTask.DedupeMap = NetworkTask.DedupeMap.init(bun.default_allocator),
async_network_task_queue: AsyncNetworkTaskQueue = .{},
@@ -1176,6 +1083,7 @@ pub const PackageManager = struct {
patch_calc_hash_batch: ThreadPool.Batch = .{},
patch_task_fifo: PatchTaskFifo = PatchTaskFifo.init(),
patch_task_queue: PatchTaskQueue = .{},
/// We actually need to calculate the patch file hashes
/// every single time, because someone could edit the patchfile at anytime
pending_pre_calc_hashes: std.atomic.Value(u32) = std.atomic.Value(u32).init(0),
@@ -3417,111 +3325,7 @@ pub const PackageManager = struct {
return &task.threadpool_task;
}
fn enqueueGitClone(
this: *PackageManager,
task_id: u64,
name: string,
repository: *const Repository,
dep_id: DependencyID,
dependency: *const Dependency,
res: *const Resolution,
/// if patched then we need to do apply step after network task is done
patch_name_and_version_hash: ?u64,
) *ThreadPool.Task {
var task = this.preallocated_resolve_tasks.get();
task.* = Task{
.package_manager = this,
.log = logger.Log.init(this.allocator),
.tag = Task.Tag.git_clone,
.request = .{
.git_clone = .{
.name = strings.StringOrTinyString.initAppendIfNeeded(
name,
*FileSystem.FilenameStore,
FileSystem.FilenameStore.instance,
) catch unreachable,
.url = strings.StringOrTinyString.initAppendIfNeeded(
this.lockfile.str(&repository.repo),
*FileSystem.FilenameStore,
FileSystem.FilenameStore.instance,
) catch unreachable,
.env = Repository.shared_env.get(this.allocator, this.env),
.dep_id = dep_id,
.res = res.*,
},
},
.id = task_id,
.apply_patch_task = if (patch_name_and_version_hash) |h| brk: {
const dep = dependency;
const pkg_id = switch (this.lockfile.package_index.get(dep.name_hash) orelse @panic("Package not found")) {
.id => |p| p,
.ids => |ps| ps.items[0], // TODO is this correct
};
const patch_hash = this.lockfile.patched_dependencies.get(h).?.patchfileHash().?;
const pt = PatchTask.newApplyPatchHash(this, pkg_id, patch_hash, h);
pt.callback.apply.task_id = task_id;
break :brk pt;
} else null,
.data = undefined,
};
return &task.threadpool_task;
}
fn enqueueGitCheckout(
this: *PackageManager,
task_id: u64,
dir: bun.FileDescriptor,
dependency_id: DependencyID,
name: string,
resolution: Resolution,
resolved: string,
/// if patched then we need to do apply step after network task is done
patch_name_and_version_hash: ?u64,
) *ThreadPool.Task {
var task = this.preallocated_resolve_tasks.get();
task.* = Task{
.package_manager = this,
.log = logger.Log.init(this.allocator),
.tag = Task.Tag.git_checkout,
.request = .{
.git_checkout = .{
.repo_dir = dir,
.resolution = resolution,
.dependency_id = dependency_id,
.name = strings.StringOrTinyString.initAppendIfNeeded(
name,
*FileSystem.FilenameStore,
FileSystem.FilenameStore.instance,
) catch unreachable,
.url = strings.StringOrTinyString.initAppendIfNeeded(
this.lockfile.str(&resolution.value.git.repo),
*FileSystem.FilenameStore,
FileSystem.FilenameStore.instance,
) catch unreachable,
.resolved = strings.StringOrTinyString.initAppendIfNeeded(
resolved,
*FileSystem.FilenameStore,
FileSystem.FilenameStore.instance,
) catch unreachable,
.env = Repository.shared_env.get(this.allocator, this.env),
},
},
.apply_patch_task = if (patch_name_and_version_hash) |h| brk: {
const dep = this.lockfile.buffers.dependencies.items[dependency_id];
const pkg_id = switch (this.lockfile.package_index.get(dep.name_hash) orelse @panic("Package not found")) {
.id => |p| p,
.ids => |ps| ps.items[0], // TODO is this correct
};
const patch_hash = this.lockfile.patched_dependencies.get(h).?.patchfileHash().?;
const pt = PatchTask.newApplyPatchHash(this, pkg_id, patch_hash, h);
pt.callback.apply.task_id = task_id;
break :brk pt;
} else null,
.id = task_id,
.data = undefined,
};
return &task.threadpool_task;
}
// git clone and checkout are now handled asynchronously via GitRunner
fn enqueueLocalTarball(
this: *PackageManager,
@@ -4011,22 +3815,10 @@ pub const PackageManager = struct {
);
if (this.git_repositories.get(clone_id)) |repo_fd| {
const resolved = try Repository.findCommit(
this.allocator,
this.env,
this.log,
repo_fd.stdDir(),
alias,
this.lockfile.str(&dep.committish),
clone_id,
);
const checkout_id = Task.Id.forGitCheckout(url, resolved);
var entry = this.task_queue.getOrPutContext(this.allocator, checkout_id, .{}) catch unreachable;
// Repository already cloned, just need to find commit
var entry = this.task_queue.getOrPut(this.allocator, clone_id) catch unreachable;
if (!entry.found_existing) entry.value_ptr.* = .{};
if (this.lockfile.buffers.resolutions.items[id] == invalid_package_id) {
try entry.value_ptr.append(this.allocator, ctx);
}
try entry.value_ptr.append(this.allocator, ctx);
if (dependency.behavior.isPeer()) {
if (!install_peer) {
@@ -4035,17 +3827,24 @@ pub const PackageManager = struct {
}
}
if (this.hasCreatedNetworkTask(checkout_id, dependency.behavior.isRequired())) return;
this.task_batch.push(ThreadPool.Batch.from(this.enqueueGitCheckout(
checkout_id,
repo_fd,
id,
alias,
res,
resolved,
null,
)));
// Start async findCommit operation only if this is the first request
if (!entry.found_existing) {
switch (try Repository.findCommit(
this,
this.allocator,
this.env.map,
this.log,
repo_fd.stdDir(),
alias,
this.lockfile.str(&dep.committish),
clone_id,
)) {
.scheduled => {
_ = this.incrementPendingTasks(1);
},
.completed => {},
}
}
} else {
var entry = this.task_queue.getOrPutContext(this.allocator, clone_id, .{}) catch unreachable;
if (!entry.found_existing) entry.value_ptr.* = .{};
@@ -4058,9 +3857,25 @@ pub const PackageManager = struct {
}
}
if (this.hasCreatedNetworkTask(clone_id, dependency.behavior.isRequired())) return;
this.task_batch.push(ThreadPool.Batch.from(this.enqueueGitClone(clone_id, alias, dep, id, dependency, &res, null)));
// Start async clone operation only if this is the first request
if (!entry.found_existing) {
switch (try Repository.downloadAndPickURL(
this,
this.allocator,
Repository.shared_env.get(this.allocator, this.env),
this.log,
this.getCacheDirectory(),
clone_id,
alias,
url,
0,
)) {
.scheduled => {
_ = this.incrementPendingTasks(1);
},
.completed => {},
}
}
}
},
.github => {
@@ -5461,141 +5276,271 @@ pub const PackageManager = struct {
}
}
},
.git_clone => {
const clone = &task.request.git_clone;
const repo_fd = task.data.git_clone;
const name = clone.name.slice();
const url = clone.url.slice();
// git_clone and git_checkout are now handled asynchronously via GitRunner
}
}
manager.git_repositories.put(manager.allocator, task.id, repo_fd) catch unreachable;
while (manager.git_tasks.readItem()) |result| {
if (result.pending) {
_ = manager.decrementPendingTasks();
}
if (task.status == .fail) {
const err = task.err orelse error.Failed;
if (result.err) |err| {
switch (result.context) {
.git_clone => |*ctx| {
const task_callbacks = manager.task_queue.get(result.task_id) orelse continue;
if (ctx.attempt == 0) {
if (Repository.tryHTTPS(ctx.url) != null and Repository.trySSH(ctx.url) != null) {
switch (try Repository.downloadAndPickURL(
manager,
manager.allocator,
Repository.shared_env.get(manager.allocator, manager.env),
manager.log,
manager.getCacheDirectory(),
result.task_id,
ctx.name,
ctx.url,
1,
)) {
.scheduled => {
_ = manager.incrementPendingTasks(1);
},
.completed => {},
}
continue;
}
}
if (@TypeOf(callbacks.onPackageManifestError) != void) {
callbacks.onPackageManifestError(
extract_ctx,
name,
err,
url,
);
for (task_callbacks.items) |callback| {
switch (callback) {
.root_dependency, .dependency => |dep_id| {
const dependency = &manager.lockfile.buffers.dependencies.items[dep_id];
const name = manager.lockfile.str(&dependency.name);
callbacks.onPackageManifestError(
extract_ctx,
name,
err,
ctx.url,
);
},
else => {},
}
}
} else if (log_level != .silent) {
for (task_callbacks.items) |callback| {
switch (callback) {
.root_dependency, .dependency => |dep_id| {
const dependency = &manager.lockfile.buffers.dependencies.items[dep_id];
const name = manager.lockfile.str(&dependency.name);
manager.log.addErrorFmt(
null,
logger.Loc.Empty,
manager.allocator,
"{s} cloning repository for <b>{s}<r>",
.{
@errorName(err),
name,
},
) catch bun.outOfMemory();
},
else => {},
}
}
}
},
.git_find_commit => |ctx| {
const task_callbacks = manager.task_queue.get(result.task_id) orelse continue;
if (@TypeOf(callbacks.onPackageManifestError) != void) {
for (task_callbacks.items) |callback| {
switch (callback) {
.root_dependency, .dependency => |dep_id| {
const dependency = &manager.lockfile.buffers.dependencies.items[dep_id];
const name = manager.lockfile.str(&dependency.name);
const url = manager.lockfile.str(&dependency.version.value.git.repo);
callbacks.onPackageManifestError(
extract_ctx,
name,
err,
url,
);
},
else => {},
}
}
} else {
if (log_level != .silent) {
manager.log.addErrorFmt(
null,
logger.Loc.Empty,
manager.allocator,
"{s} finding commit for <b>{s}<r>",
.{
@errorName(err),
ctx.name,
},
) catch bun.outOfMemory();
}
}
},
.git_checkout => |ctx| {
const task_callbacks = manager.task_queue.get(result.task_id) orelse continue;
if (@TypeOf(callbacks.onPackageManifestError) != void) {
for (task_callbacks.items) |callback| {
switch (callback) {
.root_dependency, .dependency => |dep_id| {
const dependency = &manager.lockfile.buffers.dependencies.items[dep_id];
const name = manager.lockfile.str(&dependency.name);
callbacks.onPackageManifestError(
extract_ctx,
name,
err,
ctx.url,
);
},
else => {},
}
}
} else if (log_level != .silent) {
manager.log.addErrorFmt(
null,
logger.Loc.Empty,
manager.allocator,
"{s} cloning repository for <b>{s}<r>",
"{s} checking out repository for <b>{s}<r>",
.{
@errorName(err),
name,
ctx.name,
},
) catch bun.outOfMemory();
}
continue;
}
// Note: We don't close repo_dir here because it's owned by git_repositories
// and might be reused for other operations
},
}
} else {
if (comptime @TypeOf(callbacks.onExtract) != void and ExtractCompletionContext == *PackageInstaller) {
// Installing!
// this dependency might be something other than a git dependency! only need the name and
// behavior, use the resolution from the task.
const dep_id = clone.dep_id;
const dep = manager.lockfile.buffers.dependencies.items[dep_id];
const dep_name = dep.name.slice(manager.lockfile.buffers.string_bytes.items);
// No error, process based on context
switch (result.context) {
.git_clone => |*git_clone| {
const task_callbacks = manager.task_queue.get(result.task_id) orelse continue;
const git = clone.res.value.git;
const committish = git.committish.slice(manager.lockfile.buffers.string_bytes.items);
const repo = git.repo.slice(manager.lockfile.buffers.string_bytes.items);
const resolved = try Repository.findCommit(
manager.allocator,
manager.env,
manager.log,
task.data.git_clone.stdDir(),
dep_name,
committish,
task.id,
);
const checkout_id = Task.Id.forGitCheckout(repo, resolved);
if (manager.hasCreatedNetworkTask(checkout_id, dep.behavior.isRequired())) continue;
manager.task_batch.push(ThreadPool.Batch.from(manager.enqueueGitCheckout(
checkout_id,
repo_fd,
dep_id,
dep_name,
clone.res,
resolved,
null,
)));
} else {
// Resolving!
const dependency_list_entry = manager.task_queue.getEntry(task.id).?;
const dependency_list = dependency_list_entry.value_ptr.*;
dependency_list_entry.value_ptr.* = .{};
try manager.processDependencyList(dependency_list, ExtractCompletionContext, extract_ctx, callbacks, install_peer);
}
if (log_level.showProgress()) {
if (!has_updated_this_run) {
manager.setNodeName(manager.downloads_node.?, name, ProgressStrings.download_emoji, true);
has_updated_this_run = true;
if (log_level.showProgress()) {
if (!has_updated_this_run) {
const name = if (task_callbacks.items.len > 0) blk: {
switch (task_callbacks.items[0]) {
.dependency => |dep_id| {
const dependency = &manager.lockfile.buffers.dependencies.items[dep_id];
break :blk manager.lockfile.str(&dependency.name);
},
else => break :blk "repository",
}
} else "repository";
manager.setNodeName(manager.downloads_node.?, name, ProgressStrings.download_emoji, true);
has_updated_this_run = true;
}
}
}
},
.git_checkout => {
const git_checkout = &task.request.git_checkout;
const alias = &git_checkout.name;
const resolution = &git_checkout.resolution;
var package_id: PackageID = invalid_package_id;
if (task.status == .fail) {
const err = task.err orelse error.Failed;
// Store the repository directory for later use
// Use the same key that we'll use to look it up later (based on URL)
const clone_id = Task.Id.forGitClone(git_clone.url);
const repo_dir = result.result.git_clone;
manager.git_repositories.put(manager.allocator, clone_id, .fromStdDir(repo_dir)) catch unreachable;
manager.log.addErrorFmt(
null,
logger.Loc.Empty,
manager.allocator,
"{s} checking out repository for <b>{s}<r>",
.{
@errorName(err),
alias.slice(),
},
) catch bun.outOfMemory();
// For git dependencies, we always need to continue through the full workflow
for (task_callbacks.items) |callback| {
switch (callback) {
.root_dependency, .dependency => |dep_id| {
const dependency = &manager.lockfile.buffers.dependencies.items[dep_id];
const repository = &dependency.version.value.git;
const committish = manager.lockfile.str(&repository.committish);
continue;
}
var hasher = std.hash.Wyhash.init(0);
hasher.update(git_clone.url);
hasher.update(committish);
const hash = hasher.final();
const entry = manager.git_find_commits_dedupe_map.getOrPut(manager.allocator, hash) catch unreachable;
if (!entry.found_existing) {
switch (try Repository.findCommit(
manager,
manager.allocator,
manager.env.map,
manager.log,
repo_dir,
manager.lockfile.str(&dependency.name),
committish,
switch (result.context.git_clone.dir) {
.cache => result.task_id,
.repo => result.context.git_clone.task_id,
},
)) {
.scheduled => {
_ = manager.incrementPendingTasks(1);
},
.completed => {},
}
}
},
else => @panic("Unexpected callback type"),
}
}
},
.git_find_commit => {
// Find commit succeeded. The resolved hash is in the result.
const task_callbacks = manager.task_queue.get(result.task_id) orelse continue;
if (comptime @TypeOf(callbacks.onExtract) != void and ExtractCompletionContext == *PackageInstaller) {
// We've populated the cache, package already exists in memory. Call the package installer callback
// and don't enqueue dependencies
for (task_callbacks.items) |callback| {
switch (callback) {
.root_dependency, .dependency => |dep_id| {
const dependency = &manager.lockfile.buffers.dependencies.items[dep_id];
const repository = &dependency.version.value.git;
const url = manager.lockfile.str(&repository.repo);
const resolved_hash = result.result.git_find_commit;
const checkout_id = Task.Id.forGitCheckout(url, resolved_hash);
// TODO(dylan-conway) most likely don't need to call this now that the package isn't appended, but
// keeping just in case for now
extract_ctx.fixCachedLockfilePackageSlices();
// Enqueue for checkout
var checkout_queue = manager.task_queue.getOrPut(manager.allocator, checkout_id) catch unreachable;
if (!checkout_queue.found_existing) {
checkout_queue.value_ptr.* = .{};
}
try checkout_queue.value_ptr.append(manager.allocator, callback);
callbacks.onExtract(
extract_ctx,
git_checkout.dependency_id,
&task.data.git_checkout,
log_level,
);
} else if (manager.processExtractedTarballPackage(
&package_id,
git_checkout.dependency_id,
resolution,
&task.data.git_checkout,
log_level,
)) |pkg| handle_pkg: {
if (!checkout_queue.found_existing) {
switch (try Repository.checkout(
manager,
manager.allocator,
Repository.shared_env.get(manager.allocator, manager.env),
manager.log,
manager.getCacheDirectory(),
result.context.git_find_commit.repo_dir,
manager.lockfile.str(&dependency.name),
url,
resolved_hash,
checkout_id,
)) {
.scheduled => {
_ = manager.incrementPendingTasks(1);
},
.completed => {},
}
}
},
else => @panic("Unexpected callback type"),
}
}
},
.git_checkout => |*git_checkout| {
// Note: We don't close repo_dir here because it's owned by git_repositories
// and might be reused for other operations
// Checkout succeeded. Package is ready for dependency installation.
const callbacks_from_queue = manager.task_queue.get(result.task_id) orelse continue;
var any_root = false;
var dependency_list_entry = manager.task_queue.getEntry(task.id) orelse break :handle_pkg;
var dependency_list = dependency_list_entry.value_ptr.*;
dependency_list_entry.value_ptr.* = .{};
defer {
dependency_list.deinit(manager.allocator);
if (comptime @TypeOf(callbacks) != void and @TypeOf(callbacks.onResolve) != void) {
if (any_root) {
callbacks.onResolve(extract_ctx);
@@ -5603,38 +5548,82 @@ pub const PackageManager = struct {
}
}
for (dependency_list.items) |dep| {
switch (dep) {
.dependency, .root_dependency => |id| {
var repo = &manager.lockfile.buffers.dependencies.items[id].version.value.git;
repo.resolved = pkg.resolution.value.git.resolved;
repo.package_name = pkg.name;
try manager.processDependencyListItem(dep, &any_root, install_peer);
},
else => {
// if it's a node_module folder to install, handle that after we process all the dependencies within the onExtract callback.
dependency_list_entry.value_ptr.append(manager.allocator, dep) catch unreachable;
// Process each callback
for (callbacks_from_queue.items) |callback| {
switch (callback) {
.dependency, .root_dependency => |dependency_id| {
if (callback == .root_dependency) any_root = true;
// Get the existing dependency from lockfile to create resolution
const dependency = &manager.lockfile.buffers.dependencies.items[dependency_id];
// Build a string for the resolved hash
var string_builder = manager.lockfile.stringBuilder();
string_builder.count(git_checkout.resolved);
try string_builder.allocate();
const resolved_string = string_builder.append(@TypeOf(dependency.version.value.git.resolved), git_checkout.resolved);
const resolution = Resolution{
.tag = .git,
.value = .{
.git = .{
.owner = dependency.version.value.git.owner,
.repo = dependency.version.value.git.repo,
.committish = dependency.version.value.git.committish,
.resolved = resolved_string,
.package_name = dependency.name,
},
},
};
if (comptime @TypeOf(callbacks.onExtract) != void and ExtractCompletionContext == *PackageInstaller) {
// We've populated the cache, package already exists in memory. Call the package installer callback
// and don't enqueue dependencies
extract_ctx.fixCachedLockfilePackageSlices();
callbacks.onExtract(
extract_ctx,
dependency_id,
&result.result.git_checkout,
log_level,
);
} else {
var package_id: PackageID = invalid_package_id;
if (manager.processExtractedTarballPackage(
&package_id,
dependency_id,
&resolution,
&result.result.git_checkout,
log_level,
)) |pkg| {
// Update the git resolution with the resolved commit
var repo = &manager.lockfile.buffers.dependencies.items[dependency_id].version.value.git;
// Update resolved string
var resolved_builder = manager.lockfile.stringBuilder();
resolved_builder.count(git_checkout.resolved);
try resolved_builder.allocate();
repo.resolved = resolved_builder.append(@TypeOf(repo.resolved), git_checkout.resolved);
repo.package_name = pkg.name;
// Process the dependency
try manager.processDependencyListItem(callback, &any_root, install_peer);
if (comptime @TypeOf(callbacks.onExtract) != void) {
callbacks.onExtract(
extract_ctx,
dependency_id,
&result.result.git_checkout,
log_level,
);
}
}
}
},
.root_request_id, .dependency_install_context => {},
}
}
if (comptime @TypeOf(callbacks.onExtract) != void) {
callbacks.onExtract(
extract_ctx,
git_checkout.dependency_id,
&task.data.git_checkout,
log_level,
);
}
}
if (log_level.showProgress()) {
if (!has_updated_this_run) {
manager.setNodeName(manager.downloads_node.?, alias.slice(), ProgressStrings.download_emoji, true);
has_updated_this_run = true;
}
}
},
},
}
}
}
}
@@ -10036,7 +10025,7 @@ pub const PackageManager = struct {
alias: string,
resolution: *const Resolution,
task_context: TaskCallbackContext,
patch_name_and_version_hash: ?u64,
_: ?u64,
) void {
const repository = &resolution.value.git;
const url = this.lockfile.str(&repository.repo);
@@ -10056,7 +10045,43 @@ pub const PackageManager = struct {
if (checkout_queue.found_existing) return;
if (this.git_repositories.get(clone_id)) |repo_fd| {
this.task_batch.push(ThreadPool.Batch.from(this.enqueueGitCheckout(checkout_id, repo_fd, dependency_id, alias, resolution.*, resolved, patch_name_and_version_hash)));
const result = Repository.checkout(
this,
this.allocator,
Repository.shared_env.get(this.allocator, this.env),
this.log,
this.getCacheDirectory(),
std.fs.Dir{ .fd = repo_fd.cast() },
alias,
url,
resolved,
checkout_id,
) catch |err| brk: {
// Write a proper error result with context
this.git_tasks.writeItem(.{
.task_id = checkout_id,
.err = err,
.context = .{
.git_checkout = .{
.name = alias,
.url = url,
.resolved = resolved,
.task_id = checkout_id,
.cache_dir = this.getCacheDirectory(),
.repo_dir = std.fs.Dir{ .fd = repo_fd.cast() },
},
},
.result = undefined,
}) catch {};
break :brk .completed;
};
switch (result) {
.scheduled => {
_ = this.incrementPendingTasks(1);
},
.completed => {},
}
} else {
var clone_queue = this.task_queue.getOrPut(this.allocator, clone_id) catch unreachable;
if (!clone_queue.found_existing) {
@@ -10070,15 +10095,40 @@ pub const PackageManager = struct {
if (clone_queue.found_existing) return;
this.task_batch.push(ThreadPool.Batch.from(this.enqueueGitClone(
const result = Repository.downloadAndPickURL(
this,
this.allocator,
Repository.shared_env.get(this.allocator, this.env),
this.log,
this.getCacheDirectory(),
clone_id,
alias,
repository,
dependency_id,
&this.lockfile.buffers.dependencies.items[dependency_id],
resolution,
null,
)));
url,
0,
) catch |err| brk: {
this.git_tasks.writeItem(.{
.task_id = clone_id,
.err = err,
.context = .{
.git_clone = .{
.name = alias,
.url = url,
.task_id = clone_id,
.attempt = 0,
.dir = .{ .cache = this.getCacheDirectory() },
},
},
.result = .{ .git_clone = bun.invalid_fd.stdDir() },
}) catch {};
break :brk .completed;
};
switch (result) {
.scheduled => {
_ = this.incrementPendingTasks(1);
},
.completed => {},
}
}
}
@@ -10196,3 +10246,4 @@ pub const PackageManifestError = error{
};
pub const LifecycleScriptSubprocess = @import("./lifecycle_script_runner.zig").LifecycleScriptSubprocess;
pub const GitRunner = @import("./GitRunner.zig").GitRunner;

View File

@@ -3,9 +3,7 @@ const logger = bun.logger;
const Dependency = @import("./dependency.zig");
const DotEnv = @import("../env_loader.zig");
const Environment = @import("../env.zig");
const FileSystem = @import("../fs.zig").FileSystem;
const Install = @import("./install.zig");
const ExtractData = Install.ExtractData;
const PackageManager = Install.PackageManager;
const Semver = bun.Semver;
const String = Semver.String;
@@ -16,6 +14,7 @@ const GitSHA = String;
const Path = bun.path;
const File = bun.sys.File;
const OOM = bun.OOM;
const GitRunner = @import("./GitRunner.zig").GitRunner;
threadlocal var final_path_buf: bun.PathBuffer = undefined;
threadlocal var ssh_path_buf: bun.PathBuffer = undefined;
@@ -138,33 +137,35 @@ pub const Repository = extern struct {
pub var shared_env: struct {
env: ?DotEnv.Map = null,
pub fn get(this: *@This(), allocator: std.mem.Allocator, other: *DotEnv.Loader) DotEnv.Map {
return this.env orelse brk: {
// Note: currently if the user sets this to some value that causes
// a prompt for a password, the stdout of the prompt will be masked
// by further output of the rest of the install process.
// A value can still be entered, but we need to find a workaround
// so the user can see what is being prompted. By default the settings
// below will cause no prompt and throw instead.
var cloned = other.map.cloneWithAllocator(allocator) catch bun.outOfMemory();
pub fn get(this: *@This(), allocator: std.mem.Allocator, other: *const DotEnv.Loader) *const DotEnv.Map {
if (this.env) |*env| {
return env;
}
if (cloned.get("GIT_ASKPASS") == null) {
const config = SloppyGlobalGitConfig.get();
if (!config.has_askpass) {
cloned.put("GIT_ASKPASS", "echo") catch bun.outOfMemory();
}
// Note: currently if the user sets this to some value that causes
// a prompt for a password, the stdout of the prompt will be masked
// by further output of the rest of the install process.
// A value can still be entered, but we need to find a workaround
// so the user can see what is being prompted. By default the settings
// below will cause no prompt and throw instead.
var cloned = other.map.cloneWithAllocator(allocator) catch bun.outOfMemory();
if (cloned.get("GIT_ASKPASS") == null) {
const config = SloppyGlobalGitConfig.get();
if (!config.has_askpass) {
cloned.put("GIT_ASKPASS", "echo") catch bun.outOfMemory();
}
}
if (cloned.get("GIT_SSH_COMMAND") == null) {
const config = SloppyGlobalGitConfig.get();
if (!config.has_ssh_command) {
cloned.put("GIT_SSH_COMMAND", "ssh -oStrictHostKeyChecking=accept-new") catch bun.outOfMemory();
}
if (cloned.get("GIT_SSH_COMMAND") == null) {
const config = SloppyGlobalGitConfig.get();
if (!config.has_ssh_command) {
cloned.put("GIT_SSH_COMMAND", "ssh -oStrictHostKeyChecking=accept-new") catch bun.outOfMemory();
}
}
this.env = cloned;
break :brk this.env.?;
};
this.env = cloned;
return &this.env.?;
}
} = .{};
@@ -337,45 +338,42 @@ pub const Repository = extern struct {
}
};
fn exec(
allocator: std.mem.Allocator,
_env: DotEnv.Map,
argv: []const string,
) !string {
var env = _env;
var std_map = try env.stdEnvMap(allocator);
fn rewriteSCPLikePath(after_url_scheme: string, comptime scheme: string, buf: []u8) ?string {
// Look for the pattern user@host:path (not :port)
const at_index = strings.indexOfChar(after_url_scheme, '@') orelse return null;
if (after_url_scheme.len < at_index + 1) return null;
const after_at = after_url_scheme[at_index + 1 ..];
const colon_index = strings.indexOfChar(after_at, ':') orelse return null;
if (after_at.len < colon_index + 1) return null;
const host_part = after_at[0..colon_index];
const path_part = after_at[colon_index + 1 ..];
defer std_map.deinit();
if (path_part.len == 0) return null;
const result = if (comptime Environment.isWindows)
try std.process.Child.run(.{
.allocator = allocator,
.argv = argv,
.env_map = std_map.get(),
})
else
try std.process.Child.run(.{
.allocator = allocator,
.argv = argv,
.env_map = std_map.get(),
});
switch (result.term) {
.Exited => |sig| if (sig == 0) return result.stdout else if (
// remote: The page could not be found <-- for non git
// remote: Repository not found. <-- for git
// remote: fatal repository '<url>' does not exist <-- for git
(strings.containsComptime(result.stderr, "remote:") and
strings.containsComptime(result.stderr, "not") and
strings.containsComptime(result.stderr, "found")) or
strings.containsComptime(result.stderr, "does not exist"))
{
return error.RepositoryNotFound;
},
else => {},
// Check if this looks like a port number (all digits)
var is_port = true;
for (path_part) |c| {
if (!std.ascii.isDigit(c) and c != '/' and c != '#') {
is_port = false;
break;
}
if (c == '/' or c == '#') break;
}
return error.InstallFailed;
if (is_port) return null;
// If it's not a port, treat as SCP-like syntax
buf[0..scheme.len].* = scheme[0..scheme.len].*;
var rest = buf[scheme.len..];
// Copy host
bun.copy(u8, rest, host_part);
rest[host_part.len] = '/';
// Copy path
bun.copy(u8, rest[host_part.len + 1 ..], path_part);
return buf[0 .. scheme.len + host_part.len + 1 + path_part.len];
}
pub fn trySSH(url: string) ?string {
@@ -384,32 +382,12 @@ pub const Repository = extern struct {
return null;
}
if (strings.hasPrefixComptime(url, "git@") or strings.hasPrefixComptime(url, "ssh://")) {
if (strings.hasPrefixComptime(url, "git@")) {
return url;
}
if (Dependency.isSCPLikePath(url)) {
ssh_path_buf[0.."ssh://git@".len].* = "ssh://git@".*;
var rest = ssh_path_buf["ssh://git@".len..];
const colon_index = strings.indexOfChar(url, ':');
if (colon_index) |colon| {
// make sure known hosts have `.com` or `.org`
if (Hosts.get(url[0..colon])) |tld| {
bun.copy(u8, rest, url[0..colon]);
bun.copy(u8, rest[colon..], tld);
rest[colon + tld.len] = '/';
bun.copy(u8, rest[colon + tld.len + 1 ..], url[colon + 1 ..]);
const out = ssh_path_buf[0 .. url.len + "ssh://git@".len + tld.len];
return out;
}
}
bun.copy(u8, rest, url);
if (colon_index) |colon| rest[colon] = '/';
const final = ssh_path_buf[0 .. url.len + "ssh://".len];
return final;
if (strings.hasPrefixComptime(url, "ssh://")) {
return rewriteSCPLikePath(url["ssh://".len..], "ssh://", &final_path_buf) orelse url;
}
return null;
@@ -421,240 +399,262 @@ pub const Repository = extern struct {
}
if (strings.hasPrefixComptime(url, "ssh://")) {
final_path_buf[0.."https".len].* = "https".*;
bun.copy(u8, final_path_buf["https".len..], url["ssh".len..]);
const out = final_path_buf[0 .. url.len - "ssh".len + "https".len];
return out;
return rewriteSCPLikePath(url["ssh://".len..], "https://", &final_path_buf);
}
if (Dependency.isSCPLikePath(url)) {
final_path_buf[0.."https://".len].* = "https://".*;
var rest = final_path_buf["https://".len..];
const colon_index = strings.indexOfChar(url, ':');
if (colon_index) |colon| {
// make sure known hosts have `.com` or `.org`
if (Hosts.get(url[0..colon])) |tld| {
bun.copy(u8, rest, url[0..colon]);
bun.copy(u8, rest[colon..], tld);
rest[colon + tld.len] = '/';
bun.copy(u8, rest[colon + tld.len + 1 ..], url[colon + 1 ..]);
const out = final_path_buf[0 .. url.len + "https://".len + tld.len];
return out;
}
}
bun.copy(u8, rest, url);
if (colon_index) |colon| rest[colon] = '/';
return final_path_buf[0 .. url.len + "https://".len];
return rewriteSCPLikePath(url, "https://", &final_path_buf);
}
return null;
}
pub fn download(
pub fn downloadAndPickURL(
pm: *PackageManager,
allocator: std.mem.Allocator,
env: DotEnv.Map,
env: *const DotEnv.Map,
log: *logger.Log,
cache_dir: std.fs.Dir,
task_id: u64,
name: string,
url: string,
attempt: u8,
) !std.fs.Dir {
) !GitRunner.ScheduleResult {
if (attempt > 0) {
if (strings.hasPrefixComptime(url, "git+ssh://")) {
if (trySSH(url)) |ssh| {
return download(pm, allocator, env, log, cache_dir, task_id, name, ssh, url, attempt);
}
}
}
if (tryHTTPS(url)) |https| {
return download(pm, allocator, env, log, cache_dir, task_id, name, https, url, attempt);
} else if (trySSH(url)) |ssh| {
return download(pm, allocator, env, log, cache_dir, task_id, name, ssh, url, attempt);
} else {
return download(pm, allocator, env, log, cache_dir, task_id, name, url, url, attempt);
}
}
pub fn download(
pm: *PackageManager,
allocator: std.mem.Allocator,
env: *const DotEnv.Map,
_: *logger.Log,
cache_dir: std.fs.Dir,
task_id: u64,
name: string,
clone_url: string,
input_url: string,
attempt: u8,
) !GitRunner.ScheduleResult {
bun.Analytics.Features.git_dependencies += 1;
const folder_name = try std.fmt.bufPrintZ(&folder_name_buf, "{any}.git", .{
bun.fmt.hexIntLower(task_id),
});
return if (cache_dir.openDirZ(folder_name, .{})) |dir| fetch: {
const path = Path.joinAbsString(PackageManager.get().cache_directory_path, &.{folder_name}, .auto);
if (cache_dir.openDirZ(folder_name, .{})) |dir| {
// Repository exists, just need to fetch
const buf = bun.PathBufferPool.get();
defer bun.PathBufferPool.put(buf);
const path = Path.joinAbsStringBuf(PackageManager.get().cache_directory_path, buf, &.{folder_name}, .auto);
_ = exec(
allocator,
env,
&[_]string{ "git", "-C", path, "fetch", "--quiet" },
) catch |err| {
log.addErrorFmt(
null,
logger.Loc.Empty,
allocator,
"\"git fetch\" for \"{s}\" failed",
.{name},
) catch unreachable;
return err;
const argv = &[_][]const u8{
GitRunner.gitExecutable(),
"-C",
path,
"fetch",
"--quiet",
};
break :fetch dir;
} else |not_found| clone: {
if (not_found != error.FileNotFound) return not_found;
const target = Path.joinAbsString(PackageManager.get().cache_directory_path, &.{folder_name}, .auto);
const context = GitRunner.CompletionContext{
.git_clone = .{
.name = try allocator.dupe(u8, name),
.url = try allocator.dupe(u8, input_url),
.task_id = task_id,
.attempt = attempt,
.dir = .{ .repo = dir },
},
};
_ = exec(allocator, env, &[_]string{
"git",
const runner = try GitRunner.init(allocator, pm, context);
try runner.spawn(argv, env);
return .scheduled;
} else |_| {
// Need to clone
const buf = bun.PathBufferPool.get();
defer bun.PathBufferPool.put(buf);
const target = Path.joinAbsStringBuf(PackageManager.get().cache_directory_path, buf, &.{folder_name}, .auto);
const argv = &[_][]const u8{
GitRunner.gitExecutable(),
"clone",
"-c core.longpaths=true",
"-c",
"core.longpaths=true",
"--quiet",
"--bare",
url,
clone_url,
target,
}) catch |err| {
if (err == error.RepositoryNotFound or attempt > 1) {
log.addErrorFmt(
null,
logger.Loc.Empty,
allocator,
"\"git clone\" for \"{s}\" failed",
.{name},
) catch unreachable;
}
return err;
};
break :clone try cache_dir.openDirZ(folder_name, .{});
};
const context = GitRunner.CompletionContext{
.git_clone = .{
.name = try allocator.dupe(u8, name),
.url = try allocator.dupe(u8, input_url),
.task_id = task_id,
.attempt = attempt,
.dir = .{ .cache = cache_dir },
},
};
const runner = try GitRunner.init(
allocator,
pm,
context,
);
try runner.spawn(argv, env);
return .scheduled;
}
}
pub fn findCommit(
pm: *PackageManager,
allocator: std.mem.Allocator,
env: *DotEnv.Loader,
log: *logger.Log,
env: *const DotEnv.Map,
_: *logger.Log,
repo_dir: std.fs.Dir,
name: string,
committish: string,
task_id: u64,
) !string {
const path = Path.joinAbsString(PackageManager.get().cache_directory_path, &.{try std.fmt.bufPrint(&folder_name_buf, "{any}.git", .{
) !GitRunner.ScheduleResult {
const buf = bun.PathBufferPool.get();
defer bun.PathBufferPool.put(buf);
const path = Path.joinAbsStringBuf(PackageManager.get().cache_directory_path, buf, &.{try std.fmt.bufPrint(&folder_name_buf, "{any}.git", .{
bun.fmt.hexIntLower(task_id),
})}, .auto);
_ = repo_dir;
const argv_buf = &[_][]const u8{ GitRunner.gitExecutable(), "-C", path, "log", "--format=%H", "-1", committish };
const argv: []const []const u8 = if (committish.len > 0)
argv_buf
else
argv_buf[0 .. argv_buf.len - 1];
const context = GitRunner.CompletionContext{
.git_find_commit = .{
.name = try allocator.dupe(u8, name),
.committish = try allocator.dupe(u8, committish),
.task_id = task_id,
.repo_dir = repo_dir,
},
};
return std.mem.trim(u8, exec(
allocator,
shared_env.get(allocator, env),
if (committish.len > 0)
&[_]string{ "git", "-C", path, "log", "--format=%H", "-1", committish }
else
&[_]string{ "git", "-C", path, "log", "--format=%H", "-1" },
) catch |err| {
log.addErrorFmt(
null,
logger.Loc.Empty,
allocator,
"no commit matching \"{s}\" found for \"{s}\" (but repository exists)",
.{ committish, name },
) catch unreachable;
return err;
}, " \t\r\n");
const runner = try GitRunner.init(allocator, pm, context);
try runner.spawn(argv, env);
return .scheduled;
}
pub fn checkout(
pm: *PackageManager,
allocator: std.mem.Allocator,
env: DotEnv.Map,
log: *logger.Log,
env: *const DotEnv.Map,
_: *logger.Log,
cache_dir: std.fs.Dir,
repo_dir: std.fs.Dir,
name: string,
url: string,
resolved: string,
) !ExtractData {
task_id: u64,
) !GitRunner.ScheduleResult {
bun.Analytics.Features.git_dependencies += 1;
const folder_name = PackageManager.cachedGitFolderNamePrint(&folder_name_buf, resolved, null);
var package_dir = bun.openDir(cache_dir, folder_name) catch |not_found| brk: {
if (not_found != error.ENOENT) return not_found;
switch (file: {
const path_buf = bun.PathBufferPool.get();
defer bun.PathBufferPool.put(path_buf);
const @"folder/package.json" = std.fmt.bufPrintZ(path_buf, "{s}" ++ std.fs.path.sep_str ++ "package.json", .{folder_name}) catch unreachable;
break :file bun.sys.File.readFileFrom(cache_dir, @"folder/package.json", allocator);
}) {
.result => |file_read| {
const json_file, const json_buf = file_read;
defer json_file.close();
const target = Path.joinAbsString(PackageManager.get().cache_directory_path, &.{folder_name}, .auto);
_ = exec(allocator, env, &[_]string{
"git",
"clone",
"-c core.longpaths=true",
"--quiet",
"--no-checkout",
try bun.getFdPath(.fromStdDir(repo_dir), &final_path_buf),
target,
}) catch |err| {
log.addErrorFmt(
null,
logger.Loc.Empty,
allocator,
"\"git clone\" for \"{s}\" failed",
.{name},
) catch unreachable;
return err;
};
const folder = Path.joinAbsString(PackageManager.get().cache_directory_path, &.{folder_name}, .auto);
_ = exec(allocator, env, &[_]string{ "git", "-C", folder, "checkout", "--quiet", resolved }) catch |err| {
log.addErrorFmt(
null,
logger.Loc.Empty,
allocator,
"\"git checkout\" for \"{s}\" failed",
.{name},
) catch unreachable;
return err;
};
var dir = try bun.openDir(cache_dir, folder_name);
dir.deleteTree(".git") catch {};
if (resolved.len > 0) insert_tag: {
const git_tag = dir.createFileZ(".bun-tag", .{ .truncate = true }) catch break :insert_tag;
defer git_tag.close();
git_tag.writeAll(resolved) catch {
dir.deleteFileZ(".bun-tag") catch {};
const json_path = json_file.getPath(&json_path_buf).unwrap() catch {
try pm.git_tasks.writeItem(.{
.task_id = task_id,
.err = error.InstallFailed,
.context = .{ .git_checkout = .{
.name = name,
.url = url,
.resolved = resolved,
.task_id = task_id,
.cache_dir = cache_dir,
.repo_dir = repo_dir,
} },
.result = undefined,
});
return .completed;
};
}
break :brk dir;
};
defer package_dir.close();
const ret_json_path = try @import("../fs.zig").FileSystem.instance.dirname_store.append(@TypeOf(json_path), json_path);
const json_file, const json_buf = bun.sys.File.readFileFrom(package_dir, "package.json", allocator).unwrap() catch |err| {
if (err == error.ENOENT) {
// allow git dependencies without package.json
return .{
.url = url,
.resolved = resolved,
};
}
log.addErrorFmt(
null,
logger.Loc.Empty,
allocator,
"\"package.json\" for \"{s}\" failed to open: {s}",
.{ name, @errorName(err) },
) catch unreachable;
return error.InstallFailed;
};
defer json_file.close();
const json_path = json_file.getPath(
&json_path_buf,
).unwrap() catch |err| {
log.addErrorFmt(
null,
logger.Loc.Empty,
allocator,
"\"package.json\" for \"{s}\" failed to resolve: {s}",
.{ name, @errorName(err) },
) catch unreachable;
return error.InstallFailed;
};
const ret_json_path = try FileSystem.instance.dirname_store.append(@TypeOf(json_path), json_path);
return .{
.url = url,
.resolved = resolved,
.json = .{
.path = ret_json_path,
.buf = json_buf,
// Enqueue complete GitRunner.Result with ExtractData payload
try pm.git_tasks.writeItem(.{
.task_id = task_id,
.context = .{ .git_checkout = .{
.name = name,
.url = url,
.resolved = resolved,
.task_id = task_id,
.cache_dir = cache_dir,
.repo_dir = repo_dir,
} },
.result = .{ .git_checkout = .{
.url = url,
.resolved = resolved,
.json = .{
.path = ret_json_path,
.buf = json_buf,
},
} },
});
return .completed;
},
};
.err => {
const buf = bun.PathBufferPool.get();
defer bun.PathBufferPool.put(buf);
// Need to clone and checkout
const target = Path.joinAbsStringBuf(PackageManager.get().cache_directory_path, buf, &.{folder_name}, .auto);
const repo_path = try bun.getFdPath(.fromStdDir(repo_dir), &final_path_buf);
const argv = &[_][]const u8{
GitRunner.gitExecutable(),
"clone",
"-c",
"core.longpaths=true",
"--quiet",
"--no-checkout",
repo_path,
target,
};
// Then we'll need to checkout after clone completes
// For now, we'll do both operations in sequence
const context = GitRunner.CompletionContext{
.git_checkout = .{
.name = try allocator.dupe(u8, name),
.url = try allocator.dupe(u8, url),
.resolved = try allocator.dupe(u8, resolved),
.task_id = task_id,
.cache_dir = cache_dir,
.repo_dir = repo_dir,
},
};
const runner = try GitRunner.init(allocator, pm, context);
try runner.spawn(argv, env);
return .scheduled;
},
}
}
};

View File

@@ -34,6 +34,37 @@ pub fn allocate(this: *StringBuilder, allocator: Allocator) Allocator.Error!void
this.len = 0;
}
/// Allocates a null-terminated slice in a single allocation.
pub fn createNullDelimited(allocator: Allocator, args: anytype) Allocator.Error!struct { [*:null]?[*:0]const u8, []u8 } {
var this = bun.StringBuilder{};
for (args) |arg| {
this.countZ(arg);
}
const byte_len = this.cap + ((args.len + 1) * @sizeOf([*:0]const u8));
// Slice of:
// ptrs: [*:null][*:0]const u8
// bytes: [*]u8
const raw_bytes = allocator.rawAlloc(
byte_len,
.@"8",
@returnAddress(),
) orelse return error.OutOfMemory;
const allocated_bytes = raw_bytes[0..byte_len];
const ptrs: [*:null]?[*:0]const u8 = @alignCast(@ptrCast(allocated_bytes.ptr));
const bytes: []u8 = allocated_bytes[((args.len + 1) * @sizeOf([*:0]const u8))..];
this.ptr = bytes.ptr;
for (ptrs[0..args.len], args) |*ptr, arg| {
ptr.* = this.appendZ(arg).ptr;
}
ptrs[args.len] = null;
return .{ ptrs, bytes };
}
pub fn deinit(this: *StringBuilder, allocator: Allocator) void {
if (this.ptr == null or this.cap == 0) return;
allocator.free(this.ptr.?[0..this.cap]);