From c8160b8a2ecd8ebf23e6e26a5dbb4dd6f3e60ffc Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 12 Oct 2026 17:45:44 -0700 Subject: [PATCH 1/5] Maker: --help works even when configuration fails --- lib/compiler/Maker.zig | 95 ++++++---- lib/compiler/Maker/ScannedConfig.zig | 255 ++++++++++++++------------- 2 files changed, 196 insertions(+), 154 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index 12df472dea24af6477265bbd9e922542db8a8757..bde0e08ef07b36ef113e0500238ec2101802c726 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -23,6 +23,7 @@ const Color = std.zig.Color; const Client = std.zig.Client; const Server = std.zig.Server; const EnvVar = std.zig.EnvVar; +const ErrorBundle = std.zig.ErrorBundle; const default_local_zig_cache_basename = std.zig.default_local_zig_cache_basename; const stringToEnum = std.meta.stringToEnum; @@ -35,6 +36,7 @@ const ScannedConfig = @import("Maker/ScannedConfig.zig"); const PkgConfig = @import("Maker/PkgConfig.zig"); const Fetch = @import("Maker/Fetch.zig"); const Package = @import("Maker/Package.zig"); +const Manifest = Package.Manifest; pub const std_options: std.Options = .{ .side_channels_mitigations = .none, @@ -601,10 +603,16 @@ pub fn main(init: process.Init.Minimal) !void { const cwd_path = std.zig.getResolvedCwd(io, arena) catch |err| fatal("resolving current directory path failed: {t}", .{err}); - var build_root = try findBuildRoot(arena, io, .{ + var build_root = findBuildRoot(arena, io, .{ .cwd_path = cwd_path, .build_file = build_file, - }); + }) catch |err| switch (err) { + error.BuildZigNotFound => { + if (help_menu) printUsageWithoutConfiguration(&graph, "build.zig not found"); + process.exit(1); + }, + else => |e| return e, + }; defer build_root.deinit(io); graph.build_root_directory = build_root.directory; @@ -740,7 +748,7 @@ pub fn main(init: process.Init.Minimal) !void { var configure_source_files: Cache.Manifest.SelfContainedFiles = .empty; defer configure_source_files.deinit(gpa); - var configure_error_bundle: std.zig.ErrorBundle = .empty; + var configure_error_bundle: ErrorBundle = .empty; defer configure_error_bundle.deinit(gpa); // If this fails, we can still start the server and wait for user @@ -1042,7 +1050,10 @@ pub fn main(init: process.Init.Minimal) !void { } else |err| { const can_fs_watch = switch (err) { error.AlreadyReported => false, - error.FailedButCacheIntact => true, + error.FailedButCacheIntact => c: { + if (help_menu) printUsageWithoutConfiguration(&graph, "build.zig compilation failure"); + break :c true; + }, else => |e| w: { log.err("configuration failed: {t}", .{e}); break :w false; @@ -1064,6 +1075,17 @@ pub fn main(init: process.Init.Minimal) !void { } } +fn printUsageWithoutConfiguration(graph: *Graph, reason: []const u8) noreturn { + const io = graph.io; + _ = io.lockStderr(&.{}, graph.stderr_mode) catch {}; + const writer = initStdoutWriter(io); + ScannedConfig.printUsageWithoutConfiguration(graph.zig_exe, writer, reason) catch |err| switch (err) { + error.WriteFailed => fatal("failed writing to stdout: {t}", .{stdout_writer_allocation.err.?}), + }; + stdout_writer_allocation.flush() catch |err| fatal("failed writing to stdout: {t}", .{err}); + return process.exit(0); +} + /// Temporarily adds the reconfigure pseudostep to step_stack, calls /// `Watch.update`, and then pops it again. fn updateWatch(maker: *Maker, watch: *Watch) !void { @@ -1091,7 +1113,7 @@ const ConfigureOptions = struct { print_configuration: PrintConfiguration, forks: []Fork, src_files: *Cache.Manifest.SelfContainedFiles, - error_bundle: ?*std.zig.ErrorBundle = null, + error_bundle: ?*ErrorBundle = null, }; fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { @@ -1402,7 +1424,7 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { for (fetches) |f| { if (!f.have_manifest) continue; if (config_man) |man| { - const manifest_path = try f.package_root.join(arena, Package.Manifest.basename); + const manifest_path = try f.package_root.join(arena, Manifest.basename); _ = try man.addInputPath(manifest_path, .{}); } const mod = f.cli_module orelse continue; @@ -1932,8 +1954,8 @@ fn cmdFetch(gpa: Allocator, graph: *Graph, args: []const []const u8) !void { try ast.render(gpa, &aw.writer, fixups); const rendered = aw.written(); - build_root.directory.handle.writeFile(io, .{ .sub_path = Package.Manifest.basename, .data = rendered }) catch |err| { - fatal("unable to write {s} file: {t}", .{ Package.Manifest.basename, err }); + build_root.directory.handle.writeFile(io, .{ .sub_path = Manifest.basename, .data = rendered }) catch |err| { + fatal("unable to write {s} file: {t}", .{ Manifest.basename, err }); }; return process.cleanExit(io); @@ -2135,7 +2157,7 @@ fn cmdInit(gpa: Allocator, graph: *Graph, args: []const []const u8) !void { const s = Dir.path.sep_str; const template_paths = [_][]const u8{ default_build_zig_basename, - Package.Manifest.basename, + Manifest.basename, "src" ++ s ++ "main.zig", "src" ++ s ++ "root.zig", }; @@ -2159,7 +2181,7 @@ fn cmdInit(gpa: Allocator, graph: *Graph, args: []const []const u8) !void { return process.cleanExit(io); }, .minimal => { - Templates.writeSimpleFile(io, Io.Dir.cwd(), Package.Manifest.basename, + Templates.writeSimpleFile(io, Io.Dir.cwd(), Manifest.basename, \\.{{ \\ .name = .{s}, \\ .version = "0.0.1", @@ -2173,8 +2195,8 @@ fn cmdInit(gpa: Allocator, graph: *Graph, args: []const []const u8) !void { builtin.zig_version_string, fingerprint.int(), }) catch |err| switch (err) { - else => fatal("failed to create {q}: {t}", .{ Package.Manifest.basename, err }), - error.PathAlreadyExists => fatal("refusing to overwrite {q}", .{Package.Manifest.basename}), + else => fatal("failed to create {q}: {t}", .{ Manifest.basename, err }), + error.PathAlreadyExists => fatal("refusing to overwrite {q}", .{Manifest.basename}), }; Templates.writeSimpleFile(io, Io.Dir.cwd(), default_build_zig_basename, \\const std = @import("std"); @@ -2189,12 +2211,12 @@ fn cmdInit(gpa: Allocator, graph: *Graph, args: []const []const u8) !void { // their `build.zig.zon` *after* writing their `build.zig`. So this one isn't fatal. error.PathAlreadyExists => { log.info("successfully populated {q}, preserving existing {q}", .{ - Package.Manifest.basename, default_build_zig_basename, + Manifest.basename, default_build_zig_basename, }); return process.cleanExit(io); }, }; - log.info("successfully populated {q} and {q}", .{ Package.Manifest.basename, default_build_zig_basename }); + log.info("successfully populated {q} and {q}", .{ Manifest.basename, default_build_zig_basename }); return process.cleanExit(io); }, } @@ -3277,7 +3299,7 @@ pub fn invalidateResult(maker: *Maker, step: *Step) error{MustReconfigure}!bool pub fn printErrorMessages( maker: *Maker, failing_step_index: Configuration.Step.Index, - options: std.zig.ErrorBundle.RenderOptions, + options: ErrorBundle.RenderOptions, stderr: Io.Terminal, error_style: ErrorStyle, multiline_errors: MultilineErrors, @@ -4037,7 +4059,9 @@ const FindBuildRootOptions = struct { cwd_path: []const u8, }; -fn findBuildRoot(arena: Allocator, io: Io, options: FindBuildRootOptions) !BuildRoot { +const FindBuildRootError = error{ OutOfMemory, BuildZigNotFound }; + +fn findBuildRoot(arena: Allocator, io: Io, options: FindBuildRootOptions) FindBuildRootError!BuildRoot { const build_zig_basename = if (options.build_file) |bf| Dir.path.basename(bf) else @@ -4088,11 +4112,12 @@ fn findBuildRoot(arena: Allocator, io: Io, options: FindBuildRootOptions) !Build dirname = Dir.path.dirname(dirname orelse options.cwd_path) orelse { log.info("initialize {s} template file with \"zig init\"", .{std.zig.build_zig_basename}); log.info("see \"zig --help\" for more options", .{}); - fatal("no build.zig file found, in the current directory or any parent directories", .{}); + log.err("no build.zig file found, in the current directory or any parent directories", .{}); + return error.BuildZigNotFound; }; continue; }, - else => |e| return e, + else => |e| fatal("accessing {q} while searching for build.zig file failed: {t}", .{ joined_path, e }), } } } @@ -4100,8 +4125,8 @@ fn findBuildRoot(arena: Allocator, io: Io, options: FindBuildRootOptions) !Build const Fork = struct { path: Path, manifest_ast: std.zig.Ast, - manifest: Package.Manifest, - error_bundle: std.zig.ErrorBundle.Wip, + manifest: Manifest, + error_bundle: ErrorBundle.Wip, failed: bool, arena_allocator: std.heap.ArenaAllocator, @@ -4134,12 +4159,12 @@ const Fork = struct { fork.arena_allocator = .init(gpa); const arena = fork.arena_allocator.allocator(); - var error_bundle: std.zig.ErrorBundle.Wip = try .init(gpa); + var error_bundle: ErrorBundle.Wip = try .init(gpa); defer error_bundle.deinit(); - const manifest_path = try fork.path.join(arena, Package.Manifest.basename); + const manifest_path = try fork.path.join(arena, Manifest.basename); - Package.Manifest.load( + Manifest.load( io, arena, manifest_path, @@ -4189,20 +4214,20 @@ fn loadManifest( arena: Allocator, io: Io, options: LoadManifestOptions, -) !struct { Package.Manifest, std.zig.Ast } { +) !struct { Manifest, std.zig.Ast } { const rng: std.Random.IoSource = .{ .io = io }; const manifest_bytes = while (true) { break options.dir.readFileAllocOptions( io, - Package.Manifest.basename, + Manifest.basename, arena, - .limited(Package.Manifest.max_bytes), + .limited(Manifest.max_bytes), .@"1", 0, ) catch |err| switch (err) { error.FileNotFound => { - Templates.writeSimpleFile(io, options.dir, Package.Manifest.basename, + Templates.writeSimpleFile(io, options.dir, Manifest.basename, \\.{{ \\ .name = .{s}, \\ .version = "0.0.1", @@ -4216,29 +4241,29 @@ fn loadManifest( builtin.zig_version_string, Package.Fingerprint.generate(rng.interface(), options.root_name).int(), }) catch |e| { - fatal("unable to write {s}: {t}", .{ Package.Manifest.basename, e }); + fatal("unable to write {s}: {t}", .{ Manifest.basename, e }); }; continue; }, - else => |e| fatal("unable to load {s}: {t}", .{ Package.Manifest.basename, e }), + else => |e| fatal("unable to load {s}: {t}", .{ Manifest.basename, e }), }; }; var ast = try std.zig.Ast.parse(gpa, manifest_bytes, .{ .mode = .zon }); errdefer ast.deinit(gpa); if (ast.errors.len > 0) { - try std.zig.printAstErrorsToStderr(gpa, io, ast, Package.Manifest.basename, options.color); + try std.zig.printAstErrorsToStderr(gpa, io, ast, Manifest.basename, options.color); process.exit(2); } - var manifest = try Package.Manifest.parse(gpa, &ast, rng.interface(), .{}); + var manifest = try Manifest.parse(gpa, &ast, rng.interface(), .{}); errdefer manifest.deinit(gpa); if (manifest.errors.len > 0) { - var wip_errors: std.zig.ErrorBundle.Wip = try .init(gpa); + var wip_errors: ErrorBundle.Wip = try .init(gpa); defer wip_errors.deinit(); - const src_path = try wip_errors.addString(Package.Manifest.basename); + const src_path = try wip_errors.addString(Manifest.basename); try manifest.copyErrorsIntoBundle(ast, src_path, &wip_errors); var error_bundle = try wip_errors.toOwnedBundle(""); @@ -4262,8 +4287,8 @@ fn sanitizeExampleName(arena: Allocator, bytes: []const u8) error{OutOfMemory}![ else => continue, }; if (!std.zig.isValidId(result.items)) return "foo"; - if (result.items.len > Package.Manifest.max_name_len) - result.shrinkRetainingCapacity(Package.Manifest.max_name_len); + if (result.items.len > Manifest.max_name_len) + result.shrinkRetainingCapacity(Manifest.max_name_len); return result.toOwnedSlice(arena); } diff --git a/lib/compiler/Maker/ScannedConfig.zig b/lib/compiler/Maker/ScannedConfig.zig index 0b98a78d877b69c06f9b8fdd5da3d9efa178abb2..0508ba14cac46577cf87e964cb82fcf1448df189 100644 --- a/lib/compiler/Maker/ScannedConfig.zig +++ b/lib/compiler/Maker/ScannedConfig.zig @@ -238,6 +238,18 @@ pub fn printSteps(sc: *const ScannedConfig, graph: *Graph, w: *Writer) !void { } } +pub fn printUsageWithoutConfiguration(zig_exe: []const u8, w: *Writer, reason: []const u8) Writer.Error!void { + try w.print( + \\Usage: {s} build [steps] [options] + \\ + \\Steps: (unavailable due to {s}) + \\Project-Specific Options: (unavailable due to {s}) + \\{s} + \\ (unavailable due to {s}) + \\{s} + , .{ zig_exe, reason, reason, system_integration_usage, reason, general_usage }); +} + pub fn printUsage(sc: *const ScannedConfig, graph: *Graph, w: *Writer) !void { const arena = graph.arena; @@ -274,37 +286,7 @@ pub fn printUsage(sc: *const ScannedConfig, graph: *Graph, w: *Writer) !void { } } - try w.writeAll( - \\ - \\System Integration Options: - \\ --search-prefix [path] Add a path to look for binaries, libraries, headers - \\ --sysroot [path] Set the system root directory (usually /) - \\ --libc [file] Provide a file which specifies libc paths - \\ - \\ --system [pkgdir] Disable package fetching; enable all integrations - \\ -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: - \\ - ); + try w.writeAll(system_integration_usage); if (sc.configuration.system_integrations.len == 0) { try w.writeAll(" (none) -\n"); } else { @@ -318,92 +300,127 @@ pub fn printUsage(sc: *const ScannedConfig, graph: *Graph, w: *Writer) !void { } } - try w.writeAll( - \\ - \\General Options: - \\ -h, --help Print this help to stdout and exit - \\ -l, --list-steps Print available steps to stdout and exit - \\ - \\ -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 - \\ - \\ --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 - \\ verbose (Default) Report errors with full context - \\ minimal Report errors after summary, excluding context like command lines - \\ verbose_clear Like 'verbose', but clear the terminal at the start of each update - \\ minimal_clear Like 'minimal', but clear the terminal at the start of each update - \\ --multiline-errors [style] Control how multi-line error messages are printed - \\ indent (Default) Indent non-initial lines to align with initial line - \\ newline Include a leading newline so that the error message is on its own lines - \\ none Print as usual so the first line is misaligned - \\ --summary [mode] Control the printing of the build summary - \\ all Print the build summary in its entirety - \\ new Omit cached steps - \\ failures (Default if short-lived) Only print failed steps - \\ line (Default if long-lived) Only print the single-line summary - \\ none Do not print the build summary - \\ -j Limit concurrent jobs (default is to use all CPU cores) - \\ --maxrss Limit memory usage (default is to use available memory) - \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss - \\ --test-timeout Limit execution time of unit tests, terminating if exceeded. - \\ The timeout must include a unit: ns, us, ms, s, m, h - \\ --watch Continuously rebuild when source files are modified - \\ --debounce Delay before rebuilding after changed file detected - \\ --webui[=ip] Enable the web interface on the given IP address - \\ --fuzz[=limit] Continuously search for unit test failures with an optional - \\ limit to the max number of iterations. The argument supports - \\ an optional 'K', 'M', or 'G' suffix (e.g. '10K'). Implies - \\ '--webui' when no limit is specified. - \\ --listen=- Enable the build server protocol on stdio - \\ --time-report Force full rebuild and provide detailed information on - \\ compilation time of Zig source code (implies '--webui') - \\ -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], --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 - \\ -fallow-so-scripts Allows .so files to be GNU ld scripts - \\ -fno-allow-so-scripts (default) .so files must be ELF files - \\ --error-limit [num] Set the maximum amount of distinct error values - \\ --build-file [file] Override path to build.zig - \\ --cache-dir [path] Override path to local Zig cache directory - \\ --zig-lib=[arg] Override path to Zig lib directory - \\ --seed [integer] For shuffling dependency traversal order (default: random) - \\ --cache-poison[=mode] Override configuration caching behavior - \\ pure (default) Avoid false positive cache hits - \\ poisoned Don't cache the configuration - \\ disallowed Panics when cache would be poisoned - \\ ignored A little poison never hurt anybody - \\ --print-configuration Render configuration as .zon to stdout - \\ --print-configuration-path Print the path to the binary configuration file to stdout - \\ --build-id[=style] At a minor link-time expense, embeds a build ID in binaries - \\ fast 8-byte non-cryptographic hash (COFF, ELF, WASM) - \\ sha1, tree 20-byte cryptographic hash (ELF, WASM) - \\ md5 16-byte cryptographic hash (ELF) - \\ uuid 16-byte random UUID (ELF, WASM) - \\ 0x[hexstring] Constant ID, maximum 32 bytes (ELF, WASM) - \\ none (default) No build ID - \\ --debug-log [scope] Enable debugging the compiler - \\ --debug-pkg-config Fail if unknown pkg-config flags encountered - \\ --verbose-link Enable compiler debug output for linking - \\ --verbose-air Enable compiler debug output for Zig AIR - \\ --verbose-llvm-ir Enable compiler debug output for LLVM IR - \\ --verbose-cimport Enable compiler debug output for C imports - \\ --verbose-cc Enable compiler debug output for C compilation - \\ --verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features - \\ - ); + try w.writeAll(general_usage); +} + +const system_integration_usage = + \\ + \\System Integration Options: + \\ --search-prefix [path] Add a path to look for binaries, libraries, headers + \\ --sysroot [path] Set the system root directory (usually /) + \\ --libc [file] Provide a file which specifies libc paths + \\ + \\ --system [pkgdir] Disable package fetching; enable all integrations + \\ -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: + \\ +; + +const general_usage = + \\ + \\General Options: + \\ -h, --help Print this help to stdout and exit + \\ -l, --list-steps Print available steps to stdout and exit + \\ + \\ -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 + \\ + \\ --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 + \\ verbose (Default) Report errors with full context + \\ minimal Report errors after summary, excluding context like command lines + \\ verbose_clear Like 'verbose', but clear the terminal at the start of each update + \\ minimal_clear Like 'minimal', but clear the terminal at the start of each update + \\ --multiline-errors [style] Control how multi-line error messages are printed + \\ indent (Default) Indent non-initial lines to align with initial line + \\ newline Include a leading newline so that the error message is on its own lines + \\ none Print as usual so the first line is misaligned + \\ --summary [mode] Control the printing of the build summary + \\ all Print the build summary in its entirety + \\ new Omit cached steps + \\ failures (Default if short-lived) Only print failed steps + \\ line (Default if long-lived) Only print the single-line summary + \\ none Do not print the build summary + \\ -j Limit concurrent jobs (default is to use all CPU cores) + \\ --maxrss Limit memory usage (default is to use available memory) + \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss + \\ --test-timeout Limit execution time of unit tests, terminating if exceeded. + \\ The timeout must include a unit: ns, us, ms, s, m, h + \\ --watch Continuously rebuild when source files are modified + \\ --debounce Delay before rebuilding after changed file detected + \\ --webui[=ip] Enable the web interface on the given IP address + \\ --fuzz[=limit] Continuously search for unit test failures with an optional + \\ limit to the max number of iterations. The argument supports + \\ an optional 'K', 'M', or 'G' suffix (e.g. '10K'). Implies + \\ '--webui' when no limit is specified. + \\ --listen=- Enable the build server protocol on stdio + \\ --time-report Force full rebuild and provide detailed information on + \\ compilation time of Zig source code (implies '--webui') + \\ -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], --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 + \\ -fallow-so-scripts Allows .so files to be GNU ld scripts + \\ -fno-allow-so-scripts (default) .so files must be ELF files + \\ --error-limit [num] Set the maximum amount of distinct error values + \\ --build-file [file] Override path to build.zig + \\ --cache-dir [path] Override path to local Zig cache directory + \\ --zig-lib=[arg] Override path to Zig lib directory + \\ --seed [integer] For shuffling dependency traversal order (default: random) + \\ --cache-poison[=mode] Override configuration caching behavior + \\ pure (default) Avoid false positive cache hits + \\ poisoned Don't cache the configuration + \\ disallowed Panics when cache would be poisoned + \\ ignored A little poison never hurt anybody + \\ --print-configuration Render configuration as .zon to stdout + \\ --print-configuration-path Print the path to the binary configuration file to stdout + \\ --build-id[=style] At a minor link-time expense, embeds a build ID in binaries + \\ fast 8-byte non-cryptographic hash (COFF, ELF, WASM) + \\ sha1, tree 20-byte cryptographic hash (ELF, WASM) + \\ md5 16-byte cryptographic hash (ELF) + \\ uuid 16-byte random UUID (ELF, WASM) + \\ 0x[hexstring] Constant ID, maximum 32 bytes (ELF, WASM) + \\ none (default) No build ID + \\ --debug-log [scope] Enable debugging the compiler + \\ --debug-pkg-config Fail if unknown pkg-config flags encountered + \\ --verbose-link Enable compiler debug output for linking + \\ --verbose-air Enable compiler debug output for Zig AIR + \\ --verbose-llvm-ir Enable compiler debug output for LLVM IR + \\ --verbose-cimport Enable compiler debug output for C imports + \\ --verbose-cc Enable compiler debug output for C compilation + \\ --verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features + \\ +; } -- 2.54.0 From 2f60a88308cf6358c06a6eaaff5ea61491873a0b Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 12 Oct 2026 18:05:37 -0700 Subject: [PATCH 2/5] zig build: add `--print-tree` CLI option for writing the current state of package dependencies to stdout, without running configuration. --- lib/compiler/Maker.zig | 154 +++++++++++++++++++++------ lib/compiler/Maker/ScannedConfig.zig | 4 +- 2 files changed, 125 insertions(+), 33 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index bde0e08ef07b36ef113e0500238ec2101802c726..7b25111fb5b7c8607c0178ebb16c0c792cb4df2f 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -103,8 +103,30 @@ const InstallPaths = struct { }; const PrintNode = struct { - parent: ?*PrintNode, + parent: ?*const PrintNode, last: bool = false, + + fn printPrefix(node: *const PrintNode, writer: *Writer, mode: Io.Terminal.Mode) Writer.Error!void { + const parent = node.parent orelse return; + if (parent.parent == null) return; + try printPrefix(parent, writer, mode); + if (parent.last) { + try writer.writeAll(" "); + } else { + try printTreeSibling(writer, mode); + } + } + + fn printPrefixedChild(node: *const PrintNode, writer: *Writer, mode: Io.Terminal.Mode) Writer.Error!void { + try printPrefix(node, writer, mode); + if (node.parent != null) { + if (node.last) { + try printTreeChildLast(writer, mode); + } else { + try printTreeChild(writer, mode); + } + } + } }; const ErrorStyle = enum { @@ -127,7 +149,7 @@ const ErrorStyle = enum { }; const MultilineErrors = enum { indent, newline, none }; const Summary = enum { all, new, failures, line, none }; -const PrintConfiguration = enum { none, zon, path }; +const PrintConfiguration = enum { none, zon, path, tree }; const GeneratedFile = struct { data: ?[]const u8, @@ -363,6 +385,8 @@ pub fn main(init: process.Init.Minimal) !void { override_local_cache_dir = nextArgOrFatal(args, &arg_i); } else if (mem.eql(u8, arg, "--pkg-dir")) { override_pkg_dir = nextArgOrFatal(args, &arg_i); + } else if (mem.eql(u8, arg, "--print-tree")) { + print_configuration = .tree; } else if (mem.eql(u8, arg, "--fetch")) { fetch_only = true; } else if (mem.cutPrefix(u8, arg, "--fetch=")) |rest| { @@ -797,6 +821,7 @@ pub fn main(init: process.Init.Minimal) !void { return cleanExit(io, &scanned_config); }, .path => unreachable, + .tree => unreachable, } var maker: Maker = .{ @@ -1357,8 +1382,20 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { return error.FailedButCacheIntact; } - if (options.fetch_only) { + if (options.fetch_only or options.print_configuration == .tree) { _ = io.lockStderr(&.{}, .no_color) catch {}; + if (options.print_configuration == .tree) { + const NO_COLOR = EnvVar.NO_COLOR.isSet(&graph.environ_map); + const CLICOLOR_FORCE = EnvVar.CLICOLOR_FORCE.isSet(&graph.environ_map); + printPkgTree(.{ + .writer = initStdoutWriter(io), + .mode = try .detect(io, .stdout(), NO_COLOR, CLICOLOR_FORCE), + }, arena, &job_queue, &fetch) catch |err| switch (err) { + error.WriteFailed => return stdout_writer_allocation.err.?, + else => |e| return e, + }; + try stdout_writer_allocation.flush(); + } process.exit(0); } @@ -1641,6 +1678,7 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { process.exit(0); }, .none, .zon => {}, + .tree => unreachable, } const configuration = c: { @@ -1678,6 +1716,72 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { }; } +fn printPkgTree( + terminal: Io.Terminal, + arena: Allocator, + job_queue: *const Fetch.JobQueue, + root_fetch: *const Fetch, +) !void { + if (!root_fetch.have_manifest) return; + const names = root_fetch.manifest.dependencies.keys(); + for (names, root_fetch.manifest.dependencies.values(), 0..) |name, *dep, i| { + var print_node: PrintNode = .{ + .parent = null, + .last = i == names.len - 1, + }; + try printPkgTreeChild(terminal, arena, job_queue, &print_node, name, dep.hash); + } +} + +fn printPkgTreeChild( + terminal: Io.Terminal, + arena: Allocator, + job_queue: *const Fetch.JobQueue, + parent_print_node: *const PrintNode, + dep_name: []const u8, + opt_hash: ?[]const u8, +) !void { + const w = terminal.writer; + try parent_print_node.printPrefixedChild(w, terminal.mode); + try w.writeAll(dep_name); + + const hash = opt_hash orelse { + try terminal.setColor(.red); + try w.writeAll(" (missing hash)\n"); + try terminal.setColor(.reset); + return; + }; + try terminal.setColor(.dim); + try w.print(" {s}", .{hash}); + try terminal.setColor(.reset); + const child_fetch = job_queue.table.get(.fromSlice(hash)) orelse { + try terminal.setColor(.red); + try w.writeAll(" (fetch failed)\n"); + try terminal.setColor(.reset); + return; + }; + switch (child_fetch.lazy_status) { + .eager, .available => {}, + .unavailable => { + try terminal.setColor(.yellow); + try w.writeAll(" (not fetched)\n"); + try terminal.setColor(.reset); + return; + }, + } + try w.writeByte('\n'); + if (!child_fetch.have_manifest) return; + + const names = child_fetch.manifest.dependencies.keys(); + for (names, child_fetch.manifest.dependencies.values(), 0..) |name, *dep, i| { + var print_node: PrintNode = .{ + .parent = parent_print_node, + .last = i == names.len - 1, + }; + try printPkgTreeChild(terminal, arena, job_queue, &print_node, name, dep.hash); + } +} + fn cmdFetch(gpa: Allocator, graph: *Graph, args: []const []const u8) !void { const environ_map = &graph.environ_map; const io = graph.io; @@ -2949,18 +3053,7 @@ fn printTreeStep( .failures => make_step.state == .success, }; if (skip) return; - try printPrefix(parent_node, stderr); - - if (parent_node.parent != null) { - if (parent_node.last) { - try printChildNodePrefix(stderr); - } else { - try writer.writeAll(switch (stderr.mode) { - .escape_codes => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ", // ├─ - else => "+- ", - }); - } - } + try parent_node.printPrefixedChild(writer, stderr.mode); if (!first) try stderr.setColor(.dim); @@ -3189,23 +3282,22 @@ fn printStepFailure( } } -fn printPrefix(node: *PrintNode, stderr: Io.Terminal) !void { - const parent = node.parent orelse return; - const writer = stderr.writer; - if (parent.parent == null) return; - try printPrefix(parent, stderr); - if (parent.last) { - try writer.writeAll(" "); - } else { - try writer.writeAll(switch (stderr.mode) { - .escape_codes => "\x1B\x28\x30\x78\x1B\x28\x42 ", // │ - else => "| ", - }); - } +fn printTreeChild(writer: *Writer, mode: Io.Terminal.Mode) Writer.Error!void { + return writer.writeAll(switch (mode) { + .escape_codes => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ", // ├─ + else => "+- ", + }); } -fn printChildNodePrefix(stderr: Io.Terminal) !void { - try stderr.writer.writeAll(switch (stderr.mode) { +fn printTreeSibling(writer: *Writer, mode: Io.Terminal.Mode) Writer.Error!void { + return writer.writeAll(switch (mode) { + .escape_codes => "\x1B\x28\x30\x78\x1B\x28\x42 ", // │ + else => "| ", + }); +} + +fn printTreeChildLast(writer: *Writer, mode: Io.Terminal.Mode) !void { + try writer.writeAll(switch (mode) { .escape_codes => "\x1B\x28\x30\x6d\x71\x1B\x28\x42 ", // └─ else => "+- ", }); @@ -3325,7 +3417,7 @@ pub fn printErrorMessages( while (step_stack.pop()) |step_index| : (indent += 1) { if (indent > 0) { try writer.splatByteAll(' ', (indent - 1) * 3); - try printChildNodePrefix(stderr); + try printTreeChildLast(writer, stderr.mode); } try writer.writeAll(step_index.ptr(c).name.slice(c)); diff --git a/lib/compiler/Maker/ScannedConfig.zig b/lib/compiler/Maker/ScannedConfig.zig index 0508ba14cac46577cf87e964cb82fcf1448df189..13108dfa207aa8b9b002618d99d805b1020900de 100644 --- a/lib/compiler/Maker/ScannedConfig.zig +++ b/lib/compiler/Maker/ScannedConfig.zig @@ -388,6 +388,7 @@ const general_usage = \\ needed (Default) Lazy dependencies are fetched as needed \\ all Lazy dependencies are always fetched \\ --fork=[path], --fork [path] Override one or more projects from dependency tree + \\ --print-tree Write current state of package dependencies to stdout \\ \\Advanced Options: \\ -freference-trace[=num] How many lines of reference trace should be shown per compile error @@ -404,7 +405,7 @@ const general_usage = \\ poisoned Don't cache the configuration \\ disallowed Panics when cache would be poisoned \\ ignored A little poison never hurt anybody - \\ --print-configuration Render configuration as .zon to stdout + \\ --print-configuration Render configuration as .zon to stdout and exit \\ --print-configuration-path Print the path to the binary configuration file to stdout \\ --build-id[=style] At a minor link-time expense, embeds a build ID in binaries \\ fast 8-byte non-cryptographic hash (COFF, ELF, WASM) @@ -423,4 +424,3 @@ const general_usage = \\ --verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features \\ ; -} -- 2.54.0 From 2551cf283f28e2cae27e461b51ea6df92d849fbb Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 12 Oct 2026 18:28:07 -0700 Subject: [PATCH 3/5] zig build: add --configure flag, change --fetch flag --fetch is changed to mean: > Fetch all dependencies including lazy ones; exit without configuration --configure is introduced (replaces old `--fetch` semantics): > Exit after the configuration phase --- lib/compiler/Maker.zig | 46 +++++++++++++--------------- lib/compiler/Maker/ScannedConfig.zig | 21 ++++++------- 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index 7b25111fb5b7c8607c0178ebb16c0c792cb4df2f..a27ce4d4ea998138088a17af5c26c1abfa32716c 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -149,7 +149,7 @@ const ErrorStyle = enum { }; const MultilineErrors = enum { indent, newline, none }; const Summary = enum { all, new, failures, line, none }; -const PrintConfiguration = enum { none, zon, path, tree }; +const PrintConfiguration = enum { none, zon, path, tree, stop }; const GeneratedFile = struct { data: ?[]const u8, @@ -300,7 +300,6 @@ pub fn main(init: process.Init.Minimal) !void { var cached_passthru_configure: std.ArrayList(u32) = .empty; var forks: std.ArrayList(Fork) = .empty; var system_pkg_dir_path: ?[]const u8 = null; - var fetch_only = false; var fetch_mode: Fetch.JobQueue.Mode = .needed; var debug_target: ?[]const u8 = null; var cache_poison: std.Build.Graph.CachePoison = .pure; @@ -387,12 +386,10 @@ pub fn main(init: process.Init.Minimal) !void { override_pkg_dir = nextArgOrFatal(args, &arg_i); } else if (mem.eql(u8, arg, "--print-tree")) { print_configuration = .tree; + } else if (mem.eql(u8, arg, "--configure")) { + print_configuration = .stop; } else if (mem.eql(u8, arg, "--fetch")) { - fetch_only = true; - } else if (mem.cutPrefix(u8, arg, "--fetch=")) |rest| { - fetch_only = true; - fetch_mode = stringToEnum(Fetch.JobQueue.Mode, rest) orelse - fatal("expected [needed|all] after \"--fetch=\", found {q}", .{rest}); + fetch_mode = .all; } else if (mem.cutPrefix(u8, arg, "--fork=")) |rest| { try forks.append(arena, .init(rest)); } else if (mem.eql(u8, arg, "--fork")) { @@ -619,7 +616,7 @@ pub fn main(init: process.Init.Minimal) !void { } } - const early_exit_mode = fetch_only or help_menu or steps_menu or print_configuration != .none; + const early_exit_mode = fetch_mode == .all or help_menu or steps_menu or print_configuration != .none; const server_mode = !early_exit_mode and (watch_flag or webui_listen != null or fuzz != null or listen); process.raiseFileDescriptorLimit(); @@ -793,7 +790,6 @@ pub fn main(init: process.Init.Minimal) !void { .parent_progress_node = main_progress_node, .fetch_mode = fetch_mode, .system_pkg_dir_path = system_pkg_dir_path, - .fetch_only = fetch_only, .print_configuration = print_configuration, .forks = forks.items, .src_files = &configure_source_files, @@ -822,6 +818,7 @@ pub fn main(init: process.Init.Minimal) !void { }, .path => unreachable, .tree => unreachable, + .stop => return cleanExit(io, &scanned_config), } var maker: Maker = .{ @@ -1134,7 +1131,6 @@ const ConfigureOptions = struct { parent_progress_node: std.Progress.Node, fetch_mode: Fetch.JobQueue.Mode, system_pkg_dir_path: ?[]const u8, - fetch_only: bool, print_configuration: PrintConfiguration, forks: []Fork, src_files: *Cache.Manifest.SelfContainedFiles, @@ -1382,20 +1378,22 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { return error.FailedButCacheIntact; } - if (options.fetch_only or options.print_configuration == .tree) { + if (options.fetch_mode == .all) { _ = io.lockStderr(&.{}, .no_color) catch {}; - if (options.print_configuration == .tree) { - const NO_COLOR = EnvVar.NO_COLOR.isSet(&graph.environ_map); - const CLICOLOR_FORCE = EnvVar.CLICOLOR_FORCE.isSet(&graph.environ_map); - printPkgTree(.{ - .writer = initStdoutWriter(io), - .mode = try .detect(io, .stdout(), NO_COLOR, CLICOLOR_FORCE), - }, arena, &job_queue, &fetch) catch |err| switch (err) { - error.WriteFailed => return stdout_writer_allocation.err.?, - else => |e| return e, - }; - try stdout_writer_allocation.flush(); - } + process.exit(0); + } + + if (options.print_configuration == .tree) { + const NO_COLOR = EnvVar.NO_COLOR.isSet(&graph.environ_map); + const CLICOLOR_FORCE = EnvVar.CLICOLOR_FORCE.isSet(&graph.environ_map); + printPkgTree(.{ + .writer = initStdoutWriter(io), + .mode = try .detect(io, .stdout(), NO_COLOR, CLICOLOR_FORCE), + }, arena, &job_queue, &fetch) catch |err| switch (err) { + error.WriteFailed => return stdout_writer_allocation.err.?, + else => |e| return e, + }; + try stdout_writer_allocation.flush(); process.exit(0); } @@ -1677,7 +1675,7 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { _ = io.lockStderr(&.{}, .no_color) catch {}; process.exit(0); }, - .none, .zon => {}, + .none, .zon, .stop => {}, .tree => unreachable, } diff --git a/lib/compiler/Maker/ScannedConfig.zig b/lib/compiler/Maker/ScannedConfig.zig index 13108dfa207aa8b9b002618d99d805b1020900de..bae416b9bb2523a6b042bfa118d8a60ae43213ed 100644 --- a/lib/compiler/Maker/ScannedConfig.zig +++ b/lib/compiler/Maker/ScannedConfig.zig @@ -353,8 +353,8 @@ const general_usage = \\ --error-style [style] Control how build errors are printed \\ verbose (Default) Report errors with full context \\ minimal Report errors after summary, excluding context like command lines - \\ verbose_clear Like 'verbose', but clear the terminal at the start of each update - \\ minimal_clear Like 'minimal', but clear the terminal at the start of each update + \\ verbose_clear Like "verbose", but clear the terminal at the start of each update + \\ minimal_clear Like "minimal", but clear the terminal at the start of each update \\ --multiline-errors [style] Control how multi-line error messages are printed \\ indent (Default) Indent non-initial lines to align with initial line \\ newline Include a leading newline so that the error message is on its own lines @@ -375,22 +375,21 @@ const general_usage = \\ --webui[=ip] Enable the web interface on the given IP address \\ --fuzz[=limit] Continuously search for unit test failures with an optional \\ limit to the max number of iterations. The argument supports - \\ an optional 'K', 'M', or 'G' suffix (e.g. '10K'). Implies - \\ '--webui' when no limit is specified. - \\ --listen=- Enable the build server protocol on stdio + \\ an optional 'K', 'M', or 'G' suffix (e.g. "10K"). Implies + \\ "--webui" when no limit is specified. \\ --time-report Force full rebuild and provide detailed information on - \\ compilation time of Zig source code (implies '--webui') + \\ compilation time of Zig source code (implies "--webui") \\ -fincremental Enable incremental compilation \\ -fno-incremental Disable incremental compilation + \\ --configure Exit after the configuration phase \\ \\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 + \\ --fetch Fetch all dependencies including lazy ones; exit without configuration \\ --fork=[path], --fork [path] Override one or more projects from dependency tree \\ --print-tree Write current state of package dependencies to stdout \\ \\Advanced Options: + \\ --listen=- Enable the build server protocol on stdio \\ -freference-trace[=num] How many lines of reference trace should be shown per compile error \\ -fno-reference-trace Disable reference trace \\ -fallow-so-scripts Allows .so files to be GNU ld scripts @@ -405,8 +404,8 @@ const general_usage = \\ poisoned Don't cache the configuration \\ disallowed Panics when cache would be poisoned \\ ignored A little poison never hurt anybody - \\ --print-configuration Render configuration as .zon to stdout and exit - \\ --print-configuration-path Print the path to the binary configuration file to stdout + \\ --print-configuration Render configuration as .zon to stdout. Implies "--configure" + \\ --print-configuration-path Print the path to the binary configuration file to stdout. \\ --build-id[=style] At a minor link-time expense, embeds a build ID in binaries \\ fast 8-byte non-cryptographic hash (COFF, ELF, WASM) \\ sha1, tree 20-byte cryptographic hash (ELF, WASM) -- 2.54.0 From 89ec7145f1adecce62c3d21c9d96b156b1520cb5 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 12 Oct 2026 18:46:50 -0700 Subject: [PATCH 4/5] Maker: check for --fetch after --print-tree --- lib/compiler/Maker.zig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index a27ce4d4ea998138088a17af5c26c1abfa32716c..f85a66096c3f513f50faf0e6038337b865213233 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -1378,11 +1378,6 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { return error.FailedButCacheIntact; } - if (options.fetch_mode == .all) { - _ = io.lockStderr(&.{}, .no_color) catch {}; - process.exit(0); - } - if (options.print_configuration == .tree) { const NO_COLOR = EnvVar.NO_COLOR.isSet(&graph.environ_map); const CLICOLOR_FORCE = EnvVar.CLICOLOR_FORCE.isSet(&graph.environ_map); @@ -1397,6 +1392,11 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { process.exit(0); } + if (options.fetch_mode == .all) { + _ = io.lockStderr(&.{}, .no_color) catch {}; + process.exit(0); + } + // Create the dependencies.zig file for configurer to // obtain via `@import("@dependencies")`. { -- 2.54.0 From 0c54ae3c0e0099dc8b382ea9fe9b80b37dbaee06 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 12 Oct 2026 20:34:17 -0700 Subject: [PATCH 5/5] zig build: validate package hash consistency Any time the same package (based on fingerprint) appears in the dependency tree, if the semantic version compares equal, the hash must be identical, otherwise one of three things has gone wrong and the maintainer must address it. --- lib/compiler/Maker.zig | 77 ++++++++++++++++++++++++++++++++++ lib/compiler/Maker/Package.zig | 8 +++- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index f85a66096c3f513f50faf0e6038337b865213233..0ae85d4fa3bf3ba2b6b78514eb3a8cc4a5a1429b 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -1397,6 +1397,8 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { process.exit(0); } + try validateFingerprints(gpa, &job_queue); + // Create the dependencies.zig file for configurer to // obtain via `@import("@dependencies")`. { @@ -1714,6 +1716,81 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { }; } +const ValidFingerprintKey = struct { + project_id: Package.ProjectId, + version: std.SemanticVersion, + + const Context = struct { + pub fn hash(this: @This(), a: ValidFingerprintKey) u32 { + _ = this; + var hasher: std.hash.Wyhash = .init(a.project_id.fingerprint_id); + hasher.update(&a.project_id.padded_name); + hasher.update(@ptrCast((&a.version.major)[0..1])); + hasher.update(@ptrCast((&a.version.minor)[0..1])); + hasher.update(@ptrCast((&a.version.patch)[0..1])); + if (a.version.pre) |pre| hasher.update(pre); + return @truncate(hasher.final()); + } + pub fn eql(this: @This(), a: ValidFingerprintKey, b: ValidFingerprintKey, i: usize) bool { + _ = this; + _ = i; + return a.project_id.eql(&b.project_id) and a.version.order(b.version) == .eq; + } + }; +}; + +const ValidFingerprintTable = std.array_hash_map.Custom( + ValidFingerprintKey, + struct { fetch: *Fetch, pkg_hash: Package.Hash, printed: bool }, + ValidFingerprintKey.Context, + true, +); + +/// Ensure that the dependency tree did not have any fingerprint matches with the same semantic version but +/// differing package hashes. +fn validateFingerprints(gpa: Allocator, job_queue: *const Fetch.JobQueue) !void { + var any_errors = false; + + var table: ValidFingerprintTable = .empty; + defer table.deinit(gpa); + + for (job_queue.table.keys(), job_queue.table.values()) |pkg_hash, fetch| { + if (!fetch.have_manifest) continue; + const gop = try table.getOrPut(gpa, .{ + .project_id = .init(fetch.manifest.name, fetch.manifest.id), + .version = fetch.manifest.version, + }); + if (gop.found_existing) { + any_errors = true; + const fingerprint: Package.Fingerprint = .init(fetch.manifest.id, fetch.manifest.name); + const stripped_version: std.SemanticVersion = .{ + .major = fetch.manifest.version.major, + .minor = fetch.manifest.version.minor, + .patch = fetch.manifest.version.patch, + .pre = fetch.manifest.version.pre, + }; + if (!gop.value_ptr.printed) { + gop.value_ptr.printed = true; + log.err("contradictory hash for package with fingerprint 0x{x} and version {f}: {s}", .{ + fingerprint.int(), stripped_version, gop.value_ptr.pkg_hash.toSlice(), + }); + } + log.err("contradictory hash for package with fingerprint 0x{x} and version {f}: {s}", .{ + fingerprint.int(), stripped_version, pkg_hash.toSlice(), + }); + } else { + gop.value_ptr.* = .{ .fetch = fetch, .pkg_hash = pkg_hash, .printed = false }; + } + } + if (any_errors) { + log.info("potential cause: package ownership dispute", .{}); + log.info("potential cause: maintainer neglected to bump version after making changes", .{}); + log.info("potential cause: dependency on unreleased changes", .{}); + log.info("hint: use --print-tree to visualize all package versions and hashes", .{}); + return error.FailedButCacheIntact; + } +} + fn printPkgTree( terminal: Io.Terminal, arena: Allocator, diff --git a/lib/compiler/Maker/Package.zig b/lib/compiler/Maker/Package.zig index 8baef22f375f925534380176f618a470f988117e..c31ecf6916ce4d8d279cc9dbe9f529ee99355372 100644 --- a/lib/compiler/Maker/Package.zig +++ b/lib/compiler/Maker/Package.zig @@ -8,13 +8,17 @@ pub const Fingerprint = packed struct(u64) { id: u32, checksum: u32, - pub fn generate(rng: std.Random, name: []const u8) Fingerprint { + pub fn init(id: u32, name: []const u8) Fingerprint { return .{ - .id = rng.intRangeLessThan(u32, 1, 0xffffffff), + .id = id, .checksum = std.hash.Crc32.hash(name), }; } + pub fn generate(rng: std.Random, name: []const u8) Fingerprint { + return .init(rng.intRangeLessThan(u32, 1, 0xffffffff), name); + } + pub fn validate(n: Fingerprint, name: []const u8) bool { switch (n.id) { 0x00000000, 0xffffffff => return false, -- 2.54.0