authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-08-26 16:28:42+01:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-10-18 09:28:42+01:00
loga388a8e5a7193831a349f6d00e04bd15211a931f
tree0c8a7e5df6bc3e4c88a2fcbb9e8bb77eba6d2ca3
parente4456d03f3642c4d0e19176aa09dbc12099e7a4c
signaturelock-open Commit is signed but in an unrecognized format.

std.Build: separate errors from failed commands

Recording the command in a separate field will give the build runner more freedom to choose how and when the command should be printed.

5 files changed, 71 insertions(+), 99 deletions(-)

lib/compiler/build_runner.zig+8
......@@ -1443,6 +1443,14 @@ pub fn printErrorMessages(
14431443 }
14441444 try stderr.writeAll("\n");
14451445 }
1446
1447 if (failing_step.result_failed_command) |cmd_str| {
1448 try ttyconf.setColor(stderr, .red);
1449 try stderr.writeAll("failed command: ");
1450 try ttyconf.setColor(stderr, .reset);
1451 try stderr.writeAll(cmd_str);
1452 try stderr.writeByte('\n');
1453 }
14461454}
14471455
14481456fn printSteps(builder: *std.Build, w: *Writer) !void {
lib/std/Build.zig+2-16
......@@ -1594,20 +1594,6 @@ pub fn validateUserInputDidItFail(b: *Build) bool {
15941594 return b.invalid_user_input;
15951595}
15961596
1597fn allocPrintCmd(gpa: Allocator, opt_cwd: ?[]const u8, argv: []const []const u8) error{OutOfMemory}![]u8 {
1598 var buf: ArrayList(u8) = .empty;
1599 if (opt_cwd) |cwd| try buf.print(gpa, "cd {s} && ", .{cwd});
1600 for (argv) |arg| {
1601 try buf.print(gpa, "{s} ", .{arg});
1602 }
1603 return buf.toOwnedSlice(gpa);
1604}
1605
1606fn printCmd(ally: Allocator, cwd: ?[]const u8, argv: []const []const u8) void {
1607 const text = allocPrintCmd(ally, cwd, argv) catch @panic("OOM");
1608 std.debug.print("{s}\n", .{text});
1609}
1610
16111597/// This creates the install step and adds it to the dependencies of the
16121598/// top-level install step, using all the default options.
16131599/// See `addInstallArtifact` for a more flexible function.
......@@ -1857,14 +1843,14 @@ pub fn runAllowFail(
18571843pub fn run(b: *Build, argv: []const []const u8) []u8 {
18581844 if (!process.can_spawn) {
18591845 std.debug.print("unable to spawn the following command: cannot spawn child process\n{s}\n", .{
1860 try allocPrintCmd(b.allocator, null, argv),
1846 try Step.allocPrintCmd(b.allocator, null, argv),
18611847 });
18621848 process.exit(1);
18631849 }
18641850
18651851 var code: u8 = undefined;
18661852 return b.runAllowFail(argv, &code, .Inherit) catch |err| {
1867 const printed_cmd = allocPrintCmd(b.allocator, null, argv) catch @panic("OOM");
1853 const printed_cmd = Step.allocPrintCmd(b.allocator, null, argv) catch @panic("OOM");
18681854 std.debug.print("unable to spawn the following command: {s}\n{s}\n", .{
18691855 @errorName(err), printed_cmd,
18701856 });
lib/std/Build/Step.zig+40-46
......@@ -56,6 +56,9 @@ result_cached: bool,
5656result_duration_ns: ?u64,
5757/// 0 means unavailable or not reported.
5858result_peak_rss: usize,
59/// If the step is failed and this field is populated, this is the command which failed.
60/// This field may be populated even if the step succeeded.
61result_failed_command: ?[]const u8,
5962test_results: TestResults,
6063
6164/// The return address associated with creation of this step that can be useful
......@@ -257,6 +260,7 @@ pub fn init(options: StepOptions) Step {
257260 .result_cached = false,
258261 .result_duration_ns = null,
259262 .result_peak_rss = 0,
263 .result_failed_command = null,
260264 .test_results = .{},
261265 };
262266}
......@@ -336,20 +340,20 @@ pub fn dump(step: *Step, w: *std.Io.Writer, tty_config: std.Io.tty.Config) void
336340 }
337341}
338342
339pub fn evalChildProcess(s: *Step, argv: []const []const u8) ![]u8 {
340 const run_result = try captureChildProcess(s, std.Progress.Node.none, argv);
341 try handleChildProcessTerm(s, run_result.term, null, argv);
342 return run_result.stdout;
343}
344
343/// Populates `s.result_failed_command`.
345344pub fn captureChildProcess(
346345 s: *Step,
346 gpa: Allocator,
347347 progress_node: std.Progress.Node,
348348 argv: []const []const u8,
349349) !std.process.Child.RunResult {
350350 const arena = s.owner.allocator;
351351
352 try handleChildProcUnsupported(s, null, argv);
352 // If an error occurs, it's happened in this command:
353 assert(s.result_failed_command == null);
354 s.result_failed_command = try allocPrintCmd(gpa, null, argv);
355
356 try handleChildProcUnsupported(s);
353357 try handleVerbose(s.owner, null, argv);
354358
355359 const result = std.process.Child.run(.{
......@@ -386,6 +390,7 @@ pub const ZigProcess = struct {
386390
387391/// Assumes that argv contains `--listen=-` and that the process being spawned
388392/// is the zig compiler - the same version that compiled the build runner.
393/// Populates `s.result_failed_command`.
389394pub fn evalZigProcess(
390395 s: *Step,
391396 argv: []const []const u8,
......@@ -394,6 +399,10 @@ pub fn evalZigProcess(
394399 web_server: ?*Build.WebServer,
395400 gpa: Allocator,
396401) !?Path {
402 // If an error occurs, it's happened in this command:
403 assert(s.result_failed_command == null);
404 s.result_failed_command = try allocPrintCmd(gpa, null, argv);
405
397406 if (s.getZigProcess()) |zp| update: {
398407 assert(watch);
399408 if (std.Progress.have_ipc) if (zp.progress_ipc_fd) |fd| prog_node.setIpcFd(fd);
......@@ -410,8 +419,9 @@ pub fn evalZigProcess(
410419 else => |e| return e,
411420 };
412421
413 if (s.result_error_bundle.errorMessageCount() > 0)
422 if (s.result_error_bundle.errorMessageCount() > 0) {
414423 return s.fail("{d} compilation errors", .{s.result_error_bundle.errorMessageCount()});
424 }
415425
416426 if (s.result_error_msgs.items.len > 0 and result == null) {
417427 // Crash detected.
......@@ -420,7 +430,7 @@ pub fn evalZigProcess(
420430 };
421431 s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0;
422432 s.clearZigProcess(gpa);
423 try handleChildProcessTerm(s, term, null, argv);
433 try handleChildProcessTerm(s, term);
424434 return error.MakeFailed;
425435 }
426436
......@@ -430,7 +440,7 @@ pub fn evalZigProcess(
430440 const b = s.owner;
431441 const arena = b.allocator;
432442
433 try handleChildProcUnsupported(s, null, argv);
443 try handleChildProcUnsupported(s);
434444 try handleVerbose(s.owner, null, argv);
435445
436446 var child = std.process.Child.init(argv, arena);
......@@ -484,16 +494,11 @@ pub fn evalZigProcess(
484494 else => {},
485495 };
486496
487 try handleChildProcessTerm(s, term, null, argv);
497 try handleChildProcessTerm(s, term);
488498 }
489499
490 // This is intentionally printed for failure on the first build but not for
491 // subsequent rebuilds.
492500 if (s.result_error_bundle.errorMessageCount() > 0) {
493 return s.fail("the following command failed with {d} compilation errors:\n{s}", .{
494 s.result_error_bundle.errorMessageCount(),
495 try allocPrintCmd(arena, null, argv),
496 });
501 return s.fail("{d} compilation errors", .{s.result_error_bundle.errorMessageCount()});
497502 }
498503
499504 return result;
......@@ -696,54 +701,38 @@ pub fn handleVerbose2(
696701 }
697702}
698703
699pub inline fn handleChildProcUnsupported(
700 s: *Step,
701 opt_cwd: ?[]const u8,
702 argv: []const []const u8,
703) error{ OutOfMemory, MakeFailed }!void {
704/// Asserts that the caller has already populated `s.result_failed_command`.
705pub inline fn handleChildProcUnsupported(s: *Step) error{ OutOfMemory, MakeFailed }!void {
704706 if (!std.process.can_spawn) {
705 return s.fail(
706 "unable to execute the following command: host cannot spawn child processes\n{s}",
707 .{try allocPrintCmd(s.owner.allocator, opt_cwd, argv)},
708 );
707 return s.fail("unable to spawn process: host cannot spawn child processes", .{});
709708 }
710709}
711710
712pub fn handleChildProcessTerm(
713 s: *Step,
714 term: std.process.Child.Term,
715 opt_cwd: ?[]const u8,
716 argv: []const []const u8,
717) error{ MakeFailed, OutOfMemory }!void {
718 const arena = s.owner.allocator;
711/// Asserts that the caller has already populated `s.result_failed_command`.
712pub fn handleChildProcessTerm(s: *Step, term: std.process.Child.Term) error{ MakeFailed, OutOfMemory }!void {
713 assert(s.result_failed_command != null);
719714 switch (term) {
720715 .Exited => |code| {
721716 if (code != 0) {
722 return s.fail(
723 "the following command exited with error code {d}:\n{s}",
724 .{ code, try allocPrintCmd(arena, opt_cwd, argv) },
725 );
717 return s.fail("process exited with error code {d}", .{code});
726718 }
727719 },
728720 .Signal, .Stopped, .Unknown => {
729 return s.fail(
730 "the following command terminated unexpectedly:\n{s}",
731 .{try allocPrintCmd(arena, opt_cwd, argv)},
732 );
721 return s.fail("process terminated unexpectedly", .{});
733722 },
734723 }
735724}
736725
737726pub fn allocPrintCmd(
738 arena: Allocator,
727 gpa: Allocator,
739728 opt_cwd: ?[]const u8,
740729 argv: []const []const u8,
741730) Allocator.Error![]u8 {
742 return allocPrintCmd2(arena, opt_cwd, null, argv);
731 return allocPrintCmd2(gpa, opt_cwd, null, argv);
743732}
744733
745734pub fn allocPrintCmd2(
746 arena: Allocator,
735 gpa: Allocator,
747736 opt_cwd: ?[]const u8,
748737 opt_env: ?*const std.process.EnvMap,
749738 argv: []const []const u8,
......@@ -783,11 +772,13 @@ pub fn allocPrintCmd2(
783772 }
784773 };
785774
786 var aw: std.Io.Writer.Allocating = .init(arena);
775 var aw: std.Io.Writer.Allocating = .init(gpa);
776 defer aw.deinit();
787777 const writer = &aw.writer;
788778 if (opt_cwd) |cwd| writer.print("cd {s} && ", .{cwd}) catch return error.OutOfMemory;
789779 if (opt_env) |env| {
790 const process_env_map = std.process.getEnvMap(arena) catch std.process.EnvMap.init(arena);
780 var process_env_map = std.process.getEnvMap(gpa) catch std.process.EnvMap.init(gpa);
781 defer process_env_map.deinit();
791782 var it = env.iterator();
792783 while (it.next()) |entry| {
793784 const key = entry.key_ptr.*;
......@@ -973,11 +964,14 @@ fn addWatchInputFromPath(step: *Step, path: Build.Cache.Path, basename: []const
973964pub fn reset(step: *Step, gpa: Allocator) void {
974965 assert(step.state == .precheck_done);
975966
967 if (step.result_failed_command) |cmd| gpa.free(cmd);
968
976969 step.result_error_msgs.clearRetainingCapacity();
977970 step.result_stderr = "";
978971 step.result_cached = false;
979972 step.result_duration_ns = null;
980973 step.result_peak_rss = 0;
974 step.result_failed_command = null;
981975 step.test_results = .{};
982976
983977 step.result_error_bundle.deinit(gpa);
lib/std/Build/Step/Fmt.zig+2-2
......@@ -67,7 +67,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
6767 argv.appendAssumeCapacity(b.pathFromRoot(p));
6868 }
6969
70 const run_result = try step.captureChildProcess(prog_node, argv.items);
70 const run_result = try step.captureChildProcess(options.gpa, prog_node, argv.items);
7171 if (fmt.check) switch (run_result.term) {
7272 .Exited => |code| if (code != 0 and run_result.stdout.len != 0) {
7373 var it = std.mem.tokenizeScalar(u8, run_result.stdout, '\n');
......@@ -77,5 +77,5 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
7777 },
7878 else => {},
7979 };
80 try step.handleChildProcessTerm(run_result.term, null, argv.items);
80 try step.handleChildProcessTerm(run_result.term);
8181}
lib/std/Build/Step/Run.zig+19-35
......@@ -1212,10 +1212,11 @@ fn runCommand(
12121212 const step = &run.step;
12131213 const b = step.owner;
12141214 const arena = b.allocator;
1215 const gpa = options.gpa;
12151216
12161217 const cwd: ?[]const u8 = if (run.cwd) |lazy_cwd| lazy_cwd.getPath2(b, step) else null;
12171218
1218 try step.handleChildProcUnsupported(cwd, argv);
1219 try step.handleChildProcUnsupported();
12191220 try Step.handleVerbose2(step.owner, cwd, run.env_map, argv);
12201221
12211222 const allow_skip = switch (run.stdio) {
......@@ -1365,6 +1366,8 @@ fn runCommand(
13651366 run.addPathForDynLibs(exe);
13661367 }
13671368
1369 gpa.free(step.result_failed_command.?);
1370 step.result_failed_command = null;
13681371 try Step.handleVerbose2(step.owner, cwd, run.env_map, interp_argv.items);
13691372
13701373 break :term spawnChildAndCollect(run, interp_argv.items, env_map, has_side_effects, options, fuzz_context) catch |e| {
......@@ -1380,18 +1383,11 @@ fn runCommand(
13801383 return step.fail("failed to spawn and capture stdio from {s}: {s}", .{ argv[0], @errorName(err) });
13811384 };
13821385
1383 const final_argv = if (interp_argv.items.len == 0) argv else interp_argv.items;
1384
13851386 const generic_result = opt_generic_result orelse {
13861387 assert(run.stdio == .zig_test);
1387 // Specific errors have already been reported. All we need to do is detect those and
1388 // report the general "test failed" error, which includes the command argv.
1389 if (!step.test_results.isSuccess() or step.result_error_msgs.items.len > 0) {
1390 return step.fail(
1391 "the following test command failed:\n{s}",
1392 .{try Step.allocPrintCmd(arena, cwd, final_argv)},
1393 );
1394 }
1388 // Specific errors have already been reported, and test results are populated. All we need
1389 // to do is report step failure if any test failed.
1390 if (!step.test_results.isSuccess()) return error.MakeFailed;
13951391 return;
13961392 };
13971393
......@@ -1445,93 +1441,77 @@ fn runCommand(
14451441 .expect_stderr_exact => |expected_bytes| {
14461442 if (!mem.eql(u8, expected_bytes, generic_result.stderr.?)) {
14471443 return step.fail(
1448 \\
14491444 \\========= expected this stderr: =========
14501445 \\{s}
14511446 \\========= but found: ====================
14521447 \\{s}
1453 \\========= from the following command: ===
1454 \\{s}
14551448 , .{
14561449 expected_bytes,
14571450 generic_result.stderr.?,
1458 try Step.allocPrintCmd(arena, cwd, final_argv),
14591451 });
14601452 }
14611453 },
14621454 .expect_stderr_match => |match| {
14631455 if (mem.indexOf(u8, generic_result.stderr.?, match) == null) {
14641456 return step.fail(
1465 \\
14661457 \\========= expected to find in stderr: =========
14671458 \\{s}
14681459 \\========= but stderr does not contain it: =====
14691460 \\{s}
1470 \\========= from the following command: =========
1471 \\{s}
14721461 , .{
14731462 match,
14741463 generic_result.stderr.?,
1475 try Step.allocPrintCmd(arena, cwd, final_argv),
14761464 });
14771465 }
14781466 },
14791467 .expect_stdout_exact => |expected_bytes| {
14801468 if (!mem.eql(u8, expected_bytes, generic_result.stdout.?)) {
14811469 return step.fail(
1482 \\
14831470 \\========= expected this stdout: =========
14841471 \\{s}
14851472 \\========= but found: ====================
14861473 \\{s}
1487 \\========= from the following command: ===
1488 \\{s}
14891474 , .{
14901475 expected_bytes,
14911476 generic_result.stdout.?,
1492 try Step.allocPrintCmd(arena, cwd, final_argv),
14931477 });
14941478 }
14951479 },
14961480 .expect_stdout_match => |match| {
14971481 if (mem.indexOf(u8, generic_result.stdout.?, match) == null) {
14981482 return step.fail(
1499 \\
15001483 \\========= expected to find in stdout: =========
15011484 \\{s}
15021485 \\========= but stdout does not contain it: =====
15031486 \\{s}
1504 \\========= from the following command: =========
1505 \\{s}
15061487 , .{
15071488 match,
15081489 generic_result.stdout.?,
1509 try Step.allocPrintCmd(arena, cwd, final_argv),
15101490 });
15111491 }
15121492 },
15131493 .expect_term => |expected_term| {
15141494 if (!termMatches(expected_term, generic_result.term)) {
1515 return step.fail("the following command {f} (expected {f}):\n{s}", .{
1495 return step.fail("process {f} (expected {f})", .{
15161496 fmtTerm(generic_result.term),
15171497 fmtTerm(expected_term),
1518 try Step.allocPrintCmd(arena, cwd, final_argv),
15191498 });
15201499 }
15211500 },
15221501 },
15231502 else => {
1524 // On failure, print stderr if captured.
1503 // On failure, report captured stderr like normal standard error output.
15251504 const bad_exit = switch (generic_result.term) {
15261505 .Exited => |code| code != 0,
15271506 .Signal, .Stopped, .Unknown => true,
15281507 };
1508 if (bad_exit) {
1509 if (generic_result.stderr) |bytes| {
1510 run.step.result_stderr = bytes;
1511 }
1512 }
15291513
1530 if (bad_exit) if (generic_result.stderr) |err| {
1531 try step.addError("stderr:\n{s}", .{err});
1532 };
1533
1534 try step.handleChildProcessTerm(generic_result.term, cwd, final_argv);
1514 try step.handleChildProcessTerm(generic_result.term);
15351515 },
15361516 }
15371517}
......@@ -1594,6 +1574,10 @@ fn spawnChildAndCollect(
15941574 child.stdin_behavior = .Pipe;
15951575 }
15961576
1577 // If an error occurs, it's caused by this command:
1578 assert(run.step.result_failed_command == null);
1579 run.step.result_failed_command = try Step.allocPrintCmd(options.gpa, child.cwd, argv);
1580
15971581 if (run.stdio == .zig_test) {
15981582 var timer = try std.time.Timer.start();
15991583 const res = try evalZigTest(run, &child, options, fuzz_context);