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 {...@@ -1019,6 +1019,10 @@ pub const TestOptions = struct {
1019 use_llvm: ?bool = null,1019 use_llvm: ?bool = null,
1020 use_lld: ?bool = null,1020 use_lld: ?bool = null,
1021 zig_lib_dir: ?LazyPath = null,1021 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
1023 /// Prefer populating this field (using e.g. `createModule`) instead of populating1027 /// Prefer populating this field (using e.g. `createModule`) instead of populating
1024 /// the following fields (`root_source_file` etc). In a future release, those fields1028 /// 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 {...@@ -1067,7 +1071,7 @@ pub fn addTest(b: *Build, options: TestOptions) *Step.Compile {
1067 }1071 }
1068 return .create(b, .{1072 return .create(b, .{
1069 .name = options.name,1073 .name = options.name,
1070 .kind = .@"test",1074 .kind = if (options.emit_object) .test_obj else .@"test",
1071 .root_module = options.root_module orelse b.createModule(.{1075 .root_module = options.root_module orelse b.createModule(.{
1072 .root_source_file = options.root_source_file orelse @panic("`root_module` and `root_source_file` cannot both be null"),1076 .root_source_file = options.root_source_file orelse @panic("`root_module` and `root_source_file` cannot both be null"),
1073 .target = options.target orelse b.graph.host,1077 .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 {...@@ -474,7 +474,7 @@ pub fn addObjectFile(m: *Module, object: LazyPath) void {
474}474}
475475
476pub fn addObject(m: *Module, object: *Step.Compile) void {476pub fn addObject(m: *Module, object: *Step.Compile) void {
477 assert(object.kind == .obj);477 assert(object.kind == .obj or object.kind == .test_obj);
478 m.linkLibraryOrObject(object);478 m.linkLibraryOrObject(object);
479}479}
480480
lib/std/Build/Step/Compile.zig+7-4
...@@ -293,6 +293,7 @@ pub const Kind = enum {...@@ -293,6 +293,7 @@ pub const Kind = enum {
293 lib,293 lib,
294 obj,294 obj,
295 @"test",295 @"test",
296 test_obj,
296};297};
297298
298pub const HeaderInstallation = union(enum) {299pub const HeaderInstallation = union(enum) {
...@@ -370,7 +371,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {...@@ -370,7 +371,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
370 }371 }
371372
372 // Avoid the common case of the step name looking like "zig test test".373 // 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"))
374 ""375 ""
375 else376 else
376 owner.fmt("{s} ", .{name});377 owner.fmt("{s} ", .{name});
...@@ -385,6 +386,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {...@@ -385,6 +386,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
385 .lib => "zig build-lib",386 .lib => "zig build-lib",
386 .obj => "zig build-obj",387 .obj => "zig build-obj",
387 .@"test" => "zig test",388 .@"test" => "zig test",
389 .test_obj => "zig test-obj",
388 },390 },
389 name_adjusted,391 name_adjusted,
390 @tagName(options.root_module.optimize orelse .Debug),392 @tagName(options.root_module.optimize orelse .Debug),
...@@ -396,7 +398,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {...@@ -396,7 +398,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
396 .target = target,398 .target = target,
397 .output_mode = switch (options.kind) {399 .output_mode = switch (options.kind) {
398 .lib => .Lib,400 .lib => .Lib,
399 .obj => .Obj,401 .obj, .test_obj => .Obj,
400 .exe, .@"test" => .Exe,402 .exe, .@"test" => .Exe,
401 },403 },
402 .link_mode = options.linkage,404 .link_mode = options.linkage,
...@@ -1053,6 +1055,7 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {...@@ -1053,6 +1055,7 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
1053 .exe => "build-exe",1055 .exe => "build-exe",
1054 .obj => "build-obj",1056 .obj => "build-obj",
1055 .@"test" => "test",1057 .@"test" => "test",
1058 .test_obj => "test-obj",
1056 };1059 };
1057 try zig_args.append(cmd);1060 try zig_args.append(cmd);
10581061
...@@ -1222,9 +1225,9 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {...@@ -1222,9 +1225,9 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
1222 switch (other.kind) {1225 switch (other.kind) {
1223 .exe => return step.fail("cannot link with an executable build artifact", .{}),1226 .exe => return step.fail("cannot link with an executable build artifact", .{}),
1224 .@"test" => return step.fail("cannot link with a test", .{}),1227 .@"test" => return step.fail("cannot link with a test", .{}),
1225 .obj => {1228 .obj, .test_obj => {
1226 const included_in_lib_or_obj = !my_responsibility and1229 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);
1228 if (!already_linked and !included_in_lib_or_obj) {1231 if (!already_linked and !included_in_lib_or_obj) {
1229 try zig_args.append(other.getEmittedBin().getPath2(b, step));1232 try zig_args.append(other.getEmittedBin().getPath2(b, step));
1230 total_linker_objects += 1;1233 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...@@ -56,7 +56,7 @@ pub fn create(owner: *std.Build, artifact: *Step.Compile, options: Options) *Ins
56 const dest_dir: ?InstallDir = switch (options.dest_dir) {56 const dest_dir: ?InstallDir = switch (options.dest_dir) {
57 .disabled => null,57 .disabled => null,
58 .default => switch (artifact.kind) {58 .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"),
60 .exe, .@"test" => .bin,60 .exe, .@"test" => .bin,
61 .lib => if (artifact.isDll()) .bin else .lib,61 .lib => if (artifact.isDll()) .bin else .lib,
62 },62 },
src/main.zig+52-35
...@@ -278,6 +278,9 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {...@@ -278,6 +278,9 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
278 } else if (mem.eql(u8, cmd, "test")) {278 } else if (mem.eql(u8, cmd, "test")) {
279 dev.check(.test_command);279 dev.check(.test_command);
280 return buildOutputType(gpa, arena, args, .zig_test);280 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);
281 } else if (mem.eql(u8, cmd, "run")) {284 } else if (mem.eql(u8, cmd, "run")) {
282 dev.check(.run_command);285 dev.check(.run_command);
283 return buildOutputType(gpa, arena, args, .run);286 return buildOutputType(gpa, arena, args, .run);
...@@ -764,6 +767,7 @@ const ArgMode = union(enum) {...@@ -764,6 +767,7 @@ const ArgMode = union(enum) {
764 cpp,767 cpp,
765 translate_c,768 translate_c,
766 zig_test,769 zig_test,
770 zig_test_obj,
767 run,771 run,
768};772};
769773
...@@ -975,7 +979,10 @@ fn buildOutputType(...@@ -975,7 +979,10 @@ fn buildOutputType(
975 .dynamic_linker = null,979 .dynamic_linker = null,
976 .modules = .{},980 .modules = .{},
977 .opts = .{981 .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 },
979 // Populated while parsing CLI args.986 // Populated while parsing CLI args.
980 .output_mode = undefined,987 .output_mode = undefined,
981 // Populated in the call to `createModule` for the root module.988 // Populated in the call to `createModule` for the root module.
...@@ -1030,7 +1037,7 @@ fn buildOutputType(...@@ -1030,7 +1037,7 @@ fn buildOutputType(
1030 var n_jobs: ?u32 = null;1037 var n_jobs: ?u32 = null;
10311038
1032 switch (arg_mode) {1039 switch (arg_mode) {
1033 .build, .translate_c, .zig_test, .run => {1040 .build, .translate_c, .zig_test, .zig_test_obj, .run => {
1034 switch (arg_mode) {1041 switch (arg_mode) {
1035 .build => |m| {1042 .build => |m| {
1036 create_module.opts.output_mode = m;1043 create_module.opts.output_mode = m;
...@@ -1042,6 +1049,9 @@ fn buildOutputType(...@@ -1042,6 +1049,9 @@ fn buildOutputType(
1042 .zig_test, .run => {1049 .zig_test, .run => {
1043 create_module.opts.output_mode = .Exe;1050 create_module.opts.output_mode = .Exe;
1044 },1051 },
1052 .zig_test_obj => {
1053 create_module.opts.output_mode = .Obj;
1054 },
1045 else => unreachable,1055 else => unreachable,
1046 }1056 }
10471057
...@@ -2834,6 +2844,10 @@ fn buildOutputType(...@@ -2834,6 +2844,10 @@ fn buildOutputType(
2834 },2844 },
2835 }2845 }
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
2837 if (arg_mode == .translate_c and create_module.c_source_files.items.len != 1) {2851 if (arg_mode == .translate_c and create_module.c_source_files.items.len != 1) {
2838 fatal("translate-c expects exactly 1 source file (found {d})", .{create_module.c_source_files.items.len});2852 fatal("translate-c expects exactly 1 source file (found {d})", .{create_module.c_source_files.items.len});
2839 }2853 }
...@@ -2903,10 +2917,10 @@ fn buildOutputType(...@@ -2903,10 +2917,10 @@ fn buildOutputType(
2903 create_module.opts.any_error_tracing = true;2917 create_module.opts.any_error_tracing = true;
29042918
2905 const src_path = try introspect.resolvePath(arena, unresolved_src_path);2919 const src_path = try introspect.resolvePath(arena, unresolved_src_path);
2906 const name = if (arg_mode == .zig_test)2920 const name = switch (arg_mode) {
2907 "test"2921 .zig_test => "test",
2908 else2922 .build, .cc, .cpp, .translate_c, .zig_test_obj, .run => fs.path.stem(fs.path.basename(src_path)),
2909 fs.path.stem(fs.path.basename(src_path));2923 };
29102924
2911 try create_module.modules.put(arena, name, .{2925 try create_module.modules.put(arena, name, .{
2912 .paths = .{2926 .paths = .{
...@@ -2935,7 +2949,7 @@ fn buildOutputType(...@@ -2935,7 +2949,7 @@ fn buildOutputType(
2935 rc_source_files_owner_index = create_module.rc_source_files.items.len;2949 rc_source_files_owner_index = create_module.rc_source_files.items.len;
2936 }2950 }
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) {
2939 fatal("`zig test` expects a zig source file argument", .{});2953 fatal("`zig test` expects a zig source file argument", .{});
2940 }2954 }
29412955
...@@ -3037,16 +3051,36 @@ fn buildOutputType(...@@ -3037,16 +3051,36 @@ fn buildOutputType(
3037 break :m null;3051 break :m null;
3038 };3052 };
30393053
3040 const root_mod = if (arg_mode == .zig_test) root_mod: {3054 const root_mod = switch (arg_mode) {
3041 const test_mod = if (test_runner_path) |test_runner| test_mod: {3055 .zig_test, .zig_test_obj => root_mod: {
3042 const test_mod = try Package.Module.create(arena, .{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, .{
3043 .global_cache_directory = global_cache_directory,3077 .global_cache_directory = global_cache_directory,
3044 .paths = .{3078 .paths = .{
3045 .root = .{3079 .root = .{
3046 .root_dir = Cache.Directory.cwd(),3080 .root_dir = zig_lib_directory,
3047 .sub_path = fs.path.dirname(test_runner) orelse "",3081 .sub_path = "compiler",
3048 },3082 },
3049 .root_src_path = fs.path.basename(test_runner),3083 .root_src_path = "test_runner.zig",
3050 },3084 },
3051 .fully_qualified_name = "root",3085 .fully_qualified_name = "root",
3052 .cc_argv = &.{},3086 .cc_argv = &.{},
...@@ -3056,28 +3090,11 @@ fn buildOutputType(...@@ -3056,28 +3090,11 @@ fn buildOutputType(
3056 .builtin_mod = main_mod.getBuiltinDependency(),3090 .builtin_mod = main_mod.getBuiltinDependency(),
3057 .builtin_modules = null, // `builtin_mod` is specified3091 .builtin_modules = null, // `builtin_mod` is specified
3058 });3092 });
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;3094 break :root_mod test_mod;
3080 } else main_mod;3095 },
3096 else => main_mod,
3097 };
30813098
3082 const target = main_mod.resolved_target.result;3099 const target = main_mod.resolved_target.result;
30833100
...@@ -3202,7 +3219,7 @@ fn buildOutputType(...@@ -3202,7 +3219,7 @@ fn buildOutputType(
3202 .directory = blk: {3219 .directory = blk: {
3203 switch (arg_mode) {3220 switch (arg_mode) {
3204 .run, .zig_test => break :blk null,3221 .run, .zig_test => break :blk null,
3205 else => {3222 .build, .cc, .cpp, .translate_c, .zig_test_obj => {
3206 if (output_to_cache) {3223 if (output_to_cache) {
3207 break :blk null;3224 break :blk null;
3208 } else {3225 } else {
test/standalone/build.zig.zon+3
...@@ -5,6 +5,9 @@...@@ -5,6 +5,9 @@
5 .simple = .{5 .simple = .{
6 .path = "simple",6 .path = "simple",
7 },7 },
8 .test_obj_link_run = .{
9 .path = "test_obj_link_run",
10 },
8 .test_runner_path = .{11 .test_runner_path = .{
9 .path = "test_runner_path",12 .path = "test_runner_path",
10 },13 },
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");