mirror of
https://github.com/oven-sh/bun
synced 2026-02-11 03:18:53 +00:00
* releases before prereleases * handle different repo hosts * remove log * Update bun-install.test.ts * test for `bun add` * gitlab test * use comptime hash map, another test case * don't need length * bump timeout, use tld * infer git dependencies for https and ssh
337 lines
12 KiB
Zig
337 lines
12 KiB
Zig
const bun = @import("root").bun;
|
|
const Global = bun.Global;
|
|
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 = @import("./semver.zig");
|
|
const ExternalString = Semver.ExternalString;
|
|
const String = Semver.String;
|
|
const std = @import("std");
|
|
const string = @import("../string_types.zig").string;
|
|
const strings = @import("../string_immutable.zig");
|
|
const GitSHA = String;
|
|
|
|
threadlocal var final_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
|
|
threadlocal var folder_name_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
|
|
threadlocal var json_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
|
|
|
|
pub const Repository = extern struct {
|
|
owner: String = .{},
|
|
repo: String = .{},
|
|
committish: GitSHA = .{},
|
|
resolved: GitSHA = .{},
|
|
package_name: String = .{},
|
|
|
|
pub const Hosts = bun.ComptimeStringMap(string, .{
|
|
.{ "bitbucket", ".org" },
|
|
.{ "github", ".com" },
|
|
.{ "gitlab", ".com" },
|
|
});
|
|
|
|
pub fn verify(this: *const Repository) void {
|
|
this.owner.assertDefined();
|
|
this.repo.assertDefined();
|
|
this.committish.assertDefined();
|
|
this.resolved.assertDefined();
|
|
this.package_name.assertDefined();
|
|
}
|
|
|
|
pub fn order(lhs: *const Repository, rhs: *const Repository, lhs_buf: []const u8, rhs_buf: []const u8) std.math.Order {
|
|
const owner_order = lhs.owner.order(&rhs.owner, lhs_buf, rhs_buf);
|
|
if (owner_order != .eq) return owner_order;
|
|
const repo_order = lhs.repo.order(&rhs.repo, lhs_buf, rhs_buf);
|
|
if (repo_order != .eq) return repo_order;
|
|
|
|
return lhs.committish.order(&rhs.committish, lhs_buf, rhs_buf);
|
|
}
|
|
|
|
pub fn count(this: *const Repository, buf: []const u8, comptime StringBuilder: type, builder: StringBuilder) void {
|
|
builder.count(this.owner.slice(buf));
|
|
builder.count(this.repo.slice(buf));
|
|
builder.count(this.committish.slice(buf));
|
|
builder.count(this.resolved.slice(buf));
|
|
builder.count(this.package_name.slice(buf));
|
|
}
|
|
|
|
pub fn clone(this: *const Repository, buf: []const u8, comptime StringBuilder: type, builder: StringBuilder) Repository {
|
|
return .{
|
|
.owner = builder.append(String, this.owner.slice(buf)),
|
|
.repo = builder.append(String, this.repo.slice(buf)),
|
|
.committish = builder.append(GitSHA, this.committish.slice(buf)),
|
|
.resolved = builder.append(String, this.resolved.slice(buf)),
|
|
.package_name = builder.append(String, this.package_name.slice(buf)),
|
|
};
|
|
}
|
|
|
|
pub fn eql(lhs: *const Repository, rhs: *const Repository, lhs_buf: []const u8, rhs_buf: []const u8) bool {
|
|
if (!lhs.owner.eql(rhs.owner, lhs_buf, rhs_buf)) return false;
|
|
if (!lhs.repo.eql(rhs.repo, lhs_buf, rhs_buf)) return false;
|
|
if (lhs.resolved.isEmpty() or rhs.resolved.isEmpty()) return lhs.committish.eql(rhs.committish, lhs_buf, rhs_buf);
|
|
return lhs.resolved.eql(rhs.resolved, lhs_buf, rhs_buf);
|
|
}
|
|
|
|
pub fn formatAs(this: *const Repository, label: string, buf: []const u8, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
|
|
const formatter = Formatter{ .label = label, .repository = this, .buf = buf };
|
|
return try formatter.format(layout, opts, writer);
|
|
}
|
|
|
|
pub const Formatter = struct {
|
|
label: []const u8 = "",
|
|
buf: []const u8,
|
|
repository: *const Repository,
|
|
pub fn format(formatter: Formatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
|
|
if (comptime Environment.allow_assert) std.debug.assert(formatter.label.len > 0);
|
|
try writer.writeAll(formatter.label);
|
|
|
|
const repo = formatter.repository.repo.slice(formatter.buf);
|
|
if (!formatter.repository.owner.isEmpty()) {
|
|
try writer.writeAll(formatter.repository.owner.slice(formatter.buf));
|
|
try writer.writeAll("/");
|
|
} else if (Dependency.isSCPLikePath(repo)) {
|
|
try writer.writeAll("ssh://");
|
|
}
|
|
try writer.writeAll(repo);
|
|
|
|
if (!formatter.repository.resolved.isEmpty()) {
|
|
try writer.writeAll("#");
|
|
var resolved = formatter.repository.resolved.slice(formatter.buf);
|
|
if (strings.lastIndexOfChar(resolved, '-')) |i| {
|
|
resolved = resolved[i + 1 ..];
|
|
}
|
|
try writer.writeAll(resolved);
|
|
} else if (!formatter.repository.committish.isEmpty()) {
|
|
try writer.writeAll("#");
|
|
try writer.writeAll(formatter.repository.committish.slice(formatter.buf));
|
|
}
|
|
}
|
|
};
|
|
|
|
fn exec(allocator: std.mem.Allocator, env: *DotEnv.Loader, cwd_dir: std.fs.Dir, argv: []const string) !string {
|
|
const buf_map = try env.map.cloneToEnvMap(allocator);
|
|
const result = try std.ChildProcess.exec(.{
|
|
.allocator = allocator,
|
|
.argv = argv,
|
|
.cwd_dir = cwd_dir,
|
|
.env_map = &buf_map,
|
|
});
|
|
|
|
switch (result.term) {
|
|
.Exited => |sig| if (sig == 0) return result.stdout,
|
|
else => {},
|
|
}
|
|
return error.InstallFailed;
|
|
}
|
|
|
|
pub fn tryHTTPS(url: string) ?string {
|
|
if (strings.hasPrefixComptime(url, "ssh://")) {
|
|
final_path_buf[0.."https".len].* = "https".*;
|
|
bun.copy(u8, final_path_buf["https".len..], url["ssh".len..]);
|
|
return final_path_buf[0 .. url.len - "ssh".len + "https".len];
|
|
}
|
|
|
|
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 ..]);
|
|
return final_path_buf[0 .. url.len + "https://".len + tld.len];
|
|
}
|
|
}
|
|
|
|
bun.copy(u8, rest, url);
|
|
if (colon_index) |colon| rest[colon] = '/';
|
|
return final_path_buf[0 .. url.len + "https://".len];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
pub fn download(
|
|
allocator: std.mem.Allocator,
|
|
env: *DotEnv.Loader,
|
|
log: *logger.Log,
|
|
cache_dir: std.fs.Dir,
|
|
task_id: u64,
|
|
name: string,
|
|
url: string,
|
|
) !std.fs.Dir {
|
|
const folder_name = try std.fmt.bufPrintZ(&folder_name_buf, "{any}.git", .{
|
|
bun.fmt.hexIntLower(task_id),
|
|
});
|
|
|
|
return if (cache_dir.openDirZ(folder_name, .{}, true)) |dir| fetch: {
|
|
_ = exec(allocator, env, dir, &[_]string{ "git", "fetch", "--quiet" }) catch |err| {
|
|
log.addErrorFmt(
|
|
null,
|
|
logger.Loc.Empty,
|
|
allocator,
|
|
"\"git fetch\" for \"{s}\" failed",
|
|
.{name},
|
|
) catch unreachable;
|
|
return err;
|
|
};
|
|
break :fetch dir;
|
|
} else |not_found| clone: {
|
|
if (not_found != error.FileNotFound) return not_found;
|
|
|
|
_ = exec(allocator, env, cache_dir, &[_]string{
|
|
"git",
|
|
"clone",
|
|
"--quiet",
|
|
"--bare",
|
|
url,
|
|
folder_name,
|
|
}) catch |err| {
|
|
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, .{}, true);
|
|
};
|
|
}
|
|
|
|
pub fn findCommit(
|
|
allocator: std.mem.Allocator,
|
|
env: *DotEnv.Loader,
|
|
log: *logger.Log,
|
|
repo_dir: std.fs.Dir,
|
|
name: string,
|
|
committish: string,
|
|
) !string {
|
|
return std.mem.trim(u8, exec(
|
|
allocator,
|
|
env,
|
|
repo_dir,
|
|
if (committish.len > 0)
|
|
&[_]string{ "git", "log", "--format=%H", "-1", committish }
|
|
else
|
|
&[_]string{ "git", "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");
|
|
}
|
|
|
|
pub fn checkout(
|
|
allocator: std.mem.Allocator,
|
|
env: *DotEnv.Loader,
|
|
log: *logger.Log,
|
|
cache_dir: std.fs.Dir,
|
|
repo_dir: std.fs.Dir,
|
|
name: string,
|
|
url: string,
|
|
resolved: string,
|
|
) !ExtractData {
|
|
const folder_name = PackageManager.cachedGitFolderNamePrint(&folder_name_buf, resolved);
|
|
|
|
var package_dir = cache_dir.openDirZ(folder_name, .{}, true) catch |not_found| brk: {
|
|
if (not_found != error.FileNotFound) return not_found;
|
|
|
|
_ = exec(allocator, env, cache_dir, &[_]string{
|
|
"git",
|
|
"clone",
|
|
"--quiet",
|
|
"--no-checkout",
|
|
try bun.getFdPath(repo_dir.fd, &final_path_buf),
|
|
folder_name,
|
|
}) catch |err| {
|
|
log.addErrorFmt(
|
|
null,
|
|
logger.Loc.Empty,
|
|
allocator,
|
|
"\"git clone\" for \"{s}\" failed",
|
|
.{name},
|
|
) catch unreachable;
|
|
return err;
|
|
};
|
|
|
|
var dir = try cache_dir.openDirZ(folder_name, .{}, true);
|
|
|
|
_ = exec(allocator, env, dir, &[_]string{ "git", "checkout", "--quiet", resolved }) catch |err| {
|
|
log.addErrorFmt(
|
|
null,
|
|
logger.Loc.Empty,
|
|
allocator,
|
|
"\"git checkout\" for \"{s}\" failed",
|
|
.{name},
|
|
) catch unreachable;
|
|
return err;
|
|
};
|
|
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 {};
|
|
};
|
|
}
|
|
|
|
break :brk dir;
|
|
};
|
|
defer package_dir.close();
|
|
|
|
const json_file = package_dir.openFileZ("package.json", .{ .mode = .read_only }) catch |err| {
|
|
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 size = try json_file.getEndPos();
|
|
var json_buf = try allocator.alloc(u8, size + 64);
|
|
const json_len = try json_file.preadAll(json_buf, 0);
|
|
|
|
const json_path = bun.getFdPath(
|
|
json_file.handle,
|
|
&json_path_buf,
|
|
) 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,
|
|
.json_buf = json_buf,
|
|
.json_len = json_len,
|
|
};
|
|
}
|
|
};
|