mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
Compare commits
1 Commits
claude/imp
...
cursor/ref
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ece68b9425 |
278
async_git_implementation_checklist.md
Normal file
278
async_git_implementation_checklist.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# Async Git Implementation Checklist
|
||||
|
||||
## Step 1: Add Git Completion Callbacks to PackageManager
|
||||
|
||||
Add these functions to `src/install/install.zig` after line 4785 (before `const CacheDir = struct`):
|
||||
|
||||
```zig
|
||||
// Git operation completion callbacks
|
||||
pub fn onGitDownloadComplete(
|
||||
this: *PackageManager,
|
||||
task_id: u64,
|
||||
result: anyerror!std.fs.Dir,
|
||||
ctx: anytype,
|
||||
) void {
|
||||
const dependency_list_entry = this.task_queue.getEntry(task_id) orelse {
|
||||
// Task was cancelled or already processed
|
||||
if (result) |dir| dir.close() else |_| {}
|
||||
return;
|
||||
};
|
||||
const dependency_list = dependency_list_entry.value_ptr.*;
|
||||
dependency_list_entry.value_ptr.* = .{};
|
||||
|
||||
if (result) |repo_dir| {
|
||||
this.git_repositories.put(this.allocator, task_id, .fromStdDir(repo_dir)) catch unreachable;
|
||||
|
||||
// Now we need to find the commit
|
||||
Repository.findCommit(
|
||||
this,
|
||||
repo_dir,
|
||||
ctx.name,
|
||||
if (this.lockfile.buffers.dependencies.items.len > 0 and ctx.dep_id < this.lockfile.buffers.dependencies.items.len)
|
||||
this.lockfile.str(&this.lockfile.buffers.dependencies.items[ctx.dep_id].version.value.git.committish)
|
||||
else
|
||||
"",
|
||||
task_id,
|
||||
);
|
||||
} else |err| {
|
||||
if (PackageManager.verbose_install or this.options.log_level != .silent) {
|
||||
const name = ctx.name;
|
||||
if (err == error.RepositoryNotFound or ctx.attempt > 1) {
|
||||
this.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
this.allocator,
|
||||
"\"git clone\" for \"{s}\" failed",
|
||||
.{name},
|
||||
) catch unreachable;
|
||||
} else {
|
||||
this.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
this.allocator,
|
||||
"{s} cloning repository for <b>{s}<r>",
|
||||
.{
|
||||
@errorName(err),
|
||||
name,
|
||||
},
|
||||
) catch unreachable;
|
||||
}
|
||||
}
|
||||
// Process the dependency list even on error to prevent hanging
|
||||
this.processDependencyList(dependency_list, void, {}, {}, false) catch {};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn onGitFindCommitComplete(
|
||||
this: *PackageManager,
|
||||
task_id: u64,
|
||||
result: anyerror!string,
|
||||
ctx: anytype,
|
||||
) void {
|
||||
if (result) |resolved| {
|
||||
const checkout_id = Task.Id.forGitCheckout(
|
||||
this.lockfile.str(&this.lockfile.buffers.dependencies.items[ctx.dep_id].version.value.git.repo),
|
||||
resolved,
|
||||
);
|
||||
|
||||
if (this.hasCreatedNetworkTask(
|
||||
checkout_id,
|
||||
this.lockfile.buffers.dependencies.items[ctx.dep_id].behavior.isRequired(),
|
||||
)) return;
|
||||
|
||||
// Now checkout the specific commit
|
||||
Repository.checkout(
|
||||
this,
|
||||
this.getCacheDirectory(),
|
||||
ctx.repo_dir,
|
||||
ctx.name,
|
||||
this.lockfile.str(&this.lockfile.buffers.dependencies.items[ctx.dep_id].version.value.git.repo),
|
||||
resolved,
|
||||
);
|
||||
} else |err| {
|
||||
if (PackageManager.verbose_install or this.options.log_level != .silent) {
|
||||
this.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
this.allocator,
|
||||
"no commit matching \"{s}\" found for \"{s}\" (but repository exists)",
|
||||
.{ ctx.committish, ctx.name },
|
||||
) catch unreachable;
|
||||
}
|
||||
|
||||
// Process any pending dependency list
|
||||
if (this.task_queue.getEntry(task_id)) |entry| {
|
||||
const dependency_list = entry.value_ptr.*;
|
||||
entry.value_ptr.* = .{};
|
||||
this.processDependencyList(dependency_list, void, {}, {}, false) catch {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn onGitCheckoutComplete(
|
||||
this: *PackageManager,
|
||||
_: u64, // checkout doesn't use task_id
|
||||
result: anyerror!ExtractData,
|
||||
ctx: anytype,
|
||||
) void {
|
||||
if (result) |data| {
|
||||
var package_id: PackageID = invalid_package_id;
|
||||
const dep_id = if (@hasField(@TypeOf(ctx), "dependency_id"))
|
||||
ctx.dependency_id
|
||||
else if (this.lockfile.buffers.dependencies.items.len > 0)
|
||||
@as(DependencyID, @intCast(0)) // fallback, should not happen
|
||||
else
|
||||
@as(DependencyID, @intCast(0));
|
||||
|
||||
const resolution = Resolution{
|
||||
.tag = .git,
|
||||
.value = .{
|
||||
.git = .{
|
||||
.repo = this.lockfile.buffers.dependencies.items[dep_id].version.value.git.repo,
|
||||
.committish = this.lockfile.buffers.dependencies.items[dep_id].version.value.git.committish,
|
||||
.resolved = strings.StringOrTinyString.init(data.resolved).value,
|
||||
.package_name = .{},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (this.processExtractedTarballPackage(
|
||||
&package_id,
|
||||
dep_id,
|
||||
&resolution,
|
||||
&data,
|
||||
this.options.log_level,
|
||||
)) |pkg| {
|
||||
// Update the dependency with the resolved name and commit
|
||||
var git = &this.lockfile.buffers.dependencies.items[dep_id].version.value.git;
|
||||
git.resolved = pkg.resolution.value.git.resolved;
|
||||
git.package_name = pkg.name;
|
||||
|
||||
// Process the dependency now that we have the resolved info
|
||||
var any_root = false;
|
||||
this.processDependencyListItem(.{ .dependency = dep_id }, &any_root, false) catch {};
|
||||
}
|
||||
} else |err| {
|
||||
if (PackageManager.verbose_install or this.options.log_level != .silent) {
|
||||
this.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
this.allocator,
|
||||
"\"git checkout\" for \"{s}\" failed: {s}",
|
||||
.{ ctx.name, @errorName(err) },
|
||||
) catch unreachable;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 2: Replace Git Operation Calls
|
||||
|
||||
In `src/install/install.zig`, around line 4056, replace:
|
||||
|
||||
```zig
|
||||
this.task_batch.push(ThreadPool.Batch.from(this.enqueueGitClone(clone_id, alias, dep, id, dependency, &res, null)));
|
||||
```
|
||||
|
||||
With:
|
||||
|
||||
```zig
|
||||
// Store dependency context in task queue for later processing
|
||||
var entry = this.task_queue.getOrPutContext(this.allocator, clone_id, .{}) catch unreachable;
|
||||
if (!entry.found_existing) entry.value_ptr.* = .{};
|
||||
try entry.value_ptr.append(this.allocator, ctx);
|
||||
|
||||
// Start async download - context includes dep_id for later use
|
||||
Repository.download(this, this.getCacheDirectory(), clone_id, alias, this.lockfile.str(&dep.repo), 0, id);
|
||||
```
|
||||
|
||||
And for an existing repository (around line 4033), we need to handle the case where we already have the repository:
|
||||
|
||||
```zig
|
||||
// Instead of enqueueGitCheckout, directly call checkout
|
||||
Repository.checkout(this, this.getCacheDirectory(), .fromStdDir(this.git_repositories.get(clone_id).?.stdDir()), alias, this.lockfile.str(&res.value.git.repo), resolved);
|
||||
```
|
||||
|
||||
Note: The completion callbacks will need to be updated to include `dep_id` in their context structs:
|
||||
|
||||
```zig
|
||||
// In the download completion context
|
||||
Repository.download(this, this.getCacheDirectory(), clone_id, alias, this.lockfile.str(&dep.repo), 0, id);
|
||||
// The last parameter 'id' is the dep_id that will be passed in the context
|
||||
```
|
||||
|
||||
## Step 3: Remove Git-Related ThreadPool Code
|
||||
|
||||
### Remove from Task enum (around line 917):
|
||||
```zig
|
||||
git_clone = 2,
|
||||
git_checkout = 3,
|
||||
```
|
||||
|
||||
### Remove from Task.Data union (around line 931):
|
||||
```zig
|
||||
git_clone: bun.FileDescriptor,
|
||||
git_checkout: ExtractData,
|
||||
```
|
||||
|
||||
### Remove from Task.Request union (around line 945):
|
||||
```zig
|
||||
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,
|
||||
},
|
||||
```
|
||||
|
||||
### Remove from Task.run() switch (around lines 775-848):
|
||||
Remove the entire `.git_clone => { ... }` and `.git_checkout => { ... }` cases.
|
||||
|
||||
### Remove functions (around lines 3414-3510):
|
||||
- `enqueueGitClone()`
|
||||
- `enqueueGitCheckout()`
|
||||
|
||||
### Remove from runTasks() processing:
|
||||
Remove the git_clone and git_checkout processing from the resolve_tasks switch statement.
|
||||
|
||||
## Step 4: Clean up repository.zig
|
||||
|
||||
Remove the synchronous `exec()` function and rename:
|
||||
- `downloadSync` back to `downloadLegacy` (or remove if unused)
|
||||
- `findCommitSync` back to `findCommitLegacy` (or remove if unused)
|
||||
- `checkoutSync` back to `checkoutLegacy` (or remove if unused)
|
||||
|
||||
## Step 5: Update any remaining references
|
||||
|
||||
Search for and update any remaining references to:
|
||||
- `Repository.downloadSync`
|
||||
- `Repository.findCommitSync`
|
||||
- `Repository.checkoutSync`
|
||||
- `Task.Tag.git_clone`
|
||||
- `Task.Tag.git_checkout`
|
||||
|
||||
## Testing
|
||||
|
||||
After implementation:
|
||||
1. Run `bun install` with a project containing git dependencies
|
||||
2. Test various git URL formats:
|
||||
- `"github:user/repo"`
|
||||
- `"git+ssh://git@github.com:user/repo.git"`
|
||||
- `"git+https://github.com/user/repo.git"`
|
||||
- `"git://github.com/user/repo.git#commit"`
|
||||
3. Verify error handling for:
|
||||
- Non-existent repositories
|
||||
- Invalid commits/tags
|
||||
- Network failures
|
||||
4. Check that progress reporting still works correctly
|
||||
5. Ensure lifecycle scripts in git dependencies are executed
|
||||
62
async_git_refactor_summary.md
Normal file
62
async_git_refactor_summary.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Async Git Operations Refactoring Summary
|
||||
|
||||
## Implementation Status
|
||||
|
||||
### ✅ Completed:
|
||||
1. **Created `GitRunner` struct in `repository.zig`** - A state machine for managing async git processes
|
||||
- Handles stdout/stderr buffering
|
||||
- Manages process lifecycle (spawn, I/O, exit)
|
||||
- Supports two-phase checkout operations (clone then checkout)
|
||||
- Error handling with proper logging
|
||||
|
||||
2. **Implemented async versions of git operations in `repository.zig`**:
|
||||
- `download()` - Async version for git clone/fetch
|
||||
- `findCommit()` - Async version for finding commit hash
|
||||
- `checkout()` - Async version for git checkout
|
||||
|
||||
3. **Created git completion callbacks** (in `git_callbacks.zig` as reference):
|
||||
- `onGitDownloadComplete()` - Handles download completion, triggers findCommit
|
||||
- `onGitFindCommitComplete()` - Handles commit resolution, triggers checkout
|
||||
- `onGitCheckoutComplete()` - Handles checkout completion, processes package.json
|
||||
|
||||
### 🚧 Still Needed:
|
||||
|
||||
1. **Add completion callbacks to `PackageManager` in `install.zig`**:
|
||||
- Copy the callbacks from `git_callbacks.zig` after line 4785 (before `CacheDir` struct)
|
||||
|
||||
2. **Update git operation calls in `install.zig`**:
|
||||
- Replace `this.task_batch.push(ThreadPool.Batch.from(this.enqueueGitClone(...)))` (line ~4056) with:
|
||||
```zig
|
||||
Repository.download(this, this.getCacheDirectory(), clone_id, alias, this.lockfile.str(&dep.repo), 0);
|
||||
```
|
||||
- Remove the old ThreadPool task enqueuing for git operations
|
||||
|
||||
3. **Remove obsolete code**:
|
||||
- Remove `enqueueGitClone()` and `enqueueGitCheckout()` functions
|
||||
- Remove `Task.Tag.git_clone` and `Task.Tag.git_checkout` enum values
|
||||
- Remove git processing from `runTasks()` switch statement
|
||||
- Delete `Repository.exec()` (the synchronous function)
|
||||
|
||||
4. **Update imports**:
|
||||
- Ensure `install.zig` imports the new async functions from `repository.zig`
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The new flow works as follows:
|
||||
|
||||
1. **Dependency Resolution** → When a git dependency is encountered, directly call `Repository.download()`
|
||||
2. **Download Complete** → `onGitDownloadComplete()` is called, which triggers `Repository.findCommit()`
|
||||
3. **Commit Found** → `onGitFindCommitComplete()` is called, which triggers `Repository.checkout()`
|
||||
4. **Checkout Complete** → `onGitCheckoutComplete()` is called, which processes the package.json and enqueues dependencies
|
||||
|
||||
This eliminates blocking thread pool operations in favor of event-driven async I/O through the existing event loop.
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Test `github:` dependencies
|
||||
- [ ] Test `git+ssh://` dependencies
|
||||
- [ ] Test `git+https://` dependencies
|
||||
- [ ] Test git dependencies with specific commits/tags
|
||||
- [ ] Test error cases (repository not found, invalid commit)
|
||||
- [ ] Verify progress reporting still works
|
||||
- [ ] Check that lifecycle scripts in git dependencies still run
|
||||
@@ -137,7 +137,7 @@ pub const Repository = @import("./repository.zig").Repository;
|
||||
pub const Bin = @import("./bin.zig").Bin;
|
||||
pub const Dependency = @import("./dependency.zig");
|
||||
const Behavior = @import("./dependency.zig").Behavior;
|
||||
const FolderResolution = @import("./resolvers/folder_resolver.zig").FolderResolution;
|
||||
const FolderResolution = @import("../resolvers/folder_resolver.zig").FolderResolution;
|
||||
|
||||
pub fn ExternalSlice(comptime Type: type) type {
|
||||
return extern struct {
|
||||
@@ -445,7 +445,7 @@ pub const NetworkTask = struct {
|
||||
this.unsafe_http_client.client.flags.reject_unauthorized = this.package_manager.tlsRejectUnauthorized();
|
||||
|
||||
if (PackageManager.verbose_install) {
|
||||
this.unsafe_http_client.client.verbose = .headers;
|
||||
this.unsafe_http_client.verbose = .headers;
|
||||
}
|
||||
|
||||
this.callback = .{
|
||||
@@ -617,7 +617,6 @@ pub const PreinstallState = enum(u4) {
|
||||
apply_patch,
|
||||
applying_patch,
|
||||
};
|
||||
|
||||
/// Schedule long-running callbacks for a task
|
||||
/// Slow stuff is broken into tasks, each can run independently without locks
|
||||
pub const Task = struct {
|
||||
@@ -1101,7 +1100,6 @@ const PackageManifestMap = struct {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// We can't know all the packages we need until we've downloaded all the packages
|
||||
// The easy way would be:
|
||||
// 1. Download all packages, parsing their dependencies and enqueuing all dependencies for resolution
|
||||
@@ -1725,7 +1723,6 @@ pub const PackageManager = struct {
|
||||
not_found: void,
|
||||
failure: anyerror,
|
||||
};
|
||||
|
||||
pub fn enqueueDependencyToRoot(
|
||||
this: *PackageManager,
|
||||
name: []const u8,
|
||||
@@ -1804,7 +1801,7 @@ pub const PackageManager = struct {
|
||||
};
|
||||
|
||||
if (PackageManager.verbose_install and manager.pendingTaskCount() > 0) {
|
||||
if (PackageManager.hasEnoughTimePassedBetweenWaitingMessages()) Output.prettyErrorln("<d>[PackageManager]<r> waiting for {d} tasks\n", .{closure.manager.pendingTaskCount()});
|
||||
if (PackageManager.hasEnoughTimePassedBetweenWaitingMessages()) Output.prettyErrorln("<d>[PackageManager]<r> waiting for {d} tasks\n", .{manager.pendingTaskCount()});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1900,7 +1897,6 @@ pub const PackageManager = struct {
|
||||
else => return null,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensurePreinstallStateListCapacity(this: *PackageManager, count: usize) void {
|
||||
if (this.preinstall_state.items.len >= count) {
|
||||
return;
|
||||
@@ -1995,7 +1991,7 @@ pub const PackageManager = struct {
|
||||
const non_patched_path = manager.lockfile.allocator.dupeZ(u8, non_patched_path_) catch bun.outOfMemory();
|
||||
defer manager.lockfile.allocator.free(non_patched_path);
|
||||
if (manager.isFolderInCache(non_patched_path)) {
|
||||
manager.setPreinstallState(pkg.meta.id, manager.lockfile, .apply_patch);
|
||||
manager.setPreinstallState(pkg.meta.id, lockfile, .apply_patch);
|
||||
// yay step 1 is already done for us
|
||||
return .apply_patch;
|
||||
}
|
||||
@@ -2306,7 +2302,7 @@ pub const PackageManager = struct {
|
||||
}
|
||||
|
||||
pub fn cachedGitFolderNamePrint(buf: []u8, resolved: string, patch_hash: ?u64) stringZ {
|
||||
return std.fmt.bufPrintZ(buf, "@G@{s}{}", .{ resolved, PatchHashFmt{ .hash = patch_hash } }) catch unreachable;
|
||||
return std.fmt.bufPrintZ(buf, "@G@{s}@{}", .{ resolved, PatchHashFmt{ .hash = patch_hash } }) catch unreachable;
|
||||
}
|
||||
|
||||
pub fn cachedGitFolderName(this: *const PackageManager, repository: *const Repository, patch_hash: ?u64) stringZ {
|
||||
@@ -2368,7 +2364,6 @@ pub const PackageManager = struct {
|
||||
pub fn cachedGitHubFolderName(this: *const PackageManager, repository: *const Repository, patch_hash: ?u64) stringZ {
|
||||
return cachedGitHubFolderNamePrint(&cached_package_folder_name_buf, this.lockfile.str(&repository.resolved), patch_hash);
|
||||
}
|
||||
|
||||
fn cachedGitHubFolderNamePrintGuess(buf: []u8, string_buf: []const u8, repository: *const Repository, patch_hash: ?u64) stringZ {
|
||||
return std.fmt.bufPrintZ(
|
||||
buf,
|
||||
@@ -2674,7 +2669,6 @@ pub const PackageManager = struct {
|
||||
.cache_dir_subpath = cache_dir_subpath,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getInstalledVersionsFromDiskCache(this: *PackageManager, tags_buf: *std.ArrayList(u8), package_name: []const u8, allocator: std.mem.Allocator) !std.ArrayList(Semver.Version) {
|
||||
var list = std.ArrayList(Semver.Version).init(allocator);
|
||||
var dir = this.getCacheDirectory().openDir(package_name, .{
|
||||
@@ -3013,7 +3007,6 @@ pub const PackageManager = struct {
|
||||
this.patch_task_fifo.writeItemAssumeCapacity(task);
|
||||
_ = this.pending_pre_calc_hashes.fetchAdd(1, .monotonic);
|
||||
}
|
||||
|
||||
const SuccessFn = *const fn (*PackageManager, DependencyID, PackageID) void;
|
||||
const FailFn = *const fn (*PackageManager, *const Dependency, PackageID, anyerror) void;
|
||||
fn assignResolution(this: *PackageManager, dependency_id: DependencyID, package_id: PackageID) void {
|
||||
@@ -3466,7 +3459,6 @@ pub const PackageManager = struct {
|
||||
};
|
||||
return &task.threadpool_task;
|
||||
}
|
||||
|
||||
fn enqueueGitCheckout(
|
||||
this: *PackageManager,
|
||||
task_id: u64,
|
||||
@@ -3657,7 +3649,6 @@ pub const PackageManager = struct {
|
||||
else => .{ original_name, original_name_hash },
|
||||
};
|
||||
}
|
||||
|
||||
/// Q: "What do we do with a dependency in a package.json?"
|
||||
/// A: "We enqueue it!"
|
||||
fn enqueueDependencyWithMainAndSuccessFn(
|
||||
@@ -4241,92 +4232,6 @@ pub const PackageManager = struct {
|
||||
}
|
||||
}
|
||||
},
|
||||
.tarball => {
|
||||
const res: Resolution = switch (version.value.tarball.uri) {
|
||||
.local => |path| .{
|
||||
.tag = .local_tarball,
|
||||
.value = .{
|
||||
.local_tarball = path,
|
||||
},
|
||||
},
|
||||
.remote => |url| .{
|
||||
.tag = .remote_tarball,
|
||||
.value = .{
|
||||
.remote_tarball = url,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// First: see if we already loaded the tarball package in-memory
|
||||
if (this.lockfile.getPackageID(name_hash, null, &res)) |pkg_id| {
|
||||
successFn(this, id, pkg_id);
|
||||
return;
|
||||
}
|
||||
|
||||
const url = switch (version.value.tarball.uri) {
|
||||
.local => |path| this.lockfile.str(&path),
|
||||
.remote => |url| this.lockfile.str(&url),
|
||||
};
|
||||
const task_id = Task.Id.forTarball(url);
|
||||
var entry = this.task_queue.getOrPutContext(this.allocator, task_id, .{}) catch unreachable;
|
||||
if (!entry.found_existing) {
|
||||
entry.value_ptr.* = TaskCallbackList{};
|
||||
}
|
||||
|
||||
if (comptime Environment.allow_assert)
|
||||
debug(
|
||||
"enqueueDependency({d}, {s}, {s}, {s}) = {s}",
|
||||
.{
|
||||
id,
|
||||
@tagName(version.tag),
|
||||
this.lockfile.str(&name),
|
||||
this.lockfile.str(&version.literal),
|
||||
url,
|
||||
},
|
||||
);
|
||||
|
||||
const callback_tag = comptime if (successFn == assignRootResolution) "root_dependency" else "dependency";
|
||||
try entry.value_ptr.append(this.allocator, @unionInit(TaskCallbackContext, callback_tag, id));
|
||||
|
||||
if (dependency.behavior.isPeer()) {
|
||||
if (!install_peer) {
|
||||
try this.peer_dependencies.writeItem(id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (version.value.tarball.uri) {
|
||||
.local => {
|
||||
if (this.hasCreatedNetworkTask(task_id, dependency.behavior.isRequired())) return;
|
||||
|
||||
this.task_batch.push(ThreadPool.Batch.from(this.enqueueLocalTarball(
|
||||
task_id,
|
||||
id,
|
||||
this.lockfile.str(&dependency.name),
|
||||
url,
|
||||
res,
|
||||
)));
|
||||
},
|
||||
.remote => {
|
||||
if (try this.generateNetworkTaskForTarball(
|
||||
task_id,
|
||||
url,
|
||||
dependency.behavior.isRequired(),
|
||||
id,
|
||||
.{
|
||||
.name = dependency.name,
|
||||
.name_hash = dependency.name_hash,
|
||||
.resolution = res,
|
||||
},
|
||||
null,
|
||||
.no_authorization,
|
||||
)) |network_task| {
|
||||
this.enqueueNetworkTask(network_task);
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4345,7 +4250,6 @@ pub const PackageManager = struct {
|
||||
patch_task.schedule(if (patch_task.callback == .apply) &this.patch_apply_batch else &this.patch_calc_hash_batch);
|
||||
}
|
||||
}
|
||||
|
||||
fn doFlushDependencyQueue(this: *PackageManager) void {
|
||||
var lockfile = this.lockfile;
|
||||
var dependency_queue = &lockfile.scratch.dependency_list_queue;
|
||||
@@ -4820,7 +4724,6 @@ pub const PackageManager = struct {
|
||||
var fallback_parts = [_]string{"node_modules/.bun-cache"};
|
||||
return CacheDir{ .is_node_modules = true, .path = Fs.FileSystem.instance.abs(&fallback_parts) };
|
||||
}
|
||||
|
||||
pub fn runTasks(
|
||||
manager: *PackageManager,
|
||||
comptime ExtractCompletionContext: type,
|
||||
@@ -5166,7 +5069,7 @@ pub const PackageManager = struct {
|
||||
.{
|
||||
@errorName(err),
|
||||
extract.name.slice(),
|
||||
extract.resolution.fmt(manager.lockfile.buffers.string_bytes.items, .auto),
|
||||
extract.resolution.fmt(manager.lockfile.buffers.string_bytes, .auto),
|
||||
},
|
||||
) catch bun.outOfMemory();
|
||||
}
|
||||
@@ -5266,7 +5169,6 @@ pub const PackageManager = struct {
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
var resolve_tasks_batch = manager.resolve_tasks.popBatch();
|
||||
var resolve_tasks_iter = resolve_tasks_batch.iterator();
|
||||
while (resolve_tasks_iter.next()) |task| {
|
||||
@@ -5444,7 +5346,7 @@ pub const PackageManager = struct {
|
||||
const dependency_list = dependency_list_entry.value_ptr.*;
|
||||
dependency_list_entry.value_ptr.* = .{};
|
||||
|
||||
try manager.processDependencyList(dependency_list, void, {}, {}, install_peer);
|
||||
try manager.processDependencyList(dependency_list, ExtractCompletionContext, extract_ctx, callbacks, install_peer);
|
||||
}
|
||||
|
||||
manager.setPreinstallState(package_id, manager.lockfile, .done);
|
||||
@@ -5461,180 +5363,6 @@ 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();
|
||||
|
||||
manager.git_repositories.put(manager.allocator, task.id, repo_fd) catch unreachable;
|
||||
|
||||
if (task.status == .fail) {
|
||||
const err = task.err orelse error.Failed;
|
||||
|
||||
if (@TypeOf(callbacks.onPackageManifestError) != void) {
|
||||
callbacks.onPackageManifestError(
|
||||
extract_ctx,
|
||||
name,
|
||||
err,
|
||||
url,
|
||||
);
|
||||
} else if (log_level != .silent) {
|
||||
manager.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
manager.allocator,
|
||||
"{s} cloning repository for <b>{s}<r>",
|
||||
.{
|
||||
@errorName(err),
|
||||
name,
|
||||
},
|
||||
) catch bun.outOfMemory();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
},
|
||||
.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;
|
||||
|
||||
manager.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
manager.allocator,
|
||||
"{s} checking out repository for <b>{s}<r>",
|
||||
.{
|
||||
@errorName(err),
|
||||
alias.slice(),
|
||||
},
|
||||
) catch bun.outOfMemory();
|
||||
|
||||
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
|
||||
|
||||
// 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();
|
||||
|
||||
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: {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5773,7 +5501,6 @@ pub const PackageManager = struct {
|
||||
}
|
||||
Global.crash();
|
||||
}
|
||||
|
||||
pub fn init(
|
||||
ctx: Command.Context,
|
||||
cli: CommandLineArguments,
|
||||
@@ -6374,7 +6101,6 @@ pub const PackageManager = struct {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const CommandLineArguments = @import("./PackageManager/CommandLineArguments.zig");
|
||||
|
||||
pub fn link(ctx: Command.Context) !void {
|
||||
@@ -6557,7 +6283,6 @@ pub const PackageManager = struct {
|
||||
try manager.updatePackageJSONAndInstallWithManager(ctx, original_cwd);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unlink(ctx: Command.Context) !void {
|
||||
const cli = try PackageManager.CommandLineArguments.parse(ctx.allocator, .unlink);
|
||||
var manager, const original_cwd = PackageManager.init(ctx, cli, .unlink) catch |err| brk: {
|
||||
@@ -6784,183 +6509,6 @@ pub const PackageManager = struct {
|
||||
) []UpdateRequest {
|
||||
return parseWithError(allocator, pm, log, positionals, update_requests, subcommand, true) catch Global.crash();
|
||||
}
|
||||
|
||||
fn parseWithError(
|
||||
allocator: std.mem.Allocator,
|
||||
pm: ?*PackageManager,
|
||||
log: *logger.Log,
|
||||
positionals: []const string,
|
||||
update_requests: *Array,
|
||||
subcommand: Subcommand,
|
||||
fatal: bool,
|
||||
) ![]UpdateRequest {
|
||||
// first one is always either:
|
||||
// add
|
||||
// remove
|
||||
outer: for (positionals) |positional| {
|
||||
var input: []u8 = bun.default_allocator.dupe(u8, std.mem.trim(u8, positional, " \n\r\t")) catch bun.outOfMemory();
|
||||
{
|
||||
var temp: [2048]u8 = undefined;
|
||||
const len = std.mem.replace(u8, input, "\\\\", "/", &temp);
|
||||
bun.path.platformToPosixInPlace(u8, &temp);
|
||||
const input2 = temp[0 .. input.len - len];
|
||||
@memcpy(input[0..input2.len], input2);
|
||||
input.len = input2.len;
|
||||
}
|
||||
switch (subcommand) {
|
||||
.link, .unlink => if (!strings.hasPrefixComptime(input, "link:")) {
|
||||
input = std.fmt.allocPrint(allocator, "{0s}@link:{0s}", .{input}) catch unreachable;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
var value = input;
|
||||
var alias: ?string = null;
|
||||
if (!Dependency.isTarball(input) and strings.isNPMPackageName(input)) {
|
||||
alias = input;
|
||||
value = input[input.len..];
|
||||
} else if (input.len > 1) {
|
||||
if (strings.indexOfChar(input[1..], '@')) |at| {
|
||||
const name = input[0 .. at + 1];
|
||||
if (strings.isNPMPackageName(name)) {
|
||||
alias = name;
|
||||
value = input[at + 2 ..];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const placeholder = String.from("@@@");
|
||||
var version = Dependency.parseWithOptionalTag(
|
||||
allocator,
|
||||
if (alias) |name| String.init(input, name) else placeholder,
|
||||
if (alias) |name| String.Builder.stringHash(name) else null,
|
||||
value,
|
||||
null,
|
||||
&SlicedString.init(input, value),
|
||||
log,
|
||||
pm,
|
||||
) orelse {
|
||||
if (fatal) {
|
||||
Output.errGeneric("unrecognised dependency format: {s}", .{
|
||||
positional,
|
||||
});
|
||||
} else {
|
||||
log.addErrorFmt(null, logger.Loc.Empty, allocator, "unrecognised dependency format: {s}", .{
|
||||
positional,
|
||||
}) catch bun.outOfMemory();
|
||||
}
|
||||
|
||||
return error.UnrecognizedDependencyFormat;
|
||||
};
|
||||
if (alias != null and version.tag == .git) {
|
||||
if (Dependency.parseWithOptionalTag(
|
||||
allocator,
|
||||
placeholder,
|
||||
null,
|
||||
input,
|
||||
null,
|
||||
&SlicedString.init(input, input),
|
||||
log,
|
||||
pm,
|
||||
)) |ver| {
|
||||
alias = null;
|
||||
version = ver;
|
||||
}
|
||||
}
|
||||
if (switch (version.tag) {
|
||||
.dist_tag => version.value.dist_tag.name.eql(placeholder, input, input),
|
||||
.npm => version.value.npm.name.eql(placeholder, input, input),
|
||||
else => false,
|
||||
}) {
|
||||
if (fatal) {
|
||||
Output.errGeneric("unrecognised dependency format: {s}", .{
|
||||
positional,
|
||||
});
|
||||
} else {
|
||||
log.addErrorFmt(null, logger.Loc.Empty, allocator, "unrecognised dependency format: {s}", .{
|
||||
positional,
|
||||
}) catch bun.outOfMemory();
|
||||
}
|
||||
|
||||
return error.UnrecognizedDependencyFormat;
|
||||
}
|
||||
|
||||
var request = UpdateRequest{
|
||||
.version = version,
|
||||
.version_buf = input,
|
||||
};
|
||||
if (alias) |name| {
|
||||
request.is_aliased = true;
|
||||
request.name = allocator.dupe(u8, name) catch unreachable;
|
||||
request.name_hash = String.Builder.stringHash(name);
|
||||
} else if (version.tag == .github and version.value.github.committish.isEmpty()) {
|
||||
request.name_hash = String.Builder.stringHash(version.literal.slice(input));
|
||||
} else {
|
||||
request.name_hash = String.Builder.stringHash(version.literal.slice(input));
|
||||
}
|
||||
|
||||
for (update_requests.items) |*prev| {
|
||||
if (prev.name_hash == request.name_hash and request.name.len == prev.name.len) continue :outer;
|
||||
}
|
||||
update_requests.append(allocator, request) catch bun.outOfMemory();
|
||||
}
|
||||
|
||||
return update_requests.items;
|
||||
}
|
||||
};
|
||||
|
||||
fn updatePackageJSONAndInstall(
|
||||
ctx: Command.Context,
|
||||
subcommand: Subcommand,
|
||||
) !void {
|
||||
var cli = switch (subcommand) {
|
||||
inline else => |cmd| try PackageManager.CommandLineArguments.parse(ctx.allocator, cmd),
|
||||
};
|
||||
|
||||
// The way this works:
|
||||
// 1. Run the bundler on source files
|
||||
// 2. Rewrite positional arguments to act identically to the developer
|
||||
// typing in the dependency names
|
||||
// 3. Run the install command
|
||||
if (cli.analyze) {
|
||||
const Analyzer = struct {
|
||||
ctx: Command.Context,
|
||||
cli: *PackageManager.CommandLineArguments,
|
||||
subcommand: Subcommand,
|
||||
pub fn onAnalyze(
|
||||
this: *@This(),
|
||||
result: *bun.bundle_v2.BundleV2.DependenciesScanner.Result,
|
||||
) anyerror!void {
|
||||
// TODO: add separate argument that makes it so positionals[1..] is not done and instead the positionals are passed
|
||||
var positionals = bun.default_allocator.alloc(string, result.dependencies.keys().len + 1) catch bun.outOfMemory();
|
||||
positionals[0] = "add";
|
||||
bun.copy(string, positionals[1..], result.dependencies.keys());
|
||||
this.cli.positionals = positionals;
|
||||
|
||||
try updatePackageJSONAndInstallAndCLI(this.ctx, this.subcommand, this.cli.*);
|
||||
|
||||
Global.exit(0);
|
||||
}
|
||||
};
|
||||
var analyzer = Analyzer{
|
||||
.ctx = ctx,
|
||||
.cli = &cli,
|
||||
.subcommand = subcommand,
|
||||
};
|
||||
var fetcher = bun.bundle_v2.BundleV2.DependenciesScanner{
|
||||
.ctx = &analyzer,
|
||||
.entry_points = cli.positionals[1..],
|
||||
.onFetch = @ptrCast(&Analyzer.onAnalyze),
|
||||
};
|
||||
|
||||
// This runs the bundler.
|
||||
try bun.CLI.BuildCommand.exec(bun.CLI.Command.get(), &fetcher);
|
||||
return;
|
||||
}
|
||||
|
||||
return updatePackageJSONAndInstallAndCLI(ctx, subcommand, cli);
|
||||
}
|
||||
|
||||
fn updatePackageJSONAndInstallAndCLI(
|
||||
ctx: Command.Context,
|
||||
subcommand: Subcommand,
|
||||
@@ -7606,7 +7154,6 @@ pub const PackageManager = struct {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn nodeModulesFolderForDependencyIDs(iterator: *Lockfile.Tree.Iterator(.node_modules), ids: []const IdPair) !?Lockfile.Tree.Iterator(.node_modules).Next {
|
||||
while (iterator.next(null)) |node_modules| {
|
||||
for (ids) |id| {
|
||||
@@ -7794,7 +7341,6 @@ pub const PackageManager = struct {
|
||||
const rel_path: []const u8 = workspace_res.value.workspace.slice(lockfile.buffers.string_bytes.items);
|
||||
return bun.default_allocator.dupe(u8, bun.path.join(&[_][]const u8{ rel_path, argument }, .posix)) catch bun.outOfMemory();
|
||||
}
|
||||
|
||||
/// 1. Arg is either:
|
||||
/// - name and possibly version (e.g. "is-even" or "is-even@1.0.0")
|
||||
/// - path to package in node_modules
|
||||
@@ -7985,7 +7531,6 @@ pub const PackageManager = struct {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
fn overwritePackageInNodeModulesFolder(
|
||||
manager: *PackageManager,
|
||||
cache_dir: std.fs.Dir,
|
||||
@@ -8159,7 +7704,6 @@ pub const PackageManager = struct {
|
||||
patchfile_path: []const u8,
|
||||
not_in_workspace_root: bool = false,
|
||||
};
|
||||
|
||||
/// - Arg is the dir containing the package with changes OR name and version
|
||||
/// - Get the patch file contents by running git diff on the temp dir and the original package dir
|
||||
/// - Write the patch file to $PATCHES_DIR/$PKG_NAME_AND_VERSION.patch
|
||||
@@ -8786,7 +8330,6 @@ pub const PackageManager = struct {
|
||||
manager.total_tasks += count;
|
||||
return manager.pending_tasks.fetchAdd(count, .monotonic);
|
||||
}
|
||||
|
||||
pub inline fn decrementPendingTasks(manager: *PackageManager) u32 {
|
||||
return manager.pending_tasks.fetchSub(1, .monotonic);
|
||||
}
|
||||
@@ -8813,7 +8356,6 @@ pub const PackageManager = struct {
|
||||
manager.downloads_node.?.activate();
|
||||
manager.progress.refresh();
|
||||
}
|
||||
|
||||
pub fn endProgressBar(manager: *PackageManager) void {
|
||||
var downloads_node = manager.downloads_node orelse return;
|
||||
downloads_node.setEstimatedTotalItems(downloads_node.unprotected_estimated_total_items);
|
||||
@@ -9561,7 +9103,6 @@ pub const PackageManager = struct {
|
||||
}
|
||||
}
|
||||
defer workspace_filters.deinit(manager.allocator);
|
||||
|
||||
var install_root_dependencies = workspace_filters.items.len == 0;
|
||||
if (!install_root_dependencies) {
|
||||
const pkg_names = manager.lockfile.packages.items(.name);
|
||||
@@ -10081,9 +9622,7 @@ pub const PackageManager = struct {
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
const EnqueuePackageForDownloadError = NetworkTask.ForTarballError;
|
||||
|
||||
pub fn enqueuePackageForDownload(
|
||||
this: *PackageManager,
|
||||
name: []const u8,
|
||||
@@ -10195,4 +9734,4 @@ pub const PackageManifestError = error{
|
||||
PackageManifestHTTP5xx,
|
||||
};
|
||||
|
||||
pub const LifecycleScriptSubprocess = @import("./lifecycle_script_runner.zig").LifecycleScriptSubprocess;
|
||||
pub const LifecycleScriptSubprocess = @import("./lifecycle_script_runner.zig").LifecycleScriptSubprocess;
|
||||
@@ -7,6 +7,7 @@ const FileSystem = @import("../fs.zig").FileSystem;
|
||||
const Install = @import("./install.zig");
|
||||
const ExtractData = Install.ExtractData;
|
||||
const PackageManager = Install.PackageManager;
|
||||
const DependencyID = Install.DependencyID;
|
||||
const Semver = bun.Semver;
|
||||
const String = Semver.String;
|
||||
const std = @import("std");
|
||||
@@ -16,6 +17,8 @@ const GitSHA = String;
|
||||
const Path = bun.path;
|
||||
const File = bun.sys.File;
|
||||
const OOM = bun.OOM;
|
||||
const Output = bun.Output;
|
||||
const JSC = bun.JSC;
|
||||
|
||||
threadlocal var final_path_buf: bun.PathBuffer = undefined;
|
||||
threadlocal var ssh_path_buf: bun.PathBuffer = undefined;
|
||||
@@ -453,7 +456,7 @@ pub const Repository = extern struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn download(
|
||||
pub fn downloadSync(
|
||||
allocator: std.mem.Allocator,
|
||||
env: DotEnv.Map,
|
||||
log: *logger.Log,
|
||||
@@ -516,7 +519,158 @@ pub const Repository = extern struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn findCommit(
|
||||
pub fn download(
|
||||
pm: *PackageManager,
|
||||
cache_dir: std.fs.Dir,
|
||||
task_id: u64,
|
||||
name: string,
|
||||
url: string,
|
||||
attempt: u8,
|
||||
dep_id: DependencyID,
|
||||
) void {
|
||||
bun.Analytics.Features.git_dependencies += 1;
|
||||
const folder_name = std.fmt.bufPrintZ(&folder_name_buf, "{any}.git", .{
|
||||
bun.fmt.hexIntLower(task_id),
|
||||
}) catch |err| {
|
||||
pm.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
pm.allocator,
|
||||
"Failed to format git folder name: {s}",
|
||||
.{@errorName(err)},
|
||||
) catch unreachable;
|
||||
pm.onGitDownloadComplete(task_id, err, .{
|
||||
.name = name,
|
||||
.url = url,
|
||||
.task_id = task_id,
|
||||
.attempt = attempt,
|
||||
.cache_dir = cache_dir,
|
||||
.dep_id = dep_id,
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
// Try to open existing directory first
|
||||
if (cache_dir.openDirZ(folder_name, .{})) |dir| {
|
||||
// Directory exists, do a fetch
|
||||
const path = Path.joinAbsString(pm.cache_directory_path, &.{folder_name}, .auto);
|
||||
|
||||
// Convert env map for spawning
|
||||
var env_map = shared_env.get(pm.allocator, &pm.env);
|
||||
const envp = env_map.stdEnvMap(pm.allocator).unwrap() catch |err| {
|
||||
pm.onGitDownloadComplete(task_id, err, .{
|
||||
.name = name,
|
||||
.url = url,
|
||||
.task_id = task_id,
|
||||
.attempt = attempt,
|
||||
.cache_dir = cache_dir,
|
||||
.dep_id = dep_id,
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
const runner = GitRunner.new(pm, envp.items.ptr, .{
|
||||
.download = .{
|
||||
.name = name,
|
||||
.url = url,
|
||||
.task_id = task_id,
|
||||
.attempt = attempt,
|
||||
.cache_dir = dir,
|
||||
.dep_id = dep_id,
|
||||
},
|
||||
});
|
||||
|
||||
runner.spawn(&[_]string{ "git", "-C", path, "fetch", "--quiet" }) catch |err| {
|
||||
pm.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
pm.allocator,
|
||||
"Failed to spawn git fetch for \"{s}\": {s}",
|
||||
.{name, @errorName(err)},
|
||||
) catch unreachable;
|
||||
runner.deinit();
|
||||
pm.onGitDownloadComplete(task_id, err, .{
|
||||
.name = name,
|
||||
.url = url,
|
||||
.task_id = task_id,
|
||||
.attempt = attempt,
|
||||
.cache_dir = cache_dir,
|
||||
.dep_id = dep_id,
|
||||
});
|
||||
};
|
||||
} else |not_found| {
|
||||
if (not_found != error.FileNotFound) {
|
||||
pm.onGitDownloadComplete(task_id, not_found, .{
|
||||
.name = name,
|
||||
.url = url,
|
||||
.task_id = task_id,
|
||||
.attempt = attempt,
|
||||
.cache_dir = cache_dir,
|
||||
.dep_id = dep_id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Clone new repository
|
||||
const target = Path.joinAbsString(pm.cache_directory_path, &.{folder_name}, .auto);
|
||||
|
||||
// Convert env map for spawning
|
||||
var env_map = shared_env.get(pm.allocator, &pm.env);
|
||||
const envp = env_map.stdEnvMap(pm.allocator).unwrap() catch |err| {
|
||||
pm.onGitDownloadComplete(task_id, err, .{
|
||||
.name = name,
|
||||
.url = url,
|
||||
.task_id = task_id,
|
||||
.attempt = attempt,
|
||||
.cache_dir = cache_dir,
|
||||
.dep_id = dep_id,
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
const runner = GitRunner.new(pm, envp.items.ptr, .{
|
||||
.download = .{
|
||||
.name = name,
|
||||
.url = url,
|
||||
.task_id = task_id,
|
||||
.attempt = attempt,
|
||||
.cache_dir = cache_dir,
|
||||
.dep_id = dep_id,
|
||||
},
|
||||
});
|
||||
|
||||
runner.spawn(&[_]string{
|
||||
"git",
|
||||
"clone",
|
||||
"-c", "core.longpaths=true",
|
||||
"--quiet",
|
||||
"--bare",
|
||||
url,
|
||||
target,
|
||||
}) catch |err| {
|
||||
if (err == error.RepositoryNotFound or attempt > 1) {
|
||||
pm.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
pm.allocator,
|
||||
"Failed to spawn git clone for \"{s}\": {s}",
|
||||
.{name, @errorName(err)},
|
||||
) catch unreachable;
|
||||
}
|
||||
runner.deinit();
|
||||
pm.onGitDownloadComplete(task_id, err, .{
|
||||
.name = name,
|
||||
.url = url,
|
||||
.task_id = task_id,
|
||||
.attempt = attempt,
|
||||
.cache_dir = cache_dir,
|
||||
.dep_id = dep_id,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn findCommitSync(
|
||||
allocator: std.mem.Allocator,
|
||||
env: *DotEnv.Loader,
|
||||
log: *logger.Log,
|
||||
@@ -550,7 +704,77 @@ pub const Repository = extern struct {
|
||||
}, " \t\r\n");
|
||||
}
|
||||
|
||||
pub fn checkout(
|
||||
pub fn findCommit(
|
||||
pm: *PackageManager,
|
||||
repo_dir: std.fs.Dir,
|
||||
name: string,
|
||||
committish: string,
|
||||
task_id: u64,
|
||||
) void {
|
||||
const path = Path.joinAbsString(pm.cache_directory_path, &.{std.fmt.bufPrint(&folder_name_buf, "{any}.git", .{
|
||||
bun.fmt.hexIntLower(task_id),
|
||||
}) catch |err| {
|
||||
pm.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
pm.allocator,
|
||||
"Failed to format git folder name: {s}",
|
||||
.{@errorName(err)},
|
||||
) catch unreachable;
|
||||
pm.onGitFindCommitComplete(task_id, err, .{
|
||||
.name = name,
|
||||
.committish = committish,
|
||||
.task_id = task_id,
|
||||
.repo_dir = repo_dir,
|
||||
});
|
||||
return;
|
||||
}}, .auto);
|
||||
|
||||
// Convert env map for spawning
|
||||
var env_map = shared_env.get(pm.allocator, &pm.env);
|
||||
const envp = env_map.stdEnvMap(pm.allocator).unwrap() catch |err| {
|
||||
pm.onGitFindCommitComplete(task_id, err, .{
|
||||
.name = name,
|
||||
.committish = committish,
|
||||
.task_id = task_id,
|
||||
.repo_dir = repo_dir,
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
const runner = GitRunner.new(pm, envp.items.ptr, .{
|
||||
.find_commit = .{
|
||||
.name = name,
|
||||
.committish = committish,
|
||||
.task_id = task_id,
|
||||
.repo_dir = repo_dir,
|
||||
},
|
||||
});
|
||||
|
||||
const argv = if (committish.len > 0)
|
||||
&[_]string{ "git", "-C", path, "log", "--format=%H", "-1", committish }
|
||||
else
|
||||
&[_]string{ "git", "-C", path, "log", "--format=%H", "-1" };
|
||||
|
||||
runner.spawn(argv) catch |err| {
|
||||
pm.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
pm.allocator,
|
||||
"Failed to spawn git log for \"{s}\": {s}",
|
||||
.{name, @errorName(err)},
|
||||
) catch unreachable;
|
||||
runner.deinit();
|
||||
pm.onGitFindCommitComplete(task_id, err, .{
|
||||
.name = name,
|
||||
.committish = committish,
|
||||
.task_id = task_id,
|
||||
.repo_dir = repo_dir,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn checkoutSync(
|
||||
allocator: std.mem.Allocator,
|
||||
env: DotEnv.Map,
|
||||
log: *logger.Log,
|
||||
@@ -657,4 +881,619 @@ pub const Repository = extern struct {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn checkout(
|
||||
pm: *PackageManager,
|
||||
cache_dir: std.fs.Dir,
|
||||
repo_dir: std.fs.Dir,
|
||||
name: string,
|
||||
url: string,
|
||||
resolved: string,
|
||||
) void {
|
||||
bun.Analytics.Features.git_dependencies += 1;
|
||||
const folder_name = PackageManager.cachedGitFolderNamePrint(&folder_name_buf, resolved, null);
|
||||
|
||||
// Check if package already exists
|
||||
if (bun.openDir(cache_dir, folder_name)) |package_dir| {
|
||||
// Package already exists, read package.json and complete
|
||||
defer package_dir.close();
|
||||
|
||||
const json_file, const json_buf = bun.sys.File.readFileFrom(package_dir, "package.json", pm.allocator).unwrap() catch |err| {
|
||||
if (err == error.ENOENT) {
|
||||
// allow git dependencies without package.json
|
||||
pm.onGitCheckoutComplete(0, .{
|
||||
.url = url,
|
||||
.resolved = resolved,
|
||||
}, .{
|
||||
.name = name,
|
||||
.url = url,
|
||||
.resolved = resolved,
|
||||
.cache_dir = cache_dir,
|
||||
.repo_dir = repo_dir,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
pm.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
pm.allocator,
|
||||
"\"package.json\" for \"{s}\" failed to open: {s}",
|
||||
.{ name, @errorName(err) },
|
||||
) catch unreachable;
|
||||
pm.onGitCheckoutComplete(0, error.InstallFailed, .{
|
||||
.name = name,
|
||||
.url = url,
|
||||
.resolved = resolved,
|
||||
.cache_dir = cache_dir,
|
||||
.repo_dir = repo_dir,
|
||||
});
|
||||
return;
|
||||
};
|
||||
defer json_file.close();
|
||||
|
||||
const json_path = json_file.getPath(&json_path_buf).unwrap() catch |err| {
|
||||
pm.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
pm.allocator,
|
||||
"\"package.json\" for \"{s}\" failed to resolve: {s}",
|
||||
.{ name, @errorName(err) },
|
||||
) catch unreachable;
|
||||
pm.onGitCheckoutComplete(0, error.InstallFailed, .{
|
||||
.name = name,
|
||||
.url = url,
|
||||
.resolved = resolved,
|
||||
.cache_dir = cache_dir,
|
||||
.repo_dir = repo_dir,
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
const ret_json_path = FileSystem.instance.dirname_store.append(@TypeOf(json_path), json_path) catch |err| {
|
||||
pm.onGitCheckoutComplete(0, err, .{
|
||||
.name = name,
|
||||
.url = url,
|
||||
.resolved = resolved,
|
||||
.cache_dir = cache_dir,
|
||||
.repo_dir = repo_dir,
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
pm.onGitCheckoutComplete(0, .{
|
||||
.url = url,
|
||||
.resolved = resolved,
|
||||
.json = .{
|
||||
.path = ret_json_path,
|
||||
.buf = json_buf,
|
||||
},
|
||||
}, .{
|
||||
.name = name,
|
||||
.url = url,
|
||||
.resolved = resolved,
|
||||
.cache_dir = cache_dir,
|
||||
.repo_dir = repo_dir,
|
||||
});
|
||||
return;
|
||||
} else |_| {
|
||||
// Need to clone and checkout
|
||||
const target = Path.joinAbsString(pm.cache_directory_path, &.{folder_name}, .auto);
|
||||
const repo_path = bun.getFdPath(.fromStdDir(repo_dir), &final_path_buf) catch |err| {
|
||||
pm.onGitCheckoutComplete(0, err, .{
|
||||
.name = name,
|
||||
.url = url,
|
||||
.resolved = resolved,
|
||||
.cache_dir = cache_dir,
|
||||
.repo_dir = repo_dir,
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
// Convert env map for spawning
|
||||
var env_map = shared_env.get(pm.allocator, &pm.env);
|
||||
const envp = env_map.stdEnvMap(pm.allocator).unwrap() catch |err| {
|
||||
pm.onGitCheckoutComplete(0, err, .{
|
||||
.name = name,
|
||||
.url = url,
|
||||
.resolved = resolved,
|
||||
.cache_dir = cache_dir,
|
||||
.repo_dir = repo_dir,
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
// First do the clone
|
||||
const runner = GitRunner.new(pm, envp.items.ptr, .{
|
||||
.checkout = .{
|
||||
.name = name,
|
||||
.url = url,
|
||||
.resolved = resolved,
|
||||
.cache_dir = cache_dir,
|
||||
.repo_dir = repo_dir,
|
||||
},
|
||||
});
|
||||
|
||||
runner.spawn(&[_]string{
|
||||
"git",
|
||||
"clone",
|
||||
"-c", "core.longpaths=true",
|
||||
"--quiet",
|
||||
"--no-checkout",
|
||||
repo_path,
|
||||
target,
|
||||
}) catch |err| {
|
||||
pm.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
pm.allocator,
|
||||
"Failed to spawn git clone for checkout of \"{s}\": {s}",
|
||||
.{name, @errorName(err)},
|
||||
) catch unreachable;
|
||||
runner.deinit();
|
||||
pm.onGitCheckoutComplete(0, err, .{
|
||||
.name = name,
|
||||
.url = url,
|
||||
.resolved = resolved,
|
||||
.cache_dir = cache_dir,
|
||||
.repo_dir = repo_dir,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const GitRunner = struct {
|
||||
/// The spawned git process handle
|
||||
process: ?*bun.spawn.Process = null,
|
||||
|
||||
/// Buffer stdout from the process
|
||||
stdout: bun.io.BufferedReader = bun.io.BufferedReader.init(@This()),
|
||||
|
||||
/// Buffer stderr from the process
|
||||
stderr: bun.io.BufferedReader = bun.io.BufferedReader.init(@This()),
|
||||
|
||||
/// Reference to the package manager for event loop and completion callbacks
|
||||
manager: *PackageManager,
|
||||
|
||||
/// Track open file descriptors for stdout/stderr
|
||||
remaining_fds: i8 = 0,
|
||||
|
||||
/// Track if process exit has been called
|
||||
has_called_process_exit: bool = false,
|
||||
|
||||
/// For checkout operations, track if we're in clone or checkout phase
|
||||
checkout_phase: enum { clone, checkout } = .clone,
|
||||
|
||||
/// Context for what to do when the command completes
|
||||
completion_context: union(enum) {
|
||||
download: struct {
|
||||
name: string,
|
||||
url: string,
|
||||
task_id: u64,
|
||||
attempt: u8,
|
||||
cache_dir: std.fs.Dir,
|
||||
dep_id: DependencyID,
|
||||
},
|
||||
find_commit: struct {
|
||||
name: string,
|
||||
committish: string,
|
||||
task_id: u64,
|
||||
repo_dir: std.fs.Dir,
|
||||
},
|
||||
checkout: struct {
|
||||
name: string,
|
||||
url: string,
|
||||
resolved: string,
|
||||
cache_dir: std.fs.Dir,
|
||||
repo_dir: std.fs.Dir,
|
||||
},
|
||||
},
|
||||
|
||||
/// Environment variables for the process
|
||||
envp: [:null]?[*:0]const u8,
|
||||
|
||||
/// Arguments allocated for null-termination
|
||||
argv_storage: std.ArrayList([:0]const u8),
|
||||
|
||||
/// Create a new GitRunner instance
|
||||
pub fn new(
|
||||
manager: *PackageManager,
|
||||
envp: [:null]?[*:0]const u8,
|
||||
completion_context: @TypeOf(completion_context),
|
||||
) *GitRunner {
|
||||
const runner = bun.new(GitRunner, .{
|
||||
.manager = manager,
|
||||
.envp = envp,
|
||||
.completion_context = completion_context,
|
||||
.argv_storage = std.ArrayList([:0]const u8).init(manager.allocator),
|
||||
});
|
||||
return runner;
|
||||
}
|
||||
|
||||
pub fn loop(this: *const GitRunner) *bun.uws.Loop {
|
||||
return this.manager.event_loop.loop();
|
||||
}
|
||||
|
||||
pub fn eventLoop(this: *const GitRunner) *JSC.AnyEventLoop {
|
||||
return &this.manager.event_loop;
|
||||
}
|
||||
|
||||
/// Spawn the git process with the given arguments
|
||||
pub fn spawn(this: *GitRunner, argv: []const string) !void {
|
||||
this.stdout.setParent(this);
|
||||
this.stderr.setParent(this);
|
||||
|
||||
const spawn_options = bun.spawn.SpawnOptions{
|
||||
.stdin = .ignore,
|
||||
.stdout = if (Environment.isPosix) .buffer else .{ .buffer = undefined }, // TODO: Windows pipe setup
|
||||
.stderr = if (Environment.isPosix) .buffer else .{ .buffer = undefined },
|
||||
.cwd = null, // git commands use absolute paths
|
||||
.windows = if (Environment.isWindows) .{
|
||||
.loop = JSC.EventLoopHandle.init(&this.manager.event_loop),
|
||||
} else {},
|
||||
.stream = false,
|
||||
};
|
||||
|
||||
this.remaining_fds = 0;
|
||||
|
||||
// Convert argv to null-terminated for spawn
|
||||
var argv_buf: [32]?[*:0]const u8 = undefined;
|
||||
for (argv, 0..) |arg, i| {
|
||||
const duped = try this.manager.allocator.dupeZ(u8, arg);
|
||||
try this.argv_storage.append(duped);
|
||||
argv_buf[i] = duped.ptr;
|
||||
}
|
||||
argv_buf[argv.len] = null;
|
||||
|
||||
var spawned = try (try bun.spawn.spawnProcess(&spawn_options, @ptrCast(&argv_buf), this.envp)).unwrap();
|
||||
|
||||
// Set up stdout/stderr readers
|
||||
if (comptime Environment.isPosix) {
|
||||
if (spawned.stdout) |stdout| {
|
||||
if (!spawned.memfds[1]) {
|
||||
_ = bun.sys.setNonblocking(stdout);
|
||||
this.remaining_fds += 1;
|
||||
try this.stdout.start(stdout, true).unwrap();
|
||||
} else {
|
||||
this.stdout.startMemfd(stdout);
|
||||
}
|
||||
}
|
||||
if (spawned.stderr) |stderr| {
|
||||
if (!spawned.memfds[2]) {
|
||||
_ = bun.sys.setNonblocking(stderr);
|
||||
this.remaining_fds += 1;
|
||||
try this.stderr.start(stderr, true).unwrap();
|
||||
} else {
|
||||
this.stderr.startMemfd(stderr);
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Windows pipe handling
|
||||
|
||||
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 => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when a BufferedReader finishes
|
||||
pub fn onReaderDone(this: *GitRunner) void {
|
||||
bun.assert(this.remaining_fds > 0);
|
||||
this.remaining_fds -= 1;
|
||||
this.maybeFinished();
|
||||
}
|
||||
|
||||
/// Called when a BufferedReader encounters an error
|
||||
pub fn onReaderError(this: *GitRunner, err: bun.sys.Error) void {
|
||||
bun.assert(this.remaining_fds > 0);
|
||||
this.remaining_fds -= 1;
|
||||
|
||||
const name = switch (this.completion_context) {
|
||||
.download => |ctx| ctx.name,
|
||||
.find_commit => |ctx| ctx.name,
|
||||
.checkout => |ctx| ctx.name,
|
||||
};
|
||||
|
||||
Output.prettyErrorln("<r><red>error<r>: Failed to read git output for \"<b>{s}<r>\" due to error <b>{d} {s}<r>", .{
|
||||
name,
|
||||
err.errno,
|
||||
@tagName(err.getErrno()),
|
||||
});
|
||||
Output.flush();
|
||||
this.maybeFinished();
|
||||
}
|
||||
|
||||
/// Called when the process exits
|
||||
pub fn onProcessExit(this: *GitRunner, proc: *bun.spawn.Process, _: bun.spawn.Status, _: *const bun.spawn.Rusage) void {
|
||||
if (this.process != proc) {
|
||||
return;
|
||||
}
|
||||
this.has_called_process_exit = true;
|
||||
this.maybeFinished();
|
||||
}
|
||||
|
||||
/// Check if all I/O and process are complete
|
||||
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);
|
||||
}
|
||||
|
||||
/// Reset for next phase of a multi-step operation
|
||||
fn resetForNextPhase(this: *GitRunner) void {
|
||||
if (this.process) |process| {
|
||||
this.process = null;
|
||||
process.close();
|
||||
process.deref();
|
||||
}
|
||||
|
||||
this.stdout.deinit();
|
||||
this.stderr.deinit();
|
||||
this.stdout = bun.io.BufferedReader.init(@This());
|
||||
this.stderr = bun.io.BufferedReader.init(@This());
|
||||
this.has_called_process_exit = false;
|
||||
this.remaining_fds = 0;
|
||||
|
||||
// Clear argv storage for reuse
|
||||
for (this.argv_storage.items) |arg| {
|
||||
this.manager.allocator.free(arg);
|
||||
}
|
||||
this.argv_storage.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
/// Process the git command result and call appropriate completion callback
|
||||
fn handleExit(this: *GitRunner, status: bun.spawn.Status) void {
|
||||
const allocator = this.manager.allocator;
|
||||
|
||||
switch (status) {
|
||||
.exited => |exit| {
|
||||
if (exit.code == 0) {
|
||||
// Success - process based on context
|
||||
switch (this.completion_context) {
|
||||
.download => |ctx| {
|
||||
defer this.deinit();
|
||||
// For download, open the directory and signal completion
|
||||
const folder_name = std.fmt.bufPrintZ(&folder_name_buf, "{any}.git", .{
|
||||
bun.fmt.hexIntLower(ctx.task_id),
|
||||
}) catch |err| {
|
||||
this.manager.onGitDownloadComplete(ctx.task_id, err, ctx);
|
||||
return;
|
||||
};
|
||||
|
||||
const dir = ctx.cache_dir.openDirZ(folder_name, .{}) catch |err| {
|
||||
this.manager.onGitDownloadComplete(ctx.task_id, err, ctx);
|
||||
return;
|
||||
};
|
||||
|
||||
this.manager.onGitDownloadComplete(ctx.task_id, dir, ctx);
|
||||
},
|
||||
.find_commit => |ctx| {
|
||||
defer this.deinit();
|
||||
// Process stdout to get the commit hash
|
||||
const stdout_buf = this.stdout.finalBuffer();
|
||||
const commit_hash = std.mem.trim(u8, stdout_buf.items, " \t\r\n");
|
||||
|
||||
if (commit_hash.len > 0) {
|
||||
const resolved = allocator.dupe(u8, commit_hash) catch bun.outOfMemory();
|
||||
this.manager.onGitFindCommitComplete(ctx.task_id, resolved, ctx);
|
||||
} else {
|
||||
this.manager.onGitFindCommitComplete(ctx.task_id, error.InstallFailed, ctx);
|
||||
}
|
||||
},
|
||||
.checkout => |ctx| {
|
||||
// Checkout is a two-phase operation
|
||||
if (this.checkout_phase == .clone) {
|
||||
// Clone succeeded, now do checkout
|
||||
this.checkout_phase = .checkout;
|
||||
this.resetForNextPhase();
|
||||
|
||||
const folder = Path.joinAbsString(this.manager.cache_directory_path, &.{
|
||||
PackageManager.cachedGitFolderNamePrint(&folder_name_buf, ctx.resolved, null)
|
||||
}, .auto);
|
||||
|
||||
this.spawn(&[_]string{ "git", "-C", folder, "checkout", "--quiet", ctx.resolved }) catch |err| {
|
||||
this.manager.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
allocator,
|
||||
"Failed to spawn git checkout for \"{s}\": {s}",
|
||||
.{ctx.name, @errorName(err)},
|
||||
) catch unreachable;
|
||||
this.deinit();
|
||||
this.manager.onGitCheckoutComplete(0, err, ctx);
|
||||
};
|
||||
} else {
|
||||
defer this.deinit();
|
||||
// Checkout phase succeeded, clean up and read package.json
|
||||
const folder_name = PackageManager.cachedGitFolderNamePrint(&folder_name_buf, ctx.resolved, null);
|
||||
|
||||
// Open the package directory
|
||||
var package_dir = bun.openDir(ctx.cache_dir, folder_name) catch |err| {
|
||||
this.manager.onGitCheckoutComplete(0, err, ctx);
|
||||
return;
|
||||
};
|
||||
defer package_dir.close();
|
||||
|
||||
// Remove .git directory
|
||||
package_dir.deleteTree(".git") catch {};
|
||||
|
||||
// Write .bun-tag file
|
||||
if (ctx.resolved.len > 0) insert_tag: {
|
||||
const git_tag = package_dir.createFileZ(".bun-tag", .{ .truncate = true }) catch break :insert_tag;
|
||||
defer git_tag.close();
|
||||
git_tag.writeAll(ctx.resolved) catch {
|
||||
package_dir.deleteFileZ(".bun-tag") catch {};
|
||||
};
|
||||
}
|
||||
|
||||
// Read package.json
|
||||
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
|
||||
this.manager.onGitCheckoutComplete(0, .{
|
||||
.url = ctx.url,
|
||||
.resolved = ctx.resolved,
|
||||
}, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
this.manager.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
allocator,
|
||||
"\"package.json\" for \"{s}\" failed to open: {s}",
|
||||
.{ ctx.name, @errorName(err) },
|
||||
) catch unreachable;
|
||||
this.manager.onGitCheckoutComplete(0, error.InstallFailed, ctx);
|
||||
return;
|
||||
};
|
||||
defer json_file.close();
|
||||
|
||||
const json_path = json_file.getPath(&json_path_buf).unwrap() catch |err| {
|
||||
this.manager.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
allocator,
|
||||
"\"package.json\" for \"{s}\" failed to resolve: {s}",
|
||||
.{ ctx.name, @errorName(err) },
|
||||
) catch unreachable;
|
||||
this.manager.onGitCheckoutComplete(0, error.InstallFailed, ctx);
|
||||
return;
|
||||
};
|
||||
|
||||
const ret_json_path = FileSystem.instance.dirname_store.append(@TypeOf(json_path), json_path) catch |err| {
|
||||
this.manager.onGitCheckoutComplete(0, err, ctx);
|
||||
return;
|
||||
};
|
||||
|
||||
this.manager.onGitCheckoutComplete(0, .{
|
||||
.url = ctx.url,
|
||||
.resolved = ctx.resolved,
|
||||
.json = .{
|
||||
.path = ret_json_path,
|
||||
.buf = json_buf,
|
||||
},
|
||||
}, ctx);
|
||||
}
|
||||
},
|
||||
}
|
||||
} else {
|
||||
defer this.deinit();
|
||||
// Check stderr for specific error messages
|
||||
const stderr_buf = this.stderr.finalBuffer();
|
||||
const stderr = stderr_buf.items;
|
||||
|
||||
const err: anyerror = if (
|
||||
(strings.containsComptime(stderr, "remote:") and
|
||||
strings.containsComptime(stderr, "not") and
|
||||
strings.containsComptime(stderr, "found")) or
|
||||
strings.containsComptime(stderr, "does not exist"))
|
||||
{
|
||||
error.RepositoryNotFound;
|
||||
} else {
|
||||
error.InstallFailed;
|
||||
};
|
||||
|
||||
// Log appropriate error messages
|
||||
switch (this.completion_context) {
|
||||
.download => |ctx| {
|
||||
if (err == error.RepositoryNotFound or ctx.attempt > 1) {
|
||||
this.manager.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
allocator,
|
||||
"\"git clone\" for \"{s}\" failed",
|
||||
.{ctx.name},
|
||||
) catch unreachable;
|
||||
}
|
||||
this.manager.onGitDownloadComplete(ctx.task_id, err, ctx);
|
||||
},
|
||||
.find_commit => |ctx| {
|
||||
this.manager.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
allocator,
|
||||
"no commit matching \"{s}\" found for \"{s}\" (but repository exists)",
|
||||
.{ ctx.committish, ctx.name },
|
||||
) catch unreachable;
|
||||
this.manager.onGitFindCommitComplete(ctx.task_id, err, ctx);
|
||||
},
|
||||
.checkout => |ctx| {
|
||||
const op = if (this.checkout_phase == .clone) "git clone" else "git checkout";
|
||||
this.manager.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc.Empty,
|
||||
allocator,
|
||||
"\"{s}\" for \"{s}\" failed",
|
||||
.{op, ctx.name},
|
||||
) catch unreachable;
|
||||
this.manager.onGitCheckoutComplete(0, err, ctx);
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
.err => |err| {
|
||||
defer this.deinit();
|
||||
// Process error
|
||||
switch (this.completion_context) {
|
||||
.download => |ctx| {
|
||||
this.manager.onGitDownloadComplete(ctx.task_id, err, ctx);
|
||||
},
|
||||
.find_commit => |ctx| {
|
||||
this.manager.onGitFindCommitComplete(ctx.task_id, err, ctx);
|
||||
},
|
||||
.checkout => |ctx| {
|
||||
this.manager.onGitCheckoutComplete(0, err, ctx);
|
||||
},
|
||||
}
|
||||
},
|
||||
else => {
|
||||
defer this.deinit();
|
||||
// Other status types treated as errors
|
||||
switch (this.completion_context) {
|
||||
.download => |ctx| {
|
||||
this.manager.onGitDownloadComplete(ctx.task_id, error.InstallFailed, ctx);
|
||||
},
|
||||
.find_commit => |ctx| {
|
||||
this.manager.onGitFindCommitComplete(ctx.task_id, error.InstallFailed, ctx);
|
||||
},
|
||||
.checkout => |ctx| {
|
||||
this.manager.onGitCheckoutComplete(0, error.InstallFailed, ctx);
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Clean up resources
|
||||
pub fn deinit(this: *GitRunner) void {
|
||||
if (this.process) |process| {
|
||||
this.process = null;
|
||||
process.close();
|
||||
process.deref();
|
||||
}
|
||||
|
||||
this.stdout.deinit();
|
||||
this.stderr.deinit();
|
||||
|
||||
// Free argv storage
|
||||
for (this.argv_storage.items) |arg| {
|
||||
this.manager.allocator.free(arg);
|
||||
}
|
||||
this.argv_storage.deinit();
|
||||
|
||||
bun.destroy(this);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user