Commit 5065830aa0

Cody Tapscott <topolarity@tapscott.me>
2022-02-03 23:27:01
Avoid depending on child process execution when not supported by host OS
In accordance with the requesting issue (#10750): - `zig test` skips any tests that it cannot spawn, returning success - `zig run` and `zig build` exit with failure, reporting the command the cannot be run - `zig clang`, `zig ar`, etc. already punt directly to the appropriate clang/lld main(), even before this change - Native `libc` Detection is not supported Additionally, `exec()` and related Builder functions error at run-time, reporting the command that cannot be run
1 parent 069dd01
lib/std/build/RunStep.zig
@@ -10,6 +10,8 @@ const mem = std.mem;
 const process = std.process;
 const ArrayList = std.ArrayList;
 const BufMap = std.BufMap;
+const Allocator = mem.Allocator;
+const ExecError = build.Builder.ExecError;
 
 const max_stdout_size = 1 * 1024 * 1024; // 1 MiB
 
@@ -136,6 +138,17 @@ pub fn setEnvironmentVariable(self: *RunStep, key: []const u8, value: []const u8
     ) catch unreachable;
 }
 
+fn argvCmd(allocator: Allocator, argv: []const []const u8) ![]u8 {
+    var cmd = std.ArrayList(u8).init(allocator);
+    defer cmd.deinit();
+    for (argv[0 .. argv.len - 1]) |arg| {
+        try cmd.appendSlice(arg);
+        try cmd.append(' ');
+    }
+    try cmd.appendSlice(argv[argv.len - 1]);
+    return cmd.toOwnedSlice();
+}
+
 pub fn expectStdErrEqual(self: *RunStep, bytes: []const u8) void {
     self.stderr_action = .{ .expect_exact = self.builder.dupe(bytes) };
 }
@@ -175,6 +188,13 @@ fn make(step: *Step) !void {
 
     const argv = argv_list.items;
 
+    if (!std.process.can_spawn) {
+        const cmd = try argvCmd(self.builder.allocator, argv);
+        std.debug.print("the following command cannot be executed ({s} does not support spawning a child process):\n{s}", .{ @tagName(builtin.os.tag), cmd });
+        self.builder.allocator.free(cmd);
+        return ExecError.ExecNotSupported;
+    }
+
     const child = std.ChildProcess.init(argv, self.builder.allocator) catch unreachable;
     defer child.deinit();
 
lib/std/build.zig
@@ -88,7 +88,14 @@ pub const Builder = struct {
     /// Information about the native target. Computed before build() is invoked.
     host: NativeTargetInfo,
 
-    const PkgConfigError = error{
+    pub const ExecError = error{
+        ReadFailure,
+        ExitCodeFailure,
+        ProcessTerminated,
+        ExecNotSupported,
+    } || std.ChildProcess.SpawnError;
+
+    pub const PkgConfigError = error{
         PkgConfigCrashed,
         PkgConfigFailed,
         PkgConfigNotInstalled,
@@ -959,6 +966,9 @@ pub const Builder = struct {
             printCmd(cwd, argv);
         }
 
+        if (!std.process.can_spawn)
+            return error.ExecNotSupported;
+
         const child = std.ChildProcess.init(argv, self.allocator) catch unreachable;
         defer child.deinit();
 
@@ -1168,9 +1178,12 @@ pub const Builder = struct {
         argv: []const []const u8,
         out_code: *u8,
         stderr_behavior: std.ChildProcess.StdIo,
-    ) ![]u8 {
+    ) ExecError![]u8 {
         assert(argv.len != 0);
 
+        if (!std.process.can_spawn)
+            return error.ExecNotSupported;
+
         const max_output_size = 400 * 1024;
         const child = try std.ChildProcess.init(argv, self.allocator);
         defer child.deinit();
@@ -1182,7 +1195,9 @@ pub const Builder = struct {
 
         try child.spawn();
 
-        const stdout = try child.stdout.?.reader().readAllAlloc(self.allocator, max_output_size);
+        const stdout = child.stdout.?.reader().readAllAlloc(self.allocator, max_output_size) catch {
+            return error.ReadFailure;
+        };
         errdefer self.allocator.free(stdout);
 
         const term = try child.wait();
@@ -1208,8 +1223,21 @@ pub const Builder = struct {
             printCmd(null, argv);
         }
 
+        if (!std.process.can_spawn) {
+            if (src_step) |s| warn("{s}...", .{s.name});
+            warn("Unable to spawn the following command: cannot spawn child process\n", .{});
+            printCmd(null, argv);
+            std.os.abort();
+        }
+
         var code: u8 = undefined;
         return self.execAllowFail(argv, &code, .Inherit) catch |err| switch (err) {
+            error.ExecNotSupported => {
+                if (src_step) |s| warn("{s}...", .{s.name});
+                warn("Unable to spawn the following command: cannot spawn child process\n", .{});
+                printCmd(null, argv);
+                std.os.abort();
+            },
             error.FileNotFound => {
                 if (src_step) |s| warn("{s}...", .{s.name});
                 warn("Unable to spawn the following command: file not found\n", .{});
@@ -1260,7 +1288,7 @@ pub const Builder = struct {
         ) catch unreachable;
     }
 
-    fn execPkgConfigList(self: *Builder, out_code: *u8) ![]const PkgConfigPkg {
+    fn execPkgConfigList(self: *Builder, out_code: *u8) (PkgConfigError || ExecError)![]const PkgConfigPkg {
         const stdout = try self.execAllowFail(&[_][]const u8{ "pkg-config", "--list-all" }, out_code, .Ignore);
         var list = ArrayList(PkgConfigPkg).init(self.allocator);
         errdefer list.deinit();
@@ -1287,6 +1315,7 @@ pub const Builder = struct {
         } else |err| {
             const result = switch (err) {
                 error.ProcessTerminated => error.PkgConfigCrashed,
+                error.ExecNotSupported => error.PkgConfigFailed,
                 error.ExitCodeFailure => error.PkgConfigFailed,
                 error.FileNotFound => error.PkgConfigNotInstalled,
                 error.InvalidName => error.PkgConfigNotInstalled,
@@ -1929,6 +1958,7 @@ pub const LibExeObjStep = struct {
             "--libs",
         }, &code, .Ignore)) |stdout| stdout else |err| switch (err) {
             error.ProcessTerminated => return error.PkgConfigCrashed,
+            error.ExecNotSupported => return error.PkgConfigFailed,
             error.ExitCodeFailure => return error.PkgConfigFailed,
             error.FileNotFound => return error.PkgConfigNotInstalled,
             else => return err,
lib/std/child_process.zig
@@ -124,6 +124,10 @@ pub const ChildProcess = struct {
 
     /// On success must call `kill` or `wait`.
     pub fn spawn(self: *ChildProcess) SpawnError!void {
+        if (!std.process.can_spawn) {
+            @compileError("the target operating system cannot spawn processes");
+        }
+
         if (builtin.os.tag == .windows) {
             return self.spawnWindows();
         } else {
lib/std/process.zig
@@ -950,7 +950,13 @@ pub fn getSelfExeSharedLibPaths(allocator: Allocator) error{OutOfMemory}![][:0]u
 
 /// Tells whether calling the `execv` or `execve` functions will be a compile error.
 pub const can_execv = switch (builtin.os.tag) {
-    .windows, .haiku => false,
+    .windows, .haiku, .wasi => false,
+    else => true,
+};
+
+/// Tells whether spawning child processes is supported (e.g. via ChildProcess)
+pub const can_spawn = switch (builtin.os.tag) {
+    .wasi => false,
     else => true,
 };
 
src/link/Coff.zig
@@ -9,6 +9,7 @@ const fs = std.fs;
 const allocPrint = std.fmt.allocPrint;
 const mem = std.mem;
 
+const lldMain = @import("../main.zig").lldMain;
 const trace = @import("../tracy.zig").trace;
 const Module = @import("../Module.zig");
 const Compilation = @import("../Compilation.zig");
@@ -1358,60 +1359,71 @@ fn linkWithLLD(self: *Coff, comp: *Compilation) !void {
             Compilation.dump_argv(argv.items[1..]);
         }
 
-        // Sadly, we must run LLD as a child process because it does not behave
-        // properly as a library.
-        const child = try std.ChildProcess.init(argv.items, arena);
-        defer child.deinit();
-
-        if (comp.clang_passthrough_mode) {
-            child.stdin_behavior = .Inherit;
-            child.stdout_behavior = .Inherit;
-            child.stderr_behavior = .Inherit;
-
-            const term = child.spawnAndWait() catch |err| {
-                log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
-                return error.UnableToSpawnSelf;
-            };
-            switch (term) {
-                .Exited => |code| {
-                    if (code != 0) {
-                        // TODO https://github.com/ziglang/zig/issues/6342
-                        std.process.exit(1);
-                    }
-                },
-                else => std.process.abort(),
+        if (std.process.can_spawn) {
+            // If possible, we run LLD as a child process because it does not always
+            // behave properly as a library, unfortunately.
+            // https://github.com/ziglang/zig/issues/3825
+            const child = try std.ChildProcess.init(argv.items, arena);
+            defer child.deinit();
+
+            if (comp.clang_passthrough_mode) {
+                child.stdin_behavior = .Inherit;
+                child.stdout_behavior = .Inherit;
+                child.stderr_behavior = .Inherit;
+
+                const term = child.spawnAndWait() catch |err| {
+                    log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
+                    return error.UnableToSpawnSelf;
+                };
+                switch (term) {
+                    .Exited => |code| {
+                        if (code != 0) {
+                            std.process.exit(code);
+                        }
+                    },
+                    else => std.process.abort(),
+                }
+            } else {
+                child.stdin_behavior = .Ignore;
+                child.stdout_behavior = .Ignore;
+                child.stderr_behavior = .Pipe;
+
+                try child.spawn();
+
+                const stderr = try child.stderr.?.reader().readAllAlloc(arena, 10 * 1024 * 1024);
+
+                const term = child.wait() catch |err| {
+                    log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
+                    return error.UnableToSpawnSelf;
+                };
+
+                switch (term) {
+                    .Exited => |code| {
+                        if (code != 0) {
+                            // TODO parse this output and surface with the Compilation API rather than
+                            // directly outputting to stderr here.
+                            std.debug.print("{s}", .{stderr});
+                            return error.LLDReportedFailure;
+                        }
+                    },
+                    else => {
+                        log.err("{s} terminated with stderr:\n{s}", .{ argv.items[0], stderr });
+                        return error.LLDCrashed;
+                    },
+                }
+
+                if (stderr.len != 0) {
+                    log.warn("unexpected LLD stderr:\n{s}", .{stderr});
+                }
             }
         } else {
-            child.stdin_behavior = .Ignore;
-            child.stdout_behavior = .Ignore;
-            child.stderr_behavior = .Pipe;
-
-            try child.spawn();
-
-            const stderr = try child.stderr.?.reader().readAllAlloc(arena, 10 * 1024 * 1024);
-
-            const term = child.wait() catch |err| {
-                log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
-                return error.UnableToSpawnSelf;
-            };
-
-            switch (term) {
-                .Exited => |code| {
-                    if (code != 0) {
-                        // TODO parse this output and surface with the Compilation API rather than
-                        // directly outputting to stderr here.
-                        std.debug.print("{s}", .{stderr});
-                        return error.LLDReportedFailure;
-                    }
-                },
-                else => {
-                    log.err("{s} terminated with stderr:\n{s}", .{ argv.items[0], stderr });
-                    return error.LLDCrashed;
-                },
-            }
-
-            if (stderr.len != 0) {
-                log.warn("unexpected LLD stderr:\n{s}", .{stderr});
+            const exit_code = try lldMain(arena, argv.items);
+            if (exit_code != 0) {
+                if (comp.clang_passthrough_mode) {
+                    std.process.exit(exit_code);
+                } else {
+                    return error.LLDReportedFailure;
+                }
             }
         }
     }
src/link/Elf.zig
@@ -14,6 +14,7 @@ const leb128 = std.leb;
 const Module = @import("../Module.zig");
 const Compilation = @import("../Compilation.zig");
 const codegen = @import("../codegen.zig");
+const lldMain = @import("../main.zig").lldMain;
 const trace = @import("../tracy.zig").trace;
 const Package = @import("../Package.zig");
 const Value = @import("../value.zig").Value;
@@ -1950,60 +1951,71 @@ fn linkWithLLD(self: *Elf, comp: *Compilation) !void {
             Compilation.dump_argv(argv.items[1..]);
         }
 
-        // Sadly, we must run LLD as a child process because it does not behave
-        // properly as a library.
-        const child = try std.ChildProcess.init(argv.items, arena);
-        defer child.deinit();
+        if (std.process.can_spawn) {
+            // If possible, we run LLD as a child process because it does not always
+            // behave properly as a library, unfortunately.
+            // https://github.com/ziglang/zig/issues/3825
+            const child = try std.ChildProcess.init(argv.items, arena);
+            defer child.deinit();
 
-        if (comp.clang_passthrough_mode) {
-            child.stdin_behavior = .Inherit;
-            child.stdout_behavior = .Inherit;
-            child.stderr_behavior = .Inherit;
+            if (comp.clang_passthrough_mode) {
+                child.stdin_behavior = .Inherit;
+                child.stdout_behavior = .Inherit;
+                child.stderr_behavior = .Inherit;
 
-            const term = child.spawnAndWait() catch |err| {
-                log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
-                return error.UnableToSpawnSelf;
-            };
-            switch (term) {
-                .Exited => |code| {
-                    if (code != 0) {
-                        // TODO https://github.com/ziglang/zig/issues/6342
-                        std.process.exit(1);
-                    }
-                },
-                else => std.process.abort(),
-            }
-        } else {
-            child.stdin_behavior = .Ignore;
-            child.stdout_behavior = .Ignore;
-            child.stderr_behavior = .Pipe;
+                const term = child.spawnAndWait() catch |err| {
+                    log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
+                    return error.UnableToSpawnSelf;
+                };
+                switch (term) {
+                    .Exited => |code| {
+                        if (code != 0) {
+                            std.process.exit(code);
+                        }
+                    },
+                    else => std.process.abort(),
+                }
+            } else {
+                child.stdin_behavior = .Ignore;
+                child.stdout_behavior = .Ignore;
+                child.stderr_behavior = .Pipe;
 
-            try child.spawn();
+                try child.spawn();
 
-            const stderr = try child.stderr.?.reader().readAllAlloc(arena, 10 * 1024 * 1024);
+                const stderr = try child.stderr.?.reader().readAllAlloc(arena, 10 * 1024 * 1024);
 
-            const term = child.wait() catch |err| {
-                log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
-                return error.UnableToSpawnSelf;
-            };
+                const term = child.wait() catch |err| {
+                    log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
+                    return error.UnableToSpawnSelf;
+                };
 
-            switch (term) {
-                .Exited => |code| {
-                    if (code != 0) {
-                        // TODO parse this output and surface with the Compilation API rather than
-                        // directly outputting to stderr here.
-                        std.debug.print("{s}", .{stderr});
-                        return error.LLDReportedFailure;
-                    }
-                },
-                else => {
-                    log.err("{s} terminated with stderr:\n{s}", .{ argv.items[0], stderr });
-                    return error.LLDCrashed;
-                },
-            }
+                switch (term) {
+                    .Exited => |code| {
+                        if (code != 0) {
+                            // TODO parse this output and surface with the Compilation API rather than
+                            // directly outputting to stderr here.
+                            std.debug.print("{s}", .{stderr});
+                            return error.LLDReportedFailure;
+                        }
+                    },
+                    else => {
+                        log.err("{s} terminated with stderr:\n{s}", .{ argv.items[0], stderr });
+                        return error.LLDCrashed;
+                    },
+                }
 
-            if (stderr.len != 0) {
-                log.warn("unexpected LLD stderr:\n{s}", .{stderr});
+                if (stderr.len != 0) {
+                    log.warn("unexpected LLD stderr:\n{s}", .{stderr});
+                }
+            }
+        } else {
+            const exit_code = try lldMain(arena, argv.items);
+            if (exit_code != 0) {
+                if (comp.clang_passthrough_mode) {
+                    std.process.exit(exit_code);
+                } else {
+                    return error.LLDReportedFailure;
+                }
             }
         }
     }
src/link/Wasm.zig
@@ -15,6 +15,7 @@ const Module = @import("../Module.zig");
 const Compilation = @import("../Compilation.zig");
 const CodeGen = @import("../arch/wasm/CodeGen.zig");
 const link = @import("../link.zig");
+const lldMain = @import("../main.zig").lldMain;
 const trace = @import("../tracy.zig").trace;
 const build_options = @import("build_options");
 const wasi_libc = @import("../wasi_libc.zig");
@@ -1486,60 +1487,71 @@ fn linkWithLLD(self: *Wasm, comp: *Compilation) !void {
             Compilation.dump_argv(argv.items[1..]);
         }
 
-        // Sadly, we must run LLD as a child process because it does not behave
-        // properly as a library.
-        const child = try std.ChildProcess.init(argv.items, arena);
-        defer child.deinit();
-
-        if (comp.clang_passthrough_mode) {
-            child.stdin_behavior = .Inherit;
-            child.stdout_behavior = .Inherit;
-            child.stderr_behavior = .Inherit;
+        if (std.process.can_spawn) {
+            // If possible, we run LLD as a child process because it does not always
+            // behave properly as a library, unfortunately.
+            // https://github.com/ziglang/zig/issues/3825
+            const child = try std.ChildProcess.init(argv.items, arena);
+            defer child.deinit();
+
+            if (comp.clang_passthrough_mode) {
+                child.stdin_behavior = .Inherit;
+                child.stdout_behavior = .Inherit;
+                child.stderr_behavior = .Inherit;
+
+                const term = child.spawnAndWait() catch |err| {
+                    log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
+                    return error.UnableToSpawnSelf;
+                };
+                switch (term) {
+                    .Exited => |code| {
+                        if (code != 0) {
+                            std.process.exit(code);
+                        }
+                    },
+                    else => std.process.abort(),
+                }
+            } else {
+                child.stdin_behavior = .Ignore;
+                child.stdout_behavior = .Ignore;
+                child.stderr_behavior = .Pipe;
 
-            const term = child.spawnAndWait() catch |err| {
-                log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
-                return error.UnableToSpawnSelf;
-            };
-            switch (term) {
-                .Exited => |code| {
-                    if (code != 0) {
-                        // TODO https://github.com/ziglang/zig/issues/6342
-                        std.process.exit(1);
-                    }
-                },
-                else => std.process.abort(),
-            }
-        } else {
-            child.stdin_behavior = .Ignore;
-            child.stdout_behavior = .Ignore;
-            child.stderr_behavior = .Pipe;
+                try child.spawn();
 
-            try child.spawn();
+                const stderr = try child.stderr.?.reader().readAllAlloc(arena, 10 * 1024 * 1024);
 
-            const stderr = try child.stderr.?.reader().readAllAlloc(arena, 10 * 1024 * 1024);
+                const term = child.wait() catch |err| {
+                    log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
+                    return error.UnableToSpawnSelf;
+                };
 
-            const term = child.wait() catch |err| {
-                log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
-                return error.UnableToSpawnSelf;
-            };
+                switch (term) {
+                    .Exited => |code| {
+                        if (code != 0) {
+                            // TODO parse this output and surface with the Compilation API rather than
+                            // directly outputting to stderr here.
+                            std.debug.print("{s}", .{stderr});
+                            return error.LLDReportedFailure;
+                        }
+                    },
+                    else => {
+                        log.err("{s} terminated with stderr:\n{s}", .{ argv.items[0], stderr });
+                        return error.LLDCrashed;
+                    },
+                }
 
-            switch (term) {
-                .Exited => |code| {
-                    if (code != 0) {
-                        // TODO parse this output and surface with the Compilation API rather than
-                        // directly outputting to stderr here.
-                        std.debug.print("{s}", .{stderr});
-                        return error.LLDReportedFailure;
-                    }
-                },
-                else => {
-                    log.err("{s} terminated with stderr:\n{s}", .{ argv.items[0], stderr });
-                    return error.LLDCrashed;
-                },
+                if (stderr.len != 0) {
+                    log.warn("unexpected LLD stderr:\n{s}", .{stderr});
+                }
             }
-
-            if (stderr.len != 0) {
-                log.warn("unexpected LLD stderr:\n{s}", .{stderr});
+        } else {
+            const exit_code = try lldMain(arena, argv.items);
+            if (exit_code != 0) {
+                if (comp.clang_passthrough_mode) {
+                    std.process.exit(exit_code);
+                } else {
+                    return error.LLDReportedFailure;
+                }
             }
         }
     }
src/Compilation.zig
@@ -25,6 +25,7 @@ const libunwind = @import("libunwind.zig");
 const libcxx = @import("libcxx.zig");
 const wasi_libc = @import("wasi_libc.zig");
 const fatal = @import("main.zig").fatal;
+const clangMain = @import("main.zig").clangMain;
 const Module = @import("Module.zig");
 const Cache = @import("Cache.zig");
 const stage1 = @import("stage1.zig");
@@ -3667,55 +3668,71 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P
             dump_argv(argv.items);
         }
 
-        const child = try std.ChildProcess.init(argv.items, arena);
-        defer child.deinit();
+        if (std.process.can_spawn) {
+            const child = try std.ChildProcess.init(argv.items, arena);
+            defer child.deinit();
 
-        if (comp.clang_passthrough_mode) {
-            child.stdin_behavior = .Inherit;
-            child.stdout_behavior = .Inherit;
-            child.stderr_behavior = .Inherit;
+            if (comp.clang_passthrough_mode) {
+                child.stdin_behavior = .Inherit;
+                child.stdout_behavior = .Inherit;
+                child.stderr_behavior = .Inherit;
 
-            const term = child.spawnAndWait() catch |err| {
-                return comp.failCObj(c_object, "unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
-            };
-            switch (term) {
-                .Exited => |code| {
-                    if (code != 0) {
-                        std.process.exit(code);
-                    }
-                    if (comp.clang_preprocessor_mode == .stdout)
-                        std.process.exit(0);
-                },
-                else => std.process.abort(),
-            }
-        } else {
-            child.stdin_behavior = .Ignore;
-            child.stdout_behavior = .Ignore;
-            child.stderr_behavior = .Pipe;
+                const term = child.spawnAndWait() catch |err| {
+                    return comp.failCObj(c_object, "unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
+                };
+                switch (term) {
+                    .Exited => |code| {
+                        if (code != 0) {
+                            std.process.exit(code);
+                        }
+                        if (comp.clang_preprocessor_mode == .stdout)
+                            std.process.exit(0);
+                    },
+                    else => std.process.abort(),
+                }
+            } else {
+                child.stdin_behavior = .Ignore;
+                child.stdout_behavior = .Ignore;
+                child.stderr_behavior = .Pipe;
 
-            try child.spawn();
+                try child.spawn();
 
-            const stderr_reader = child.stderr.?.reader();
+                const stderr_reader = child.stderr.?.reader();
 
-            const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024);
+                const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024);
 
-            const term = child.wait() catch |err| {
-                return comp.failCObj(c_object, "unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
-            };
+                const term = child.wait() catch |err| {
+                    return comp.failCObj(c_object, "unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
+                };
 
-            switch (term) {
-                .Exited => |code| {
-                    if (code != 0) {
-                        // TODO parse clang stderr and turn it into an error message
-                        // and then call failCObjWithOwnedErrorMsg
-                        log.err("clang failed with stderr: {s}", .{stderr});
-                        return comp.failCObj(c_object, "clang exited with code {d}", .{code});
-                    }
-                },
-                else => {
-                    log.err("clang terminated with stderr: {s}", .{stderr});
-                    return comp.failCObj(c_object, "clang terminated unexpectedly", .{});
-                },
+                switch (term) {
+                    .Exited => |code| {
+                        if (code != 0) {
+                            // TODO parse clang stderr and turn it into an error message
+                            // and then call failCObjWithOwnedErrorMsg
+                            log.err("clang failed with stderr: {s}", .{stderr});
+                            return comp.failCObj(c_object, "clang exited with code {d}", .{code});
+                        }
+                    },
+                    else => {
+                        log.err("clang terminated with stderr: {s}", .{stderr});
+                        return comp.failCObj(c_object, "clang terminated unexpectedly", .{});
+                    },
+                }
+            }
+        } else {
+            const exit_code = try clangMain(arena, argv.items);
+            if (exit_code != 0) {
+                if (comp.clang_passthrough_mode) {
+                    std.process.exit(exit_code);
+                } else {
+                    return comp.failCObj(c_object, "clang exited with code {d}", .{exit_code});
+                }
+            }
+            if (comp.clang_passthrough_mode and
+                comp.clang_preprocessor_mode == .stdout)
+            {
+                std.process.exit(0);
             }
         }
 
src/libc_installation.zig
@@ -216,7 +216,7 @@ pub const LibCInstallation = struct {
                 self.crt_dir = try args.allocator.dupeZ(u8, "/system/develop/lib");
                 break :blk batch.wait();
             };
-        } else {
+        } else if (std.process.can_spawn) {
             try blk: {
                 var batch = Batch(FindError!void, 2, .auto_async).init();
                 errdefer batch.wait() catch {};
@@ -229,6 +229,8 @@ pub const LibCInstallation = struct {
                 }
                 break :blk batch.wait();
             };
+        } else {
+            return error.LibCRuntimeNotFound;
         }
         return self;
     }
src/main.zig
@@ -221,7 +221,7 @@ pub fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
         mem.eql(u8, cmd, "lib") or
         mem.eql(u8, cmd, "ar"))
     {
-        return punt_to_llvm_ar(arena, args);
+        return process.exit(try llvmArMain(arena, args));
     } else if (mem.eql(u8, cmd, "cc")) {
         return buildOutputType(gpa, arena, args, .cc);
     } else if (mem.eql(u8, cmd, "c++")) {
@@ -231,12 +231,12 @@ pub fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
     } else if (mem.eql(u8, cmd, "clang") or
         mem.eql(u8, cmd, "-cc1") or mem.eql(u8, cmd, "-cc1as"))
     {
-        return punt_to_clang(arena, args);
+        return process.exit(try clangMain(arena, args));
     } else if (mem.eql(u8, cmd, "ld.lld") or
         mem.eql(u8, cmd, "lld-link") or
         mem.eql(u8, cmd, "wasm-ld"))
     {
-        return punt_to_lld(arena, args);
+        return process.exit(try lldMain(arena, args));
     } else if (mem.eql(u8, cmd, "build")) {
         return cmdBuild(gpa, arena, cmd_args);
     } else if (mem.eql(u8, cmd, "fmt")) {
@@ -1347,7 +1347,7 @@ fn buildOutputType(
                     .ignore => {},
                     .driver_punt => {
                         // Never mind what we're doing, just pass the args directly. For example --help.
-                        return punt_to_clang(arena, all_args);
+                        return process.exit(try clangMain(arena, all_args));
                     },
                     .pic => want_pic = true,
                     .no_pic => want_pic = false,
@@ -1866,7 +1866,7 @@ fn buildOutputType(
                     // An error message is generated when there is more than 1 C source file.
                     if (c_source_files.items.len != 1) {
                         // For example `zig cc` and no args should print the "no input files" message.
-                        return punt_to_clang(arena, all_args);
+                        return process.exit(try clangMain(arena, all_args));
                     }
                     if (out_path) |p| {
                         emit_bin = .{ .yes = p };
@@ -1882,7 +1882,7 @@ fn buildOutputType(
             {
                 // For example `zig cc` and no args should print the "no input files" message.
                 // There could be other reasons to punt to clang, for example, --help.
-                return punt_to_clang(arena, all_args);
+                return process.exit(try clangMain(arena, all_args));
             }
         },
     }
@@ -2883,7 +2883,7 @@ fn runOrTest(
         try warnAboutForeignBinaries(gpa, arena, arg_mode, target_info, link_libc);
         const cmd = try argvCmd(arena, argv.items);
         fatal("the following command failed to execve with '{s}':\n{s}", .{ @errorName(err), cmd });
-    } else {
+    } else if (std.process.can_spawn) {
         const child = try std.ChildProcess.init(argv.items, gpa);
         defer child.deinit();
 
@@ -2943,6 +2943,9 @@ fn runOrTest(
             },
             else => unreachable,
         }
+    } else {
+        const cmd = try argvCmd(arena, argv.items);
+        fatal("the following command cannot be executed ({s} does not support spawning a child process):\n{s}", .{ @tagName(builtin.os.tag), cmd });
     }
 }
 
@@ -3553,29 +3556,35 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
 
         break :argv child_argv.items;
     };
-    const child = try std.ChildProcess.init(child_argv, gpa);
-    defer child.deinit();
 
-    child.stdin_behavior = .Inherit;
-    child.stdout_behavior = .Inherit;
-    child.stderr_behavior = .Inherit;
+    if (std.process.can_spawn) {
+        const child = try std.ChildProcess.init(child_argv, gpa);
+        defer child.deinit();
+
+        child.stdin_behavior = .Inherit;
+        child.stdout_behavior = .Inherit;
+        child.stderr_behavior = .Inherit;
 
-    const term = try child.spawnAndWait();
-    switch (term) {
-        .Exited => |code| {
-            if (code == 0) return cleanExit();
+        const term = try child.spawnAndWait();
+        switch (term) {
+            .Exited => |code| {
+                if (code == 0) return cleanExit();
 
-            if (prominent_compile_errors) {
-                fatal("the build command failed with exit code {d}", .{code});
-            } else {
+                if (prominent_compile_errors) {
+                    fatal("the build command failed with exit code {d}", .{code});
+                } else {
+                    const cmd = try argvCmd(arena, child_argv);
+                    fatal("the following build command failed with exit code {d}:\n{s}", .{ code, cmd });
+                }
+            },
+            else => {
                 const cmd = try argvCmd(arena, child_argv);
-                fatal("the following build command failed with exit code {d}:\n{s}", .{ code, cmd });
-            }
-        },
-        else => {
-            const cmd = try argvCmd(arena, child_argv);
-            fatal("the following build command crashed:\n{s}", .{cmd});
-        },
+                fatal("the following build command crashed:\n{s}", .{cmd});
+            },
+        }
+    } else {
+        const cmd = try argvCmd(arena, child_argv);
+        fatal("the following command cannot be executed ({s} does not support spawning a child process):\n{s}", .{ @tagName(builtin.os.tag), cmd });
     }
 }
 
@@ -4080,51 +4089,69 @@ pub const info_zen =
 extern "c" fn ZigClang_main(argc: c_int, argv: [*:null]?[*:0]u8) c_int;
 extern "c" fn ZigLlvmAr_main(argc: c_int, argv: [*:null]?[*:0]u8) c_int;
 
-/// TODO https://github.com/ziglang/zig/issues/3257
-fn punt_to_clang(arena: Allocator, args: []const []const u8) error{OutOfMemory} {
-    if (!build_options.have_llvm)
-        fatal("`zig cc` and `zig c++` unavailable: compiler built without LLVM extensions", .{});
-    // Convert the args to the format Clang expects.
-    const argv = try arena.alloc(?[*:0]u8, args.len + 1);
+fn argsCopyZ(alloc: Allocator, args: []const []const u8) ![:null]?[*:0]u8 {
+    var argv = try alloc.allocSentinel(?[*:0]u8, args.len, null);
     for (args) |arg, i| {
-        argv[i] = try arena.dupeZ(u8, arg); // TODO If there was an argsAllocZ we could avoid this allocation.
+        argv[i] = try alloc.dupeZ(u8, arg); // TODO If there was an argsAllocZ we could avoid this allocation.
     }
-    argv[args.len] = null;
-    const exit_code = ZigClang_main(@intCast(c_int, args.len), argv[0..args.len :null].ptr);
-    process.exit(@bitCast(u8, @truncate(i8, exit_code)));
+    return argv;
+}
+
+pub fn clangMain(alloc: Allocator, args: []const []const u8) error{OutOfMemory}!u8 {
+    if (!build_options.have_llvm)
+        fatal("`zig cc` and `zig c++` unavailable: compiler built without LLVM extensions", .{});
+
+    var arena_instance = std.heap.ArenaAllocator.init(alloc);
+    defer arena_instance.deinit();
+    const arena = arena_instance.allocator();
+
+    // Convert the args to the null-terminated format Clang expects.
+    const argv = try argsCopyZ(arena, args);
+    const exit_code = ZigClang_main(@intCast(c_int, argv.len), argv.ptr);
+    return @bitCast(u8, @truncate(i8, exit_code));
 }
 
-/// TODO https://github.com/ziglang/zig/issues/3257
-fn punt_to_llvm_ar(arena: Allocator, args: []const []const u8) error{OutOfMemory} {
+pub fn llvmArMain(alloc: Allocator, args: []const []const u8) error{OutOfMemory}!u8 {
     if (!build_options.have_llvm)
         fatal("`zig ar`, `zig dlltool`, `zig ranlib', and `zig lib` unavailable: compiler built without LLVM extensions", .{});
 
+    var arena_instance = std.heap.ArenaAllocator.init(alloc);
+    defer arena_instance.deinit();
+    const arena = arena_instance.allocator();
+
     // Convert the args to the format llvm-ar expects.
-    // We subtract 1 to shave off the zig binary from args[0].
-    const argv = try arena.allocSentinel(?[*:0]u8, args.len - 1, null);
-    for (args[1..]) |arg, i| {
-        // TODO If there was an argsAllocZ we could avoid this allocation.
-        argv[i] = try arena.dupeZ(u8, arg);
-    }
-    const argc = @intCast(c_int, argv.len);
-    const exit_code = ZigLlvmAr_main(argc, argv.ptr);
-    process.exit(@bitCast(u8, @truncate(i8, exit_code)));
+    // We intentionally shave off the zig binary at args[0].
+    const argv = try argsCopyZ(arena, args[1..]);
+    const exit_code = ZigLlvmAr_main(@intCast(c_int, argv.len), argv.ptr);
+    return @bitCast(u8, @truncate(i8, exit_code));
 }
 
 /// The first argument determines which backend is invoked. The options are:
 /// * `ld.lld` - ELF
 /// * `lld-link` - COFF
 /// * `wasm-ld` - WebAssembly
-/// TODO https://github.com/ziglang/zig/issues/3257
-pub fn punt_to_lld(arena: Allocator, args: []const []const u8) error{OutOfMemory} {
+pub fn lldMain(alloc: Allocator, args: []const []const u8) error{OutOfMemory}!u8 {
     if (!build_options.have_llvm)
         fatal("`zig {s}` unavailable: compiler built without LLVM extensions", .{args[0]});
-    // Convert the args to the format LLD expects.
-    // We subtract 1 to shave off the zig binary from args[0].
-    const argv = try arena.allocSentinel(?[*:0]const u8, args.len - 1, null);
-    for (args[1..]) |arg, i| {
-        argv[i] = try arena.dupeZ(u8, arg); // TODO If there was an argsAllocZ we could avoid this allocation.
+
+    // Print a warning if lld is called multiple times in the same process,
+    // since it may misbehave
+    // https://github.com/ziglang/zig/issues/3825
+    const CallCounter = struct {
+        var count: usize = 0;
+    };
+    if (CallCounter.count == 1) { // Issue the warning on the first repeat call
+        warn("calling lldMain repeatedly within the same process can have side effects (https://github.com/ziglang/zig/issues/3825)", .{});
     }
+    CallCounter.count += 1;
+
+    var arena_instance = std.heap.ArenaAllocator.init(alloc);
+    defer arena_instance.deinit();
+    const arena = arena_instance.allocator();
+
+    // Convert the args to the format llvm-ar expects.
+    // We intentionally shave off the zig binary at args[0].
+    const argv = try argsCopyZ(arena, args[1..]);
     const exit_code = rc: {
         const llvm = @import("codegen/llvm/bindings.zig");
         const argc = @intCast(c_int, argv.len);
@@ -4138,7 +4165,7 @@ pub fn punt_to_lld(arena: Allocator, args: []const []const u8) error{OutOfMemory
             unreachable;
         }
     };
-    process.exit(@bitCast(u8, @truncate(i8, exit_code)));
+    return @bitCast(u8, @truncate(i8, exit_code));
 }
 
 const clang_args = @import("clang_options.zig").list;
src/mingw.zig
@@ -5,6 +5,7 @@ const path = std.fs.path;
 const assert = std.debug.assert;
 const log = std.log.scoped(.mingw);
 
+const builtin = @import("builtin");
 const target_util = @import("target.zig");
 const Compilation = @import("Compilation.zig");
 const build_options = @import("build_options");
@@ -367,39 +368,43 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void {
         Compilation.dump_argv(&args);
     }
 
-    const child = try std.ChildProcess.init(&args, arena);
-    defer child.deinit();
+    if (std.process.can_spawn) {
+        const child = try std.ChildProcess.init(&args, arena);
+        defer child.deinit();
 
-    child.stdin_behavior = .Ignore;
-    child.stdout_behavior = .Pipe;
-    child.stderr_behavior = .Pipe;
+        child.stdin_behavior = .Ignore;
+        child.stdout_behavior = .Pipe;
+        child.stderr_behavior = .Pipe;
 
-    try child.spawn();
+        try child.spawn();
 
-    const stderr_reader = child.stderr.?.reader();
+        const stderr_reader = child.stderr.?.reader();
 
-    // TODO https://github.com/ziglang/zig/issues/6343
-    const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024);
+        // TODO https://github.com/ziglang/zig/issues/6343
+        const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024);
 
-    const term = child.wait() catch |err| {
-        // TODO surface a proper error here
-        log.err("unable to spawn {s}: {s}", .{ args[0], @errorName(err) });
-        return error.ClangPreprocessorFailed;
-    };
-
-    switch (term) {
-        .Exited => |code| {
-            if (code != 0) {
-                // TODO surface a proper error here
-                log.err("clang exited with code {d} and stderr: {s}", .{ code, stderr });
-                return error.ClangPreprocessorFailed;
-            }
-        },
-        else => {
+        const term = child.wait() catch |err| {
             // TODO surface a proper error here
-            log.err("clang terminated unexpectedly with stderr: {s}", .{stderr});
+            log.err("unable to spawn {s}: {s}", .{ args[0], @errorName(err) });
             return error.ClangPreprocessorFailed;
-        },
+        };
+        switch (term) {
+            .Exited => |code| {
+                if (code != 0) {
+                    // TODO surface a proper error here
+                    log.err("clang exited with code {d} and stderr: {s}", .{ code, stderr });
+                    return error.ClangPreprocessorFailed;
+                }
+            },
+            else => {
+                // TODO surface a proper error here
+                log.err("clang terminated unexpectedly with stderr: {s}", .{stderr});
+                return error.ClangPreprocessorFailed;
+            },
+        }
+    } else {
+        log.err("unable to spawn {s}: spawning child process not supported on {s}", .{ args[0], @tagName(builtin.os.tag) });
+        return error.ClangPreprocessorFailed;
     }
 
     const lib_final_path = try comp.global_cache_directory.join(comp.gpa, &[_][]const u8{
src/test.zig
@@ -730,6 +730,12 @@ pub const TestContext = struct {
             // * cannot handle updates
             // because of this we must spawn a child process rather than
             // using Compilation directly.
+
+            if (!std.process.can_spawn) {
+                print("Unable to spawn child processes on {s}, skipping test.\n", .{@tagName(builtin.os.tag)});
+                return; // Pass test.
+            }
+
             assert(case.updates.items.len == 1);
             const update = case.updates.items[0];
             try tmp.dir.writeFile(tmp_src_path, update.src);
@@ -1104,6 +1110,11 @@ pub const TestContext = struct {
                     }
                 },
                 .Execution => |expected_stdout| {
+                    if (!std.process.can_spawn) {
+                        print("Unable to spawn child processes on {s}, skipping test.\n", .{@tagName(builtin.os.tag)});
+                        return; // Pass test.
+                    }
+
                     update_node.setEstimatedTotalItems(4);
 
                     var argv = std.ArrayList([]const u8).init(allocator);
src/ThreadPool.zig
@@ -82,6 +82,9 @@ pub fn init(self: *ThreadPool, allocator: std.mem.Allocator) !void {
 }
 
 fn destroyWorkers(self: *ThreadPool, spawned: usize) void {
+    if (builtin.single_threaded)
+        return;
+
     for (self.workers[0..spawned]) |*worker| {
         worker.thread.join();
         worker.idle_node.data.deinit();
test/tests.zig
@@ -10,6 +10,8 @@ const fmt = std.fmt;
 const ArrayList = std.ArrayList;
 const Mode = std.builtin.Mode;
 const LibExeObjStep = build.LibExeObjStep;
+const Allocator = mem.Allocator;
+const ExecError = build.Builder.ExecError;
 
 // Cases
 const compare_output = @import("compare_output.zig");
@@ -26,6 +28,17 @@ 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;
 
+fn argvCmd(allocator: Allocator, argv: []const []const u8) ![]u8 {
+    var cmd = std.ArrayList(u8).init(allocator);
+    defer cmd.deinit();
+    for (argv[0 .. argv.len - 1]) |arg| {
+        try cmd.appendSlice(arg);
+        try cmd.append(' ');
+    }
+    try cmd.appendSlice(argv[argv.len - 1]);
+    return cmd.toOwnedSlice();
+}
+
 const TestTarget = struct {
     target: CrossTarget = @as(CrossTarget, .{}),
     mode: std.builtin.Mode = .Debug,
@@ -722,6 +735,13 @@ pub const StackTracesContext = struct {
 
             std.debug.print("Test {d}/{d} {s}...", .{ self.test_index + 1, self.context.test_index, self.name });
 
+            if (!std.process.can_spawn) {
+                const cmd = try argvCmd(b.allocator, args.items);
+                std.debug.print("the following command cannot be executed ({s} does not support spawning a child process):\n{s}", .{ @tagName(builtin.os.tag), cmd });
+                b.allocator.free(cmd);
+                return ExecError.ExecNotSupported;
+            }
+
             const child = std.ChildProcess.init(args.items, b.allocator) catch unreachable;
             defer child.deinit();
 
build.zig
@@ -229,6 +229,10 @@ pub fn build(b: *Builder) !void {
     const version = if (opt_version_string) |version| version else v: {
         const version_string = b.fmt("{d}.{d}.{d}", .{ zig_version.major, zig_version.minor, zig_version.patch });
 
+        if (!std.process.can_spawn) {
+            std.debug.print("error: version info cannot be retrieved from git. Zig version must be provided using -Dversion-string\n", .{});
+            std.process.exit(1);
+        }
         var code: u8 = undefined;
         const git_describe_untrimmed = b.execAllowFail(&[_][]const u8{
             "git", "-C", b.build_root, "describe", "--match", "*.*.*", "--tags",
@@ -542,6 +546,9 @@ fn addCxxKnownPath(
     errtxt: ?[]const u8,
     need_cpp_includes: bool,
 ) !void {
+    if (!std.process.can_spawn)
+        return error.RequiredLibraryNotFound;
+
     const path_padded = try b.exec(&[_][]const u8{
         ctx.cxx_compiler,
         b.fmt("-print-file-name={s}", .{objname}),