Commit 85c1db9222

Andrew Kelley <andrew@ziglang.org>
2020-12-09 08:26:13
Merge pull request #7355 from ziglang/lld-child-process
invoke LLD as a child process rather than a library
1 parent 40e37d4
lib/std/testing.zig
@@ -247,6 +247,7 @@ test "expectWithinEpsilon" {
 /// This function is intended to be used only in tests. When the two slices are not
 /// equal, prints diagnostics to stderr to show exactly how they are not equal,
 /// then aborts.
+/// If your inputs are UTF-8 encoded strings, consider calling `expectEqualStrings` instead.
 pub fn expectEqualSlices(comptime T: type, expected: []const T, actual: []const T) void {
     // TODO better printing of the difference
     // If the arrays are small enough we could print the whole thing
@@ -368,6 +369,26 @@ pub fn expectEqualStrings(expected: []const u8, actual: []const u8) void {
     }
 }
 
+pub fn expectStringEndsWith(actual: []const u8, expected_ends_with: []const u8) void {
+    if (std.mem.endsWith(u8, actual, expected_ends_with))
+        return;
+
+    const shortened_actual = if (actual.len >= expected_ends_with.len)
+        actual[0..expected_ends_with.len]
+    else
+        actual;
+
+    print("\n====== expected to end with: =========\n", .{});
+    printWithVisibleNewlines(expected_ends_with);
+    print("\n====== instead ended with: ===========\n", .{});
+    printWithVisibleNewlines(shortened_actual);
+    print("\n========= full output: ==============\n", .{});
+    printWithVisibleNewlines(actual);
+    print("\n======================================\n", .{});
+
+    @panic("test failure");
+}
+
 fn printIndicatorLine(source: []const u8, indicator_index: usize) void {
     const line_begin_index = if (std.mem.lastIndexOfScalar(u8, source[0..indicator_index], '\n')) |line_begin|
         line_begin + 1
src/link/Coff.zig
@@ -907,8 +907,10 @@ fn linkWithLLD(self: *Coff, comp: *Compilation) !void {
         // Create an LLD command line and invoke it.
         var argv = std.ArrayList([]const u8).init(self.base.allocator);
         defer argv.deinit();
-        // Even though we're calling LLD as a library it thinks the first argument is its own exe name.
-        try argv.append("lld");
+        // We will invoke ourselves as a child process to gain access to LLD.
+        // This is necessary because LLD does not behave properly as a library -
+        // it calls exit() and does not reset all global data between invocations.
+        try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, "lld-link" });
 
         try argv.append("-ERRORLIMIT:0");
         try argv.append("-NOLOGO");
@@ -1146,45 +1148,65 @@ fn linkWithLLD(self: *Coff, comp: *Compilation) !void {
         }
 
         if (self.base.options.verbose_link) {
-            Compilation.dump_argv(argv.items);
+            // Skip over our own name so that the LLD linker name is the first argv item.
+            Compilation.dump_argv(argv.items[1..]);
         }
 
-        const new_argv = try arena.allocSentinel(?[*:0]const u8, argv.items.len, null);
-        for (argv.items) |arg, i| {
-            new_argv[i] = try arena.dupeZ(u8, arg);
-        }
+        // 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(),
+            }
+        } 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;
+                },
+            }
 
-        var stderr_context: LLDContext = .{
-            .coff = self,
-            .data = std.ArrayList(u8).init(self.base.allocator),
-        };
-        defer stderr_context.data.deinit();
-        var stdout_context: LLDContext = .{
-            .coff = self,
-            .data = std.ArrayList(u8).init(self.base.allocator),
-        };
-        defer stdout_context.data.deinit();
-        const llvm = @import("../llvm.zig");
-        const ok = llvm.Link(
-            .COFF,
-            new_argv.ptr,
-            new_argv.len,
-            append_diagnostic,
-            @ptrToInt(&stdout_context),
-            @ptrToInt(&stderr_context),
-        );
-        if (stderr_context.oom or stdout_context.oom) return error.OutOfMemory;
-        if (stdout_context.data.items.len != 0) {
-            std.log.warn("unexpected LLD stdout: {}", .{stdout_context.data.items});
-        }
-        if (!ok) {
-            // TODO parse this output and surface with the Compilation API rather than
-            // directly outputting to stderr here.
-            std.debug.print("{}", .{stderr_context.data.items});
-            return error.LLDReportedFailure;
-        }
-        if (stderr_context.data.items.len != 0) {
-            std.log.warn("unexpected LLD stderr: {}", .{stderr_context.data.items});
+            if (stderr.len != 0) {
+                std.log.warn("unexpected LLD stderr:\n{s}", .{stderr});
+            }
         }
     }
 
@@ -1204,20 +1226,6 @@ fn linkWithLLD(self: *Coff, comp: *Compilation) !void {
     }
 }
 
-const LLDContext = struct {
-    data: std.ArrayList(u8),
-    coff: *Coff,
-    oom: bool = false,
-};
-
-fn append_diagnostic(context: usize, ptr: [*]const u8, len: usize) callconv(.C) void {
-    const lld_context = @intToPtr(*LLDContext, context);
-    const msg = ptr[0..len];
-    lld_context.data.appendSlice(msg) catch |err| switch (err) {
-        error.OutOfMemory => lld_context.oom = true,
-    };
-}
-
 pub fn getDeclVAddr(self: *Coff, decl: *const Module.Decl) u64 {
     return self.text_section_virtual_address + decl.link.coff.text_offset;
 }
src/link/Elf.zig
@@ -1360,8 +1360,10 @@ fn linkWithLLD(self: *Elf, comp: *Compilation) !void {
     // Create an LLD command line and invoke it.
     var argv = std.ArrayList([]const u8).init(self.base.allocator);
     defer argv.deinit();
-    // Even though we're calling LLD as a library it thinks the first argument is its own exe name.
-    try argv.append("lld");
+    // We will invoke ourselves as a child process to gain access to LLD.
+    // This is necessary because LLD does not behave properly as a library -
+    // it calls exit() and does not reset all global data between invocations.
+    try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, "ld.lld" });
     if (is_obj) {
         try argv.append("-r");
     }
@@ -1621,46 +1623,65 @@ fn linkWithLLD(self: *Elf, comp: *Compilation) !void {
     }
 
     if (self.base.options.verbose_link) {
-        Compilation.dump_argv(argv.items);
+        // Skip over our own name so that the LLD linker name is the first argv item.
+        Compilation.dump_argv(argv.items[1..]);
     }
 
-    // Oh, snapplesauce! We need null terminated argv.
-    const new_argv = try arena.allocSentinel(?[*:0]const u8, argv.items.len, null);
-    for (argv.items) |arg, i| {
-        new_argv[i] = try arena.dupeZ(u8, arg);
-    }
+    // 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();
 
-    var stderr_context: LLDContext = .{
-        .elf = self,
-        .data = std.ArrayList(u8).init(self.base.allocator),
-    };
-    defer stderr_context.data.deinit();
-    var stdout_context: LLDContext = .{
-        .elf = self,
-        .data = std.ArrayList(u8).init(self.base.allocator),
-    };
-    defer stdout_context.data.deinit();
-    const llvm = @import("../llvm.zig");
-    const ok = llvm.Link(
-        .ELF,
-        new_argv.ptr,
-        new_argv.len,
-        append_diagnostic,
-        @ptrToInt(&stdout_context),
-        @ptrToInt(&stderr_context),
-    );
-    if (stderr_context.oom or stdout_context.oom) return error.OutOfMemory;
-    if (stdout_context.data.items.len != 0) {
-        std.log.warn("unexpected LLD stdout: {}", .{stdout_context.data.items});
-    }
-    if (!ok) {
-        // TODO parse this output and surface with the Compilation API rather than
-        // directly outputting to stderr here.
-        std.debug.print("{}", .{stderr_context.data.items});
-        return error.LLDReportedFailure;
-    }
-    if (stderr_context.data.items.len != 0) {
-        std.log.warn("unexpected LLD stderr: {}", .{stderr_context.data.items});
+    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;
+
+        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) {
+            std.log.warn("unexpected LLD stderr:\n{s}", .{stderr});
+        }
     }
 
     if (!self.base.options.disable_lld_caching) {
@@ -1679,20 +1700,6 @@ fn linkWithLLD(self: *Elf, comp: *Compilation) !void {
     }
 }
 
-const LLDContext = struct {
-    data: std.ArrayList(u8),
-    elf: *Elf,
-    oom: bool = false,
-};
-
-fn append_diagnostic(context: usize, ptr: [*]const u8, len: usize) callconv(.C) void {
-    const lld_context = @intToPtr(*LLDContext, context);
-    const msg = ptr[0..len];
-    lld_context.data.appendSlice(msg) catch |err| switch (err) {
-        error.OutOfMemory => lld_context.oom = true,
-    };
-}
-
 fn writeDwarfAddrAssumeCapacity(self: *Elf, buf: *std.ArrayList(u8), addr: u64) void {
     const target_endian = self.base.options.target.cpu.arch.endian();
     switch (self.ptr_width) {
src/link/MachO.zig
@@ -544,8 +544,10 @@ fn linkWithLLD(self: *MachO, comp: *Compilation) !void {
         if (self.base.options.system_linker_hack) {
             try argv.append("ld");
         } else {
-            // Even though we're calling LLD as a library it thinks the first argument is its own exe name.
-            try argv.append("lld");
+            // We will invoke ourselves as a child process to gain access to LLD.
+            // This is necessary because LLD does not behave properly as a library -
+            // it calls exit() and does not reset all global data between invocations.
+            try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, "ld64.lld" });
 
             try argv.append("-error-limit");
             try argv.append("0");
@@ -711,7 +713,9 @@ fn linkWithLLD(self: *MachO, comp: *Compilation) !void {
         }
 
         if (self.base.options.verbose_link) {
-            Compilation.dump_argv(argv.items);
+            // Potentially skip over our own name so that the LLD linker name is the first argv item.
+            const adjusted_argv = if (self.base.options.system_linker_hack) argv.items else argv.items[1..];
+            Compilation.dump_argv(adjusted_argv);
         }
 
         // TODO https://github.com/ziglang/zig/issues/6971
@@ -736,42 +740,61 @@ fn linkWithLLD(self: *MachO, comp: *Compilation) !void {
                 return error.LDReportedFailure;
             }
         } else {
-            const new_argv = try arena.allocSentinel(?[*:0]const u8, argv.items.len, null);
-            for (argv.items) |arg, i| {
-                new_argv[i] = try arena.dupeZ(u8, arg);
-            }
+            // 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(),
+                }
+            } 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;
+                    },
+                }
 
-            var stderr_context: LLDContext = .{
-                .macho = self,
-                .data = std.ArrayList(u8).init(self.base.allocator),
-            };
-            defer stderr_context.data.deinit();
-            var stdout_context: LLDContext = .{
-                .macho = self,
-                .data = std.ArrayList(u8).init(self.base.allocator),
-            };
-            defer stdout_context.data.deinit();
-            const llvm = @import("../llvm.zig");
-            const ok = llvm.Link(
-                .MachO,
-                new_argv.ptr,
-                new_argv.len,
-                append_diagnostic,
-                @ptrToInt(&stdout_context),
-                @ptrToInt(&stderr_context),
-            );
-            if (stderr_context.oom or stdout_context.oom) return error.OutOfMemory;
-            if (stdout_context.data.items.len != 0) {
-                std.log.warn("unexpected LLD stdout: {}", .{stdout_context.data.items});
-            }
-            if (!ok) {
-                // TODO parse this output and surface with the Compilation API rather than
-                // directly outputting to stderr here.
-                std.debug.print("{}", .{stderr_context.data.items});
-                return error.LLDReportedFailure;
-            }
-            if (stderr_context.data.items.len != 0) {
-                std.log.warn("unexpected LLD stderr: {}", .{stderr_context.data.items});
+                if (stderr.len != 0) {
+                    std.log.warn("unexpected LLD stderr:\n{s}", .{stderr});
+                }
             }
         }
     }
@@ -792,20 +815,6 @@ fn linkWithLLD(self: *MachO, comp: *Compilation) !void {
     }
 }
 
-const LLDContext = struct {
-    data: std.ArrayList(u8),
-    macho: *MachO,
-    oom: bool = false,
-};
-
-fn append_diagnostic(context: usize, ptr: [*]const u8, len: usize) callconv(.C) void {
-    const lld_context = @intToPtr(*LLDContext, context);
-    const msg = ptr[0..len];
-    lld_context.data.appendSlice(msg) catch |err| switch (err) {
-        error.OutOfMemory => lld_context.oom = true,
-    };
-}
-
 fn darwinArchString(arch: std.Target.Cpu.Arch) []const u8 {
     return switch (arch) {
         .aarch64, .aarch64_be, .aarch64_32 => "arm64",
src/link/Wasm.zig
@@ -345,8 +345,10 @@ fn linkWithLLD(self: *Wasm, comp: *Compilation) !void {
     // Create an LLD command line and invoke it.
     var argv = std.ArrayList([]const u8).init(self.base.allocator);
     defer argv.deinit();
-    // Even though we're calling LLD as a library it thinks the first argument is its own exe name.
-    try argv.append("lld");
+    // We will invoke ourselves as a child process to gain access to LLD.
+    // This is necessary because LLD does not behave properly as a library -
+    // it calls exit() and does not reset all global data between invocations.
+    try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, "wasm-ld" });
     if (is_obj) {
         try argv.append("-r");
     }
@@ -396,45 +398,65 @@ fn linkWithLLD(self: *Wasm, comp: *Compilation) !void {
     }
 
     if (self.base.options.verbose_link) {
-        Compilation.dump_argv(argv.items);
+        // Skip over our own name so that the LLD linker name is the first argv item.
+        Compilation.dump_argv(argv.items[1..]);
     }
 
-    const new_argv = try arena.allocSentinel(?[*:0]const u8, argv.items.len, null);
-    for (argv.items) |arg, i| {
-        new_argv[i] = try arena.dupeZ(u8, arg);
-    }
+    // 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();
 
-    var stderr_context: LLDContext = .{
-        .wasm = self,
-        .data = std.ArrayList(u8).init(self.base.allocator),
-    };
-    defer stderr_context.data.deinit();
-    var stdout_context: LLDContext = .{
-        .wasm = self,
-        .data = std.ArrayList(u8).init(self.base.allocator),
-    };
-    defer stdout_context.data.deinit();
-    const llvm = @import("../llvm.zig");
-    const ok = llvm.Link(
-        .Wasm,
-        new_argv.ptr,
-        new_argv.len,
-        append_diagnostic,
-        @ptrToInt(&stdout_context),
-        @ptrToInt(&stderr_context),
-    );
-    if (stderr_context.oom or stdout_context.oom) return error.OutOfMemory;
-    if (stdout_context.data.items.len != 0) {
-        std.log.warn("unexpected LLD stdout: {}", .{stdout_context.data.items});
-    }
-    if (!ok) {
-        // TODO parse this output and surface with the Compilation API rather than
-        // directly outputting to stderr here.
-        std.debug.print("{}", .{stderr_context.data.items});
-        return error.LLDReportedFailure;
-    }
-    if (stderr_context.data.items.len != 0) {
-        std.log.warn("unexpected LLD stderr: {}", .{stderr_context.data.items});
+    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;
+
+        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) {
+            std.log.warn("unexpected LLD stderr:\n{s}", .{stderr});
+        }
     }
 
     if (!self.base.options.disable_lld_caching) {
@@ -453,20 +475,6 @@ fn linkWithLLD(self: *Wasm, comp: *Compilation) !void {
     }
 }
 
-const LLDContext = struct {
-    data: std.ArrayList(u8),
-    wasm: *Wasm,
-    oom: bool = false,
-};
-
-fn append_diagnostic(context: usize, ptr: [*]const u8, len: usize) callconv(.C) void {
-    const lld_context = @intToPtr(*LLDContext, context);
-    const msg = ptr[0..len];
-    lld_context.data.appendSlice(msg) catch |err| switch (err) {
-        error.OutOfMemory => lld_context.oom = true,
-    };
-}
-
 /// Get the current index of a given Decl in the function list
 /// TODO: we could maintain a hash map to potentially make this
 fn getFuncidx(self: Wasm, decl: *Module.Decl) ?u32 {
src/Compilation.zig
@@ -1756,7 +1756,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_comp_progress_node: *
                     if (comp.clang_preprocessor_mode == .stdout)
                         std.process.exit(0);
                 },
-                else => std.process.exit(1),
+                else => std.process.abort(),
             }
         } else {
             child.stdin_behavior = .Ignore;
src/llvm.zig
@@ -1,15 +1,15 @@
 //! We do this instead of @cImport because the self-hosted compiler is easier
 //! to bootstrap if it does not depend on translate-c.
 
-pub const Link = ZigLLDLink;
-extern fn ZigLLDLink(
-    oformat: ObjectFormatType,
-    args: [*:null]const ?[*:0]const u8,
-    arg_count: usize,
-    append_diagnostic: fn (context: usize, ptr: [*]const u8, len: usize) callconv(.C) void,
-    context_stdout: usize,
-    context_stderr: usize,
-) bool;
+extern fn ZigLLDLinkCOFF(argc: c_int, argv: [*:null]const ?[*:0]const u8, can_exit_early: bool) c_int;
+extern fn ZigLLDLinkELF(argc: c_int, argv: [*:null]const ?[*:0]const u8, can_exit_early: bool) c_int;
+extern fn ZigLLDLinkMachO(argc: c_int, argv: [*:null]const ?[*:0]const u8, can_exit_early: bool) c_int;
+extern fn ZigLLDLinkWasm(argc: c_int, argv: [*:null]const ?[*:0]const u8, can_exit_early: bool) c_int;
+
+pub const LinkCOFF = ZigLLDLinkCOFF;
+pub const LinkELF = ZigLLDLinkELF;
+pub const LinkMachO = ZigLLDLinkMachO;
+pub const LinkWasm = ZigLLDLinkWasm;
 
 pub const ObjectFormatType = extern enum(c_int) {
     Unknown,
src/main.zig
@@ -176,6 +176,12 @@ pub fn mainArgs(gpa: *Allocator, arena: *Allocator, args: []const []const u8) !v
         mem.eql(u8, cmd, "-cc1") or mem.eql(u8, cmd, "-cc1as"))
     {
         return punt_to_clang(arena, args);
+    } else if (mem.eql(u8, cmd, "ld.lld") or
+        mem.eql(u8, cmd, "ld64.lld") or
+        mem.eql(u8, cmd, "lld-link") or
+        mem.eql(u8, cmd, "wasm-ld"))
+    {
+        return punt_to_lld(arena, args);
     } else if (mem.eql(u8, cmd, "build")) {
         return cmdBuild(gpa, arena, cmd_args);
     } else if (mem.eql(u8, cmd, "fmt")) {
@@ -2786,6 +2792,39 @@ fn punt_to_clang(arena: *Allocator, args: []const []const u8) error{OutOfMemory}
     process.exit(@bitCast(u8, @truncate(i8, exit_code)));
 }
 
+/// The first argument determines which backend is invoked. The options are:
+/// * `ld.lld` - ELF
+/// * `ld64.lld` - Mach-O
+/// * `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} {
+    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.
+    }
+    const exit_code = rc: {
+        const llvm = @import("llvm.zig");
+        const argc = @intCast(c_int, argv.len);
+        if (mem.eql(u8, args[1], "ld.lld")) {
+            break :rc llvm.LinkELF(argc, argv.ptr, true);
+        } else if (mem.eql(u8, args[1], "ld64.lld")) {
+            break :rc llvm.LinkMachO(argc, argv.ptr, true);
+        } else if (mem.eql(u8, args[1], "lld-link")) {
+            break :rc llvm.LinkCOFF(argc, argv.ptr, true);
+        } else if (mem.eql(u8, args[1], "wasm-ld")) {
+            break :rc llvm.LinkWasm(argc, argv.ptr, true);
+        } else {
+            unreachable;
+        }
+    };
+    process.exit(@bitCast(u8, @truncate(i8, exit_code)));
+}
+
 const clang_args = @import("clang_options.zig").list;
 
 pub const ClangArgIterator = struct {
src/zig_llvm.cpp
@@ -1048,39 +1048,24 @@ bool ZigLLVMWriteArchive(const char *archive_name, const char **file_names, size
     return false;
 }
 
+int ZigLLDLinkCOFF(int argc, const char **argv, bool can_exit_early) {
+    std::vector<const char *> args(argv, argv + argc);
+    return lld::coff::link(args, can_exit_early, llvm::outs(), llvm::errs());
+}
 
-bool ZigLLDLink(ZigLLVM_ObjectFormatType oformat, const char **args, size_t arg_count,
-        void (*append_diagnostic)(void *, const char *, size_t),
-        void *context_stdout, void *context_stderr)
-{
-    ArrayRef<const char *> array_ref_args(args, arg_count);
-
-    MyOStream diag_stdout(append_diagnostic, context_stdout);
-    MyOStream diag_stderr(append_diagnostic, context_stderr);
-
-    switch (oformat) {
-        case ZigLLVM_UnknownObjectFormat:
-        case ZigLLVM_XCOFF:
-            assert(false); // unreachable
-            break;
-
-        case ZigLLVM_COFF:
-            return lld::coff::link(array_ref_args, false, diag_stdout, diag_stderr);
-
-        case ZigLLVM_ELF:
-            return lld::elf::link(array_ref_args, false, diag_stdout, diag_stderr);
-
-        case ZigLLVM_MachO:
-            return lld::mach_o::link(array_ref_args, false, diag_stdout, diag_stderr);
+int ZigLLDLinkELF(int argc, const char **argv, bool can_exit_early) {
+    std::vector<const char *> args(argv, argv + argc);
+    return lld::elf::link(args, can_exit_early, llvm::outs(), llvm::errs());
+}
 
-        case ZigLLVM_Wasm:
-            return lld::wasm::link(array_ref_args, false, diag_stdout, diag_stderr);
+int ZigLLDLinkMachO(int argc, const char **argv, bool can_exit_early) {
+    std::vector<const char *> args(argv, argv + argc);
+    return lld::mach_o::link(args, can_exit_early, llvm::outs(), llvm::errs());
+}
 
-        default:
-            break;
-    }
-    assert(false); // unreachable
-    abort();
+int ZigLLDLinkWasm(int argc, const char **argv, bool can_exit_early) {
+    std::vector<const char *> args(argv, argv + argc);
+    return lld::wasm::link(args, can_exit_early, llvm::outs(), llvm::errs());
 }
 
 static AtomicRMWInst::BinOp toLLVMRMWBinOp(enum ZigLLVM_AtomicRMWBinOp BinOp) {
src/zig_llvm.h
@@ -505,9 +505,10 @@ ZIG_EXTERN_C const char *ZigLLVMGetVendorTypeName(enum ZigLLVM_VendorType vendor
 ZIG_EXTERN_C const char *ZigLLVMGetOSTypeName(enum ZigLLVM_OSType os);
 ZIG_EXTERN_C const char *ZigLLVMGetEnvironmentTypeName(enum ZigLLVM_EnvironmentType abi);
 
-ZIG_EXTERN_C bool ZigLLDLink(enum ZigLLVM_ObjectFormatType oformat, const char **args, size_t arg_count,
-        void (*append_diagnostic)(void *, const char *, size_t),
-        void *context_stdout, void *context_stderr);
+ZIG_EXTERN_C int ZigLLDLinkCOFF(int argc, const char **argv, bool can_exit_early);
+ZIG_EXTERN_C int ZigLLDLinkELF(int argc, const char **argv, bool can_exit_early);
+ZIG_EXTERN_C int ZigLLDLinkMachO(int argc, const char **argv, bool can_exit_early);
+ZIG_EXTERN_C int ZigLLDLinkWasm(int argc, const char **argv, bool can_exit_early);
 
 ZIG_EXTERN_C bool ZigLLVMWriteArchive(const char *archive_name, const char **file_names, size_t file_name_count,
         enum ZigLLVM_OSType os_type);
test/cli.zig
@@ -92,13 +92,13 @@ fn exec(cwd: []const u8, expect_0: bool, argv: []const []const u8) !ChildProcess
 fn testZigInitLib(zig_exe: []const u8, dir_path: []const u8) !void {
     _ = try exec(dir_path, true, &[_][]const u8{ zig_exe, "init-lib" });
     const test_result = try exec(dir_path, true, &[_][]const u8{ zig_exe, "build", "test" });
-    testing.expect(std.mem.endsWith(u8, test_result.stderr, "All 1 tests passed.\n"));
+    testing.expectStringEndsWith(test_result.stderr, "All 1 tests passed.\n");
 }
 
 fn testZigInitExe(zig_exe: []const u8, dir_path: []const u8) !void {
     _ = try exec(dir_path, true, &[_][]const u8{ zig_exe, "init-exe" });
     const run_result = try exec(dir_path, true, &[_][]const u8{ zig_exe, "build", "run" });
-    testing.expect(std.mem.eql(u8, run_result.stderr, "info: All your codebase are belong to us.\n"));
+    testing.expectEqualStrings("info: All your codebase are belong to us.\n", run_result.stderr);
 }
 
 fn testGodboltApi(zig_exe: []const u8, dir_path: []const u8) anyerror!void {