From fd26c12469a3eda2c99cf58bf951b2223099b9ef Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Fri, 8 Jul 2022 22:57:17 +0200 Subject: [PATCH] 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. --- lib/std/build.zig | 2 + lib/std/build/CheckObjectStep.zig | 11 +++ lib/std/build/RunCompareStep.zig | 147 ++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 lib/std/build/RunCompareStep.zig diff --git a/lib/std/build.zig b/lib/std/build.zig index 83e30b278f..d338aef4fb 100644 --- a/lib/std/build.zig +++ b/lib/std/build.zig @@ -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, diff --git a/lib/std/build/CheckObjectStep.zig b/lib/std/build/CheckObjectStep.zig index b807e1de45..be1e741ce6 100644 --- a/lib/std/build/CheckObjectStep.zig +++ b/lib/std/build/CheckObjectStep.zig @@ -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 diff --git a/lib/std/build/RunCompareStep.zig b/lib/std/build/RunCompareStep.zig new file mode 100644 index 0000000000..4e6396e010 --- /dev/null +++ b/lib/std/build/RunCompareStep.zig @@ -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; + }; +}