zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

commit 12cb5b92851f601c44d48deadea9934146edcffa (tree)
parent 36b65ab59e5e514ad06a11cde96b87c565d86ac8
Author: Andrew Kelley <andrew@ziglang.org>
Date:   Sat,  7 Feb 2026 02:46:47 +0100

Merge pull request 'ability to override packages locally' (#31138) from fork-cli into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/31138

Diffstat:
Mlib/compiler/build_runner.zig | 49++++++++++++++++++++++++++-----------------------
Msrc/Package.zig | 108+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/Package/Fetch.zig | 331+++++++++++++++++++------------------------------------------------------------
Dsrc/Package/Fetch/testdata/executables.tar.gz | 0
Msrc/Package/Manifest.zig | 52+++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/main.zig | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
6 files changed, 343 insertions(+), 358 deletions(-)

diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig @@ -1573,6 +1573,23 @@ fn printUsage(b: *std.Build, w: *Writer) !void { \\ -fsys=[name] Enable a system integration \\ -fno-sys=[name] Disable a system integration \\ + \\ -fdarling, -fno-darling Integration with system-installed Darling to + \\ execute macOS programs on Linux hosts + \\ (default: no) + \\ -fqemu, -fno-qemu Integration with system-installed QEMU to execute + \\ foreign-architecture programs on Linux hosts + \\ (default: no) + \\ --libc-runtimes [path] Enhances QEMU integration by providing dynamic libc + \\ (e.g. glibc or musl) built for multiple foreign + \\ architectures, allowing execution of non-native + \\ programs that link with libc. + \\ -frosetta, -fno-rosetta Rely on Rosetta to execute x86_64 programs on + \\ ARM64 macOS hosts. (default: no) + \\ -fwasmtime, -fno-wasmtime Integration with system-installed wasmtime to + \\ execute WASI binaries. (default: no) + \\ -fwine, -fno-wine Integration with system-installed Wine to execute + \\ Windows programs on Linux hosts. (default: no) + \\ \\ Available System Integrations: Enabled: \\ ); @@ -1592,33 +1609,16 @@ fn printUsage(b: *std.Build, w: *Writer) !void { try w.writeAll( \\ \\General Options: + \\ -h, --help Print this help and exit + \\ -l, --list-steps Print available steps + \\ \\ -p, --prefix [path] Where to install files (default: zig-out) \\ --prefix-lib-dir [path] Where to install libraries \\ --prefix-exe-dir [path] Where to install executables \\ --prefix-include-dir [path] Where to install C header files - \\ \\ --release[=mode] Request release mode, optionally specifying a \\ preferred optimization mode: fast, safe, small \\ - \\ -fdarling, -fno-darling Integration with system-installed Darling to - \\ execute macOS programs on Linux hosts - \\ (default: no) - \\ -fqemu, -fno-qemu Integration with system-installed QEMU to execute - \\ foreign-architecture programs on Linux hosts - \\ (default: no) - \\ --libc-runtimes [path] Enhances QEMU integration by providing dynamic libc - \\ (e.g. glibc or musl) built for multiple foreign - \\ architectures, allowing execution of non-native - \\ programs that link with libc. - \\ -frosetta, -fno-rosetta Rely on Rosetta to execute x86_64 programs on - \\ ARM64 macOS hosts. (default: no) - \\ -fwasmtime, -fno-wasmtime Integration with system-installed wasmtime to - \\ execute WASI binaries. (default: no) - \\ -fwine, -fno-wine Integration with system-installed Wine to execute - \\ Windows programs on Linux hosts. (default: no) - \\ - \\ -h, --help Print this help and exit - \\ -l, --list-steps Print available steps \\ --verbose Print commands before executing them \\ --color [auto|off|on] Enable or disable colored error messages \\ --error-style [style] Control how build errors are printed @@ -1641,9 +1641,6 @@ fn printUsage(b: *std.Build, w: *Writer) !void { \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss \\ --test-timeout <timeout> Limit execution time of unit tests, terminating if exceeded. \\ The timeout must include a unit: ns, us, ms, s, m, h - \\ --fetch[=mode] Fetch dependency tree (optionally choose laziness) and exit - \\ needed (Default) Lazy dependencies are fetched as needed - \\ all Lazy dependencies are always fetched \\ --watch Continuously rebuild when source files are modified \\ --debounce <ms> Delay before rebuilding after changed file detected \\ --webui[=ip] Enable the web interface on the given IP address @@ -1656,6 +1653,12 @@ fn printUsage(b: *std.Build, w: *Writer) !void { \\ -fincremental Enable incremental compilation \\ -fno-incremental Disable incremental compilation \\ + \\Package Management Options: + \\ --fetch[=mode] Fetch dependency tree (optionally choose laziness) and exit + \\ needed (Default) Lazy dependencies are fetched as needed + \\ all Lazy dependencies are always fetched + \\ --fork=[path] Override one or more projects from dependency tree + \\ \\Advanced Options: \\ -freference-trace[=num] How many lines of reference trace should be shown per compile error \\ -fno-reference-trace Disable reference trace diff --git a/src/Package.zig b/src/Package.zig @@ -6,10 +6,6 @@ pub const Fetch = @import("Package/Fetch.zig"); pub const build_zig_basename = "build.zig"; pub const Manifest = @import("Package/Manifest.zig"); -pub const multihash_len = 1 + 1 + Hash.Algo.digest_length; -pub const multihash_hex_digest_len = 2 * multihash_len; -pub const MultiHashHexDigest = [multihash_hex_digest_len]u8; - pub const Fingerprint = packed struct(u64) { id: u32, checksum: u32, @@ -77,20 +73,6 @@ pub const Hash = struct { return std.mem.eql(u8, &a.bytes, &b.bytes); } - /// Distinguishes whether the legacy multihash format is being stored here. - pub fn isOld(h: *const Hash) bool { - if (h.bytes.len < 2) return false; - const their_multihash_func = std.fmt.parseInt(u8, h.bytes[0..2], 16) catch return false; - if (@as(MultihashFunction, @enumFromInt(their_multihash_func)) != multihash_function) return false; - if (h.toSlice().len != multihash_hex_digest_len) return false; - return std.mem.indexOfScalar(u8, &h.bytes, '-') == null; - } - - test isOld { - const h: Hash = .fromSlice("1220138f4aba0c01e66b68ed9e1e1e74614c06e4743d88bc58af4f1c3dd0aae5fea7"); - try std.testing.expect(h.isOld()); - } - /// Produces "$name-$semver-$hashplus". /// * name is the name field from build.zig.zon, asserted to be at most 32 /// bytes and assumed be a valid zig identifier @@ -137,55 +119,65 @@ pub const Hash = struct { _ = std.fmt.bufPrint(result.bytes[i..], "{x}", .{&bin_digest}) catch unreachable; return result; } -}; -pub const MultihashFunction = enum(u16) { - identity = 0x00, - sha1 = 0x11, - @"sha2-256" = 0x12, - @"sha2-512" = 0x13, - @"sha3-512" = 0x14, - @"sha3-384" = 0x15, - @"sha3-256" = 0x16, - @"sha3-224" = 0x17, - @"sha2-384" = 0x20, - @"sha2-256-trunc254-padded" = 0x1012, - @"sha2-224" = 0x1013, - @"sha2-512-224" = 0x1014, - @"sha2-512-256" = 0x1015, - @"blake2b-256" = 0xb220, - _, -}; + pub fn projectId(hash: *const Hash) ProjectId { + const bytes = hash.toSlice(); + const name = std.mem.sliceTo(bytes, '-'); + const encoded_hashplus = bytes[bytes.len - 44 ..]; + var hashplus: [33]u8 = undefined; + std.base64.url_safe_no_pad.Decoder.decode(&hashplus, encoded_hashplus) catch unreachable; + const fingerprint_id = std.mem.readInt(u32, hashplus[0..4], .little); + return .init(name, fingerprint_id); + } -pub const multihash_function: MultihashFunction = switch (Hash.Algo) { - std.crypto.hash.sha2.Sha256 => .@"sha2-256", - else => unreachable, -}; + test projectId { + const hash: Hash = .fromSlice("pulseaudio-16.1.1-9-mk_62MZkNwBaFwiZ7ZVrYRIf_3dTqqJR5PbMRCJzSuLw"); + const project_id = hash.projectId(); -pub fn multiHashHexDigest(digest: Hash.Digest) MultiHashHexDigest { - const hex_charset = std.fmt.hex_charset; + var expected_name: [32]u8 = @splat(0); + expected_name[0.."pulseaudio".len].* = "pulseaudio".*; + try std.testing.expectEqualSlices(u8, &expected_name, &project_id.padded_name); - var result: MultiHashHexDigest = undefined; + try std.testing.expectEqual(0xd8fa4f9a, project_id.fingerprint_id); + } - result[0] = hex_charset[@intFromEnum(multihash_function) >> 4]; - result[1] = hex_charset[@intFromEnum(multihash_function) & 15]; + test "projectId with dashes in the base64" { + const hash: Hash = .fromSlice("dvui-0.4.0-dev-AQFJmayi2gAKE7FeJoF61v5U1IV9-SupoEcFutIZYpkC"); + const project_id = hash.projectId(); - result[2] = hex_charset[Hash.Algo.digest_length >> 4]; - result[3] = hex_charset[Hash.Algo.digest_length & 15]; + var expected_name: [32]u8 = @splat(0); + expected_name[0.."dvui".len].* = "dvui".*; + try std.testing.expectEqualSlices(u8, &expected_name, &project_id.padded_name); - for (digest, 0..) |byte, i| { - result[4 + i * 2] = hex_charset[byte >> 4]; - result[5 + i * 2] = hex_charset[byte & 15]; + try std.testing.expectEqual(0x99490101, project_id.fingerprint_id); } - return result; -} +}; -comptime { - // We avoid unnecessary uleb128 code in hexDigest by asserting here the - // values are small enough to be contained in the one-byte encoding. - assert(@intFromEnum(multihash_function) < 127); - assert(Hash.Algo.digest_length < 127); -} +/// Minimum information required to identify whether a package is an artifact +/// of a given project. +pub const ProjectId = struct { + /// Bytes after name.len are set to zero. + padded_name: [32]u8, + fingerprint_id: u32, + + pub fn init(name: []const u8, fingerprint_id: u32) ProjectId { + var padded_name: [32]u8 = @splat(0); + @memcpy(padded_name[0..name.len], name); + return .{ + .padded_name = padded_name, + .fingerprint_id = fingerprint_id, + }; + } + + pub fn eql(a: *const ProjectId, b: *const ProjectId) bool { + return a.fingerprint_id == b.fingerprint_id and std.mem.eql(u8, &a.padded_name, &b.padded_name); + } + + pub fn hash(a: *const ProjectId) u64 { + const x: u64 = @bitCast(a.padded_name[0..8].*); + return std.hash.int(x | a.fingerprint_id); + } +}; test Hash { const example_digest: Hash.Digest = .{ diff --git a/src/Package/Fetch.zig b/src/Package/Fetch.zig @@ -76,8 +76,9 @@ use_latest_commit: bool, /// Relative to the build root of the root package. package_root: Cache.Path, error_bundle: ErrorBundle.Wip, -manifest: ?Manifest, +manifest: Manifest, manifest_ast: std.zig.Ast, +have_manifest: bool, computed_hash: ComputedHash, /// Fetch logic notices whether a package has a build.zig file and sets this flag. has_build_zig: bool, @@ -142,6 +143,9 @@ pub const JobQueue = struct { /// Set of hashes that will be additionally fetched even if they are marked /// as lazy. unlazy_set: UnlazySet = .{}, + /// Identifies paths that override all packages in the tree with matching + /// project ids. + fork_set: ForkSet = .{}, pub const Mode = enum { /// Non-lazy dependencies are always fetched. @@ -152,6 +156,38 @@ pub const JobQueue = struct { }; pub const Table = std.AutoArrayHashMapUnmanaged(Package.Hash, *Fetch); pub const UnlazySet = std.AutoArrayHashMapUnmanaged(Package.Hash, void); + pub const ForkSet = std.ArrayHashMapUnmanaged(Fork, void, Fork.Context, false); + + pub const Fork = struct { + path: Cache.Path, + manifest_ast: std.zig.Ast, + manifest: Package.Manifest, + uses: usize, + + pub const Context = struct { + pub fn hash(_: @This(), a: Fork) u32 { + const project_id: Package.ProjectId = .init(a.manifest.name, a.manifest.id); + return @truncate(project_id.hash()); + } + + pub fn eql(_: @This(), a: Fork, b: Fork, _: usize) bool { + const a_project_id: Package.ProjectId = .init(a.manifest.name, a.manifest.id); + const b_project_id: Package.ProjectId = .init(b.manifest.name, b.manifest.id); + return a_project_id.eql(&b_project_id); + } + }; + + pub const Adapter = struct { + pub fn hash(_: @This(), a: Package.ProjectId) u32 { + return @truncate(a.hash()); + } + + pub fn eql(_: @This(), a_project_id: Package.ProjectId, b: Fork, _: usize) bool { + const b_project_id: Package.ProjectId = .init(b.manifest.name, b.manifest.id); + return a_project_id.eql(&b_project_id); + } + }; + }; pub fn deinit(jq: *JobQueue) void { const io = jq.io; @@ -248,7 +284,8 @@ pub const JobQueue = struct { , .{std.zig.fmtString(hash_slice)}); } - if (fetch.manifest) |*manifest| { + if (fetch.have_manifest) { + const manifest = &fetch.manifest; try buf.appendSlice( \\ pub const deps: []const struct { []const u8, []const u8 } = &.{ \\ @@ -283,7 +320,8 @@ pub const JobQueue = struct { ); const root_fetch = jq.all_fetches.items[0]; - const root_manifest = &root_fetch.manifest.?; + assert(root_fetch.have_manifest); + const root_manifest = &root_fetch.manifest; for (root_manifest.dependencies.keys(), root_manifest.dependencies.values()) |name, dep| { const h = depDigest(root_fetch.package_root, jq.global_cache, dep) orelse continue; @@ -536,6 +574,19 @@ pub fn run(f: *Fetch) RunError!void { var resource_buffer: [init_resource_buffer_size]u8 = undefined; if (remote.hash) |expected_hash| { + const expected_project_id: Package.ProjectId = expected_hash.projectId(); + if (job_queue.fork_set.getKeyPtrAdapted(expected_project_id, @as(JobQueue.Fork.Adapter, .{}))) |fork| { + log.debug("using fork {f} for {s}", .{ fork.path, fork.manifest.name }); + fork.uses += 1; + f.package_root = fork.path; + f.manifest_ast = fork.manifest_ast; + f.manifest = fork.manifest; + f.have_manifest = true; + try checkBuildFileExistence(f); + if (!job_queue.recursive) return; + return queueJobsForDeps(f); + } + const package_root = try job_queue.root_pkg_path.join(arena, expected_hash.toSlice()); if (package_root.root_dir.handle.access(io, package_root.sub_path, .{})) |_| { assert(f.lazy_status != .unavailable); @@ -673,7 +724,7 @@ fn runResource( try loadManifest(f, pkg_path); const filter: Filter = .{ - .include_paths = if (f.manifest) |m| m.paths else .{}, + .include_paths = if (f.have_manifest) f.manifest.paths else .{}, }; // Ignore errors that were excluded by manifest, such as failure to @@ -728,21 +779,11 @@ fn runResource( if (remote_hash) |declared_hash| { const hash_tok = f.hash_tok.unwrap().?; - if (declared_hash.isOld()) { - const actual_hex = Package.multiHashHexDigest(f.computed_hash.digest); - if (!std.mem.eql(u8, declared_hash.toSlice(), &actual_hex)) { - return f.fail(hash_tok, try eb.printString( - "hash mismatch: manifest declares '{s}' but the fetched package has '{s}'", - .{ declared_hash.toSlice(), actual_hex }, - )); - } - } else { - if (!computed_package_hash.eql(&declared_hash)) { - return f.fail(hash_tok, try eb.printString( - "hash mismatch: manifest declares '{s}' but the fetched package has '{s}'", - .{ declared_hash.toSlice(), computed_package_hash.toSlice() }, - )); - } + if (!computed_package_hash.eql(&declared_hash)) { + return f.fail(hash_tok, try eb.printString( + "hash mismatch: manifest declares '{s}' but the fetched package has '{s}'", + .{ declared_hash.toSlice(), computed_package_hash.toSlice() }, + )); } } else if (!f.omit_missing_hash_error) { const notes_len = 1; @@ -766,7 +807,8 @@ fn runResource( pub fn computedPackageHash(f: *const Fetch) Package.Hash { const saturated_size = std.math.cast(u32, f.computed_hash.total_size) orelse std.math.maxInt(u32); - if (f.manifest) |man| { + if (f.have_manifest) { + const man = &f.manifest; var version_buffer: [32]u8 = undefined; const version: []const u8 = std.fmt.bufPrint(&version_buffer, "{f}", .{man.version}) catch &version_buffer; return .init(f.computed_hash.digest, man.name, version, man.id, saturated_size); @@ -801,45 +843,28 @@ fn loadManifest(f: *Fetch, pkg_root: Cache.Path) RunError!void { const io = f.job_queue.io; const eb = &f.error_bundle; const arena = f.arena.allocator(); - const manifest_bytes = pkg_root.root_dir.handle.readFileAllocOptions( + const manifest_path = try pkg_root.join(arena, Manifest.basename); + + Manifest.load( io, - try fs.path.join(arena, &.{ pkg_root.sub_path, Manifest.basename }), arena, - .limited(Manifest.max_bytes), - .@"1", - 0, + manifest_path, + &f.manifest_ast, + eb, + &f.manifest, + f.allow_missing_paths_field, ) catch |err| switch (err) { error.FileNotFound => return, + error.Canceled => |e| return e, + error.ErrorsBundled => return error.FetchFailed, else => |e| { - const file_path = try pkg_root.join(arena, Manifest.basename); try eb.addRootErrorMessage(.{ - .msg = try eb.printString("unable to load package manifest '{f}': {t}", .{ file_path, e }), + .msg = try eb.printString("unable to load package manifest '{f}': {t}", .{ manifest_path, e }), }); return error.FetchFailed; }, }; - - const ast = &f.manifest_ast; - ast.* = try std.zig.Ast.parse(arena, manifest_bytes, .zon); - - if (ast.errors.len > 0) { - const file_path = try std.fmt.allocPrint(arena, "{f}" ++ fs.path.sep_str ++ Manifest.basename, .{pkg_root}); - try std.zig.putAstErrorsIntoBundle(arena, ast.*, file_path, eb); - return error.FetchFailed; - } - - const rng: std.Random.IoSource = .{ .io = io }; - - f.manifest = try Manifest.parse(arena, ast.*, rng.interface(), .{ - .allow_missing_paths_field = f.allow_missing_paths_field, - }); - const manifest = &f.manifest.?; - - if (manifest.errors.len > 0) { - const src_path = try eb.printString("{f}" ++ fs.path.sep_str ++ "{s}", .{ pkg_root, Manifest.basename }); - try manifest.copyErrorsIntoBundle(ast.*, src_path, eb); - return error.FetchFailed; - } + f.have_manifest = true; } fn queueJobsForDeps(f: *Fetch) RunError!void { @@ -848,7 +873,8 @@ fn queueJobsForDeps(f: *Fetch) RunError!void { assert(f.job_queue.recursive); // If the package does not have a build.zig.zon file then there are no dependencies. - const manifest = f.manifest orelse return; + if (!f.have_manifest) return; + const manifest = &f.manifest; const new_fetches, const prog_names = nf: { const parent_arena = f.arena.allocator(); @@ -953,8 +979,9 @@ fn queueJobsForDeps(f: *Fetch) RunError!void { .package_root = undefined, .error_bundle = undefined, - .manifest = null, + .manifest = undefined, .manifest_ast = undefined, + .have_manifest = false, .computed_hash = undefined, .has_build_zig = false, .oom_flag = false, @@ -1931,7 +1958,7 @@ const Filter = struct { include_paths: std.StringArrayHashMapUnmanaged(void) = .empty, /// sub_path is relative to the package root. - pub fn includePath(self: Filter, sub_path: []const u8) bool { + pub fn includePath(self: *const Filter, sub_path: []const u8) bool { if (self.include_paths.count() == 0) return true; if (self.include_paths.contains("")) return true; if (self.include_paths.contains(".")) return true; @@ -2202,206 +2229,6 @@ const UnpackResult = struct { } }; -test "set executable bit based on file content" { - if (!Io.File.Permissions.has_executable_bit) return error.SkipZigTest; - const gpa = std.testing.allocator; - const io = std.testing.io; - - var tmp = std.testing.tmpDir(.{}); - defer tmp.cleanup(); - - const tarball_name = "executables.tar.gz"; - try saveEmbedFile(io, tarball_name, tmp.dir); - const tarball_path = try std.fmt.allocPrint(gpa, ".zig-cache/tmp/{s}/{s}", .{ tmp.sub_path, tarball_name }); - defer gpa.free(tarball_path); - - // $ tar -tvf executables.tar.gz - // drwxrwxr-x 0 executables/ - // -rwxrwxr-x 170 executables/hello - // lrwxrwxrwx 0 executables/hello_ln -> hello - // -rw-rw-r-- 0 executables/file1 - // -rw-rw-r-- 17 executables/script_with_shebang_without_exec_bit - // -rwxrwxr-x 7 executables/script_without_shebang - // -rwxrwxr-x 17 executables/script - - var fb: TestFetchBuilder = undefined; - var fetch = try fb.build(gpa, io, tmp.dir, tarball_path); - defer fb.deinit(); - - try fetch.run(); - try std.testing.expectEqualStrings( - "1220fecb4c06a9da8673c87fe8810e15785f1699212f01728eadce094d21effeeef3", - &Package.multiHashHexDigest(fetch.computed_hash.digest), - ); - - var out = try fb.packageDir(); - defer out.close(io); - const S = std.posix.S; - // expect executable bit not set - try std.testing.expect((try out.statFile(io, "file1", .{})).permissions.toMode() & S.IXUSR == 0); - try std.testing.expect((try out.statFile(io, "script_without_shebang", .{})).permissions.toMode() & S.IXUSR == 0); - // expect executable bit set - try std.testing.expect((try out.statFile(io, "hello", .{})).permissions.toMode() & S.IXUSR != 0); - try std.testing.expect((try out.statFile(io, "script", .{})).permissions.toMode() & S.IXUSR != 0); - try std.testing.expect((try out.statFile(io, "script_with_shebang_without_exec_bit", .{})).permissions.toMode() & S.IXUSR != 0); - try std.testing.expect((try out.statFile(io, "hello_ln", .{})).permissions.toMode() & S.IXUSR != 0); - - // - // $ ls -al zig-cache/tmp/OCz9ovUcstDjTC_U/zig-global-cache/p/1220fecb4c06a9da8673c87fe8810e15785f1699212f01728eadce094d21effeeef3 - // -rw-rw-r-- 1 0 Apr file1 - // -rwxrwxr-x 1 170 Apr hello - // lrwxrwxrwx 1 5 Apr hello_ln -> hello - // -rwxrwxr-x 1 17 Apr script - // -rw-rw-r-- 1 7 Apr script_without_shebang - // -rwxrwxr-x 1 17 Apr script_with_shebang_without_exec_bit -} - -fn saveEmbedFile(io: Io, comptime tarball_name: []const u8, dir: Io.Dir) !void { - //const tarball_name = "duplicate_paths_excluded.tar.gz"; - const tarball_content = @embedFile("Fetch/testdata/" ++ tarball_name); - var tmp_file = try dir.createFile(io, tarball_name, .{}); - defer tmp_file.close(io); - try tmp_file.writeStreamingAll(io, tarball_content); -} - -// Builds Fetch with required dependencies, clears dependencies on deinit(). -const TestFetchBuilder = struct { - http_client: std.http.Client, - global_cache_directory: Cache.Directory, - local_cache_path: Cache.Path, - job_queue: Fetch.JobQueue, - fetch: Fetch, - - fn build( - self: *TestFetchBuilder, - allocator: std.mem.Allocator, - io: Io, - cache_parent_dir: std.Io.Dir, - path_or_url: []const u8, - ) !*Fetch { - const global_cache_dir = try cache_parent_dir.createDirPathOpen(io, "zig-global-cache", .{}); - const package_root_dir = try cache_parent_dir.createDirPathOpen(io, "local-project-root", .{}); - - self.http_client = .{ .allocator = allocator, .io = io }; - self.global_cache_directory = .{ .handle = global_cache_dir, .path = "zig-global-cache" }; - self.local_cache_path = .{ - .root_dir = .{ .handle = package_root_dir, .path = "local-project-root" }, - .sub_path = ".zig-cache", - }; - - self.job_queue = .{ - .io = io, - .http_client = &self.http_client, - .global_cache = self.global_cache_directory, - .local_cache = self.local_cache_path, - .root_pkg_path = .{ - .root_dir = .{ .handle = package_root_dir, .path = "local-project-root" }, - .sub_path = "zig-pkg", - }, - .recursive = false, - .read_only = false, - .debug_hash = false, - .mode = .needed, - .prog_node = std.Progress.Node.none, - }; - - self.fetch = .{ - .arena = std.heap.ArenaAllocator.init(allocator), - .location = .{ .path_or_url = path_or_url }, - .location_tok = 0, - .hash_tok = .none, - .name_tok = 0, - .lazy_status = .eager, - .parent_package_root = .{ .root_dir = .{ .handle = package_root_dir, .path = null } }, - .parent_manifest_ast = null, - .prog_node = std.Progress.Node.none, - .job_queue = &self.job_queue, - .omit_missing_hash_error = true, - .allow_missing_paths_field = false, - .use_latest_commit = true, - - .package_root = undefined, - .error_bundle = undefined, - .manifest = null, - .manifest_ast = undefined, - .computed_hash = undefined, - .has_build_zig = false, - .oom_flag = false, - .latest_commit = null, - - .module = null, - }; - return &self.fetch; - } - - fn deinit(self: *TestFetchBuilder) void { - const io = self.job_queue.io; - self.fetch.deinit(); - self.job_queue.deinit(); - self.fetch.prog_node.end(); - self.global_cache_directory.handle.close(io); - self.http_client.deinit(); - } - - fn packageDir(self: *TestFetchBuilder) !Io.Dir { - const io = self.job_queue.io; - const root = self.fetch.package_root; - return try root.root_dir.handle.openDir(io, root.sub_path, .{ .iterate = true }); - } - - // Test helper, asserts thet package dir constains expected_files. - // expected_files must be sorted. - fn expectPackageFiles(self: *TestFetchBuilder, expected_files: []const []const u8) !void { - const io = self.job_queue.io; - const gpa = std.testing.allocator; - - var package_dir = try self.packageDir(); - defer package_dir.close(io); - - var actual_files: std.ArrayList([]u8) = .empty; - defer actual_files.deinit(gpa); - defer for (actual_files.items) |file| gpa.free(file); - var walker = try package_dir.walk(gpa); - defer walker.deinit(); - while (try walker.next(io)) |entry| { - if (entry.kind != .file) continue; - const path = try gpa.dupe(u8, entry.path); - errdefer gpa.free(path); - std.mem.replaceScalar(u8, path, std.fs.path.sep, '/'); - try actual_files.append(gpa, path); - } - std.mem.sortUnstable([]u8, actual_files.items, {}, struct { - fn lessThan(_: void, a: []u8, b: []u8) bool { - return std.mem.lessThan(u8, a, b); - } - }.lessThan); - - try std.testing.expectEqual(expected_files.len, actual_files.items.len); - for (expected_files, 0..) |file_name, i| { - try std.testing.expectEqualStrings(file_name, actual_files.items[i]); - } - try std.testing.expectEqualDeep(expected_files, actual_files.items); - } - - // Test helper, asserts that fetch has failed with `msg` error message. - fn expectFetchErrors(self: *TestFetchBuilder, notes_len: usize, msg: []const u8) !void { - const gpa = std.testing.allocator; - - var errors = try self.fetch.error_bundle.toOwnedBundle(""); - defer errors.deinit(gpa); - - const em = errors.getErrorMessage(errors.getMessages()[0]); - try std.testing.expectEqual(1, em.count); - if (notes_len > 0) { - try std.testing.expectEqual(notes_len, em.notes_len); - } - var aw: Io.Writer.Allocating = .init(gpa); - defer aw.deinit(); - try errors.renderToWriter(.{}, &aw.writer); - try std.testing.expectEqualStrings(msg, aw.written()); - } -}; - test { _ = Filter; _ = FileType; diff --git a/src/Package/Fetch/testdata/executables.tar.gz b/src/Package/Fetch/testdata/executables.tar.gz Binary files differ. diff --git a/src/Package/Manifest.zig b/src/Package/Manifest.zig @@ -1,10 +1,13 @@ const Manifest = @This(); + const std = @import("std"); +const Io = std.Io; const mem = std.mem; const Allocator = std.mem.Allocator; const assert = std.debug.assert; const Ast = std.zig.Ast; const testing = std.testing; + const Package = @import("../Package.zig"); pub const max_bytes = 10 * 1024 * 1024; @@ -53,7 +56,7 @@ pub const ParseOptions = struct { pub const Error = Allocator.Error; -pub fn parse(gpa: Allocator, ast: Ast, rng: std.Random, options: ParseOptions) Error!Manifest { +pub fn parse(gpa: Allocator, ast: *const Ast, rng: std.Random, options: ParseOptions) Error!Manifest { const main_node_index = ast.nodeData(.root).node; var arena_instance = std.heap.ArenaAllocator.init(gpa); @@ -61,7 +64,7 @@ pub fn parse(gpa: Allocator, ast: Ast, rng: std.Random, options: ParseOptions) E var p: Parse = .{ .gpa = gpa, - .ast = ast, + .ast = ast.*, .arena = arena_instance.allocator(), .errors = .{}, @@ -578,6 +581,45 @@ const Parse = struct { } }; +pub fn load( + io: Io, + arena: Allocator, + manifest_path: std.Build.Cache.Path, + ast: *std.zig.Ast, + error_bundle: *std.zig.ErrorBundle.Wip, + manifest: *Manifest, + allow_missing_paths_field: bool, +) !void { + const manifest_bytes = try manifest_path.root_dir.handle.readFileAllocOptions( + io, + manifest_path.sub_path, + arena, + .limited(max_bytes), + .@"1", + 0, + ); + + ast.* = try std.zig.Ast.parse(arena, manifest_bytes, .zon); + + if (ast.errors.len > 0) { + const file_path = try manifest_path.joinString(arena, ""); + try std.zig.putAstErrorsIntoBundle(arena, ast.*, file_path, error_bundle); + return error.ErrorsBundled; + } + + const rng: std.Random.IoSource = .{ .io = io }; + + manifest.* = try parse(arena, ast, rng.interface(), .{ + .allow_missing_paths_field = allow_missing_paths_field, + }); + + if (manifest.errors.len > 0) { + const src_path = try error_bundle.printString("{f}", .{manifest_path}); + try manifest.copyErrorsIntoBundle(ast.*, src_path, error_bundle); + return error.ErrorsBundled; + } +} + test "basic" { const gpa = testing.allocator; @@ -603,7 +645,7 @@ test "basic" { var rng = std.Random.DefaultPrng.init(0); - var manifest = try Manifest.parse(gpa, ast, rng.random(), .{}); + var manifest = try Manifest.parse(gpa, &ast, rng.random(), .{}); defer manifest.deinit(gpa); try testing.expect(manifest.errors.len == 0); @@ -649,7 +691,7 @@ test "minimum_zig_version" { var rng = std.Random.DefaultPrng.init(0); - var manifest = try Manifest.parse(gpa, ast, rng.random(), .{}); + var manifest = try Manifest.parse(gpa, &ast, rng.random(), .{}); defer manifest.deinit(gpa); try testing.expect(manifest.errors.len == 0); @@ -684,7 +726,7 @@ test "minimum_zig_version - invalid version" { var rng = std.Random.DefaultPrng.init(0); - var manifest = try Manifest.parse(gpa, ast, rng.random(), .{}); + var manifest = try Manifest.parse(gpa, &ast, rng.random(), .{}); defer manifest.deinit(gpa); try testing.expect(manifest.errors.len == 1); diff --git a/src/main.zig b/src/main.zig @@ -4896,7 +4896,8 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, var override_global_cache_dir: ?[]const u8 = EnvVar.ZIG_GLOBAL_CACHE_DIR.get(environ_map); var override_local_cache_dir: ?[]const u8 = EnvVar.ZIG_LOCAL_CACHE_DIR.get(environ_map); var override_build_runner: ?[]const u8 = EnvVar.ZIG_BUILD_RUNNER.get(environ_map); - var child_argv = std.array_list.Managed([]const u8).init(arena); + var child_argv: std.ArrayList([]const u8) = .empty; + var forks: std.ArrayList(Fork) = .empty; var reference_trace: ?u32 = null; var debug_compile_errors = false; var verbose_link = (native_os != .wasi or builtin.link_libc) and @@ -4917,24 +4918,24 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, var debug_libc_paths_file: ?[]const u8 = null; const argv_index_exe = child_argv.items.len; - _ = try child_argv.addOne(); + _ = try child_argv.addOne(arena); const self_exe_path = try process.executablePathAlloc(io, arena); - try child_argv.append(self_exe_path); + try child_argv.append(arena, self_exe_path); const argv_index_zig_lib_dir = child_argv.items.len; - _ = try child_argv.addOne(); + _ = try child_argv.addOne(arena); const argv_index_build_file = child_argv.items.len; - _ = try child_argv.addOne(); + _ = try child_argv.addOne(arena); const argv_index_cache_dir = child_argv.items.len; - _ = try child_argv.addOne(); + _ = try child_argv.addOne(arena); const argv_index_global_cache_dir = child_argv.items.len; - _ = try child_argv.addOne(); + _ = try child_argv.addOne(arena); - try child_argv.appendSlice(&.{ + try child_argv.appendSlice(arena, &.{ "--seed", try std.fmt.allocPrint(arena, "0x{x}", .{randInt(io, u32)}), }); @@ -4955,7 +4956,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, // read this file in the parent to obtain the results, in the case the child // exits with code 3. const results_tmp_file_nonce = std.fmt.hex(randInt(io, u64)); - try child_argv.append("-Z" ++ results_tmp_file_nonce); + try child_argv.append(arena, "-Z" ++ results_tmp_file_nonce); var color: Color = .auto; var n_jobs: ?u32 = null; @@ -5000,11 +5001,24 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, fatal("expected [needed|all] after '--fetch=', found '{s}'", .{ sub_arg, }); + } else if (mem.cutPrefix(u8, arg, "--fork=")) |sub_arg| { + try forks.append(arena, .{ + .manifest_ast = undefined, + .manifest = undefined, + .error_bundle = undefined, + .arena_allocator = undefined, + .path = .{ + .root_dir = .cwd(), + .sub_path = sub_arg, + }, + .failed = false, + }); + continue; } else if (mem.eql(u8, arg, "--system")) { if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg}); i += 1; system_pkg_dir_path = args[i]; - try child_argv.append("--system"); + try child_argv.append(arena, "--system"); continue; } else if (mem.cutPrefix(u8, arg, "-freference-trace=")) |num| { reference_trace = std.fmt.parseUnsigned(u32, num, 10) catch |err| { @@ -5014,7 +5028,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, reference_trace = null; } else if (mem.eql(u8, arg, "--debug-log")) { if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg}); - try child_argv.appendSlice(args[i .. i + 2]); + try child_argv.appendSlice(arena, args[i .. i + 2]); i += 1; if (!build_options.enable_logging) { warn("Zig was compiled without logging enabled (-Dlog). --debug-log has no effect.", .{}); @@ -5070,7 +5084,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, color = std.meta.stringToEnum(Color, args[i]) orelse { fatal("expected [auto|on|off] after {s}, found '{s}'", .{ arg, args[i] }); }; - try child_argv.appendSlice(&.{ arg, args[i] }); + try child_argv.appendSlice(arena, &.{ arg, args[i] }); continue; } else if (mem.cutPrefix(u8, arg, "-j")) |str| { const num = std.fmt.parseUnsigned(u32, str, 10) catch |err| { @@ -5090,11 +5104,11 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, } else if (mem.eql(u8, arg, "--")) { // The rest of the args are supposed to get passed onto // build runner's `build.args` - try child_argv.appendSlice(args[i..]); + try child_argv.appendSlice(arena, args[i..]); break; } } - try child_argv.append(arg); + try child_argv.append(arena, arg); } } @@ -5182,6 +5196,29 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, defer http_client.deinit(); var unlazy_set: Package.Fetch.JobQueue.UnlazySet = .{}; + var fork_set: Package.Fetch.JobQueue.ForkSet = .{}; + + { + // Populate fork_set. + var group: Io.Group = .init; + defer group.cancel(io); + + for (forks.items) |*fork| + group.async(io, Fork.load, .{ io, gpa, fork, color }); + + try group.await(io); + + for (forks.items) |*fork| { + if (fork.failed) process.exit(1); + try fork_set.put(arena, .{ + .path = fork.path, + .manifest_ast = fork.manifest_ast, + .manifest = fork.manifest, + .uses = 0, + }, {}); + } + } + defer Fork.deinitList(forks.items); // This loop is re-evaluated when the build script exits with an indication that it // could not continue due to missing lazy dependencies. @@ -5235,6 +5272,9 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, const fetch_prog_node = root_prog_node.start("Fetch Packages", 0); defer fetch_prog_node.end(); + // Reset fork match counts. + for (fork_set.keys()) |*fork| fork.uses = 0; + var job_queue: Package.Fetch.JobQueue = .{ .io = io, .http_client = &http_client, @@ -5245,6 +5285,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, .recursive = true, .debug_hash = false, .unlazy_set = unlazy_set, + .fork_set = fork_set, .mode = fetch_mode, .prog_node = fetch_prog_node, }; @@ -5290,8 +5331,9 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, .package_root = undefined, .error_bundle = undefined, - .manifest = null, + .manifest = undefined, .manifest_ast = undefined, + .have_manifest = false, .computed_hash = undefined, .has_build_zig = true, .oom_flag = false, @@ -5309,6 +5351,26 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, job_queue.group.async(io, Package.Fetch.workerRun, .{ &fetch, "root" }); try job_queue.group.await(io); + { + // Ensure that forks were actually used. This is done + // before printing manifest errors because using a fork can + // prevent them. + var any_unused = false; + for (fork_set.keys()) |*fork| { + if (fork.uses == 0) { + std.log.err("fork {f} matched no {s} packages", .{ + fork.path, fork.manifest.name, + }); + any_unused = true; + } else { + std.log.info("fork {f} matched {d} {s} packages", .{ + fork.path, fork.uses, fork.manifest.name, + }); + } + } + if (any_unused) process.exit(1); + } + try job_queue.consolidateErrors(); if (fetch.error_bundle.root_list.items.len > 0) { @@ -5369,7 +5431,8 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, // dependencies' build.zig modules by name. for (fetches) |f| { const mod = f.module orelse continue; - const man = f.manifest orelse continue; + if (!f.have_manifest) continue; + const man = &f.manifest; const dep_names = man.dependencies.keys(); try mod.deps.ensureUnusedCapacity(arena, @intCast(dep_names.len)); for (dep_names, man.dependencies.values()) |name, dep| { @@ -5518,6 +5581,63 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, } } +const Fork = struct { + path: Path, + manifest_ast: std.zig.Ast, + manifest: Package.Manifest, + error_bundle: std.zig.ErrorBundle.Wip, + failed: bool, + arena_allocator: std.heap.ArenaAllocator, + + fn load(io: Io, gpa: Allocator, fork: *Fork, color: Color) Io.Cancelable!void { + loadFallible(io, gpa, fork, color) catch |err| switch (err) { + error.Canceled => |e| return e, + error.AlreadyReported => fork.failed = true, + else => |e| { + std.log.err("failed to load fork at {f}: {t}", .{ fork.path, e }); + fork.failed = true; + }, + }; + } + + fn loadFallible(io: Io, gpa: Allocator, fork: *Fork, color: Color) !void { + fork.arena_allocator = .init(gpa); + const arena = fork.arena_allocator.allocator(); + + var error_bundle: std.zig.ErrorBundle.Wip = undefined; + try error_bundle.init(gpa); + defer error_bundle.deinit(); + + const manifest_path = try fork.path.join(arena, Package.Manifest.basename); + + Package.Manifest.load( + io, + arena, + manifest_path, + &fork.manifest_ast, + &error_bundle, + &fork.manifest, + true, + ) catch |err| switch (err) { + error.Canceled => |e| return e, + error.ErrorsBundled => { + assert(error_bundle.root_list.items.len > 0); + var errors = try error_bundle.toOwnedBundle(""); + errors.renderToStderr(io, .{}, color) catch {}; + return error.AlreadyReported; + }, + else => |e| { + std.log.err("failed to load package manifest {f}: {t}", .{ manifest_path, e }); + return error.AlreadyReported; + }, + }; + } + + fn deinitList(forks: []Fork) void { + for (forks) |*fork| fork.arena_allocator.deinit(); + } +}; + const JitCmdOptions = struct { cmd_name: []const u8, root_src_path: []const u8, @@ -7048,8 +7168,9 @@ fn cmdFetch( .package_root = undefined, .error_bundle = undefined, - .manifest = null, + .manifest = undefined, .manifest_ast = undefined, + .have_manifest = false, .computed_hash = undefined, .has_build_zig = false, .oom_flag = false, @@ -7085,9 +7206,9 @@ fn cmdFetch( }, .yes, .exact => |name| name: { if (name) |n| break :name n; - const fetched_manifest = fetch.manifest orelse + if (!fetch.have_manifest) fatal("unable to determine name; fetched package has no build.zig.zon file", .{}); - break :name fetched_manifest.name; + break :name fetch.manifest.name; }, }; @@ -7410,7 +7531,7 @@ fn loadManifest( process.exit(2); } - var manifest = try Package.Manifest.parse(gpa, ast, rng.interface(), .{}); + var manifest = try Package.Manifest.parse(gpa, &ast, rng.interface(), .{}); errdefer manifest.deinit(gpa); if (manifest.errors.len > 0) {