Commit 9b8e23934b

Andrew Kelley <andrew@ziglang.org>
2019-02-01 23:49:29
introduce --single-threaded build option
closes #1764 This adds another boolean to the test matrix; hopefully it does not inflate the time too much. std.event.Loop does not work with this option yet. See #1908
1 parent 8bedb10
doc/langref.html.in
@@ -6714,6 +6714,25 @@ pub fn build(b: *Builder) void {
       {#header_close#}
       {#see_also|Compile Variables|Zig Build System|Undefined Behavior#}
       {#header_close#}
+
+      {#header_open|Single Threaded Builds#}
+      <p>Zig has a compile option <code>--single-threaded</code> which has the following effects:
+      <ul>
+        <li>{#link|@atomicLoad#} is emitted as a normal load.</li>
+        <li>{#link|@atomicRmw#} is emitted as a normal memory load, modify, store.</li>
+        <li>{#link|@fence#} becomes a no-op.</li>
+        <li>Variables which have Thread Local Storage instead become globals. TODO thread local variables
+        are not implemented yet.</li>
+        <li>The overhead of {#link|Coroutines#} becomes equivalent to function call overhead.
+          TODO: please note this will not be implemented until the upcoming Coroutine Rewrite</li>
+        <li>The {#syntax#}@import("builtin").single_threaded{#endsyntax#} becomes {#syntax#}true{#endsyntax#}
+          and therefore various userland APIs which read this variable become more efficient.
+          For example {#syntax#}std.Mutex{#endsyntax#} becomes
+          an empty data structure and all of its functions become no-ops.</li>
+      </ul>
+      </p>
+      {#header_close#}
+
       {#header_open|Undefined Behavior#}
       <p>
       Zig has many instances of undefined behavior. If undefined behavior is
src/all_types.hpp
@@ -1808,6 +1808,7 @@ struct CodeGen {
     bool is_static;
     bool strip_debug_symbols;
     bool is_test_build;
+    bool is_single_threaded;
     bool is_native_target;
     bool linker_rdynamic;
     bool no_rosegment_workaround;
src/codegen.cpp
@@ -118,6 +118,7 @@ CodeGen *codegen_create(Buf *root_src_path, const ZigTarget *target, OutType out
     g->string_literals_table.init(16);
     g->type_info_cache.init(32);
     g->is_test_build = false;
+    g->is_single_threaded = false;
     buf_resize(&g->global_asm, 0);
 
     for (size_t i = 0; i < array_length(symbols_that_llvm_depends_on); i += 1) {
@@ -7377,6 +7378,7 @@ Buf *codegen_generate_builtin_source(CodeGen *g) {
         buf_appendf(contents, "pub const endian = %s;\n", endian_str);
     }
     buf_appendf(contents, "pub const is_test = %s;\n", bool_to_str(g->is_test_build));
+    buf_appendf(contents, "pub const single_threaded = %s;\n", bool_to_str(g->is_single_threaded));
     buf_appendf(contents, "pub const os = Os.%s;\n", cur_os);
     buf_appendf(contents, "pub const arch = Arch.%s;\n", cur_arch);
     buf_appendf(contents, "pub const environ = Environ.%s;\n", cur_environ);
@@ -7411,6 +7413,7 @@ static Error define_builtin_compile_vars(CodeGen *g) {
     cache_buf(&cache_hash, compiler_id);
     cache_int(&cache_hash, g->build_mode);
     cache_bool(&cache_hash, g->is_test_build);
+    cache_bool(&cache_hash, g->is_single_threaded);
     cache_int(&cache_hash, g->zig_target.arch.arch);
     cache_int(&cache_hash, g->zig_target.arch.sub_arch);
     cache_int(&cache_hash, g->zig_target.vendor);
@@ -8329,6 +8332,7 @@ static Error check_cache(CodeGen *g, Buf *manifest_dir, Buf *digest) {
     cache_bool(ch, g->is_static);
     cache_bool(ch, g->strip_debug_symbols);
     cache_bool(ch, g->is_test_build);
+    cache_bool(ch, g->is_single_threaded);
     cache_bool(ch, g->is_native_target);
     cache_bool(ch, g->linker_rdynamic);
     cache_bool(ch, g->no_rosegment_workaround);
src/main.cpp
@@ -59,6 +59,7 @@ static int print_full_usage(const char *arg0) {
         "  --release-fast               build with optimizations on and safety off\n"
         "  --release-safe               build with optimizations on and safety on\n"
         "  --release-small              build with size optimizations on and safety off\n"
+        "  --single-threaded            source may assume it is only used single-threaded\n"
         "  --static                     output will be statically linked\n"
         "  --strip                      exclude debug symbols\n"
         "  --target-arch [name]         specify target architecture\n"
@@ -393,6 +394,7 @@ int main(int argc, char **argv) {
     bool no_rosegment_workaround = false;
     bool system_linker_hack = false;
     TargetSubsystem subsystem = TargetSubsystemAuto;
+    bool is_single_threaded = false;
 
     if (argc >= 2 && strcmp(argv[1], "build") == 0) {
         Buf zig_exe_path_buf = BUF_INIT;
@@ -550,6 +552,8 @@ int main(int argc, char **argv) {
                 disable_pic = true;
             } else if (strcmp(arg, "--system-linker-hack") == 0) {
                 system_linker_hack = true;
+            } else if (strcmp(arg, "--single-threaded") == 0) {
+                is_single_threaded = true;
             } else if (strcmp(arg, "--test-cmd-bin") == 0) {
                 test_exec_args.append(nullptr);
             } else if (arg[1] == 'L' && arg[2] != 0) {
@@ -816,6 +820,7 @@ int main(int argc, char **argv) {
     switch (cmd) {
     case CmdBuiltin: {
         CodeGen *g = codegen_create(nullptr, target, out_type, build_mode, get_zig_lib_dir());
+        g->is_single_threaded = is_single_threaded;
         Buf *builtin_source = codegen_generate_builtin_source(g);
         if (fwrite(buf_ptr(builtin_source), 1, buf_len(builtin_source), stdout) != buf_len(builtin_source)) {
             fprintf(stderr, "unable to write to stdout: %s\n", strerror(ferror(stdout)));
@@ -889,6 +894,7 @@ int main(int argc, char **argv) {
             codegen_set_out_name(g, buf_out_name);
             codegen_set_lib_version(g, ver_major, ver_minor, ver_patch);
             codegen_set_is_test(g, cmd == CmdTest);
+            g->is_single_threaded = is_single_threaded;
             codegen_set_linker_script(g, linker_script);
             if (each_lib_rpath)
                 codegen_set_each_lib_rpath(g, each_lib_rpath);
std/atomic/queue.zig
@@ -170,20 +170,36 @@ test "std.atomic.Queue" {
         .get_count = 0,
     };
 
-    var putters: [put_thread_count]*std.os.Thread = undefined;
-    for (putters) |*t| {
-        t.* = try std.os.spawnThread(&context, startPuts);
-    }
-    var getters: [put_thread_count]*std.os.Thread = undefined;
-    for (getters) |*t| {
-        t.* = try std.os.spawnThread(&context, startGets);
-    }
+    if (builtin.single_threaded) {
+        {
+            var i: usize = 0;
+            while (i < put_thread_count) : (i += 1) {
+                std.debug.assertOrPanic(startPuts(&context) == 0);
+            }
+        }
+        context.puts_done = 1;
+        {
+            var i: usize = 0;
+            while (i < put_thread_count) : (i += 1) {
+                std.debug.assertOrPanic(startGets(&context) == 0);
+            }
+        }
+    } else {
+        var putters: [put_thread_count]*std.os.Thread = undefined;
+        for (putters) |*t| {
+            t.* = try std.os.spawnThread(&context, startPuts);
+        }
+        var getters: [put_thread_count]*std.os.Thread = undefined;
+        for (getters) |*t| {
+            t.* = try std.os.spawnThread(&context, startGets);
+        }
 
-    for (putters) |t|
-        t.wait();
-    _ = @atomicRmw(u8, &context.puts_done, builtin.AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst);
-    for (getters) |t|
-        t.wait();
+        for (putters) |t|
+            t.wait();
+        _ = @atomicRmw(u8, &context.puts_done, builtin.AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst);
+        for (getters) |t|
+            t.wait();
+    }
 
     if (context.put_sum != context.get_sum) {
         std.debug.panic("failure\nput_sum:{} != get_sum:{}", context.put_sum, context.get_sum);
std/atomic/stack.zig
@@ -4,10 +4,13 @@ const AtomicOrder = builtin.AtomicOrder;
 
 /// Many reader, many writer, non-allocating, thread-safe
 /// Uses a spinlock to protect push() and pop()
+/// When building in single threaded mode, this is a simple linked list.
 pub fn Stack(comptime T: type) type {
     return struct {
         root: ?*Node,
-        lock: u8,
+        lock: @typeOf(lock_init),
+
+        const lock_init = if (builtin.single_threaded) {} else u8(0);
 
         pub const Self = @This();
 
@@ -19,7 +22,7 @@ pub fn Stack(comptime T: type) type {
         pub fn init() Self {
             return Self{
                 .root = null,
-                .lock = 0,
+                .lock = lock_init,
             };
         }
 
@@ -31,20 +34,31 @@ pub fn Stack(comptime T: type) type {
         }
 
         pub fn push(self: *Self, node: *Node) void {
-            while (@atomicRmw(u8, &self.lock, builtin.AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst) != 0) {}
-            defer assert(@atomicRmw(u8, &self.lock, builtin.AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst) == 1);
-
-            node.next = self.root;
-            self.root = node;
+            if (builtin.single_threaded) {
+                node.next = self.root;
+                self.root = node;
+            } else {
+                while (@atomicRmw(u8, &self.lock, builtin.AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst) != 0) {}
+                defer assert(@atomicRmw(u8, &self.lock, builtin.AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst) == 1);
+
+                node.next = self.root;
+                self.root = node;
+            }
         }
 
         pub fn pop(self: *Self) ?*Node {
-            while (@atomicRmw(u8, &self.lock, builtin.AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst) != 0) {}
-            defer assert(@atomicRmw(u8, &self.lock, builtin.AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst) == 1);
-
-            const root = self.root orelse return null;
-            self.root = root.next;
-            return root;
+            if (builtin.single_threaded) {
+                const root = self.root orelse return null;
+                self.root = root.next;
+                return root;
+            } else {
+                while (@atomicRmw(u8, &self.lock, builtin.AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst) != 0) {}
+                defer assert(@atomicRmw(u8, &self.lock, builtin.AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst) == 1);
+
+                const root = self.root orelse return null;
+                self.root = root.next;
+                return root;
+            }
         }
 
         pub fn isEmpty(self: *Self) bool {
@@ -90,20 +104,36 @@ test "std.atomic.stack" {
         .get_count = 0,
     };
 
-    var putters: [put_thread_count]*std.os.Thread = undefined;
-    for (putters) |*t| {
-        t.* = try std.os.spawnThread(&context, startPuts);
-    }
-    var getters: [put_thread_count]*std.os.Thread = undefined;
-    for (getters) |*t| {
-        t.* = try std.os.spawnThread(&context, startGets);
-    }
+    if (builtin.single_threaded) {
+        {
+            var i: usize = 0;
+            while (i < put_thread_count) : (i += 1) {
+                std.debug.assertOrPanic(startPuts(&context) == 0);
+            }
+        }
+        context.puts_done = 1;
+        {
+            var i: usize = 0;
+            while (i < put_thread_count) : (i += 1) {
+                std.debug.assertOrPanic(startGets(&context) == 0);
+            }
+        }
+    } else {
+        var putters: [put_thread_count]*std.os.Thread = undefined;
+        for (putters) |*t| {
+            t.* = try std.os.spawnThread(&context, startPuts);
+        }
+        var getters: [put_thread_count]*std.os.Thread = undefined;
+        for (getters) |*t| {
+            t.* = try std.os.spawnThread(&context, startGets);
+        }
 
-    for (putters) |t|
-        t.wait();
-    _ = @atomicRmw(u8, &context.puts_done, builtin.AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst);
-    for (getters) |t|
-        t.wait();
+        for (putters) |t|
+            t.wait();
+        _ = @atomicRmw(u8, &context.puts_done, builtin.AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst);
+        for (getters) |t|
+            t.wait();
+    }
 
     if (context.put_sum != context.get_sum) {
         std.debug.panic("failure\nput_sum:{} != get_sum:{}", context.put_sum, context.get_sum);
std/event/channel.zig
@@ -319,6 +319,9 @@ pub fn Channel(comptime T: type) type {
 }
 
 test "std.event.Channel" {
+    // https://github.com/ziglang/zig/issues/1908
+    if (builtin.single_threaded) return error.SkipZigTest;
+
     var da = std.heap.DirectAllocator.init();
     defer da.deinit();
 
std/event/future.zig
@@ -84,6 +84,9 @@ pub fn Future(comptime T: type) type {
 }
 
 test "std.event.Future" {
+    // https://github.com/ziglang/zig/issues/1908
+    if (builtin.single_threaded) return error.SkipZigTest;
+
     var da = std.heap.DirectAllocator.init();
     defer da.deinit();
 
std/event/group.zig
@@ -121,6 +121,9 @@ pub fn Group(comptime ReturnType: type) type {
 }
 
 test "std.event.Group" {
+    // https://github.com/ziglang/zig/issues/1908
+    if (builtin.single_threaded) return error.SkipZigTest;
+
     var da = std.heap.DirectAllocator.init();
     defer da.deinit();
 
std/event/lock.zig
@@ -122,6 +122,9 @@ pub const Lock = struct {
 };
 
 test "std.event.Lock" {
+    // https://github.com/ziglang/zig/issues/1908
+    if (builtin.single_threaded) return error.SkipZigTest;
+
     var da = std.heap.DirectAllocator.init();
     defer da.deinit();
 
std/event/loop.zig
@@ -97,6 +97,7 @@ pub const Loop = struct {
     /// TODO copy elision / named return values so that the threads referencing *Loop
     /// have the correct pointer value.
     pub fn initMultiThreaded(self: *Loop, allocator: *mem.Allocator) !void {
+        if (builtin.single_threaded) @compileError("initMultiThreaded unavailable when building in single-threaded mode");
         const core_count = try os.cpuCount(allocator);
         return self.initInternal(allocator, core_count);
     }
@@ -201,6 +202,11 @@ pub const Loop = struct {
                     self.os_data.fs_thread.wait();
                 }
 
+                if (builtin.single_threaded) {
+                    assert(extra_thread_count == 0);
+                    return;
+                }
+
                 var extra_thread_index: usize = 0;
                 errdefer {
                     // writing 8 bytes to an eventfd cannot fail
@@ -301,6 +307,11 @@ pub const Loop = struct {
                     self.os_data.fs_thread.wait();
                 }
 
+                if (builtin.single_threaded) {
+                    assert(extra_thread_count == 0);
+                    return;
+                }
+
                 var extra_thread_index: usize = 0;
                 errdefer {
                     _ = os.bsdKEvent(self.os_data.kqfd, final_kev_arr, empty_kevs, null) catch unreachable;
@@ -338,6 +349,11 @@ pub const Loop = struct {
                     self.available_eventfd_resume_nodes.push(eventfd_node);
                 }
 
+                if (builtin.single_threaded) {
+                    assert(extra_thread_count == 0);
+                    return;
+                }
+
                 var extra_thread_index: usize = 0;
                 errdefer {
                     var i: usize = 0;
@@ -845,6 +861,9 @@ pub const Loop = struct {
 };
 
 test "std.event.Loop - basic" {
+    // https://github.com/ziglang/zig/issues/1908
+    if (builtin.single_threaded) return error.SkipZigTest;
+
     var da = std.heap.DirectAllocator.init();
     defer da.deinit();
 
@@ -858,6 +877,9 @@ test "std.event.Loop - basic" {
 }
 
 test "std.event.Loop - call" {
+    // https://github.com/ziglang/zig/issues/1908
+    if (builtin.single_threaded) return error.SkipZigTest;
+
     var da = std.heap.DirectAllocator.init();
     defer da.deinit();
 
std/event/net.zig
@@ -269,6 +269,9 @@ pub async fn connect(loop: *Loop, _address: *const std.net.Address) !os.File {
 }
 
 test "listen on a port, send bytes, receive bytes" {
+    // https://github.com/ziglang/zig/issues/1908
+    if (builtin.single_threaded) return error.SkipZigTest;
+
     if (builtin.os != builtin.Os.linux) {
         // TODO build abstractions for other operating systems
         return error.SkipZigTest;
std/event/rwlock.zig
@@ -211,6 +211,9 @@ pub const RwLock = struct {
 };
 
 test "std.event.RwLock" {
+    // https://github.com/ziglang/zig/issues/1908
+    if (builtin.single_threaded) return error.SkipZigTest;
+
     var da = std.heap.DirectAllocator.init();
     defer da.deinit();
 
std/os/index.zig
@@ -3013,6 +3013,7 @@ pub const SpawnThreadError = error{
 /// where T is u8, noreturn, void, or !void
 /// caller must call wait on the returned thread
 pub fn spawnThread(context: var, comptime startFn: var) SpawnThreadError!*Thread {
+    if (builtin.single_threaded) @compileError("cannot spawn thread when building in single-threaded mode");
     // TODO compile-time call graph analysis to determine stack upper bound
     // https://github.com/ziglang/zig/issues/157
     const default_stack_size = 8 * 1024 * 1024;
std/os/test.zig
@@ -40,6 +40,8 @@ fn testThreadIdFn(thread_id: *os.Thread.Id) void {
 }
 
 test "std.os.Thread.getCurrentId" {
+    if (builtin.single_threaded) return error.SkipZigTest;
+
     var thread_current_id: os.Thread.Id = undefined;
     const thread = try os.spawnThread(&thread_current_id, testThreadIdFn);
     const thread_id = thread.handle();
@@ -53,6 +55,8 @@ test "std.os.Thread.getCurrentId" {
 }
 
 test "spawn threads" {
+    if (builtin.single_threaded) return error.SkipZigTest;
+
     var shared_ctx: i32 = 1;
 
     const thread1 = try std.os.spawnThread({}, start1);
std/mutex.zig
@@ -14,7 +14,36 @@ const windows = std.os.windows;
 /// If you need static initialization, use std.StaticallyInitializedMutex.
 /// The Linux implementation is based on mutex3 from
 /// https://www.akkadia.org/drepper/futex.pdf
-pub const Mutex = switch(builtin.os) {
+/// When an application is built in single threaded release mode, all the functions are
+/// no-ops. In single threaded debug mode, there is deadlock detection.
+pub const Mutex = if (builtin.single_threaded)
+    struct {
+        lock: @typeOf(lock_init),
+
+        const lock_init = if (std.debug.runtime_safety) false else {};
+
+        pub const Held = struct {
+            mutex: *Mutex,
+
+            pub fn release(self: Held) void {
+                if (std.debug.runtime_safety) {
+                    self.mutex.lock = false;
+                }
+            }
+        };
+        pub fn init() Mutex {
+            return Mutex{ .lock = lock_init };
+        }
+        pub fn deinit(self: *Mutex) void {}
+
+        pub fn acquire(self: *Mutex) Held {
+            if (std.debug.runtime_safety and self.lock) {
+                @panic("deadlock detected");
+            }
+            return Held{ .mutex = self };
+        }
+    }
+else switch (builtin.os) {
     builtin.Os.linux => struct {
         /// 0: unlocked
         /// 1: locked, no waiters
@@ -39,9 +68,7 @@ pub const Mutex = switch(builtin.os) {
         };
 
         pub fn init() Mutex {
-            return Mutex {
-                .lock = 0,
-            };
+            return Mutex{ .lock = 0 };
         }
 
         pub fn deinit(self: *Mutex) void {}
@@ -60,7 +87,7 @@ pub const Mutex = switch(builtin.os) {
                 }
                 c = @atomicRmw(i32, &self.lock, AtomicRmwOp.Xchg, 2, AtomicOrder.Acquire);
             }
-            return Held { .mutex = self };
+            return Held{ .mutex = self };
         }
     },
     // TODO once https://github.com/ziglang/zig/issues/287 (copy elision) is solved, we can make a
@@ -78,21 +105,19 @@ pub const Mutex = switch(builtin.os) {
             mutex: *Mutex,
 
             pub fn release(self: Held) void {
-                SpinLock.Held.release(SpinLock.Held { .spinlock = &self.mutex.lock });
+                SpinLock.Held.release(SpinLock.Held{ .spinlock = &self.mutex.lock });
             }
         };
 
         pub fn init() Mutex {
-            return Mutex {
-                .lock = SpinLock.init(),
-            };
+            return Mutex{ .lock = SpinLock.init() };
         }
 
         pub fn deinit(self: *Mutex) void {}
 
         pub fn acquire(self: *Mutex) Held {
             _ = self.lock.acquire();
-            return Held { .mutex = self };
+            return Held{ .mutex = self };
         }
     },
 };
@@ -122,15 +147,20 @@ test "std.Mutex" {
         .data = 0,
     };
 
-    const thread_count = 10;
-    var threads: [thread_count]*std.os.Thread = undefined;
-    for (threads) |*t| {
-        t.* = try std.os.spawnThread(&context, worker);
-    }
-    for (threads) |t|
-        t.wait();
+    if (builtin.single_threaded) {
+        worker(&context);
+        std.debug.assertOrPanic(context.data == TestContext.incr_count);
+    } else {
+        const thread_count = 10;
+        var threads: [thread_count]*std.os.Thread = undefined;
+        for (threads) |*t| {
+            t.* = try std.os.spawnThread(&context, worker);
+        }
+        for (threads) |t|
+            t.wait();
 
-    std.debug.assertOrPanic(context.data == thread_count * TestContext.incr_count);
+        std.debug.assertOrPanic(context.data == thread_count * TestContext.incr_count);
+    }
 }
 
 fn worker(ctx: *TestContext) void {
std/statically_initialized_mutex.zig
@@ -93,13 +93,18 @@ test "std.StaticallyInitializedMutex" {
         .data = 0,
     };
 
-    const thread_count = 10;
-    var threads: [thread_count]*std.os.Thread = undefined;
-    for (threads) |*t| {
-        t.* = try std.os.spawnThread(&context, TestContext.worker);
-    }
-    for (threads) |t|
-        t.wait();
+    if (builtin.single_threaded) {
+        TestContext.worker(&context);
+        std.debug.assertOrPanic(context.data == TestContext.incr_count);
+    } else {
+        const thread_count = 10;
+        var threads: [thread_count]*std.os.Thread = undefined;
+        for (threads) |*t| {
+            t.* = try std.os.spawnThread(&context, TestContext.worker);
+        }
+        for (threads) |t|
+            t.wait();
 
-    std.debug.assertOrPanic(context.data == thread_count * TestContext.incr_count);
+        std.debug.assertOrPanic(context.data == thread_count * TestContext.incr_count);
+    }
 }
test/tests.zig
@@ -163,25 +163,32 @@ pub fn addPkgTests(b: *build.Builder, test_filter: ?[]const u8, root_src: []cons
     for (test_targets) |test_target| {
         const is_native = (test_target.os == builtin.os and test_target.arch == builtin.arch);
         for (modes) |mode| {
-            for ([]bool{
-                false,
-                true,
-            }) |link_libc| {
-                if (link_libc and !is_native) {
-                    // don't assume we have a cross-compiling libc set up
-                    continue;
-                }
-                const these_tests = b.addTest(root_src);
-                these_tests.setNamePrefix(b.fmt("{}-{}-{}-{}-{} ", name, @tagName(test_target.os), @tagName(test_target.arch), @tagName(mode), if (link_libc) "c" else "bare"));
-                these_tests.setFilter(test_filter);
-                these_tests.setBuildMode(mode);
-                if (!is_native) {
-                    these_tests.setTarget(test_target.arch, test_target.os, test_target.environ);
-                }
-                if (link_libc) {
-                    these_tests.linkSystemLibrary("c");
+            for ([]bool{ false, true }) |link_libc| {
+                for ([]bool{ false, true }) |single_threaded| {
+                    if (link_libc and !is_native) {
+                        // don't assume we have a cross-compiling libc set up
+                        continue;
+                    }
+                    const these_tests = b.addTest(root_src);
+                    these_tests.setNamePrefix(b.fmt(
+                        "{}-{}-{}-{}-{}-{} ",
+                        name,
+                        @tagName(test_target.os),
+                        @tagName(test_target.arch),
+                        @tagName(mode),
+                        if (link_libc) "c" else "bare",
+                        if (single_threaded) "single" else "multi",
+                    ));
+                    these_tests.setFilter(test_filter);
+                    these_tests.setBuildMode(mode);
+                    if (!is_native) {
+                        these_tests.setTarget(test_target.arch, test_target.os, test_target.environ);
+                    }
+                    if (link_libc) {
+                        these_tests.linkSystemLibrary("c");
+                    }
+                    step.dependOn(&these_tests.step);
                 }
-                step.dependOn(&these_tests.step);
             }
         }
     }