mirror of
https://github.com/ziglang/zig.git
synced 2025-12-06 14:23:09 +00:00
532 lines
18 KiB
Zig
532 lines
18 KiB
Zig
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 max_stdout_size = 1 * 1024 * 1024; // 1 MiB
|
|
|
|
const RunStep = @This();
|
|
|
|
pub const base_id: Step.Id = .run;
|
|
|
|
step: Step,
|
|
builder: *std.Build,
|
|
|
|
/// See also addArg and addArgs to modifying this directly
|
|
argv: ArrayList(Arg),
|
|
|
|
/// Set this to modify the current working directory
|
|
cwd: ?[]const u8,
|
|
|
|
/// Override this field to modify the environment, or use setEnvironmentVariable
|
|
env_map: ?*EnvMap,
|
|
|
|
stdout_action: StdIoAction = .inherit,
|
|
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_term: ?std.ChildProcess.Term = .{ .Exited = 0 },
|
|
|
|
/// Print the command before running it
|
|
print: bool,
|
|
/// Controls whether execution is skipped if the output file is up-to-date.
|
|
/// The default is to always run if there is no output file, and to skip
|
|
/// running if all output files are up-to-date.
|
|
condition: enum { output_outdated, always } = .output_outdated,
|
|
|
|
/// Additional file paths relative to build.zig that, when modified, indicate
|
|
/// that the RunStep should be re-executed.
|
|
extra_file_dependencies: []const []const u8 = &.{},
|
|
|
|
pub const StdIoAction = union(enum) {
|
|
inherit,
|
|
ignore,
|
|
expect_exact: []const u8,
|
|
expect_matches: []const []const u8,
|
|
};
|
|
|
|
pub const Arg = union(enum) {
|
|
artifact: *CompileStep,
|
|
file_source: std.Build.FileSource,
|
|
bytes: []u8,
|
|
output: Output,
|
|
|
|
pub const Output = struct {
|
|
generated_file: *std.Build.GeneratedFile,
|
|
basename: []const u8,
|
|
};
|
|
};
|
|
|
|
pub fn create(builder: *std.Build, name: []const u8) *RunStep {
|
|
const self = builder.allocator.create(RunStep) catch @panic("OOM");
|
|
self.* = RunStep{
|
|
.builder = builder,
|
|
.step = Step.init(base_id, name, builder.allocator, make),
|
|
.argv = ArrayList(Arg).init(builder.allocator),
|
|
.cwd = null,
|
|
.env_map = null,
|
|
.print = builder.verbose,
|
|
};
|
|
return self;
|
|
}
|
|
|
|
pub fn addArtifactArg(self: *RunStep, artifact: *CompileStep) void {
|
|
self.argv.append(Arg{ .artifact = artifact }) catch @panic("OOM");
|
|
self.step.dependOn(&artifact.step);
|
|
}
|
|
|
|
/// This provides file path as a command line argument to the command being
|
|
/// run, and returns a FileSource which can be used as inputs to other APIs
|
|
/// throughout the build system.
|
|
pub fn addOutputFileArg(rs: *RunStep, basename: []const u8) std.Build.FileSource {
|
|
const generated_file = rs.builder.allocator.create(std.Build.GeneratedFile) catch @panic("OOM");
|
|
generated_file.* = .{ .step = &rs.step };
|
|
rs.argv.append(.{ .output = .{
|
|
.generated_file = generated_file,
|
|
.basename = rs.builder.dupe(basename),
|
|
} }) catch @panic("OOM");
|
|
|
|
return .{ .generated = generated_file };
|
|
}
|
|
|
|
pub fn addFileSourceArg(self: *RunStep, file_source: std.Build.FileSource) void {
|
|
self.argv.append(Arg{
|
|
.file_source = file_source.dupe(self.builder),
|
|
}) catch @panic("OOM");
|
|
file_source.addStepDependencies(&self.step);
|
|
}
|
|
|
|
pub fn addArg(self: *RunStep, arg: []const u8) void {
|
|
self.argv.append(Arg{ .bytes = self.builder.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 new_env_map = self.builder.allocator.create(EnvMap) catch @panic("OOM");
|
|
new_env_map.* = EnvMap.init(self.builder.allocator);
|
|
self.env_map = new_env_map;
|
|
}
|
|
|
|
pub fn addPathDir(self: *RunStep, search_path: []const u8) void {
|
|
addPathDirInternal(&self.step, self.builder, search_path);
|
|
}
|
|
|
|
/// For internal use only, users of `RunStep` should use `addPathDir` directly.
|
|
pub fn addPathDirInternal(step: *Step, builder: *std.Build, search_path: []const u8) void {
|
|
const env_map = getEnvMapInternal(step, builder.allocator);
|
|
|
|
const key = "PATH";
|
|
var prev_path = env_map.get(key);
|
|
|
|
if (prev_path) |pp| {
|
|
const new_path = builder.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, builder.dupePath(search_path)) catch @panic("OOM");
|
|
}
|
|
}
|
|
|
|
pub fn getEnvMap(self: *RunStep) *EnvMap {
|
|
return getEnvMapInternal(&self.step, self.builder.allocator);
|
|
}
|
|
|
|
fn getEnvMapInternal(step: *Step, allocator: Allocator) *EnvMap {
|
|
const maybe_env_map = switch (step.id) {
|
|
.run => step.cast(RunStep).?.env_map,
|
|
.emulatable_run => step.cast(std.Build.EmulatableRunStep).?.env_map,
|
|
else => unreachable,
|
|
};
|
|
return maybe_env_map orelse {
|
|
const env_map = allocator.create(EnvMap) catch @panic("OOM");
|
|
env_map.* = process.getEnvMap(allocator) catch @panic("unhandled error");
|
|
switch (step.id) {
|
|
.run => step.cast(RunStep).?.env_map = env_map,
|
|
.emulatable_run => step.cast(RunStep).?.env_map = env_map,
|
|
else => unreachable,
|
|
}
|
|
return env_map;
|
|
};
|
|
}
|
|
|
|
pub fn setEnvironmentVariable(self: *RunStep, key: []const u8, value: []const u8) void {
|
|
const env_map = self.getEnvMap();
|
|
env_map.put(
|
|
self.builder.dupe(key),
|
|
self.builder.dupe(value),
|
|
) catch @panic("unhandled error");
|
|
}
|
|
|
|
pub fn expectStdErrEqual(self: *RunStep, bytes: []const u8) void {
|
|
self.stderr_action = .{ .expect_exact = self.builder.dupe(bytes) };
|
|
}
|
|
|
|
pub fn expectStdOutEqual(self: *RunStep, bytes: []const u8) void {
|
|
self.stdout_action = .{ .expect_exact = self.builder.dupe(bytes) };
|
|
}
|
|
|
|
fn stdIoActionToBehavior(action: StdIoAction) std.ChildProcess.StdIo {
|
|
return switch (action) {
|
|
.ignore => .Ignore,
|
|
.inherit => .Inherit,
|
|
.expect_exact, .expect_matches => .Pipe,
|
|
};
|
|
}
|
|
|
|
fn needOutputCheck(self: RunStep) bool {
|
|
switch (self.condition) {
|
|
.always => return false,
|
|
.output_outdated => {},
|
|
}
|
|
if (self.extra_file_dependencies.len > 0) return true;
|
|
|
|
for (self.argv.items) |arg| switch (arg) {
|
|
.output => return true,
|
|
else => continue,
|
|
};
|
|
|
|
return false;
|
|
}
|
|
|
|
fn make(step: *Step) !void {
|
|
const self = @fieldParentPtr(RunStep, "step", step);
|
|
const need_output_check = self.needOutputCheck();
|
|
|
|
var argv_list = ArrayList([]const u8).init(self.builder.allocator);
|
|
var output_placeholders = ArrayList(struct {
|
|
index: usize,
|
|
output: Arg.Output,
|
|
}).init(self.builder.allocator);
|
|
|
|
var man = self.builder.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(self.builder);
|
|
try argv_list.append(file_path);
|
|
_ = try man.addFile(file_path, null);
|
|
},
|
|
.artifact => |artifact| {
|
|
if (artifact.target.isWindows()) {
|
|
// On Windows we don't have rpaths so we have to add .dll search paths to PATH
|
|
self.addPathForDynLibs(artifact);
|
|
}
|
|
const file_path = artifact.installed_path orelse
|
|
artifact.getOutputSource().getPath(self.builder);
|
|
|
|
try argv_list.append(file_path);
|
|
|
|
_ = try man.addFile(file_path, null);
|
|
},
|
|
.output => |output| {
|
|
man.hash.addBytes(output.basename);
|
|
// Add a placeholder into the argument list because we need the
|
|
// manifest hash to be updated with all arguments before the
|
|
// object directory is computed.
|
|
try argv_list.append("");
|
|
try output_placeholders.append(.{
|
|
.index = argv_list.items.len - 1,
|
|
.output = output,
|
|
});
|
|
},
|
|
}
|
|
}
|
|
|
|
if (need_output_check) {
|
|
for (self.extra_file_dependencies) |file_path| {
|
|
_ = try man.addFile(self.builder.pathFromRoot(file_path), null);
|
|
}
|
|
|
|
if (man.hit() catch |err| failWithCacheError(man, err)) {
|
|
// cache hit, skip running command
|
|
const digest = man.final();
|
|
for (output_placeholders.items) |placeholder| {
|
|
placeholder.output.generated_file.path = try self.builder.cache_root.join(
|
|
self.builder.allocator,
|
|
&.{ "o", &digest, placeholder.output.basename },
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const digest = man.final();
|
|
|
|
for (output_placeholders.items) |placeholder| {
|
|
const output_path = try self.builder.cache_root.join(
|
|
self.builder.allocator,
|
|
&.{ "o", &digest, placeholder.output.basename },
|
|
);
|
|
const output_dir = fs.path.dirname(output_path).?;
|
|
fs.cwd().makePath(output_dir) catch |err| {
|
|
std.debug.print("unable to make path {s}: {s}\n", .{ output_dir, @errorName(err) });
|
|
return err;
|
|
};
|
|
|
|
placeholder.output.generated_file.path = output_path;
|
|
argv_list.items[placeholder.index] = output_path;
|
|
}
|
|
}
|
|
|
|
try runCommand(
|
|
argv_list.items,
|
|
self.builder,
|
|
self.expected_term,
|
|
self.stdout_action,
|
|
self.stderr_action,
|
|
self.stdin_behavior,
|
|
self.env_map,
|
|
self.cwd,
|
|
self.print,
|
|
);
|
|
|
|
if (need_output_check) {
|
|
try man.writeManifest();
|
|
}
|
|
}
|
|
|
|
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_term: ?std.ChildProcess.Term,
|
|
stdout_action: StdIoAction,
|
|
stderr_action: StdIoAction,
|
|
stdin_behavior: std.ChildProcess.StdIo,
|
|
env_map: ?*EnvMap,
|
|
maybe_cwd: ?[]const u8,
|
|
print: bool,
|
|
) !void {
|
|
const cwd = if (maybe_cwd) |cwd| builder.pathFromRoot(cwd) else builder.build_root.path;
|
|
|
|
if (!std.process.can_spawn) {
|
|
const cmd = try std.mem.join(builder.allocator, " ", argv);
|
|
std.debug.print("the following command cannot be executed ({s} does not support spawning a child process):\n{s}", .{
|
|
@tagName(builtin.os.tag), cmd,
|
|
});
|
|
builder.allocator.free(cmd);
|
|
return ExecError.ExecNotSupported;
|
|
}
|
|
|
|
var child = std.ChildProcess.init(argv, builder.allocator);
|
|
child.cwd = cwd;
|
|
child.env_map = env_map orelse builder.env_map;
|
|
|
|
child.stdin_behavior = stdin_behavior;
|
|
child.stdout_behavior = stdIoActionToBehavior(stdout_action);
|
|
child.stderr_behavior = stdIoActionToBehavior(stderr_action);
|
|
|
|
if (print)
|
|
printCmd(cwd, argv);
|
|
|
|
child.spawn() catch |err| {
|
|
std.debug.print("Unable to spawn {s}: {s}\n", .{ argv[0], @errorName(err) });
|
|
return err;
|
|
};
|
|
|
|
// TODO need to poll to read these streams to prevent a deadlock (or rely on evented I/O).
|
|
|
|
var stdout: ?[]const u8 = null;
|
|
defer if (stdout) |s| builder.allocator.free(s);
|
|
|
|
switch (stdout_action) {
|
|
.expect_exact, .expect_matches => {
|
|
stdout = try child.stdout.?.reader().readAllAlloc(builder.allocator, max_stdout_size);
|
|
},
|
|
.inherit, .ignore => {},
|
|
}
|
|
|
|
var stderr: ?[]const u8 = null;
|
|
defer if (stderr) |s| builder.allocator.free(s);
|
|
|
|
switch (stderr_action) {
|
|
.expect_exact, .expect_matches => {
|
|
stderr = try child.stderr.?.reader().readAllAlloc(builder.allocator, max_stdout_size);
|
|
},
|
|
.inherit, .ignore => {},
|
|
}
|
|
|
|
const term = child.wait() catch |err| {
|
|
std.debug.print("Unable to spawn {s}: {s}\n", .{ argv[0], @errorName(err) });
|
|
return err;
|
|
};
|
|
|
|
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.UnexpectedExit;
|
|
}
|
|
|
|
switch (stderr_action) {
|
|
.inherit, .ignore => {},
|
|
.expect_exact => |expected_bytes| {
|
|
if (!mem.eql(u8, expected_bytes, stderr.?)) {
|
|
std.debug.print(
|
|
\\
|
|
\\========= Expected this stderr: =========
|
|
\\{s}
|
|
\\========= But found: ====================
|
|
\\{s}
|
|
\\
|
|
, .{ expected_bytes, stderr.? });
|
|
printCmd(cwd, argv);
|
|
return error.TestFailed;
|
|
}
|
|
},
|
|
.expect_matches => |matches| for (matches) |match| {
|
|
if (mem.indexOf(u8, stderr.?, match) == null) {
|
|
std.debug.print(
|
|
\\
|
|
\\========= Expected to find in stderr: =========
|
|
\\{s}
|
|
\\========= But stderr does not contain it: =====
|
|
\\{s}
|
|
\\
|
|
, .{ match, stderr.? });
|
|
printCmd(cwd, argv);
|
|
return error.TestFailed;
|
|
}
|
|
},
|
|
}
|
|
|
|
switch (stdout_action) {
|
|
.inherit, .ignore => {},
|
|
.expect_exact => |expected_bytes| {
|
|
if (!mem.eql(u8, expected_bytes, stdout.?)) {
|
|
std.debug.print(
|
|
\\
|
|
\\========= Expected this stdout: =========
|
|
\\{s}
|
|
\\========= But found: ====================
|
|
\\{s}
|
|
\\
|
|
, .{ expected_bytes, stdout.? });
|
|
printCmd(cwd, argv);
|
|
return error.TestFailed;
|
|
}
|
|
},
|
|
.expect_matches => |matches| for (matches) |match| {
|
|
if (mem.indexOf(u8, stdout.?, match) == null) {
|
|
std.debug.print(
|
|
\\
|
|
\\========= Expected to find in stdout: =========
|
|
\\{s}
|
|
\\========= But stdout does not contain it: =====
|
|
\\{s}
|
|
\\
|
|
, .{ match, stdout.? });
|
|
printCmd(cwd, argv);
|
|
return error.TestFailed;
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
fn failWithCacheError(man: std.Build.Cache.Manifest, err: anyerror) noreturn {
|
|
const i = man.failed_file_index orelse failWithSimpleError(err);
|
|
const pp = man.files.items[i].prefixed_path orelse failWithSimpleError(err);
|
|
const prefix = man.cache.prefixes()[pp.prefix].path orelse "";
|
|
std.debug.print("{s}: {s}/{s}\n", .{ @errorName(err), prefix, pp.sub_path });
|
|
std.process.exit(1);
|
|
}
|
|
|
|
fn failWithSimpleError(err: anyerror) noreturn {
|
|
std.debug.print("{s}\n", .{@errorName(err)});
|
|
std.process.exit(1);
|
|
}
|
|
|
|
fn printCmd(cwd: ?[]const u8, argv: []const []const u8) void {
|
|
if (cwd) |yes_cwd| std.debug.print("cd {s} && ", .{yes_cwd});
|
|
for (argv) |arg| {
|
|
std.debug.print("{s} ", .{arg});
|
|
}
|
|
std.debug.print("\n", .{});
|
|
}
|
|
|
|
fn addPathForDynLibs(self: *RunStep, artifact: *CompileStep) void {
|
|
addPathForDynLibsInternal(&self.step, self.builder, artifact);
|
|
}
|
|
|
|
/// This should only be used for internal usage, this is called automatically
|
|
/// for the user.
|
|
pub fn addPathForDynLibsInternal(step: *Step, builder: *std.Build, artifact: *CompileStep) void {
|
|
for (artifact.link_objects.items) |link_object| {
|
|
switch (link_object) {
|
|
.other_step => |other| {
|
|
if (other.target.isWindows() and other.isDynamicLibrary()) {
|
|
addPathDirInternal(step, builder, fs.path.dirname(other.getOutputSource().getPath(builder)).?);
|
|
addPathForDynLibsInternal(step, builder, other);
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
}
|