authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-04-20 17:54:11+01:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-04-22 22:50:36+01:00
log927f233ff82bd422b387b4eef7fcbc67823e4b85
treeb921686a62e6e60efa6db973488d9fffb93c8ebb
parent6a7ca4b8b0fcce9e5f6a4d3f799e83021929c975

compiler: allow emitting tests to an object file

This is fairly straightforward; the actual compiler changes are limited to the CLI, since `Compilation` already supports this combination. A new `std.Build` API is introduced to allow representing this. By passing the `emit_object` option to `std.Build.addTest`, you get a `Step.Compile` which emits an object file; you can then use that as you would any other object, such as either installing it for external use, or linking it into another step. A standalone test is added to cover the build system API. It builds a test into an object, and links it into a final executable, which it then runs. Using this build system mechanism prevents the build system from noticing that you're running a `zig test`, so the build runner and test runner do not communicate over stdio. However, that's okay, because the real-world use cases for this feature don't want to do that anyway! Resolves: #23374

8 files changed, 118 insertions(+), 42 deletions(-)

lib/std/Build.zig+5-1
......@@ -1019,6 +1019,10 @@ pub const TestOptions = struct {
10191019 use_llvm: ?bool = null,
10201020 use_lld: ?bool = null,
10211021 zig_lib_dir: ?LazyPath = null,
1022 /// Emits an object file instead of a test binary.
1023 /// The object must be linked separately.
1024 /// Usually used in conjunction with a custom `test_runner`.
1025 emit_object: bool = false,
10221026
10231027 /// Prefer populating this field (using e.g. `createModule`) instead of populating
10241028 /// the following fields (`root_source_file` etc). In a future release, those fields
......@@ -1067,7 +1071,7 @@ pub fn addTest(b: *Build, options: TestOptions) *Step.Compile {
10671071 }
10681072 return .create(b, .{
10691073 .name = options.name,
1070 .kind = .@"test",
1074 .kind = if (options.emit_object) .test_obj else .@"test",
10711075 .root_module = options.root_module orelse b.createModule(.{
10721076 .root_source_file = options.root_source_file orelse @panic("`root_module` and `root_source_file` cannot both be null"),
10731077 .target = options.target orelse b.graph.host,
lib/std/Build/Module.zig+1-1
......@@ -474,7 +474,7 @@ pub fn addObjectFile(m: *Module, object: LazyPath) void {
474474}
475475
476476pub fn addObject(m: *Module, object: *Step.Compile) void {
477 assert(object.kind == .obj);
477 assert(object.kind == .obj or object.kind == .test_obj);
478478 m.linkLibraryOrObject(object);
479479}
480480
lib/std/Build/Step/Compile.zig+7-4
......@@ -293,6 +293,7 @@ pub const Kind = enum {
293293 lib,
294294 obj,
295295 @"test",
296 test_obj,
296297};
297298
298299pub const HeaderInstallation = union(enum) {
......@@ -370,7 +371,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
370371 }
371372
372373 // Avoid the common case of the step name looking like "zig test test".
373 const name_adjusted = if (options.kind == .@"test" and mem.eql(u8, name, "test"))
374 const name_adjusted = if ((options.kind == .@"test" or options.kind == .test_obj) and mem.eql(u8, name, "test"))
374375 ""
375376 else
376377 owner.fmt("{s} ", .{name});
......@@ -385,6 +386,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
385386 .lib => "zig build-lib",
386387 .obj => "zig build-obj",
387388 .@"test" => "zig test",
389 .test_obj => "zig test-obj",
388390 },
389391 name_adjusted,
390392 @tagName(options.root_module.optimize orelse .Debug),
......@@ -396,7 +398,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
396398 .target = target,
397399 .output_mode = switch (options.kind) {
398400 .lib => .Lib,
399 .obj => .Obj,
401 .obj, .test_obj => .Obj,
400402 .exe, .@"test" => .Exe,
401403 },
402404 .link_mode = options.linkage,
......@@ -1053,6 +1055,7 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
10531055 .exe => "build-exe",
10541056 .obj => "build-obj",
10551057 .@"test" => "test",
1058 .test_obj => "test-obj",
10561059 };
10571060 try zig_args.append(cmd);
10581061
......@@ -1222,9 +1225,9 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
12221225 switch (other.kind) {
12231226 .exe => return step.fail("cannot link with an executable build artifact", .{}),
12241227 .@"test" => return step.fail("cannot link with a test", .{}),
1225 .obj => {
1228 .obj, .test_obj => {
12261229 const included_in_lib_or_obj = !my_responsibility and
1227 (dep_compile.kind == .lib or dep_compile.kind == .obj);
1230 (dep_compile.kind == .lib or dep_compile.kind == .obj or dep_compile.kind == .test_obj);
12281231 if (!already_linked and !included_in_lib_or_obj) {
12291232 try zig_args.append(other.getEmittedBin().getPath2(b, step));
12301233 total_linker_objects += 1;
lib/std/Build/Step/InstallArtifact.zig+1-1
......@@ -56,7 +56,7 @@ pub fn create(owner: *std.Build, artifact: *Step.Compile, options: Options) *Ins
5656 const dest_dir: ?InstallDir = switch (options.dest_dir) {
5757 .disabled => null,
5858 .default => switch (artifact.kind) {
59 .obj => @panic("object files have no standard installation procedure"),
59 .obj, .test_obj => @panic("object files have no standard installation procedure"),
6060 .exe, .@"test" => .bin,
6161 .lib => if (artifact.isDll()) .bin else .lib,
6262 },
src/main.zig+52-35
......@@ -278,6 +278,9 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
278278 } else if (mem.eql(u8, cmd, "test")) {
279279 dev.check(.test_command);
280280 return buildOutputType(gpa, arena, args, .zig_test);
281 } else if (mem.eql(u8, cmd, "test-obj")) {
282 dev.check(.test_command);
283 return buildOutputType(gpa, arena, args, .zig_test_obj);
281284 } else if (mem.eql(u8, cmd, "run")) {
282285 dev.check(.run_command);
283286 return buildOutputType(gpa, arena, args, .run);
......@@ -764,6 +767,7 @@ const ArgMode = union(enum) {
764767 cpp,
765768 translate_c,
766769 zig_test,
770 zig_test_obj,
767771 run,
768772};
769773
......@@ -975,7 +979,10 @@ fn buildOutputType(
975979 .dynamic_linker = null,
976980 .modules = .{},
977981 .opts = .{
978 .is_test = arg_mode == .zig_test,
982 .is_test = switch (arg_mode) {
983 .zig_test, .zig_test_obj => true,
984 .build, .cc, .cpp, .translate_c, .run => false,
985 },
979986 // Populated while parsing CLI args.
980987 .output_mode = undefined,
981988 // Populated in the call to `createModule` for the root module.
......@@ -1030,7 +1037,7 @@ fn buildOutputType(
10301037 var n_jobs: ?u32 = null;
10311038
10321039 switch (arg_mode) {
1033 .build, .translate_c, .zig_test, .run => {
1040 .build, .translate_c, .zig_test, .zig_test_obj, .run => {
10341041 switch (arg_mode) {
10351042 .build => |m| {
10361043 create_module.opts.output_mode = m;
......@@ -1042,6 +1049,9 @@ fn buildOutputType(
10421049 .zig_test, .run => {
10431050 create_module.opts.output_mode = .Exe;
10441051 },
1052 .zig_test_obj => {
1053 create_module.opts.output_mode = .Obj;
1054 },
10451055 else => unreachable,
10461056 }
10471057
......@@ -2834,6 +2844,10 @@ fn buildOutputType(
28342844 },
28352845 }
28362846
2847 if (arg_mode == .zig_test_obj and !test_no_exec and listen == .none) {
2848 fatal("test-obj requires --test-no-exec", .{});
2849 }
2850
28372851 if (arg_mode == .translate_c and create_module.c_source_files.items.len != 1) {
28382852 fatal("translate-c expects exactly 1 source file (found {d})", .{create_module.c_source_files.items.len});
28392853 }
......@@ -2903,10 +2917,10 @@ fn buildOutputType(
29032917 create_module.opts.any_error_tracing = true;
29042918
29052919 const src_path = try introspect.resolvePath(arena, unresolved_src_path);
2906 const name = if (arg_mode == .zig_test)
2907 "test"
2908 else
2909 fs.path.stem(fs.path.basename(src_path));
2920 const name = switch (arg_mode) {
2921 .zig_test => "test",
2922 .build, .cc, .cpp, .translate_c, .zig_test_obj, .run => fs.path.stem(fs.path.basename(src_path)),
2923 };
29102924
29112925 try create_module.modules.put(arena, name, .{
29122926 .paths = .{
......@@ -2935,7 +2949,7 @@ fn buildOutputType(
29352949 rc_source_files_owner_index = create_module.rc_source_files.items.len;
29362950 }
29372951
2938 if (!create_module.opts.have_zcu and arg_mode == .zig_test) {
2952 if (!create_module.opts.have_zcu and create_module.opts.is_test) {
29392953 fatal("`zig test` expects a zig source file argument", .{});
29402954 }
29412955
......@@ -3037,16 +3051,36 @@ fn buildOutputType(
30373051 break :m null;
30383052 };
30393053
3040 const root_mod = if (arg_mode == .zig_test) root_mod: {
3041 const test_mod = if (test_runner_path) |test_runner| test_mod: {
3042 const test_mod = try Package.Module.create(arena, .{
3054 const root_mod = switch (arg_mode) {
3055 .zig_test, .zig_test_obj => root_mod: {
3056 const test_mod = if (test_runner_path) |test_runner| test_mod: {
3057 const test_mod = try Package.Module.create(arena, .{
3058 .global_cache_directory = global_cache_directory,
3059 .paths = .{
3060 .root = .{
3061 .root_dir = Cache.Directory.cwd(),
3062 .sub_path = fs.path.dirname(test_runner) orelse "",
3063 },
3064 .root_src_path = fs.path.basename(test_runner),
3065 },
3066 .fully_qualified_name = "root",
3067 .cc_argv = &.{},
3068 .inherited = .{},
3069 .global = create_module.resolved_options,
3070 .parent = main_mod,
3071 .builtin_mod = main_mod.getBuiltinDependency(),
3072 .builtin_modules = null, // `builtin_mod` is specified
3073 });
3074 test_mod.deps = try main_mod.deps.clone(arena);
3075 break :test_mod test_mod;
3076 } else try Package.Module.create(arena, .{
30433077 .global_cache_directory = global_cache_directory,
30443078 .paths = .{
30453079 .root = .{
3046 .root_dir = Cache.Directory.cwd(),
3047 .sub_path = fs.path.dirname(test_runner) orelse "",
3080 .root_dir = zig_lib_directory,
3081 .sub_path = "compiler",
30483082 },
3049 .root_src_path = fs.path.basename(test_runner),
3083 .root_src_path = "test_runner.zig",
30503084 },
30513085 .fully_qualified_name = "root",
30523086 .cc_argv = &.{},
......@@ -3056,28 +3090,11 @@ fn buildOutputType(
30563090 .builtin_mod = main_mod.getBuiltinDependency(),
30573091 .builtin_modules = null, // `builtin_mod` is specified
30583092 });
3059 test_mod.deps = try main_mod.deps.clone(arena);
3060 break :test_mod test_mod;
3061 } else try Package.Module.create(arena, .{
3062 .global_cache_directory = global_cache_directory,
3063 .paths = .{
3064 .root = .{
3065 .root_dir = zig_lib_directory,
3066 .sub_path = "compiler",
3067 },
3068 .root_src_path = "test_runner.zig",
3069 },
3070 .fully_qualified_name = "root",
3071 .cc_argv = &.{},
3072 .inherited = .{},
3073 .global = create_module.resolved_options,
3074 .parent = main_mod,
3075 .builtin_mod = main_mod.getBuiltinDependency(),
3076 .builtin_modules = null, // `builtin_mod` is specified
3077 });
30783093
3079 break :root_mod test_mod;
3080 } else main_mod;
3094 break :root_mod test_mod;
3095 },
3096 else => main_mod,
3097 };
30813098
30823099 const target = main_mod.resolved_target.result;
30833100
......@@ -3202,7 +3219,7 @@ fn buildOutputType(
32023219 .directory = blk: {
32033220 switch (arg_mode) {
32043221 .run, .zig_test => break :blk null,
3205 else => {
3222 .build, .cc, .cpp, .translate_c, .zig_test_obj => {
32063223 if (output_to_cache) {
32073224 break :blk null;
32083225 } else {
test/standalone/build.zig.zon+3
......@@ -5,6 +5,9 @@
55 .simple = .{
66 .path = "simple",
77 },
8 .test_obj_link_run = .{
9 .path = "test_obj_link_run",
10 },
811 .test_runner_path = .{
912 .path = "test_runner_path",
1013 },
test/standalone/test_obj_link_run/build.zig created+32
......@@ -0,0 +1,32 @@
1pub fn build(b: *std.Build) void {
2 // To avoid having to explicitly link required system libraries into the final test
3 // executable (e.g. ntdll on Windows), we'll just link everything with libc here.
4
5 const test_obj = b.addTest(.{
6 .emit_object = true,
7 .root_module = b.createModule(.{
8 .root_source_file = b.path("src/main.zig"),
9 .target = b.graph.host,
10 .link_libc = true,
11 }),
12 });
13
14 const test_exe_mod = b.createModule(.{
15 .root_source_file = null,
16 .target = b.graph.host,
17 .link_libc = true,
18 });
19 test_exe_mod.addObject(test_obj);
20 const test_exe = b.addExecutable(.{
21 .name = "test",
22 .root_module = test_exe_mod,
23 });
24
25 const test_step = b.step("test", "Test the program");
26 b.default_step = test_step;
27
28 const test_run = b.addRunArtifact(test_exe);
29 test_step.dependOn(&test_run.step);
30}
31
32const std = @import("std");
test/standalone/test_obj_link_run/src/main.zig created+17
......@@ -0,0 +1,17 @@
1test {
2 try std.testing.expect(true);
3}
4
5test "equality" {
6 try std.testing.expect(one() == 1);
7}
8
9test "arithmetic" {
10 try std.testing.expect(one() + 2 == 3);
11}
12
13fn one() u32 {
14 return 1;
15}
16
17const std = @import("std");