master
   1const Step = @This();
   2const builtin = @import("builtin");
   3
   4const std = @import("../std.zig");
   5const Io = std.Io;
   6const Build = std.Build;
   7const Allocator = std.mem.Allocator;
   8const assert = std.debug.assert;
   9const Cache = Build.Cache;
  10const Path = Cache.Path;
  11const ArrayList = std.ArrayList;
  12
  13id: Id,
  14name: []const u8,
  15owner: *Build,
  16makeFn: MakeFn,
  17
  18dependencies: std.array_list.Managed(*Step),
  19/// This field is empty during execution of the user's build script, and
  20/// then populated during dependency loop checking in the build runner.
  21dependants: ArrayList(*Step),
  22/// Collects the set of files that retrigger this step to run.
  23///
  24/// This is used by the build system's implementation of `--watch` but it can
  25/// also be potentially useful for IDEs to know what effects editing a
  26/// particular file has.
  27///
  28/// Populated within `make`. Implementation may choose to clear and repopulate,
  29/// retain previous value, or update.
  30inputs: Inputs,
  31
  32state: State,
  33/// Set this field to declare an upper bound on the amount of bytes of memory it will
  34/// take to run the step. Zero means no limit.
  35///
  36/// The idea to annotate steps that might use a high amount of RAM with an
  37/// upper bound. For example, perhaps a particular set of unit tests require 4
  38/// GiB of RAM, and those tests will be run under 4 different build
  39/// configurations at once. This would potentially require 16 GiB of memory on
  40/// the system if all 4 steps executed simultaneously, which could easily be
  41/// greater than what is actually available, potentially causing the system to
  42/// crash when using `zig build` at the default concurrency level.
  43///
  44/// This field causes the build runner to do two things:
  45/// 1. ulimit child processes, so that they will fail if it would exceed this
  46/// memory limit. This serves to enforce that this upper bound value is
  47/// correct.
  48/// 2. Ensure that the set of concurrent steps at any given time have a total
  49/// max_rss value that does not exceed the `max_total_rss` value of the build
  50/// runner. This value is configurable on the command line, and defaults to the
  51/// total system memory available.
  52max_rss: usize,
  53
  54result_error_msgs: ArrayList([]const u8),
  55result_error_bundle: std.zig.ErrorBundle,
  56result_stderr: []const u8,
  57result_cached: bool,
  58result_duration_ns: ?u64,
  59/// 0 means unavailable or not reported.
  60result_peak_rss: usize,
  61/// If the step is failed and this field is populated, this is the command which failed.
  62/// This field may be populated even if the step succeeded.
  63result_failed_command: ?[]const u8,
  64test_results: TestResults,
  65
  66/// The return address associated with creation of this step that can be useful
  67/// to print along with debugging messages.
  68debug_stack_trace: std.builtin.StackTrace,
  69
  70pub const TestResults = struct {
  71    /// The total number of tests in the step. Every test has a "status" from the following:
  72    /// * passed
  73    /// * skipped
  74    /// * failed cleanly
  75    /// * crashed
  76    /// * timed out
  77    test_count: u32 = 0,
  78
  79    /// The number of tests which were skipped (`error.SkipZigTest`).
  80    skip_count: u32 = 0,
  81    /// The number of tests which failed cleanly.
  82    fail_count: u32 = 0,
  83    /// The number of tests which terminated unexpectedly, i.e. crashed.
  84    crash_count: u32 = 0,
  85    /// The number of tests which timed out.
  86    timeout_count: u32 = 0,
  87
  88    /// The number of detected memory leaks. The associated test may still have passed; indeed, *all*
  89    /// individual tests may have passed. However, the step as a whole fails if any test has leaks.
  90    leak_count: u32 = 0,
  91    /// The number of detected error logs. The associated test may still have passed; indeed, *all*
  92    /// individual tests may have passed. However, the step as a whole fails if any test logs errors.
  93    log_err_count: u32 = 0,
  94
  95    pub fn isSuccess(tr: TestResults) bool {
  96        // all steps are success or skip
  97        return tr.fail_count == 0 and
  98            tr.crash_count == 0 and
  99            tr.timeout_count == 0 and
 100            // no (otherwise successful) step leaked memory or logged errors
 101            tr.leak_count == 0 and
 102            tr.log_err_count == 0;
 103    }
 104
 105    /// Computes the number of tests which passed from the other values.
 106    pub fn passCount(tr: TestResults) u32 {
 107        return tr.test_count - tr.skip_count - tr.fail_count - tr.crash_count - tr.timeout_count;
 108    }
 109};
 110
 111pub const MakeOptions = struct {
 112    progress_node: std.Progress.Node,
 113    watch: bool,
 114    web_server: switch (builtin.target.cpu.arch) {
 115        else => ?*Build.WebServer,
 116        // WASM code references `Build.abi` which happens to incidentally reference this type, but
 117        // it currently breaks because `std.net.Address` doesn't work there. Work around for now.
 118        .wasm32 => void,
 119    },
 120    ttyconf: std.Io.tty.Config,
 121    /// If set, this is a timeout to enforce on all individual unit tests, in nanoseconds.
 122    unit_test_timeout_ns: ?u64,
 123    /// Not to be confused with `Build.allocator`, which is an alias of `Build.graph.arena`.
 124    gpa: Allocator,
 125};
 126
 127pub const MakeFn = *const fn (step: *Step, options: MakeOptions) anyerror!void;
 128
 129pub const State = enum {
 130    precheck_unstarted,
 131    precheck_started,
 132    /// This is also used to indicate "dirty" steps that have been modified
 133    /// after a previous build completed, in which case, the step may or may
 134    /// not have been completed before. Either way, one or more of its direct
 135    /// file system inputs have been modified, meaning that the step needs to
 136    /// be re-evaluated.
 137    precheck_done,
 138    running,
 139    dependency_failure,
 140    success,
 141    failure,
 142    /// This state indicates that the step did not complete, however, it also did not fail,
 143    /// and it is safe to continue executing its dependencies.
 144    skipped,
 145    /// This step was skipped because it specified a max_rss that exceeded the runner's maximum.
 146    /// It is not safe to run its dependencies.
 147    skipped_oom,
 148};
 149
 150pub const Id = enum {
 151    top_level,
 152    compile,
 153    install_artifact,
 154    install_file,
 155    install_dir,
 156    remove_dir,
 157    fail,
 158    fmt,
 159    translate_c,
 160    write_file,
 161    update_source_files,
 162    run,
 163    check_file,
 164    check_object,
 165    config_header,
 166    objcopy,
 167    options,
 168    custom,
 169
 170    pub fn Type(comptime id: Id) type {
 171        return switch (id) {
 172            .top_level => Build.TopLevelStep,
 173            .compile => Compile,
 174            .install_artifact => InstallArtifact,
 175            .install_file => InstallFile,
 176            .install_dir => InstallDir,
 177            .remove_dir => RemoveDir,
 178            .fail => Fail,
 179            .fmt => Fmt,
 180            .translate_c => TranslateC,
 181            .write_file => WriteFile,
 182            .update_source_files => UpdateSourceFiles,
 183            .run => Run,
 184            .check_file => CheckFile,
 185            .check_object => CheckObject,
 186            .config_header => ConfigHeader,
 187            .objcopy => ObjCopy,
 188            .options => Options,
 189            .custom => @compileError("no type available for custom step"),
 190        };
 191    }
 192};
 193
 194pub const CheckFile = @import("Step/CheckFile.zig");
 195pub const CheckObject = @import("Step/CheckObject.zig");
 196pub const ConfigHeader = @import("Step/ConfigHeader.zig");
 197pub const Fail = @import("Step/Fail.zig");
 198pub const Fmt = @import("Step/Fmt.zig");
 199pub const InstallArtifact = @import("Step/InstallArtifact.zig");
 200pub const InstallDir = @import("Step/InstallDir.zig");
 201pub const InstallFile = @import("Step/InstallFile.zig");
 202pub const ObjCopy = @import("Step/ObjCopy.zig");
 203pub const Compile = @import("Step/Compile.zig");
 204pub const Options = @import("Step/Options.zig");
 205pub const RemoveDir = @import("Step/RemoveDir.zig");
 206pub const Run = @import("Step/Run.zig");
 207pub const TranslateC = @import("Step/TranslateC.zig");
 208pub const WriteFile = @import("Step/WriteFile.zig");
 209pub const UpdateSourceFiles = @import("Step/UpdateSourceFiles.zig");
 210
 211pub const Inputs = struct {
 212    table: Table,
 213
 214    pub const init: Inputs = .{
 215        .table = .{},
 216    };
 217
 218    pub const Table = std.ArrayHashMapUnmanaged(Build.Cache.Path, Files, Build.Cache.Path.TableAdapter, false);
 219    /// The special file name "." means any changes inside the directory.
 220    pub const Files = ArrayList([]const u8);
 221
 222    pub fn populated(inputs: *Inputs) bool {
 223        return inputs.table.count() != 0;
 224    }
 225
 226    pub fn clear(inputs: *Inputs, gpa: Allocator) void {
 227        for (inputs.table.values()) |*files| files.deinit(gpa);
 228        inputs.table.clearRetainingCapacity();
 229    }
 230};
 231
 232pub const StepOptions = struct {
 233    id: Id,
 234    name: []const u8,
 235    owner: *Build,
 236    makeFn: MakeFn = makeNoOp,
 237    first_ret_addr: ?usize = null,
 238    max_rss: usize = 0,
 239};
 240
 241pub fn init(options: StepOptions) Step {
 242    const arena = options.owner.allocator;
 243
 244    return .{
 245        .id = options.id,
 246        .name = arena.dupe(u8, options.name) catch @panic("OOM"),
 247        .owner = options.owner,
 248        .makeFn = options.makeFn,
 249        .dependencies = std.array_list.Managed(*Step).init(arena),
 250        .dependants = .empty,
 251        .inputs = Inputs.init,
 252        .state = .precheck_unstarted,
 253        .max_rss = options.max_rss,
 254        .debug_stack_trace = blk: {
 255            const addr_buf = arena.alloc(usize, options.owner.debug_stack_frames_count) catch @panic("OOM");
 256            const first_ret_addr = options.first_ret_addr orelse @returnAddress();
 257            break :blk std.debug.captureCurrentStackTrace(.{ .first_address = first_ret_addr }, addr_buf);
 258        },
 259        .result_error_msgs = .{},
 260        .result_error_bundle = std.zig.ErrorBundle.empty,
 261        .result_stderr = "",
 262        .result_cached = false,
 263        .result_duration_ns = null,
 264        .result_peak_rss = 0,
 265        .result_failed_command = null,
 266        .test_results = .{},
 267    };
 268}
 269
 270/// If the Step's `make` function reports `error.MakeFailed`, it indicates they
 271/// have already reported the error. Otherwise, we add a simple error report
 272/// here.
 273pub fn make(s: *Step, options: MakeOptions) error{ MakeFailed, MakeSkipped }!void {
 274    const arena = s.owner.allocator;
 275
 276    var timer: ?std.time.Timer = t: {
 277        if (!s.owner.graph.time_report) break :t null;
 278        if (s.id == .compile) break :t null;
 279        if (s.id == .run and s.cast(Run).?.stdio == .zig_test) break :t null;
 280        break :t std.time.Timer.start() catch @panic("--time-report not supported on this host");
 281    };
 282    const make_result = s.makeFn(s, options);
 283    if (timer) |*t| {
 284        options.web_server.?.updateTimeReportGeneric(s, t.read());
 285    }
 286
 287    make_result catch |err| switch (err) {
 288        error.MakeFailed => return error.MakeFailed,
 289        error.MakeSkipped => return error.MakeSkipped,
 290        else => {
 291            s.result_error_msgs.append(arena, @errorName(err)) catch @panic("OOM");
 292            return error.MakeFailed;
 293        },
 294    };
 295
 296    if (!s.test_results.isSuccess()) {
 297        return error.MakeFailed;
 298    }
 299
 300    if (s.max_rss != 0 and s.result_peak_rss > s.max_rss) {
 301        const msg = std.fmt.allocPrint(arena, "memory usage peaked at {0B:.2} ({0d} bytes), exceeding the declared upper bound of {1B:.2} ({1d} bytes)", .{
 302            s.result_peak_rss, s.max_rss,
 303        }) catch @panic("OOM");
 304        s.result_error_msgs.append(arena, msg) catch @panic("OOM");
 305    }
 306}
 307
 308pub fn dependOn(step: *Step, other: *Step) void {
 309    step.dependencies.append(other) catch @panic("OOM");
 310}
 311
 312fn makeNoOp(step: *Step, options: MakeOptions) anyerror!void {
 313    _ = options;
 314
 315    var all_cached = true;
 316
 317    for (step.dependencies.items) |dep| {
 318        all_cached = all_cached and dep.result_cached;
 319    }
 320
 321    step.result_cached = all_cached;
 322}
 323
 324pub fn cast(step: *Step, comptime T: type) ?*T {
 325    if (step.id == T.base_id) {
 326        return @fieldParentPtr("step", step);
 327    }
 328    return null;
 329}
 330
 331/// For debugging purposes, prints identifying information about this Step.
 332pub fn dump(step: *Step, w: *Io.Writer, tty_config: Io.tty.Config) void {
 333    if (step.debug_stack_trace.instruction_addresses.len > 0) {
 334        w.print("name: '{s}'. creation stack trace:\n", .{step.name}) catch {};
 335        std.debug.writeStackTrace(&step.debug_stack_trace, w, tty_config) catch {};
 336    } else {
 337        const field = "debug_stack_frames_count";
 338        comptime assert(@hasField(Build, field));
 339        tty_config.setColor(w, .yellow) catch {};
 340        w.print("name: '{s}'. no stack trace collected for this step, see std.Build." ++ field ++ "\n", .{step.name}) catch {};
 341        tty_config.setColor(w, .reset) catch {};
 342    }
 343}
 344
 345/// Populates `s.result_failed_command`.
 346pub fn captureChildProcess(
 347    s: *Step,
 348    gpa: Allocator,
 349    progress_node: std.Progress.Node,
 350    argv: []const []const u8,
 351) !std.process.Child.RunResult {
 352    const arena = s.owner.allocator;
 353
 354    // If an error occurs, it's happened in this command:
 355    assert(s.result_failed_command == null);
 356    s.result_failed_command = try allocPrintCmd(gpa, null, argv);
 357
 358    try handleChildProcUnsupported(s);
 359    try handleVerbose(s.owner, null, argv);
 360
 361    const result = std.process.Child.run(.{
 362        .allocator = arena,
 363        .argv = argv,
 364        .progress_node = progress_node,
 365    }) catch |err| return s.fail("failed to run {s}: {t}", .{ argv[0], err });
 366
 367    if (result.stderr.len > 0) {
 368        try s.result_error_msgs.append(arena, result.stderr);
 369    }
 370
 371    return result;
 372}
 373
 374pub fn fail(step: *Step, comptime fmt: []const u8, args: anytype) error{ OutOfMemory, MakeFailed } {
 375    try step.addError(fmt, args);
 376    return error.MakeFailed;
 377}
 378
 379pub fn addError(step: *Step, comptime fmt: []const u8, args: anytype) error{OutOfMemory}!void {
 380    const arena = step.owner.allocator;
 381    const msg = try std.fmt.allocPrint(arena, fmt, args);
 382    try step.result_error_msgs.append(arena, msg);
 383}
 384
 385pub const ZigProcess = struct {
 386    child: std.process.Child,
 387    poller: Io.Poller(StreamEnum),
 388    progress_ipc_fd: if (std.Progress.have_ipc) ?std.posix.fd_t else void,
 389
 390    pub const StreamEnum = enum { stdout, stderr };
 391};
 392
 393/// Assumes that argv contains `--listen=-` and that the process being spawned
 394/// is the zig compiler - the same version that compiled the build runner.
 395/// Populates `s.result_failed_command`.
 396pub fn evalZigProcess(
 397    s: *Step,
 398    argv: []const []const u8,
 399    prog_node: std.Progress.Node,
 400    watch: bool,
 401    web_server: ?*Build.WebServer,
 402    gpa: Allocator,
 403) !?Path {
 404    // If an error occurs, it's happened in this command:
 405    assert(s.result_failed_command == null);
 406    s.result_failed_command = try allocPrintCmd(gpa, null, argv);
 407
 408    if (s.getZigProcess()) |zp| update: {
 409        assert(watch);
 410        if (std.Progress.have_ipc) if (zp.progress_ipc_fd) |fd| prog_node.setIpcFd(fd);
 411        const result = zigProcessUpdate(s, zp, watch, web_server, gpa) catch |err| switch (err) {
 412            error.BrokenPipe => {
 413                // Process restart required.
 414                const term = zp.child.wait() catch |e| {
 415                    return s.fail("unable to wait for {s}: {t}", .{ argv[0], e });
 416                };
 417                _ = term;
 418                s.clearZigProcess(gpa);
 419                break :update;
 420            },
 421            else => |e| return e,
 422        };
 423
 424        if (s.result_error_bundle.errorMessageCount() > 0) {
 425            return s.fail("{d} compilation errors", .{s.result_error_bundle.errorMessageCount()});
 426        }
 427
 428        if (s.result_error_msgs.items.len > 0 and result == null) {
 429            // Crash detected.
 430            const term = zp.child.wait() catch |e| {
 431                return s.fail("unable to wait for {s}: {t}", .{ argv[0], e });
 432            };
 433            s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0;
 434            s.clearZigProcess(gpa);
 435            try handleChildProcessTerm(s, term);
 436            return error.MakeFailed;
 437        }
 438
 439        return result;
 440    }
 441    assert(argv.len != 0);
 442    const b = s.owner;
 443    const arena = b.allocator;
 444
 445    try handleChildProcUnsupported(s);
 446    try handleVerbose(s.owner, null, argv);
 447
 448    var child = std.process.Child.init(argv, arena);
 449    child.env_map = &b.graph.env_map;
 450    child.stdin_behavior = .Pipe;
 451    child.stdout_behavior = .Pipe;
 452    child.stderr_behavior = .Pipe;
 453    child.request_resource_usage_statistics = true;
 454    child.progress_node = prog_node;
 455
 456    child.spawn() catch |err| return s.fail("failed to spawn zig compiler {s}: {t}", .{ argv[0], err });
 457
 458    const zp = try gpa.create(ZigProcess);
 459    zp.* = .{
 460        .child = child,
 461        .poller = Io.poll(gpa, ZigProcess.StreamEnum, .{
 462            .stdout = child.stdout.?,
 463            .stderr = child.stderr.?,
 464        }),
 465        .progress_ipc_fd = if (std.Progress.have_ipc) child.progress_node.getIpcFd() else {},
 466    };
 467    if (watch) s.setZigProcess(zp);
 468    defer if (!watch) {
 469        zp.poller.deinit();
 470        gpa.destroy(zp);
 471    };
 472
 473    const result = try zigProcessUpdate(s, zp, watch, web_server, gpa);
 474
 475    if (!watch) {
 476        // Send EOF to stdin.
 477        zp.child.stdin.?.close();
 478        zp.child.stdin = null;
 479
 480        const term = zp.child.wait() catch |err| {
 481            return s.fail("unable to wait for {s}: {t}", .{ argv[0], err });
 482        };
 483        s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0;
 484
 485        // Special handling for Compile step that is expecting compile errors.
 486        if (s.cast(Compile)) |compile| switch (term) {
 487            .Exited => {
 488                // Note that the exit code may be 0 in this case due to the
 489                // compiler server protocol.
 490                if (compile.expect_errors != null) {
 491                    return error.NeedCompileErrorCheck;
 492                }
 493            },
 494            else => {},
 495        };
 496
 497        try handleChildProcessTerm(s, term);
 498    }
 499
 500    if (s.result_error_bundle.errorMessageCount() > 0) {
 501        return s.fail("{d} compilation errors", .{s.result_error_bundle.errorMessageCount()});
 502    }
 503
 504    return result;
 505}
 506
 507/// Wrapper around `std.fs.Dir.updateFile` that handles verbose and error output.
 508pub fn installFile(s: *Step, src_lazy_path: Build.LazyPath, dest_path: []const u8) !Io.Dir.PrevStatus {
 509    const b = s.owner;
 510    const io = b.graph.io;
 511    const src_path = src_lazy_path.getPath3(b, s);
 512    try handleVerbose(b, null, &.{ "install", "-C", b.fmt("{f}", .{src_path}), dest_path });
 513    return Io.Dir.updateFile(src_path.root_dir.handle.adaptToNewApi(), io, src_path.sub_path, .cwd(), dest_path, .{}) catch |err| {
 514        return s.fail("unable to update file from '{f}' to '{s}': {t}", .{
 515            src_path, dest_path, err,
 516        });
 517    };
 518}
 519
 520/// Wrapper around `std.fs.Dir.makePathStatus` that handles verbose and error output.
 521pub fn installDir(s: *Step, dest_path: []const u8) !std.fs.Dir.MakePathStatus {
 522    const b = s.owner;
 523    try handleVerbose(b, null, &.{ "install", "-d", dest_path });
 524    return std.fs.cwd().makePathStatus(dest_path) catch |err| {
 525        return s.fail("unable to create dir '{s}': {t}", .{ dest_path, err });
 526    };
 527}
 528
 529fn zigProcessUpdate(s: *Step, zp: *ZigProcess, watch: bool, web_server: ?*Build.WebServer, gpa: Allocator) !?Path {
 530    const b = s.owner;
 531    const arena = b.allocator;
 532
 533    var timer = try std.time.Timer.start();
 534
 535    try sendMessage(zp.child.stdin.?, .update);
 536    if (!watch) try sendMessage(zp.child.stdin.?, .exit);
 537
 538    var result: ?Path = null;
 539
 540    const stdout = zp.poller.reader(.stdout);
 541
 542    poll: while (true) {
 543        const Header = std.zig.Server.Message.Header;
 544        while (stdout.buffered().len < @sizeOf(Header)) if (!try zp.poller.poll()) break :poll;
 545        const header = stdout.takeStruct(Header, .little) catch unreachable;
 546        while (stdout.buffered().len < header.bytes_len) if (!try zp.poller.poll()) break :poll;
 547        const body = stdout.take(header.bytes_len) catch unreachable;
 548        switch (header.tag) {
 549            .zig_version => {
 550                if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
 551                    return s.fail(
 552                        "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
 553                        .{ builtin.zig_version_string, body },
 554                    );
 555                }
 556            },
 557            .error_bundle => {
 558                s.result_error_bundle = try std.zig.Server.allocErrorBundle(gpa, body);
 559                // This message indicates the end of the update.
 560                if (watch) break :poll;
 561            },
 562            .emit_digest => {
 563                const EmitDigest = std.zig.Server.Message.EmitDigest;
 564                const emit_digest = @as(*align(1) const EmitDigest, @ptrCast(body));
 565                s.result_cached = emit_digest.flags.cache_hit;
 566                const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
 567                result = .{
 568                    .root_dir = b.cache_root,
 569                    .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)),
 570                };
 571            },
 572            .file_system_inputs => {
 573                s.clearWatchInputs();
 574                var it = std.mem.splitScalar(u8, body, 0);
 575                while (it.next()) |prefixed_path| {
 576                    const prefix_index: std.zig.Server.Message.PathPrefix = @enumFromInt(prefixed_path[0] - 1);
 577                    const sub_path = try arena.dupe(u8, prefixed_path[1..]);
 578                    const sub_path_dirname = std.fs.path.dirname(sub_path) orelse "";
 579                    switch (prefix_index) {
 580                        .cwd => {
 581                            const path: Build.Cache.Path = .{
 582                                .root_dir = Build.Cache.Directory.cwd(),
 583                                .sub_path = sub_path_dirname,
 584                            };
 585                            try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path));
 586                        },
 587                        .zig_lib => zl: {
 588                            if (s.cast(Step.Compile)) |compile| {
 589                                if (compile.zig_lib_dir) |zig_lib_dir| {
 590                                    const lp = try zig_lib_dir.join(arena, sub_path);
 591                                    try addWatchInput(s, lp);
 592                                    break :zl;
 593                                }
 594                            }
 595                            const path: Build.Cache.Path = .{
 596                                .root_dir = s.owner.graph.zig_lib_directory,
 597                                .sub_path = sub_path_dirname,
 598                            };
 599                            try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path));
 600                        },
 601                        .local_cache => {
 602                            const path: Build.Cache.Path = .{
 603                                .root_dir = b.cache_root,
 604                                .sub_path = sub_path_dirname,
 605                            };
 606                            try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path));
 607                        },
 608                        .global_cache => {
 609                            const path: Build.Cache.Path = .{
 610                                .root_dir = s.owner.graph.global_cache_root,
 611                                .sub_path = sub_path_dirname,
 612                            };
 613                            try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path));
 614                        },
 615                    }
 616                }
 617            },
 618            .time_report => if (web_server) |ws| {
 619                const TimeReport = std.zig.Server.Message.TimeReport;
 620                const tr: *align(1) const TimeReport = @ptrCast(body[0..@sizeOf(TimeReport)]);
 621                ws.updateTimeReportCompile(.{
 622                    .compile = s.cast(Step.Compile).?,
 623                    .use_llvm = tr.flags.use_llvm,
 624                    .stats = tr.stats,
 625                    .ns_total = timer.read(),
 626                    .llvm_pass_timings_len = tr.llvm_pass_timings_len,
 627                    .files_len = tr.files_len,
 628                    .decls_len = tr.decls_len,
 629                    .trailing = body[@sizeOf(TimeReport)..],
 630                });
 631            },
 632            else => {}, // ignore other messages
 633        }
 634    }
 635
 636    s.result_duration_ns = timer.read();
 637
 638    const stderr_contents = try zp.poller.toOwnedSlice(.stderr);
 639    if (stderr_contents.len > 0) {
 640        try s.result_error_msgs.append(arena, try arena.dupe(u8, stderr_contents));
 641    }
 642
 643    return result;
 644}
 645
 646pub fn getZigProcess(s: *Step) ?*ZigProcess {
 647    return switch (s.id) {
 648        .compile => s.cast(Compile).?.zig_process,
 649        else => null,
 650    };
 651}
 652
 653fn setZigProcess(s: *Step, zp: *ZigProcess) void {
 654    switch (s.id) {
 655        .compile => s.cast(Compile).?.zig_process = zp,
 656        else => unreachable,
 657    }
 658}
 659
 660fn clearZigProcess(s: *Step, gpa: Allocator) void {
 661    switch (s.id) {
 662        .compile => {
 663            const compile = s.cast(Compile).?;
 664            if (compile.zig_process) |zp| {
 665                gpa.destroy(zp);
 666                compile.zig_process = null;
 667            }
 668        },
 669        else => unreachable,
 670    }
 671}
 672
 673fn sendMessage(file: std.fs.File, tag: std.zig.Client.Message.Tag) !void {
 674    const header: std.zig.Client.Message.Header = .{
 675        .tag = tag,
 676        .bytes_len = 0,
 677    };
 678    var w = file.writer(&.{});
 679    w.interface.writeStruct(header, .little) catch |err| switch (err) {
 680        error.WriteFailed => return w.err.?,
 681    };
 682}
 683
 684pub fn handleVerbose(
 685    b: *Build,
 686    opt_cwd: ?[]const u8,
 687    argv: []const []const u8,
 688) error{OutOfMemory}!void {
 689    return handleVerbose2(b, opt_cwd, null, argv);
 690}
 691
 692pub fn handleVerbose2(
 693    b: *Build,
 694    opt_cwd: ?[]const u8,
 695    opt_env: ?*const std.process.EnvMap,
 696    argv: []const []const u8,
 697) error{OutOfMemory}!void {
 698    if (b.verbose) {
 699        // Intention of verbose is to print all sub-process command lines to
 700        // stderr before spawning them.
 701        const text = try allocPrintCmd2(b.allocator, opt_cwd, opt_env, argv);
 702        std.debug.print("{s}\n", .{text});
 703    }
 704}
 705
 706/// Asserts that the caller has already populated `s.result_failed_command`.
 707pub inline fn handleChildProcUnsupported(s: *Step) error{ OutOfMemory, MakeFailed }!void {
 708    if (!std.process.can_spawn) {
 709        return s.fail("unable to spawn process: host cannot spawn child processes", .{});
 710    }
 711}
 712
 713/// Asserts that the caller has already populated `s.result_failed_command`.
 714pub fn handleChildProcessTerm(s: *Step, term: std.process.Child.Term) error{ MakeFailed, OutOfMemory }!void {
 715    assert(s.result_failed_command != null);
 716    switch (term) {
 717        .Exited => |code| {
 718            if (code != 0) {
 719                return s.fail("process exited with error code {d}", .{code});
 720            }
 721        },
 722        .Signal, .Stopped, .Unknown => {
 723            return s.fail("process terminated unexpectedly", .{});
 724        },
 725    }
 726}
 727
 728pub fn allocPrintCmd(
 729    gpa: Allocator,
 730    opt_cwd: ?[]const u8,
 731    argv: []const []const u8,
 732) Allocator.Error![]u8 {
 733    return allocPrintCmd2(gpa, opt_cwd, null, argv);
 734}
 735
 736pub fn allocPrintCmd2(
 737    gpa: Allocator,
 738    opt_cwd: ?[]const u8,
 739    opt_env: ?*const std.process.EnvMap,
 740    argv: []const []const u8,
 741) Allocator.Error![]u8 {
 742    const shell = struct {
 743        fn escape(writer: *Io.Writer, string: []const u8, is_argv0: bool) !void {
 744            for (string) |c| {
 745                if (switch (c) {
 746                    else => true,
 747                    '%', '+'...':', '@'...'Z', '_', 'a'...'z' => false,
 748                    '=' => is_argv0,
 749                }) break;
 750            } else return writer.writeAll(string);
 751
 752            try writer.writeByte('"');
 753            for (string) |c| {
 754                if (switch (c) {
 755                    std.ascii.control_code.nul => break,
 756                    '!', '"', '$', '\\', '`' => true,
 757                    else => !std.ascii.isPrint(c),
 758                }) try writer.writeByte('\\');
 759                switch (c) {
 760                    std.ascii.control_code.nul => unreachable,
 761                    std.ascii.control_code.bel => try writer.writeByte('a'),
 762                    std.ascii.control_code.bs => try writer.writeByte('b'),
 763                    std.ascii.control_code.ht => try writer.writeByte('t'),
 764                    std.ascii.control_code.lf => try writer.writeByte('n'),
 765                    std.ascii.control_code.vt => try writer.writeByte('v'),
 766                    std.ascii.control_code.ff => try writer.writeByte('f'),
 767                    std.ascii.control_code.cr => try writer.writeByte('r'),
 768                    std.ascii.control_code.esc => try writer.writeByte('E'),
 769                    ' '...'~' => try writer.writeByte(c),
 770                    else => try writer.print("{o:0>3}", .{c}),
 771                }
 772            }
 773            try writer.writeByte('"');
 774        }
 775    };
 776
 777    var aw: Io.Writer.Allocating = .init(gpa);
 778    defer aw.deinit();
 779    const writer = &aw.writer;
 780    if (opt_cwd) |cwd| writer.print("cd {s} && ", .{cwd}) catch return error.OutOfMemory;
 781    if (opt_env) |env| {
 782        var process_env_map = std.process.getEnvMap(gpa) catch std.process.EnvMap.init(gpa);
 783        defer process_env_map.deinit();
 784        var it = env.iterator();
 785        while (it.next()) |entry| {
 786            const key = entry.key_ptr.*;
 787            const value = entry.value_ptr.*;
 788            if (process_env_map.get(key)) |process_value| {
 789                if (std.mem.eql(u8, value, process_value)) continue;
 790            }
 791            writer.print("{s}=", .{key}) catch return error.OutOfMemory;
 792            shell.escape(writer, value, false) catch return error.OutOfMemory;
 793            writer.writeByte(' ') catch return error.OutOfMemory;
 794        }
 795    }
 796    shell.escape(writer, argv[0], true) catch return error.OutOfMemory;
 797    for (argv[1..]) |arg| {
 798        writer.writeByte(' ') catch return error.OutOfMemory;
 799        shell.escape(writer, arg, false) catch return error.OutOfMemory;
 800    }
 801    return aw.toOwnedSlice();
 802}
 803
 804/// Prefer `cacheHitAndWatch` unless you already added watch inputs
 805/// separately from using the cache system.
 806pub fn cacheHit(s: *Step, man: *Build.Cache.Manifest) !bool {
 807    s.result_cached = man.hit() catch |err| return failWithCacheError(s, man, err);
 808    return s.result_cached;
 809}
 810
 811/// Clears previous watch inputs, if any, and then populates watch inputs from
 812/// the full set of files picked up by the cache manifest.
 813///
 814/// Must be accompanied with `writeManifestAndWatch`.
 815pub fn cacheHitAndWatch(s: *Step, man: *Build.Cache.Manifest) !bool {
 816    const is_hit = man.hit() catch |err| return failWithCacheError(s, man, err);
 817    s.result_cached = is_hit;
 818    // The above call to hit() populates the manifest with files, so in case of
 819    // a hit, we need to populate watch inputs.
 820    if (is_hit) try setWatchInputsFromManifest(s, man);
 821    return is_hit;
 822}
 823
 824fn failWithCacheError(
 825    s: *Step,
 826    man: *const Build.Cache.Manifest,
 827    err: Build.Cache.Manifest.HitError,
 828) error{ OutOfMemory, Canceled, MakeFailed } {
 829    switch (err) {
 830        error.CacheCheckFailed => switch (man.diagnostic) {
 831            .none => unreachable,
 832            .manifest_create, .manifest_read, .manifest_lock => |e| return s.fail("failed to check cache: {t} {t}", .{
 833                man.diagnostic, e,
 834            }),
 835            .file_open, .file_stat, .file_read, .file_hash => |op| {
 836                const pp = man.files.keys()[op.file_index].prefixed_path;
 837                const prefix = man.cache.prefixes()[pp.prefix].path orelse "";
 838                return s.fail("failed to check cache: '{s}{c}{s}' {t} {t}", .{
 839                    prefix, std.fs.path.sep, pp.sub_path, man.diagnostic, op.err,
 840                });
 841            },
 842        },
 843        error.OutOfMemory => return error.OutOfMemory,
 844        error.Canceled => return error.Canceled,
 845        error.InvalidFormat => return s.fail("failed to check cache: invalid manifest file format", .{}),
 846    }
 847}
 848
 849/// Prefer `writeManifestAndWatch` unless you already added watch inputs
 850/// separately from using the cache system.
 851pub fn writeManifest(s: *Step, man: *Build.Cache.Manifest) !void {
 852    if (s.test_results.isSuccess()) {
 853        man.writeManifest() catch |err| {
 854            try s.addError("unable to write cache manifest: {t}", .{err});
 855        };
 856    }
 857}
 858
 859/// Clears previous watch inputs, if any, and then populates watch inputs from
 860/// the full set of files picked up by the cache manifest.
 861///
 862/// Must be accompanied with `cacheHitAndWatch`.
 863pub fn writeManifestAndWatch(s: *Step, man: *Build.Cache.Manifest) !void {
 864    try writeManifest(s, man);
 865    try setWatchInputsFromManifest(s, man);
 866}
 867
 868fn setWatchInputsFromManifest(s: *Step, man: *Build.Cache.Manifest) !void {
 869    const arena = s.owner.allocator;
 870    const prefixes = man.cache.prefixes();
 871    clearWatchInputs(s);
 872    for (man.files.keys()) |file| {
 873        // The file path data is freed when the cache manifest is cleaned up at the end of `make`.
 874        const sub_path = try arena.dupe(u8, file.prefixed_path.sub_path);
 875        try addWatchInputFromPath(s, .{
 876            .root_dir = prefixes[file.prefixed_path.prefix],
 877            .sub_path = std.fs.path.dirname(sub_path) orelse "",
 878        }, std.fs.path.basename(sub_path));
 879    }
 880}
 881
 882/// For steps that have a single input that never changes when re-running `make`.
 883pub fn singleUnchangingWatchInput(step: *Step, lazy_path: Build.LazyPath) Allocator.Error!void {
 884    if (!step.inputs.populated()) try step.addWatchInput(lazy_path);
 885}
 886
 887pub fn clearWatchInputs(step: *Step) void {
 888    const gpa = step.owner.allocator;
 889    step.inputs.clear(gpa);
 890}
 891
 892/// Places a *file* dependency on the path.
 893pub fn addWatchInput(step: *Step, lazy_file: Build.LazyPath) Allocator.Error!void {
 894    switch (lazy_file) {
 895        .src_path => |src_path| try addWatchInputFromBuilder(step, src_path.owner, src_path.sub_path),
 896        .dependency => |d| try addWatchInputFromBuilder(step, d.dependency.builder, d.sub_path),
 897        .cwd_relative => |path_string| {
 898            try addWatchInputFromPath(step, .{
 899                .root_dir = .{
 900                    .path = null,
 901                    .handle = std.fs.cwd(),
 902                },
 903                .sub_path = std.fs.path.dirname(path_string) orelse "",
 904            }, std.fs.path.basename(path_string));
 905        },
 906        // Nothing to watch because this dependency edge is modeled instead via `dependants`.
 907        .generated => {},
 908    }
 909}
 910
 911/// Any changes inside the directory will trigger invalidation.
 912///
 913/// See also `addDirectoryWatchInputFromPath` which takes a `Build.Cache.Path` instead.
 914///
 915/// Paths derived from this directory should also be manually added via
 916/// `addDirectoryWatchInputFromPath` if and only if this function returns
 917/// `true`.
 918pub fn addDirectoryWatchInput(step: *Step, lazy_directory: Build.LazyPath) Allocator.Error!bool {
 919    switch (lazy_directory) {
 920        .src_path => |src_path| try addDirectoryWatchInputFromBuilder(step, src_path.owner, src_path.sub_path),
 921        .dependency => |d| try addDirectoryWatchInputFromBuilder(step, d.dependency.builder, d.sub_path),
 922        .cwd_relative => |path_string| {
 923            try addDirectoryWatchInputFromPath(step, .{
 924                .root_dir = .{
 925                    .path = null,
 926                    .handle = std.fs.cwd(),
 927                },
 928                .sub_path = path_string,
 929            });
 930        },
 931        // Nothing to watch because this dependency edge is modeled instead via `dependants`.
 932        .generated => return false,
 933    }
 934    return true;
 935}
 936
 937/// Any changes inside the directory will trigger invalidation.
 938///
 939/// See also `addDirectoryWatchInput` which takes a `Build.LazyPath` instead.
 940///
 941/// This function should only be called when it has been verified that the
 942/// dependency on `path` is not already accounted for by a `Step` dependency.
 943/// In other words, before calling this function, first check that the
 944/// `Build.LazyPath` which this `path` is derived from is not `generated`.
 945pub fn addDirectoryWatchInputFromPath(step: *Step, path: Build.Cache.Path) !void {
 946    return addWatchInputFromPath(step, path, ".");
 947}
 948
 949fn addWatchInputFromBuilder(step: *Step, builder: *Build, sub_path: []const u8) !void {
 950    return addWatchInputFromPath(step, .{
 951        .root_dir = builder.build_root,
 952        .sub_path = std.fs.path.dirname(sub_path) orelse "",
 953    }, std.fs.path.basename(sub_path));
 954}
 955
 956fn addDirectoryWatchInputFromBuilder(step: *Step, builder: *Build, sub_path: []const u8) !void {
 957    return addDirectoryWatchInputFromPath(step, .{
 958        .root_dir = builder.build_root,
 959        .sub_path = sub_path,
 960    });
 961}
 962
 963fn addWatchInputFromPath(step: *Step, path: Build.Cache.Path, basename: []const u8) !void {
 964    const gpa = step.owner.allocator;
 965    const gop = try step.inputs.table.getOrPut(gpa, path);
 966    if (!gop.found_existing) gop.value_ptr.* = .empty;
 967    try gop.value_ptr.append(gpa, basename);
 968}
 969
 970/// Implementation detail of file watching and forced rebuilds. Prepares the step for being re-evaluated.
 971pub fn reset(step: *Step, gpa: Allocator) void {
 972    assert(step.state == .precheck_done);
 973
 974    if (step.result_failed_command) |cmd| gpa.free(cmd);
 975
 976    step.result_error_msgs.clearRetainingCapacity();
 977    step.result_stderr = "";
 978    step.result_cached = false;
 979    step.result_duration_ns = null;
 980    step.result_peak_rss = 0;
 981    step.result_failed_command = null;
 982    step.test_results = .{};
 983
 984    step.result_error_bundle.deinit(gpa);
 985    step.result_error_bundle = std.zig.ErrorBundle.empty;
 986}
 987
 988/// Implementation detail of file watching. Prepares the step for being re-evaluated.
 989pub fn recursiveReset(step: *Step, gpa: Allocator) void {
 990    assert(step.state != .precheck_done);
 991    step.state = .precheck_done;
 992    step.reset(gpa);
 993    for (step.dependants.items) |dep| {
 994        if (dep.state == .precheck_done) continue;
 995        dep.recursiveReset(gpa);
 996    }
 997}
 998
 999test {
1000    _ = CheckFile;
1001    _ = CheckObject;
1002    _ = Fail;
1003    _ = Fmt;
1004    _ = InstallArtifact;
1005    _ = InstallDir;
1006    _ = InstallFile;
1007    _ = ObjCopy;
1008    _ = Compile;
1009    _ = Options;
1010    _ = RemoveDir;
1011    _ = Run;
1012    _ = TranslateC;
1013    _ = WriteFile;
1014    _ = UpdateSourceFiles;
1015}