Commit a690a5085d

Andrew Kelley <andrew@ziglang.org>
2020-01-05 08:01:28
rework and improve some of the zig build steps
* `RunStep` moved to lib/std/build/run.zig and gains ability to compare output and exit code against expected values. Multiple redundant locations in the test harness code are replaced to use `RunStep`. * `WriteFileStep` moved to lib/std/build/write_file.zig and gains ability to write more than one file into the cache directory, for when the files need to be relative to each other. This makes usage of `WriteFileStep` no longer problematic when parallelizing zig build. * Added `CheckFileStep`, which can be used to validate that the output of another step produced a valid file. Multiple redundant locations in the test harness code are replaced to use `CheckFileStep`. * Added `TranslateCStep`. This exposes `zig translate-c` to the build system, which is likely to be rarely useful by most Zig users; however Zig's own test suite uses it both for translate-c tests and for run-translated-c tests. * Refactored ad-hoc code to handle source files coming from multiple kinds of sources, into `std.build.FileSource`. * Added `std.build.Builder.addExecutableFromWriteFileStep`. * Added `std.build.Builder.addExecutableSource`. * Added `std.build.Builder.addWriteFiles`. * Added `std.build.Builder.addTranslateC`. * Added `std.build.LibExeObjStep.addCSourceFileSource`. * Added `std.build.LibExeObjStep.addAssemblyFileFromWriteFileStep`. * Added `std.build.LibExeObjStep.addAssemblyFileSource`. * Exposed `std.fs.base64_encoder`.
1 parent 14fcfe2
lib/std/build/check_file.zig
@@ -0,0 +1,52 @@
+const std = @import("../std.zig");
+const build = std.build;
+const Step = build.Step;
+const Builder = build.Builder;
+const fs = std.fs;
+const mem = std.mem;
+const warn = std.debug.warn;
+
+pub const CheckFileStep = struct {
+    step: Step,
+    builder: *Builder,
+    expected_matches: []const []const u8,
+    source: build.FileSource,
+    max_bytes: usize = 20 * 1024 * 1024,
+
+    pub fn create(
+        builder: *Builder,
+        source: build.FileSource,
+        expected_matches: []const []const u8,
+    ) *CheckFileStep {
+        const self = builder.allocator.create(CheckFileStep) catch unreachable;
+        self.* = CheckFileStep{
+            .builder = builder,
+            .step = Step.init("CheckFile", builder.allocator, make),
+            .source = source,
+            .expected_matches = expected_matches,
+        };
+        self.source.addStepDependencies(&self.step);
+        return self;
+    }
+
+    fn make(step: *Step) !void {
+        const self = @fieldParentPtr(CheckFileStep, "step", step);
+
+        const src_path = self.source.getPath(self.builder);
+        const contents = try fs.cwd().readFileAlloc(self.builder.allocator, src_path, self.max_bytes);
+
+        for (self.expected_matches) |expected_match| {
+            if (mem.indexOf(u8, contents, expected_match) == null) {
+                warn(
+                    \\
+                    \\========= Expected to find: ===================
+                    \\{}
+                    \\========= But file does not contain it: =======
+                    \\{}
+                    \\
+                , .{ expected_match, contents });
+                return error.TestFailed;
+            }
+        }
+    }
+};
lib/std/build/run.zig
@@ -0,0 +1,302 @@
+const std = @import("../std.zig");
+const builtin = std.builtin;
+const build = std.build;
+const Step = build.Step;
+const Builder = build.Builder;
+const LibExeObjStep = build.LibExeObjStep;
+const fs = std.fs;
+const mem = std.mem;
+const process = std.process;
+const ArrayList = std.ArrayList;
+const BufMap = std.BufMap;
+const Buffer = std.Buffer;
+const warn = std.debug.warn;
+
+const max_stdout_size = 1 * 1024 * 1024; // 1 MiB
+
+pub const RunStep = struct {
+    step: Step,
+    builder: *Builder,
+
+    /// 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: ?*BufMap,
+
+    stdout_action: StdIoAction = .inherit,
+    stderr_action: StdIoAction = .inherit,
+
+    expected_exit_code: u8 = 0,
+
+    pub const StdIoAction = union(enum) {
+        inherit,
+        ignore,
+        expect_exact: []const u8,
+        expect_matches: []const []const u8,
+    };
+
+    pub const Arg = union(enum) {
+        Artifact: *LibExeObjStep,
+        Bytes: []u8,
+    };
+
+    pub fn create(builder: *Builder, name: []const u8) *RunStep {
+        const self = builder.allocator.create(RunStep) catch unreachable;
+        self.* = RunStep{
+            .builder = builder,
+            .step = Step.init(name, builder.allocator, make),
+            .argv = ArrayList(Arg).init(builder.allocator),
+            .cwd = null,
+            .env_map = null,
+        };
+        return self;
+    }
+
+    pub fn addArtifactArg(self: *RunStep, artifact: *LibExeObjStep) void {
+        self.argv.append(Arg{ .Artifact = artifact }) catch unreachable;
+        self.step.dependOn(&artifact.step);
+    }
+
+    pub fn addArg(self: *RunStep, arg: []const u8) void {
+        self.argv.append(Arg{ .Bytes = self.builder.dupe(arg) }) catch unreachable;
+    }
+
+    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(BufMap) catch unreachable;
+        new_env_map.* = BufMap.init(self.builder.allocator);
+        self.env_map = new_env_map;
+    }
+
+    pub fn addPathDir(self: *RunStep, search_path: []const u8) void {
+        const env_map = self.getEnvMap();
+
+        var key: []const u8 = undefined;
+        var prev_path: ?[]const u8 = undefined;
+        if (builtin.os == .windows) {
+            key = "Path";
+            prev_path = env_map.get(key);
+            if (prev_path == null) {
+                key = "PATH";
+                prev_path = env_map.get(key);
+            }
+        } else {
+            key = "PATH";
+            prev_path = env_map.get(key);
+        }
+
+        if (prev_path) |pp| {
+            const new_path = self.builder.fmt("{}" ++ [1]u8{fs.path.delimiter} ++ "{}", .{ pp, search_path });
+            env_map.set(key, new_path) catch unreachable;
+        } else {
+            env_map.set(key, search_path) catch unreachable;
+        }
+    }
+
+    pub fn getEnvMap(self: *RunStep) *BufMap {
+        return self.env_map orelse {
+            const env_map = self.builder.allocator.create(BufMap) catch unreachable;
+            env_map.* = process.getEnvMap(self.builder.allocator) catch unreachable;
+            self.env_map = env_map;
+            return env_map;
+        };
+    }
+
+    pub fn setEnvironmentVariable(self: *RunStep, key: []const u8, value: []const u8) void {
+        const env_map = self.getEnvMap();
+        env_map.set(key, value) catch unreachable;
+    }
+
+    pub fn expectStdErrEqual(self: *RunStep, bytes: []const u8) void {
+        self.stderr_action = .{ .expect_exact = bytes };
+    }
+
+    pub fn expectStdOutEqual(self: *RunStep, bytes: []const u8) void {
+        self.stdout_action = .{ .expect_exact = bytes };
+    }
+
+    fn stdIoActionToBehavior(action: StdIoAction) std.ChildProcess.StdIo {
+        return switch (action) {
+            .ignore => .Ignore,
+            .inherit => .Inherit,
+            .expect_exact, .expect_matches => .Pipe,
+        };
+    }
+
+    fn make(step: *Step) !void {
+        const self = @fieldParentPtr(RunStep, "step", step);
+
+        const cwd = if (self.cwd) |cwd| self.builder.pathFromRoot(cwd) else self.builder.build_root;
+
+        var argv_list = ArrayList([]const u8).init(self.builder.allocator);
+        for (self.argv.toSlice()) |arg| {
+            switch (arg) {
+                Arg.Bytes => |bytes| try argv_list.append(bytes),
+                Arg.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 executable_path = artifact.installed_path orelse artifact.getOutputPath();
+                    try argv_list.append(executable_path);
+                },
+            }
+        }
+
+        const argv = argv_list.toSliceConst();
+
+        const child = std.ChildProcess.init(argv, self.builder.allocator) catch unreachable;
+        defer child.deinit();
+
+        child.cwd = cwd;
+        child.env_map = self.env_map orelse self.builder.env_map;
+
+        child.stdin_behavior = .Ignore;
+        child.stdout_behavior = stdIoActionToBehavior(self.stdout_action);
+        child.stderr_behavior = stdIoActionToBehavior(self.stderr_action);
+
+        child.spawn() catch |err| {
+            warn("Unable to spawn {}: {}\n", .{ argv[0], @errorName(err) });
+            return err;
+        };
+
+        var stdout = Buffer.initNull(self.builder.allocator);
+        var stderr = Buffer.initNull(self.builder.allocator);
+
+        // TODO need to poll to read these streams to prevent a deadlock (or rely on evented I/O).
+
+        switch (self.stdout_action) {
+            .expect_exact, .expect_matches => {
+                var stdout_file_in_stream = child.stdout.?.inStream();
+                stdout_file_in_stream.stream.readAllBuffer(&stdout, max_stdout_size) catch unreachable;
+            },
+            .inherit, .ignore => {},
+        }
+
+        switch (self.stdout_action) {
+            .expect_exact, .expect_matches => {
+                var stderr_file_in_stream = child.stderr.?.inStream();
+                stderr_file_in_stream.stream.readAllBuffer(&stderr, max_stdout_size) catch unreachable;
+            },
+            .inherit, .ignore => {},
+        }
+
+        const term = child.wait() catch |err| {
+            warn("Unable to spawn {}: {}\n", .{ argv[0], @errorName(err) });
+            return err;
+        };
+
+        switch (term) {
+            .Exited => |code| {
+                if (code != self.expected_exit_code) {
+                    warn("The following command exited with error code {} (expected {}):\n", .{
+                        code,
+                        self.expected_exit_code,
+                    });
+                    printCmd(cwd, argv);
+                    return error.UncleanExit;
+                }
+            },
+            else => {
+                warn("The following command terminated unexpectedly:\n", .{});
+                printCmd(cwd, argv);
+                return error.UncleanExit;
+            },
+        }
+
+        switch (self.stderr_action) {
+            .inherit, .ignore => {},
+            .expect_exact => |expected_bytes| {
+                if (!mem.eql(u8, expected_bytes, stderr.toSliceConst())) {
+                    warn(
+                        \\
+                        \\========= Expected this stderr: =========
+                        \\{}
+                        \\========= But found: ====================
+                        \\{}
+                        \\
+                    , .{ expected_bytes, stderr.toSliceConst() });
+                    printCmd(cwd, argv);
+                    return error.TestFailed;
+                }
+            },
+            .expect_matches => |matches| for (matches) |match| {
+                if (mem.indexOf(u8, stderr.toSliceConst(), match) == null) {
+                    warn(
+                        \\
+                        \\========= Expected to find in stderr: =========
+                        \\{}
+                        \\========= But stderr does not contain it: =====
+                        \\{}
+                        \\
+                    , .{ match, stderr.toSliceConst() });
+                    printCmd(cwd, argv);
+                    return error.TestFailed;
+                }
+            },
+        }
+
+        switch (self.stdout_action) {
+            .inherit, .ignore => {},
+            .expect_exact => |expected_bytes| {
+                if (!mem.eql(u8, expected_bytes, stdout.toSliceConst())) {
+                    warn(
+                        \\
+                        \\========= Expected this stdout: =========
+                        \\{}
+                        \\========= But found: ====================
+                        \\{}
+                        \\
+                    , .{ expected_bytes, stdout.toSliceConst() });
+                    printCmd(cwd, argv);
+                    return error.TestFailed;
+                }
+            },
+            .expect_matches => |matches| for (matches) |match| {
+                if (mem.indexOf(u8, stdout.toSliceConst(), match) == null) {
+                    warn(
+                        \\
+                        \\========= Expected to find in stdout: =========
+                        \\{}
+                        \\========= But stdout does not contain it: =====
+                        \\{}
+                        \\
+                    , .{ match, stdout.toSliceConst() });
+                    printCmd(cwd, argv);
+                    return error.TestFailed;
+                }
+            },
+        }
+    }
+
+    fn printCmd(cwd: ?[]const u8, argv: []const []const u8) void {
+        if (cwd) |yes_cwd| warn("cd {} && ", .{yes_cwd});
+        for (argv) |arg| {
+            warn("{} ", .{arg});
+        }
+        warn("\n", .{});
+    }
+
+    fn addPathForDynLibs(self: *RunStep, artifact: *LibExeObjStep) void {
+        for (artifact.link_objects.toSliceConst()) |link_object| {
+            switch (link_object) {
+                .OtherStep => |other| {
+                    if (other.target.isWindows() and other.isDynamicLibrary()) {
+                        self.addPathDir(fs.path.dirname(other.getOutputPath()).?);
+                        self.addPathForDynLibs(other);
+                    }
+                },
+                else => {},
+            }
+        }
+    }
+};
lib/std/build/translate_c.zig
@@ -0,0 +1,73 @@
+const std = @import("../std.zig");
+const build = std.build;
+const Step = build.Step;
+const Builder = build.Builder;
+const WriteFileStep = build.WriteFileStep;
+const LibExeObjStep = build.LibExeObjStep;
+const CheckFileStep = build.CheckFileStep;
+const fs = std.fs;
+const mem = std.mem;
+
+pub const TranslateCStep = struct {
+    step: Step,
+    builder: *Builder,
+    source: build.FileSource,
+    output_dir: ?[]const u8,
+    out_basename: []const u8,
+
+    pub fn create(builder: *Builder, source: build.FileSource) *TranslateCStep {
+        const self = builder.allocator.create(TranslateCStep) catch unreachable;
+        self.* = TranslateCStep{
+            .step = Step.init("zig translate-c", builder.allocator, make),
+            .builder = builder,
+            .source = source,
+            .output_dir = null,
+            .out_basename = undefined,
+        };
+        source.addStepDependencies(&self.step);
+        return self;
+    }
+
+    /// Unless setOutputDir was called, this function must be called only in
+    /// the make step, from a step that has declared a dependency on this one.
+    /// To run an executable built with zig build, use `run`, or create an install step and invoke it.
+    pub fn getOutputPath(self: *TranslateCStep) []const u8 {
+        return fs.path.join(
+            self.builder.allocator,
+            &[_][]const u8{ self.output_dir.?, self.out_basename },
+        ) catch unreachable;
+    }
+
+    /// Creates a step to build an executable from the translated source.
+    pub fn addExecutable(self: *TranslateCStep) *LibExeObjStep {
+        return self.builder.addExecutableSource("translated_c", @as(build.FileSource, .{ .translate_c = self }));
+    }
+
+    pub fn addCheckFile(self: *TranslateCStep, expected_matches: []const []const u8) *CheckFileStep {
+        return CheckFileStep.create(self.builder, .{ .translate_c = self }, expected_matches);
+    }
+
+    fn make(step: *Step) !void {
+        const self = @fieldParentPtr(TranslateCStep, "step", step);
+
+        const argv = [_][]const u8{
+            self.builder.zig_exe,
+            "translate-c",
+            "-lc",
+            "--cache",
+            "on",
+            self.source.getPath(self.builder),
+        };
+
+        const output_path_nl = try self.builder.exec(&argv);
+        const output_path = mem.trimRight(u8, output_path_nl, "\r\n");
+
+        self.out_basename = fs.path.basename(output_path);
+        if (self.output_dir) |output_dir| {
+            const full_dest = try fs.path.join(self.builder.allocator, &[_][]const u8{ output_dir, self.out_basename });
+            try self.builder.updateFile(output_path, full_dest);
+        } else {
+            self.output_dir = fs.path.dirname(output_path).?;
+        }
+    }
+};
lib/std/build/write_file.zig
@@ -0,0 +1,94 @@
+const std = @import("../std.zig");
+const build = @import("../build.zig");
+const Step = build.Step;
+const Builder = build.Builder;
+const fs = std.fs;
+const warn = std.debug.warn;
+const ArrayList = std.ArrayList;
+
+pub const WriteFileStep = struct {
+    step: Step,
+    builder: *Builder,
+    output_dir: []const u8,
+    files: ArrayList(File),
+
+    pub const File = struct {
+        basename: []const u8,
+        bytes: []const u8,
+    };
+
+    pub fn init(builder: *Builder) WriteFileStep {
+        return WriteFileStep{
+            .builder = builder,
+            .step = Step.init("writefile", builder.allocator, make),
+            .files = ArrayList(File).init(builder.allocator),
+            .output_dir = undefined,
+        };
+    }
+
+    pub fn add(self: *WriteFileStep, basename: []const u8, bytes: []const u8) void {
+        self.files.append(.{ .basename = basename, .bytes = bytes }) catch unreachable;
+    }
+
+    /// Unless setOutputDir was called, this function must be called only in
+    /// the make step, from a step that has declared a dependency on this one.
+    /// To run an executable built with zig build, use `run`, or create an install step and invoke it.
+    pub fn getOutputPath(self: *WriteFileStep, basename: []const u8) []const u8 {
+        return fs.path.join(
+            self.builder.allocator,
+            &[_][]const u8{ self.output_dir, basename },
+        ) catch unreachable;
+    }
+
+    fn make(step: *Step) !void {
+        const self = @fieldParentPtr(WriteFileStep, "step", step);
+
+        // The cache is used here not really as a way to speed things up - because writing
+        // the data to a file would probably be very fast - but as a way to find a canonical
+        // location to put build artifacts.
+
+        // If, for example, a hard-coded path was used as the location to put WriteFileStep
+        // files, then two WriteFileSteps executing in parallel might clobber each other.
+
+        // TODO port the cache system from stage1 to zig std lib. Until then we use blake2b
+        // directly and construct the path, and no "cache hit" detection happens; the files
+        // are always written.
+        var hash = std.crypto.Blake2b384.init();
+
+        // Random bytes to make WriteFileStep unique. Refresh this with
+        // new random bytes when WriteFileStep implementation is modified
+        // in a non-backwards-compatible way.
+        hash.update("eagVR1dYXoE7ARDP");
+        for (self.files.toSliceConst()) |file| {
+            hash.update(file.basename);
+            hash.update(file.bytes);
+            hash.update("|");
+        }
+        var digest: [48]u8 = undefined;
+        hash.final(&digest);
+        var hash_basename: [64]u8 = undefined;
+        fs.base64_encoder.encode(&hash_basename, &digest);
+        self.output_dir = try fs.path.join(self.builder.allocator, &[_][]const u8{
+            self.builder.cache_root,
+            "o",
+            &hash_basename,
+        });
+        // TODO replace with something like fs.makePathAndOpenDir
+        fs.makePath(self.builder.allocator, self.output_dir) catch |err| {
+            warn("unable to make path {}: {}\n", .{ self.output_dir, @errorName(err) });
+            return err;
+        };
+        var dir = try fs.cwd().openDirTraverse(self.output_dir);
+        defer dir.close();
+        for (self.files.toSliceConst()) |file| {
+            dir.writeFile(file.basename, file.bytes) catch |err| {
+                warn("unable to write {} into {}: {}\n", .{
+                    file.basename,
+                    self.output_dir,
+                    @errorName(err),
+                });
+                return err;
+            };
+        }
+    }
+};
lib/std/build.zig
@@ -17,6 +17,10 @@ const fmt_lib = std.fmt;
 const File = std.fs.File;
 
 pub const FmtStep = @import("build/fmt.zig").FmtStep;
+pub const TranslateCStep = @import("build/translate_c.zig").TranslateCStep;
+pub const WriteFileStep = @import("build/write_file.zig").WriteFileStep;
+pub const RunStep = @import("build/run.zig").RunStep;
+pub const CheckFileStep = @import("build/check_file.zig").CheckFileStep;
 
 pub const Builder = struct {
     install_tls: TopLevelStep,
@@ -203,23 +207,53 @@ pub const Builder = struct {
     }
 
     pub fn addExecutable(self: *Builder, name: []const u8, root_src: ?[]const u8) *LibExeObjStep {
+        return LibExeObjStep.createExecutable(
+            self,
+            name,
+            if (root_src) |p| FileSource{ .path = p } else null,
+            false,
+        );
+    }
+
+    pub fn addExecutableFromWriteFileStep(
+        self: *Builder,
+        name: []const u8,
+        wfs: *WriteFileStep,
+        basename: []const u8,
+    ) *LibExeObjStep {
+        return LibExeObjStep.createExecutable(self, name, @as(FileSource, .{
+            .write_file = .{
+                .step = wfs,
+                .basename = basename,
+            },
+        }), false);
+    }
+
+    pub fn addExecutableSource(
+        self: *Builder,
+        name: []const u8,
+        root_src: ?FileSource,
+    ) *LibExeObjStep {
         return LibExeObjStep.createExecutable(self, name, root_src, false);
     }
 
     pub fn addObject(self: *Builder, name: []const u8, root_src: ?[]const u8) *LibExeObjStep {
-        return LibExeObjStep.createObject(self, name, root_src);
+        const root_src_param = if (root_src) |p| @as(FileSource, .{ .path = p }) else null;
+        return LibExeObjStep.createObject(self, name, root_src_param);
     }
 
     pub fn addSharedLibrary(self: *Builder, name: []const u8, root_src: ?[]const u8, ver: Version) *LibExeObjStep {
-        return LibExeObjStep.createSharedLibrary(self, name, root_src, ver);
+        const root_src_param = if (root_src) |p| @as(FileSource, .{ .path = p }) else null;
+        return LibExeObjStep.createSharedLibrary(self, name, root_src_param, ver);
     }
 
     pub fn addStaticLibrary(self: *Builder, name: []const u8, root_src: ?[]const u8) *LibExeObjStep {
-        return LibExeObjStep.createStaticLibrary(self, name, root_src);
+        const root_src_param = if (root_src) |p| @as(FileSource, .{ .path = p }) else null;
+        return LibExeObjStep.createStaticLibrary(self, name, root_src_param);
     }
 
     pub fn addTest(self: *Builder, root_src: []const u8) *LibExeObjStep {
-        return LibExeObjStep.createTest(self, "test", root_src);
+        return LibExeObjStep.createTest(self, "test", .{ .path = root_src });
     }
 
     pub fn addAssemble(self: *Builder, name: []const u8, src: []const u8) *LibExeObjStep {
@@ -256,8 +290,14 @@ pub const Builder = struct {
     }
 
     pub fn addWriteFile(self: *Builder, file_path: []const u8, data: []const u8) *WriteFileStep {
+        const write_file_step = self.addWriteFiles();
+        write_file_step.add(file_path, data);
+        return write_file_step;
+    }
+
+    pub fn addWriteFiles(self: *Builder) *WriteFileStep {
         const write_file_step = self.allocator.create(WriteFileStep) catch unreachable;
-        write_file_step.* = WriteFileStep.init(self, file_path, data);
+        write_file_step.* = WriteFileStep.init(self);
         return write_file_step;
     }
 
@@ -278,6 +318,10 @@ pub const Builder = struct {
         return FmtStep.create(self, paths);
     }
 
+    pub fn addTranslateC(self: *Builder, source: FileSource) *TranslateCStep {
+        return TranslateCStep.create(self, source);
+    }
+
     pub fn version(self: *const Builder, major: u32, minor: u32, patch: u32) Version {
         return Version{
             .major = major,
@@ -1002,7 +1046,7 @@ const Pkg = struct {
 };
 
 const CSourceFile = struct {
-    source_path: []const u8,
+    source: FileSource,
     args: []const []const u8,
 };
 
@@ -1015,6 +1059,33 @@ fn isLibCLibrary(name: []const u8) bool {
     return false;
 }
 
+pub const FileSource = union(enum) {
+    /// Relative to build root
+    path: []const u8,
+    write_file: struct {
+        step: *WriteFileStep,
+        basename: []const u8,
+    },
+    translate_c: *TranslateCStep,
+
+    pub fn addStepDependencies(self: FileSource, step: *Step) void {
+        switch (self) {
+            .path => {},
+            .write_file => |wf| step.dependOn(&wf.step.step),
+            .translate_c => |tc| step.dependOn(&tc.step),
+        }
+    }
+
+    /// Should only be called during make()
+    pub fn getPath(self: FileSource, builder: *Builder) []const u8 {
+        return switch (self) {
+            .path => |p| builder.pathFromRoot(p),
+            .write_file => |wf| wf.step.getOutputPath(wf.basename),
+            .translate_c => |tc| tc.getOutputPath(),
+        };
+    }
+};
+
 pub const LibExeObjStep = struct {
     step: Step,
     builder: *Builder,
@@ -1047,7 +1118,7 @@ pub const LibExeObjStep = struct {
     filter: ?[]const u8,
     single_threaded: bool,
 
-    root_src: ?[]const u8,
+    root_src: ?FileSource,
     out_h_filename: []const u8,
     out_lib_filename: []const u8,
     out_pdb_filename: []const u8,
@@ -1099,7 +1170,7 @@ pub const LibExeObjStep = struct {
         StaticPath: []const u8,
         OtherStep: *LibExeObjStep,
         SystemLib: []const u8,
-        AssemblyFile: []const u8,
+        AssemblyFile: FileSource,
         CSourceFile: *CSourceFile,
     };
 
@@ -1116,37 +1187,44 @@ pub const LibExeObjStep = struct {
         Test,
     };
 
-    pub fn createSharedLibrary(builder: *Builder, name: []const u8, root_src: ?[]const u8, ver: Version) *LibExeObjStep {
+    pub fn createSharedLibrary(builder: *Builder, name: []const u8, root_src: ?FileSource, ver: Version) *LibExeObjStep {
         const self = builder.allocator.create(LibExeObjStep) catch unreachable;
         self.* = initExtraArgs(builder, name, root_src, Kind.Lib, true, ver);
         return self;
     }
 
-    pub fn createStaticLibrary(builder: *Builder, name: []const u8, root_src: ?[]const u8) *LibExeObjStep {
+    pub fn createStaticLibrary(builder: *Builder, name: []const u8, root_src: ?FileSource) *LibExeObjStep {
         const self = builder.allocator.create(LibExeObjStep) catch unreachable;
         self.* = initExtraArgs(builder, name, root_src, Kind.Lib, false, builder.version(0, 0, 0));
         return self;
     }
 
-    pub fn createObject(builder: *Builder, name: []const u8, root_src: ?[]const u8) *LibExeObjStep {
+    pub fn createObject(builder: *Builder, name: []const u8, root_src: ?FileSource) *LibExeObjStep {
         const self = builder.allocator.create(LibExeObjStep) catch unreachable;
         self.* = initExtraArgs(builder, name, root_src, Kind.Obj, false, builder.version(0, 0, 0));
         return self;
     }
 
-    pub fn createExecutable(builder: *Builder, name: []const u8, root_src: ?[]const u8, is_dynamic: bool) *LibExeObjStep {
+    pub fn createExecutable(builder: *Builder, name: []const u8, root_src: ?FileSource, is_dynamic: bool) *LibExeObjStep {
         const self = builder.allocator.create(LibExeObjStep) catch unreachable;
         self.* = initExtraArgs(builder, name, root_src, Kind.Exe, is_dynamic, builder.version(0, 0, 0));
         return self;
     }
 
-    pub fn createTest(builder: *Builder, name: []const u8, root_src: []const u8) *LibExeObjStep {
+    pub fn createTest(builder: *Builder, name: []const u8, root_src: FileSource) *LibExeObjStep {
         const self = builder.allocator.create(LibExeObjStep) catch unreachable;
         self.* = initExtraArgs(builder, name, root_src, Kind.Test, false, builder.version(0, 0, 0));
         return self;
     }
 
-    fn initExtraArgs(builder: *Builder, name: []const u8, root_src: ?[]const u8, kind: Kind, is_dynamic: bool, ver: Version) LibExeObjStep {
+    fn initExtraArgs(
+        builder: *Builder,
+        name: []const u8,
+        root_src: ?FileSource,
+        kind: Kind,
+        is_dynamic: bool,
+        ver: Version,
+    ) LibExeObjStep {
         if (mem.indexOf(u8, name, "/") != null or mem.indexOf(u8, name, "\\") != null) {
             panic("invalid name: '{}'. It looks like a file path, but it is supposed to be the library or application name.", .{name});
         }
@@ -1196,6 +1274,7 @@ pub const LibExeObjStep = struct {
             .install_step = null,
         };
         self.computeOutFileNames();
+        if (root_src) |rs| rs.addStepDependencies(&self.step);
         return self;
     }
 
@@ -1486,15 +1565,22 @@ pub const LibExeObjStep = struct {
     }
 
     pub fn addCSourceFile(self: *LibExeObjStep, file: []const u8, args: []const []const u8) void {
+        self.addCSourceFileSource(.{
+            .args = args,
+            .source = .{ .path = file },
+        });
+    }
+
+    pub fn addCSourceFileSource(self: *LibExeObjStep, source: CSourceFile) void {
         const c_source_file = self.builder.allocator.create(CSourceFile) catch unreachable;
-        const args_copy = self.builder.allocator.alloc([]u8, args.len) catch unreachable;
-        for (args) |arg, i| {
+
+        const args_copy = self.builder.allocator.alloc([]u8, source.args.len) catch unreachable;
+        for (source.args) |arg, i| {
             args_copy[i] = self.builder.dupe(arg);
         }
-        c_source_file.* = CSourceFile{
-            .source_path = self.builder.dupe(file),
-            .args = args_copy,
-        };
+
+        c_source_file.* = source;
+        c_source_file.args = args_copy;
         self.link_objects.append(LinkObject{ .CSourceFile = c_source_file }) catch unreachable;
     }
 
@@ -1571,6 +1657,20 @@ pub const LibExeObjStep = struct {
         self.link_objects.append(LinkObject{ .AssemblyFile = self.builder.dupe(path) }) catch unreachable;
     }
 
+    pub fn addAssemblyFileFromWriteFileStep(self: *LibExeObjStep, wfs: *WriteFileStep, basename: []const u8) void {
+        self.addAssemblyFileSource(.{
+            .write_file = .{
+                .step = wfs,
+                .basename = self.builder.dupe(basename),
+            },
+        });
+    }
+
+    pub fn addAssemblyFileSource(self: *LibExeObjStep, source: FileSource) void {
+        self.link_objects.append(LinkObject{ .AssemblyFile = source }) catch unreachable;
+        source.addStepDependencies(&self.step);
+    }
+
     pub fn addObjectFile(self: *LibExeObjStep, path: []const u8) void {
         self.link_objects.append(LinkObject{ .StaticPath = self.builder.dupe(path) }) catch unreachable;
     }
@@ -1698,25 +1798,23 @@ pub const LibExeObjStep = struct {
         };
         zig_args.append(cmd) catch unreachable;
 
-        if (self.root_src) |root_src| {
-            zig_args.append(builder.pathFromRoot(root_src)) catch unreachable;
-        }
+        if (self.root_src) |root_src| try zig_args.append(root_src.getPath(builder));
 
         for (self.link_objects.toSlice()) |link_object| {
             switch (link_object) {
-                LinkObject.StaticPath => |static_path| {
+                .StaticPath => |static_path| {
                     try zig_args.append("--object");
                     try zig_args.append(builder.pathFromRoot(static_path));
                 },
 
-                LinkObject.OtherStep => |other| switch (other.kind) {
-                    LibExeObjStep.Kind.Exe => unreachable,
-                    LibExeObjStep.Kind.Test => unreachable,
-                    LibExeObjStep.Kind.Obj => {
+                .OtherStep => |other| switch (other.kind) {
+                    .Exe => unreachable,
+                    .Test => unreachable,
+                    .Obj => {
                         try zig_args.append("--object");
                         try zig_args.append(other.getOutputPath());
                     },
-                    LibExeObjStep.Kind.Lib => {
+                    .Lib => {
                         if (!other.is_dynamic or self.target.isWindows()) {
                             try zig_args.append("--object");
                             try zig_args.append(other.getOutputLibPath());
@@ -1732,20 +1830,20 @@ pub const LibExeObjStep = struct {
                         }
                     },
                 },
-                LinkObject.SystemLib => |name| {
+                .SystemLib => |name| {
                     try zig_args.append("--library");
                     try zig_args.append(name);
                 },
-                LinkObject.AssemblyFile => |asm_file| {
+                .AssemblyFile => |asm_file| {
                     try zig_args.append("--c-source");
-                    try zig_args.append(builder.pathFromRoot(asm_file));
+                    try zig_args.append(asm_file.getPath(builder));
                 },
-                LinkObject.CSourceFile => |c_source_file| {
+                .CSourceFile => |c_source_file| {
                     try zig_args.append("--c-source");
                     for (c_source_file.args) |arg| {
                         try zig_args.append(arg);
                     }
-                    try zig_args.append(self.builder.pathFromRoot(c_source_file.source_path));
+                    try zig_args.append(c_source_file.source.getPath(builder));
                 },
             }
         }
@@ -2041,134 +2139,6 @@ pub const LibExeObjStep = struct {
     }
 };
 
-pub const RunStep = struct {
-    step: Step,
-    builder: *Builder,
-
-    /// 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: ?*BufMap,
-
-    pub const Arg = union(enum) {
-        Artifact: *LibExeObjStep,
-        Bytes: []u8,
-    };
-
-    pub fn create(builder: *Builder, name: []const u8) *RunStep {
-        const self = builder.allocator.create(RunStep) catch unreachable;
-        self.* = RunStep{
-            .builder = builder,
-            .step = Step.init(name, builder.allocator, make),
-            .argv = ArrayList(Arg).init(builder.allocator),
-            .cwd = null,
-            .env_map = null,
-        };
-        return self;
-    }
-
-    pub fn addArtifactArg(self: *RunStep, artifact: *LibExeObjStep) void {
-        self.argv.append(Arg{ .Artifact = artifact }) catch unreachable;
-        self.step.dependOn(&artifact.step);
-    }
-
-    pub fn addArg(self: *RunStep, arg: []const u8) void {
-        self.argv.append(Arg{ .Bytes = self.builder.dupe(arg) }) catch unreachable;
-    }
-
-    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(BufMap) catch unreachable;
-        new_env_map.* = BufMap.init(self.builder.allocator);
-        self.env_map = new_env_map;
-    }
-
-    pub fn addPathDir(self: *RunStep, search_path: []const u8) void {
-        const env_map = self.getEnvMap();
-
-        var key: []const u8 = undefined;
-        var prev_path: ?[]const u8 = undefined;
-        if (builtin.os == .windows) {
-            key = "Path";
-            prev_path = env_map.get(key);
-            if (prev_path == null) {
-                key = "PATH";
-                prev_path = env_map.get(key);
-            }
-        } else {
-            key = "PATH";
-            prev_path = env_map.get(key);
-        }
-
-        if (prev_path) |pp| {
-            const new_path = self.builder.fmt("{}" ++ [1]u8{fs.path.delimiter} ++ "{}", .{ pp, search_path });
-            env_map.set(key, new_path) catch unreachable;
-        } else {
-            env_map.set(key, search_path) catch unreachable;
-        }
-    }
-
-    pub fn getEnvMap(self: *RunStep) *BufMap {
-        return self.env_map orelse {
-            const env_map = self.builder.allocator.create(BufMap) catch unreachable;
-            env_map.* = process.getEnvMap(self.builder.allocator) catch unreachable;
-            self.env_map = env_map;
-            return env_map;
-        };
-    }
-
-    pub fn setEnvironmentVariable(self: *RunStep, key: []const u8, value: []const u8) void {
-        const env_map = self.getEnvMap();
-        env_map.set(key, value) catch unreachable;
-    }
-
-    fn make(step: *Step) !void {
-        const self = @fieldParentPtr(RunStep, "step", step);
-
-        const cwd = if (self.cwd) |cwd| self.builder.pathFromRoot(cwd) else self.builder.build_root;
-
-        var argv = ArrayList([]const u8).init(self.builder.allocator);
-        for (self.argv.toSlice()) |arg| {
-            switch (arg) {
-                Arg.Bytes => |bytes| try argv.append(bytes),
-                Arg.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 executable_path = artifact.installed_path orelse artifact.getOutputPath();
-                    try argv.append(executable_path);
-                },
-            }
-        }
-
-        return self.builder.spawnChildEnvMap(cwd, self.env_map orelse self.builder.env_map, argv.toSliceConst());
-    }
-
-    fn addPathForDynLibs(self: *RunStep, artifact: *LibExeObjStep) void {
-        for (artifact.link_objects.toSliceConst()) |link_object| {
-            switch (link_object) {
-                LibExeObjStep.LinkObject.OtherStep => |other| {
-                    if (other.target.isWindows() and other.isDynamicLibrary()) {
-                        self.addPathDir(fs.path.dirname(other.getOutputPath()).?);
-                        self.addPathForDynLibs(other);
-                    }
-                },
-                else => {},
-            }
-        }
-    }
-};
-
 const InstallArtifactStep = struct {
     step: Step,
     builder: *Builder,
@@ -2321,36 +2291,6 @@ pub const InstallDirStep = struct {
     }
 };
 
-pub const WriteFileStep = struct {
-    step: Step,
-    builder: *Builder,
-    file_path: []const u8,
-    data: []const u8,
-
-    pub fn init(builder: *Builder, file_path: []const u8, data: []const u8) WriteFileStep {
-        return WriteFileStep{
-            .builder = builder,
-            .step = Step.init(builder.fmt("writefile {}", .{file_path}), builder.allocator, make),
-            .file_path = file_path,
-            .data = data,
-        };
-    }
-
-    fn make(step: *Step) !void {
-        const self = @fieldParentPtr(WriteFileStep, "step", step);
-        const full_path = self.builder.pathFromRoot(self.file_path);
-        const full_path_dir = fs.path.dirname(full_path) orelse ".";
-        fs.makePath(self.builder.allocator, full_path_dir) catch |err| {
-            warn("unable to make path {}: {}\n", .{ full_path_dir, @errorName(err) });
-            return err;
-        };
-        io.writeFile(full_path, self.data) catch |err| {
-            warn("unable to write {}: {}\n", .{ full_path, @errorName(err) });
-            return err;
-        };
-    }
-};
-
 pub const LogStep = struct {
     step: Step,
     builder: *Builder,
lib/std/fs.zig
@@ -37,8 +37,11 @@ pub const MAX_PATH_BYTES = switch (builtin.os) {
     else => @compileError("Unsupported OS"),
 };
 
-// here we replace the standard +/ with -_ so that it can be used in a file name
-const b64_fs_encoder = base64.Base64Encoder.init("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", base64.standard_pad_char);
+/// Base64, replacing the standard `+/` with `-_` so that it can be used in a file name on any filesystem.
+pub const base64_encoder = base64.Base64Encoder.init(
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
+    base64.standard_pad_char,
+);
 
 /// TODO remove the allocator requirement from this API
 pub fn atomicSymLink(allocator: *Allocator, existing_path: []const u8, new_path: []const u8) !void {
@@ -58,7 +61,7 @@ pub fn atomicSymLink(allocator: *Allocator, existing_path: []const u8, new_path:
     tmp_path[dirname.len] = path.sep;
     while (true) {
         try crypto.randomBytes(rand_buf[0..]);
-        b64_fs_encoder.encode(tmp_path[dirname.len + 1 ..], &rand_buf);
+        base64_encoder.encode(tmp_path[dirname.len + 1 ..], &rand_buf);
 
         if (symLink(existing_path, tmp_path)) {
             return rename(tmp_path, new_path);
@@ -227,10 +230,10 @@ pub const AtomicFile = struct {
 
         while (true) {
             try crypto.randomBytes(rand_buf[0..]);
-            b64_fs_encoder.encode(tmp_path_slice[dirname_component_len..tmp_path_len], &rand_buf);
+            base64_encoder.encode(tmp_path_slice[dirname_component_len..tmp_path_len], &rand_buf);
 
             const file = my_cwd.createFileC(
-                tmp_path_slice, 
+                tmp_path_slice,
                 .{ .mode = mode, .exclusive = true },
             ) catch |err| switch (err) {
                 error.PathAlreadyExists => continue,
test/src/compare_output.zig
@@ -0,0 +1,165 @@
+// This is the implementation of the test harness.
+// For the actual test cases, see test/compare_output.zig.
+const std = @import("std");
+const builtin = std.builtin;
+const build = std.build;
+const ArrayList = std.ArrayList;
+const fmt = std.fmt;
+const mem = std.mem;
+const fs = std.fs;
+const warn = std.debug.warn;
+const Mode = builtin.Mode;
+
+pub const CompareOutputContext = struct {
+    b: *build.Builder,
+    step: *build.Step,
+    test_index: usize,
+    test_filter: ?[]const u8,
+    modes: []const Mode,
+
+    const Special = enum {
+        None,
+        Asm,
+        RuntimeSafety,
+    };
+
+    const TestCase = struct {
+        name: []const u8,
+        sources: ArrayList(SourceFile),
+        expected_output: []const u8,
+        link_libc: bool,
+        special: Special,
+        cli_args: []const []const u8,
+
+        const SourceFile = struct {
+            filename: []const u8,
+            source: []const u8,
+        };
+
+        pub fn addSourceFile(self: *TestCase, filename: []const u8, source: []const u8) void {
+            self.sources.append(SourceFile{
+                .filename = filename,
+                .source = source,
+            }) catch unreachable;
+        }
+
+        pub fn setCommandLineArgs(self: *TestCase, args: []const []const u8) void {
+            self.cli_args = args;
+        }
+    };
+
+    pub fn createExtra(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8, special: Special) TestCase {
+        var tc = TestCase{
+            .name = name,
+            .sources = ArrayList(TestCase.SourceFile).init(self.b.allocator),
+            .expected_output = expected_output,
+            .link_libc = false,
+            .special = special,
+            .cli_args = &[_][]const u8{},
+        };
+        const root_src_name = if (special == Special.Asm) "source.s" else "source.zig";
+        tc.addSourceFile(root_src_name, source);
+        return tc;
+    }
+
+    pub fn create(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8) TestCase {
+        return createExtra(self, name, source, expected_output, Special.None);
+    }
+
+    pub fn addC(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8) void {
+        var tc = self.create(name, source, expected_output);
+        tc.link_libc = true;
+        self.addCase(tc);
+    }
+
+    pub fn add(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8) void {
+        const tc = self.create(name, source, expected_output);
+        self.addCase(tc);
+    }
+
+    pub fn addAsm(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8) void {
+        const tc = self.createExtra(name, source, expected_output, Special.Asm);
+        self.addCase(tc);
+    }
+
+    pub fn addRuntimeSafety(self: *CompareOutputContext, name: []const u8, source: []const u8) void {
+        const tc = self.createExtra(name, source, undefined, Special.RuntimeSafety);
+        self.addCase(tc);
+    }
+
+    pub fn addCase(self: *CompareOutputContext, case: TestCase) void {
+        const b = self.b;
+
+        const write_src = b.addWriteFiles();
+        for (case.sources.toSliceConst()) |src_file| {
+            write_src.add(src_file.filename, src_file.source);
+        }
+
+        switch (case.special) {
+            Special.Asm => {
+                const annotated_case_name = fmt.allocPrint(self.b.allocator, "assemble-and-link {}", .{
+                    case.name,
+                }) catch unreachable;
+                if (self.test_filter) |filter| {
+                    if (mem.indexOf(u8, annotated_case_name, filter) == null) return;
+                }
+
+                const exe = b.addExecutable("test", null);
+                exe.addAssemblyFileFromWriteFileStep(write_src, case.sources.toSliceConst()[0].filename);
+
+                const run = exe.run();
+                run.addArgs(case.cli_args);
+                run.expectStdErrEqual("");
+                run.expectStdOutEqual(case.expected_output);
+
+                self.step.dependOn(&run.step);
+            },
+            Special.None => {
+                for (self.modes) |mode| {
+                    const annotated_case_name = fmt.allocPrint(self.b.allocator, "{} {} ({})", .{
+                        "compare-output",
+                        case.name,
+                        @tagName(mode),
+                    }) catch unreachable;
+                    if (self.test_filter) |filter| {
+                        if (mem.indexOf(u8, annotated_case_name, filter) == null) continue;
+                    }
+
+                    const basename = case.sources.toSliceConst()[0].filename;
+                    const exe = b.addExecutableFromWriteFileStep("test", write_src, basename);
+                    exe.setBuildMode(mode);
+                    if (case.link_libc) {
+                        exe.linkSystemLibrary("c");
+                    }
+
+                    const run = exe.run();
+                    run.addArgs(case.cli_args);
+                    run.expectStdErrEqual("");
+                    run.expectStdOutEqual(case.expected_output);
+
+                    self.step.dependOn(&run.step);
+                }
+            },
+            Special.RuntimeSafety => {
+                const annotated_case_name = fmt.allocPrint(self.b.allocator, "safety {}", .{case.name}) catch unreachable;
+                if (self.test_filter) |filter| {
+                    if (mem.indexOf(u8, annotated_case_name, filter) == null) return;
+                }
+
+                const basename = case.sources.toSliceConst()[0].filename;
+                const exe = b.addExecutableFromWriteFileStep("test", write_src, basename);
+                if (case.link_libc) {
+                    exe.linkSystemLibrary("c");
+                }
+
+                const run = exe.run();
+                run.addArgs(case.cli_args);
+                run.stderr_action = .ignore;
+                run.stdout_action = .ignore;
+                run.expected_exit_code = 126;
+
+                self.step.dependOn(&run.step);
+            },
+        }
+    }
+};
test/src/run_translated_c.zig
@@ -33,89 +33,6 @@ pub const RunTranslatedCContext = struct {
         }
     };
 
-    const DoEverythingStep = struct {
-        step: build.Step,
-        context: *RunTranslatedCContext,
-        name: []const u8,
-        case: *const TestCase,
-        test_index: usize,
-
-        pub fn create(
-            context: *RunTranslatedCContext,
-            name: []const u8,
-            case: *const TestCase,
-        ) *DoEverythingStep {
-            const allocator = context.b.allocator;
-            const ptr = allocator.create(DoEverythingStep) catch unreachable;
-            ptr.* = DoEverythingStep{
-                .context = context,
-                .name = name,
-                .case = case,
-                .test_index = context.test_index,
-                .step = build.Step.init("RunTranslatedC", allocator, make),
-            };
-            context.test_index += 1;
-            return ptr;
-        }
-
-        fn make(step: *build.Step) !void {
-            const self = @fieldParentPtr(DoEverythingStep, "step", step);
-            const b = self.context.b;
-
-            warn("Test {}/{} {}...", .{ self.test_index + 1, self.context.test_index, self.name });
-            // translate from c to zig
-            const translated_c_code = blk: {
-                var zig_args = ArrayList([]const u8).init(b.allocator);
-                defer zig_args.deinit();
-
-                const rel_c_filename = try fs.path.join(b.allocator, &[_][]const u8{
-                    b.cache_root,
-                    self.case.sources.toSliceConst()[0].filename,
-                });
-
-                try zig_args.append(b.zig_exe);
-                try zig_args.append("translate-c");
-                try zig_args.append("-lc");
-                try zig_args.append(b.pathFromRoot(rel_c_filename));
-
-                break :blk try b.exec(zig_args.toSliceConst());
-            };
-
-            // write stdout to a file
-
-            const translated_c_path = try fs.path.join(b.allocator,
-                &[_][]const u8{ b.cache_root, "translated_c.zig" });
-            try fs.cwd().writeFile(translated_c_path, translated_c_code);
-
-            // zig run the result
-            const run_stdout = blk: {
-                var zig_args = ArrayList([]const u8).init(b.allocator);
-                defer zig_args.deinit();
-
-                try zig_args.append(b.zig_exe);
-                try zig_args.append("-lc");
-                try zig_args.append("run");
-                try zig_args.append(translated_c_path);
-
-                break :blk try b.exec(zig_args.toSliceConst());
-            };
-            // compare stdout
-            if (!mem.eql(u8, self.case.expected_stdout, run_stdout)) {
-                warn(
-                    \\
-                    \\========= Expected this output: =========
-                    \\{}
-                    \\========= But found: ====================
-                    \\{}
-                    \\
-                , .{ self.case.expected_stdout, run_stdout });
-                return error.TestFailed;
-            }
-
-            warn("OK\n", .{});
-        }
-    };
-
     pub fn create(
         self: *RunTranslatedCContext,
         allow_warnings: bool,
@@ -159,22 +76,29 @@ pub const RunTranslatedCContext = struct {
     pub fn addCase(self: *RunTranslatedCContext, case: *const TestCase) void {
         const b = self.b;
 
-        const annotated_case_name = fmt.allocPrint(self.b.allocator, "run-translated-c {}", .{ case.name }) catch unreachable;
+        const annotated_case_name = fmt.allocPrint(self.b.allocator, "run-translated-c {}", .{case.name}) catch unreachable;
         if (self.test_filter) |filter| {
             if (mem.indexOf(u8, annotated_case_name, filter) == null) return;
         }
 
-        const do_everything_step = DoEverythingStep.create(self, annotated_case_name, case);
-        self.step.dependOn(&do_everything_step.step);
-
+        const write_src = b.addWriteFiles();
         for (case.sources.toSliceConst()) |src_file| {
-            const expanded_src_path = fs.path.join(
-                b.allocator,
-                &[_][]const u8{ b.cache_root, src_file.filename },
-            ) catch unreachable;
-            const write_src = b.addWriteFile(expanded_src_path, src_file.source);
-            do_everything_step.step.dependOn(&write_src.step);
+            write_src.add(src_file.filename, src_file.source);
+        }
+        const translate_c = b.addTranslateC(.{
+            .write_file = .{
+                .step = write_src,
+                .basename = case.sources.toSliceConst()[0].filename,
+            },
+        });
+        const exe = translate_c.addExecutable();
+        exe.linkLibC();
+        const run = exe.run();
+        if (!case.allow_warnings) {
+            run.expectStdErrEqual("");
         }
+        run.expectStdOutEqual(case.expected_stdout);
+
+        self.step.dependOn(&run.step);
     }
 };
-
test/src/translate_c.zig
@@ -0,0 +1,109 @@
+// This is the implementation of the test harness.
+// For the actual test cases, see test/translate_c.zig.
+const std = @import("std");
+const build = std.build;
+const ArrayList = std.ArrayList;
+const fmt = std.fmt;
+const mem = std.mem;
+const fs = std.fs;
+const warn = std.debug.warn;
+
+pub const TranslateCContext = struct {
+    b: *build.Builder,
+    step: *build.Step,
+    test_index: usize,
+    test_filter: ?[]const u8,
+
+    const TestCase = struct {
+        name: []const u8,
+        sources: ArrayList(SourceFile),
+        expected_lines: ArrayList([]const u8),
+        allow_warnings: bool,
+
+        const SourceFile = struct {
+            filename: []const u8,
+            source: []const u8,
+        };
+
+        pub fn addSourceFile(self: *TestCase, filename: []const u8, source: []const u8) void {
+            self.sources.append(SourceFile{
+                .filename = filename,
+                .source = source,
+            }) catch unreachable;
+        }
+
+        pub fn addExpectedLine(self: *TestCase, text: []const u8) void {
+            self.expected_lines.append(text) catch unreachable;
+        }
+    };
+
+    pub fn create(
+        self: *TranslateCContext,
+        allow_warnings: bool,
+        filename: []const u8,
+        name: []const u8,
+        source: []const u8,
+        expected_lines: []const []const u8,
+    ) *TestCase {
+        const tc = self.b.allocator.create(TestCase) catch unreachable;
+        tc.* = TestCase{
+            .name = name,
+            .sources = ArrayList(TestCase.SourceFile).init(self.b.allocator),
+            .expected_lines = ArrayList([]const u8).init(self.b.allocator),
+            .allow_warnings = allow_warnings,
+        };
+
+        tc.addSourceFile(filename, source);
+        var arg_i: usize = 0;
+        while (arg_i < expected_lines.len) : (arg_i += 1) {
+            tc.addExpectedLine(expected_lines[arg_i]);
+        }
+        return tc;
+    }
+
+    pub fn add(
+        self: *TranslateCContext,
+        name: []const u8,
+        source: []const u8,
+        expected_lines: []const []const u8,
+    ) void {
+        const tc = self.create(false, "source.h", name, source, expected_lines);
+        self.addCase(tc);
+    }
+
+    pub fn addAllowWarnings(
+        self: *TranslateCContext,
+        name: []const u8,
+        source: []const u8,
+        expected_lines: []const []const u8,
+    ) void {
+        const tc = self.create(true, "source.h", name, source, expected_lines);
+        self.addCase(tc);
+    }
+
+    pub fn addCase(self: *TranslateCContext, case: *const TestCase) void {
+        const b = self.b;
+
+        const translate_c_cmd = "translate-c";
+        const annotated_case_name = fmt.allocPrint(self.b.allocator, "{} {}", .{ translate_c_cmd, case.name }) catch unreachable;
+        if (self.test_filter) |filter| {
+            if (mem.indexOf(u8, annotated_case_name, filter) == null) return;
+        }
+
+        const write_src = b.addWriteFiles();
+        for (case.sources.toSliceConst()) |src_file| {
+            write_src.add(src_file.filename, src_file.source);
+        }
+
+        const translate_c = b.addTranslateC(.{
+            .write_file = .{
+                .step = write_src,
+                .basename = case.sources.toSliceConst()[0].filename,
+            },
+        });
+
+        const check_file = translate_c.addCheckFile(case.expected_lines.toSliceConst());
+
+        self.step.dependOn(&check_file.step);
+    }
+};
test/tests.zig
@@ -26,7 +26,9 @@ const run_translated_c = @import("run_translated_c.zig");
 const gen_h = @import("gen_h.zig");
 
 // Implementations
+pub const TranslateCContext = @import("src/translate_c.zig").TranslateCContext;
 pub const RunTranslatedCContext = @import("src/run_translated_c.zig").RunTranslatedCContext;
+pub const CompareOutputContext = @import("src/compare_output.zig").CompareOutputContext;
 
 const TestTarget = struct {
     target: Target = .Native,
@@ -498,356 +500,6 @@ pub fn addPkgTests(
     return step;
 }
 
-pub const CompareOutputContext = struct {
-    b: *build.Builder,
-    step: *build.Step,
-    test_index: usize,
-    test_filter: ?[]const u8,
-    modes: []const Mode,
-
-    const Special = enum {
-        None,
-        Asm,
-        RuntimeSafety,
-    };
-
-    const TestCase = struct {
-        name: []const u8,
-        sources: ArrayList(SourceFile),
-        expected_output: []const u8,
-        link_libc: bool,
-        special: Special,
-        cli_args: []const []const u8,
-
-        const SourceFile = struct {
-            filename: []const u8,
-            source: []const u8,
-        };
-
-        pub fn addSourceFile(self: *TestCase, filename: []const u8, source: []const u8) void {
-            self.sources.append(SourceFile{
-                .filename = filename,
-                .source = source,
-            }) catch unreachable;
-        }
-
-        pub fn setCommandLineArgs(self: *TestCase, args: []const []const u8) void {
-            self.cli_args = args;
-        }
-    };
-
-    const RunCompareOutputStep = struct {
-        step: build.Step,
-        context: *CompareOutputContext,
-        exe: *LibExeObjStep,
-        name: []const u8,
-        expected_output: []const u8,
-        test_index: usize,
-        cli_args: []const []const u8,
-
-        pub fn create(
-            context: *CompareOutputContext,
-            exe: *LibExeObjStep,
-            name: []const u8,
-            expected_output: []const u8,
-            cli_args: []const []const u8,
-        ) *RunCompareOutputStep {
-            const allocator = context.b.allocator;
-            const ptr = allocator.create(RunCompareOutputStep) catch unreachable;
-            ptr.* = RunCompareOutputStep{
-                .context = context,
-                .exe = exe,
-                .name = name,
-                .expected_output = expected_output,
-                .test_index = context.test_index,
-                .step = build.Step.init("RunCompareOutput", allocator, make),
-                .cli_args = cli_args,
-            };
-            ptr.step.dependOn(&exe.step);
-            context.test_index += 1;
-            return ptr;
-        }
-
-        fn make(step: *build.Step) !void {
-            const self = @fieldParentPtr(RunCompareOutputStep, "step", step);
-            const b = self.context.b;
-
-            const full_exe_path = self.exe.getOutputPath();
-            var args = ArrayList([]const u8).init(b.allocator);
-            defer args.deinit();
-
-            args.append(full_exe_path) catch unreachable;
-            for (self.cli_args) |arg| {
-                args.append(arg) catch unreachable;
-            }
-
-            warn("Test {}/{} {}...", .{ self.test_index + 1, self.context.test_index, self.name });
-
-            const child = std.ChildProcess.init(args.toSliceConst(), b.allocator) catch unreachable;
-            defer child.deinit();
-
-            child.stdin_behavior = .Ignore;
-            child.stdout_behavior = .Pipe;
-            child.stderr_behavior = .Pipe;
-            child.env_map = b.env_map;
-
-            child.spawn() catch |err| debug.panic("Unable to spawn {}: {}\n", .{ full_exe_path, @errorName(err) });
-
-            var stdout = Buffer.initNull(b.allocator);
-            var stderr = Buffer.initNull(b.allocator);
-
-            var stdout_file_in_stream = child.stdout.?.inStream();
-            var stderr_file_in_stream = child.stderr.?.inStream();
-
-            stdout_file_in_stream.stream.readAllBuffer(&stdout, max_stdout_size) catch unreachable;
-            stderr_file_in_stream.stream.readAllBuffer(&stderr, max_stdout_size) catch unreachable;
-
-            const term = child.wait() catch |err| {
-                debug.panic("Unable to spawn {}: {}\n", .{ full_exe_path, @errorName(err) });
-            };
-            switch (term) {
-                .Exited => |code| {
-                    if (code != 0) {
-                        warn("Process {} exited with error code {}\n", .{ full_exe_path, code });
-                        printInvocation(args.toSliceConst());
-                        return error.TestFailed;
-                    }
-                },
-                else => {
-                    warn("Process {} terminated unexpectedly\n", .{full_exe_path});
-                    printInvocation(args.toSliceConst());
-                    return error.TestFailed;
-                },
-            }
-
-            if (!mem.eql(u8, self.expected_output, stdout.toSliceConst())) {
-                warn(
-                    \\
-                    \\========= Expected this output: =========
-                    \\{}
-                    \\========= But found: ====================
-                    \\{}
-                    \\
-                , .{ self.expected_output, stdout.toSliceConst() });
-                return error.TestFailed;
-            }
-            warn("OK\n", .{});
-        }
-    };
-
-    const RuntimeSafetyRunStep = struct {
-        step: build.Step,
-        context: *CompareOutputContext,
-        exe: *LibExeObjStep,
-        name: []const u8,
-        test_index: usize,
-
-        pub fn create(context: *CompareOutputContext, exe: *LibExeObjStep, name: []const u8) *RuntimeSafetyRunStep {
-            const allocator = context.b.allocator;
-            const ptr = allocator.create(RuntimeSafetyRunStep) catch unreachable;
-            ptr.* = RuntimeSafetyRunStep{
-                .context = context,
-                .exe = exe,
-                .name = name,
-                .test_index = context.test_index,
-                .step = build.Step.init("RuntimeSafetyRun", allocator, make),
-            };
-            ptr.step.dependOn(&exe.step);
-            context.test_index += 1;
-            return ptr;
-        }
-
-        fn make(step: *build.Step) !void {
-            const self = @fieldParentPtr(RuntimeSafetyRunStep, "step", step);
-            const b = self.context.b;
-
-            const full_exe_path = self.exe.getOutputPath();
-
-            warn("Test {}/{} {}...", .{ self.test_index + 1, self.context.test_index, self.name });
-
-            const child = std.ChildProcess.init(&[_][]const u8{full_exe_path}, b.allocator) catch unreachable;
-            defer child.deinit();
-
-            child.env_map = b.env_map;
-            child.stdin_behavior = .Ignore;
-            child.stdout_behavior = .Ignore;
-            child.stderr_behavior = .Ignore;
-
-            const term = child.spawnAndWait() catch |err| {
-                debug.panic("Unable to spawn {}: {}\n", .{ full_exe_path, @errorName(err) });
-            };
-
-            const expected_exit_code: u32 = 126;
-            switch (term) {
-                .Exited => |code| {
-                    if (code != expected_exit_code) {
-                        warn("\nProgram expected to exit with code {} but exited with code {}\n", .{
-                            expected_exit_code, code,
-                        });
-                        return error.TestFailed;
-                    }
-                },
-                .Signal => |sig| {
-                    warn("\nProgram expected to exit with code {} but instead signaled {}\n", .{
-                        expected_exit_code, sig,
-                    });
-                    return error.TestFailed;
-                },
-                else => {
-                    warn("\nProgram expected to exit with code {} but exited in an unexpected way\n", .{
-                        expected_exit_code,
-                    });
-                    return error.TestFailed;
-                },
-            }
-
-            warn("OK\n", .{});
-        }
-    };
-
-    pub fn createExtra(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8, special: Special) TestCase {
-        var tc = TestCase{
-            .name = name,
-            .sources = ArrayList(TestCase.SourceFile).init(self.b.allocator),
-            .expected_output = expected_output,
-            .link_libc = false,
-            .special = special,
-            .cli_args = &[_][]const u8{},
-        };
-        const root_src_name = if (special == Special.Asm) "source.s" else "source.zig";
-        tc.addSourceFile(root_src_name, source);
-        return tc;
-    }
-
-    pub fn create(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8) TestCase {
-        return createExtra(self, name, source, expected_output, Special.None);
-    }
-
-    pub fn addC(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8) void {
-        var tc = self.create(name, source, expected_output);
-        tc.link_libc = true;
-        self.addCase(tc);
-    }
-
-    pub fn add(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8) void {
-        const tc = self.create(name, source, expected_output);
-        self.addCase(tc);
-    }
-
-    pub fn addAsm(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8) void {
-        const tc = self.createExtra(name, source, expected_output, Special.Asm);
-        self.addCase(tc);
-    }
-
-    pub fn addRuntimeSafety(self: *CompareOutputContext, name: []const u8, source: []const u8) void {
-        const tc = self.createExtra(name, source, undefined, Special.RuntimeSafety);
-        self.addCase(tc);
-    }
-
-    pub fn addCase(self: *CompareOutputContext, case: TestCase) void {
-        const b = self.b;
-
-        const root_src = fs.path.join(
-            b.allocator,
-            &[_][]const u8{ b.cache_root, case.sources.items[0].filename },
-        ) catch unreachable;
-
-        switch (case.special) {
-            Special.Asm => {
-                const annotated_case_name = fmt.allocPrint(self.b.allocator, "assemble-and-link {}", .{
-                    case.name,
-                }) catch unreachable;
-                if (self.test_filter) |filter| {
-                    if (mem.indexOf(u8, annotated_case_name, filter) == null) return;
-                }
-
-                const exe = b.addExecutable("test", null);
-                exe.addAssemblyFile(root_src);
-
-                for (case.sources.toSliceConst()) |src_file| {
-                    const expanded_src_path = fs.path.join(
-                        b.allocator,
-                        &[_][]const u8{ b.cache_root, src_file.filename },
-                    ) catch unreachable;
-                    const write_src = b.addWriteFile(expanded_src_path, src_file.source);
-                    exe.step.dependOn(&write_src.step);
-                }
-
-                const run_and_cmp_output = RunCompareOutputStep.create(
-                    self,
-                    exe,
-                    annotated_case_name,
-                    case.expected_output,
-                    case.cli_args,
-                );
-
-                self.step.dependOn(&run_and_cmp_output.step);
-            },
-            Special.None => {
-                for (self.modes) |mode| {
-                    const annotated_case_name = fmt.allocPrint(self.b.allocator, "{} {} ({})", .{
-                        "compare-output",
-                        case.name,
-                        @tagName(mode),
-                    }) catch unreachable;
-                    if (self.test_filter) |filter| {
-                        if (mem.indexOf(u8, annotated_case_name, filter) == null) continue;
-                    }
-
-                    const exe = b.addExecutable("test", root_src);
-                    exe.setBuildMode(mode);
-                    if (case.link_libc) {
-                        exe.linkSystemLibrary("c");
-                    }
-
-                    for (case.sources.toSliceConst()) |src_file| {
-                        const expanded_src_path = fs.path.join(
-                            b.allocator,
-                            &[_][]const u8{ b.cache_root, src_file.filename },
-                        ) catch unreachable;
-                        const write_src = b.addWriteFile(expanded_src_path, src_file.source);
-                        exe.step.dependOn(&write_src.step);
-                    }
-
-                    const run_and_cmp_output = RunCompareOutputStep.create(
-                        self,
-                        exe,
-                        annotated_case_name,
-                        case.expected_output,
-                        case.cli_args,
-                    );
-
-                    self.step.dependOn(&run_and_cmp_output.step);
-                }
-            },
-            Special.RuntimeSafety => {
-                const annotated_case_name = fmt.allocPrint(self.b.allocator, "safety {}", .{case.name}) catch unreachable;
-                if (self.test_filter) |filter| {
-                    if (mem.indexOf(u8, annotated_case_name, filter) == null) return;
-                }
-
-                const exe = b.addExecutable("test", root_src);
-                if (case.link_libc) {
-                    exe.linkSystemLibrary("c");
-                }
-
-                for (case.sources.toSliceConst()) |src_file| {
-                    const expanded_src_path = fs.path.join(
-                        b.allocator,
-                        &[_][]const u8{ b.cache_root, src_file.filename },
-                    ) catch unreachable;
-                    const write_src = b.addWriteFile(expanded_src_path, src_file.source);
-                    exe.step.dependOn(&write_src.step);
-                }
-
-                const run_and_cmp_output = RuntimeSafetyRunStep.create(self, exe, annotated_case_name);
-
-                self.step.dependOn(&run_and_cmp_output.step);
-            },
-        }
-    }
-};
-
 pub const StackTracesContext = struct {
     b: *build.Builder,
     step: *build.Step,
@@ -1430,230 +1082,6 @@ pub const StandaloneContext = struct {
     }
 };
 
-pub const TranslateCContext = struct {
-    b: *build.Builder,
-    step: *build.Step,
-    test_index: usize,
-    test_filter: ?[]const u8,
-
-    const TestCase = struct {
-        name: []const u8,
-        sources: ArrayList(SourceFile),
-        expected_lines: ArrayList([]const u8),
-        allow_warnings: bool,
-
-        const SourceFile = struct {
-            filename: []const u8,
-            source: []const u8,
-        };
-
-        pub fn addSourceFile(self: *TestCase, filename: []const u8, source: []const u8) void {
-            self.sources.append(SourceFile{
-                .filename = filename,
-                .source = source,
-            }) catch unreachable;
-        }
-
-        pub fn addExpectedLine(self: *TestCase, text: []const u8) void {
-            self.expected_lines.append(text) catch unreachable;
-        }
-    };
-
-    const TranslateCCmpOutputStep = struct {
-        step: build.Step,
-        context: *TranslateCContext,
-        name: []const u8,
-        test_index: usize,
-        case: *const TestCase,
-
-        pub fn create(context: *TranslateCContext, name: []const u8, case: *const TestCase) *TranslateCCmpOutputStep {
-            const allocator = context.b.allocator;
-            const ptr = allocator.create(TranslateCCmpOutputStep) catch unreachable;
-            ptr.* = TranslateCCmpOutputStep{
-                .step = build.Step.init("ParseCCmpOutput", allocator, make),
-                .context = context,
-                .name = name,
-                .test_index = context.test_index,
-                .case = case,
-            };
-
-            context.test_index += 1;
-            return ptr;
-        }
-
-        fn make(step: *build.Step) !void {
-            const self = @fieldParentPtr(TranslateCCmpOutputStep, "step", step);
-            const b = self.context.b;
-
-            const root_src = fs.path.join(
-                b.allocator,
-                &[_][]const u8{ b.cache_root, self.case.sources.items[0].filename },
-            ) catch unreachable;
-
-            var zig_args = ArrayList([]const u8).init(b.allocator);
-            zig_args.append(b.zig_exe) catch unreachable;
-
-            const translate_c_cmd = "translate-c";
-            zig_args.append(translate_c_cmd) catch unreachable;
-            zig_args.append(b.pathFromRoot(root_src)) catch unreachable;
-
-            warn("Test {}/{} {}...", .{ self.test_index + 1, self.context.test_index, self.name });
-
-            if (b.verbose) {
-                printInvocation(zig_args.toSliceConst());
-            }
-
-            const child = std.ChildProcess.init(zig_args.toSliceConst(), b.allocator) catch unreachable;
-            defer child.deinit();
-
-            child.env_map = b.env_map;
-            child.stdin_behavior = .Ignore;
-            child.stdout_behavior = .Pipe;
-            child.stderr_behavior = .Pipe;
-
-            child.spawn() catch |err| debug.panic("Unable to spawn {}: {}\n", .{
-                zig_args.toSliceConst()[0],
-                @errorName(err),
-            });
-
-            var stdout_buf = Buffer.initNull(b.allocator);
-            var stderr_buf = Buffer.initNull(b.allocator);
-
-            var stdout_file_in_stream = child.stdout.?.inStream();
-            var stderr_file_in_stream = child.stderr.?.inStream();
-
-            stdout_file_in_stream.stream.readAllBuffer(&stdout_buf, max_stdout_size) catch unreachable;
-            stderr_file_in_stream.stream.readAllBuffer(&stderr_buf, max_stdout_size) catch unreachable;
-
-            const term = child.wait() catch |err| {
-                debug.panic("Unable to spawn {}: {}\n", .{ zig_args.toSliceConst()[0], @errorName(err) });
-            };
-            switch (term) {
-                .Exited => |code| {
-                    if (code != 0) {
-                        warn("Compilation failed with exit code {}\n{}\n", .{ code, stderr_buf.toSliceConst() });
-                        printInvocation(zig_args.toSliceConst());
-                        return error.TestFailed;
-                    }
-                },
-                .Signal => |code| {
-                    warn("Compilation failed with signal {}\n", .{code});
-                    printInvocation(zig_args.toSliceConst());
-                    return error.TestFailed;
-                },
-                else => {
-                    warn("Compilation terminated unexpectedly\n", .{});
-                    printInvocation(zig_args.toSliceConst());
-                    return error.TestFailed;
-                },
-            }
-
-            const stdout = stdout_buf.toSliceConst();
-            const stderr = stderr_buf.toSliceConst();
-
-            if (stderr.len != 0 and !self.case.allow_warnings) {
-                warn(
-                    \\====== translate-c emitted warnings: =======
-                    \\{}
-                    \\============================================
-                    \\
-                , .{stderr});
-                printInvocation(zig_args.toSliceConst());
-                return error.TestFailed;
-            }
-
-            for (self.case.expected_lines.toSliceConst()) |expected_line| {
-                if (mem.indexOf(u8, stdout, expected_line) == null) {
-                    warn(
-                        \\
-                        \\========= Expected this output: ================
-                        \\{}
-                        \\========= But found: ===========================
-                        \\{}
-                        \\
-                    , .{ expected_line, stdout });
-                    printInvocation(zig_args.toSliceConst());
-                    return error.TestFailed;
-                }
-            }
-            warn("OK\n", .{});
-        }
-    };
-
-    fn printInvocation(args: []const []const u8) void {
-        for (args) |arg| {
-            warn("{} ", .{arg});
-        }
-        warn("\n", .{});
-    }
-
-    pub fn create(
-        self: *TranslateCContext,
-        allow_warnings: bool,
-        filename: []const u8,
-        name: []const u8,
-        source: []const u8,
-        expected_lines: []const []const u8,
-    ) *TestCase {
-        const tc = self.b.allocator.create(TestCase) catch unreachable;
-        tc.* = TestCase{
-            .name = name,
-            .sources = ArrayList(TestCase.SourceFile).init(self.b.allocator),
-            .expected_lines = ArrayList([]const u8).init(self.b.allocator),
-            .allow_warnings = allow_warnings,
-        };
-
-        tc.addSourceFile(filename, source);
-        var arg_i: usize = 0;
-        while (arg_i < expected_lines.len) : (arg_i += 1) {
-            tc.addExpectedLine(expected_lines[arg_i]);
-        }
-        return tc;
-    }
-
-    pub fn add(
-        self: *TranslateCContext,
-        name: []const u8,
-        source: []const u8,
-        expected_lines: []const []const u8,
-    ) void {
-        const tc = self.create(false, "source.h", name, source, expected_lines);
-        self.addCase(tc);
-    }
-
-    pub fn addAllowWarnings(
-        self: *TranslateCContext,
-        name: []const u8,
-        source: []const u8,
-        expected_lines: []const []const u8,
-    ) void {
-        const tc = self.create(true, "source.h", name, source, expected_lines);
-        self.addCase(tc);
-    }
-
-    pub fn addCase(self: *TranslateCContext, case: *const TestCase) void {
-        const b = self.b;
-
-        const translate_c_cmd = "translate-c";
-        const annotated_case_name = fmt.allocPrint(self.b.allocator, "{} {}", .{ translate_c_cmd, case.name }) catch unreachable;
-        if (self.test_filter) |filter| {
-            if (mem.indexOf(u8, annotated_case_name, filter) == null) return;
-        }
-
-        const translate_c_and_cmp = TranslateCCmpOutputStep.create(self, annotated_case_name, case);
-        self.step.dependOn(&translate_c_and_cmp.step);
-
-        for (case.sources.toSliceConst()) |src_file| {
-            const expanded_src_path = fs.path.join(
-                b.allocator,
-                &[_][]const u8{ b.cache_root, src_file.filename },
-            ) catch unreachable;
-            const write_src = b.addWriteFile(expanded_src_path, src_file.source);
-            translate_c_and_cmp.step.dependOn(&write_src.step);
-        }
-    }
-};
-
 pub const GenHContext = struct {
     b: *build.Builder,
     step: *build.Step,