const std = @import("../std.zig");
const builtin = @import("builtin");
const Step = std.Build.Step;
const CompileStep = std.Build.CompileStep;
const WriteFileStep = std.Build.WriteFileStep;
const fs = std.fs;
const mem = std.mem;
const process = std.process;
const ArrayList = std.ArrayList;
const EnvMap = process.EnvMap;
const Allocator = mem.Allocator;
const ExecError = std.Build.ExecError;
const assert = std.debug.assert;
const RunStep = @This();
pub const base_id: Step.Id = .run;
step: Step,
argv: ArrayList(Arg),
cwd: ?[]const u8,
env_map: ?*EnvMap,
stdio: StdIo = .infer_from_args,
stdin: ?[]const u8 = null,
extra_file_dependencies: []const []const u8 = &.{},
rename_step_with_output_arg: bool = true,
skip_foreign_checks: bool = false,
max_stdio_size: usize = 10 * 1024 * 1024,
captured_stdout: ?*Output = null,
captured_stderr: ?*Output = null,
has_side_effects: bool = false,
pub const StdIo = union(enum) {
infer_from_args,
inherit,
check: std.ArrayList(Check),
zig_test,
pub const Check = union(enum) {
expect_stderr_exact: []const u8,
expect_stderr_match: []const u8,
expect_stdout_exact: []const u8,
expect_stdout_match: []const u8,
expect_term: std.process.Child.Term,
};
};
pub const Arg = union(enum) {
artifact: *CompileStep,
file_source: std.Build.FileSource,
directory_source: std.Build.FileSource,
bytes: []u8,
output: *Output,
};
pub const Output = struct {
generated_file: std.Build.GeneratedFile,
prefix: []const u8,
basename: []const u8,
};
pub fn create(owner: *std.Build, name: []const u8) *RunStep {
const self = owner.allocator.create(RunStep) catch @panic("OOM");
self.* = .{
.step = Step.init(.{
.id = base_id,
.name = name,
.owner = owner,
.makeFn = make,
}),
.argv = ArrayList(Arg).init(owner.allocator),
.cwd = null,
.env_map = null,
};
return self;
}
pub fn setName(self: *RunStep, name: []const u8) void {
self.step.name = name;
self.rename_step_with_output_arg = false;
}
pub fn enableTestRunnerMode(rs: *RunStep) void {
rs.stdio = .zig_test;
rs.addArgs(&.{"--listen=-"});
}
pub fn addArtifactArg(self: *RunStep, artifact: *CompileStep) void {
self.argv.append(Arg{ .artifact = artifact }) catch @panic("OOM");
self.step.dependOn(&artifact.step);
}
pub fn addOutputFileArg(rs: *RunStep, basename: []const u8) std.Build.FileSource {
return addPrefixedOutputFileArg(rs, "", basename);
}
pub fn addPrefixedOutputFileArg(
rs: *RunStep,
prefix: []const u8,
basename: []const u8,
) std.Build.FileSource {
const b = rs.step.owner;
const output = b.allocator.create(Output) catch @panic("OOM");
output.* = .{
.prefix = prefix,
.basename = basename,
.generated_file = .{ .step = &rs.step },
};
rs.argv.append(.{ .output = output }) catch @panic("OOM");
if (rs.rename_step_with_output_arg) {
rs.setName(b.fmt("{s} ({s})", .{ rs.step.name, basename }));
}
return .{ .generated = &output.generated_file };
}
pub fn addFileSourceArg(self: *RunStep, file_source: std.Build.FileSource) void {
self.argv.append(.{
.file_source = file_source.dupe(self.step.owner),
}) catch @panic("OOM");
file_source.addStepDependencies(&self.step);
}
pub fn addDirectorySourceArg(self: *RunStep, directory_source: std.Build.FileSource) void {
self.argv.append(.{
.directory_source = directory_source.dupe(self.step.owner),
}) catch @panic("OOM");
directory_source.addStepDependencies(&self.step);
}
pub fn addArg(self: *RunStep, arg: []const u8) void {
self.argv.append(.{ .bytes = self.step.owner.dupe(arg) }) catch @panic("OOM");
}
pub fn addArgs(self: *RunStep, args: []const []const u8) void {
for (args) |arg| {
self.addArg(arg);
}
}
pub fn clearEnvironment(self: *RunStep) void {
const b = self.step.owner;
const new_env_map = b.allocator.create(EnvMap) catch @panic("OOM");
new_env_map.* = EnvMap.init(b.allocator);
self.env_map = new_env_map;
}
pub fn addPathDir(self: *RunStep, search_path: []const u8) void {
const b = self.step.owner;
const env_map = getEnvMapInternal(self);
const key = "PATH";
var prev_path = env_map.get(key);
if (prev_path) |pp| {
const new_path = b.fmt("{s}" ++ [1]u8{fs.path.delimiter} ++ "{s}", .{ pp, search_path });
env_map.put(key, new_path) catch @panic("OOM");
} else {
env_map.put(key, b.dupePath(search_path)) catch @panic("OOM");
}
}
pub fn getEnvMap(self: *RunStep) *EnvMap {
return getEnvMapInternal(self);
}
fn getEnvMapInternal(self: *RunStep) *EnvMap {
const arena = self.step.owner.allocator;
return self.env_map orelse {
const env_map = arena.create(EnvMap) catch @panic("OOM");
env_map.* = process.getEnvMap(arena) catch @panic("unhandled error");
self.env_map = env_map;
return env_map;
};
}
pub fn setEnvironmentVariable(self: *RunStep, key: []const u8, value: []const u8) void {
const b = self.step.owner;
const env_map = self.getEnvMap();
env_map.put(b.dupe(key), b.dupe(value)) catch @panic("unhandled error");
}
pub fn removeEnvironmentVariable(self: *RunStep, key: []const u8) void {
self.getEnvMap().remove(key);
}
pub fn expectStdErrEqual(self: *RunStep, bytes: []const u8) void {
const new_check: StdIo.Check = .{ .expect_stderr_exact = self.step.owner.dupe(bytes) };
self.addCheck(new_check);
}
pub fn expectStdOutEqual(self: *RunStep, bytes: []const u8) void {
const new_check: StdIo.Check = .{ .expect_stdout_exact = self.step.owner.dupe(bytes) };
self.addCheck(new_check);
if (!self.hasTermCheck()) {
self.expectExitCode(0);
}
}
pub fn expectExitCode(self: *RunStep, code: u8) void {
const new_check: StdIo.Check = .{ .expect_term = .{ .Exited = code } };
self.addCheck(new_check);
}
pub fn hasTermCheck(self: RunStep) bool {
for (self.stdio.check.items) |check| switch (check) {
.expect_term => return true,
else => continue,
};
return false;
}
pub fn addCheck(self: *RunStep, new_check: StdIo.Check) void {
switch (self.stdio) {
.infer_from_args => {
self.stdio = .{ .check = std.ArrayList(StdIo.Check).init(self.step.owner.allocator) };
self.stdio.check.append(new_check) catch @panic("OOM");
},
.check => |*checks| checks.append(new_check) catch @panic("OOM"),
else => @panic("illegal call to addCheck: conflicting helper method calls. Suggest to directly set stdio field of RunStep instead"),
}
}
pub fn captureStdErr(self: *RunStep) std.Build.FileSource {
assert(self.stdio != .inherit);
if (self.captured_stderr) |output| return .{ .generated = &output.generated_file };
const output = self.step.owner.allocator.create(Output) catch @panic("OOM");
output.* = .{
.prefix = "",
.basename = "stderr",
.generated_file = .{ .step = &self.step },
};
self.captured_stderr = output;
return .{ .generated = &output.generated_file };
}
pub fn captureStdOut(self: *RunStep) std.Build.FileSource {
assert(self.stdio != .inherit);
if (self.captured_stdout) |output| return .{ .generated = &output.generated_file };
const output = self.step.owner.allocator.create(Output) catch @panic("OOM");
output.* = .{
.prefix = "",
.basename = "stdout",
.generated_file = .{ .step = &self.step },
};
self.captured_stdout = output;
return .{ .generated = &output.generated_file };
}
fn hasSideEffects(self: RunStep) bool {
if (self.has_side_effects) return true;
return switch (self.stdio) {
.infer_from_args => !self.hasAnyOutputArgs(),
.inherit => true,
.check => false,
.zig_test => false,
};
}
fn hasAnyOutputArgs(self: RunStep) bool {
if (self.captured_stdout != null) return true;
if (self.captured_stderr != null) return true;
for (self.argv.items) |arg| switch (arg) {
.output => return true,
else => continue,
};
return false;
}
fn checksContainStdout(checks: []const StdIo.Check) bool {
for (checks) |check| switch (check) {
.expect_stderr_exact,
.expect_stderr_match,
.expect_term,
=> continue,
.expect_stdout_exact,
.expect_stdout_match,
=> return true,
};
return false;
}
fn checksContainStderr(checks: []const StdIo.Check) bool {
for (checks) |check| switch (check) {
.expect_stdout_exact,
.expect_stdout_match,
.expect_term,
=> continue,
.expect_stderr_exact,
.expect_stderr_match,
=> return true,
};
return false;
}
fn make(step: *Step, prog_node: *std.Progress.Node) !void {
const b = step.owner;
const arena = b.allocator;
const self = @fieldParentPtr(RunStep, "step", step);
const has_side_effects = self.hasSideEffects();
var argv_list = ArrayList([]const u8).init(arena);
var output_placeholders = ArrayList(struct {
index: usize,
output: *Output,
}).init(arena);
var man = b.cache.obtain();
defer man.deinit();
for (self.argv.items) |arg| {
switch (arg) {
.bytes => |bytes| {
try argv_list.append(bytes);
man.hash.addBytes(bytes);
},
.file_source => |file| {
const file_path = file.getPath(b);
try argv_list.append(file_path);
_ = try man.addFile(file_path, null);
},
.directory_source => |file| {
const file_path = file.getPath(b);
try argv_list.append(file_path);
man.hash.addBytes(file_path);
},
.artifact => |artifact| {
if (artifact.target.isWindows()) {
self.addPathForDynLibs(artifact);
}
const file_path = artifact.installed_path orelse
artifact.getOutputSource().getPath(b);
try argv_list.append(file_path);
_ = try man.addFile(file_path, null);
},
.output => |output| {
man.hash.addBytes(output.prefix);
man.hash.addBytes(output.basename);
try argv_list.append("");
try output_placeholders.append(.{
.index = argv_list.items.len - 1,
.output = output,
});
},
}
}
if (self.captured_stdout) |output| {
man.hash.addBytes(output.basename);
}
if (self.captured_stderr) |output| {
man.hash.addBytes(output.basename);
}
hashStdIo(&man.hash, self.stdio);
if (has_side_effects) {
try runCommand(self, argv_list.items, has_side_effects, null, prog_node);
return;
}
for (self.extra_file_dependencies) |file_path| {
_ = try man.addFile(b.pathFromRoot(file_path), null);
}
if (try step.cacheHit(&man)) {
const digest = man.final();
for (output_placeholders.items) |placeholder| {
placeholder.output.generated_file.path = try b.cache_root.join(arena, &.{
"o", &digest, placeholder.output.basename,
});
}
if (self.captured_stdout) |output| {
output.generated_file.path = try b.cache_root.join(arena, &.{
"o", &digest, output.basename,
});
}
if (self.captured_stderr) |output| {
output.generated_file.path = try b.cache_root.join(arena, &.{
"o", &digest, output.basename,
});
}
step.result_cached = true;
return;
}
const digest = man.final();
for (output_placeholders.items) |placeholder| {
const output_components = .{ "o", &digest, placeholder.output.basename };
const output_sub_path = try fs.path.join(arena, &output_components);
const output_sub_dir_path = fs.path.dirname(output_sub_path).?;
b.cache_root.handle.makePath(output_sub_dir_path) catch |err| {
return step.fail("unable to make path '{}{s}': {s}", .{
b.cache_root, output_sub_dir_path, @errorName(err),
});
};
const output_path = try b.cache_root.join(arena, &output_components);
placeholder.output.generated_file.path = output_path;
const cli_arg = if (placeholder.output.prefix.len == 0)
output_path
else
b.fmt("{s}{s}", .{ placeholder.output.prefix, output_path });
argv_list.items[placeholder.index] = cli_arg;
}
try runCommand(self, argv_list.items, has_side_effects, &digest, prog_node);
try step.writeManifest(&man);
}
fn formatTerm(
term: ?std.process.Child.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.process.Child.Term) std.fmt.Formatter(formatTerm) {
return .{ .data = term };
}
fn termMatches(expected: ?std.process.Child.Term, actual: std.process.Child.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,
};
}
fn runCommand(
self: *RunStep,
argv: []const []const u8,
has_side_effects: bool,
digest: ?*const [std.Build.Cache.hex_digest_len]u8,
prog_node: *std.Progress.Node,
) !void {
const step = &self.step;
const b = step.owner;
const arena = b.allocator;
try step.handleChildProcUnsupported(self.cwd, argv);
try Step.handleVerbose2(step.owner, self.cwd, self.env_map, argv);
const allow_skip = switch (self.stdio) {
.check, .zig_test => self.skip_foreign_checks,
else => false,
};
var interp_argv = std.ArrayList([]const u8).init(b.allocator);
defer interp_argv.deinit();
const result = spawnChildAndCollect(self, argv, has_side_effects, prog_node) catch |err| term: {
if (err == error.InvalidExe or err == error.FileNotFound) interpret: {
const exe = switch (self.argv.items[0]) {
.artifact => |exe| exe,
else => break :interpret,
};
switch (exe.kind) {
.exe, .@"test" => {},
else => break :interpret,
}
const need_cross_glibc = exe.target.isGnuLibC() and exe.is_linking_libc;
switch (b.host.getExternalExecutor(exe.target_info, .{
.qemu_fixes_dl = need_cross_glibc and b.glibc_runtimes_dir != null,
.link_libc = exe.is_linking_libc,
})) {
.native, .rosetta => {
if (allow_skip) return error.MakeSkipped;
break :interpret;
},
.wine => |bin_name| {
if (b.enable_wine) {
try interp_argv.append(bin_name);
try interp_argv.appendSlice(argv);
} else {
return failForeign(self, "-fwine", argv[0], exe);
}
},
.qemu => |bin_name| {
if (b.enable_qemu) {
const glibc_dir_arg = if (need_cross_glibc)
b.glibc_runtimes_dir orelse
return failForeign(self, "--glibc-runtimes", argv[0], exe)
else
null;
try interp_argv.append(bin_name);
if (glibc_dir_arg) |dir| {
const fmt_str = "{s}" ++ fs.path.sep_str ++ "{s}-{s}-{s}";
const cpu_arch = exe.target.getCpuArch();
const os_tag = exe.target.getOsTag();
const abi = exe.target.getAbi();
const cpu_arch_name: []const u8 = if (cpu_arch == .x86)
"i686"
else
@tagName(cpu_arch);
const full_dir = try std.fmt.allocPrint(b.allocator, fmt_str, .{
dir, cpu_arch_name, @tagName(os_tag), @tagName(abi),
});
try interp_argv.append("-L");
try interp_argv.append(full_dir);
}
try interp_argv.appendSlice(argv);
} else {
return failForeign(self, "-fqemu", argv[0], exe);
}
},
.darling => |bin_name| {
if (b.enable_darling) {
try interp_argv.append(bin_name);
try interp_argv.appendSlice(argv);
} else {
return failForeign(self, "-fdarling", argv[0], exe);
}
},
.wasmtime => |bin_name| {
if (b.enable_wasmtime) {
try interp_argv.append(bin_name);
try interp_argv.append("--dir=.");
try interp_argv.append(argv[0]);
try interp_argv.append("--");
try interp_argv.appendSlice(argv[1..]);
} else {
return failForeign(self, "-fwasmtime", argv[0], exe);
}
},
.bad_dl => |foreign_dl| {
if (allow_skip) return error.MakeSkipped;
const host_dl = b.host.dynamic_linker.get() orelse "(none)";
return step.fail(
\\the host system is unable to execute binaries from the target
\\ because the host dynamic linker is '{s}',
\\ while the target dynamic linker is '{s}'.
\\ consider setting the dynamic linker or enabling skip_foreign_checks in the Run step
, .{ host_dl, foreign_dl });
},
.bad_os_or_cpu => {
if (allow_skip) return error.MakeSkipped;
const host_name = try b.host.target.zigTriple(b.allocator);
const foreign_name = try exe.target.zigTriple(b.allocator);
return step.fail("the host system ({s}) is unable to execute binaries from the target ({s})", .{
host_name, foreign_name,
});
},
}
if (exe.target.isWindows()) {
self.addPathForDynLibs(exe);
}
try Step.handleVerbose2(step.owner, self.cwd, self.env_map, interp_argv.items);
break :term spawnChildAndCollect(self, interp_argv.items, has_side_effects, prog_node) catch |e| {
return step.fail("unable to spawn interpreter {s}: {s}", .{
interp_argv.items[0], @errorName(e),
});
};
}
return step.fail("unable to spawn {s}: {s}", .{ argv[0], @errorName(err) });
};
step.result_duration_ns = result.elapsed_ns;
step.result_peak_rss = result.peak_rss;
step.test_results = result.stdio.test_results;
const Stream = struct {
captured: ?*Output,
is_null: bool,
bytes: []const u8,
};
for ([_]Stream{
.{
.captured = self.captured_stdout,
.is_null = result.stdio.stdout_null,
.bytes = result.stdio.stdout,
},
.{
.captured = self.captured_stderr,
.is_null = result.stdio.stderr_null,
.bytes = result.stdio.stderr,
},
}) |stream| {
if (stream.captured) |output| {
assert(!stream.is_null);
const output_components = .{ "o", digest.?, output.basename };
const output_path = try b.cache_root.join(arena, &output_components);
output.generated_file.path = output_path;
const sub_path = try fs.path.join(arena, &output_components);
const sub_path_dirname = fs.path.dirname(sub_path).?;
b.cache_root.handle.makePath(sub_path_dirname) catch |err| {
return step.fail("unable to make path '{}{s}': {s}", .{
b.cache_root, sub_path_dirname, @errorName(err),
});
};
b.cache_root.handle.writeFile(sub_path, stream.bytes) catch |err| {
return step.fail("unable to write file '{}{s}': {s}", .{
b.cache_root, sub_path, @errorName(err),
});
};
}
}
const final_argv = if (interp_argv.items.len == 0) argv else interp_argv.items;
switch (self.stdio) {
.check => |checks| for (checks.items) |check| switch (check) {
.expect_stderr_exact => |expected_bytes| {
assert(!result.stdio.stderr_null);
if (!mem.eql(u8, expected_bytes, result.stdio.stderr)) {
return step.fail(
\\
\\========= expected this stderr: =========
\\{s}
\\========= but found: ====================
\\{s}
\\========= from the following command: ===
\\{s}
, .{
expected_bytes,
result.stdio.stderr,
try Step.allocPrintCmd(arena, self.cwd, final_argv),
});
}
},
.expect_stderr_match => |match| {
assert(!result.stdio.stderr_null);
if (mem.indexOf(u8, result.stdio.stderr, match) == null) {
return step.fail(
\\
\\========= expected to find in stderr: =========
\\{s}
\\========= but stderr does not contain it: =====
\\{s}
\\========= from the following command: =========
\\{s}
, .{
match,
result.stdio.stderr,
try Step.allocPrintCmd(arena, self.cwd, final_argv),
});
}
},
.expect_stdout_exact => |expected_bytes| {
assert(!result.stdio.stdout_null);
if (!mem.eql(u8, expected_bytes, result.stdio.stdout)) {
return step.fail(
\\
\\========= expected this stdout: =========
\\{s}
\\========= but found: ====================
\\{s}
\\========= from the following command: ===
\\{s}
, .{
expected_bytes,
result.stdio.stdout,
try Step.allocPrintCmd(arena, self.cwd, final_argv),
});
}
},
.expect_stdout_match => |match| {
assert(!result.stdio.stdout_null);
if (mem.indexOf(u8, result.stdio.stdout, match) == null) {
return step.fail(
\\
\\========= expected to find in stdout: =========
\\{s}
\\========= but stdout does not contain it: =====
\\{s}
\\========= from the following command: =========
\\{s}
, .{
match,
result.stdio.stdout,
try Step.allocPrintCmd(arena, self.cwd, final_argv),
});
}
},
.expect_term => |expected_term| {
if (!termMatches(expected_term, result.term)) {
return step.fail("the following command {} (expected {}):\n{s}", .{
fmtTerm(result.term),
fmtTerm(expected_term),
try Step.allocPrintCmd(arena, self.cwd, final_argv),
});
}
},
},
.zig_test => {
const expected_term: std.process.Child.Term = .{ .Exited = 0 };
if (!termMatches(expected_term, result.term)) {
return step.fail("the following command {} (expected {}):\n{s}", .{
fmtTerm(result.term),
fmtTerm(expected_term),
try Step.allocPrintCmd(arena, self.cwd, final_argv),
});
}
if (!result.stdio.test_results.isSuccess()) {
return step.fail(
"the following test command failed:\n{s}",
.{try Step.allocPrintCmd(arena, self.cwd, final_argv)},
);
}
},
else => {
try step.handleChildProcessTerm(result.term, self.cwd, final_argv);
},
}
}
const ChildProcResult = struct {
term: std.process.Child.Term,
elapsed_ns: u64,
peak_rss: usize,
stdio: StdIoResult,
};
fn spawnChildAndCollect(
self: *RunStep,
argv: []const []const u8,
has_side_effects: bool,
prog_node: *std.Progress.Node,
) !ChildProcResult {
const b = self.step.owner;
const arena = b.allocator;
var child = std.process.Child.init(argv, arena);
if (self.cwd) |cwd| {
child.cwd = b.pathFromRoot(cwd);
} else {
child.cwd = b.build_root.path;
child.cwd_dir = b.build_root.handle;
}
child.env_map = self.env_map orelse b.env_map;
child.request_resource_usage_statistics = true;
child.stdin_behavior = switch (self.stdio) {
.infer_from_args => if (has_side_effects) .Inherit else .Ignore,
.inherit => .Inherit,
.check => .Ignore,
.zig_test => .Pipe,
};
child.stdout_behavior = switch (self.stdio) {
.infer_from_args => if (has_side_effects) .Inherit else .Ignore,
.inherit => .Inherit,
.check => |checks| if (checksContainStdout(checks.items)) .Pipe else .Ignore,
.zig_test => .Pipe,
};
child.stderr_behavior = switch (self.stdio) {
.infer_from_args => if (has_side_effects) .Inherit else .Pipe,
.inherit => .Inherit,
.check => .Pipe,
.zig_test => .Pipe,
};
if (self.captured_stdout != null) child.stdout_behavior = .Pipe;
if (self.captured_stderr != null) child.stderr_behavior = .Pipe;
if (self.stdin != null) {
assert(child.stdin_behavior != .Inherit);
child.stdin_behavior = .Pipe;
}
try child.spawn();
var timer = try std.time.Timer.start();
const result = if (self.stdio == .zig_test)
evalZigTest(self, &child, prog_node)
else
evalGeneric(self, &child);
const term = try child.wait();
const elapsed_ns = timer.read();
return .{
.stdio = try result,
.term = term,
.elapsed_ns = elapsed_ns,
.peak_rss = child.resource_usage_statistics.getMaxRss() orelse 0,
};
}
const StdIoResult = struct {
stdout: []const u8,
stderr: []const u8,
stdout_null: bool,
stderr_null: bool,
test_results: Step.TestResults,
};
fn evalZigTest(
self: *RunStep,
child: *std.process.Child,
prog_node: *std.Progress.Node,
) !StdIoResult {
const gpa = self.step.owner.allocator;
const arena = self.step.owner.allocator;
var poller = std.io.poll(gpa, enum { stdout, stderr }, .{
.stdout = child.stdout.?,
.stderr = child.stderr.?,
});
defer poller.deinit();
try sendMessage(child.stdin.?, .query_test_metadata);
const Header = std.zig.Server.Message.Header;
const stdout = poller.fifo(.stdout);
const stderr = poller.fifo(.stderr);
var fail_count: u32 = 0;
var skip_count: u32 = 0;
var leak_count: u32 = 0;
var test_count: u32 = 0;
var metadata: ?TestMetadata = null;
var sub_prog_node: ?std.Progress.Node = null;
defer if (sub_prog_node) |*n| n.end();
poll: while (true) {
while (stdout.readableLength() < @sizeOf(Header)) {
if (!(try poller.poll())) break :poll;
}
const header = stdout.reader().readStruct(Header) catch unreachable;
while (stdout.readableLength() < header.bytes_len) {
if (!(try poller.poll())) break :poll;
}
const body = stdout.readableSliceOfLen(header.bytes_len);
switch (header.tag) {
.zig_version => {
if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
return self.step.fail(
"zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
.{ builtin.zig_version_string, body },
);
}
},
.test_metadata => {
const TmHdr = std.zig.Server.Message.TestMetadata;
const tm_hdr = @ptrCast(*align(1) const TmHdr, body);
test_count = tm_hdr.tests_len;
const names_bytes = body[@sizeOf(TmHdr)..][0 .. test_count * @sizeOf(u32)];
const async_frame_lens_bytes = body[@sizeOf(TmHdr) + names_bytes.len ..][0 .. test_count * @sizeOf(u32)];
const expected_panic_msgs_bytes = body[@sizeOf(TmHdr) + names_bytes.len + async_frame_lens_bytes.len ..][0 .. test_count * @sizeOf(u32)];
const string_bytes = body[@sizeOf(TmHdr) + names_bytes.len + async_frame_lens_bytes.len + expected_panic_msgs_bytes.len ..][0..tm_hdr.string_bytes_len];
const names = std.mem.bytesAsSlice(u32, names_bytes);
const async_frame_lens = std.mem.bytesAsSlice(u32, async_frame_lens_bytes);
const expected_panic_msgs = std.mem.bytesAsSlice(u32, expected_panic_msgs_bytes);
const names_aligned = try arena.alloc(u32, names.len);
for (names_aligned, names) |*dest, src| dest.* = src;
const async_frame_lens_aligned = try arena.alloc(u32, async_frame_lens.len);
for (async_frame_lens_aligned, async_frame_lens) |*dest, src| dest.* = src;
const expected_panic_msgs_aligned = try arena.alloc(u32, expected_panic_msgs.len);
for (expected_panic_msgs_aligned, expected_panic_msgs) |*dest, src| dest.* = src;
prog_node.setEstimatedTotalItems(names.len);
metadata = .{
.string_bytes = try arena.dupe(u8, string_bytes),
.names = names_aligned,
.async_frame_lens = async_frame_lens_aligned,
.expected_panic_msgs = expected_panic_msgs_aligned,
.next_index = 0,
.prog_node = prog_node,
};
try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node);
},
.test_results => {
const md = metadata.?;
const TrHdr = std.zig.Server.Message.TestResults;
const tr_hdr = @ptrCast(*align(1) const TrHdr, body);
fail_count += @boolToInt(tr_hdr.flags.fail);
skip_count += @boolToInt(tr_hdr.flags.skip);
leak_count += @boolToInt(tr_hdr.flags.leak);
if (tr_hdr.flags.fail or tr_hdr.flags.leak) {
const name = std.mem.sliceTo(md.string_bytes[md.names[tr_hdr.index]..], 0);
const msg = std.mem.trim(u8, stderr.readableSlice(0), "\n");
const label = if (tr_hdr.flags.fail) "failed" else "leaked";
if (msg.len > 0) {
try self.step.addError("'{s}' {s}: {s}", .{ name, label, msg });
} else {
try self.step.addError("'{s}' {s}", .{ name, label });
}
stderr.discard(msg.len);
}
try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node);
},
else => {},
}
stdout.discard(body.len);
}
if (stderr.readableLength() > 0) {
const msg = std.mem.trim(u8, try stderr.toOwnedSlice(), "\n");
if (msg.len > 0) try self.step.result_error_msgs.append(arena, msg);
}
child.stdin.?.close();
child.stdin = null;
return .{
.stdout = &.{},
.stderr = &.{},
.stdout_null = true,
.stderr_null = true,
.test_results = .{
.test_count = test_count,
.fail_count = fail_count,
.skip_count = skip_count,
.leak_count = leak_count,
},
};
}
const TestMetadata = struct {
names: []const u32,
async_frame_lens: []const u32,
expected_panic_msgs: []const u32,
string_bytes: []const u8,
next_index: u32,
prog_node: *std.Progress.Node,
fn testName(tm: TestMetadata, index: u32) []const u8 {
return std.mem.sliceTo(tm.string_bytes[tm.names[index]..], 0);
}
};
fn requestNextTest(in: fs.File, metadata: *TestMetadata, sub_prog_node: *?std.Progress.Node) !void {
while (metadata.next_index < metadata.names.len) {
const i = metadata.next_index;
metadata.next_index += 1;
if (metadata.async_frame_lens[i] != 0) continue;
if (metadata.expected_panic_msgs[i] != 0) continue;
const name = metadata.testName(i);
if (sub_prog_node.*) |*n| n.end();
sub_prog_node.* = metadata.prog_node.start(name, 0);
try sendRunTestMessage(in, i);
return;
} else {
try sendMessage(in, .exit);
}
}
fn sendMessage(file: std.fs.File, tag: std.zig.Client.Message.Tag) !void {
const header: std.zig.Client.Message.Header = .{
.tag = tag,
.bytes_len = 0,
};
try file.writeAll(std.mem.asBytes(&header));
}
fn sendRunTestMessage(file: std.fs.File, index: u32) !void {
const header: std.zig.Client.Message.Header = .{
.tag = .run_test,
.bytes_len = 4,
};
const full_msg = std.mem.asBytes(&header) ++ std.mem.asBytes(&index);
try file.writeAll(full_msg);
}
fn evalGeneric(self: *RunStep, child: *std.process.Child) !StdIoResult {
const arena = self.step.owner.allocator;
if (self.stdin) |stdin| {
child.stdin.?.writeAll(stdin) catch |err| {
return self.step.fail("unable to write stdin: {s}", .{@errorName(err)});
};
child.stdin.?.close();
child.stdin = null;
}
var stdout_bytes: []const u8 = undefined;
var stderr_bytes: []const u8 = undefined;
var stdout_null = true;
var stderr_null = true;
if (child.stdout) |stdout| {
if (child.stderr) |stderr| {
var poller = std.io.poll(arena, enum { stdout, stderr }, .{
.stdout = stdout,
.stderr = stderr,
});
defer poller.deinit();
while (try poller.poll()) {
if (poller.fifo(.stdout).count > self.max_stdio_size)
return error.StdoutStreamTooLong;
if (poller.fifo(.stderr).count > self.max_stdio_size)
return error.StderrStreamTooLong;
}
stdout_bytes = try poller.fifo(.stdout).toOwnedSlice();
stderr_bytes = try poller.fifo(.stderr).toOwnedSlice();
stdout_null = false;
stderr_null = false;
} else {
stdout_bytes = try stdout.reader().readAllAlloc(arena, self.max_stdio_size);
stdout_null = false;
}
} else if (child.stderr) |stderr| {
stderr_bytes = try stderr.reader().readAllAlloc(arena, self.max_stdio_size);
stderr_null = false;
}
if (!stderr_null and stderr_bytes.len > 0) {
const stderr_is_diagnostic = self.captured_stderr == null and switch (self.stdio) {
.check => |checks| !checksContainStderr(checks.items),
else => true,
};
if (stderr_is_diagnostic) {
try self.step.result_error_msgs.append(arena, stderr_bytes);
}
}
return .{
.stdout = stdout_bytes,
.stderr = stderr_bytes,
.stdout_null = stdout_null,
.stderr_null = stderr_null,
.test_results = .{},
};
}
fn addPathForDynLibs(self: *RunStep, artifact: *CompileStep) void {
const b = self.step.owner;
for (artifact.link_objects.items) |link_object| {
switch (link_object) {
.other_step => |other| {
if (other.target.isWindows() and other.isDynamicLibrary()) {
addPathDir(self, fs.path.dirname(other.getOutputSource().getPath(b)).?);
addPathForDynLibs(self, other);
}
},
else => {},
}
}
}
fn failForeign(
self: *RunStep,
suggested_flag: []const u8,
argv0: []const u8,
exe: *CompileStep,
) error{ MakeFailed, MakeSkipped, OutOfMemory } {
switch (self.stdio) {
.check, .zig_test => {
if (self.skip_foreign_checks)
return error.MakeSkipped;
const b = self.step.owner;
const host_name = try b.host.target.zigTriple(b.allocator);
const foreign_name = try exe.target.zigTriple(b.allocator);
return self.step.fail(
\\unable to spawn foreign binary '{s}' ({s}) on host system ({s})
\\ consider using {s} or enabling skip_foreign_checks in the Run step
, .{ argv0, foreign_name, host_name, suggested_flag });
},
else => {
return self.step.fail("unable to spawn foreign binary '{s}'", .{argv0});
},
}
}
fn hashStdIo(hh: *std.Build.Cache.HashHelper, stdio: StdIo) void {
switch (stdio) {
.infer_from_args, .inherit, .zig_test => {},
.check => |checks| for (checks.items) |check| {
hh.add(@as(std.meta.Tag(StdIo.Check), check));
switch (check) {
.expect_stderr_exact,
.expect_stderr_match,
.expect_stdout_exact,
.expect_stdout_match,
=> |s| hh.addBytes(s),
.expect_term => |term| {
hh.add(@as(std.meta.Tag(std.process.Child.Term), term));
switch (term) {
.Exited => |x| hh.add(x),
.Signal, .Stopped, .Unknown => |x| hh.add(x),
}
},
}
},
}
}