aboutsummaryrefslogtreecommitdiff
path: root/src/install/install.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/install/install.zig')
-rw-r--r--src/install/install.zig434
1 files changed, 384 insertions, 50 deletions
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();
}
}