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}