Skip to content

Commit 2dc85c4

Browse files
authored
[install] support SCP-style git dependencies (#2124)
- try HTTPS first before SSH - improve package name resolution - improve report messages fixes #2119
1 parent 88c238d commit 2dc85c4

File tree

6 files changed

+339
-93
lines changed

6 files changed

+339
-93
lines changed

src/install/dependency.zig

Lines changed: 67 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,68 @@ pub fn toExternal(this: Dependency) External {
159159
return bytes;
160160
}
161161

162+
pub inline fn isSCPLikePath(dependency: string) bool {
163+
// Shortest valid expression: h:p
164+
if (dependency.len < 3) return false;
165+
166+
var at_index: ?usize = null;
167+
168+
for (dependency) |c, i| {
169+
switch (c) {
170+
'@' => {
171+
if (at_index == null) at_index = i;
172+
},
173+
':' => {
174+
if (strings.hasPrefixComptime(dependency[i..], "://")) return false;
175+
return i > if (at_index) |index| index + 1 else 0;
176+
},
177+
'/' => return if (at_index) |index| i > index + 1 else false,
178+
else => {},
179+
}
180+
}
181+
182+
return false;
183+
}
184+
185+
pub inline fn isGitHubRepoPath(dependency: string) bool {
186+
// Shortest valid expression: u/r
187+
if (dependency.len < 3) return false;
188+
189+
var hash_index: usize = 0;
190+
var slash_index: usize = 0;
191+
192+
for (dependency) |c, i| {
193+
switch (c) {
194+
'/' => {
195+
if (i == 0) return false;
196+
if (slash_index > 0) return false;
197+
slash_index = i;
198+
},
199+
'#' => {
200+
if (i == 0) return false;
201+
if (hash_index > 0) return false;
202+
if (slash_index == 0) return false;
203+
hash_index = i;
204+
},
205+
// Not allowed in username
206+
'.', '_' => {
207+
if (slash_index == 0) return false;
208+
},
209+
// Must be alphanumeric
210+
'-', 'a'...'z', 'A'...'Z', '0'...'9' => {},
211+
else => return false,
212+
}
213+
}
214+
215+
return hash_index != dependency.len - 1 and slash_index > 0 and slash_index != dependency.len - 1;
216+
}
217+
218+
// This won't work for query string params, but I'll let someone file an issue
219+
// before I add that.
220+
pub inline fn isTarball(dependency: string) bool {
221+
return strings.endsWithComptime(dependency, ".tgz") or strings.endsWithComptime(dependency, ".tar.gz");
222+
}
223+
162224
pub const Version = struct {
163225
tag: Dependency.Version.Tag = .uninitialized,
164226
literal: String = .{},
@@ -278,45 +340,6 @@ pub const Version = struct {
278340
return @enumToInt(this) < 3;
279341
}
280342

281-
pub inline fn isGitHubRepoPath(dependency: string) bool {
282-
// Shortest valid expression: u/r
283-
if (dependency.len < 3) return false;
284-
285-
var hash_index: usize = 0;
286-
var slash_index: usize = 0;
287-
288-
for (dependency) |c, i| {
289-
switch (c) {
290-
'/' => {
291-
if (i == 0) return false;
292-
if (slash_index > 0) return false;
293-
slash_index = i;
294-
},
295-
'#' => {
296-
if (i == 0) return false;
297-
if (hash_index > 0) return false;
298-
if (slash_index == 0) return false;
299-
hash_index = i;
300-
},
301-
// Not allowed in username
302-
'.', '_' => {
303-
if (slash_index == 0) return false;
304-
},
305-
// Must be alphanumeric
306-
'-', 'a'...'z', 'A'...'Z', '0'...'9' => {},
307-
else => return false,
308-
}
309-
}
310-
311-
return hash_index != dependency.len - 1 and slash_index > 0 and slash_index != dependency.len - 1;
312-
}
313-
314-
// this won't work for query string params
315-
// i'll let someone file an issue before I add that
316-
pub inline fn isTarball(dependency: string) bool {
317-
return strings.endsWithComptime(dependency, ".tgz") or strings.endsWithComptime(dependency, ".tar.gz");
318-
}
319-
320343
pub fn infer(dependency: string) Tag {
321344
// empty string means `latest`
322345
if (dependency.len == 0) return .dist_tag;
@@ -482,9 +505,11 @@ pub const Version = struct {
482505
// verilog
483506
// verilog.tar.gz
484507
// verilog/repo
508+
// [email protected]:repo.git
485509
'v' => {
486510
if (isTarball(dependency)) return .tarball;
487511
if (isGitHubRepoPath(dependency)) return .github;
512+
if (isSCPLikePath(dependency)) return .git;
488513
if (dependency.len == 1) return .dist_tag;
489514
return switch (dependency[1]) {
490515
'0'...'9' => .npm,
@@ -514,6 +539,8 @@ pub const Version = struct {
514539
// user/repo
515540
// user/repo#main
516541
if (isGitHubRepoPath(dependency)) return .github;
542+
// [email protected]:path/to/repo.git
543+
if (isSCPLikePath(dependency)) return .git;
517544
// beta
518545
return .dist_tag;
519546
}
@@ -563,14 +590,6 @@ pub fn eql(
563590
return a.name_hash == b.name_hash and a.name.len() == b.name.len() and a.version.eql(&b.version, lhs_buf, rhs_buf);
564591
}
565592

566-
pub fn eqlResolved(a: *const Dependency, b: *const Dependency) bool {
567-
if (a.isNPM() and b.tag.isNPM()) {
568-
return a.resolution == b.resolution;
569-
}
570-
571-
return @as(Dependency.Version.Tag, a.version) == @as(Dependency.Version.Tag, b.version) and a.resolution == b.resolution;
572-
}
573-
574593
pub inline fn parse(
575594
allocator: std.mem.Allocator,
576595
alias: String,
@@ -749,7 +768,7 @@ pub fn parseWithTag(
749768
}
750769
}
751770

752-
if (comptime Environment.allow_assert) std.debug.assert(Version.Tag.isGitHubRepoPath(input));
771+
if (comptime Environment.allow_assert) std.debug.assert(isGitHubRepoPath(input));
753772

754773
var hash_index: usize = 0;
755774
var slash_index: usize = 0;

src/install/install.zig

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@ const HeaderBuilder = @import("bun").HTTP.HeaderBuilder;
3434
const Fs = @import("../fs.zig");
3535
const FileSystem = Fs.FileSystem;
3636
const Lock = @import("../lock.zig").Lock;
37-
var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
38-
var path_buf2: [bun.MAX_PATH_BYTES]u8 = undefined;
3937
const URL = @import("../url.zig").URL;
4038
const AsyncHTTP = @import("bun").HTTP.AsyncHTTP;
4139
const HTTPChannel = @import("bun").HTTP.HTTPChannel;
@@ -600,14 +598,27 @@ const Task = struct {
600598
},
601599
.git_clone => {
602600
const manager = this.package_manager;
603-
const dir = Repository.download(
601+
const name = this.request.git_clone.name.slice();
602+
const url = this.request.git_clone.url.slice();
603+
const dir = brk: {
604+
if (Repository.tryHTTPS(url)) |https| break :brk Repository.download(
605+
manager.allocator,
606+
manager.env,
607+
manager.log,
608+
manager.getCacheDirectory().dir,
609+
this.id,
610+
name,
611+
https,
612+
) catch null;
613+
break :brk null;
614+
} orelse Repository.download(
604615
manager.allocator,
605616
manager.env,
606617
manager.log,
607618
manager.getCacheDirectory().dir,
608619
this.id,
609-
this.request.git_clone.name.slice(),
610-
this.request.git_clone.url.slice(),
620+
name,
621+
url,
611622
) catch |err| {
612623
this.err = err;
613624
this.status = Status.fail;
@@ -1907,7 +1918,8 @@ pub const PackageManager = struct {
19071918
if (this.options.log_level != .silent) {
19081919
const elapsed = timer.read();
19091920
if (elapsed > std.time.ns_per_ms * 100) {
1910-
var cache_dir_path = bun.getFdPath(cache_directory.dir.fd, &path_buf) catch "it's";
1921+
var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
1922+
const cache_dir_path = bun.getFdPath(cache_directory.dir.fd, &path_buf) catch "it";
19111923
Output.prettyErrorln(
19121924
"<r><yellow>warn<r>: Slow filesystem detected. If {s} is a network drive, consider setting $BUN_INSTALL_CACHE_DIR to a local folder.",
19131925
.{cache_dir_path},
@@ -2379,21 +2391,33 @@ pub const PackageManager = struct {
23792391
const SuccessFn = *const fn (*PackageManager, DependencyID, PackageID) void;
23802392
const FailFn = *const fn (*PackageManager, *const Dependency, PackageID, anyerror) void;
23812393
fn assignResolution(this: *PackageManager, dependency_id: DependencyID, package_id: PackageID) void {
2394+
const buffers = &this.lockfile.buffers;
23822395
if (comptime Environment.allow_assert) {
2383-
std.debug.assert(dependency_id < this.lockfile.buffers.resolutions.items.len);
2396+
std.debug.assert(dependency_id < buffers.resolutions.items.len);
23842397
std.debug.assert(package_id < this.lockfile.packages.len);
2385-
std.debug.assert(this.lockfile.buffers.resolutions.items[dependency_id] == invalid_package_id);
2398+
std.debug.assert(buffers.resolutions.items[dependency_id] == invalid_package_id);
2399+
}
2400+
buffers.resolutions.items[dependency_id] = package_id;
2401+
var dep = &buffers.dependencies.items[dependency_id];
2402+
if (dep.name.isEmpty()) {
2403+
dep.name = this.lockfile.packages.items(.name)[package_id];
2404+
dep.name_hash = this.lockfile.packages.items(.name_hash)[package_id];
23862405
}
2387-
this.lockfile.buffers.resolutions.items[dependency_id] = package_id;
23882406
}
23892407

23902408
fn assignRootResolution(this: *PackageManager, dependency_id: DependencyID, package_id: PackageID) void {
2409+
const buffers = &this.lockfile.buffers;
23912410
if (comptime Environment.allow_assert) {
2392-
std.debug.assert(dependency_id < this.lockfile.buffers.resolutions.items.len);
2411+
std.debug.assert(dependency_id < buffers.resolutions.items.len);
23932412
std.debug.assert(package_id < this.lockfile.packages.len);
2394-
std.debug.assert(this.lockfile.buffers.resolutions.items[dependency_id] == invalid_package_id);
2413+
std.debug.assert(buffers.resolutions.items[dependency_id] == invalid_package_id);
2414+
}
2415+
buffers.resolutions.items[dependency_id] = package_id;
2416+
var dep = &buffers.dependencies.items[dependency_id];
2417+
if (dep.name.isEmpty()) {
2418+
dep.name = this.lockfile.packages.items(.name)[package_id];
2419+
dep.name_hash = this.lockfile.packages.items(.name_hash)[package_id];
23952420
}
2396-
this.lockfile.buffers.resolutions.items[dependency_id] = package_id;
23972421
}
23982422

23992423
fn getOrPutResolvedPackage(
@@ -5478,6 +5502,13 @@ pub const PackageManager = struct {
54785502

54795503
pub const Array = std.BoundedArray(UpdateRequest, 64);
54805504

5505+
pub inline fn matches(this: PackageManager.UpdateRequest, dependency: Dependency, string_buf: []const u8) bool {
5506+
return this.name_hash == if (this.name.len == 0)
5507+
String.Builder.stringHash(dependency.version.literal.slice(string_buf))
5508+
else
5509+
dependency.name_hash;
5510+
}
5511+
54815512
pub fn parse(
54825513
allocator: std.mem.Allocator,
54835514
log: *logger.Log,
@@ -5492,7 +5523,7 @@ pub const PackageManager = struct {
54925523
var input = std.mem.trim(u8, positional, " \n\r\t");
54935524
switch (op) {
54945525
.link, .unlink => if (!strings.hasPrefixComptime(input, "link:")) {
5495-
input = std.fmt.allocPrint(allocator, "link:{s}", .{input}) catch unreachable;
5526+
input = std.fmt.allocPrint(allocator, "{0s}@link:{0s}", .{input}) catch unreachable;
54965527
},
54975528
else => {},
54985529
}
@@ -5526,6 +5557,19 @@ pub const PackageManager = struct {
55265557
});
55275558
Global.crash();
55285559
};
5560+
if (alias != null and version.tag == .git) {
5561+
if (Dependency.parseWithOptionalTag(
5562+
allocator,
5563+
placeholder,
5564+
input,
5565+
null,
5566+
&SlicedString.init(input, input),
5567+
log,
5568+
)) |ver| {
5569+
alias = null;
5570+
version = ver;
5571+
}
5572+
}
55295573
if (switch (version.tag) {
55305574
.dist_tag => version.value.dist_tag.name.eql(placeholder, input, input),
55315575
.npm => version.value.npm.name.eql(placeholder, input, input),
@@ -5538,18 +5582,16 @@ pub const PackageManager = struct {
55385582
}
55395583

55405584
var request = UpdateRequest{
5541-
.name = allocator.dupe(u8, alias orelse switch (version.tag) {
5542-
.dist_tag => version.value.dist_tag.name,
5543-
.github => version.value.github.repo,
5544-
.npm => version.value.npm.name,
5545-
.symlink => version.value.symlink,
5546-
else => version.literal,
5547-
}.slice(input)) catch unreachable,
5548-
.is_aliased = alias != null,
55495585
.version = version,
55505586
.version_buf = input,
55515587
};
5552-
request.name_hash = String.Builder.stringHash(request.name);
5588+
if (alias) |name| {
5589+
request.is_aliased = true;
5590+
request.name = allocator.dupe(u8, name) catch unreachable;
5591+
request.name_hash = String.Builder.stringHash(name);
5592+
} else {
5593+
request.name_hash = String.Builder.stringHash(version.literal.slice(input));
5594+
}
55535595

55545596
for (update_requests.constSlice()) |*prev| {
55555597
if (prev.name_hash == request.name_hash and request.name.len == prev.name.len) continue :outer;
@@ -6924,11 +6966,11 @@ pub const PackageManager = struct {
69246966
_ = manager.getTemporaryDirectory();
69256967
}
69266968
manager.enqueueDependencyList(root.dependencies, true);
6969+
} else {
6970+
// Anything that needs to be downloaded from an update needs to be scheduled here
6971+
manager.drainDependencyList();
69276972
}
69286973

6929-
// Anything that needs to be downloaded from an update needs to be scheduled here
6930-
manager.drainDependencyList();
6931-
69326974
if (manager.pending_tasks > 0) {
69336975
if (root.dependencies.len > 0) {
69346976
_ = manager.getCacheDirectory();

0 commit comments

Comments
 (0)