diff --git a/lib/std/Build/EmulatableRunStep.zig b/lib/std/Build/EmulatableRunStep.zig index 5517f7f9aa..d4b5238524 100644 --- a/lib/std/Build/EmulatableRunStep.zig +++ b/lib/std/Build/EmulatableRunStep.zig @@ -26,7 +26,7 @@ builder: *std.Build, exe: *CompileStep, /// Set this to `null` to ignore the exit code for the purpose of determining a successful execution -expected_exit_code: ?u8 = 0, +expected_term: ?std.ChildProcess.Term = .{ .Exited = 0 }, /// Override this field to modify the environment env_map: ?*EnvMap, @@ -131,7 +131,7 @@ fn make(step: *Step) !void { try RunStep.runCommand( argv_list.items, self.builder, - self.expected_exit_code, + self.expected_term, self.stdout_action, self.stderr_action, .Inherit, diff --git a/lib/std/Build/RunStep.zig b/lib/std/Build/RunStep.zig index 5bc271409a..0ef78dfdeb 100644 --- a/lib/std/Build/RunStep.zig +++ b/lib/std/Build/RunStep.zig @@ -35,7 +35,7 @@ stderr_action: StdIoAction = .inherit, stdin_behavior: std.ChildProcess.StdIo = .Inherit, /// Set this to `null` to ignore the exit code for the purpose of determining a successful execution -expected_exit_code: ?u8 = 0, +expected_term: ?std.ChildProcess.Term = .{ .Exited = 0 }, /// Print the command before running it print: bool, @@ -289,7 +289,7 @@ fn make(step: *Step) !void { try runCommand( argv_list.items, self.builder, - self.expected_exit_code, + self.expected_term, self.stdout_action, self.stderr_action, self.stdin_behavior, @@ -303,10 +303,55 @@ fn make(step: *Step) !void { } } +fn formatTerm( + term: ?std.ChildProcess.Term, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, +) !void { + _ = fmt; + _ = options; + if (term) |t| switch (t) { + .Exited => |code| try writer.print("exited with code {}", .{code}), + .Signal => |sig| try writer.print("terminated with signal {}", .{sig}), + .Stopped => |sig| try writer.print("stopped with signal {}", .{sig}), + .Unknown => |code| try writer.print("terminated for unknown reason with code {}", .{code}), + } else { + try writer.writeAll("exited with any code"); + } +} +fn fmtTerm(term: ?std.ChildProcess.Term) std.fmt.Formatter(formatTerm) { + return .{ .data = term }; +} + +fn termMatches(expected: ?std.ChildProcess.Term, actual: std.ChildProcess.Term) bool { + return if (expected) |e| switch (e) { + .Exited => |expected_code| switch (actual) { + .Exited => |actual_code| expected_code == actual_code, + else => false, + }, + .Signal => |expected_sig| switch (actual) { + .Signal => |actual_sig| expected_sig == actual_sig, + else => false, + }, + .Stopped => |expected_sig| switch (actual) { + .Stopped => |actual_sig| expected_sig == actual_sig, + else => false, + }, + .Unknown => |expected_code| switch (actual) { + .Unknown => |actual_code| expected_code == actual_code, + else => false, + }, + } else switch (actual) { + .Exited => true, + else => false, + }; +} + pub fn runCommand( argv: []const []const u8, builder: *std.Build, - expected_exit_code: ?u8, + expected_term: ?std.ChildProcess.Term, stdout_action: StdIoAction, stderr_action: StdIoAction, stdin_behavior: std.ChildProcess.StdIo, @@ -368,32 +413,14 @@ pub fn runCommand( return err; }; - switch (term) { - .Exited => |code| blk: { - const expected_code = expected_exit_code orelse break :blk; - - if (code != expected_code) { - if (builder.prominent_compile_errors) { - std.debug.print("Run step exited with error code {} (expected {})\n", .{ - code, - expected_code, - }); - } else { - std.debug.print("The following command exited with error code {} (expected {}):\n", .{ - code, - expected_code, - }); - printCmd(cwd, argv); - } - - return error.UnexpectedExitCode; - } - }, - else => { - std.debug.print("The following command terminated unexpectedly:\n", .{}); + if (!termMatches(expected_term, term)) { + if (builder.prominent_compile_errors) { + std.debug.print("Run step {} (expected {})\n", .{ fmtTerm(term), fmtTerm(expected_term) }); + } else { + std.debug.print("The following command {} (expected {}):\n", .{ fmtTerm(term), fmtTerm(expected_term) }); printCmd(cwd, argv); - return error.UncleanExit; - }, + } + return error.UnexpectedExit; } switch (stderr_action) { diff --git a/lib/std/os.zig b/lib/std/os.zig index eb4dd82164..9752df7f16 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -7074,6 +7074,8 @@ pub const keep_sigpipe: bool = if (@hasDecl(root, "keep_sigpipe")) else false; +fn noopSigHandler(_: c_int) callconv(.C) void {} + /// This function will tell the kernel to ignore SIGPIPE rather than terminate /// the process. This function is automatically called in `start.zig` before /// `main`. This behavior can be disabled by adding this to your root module: @@ -7092,12 +7094,13 @@ else pub fn maybeIgnoreSigpipe() void { if (have_sigpipe_support and !keep_sigpipe) { const act = Sigaction{ - .handler = .{ .sigaction = SIG.IGN }, + // We set handler to a noop function instead of SIG.IGN so we don't leak our + // signal disposition to a child process + .handler = .{ .handler = noopSigHandler }, .mask = empty_sigset, - .flags = SA.SIGINFO, + .flags = 0, }; - sigaction(SIG.PIPE, &act, null) catch |err| std.debug.panic("ignore SIGPIPE failed with '{s}'" ++ - ", add `pub const keep_sigpipe = true;` to your root module" ++ - " or adjust have_sigpipe_support in std/os.zig", .{@errorName(err)}); + sigaction(SIG.PIPE, &act, null) catch |err| + std.debug.panic("failed to install noop SIGPIPE handler with '{s}'", .{@errorName(err)}); } } diff --git a/test/link/macho/dead_strip_dylibs/build.zig b/test/link/macho/dead_strip_dylibs/build.zig index 8b62cec6e6..af2f5cf0dc 100644 --- a/test/link/macho/dead_strip_dylibs/build.zig +++ b/test/link/macho/dead_strip_dylibs/build.zig @@ -29,7 +29,7 @@ pub fn build(b: *std.Build) void { exe.dead_strip_dylibs = true; const run_cmd = exe.run(); - run_cmd.expected_exit_code = @bitCast(u8, @as(i8, -2)); // should fail + run_cmd.expected_term = .{ .Exited = @bitCast(u8, @as(i8, -2)) }; // should fail test_step.dependOn(&run_cmd.step); } } diff --git a/test/src/compare_output.zig b/test/src/compare_output.zig index edd48321c9..3bda3bdacd 100644 --- a/test/src/compare_output.zig +++ b/test/src/compare_output.zig @@ -168,7 +168,7 @@ pub const CompareOutputContext = struct { run.addArgs(case.cli_args); run.stderr_action = .ignore; run.stdout_action = .ignore; - run.expected_exit_code = 126; + run.expected_term = .{ .Exited = 126 }; self.step.dependOn(&run.step); }, diff --git a/test/standalone.zig b/test/standalone.zig index 81eb1b0042..ed0d2c2d30 100644 --- a/test/standalone.zig +++ b/test/standalone.zig @@ -84,6 +84,9 @@ pub fn addCases(cases: *tests.StandaloneContext) void { cases.addBuildFile("test/standalone/pie/build.zig", .{}); } cases.addBuildFile("test/standalone/issue_12706/build.zig", .{}); + if (std.os.have_sigpipe_support) { + cases.addBuildFile("test/standalone/sigpipe/build.zig", .{}); + } // Ensure the development tools are buildable. Alphabetically sorted. // No need to build `tools/spirv/grammar.zig`. diff --git a/test/standalone/sigpipe/breakpipe.zig b/test/standalone/sigpipe/breakpipe.zig new file mode 100644 index 0000000000..6498f5b2eb --- /dev/null +++ b/test/standalone/sigpipe/breakpipe.zig @@ -0,0 +1,21 @@ +const std = @import("std"); +const build_options = @import("build_options"); + +pub usingnamespace if (build_options.keep_sigpipe) struct { + pub const keep_sigpipe = true; +} else struct { + // intentionally not setting keep_sigpipe to ensure the default behavior is equivalent to false +}; + +pub fn main() !void { + const pipe = try std.os.pipe(); + std.os.close(pipe[0]); + _ = std.os.write(pipe[1], "a") catch |err| switch (err) { + error.BrokenPipe => { + try std.io.getStdOut().writer().writeAll("BrokenPipe\n"); + std.os.exit(123); + }, + else => |e| return e, + }; + unreachable; +} diff --git a/test/standalone/sigpipe/build.zig b/test/standalone/sigpipe/build.zig new file mode 100644 index 0000000000..763df5fe46 --- /dev/null +++ b/test/standalone/sigpipe/build.zig @@ -0,0 +1,35 @@ +const std = @import("std"); +const os = std.os; + +pub fn build(b: *std.build.Builder) !void { + const test_step = b.step("test", "Run the tests"); + + // This test runs "breakpipe" as a child process and that process + // depends on inheriting a SIGPIPE disposition of "default". + { + const act = os.Sigaction{ + .handler = .{ .handler = os.SIG.DFL }, + .mask = os.empty_sigset, + .flags = 0, + }; + try os.sigaction(os.SIG.PIPE, &act, null); + } + + for ([_]bool{ false, true }) |keep_sigpipe| { + const options = b.addOptions(); + options.addOption(bool, "keep_sigpipe", keep_sigpipe); + const exe = b.addExecutable(.{ + .name = "breakpipe", + .root_source_file = .{ .path = "breakpipe.zig" }, + }); + exe.addOptions("build_options", options); + const run = exe.run(); + if (keep_sigpipe) { + run.expected_term = .{ .Signal = std.os.SIG.PIPE }; + } else { + run.stdout_action = .{ .expect_exact = "BrokenPipe\n" }; + run.expected_term = .{ .Exited = 123 }; + } + test_step.dependOn(&run.step); + } +}