diff options
-rw-r--r-- | src/install/dependency.zig | 70 | ||||
-rw-r--r-- | src/install/install.zig | 434 | ||||
-rw-r--r-- | src/install/repository.zig | 160 | ||||
-rw-r--r-- | src/install/resolvers/folder_resolver.zig | 127 |
4 files changed, 728 insertions, 63 deletions
diff --git a/src/install/dependency.zig b/src/install/dependency.zig index e85d7cb85..6ea562c34 100644 --- a/src/install/dependency.zig +++ b/src/install/dependency.zig @@ -13,6 +13,8 @@ const string = @import("../string_types.zig").string; const strings = @import("../string_immutable.zig"); const bun = @import("bun"); +const Repository = @import("./repository.zig").Repository; + pub const Pair = struct { resolution_id: Install.PackageID = Install.invalid_package_id, dependency: Dependency = .{}, @@ -235,6 +237,8 @@ pub const Version = struct { .folder, .dist_tag => lhs.literal.eql(rhs.literal, lhs_buf, rhs_buf), .tarball => lhs.value.tarball.eql(rhs.value.tarball, lhs_buf, rhs_buf), .symlink => lhs.value.symlink.eql(rhs.value.symlink, lhs_buf, rhs_buf), + .git => lhs.value.git.eql(rhs.value.git, lhs_buf, rhs_buf), + .github => lhs.value.github.eql(rhs.value.github, lhs_buf, rhs_buf), else => true, }; } @@ -259,17 +263,23 @@ pub const Version = struct { /// https://stackoverflow.com/questions/51954956/whats-the-difference-between-yarn-link-and-npm-link symlink = 5, - /// TODO: - workspace = 6, - /// TODO: + // git+https://example.com/repo#commit git = 7, - /// TODO: + + // profile/repo#commit github = 8, + /// TODO: + workspace = 6, + pub inline fn isNPM(this: Tag) bool { return @enumToInt(this) < 3; } + pub inline fn isGitHub(this: Tag) bool { + return @enumToInt(this) == 8; + } + pub inline fn isGitHubRepoPath(dependency: string) bool { var slash_count: u8 = 0; @@ -279,7 +289,7 @@ pub const Version = struct { // Must be alphanumeric switch (c) { - '\\', '/', 'a'...'z', 'A'...'Z', '0'...'9', '%' => {}, + '\\', '/', 'a'...'z', 'A'...'Z', '0'...'9', '%', '-' => {}, else => return false, } } @@ -495,16 +505,14 @@ pub const Version = struct { dist_tag: String, tarball: URI, folder: String, + git: Repository, + github: Repository, /// Equivalent to npm link symlink: String, /// Unsupported, but still parsed so an error can be thrown workspace: void, - /// Unsupported, but still parsed so an error can be thrown - git: void, - /// Unsupported, but still parsed so an error can be thrown - github: void, }; }; @@ -580,6 +588,46 @@ pub fn parseWithTag( .tag = .npm, }; }, + .git => return Version{ + .literal = sliced.value(), + .value = .{ + .git = Repository.parse(sliced) catch |err| { + if (log_) |log| log.addErrorFmt( + null, + logger.Loc.Empty, + allocator, + "{s} parsing dependency \"{s}\"", + .{ + @errorName(err), + dependency, + }, + ) catch unreachable; + return null; + }, + }, + .tag = .git, + }, + .github => { + return Version{ + .literal = sliced.value(), + .value = .{ + .github = Repository.parseGitHub(sliced) catch |err| { + if (log_) |log| log.addErrorFmt( + null, + logger.Loc.Empty, + allocator, + "{s} parsing dependency \"{s}\"", + .{ + @errorName(err), + dependency, + }, + ) catch unreachable; + return null; + }, + }, + .tag = .github, + }; + }, .dist_tag => { return Version{ .literal = sliced.value(), @@ -652,8 +700,8 @@ pub fn parseWithTag( .literal = sliced.value(), }; }, - .workspace, .git, .github => { - if (log_) |log| log.addErrorFmt(null, logger.Loc.Empty, allocator, "Support for dependency type \"{s}\" is not implemented yet (\"{s}\")", .{ @tagName(tag), dependency }) catch unreachable; + .workspace => { + if (log_) |log| log.addErrorFmt(null, logger.Loc.Empty, allocator, "Dependency type not implemented yet {s} for \"{s}\"", .{ @tagName(tag), dependency }) catch unreachable; return null; }, } diff --git a/src/install/install.zig b/src/install/install.zig index cd915a900..3221605cd 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -183,6 +183,7 @@ const NetworkTask = struct { }, extract: ExtractTarball, binlink: void, + git_clone: void, }, pub fn notify(this: *NetworkTask, _: anytype) void { @@ -469,6 +470,8 @@ pub const Features = struct { .optional_dependencies = true, }; + pub const git = npm; + pub const tarball = npm; pub const npm_manifest = Features{ @@ -506,6 +509,14 @@ const Task = struct { return @as(u64, @truncate(u63, hasher.final())) | @as(u64, 1 << 63); } + pub fn forGitHubPackage(repo: string, owner: string) u64 { + var hasher = std.hash.Wyhash.init(0); + hasher.update(owner); + hasher.update("/~~"); + hasher.update(repo); + return @as(u64, @truncate(u63, hasher.final())) | @as(u64, 1 << 63); + } + pub fn forBinLink(package_id: PackageID) u64 { const hash = std.hash.Wyhash.hash(0, std.mem.asBytes(&package_id)); return @as(u64, @truncate(u62, hash)) | @as(u64, 1 << 62) | @as(u64, 1 << 63); @@ -528,6 +539,95 @@ const Task = struct { defer this.package_manager.wake(); switch (this.tag) { + .git_clone => { + const allocator = this.package_manager.allocator; + + const PATH = this.package_manager.env_loader.get("PATH") orelse ""; + + var git_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + + if (which(&git_path_buf, PATH, ".", "git")) |git| { + const lockfile = this.package_manager.lockfile; + const git_path = std.mem.span(git); + + const git_repo = this.request.git_clone.repository; + + const repo_name = lockfile.str(git_repo.repo); + + var url_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const url = if (this.request.git_clone.version.tag == .github) git_repo.getGitHubURL(lockfile, &url_buf) else git_repo.getURL(lockfile, &url_buf); + + var temp_dir_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const temp_dir = std.os.getFdPath(this.package_manager.getTemporaryDirectory().dir.fd, &temp_dir_path_buf) catch unreachable; + + var destination_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + std.mem.copy(u8, &destination_buf, temp_dir); + destination_buf[temp_dir.len] = '/'; + std.mem.copy(u8, destination_buf[temp_dir.len + 1 ..], repo_name); + const destination = destination_buf[0 .. temp_dir.len + repo_name.len + 1]; + + const args = [_]string{ + git_path, + "clone", + "-q", + url, + destination, + }; + + var process = std.ChildProcess.init(&args, allocator); + process.stdout_behavior = .Close; + process.stderr_behavior = .Close; + process.stdin_behavior = .Close; + + const term = process.spawnAndWait() catch { + this.log.addErrorFmt(null, logger.Loc.Empty, allocator, "Failed to spawn git process to clone github dependency \"{s}\"", .{repo_name}) catch unreachable; + this.status = .fail; + this.package_manager.resolve_tasks.writeItem(this.*) catch unreachable; + return; + }; + switch (term) { + else => {}, + } + + // get package.json bytes, send pointer back to main thread + var package_json_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + std.mem.copy(u8, &package_json_path_buf, destination); + std.mem.copy(u8, package_json_path_buf[destination.len..], "/package.json"); + + const package_json_path = package_json_path_buf[0 .. destination.len + "/package.json".len]; + + const package_json_file = std.fs.openFileAbsolute(package_json_path, .{}) catch { + this.status = .fail; + this.log.addErrorFmt(null, logger.Loc.Empty, allocator, "Failed to find package.json for github dependency \"{s}\"", .{repo_name}) catch unreachable; + this.package_manager.resolve_tasks.writeItem(this.*) catch unreachable; + return; + }; + + const package_json_file_stat = package_json_file.stat() catch unreachable; + const package_json_file_size = package_json_file_stat.size; + + const package_json_source = allocator.alloc(u8, package_json_file_size) catch unreachable; + _ = package_json_file.preadAll(package_json_source, 0) catch unreachable; + + if (package_json_file_size < "{\"name\":\"\",\"version\":\"\"}".len + repo_name.len + "0.0.0".len) { + // package.json smaller than minimum possible + this.status = .fail; + this.log.addErrorFmt(null, logger.Loc.Empty, allocator, "Invalid package.json for github dependency \"{s}\"", .{repo_name}) catch unreachable; + this.package_manager.resolve_tasks.writeItem(this.*) catch unreachable; + return; + } + + this.status = .success; + this.data = .{ .package_json = package_json_source }; + this.package_manager.resolve_tasks.writeItem(this.*) catch unreachable; + return; + } + + this.status = .fail; + this.log.addErrorFmt(null, logger.Loc.Empty, allocator, "Failed to find git executable", .{}) catch unreachable; + this.package_manager.resolve_tasks.writeItem(this.*) catch unreachable; + return; + }, .package_manifest => { var allocator = bun.default_allocator; const package_manifest = Npm.Registry.getPackageMetadata( @@ -595,10 +695,11 @@ const Task = struct { } } - pub const Tag = enum(u2) { + pub const Tag = enum(u3) { package_manifest = 1, extract = 2, binlink = 3, + git_clone = 4, // install = 3, }; @@ -612,6 +713,7 @@ const Task = struct { package_manifest: Npm.PackageManifest, extract: string, binlink: bool, + package_json: string, }; pub const Request = union { @@ -626,12 +728,18 @@ const Task = struct { tarball: ExtractTarball, }, binlink: Bin.Linker, + git_clone: struct { + repository: Repository, + version: Dependency.Version, + dependency_id: u32, + }, // install: PackageInstall, }; }; const PackageInstall = struct { cache_dir: std.fs.IterableDir, + git_cache_dir: std.fs.IterableDir, destination_dir: std.fs.IterableDir, cache_dir_subpath: stringZ = "", destination_dir_subpath: stringZ = "", @@ -688,6 +796,7 @@ const PackageInstall = struct { this.package_install = PackageInstall{ .cache_dir = undefined, + .git_cache_dir = undefined, .cache_dir_subpath = undefined, .progress = ctx.progress, @@ -1436,6 +1545,7 @@ const Waker = AsyncIO.Waker; // 2. pub const PackageManager = struct { cache_directory_: ?std.fs.IterableDir = null, + git_cache_directory_: ?std.fs.IterableDir = null, temp_dir_: ?std.fs.IterableDir = null, root_dir: *Fs.FileSystem.DirEntry, env_loader: *DotEnv.Loader, @@ -1760,6 +1870,17 @@ pub const PackageManager = struct { }; } + pub noinline fn getGitCacheDirectory(this: *PackageManager) std.fs.IterableDir { + return this.git_cache_directory_ orelse brk: { + const cache_dir = this.getCacheDirectory(); + this.git_cache_directory_ = cache_dir.dir.openIterableDir("../git", .{}) catch { + this.git_cache_directory_ = cache_dir.dir.makeOpenPathIterable("../git", .{}) catch unreachable; + break :brk this.git_cache_directory_.?; + }; + break :brk this.git_cache_directory_.?; + }; + } + pub inline fn getTemporaryDirectory(this: *PackageManager) std.fs.IterableDir { return this.temp_dir_ orelse brk: { this.temp_dir_ = this.ensureTemporaryDirectory(); @@ -2022,8 +2143,8 @@ pub const PackageManager = struct { } pub fn resolveFromDiskCache(this: *PackageManager, package_name: []const u8, version: Dependency.Version) ?PackageID { - if (version.tag != .npm) { - // only npm supported right now + if (version.tag != .npm and version.tag != .github and version.tag != .git) { + // only npm, git, and github supported right now // tags are more ambiguous return null; } @@ -2047,32 +2168,38 @@ pub const PackageManager = struct { Semver.Version.sortGt, ); for (installed_versions.items) |installed_version| { - if (version.value.npm.satisfies(installed_version)) { - var buf: [bun.MAX_PATH_BYTES]u8 = undefined; - var npm_package_path = this.pathForCachedNPMPath(&buf, package_name, installed_version) catch |err| { - Output.debug("error getting path for cached npm path: {s}", .{std.mem.span(@errorName(err))}); - return null; - }; - const dependency = Dependency.Version{ - .tag = .npm, - .value = .{ - .npm = Semver.Query.Group.from(installed_version), - }, - }; - switch (FolderResolution.getOrPut(.{ .cache_folder = npm_package_path }, dependency, ".", this)) { - .new_package_id => |id| { - this.enqueueDependencyList(this.lockfile.packages.items(.dependencies)[id], false); - return id; - }, - .package_id => |id| { - this.enqueueDependencyList(this.lockfile.packages.items(.dependencies)[id], false); - return id; - }, - .err => |err| { - Output.debug("error getting or putting folder resolution: {s}", .{std.mem.span(@errorName(err))}); - return null; - }, - } + switch (version.tag) { + .npm => { + if (version.value.npm.satisfies(installed_version)) { + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + var npm_package_path = this.pathForCachedNPMPath(&buf, package_name, installed_version) catch |err| { + Output.debug("error getting path for cached npm path: {s}", .{std.mem.span(@errorName(err))}); + return null; + }; + const dependency = Dependency.Version{ + .tag = .npm, + .value = .{ + .npm = Semver.Query.Group.from(installed_version), + }, + }; + switch (FolderResolution.getOrPut(.{ .cache_folder = npm_package_path }, dependency, ".", this)) { + .new_package_id => |id| { + this.enqueueDependencyList(this.lockfile.packages.items(.dependencies)[id], false); + return id; + }, + .package_id => |id| { + this.enqueueDependencyList(this.lockfile.packages.items(.dependencies)[id], false); + return id; + }, + .err => |err| { + Output.debug("error getting or putting folder resolution: {s}", .{std.mem.span(@errorName(err))}); + return null; + }, + } + } + }, + // TODO: handle git and github + else => {}, } } @@ -2347,6 +2474,49 @@ pub const PackageManager = struct { ); }, + .github => { + var cache_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const cache_path = try version.value.github.getCachePathForGitHub(this, &cache_path_buf); + + const res = FolderResolution.getOrPut(.{ .git_cache_folder = cache_path }, version, cache_path, this); + + switch (res) { + .err => |err| return err, + .package_id => |package_id| { + successFn(this, dependency_id, package_id); + return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id) }; + }, + .new_package_id => |package_id| { + successFn(this, dependency_id, package_id); + return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id), .is_first_time = true }; + }, + } + + return null; + }, + + .git => { + var cache_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + + const cache_path = try version.value.git.getCachePath(this, &cache_path_buf); + + const res = FolderResolution.getOrPut(.{ .git_cache_folder = cache_path }, version, cache_path, this); + + switch (res) { + .err => |err| return err, + .package_id => |package_id| { + successFn(this, dependency_id, package_id); + return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id) }; + }, + .new_package_id => |package_id| { + successFn(this, dependency_id, package_id); + return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id), .is_first_time = true }; + }, + } + + return null; + }, + .folder => { // relative to cwd const res = FolderResolution.getOrPut(.{ .relative = void{} }, version, version.value.folder.slice(this.lockfile.buffers.string_bytes.items), this); @@ -2408,6 +2578,32 @@ pub const PackageManager = struct { return &task.threadpool_task; } + fn enqueueCloneGitPackage( + this: *PackageManager, + task_id: u64, + repository: Repository, + dependency_id: u32, + dep_version: Dependency.Version, + ) *ThreadPool.Task { + var task = this.allocator.create(Task) catch unreachable; + task.* = Task{ + .package_manager = &PackageManager.instance, + .log = logger.Log.init(this.allocator), + .tag = Task.Tag.git_clone, + .request = .{ + .git_clone = .{ + .repository = repository, + .version = dep_version, + .dependency_id = dependency_id, + }, + }, + .id = task_id, + .data = undefined, + }; + + return &task.threadpool_task; + } + fn enqueueExtractNPMPackage( this: *PackageManager, tarball: ExtractTarball, @@ -2543,13 +2739,60 @@ pub const PackageManager = struct { if (!this.isRootDependency(id)) if (!dependency.behavior.isEnabled(switch (dependency.version.tag) { .folder => this.options.remote_package_features, - .dist_tag, .npm => this.options.remote_package_features, + .dist_tag, .npm, .git, .github => this.options.remote_package_features, else => Features{}, })) return; } switch (dependency.version.tag) { + .github, .git => { + var resolve_result = this.getOrPutResolvedPackage( + name_hash, + name, + version, + dependency.behavior, + id, + resolution, + successFn, + ) catch |err| brk: { + if (err == error.MissingPackageJSON) { + break :brk @as(?ResolvedPackageResult, null); + } + + return err; + }; + + if (resolve_result == null) { + const lockfile = this.lockfile; + + const repo = if (version.tag == .github) version.value.github else version.value.git; + + const task_id = Task.Id.forGitHubPackage(lockfile.str(repo.repo), lockfile.str(repo.owner)); + const network_id = try this.network_dedupe_map.getOrPutContext(this.allocator, task_id, .{}); + if (!network_id.found_existing) { + var batch = ThreadPool.Batch{}; + batch.push(ThreadPool.Batch.from(this.enqueueCloneGitPackage( + task_id, + repo, + id, + dependency.version, + ))); + + const count = batch.len; + this.pending_tasks += @truncate(u32, count); + this.total_tasks += @truncate(u32, count); + this.thread_pool.schedule(batch); + } + var task_queue = try this.task_queue.getOrPutContext(this.allocator, task_id, .{}); + if (!task_queue.found_existing) { + task_queue.value_ptr.* = TaskCallbackList{}; + } + + const callback_tag = comptime if (successFn == assignRootResolution) "root_dependency" else "dependency"; + try task_queue.value_ptr.append(this.allocator, @unionInit(TaskCallbackContext, callback_tag, id)); + } + }, .folder, .npm, .dist_tag => { retry_from_manifests_ptr: while (true) { var resolve_result_ = this.getOrPutResolvedPackage( @@ -3293,6 +3536,7 @@ pub const PackageManager = struct { batch.push(ThreadPool.Batch.from(manager.enqueueExtractNPMPackage(extract, task))); }, .binlink => {}, + .git_clone => {}, } } @@ -3363,6 +3607,69 @@ pub const PackageManager = struct { } } }, + .git_clone => { + if (task.status == .fail) { + continue; + } + + const allocator = manager.allocator; + const package_json = task.data.package_json; + defer allocator.free(package_json); + const git_repo = task.request.git_clone.repository; + var package_name = manager.lockfile.str(git_repo.repo); + + package_name = manager.lockfile.str(task.request.git_clone.repository.repo); + + const temp_dir = manager.getTemporaryDirectory().dir; + var temp_dir_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + _ = try std.os.getFdPath(temp_dir.fd, &temp_dir_buf); + const git_cache_dir = manager.getGitCacheDirectory().dir; + git_cache_dir.deleteTree(package_name) catch unreachable; + + var destination_name_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const destination_name = try git_repo.getCacheDirectoryForGitHub(manager, &destination_name_buf); + std.fs.rename(temp_dir, package_name, git_cache_dir, destination_name) catch unreachable; + temp_dir.deleteTree(package_name) catch unreachable; + + var cache_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const cache_path = try git_repo.getCachePathForGitHub(manager, &cache_path_buf); + const res = FolderResolution.getOrPutWithPackageJSONBytes( + .{ .git_cache_folder = cache_path }, + task.request.git_clone.version, + cache_path, + manager, + package_json, + ); + + const dependency_id = task.request.git_clone.dependency_id; + const pkg_id = brk: { + switch (res) { + .err => |err| return err, + .package_id => |package_id| { + manager.assignResolution(dependency_id, package_id); + break :brk package_id; + }, + .new_package_id => |package_id| { + manager.assignResolution(dependency_id, package_id); + break :brk package_id; + }, + } + }; + + const dependencies = manager.lockfile.packages.items(.dependencies)[pkg_id]; + + var dependency_list_entry = manager.task_queue.getEntry(task.id).?; + var dependency_list = dependency_list_entry.value_ptr.*; + dependency_list_entry.value_ptr.* = .{}; + + const end = dependencies.off + dependencies.len; + var i = dependencies.off; + while (i < end) : (i += 1) { + dependency_list.append(allocator, @unionInit(TaskCallbackContext, "dependency", i)) catch unreachable; + } + + try manager.processDependencyList(dependency_list, ExtractCompletionContext, extract_ctx, callbacks); + }, .extract => { if (task.status == .fail) { const err = task.err orelse error.TarballFailedToExtract; @@ -5041,8 +5348,8 @@ pub const PackageManager = struct { if (unscoped_name.len > i + 1) request.version_buf = unscoped_name[i + 1 ..]; } - if (strings.hasPrefix("http://", request.name) or - strings.hasPrefix("https://", request.name)) + if (strings.hasPrefixComptime(request.name, "http://") or + strings.hasPrefixComptime(request.name, "https://")) { if (Output.isEmojiEnabled()) { Output.prettyErrorln("<r>😢 <red>error<r><d>:<r> bun {s} http://url is not implemented yet.", .{ @@ -5068,8 +5375,8 @@ pub const PackageManager = struct { request.version_buf = std.mem.trim(u8, request.version_buf, "\n\r\t"); // https://github.com/npm/npm-package-arg/blob/fbaf2fd0b72a0f38e7c24260fd4504f4724c9466/npa.js#L330 - if (strings.hasPrefix("https://", request.version_buf) or - strings.hasPrefix("http://", request.version_buf)) + if (strings.hasPrefixComptime(request.version_buf, "https://") or + strings.hasPrefixComptime(request.version_buf, "http://")) { if (Output.isEmojiEnabled()) { Output.prettyErrorln("<r>😢 <red>error<r><d>:<r> bun {s} http://url is not implemented yet.", .{ @@ -5636,6 +5943,7 @@ pub const PackageManager = struct { var installer = PackageInstall{ .progress = this.progress, .cache_dir = undefined, + .git_cache_dir = undefined, .cache_dir_subpath = undefined, .destination_dir = this.node_modules_folder, .destination_dir_subpath = destination_dir_subpath, @@ -6370,6 +6678,7 @@ pub const PackageManager = struct { const changes = @truncate(PackageID, mapping.len); _ = manager.getCacheDirectory(); + _ = manager.getGitCacheDirectory(); _ = manager.getTemporaryDirectory(); var counter_i: PackageID = 0; while (counter_i < changes) : (counter_i += 1) { @@ -6424,6 +6733,7 @@ pub const PackageManager = struct { if (root.dependencies.len > 0) { _ = manager.getCacheDirectory(); + _ = manager.getGitCacheDirectory(); _ = manager.getTemporaryDirectory(); } manager.enqueueDependencyList( @@ -6440,6 +6750,7 @@ pub const PackageManager = struct { if (manager.pending_tasks > 0) { if (root.dependencies.len > 0) { _ = manager.getCacheDirectory(); + _ = manager.getGitCacheDirectory(); _ = manager.getTemporaryDirectory(); } @@ -6649,13 +6960,12 @@ pub const PackageManager = struct { if (install_summary.success > 0) { // it's confusing when it shows 3 packages and says it installed 1 - Output.pretty("\n <green>{d}<r> packages<r> installed ", .{@max( - install_summary.success, - @truncate( - u32, - manager.package_json_updates.len, - ), - )}); + const count = @max(install_summary.success, @truncate(u32, manager.package_json_updates.len)); + if (count == 1) { + Output.pretty("\n <green>1<r> package<r> installed ", .{}); + } else { + Output.pretty("\n <green>{d}<r> packages<r> installed ", .{count}); + } Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); printed_timestamp = true; Output.pretty("<r>\n", .{}); @@ -6670,7 +6980,11 @@ pub const PackageManager = struct { } } - Output.pretty("\n <r><b>{d}<r> packages removed ", .{manager.summary.remove}); + if (manager.summary.remove == 1) { + Output.pretty("\n <r><b>1<r> package removed ", .{}); + } else { + Output.pretty("\n <r><b>{d}<r> packages removed ", .{manager.summary.remove}); + } Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); printed_timestamp = true; Output.pretty("<r>\n", .{}); @@ -6679,17 +6993,33 @@ pub const PackageManager = struct { const count = @truncate(PackageID, manager.lockfile.packages.len); if (count != install_summary.skipped) { - Output.pretty("Checked <green>{d} installs<r> across {d} packages <d>(no changes)<r> ", .{ - install_summary.skipped, - count, - }); + if (install_summary.skipped == 1 and count == 1) { + Output.pretty("Checked <green>1 install<r> across 1 package <d>(no changes)<r> ", .{}); + } else if (install_summary.skipped == 1) { + Output.pretty("Checked <green>1 install<r> across {d} packages <d>(no changes)<r> ", .{ + count, + }); + } else if (count == 1) { + Output.pretty("Checked <green>{d} installs<r> across 1 package <d>(no changes)<r> ", .{ + install_summary.skipped, + }); + } else { + Output.pretty("Checked <green>{d} installs<r> across {d} packages <d>(no changes)<r> ", .{ + install_summary.skipped, + count, + }); + } Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); printed_timestamp = true; Output.pretty("<r>\n", .{}); } else { - Output.pretty("<r> <green>Done<r>! Checked {d} packages<r> <d>(no changes)<r> ", .{ - install_summary.skipped, - }); + if (install_summary.skipped == 1) { + Output.pretty("<r> <green>Done<r>! Checked 1 package<r> <d>(no changes)<r> ", .{}); + } else { + Output.pretty("<r> <green>Done<r>! Checked {d} packages<r> <d>(no changes)<r> ", .{ + install_summary.skipped, + }); + } Output.printStartEndStdout(ctx.start_time, std.time.nanoTimestamp()); printed_timestamp = true; Output.pretty("<r>\n", .{}); @@ -6697,7 +7027,11 @@ pub const PackageManager = struct { } if (install_summary.fail > 0) { - Output.prettyln("<r>Failed to install <red><b>{d}<r> packages\n", .{install_summary.fail}); + if (install_summary.fail == 1) { + Output.prettyln("<r>Failed to install <red><b>1<r> package\n", .{}); + } else { + Output.prettyln("<r>Failed to install <red><b>{d}<r> packages\n", .{install_summary.fail}); + } Output.flush(); } } diff --git a/src/install/repository.zig b/src/install/repository.zig index 109e24ce9..d53fca8d5 100644 --- a/src/install/repository.zig +++ b/src/install/repository.zig @@ -1,11 +1,16 @@ const PackageManager = @import("./install.zig").PackageManager; +const Lockfile = @import("./lockfile.zig"); const Semver = @import("./semver.zig"); const ExternalString = Semver.ExternalString; const String = Semver.String; +const SlicedString = Semver.SlicedString; const std = @import("std"); const GitSHA = String; +const bun = @import("../bun.zig"); const string = @import("../string_types.zig").string; +const strings = @import("../bun.zig").strings; const Environment = @import("../env.zig"); +const Group = Semver.Query.Group; pub const Repository = extern struct { owner: String = String{}, @@ -46,6 +51,161 @@ pub const Repository = extern struct { return try formatter.format(layout, opts, writer); } + pub fn getGitHubURL(this: Repository, lockfile: *Lockfile, buf: *[bun.MAX_PATH_BYTES]u8) []u8 { + const github = "https://github.com/"; + const owner = lockfile.str(this.owner); + const repo = lockfile.str(this.repo); + const committish = lockfile.str(this.committish); + + var i: usize = 0; + std.mem.copy(u8, buf[i..], github); + i += github.len; + + std.mem.copy(u8, buf[i..], owner); + i += owner.len; + buf[i] = '/'; + i += 1; + std.mem.copy(u8, buf[i..], repo); + i += repo.len; + if (committish.len > 0) { + buf[i] = '#'; + i += 1; + std.mem.copy(u8, buf[i..], committish); + i += committish.len; + } + + return buf[0..i]; + } + + pub fn getURL(this: Repository, lockfile: *Lockfile, buf: *[bun.MAX_PATH_BYTES]u8) []u8 { + const owner = lockfile.str(this.owner); + const repo = lockfile.str(this.repo); + const committish = lockfile.str(this.committish); + + var i: usize = 0; + std.mem.copy(u8, buf[i..], owner); + i += owner.len; + buf[i] = '/'; + i += 1; + std.mem.copy(u8, buf[i..], repo); + i += repo.len; + if (committish.len > 0) { + buf[i] = '#'; + i += 1; + std.mem.copy(u8, buf[i..], committish); + i += committish.len; + } + + return buf[0..i]; + } + + pub fn getCacheDirectoryForGitHub(this: Repository, manager: *PackageManager, buf: *[bun.MAX_PATH_BYTES]u8) ![]u8 { + var url_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const url = this.getGitHubURL(manager.lockfile, &url_buf); + + const url_hash = std.hash.Wyhash.hash(0, url); + const hex_fmt = bun.fmt.hexIntLower(url_hash); + + const repo = manager.lockfile.str(this.repo); + + return try std.fmt.bufPrint(buf, "{s}-{any}", .{ repo[0..@min(16, repo.len)], hex_fmt }); + } + + pub fn getCacheDirectory(this: Repository, manager: *PackageManager, buf: *[bun.MAX_PATH_BYTES]u8) ![]u8 { + var url_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const url = this.getURL(manager.lockfile, &url_buf); + + const url_hash = std.hash.Wyhash.hash(0, url); + const hex_fmt = bun.fmt.hexIntLower(url_hash); + + const repo = manager.lockfile.str(this.repo); + + return try std.fmt.bufPrint(buf, "{s}-{any}", .{ repo[0..@min(16, repo.len)], hex_fmt }); + } + + pub fn getCachePathForGitHub(this: Repository, manager: *PackageManager, buf: *[bun.MAX_PATH_BYTES]u8) ![]u8 { + var url_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const url = this.getGitHubURL(manager.lockfile, &url_buf); + + var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const path = try std.os.getFdPath(manager.getGitCacheDirectory().dir.fd, &path_buf); + + const url_hash = std.hash.Wyhash.hash(0, url); + const hex_fmt = bun.fmt.hexIntLower(url_hash); + + const repo = manager.lockfile.str(this.repo); + + return try std.fmt.bufPrint(buf, "{s}/{s}-{any}", .{ path, repo[0..@min(16, repo.len)], hex_fmt }); + } + + pub fn getCachePath(this: Repository, manager: *PackageManager, buf: *[bun.MAX_PATH_BYTES]u8) ![]u8 { + var url_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const url = this.getURL(manager.lockfile, &url_buf); + + var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const path = try std.os.getFdPath(manager.getGitCacheDirectory().dir.fd, &path_buf); + + const url_hash = std.hash.Wyhash.hash(0, url); + const hex_fmt = bun.fmt.hexIntLower(url_hash); + + const repo = manager.lockfile.str(this.repo); + + return try std.fmt.bufPrint(buf, "{s}/{s}-{any}", .{ path, repo[0..@min(16, repo.len)], hex_fmt }); + } + + pub fn parse(input: *const SlicedString) !Repository { + var repo = Repository{}; + const slice = input.slice; + + // ignore "git+" + const i: usize = if (strings.indexOfChar(slice, '+')) |j| j + 1 else 0; + if (strings.indexOfChar(slice[i..], ':')) |_j| { + var j = i + _j + 1; + if (!strings.hasPrefixComptime(slice[j..], "//")) return error.InvalidGitURL; + j += 2; + if (strings.indexOfAny(slice[j..], ":/")) |k| { + j += k + 1; + if (strings.indexOfChar(slice[j..], '/')) |l| { + j += l; + repo.owner = String.init(input.buf, slice[i..j]); + } else return error.InvalidGitURL; + } else return error.InvalidGitURL; + + if (strings.indexOfChar(slice[j..], '#')) |_k| { + var k = _k + j; + if (strings.endsWithComptime(slice[j + 1 .. k], ".git")) { + repo.repo = String.init(input.buf, slice[j + 1 .. k - ".git".len]); + } else { + repo.repo = String.init(input.buf, slice[j + 1 .. k]); + } + repo.committish = String.init(input.buf, slice[k + 1 ..]); + } else { + const end = if (strings.endsWithComptime(slice[j + 1 ..], ".git")) slice.len - ".git".len else slice.len; + repo.repo = String.init(input.buf, slice[j + 1 .. end]); + } + } else return error.InvalidGitURL; + + return repo; + } + + pub fn parseGitHub(input: *const SlicedString) !Repository { + var repo = Repository{}; + // ignore "github:" + const i: usize = if (strings.indexOfChar(input.slice, ':')) |j| j + 1 else 0; + if (strings.indexOfChar(input.slice, '/')) |j| { + repo.owner = String.init(input.buf, input.slice[i..j]); + if (strings.indexOfChar(input.slice[j + 1 ..], '#')) |k| { + repo.repo = String.init(input.buf, input.slice[j + 1 .. k]); + repo.committish = String.init(input.buf, input.slice[k + 1 ..]); + } else { + repo.repo = String.init(input.buf, input.slice[j + 1 ..]); + } + } else { + return error.InvalidGitURL; + } + return repo; + } + pub const Formatter = struct { label: []const u8 = "", buf: []const u8, diff --git a/src/install/resolvers/folder_resolver.zig b/src/install/resolvers/folder_resolver.zig index b25623cfe..b9e222f22 100644 --- a/src/install/resolvers/folder_resolver.zig +++ b/src/install/resolvers/folder_resolver.zig @@ -11,6 +11,7 @@ const Features = @import("../install.zig").Features; const IdentityContext = @import("../../identity_context.zig").IdentityContext; const strings = @import("bun").strings; const Resolution = @import("../resolution.zig").Resolution; +const Repository = @import("../repository.zig").Repository; const String = @import("../semver.zig").String; const Semver = @import("../semver.zig"); const bun = @import("bun"); @@ -74,7 +75,18 @@ pub const FolderResolution = union(Tag) { var abs: string = ""; var rel: string = ""; // We consider it valid if there is a package.json in the folder - const normalized = std.mem.trimRight(u8, normalize(non_normalized_path), std.fs.path.sep_str); + const temp_normalized = std.mem.trimRight(u8, normalize(non_normalized_path), std.fs.path.sep_str); + var normalized_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + std.mem.copy(u8, &normalized_buf, temp_normalized); + var normalized = normalized_buf[0..temp_normalized.len]; + + if (global_or_relative == .git_cache_folder and normalized[0] != '/') { + var temp_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + temp_path_buf[0] = '/'; + std.mem.copy(u8, temp_path_buf[1..], normalized); + normalized.len += 1; + std.mem.copy(u8, normalized, temp_path_buf[0..normalized.len]); + } if (strings.startsWithChar(normalized, '.')) { var tempcat: [bun.MAX_PATH_BYTES]u8 = undefined; @@ -117,6 +129,39 @@ pub const FolderResolution = union(Tag) { return .{ abs, rel }; } + pub fn readPackageJSONFromBytes( + manager: *PackageManager, + abs: []const u8, + json_bytes: string, + version: Dependency.Version, + comptime features: Features, + comptime ResolverType: type, + resolver: ResolverType, + ) !Lockfile.Package { + var package = Lockfile.Package{}; + var body = Npm.Registry.BodyPool.get(manager.allocator); + defer Npm.Registry.BodyPool.release(body); + + const source = logger.Source.initPathString(abs, json_bytes); + + try Lockfile.Package.parse( + manager.lockfile, + &package, + manager.allocator, + manager.log, + source, + ResolverType, + resolver, + features, + ); + + if (manager.lockfile.getPackageID(package.name_hash, version, package.resolution)) |existing_id| { + return manager.lockfile.packages.get(existing_id); + } + + return manager.lockfile.appendPackage(package) catch unreachable; + } + pub fn readPackageJSONFromDisk( manager: *PackageManager, joinedZ: [:0]const u8, @@ -162,8 +207,73 @@ pub const FolderResolution = union(Tag) { global: []const u8, relative: void, cache_folder: []const u8, + git_cache_folder: []const u8, }; + pub fn getOrPutWithPackageJSONBytes(global_or_relative: GlobalOrRelative, version: Dependency.Version, non_normalized_path: string, manager: *PackageManager, json_bytes: string) FolderResolution { + var joined: [bun.MAX_PATH_BYTES]u8 = undefined; + const paths = normalizePackageJSONPath(global_or_relative, &joined, non_normalized_path); + const abs = paths[0]; + const rel = paths[1]; + + var entry = manager.folders.getOrPut(manager.allocator, hash(abs)) catch unreachable; + if (entry.found_existing) { + if (global_or_relative != .git_cache_folder or entry.value_ptr.*.err != error.MissingPackageJSON) { + return entry.value_ptr.*; + } + } + + const package: Lockfile.Package = switch (global_or_relative) { + .global => readPackageJSONFromBytes( + manager, + abs, + json_bytes, + version, + Features.link, + SymlinkResolver, + SymlinkResolver{ .folder_path = non_normalized_path }, + ), + .relative => readPackageJSONFromBytes( + manager, + abs, + json_bytes, + version, + Features.folder, + Resolver, + Resolver{ .folder_path = rel }, + ), + .cache_folder => readPackageJSONFromBytes( + manager, + abs, + json_bytes, + version, + Features.npm, + CacheFolderResolver, + CacheFolderResolver{ .version = version.value.npm.toVersion() }, + ), + .git_cache_folder => readPackageJSONFromBytes( + manager, + abs, + json_bytes, + version, + Features.git, + Resolver, + Resolver{ .folder_path = rel }, + ), + } catch |err| { + if (err == error.FileNotFound) { + entry.value_ptr.* = .{ .err = error.MissingPackageJSON }; + } else { + entry.value_ptr.* = .{ .err = err }; + } + + return entry.value_ptr.*; + }; + + entry.value_ptr.* = .{ .package_id = package.meta.id }; + return FolderResolution{ .new_package_id = package.meta.id }; + } + pub fn getOrPut(global_or_relative: GlobalOrRelative, version: Dependency.Version, non_normalized_path: string, manager: *PackageManager) FolderResolution { var joined: [bun.MAX_PATH_BYTES]u8 = undefined; const paths = normalizePackageJSONPath(global_or_relative, &joined, non_normalized_path); @@ -171,7 +281,11 @@ pub const FolderResolution = union(Tag) { const rel = paths[1]; var entry = manager.folders.getOrPut(manager.allocator, hash(abs)) catch unreachable; - if (entry.found_existing) return entry.value_ptr.*; + if (entry.found_existing) { + if (global_or_relative != .git_cache_folder or entry.value_ptr.*.err != error.MissingPackageJSON) { + return entry.value_ptr.*; + } + } joined[abs.len] = 0; var joinedZ: [:0]u8 = joined[0..abs.len :0]; @@ -203,6 +317,15 @@ pub const FolderResolution = union(Tag) { CacheFolderResolver, CacheFolderResolver{ .version = version.value.npm.toVersion() }, ), + .git_cache_folder => readPackageJSONFromDisk( + manager, + joinedZ, + abs, + version, + Features.git, + Resolver, + Resolver{ .folder_path = rel }, + ), } catch |err| { if (err == error.FileNotFound) { entry.value_ptr.* = .{ .err = error.MissingPackageJSON }; |