1//! The *mutable* state that `Maker` needs in order to process one node from
2//! the build graph.
3const Step = @This();
4
5const builtin = @import("builtin");
6
7const std = @import("std");
8const Allocator = std.mem.Allocator;
9const Cache = std.Build.Cache;
10const Io = std.Io;
11const Dir = std.Io.Dir;
12const LazyPath = std.Build.Configuration.LazyPath;
13const Package = std.Build.Configuration.Package;
14const Path = std.Build.Cache.Path;
15const Configuration = std.Build.Configuration;
16const assert = std.debug.assert;
17
18const WebServer = @import("WebServer.zig");
19const Maker = @import("../Maker.zig");
20
21pub const CheckFile = @import("Step/CheckFile.zig");
22pub const Compile = @import("Step/Compile.zig");
23pub const ConfigHeader = @import("Step/ConfigHeader.zig");
24pub const FindProgram = @import("Step/FindProgram.zig");
25pub const Fmt = @import("Step/Fmt.zig");
26pub const InstallArtifact = @import("Step/InstallArtifact.zig");
27pub const InstallDir = @import("Step/InstallDir.zig");
28pub const InstallFile = @import("Step/InstallFile.zig");
29pub const ObjCopy = @import("Step/ObjCopy.zig");
30pub const Options = @import("Step/Options.zig");
31pub const Run = @import("Step/Run.zig");
32pub const TranslateC = @import("Step/TranslateC.zig");
33pub const UpdateSourceFiles = @import("Step/UpdateSourceFiles.zig");
34pub const WriteFile = @import("Step/WriteFile.zig");
35
36/// Avoid false sharing.
37_: void align(std.atomic.cache_line) = {},
38
39/// Extra data for specific types of steps.
40extended: Extended,
41
42/// This field is atomically accessed multi-threaded.
43state: State = .precheck_unstarted,
44
45dependants: std.ArrayList(Configuration.Step.Index) = .empty,
46/// Collects the set of files that retrigger this step to run.
47///
48/// This is used by the build system's implementation of `--watch` but it can
49/// also be potentially useful for IDEs to know what effects editing a
50/// particular file has.
51///
52/// Populated within `make`. Implementation may choose to clear and repopulate,
53/// retain previous value, or update.
54inputs: Inputs = .init,
55pending_deps: u32 = undefined,
56
57/// Array list and internal memory owned by process arena.
58result_error_msgs: std.ArrayList([]const u8) = .empty,
59result_error_bundle: std.zig.ErrorBundle = .empty,
60/// Owned by `Maker.gpa`.
61result_stderr: []const u8 = "",
62result_cached: bool = false,
63/// Indicates error information is missing due to allocation failure.
64result_oom: bool = false,
65result_duration_ns: ?u64 = null,
66/// 0 means unavailable or not reported.
67result_peak_rss: usize = 0,
68/// If the step is failed and this field is populated, this is the command which failed.
69/// This field may be populated even if the step succeeded.
70/// Memory owned by `Maker.gpa`.
71result_failed_command: ?[]const u8 = null,
72test_results: TestResults = .{},
73
74comptime {
75 // Common cache line size is 128. This check prevents accidentally crossing
76 // an additional cache line. In the future it might be nice to try to fit
77 // this struct in 128 bytes or less.
78 if (std.atomic.cache_line <= 128) assert(@sizeOf(@This()) <= 128 * 3);
79}
80
81pub const Extended = union(enum) {
82 check_file: CheckFile,
83 compile: Compile,
84 config_header: ConfigHeader,
85 fail: Fail,
86 find_program: FindProgram,
87 fmt: Fmt,
88 install_artifact: InstallArtifact,
89 install_dir: InstallDir,
90 install_file: InstallFile,
91 obj_copy: ObjCopy,
92 options: Options,
93 run: Run,
94 top_level: TopLevel,
95 translate_c: TranslateC,
96 update_source_files: UpdateSourceFiles,
97 write_file: WriteFile,
98
99 pub fn init(tag: Configuration.Step.Tag) Extended {
100 return switch (tag) {
101 .check_file => .{ .check_file = .{} },
102 .compile => .{ .compile = .{} },
103 .config_header => .{ .config_header = .{} },
104 .fail => .{ .fail = .{} },
105 .find_program => .{ .find_program = .{} },
106 .fmt => .{ .fmt = .{} },
107 .install_artifact => .{ .install_artifact = .{} },
108 .install_dir => .{ .install_dir = .{} },
109 .install_file => .{ .install_file = .{} },
110 .obj_copy => .{ .obj_copy = .{} },
111 .options => .{ .options = .{} },
112 .run => .{ .run = .{} },
113 .top_level => .{ .top_level = .{} },
114 .translate_c => .{ .translate_c = .{} },
115 .update_source_files => .{ .update_source_files = .{} },
116 .write_file => .{ .write_file = .{} },
117 };
118 }
119
120 pub const TopLevel = struct {
121 pub fn make(
122 top_level: *TopLevel,
123 step_index: Configuration.Step.Index,
124 maker: *Maker,
125 progress_node: std.Progress.Node,
126 ) Step.ExtendedMakeError!void {
127 _ = top_level;
128 _ = step_index;
129 _ = maker;
130 _ = progress_node;
131 }
132 };
133
134 pub const Fail = struct {
135 pub fn make(
136 this: *@This(),
137 step_index: Configuration.Step.Index,
138 maker: *Maker,
139 progress_node: std.Progress.Node,
140 ) Step.ExtendedMakeError!void {
141 _ = this;
142 _ = progress_node;
143 const graph = maker.graph;
144 const arena = graph.arena; // TODO don't leak into the process arena
145 const conf = &maker.scanned_config.configuration;
146 const step = maker.stepByIndex(step_index);
147 const conf_step = step_index.ptr(conf);
148 const conf_fail = conf_step.extended.get(conf.extra).fail;
149
150 try step.result_error_msgs.append(arena, conf_fail.msg.slice(conf));
151 return error.MakeFailed;
152 }
153 };
154};
155
156pub const State = enum {
157 precheck_unstarted,
158 precheck_started,
159 /// This is also used to indicate "dirty" steps that have been modified
160 /// after a previous build completed, in which case, the step may or may
161 /// not have been completed before. Either way, one or more of its direct
162 /// file system inputs have been modified, meaning that the step needs to
163 /// be re-evaluated.
164 precheck_done,
165 dependency_failure,
166 /// Handled exactly the same as `dependency_failure` except communicates
167 /// that the dependency didn't fail but rather was skipped.
168 dependency_skipped,
169 success,
170 failure,
171 /// This state indicates that the step did not complete, however, it also did not fail,
172 /// and it is safe to continue executing its dependencies.
173 skipped,
174 /// This step was skipped because it specified a max_rss that exceeded the runner's maximum.
175 /// It is not safe to run its dependencies.
176 skipped_oom,
177};
178
179pub const Inputs = struct {
180 table: Table,
181
182 pub const init: Inputs = .{
183 .table = .{},
184 };
185
186 pub const Table = std.array_hash_map.Custom(Path, Files, Path.TableAdapter, false);
187 /// The special file name "." means any changes inside the directory.
188 pub const Files = std.ArrayList([]const u8);
189
190 pub fn populated(inputs: *Inputs) bool {
191 return inputs.table.count() != 0;
192 }
193
194 pub fn clear(inputs: *Inputs, gpa: Allocator) void {
195 for (inputs.table.values()) |*files| files.deinit(gpa);
196 inputs.table.clearRetainingCapacity();
197 }
198
199 pub fn deinit(inputs: *Inputs, gpa: Allocator) void {
200 clear(inputs, gpa);
201 inputs.table.deinit(gpa);
202 }
203};
204
205pub const TestResults = struct {
206 /// The total number of tests in the step. Every test has a "status" from the following:
207 /// * passed
208 /// * skipped
209 /// * failed cleanly
210 /// * crashed
211 /// * timed out
212 test_count: u32 = 0,
213
214 /// The number of tests which were skipped (`error.SkipZigTest`).
215 skip_count: u32 = 0,
216 /// The number of tests which failed cleanly.
217 fail_count: u32 = 0,
218 /// The number of tests which terminated unexpectedly, i.e. crashed.
219 crash_count: u32 = 0,
220 /// The number of tests which timed out.
221 timeout_count: u32 = 0,
222
223 /// The number of detected memory leaks. The associated test may still have passed; indeed, *all*
224 /// individual tests may have passed. However, the step as a whole fails if any test has leaks.
225 leak_count: u32 = 0,
226 /// The number of detected error logs. The associated test may still have passed; indeed, *all*
227 /// individual tests may have passed. However, the step as a whole fails if any test logs errors.
228 log_err_count: u32 = 0,
229
230 pub fn isSuccess(tr: TestResults) bool {
231 // all steps are success or skip
232 return tr.fail_count == 0 and
233 tr.crash_count == 0 and
234 tr.timeout_count == 0 and
235 // no (otherwise successful) step leaked memory or logged errors
236 tr.leak_count == 0 and
237 tr.log_err_count == 0;
238 }
239
240 /// Computes the number of tests which passed from the other values.
241 pub fn passCount(tr: TestResults) u32 {
242 return tr.test_count - tr.skip_count - tr.fail_count - tr.crash_count - tr.timeout_count;
243 }
244};
245
246pub const MakeError = error{
247 /// Indicates the error is already reported.
248 MakeFailed,
249 MakeSkipped,
250} || Io.Cancelable;
251
252pub const ExtendedMakeError = MakeError || Allocator.Error;
253
254pub fn make(
255 step_index: Configuration.Step.Index,
256 maker: *Maker,
257 progress_node: std.Progress.Node,
258) MakeError!void {
259 const graph = maker.graph;
260 const arena = graph.arena; // TODO don't leak into the process arena
261 const io = graph.io;
262 const c = &maker.scanned_config.configuration;
263 const conf_step = step_index.ptr(c);
264 const s = maker.stepByIndex(step_index);
265
266 var start_ts: ?Io.Timestamp = t: {
267 if (!graph.time_report) break :t null;
268 const flags = conf_step.flags(c);
269 switch (flags.tag) {
270 .compile => break :t null,
271 .run => {
272 const run_flags: Configuration.Step.Run.Flags = @bitCast(flags);
273 if (run_flags.stdio == .zig_test) break :t null;
274 },
275 else => {},
276 }
277 break :t Io.Clock.awake.now(io);
278 };
279 const make_result = switch (s.extended) {
280 inline else => |*extended| extended.make(step_index, maker, progress_node),
281 };
282 if (start_ts) |*ts| {
283 const duration = ts.untilNow(io, .awake);
284 maker.web_server.?.updateTimeReportGeneric(step_index, duration);
285 }
286
287 make_result catch |err| switch (err) {
288 error.MakeFailed, error.MakeSkipped => |e| return e,
289 error.OutOfMemory => {
290 s.result_oom = true;
291 return error.MakeFailed;
292 },
293 error.Canceled => |e| return e,
294 };
295
296 if (!s.test_results.isSuccess()) {
297 return error.MakeFailed;
298 }
299
300 const max_rss = conf_step.max_rss.toBytes();
301 if (max_rss != 0 and s.result_peak_rss > max_rss) {
302 if (std.fmt.allocPrint(
303 arena,
304 "memory usage peaked at {0B:.2} ({0d} bytes), exceeding the declared upper bound of {1B:.2} ({1d} bytes)",
305 .{ s.result_peak_rss, max_rss },
306 )) |msg| {
307 s.oomWrap(s.result_error_msgs.append(arena, msg));
308 } else |_| s.result_oom = true;
309 }
310}
311
312/// Prepares the step for being re-evaluated.
313pub fn reset(step: *Step, maker: *Maker) void {
314 assert(step.state == .precheck_done);
315 const gpa = maker.gpa;
316
317 clearFailedCommand(step, gpa);
318 clearResultStderr(step, gpa);
319 step.result_error_msgs.clearRetainingCapacity();
320 step.result_cached = false;
321 step.result_duration_ns = null;
322 step.result_peak_rss = 0;
323 step.test_results = .{};
324 clearWatchInputs(step, maker);
325 clearErrorBundle(step, gpa);
326}
327
328pub const CaptureChildProcessError = error{
329 FileNotFound,
330} || ExtendedMakeError;
331
332pub const CaptureChildProcessOptions = struct {
333 argv: []const []const u8,
334 progress_node: std.Progress.Node = .none,
335 environ_map: ?*const std.process.Environ.Map = null,
336 allow_failure: bool = false,
337};
338
339/// Populates `s.result_failed_command` unconditionally.
340pub fn captureChildProcess(
341 s: *Step,
342 maker: *Maker,
343 allocator: Allocator,
344 options: CaptureChildProcessOptions,
345) !std.process.RunResult {
346 const gpa = maker.gpa;
347 const graph = maker.graph;
348 const io = graph.io;
349
350 s.setFailedCommand(gpa, options.argv, .{});
351
352 try handleChildProcUnsupported(s, maker);
353 try graph.handleVerbose(null, null, options.argv);
354
355 const result = std.process.run(allocator, io, .{
356 .argv = options.argv,
357 .environ_map = options.environ_map orelse &graph.environ_map,
358 .progress_node = options.progress_node,
359 }) catch |err| {
360 switch (err) {
361 error.OutOfMemory, error.Canceled => |e| return e,
362 error.FileNotFound => |e| if (options.allow_failure) return e,
363 else => {},
364 }
365 return s.fail(maker, "failed to run {s}: {t}", .{ options.argv[0], err });
366 };
367
368 if (result.stderr.len > 0) try s.result_error_msgs.append(graph.arena, result.stderr);
369
370 return result;
371}
372
373pub fn clearErrorBundle(s: *Step, gpa: Allocator) void {
374 s.result_error_bundle.deinit(gpa);
375 s.result_error_bundle = .empty;
376}
377
378pub fn clearFailedCommand(s: *Step, gpa: Allocator) void {
379 if (s.result_failed_command) |cmd| {
380 gpa.free(cmd);
381 s.result_failed_command = null;
382 }
383}
384
385pub fn setFailedCommand(
386 s: *Step,
387 gpa: Allocator,
388 argv: []const []const u8,
389 options: std.zig.AllocPrintCmdOptions,
390) void {
391 s.clearFailedCommand(gpa);
392 s.result_failed_command = std.zig.allocPrintCmd(gpa, argv, options) catch |err| switch (err) {
393 error.OutOfMemory => {
394 s.result_oom = true;
395 return;
396 },
397 };
398}
399
400pub const FailError = error{ OutOfMemory, MakeFailed };
401
402pub fn fail(step: *Step, maker: *const Maker, comptime fmt: []const u8, args: anytype) FailError {
403 try step.addError(maker, fmt, args);
404 return error.MakeFailed;
405}
406
407pub fn addError(step: *Step, maker: *const Maker, comptime fmt: []const u8, args: anytype) error{OutOfMemory}!void {
408 const graph = maker.graph;
409 const arena = graph.arena; // TODO don't leak into the process_arena
410 const msg = try std.fmt.allocPrint(arena, fmt, args);
411 try step.result_error_msgs.append(arena, msg);
412}
413
414pub const ZigProcess = struct {
415 child: std.process.Child,
416 multi_reader_buffer: Io.File.MultiReader.Buffer(2),
417 multi_reader: Io.File.MultiReader,
418 progress_ipc_index: ?if (std.Progress.have_ipc) std.Progress.Ipc.Index else noreturn,
419
420 pub const StreamEnum = enum { stdout, stderr };
421
422 pub fn saveState(zp: *ZigProcess, prog_node: std.Progress.Node) void {
423 zp.progress_ipc_index = if (std.Progress.have_ipc) prog_node.takeIpcIndex() else null;
424 }
425
426 pub fn deinit(zp: *ZigProcess, io: Io) void {
427 zp.child.kill(io);
428 zp.multi_reader.deinit();
429 zp.* = undefined;
430 }
431};
432
433/// Assumes that argv contains `--listen=-` and that the process being spawned
434/// is the zig compiler - the same version that compiled the build runner.
435///
436/// Populates `s.result_failed_command` on failure.
437pub fn evalZigProcess(
438 step_index: Configuration.Step.Index,
439 maker: *Maker,
440 argv: []const []const u8,
441 prog_node: std.Progress.Node,
442 watch: bool,
443) (Step.ExtendedMakeError || error{NeedCompileErrorCheck})!?Path {
444 const s = maker.stepByIndex(step_index);
445 const gpa = maker.gpa;
446 const graph = maker.graph;
447 const io = graph.io;
448
449 // If an error occurs, it's happened in this command:
450 errdefer s.setFailedCommand(gpa, argv, .{});
451
452 if (s.getZigProcess()) |zp| update: {
453 assert(watch);
454 if (zp.progress_ipc_index) |ipc_index| prog_node.setIpcIndex(ipc_index);
455 zp.progress_ipc_index = null;
456 var exited = false;
457 defer if (exited) {
458 s.extended.compile.zig_process = null;
459 zp.deinit(io);
460 gpa.destroy(zp);
461 } else zp.saveState(prog_node);
462 const result = zigProcessUpdate(step_index, maker, zp, watch) catch |err| switch (err) {
463 error.BrokenPipe, error.EndOfStream => |reason| {
464 // Process restart required.
465 std.log.info("{s} restart required: {t}", .{ argv[0], reason });
466 _ = zp.child.wait(io) catch |e| return s.fail(maker, "unable to wait for {s}: {t}", .{ argv[0], e });
467 exited = true;
468 break :update;
469 },
470 error.OutOfMemory, error.Canceled, error.MakeFailed => |e| return e,
471 else => |e| return s.fail(maker, "zig child process monitoring failed: {t}", .{e}),
472 };
473
474 if (s.result_error_bundle.errorMessageCount() > 0)
475 return s.fail(maker, "{d} compilation errors", .{s.result_error_bundle.errorMessageCount()});
476
477 if (s.result_error_msgs.items.len > 0 and result == null) {
478 // Crash detected.
479 const term = zp.child.wait(io) catch |e| {
480 return s.fail(maker, "unable to wait for {s}: {t}", .{ argv[0], e });
481 };
482 s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0;
483 exited = true;
484 try handleChildProcessTerm(s, maker, term);
485 return error.MakeFailed;
486 }
487
488 return result;
489 }
490 assert(argv.len != 0);
491
492 try handleChildProcUnsupported(s, maker);
493 try graph.handleVerbose(null, null, argv);
494
495 const zp = try gpa.create(ZigProcess);
496 defer if (!watch) gpa.destroy(zp);
497
498 zp.child = std.process.spawn(io, .{
499 .argv = argv,
500 .environ_map = &graph.environ_map,
501 .stdin = .pipe,
502 .stdout = .pipe,
503 .stderr = .pipe,
504 .request_resource_usage_statistics = true,
505 .progress_node = prog_node,
506 }) catch |err| return s.fail(maker, "failed to spawn zig compiler {s}: {t}", .{ argv[0], err });
507
508 zp.multi_reader.init(gpa, io, zp.multi_reader_buffer.toStreams(), &.{
509 zp.child.stdout.?, zp.child.stderr.?,
510 });
511 if (watch) s.extended.compile.zig_process = zp;
512 defer if (!watch) zp.deinit(io);
513
514 const result = result: {
515 defer if (watch) zp.saveState(prog_node);
516 break :result zigProcessUpdate(step_index, maker, zp, watch) catch |err| switch (err) {
517 error.OutOfMemory, error.Canceled, error.MakeFailed => |e| return e,
518 else => |e| return s.fail(maker, "zig child process monitoring failed: {t}", .{e}),
519 };
520 };
521
522 if (!watch) {
523 // Send EOF to stdin.
524 zp.child.stdin.?.close(io);
525 zp.child.stdin = null;
526
527 const term = zp.child.wait(io) catch |err| {
528 return s.fail(maker, "unable to wait for {s}: {t}", .{ argv[0], err });
529 };
530 s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0;
531
532 // Special handling for compile step that is expecting compile errors.
533 const conf = &maker.scanned_config.configuration;
534 if (term == .exited) switch (step_index.ptr(conf).extended.get(conf.extra)) {
535 .compile => |compile| if (compile.flags4.expect_errors != .none) {
536 // Note that the exit code may be 0 in this case due to the
537 // compiler server protocol.
538 return error.NeedCompileErrorCheck;
539 },
540 else => {},
541 };
542 try handleChildProcessTerm(s, maker, term);
543 }
544
545 if (s.result_error_bundle.errorMessageCount() > 0) {
546 return s.fail(maker, "{d} compilation errors", .{s.result_error_bundle.errorMessageCount()});
547 }
548
549 return result;
550}
551
552fn zigProcessUpdate(step_index: Configuration.Step.Index, maker: *Maker, zp: *ZigProcess, watch: bool) !?Path {
553 const s = maker.stepByIndex(step_index);
554 const gpa = maker.gpa;
555 const graph = maker.graph;
556 const arena = graph.arena; // TODO don't leak into the process arena
557 const io = graph.io;
558
559 const start_ts = Io.Clock.awake.now(io);
560
561 try sendMessage(io, zp.child.stdin.?, .update);
562 if (!watch) try sendMessage(io, zp.child.stdin.?, .exit);
563
564 var result: ?Path = null;
565 var eos_err: error{EndOfStream}!void = {};
566
567 var client: std.zig.Client = .{
568 .in = zp.multi_reader.reader(0),
569 .out = undefined,
570 };
571
572 while (true) {
573 const header = client.receiveMessageWithMultiReader(&zp.multi_reader, .none) catch |err| switch (err) {
574 error.Timeout => unreachable,
575 error.EndOfStream => |e| {
576 if (client.in.bufferedLen() == 0) break;
577 // Better to report the crash with stderr below, but we set
578 // this in case the child exits successfully while violating
579 // this protocol.
580 eos_err = e;
581 break;
582 },
583 else => |e| return e,
584 };
585 const body = client.in.take(header.bytes_len) catch unreachable;
586
587 switch (header.tag) {
588 .zig_version => {
589 if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
590 return s.fail(
591 maker,
592 "zig version mismatch build runner vs compiler: {q} vs {q}",
593 .{ builtin.zig_version_string, body },
594 );
595 }
596 },
597 .error_bundle => {
598 s.result_error_bundle = try std.zig.Server.allocErrorBundle(gpa, body);
599 // This message indicates the end of the update.
600 if (watch) break;
601 },
602 .emit_digest => {
603 const EmitDigest = std.zig.Server.Message.EmitDigest;
604 const emit_digest: *align(1) const EmitDigest = @ptrCast(body);
605 s.result_cached = emit_digest.flags.cache_hit;
606 const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
607 result = .{
608 .root_dir = graph.local_cache_root,
609 .sub_path = try arena.dupe(u8, "o" ++ Dir.path.sep_str ++ Cache.binToHex(digest.*)),
610 };
611 },
612 .file_system_inputs => {
613 clearWatchInputs(s, maker);
614 const conf = &maker.scanned_config.configuration;
615 const conf_step = step_index.ptr(conf);
616 var it = std.mem.splitScalar(u8, body, 0);
617 while (it.next()) |prefixed_path| {
618 const prefix_index: std.zig.Server.Message.PathPrefix = @fromBackingInt(@intCast(prefixed_path[0] - 1));
619 const sub_path = try arena.dupe(u8, prefixed_path[1..]);
620 const sub_path_dirname = Dir.path.dirname(sub_path) orelse "";
621 switch (prefix_index) {
622 .cwd => {
623 const path: Path = .{
624 .root_dir = .cwd(),
625 .sub_path = sub_path_dirname,
626 };
627 try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path));
628 },
629 .zig_lib => zl: {
630 switch (conf_step.extended.get(conf.extra)) {
631 .compile => |compile| if (compile.zig_lib_dir.value) |zig_lib_dir| {
632 const resolved = try maker.resolveLazyPathIndex(arena, zig_lib_dir, step_index);
633 const appended = try resolved.join(arena, sub_path);
634 try addWatchInputPath(s, maker, appended);
635 break :zl;
636 },
637 else => {},
638 }
639 const path: Path = .{
640 .root_dir = graph.zig_lib_directory,
641 .sub_path = sub_path_dirname,
642 };
643 try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path));
644 },
645 .local_cache => {
646 const path: Path = .{
647 .root_dir = graph.local_cache_root,
648 .sub_path = sub_path_dirname,
649 };
650 try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path));
651 },
652 .global_cache => {
653 const path: Path = .{
654 .root_dir = graph.global_cache_root,
655 .sub_path = sub_path_dirname,
656 };
657 try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path));
658 },
659 .build_root => {
660 const path: Path = .{
661 .root_dir = graph.build_root_directory,
662 .sub_path = sub_path_dirname,
663 };
664 try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path));
665 },
666 }
667 }
668 },
669 .time_report => if (maker.web_server) |ws| {
670 const TimeReport = std.zig.Server.Message.TimeReport;
671 const tr: *align(1) const TimeReport = @ptrCast(body[0..@sizeOf(TimeReport)]);
672 ws.updateTimeReportCompile(.{
673 .compile_step = step_index,
674 .use_llvm = tr.flags.use_llvm,
675 .stats = tr.stats,
676 .ns_total = @intCast(start_ts.untilNow(io, .awake).toNanoseconds()),
677 .llvm_pass_timings_len = tr.llvm_pass_timings_len,
678 .files_len = tr.files_len,
679 .decls_len = tr.decls_len,
680 .trailing = body[@sizeOf(TimeReport)..],
681 });
682 },
683 else => {}, // ignore other messages
684 }
685 }
686
687 s.result_duration_ns = @intCast(start_ts.untilNow(io, .awake).toNanoseconds());
688
689 const stderr_contents = zp.multi_reader.reader(1).buffered();
690 if (stderr_contents.len > 0) {
691 try s.result_error_msgs.append(arena, try arena.dupe(u8, stderr_contents));
692 }
693
694 try eos_err;
695
696 return result;
697}
698
699pub fn getZigProcess(s: *Step) ?*ZigProcess {
700 return switch (s.extended) {
701 .compile => |*compile| compile.zig_process,
702 else => null,
703 };
704}
705
706fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void {
707 const header: std.zig.Client.Message.Header = .{
708 .tag = tag,
709 .bytes_len = 0,
710 };
711 var w = file.writer(io, &.{});
712 w.interface.writeStruct(header, .little) catch |err| switch (err) {
713 error.WriteFailed => return w.err.?,
714 };
715}
716
717pub inline fn handleChildProcUnsupported(s: *Step, maker: *Maker) FailError!void {
718 if (!std.process.can_spawn)
719 return s.fail(maker, "unable to spawn process: host cannot spawn child processes", .{});
720}
721
722pub fn handleChildProcessTerm(s: *Step, maker: *Maker, term: std.process.Child.Term) FailError!void {
723 if (!term.success()) return s.fail(maker, "process {f}", .{term});
724}
725
726/// Prefer `cacheHitWatched` unless you already added watch inputs
727/// separately from using the cache system.
728pub fn cacheHit(s: *Step, maker: *Maker, man: *Cache.Manifest, parent_progress_node: std.Progress.Node) !bool {
729 s.result_cached = man.hit(parent_progress_node) catch |err| return failWithCacheError(s, maker, man, err);
730 return s.result_cached;
731}
732
733/// Clears previous watch inputs, if any, and then populates watch inputs from
734/// the full set of files picked up by the cache manifest.
735///
736/// Must be accompanied with `writeManifestAndWatch`.
737pub fn cacheHitWatched(s: *Step, maker: *Maker, man: *Cache.Manifest, parent_progress_node: std.Progress.Node) !bool {
738 const is_hit = man.hit(parent_progress_node) catch |err| return failWithCacheError(s, maker, man, err);
739 s.result_cached = is_hit;
740 // The above call to hit() populates the manifest with files, so in case of
741 // a hit, we need to populate watch inputs.
742 if (is_hit) try setWatchInputsFromManifest(s, maker, man);
743 return is_hit;
744}
745
746fn failWithCacheError(
747 s: *Step,
748 maker: *Maker,
749 man: *const Cache.Manifest,
750 err: Cache.Manifest.HitError,
751) error{ OutOfMemory, Canceled, MakeFailed } {
752 switch (err) {
753 error.CacheCheckFailed => switch (man.diagnostic) {
754 .none => unreachable,
755 .manifest_create, .manifest_read, .manifest_lock => |e| return s.fail(maker, "failed checking cache: {t} {t}", .{
756 man.diagnostic, e,
757 }),
758 .file_open, .file_stat, .file_read, .file_hash => |op| {
759 const pp = man.files.keys()[op.file_index].prefixed_path;
760 const prefix = man.cache.prefixes()[pp.prefix].path orelse "";
761 return s.fail(maker, "failed checking cache: {s}{c}{s} {t} {t}", .{
762 prefix, Dir.path.sep, pp.sub_path, man.diagnostic, op.err,
763 });
764 },
765 },
766 error.OutOfMemory, error.Canceled => |e| return e,
767 error.InvalidFormat => return s.fail(maker, "failed checking cache: invalid manifest file format", .{}),
768 }
769}
770
771/// Prefer `writeManifestAndWatch` unless you already added watch inputs
772/// separately from using the cache system.
773pub fn writeManifest(s: *Step, maker: *Maker, man: *Cache.Manifest) !void {
774 if (s.test_results.isSuccess()) {
775 man.writeManifest() catch |err| switch (err) {
776 error.Canceled => |e| return e,
777 else => |e| try s.addError(maker, "failed writing cache manifest: {t}", .{e}),
778 };
779 }
780}
781
782/// Clears previous watch inputs, if any, and then populates watch inputs from
783/// the full set of files picked up by the cache manifest.
784///
785/// Must be accompanied with `cacheHitWatched`.
786pub fn writeManifestAndWatch(s: *Step, maker: *Maker, man: *Cache.Manifest) !void {
787 try writeManifest(s, maker, man);
788 try setWatchInputsFromManifest(s, maker, man);
789}
790
791pub fn setWatchInputsFromManifest(s: *Step, maker: *Maker, man: *Cache.Manifest) !void {
792 return setWatchInputsFromManifestFiles(s, maker, &man.files, man.cache.prefixes());
793}
794
795pub fn setWatchInputsFromManifestFiles(
796 s: *Step,
797 maker: *Maker,
798 files: *const Cache.Manifest.Files,
799 prefixes: []const Cache.Directory,
800) !void {
801 const graph = maker.graph;
802 const arena = graph.arena; // TODO don't leak into process arena
803 clearWatchInputs(s, maker);
804 for (files.keys()) |file| {
805 // The file path data is freed when the cache manifest is cleaned up at the end of `make`.
806 const sub_path = try arena.dupe(u8, file.prefixed_path.sub_path);
807 try addWatchInputFromPath(s, maker, .{
808 .root_dir = prefixes[file.prefixed_path.prefix],
809 .sub_path = Dir.path.dirname(sub_path) orelse "",
810 }, Dir.path.basename(sub_path));
811 }
812}
813
814/// For steps that have a single input that never changes when re-running `make`.
815pub fn singleUnchangingWatchInput(step: *Step, maker: *Maker, arena: Allocator, lazy_path: LazyPath) Allocator.Error!void {
816 if (!step.inputs.populated()) try step.addWatchInput(maker, arena, lazy_path);
817}
818
819pub fn clearWatchInputs(step: *Step, maker: *Maker) void {
820 step.inputs.clear(maker.gpa);
821}
822
823/// Places a *file* dependency on the path.
824pub fn addWatchInput(step: *Step, maker: *Maker, arena: Allocator, lazy_file: LazyPath) Allocator.Error!void {
825 const conf = &maker.scanned_config.configuration;
826 switch (lazy_file) {
827 .source_path => |source_path| {
828 const sub_path = source_path.sub_path.slice(conf);
829 const pkg_path = try maker.packagePath(arena, source_path.owner, sub_path);
830 try addWatchInputPath(step, maker, pkg_path);
831 },
832 .relative => |relative| {
833 const resolved_path = try maker.relativePath(arena, relative);
834 try addWatchInputPath(step, maker, resolved_path);
835 },
836 // Nothing to watch because this dependency edge is modeled instead via `dependants`.
837 .generated => {},
838 }
839}
840
841/// Any changes inside the directory will trigger invalidation.
842///
843/// See also `addDirectoryWatchInputFromPath` which takes a `Path` instead.
844///
845/// Paths derived from this directory should also be manually added via
846/// `addDirectoryWatchInputFromPath` if and only if this function returns
847/// `true`.
848pub fn addDirectoryWatchInput(step: *Step, maker: *Maker, lazy_directory: LazyPath) Allocator.Error!bool {
849 const graph = maker.graph;
850 const arena = graph.arena; // TODO don't leak into the process arena
851 switch (lazy_directory) {
852 .source_path => |source_path| {
853 const conf = &maker.scanned_config.configuration;
854 const sub_path = source_path.sub_path.slice(conf);
855 const pkg_path = try maker.packagePath(arena, source_path.owner, sub_path);
856 try addDirectoryWatchInputFromPath(step, maker, pkg_path);
857 },
858 .relative => |relative| {
859 const resolved_path = try maker.relativePath(arena, relative);
860 try addDirectoryWatchInputFromPath(step, maker, resolved_path);
861 },
862 // Nothing to watch because this dependency edge is modeled instead via `dependants`.
863 .generated => return false,
864 }
865 return true;
866}
867
868/// Any changes inside the directory will trigger invalidation.
869///
870/// See also `addDirectoryWatchInput` which takes a `LazyPath` instead.
871///
872/// This function should only be called when it has been verified that the
873/// dependency on `path` is not already accounted for by a `Step` dependency.
874/// In other words, before calling this function, first check that the
875/// `LazyPath` which this `path` is derived from is not `generated`.
876pub fn addDirectoryWatchInputFromPath(step: *Step, maker: *Maker, path: Path) !void {
877 return addWatchInputFromPath(step, maker, path, ".");
878}
879
880fn addWatchInputPath(step: *Step, maker: *Maker, path: Path) Allocator.Error!void {
881 return addWatchInputFromPath(step, maker, .{
882 .root_dir = path.root_dir,
883 .sub_path = Dir.path.dirname(path.sub_path) orelse "",
884 }, Dir.path.basename(path.sub_path));
885}
886
887fn addWatchInputFromPath(step: *Step, maker: *Maker, directory: Path, basename: []const u8) Allocator.Error!void {
888 const gpa = maker.gpa;
889 const gop = try step.inputs.table.getOrPut(gpa, directory);
890 if (!gop.found_existing) gop.value_ptr.* = .empty;
891 try gop.value_ptr.append(gpa, basename);
892}
893
894fn oomWrap(s: *Step, result: error{OutOfMemory}!void) void {
895 result catch {
896 s.result_oom = true;
897 };
898}
899
900pub fn clearResultStderr(step: *Step, gpa: Allocator) void {
901 if (step.result_stderr.len != 0) {
902 gpa.free(step.result_stderr);
903 step.result_stderr = "";
904 }
905}
906
907pub fn setResultStderr(step: *Step, gpa: Allocator, bytes: []const u8) Allocator.Error!void {
908 takeResultStderr(step, gpa, try gpa.dupe(u8, bytes));
909}
910
911pub fn takeResultStderr(step: *Step, gpa: Allocator, owned: []const u8) void {
912 clearResultStderr(step, gpa);
913 step.result_stderr = owned;
914}