diff --git a/lib/std/Build.zig b/lib/std/Build.zig index d31d9f3ee0..8ce4fe40e3 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -1019,6 +1019,10 @@ pub const TestOptions = struct { use_llvm: ?bool = null, use_lld: ?bool = null, zig_lib_dir: ?LazyPath = null, + /// Emits an object file instead of a test binary. + /// The object must be linked separately. + /// Usually used in conjunction with a custom `test_runner`. + emit_object: bool = false, /// Prefer populating this field (using e.g. `createModule`) instead of populating /// 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 { } return .create(b, .{ .name = options.name, - .kind = .@"test", + .kind = if (options.emit_object) .test_obj else .@"test", .root_module = options.root_module orelse b.createModule(.{ .root_source_file = options.root_source_file orelse @panic("`root_module` and `root_source_file` cannot both be null"), .target = options.target orelse b.graph.host, diff --git a/lib/std/Build/Module.zig b/lib/std/Build/Module.zig index 0bc77b0741..ff85d5a6c7 100644 --- a/lib/std/Build/Module.zig +++ b/lib/std/Build/Module.zig @@ -474,7 +474,7 @@ pub fn addObjectFile(m: *Module, object: LazyPath) void { } pub fn addObject(m: *Module, object: *Step.Compile) void { - assert(object.kind == .obj); + assert(object.kind == .obj or object.kind == .test_obj); m.linkLibraryOrObject(object); } diff --git a/lib/std/Build/Step/Compile.zig b/lib/std/Build/Step/Compile.zig index ff6e766c58..e436865108 100644 --- a/lib/std/Build/Step/Compile.zig +++ b/lib/std/Build/Step/Compile.zig @@ -293,6 +293,7 @@ pub const Kind = enum { lib, obj, @"test", + test_obj, }; pub const HeaderInstallation = union(enum) { @@ -370,7 +371,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile { } // Avoid the common case of the step name looking like "zig test test". - const name_adjusted = if (options.kind == .@"test" and mem.eql(u8, name, "test")) + const name_adjusted = if ((options.kind == .@"test" or options.kind == .test_obj) and mem.eql(u8, name, "test")) "" else owner.fmt("{s} ", .{name}); @@ -385,6 +386,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile { .lib => "zig build-lib", .obj => "zig build-obj", .@"test" => "zig test", + .test_obj => "zig test-obj", }, name_adjusted, @tagName(options.root_module.optimize orelse .Debug), @@ -396,7 +398,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile { .target = target, .output_mode = switch (options.kind) { .lib => .Lib, - .obj => .Obj, + .obj, .test_obj => .Obj, .exe, .@"test" => .Exe, }, .link_mode = options.linkage, @@ -1053,6 +1055,7 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { .exe => "build-exe", .obj => "build-obj", .@"test" => "test", + .test_obj => "test-obj", }; try zig_args.append(cmd); @@ -1222,9 +1225,9 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { switch (other.kind) { .exe => return step.fail("cannot link with an executable build artifact", .{}), .@"test" => return step.fail("cannot link with a test", .{}), - .obj => { + .obj, .test_obj => { const included_in_lib_or_obj = !my_responsibility and - (dep_compile.kind == .lib or dep_compile.kind == .obj); + (dep_compile.kind == .lib or dep_compile.kind == .obj or dep_compile.kind == .test_obj); if (!already_linked and !included_in_lib_or_obj) { try zig_args.append(other.getEmittedBin().getPath2(b, step)); total_linker_objects += 1; diff --git a/lib/std/Build/Step/InstallArtifact.zig b/lib/std/Build/Step/InstallArtifact.zig index acf392f49f..9d8cb92bb7 100644 --- a/lib/std/Build/Step/InstallArtifact.zig +++ b/lib/std/Build/Step/InstallArtifact.zig @@ -56,7 +56,7 @@ pub fn create(owner: *std.Build, artifact: *Step.Compile, options: Options) *Ins const dest_dir: ?InstallDir = switch (options.dest_dir) { .disabled => null, .default => switch (artifact.kind) { - .obj => @panic("object files have no standard installation procedure"), + .obj, .test_obj => @panic("object files have no standard installation procedure"), .exe, .@"test" => .bin, .lib => if (artifact.isDll()) .bin else .lib, }, diff --git a/src/main.zig b/src/main.zig index 740cea1cdb..98807cb73c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -278,6 +278,9 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { } else if (mem.eql(u8, cmd, "test")) { dev.check(.test_command); return buildOutputType(gpa, arena, args, .zig_test); + } else if (mem.eql(u8, cmd, "test-obj")) { + dev.check(.test_command); + return buildOutputType(gpa, arena, args, .zig_test_obj); } else if (mem.eql(u8, cmd, "run")) { dev.check(.run_command); return buildOutputType(gpa, arena, args, .run); @@ -764,6 +767,7 @@ const ArgMode = union(enum) { cpp, translate_c, zig_test, + zig_test_obj, run, }; @@ -975,7 +979,10 @@ fn buildOutputType( .dynamic_linker = null, .modules = .{}, .opts = .{ - .is_test = arg_mode == .zig_test, + .is_test = switch (arg_mode) { + .zig_test, .zig_test_obj => true, + .build, .cc, .cpp, .translate_c, .run => false, + }, // Populated while parsing CLI args. .output_mode = undefined, // Populated in the call to `createModule` for the root module. @@ -1030,7 +1037,7 @@ fn buildOutputType( var n_jobs: ?u32 = null; switch (arg_mode) { - .build, .translate_c, .zig_test, .run => { + .build, .translate_c, .zig_test, .zig_test_obj, .run => { switch (arg_mode) { .build => |m| { create_module.opts.output_mode = m; @@ -1042,6 +1049,9 @@ fn buildOutputType( .zig_test, .run => { create_module.opts.output_mode = .Exe; }, + .zig_test_obj => { + create_module.opts.output_mode = .Obj; + }, else => unreachable, } @@ -2834,6 +2844,10 @@ fn buildOutputType( }, } + if (arg_mode == .zig_test_obj and !test_no_exec and listen == .none) { + fatal("test-obj requires --test-no-exec", .{}); + } + if (arg_mode == .translate_c and create_module.c_source_files.items.len != 1) { fatal("translate-c expects exactly 1 source file (found {d})", .{create_module.c_source_files.items.len}); } @@ -2903,10 +2917,10 @@ fn buildOutputType( create_module.opts.any_error_tracing = true; const src_path = try introspect.resolvePath(arena, unresolved_src_path); - const name = if (arg_mode == .zig_test) - "test" - else - fs.path.stem(fs.path.basename(src_path)); + const name = switch (arg_mode) { + .zig_test => "test", + .build, .cc, .cpp, .translate_c, .zig_test_obj, .run => fs.path.stem(fs.path.basename(src_path)), + }; try create_module.modules.put(arena, name, .{ .paths = .{ @@ -2935,7 +2949,7 @@ fn buildOutputType( rc_source_files_owner_index = create_module.rc_source_files.items.len; } - if (!create_module.opts.have_zcu and arg_mode == .zig_test) { + if (!create_module.opts.have_zcu and create_module.opts.is_test) { fatal("`zig test` expects a zig source file argument", .{}); } @@ -3037,16 +3051,36 @@ fn buildOutputType( break :m null; }; - const root_mod = if (arg_mode == .zig_test) root_mod: { - const test_mod = if (test_runner_path) |test_runner| test_mod: { - const test_mod = try Package.Module.create(arena, .{ + const root_mod = switch (arg_mode) { + .zig_test, .zig_test_obj => root_mod: { + const test_mod = if (test_runner_path) |test_runner| test_mod: { + const test_mod = try Package.Module.create(arena, .{ + .global_cache_directory = global_cache_directory, + .paths = .{ + .root = .{ + .root_dir = Cache.Directory.cwd(), + .sub_path = fs.path.dirname(test_runner) orelse "", + }, + .root_src_path = fs.path.basename(test_runner), + }, + .fully_qualified_name = "root", + .cc_argv = &.{}, + .inherited = .{}, + .global = create_module.resolved_options, + .parent = main_mod, + .builtin_mod = main_mod.getBuiltinDependency(), + .builtin_modules = null, // `builtin_mod` is specified + }); + test_mod.deps = try main_mod.deps.clone(arena); + break :test_mod test_mod; + } else try Package.Module.create(arena, .{ .global_cache_directory = global_cache_directory, .paths = .{ .root = .{ - .root_dir = Cache.Directory.cwd(), - .sub_path = fs.path.dirname(test_runner) orelse "", + .root_dir = zig_lib_directory, + .sub_path = "compiler", }, - .root_src_path = fs.path.basename(test_runner), + .root_src_path = "test_runner.zig", }, .fully_qualified_name = "root", .cc_argv = &.{}, @@ -3056,28 +3090,11 @@ fn buildOutputType( .builtin_mod = main_mod.getBuiltinDependency(), .builtin_modules = null, // `builtin_mod` is specified }); - test_mod.deps = try main_mod.deps.clone(arena); - break :test_mod test_mod; - } else try Package.Module.create(arena, .{ - .global_cache_directory = global_cache_directory, - .paths = .{ - .root = .{ - .root_dir = zig_lib_directory, - .sub_path = "compiler", - }, - .root_src_path = "test_runner.zig", - }, - .fully_qualified_name = "root", - .cc_argv = &.{}, - .inherited = .{}, - .global = create_module.resolved_options, - .parent = main_mod, - .builtin_mod = main_mod.getBuiltinDependency(), - .builtin_modules = null, // `builtin_mod` is specified - }); - break :root_mod test_mod; - } else main_mod; + break :root_mod test_mod; + }, + else => main_mod, + }; const target = main_mod.resolved_target.result; @@ -3202,7 +3219,7 @@ fn buildOutputType( .directory = blk: { switch (arg_mode) { .run, .zig_test => break :blk null, - else => { + .build, .cc, .cpp, .translate_c, .zig_test_obj => { if (output_to_cache) { break :blk null; } else { diff --git a/test/standalone/build.zig.zon b/test/standalone/build.zig.zon index 431ea01452..44358b37f0 100644 --- a/test/standalone/build.zig.zon +++ b/test/standalone/build.zig.zon @@ -5,6 +5,9 @@ .simple = .{ .path = "simple", }, + .test_obj_link_run = .{ + .path = "test_obj_link_run", + }, .test_runner_path = .{ .path = "test_runner_path", }, diff --git a/test/standalone/test_obj_link_run/build.zig b/test/standalone/test_obj_link_run/build.zig new file mode 100644 index 0000000000..49d184861d --- /dev/null +++ b/test/standalone/test_obj_link_run/build.zig @@ -0,0 +1,32 @@ +pub fn build(b: *std.Build) void { + // To avoid having to explicitly link required system libraries into the final test + // executable (e.g. ntdll on Windows), we'll just link everything with libc here. + + const test_obj = b.addTest(.{ + .emit_object = true, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = b.graph.host, + .link_libc = true, + }), + }); + + const test_exe_mod = b.createModule(.{ + .root_source_file = null, + .target = b.graph.host, + .link_libc = true, + }); + test_exe_mod.addObject(test_obj); + const test_exe = b.addExecutable(.{ + .name = "test", + .root_module = test_exe_mod, + }); + + const test_step = b.step("test", "Test the program"); + b.default_step = test_step; + + const test_run = b.addRunArtifact(test_exe); + test_step.dependOn(&test_run.step); +} + +const std = @import("std"); diff --git a/test/standalone/test_obj_link_run/src/main.zig b/test/standalone/test_obj_link_run/src/main.zig new file mode 100644 index 0000000000..119092ec6d --- /dev/null +++ b/test/standalone/test_obj_link_run/src/main.zig @@ -0,0 +1,17 @@ +test { + try std.testing.expect(true); +} + +test "equality" { + try std.testing.expect(one() == 1); +} + +test "arithmetic" { + try std.testing.expect(one() + 2 == 3); +} + +fn one() u32 { + return 1; +} + +const std = @import("std");