RunCompareStep: implement new step

This creates a new step that can run foreign binaries when
emulation is enabled using options such as `enable_qemu`.
When an incompatible binary is found, the binary will not be executed.
This differs from `RunStep` which will always execute a binary,
regardless of the compatibility.

This is useful for usecases where the user wishes to allow for running the
binary on any supported platform either natively or through emulation,
but not generate an error when met with an incompatibility.
The above is useful when creating test cases that rely on running the binary
and optionally verifying its output.

The addition of this Step was generated by the need for our linker tests.
For that reason, a handy function was created on `CheckObjectStep` to ease
the setup for that.
This commit is contained in:
Luuk de Gram 2022-07-08 22:57:17 +02:00
parent a8bfddfaea
commit fd26c12469
No known key found for this signature in database
GPG Key ID: A8CFE58E4DC7D664
3 changed files with 160 additions and 0 deletions

View File

@ -26,6 +26,7 @@ pub const CheckFileStep = @import("build/CheckFileStep.zig");
pub const CheckObjectStep = @import("build/CheckObjectStep.zig");
pub const InstallRawStep = @import("build/InstallRawStep.zig");
pub const OptionsStep = @import("build/OptionsStep.zig");
pub const RunCompareStep = @import("build/RunCompareStep.zig");
pub const Builder = struct {
install_tls: TopLevelStep,
@ -3604,6 +3605,7 @@ pub const Step = struct {
translate_c,
write_file,
run,
run_and_compare,
check_file,
check_object,
install_raw,

View File

@ -12,6 +12,7 @@ const CheckObjectStep = @This();
const Allocator = mem.Allocator;
const Builder = build.Builder;
const Step = build.Step;
const RunCompareStep = build.RunCompareStep;
pub const base_id = .check_obj;
@ -37,6 +38,16 @@ pub fn create(builder: *Builder, source: build.FileSource, obj_format: std.Targe
return self;
}
/// Runs and (optionally) compares the output of a binary.
/// Asserts `self` was generated from an executable step.
pub fn runAndCompare(self: *CheckObjectStep) *RunCompareStep {
const dependencies_len = self.step.dependencies.items.len;
assert(dependencies_len > 0);
const exe_step = self.step.dependencies.items[dependencies_len - 1];
const exe = exe_step.cast(std.build.LibExeObjStep).?;
return RunCompareStep.create(self.builder, "RunCompare", exe);
}
/// There two types of actions currently suported:
/// * `.match` - is the main building block of standard matchers with optional eat-all token `{*}`
/// and extractors by name such as `{n_value}`. Please note this action is very simplistic in nature

View File

@ -0,0 +1,147 @@
//! Unlike `RunStep` this step will provide emulation, when enabled, to run foreign binaries.
//! When a binary is foreign, but emulation for the target is disabled, the specified binary
//! will not be run and therefore also not validated against its output.
//! This step can be useful when wishing to run a built binary on multiple platforms,
//! without having to verify if it's possible to be ran against.
const std = @import("../std.zig");
const build = std.build;
const Step = std.build.Step;
const Builder = std.build.Builder;
const LibExeObjStep = std.build.LibExeObjStep;
const fs = std.fs;
const process = std.process;
const EnvMap = process.EnvMap;
const RunCompareStep = @This();
pub const step_id = .run_and_compare;
step: Step,
builder: *Builder,
/// The artifact (executable) to be run by this step
exe: *LibExeObjStep,
/// Set this to `null` to ignore the exit code for the purpose of determining a successful execution
expected_exit_code: ?u8 = 0,
/// Override this field to modify the environment
env_map: ?*EnvMap,
pub fn create(builder: *Builder, name: []const u8, artifact: *LibExeObjStep) *RunCompareStep {
std.debug.assert(artifact.kind == .exe or artifact.kind == .test_exe);
const self = builder.allocator.create(RunCompareStep) catch unreachable;
self.* = .{
.builder = builder,
.step = Step.init(.run_and_compare, name, builder.allocator, make),
.exe = artifact,
.env_map = null,
};
self.step.dependOn(&artifact.step);
return self;
}
fn make(step: *Step) !void {
const self = @fieldParentPtr(RunCompareStep, "step", step);
const host_info = self.builder.host;
const cwd = self.builder.build_root;
_ = cwd;
std.debug.print("Make called!\n", .{});
var argv_list = std.ArrayList([]const u8).init(self.builder.allocator);
_ = argv_list;
const need_cross_glibc = self.exe.target.isGnuLibC() and self.exe.is_linking_libc;
switch (host_info.getExternalExecutor(self.exe.target_info, .{
.qemu_fixes_dl = need_cross_glibc and self.builder.glibc_runtimes_dir != null,
.link_libc = self.exe.is_linking_libc,
})) {
.native => {},
.rosetta => if (!self.builder.enable_rosetta) return,
.wine => |bin_name| if (self.builder.enable_wine) {
try argv_list.append(bin_name);
} else return,
.qemu => |bin_name| if (self.builder.enable_qemu) {
const glibc_dir_arg = if (need_cross_glibc)
self.builder.glibc_runtimes_dir orelse return
else
null;
try argv_list.append(bin_name);
if (glibc_dir_arg) |dir| {
// TODO look into making this a call to `linuxTriple`. This
// needs the directory to be called "i686" rather than
// "i386" which is why we do it manually here.
const fmt_str = "{s}" ++ fs.path.sep_str ++ "{s}-{s}-{s}";
const cpu_arch = self.exe.target.getCpuArch();
const os_tag = self.exe.target.getOsTag();
const abi = self.exe.target.getAbi();
const cpu_arch_name: []const u8 = if (cpu_arch == .i386)
"i686"
else
@tagName(cpu_arch);
const full_dir = try std.fmt.allocPrint(self.builder.allocator, fmt_str, .{
dir, cpu_arch_name, @tagName(os_tag), @tagName(abi),
});
try argv_list.append("-L");
try argv_list.append(full_dir);
}
} else return,
.darling => |bin_name| if (self.builder.enable_darling) {
try argv_list.append(bin_name);
} else return,
.wasmtime => |bin_name| if (self.builder.enable_wasmtime) {
try argv_list.append(bin_name);
try argv_list.append("--dir=.");
} else return,
else => return, // on any failures we skip
}
if (self.exe.target.isWindows()) {
// On Windows we don't have rpaths so we have to add .dll search paths to PATH
self.addPathForDynLibs(self.exe);
}
const executable_path = self.exe.installed_path orelse self.exe.getOutputSource().getPath(self.builder);
try argv_list.append(executable_path);
}
fn addPathForDynLibs(self: *RunCompareStep, artifact: *LibExeObjStep) void {
for (artifact.link_objects.items) |link_object| {
switch (link_object) {
.other_step => |other| {
if (other.target.isWindows() and other.isDynamicLibrary()) {
self.addPathDir(fs.path.dirname(other.getOutputSource().getPath(self.builder)).?);
self.addPathForDynLibs(other);
}
},
else => {},
}
}
}
pub fn addPathDir(self: *RunCompareStep, search_path: []const u8) void {
const env_map = self.getEnvMap();
const key = "PATH";
var prev_path = env_map.get(key);
if (prev_path) |pp| {
const new_path = self.builder.fmt("{s}" ++ [1]u8{fs.path.delimiter} ++ "{s}", .{ pp, search_path });
env_map.put(key, new_path) catch unreachable;
} else {
env_map.put(key, self.builder.dupePath(search_path)) catch unreachable;
}
}
pub fn getEnvMap(self: *RunCompareStep) *EnvMap {
return self.env_map orelse {
const env_map = self.builder.allocator.create(EnvMap) catch unreachable;
env_map.* = process.getEnvMap(self.builder.allocator) catch unreachable;
self.env_map = env_map;
return env_map;
};
}