Compare commits

...

8 Commits

Author SHA1 Message Date
Dylan Conway
009adfd232 update 2025-10-20 19:56:58 -07:00
Jarred Sumner
8ce582d0e9 Merge branch 'main' into claude/fix-git-clone-to-temp-first 2025-10-19 22:23:02 -07:00
Jarred Sumner
2e062b1f7d Merge branch 'main' into claude/fix-git-clone-to-temp-first 2025-10-14 10:19:02 -07:00
Claude Bot
c7a2c79c51 Update ban-limits.json for std.fs.Dir usage
Increased limit from 165 to 167 to account for new temp_dir parameters
in Repository.download() and Repository.checkout() functions.

These follow the same pattern as extract_tarball.zig which also uses
std.fs.Dir for cache_dir and temp_dir parameters.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 18:21:33 +00:00
Claude Bot
bc6cddacbe Address CodeRabbit nitpicks: improve logging clarity
- Show absolute temp path (temp_target_full) in verbose logs instead of tmpname
- Set moved=true on EEXIST path to cancel errdefer cleanup
- Log absolute destination path in error messages for better diagnostics

Co-Authored-By: CodeRabbit AI <noreply@coderabbit.ai>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 17:23:44 +00:00
Claude Bot
f0b70424fc Add verbose logging for git repository operations
Adds --verbose logging to track git clone and checkout operations:
- Log clone start with temp directory name
- Log checkout operations with resolved commit
- Log successful completion with timing
- Log when another process wins the race

Follows same pattern as extract_tarball.zig verbose logs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 17:04:37 +00:00
Claude Bot
63db5023dd Address CodeRabbit review: Add cleanup and race handling
- Add errdefer cleanup for temp directories on failure
- Handle race condition when destination already exists
- Set moved flag to prevent cleanup after successful rename
- Log absolute paths instead of relative in error messages
- Best-effort cleanup when another process wins the race

Co-Authored-By: CodeRabbit AI <noreply@coderabbit.ai>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 16:52:15 +00:00
Claude Bot
c61f11242b Fix git repository cloning to use temp directory first
Changes git clone operations during bun install to:
1. Clone to temporary directory first
2. Use renameatConcurrently to atomically move to cache

This prevents race conditions when multiple bun install processes
run concurrently and clone the same git dependency.

Both download() and checkout() functions now:
- Accept a temp_dir parameter
- Use FileSystem.tmpname() for temporary directory names
- Clone to temp location first
- Atomically move to final cache location using renameatConcurrently

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 16:09:59 +00:00
3 changed files with 132 additions and 12 deletions

View File

@@ -162,6 +162,7 @@ pub fn callback(task: *ThreadPool.Task) void {
this.request.git_clone.env,
&this.log,
manager.getCacheDirectory(),
manager.getTemporaryDirectory().handle,
this.id,
name,
https,
@@ -189,6 +190,7 @@ pub fn callback(task: *ThreadPool.Task) void {
this.request.git_clone.env,
&this.log,
manager.getCacheDirectory(),
manager.getTemporaryDirectory().handle,
this.id,
name,
ssh,
@@ -213,6 +215,7 @@ pub fn callback(task: *ThreadPool.Task) void {
this.request.git_checkout.env,
&this.log,
manager.getCacheDirectory(),
manager.getTemporaryDirectory().handle,
git_checkout.repo_dir.stdDir(),
git_checkout.name.slice(),
git_checkout.url.slice(),

View File

@@ -480,6 +480,7 @@ pub const Repository = extern struct {
env: DotEnv.Map,
log: *logger.Log,
cache_dir: std.fs.Dir,
temp_dir: std.fs.Dir,
task_id: Install.Task.Id,
name: string,
url: string,
@@ -511,7 +512,24 @@ pub const Repository = extern struct {
} 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 time_started_for_verbose_logs: u64 = if (PackageManager.verbose_install) bun.getRoughTickCount().ns() else 0;
const temp_path_buf = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(temp_path_buf);
const tmpname_buf = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(tmpname_buf);
// Clone to temp directory first
const tmpname = try FileSystem.tmpname(folder_name[0..@min(folder_name.len, 32)], tmpname_buf, bun.fastRandom());
const temp_target = try bun.getFdPath(.fromStdDir(temp_dir), temp_path_buf);
const temp_target_full = Path.joinAbsString(temp_target, &.{tmpname}, .auto);
var moved = false;
errdefer if (!moved) temp_dir.deleteTree(tmpname) catch {};
if (PackageManager.verbose_install) {
Output.prettyErrorln("[{s}] Cloning git repository (bare) to {s}<r>", .{ name, temp_target_full });
Output.flush();
}
_ = exec(allocator, env, &[_]string{
"git",
@@ -520,7 +538,7 @@ pub const Repository = extern struct {
"--quiet",
"--bare",
url,
target,
temp_target_full,
}) catch |err| {
if (err == error.RepositoryNotFound or attempt > 1) {
log.addErrorFmt(
@@ -534,6 +552,44 @@ pub const Repository = extern struct {
return err;
};
// Move from temp to cache atomically
if (bun.sys.renameatConcurrently(
.fromStdDir(temp_dir),
tmpname,
.fromStdDir(cache_dir),
folder_name,
.{ .move_fallback = true },
).asErr()) |err| {
// If destination exists, another process likely completed the work
if (cache_dir.openDirZ(folder_name, .{})) |dir2| {
// Best-effort cleanup of our temp path
temp_dir.deleteTree(tmpname) catch {};
moved = true; // cancel errdefer; already cleaned up
if (PackageManager.verbose_install) {
Output.prettyErrorln("[{s}] Cache dir already exists (another process completed first)<r>", .{name});
Output.flush();
}
break :clone dir2;
} else |_| {}
const cache_target = try bun.getFdPath(.fromStdDir(cache_dir), &final_path_buf);
const dest_full = Path.joinAbsString(cache_target, &.{folder_name}, .auto);
log.addErrorFmt(
null,
logger.Loc.Empty,
allocator,
"moving \"{s}\" to cache dir failed: {}\n From: {s}\n To: {s}",
.{ name, err, temp_target_full, dest_full },
) catch unreachable;
return error.InstallFailed;
}
moved = true;
if (PackageManager.verbose_install) {
const elapsed = bun.getRoughTickCount().ns() - time_started_for_verbose_logs;
Output.prettyErrorln("[{s}] Cloned bare repository to cache ({})<r>", .{ name, std.fmt.fmtDuration(elapsed) });
Output.flush();
}
break :clone try cache_dir.openDirZ(folder_name, .{});
};
}
@@ -577,6 +633,7 @@ pub const Repository = extern struct {
env: DotEnv.Map,
log: *logger.Log,
cache_dir: std.fs.Dir,
temp_dir: std.fs.Dir,
repo_dir: std.fs.Dir,
name: string,
url: string,
@@ -588,7 +645,23 @@ pub const Repository = extern struct {
var package_dir = bun.openDir(cache_dir, folder_name) catch |not_found| brk: {
if (not_found != error.ENOENT) return not_found;
const target = Path.joinAbsString(PackageManager.get().cache_directory_path, &.{folder_name}, .auto);
const time_started_for_verbose_logs: u64 = if (PackageManager.verbose_install) bun.getRoughTickCount().ns() else 0;
const temp_path_buf = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(temp_path_buf);
const tmpname_buf = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(tmpname_buf);
const tmpname = try FileSystem.tmpname(folder_name[0..@min(folder_name.len, 32)], tmpname_buf, bun.fastRandom());
const temp_target = try bun.getFdPath(.fromStdDir(temp_dir), temp_path_buf);
const temp_target_full = Path.joinAbsString(temp_target, &.{tmpname}, .auto);
var moved = false;
errdefer if (!moved) temp_dir.deleteTree(tmpname) catch {};
if (PackageManager.verbose_install) {
Output.prettyErrorln("[{s}] Cloning git repository to {s}<r>", .{ name, temp_target_full });
Output.flush();
}
_ = exec(allocator, env, &[_]string{
"git",
@@ -597,7 +670,7 @@ pub const Repository = extern struct {
"--quiet",
"--no-checkout",
try bun.getFdPath(.fromStdDir(repo_dir), &final_path_buf),
target,
temp_target_full,
}) catch |err| {
log.addErrorFmt(
null,
@@ -609,9 +682,12 @@ pub const Repository = extern struct {
return err;
};
const folder = Path.joinAbsString(PackageManager.get().cache_directory_path, &.{folder_name}, .auto);
if (PackageManager.verbose_install) {
Output.prettyErrorln("[{s}] Checking out {s}<r>", .{ name, resolved });
Output.flush();
}
_ = exec(allocator, env, &[_]string{ "git", "-C", folder, "checkout", "--quiet", resolved }) catch |err| {
_ = exec(allocator, env, &[_]string{ "git", "-C", temp_target_full, "checkout", "--quiet", resolved }) catch |err| {
log.addErrorFmt(
null,
logger.Loc.Empty,
@@ -621,18 +697,58 @@ pub const Repository = extern struct {
) catch unreachable;
return err;
};
var dir = try bun.openDir(cache_dir, folder_name);
dir.deleteTree(".git") catch {};
var temp_pkg_dir = try bun.openDir(temp_dir, tmpname);
temp_pkg_dir.deleteTree(".git") catch {};
if (resolved.len > 0) insert_tag: {
const git_tag = dir.createFileZ(".bun-tag", .{ .truncate = true }) catch break :insert_tag;
const git_tag = temp_pkg_dir.createFileZ(".bun-tag", .{ .truncate = true }) catch break :insert_tag;
defer git_tag.close();
git_tag.writeAll(resolved) catch {
dir.deleteFileZ(".bun-tag") catch {};
temp_pkg_dir.deleteFileZ(".bun-tag") catch {};
};
}
temp_pkg_dir.close();
break :brk dir;
// Move from temp to cache atomically
if (bun.sys.renameatConcurrently(
.fromStdDir(temp_dir),
tmpname,
.fromStdDir(cache_dir),
folder_name,
.{ .move_fallback = true },
).asErr()) |err| {
// If destination exists, another process likely completed the work
if (bun.openDir(cache_dir, folder_name)) |_| {
// Best-effort cleanup of our temp path
temp_dir.deleteTree(tmpname) catch {};
moved = true; // cancel errdefer; already cleaned up
if (PackageManager.verbose_install) {
Output.prettyErrorln("[{s}] Cache dir already exists (another process completed first)<r>", .{name});
Output.flush();
}
break :brk try bun.openDir(cache_dir, folder_name);
} else |_| {}
const cache_target = try bun.getFdPath(.fromStdDir(cache_dir), &final_path_buf);
const dest_full = Path.joinAbsString(cache_target, &.{folder_name}, .auto);
log.addErrorFmt(
null,
logger.Loc.Empty,
allocator,
"moving \"{s}\" to cache dir failed: {}\n From: {s}\n To: {s}",
.{ name, err, temp_target_full, dest_full },
) catch unreachable;
return error.InstallFailed;
}
moved = true;
if (PackageManager.verbose_install) {
const elapsed = bun.getRoughTickCount().ns() - time_started_for_verbose_logs;
Output.prettyErrorln("[{s}] Checked out to cache ({})<r>", .{ name, std.fmt.fmtDuration(elapsed) });
Output.flush();
}
break :brk try bun.openDir(cache_dir, folder_name);
};
defer package_dir.close();
@@ -695,6 +811,7 @@ const PackageManager = Install.PackageManager;
const bun = @import("bun");
const OOM = bun.OOM;
const Output = bun.Output;
const Path = bun.path;
const logger = bun.logger;
const strings = bun.strings;

View File

@@ -34,7 +34,7 @@
"std.debug.dumpStackTrace": 0,
"std.debug.print": 0,
"std.enums.tagName(": 2,
"std.fs.Dir": 165,
"std.fs.Dir": 167,
"std.fs.File": 62,
"std.fs.cwd": 103,
"std.log": 1,