From 54bb8d2dd9369f5e5b43b4773878edc32fd3851e Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 21 May 2026 22:00:29 -0700 Subject: [PATCH] implement the concept of configure cache poisoning --- lib/compiler/Maker.zig | 27 +++- lib/compiler/Maker/ScannedConfig.zig | 6 + lib/compiler/configurer.zig | 7 + lib/std/Build.zig | 114 +++++++++++++- lib/std/Build/Configuration.zig | 13 ++ src/main.zig | 228 +++++++++++++-------------- 6 files changed, 270 insertions(+), 125 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index 1c26b6052aadf3a5d100a5730e90a755c27a5ba0..b1b106969be24639dd23bc03b0fff317725dc094 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -425,6 +425,10 @@ pub fn main(init: process.Init.Minimal) !void { break :c Configuration.loadFile(arena, io, file) catch |err| fatal("failed to load configuration file {s}: {t}", .{ configure_path, err }); }; + // Technically if the configuration is marked as poisoned, we could + // already delete the file now, but we leave it around in case the + // maker process fails or crashes and it's helpful to be able to repeat + // execution of the command line or otherwise inspect the configuration file. const c = &configuration; var top_level_steps: std.StringArrayHashMapUnmanaged(Configuration.Step.Index) = .empty; for (configuration.steps, 0..) |*conf_step, step_index_usize| { @@ -445,6 +449,7 @@ pub fn main(init: process.Init.Minimal) !void { break :sc .{ .configuration = configuration, .top_level_steps = top_level_steps, + .path = configure_path, }; }; @@ -455,7 +460,7 @@ pub fn main(init: process.Init.Minimal) !void { else => |e| return e, }; w.flush() catch return stdout_writer_allocation.err.?; - return; + return cleanExit(io, &scanned_config); } else if (steps_menu) { var w = initStdoutWriter(io); scanned_config.printSteps(&graph, w) catch |err| switch (err) { @@ -463,12 +468,12 @@ pub fn main(init: process.Init.Minimal) !void { else => |e| return e, }; w.flush() catch return stdout_writer_allocation.err.?; - return; + return cleanExit(io, &scanned_config); } else if (print_configuration) { var w = initStdoutWriter(io); scanned_config.print(w) catch return stdout_writer_allocation.err.?; w.flush() catch return stdout_writer_allocation.err.?; - return; + return cleanExit(io, &scanned_config); } if (webui_listen != null) { @@ -1000,6 +1005,8 @@ fn makeStepNames( if (maker.error_style.verboseContext()) break :code 1; // failure; print build command break :code 2; // failure; do not print build command }; + if (code == 0) removePoisonedConfiguration(io, maker.scanned_config); + cleanup_task.await(io); // There is a defer above but an exit below. _ = io.lockStderr(&.{}, graph.stderr_mode) catch {}; process.exit(code); } @@ -2000,3 +2007,17 @@ fn installSymLinksInner( return step.fail(maker, "unable to symlink {f} -> {s}: {t}", .{ name_only_path, filename_major_only, err }); }; } + +fn cleanExit(io: Io, scanned_config: *const ScannedConfig) void { + removePoisonedConfiguration(io, scanned_config); + return process.cleanExit(io); +} + +fn removePoisonedConfiguration(io: Io, scanned_config: *const ScannedConfig) void { + if (scanned_config.configuration.poisoned) { + // This configuration file was good for only 1 invocation of the maker + // process. Delete it to save space on disk. + Io.Dir.cwd().deleteFile(io, scanned_config.path) catch |err| + log.warn("failed deleting poisoned configuration file {s}: {t}", .{ scanned_config.path, err }); + } +} diff --git a/lib/compiler/Maker/ScannedConfig.zig b/lib/compiler/Maker/ScannedConfig.zig index 787cd4dc5cc905bf1dc41ca1ebc975de6714e0c4..748cc84e97fc52872056cc32102ecf382ca97038 100644 --- a/lib/compiler/Maker/ScannedConfig.zig +++ b/lib/compiler/Maker/ScannedConfig.zig @@ -9,6 +9,7 @@ const Graph = @import("Graph.zig"); configuration: Configuration, top_level_steps: std.StringArrayHashMapUnmanaged(Configuration.Step.Index), +path: []const u8, pub fn print(sc: *const ScannedConfig, w: *Writer) Writer.Error!void { std.log.err("TODO also print paths", .{}); @@ -343,6 +344,11 @@ pub fn printUsage(sc: *const ScannedConfig, graph: *Graph, w: *Writer) !void { \\ --zig-lib-dir [arg] Override path to Zig lib directory \\ --build-runner [file] Override path to build runner \\ --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 \\ --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) diff --git a/lib/compiler/configurer.zig b/lib/compiler/configurer.zig index e16e9caac769b7c9eb53d315bbd0f9d47adb52eb..c8aa77edd8d79c61533ec00e6ac0cd5248cbc86f 100644 --- a/lib/compiler/configurer.zig +++ b/lib/compiler/configurer.zig @@ -114,6 +114,9 @@ pub fn main(init: process.Init.Minimal) !void { graph.system_package_mode = true; } else if (mem.eql(u8, arg, "--verbose")) { graph.verbose = true; + } else if (mem.cutPrefix(u8, arg, "--cache-poison=")) |rest| { + graph.cache_poison = std.meta.stringToEnum(std.Build.Graph.CachePoison, rest) orelse + fatalWithHint("expected --cache-poison=[pure|poisoned|disallowed|ignored]; found: {s}", .{arg}); } else { fatalWithHint("unrecognized argument: {s}", .{arg}); } @@ -1222,6 +1225,10 @@ fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void { try wc.write(writer, .{ .default_step = s.stepIndex(b.default_step), .generated_files_len = @intCast(graph.generated_files.items.len), + .poisoned = switch (graph.cache_poison) { + .pure, .disallowed, .ignored => false, + .poisoned => true, + }, }); } diff --git a/lib/std/Build.zig b/lib/std/Build.zig index 9081e3e29260e763b3915540782d714b16c8b0a2..fbf2fb0e5047b7c598b86635fdf9e139ce96fc35 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -98,6 +98,35 @@ pub const Graph = struct { generated_files: std.ArrayList(*Step), wip_configuration: Configuration.Wip, + cache_poison: CachePoison = .pure, + + /// If the cache is poisoned means that the **configure logic** had side + /// effects, or otherwise did something that could not be tracked by the + /// cache system. + /// + /// This is not to be confused with whether individual steps may have side + /// effects when being evaluated; it has to do with the logic inside build.zig + /// itself. For example, a `Run` step that prints "hello world" has side + /// effects *at make time* and therefore does not warrant setting this flag, + /// while checking for the existence of `scdoc` *at configure time* in order to + /// choose the default value for a configuration option does. + /// + /// Keeping the cache pure will make `zig build` faster, bypassing the + /// configurer process when identical configuration would be generated. + /// + /// When the cache is poisoned, the maker process will delete the build + /// configuration file upon ingesting it since it cannot be reused. + pub const CachePoison = enum { + pure, + poisoned, + /// Indicates the user would like to see a stack trace if the cache + /// would become poisoned. + disallowed, + /// Indicates the user would like to ignore the cache being poisoned + /// and cache anyway, opting into cache hits on stale configuration. + ignored, + }; + pub fn addGeneratedFile(graph: *Graph, owner: *Step) Configuration.GeneratedFileIndex { graph.generated_files.append(graph.arena, owner) catch @panic("OOM"); return @enumFromInt(graph.generated_files.items.len - 1); @@ -169,6 +198,19 @@ pub const Graph = struct { const wc = &graph.wip_configuration; return wc.addString(bytes) catch @panic("OOM"); } + + /// Indicates that the **configure logic** had side effects, or otherwise + /// did something that could not be tracked by the cache system. + /// + /// See `CachePoison` documentation for more details. + pub fn poisonCache(graph: *Graph) void { + switch (graph.cache_poison) { + .pure => graph.cache_poison = .poisoned, + .poisoned => return, + .disallowed => @panic("cache poisoned"), + .ignored => log.warn("ignoring cache poisoning", .{}), + } + } }; const AvailableDeps = []const struct { []const u8, []const u8 }; @@ -798,11 +840,23 @@ pub fn createModule(b: *Build, options: Module.CreateOptions) *Module { return Module.create(b, options); } -/// Initializes a `Step.Run` with argv, which must at least have the path to the -/// executable. More command line arguments can be added with `addArg`, -/// `addArgs`, and `addArtifactArg`. -/// Be careful using this function, as it introduces a system dependency. -/// To run an executable built with zig build, see `Step.Compile.run`. +/// Creates a step that executes a process on the host system. +/// +/// `argv` is one or more command line arguments passed to the executed +/// process. The first element is the name of the executable to run. More +/// command line arguments can be added with methods of `Step.Run`, such as: +/// * `Step.Run.addArgs` +/// * `Step.Run.addArtifactArg` +/// * `Step.Run.addFileArg` +/// * `Step.Run.addOutputFileArg` +/// +/// This function introduces a system dependency, compromising reproducibility +/// and making it more difficult to set up one's computer in order to build the +/// project from source. +/// +/// See also: +/// * `addRunArtifact` +/// * `addRunFile` pub fn addSystemCommand(b: *Build, argv: []const []const u8) *Step.Run { assert(argv.len >= 1); const run_step = Step.Run.create(b, b.fmt("run {s}", .{argv[0]})); @@ -818,8 +872,11 @@ pub fn addSystemCommand(b: *Build, argv: []const []const u8) *Step.Run { /// /// This is declarative; it constructs a build step that may or may not be run /// depending on the options provided by the user to the build command. +/// +/// See also: +/// * `addSystemCommand` +/// * `addRunFile` pub fn addRunArtifact(b: *Build, exe: *Step.Compile) *Step.Run { - // Avoid the common case of the step name looking like "run test test". const step_name = if (exe.kind.isTest() and mem.eql(u8, exe.name, "test")) b.fmt("run {t}", .{exe.kind}) @@ -879,6 +936,19 @@ pub fn addRunArtifact(b: *Build, exe: *Step.Compile) *Step.Run { return run_step; } +/// Creates a step that executes the provided file. +/// +/// Add more command line arguments via methods of `Step.Run`. +/// +/// See also: +/// * `addSystemCommand` +/// * `addRunArtifact` +pub fn addRunFile(b: *Build, executable: LazyPath) *Step.Run { + const run_step = Step.Run.create(b, b.fmt("run {f}", .{executable.fmt(b.graph)})); + run_step.addFileArg(executable); + return run_step; +} + /// Using the `values` provided, produces a C header file, possibly based on a /// template input file (e.g. config.h.in). /// When an input template file is provided, this function will fail the build @@ -1641,7 +1711,17 @@ pub fn fmt(b: *Build, comptime format: []const u8, args: anytype) []u8 { /// /// Returns the `LazyPath` of the found executable. The search only takes place /// if the `LazyPath` will be used by a depending `Step`. -pub fn findProgram(b: *Build, names: []const []const u8) LazyPath { +/// +/// This API is useful in the following cases: +/// * The binary is not named the same across all systems (for example "python" +/// vs "python3"). +/// * The binary may be produced by building from source rather than being +/// globally installed and will therefore be possibly found in one of the +/// search prefix paths. +/// +/// See also: +/// * `findProgram` +pub fn findProgramLazy(b: *Build, names: []const []const u8) LazyPath { const graph = b.graph; const wc = &graph.wip_configuration; const string_list = wc.addStringList(names) catch @panic("OOM"); @@ -1649,6 +1729,26 @@ pub fn findProgram(b: *Build, names: []const []const u8) LazyPath { @panic("TODO"); } +/// Immediately (in the configure phase), searches for an executable on the host +/// that has more than one possible name. +/// +/// Names are searched in order, observing search prefixes first and then PATH +/// environment variable. +/// +/// Calling this function poisons the configuration cache. For more +/// information, see `Graph.CachePoison` documentation. +/// +/// See also: +/// * `findProgramLazy` +pub fn findProgram(b: *Build, names: []const []const u8) ?[]const u8 { + const graph = b.graph; + const wc = &graph.wip_configuration; + const string_list = wc.addStringList(names) catch @panic("OOM"); + _ = string_list; + graph.poisonCache(); + @panic("TODO"); +} + /// Deprecated; use `runFallible`. pub fn runAllowFail( b: *Build, diff --git a/lib/std/Build/Configuration.zig b/lib/std/Build/Configuration.zig index b96e7922c5ac98db8aa1da14ab4a137ec5410293..02a47a543575deb5fdef442c06b2a52a2eaccd16 100644 --- a/lib/std/Build/Configuration.zig +++ b/lib/std/Build/Configuration.zig @@ -17,6 +17,7 @@ search_prefixes: []String, extra: []u32, default_step: Step.Index, generated_files_len: u32, +poisoned: bool, /// The field order here matches `Configuration` which documents the order in /// the serialized format. @@ -34,6 +35,12 @@ pub const Header = extern struct { /// There is not actually any data stored for this - it just provides a way /// for maker process to preallocate an array for these. generated_files_len: u32, + flags: Flags, + + pub const Flags = packed struct(u32) { + poisoned: bool, + _: u31 = 0, + }; }; pub const Wip = struct { @@ -52,6 +59,7 @@ pub const Wip = struct { search_prefixes: std.ArrayList(String) = .empty, extra: std.ArrayList(u32) = .empty, next_generated_file_index: u32 = 0, + cache_poison: bool = false, const DedupeTable = std.HashMapUnmanaged(ExtraSlice, void, ExtraSlice.Context, std.hash_map.default_max_load_percentage); const TargetsTable = std.HashMapUnmanaged(TargetQuery.Index, void, TargetsTableContext, std.hash_map.default_max_load_percentage); @@ -137,6 +145,7 @@ pub const Wip = struct { pub const Static = struct { default_step: Step.Index, generated_files_len: u32, + poisoned: bool, }; pub fn write(wip: *Wip, w: *Io.Writer, static: Static) Io.Writer.Error!void { @@ -152,6 +161,9 @@ pub const Wip = struct { .default_step = static.default_step, .generated_files_len = static.generated_files_len, + .flags = .{ + .poisoned = static.poisoned, + }, }; var buffers = [_][]const u8{ @ptrCast(&header), @@ -3325,6 +3337,7 @@ pub fn load(arena: Allocator, reader: *Io.Reader) LoadError!Configuration { .extra = try arena.alloc(u32, header.extra_len), .default_step = header.default_step, .generated_files_len = header.generated_files_len, + .poisoned = header.flags.poisoned, }; var vecs = [_][]u8{ result.string_bytes, diff --git a/src/main.zig b/src/main.zig index 36340131acb980e1051cea464f1181dadd796d9e..9073b863f9f6185bdcbf95c1b5624a3b19254ce4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4970,6 +4970,7 @@ fn cmdBuild( var system_pkg_dir_path: ?[]const u8 = null; var debug_target: ?[]const u8 = null; var debug_libc_paths_file: ?[]const u8 = null; + var cache_poison: std.Build.Graph.CachePoison = .pure; const self_exe_path = try process.executablePathAlloc(io, arena); const default_seed = try std.fmt.allocPrint(arena, "0x{x}", .{randInt(io, u32)}); @@ -5039,6 +5040,21 @@ fn cmdBuild( try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len)); configure_argv.appendAssumeCapacity(arg); continue; + } else if (mem.eql(u8, arg, "--cache-poison")) { + cache_poison = .poisoned; + configure_argv.appendAssumeCapacity("--cache-poison=poisoned"); + continue; + } else if (mem.cutPrefix(u8, arg, "--cache-poison=")) |rest| { + // Allow the configurer process to report parse failure. + if (std.meta.stringToEnum(std.Build.Graph.CachePoison, rest)) |poison| { + cache_poison = poison; + } + configure_argv.appendAssumeCapacity(arg); + continue; + } else if (mem.eql(u8, arg, "--verbose")) { + // Intentionally is added both to make and configure but + // does not go into the cache hash. + configure_argv.appendAssumeCapacity(arg); } else if (mem.eql(u8, arg, "--build-file")) { if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg}); i += 1; @@ -5069,10 +5085,6 @@ fn cmdBuild( i += 1; override_global_cache_dir = args[i]; continue; - } else if (mem.eql(u8, arg, "--verbose")) { - // Intentionally is added both to make and configure but - // does not go into the cache hash. - configure_argv.appendAssumeCapacity(arg); } else if (mem.eql(u8, arg, "-freference-trace")) { reference_trace = 256; } else if (mem.eql(u8, arg, "--fetch")) { @@ -5229,6 +5241,10 @@ fn cmdBuild( for (cached_passthru_configure.items) |i| config_man.hash.addBytes(configure_argv.items[i]); + // Prevents a `zig build` from getting a false positive cache hit following + // a `zig build --cache-poison=ignored`. + config_man.hash.add(cache_poison == .ignored); + // Normally the build runner is compiled for the host target but here is // some code to help when debugging edits to the build runner so that you // can make sure it compiles successfully on other targets. @@ -5338,7 +5354,7 @@ fn cmdBuild( // This loop is re-evaluated when the build script exits with an indication that it // could not continue due to missing lazy dependencies. - const configuration_path: Path = cp: while (true) { + const configuration_path: Path, const poisoned: bool = cp: while (true) { // We want to release all the locks before executing the child process, so we make a nice // big block here to ensure the cleanup gets run when we extract out our argv. { @@ -5609,12 +5625,18 @@ fn cmdBuild( _ = try config_man.addFilePath(exe_path, null); configure_argv.items[0] = try exe_path.toString(arena); - if (try config_man.hit()) { - const digest = config_man.final(); - break :cp .{ - .root_dir = dirs.local_cache, - .sub_path = try std.fmt.allocPrint(arena, "o/{s}", .{&digest}), - }; + switch (cache_poison) { + .pure, .disallowed, .ignored => if (try config_man.hit()) { + const digest = config_man.final(); + break :cp .{ + .{ + .root_dir = dirs.local_cache, + .sub_path = try std.fmt.allocPrint(arena, "o/{s}", .{&digest}), + }, + false, + }; + }, + .poisoned => {}, // Don't bother checking for cache hit. } } @@ -5636,7 +5658,7 @@ fn cmdBuild( ); defer config_tmp_file.close(io); - switch (term: { + const term = term: { const child_node = root_prog_node.start("Run Configure Script", 0); defer child_node.end(); var child = std.process.spawn(io, .{ @@ -5647,101 +5669,86 @@ fn cmdBuild( defer child.kill(io); break :term child.wait(io) catch |err| fatal("failed to wait configure script {s}: {t}", .{ configure_argv.items[0], err }); - }) { - .exited => |code| { - if (code != 0) { - // Failure to produce the configuration file. - const cmd = try std.mem.join(arena, " ", configure_argv.items); - fatal("the following configure command failed with exit code {d}:\n{s}", .{ code, cmd }); - } - // Even though the file is designed to be sent directly to make - // runner, we must load it now because: - // * If it contains additional file dependencies, we need to - // add them to `config_man` before obtaining the final digest. - // * If it contains a set of lazy packages that need to be - // fetched, we need to fetch those now and re-run configure. - var configuration = std.Build.Configuration.loadFile(arena, io, config_tmp_file) catch |err| - fatal("failed to load configuration file {f}: {t}", .{ config_tmp_path, err }); + }; + if (!term.success()) { + // Failure to produce the configuration file. + const cmd = try std.mem.join(arena, " ", configure_argv.items); + fatal("the following configure command {f}:\n{s}", .{ term, cmd }); + } + // Even though the file is designed to be sent directly to make + // runner, we must load it now because: + // * If it contains additional file dependencies, we need to + // add them to `config_man` before obtaining the final digest. + // * If it contains a set of lazy packages that need to be + // fetched, we need to fetch those now and re-run configure. + var configuration = std.Build.Configuration.loadFile(arena, io, config_tmp_file) catch |err| + fatal("failed to load configuration file {f}: {t}", .{ config_tmp_path, err }); - if (configuration.unlazy_deps.len != 0) { - if (!dev.env.supports(.fetch_command)) process.exit(1); - var any_errors = false; - for (configuration.unlazy_deps) |hash_string| { - const hash = hash_string.slice(&configuration); - assert(hash.len != 0); - if (hash.len > Package.Hash.max_len) { - std.log.err("invalid digest (length {d} exceeds maximum): '{s}'", .{ - hash.len, hash, - }); - any_errors = true; - continue; - } - try unlazy_set.put(arena, .fromSlice(hash), {}); - } - if (any_errors) process.exit(1); - if (system_pkg_dir_path) |p| { - // In this mode, the system needs to provide these packages; they - // cannot be fetched by Zig. - const s = fs.path.sep_str; - for (unlazy_set.keys()) |*hash| { - std.log.err("lazy dependency package not found: {s}" ++ s ++ "{s}", .{ - p, hash.toSlice(), - }); - } - std.log.info("remote package fetching disabled due to --system mode", .{}); - std.log.info("dependencies might be avoidable depending on build configuration", .{}); - process.exit(1); - } - continue :cp; + if (configuration.unlazy_deps.len != 0) { + if (!dev.env.supports(.fetch_command)) process.exit(1); + var any_errors = false; + for (configuration.unlazy_deps) |hash_string| { + const hash = hash_string.slice(&configuration); + assert(hash.len != 0); + if (hash.len > Package.Hash.max_len) { + std.log.err("invalid digest (length {d} exceeds maximum): '{s}'", .{ hash.len, hash }); + any_errors = true; + continue; } - - for (configuration.path_deps_base, configuration.path_deps_sub) |base, sub| { - const conf_path: std.Build.Configuration.Path = .{ .base = base, .sub = sub }; - try config_man.addPathPost(conf_path.toCachePath(&configuration, arena)); + try unlazy_set.put(arena, .fromSlice(hash), {}); + } + if (any_errors) process.exit(1); + if (system_pkg_dir_path) |p| { + // In this mode, the system needs to provide these packages; they + // cannot be fetched by Zig. + const s = fs.path.sep_str; + for (unlazy_set.keys()) |*hash| { + std.log.err("lazy dependency package not found: {s}" ++ s ++ "{s}", .{ p, hash.toSlice() }); } + std.log.info("remote package fetching disabled due to --system mode", .{}); + std.log.info("dependencies might be avoidable depending on build configuration", .{}); + process.exit(1); + } + continue :cp; + } - const digest = config_man.final(); - const final_path: Path = .{ - .root_dir = dirs.local_cache, - .sub_path = try std.fmt.allocPrint(arena, "o/{s}", .{&digest}), - }; - Io.Dir.rename( - config_tmp_path.root_dir.handle, - config_tmp_path.sub_path, - final_path.root_dir.handle, - final_path.sub_path, - io, - ) catch |err| { - fatal("failed to rename configuration file from {f} into {f}: {t}", .{ - config_tmp_path, final_path, err, - }); - }; - config_man.writeManifest() catch |err| warn("failed to write cache manifest: {t}", .{err}); + for (configuration.path_deps_base, configuration.path_deps_sub) |base, sub| { + const conf_path: std.Build.Configuration.Path = .{ .base = base, .sub = sub }; + try config_man.addPathPost(conf_path.toCachePath(&configuration, arena)); + } - break :cp final_path; - }, - .signal => |sig| { - const cmd = try std.mem.join(arena, " ", configure_argv.items); - fatal("the following configure command terminated with signal {t}:\n{s}", .{ sig, cmd }); - }, - .stopped => |sig| { - const cmd = try std.mem.join(arena, " ", configure_argv.items); - fatal("the following build command stopped with signal {t}:\n{s}", .{ sig, cmd }); - }, - .unknown => { - const cmd = try std.mem.join(arena, " ", configure_argv.items); - fatal("the following build command crashed:\n{s}", .{cmd}); - }, + // If it is poisoned, there is no point in moving it to cached + // location. Just leave it in the tmp directory. + if (configuration.poisoned) { + break :cp .{ config_tmp_path, true }; + } else { + const digest = config_man.final(); + const final_path: Path = .{ + .root_dir = dirs.local_cache, + .sub_path = try std.fmt.allocPrint(arena, "o/{s}", .{&digest}), + }; + Io.Dir.rename( + config_tmp_path.root_dir.handle, + config_tmp_path.sub_path, + final_path.root_dir.handle, + final_path.sub_path, + io, + ) catch |err| { + fatal("failed to rename configuration file from {f} into {f}: {t}", .{ + config_tmp_path, final_path, err, + }); + }; + config_man.writeManifest() catch |err| warn("failed to write cache manifest: {t}", .{err}); + break :cp .{ final_path, false }; } }; { // Release all file system locks just before running the maker process. - var configuration_lock = config_man.toOwnedLock(); - defer configuration_lock.release(io); + var configuration_lock = if (!poisoned) config_man.toOwnedLock() else null; + defer if (configuration_lock) |*l| l.release(io); - const make_runner = make_runner_task.await(io) catch |err| - fatal("failed to compile maker: {t}", .{err}); + const make_runner = make_runner_task.await(io) catch |err| fatal("failed compiling maker: {t}", .{err}); make_argv.items[0] = try make_runner.exe_path.toString(arena); make_argv.items[argv_index_configuration_file] = try configuration_path.toString(arena); @@ -5749,33 +5756,24 @@ fn cmdBuild( if (!process.can_spawn) { const cmd = try std.mem.join(arena, " ", make_argv.items); - fatal("the following command cannot be executed ({t} does not support spawning a child process):\n{s}", .{ native_os, cmd }); + fatal("the following command cannot be executed ({t} does not support spawning a child process):\n{s}", .{ + native_os, cmd, + }); } - switch (term: { + const term = term: { _ = try io.lockStderr(&.{}, .no_color); defer io.unlockStderr(); var child = std.process.spawn(io, .{ .argv = make_argv.items, - }) catch |err| fatal("failed to spawn maker {s}: {t}", .{ make_argv.items[0], err }); + }) catch |err| fatal("failed spawning maker {s}: {t}", .{ make_argv.items[0], err }); defer child.kill(io); break :term child.wait(io) catch |err| - fatal("failed to wait maker {s}: {t}", .{ make_argv.items[0], err }); - }) { - .exited => |code| { - if (code == 0) return cleanExit(io); - const cmd = try std.mem.join(arena, " ", make_argv.items); - fatal("the following maker command failed with exit code {d}:\n{s}", .{ code, cmd }); - }, - .signal => |sig| { - const cmd = try std.mem.join(arena, " ", make_argv.items); - fatal("the following maker command terminated with signal {t}:\n{s}", .{ sig, cmd }); - }, - else => { - const cmd = try std.mem.join(arena, " ", make_argv.items); - fatal("the following maker command crashed:\n{s}", .{cmd}); - }, - } + fatal("failed waiting on maker {s}: {t}", .{ make_argv.items[0], err }); + }; + if (term.success()) return cleanExit(io); + const cmd = try std.mem.join(arena, " ", make_argv.items); + fatal("the following maker command {f}:\n{s}", .{ term, cmd }); } const MakeRunner = struct { -- 2.54.0