Commit 2fb6f5c1ad

mlugg <mlugg@mlugg.co.uk>
2025-05-28 10:30:31
link: divorce LLD from the self-hosted linkers
Similar to the previous commit, this commit untangles LLD integration from the self-hosted linkers. Despite the big network of functions which were involved, it turns out what was going on here is quite simple. The LLD linking logic is actually very self-contained; it requires a few flags from the `link.File.OpenOptions`, but that's really about it. We don't need any of the mutable state on `Elf`/`Coff`/`Wasm`, for instance. There was some legacy code trying to handle support for using self-hosted codegen with LLD, but that's not a supported use case, so I've just stripped it out. For now, I've just pasted the logic for linking the 3 targets we currently support using LLD for into this new linker implementation, `link.Lld`; however, it's almost certainly possible to combine some of the logic and simplify this file a bit. But to be honest, it's not actually that bad right now. This commit ends up eliminating the distinction between `flush` and `flushZcu` (formerly `flushModule`) in linkers, where the latter previously meant something along the lines of "flush, but if you're going to be linking with LLD, just flush the ZCU object file, don't actually link"?. The distinction here doesn't seem like it was properly defined, and most linkers seem to treat them as essentially identical anyway. Regardless, all calls to `flushZcu` are gone now, so it's deleted -- one `flush` to rule them all! The end result of this commit and the preceding one is that LLVM and LLD fit into the pipeline much more sanely: * If we're using LLVM for the ZCU, that state is on `zcu.llvm_object` * If we're using LLD to link, then the `link.File` is a `link.Lld` * Calls to "ZCU link functions" (e.g. `updateNav`) lower to calls to the LLVM object if it's available, or otherwise to the `link.File` if it's available (neither is available under `-fno-emit-bin`) * After everything is done, linking is finalized by calling `flush` on the `link.File`; for `link.Lld` this invokes LLD, for other linkers it flushes self-hosted linker state There's one messy thing remaining, and that's how self-hosted function codegen in a ZCU works; right now, we process AIR with a call sequence something like this: * `link.doTask` * `Zcu.PerThread.linkerUpdateFunc` * `link.File.updateFunc` * `link.Elf.updateFunc` * `link.Elf.ZigObject.updateFunc` * `codegen.generateFunction` * `arch.x86_64.CodeGen.generate` So, we start in the linker, take a scenic detour through `Zcu`, go back to the linker, into its implementation, and then... right back out, into code which is generic over the linker implementation, and then dispatch on the *backend* instead! Of course, within `arch.x86_64.CodeGen`, there are some more places which switch on the `link` implementation being used. This is all pretty silly... so it shall be my next target.
1 parent 3743c3e
src/codegen/llvm.zig
@@ -1587,12 +1587,15 @@ pub const Object = struct {
         const comp = zcu.comp;
 
         // If we're on COFF and linking with LLD, the linker cares about our exports to determine the subsystem in use.
-        if (comp.bin_file != null and
-            comp.bin_file.?.tag == .coff and
-            zcu.comp.config.use_lld and
-            ip.isFunctionType(ip.getNav(nav_index).typeOf(ip)))
-        {
-            const flags = &comp.bin_file.?.cast(.coff).?.lld_export_flags;
+        coff_export_flags: {
+            const lf = comp.bin_file orelse break :coff_export_flags;
+            const lld = lf.cast(.lld) orelse break :coff_export_flags;
+            const coff = switch (lld.ofmt) {
+                .elf, .wasm => break :coff_export_flags,
+                .coff => |*coff| coff,
+            };
+            if (!ip.isFunctionType(ip.getNav(nav_index).typeOf(ip))) break :coff_export_flags;
+            const flags = &coff.lld_export_flags;
             for (export_indices) |export_index| {
                 const name = export_index.ptr(zcu).opts.name;
                 if (name.eqlSlice("main", ip)) flags.c_main = true;
src/link/Elf/ZigObject.zig
@@ -310,7 +310,7 @@ pub fn flush(self: *ZigObject, elf_file: *Elf, tid: Zcu.PerThread.Id) !void {
     if (self.dwarf) |*dwarf| {
         const pt: Zcu.PerThread = .activate(elf_file.base.comp.zcu.?, tid);
         defer pt.deactivate();
-        try dwarf.flushZcu(pt);
+        try dwarf.flush(pt);
 
         const gpa = elf_file.base.comp.gpa;
         const cpu_arch = elf_file.getTarget().cpu.arch;
@@ -481,7 +481,7 @@ pub fn flush(self: *ZigObject, elf_file: *Elf, tid: Zcu.PerThread.Id) !void {
         self.debug_str_section_dirty = false;
     }
 
-    // The point of flushZcu() is to commit changes, so in theory, nothing should
+    // The point of flush() is to commit changes, so in theory, nothing should
     // be dirty after this. However, it is possible for some things to remain
     // dirty because they fail to be written in the event of compile errors,
     // such as debug_line_header_dirty and debug_info_header_dirty.
@@ -661,7 +661,7 @@ pub fn scanRelocs(self: *ZigObject, elf_file: *Elf, undefs: anytype) !void {
         if (shdr.sh_type == elf.SHT_NOBITS) continue;
         if (atom_ptr.scanRelocsRequiresCode(elf_file)) {
             // TODO ideally we don't have to fetch the code here.
-            // Perhaps it would make sense to save the code until flushZcu where we
+            // Perhaps it would make sense to save the code until flush where we
             // would free all of generated code?
             const code = try self.codeAlloc(elf_file, atom_index);
             defer gpa.free(code);
@@ -1075,7 +1075,7 @@ pub fn getOrCreateMetadataForLazySymbol(
     }
     state_ptr.* = .pending_flush;
     const symbol_index = symbol_index_ptr.*;
-    // anyerror needs to be deferred until flushZcu
+    // anyerror needs to be deferred until flush
     if (lazy_sym.ty != .anyerror_type) try self.updateLazySymbol(elf_file, pt, lazy_sym, symbol_index);
     return symbol_index;
 }
src/link/MachO/DebugSymbols.zig
@@ -178,7 +178,7 @@ fn findFreeSpace(self: *DebugSymbols, object_size: u64, min_alignment: u64) !u64
     return offset;
 }
 
-pub fn flushZcu(self: *DebugSymbols, macho_file: *MachO) !void {
+pub fn flush(self: *DebugSymbols, macho_file: *MachO) !void {
     const zo = macho_file.getZigObject().?;
     for (self.relocs.items) |*reloc| {
         const sym = zo.symbols.items[reloc.target];
src/link/MachO/ZigObject.zig
@@ -550,7 +550,7 @@ pub fn getInputSection(self: ZigObject, atom: Atom, macho_file: *MachO) macho.se
     return sect;
 }
 
-pub fn flushZcu(self: *ZigObject, macho_file: *MachO, tid: Zcu.PerThread.Id) link.File.FlushError!void {
+pub fn flush(self: *ZigObject, macho_file: *MachO, tid: Zcu.PerThread.Id) link.File.FlushError!void {
     const diags = &macho_file.base.comp.link_diags;
 
     // Handle any lazy symbols that were emitted by incremental compilation.
@@ -589,7 +589,7 @@ pub fn flushZcu(self: *ZigObject, macho_file: *MachO, tid: Zcu.PerThread.Id) lin
     if (self.dwarf) |*dwarf| {
         const pt: Zcu.PerThread = .activate(macho_file.base.comp.zcu.?, tid);
         defer pt.deactivate();
-        dwarf.flushZcu(pt) catch |err| switch (err) {
+        dwarf.flush(pt) catch |err| switch (err) {
             error.OutOfMemory => return error.OutOfMemory,
             else => |e| return diags.fail("failed to flush dwarf module: {s}", .{@errorName(e)}),
         };
@@ -599,7 +599,7 @@ pub fn flushZcu(self: *ZigObject, macho_file: *MachO, tid: Zcu.PerThread.Id) lin
         self.debug_strtab_dirty = false;
     }
 
-    // The point of flushZcu() is to commit changes, so in theory, nothing should
+    // The point of flush() is to commit changes, so in theory, nothing should
     // be dirty after this. However, it is possible for some things to remain
     // dirty because they fail to be written in the event of compile errors,
     // such as debug_line_header_dirty and debug_info_header_dirty.
@@ -1537,7 +1537,7 @@ pub fn getOrCreateMetadataForLazySymbol(
     }
     state_ptr.* = .pending_flush;
     const symbol_index = symbol_index_ptr.*;
-    // anyerror needs to be deferred until flushZcu
+    // anyerror needs to be deferred until flush
     if (lazy_sym.ty != .anyerror_type) try self.updateLazySymbol(macho_file, pt, lazy_sym, symbol_index);
     return symbol_index;
 }
src/link/C.zig
@@ -145,7 +145,6 @@ pub fn createEmpty(
             .stack_size = options.stack_size orelse 16777216,
             .allow_shlib_undefined = options.allow_shlib_undefined orelse false,
             .file = file,
-            .disable_lld_caching = options.disable_lld_caching,
             .build_id = options.build_id,
         },
     };
@@ -381,10 +380,6 @@ pub fn updateLineNumber(self: *C, pt: Zcu.PerThread, ti_id: InternPool.TrackedIn
     _ = ti_id;
 }
 
-pub fn flush(self: *C, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
-    return self.flushZcu(arena, tid, prog_node);
-}
-
 fn abiDefines(self: *C, target: std.Target) !std.ArrayList(u8) {
     const gpa = self.base.comp.gpa;
     var defines = std.ArrayList(u8).init(gpa);
@@ -400,7 +395,7 @@ fn abiDefines(self: *C, target: std.Target) !std.ArrayList(u8) {
     return defines;
 }
 
-pub fn flushZcu(self: *C, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
+pub fn flush(self: *C, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
     _ = arena; // Has the same lifetime as the call to Compilation.update.
 
     const tracy = trace(@src());
src/link/Coff.zig
@@ -1,23 +1,14 @@
-//! The main driver of the COFF linker.
-//! Currently uses our own implementation for the incremental linker, and falls back to
-//! LLD for traditional linking (linking relocatable object files).
-//! LLD is also the default linker for LLVM.
+//! The main driver of the self-hosted COFF linker.
 
 base: link.File,
 image_base: u64,
-subsystem: ?std.Target.SubSystem,
-tsaware: bool,
-nxcompat: bool,
-dynamicbase: bool,
 /// TODO this and minor_subsystem_version should be combined into one property and left as
 /// default or populated together. They should not be separate fields.
 major_subsystem_version: u16,
 minor_subsystem_version: u16,
-lib_directories: []const Directory,
 entry: link.File.OpenOptions.Entry,
 entry_addr: ?u32,
 module_definition_file: ?[]const u8,
-pdb_out_path: ?[]const u8,
 repro: bool,
 
 ptr_width: PtrWidth,
@@ -84,16 +75,6 @@ base_relocs: BaseRelocationTable = .{},
 /// Hot-code swapping state.
 hot_state: if (is_hot_update_compatible) HotUpdateState else struct {} = .{},
 
-/// When linking with LLD, these flags are used to determine the subsystem to pass on the LLD command line.
-lld_export_flags: struct {
-    c_main: bool = false,
-    winmain: bool = false,
-    wwinmain: bool = false,
-    winmain_crt_startup: bool = false,
-    wwinmain_crt_startup: bool = false,
-    dllmain_crt_startup: bool = false,
-} = .{},
-
 const is_hot_update_compatible = switch (builtin.target.os.tag) {
     .windows => true,
     else => false,
@@ -233,7 +214,6 @@ pub fn createEmpty(
     const output_mode = comp.config.output_mode;
     const link_mode = comp.config.link_mode;
     const use_llvm = comp.config.use_llvm;
-    const use_lld = build_options.have_llvm and comp.config.use_lld;
 
     const ptr_width: PtrWidth = switch (target.ptrBitWidth()) {
         0...32 => .p32,
@@ -244,12 +224,10 @@ pub fn createEmpty(
         else => 0x1000,
     };
 
-    // If using LLD to link, this code should produce an object file so that it
-    // can be passed to LLD.
     // If using LLVM to generate the object file for the zig compilation unit,
     // we need a place to put the object file so that it can be subsequently
     // handled.
-    const zcu_object_sub_path = if (!use_lld and !use_llvm)
+    const zcu_object_sub_path = if (!use_llvm)
         null
     else
         try allocPrint(arena, "{s}.obj", .{emit.sub_path});
@@ -266,7 +244,6 @@ pub fn createEmpty(
             .print_gc_sections = options.print_gc_sections,
             .allow_shlib_undefined = options.allow_shlib_undefined orelse false,
             .file = null,
-            .disable_lld_caching = options.disable_lld_caching,
             .build_id = options.build_id,
         },
         .ptr_width = ptr_width,
@@ -291,39 +268,21 @@ pub fn createEmpty(
             .Obj => 0,
         },
 
-        // Subsystem depends on the set of public symbol names from linked objects.
-        // See LinkerDriver::inferSubsystem from the LLD project for the flow chart.
-        .subsystem = options.subsystem,
-
         .entry = options.entry,
 
-        .tsaware = options.tsaware,
-        .nxcompat = options.nxcompat,
-        .dynamicbase = options.dynamicbase,
         .major_subsystem_version = options.major_subsystem_version orelse 6,
         .minor_subsystem_version = options.minor_subsystem_version orelse 0,
-        .lib_directories = options.lib_directories,
         .entry_addr = math.cast(u32, options.entry_addr orelse 0) orelse
             return error.EntryAddressTooBig,
         .module_definition_file = options.module_definition_file,
-        .pdb_out_path = options.pdb_out_path,
         .repro = options.repro,
     };
     errdefer coff.base.destroy();
 
-    if (use_lld and (use_llvm or !comp.config.have_zcu)) {
-        // LLVM emits the object file (if any); LLD links it into the final product.
-        return coff;
-    }
-
-    // What path should this COFF linker code output to?
-    // If using LLD to link, this code should produce an object file so that it
-    // can be passed to LLD.
-    const sub_path = if (use_lld) zcu_object_sub_path.? else emit.sub_path;
-    coff.base.file = try emit.root_dir.handle.createFile(sub_path, .{
+    coff.base.file = try emit.root_dir.handle.createFile(emit.sub_path, .{
         .truncate = true,
         .read = true,
-        .mode = link.File.determineMode(use_lld, output_mode, link_mode),
+        .mode = link.File.determineMode(output_mode, link_mode),
     });
 
     const gpa = comp.gpa;
@@ -1327,7 +1286,7 @@ pub fn getOrCreateAtomForLazySymbol(
     }
     state_ptr.* = .pending_flush;
     const atom = atom_ptr.*;
-    // anyerror needs to be deferred until flushZcu
+    // anyerror needs to be deferred until flush
     if (lazy_sym.ty != .anyerror_type) try coff.updateLazySymbolAtom(pt, lazy_sym, atom, switch (lazy_sym.kind) {
         .code => coff.text_section_index.?,
         .const_data => coff.rdata_section_index.?,
@@ -1631,575 +1590,7 @@ fn resolveGlobalSymbol(coff: *Coff, current: SymbolWithLoc) !void {
     gop.value_ptr.* = current;
 }
 
-pub fn flush(coff: *Coff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
-    const comp = coff.base.comp;
-    const use_lld = build_options.have_llvm and comp.config.use_lld;
-    const diags = &comp.link_diags;
-    if (use_lld) {
-        return coff.linkWithLLD(arena, tid, prog_node) catch |err| switch (err) {
-            error.OutOfMemory => return error.OutOfMemory,
-            error.LinkFailure => return error.LinkFailure,
-            else => |e| return diags.fail("failed to link with LLD: {s}", .{@errorName(e)}),
-        };
-    }
-    switch (comp.config.output_mode) {
-        .Exe, .Obj => return coff.flushZcu(arena, tid, prog_node),
-        .Lib => return diags.fail("writing lib files not yet implemented for COFF", .{}),
-    }
-}
-
-fn linkWithLLD(coff: *Coff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) !void {
-    dev.check(.lld_linker);
-
-    const tracy = trace(@src());
-    defer tracy.end();
-
-    const comp = coff.base.comp;
-    const gpa = comp.gpa;
-
-    const directory = coff.base.emit.root_dir; // Just an alias to make it shorter to type.
-    const full_out_path = try directory.join(arena, &[_][]const u8{coff.base.emit.sub_path});
-
-    // If there is no Zig code to compile, then we should skip flushing the output file because it
-    // will not be part of the linker line anyway.
-    const module_obj_path: ?[]const u8 = if (comp.zcu) |zcu| blk: {
-        if (zcu.llvm_object == null) {
-            try coff.flushZcu(arena, tid, prog_node);
-        } else {
-            // `Compilation.flush` has already made LLVM emit this object file for us.
-        }
-
-        if (fs.path.dirname(full_out_path)) |dirname| {
-            break :blk try fs.path.join(arena, &.{ dirname, coff.base.zcu_object_sub_path.? });
-        } else {
-            break :blk coff.base.zcu_object_sub_path.?;
-        }
-    } else null;
-
-    const sub_prog_node = prog_node.start("LLD Link", 0);
-    defer sub_prog_node.end();
-
-    const is_lib = comp.config.output_mode == .Lib;
-    const is_dyn_lib = comp.config.link_mode == .dynamic and is_lib;
-    const is_exe_or_dyn_lib = is_dyn_lib or comp.config.output_mode == .Exe;
-    const link_in_crt = comp.config.link_libc and is_exe_or_dyn_lib;
-    const target = comp.root_mod.resolved_target.result;
-    const optimize_mode = comp.root_mod.optimize_mode;
-    const entry_name: ?[]const u8 = switch (coff.entry) {
-        // This logic isn't quite right for disabled or enabled. No point in fixing it
-        // when the goal is to eliminate dependency on LLD anyway.
-        // https://github.com/ziglang/zig/issues/17751
-        .disabled, .default, .enabled => null,
-        .named => |name| name,
-    };
-
-    // See link/Elf.zig for comments on how this mechanism works.
-    const id_symlink_basename = "lld.id";
-
-    var man: Cache.Manifest = undefined;
-    defer if (!coff.base.disable_lld_caching) man.deinit();
-
-    var digest: [Cache.hex_digest_len]u8 = undefined;
-
-    if (!coff.base.disable_lld_caching) {
-        man = comp.cache_parent.obtain();
-        coff.base.releaseLock();
-
-        comptime assert(Compilation.link_hash_implementation_version == 14);
-
-        try link.hashInputs(&man, comp.link_inputs);
-        for (comp.c_object_table.keys()) |key| {
-            _ = try man.addFilePath(key.status.success.object_path, null);
-        }
-        for (comp.win32_resource_table.keys()) |key| {
-            _ = try man.addFile(key.status.success.res_path, null);
-        }
-        try man.addOptionalFile(module_obj_path);
-        man.hash.addOptionalBytes(entry_name);
-        man.hash.add(coff.base.stack_size);
-        man.hash.add(coff.image_base);
-        man.hash.add(coff.base.build_id);
-        {
-            // TODO remove this, libraries must instead be resolved by the frontend.
-            for (coff.lib_directories) |lib_directory| man.hash.addOptionalBytes(lib_directory.path);
-        }
-        man.hash.add(comp.skip_linker_dependencies);
-        if (comp.config.link_libc) {
-            man.hash.add(comp.libc_installation != null);
-            if (comp.libc_installation) |libc_installation| {
-                man.hash.addBytes(libc_installation.crt_dir.?);
-                if (target.abi == .msvc or target.abi == .itanium) {
-                    man.hash.addBytes(libc_installation.msvc_lib_dir.?);
-                    man.hash.addBytes(libc_installation.kernel32_lib_dir.?);
-                }
-            }
-        }
-        man.hash.addListOfBytes(comp.windows_libs.keys());
-        man.hash.addListOfBytes(comp.force_undefined_symbols.keys());
-        man.hash.addOptional(coff.subsystem);
-        man.hash.add(comp.config.is_test);
-        man.hash.add(coff.tsaware);
-        man.hash.add(coff.nxcompat);
-        man.hash.add(coff.dynamicbase);
-        man.hash.add(coff.base.allow_shlib_undefined);
-        // strip does not need to go into the linker hash because it is part of the hash namespace
-        man.hash.add(coff.major_subsystem_version);
-        man.hash.add(coff.minor_subsystem_version);
-        man.hash.add(coff.repro);
-        man.hash.addOptional(comp.version);
-        try man.addOptionalFile(coff.module_definition_file);
-
-        // We don't actually care whether it's a cache hit or miss; we just need the digest and the lock.
-        _ = try man.hit();
-        digest = man.final();
-        var prev_digest_buf: [digest.len]u8 = undefined;
-        const prev_digest: []u8 = Cache.readSmallFile(
-            directory.handle,
-            id_symlink_basename,
-            &prev_digest_buf,
-        ) catch |err| blk: {
-            log.debug("COFF LLD new_digest={s} error: {s}", .{ std.fmt.fmtSliceHexLower(&digest), @errorName(err) });
-            // Handle this as a cache miss.
-            break :blk prev_digest_buf[0..0];
-        };
-        if (mem.eql(u8, prev_digest, &digest)) {
-            log.debug("COFF LLD digest={s} match - skipping invocation", .{std.fmt.fmtSliceHexLower(&digest)});
-            // Hot diggity dog! The output binary is already there.
-            coff.base.lock = man.toOwnedLock();
-            return;
-        }
-        log.debug("COFF LLD prev_digest={s} new_digest={s}", .{ std.fmt.fmtSliceHexLower(prev_digest), std.fmt.fmtSliceHexLower(&digest) });
-
-        // We are about to change the output file to be different, so we invalidate the build hash now.
-        directory.handle.deleteFile(id_symlink_basename) catch |err| switch (err) {
-            error.FileNotFound => {},
-            else => |e| return e,
-        };
-    }
-
-    if (comp.config.output_mode == .Obj) {
-        // LLD's COFF driver does not support the equivalent of `-r` so we do a simple file copy
-        // here. TODO: think carefully about how we can avoid this redundant operation when doing
-        // build-obj. See also the corresponding TODO in linkAsArchive.
-        const the_object_path = blk: {
-            if (link.firstObjectInput(comp.link_inputs)) |obj| break :blk obj.path;
-
-            if (comp.c_object_table.count() != 0)
-                break :blk comp.c_object_table.keys()[0].status.success.object_path;
-
-            if (module_obj_path) |p|
-                break :blk Path.initCwd(p);
-
-            // TODO I think this is unreachable. Audit this situation when solving the above TODO
-            // regarding eliding redundant object -> object transformations.
-            return error.NoObjectsToLink;
-        };
-        try std.fs.Dir.copyFile(
-            the_object_path.root_dir.handle,
-            the_object_path.sub_path,
-            directory.handle,
-            coff.base.emit.sub_path,
-            .{},
-        );
-    } else {
-        // Create an LLD command line and invoke it.
-        var argv = std.ArrayList([]const u8).init(gpa);
-        defer argv.deinit();
-        // 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.
-        const linker_command = "lld-link";
-        try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, linker_command });
-
-        if (target.isMinGW()) {
-            try argv.append("-lldmingw");
-        }
-
-        try argv.append("-ERRORLIMIT:0");
-        try argv.append("-NOLOGO");
-        if (comp.config.debug_format != .strip) {
-            try argv.append("-DEBUG");
-
-            const out_ext = std.fs.path.extension(full_out_path);
-            const out_pdb = coff.pdb_out_path orelse try allocPrint(arena, "{s}.pdb", .{
-                full_out_path[0 .. full_out_path.len - out_ext.len],
-            });
-            const out_pdb_basename = std.fs.path.basename(out_pdb);
-
-            try argv.append(try allocPrint(arena, "-PDB:{s}", .{out_pdb}));
-            try argv.append(try allocPrint(arena, "-PDBALTPATH:{s}", .{out_pdb_basename}));
-        }
-        if (comp.version) |version| {
-            try argv.append(try allocPrint(arena, "-VERSION:{}.{}", .{ version.major, version.minor }));
-        }
-
-        if (target_util.llvmMachineAbi(target)) |mabi| {
-            try argv.append(try allocPrint(arena, "-MLLVM:-target-abi={s}", .{mabi}));
-        }
-
-        try argv.append(try allocPrint(arena, "-MLLVM:-float-abi={s}", .{if (target.abi.float() == .hard) "hard" else "soft"}));
-
-        if (comp.config.lto != .none) {
-            switch (optimize_mode) {
-                .Debug => {},
-                .ReleaseSmall => try argv.append("-OPT:lldlto=2"),
-                .ReleaseFast, .ReleaseSafe => try argv.append("-OPT:lldlto=3"),
-            }
-        }
-        if (comp.config.output_mode == .Exe) {
-            try argv.append(try allocPrint(arena, "-STACK:{d}", .{coff.base.stack_size}));
-        }
-        try argv.append(try allocPrint(arena, "-BASE:{d}", .{coff.image_base}));
-
-        switch (coff.base.build_id) {
-            .none => try argv.append("-BUILD-ID:NO"),
-            .fast => try argv.append("-BUILD-ID"),
-            .uuid, .sha1, .md5, .hexstring => {},
-        }
-
-        if (target.cpu.arch == .x86) {
-            try argv.append("-MACHINE:X86");
-        } else if (target.cpu.arch == .x86_64) {
-            try argv.append("-MACHINE:X64");
-        } else if (target.cpu.arch == .thumb) {
-            try argv.append("-MACHINE:ARM");
-        } else if (target.cpu.arch == .aarch64) {
-            try argv.append("-MACHINE:ARM64");
-        }
-
-        for (comp.force_undefined_symbols.keys()) |symbol| {
-            try argv.append(try allocPrint(arena, "-INCLUDE:{s}", .{symbol}));
-        }
-
-        if (is_dyn_lib) {
-            try argv.append("-DLL");
-        }
-
-        if (entry_name) |name| {
-            try argv.append(try allocPrint(arena, "-ENTRY:{s}", .{name}));
-        }
-
-        if (coff.repro) {
-            try argv.append("-BREPRO");
-        }
-
-        if (coff.tsaware) {
-            try argv.append("-tsaware");
-        }
-        if (coff.nxcompat) {
-            try argv.append("-nxcompat");
-        }
-        if (!coff.dynamicbase) {
-            try argv.append("-dynamicbase:NO");
-        }
-        if (coff.base.allow_shlib_undefined) {
-            try argv.append("-FORCE:UNRESOLVED");
-        }
-
-        try argv.append(try allocPrint(arena, "-OUT:{s}", .{full_out_path}));
-
-        if (comp.implib_emit) |emit| {
-            const implib_out_path = try emit.root_dir.join(arena, &[_][]const u8{emit.sub_path});
-            try argv.append(try allocPrint(arena, "-IMPLIB:{s}", .{implib_out_path}));
-        }
-
-        if (comp.config.link_libc) {
-            if (comp.libc_installation) |libc_installation| {
-                try argv.append(try allocPrint(arena, "-LIBPATH:{s}", .{libc_installation.crt_dir.?}));
-
-                if (target.abi == .msvc or target.abi == .itanium) {
-                    try argv.append(try allocPrint(arena, "-LIBPATH:{s}", .{libc_installation.msvc_lib_dir.?}));
-                    try argv.append(try allocPrint(arena, "-LIBPATH:{s}", .{libc_installation.kernel32_lib_dir.?}));
-                }
-            }
-        }
-
-        for (coff.lib_directories) |lib_directory| {
-            try argv.append(try allocPrint(arena, "-LIBPATH:{s}", .{lib_directory.path orelse "."}));
-        }
-
-        try argv.ensureUnusedCapacity(comp.link_inputs.len);
-        for (comp.link_inputs) |link_input| switch (link_input) {
-            .dso_exact => unreachable, // not applicable to PE/COFF
-            inline .dso, .res => |x| {
-                argv.appendAssumeCapacity(try x.path.toString(arena));
-            },
-            .object, .archive => |obj| {
-                if (obj.must_link) {
-                    argv.appendAssumeCapacity(try allocPrint(arena, "-WHOLEARCHIVE:{}", .{@as(Path, obj.path)}));
-                } else {
-                    argv.appendAssumeCapacity(try obj.path.toString(arena));
-                }
-            },
-        };
-
-        for (comp.c_object_table.keys()) |key| {
-            try argv.append(try key.status.success.object_path.toString(arena));
-        }
-
-        for (comp.win32_resource_table.keys()) |key| {
-            try argv.append(key.status.success.res_path);
-        }
-
-        if (module_obj_path) |p| {
-            try argv.append(p);
-        }
-
-        if (coff.module_definition_file) |def| {
-            try argv.append(try allocPrint(arena, "-DEF:{s}", .{def}));
-        }
-
-        const resolved_subsystem: ?std.Target.SubSystem = blk: {
-            if (coff.subsystem) |explicit| break :blk explicit;
-            switch (target.os.tag) {
-                .windows => {
-                    if (comp.zcu != null) {
-                        if (coff.lld_export_flags.dllmain_crt_startup or is_dyn_lib)
-                            break :blk null;
-                        if (coff.lld_export_flags.c_main or comp.config.is_test or
-                            coff.lld_export_flags.winmain_crt_startup or
-                            coff.lld_export_flags.wwinmain_crt_startup)
-                        {
-                            break :blk .Console;
-                        }
-                        if (coff.lld_export_flags.winmain or coff.lld_export_flags.wwinmain)
-                            break :blk .Windows;
-                    }
-                },
-                .uefi => break :blk .EfiApplication,
-                else => {},
-            }
-            break :blk null;
-        };
-
-        const Mode = enum { uefi, win32 };
-        const mode: Mode = mode: {
-            if (resolved_subsystem) |subsystem| {
-                const subsystem_suffix = try allocPrint(arena, ",{d}.{d}", .{
-                    coff.major_subsystem_version, coff.minor_subsystem_version,
-                });
-
-                switch (subsystem) {
-                    .Console => {
-                        try argv.append(try allocPrint(arena, "-SUBSYSTEM:console{s}", .{
-                            subsystem_suffix,
-                        }));
-                        break :mode .win32;
-                    },
-                    .EfiApplication => {
-                        try argv.append(try allocPrint(arena, "-SUBSYSTEM:efi_application{s}", .{
-                            subsystem_suffix,
-                        }));
-                        break :mode .uefi;
-                    },
-                    .EfiBootServiceDriver => {
-                        try argv.append(try allocPrint(arena, "-SUBSYSTEM:efi_boot_service_driver{s}", .{
-                            subsystem_suffix,
-                        }));
-                        break :mode .uefi;
-                    },
-                    .EfiRom => {
-                        try argv.append(try allocPrint(arena, "-SUBSYSTEM:efi_rom{s}", .{
-                            subsystem_suffix,
-                        }));
-                        break :mode .uefi;
-                    },
-                    .EfiRuntimeDriver => {
-                        try argv.append(try allocPrint(arena, "-SUBSYSTEM:efi_runtime_driver{s}", .{
-                            subsystem_suffix,
-                        }));
-                        break :mode .uefi;
-                    },
-                    .Native => {
-                        try argv.append(try allocPrint(arena, "-SUBSYSTEM:native{s}", .{
-                            subsystem_suffix,
-                        }));
-                        break :mode .win32;
-                    },
-                    .Posix => {
-                        try argv.append(try allocPrint(arena, "-SUBSYSTEM:posix{s}", .{
-                            subsystem_suffix,
-                        }));
-                        break :mode .win32;
-                    },
-                    .Windows => {
-                        try argv.append(try allocPrint(arena, "-SUBSYSTEM:windows{s}", .{
-                            subsystem_suffix,
-                        }));
-                        break :mode .win32;
-                    },
-                }
-            } else if (target.os.tag == .uefi) {
-                break :mode .uefi;
-            } else {
-                break :mode .win32;
-            }
-        };
-
-        switch (mode) {
-            .uefi => try argv.appendSlice(&[_][]const u8{
-                "-BASE:0",
-                "-ENTRY:EfiMain",
-                "-OPT:REF",
-                "-SAFESEH:NO",
-                "-MERGE:.rdata=.data",
-                "-NODEFAULTLIB",
-                "-SECTION:.xdata,D",
-            }),
-            .win32 => {
-                if (link_in_crt) {
-                    if (target.abi.isGnu()) {
-                        if (target.cpu.arch == .x86) {
-                            try argv.append("-ALTERNATENAME:__image_base__=___ImageBase");
-                        } else {
-                            try argv.append("-ALTERNATENAME:__image_base__=__ImageBase");
-                        }
-
-                        if (is_dyn_lib) {
-                            try argv.append(try comp.crtFileAsString(arena, "dllcrt2.obj"));
-                            if (target.cpu.arch == .x86) {
-                                try argv.append("-ALTERNATENAME:__DllMainCRTStartup@12=_DllMainCRTStartup@12");
-                            } else {
-                                try argv.append("-ALTERNATENAME:_DllMainCRTStartup=DllMainCRTStartup");
-                            }
-                        } else {
-                            try argv.append(try comp.crtFileAsString(arena, "crt2.obj"));
-                        }
-
-                        try argv.append(try comp.crtFileAsString(arena, "libmingw32.lib"));
-                    } else {
-                        try argv.append(switch (comp.config.link_mode) {
-                            .static => "libcmt.lib",
-                            .dynamic => "msvcrt.lib",
-                        });
-
-                        const lib_str = switch (comp.config.link_mode) {
-                            .static => "lib",
-                            .dynamic => "",
-                        };
-                        try argv.append(try allocPrint(arena, "{s}vcruntime.lib", .{lib_str}));
-                        try argv.append(try allocPrint(arena, "{s}ucrt.lib", .{lib_str}));
-
-                        //Visual C++ 2015 Conformance Changes
-                        //https://msdn.microsoft.com/en-us/library/bb531344.aspx
-                        try argv.append("legacy_stdio_definitions.lib");
-
-                        // msvcrt depends on kernel32 and ntdll
-                        try argv.append("kernel32.lib");
-                        try argv.append("ntdll.lib");
-                    }
-                } else {
-                    try argv.append("-NODEFAULTLIB");
-                    if (!is_lib and entry_name == null) {
-                        if (comp.zcu != null) {
-                            if (coff.lld_export_flags.winmain_crt_startup) {
-                                try argv.append("-ENTRY:WinMainCRTStartup");
-                            } else {
-                                try argv.append("-ENTRY:wWinMainCRTStartup");
-                            }
-                        } else {
-                            try argv.append("-ENTRY:wWinMainCRTStartup");
-                        }
-                    }
-                }
-            },
-        }
-
-        if (comp.config.link_libc and link_in_crt) {
-            if (comp.zigc_static_lib) |zigc| {
-                try argv.append(try zigc.full_object_path.toString(arena));
-            }
-        }
-
-        // libc++ dep
-        if (comp.config.link_libcpp) {
-            try argv.append(try comp.libcxxabi_static_lib.?.full_object_path.toString(arena));
-            try argv.append(try comp.libcxx_static_lib.?.full_object_path.toString(arena));
-        }
-
-        // libunwind dep
-        if (comp.config.link_libunwind) {
-            try argv.append(try comp.libunwind_static_lib.?.full_object_path.toString(arena));
-        }
-
-        if (comp.config.any_fuzz) {
-            try argv.append(try comp.fuzzer_lib.?.full_object_path.toString(arena));
-        }
-
-        const ubsan_rt_path: ?Path = blk: {
-            if (comp.ubsan_rt_lib) |x| break :blk x.full_object_path;
-            if (comp.ubsan_rt_obj) |x| break :blk x.full_object_path;
-            break :blk null;
-        };
-        if (ubsan_rt_path) |path| {
-            try argv.append(try path.toString(arena));
-        }
-
-        if (is_exe_or_dyn_lib and !comp.skip_linker_dependencies) {
-            // MSVC compiler_rt is missing some stuff, so we build it unconditionally but
-            // and rely on weak linkage to allow MSVC compiler_rt functions to override ours.
-            if (comp.compiler_rt_obj) |obj| try argv.append(try obj.full_object_path.toString(arena));
-            if (comp.compiler_rt_lib) |lib| try argv.append(try lib.full_object_path.toString(arena));
-        }
-
-        try argv.ensureUnusedCapacity(comp.windows_libs.count());
-        for (comp.windows_libs.keys()) |key| {
-            const lib_basename = try allocPrint(arena, "{s}.lib", .{key});
-            if (comp.crt_files.get(lib_basename)) |crt_file| {
-                argv.appendAssumeCapacity(try crt_file.full_object_path.toString(arena));
-                continue;
-            }
-            if (try findLib(arena, lib_basename, coff.lib_directories)) |full_path| {
-                argv.appendAssumeCapacity(full_path);
-                continue;
-            }
-            if (target.abi.isGnu()) {
-                const fallback_name = try allocPrint(arena, "lib{s}.dll.a", .{key});
-                if (try findLib(arena, fallback_name, coff.lib_directories)) |full_path| {
-                    argv.appendAssumeCapacity(full_path);
-                    continue;
-                }
-            }
-            if (target.abi == .msvc or target.abi == .itanium) {
-                argv.appendAssumeCapacity(lib_basename);
-                continue;
-            }
-
-            log.err("DLL import library for -l{s} not found", .{key});
-            return error.DllImportLibraryNotFound;
-        }
-
-        try link.spawnLld(comp, arena, argv.items);
-    }
-
-    if (!coff.base.disable_lld_caching) {
-        // Update the file with the digest. If it fails we can continue; it only
-        // means that the next invocation will have an unnecessary cache miss.
-        Cache.writeSmallFile(directory.handle, id_symlink_basename, &digest) catch |err| {
-            log.warn("failed to save linking hash digest file: {s}", .{@errorName(err)});
-        };
-        // Again failure here only means an unnecessary cache miss.
-        man.writeManifest() catch |err| {
-            log.warn("failed to write cache manifest when linking: {s}", .{@errorName(err)});
-        };
-        // We hang on to this lock so that the output file path can be used without
-        // other processes clobbering it.
-        coff.base.lock = man.toOwnedLock();
-    }
-}
-
-fn findLib(arena: Allocator, name: []const u8, lib_directories: []const Directory) !?[]const u8 {
-    for (lib_directories) |lib_directory| {
-        lib_directory.handle.access(name, .{}) catch |err| switch (err) {
-            error.FileNotFound => continue,
-            else => |e| return e,
-        };
-        return try lib_directory.join(arena, &.{name});
-    }
-    return null;
-}
-
-pub fn flushZcu(
+pub fn flush(
     coff: *Coff,
     arena: Allocator,
     tid: Zcu.PerThread.Id,
@@ -2211,17 +1602,22 @@ pub fn flushZcu(
     const comp = coff.base.comp;
     const diags = &comp.link_diags;
 
+    switch (coff.base.comp.config.output_mode) {
+        .Exe, .Obj => {},
+        .Lib => return diags.fail("writing lib files not yet implemented for COFF", .{}),
+    }
+
     const sub_prog_node = prog_node.start("COFF Flush", 0);
     defer sub_prog_node.end();
 
-    return flushZcuInner(coff, arena, tid) catch |err| switch (err) {
+    return flushInner(coff, arena, tid) catch |err| switch (err) {
         error.OutOfMemory => return error.OutOfMemory,
         error.LinkFailure => return error.LinkFailure,
         else => |e| return diags.fail("COFF flush failed: {s}", .{@errorName(e)}),
     };
 }
 
-fn flushZcuInner(coff: *Coff, arena: Allocator, tid: Zcu.PerThread.Id) !void {
+fn flushInner(coff: *Coff, arena: Allocator, tid: Zcu.PerThread.Id) !void {
     _ = arena;
 
     const comp = coff.base.comp;
src/link/Dwarf.zig
@@ -4391,7 +4391,7 @@ fn refAbbrevCode(dwarf: *Dwarf, abbrev_code: AbbrevCode) UpdateError!@typeInfo(A
     return @intFromEnum(abbrev_code);
 }
 
-pub fn flushZcu(dwarf: *Dwarf, pt: Zcu.PerThread) FlushError!void {
+pub fn flush(dwarf: *Dwarf, pt: Zcu.PerThread) FlushError!void {
     const zcu = pt.zcu;
     const ip = &zcu.intern_pool;
 
src/link/Elf.zig
@@ -4,7 +4,6 @@ base: link.File,
 zig_object: ?*ZigObject,
 rpath_table: std.StringArrayHashMapUnmanaged(void),
 image_base: u64,
-emit_relocs: bool,
 z_nodelete: bool,
 z_notext: bool,
 z_defs: bool,
@@ -16,18 +15,7 @@ z_relro: bool,
 z_common_page_size: ?u64,
 /// TODO make this non optional and resolve the default in open()
 z_max_page_size: ?u64,
-hash_style: HashStyle,
-compress_debug_sections: CompressDebugSections,
-symbol_wrap_set: std.StringArrayHashMapUnmanaged(void),
-sort_section: ?SortSection,
 soname: ?[]const u8,
-bind_global_refs_locally: bool,
-linker_script: ?[]const u8,
-version_script: ?[]const u8,
-allow_undefined_version: bool,
-enable_new_dtags: ?bool,
-print_icf_sections: bool,
-print_map: bool,
 entry_name: ?[]const u8,
 
 ptr_width: PtrWidth,
@@ -201,9 +189,6 @@ const minimum_atom_size = 64;
 pub const min_text_capacity = padToIdeal(minimum_atom_size);
 
 pub const PtrWidth = enum { p32, p64 };
-pub const HashStyle = enum { sysv, gnu, both };
-pub const CompressDebugSections = enum { none, zlib, zstd };
-pub const SortSection = enum { name, alignment };
 
 pub fn createEmpty(
     arena: Allocator,
@@ -214,7 +199,6 @@ pub fn createEmpty(
     const target = comp.root_mod.resolved_target.result;
     assert(target.ofmt == .elf);
 
-    const use_lld = build_options.have_llvm and comp.config.use_lld;
     const use_llvm = comp.config.use_llvm;
     const opt_zcu = comp.zcu;
     const output_mode = comp.config.output_mode;
@@ -265,12 +249,10 @@ pub fn createEmpty(
     const is_dyn_lib = output_mode == .Lib and link_mode == .dynamic;
     const default_sym_version: elf.Versym = if (is_dyn_lib or comp.config.rdynamic) .GLOBAL else .LOCAL;
 
-    // If using LLD to link, this code should produce an object file so that it
-    // can be passed to LLD.
     // If using LLVM to generate the object file for the zig compilation unit,
     // we need a place to put the object file so that it can be subsequently
     // handled.
-    const zcu_object_sub_path = if (!use_lld and !use_llvm)
+    const zcu_object_sub_path = if (!use_llvm)
         null
     else
         try std.fmt.allocPrint(arena, "{s}.o", .{emit.sub_path});
@@ -292,7 +274,6 @@ pub fn createEmpty(
             .stack_size = options.stack_size orelse 16777216,
             .allow_shlib_undefined = options.allow_shlib_undefined orelse !is_native_os,
             .file = null,
-            .disable_lld_caching = options.disable_lld_caching,
             .build_id = options.build_id,
         },
         .zig_object = null,
@@ -317,7 +298,6 @@ pub fn createEmpty(
             };
         },
 
-        .emit_relocs = options.emit_relocs,
         .z_nodelete = options.z_nodelete,
         .z_notext = options.z_notext,
         .z_defs = options.z_defs,
@@ -327,27 +307,11 @@ pub fn createEmpty(
         .z_relro = options.z_relro,
         .z_common_page_size = options.z_common_page_size,
         .z_max_page_size = options.z_max_page_size,
-        .hash_style = options.hash_style,
-        .compress_debug_sections = options.compress_debug_sections,
-        .symbol_wrap_set = options.symbol_wrap_set,
-        .sort_section = options.sort_section,
         .soname = options.soname,
-        .bind_global_refs_locally = options.bind_global_refs_locally,
-        .linker_script = options.linker_script,
-        .version_script = options.version_script,
-        .allow_undefined_version = options.allow_undefined_version,
-        .enable_new_dtags = options.enable_new_dtags,
-        .print_icf_sections = options.print_icf_sections,
-        .print_map = options.print_map,
         .dump_argv_list = .empty,
     };
     errdefer self.base.destroy();
 
-    if (use_lld and (use_llvm or !comp.config.have_zcu)) {
-        // LLVM emits the object file (if any); LLD links it into the final product.
-        return self;
-    }
-
     // --verbose-link
     if (comp.verbose_link) try dumpArgvInit(self, arena);
 
@@ -355,13 +319,11 @@ pub fn createEmpty(
     const is_obj_or_ar = is_obj or (output_mode == .Lib and link_mode == .static);
 
     // What path should this ELF linker code output to?
-    // If using LLD to link, this code should produce an object file so that it
-    // can be passed to LLD.
-    const sub_path = if (use_lld) zcu_object_sub_path.? else emit.sub_path;
+    const sub_path = emit.sub_path;
     self.base.file = try emit.root_dir.handle.createFile(sub_path, .{
         .truncate = true,
         .read = true,
-        .mode = link.File.determineMode(use_lld, output_mode, link_mode),
+        .mode = link.File.determineMode(output_mode, link_mode),
     });
 
     const gpa = comp.gpa;
@@ -785,20 +747,6 @@ pub fn loadInput(self: *Elf, input: link.Input) !void {
 }
 
 pub fn flush(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
-    const comp = self.base.comp;
-    const use_lld = build_options.have_llvm and comp.config.use_lld;
-    const diags = &comp.link_diags;
-    if (use_lld) {
-        return self.linkWithLLD(arena, tid, prog_node) catch |err| switch (err) {
-            error.OutOfMemory => return error.OutOfMemory,
-            error.LinkFailure => return error.LinkFailure,
-            else => |e| return diags.fail("failed to link with LLD: {s}", .{@errorName(e)}),
-        };
-    }
-    try self.flushZcu(arena, tid, prog_node);
-}
-
-pub fn flushZcu(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
     const tracy = trace(@src());
     defer tracy.end();
 
@@ -810,14 +758,14 @@ pub fn flushZcu(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node:
     const sub_prog_node = prog_node.start("ELF Flush", 0);
     defer sub_prog_node.end();
 
-    return flushZcuInner(self, arena, tid) catch |err| switch (err) {
+    return flushInner(self, arena, tid) catch |err| switch (err) {
         error.OutOfMemory => return error.OutOfMemory,
         error.LinkFailure => return error.LinkFailure,
         else => |e| return diags.fail("ELF flush failed: {s}", .{@errorName(e)}),
     };
 }
 
-fn flushZcuInner(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id) !void {
+fn flushInner(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id) !void {
     const comp = self.base.comp;
     const gpa = comp.gpa;
     const diags = &comp.link_diags;
@@ -1492,643 +1440,6 @@ pub fn initOutputSection(self: *Elf, args: struct {
     return out_shndx;
 }
 
-fn linkWithLLD(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) !void {
-    dev.check(.lld_linker);
-
-    const tracy = trace(@src());
-    defer tracy.end();
-
-    const comp = self.base.comp;
-    const gpa = comp.gpa;
-    const diags = &comp.link_diags;
-
-    const directory = self.base.emit.root_dir; // Just an alias to make it shorter to type.
-    const full_out_path = try directory.join(arena, &[_][]const u8{self.base.emit.sub_path});
-
-    // If there is no Zig code to compile, then we should skip flushing the output file because it
-    // will not be part of the linker line anyway.
-    const module_obj_path: ?[]const u8 = if (comp.zcu) |zcu| blk: {
-        if (zcu.llvm_object == null) {
-            try self.flushZcu(arena, tid, prog_node);
-        } else {
-            // `Compilation.flush` has already made LLVM emit this object file for us.
-        }
-
-        if (fs.path.dirname(full_out_path)) |dirname| {
-            break :blk try fs.path.join(arena, &.{ dirname, self.base.zcu_object_sub_path.? });
-        } else {
-            break :blk self.base.zcu_object_sub_path.?;
-        }
-    } else null;
-
-    const sub_prog_node = prog_node.start("LLD Link", 0);
-    defer sub_prog_node.end();
-
-    const output_mode = comp.config.output_mode;
-    const is_obj = output_mode == .Obj;
-    const is_lib = output_mode == .Lib;
-    const link_mode = comp.config.link_mode;
-    const is_dyn_lib = link_mode == .dynamic and is_lib;
-    const is_exe_or_dyn_lib = is_dyn_lib or output_mode == .Exe;
-    const have_dynamic_linker = link_mode == .dynamic and is_exe_or_dyn_lib;
-    const target = self.getTarget();
-    const compiler_rt_path: ?Path = blk: {
-        if (comp.compiler_rt_lib) |x| break :blk x.full_object_path;
-        if (comp.compiler_rt_obj) |x| break :blk x.full_object_path;
-        break :blk null;
-    };
-    const ubsan_rt_path: ?Path = blk: {
-        if (comp.ubsan_rt_lib) |x| break :blk x.full_object_path;
-        if (comp.ubsan_rt_obj) |x| break :blk x.full_object_path;
-        break :blk null;
-    };
-
-    // Here we want to determine whether we can save time by not invoking LLD when the
-    // output is unchanged. None of the linker options or the object files that are being
-    // linked are in the hash that namespaces the directory we are outputting to. Therefore,
-    // we must hash those now, and the resulting digest will form the "id" of the linking
-    // job we are about to perform.
-    // After a successful link, we store the id in the metadata of a symlink named "lld.id" in
-    // the artifact directory. So, now, we check if this symlink exists, and if it matches
-    // our digest. If so, we can skip linking. Otherwise, we proceed with invoking LLD.
-    const id_symlink_basename = "lld.id";
-
-    var man: std.Build.Cache.Manifest = undefined;
-    defer if (!self.base.disable_lld_caching) man.deinit();
-
-    var digest: [std.Build.Cache.hex_digest_len]u8 = undefined;
-
-    if (!self.base.disable_lld_caching) {
-        man = comp.cache_parent.obtain();
-
-        // We are about to obtain this lock, so here we give other processes a chance first.
-        self.base.releaseLock();
-
-        comptime assert(Compilation.link_hash_implementation_version == 14);
-
-        try man.addOptionalFile(self.linker_script);
-        try man.addOptionalFile(self.version_script);
-        man.hash.add(self.allow_undefined_version);
-        man.hash.addOptional(self.enable_new_dtags);
-        try link.hashInputs(&man, comp.link_inputs);
-        for (comp.c_object_table.keys()) |key| {
-            _ = try man.addFilePath(key.status.success.object_path, null);
-        }
-        try man.addOptionalFile(module_obj_path);
-        try man.addOptionalFilePath(compiler_rt_path);
-        try man.addOptionalFilePath(ubsan_rt_path);
-        try man.addOptionalFilePath(if (comp.tsan_lib) |l| l.full_object_path else null);
-        try man.addOptionalFilePath(if (comp.fuzzer_lib) |l| l.full_object_path else null);
-
-        // We can skip hashing libc and libc++ components that we are in charge of building from Zig
-        // installation sources because they are always a product of the compiler version + target information.
-        man.hash.addOptionalBytes(self.entry_name);
-        man.hash.add(self.image_base);
-        man.hash.add(self.base.gc_sections);
-        man.hash.addOptional(self.sort_section);
-        man.hash.add(comp.link_eh_frame_hdr);
-        man.hash.add(self.emit_relocs);
-        man.hash.add(comp.config.rdynamic);
-        man.hash.addListOfBytes(self.rpath_table.keys());
-        if (output_mode == .Exe) {
-            man.hash.add(self.base.stack_size);
-        }
-        man.hash.add(self.base.build_id);
-        man.hash.addListOfBytes(self.symbol_wrap_set.keys());
-        man.hash.add(comp.skip_linker_dependencies);
-        man.hash.add(self.z_nodelete);
-        man.hash.add(self.z_notext);
-        man.hash.add(self.z_defs);
-        man.hash.add(self.z_origin);
-        man.hash.add(self.z_nocopyreloc);
-        man.hash.add(self.z_now);
-        man.hash.add(self.z_relro);
-        man.hash.add(self.z_common_page_size orelse 0);
-        man.hash.add(self.z_max_page_size orelse 0);
-        man.hash.add(self.hash_style);
-        // strip does not need to go into the linker hash because it is part of the hash namespace
-        if (comp.config.link_libc) {
-            man.hash.add(comp.libc_installation != null);
-            if (comp.libc_installation) |libc_installation| {
-                man.hash.addBytes(libc_installation.crt_dir.?);
-            }
-        }
-        if (have_dynamic_linker) {
-            man.hash.addOptionalBytes(target.dynamic_linker.get());
-        }
-        man.hash.addOptionalBytes(self.soname);
-        man.hash.addOptional(comp.version);
-        man.hash.addListOfBytes(comp.force_undefined_symbols.keys());
-        man.hash.add(self.base.allow_shlib_undefined);
-        man.hash.add(self.bind_global_refs_locally);
-        man.hash.add(self.compress_debug_sections);
-        man.hash.add(comp.config.any_sanitize_thread);
-        man.hash.add(comp.config.any_fuzz);
-        man.hash.addOptionalBytes(comp.sysroot);
-
-        // We don't actually care whether it's a cache hit or miss; we just need the digest and the lock.
-        _ = try man.hit();
-        digest = man.final();
-
-        var prev_digest_buf: [digest.len]u8 = undefined;
-        const prev_digest: []u8 = std.Build.Cache.readSmallFile(
-            directory.handle,
-            id_symlink_basename,
-            &prev_digest_buf,
-        ) catch |err| blk: {
-            log.debug("ELF LLD new_digest={s} error: {s}", .{ std.fmt.fmtSliceHexLower(&digest), @errorName(err) });
-            // Handle this as a cache miss.
-            break :blk prev_digest_buf[0..0];
-        };
-        if (mem.eql(u8, prev_digest, &digest)) {
-            log.debug("ELF LLD digest={s} match - skipping invocation", .{std.fmt.fmtSliceHexLower(&digest)});
-            // Hot diggity dog! The output binary is already there.
-            self.base.lock = man.toOwnedLock();
-            return;
-        }
-        log.debug("ELF LLD prev_digest={s} new_digest={s}", .{ std.fmt.fmtSliceHexLower(prev_digest), std.fmt.fmtSliceHexLower(&digest) });
-
-        // We are about to change the output file to be different, so we invalidate the build hash now.
-        directory.handle.deleteFile(id_symlink_basename) catch |err| switch (err) {
-            error.FileNotFound => {},
-            else => |e| return e,
-        };
-    }
-
-    // Due to a deficiency in LLD, we need to special-case BPF to a simple file
-    // copy when generating relocatables. Normally, we would expect `lld -r` to work.
-    // However, because LLD wants to resolve BPF relocations which it shouldn't, it fails
-    // before even generating the relocatable.
-    //
-    // For m68k, we go through this path because LLD doesn't support it yet, but LLVM can
-    // produce usable object files.
-    if (output_mode == .Obj and
-        (comp.config.lto != .none or
-            target.cpu.arch.isBpf() or
-            target.cpu.arch == .lanai or
-            target.cpu.arch == .m68k or
-            target.cpu.arch.isSPARC() or
-            target.cpu.arch == .ve or
-            target.cpu.arch == .xcore))
-    {
-        // In this case we must do a simple file copy
-        // here. TODO: think carefully about how we can avoid this redundant operation when doing
-        // build-obj. See also the corresponding TODO in linkAsArchive.
-        const the_object_path = blk: {
-            if (link.firstObjectInput(comp.link_inputs)) |obj| break :blk obj.path;
-
-            if (comp.c_object_table.count() != 0)
-                break :blk comp.c_object_table.keys()[0].status.success.object_path;
-
-            if (module_obj_path) |p|
-                break :blk Path.initCwd(p);
-
-            // TODO I think this is unreachable. Audit this situation when solving the above TODO
-            // regarding eliding redundant object -> object transformations.
-            return error.NoObjectsToLink;
-        };
-        try std.fs.Dir.copyFile(
-            the_object_path.root_dir.handle,
-            the_object_path.sub_path,
-            directory.handle,
-            self.base.emit.sub_path,
-            .{},
-        );
-    } else {
-        // Create an LLD command line and invoke it.
-        var argv = std.ArrayList([]const u8).init(gpa);
-        defer argv.deinit();
-        // 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.
-        const linker_command = "ld.lld";
-        try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, linker_command });
-        if (is_obj) {
-            try argv.append("-r");
-        }
-
-        try argv.append("--error-limit=0");
-
-        if (comp.sysroot) |sysroot| {
-            try argv.append(try std.fmt.allocPrint(arena, "--sysroot={s}", .{sysroot}));
-        }
-
-        if (target_util.llvmMachineAbi(target)) |mabi| {
-            try argv.appendSlice(&.{
-                "-mllvm",
-                try std.fmt.allocPrint(arena, "-target-abi={s}", .{mabi}),
-            });
-        }
-
-        try argv.appendSlice(&.{
-            "-mllvm",
-            try std.fmt.allocPrint(arena, "-float-abi={s}", .{if (target.abi.float() == .hard) "hard" else "soft"}),
-        });
-
-        if (comp.config.lto != .none) {
-            switch (comp.root_mod.optimize_mode) {
-                .Debug => {},
-                .ReleaseSmall => try argv.append("--lto-O2"),
-                .ReleaseFast, .ReleaseSafe => try argv.append("--lto-O3"),
-            }
-        }
-        switch (comp.root_mod.optimize_mode) {
-            .Debug => {},
-            .ReleaseSmall => try argv.append("-O2"),
-            .ReleaseFast, .ReleaseSafe => try argv.append("-O3"),
-        }
-
-        if (self.entry_name) |name| {
-            try argv.appendSlice(&.{ "--entry", name });
-        }
-
-        for (comp.force_undefined_symbols.keys()) |sym| {
-            try argv.append("-u");
-            try argv.append(sym);
-        }
-
-        switch (self.hash_style) {
-            .gnu => try argv.append("--hash-style=gnu"),
-            .sysv => try argv.append("--hash-style=sysv"),
-            .both => {}, // this is the default
-        }
-
-        if (output_mode == .Exe) {
-            try argv.appendSlice(&.{
-                "-z",
-                try std.fmt.allocPrint(arena, "stack-size={d}", .{self.base.stack_size}),
-            });
-        }
-
-        switch (self.base.build_id) {
-            .none => try argv.append("--build-id=none"),
-            .fast, .uuid, .sha1, .md5 => try argv.append(try std.fmt.allocPrint(arena, "--build-id={s}", .{
-                @tagName(self.base.build_id),
-            })),
-            .hexstring => |hs| try argv.append(try std.fmt.allocPrint(arena, "--build-id=0x{s}", .{
-                std.fmt.fmtSliceHexLower(hs.toSlice()),
-            })),
-        }
-
-        try argv.append(try std.fmt.allocPrint(arena, "--image-base={d}", .{self.image_base}));
-
-        if (self.linker_script) |linker_script| {
-            try argv.append("-T");
-            try argv.append(linker_script);
-        }
-
-        if (self.sort_section) |how| {
-            const arg = try std.fmt.allocPrint(arena, "--sort-section={s}", .{@tagName(how)});
-            try argv.append(arg);
-        }
-
-        if (self.base.gc_sections) {
-            try argv.append("--gc-sections");
-        }
-
-        if (self.base.print_gc_sections) {
-            try argv.append("--print-gc-sections");
-        }
-
-        if (self.print_icf_sections) {
-            try argv.append("--print-icf-sections");
-        }
-
-        if (self.print_map) {
-            try argv.append("--print-map");
-        }
-
-        if (comp.link_eh_frame_hdr) {
-            try argv.append("--eh-frame-hdr");
-        }
-
-        if (self.emit_relocs) {
-            try argv.append("--emit-relocs");
-        }
-
-        if (comp.config.rdynamic) {
-            try argv.append("--export-dynamic");
-        }
-
-        if (comp.config.debug_format == .strip) {
-            try argv.append("-s");
-        }
-
-        if (self.z_nodelete) {
-            try argv.append("-z");
-            try argv.append("nodelete");
-        }
-        if (self.z_notext) {
-            try argv.append("-z");
-            try argv.append("notext");
-        }
-        if (self.z_defs) {
-            try argv.append("-z");
-            try argv.append("defs");
-        }
-        if (self.z_origin) {
-            try argv.append("-z");
-            try argv.append("origin");
-        }
-        if (self.z_nocopyreloc) {
-            try argv.append("-z");
-            try argv.append("nocopyreloc");
-        }
-        if (self.z_now) {
-            // LLD defaults to -zlazy
-            try argv.append("-znow");
-        }
-        if (!self.z_relro) {
-            // LLD defaults to -zrelro
-            try argv.append("-znorelro");
-        }
-        if (self.z_common_page_size) |size| {
-            try argv.append("-z");
-            try argv.append(try std.fmt.allocPrint(arena, "common-page-size={d}", .{size}));
-        }
-        if (self.z_max_page_size) |size| {
-            try argv.append("-z");
-            try argv.append(try std.fmt.allocPrint(arena, "max-page-size={d}", .{size}));
-        }
-
-        if (getLDMOption(target)) |ldm| {
-            try argv.append("-m");
-            try argv.append(ldm);
-        }
-
-        if (link_mode == .static) {
-            if (target.cpu.arch.isArm()) {
-                try argv.append("-Bstatic");
-            } else {
-                try argv.append("-static");
-            }
-        } else if (switch (target.os.tag) {
-            else => is_dyn_lib,
-            .haiku => is_exe_or_dyn_lib,
-        }) {
-            try argv.append("-shared");
-        }
-
-        if (comp.config.pie and output_mode == .Exe) {
-            try argv.append("-pie");
-        }
-
-        if (is_exe_or_dyn_lib and target.os.tag == .netbsd) {
-            // Add options to produce shared objects with only 2 PT_LOAD segments.
-            // NetBSD expects 2 PT_LOAD segments in a shared object, otherwise
-            // ld.elf_so fails loading dynamic libraries with "not found" error.
-            // See https://github.com/ziglang/zig/issues/9109 .
-            try argv.append("--no-rosegment");
-            try argv.append("-znorelro");
-        }
-
-        try argv.append("-o");
-        try argv.append(full_out_path);
-
-        // csu prelude
-        const csu = try comp.getCrtPaths(arena);
-        if (csu.crt0) |p| try argv.append(try p.toString(arena));
-        if (csu.crti) |p| try argv.append(try p.toString(arena));
-        if (csu.crtbegin) |p| try argv.append(try p.toString(arena));
-
-        for (self.rpath_table.keys()) |rpath| {
-            try argv.appendSlice(&.{ "-rpath", rpath });
-        }
-
-        for (self.symbol_wrap_set.keys()) |symbol_name| {
-            try argv.appendSlice(&.{ "-wrap", symbol_name });
-        }
-
-        if (comp.config.link_libc) {
-            if (comp.libc_installation) |libc_installation| {
-                try argv.append("-L");
-                try argv.append(libc_installation.crt_dir.?);
-            }
-        }
-
-        if (have_dynamic_linker and
-            (comp.config.link_libc or comp.root_mod.resolved_target.is_explicit_dynamic_linker))
-        {
-            if (target.dynamic_linker.get()) |dynamic_linker| {
-                try argv.append("-dynamic-linker");
-                try argv.append(dynamic_linker);
-            }
-        }
-
-        if (is_dyn_lib) {
-            if (self.soname) |soname| {
-                try argv.append("-soname");
-                try argv.append(soname);
-            }
-            if (self.version_script) |version_script| {
-                try argv.append("-version-script");
-                try argv.append(version_script);
-            }
-            if (self.allow_undefined_version) {
-                try argv.append("--undefined-version");
-            } else {
-                try argv.append("--no-undefined-version");
-            }
-            if (self.enable_new_dtags) |enable_new_dtags| {
-                if (enable_new_dtags) {
-                    try argv.append("--enable-new-dtags");
-                } else {
-                    try argv.append("--disable-new-dtags");
-                }
-            }
-        }
-
-        // Positional arguments to the linker such as object files.
-        var whole_archive = false;
-
-        for (self.base.comp.link_inputs) |link_input| switch (link_input) {
-            .res => unreachable, // Windows-only
-            .dso => continue,
-            .object, .archive => |obj| {
-                if (obj.must_link and !whole_archive) {
-                    try argv.append("-whole-archive");
-                    whole_archive = true;
-                } else if (!obj.must_link and whole_archive) {
-                    try argv.append("-no-whole-archive");
-                    whole_archive = false;
-                }
-                try argv.append(try obj.path.toString(arena));
-            },
-            .dso_exact => |dso_exact| {
-                assert(dso_exact.name[0] == ':');
-                try argv.appendSlice(&.{ "-l", dso_exact.name });
-            },
-        };
-
-        if (whole_archive) {
-            try argv.append("-no-whole-archive");
-            whole_archive = false;
-        }
-
-        for (comp.c_object_table.keys()) |key| {
-            try argv.append(try key.status.success.object_path.toString(arena));
-        }
-
-        if (module_obj_path) |p| {
-            try argv.append(p);
-        }
-
-        if (comp.tsan_lib) |lib| {
-            assert(comp.config.any_sanitize_thread);
-            try argv.append(try lib.full_object_path.toString(arena));
-        }
-
-        if (comp.fuzzer_lib) |lib| {
-            assert(comp.config.any_fuzz);
-            try argv.append(try lib.full_object_path.toString(arena));
-        }
-
-        if (ubsan_rt_path) |p| {
-            try argv.append(try p.toString(arena));
-        }
-
-        // Shared libraries.
-        if (is_exe_or_dyn_lib) {
-            // Worst-case, we need an --as-needed argument for every lib, as well
-            // as one before and one after.
-            try argv.ensureUnusedCapacity(2 * self.base.comp.link_inputs.len + 2);
-            argv.appendAssumeCapacity("--as-needed");
-            var as_needed = true;
-
-            for (self.base.comp.link_inputs) |link_input| switch (link_input) {
-                .res => unreachable, // Windows-only
-                .object, .archive, .dso_exact => continue,
-                .dso => |dso| {
-                    const lib_as_needed = !dso.needed;
-                    switch ((@as(u2, @intFromBool(lib_as_needed)) << 1) | @intFromBool(as_needed)) {
-                        0b00, 0b11 => {},
-                        0b01 => {
-                            argv.appendAssumeCapacity("--no-as-needed");
-                            as_needed = false;
-                        },
-                        0b10 => {
-                            argv.appendAssumeCapacity("--as-needed");
-                            as_needed = true;
-                        },
-                    }
-
-                    // By this time, we depend on these libs being dynamically linked
-                    // libraries and not static libraries (the check for that needs to be earlier),
-                    // but they could be full paths to .so files, in which case we
-                    // want to avoid prepending "-l".
-                    argv.appendAssumeCapacity(try dso.path.toString(arena));
-                },
-            };
-
-            if (!as_needed) {
-                argv.appendAssumeCapacity("--as-needed");
-                as_needed = true;
-            }
-
-            // libc++ dep
-            if (comp.config.link_libcpp) {
-                try argv.append(try comp.libcxxabi_static_lib.?.full_object_path.toString(arena));
-                try argv.append(try comp.libcxx_static_lib.?.full_object_path.toString(arena));
-            }
-
-            // libunwind dep
-            if (comp.config.link_libunwind) {
-                try argv.append(try comp.libunwind_static_lib.?.full_object_path.toString(arena));
-            }
-
-            // libc dep
-            diags.flags.missing_libc = false;
-            if (comp.config.link_libc) {
-                if (comp.libc_installation != null) {
-                    const needs_grouping = link_mode == .static;
-                    if (needs_grouping) try argv.append("--start-group");
-                    try argv.appendSlice(target_util.libcFullLinkFlags(target));
-                    if (needs_grouping) try argv.append("--end-group");
-                } else if (target.isGnuLibC()) {
-                    for (glibc.libs) |lib| {
-                        if (lib.removed_in) |rem_in| {
-                            if (target.os.versionRange().gnuLibCVersion().?.order(rem_in) != .lt) continue;
-                        }
-
-                        const lib_path = try std.fmt.allocPrint(arena, "{}{c}lib{s}.so.{d}", .{
-                            comp.glibc_so_files.?.dir_path, fs.path.sep, lib.name, lib.sover,
-                        });
-                        try argv.append(lib_path);
-                    }
-                    try argv.append(try comp.crtFileAsString(arena, "libc_nonshared.a"));
-                } else if (target.isMuslLibC()) {
-                    try argv.append(try comp.crtFileAsString(arena, switch (link_mode) {
-                        .static => "libc.a",
-                        .dynamic => "libc.so",
-                    }));
-                } else if (target.isFreeBSDLibC()) {
-                    for (freebsd.libs) |lib| {
-                        const lib_path = try std.fmt.allocPrint(arena, "{}{c}lib{s}.so.{d}", .{
-                            comp.freebsd_so_files.?.dir_path, fs.path.sep, lib.name, lib.sover,
-                        });
-                        try argv.append(lib_path);
-                    }
-                } else if (target.isNetBSDLibC()) {
-                    for (netbsd.libs) |lib| {
-                        const lib_path = try std.fmt.allocPrint(arena, "{}{c}lib{s}.so.{d}", .{
-                            comp.netbsd_so_files.?.dir_path, fs.path.sep, lib.name, lib.sover,
-                        });
-                        try argv.append(lib_path);
-                    }
-                } else {
-                    diags.flags.missing_libc = true;
-                }
-
-                if (comp.zigc_static_lib) |zigc| {
-                    try argv.append(try zigc.full_object_path.toString(arena));
-                }
-            }
-        }
-
-        // compiler-rt. Since compiler_rt exports symbols like `memset`, it needs
-        // to be after the shared libraries, so they are picked up from the shared
-        // libraries, not libcompiler_rt.
-        if (compiler_rt_path) |p| {
-            try argv.append(try p.toString(arena));
-        }
-
-        // crt postlude
-        if (csu.crtend) |p| try argv.append(try p.toString(arena));
-        if (csu.crtn) |p| try argv.append(try p.toString(arena));
-
-        if (self.base.allow_shlib_undefined) {
-            try argv.append("--allow-shlib-undefined");
-        }
-
-        switch (self.compress_debug_sections) {
-            .none => {},
-            .zlib => try argv.append("--compress-debug-sections=zlib"),
-            .zstd => try argv.append("--compress-debug-sections=zstd"),
-        }
-
-        if (self.bind_global_refs_locally) {
-            try argv.append("-Bsymbolic");
-        }
-
-        try link.spawnLld(comp, arena, argv.items);
-    }
-
-    if (!self.base.disable_lld_caching) {
-        // Update the file with the digest. If it fails we can continue; it only
-        // means that the next invocation will have an unnecessary cache miss.
-        std.Build.Cache.writeSmallFile(directory.handle, id_symlink_basename, &digest) catch |err| {
-            log.warn("failed to save linking hash digest file: {s}", .{@errorName(err)});
-        };
-        // Again failure here only means an unnecessary cache miss.
-        man.writeManifest() catch |err| {
-            log.warn("failed to write cache manifest when linking: {s}", .{@errorName(err)});
-        };
-        // We hang on to this lock so that the output file path can be used without
-        // other processes clobbering it.
-        self.base.lock = man.toOwnedLock();
-    }
-}
-
 pub fn writeShdrTable(self: *Elf) !void {
     const gpa = self.base.comp.gpa;
     const target_endian = self.getTarget().cpu.arch.endian();
@@ -4121,85 +3432,6 @@ fn shdrTo32(shdr: elf.Elf64_Shdr) elf.Elf32_Shdr {
     };
 }
 
-fn getLDMOption(target: std.Target) ?[]const u8 {
-    // This should only return emulations understood by LLD's parseEmulation().
-    return switch (target.cpu.arch) {
-        .aarch64 => switch (target.os.tag) {
-            .linux => "aarch64linux",
-            else => "aarch64elf",
-        },
-        .aarch64_be => switch (target.os.tag) {
-            .linux => "aarch64linuxb",
-            else => "aarch64elfb",
-        },
-        .amdgcn => "elf64_amdgpu",
-        .arm, .thumb => switch (target.os.tag) {
-            .linux => "armelf_linux_eabi",
-            else => "armelf",
-        },
-        .armeb, .thumbeb => switch (target.os.tag) {
-            .linux => "armelfb_linux_eabi",
-            else => "armelfb",
-        },
-        .hexagon => "hexagonelf",
-        .loongarch32 => "elf32loongarch",
-        .loongarch64 => "elf64loongarch",
-        .mips => switch (target.os.tag) {
-            .freebsd => "elf32btsmip_fbsd",
-            else => "elf32btsmip",
-        },
-        .mipsel => switch (target.os.tag) {
-            .freebsd => "elf32ltsmip_fbsd",
-            else => "elf32ltsmip",
-        },
-        .mips64 => switch (target.os.tag) {
-            .freebsd => switch (target.abi) {
-                .gnuabin32, .muslabin32 => "elf32btsmipn32_fbsd",
-                else => "elf64btsmip_fbsd",
-            },
-            else => switch (target.abi) {
-                .gnuabin32, .muslabin32 => "elf32btsmipn32",
-                else => "elf64btsmip",
-            },
-        },
-        .mips64el => switch (target.os.tag) {
-            .freebsd => switch (target.abi) {
-                .gnuabin32, .muslabin32 => "elf32ltsmipn32_fbsd",
-                else => "elf64ltsmip_fbsd",
-            },
-            else => switch (target.abi) {
-                .gnuabin32, .muslabin32 => "elf32ltsmipn32",
-                else => "elf64ltsmip",
-            },
-        },
-        .msp430 => "msp430elf",
-        .powerpc => switch (target.os.tag) {
-            .freebsd => "elf32ppc_fbsd",
-            .linux => "elf32ppclinux",
-            else => "elf32ppc",
-        },
-        .powerpcle => switch (target.os.tag) {
-            .linux => "elf32lppclinux",
-            else => "elf32lppc",
-        },
-        .powerpc64 => "elf64ppc",
-        .powerpc64le => "elf64lppc",
-        .riscv32 => "elf32lriscv",
-        .riscv64 => "elf64lriscv",
-        .s390x => "elf64_s390",
-        .sparc64 => "elf64_sparc",
-        .x86 => switch (target.os.tag) {
-            .freebsd => "elf_i386_fbsd",
-            else => "elf_i386",
-        },
-        .x86_64 => switch (target.abi) {
-            .gnux32, .muslx32 => "elf32_x86_64",
-            else => "elf_x86_64",
-        },
-        else => null,
-    };
-}
-
 pub fn padToIdeal(actual_size: anytype) @TypeOf(actual_size) {
     return actual_size +| (actual_size / ideal_factor);
 }
@@ -5284,10 +4516,7 @@ const codegen = @import("../codegen.zig");
 const dev = @import("../dev.zig");
 const eh_frame = @import("Elf/eh_frame.zig");
 const gc = @import("Elf/gc.zig");
-const glibc = @import("../libs/glibc.zig");
 const musl = @import("../libs/musl.zig");
-const freebsd = @import("../libs/freebsd.zig");
-const netbsd = @import("../libs/netbsd.zig");
 const link = @import("../link.zig");
 const relocatable = @import("Elf/relocatable.zig");
 const relocation = @import("Elf/relocation.zig");
src/link/Goff.zig
@@ -46,7 +46,6 @@ pub fn createEmpty(
             .stack_size = options.stack_size orelse 0,
             .allow_shlib_undefined = options.allow_shlib_undefined orelse false,
             .file = null,
-            .disable_lld_caching = options.disable_lld_caching,
             .build_id = options.build_id,
         },
     };
@@ -105,10 +104,6 @@ pub fn updateExports(
 }
 
 pub fn flush(self: *Goff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
-    return self.flushZcu(arena, tid, prog_node);
-}
-
-pub fn flushZcu(self: *Goff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
     _ = self;
     _ = arena;
     _ = tid;
src/link/Lld.zig
@@ -0,0 +1,2148 @@
+base: link.File,
+disable_caching: bool,
+ofmt: union(enum) {
+    elf: Elf,
+    coff: Coff,
+    wasm: Wasm,
+},
+
+const Coff = struct {
+    image_base: u64,
+    entry: link.File.OpenOptions.Entry,
+    pdb_out_path: ?[]const u8,
+    repro: bool,
+    tsaware: bool,
+    nxcompat: bool,
+    dynamicbase: bool,
+    /// TODO this and minor_subsystem_version should be combined into one property and left as
+    /// default or populated together. They should not be separate fields.
+    major_subsystem_version: u16,
+    minor_subsystem_version: u16,
+    lib_directories: []const Cache.Directory,
+    module_definition_file: ?[]const u8,
+    subsystem: ?std.Target.SubSystem,
+    /// These flags are populated by `codegen.llvm.updateExports` to allow us to guess the subsystem.
+    lld_export_flags: struct {
+        c_main: bool,
+        winmain: bool,
+        wwinmain: bool,
+        winmain_crt_startup: bool,
+        wwinmain_crt_startup: bool,
+        dllmain_crt_startup: bool,
+    },
+    fn init(comp: *Compilation, options: link.File.OpenOptions) !Coff {
+        const target = comp.root_mod.resolved_target.result;
+        const output_mode = comp.config.output_mode;
+        return .{
+            .image_base = options.image_base orelse switch (output_mode) {
+                .Exe => switch (target.cpu.arch) {
+                    .aarch64, .x86_64 => 0x140000000,
+                    .thumb, .x86 => 0x400000,
+                    else => unreachable,
+                },
+                .Lib => switch (target.cpu.arch) {
+                    .aarch64, .x86_64 => 0x180000000,
+                    .thumb, .x86 => 0x10000000,
+                    else => unreachable,
+                },
+                .Obj => 0,
+            },
+            .entry = options.entry,
+            .pdb_out_path = options.pdb_out_path,
+            .repro = options.repro,
+            .tsaware = options.tsaware,
+            .nxcompat = options.nxcompat,
+            .dynamicbase = options.dynamicbase,
+            .major_subsystem_version = options.major_subsystem_version orelse 6,
+            .minor_subsystem_version = options.minor_subsystem_version orelse 0,
+            .lib_directories = options.lib_directories,
+            .module_definition_file = options.module_definition_file,
+            // Subsystem depends on the set of public symbol names from linked objects.
+            // See LinkerDriver::inferSubsystem from the LLD project for the flow chart.
+            .subsystem = options.subsystem,
+            // These flags are initially all `false`; the LLVM backend populates them when it learns about exports.
+            .lld_export_flags = .{
+                .c_main = false,
+                .winmain = false,
+                .wwinmain = false,
+                .winmain_crt_startup = false,
+                .wwinmain_crt_startup = false,
+                .dllmain_crt_startup = false,
+            },
+        };
+    }
+};
+pub const Elf = struct {
+    entry_name: ?[]const u8,
+    hash_style: HashStyle,
+    image_base: u64,
+    linker_script: ?[]const u8,
+    version_script: ?[]const u8,
+    sort_section: ?SortSection,
+    print_icf_sections: bool,
+    print_map: bool,
+    emit_relocs: bool,
+    z_nodelete: bool,
+    z_notext: bool,
+    z_defs: bool,
+    z_origin: bool,
+    z_nocopyreloc: bool,
+    z_now: bool,
+    z_relro: bool,
+    z_common_page_size: ?u64,
+    z_max_page_size: ?u64,
+    rpath_list: []const []const u8,
+    symbol_wrap_set: []const []const u8,
+    soname: ?[]const u8,
+    allow_undefined_version: bool,
+    enable_new_dtags: ?bool,
+    compress_debug_sections: CompressDebugSections,
+    bind_global_refs_locally: bool,
+    pub const HashStyle = enum { sysv, gnu, both };
+    pub const SortSection = enum { name, alignment };
+    pub const CompressDebugSections = enum { none, zlib, zstd };
+
+    fn init(comp: *Compilation, options: link.File.OpenOptions) !Elf {
+        const PtrWidth = enum { p32, p64 };
+        const target = comp.root_mod.resolved_target.result;
+        const output_mode = comp.config.output_mode;
+        const is_dyn_lib = output_mode == .Lib and comp.config.link_mode == .dynamic;
+        const ptr_width: PtrWidth = switch (target.ptrBitWidth()) {
+            0...32 => .p32,
+            33...64 => .p64,
+            else => return error.UnsupportedElfArchitecture,
+        };
+        const default_entry_name: []const u8 = switch (target.cpu.arch) {
+            .mips, .mipsel, .mips64, .mips64el => "__start",
+            else => "_start",
+        };
+        return .{
+            .entry_name = switch (options.entry) {
+                .disabled => null,
+                .default => if (output_mode != .Exe) null else default_entry_name,
+                .enabled => default_entry_name,
+                .named => |name| name,
+            },
+            .hash_style = options.hash_style,
+            .image_base = b: {
+                if (is_dyn_lib) break :b 0;
+                if (output_mode == .Exe and comp.config.pie) break :b 0;
+                break :b options.image_base orelse switch (ptr_width) {
+                    .p32 => 0x10000,
+                    .p64 => 0x1000000,
+                };
+            },
+            .linker_script = options.linker_script,
+            .version_script = options.version_script,
+            .sort_section = options.sort_section,
+            .print_icf_sections = options.print_icf_sections,
+            .print_map = options.print_map,
+            .emit_relocs = options.emit_relocs,
+            .z_nodelete = options.z_nodelete,
+            .z_notext = options.z_notext,
+            .z_defs = options.z_defs,
+            .z_origin = options.z_origin,
+            .z_nocopyreloc = options.z_nocopyreloc,
+            .z_now = options.z_now,
+            .z_relro = options.z_relro,
+            .z_common_page_size = options.z_common_page_size,
+            .z_max_page_size = options.z_max_page_size,
+            .rpath_list = options.rpath_list,
+            .symbol_wrap_set = options.symbol_wrap_set.keys(),
+            .soname = options.soname,
+            .allow_undefined_version = options.allow_undefined_version,
+            .enable_new_dtags = options.enable_new_dtags,
+            .compress_debug_sections = options.compress_debug_sections,
+            .bind_global_refs_locally = options.bind_global_refs_locally,
+        };
+    }
+};
+const Wasm = struct {
+    /// Symbol name of the entry function to export
+    entry_name: ?[]const u8,
+    /// When true, will import the function table from the host environment.
+    import_table: bool,
+    /// When true, will export the function table to the host environment.
+    export_table: bool,
+    /// When defined, sets the initial memory size of the memory.
+    initial_memory: ?u64,
+    /// When defined, sets the maximum memory size of the memory.
+    max_memory: ?u64,
+    /// When defined, sets the start of the data section.
+    global_base: ?u64,
+    /// Set of *global* symbol names to export to the host environment.
+    export_symbol_names: []const []const u8,
+    /// When true, will allow undefined symbols
+    import_symbols: bool,
+    fn init(comp: *Compilation, options: link.File.OpenOptions) !Wasm {
+        const default_entry_name: []const u8 = switch (comp.config.wasi_exec_model) {
+            .reactor => "_initialize",
+            .command => "_start",
+        };
+        return .{
+            .entry_name = switch (options.entry) {
+                .disabled => null,
+                .default => if (comp.config.output_mode != .Exe) null else default_entry_name,
+                .enabled => default_entry_name,
+                .named => |name| name,
+            },
+            .import_table = options.import_table,
+            .export_table = options.export_table,
+            .initial_memory = options.initial_memory,
+            .max_memory = options.max_memory,
+            .global_base = options.global_base,
+            .export_symbol_names = options.export_symbol_names,
+            .import_symbols = options.import_symbols,
+        };
+    }
+};
+
+pub fn createEmpty(
+    arena: Allocator,
+    comp: *Compilation,
+    emit: Cache.Path,
+    options: link.File.OpenOptions,
+) !*Lld {
+    const target = comp.root_mod.resolved_target.result;
+    const output_mode = comp.config.output_mode;
+    const optimize_mode = comp.root_mod.optimize_mode;
+    const is_native_os = comp.root_mod.resolved_target.is_native_os;
+
+    const obj_file_ext: []const u8 = switch (target.ofmt) {
+        .coff => "obj",
+        .elf, .wasm => "o",
+        else => unreachable,
+    };
+    const gc_sections: bool = options.gc_sections orelse switch (target.ofmt) {
+        .coff => optimize_mode != .Debug,
+        .elf => optimize_mode != .Debug and output_mode != .Obj,
+        .wasm => output_mode != .Obj,
+        else => unreachable,
+    };
+    const stack_size: u64 = options.stack_size orelse default: {
+        if (target.ofmt == .wasm and target.os.tag == .freestanding)
+            break :default 1 * 1024 * 1024; // 1 MiB
+        break :default 16 * 1024 * 1024; // 16 MiB
+    };
+
+    const lld = try arena.create(Lld);
+    lld.* = .{
+        .base = .{
+            .tag = .lld,
+            .comp = comp,
+            .emit = emit,
+            .zcu_object_sub_path = try allocPrint(arena, "{s}.{s}", .{ emit.sub_path, obj_file_ext }),
+            .gc_sections = gc_sections,
+            .print_gc_sections = options.print_gc_sections,
+            .stack_size = stack_size,
+            .allow_shlib_undefined = options.allow_shlib_undefined orelse !is_native_os,
+            .file = null,
+            .build_id = options.build_id,
+        },
+        .disable_caching = options.disable_lld_caching,
+        .ofmt = switch (target.ofmt) {
+            .coff => .{ .coff = try .init(comp, options) },
+            .elf => .{ .elf = try .init(comp, options) },
+            .wasm => .{ .wasm = try .init(comp, options) },
+            else => unreachable,
+        },
+    };
+    return lld;
+}
+pub fn deinit(lld: *Lld) void {
+    _ = lld;
+}
+pub fn flush(
+    lld: *Lld,
+    arena: Allocator,
+    tid: Zcu.PerThread.Id,
+    prog_node: std.Progress.Node,
+) link.File.FlushError!void {
+    dev.check(.lld_linker);
+    _ = tid;
+
+    const tracy = trace(@src());
+    defer tracy.end();
+
+    const sub_prog_node = prog_node.start("LLD Link", 0);
+    defer sub_prog_node.end();
+
+    const comp = lld.base.comp;
+    const result = if (comp.config.output_mode == .Lib and comp.config.link_mode == .static) r: {
+        break :r linkAsArchive(lld, arena);
+    } else switch (lld.ofmt) {
+        .coff => coffLink(lld, arena),
+        .elf => elfLink(lld, arena),
+        .wasm => wasmLink(lld, arena),
+    };
+    result catch |err| switch (err) {
+        error.OutOfMemory, error.LinkFailure => |e| return e,
+        else => |e| return lld.base.comp.link_diags.fail("failed to link with LLD: {s}", .{@errorName(e)}),
+    };
+}
+
+fn linkAsArchive(lld: *Lld, arena: Allocator) !void {
+    const base = &lld.base;
+    const comp = base.comp;
+    const directory = base.emit.root_dir; // Just an alias to make it shorter to type.
+    const full_out_path = try directory.join(arena, &[_][]const u8{base.emit.sub_path});
+    const full_out_path_z = try arena.dupeZ(u8, full_out_path);
+    const opt_zcu = comp.zcu;
+
+    // If there is no Zig code to compile, then we should skip flushing the output file
+    // because it will not be part of the linker line anyway.
+    const zcu_obj_path: ?[]const u8 = if (opt_zcu != null) blk: {
+        const dirname = fs.path.dirname(full_out_path_z) orelse ".";
+        break :blk try fs.path.join(arena, &.{ dirname, base.zcu_object_sub_path.? });
+    } else null;
+
+    log.debug("zcu_obj_path={s}", .{if (zcu_obj_path) |s| s else "(null)"});
+
+    const compiler_rt_path: ?Cache.Path = if (comp.compiler_rt_strat == .obj)
+        comp.compiler_rt_obj.?.full_object_path
+    else
+        null;
+
+    const ubsan_rt_path: ?Cache.Path = if (comp.ubsan_rt_strat == .obj)
+        comp.ubsan_rt_obj.?.full_object_path
+    else
+        null;
+
+    // This function follows the same pattern as link.Elf.linkWithLLD so if you want some
+    // insight as to what's going on here you can read that function body which is more
+    // well-commented.
+
+    const id_symlink_basename = "llvm-ar.id";
+
+    var man: Cache.Manifest = undefined;
+    defer if (!lld.disable_caching) man.deinit();
+
+    const link_inputs = comp.link_inputs;
+
+    var digest: [Cache.hex_digest_len]u8 = undefined;
+
+    if (!lld.disable_caching) {
+        man = comp.cache_parent.obtain();
+
+        // We are about to obtain this lock, so here we give other processes a chance first.
+        base.releaseLock();
+
+        try link.hashInputs(&man, link_inputs);
+
+        for (comp.c_object_table.keys()) |key| {
+            _ = try man.addFilePath(key.status.success.object_path, null);
+        }
+        for (comp.win32_resource_table.keys()) |key| {
+            _ = try man.addFile(key.status.success.res_path, null);
+        }
+        try man.addOptionalFile(zcu_obj_path);
+        try man.addOptionalFilePath(compiler_rt_path);
+        try man.addOptionalFilePath(ubsan_rt_path);
+
+        // We don't actually care whether it's a cache hit or miss; we just need the digest and the lock.
+        _ = try man.hit();
+        digest = man.final();
+
+        var prev_digest_buf: [digest.len]u8 = undefined;
+        const prev_digest: []u8 = Cache.readSmallFile(
+            directory.handle,
+            id_symlink_basename,
+            &prev_digest_buf,
+        ) catch |err| b: {
+            log.debug("archive new_digest={s} readFile error: {s}", .{ std.fmt.fmtSliceHexLower(&digest), @errorName(err) });
+            break :b prev_digest_buf[0..0];
+        };
+        if (mem.eql(u8, prev_digest, &digest)) {
+            log.debug("archive digest={s} match - skipping invocation", .{std.fmt.fmtSliceHexLower(&digest)});
+            base.lock = man.toOwnedLock();
+            return;
+        }
+
+        // We are about to change the output file to be different, so we invalidate the build hash now.
+        directory.handle.deleteFile(id_symlink_basename) catch |err| switch (err) {
+            error.FileNotFound => {},
+            else => |e| return e,
+        };
+    }
+
+    var object_files: std.ArrayListUnmanaged([*:0]const u8) = .empty;
+
+    try object_files.ensureUnusedCapacity(arena, link_inputs.len);
+    for (link_inputs) |input| {
+        object_files.appendAssumeCapacity(try input.path().?.toStringZ(arena));
+    }
+
+    try object_files.ensureUnusedCapacity(arena, comp.c_object_table.count() +
+        comp.win32_resource_table.count() + 2);
+
+    for (comp.c_object_table.keys()) |key| {
+        object_files.appendAssumeCapacity(try key.status.success.object_path.toStringZ(arena));
+    }
+    for (comp.win32_resource_table.keys()) |key| {
+        object_files.appendAssumeCapacity(try arena.dupeZ(u8, key.status.success.res_path));
+    }
+    if (zcu_obj_path) |p| object_files.appendAssumeCapacity(try arena.dupeZ(u8, p));
+    if (compiler_rt_path) |p| object_files.appendAssumeCapacity(try p.toStringZ(arena));
+    if (ubsan_rt_path) |p| object_files.appendAssumeCapacity(try p.toStringZ(arena));
+
+    if (comp.verbose_link) {
+        std.debug.print("ar rcs {s}", .{full_out_path_z});
+        for (object_files.items) |arg| {
+            std.debug.print(" {s}", .{arg});
+        }
+        std.debug.print("\n", .{});
+    }
+
+    const llvm_bindings = @import("../codegen/llvm/bindings.zig");
+    const llvm = @import("../codegen/llvm.zig");
+    const target = comp.root_mod.resolved_target.result;
+    llvm.initializeLLVMTarget(target.cpu.arch);
+    const bad = llvm_bindings.WriteArchive(
+        full_out_path_z,
+        object_files.items.ptr,
+        object_files.items.len,
+        switch (target.os.tag) {
+            .aix => .AIXBIG,
+            .windows => .COFF,
+            else => if (target.os.tag.isDarwin()) .DARWIN else .GNU,
+        },
+    );
+    if (bad) return error.UnableToWriteArchive;
+
+    if (!lld.disable_caching) {
+        Cache.writeSmallFile(directory.handle, id_symlink_basename, &digest) catch |err| {
+            log.warn("failed to save archive hash digest file: {s}", .{@errorName(err)});
+        };
+
+        if (man.have_exclusive_lock) {
+            man.writeManifest() catch |err| {
+                log.warn("failed to write cache manifest when archiving: {s}", .{@errorName(err)});
+            };
+        }
+
+        base.lock = man.toOwnedLock();
+    }
+}
+
+fn coffLink(lld: *Lld, arena: Allocator) !void {
+    const comp = lld.base.comp;
+    const gpa = comp.gpa;
+    const base = &lld.base;
+    const coff = &lld.ofmt.coff;
+
+    const directory = base.emit.root_dir; // Just an alias to make it shorter to type.
+    const full_out_path = try directory.join(arena, &[_][]const u8{base.emit.sub_path});
+
+    // If there is no Zig code to compile, then we should skip flushing the output file because it
+    // will not be part of the linker line anyway.
+    const module_obj_path: ?[]const u8 = if (comp.zcu != null) p: {
+        if (fs.path.dirname(full_out_path)) |dirname| {
+            break :p try fs.path.join(arena, &.{ dirname, base.zcu_object_sub_path.? });
+        } else {
+            break :p base.zcu_object_sub_path.?;
+        }
+    } else null;
+
+    const is_lib = comp.config.output_mode == .Lib;
+    const is_dyn_lib = comp.config.link_mode == .dynamic and is_lib;
+    const is_exe_or_dyn_lib = is_dyn_lib or comp.config.output_mode == .Exe;
+    const link_in_crt = comp.config.link_libc and is_exe_or_dyn_lib;
+    const target = comp.root_mod.resolved_target.result;
+    const optimize_mode = comp.root_mod.optimize_mode;
+    const entry_name: ?[]const u8 = switch (coff.entry) {
+        // This logic isn't quite right for disabled or enabled. No point in fixing it
+        // when the goal is to eliminate dependency on LLD anyway.
+        // https://github.com/ziglang/zig/issues/17751
+        .disabled, .default, .enabled => null,
+        .named => |name| name,
+    };
+
+    // See link/Elf.zig for comments on how this mechanism works.
+    const id_symlink_basename = "lld.id";
+
+    var man: Cache.Manifest = undefined;
+    defer if (!lld.disable_caching) man.deinit();
+
+    var digest: [Cache.hex_digest_len]u8 = undefined;
+
+    if (!lld.disable_caching) {
+        man = comp.cache_parent.obtain();
+        base.releaseLock();
+
+        comptime assert(Compilation.link_hash_implementation_version == 14);
+
+        try link.hashInputs(&man, comp.link_inputs);
+        for (comp.c_object_table.keys()) |key| {
+            _ = try man.addFilePath(key.status.success.object_path, null);
+        }
+        for (comp.win32_resource_table.keys()) |key| {
+            _ = try man.addFile(key.status.success.res_path, null);
+        }
+        try man.addOptionalFile(module_obj_path);
+        man.hash.addOptionalBytes(entry_name);
+        man.hash.add(base.stack_size);
+        man.hash.add(coff.image_base);
+        man.hash.add(base.build_id);
+        {
+            // TODO remove this, libraries must instead be resolved by the frontend.
+            for (coff.lib_directories) |lib_directory| man.hash.addOptionalBytes(lib_directory.path);
+        }
+        man.hash.add(comp.skip_linker_dependencies);
+        if (comp.config.link_libc) {
+            man.hash.add(comp.libc_installation != null);
+            if (comp.libc_installation) |libc_installation| {
+                man.hash.addBytes(libc_installation.crt_dir.?);
+                if (target.abi == .msvc or target.abi == .itanium) {
+                    man.hash.addBytes(libc_installation.msvc_lib_dir.?);
+                    man.hash.addBytes(libc_installation.kernel32_lib_dir.?);
+                }
+            }
+        }
+        man.hash.addListOfBytes(comp.windows_libs.keys());
+        man.hash.addListOfBytes(comp.force_undefined_symbols.keys());
+        man.hash.addOptional(coff.subsystem);
+        man.hash.add(comp.config.is_test);
+        man.hash.add(coff.tsaware);
+        man.hash.add(coff.nxcompat);
+        man.hash.add(coff.dynamicbase);
+        man.hash.add(base.allow_shlib_undefined);
+        // strip does not need to go into the linker hash because it is part of the hash namespace
+        man.hash.add(coff.major_subsystem_version);
+        man.hash.add(coff.minor_subsystem_version);
+        man.hash.add(coff.repro);
+        man.hash.addOptional(comp.version);
+        try man.addOptionalFile(coff.module_definition_file);
+
+        // We don't actually care whether it's a cache hit or miss; we just need the digest and the lock.
+        _ = try man.hit();
+        digest = man.final();
+        var prev_digest_buf: [digest.len]u8 = undefined;
+        const prev_digest: []u8 = Cache.readSmallFile(
+            directory.handle,
+            id_symlink_basename,
+            &prev_digest_buf,
+        ) catch |err| blk: {
+            log.debug("COFF LLD new_digest={s} error: {s}", .{ std.fmt.fmtSliceHexLower(&digest), @errorName(err) });
+            // Handle this as a cache miss.
+            break :blk prev_digest_buf[0..0];
+        };
+        if (mem.eql(u8, prev_digest, &digest)) {
+            log.debug("COFF LLD digest={s} match - skipping invocation", .{std.fmt.fmtSliceHexLower(&digest)});
+            // Hot diggity dog! The output binary is already there.
+            base.lock = man.toOwnedLock();
+            return;
+        }
+        log.debug("COFF LLD prev_digest={s} new_digest={s}", .{ std.fmt.fmtSliceHexLower(prev_digest), std.fmt.fmtSliceHexLower(&digest) });
+
+        // We are about to change the output file to be different, so we invalidate the build hash now.
+        directory.handle.deleteFile(id_symlink_basename) catch |err| switch (err) {
+            error.FileNotFound => {},
+            else => |e| return e,
+        };
+    }
+
+    if (comp.config.output_mode == .Obj) {
+        // LLD's COFF driver does not support the equivalent of `-r` so we do a simple file copy
+        // here. TODO: think carefully about how we can avoid this redundant operation when doing
+        // build-obj. See also the corresponding TODO in linkAsArchive.
+        const the_object_path = blk: {
+            if (link.firstObjectInput(comp.link_inputs)) |obj| break :blk obj.path;
+
+            if (comp.c_object_table.count() != 0)
+                break :blk comp.c_object_table.keys()[0].status.success.object_path;
+
+            if (module_obj_path) |p|
+                break :blk Cache.Path.initCwd(p);
+
+            // TODO I think this is unreachable. Audit this situation when solving the above TODO
+            // regarding eliding redundant object -> object transformations.
+            return error.NoObjectsToLink;
+        };
+        try std.fs.Dir.copyFile(
+            the_object_path.root_dir.handle,
+            the_object_path.sub_path,
+            directory.handle,
+            base.emit.sub_path,
+            .{},
+        );
+    } else {
+        // Create an LLD command line and invoke it.
+        var argv = std.ArrayList([]const u8).init(gpa);
+        defer argv.deinit();
+        // 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.
+        const linker_command = "lld-link";
+        try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, linker_command });
+
+        if (target.isMinGW()) {
+            try argv.append("-lldmingw");
+        }
+
+        try argv.append("-ERRORLIMIT:0");
+        try argv.append("-NOLOGO");
+        if (comp.config.debug_format != .strip) {
+            try argv.append("-DEBUG");
+
+            const out_ext = std.fs.path.extension(full_out_path);
+            const out_pdb = coff.pdb_out_path orelse try allocPrint(arena, "{s}.pdb", .{
+                full_out_path[0 .. full_out_path.len - out_ext.len],
+            });
+            const out_pdb_basename = std.fs.path.basename(out_pdb);
+
+            try argv.append(try allocPrint(arena, "-PDB:{s}", .{out_pdb}));
+            try argv.append(try allocPrint(arena, "-PDBALTPATH:{s}", .{out_pdb_basename}));
+        }
+        if (comp.version) |version| {
+            try argv.append(try allocPrint(arena, "-VERSION:{}.{}", .{ version.major, version.minor }));
+        }
+
+        if (target_util.llvmMachineAbi(target)) |mabi| {
+            try argv.append(try allocPrint(arena, "-MLLVM:-target-abi={s}", .{mabi}));
+        }
+
+        try argv.append(try allocPrint(arena, "-MLLVM:-float-abi={s}", .{if (target.abi.float() == .hard) "hard" else "soft"}));
+
+        if (comp.config.lto != .none) {
+            switch (optimize_mode) {
+                .Debug => {},
+                .ReleaseSmall => try argv.append("-OPT:lldlto=2"),
+                .ReleaseFast, .ReleaseSafe => try argv.append("-OPT:lldlto=3"),
+            }
+        }
+        if (comp.config.output_mode == .Exe) {
+            try argv.append(try allocPrint(arena, "-STACK:{d}", .{base.stack_size}));
+        }
+        try argv.append(try allocPrint(arena, "-BASE:{d}", .{coff.image_base}));
+
+        switch (base.build_id) {
+            .none => try argv.append("-BUILD-ID:NO"),
+            .fast => try argv.append("-BUILD-ID"),
+            .uuid, .sha1, .md5, .hexstring => {},
+        }
+
+        if (target.cpu.arch == .x86) {
+            try argv.append("-MACHINE:X86");
+        } else if (target.cpu.arch == .x86_64) {
+            try argv.append("-MACHINE:X64");
+        } else if (target.cpu.arch == .thumb) {
+            try argv.append("-MACHINE:ARM");
+        } else if (target.cpu.arch == .aarch64) {
+            try argv.append("-MACHINE:ARM64");
+        }
+
+        for (comp.force_undefined_symbols.keys()) |symbol| {
+            try argv.append(try allocPrint(arena, "-INCLUDE:{s}", .{symbol}));
+        }
+
+        if (is_dyn_lib) {
+            try argv.append("-DLL");
+        }
+
+        if (entry_name) |name| {
+            try argv.append(try allocPrint(arena, "-ENTRY:{s}", .{name}));
+        }
+
+        if (coff.repro) {
+            try argv.append("-BREPRO");
+        }
+
+        if (coff.tsaware) {
+            try argv.append("-tsaware");
+        }
+        if (coff.nxcompat) {
+            try argv.append("-nxcompat");
+        }
+        if (!coff.dynamicbase) {
+            try argv.append("-dynamicbase:NO");
+        }
+        if (base.allow_shlib_undefined) {
+            try argv.append("-FORCE:UNRESOLVED");
+        }
+
+        try argv.append(try allocPrint(arena, "-OUT:{s}", .{full_out_path}));
+
+        if (comp.implib_emit) |emit| {
+            const implib_out_path = try emit.root_dir.join(arena, &[_][]const u8{emit.sub_path});
+            try argv.append(try allocPrint(arena, "-IMPLIB:{s}", .{implib_out_path}));
+        }
+
+        if (comp.config.link_libc) {
+            if (comp.libc_installation) |libc_installation| {
+                try argv.append(try allocPrint(arena, "-LIBPATH:{s}", .{libc_installation.crt_dir.?}));
+
+                if (target.abi == .msvc or target.abi == .itanium) {
+                    try argv.append(try allocPrint(arena, "-LIBPATH:{s}", .{libc_installation.msvc_lib_dir.?}));
+                    try argv.append(try allocPrint(arena, "-LIBPATH:{s}", .{libc_installation.kernel32_lib_dir.?}));
+                }
+            }
+        }
+
+        for (coff.lib_directories) |lib_directory| {
+            try argv.append(try allocPrint(arena, "-LIBPATH:{s}", .{lib_directory.path orelse "."}));
+        }
+
+        try argv.ensureUnusedCapacity(comp.link_inputs.len);
+        for (comp.link_inputs) |link_input| switch (link_input) {
+            .dso_exact => unreachable, // not applicable to PE/COFF
+            inline .dso, .res => |x| {
+                argv.appendAssumeCapacity(try x.path.toString(arena));
+            },
+            .object, .archive => |obj| {
+                if (obj.must_link) {
+                    argv.appendAssumeCapacity(try allocPrint(arena, "-WHOLEARCHIVE:{}", .{@as(Cache.Path, obj.path)}));
+                } else {
+                    argv.appendAssumeCapacity(try obj.path.toString(arena));
+                }
+            },
+        };
+
+        for (comp.c_object_table.keys()) |key| {
+            try argv.append(try key.status.success.object_path.toString(arena));
+        }
+
+        for (comp.win32_resource_table.keys()) |key| {
+            try argv.append(key.status.success.res_path);
+        }
+
+        if (module_obj_path) |p| {
+            try argv.append(p);
+        }
+
+        if (coff.module_definition_file) |def| {
+            try argv.append(try allocPrint(arena, "-DEF:{s}", .{def}));
+        }
+
+        const resolved_subsystem: ?std.Target.SubSystem = blk: {
+            if (coff.subsystem) |explicit| break :blk explicit;
+            switch (target.os.tag) {
+                .windows => {
+                    if (comp.zcu != null) {
+                        if (coff.lld_export_flags.dllmain_crt_startup or is_dyn_lib)
+                            break :blk null;
+                        if (coff.lld_export_flags.c_main or comp.config.is_test or
+                            coff.lld_export_flags.winmain_crt_startup or
+                            coff.lld_export_flags.wwinmain_crt_startup)
+                        {
+                            break :blk .Console;
+                        }
+                        if (coff.lld_export_flags.winmain or coff.lld_export_flags.wwinmain)
+                            break :blk .Windows;
+                    }
+                },
+                .uefi => break :blk .EfiApplication,
+                else => {},
+            }
+            break :blk null;
+        };
+
+        const Mode = enum { uefi, win32 };
+        const mode: Mode = mode: {
+            if (resolved_subsystem) |subsystem| {
+                const subsystem_suffix = try allocPrint(arena, ",{d}.{d}", .{
+                    coff.major_subsystem_version, coff.minor_subsystem_version,
+                });
+
+                switch (subsystem) {
+                    .Console => {
+                        try argv.append(try allocPrint(arena, "-SUBSYSTEM:console{s}", .{
+                            subsystem_suffix,
+                        }));
+                        break :mode .win32;
+                    },
+                    .EfiApplication => {
+                        try argv.append(try allocPrint(arena, "-SUBSYSTEM:efi_application{s}", .{
+                            subsystem_suffix,
+                        }));
+                        break :mode .uefi;
+                    },
+                    .EfiBootServiceDriver => {
+                        try argv.append(try allocPrint(arena, "-SUBSYSTEM:efi_boot_service_driver{s}", .{
+                            subsystem_suffix,
+                        }));
+                        break :mode .uefi;
+                    },
+                    .EfiRom => {
+                        try argv.append(try allocPrint(arena, "-SUBSYSTEM:efi_rom{s}", .{
+                            subsystem_suffix,
+                        }));
+                        break :mode .uefi;
+                    },
+                    .EfiRuntimeDriver => {
+                        try argv.append(try allocPrint(arena, "-SUBSYSTEM:efi_runtime_driver{s}", .{
+                            subsystem_suffix,
+                        }));
+                        break :mode .uefi;
+                    },
+                    .Native => {
+                        try argv.append(try allocPrint(arena, "-SUBSYSTEM:native{s}", .{
+                            subsystem_suffix,
+                        }));
+                        break :mode .win32;
+                    },
+                    .Posix => {
+                        try argv.append(try allocPrint(arena, "-SUBSYSTEM:posix{s}", .{
+                            subsystem_suffix,
+                        }));
+                        break :mode .win32;
+                    },
+                    .Windows => {
+                        try argv.append(try allocPrint(arena, "-SUBSYSTEM:windows{s}", .{
+                            subsystem_suffix,
+                        }));
+                        break :mode .win32;
+                    },
+                }
+            } else if (target.os.tag == .uefi) {
+                break :mode .uefi;
+            } else {
+                break :mode .win32;
+            }
+        };
+
+        switch (mode) {
+            .uefi => try argv.appendSlice(&[_][]const u8{
+                "-BASE:0",
+                "-ENTRY:EfiMain",
+                "-OPT:REF",
+                "-SAFESEH:NO",
+                "-MERGE:.rdata=.data",
+                "-NODEFAULTLIB",
+                "-SECTION:.xdata,D",
+            }),
+            .win32 => {
+                if (link_in_crt) {
+                    if (target.abi.isGnu()) {
+                        if (target.cpu.arch == .x86) {
+                            try argv.append("-ALTERNATENAME:__image_base__=___ImageBase");
+                        } else {
+                            try argv.append("-ALTERNATENAME:__image_base__=__ImageBase");
+                        }
+
+                        if (is_dyn_lib) {
+                            try argv.append(try comp.crtFileAsString(arena, "dllcrt2.obj"));
+                            if (target.cpu.arch == .x86) {
+                                try argv.append("-ALTERNATENAME:__DllMainCRTStartup@12=_DllMainCRTStartup@12");
+                            } else {
+                                try argv.append("-ALTERNATENAME:_DllMainCRTStartup=DllMainCRTStartup");
+                            }
+                        } else {
+                            try argv.append(try comp.crtFileAsString(arena, "crt2.obj"));
+                        }
+
+                        try argv.append(try comp.crtFileAsString(arena, "libmingw32.lib"));
+                    } else {
+                        try argv.append(switch (comp.config.link_mode) {
+                            .static => "libcmt.lib",
+                            .dynamic => "msvcrt.lib",
+                        });
+
+                        const lib_str = switch (comp.config.link_mode) {
+                            .static => "lib",
+                            .dynamic => "",
+                        };
+                        try argv.append(try allocPrint(arena, "{s}vcruntime.lib", .{lib_str}));
+                        try argv.append(try allocPrint(arena, "{s}ucrt.lib", .{lib_str}));
+
+                        //Visual C++ 2015 Conformance Changes
+                        //https://msdn.microsoft.com/en-us/library/bb531344.aspx
+                        try argv.append("legacy_stdio_definitions.lib");
+
+                        // msvcrt depends on kernel32 and ntdll
+                        try argv.append("kernel32.lib");
+                        try argv.append("ntdll.lib");
+                    }
+                } else {
+                    try argv.append("-NODEFAULTLIB");
+                    if (!is_lib and entry_name == null) {
+                        if (comp.zcu != null) {
+                            if (coff.lld_export_flags.winmain_crt_startup) {
+                                try argv.append("-ENTRY:WinMainCRTStartup");
+                            } else {
+                                try argv.append("-ENTRY:wWinMainCRTStartup");
+                            }
+                        } else {
+                            try argv.append("-ENTRY:wWinMainCRTStartup");
+                        }
+                    }
+                }
+            },
+        }
+
+        if (comp.config.link_libc and link_in_crt) {
+            if (comp.zigc_static_lib) |zigc| {
+                try argv.append(try zigc.full_object_path.toString(arena));
+            }
+        }
+
+        // libc++ dep
+        if (comp.config.link_libcpp) {
+            try argv.append(try comp.libcxxabi_static_lib.?.full_object_path.toString(arena));
+            try argv.append(try comp.libcxx_static_lib.?.full_object_path.toString(arena));
+        }
+
+        // libunwind dep
+        if (comp.config.link_libunwind) {
+            try argv.append(try comp.libunwind_static_lib.?.full_object_path.toString(arena));
+        }
+
+        if (comp.config.any_fuzz) {
+            try argv.append(try comp.fuzzer_lib.?.full_object_path.toString(arena));
+        }
+
+        const ubsan_rt_path: ?Cache.Path = blk: {
+            if (comp.ubsan_rt_lib) |x| break :blk x.full_object_path;
+            if (comp.ubsan_rt_obj) |x| break :blk x.full_object_path;
+            break :blk null;
+        };
+        if (ubsan_rt_path) |path| {
+            try argv.append(try path.toString(arena));
+        }
+
+        if (is_exe_or_dyn_lib and !comp.skip_linker_dependencies) {
+            // MSVC compiler_rt is missing some stuff, so we build it unconditionally but
+            // and rely on weak linkage to allow MSVC compiler_rt functions to override ours.
+            if (comp.compiler_rt_obj) |obj| try argv.append(try obj.full_object_path.toString(arena));
+            if (comp.compiler_rt_lib) |lib| try argv.append(try lib.full_object_path.toString(arena));
+        }
+
+        try argv.ensureUnusedCapacity(comp.windows_libs.count());
+        for (comp.windows_libs.keys()) |key| {
+            const lib_basename = try allocPrint(arena, "{s}.lib", .{key});
+            if (comp.crt_files.get(lib_basename)) |crt_file| {
+                argv.appendAssumeCapacity(try crt_file.full_object_path.toString(arena));
+                continue;
+            }
+            if (try findLib(arena, lib_basename, coff.lib_directories)) |full_path| {
+                argv.appendAssumeCapacity(full_path);
+                continue;
+            }
+            if (target.abi.isGnu()) {
+                const fallback_name = try allocPrint(arena, "lib{s}.dll.a", .{key});
+                if (try findLib(arena, fallback_name, coff.lib_directories)) |full_path| {
+                    argv.appendAssumeCapacity(full_path);
+                    continue;
+                }
+            }
+            if (target.abi == .msvc or target.abi == .itanium) {
+                argv.appendAssumeCapacity(lib_basename);
+                continue;
+            }
+
+            log.err("DLL import library for -l{s} not found", .{key});
+            return error.DllImportLibraryNotFound;
+        }
+
+        try spawnLld(comp, arena, argv.items);
+    }
+
+    if (!lld.disable_caching) {
+        // Update the file with the digest. If it fails we can continue; it only
+        // means that the next invocation will have an unnecessary cache miss.
+        Cache.writeSmallFile(directory.handle, id_symlink_basename, &digest) catch |err| {
+            log.warn("failed to save linking hash digest file: {s}", .{@errorName(err)});
+        };
+        // Again failure here only means an unnecessary cache miss.
+        man.writeManifest() catch |err| {
+            log.warn("failed to write cache manifest when linking: {s}", .{@errorName(err)});
+        };
+        // We hang on to this lock so that the output file path can be used without
+        // other processes clobbering it.
+        base.lock = man.toOwnedLock();
+    }
+}
+fn findLib(arena: Allocator, name: []const u8, lib_directories: []const Cache.Directory) !?[]const u8 {
+    for (lib_directories) |lib_directory| {
+        lib_directory.handle.access(name, .{}) catch |err| switch (err) {
+            error.FileNotFound => continue,
+            else => |e| return e,
+        };
+        return try lib_directory.join(arena, &.{name});
+    }
+    return null;
+}
+
+fn elfLink(lld: *Lld, arena: Allocator) !void {
+    const comp = lld.base.comp;
+    const gpa = comp.gpa;
+    const diags = &comp.link_diags;
+    const base = &lld.base;
+    const elf = &lld.ofmt.elf;
+
+    const directory = base.emit.root_dir; // Just an alias to make it shorter to type.
+    const full_out_path = try directory.join(arena, &[_][]const u8{base.emit.sub_path});
+
+    // If there is no Zig code to compile, then we should skip flushing the output file because it
+    // will not be part of the linker line anyway.
+    const module_obj_path: ?[]const u8 = if (comp.zcu != null) p: {
+        if (fs.path.dirname(full_out_path)) |dirname| {
+            break :p try fs.path.join(arena, &.{ dirname, base.zcu_object_sub_path.? });
+        } else {
+            break :p base.zcu_object_sub_path.?;
+        }
+    } else null;
+
+    const output_mode = comp.config.output_mode;
+    const is_obj = output_mode == .Obj;
+    const is_lib = output_mode == .Lib;
+    const link_mode = comp.config.link_mode;
+    const is_dyn_lib = link_mode == .dynamic and is_lib;
+    const is_exe_or_dyn_lib = is_dyn_lib or output_mode == .Exe;
+    const have_dynamic_linker = link_mode == .dynamic and is_exe_or_dyn_lib;
+    const target = comp.root_mod.resolved_target.result;
+    const compiler_rt_path: ?Cache.Path = blk: {
+        if (comp.compiler_rt_lib) |x| break :blk x.full_object_path;
+        if (comp.compiler_rt_obj) |x| break :blk x.full_object_path;
+        break :blk null;
+    };
+    const ubsan_rt_path: ?Cache.Path = blk: {
+        if (comp.ubsan_rt_lib) |x| break :blk x.full_object_path;
+        if (comp.ubsan_rt_obj) |x| break :blk x.full_object_path;
+        break :blk null;
+    };
+
+    // Here we want to determine whether we can save time by not invoking LLD when the
+    // output is unchanged. None of the linker options or the object files that are being
+    // linked are in the hash that namespaces the directory we are outputting to. Therefore,
+    // we must hash those now, and the resulting digest will form the "id" of the linking
+    // job we are about to perform.
+    // After a successful link, we store the id in the metadata of a symlink named "lld.id" in
+    // the artifact directory. So, now, we check if this symlink exists, and if it matches
+    // our digest. If so, we can skip linking. Otherwise, we proceed with invoking LLD.
+    const id_symlink_basename = "lld.id";
+
+    var man: std.Build.Cache.Manifest = undefined;
+    defer if (!lld.disable_caching) man.deinit();
+
+    var digest: [std.Build.Cache.hex_digest_len]u8 = undefined;
+
+    if (!lld.disable_caching) {
+        man = comp.cache_parent.obtain();
+
+        // We are about to obtain this lock, so here we give other processes a chance first.
+        base.releaseLock();
+
+        comptime assert(Compilation.link_hash_implementation_version == 14);
+
+        try man.addOptionalFile(elf.linker_script);
+        try man.addOptionalFile(elf.version_script);
+        man.hash.add(elf.allow_undefined_version);
+        man.hash.addOptional(elf.enable_new_dtags);
+        try link.hashInputs(&man, comp.link_inputs);
+        for (comp.c_object_table.keys()) |key| {
+            _ = try man.addFilePath(key.status.success.object_path, null);
+        }
+        try man.addOptionalFile(module_obj_path);
+        try man.addOptionalFilePath(compiler_rt_path);
+        try man.addOptionalFilePath(ubsan_rt_path);
+        try man.addOptionalFilePath(if (comp.tsan_lib) |l| l.full_object_path else null);
+        try man.addOptionalFilePath(if (comp.fuzzer_lib) |l| l.full_object_path else null);
+
+        // We can skip hashing libc and libc++ components that we are in charge of building from Zig
+        // installation sources because they are always a product of the compiler version + target information.
+        man.hash.addOptionalBytes(elf.entry_name);
+        man.hash.add(elf.image_base);
+        man.hash.add(base.gc_sections);
+        man.hash.addOptional(elf.sort_section);
+        man.hash.add(comp.link_eh_frame_hdr);
+        man.hash.add(elf.emit_relocs);
+        man.hash.add(comp.config.rdynamic);
+        man.hash.addListOfBytes(elf.rpath_list);
+        if (output_mode == .Exe) {
+            man.hash.add(base.stack_size);
+        }
+        man.hash.add(base.build_id);
+        man.hash.addListOfBytes(elf.symbol_wrap_set);
+        man.hash.add(comp.skip_linker_dependencies);
+        man.hash.add(elf.z_nodelete);
+        man.hash.add(elf.z_notext);
+        man.hash.add(elf.z_defs);
+        man.hash.add(elf.z_origin);
+        man.hash.add(elf.z_nocopyreloc);
+        man.hash.add(elf.z_now);
+        man.hash.add(elf.z_relro);
+        man.hash.add(elf.z_common_page_size orelse 0);
+        man.hash.add(elf.z_max_page_size orelse 0);
+        man.hash.add(elf.hash_style);
+        // strip does not need to go into the linker hash because it is part of the hash namespace
+        if (comp.config.link_libc) {
+            man.hash.add(comp.libc_installation != null);
+            if (comp.libc_installation) |libc_installation| {
+                man.hash.addBytes(libc_installation.crt_dir.?);
+            }
+        }
+        if (have_dynamic_linker) {
+            man.hash.addOptionalBytes(target.dynamic_linker.get());
+        }
+        man.hash.addOptionalBytes(elf.soname);
+        man.hash.addOptional(comp.version);
+        man.hash.addListOfBytes(comp.force_undefined_symbols.keys());
+        man.hash.add(base.allow_shlib_undefined);
+        man.hash.add(elf.bind_global_refs_locally);
+        man.hash.add(elf.compress_debug_sections);
+        man.hash.add(comp.config.any_sanitize_thread);
+        man.hash.add(comp.config.any_fuzz);
+        man.hash.addOptionalBytes(comp.sysroot);
+
+        // We don't actually care whether it's a cache hit or miss; we just need the digest and the lock.
+        _ = try man.hit();
+        digest = man.final();
+
+        var prev_digest_buf: [digest.len]u8 = undefined;
+        const prev_digest: []u8 = std.Build.Cache.readSmallFile(
+            directory.handle,
+            id_symlink_basename,
+            &prev_digest_buf,
+        ) catch |err| blk: {
+            log.debug("ELF LLD new_digest={s} error: {s}", .{ std.fmt.fmtSliceHexLower(&digest), @errorName(err) });
+            // Handle this as a cache miss.
+            break :blk prev_digest_buf[0..0];
+        };
+        if (mem.eql(u8, prev_digest, &digest)) {
+            log.debug("ELF LLD digest={s} match - skipping invocation", .{std.fmt.fmtSliceHexLower(&digest)});
+            // Hot diggity dog! The output binary is already there.
+            base.lock = man.toOwnedLock();
+            return;
+        }
+        log.debug("ELF LLD prev_digest={s} new_digest={s}", .{ std.fmt.fmtSliceHexLower(prev_digest), std.fmt.fmtSliceHexLower(&digest) });
+
+        // We are about to change the output file to be different, so we invalidate the build hash now.
+        directory.handle.deleteFile(id_symlink_basename) catch |err| switch (err) {
+            error.FileNotFound => {},
+            else => |e| return e,
+        };
+    }
+
+    // Due to a deficiency in LLD, we need to special-case BPF to a simple file
+    // copy when generating relocatables. Normally, we would expect `lld -r` to work.
+    // However, because LLD wants to resolve BPF relocations which it shouldn't, it fails
+    // before even generating the relocatable.
+    //
+    // For m68k, we go through this path because LLD doesn't support it yet, but LLVM can
+    // produce usable object files.
+    if (output_mode == .Obj and
+        (comp.config.lto != .none or
+            target.cpu.arch.isBpf() or
+            target.cpu.arch == .lanai or
+            target.cpu.arch == .m68k or
+            target.cpu.arch.isSPARC() or
+            target.cpu.arch == .ve or
+            target.cpu.arch == .xcore))
+    {
+        // In this case we must do a simple file copy
+        // here. TODO: think carefully about how we can avoid this redundant operation when doing
+        // build-obj. See also the corresponding TODO in linkAsArchive.
+        const the_object_path = blk: {
+            if (link.firstObjectInput(comp.link_inputs)) |obj| break :blk obj.path;
+
+            if (comp.c_object_table.count() != 0)
+                break :blk comp.c_object_table.keys()[0].status.success.object_path;
+
+            if (module_obj_path) |p|
+                break :blk Cache.Path.initCwd(p);
+
+            // TODO I think this is unreachable. Audit this situation when solving the above TODO
+            // regarding eliding redundant object -> object transformations.
+            return error.NoObjectsToLink;
+        };
+        try std.fs.Dir.copyFile(
+            the_object_path.root_dir.handle,
+            the_object_path.sub_path,
+            directory.handle,
+            base.emit.sub_path,
+            .{},
+        );
+    } else {
+        // Create an LLD command line and invoke it.
+        var argv = std.ArrayList([]const u8).init(gpa);
+        defer argv.deinit();
+        // 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.
+        const linker_command = "ld.lld";
+        try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, linker_command });
+        if (is_obj) {
+            try argv.append("-r");
+        }
+
+        try argv.append("--error-limit=0");
+
+        if (comp.sysroot) |sysroot| {
+            try argv.append(try std.fmt.allocPrint(arena, "--sysroot={s}", .{sysroot}));
+        }
+
+        if (target_util.llvmMachineAbi(target)) |mabi| {
+            try argv.appendSlice(&.{
+                "-mllvm",
+                try std.fmt.allocPrint(arena, "-target-abi={s}", .{mabi}),
+            });
+        }
+
+        try argv.appendSlice(&.{
+            "-mllvm",
+            try std.fmt.allocPrint(arena, "-float-abi={s}", .{if (target.abi.float() == .hard) "hard" else "soft"}),
+        });
+
+        if (comp.config.lto != .none) {
+            switch (comp.root_mod.optimize_mode) {
+                .Debug => {},
+                .ReleaseSmall => try argv.append("--lto-O2"),
+                .ReleaseFast, .ReleaseSafe => try argv.append("--lto-O3"),
+            }
+        }
+        switch (comp.root_mod.optimize_mode) {
+            .Debug => {},
+            .ReleaseSmall => try argv.append("-O2"),
+            .ReleaseFast, .ReleaseSafe => try argv.append("-O3"),
+        }
+
+        if (elf.entry_name) |name| {
+            try argv.appendSlice(&.{ "--entry", name });
+        }
+
+        for (comp.force_undefined_symbols.keys()) |sym| {
+            try argv.append("-u");
+            try argv.append(sym);
+        }
+
+        switch (elf.hash_style) {
+            .gnu => try argv.append("--hash-style=gnu"),
+            .sysv => try argv.append("--hash-style=sysv"),
+            .both => {}, // this is the default
+        }
+
+        if (output_mode == .Exe) {
+            try argv.appendSlice(&.{
+                "-z",
+                try std.fmt.allocPrint(arena, "stack-size={d}", .{base.stack_size}),
+            });
+        }
+
+        switch (base.build_id) {
+            .none => try argv.append("--build-id=none"),
+            .fast, .uuid, .sha1, .md5 => try argv.append(try std.fmt.allocPrint(arena, "--build-id={s}", .{
+                @tagName(base.build_id),
+            })),
+            .hexstring => |hs| try argv.append(try std.fmt.allocPrint(arena, "--build-id=0x{s}", .{
+                std.fmt.fmtSliceHexLower(hs.toSlice()),
+            })),
+        }
+
+        try argv.append(try std.fmt.allocPrint(arena, "--image-base={d}", .{elf.image_base}));
+
+        if (elf.linker_script) |linker_script| {
+            try argv.append("-T");
+            try argv.append(linker_script);
+        }
+
+        if (elf.sort_section) |how| {
+            const arg = try std.fmt.allocPrint(arena, "--sort-section={s}", .{@tagName(how)});
+            try argv.append(arg);
+        }
+
+        if (base.gc_sections) {
+            try argv.append("--gc-sections");
+        }
+
+        if (base.print_gc_sections) {
+            try argv.append("--print-gc-sections");
+        }
+
+        if (elf.print_icf_sections) {
+            try argv.append("--print-icf-sections");
+        }
+
+        if (elf.print_map) {
+            try argv.append("--print-map");
+        }
+
+        if (comp.link_eh_frame_hdr) {
+            try argv.append("--eh-frame-hdr");
+        }
+
+        if (elf.emit_relocs) {
+            try argv.append("--emit-relocs");
+        }
+
+        if (comp.config.rdynamic) {
+            try argv.append("--export-dynamic");
+        }
+
+        if (comp.config.debug_format == .strip) {
+            try argv.append("-s");
+        }
+
+        if (elf.z_nodelete) {
+            try argv.append("-z");
+            try argv.append("nodelete");
+        }
+        if (elf.z_notext) {
+            try argv.append("-z");
+            try argv.append("notext");
+        }
+        if (elf.z_defs) {
+            try argv.append("-z");
+            try argv.append("defs");
+        }
+        if (elf.z_origin) {
+            try argv.append("-z");
+            try argv.append("origin");
+        }
+        if (elf.z_nocopyreloc) {
+            try argv.append("-z");
+            try argv.append("nocopyreloc");
+        }
+        if (elf.z_now) {
+            // LLD defaults to -zlazy
+            try argv.append("-znow");
+        }
+        if (!elf.z_relro) {
+            // LLD defaults to -zrelro
+            try argv.append("-znorelro");
+        }
+        if (elf.z_common_page_size) |size| {
+            try argv.append("-z");
+            try argv.append(try std.fmt.allocPrint(arena, "common-page-size={d}", .{size}));
+        }
+        if (elf.z_max_page_size) |size| {
+            try argv.append("-z");
+            try argv.append(try std.fmt.allocPrint(arena, "max-page-size={d}", .{size}));
+        }
+
+        if (getLDMOption(target)) |ldm| {
+            try argv.append("-m");
+            try argv.append(ldm);
+        }
+
+        if (link_mode == .static) {
+            if (target.cpu.arch.isArm()) {
+                try argv.append("-Bstatic");
+            } else {
+                try argv.append("-static");
+            }
+        } else if (switch (target.os.tag) {
+            else => is_dyn_lib,
+            .haiku => is_exe_or_dyn_lib,
+        }) {
+            try argv.append("-shared");
+        }
+
+        if (comp.config.pie and output_mode == .Exe) {
+            try argv.append("-pie");
+        }
+
+        if (is_exe_or_dyn_lib and target.os.tag == .netbsd) {
+            // Add options to produce shared objects with only 2 PT_LOAD segments.
+            // NetBSD expects 2 PT_LOAD segments in a shared object, otherwise
+            // ld.elf_so fails loading dynamic libraries with "not found" error.
+            // See https://github.com/ziglang/zig/issues/9109 .
+            try argv.append("--no-rosegment");
+            try argv.append("-znorelro");
+        }
+
+        try argv.append("-o");
+        try argv.append(full_out_path);
+
+        // csu prelude
+        const csu = try comp.getCrtPaths(arena);
+        if (csu.crt0) |p| try argv.append(try p.toString(arena));
+        if (csu.crti) |p| try argv.append(try p.toString(arena));
+        if (csu.crtbegin) |p| try argv.append(try p.toString(arena));
+
+        for (elf.rpath_list) |rpath| {
+            try argv.appendSlice(&.{ "-rpath", rpath });
+        }
+
+        for (elf.symbol_wrap_set) |symbol_name| {
+            try argv.appendSlice(&.{ "-wrap", symbol_name });
+        }
+
+        if (comp.config.link_libc) {
+            if (comp.libc_installation) |libc_installation| {
+                try argv.append("-L");
+                try argv.append(libc_installation.crt_dir.?);
+            }
+        }
+
+        if (have_dynamic_linker and
+            (comp.config.link_libc or comp.root_mod.resolved_target.is_explicit_dynamic_linker))
+        {
+            if (target.dynamic_linker.get()) |dynamic_linker| {
+                try argv.append("-dynamic-linker");
+                try argv.append(dynamic_linker);
+            }
+        }
+
+        if (is_dyn_lib) {
+            if (elf.soname) |soname| {
+                try argv.append("-soname");
+                try argv.append(soname);
+            }
+            if (elf.version_script) |version_script| {
+                try argv.append("-version-script");
+                try argv.append(version_script);
+            }
+            if (elf.allow_undefined_version) {
+                try argv.append("--undefined-version");
+            } else {
+                try argv.append("--no-undefined-version");
+            }
+            if (elf.enable_new_dtags) |enable_new_dtags| {
+                if (enable_new_dtags) {
+                    try argv.append("--enable-new-dtags");
+                } else {
+                    try argv.append("--disable-new-dtags");
+                }
+            }
+        }
+
+        // Positional arguments to the linker such as object files.
+        var whole_archive = false;
+
+        for (base.comp.link_inputs) |link_input| switch (link_input) {
+            .res => unreachable, // Windows-only
+            .dso => continue,
+            .object, .archive => |obj| {
+                if (obj.must_link and !whole_archive) {
+                    try argv.append("-whole-archive");
+                    whole_archive = true;
+                } else if (!obj.must_link and whole_archive) {
+                    try argv.append("-no-whole-archive");
+                    whole_archive = false;
+                }
+                try argv.append(try obj.path.toString(arena));
+            },
+            .dso_exact => |dso_exact| {
+                assert(dso_exact.name[0] == ':');
+                try argv.appendSlice(&.{ "-l", dso_exact.name });
+            },
+        };
+
+        if (whole_archive) {
+            try argv.append("-no-whole-archive");
+            whole_archive = false;
+        }
+
+        for (comp.c_object_table.keys()) |key| {
+            try argv.append(try key.status.success.object_path.toString(arena));
+        }
+
+        if (module_obj_path) |p| {
+            try argv.append(p);
+        }
+
+        if (comp.tsan_lib) |lib| {
+            assert(comp.config.any_sanitize_thread);
+            try argv.append(try lib.full_object_path.toString(arena));
+        }
+
+        if (comp.fuzzer_lib) |lib| {
+            assert(comp.config.any_fuzz);
+            try argv.append(try lib.full_object_path.toString(arena));
+        }
+
+        if (ubsan_rt_path) |p| {
+            try argv.append(try p.toString(arena));
+        }
+
+        // Shared libraries.
+        if (is_exe_or_dyn_lib) {
+            // Worst-case, we need an --as-needed argument for every lib, as well
+            // as one before and one after.
+            try argv.ensureUnusedCapacity(2 * base.comp.link_inputs.len + 2);
+            argv.appendAssumeCapacity("--as-needed");
+            var as_needed = true;
+
+            for (base.comp.link_inputs) |link_input| switch (link_input) {
+                .res => unreachable, // Windows-only
+                .object, .archive, .dso_exact => continue,
+                .dso => |dso| {
+                    const lib_as_needed = !dso.needed;
+                    switch ((@as(u2, @intFromBool(lib_as_needed)) << 1) | @intFromBool(as_needed)) {
+                        0b00, 0b11 => {},
+                        0b01 => {
+                            argv.appendAssumeCapacity("--no-as-needed");
+                            as_needed = false;
+                        },
+                        0b10 => {
+                            argv.appendAssumeCapacity("--as-needed");
+                            as_needed = true;
+                        },
+                    }
+
+                    // By this time, we depend on these libs being dynamically linked
+                    // libraries and not static libraries (the check for that needs to be earlier),
+                    // but they could be full paths to .so files, in which case we
+                    // want to avoid prepending "-l".
+                    argv.appendAssumeCapacity(try dso.path.toString(arena));
+                },
+            };
+
+            if (!as_needed) {
+                argv.appendAssumeCapacity("--as-needed");
+                as_needed = true;
+            }
+
+            // libc++ dep
+            if (comp.config.link_libcpp) {
+                try argv.append(try comp.libcxxabi_static_lib.?.full_object_path.toString(arena));
+                try argv.append(try comp.libcxx_static_lib.?.full_object_path.toString(arena));
+            }
+
+            // libunwind dep
+            if (comp.config.link_libunwind) {
+                try argv.append(try comp.libunwind_static_lib.?.full_object_path.toString(arena));
+            }
+
+            // libc dep
+            diags.flags.missing_libc = false;
+            if (comp.config.link_libc) {
+                if (comp.libc_installation != null) {
+                    const needs_grouping = link_mode == .static;
+                    if (needs_grouping) try argv.append("--start-group");
+                    try argv.appendSlice(target_util.libcFullLinkFlags(target));
+                    if (needs_grouping) try argv.append("--end-group");
+                } else if (target.isGnuLibC()) {
+                    for (glibc.libs) |lib| {
+                        if (lib.removed_in) |rem_in| {
+                            if (target.os.versionRange().gnuLibCVersion().?.order(rem_in) != .lt) continue;
+                        }
+
+                        const lib_path = try std.fmt.allocPrint(arena, "{}{c}lib{s}.so.{d}", .{
+                            comp.glibc_so_files.?.dir_path, fs.path.sep, lib.name, lib.sover,
+                        });
+                        try argv.append(lib_path);
+                    }
+                    try argv.append(try comp.crtFileAsString(arena, "libc_nonshared.a"));
+                } else if (target.isMuslLibC()) {
+                    try argv.append(try comp.crtFileAsString(arena, switch (link_mode) {
+                        .static => "libc.a",
+                        .dynamic => "libc.so",
+                    }));
+                } else if (target.isFreeBSDLibC()) {
+                    for (freebsd.libs) |lib| {
+                        const lib_path = try std.fmt.allocPrint(arena, "{}{c}lib{s}.so.{d}", .{
+                            comp.freebsd_so_files.?.dir_path, fs.path.sep, lib.name, lib.sover,
+                        });
+                        try argv.append(lib_path);
+                    }
+                } else if (target.isNetBSDLibC()) {
+                    for (netbsd.libs) |lib| {
+                        const lib_path = try std.fmt.allocPrint(arena, "{}{c}lib{s}.so.{d}", .{
+                            comp.netbsd_so_files.?.dir_path, fs.path.sep, lib.name, lib.sover,
+                        });
+                        try argv.append(lib_path);
+                    }
+                } else {
+                    diags.flags.missing_libc = true;
+                }
+
+                if (comp.zigc_static_lib) |zigc| {
+                    try argv.append(try zigc.full_object_path.toString(arena));
+                }
+            }
+        }
+
+        // compiler-rt. Since compiler_rt exports symbols like `memset`, it needs
+        // to be after the shared libraries, so they are picked up from the shared
+        // libraries, not libcompiler_rt.
+        if (compiler_rt_path) |p| {
+            try argv.append(try p.toString(arena));
+        }
+
+        // crt postlude
+        if (csu.crtend) |p| try argv.append(try p.toString(arena));
+        if (csu.crtn) |p| try argv.append(try p.toString(arena));
+
+        if (base.allow_shlib_undefined) {
+            try argv.append("--allow-shlib-undefined");
+        }
+
+        switch (elf.compress_debug_sections) {
+            .none => {},
+            .zlib => try argv.append("--compress-debug-sections=zlib"),
+            .zstd => try argv.append("--compress-debug-sections=zstd"),
+        }
+
+        if (elf.bind_global_refs_locally) {
+            try argv.append("-Bsymbolic");
+        }
+
+        try spawnLld(comp, arena, argv.items);
+    }
+
+    if (!lld.disable_caching) {
+        // Update the file with the digest. If it fails we can continue; it only
+        // means that the next invocation will have an unnecessary cache miss.
+        std.Build.Cache.writeSmallFile(directory.handle, id_symlink_basename, &digest) catch |err| {
+            log.warn("failed to save linking hash digest file: {s}", .{@errorName(err)});
+        };
+        // Again failure here only means an unnecessary cache miss.
+        man.writeManifest() catch |err| {
+            log.warn("failed to write cache manifest when linking: {s}", .{@errorName(err)});
+        };
+        // We hang on to this lock so that the output file path can be used without
+        // other processes clobbering it.
+        base.lock = man.toOwnedLock();
+    }
+}
+fn getLDMOption(target: std.Target) ?[]const u8 {
+    // This should only return emulations understood by LLD's parseEmulation().
+    return switch (target.cpu.arch) {
+        .aarch64 => switch (target.os.tag) {
+            .linux => "aarch64linux",
+            else => "aarch64elf",
+        },
+        .aarch64_be => switch (target.os.tag) {
+            .linux => "aarch64linuxb",
+            else => "aarch64elfb",
+        },
+        .amdgcn => "elf64_amdgpu",
+        .arm, .thumb => switch (target.os.tag) {
+            .linux => "armelf_linux_eabi",
+            else => "armelf",
+        },
+        .armeb, .thumbeb => switch (target.os.tag) {
+            .linux => "armelfb_linux_eabi",
+            else => "armelfb",
+        },
+        .hexagon => "hexagonelf",
+        .loongarch32 => "elf32loongarch",
+        .loongarch64 => "elf64loongarch",
+        .mips => switch (target.os.tag) {
+            .freebsd => "elf32btsmip_fbsd",
+            else => "elf32btsmip",
+        },
+        .mipsel => switch (target.os.tag) {
+            .freebsd => "elf32ltsmip_fbsd",
+            else => "elf32ltsmip",
+        },
+        .mips64 => switch (target.os.tag) {
+            .freebsd => switch (target.abi) {
+                .gnuabin32, .muslabin32 => "elf32btsmipn32_fbsd",
+                else => "elf64btsmip_fbsd",
+            },
+            else => switch (target.abi) {
+                .gnuabin32, .muslabin32 => "elf32btsmipn32",
+                else => "elf64btsmip",
+            },
+        },
+        .mips64el => switch (target.os.tag) {
+            .freebsd => switch (target.abi) {
+                .gnuabin32, .muslabin32 => "elf32ltsmipn32_fbsd",
+                else => "elf64ltsmip_fbsd",
+            },
+            else => switch (target.abi) {
+                .gnuabin32, .muslabin32 => "elf32ltsmipn32",
+                else => "elf64ltsmip",
+            },
+        },
+        .msp430 => "msp430elf",
+        .powerpc => switch (target.os.tag) {
+            .freebsd => "elf32ppc_fbsd",
+            .linux => "elf32ppclinux",
+            else => "elf32ppc",
+        },
+        .powerpcle => switch (target.os.tag) {
+            .linux => "elf32lppclinux",
+            else => "elf32lppc",
+        },
+        .powerpc64 => "elf64ppc",
+        .powerpc64le => "elf64lppc",
+        .riscv32 => "elf32lriscv",
+        .riscv64 => "elf64lriscv",
+        .s390x => "elf64_s390",
+        .sparc64 => "elf64_sparc",
+        .x86 => switch (target.os.tag) {
+            .freebsd => "elf_i386_fbsd",
+            else => "elf_i386",
+        },
+        .x86_64 => switch (target.abi) {
+            .gnux32, .muslx32 => "elf32_x86_64",
+            else => "elf_x86_64",
+        },
+        else => null,
+    };
+}
+fn wasmLink(lld: *Lld, arena: Allocator) !void {
+    const comp = lld.base.comp;
+    const shared_memory = comp.config.shared_memory;
+    const export_memory = comp.config.export_memory;
+    const import_memory = comp.config.import_memory;
+    const target = comp.root_mod.resolved_target.result;
+    const base = &lld.base;
+    const wasm = &lld.ofmt.wasm;
+
+    const gpa = comp.gpa;
+
+    const directory = base.emit.root_dir; // Just an alias to make it shorter to type.
+    const full_out_path = try directory.join(arena, &[_][]const u8{base.emit.sub_path});
+
+    // If there is no Zig code to compile, then we should skip flushing the output file because it
+    // will not be part of the linker line anyway.
+    const module_obj_path: ?[]const u8 = if (comp.zcu != null) p: {
+        if (fs.path.dirname(full_out_path)) |dirname| {
+            break :p try fs.path.join(arena, &.{ dirname, base.zcu_object_sub_path.? });
+        } else {
+            break :p base.zcu_object_sub_path.?;
+        }
+    } else null;
+
+    const is_obj = comp.config.output_mode == .Obj;
+    const compiler_rt_path: ?Cache.Path = blk: {
+        if (comp.compiler_rt_lib) |lib| break :blk lib.full_object_path;
+        if (comp.compiler_rt_obj) |obj| break :blk obj.full_object_path;
+        break :blk null;
+    };
+    const ubsan_rt_path: ?Cache.Path = blk: {
+        if (comp.ubsan_rt_lib) |lib| break :blk lib.full_object_path;
+        if (comp.ubsan_rt_obj) |obj| break :blk obj.full_object_path;
+        break :blk null;
+    };
+
+    const id_symlink_basename = "lld.id";
+
+    var man: Cache.Manifest = undefined;
+    defer if (!lld.disable_caching) man.deinit();
+
+    var digest: [Cache.hex_digest_len]u8 = undefined;
+
+    if (!lld.disable_caching) {
+        man = comp.cache_parent.obtain();
+
+        // We are about to obtain this lock, so here we give other processes a chance first.
+        base.releaseLock();
+
+        comptime assert(Compilation.link_hash_implementation_version == 14);
+
+        try link.hashInputs(&man, comp.link_inputs);
+        for (comp.c_object_table.keys()) |key| {
+            _ = try man.addFilePath(key.status.success.object_path, null);
+        }
+        try man.addOptionalFile(module_obj_path);
+        try man.addOptionalFilePath(compiler_rt_path);
+        try man.addOptionalFilePath(ubsan_rt_path);
+        man.hash.addOptionalBytes(wasm.entry_name);
+        man.hash.add(base.stack_size);
+        man.hash.add(base.build_id);
+        man.hash.add(import_memory);
+        man.hash.add(export_memory);
+        man.hash.add(wasm.import_table);
+        man.hash.add(wasm.export_table);
+        man.hash.addOptional(wasm.initial_memory);
+        man.hash.addOptional(wasm.max_memory);
+        man.hash.add(shared_memory);
+        man.hash.addOptional(wasm.global_base);
+        man.hash.addListOfBytes(wasm.export_symbol_names);
+        // strip does not need to go into the linker hash because it is part of the hash namespace
+
+        // We don't actually care whether it's a cache hit or miss; we just need the digest and the lock.
+        _ = try man.hit();
+        digest = man.final();
+
+        var prev_digest_buf: [digest.len]u8 = undefined;
+        const prev_digest: []u8 = Cache.readSmallFile(
+            directory.handle,
+            id_symlink_basename,
+            &prev_digest_buf,
+        ) catch |err| blk: {
+            log.debug("WASM LLD new_digest={s} error: {s}", .{ std.fmt.fmtSliceHexLower(&digest), @errorName(err) });
+            // Handle this as a cache miss.
+            break :blk prev_digest_buf[0..0];
+        };
+        if (mem.eql(u8, prev_digest, &digest)) {
+            log.debug("WASM LLD digest={s} match - skipping invocation", .{std.fmt.fmtSliceHexLower(&digest)});
+            // Hot diggity dog! The output binary is already there.
+            base.lock = man.toOwnedLock();
+            return;
+        }
+        log.debug("WASM LLD prev_digest={s} new_digest={s}", .{ std.fmt.fmtSliceHexLower(prev_digest), std.fmt.fmtSliceHexLower(&digest) });
+
+        // We are about to change the output file to be different, so we invalidate the build hash now.
+        directory.handle.deleteFile(id_symlink_basename) catch |err| switch (err) {
+            error.FileNotFound => {},
+            else => |e| return e,
+        };
+    }
+
+    if (is_obj) {
+        // LLD's WASM driver does not support the equivalent of `-r` so we do a simple file copy
+        // here. TODO: think carefully about how we can avoid this redundant operation when doing
+        // build-obj. See also the corresponding TODO in linkAsArchive.
+        const the_object_path = blk: {
+            if (link.firstObjectInput(comp.link_inputs)) |obj| break :blk obj.path;
+
+            if (comp.c_object_table.count() != 0)
+                break :blk comp.c_object_table.keys()[0].status.success.object_path;
+
+            if (module_obj_path) |p|
+                break :blk Cache.Path.initCwd(p);
+
+            // TODO I think this is unreachable. Audit this situation when solving the above TODO
+            // regarding eliding redundant object -> object transformations.
+            return error.NoObjectsToLink;
+        };
+        try fs.Dir.copyFile(
+            the_object_path.root_dir.handle,
+            the_object_path.sub_path,
+            directory.handle,
+            base.emit.sub_path,
+            .{},
+        );
+    } else {
+        // Create an LLD command line and invoke it.
+        var argv = std.ArrayList([]const u8).init(gpa);
+        defer argv.deinit();
+        // 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.
+        const linker_command = "wasm-ld";
+        try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, linker_command });
+        try argv.append("--error-limit=0");
+
+        if (comp.config.lto != .none) {
+            switch (comp.root_mod.optimize_mode) {
+                .Debug => {},
+                .ReleaseSmall => try argv.append("-O2"),
+                .ReleaseFast, .ReleaseSafe => try argv.append("-O3"),
+            }
+        }
+
+        if (import_memory) {
+            try argv.append("--import-memory");
+        }
+
+        if (export_memory) {
+            try argv.append("--export-memory");
+        }
+
+        if (wasm.import_table) {
+            assert(!wasm.export_table);
+            try argv.append("--import-table");
+        }
+
+        if (wasm.export_table) {
+            assert(!wasm.import_table);
+            try argv.append("--export-table");
+        }
+
+        // For wasm-ld we only need to specify '--no-gc-sections' when the user explicitly
+        // specified it as garbage collection is enabled by default.
+        if (!base.gc_sections) {
+            try argv.append("--no-gc-sections");
+        }
+
+        if (comp.config.debug_format == .strip) {
+            try argv.append("-s");
+        }
+
+        if (wasm.initial_memory) |initial_memory| {
+            const arg = try std.fmt.allocPrint(arena, "--initial-memory={d}", .{initial_memory});
+            try argv.append(arg);
+        }
+
+        if (wasm.max_memory) |max_memory| {
+            const arg = try std.fmt.allocPrint(arena, "--max-memory={d}", .{max_memory});
+            try argv.append(arg);
+        }
+
+        if (shared_memory) {
+            try argv.append("--shared-memory");
+        }
+
+        if (wasm.global_base) |global_base| {
+            const arg = try std.fmt.allocPrint(arena, "--global-base={d}", .{global_base});
+            try argv.append(arg);
+        } else {
+            // We prepend it by default, so when a stack overflow happens the runtime will trap correctly,
+            // rather than silently overwrite all global declarations. See https://github.com/ziglang/zig/issues/4496
+            //
+            // The user can overwrite this behavior by setting the global-base
+            try argv.append("--stack-first");
+        }
+
+        // Users are allowed to specify which symbols they want to export to the wasm host.
+        for (wasm.export_symbol_names) |symbol_name| {
+            const arg = try std.fmt.allocPrint(arena, "--export={s}", .{symbol_name});
+            try argv.append(arg);
+        }
+
+        if (comp.config.rdynamic) {
+            try argv.append("--export-dynamic");
+        }
+
+        if (wasm.entry_name) |entry_name| {
+            try argv.appendSlice(&.{ "--entry", entry_name });
+        } else {
+            try argv.append("--no-entry");
+        }
+
+        try argv.appendSlice(&.{
+            "-z",
+            try std.fmt.allocPrint(arena, "stack-size={d}", .{base.stack_size}),
+        });
+
+        switch (base.build_id) {
+            .none => try argv.append("--build-id=none"),
+            .fast, .uuid, .sha1 => try argv.append(try std.fmt.allocPrint(arena, "--build-id={s}", .{
+                @tagName(base.build_id),
+            })),
+            .hexstring => |hs| try argv.append(try std.fmt.allocPrint(arena, "--build-id=0x{s}", .{
+                std.fmt.fmtSliceHexLower(hs.toSlice()),
+            })),
+            .md5 => {},
+        }
+
+        if (wasm.import_symbols) {
+            try argv.append("--allow-undefined");
+        }
+
+        if (comp.config.output_mode == .Lib and comp.config.link_mode == .dynamic) {
+            try argv.append("--shared");
+        }
+        if (comp.config.pie) {
+            try argv.append("--pie");
+        }
+
+        try argv.appendSlice(&.{ "-o", full_out_path });
+
+        if (target.cpu.arch == .wasm64) {
+            try argv.append("-mwasm64");
+        }
+
+        const is_exe_or_dyn_lib = comp.config.output_mode == .Exe or
+            (comp.config.output_mode == .Lib and comp.config.link_mode == .dynamic);
+
+        if (comp.config.link_libc and is_exe_or_dyn_lib) {
+            if (target.os.tag == .wasi) {
+                for (comp.wasi_emulated_libs) |crt_file| {
+                    try argv.append(try comp.crtFileAsString(
+                        arena,
+                        wasi_libc.emulatedLibCRFileLibName(crt_file),
+                    ));
+                }
+
+                try argv.append(try comp.crtFileAsString(
+                    arena,
+                    wasi_libc.execModelCrtFileFullName(comp.config.wasi_exec_model),
+                ));
+                try argv.append(try comp.crtFileAsString(arena, "libc.a"));
+            }
+
+            if (comp.zigc_static_lib) |zigc| {
+                try argv.append(try zigc.full_object_path.toString(arena));
+            }
+
+            if (comp.config.link_libcpp) {
+                try argv.append(try comp.libcxx_static_lib.?.full_object_path.toString(arena));
+                try argv.append(try comp.libcxxabi_static_lib.?.full_object_path.toString(arena));
+            }
+        }
+
+        // Positional arguments to the linker such as object files.
+        var whole_archive = false;
+        for (comp.link_inputs) |link_input| switch (link_input) {
+            .object, .archive => |obj| {
+                if (obj.must_link and !whole_archive) {
+                    try argv.append("-whole-archive");
+                    whole_archive = true;
+                } else if (!obj.must_link and whole_archive) {
+                    try argv.append("-no-whole-archive");
+                    whole_archive = false;
+                }
+                try argv.append(try obj.path.toString(arena));
+            },
+            .dso => |dso| {
+                try argv.append(try dso.path.toString(arena));
+            },
+            .dso_exact => unreachable,
+            .res => unreachable,
+        };
+        if (whole_archive) {
+            try argv.append("-no-whole-archive");
+            whole_archive = false;
+        }
+
+        for (comp.c_object_table.keys()) |key| {
+            try argv.append(try key.status.success.object_path.toString(arena));
+        }
+        if (module_obj_path) |p| {
+            try argv.append(p);
+        }
+
+        if (compiler_rt_path) |p| {
+            try argv.append(try p.toString(arena));
+        }
+
+        if (ubsan_rt_path) |p| {
+            try argv.append(try p.toStringZ(arena));
+        }
+
+        try spawnLld(comp, arena, argv.items);
+
+        // Give +x to the .wasm file if it is an executable and the OS is WASI.
+        // Some systems may be configured to execute such binaries directly. Even if that
+        // is not the case, it means we will get "exec format error" when trying to run
+        // it, and then can react to that in the same way as trying to run an ELF file
+        // from a foreign CPU architecture.
+        if (fs.has_executable_bit and target.os.tag == .wasi and
+            comp.config.output_mode == .Exe)
+        {
+            // TODO: what's our strategy for reporting linker errors from this function?
+            // report a nice error here with the file path if it fails instead of
+            // just returning the error code.
+            // chmod does not interact with umask, so we use a conservative -rwxr--r-- here.
+            std.posix.fchmodat(fs.cwd().fd, full_out_path, 0o744, 0) catch |err| switch (err) {
+                error.OperationNotSupported => unreachable, // Not a symlink.
+                else => |e| return e,
+            };
+        }
+    }
+
+    if (!lld.disable_caching) {
+        // Update the file with the digest. If it fails we can continue; it only
+        // means that the next invocation will have an unnecessary cache miss.
+        Cache.writeSmallFile(directory.handle, id_symlink_basename, &digest) catch |err| {
+            log.warn("failed to save linking hash digest symlink: {s}", .{@errorName(err)});
+        };
+        // Again failure here only means an unnecessary cache miss.
+        man.writeManifest() catch |err| {
+            log.warn("failed to write cache manifest when linking: {s}", .{@errorName(err)});
+        };
+        // We hang on to this lock so that the output file path can be used without
+        // other processes clobbering it.
+        base.lock = man.toOwnedLock();
+    }
+}
+
+fn spawnLld(
+    comp: *Compilation,
+    arena: Allocator,
+    argv: []const []const u8,
+) !void {
+    if (comp.verbose_link) {
+        // Skip over our own name so that the LLD linker name is the first argv item.
+        Compilation.dump_argv(argv[1..]);
+    }
+
+    // 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
+    if (!std.process.can_spawn) {
+        const exit_code = try lldMain(arena, argv, false);
+        if (exit_code == 0) return;
+        if (comp.clang_passthrough_mode) std.process.exit(exit_code);
+        return error.LinkFailure;
+    }
+
+    var stderr: []u8 = &.{};
+    defer comp.gpa.free(stderr);
+
+    var child = std.process.Child.init(argv, arena);
+    const term = (if (comp.clang_passthrough_mode) term: {
+        child.stdin_behavior = .Inherit;
+        child.stdout_behavior = .Inherit;
+        child.stderr_behavior = .Inherit;
+
+        break :term child.spawnAndWait();
+    } else term: {
+        child.stdin_behavior = .Ignore;
+        child.stdout_behavior = .Ignore;
+        child.stderr_behavior = .Pipe;
+
+        child.spawn() catch |err| break :term err;
+        stderr = try child.stderr.?.reader().readAllAlloc(comp.gpa, std.math.maxInt(usize));
+        break :term child.wait();
+    }) catch |first_err| term: {
+        const err = switch (first_err) {
+            error.NameTooLong => err: {
+                const s = fs.path.sep_str;
+                const rand_int = std.crypto.random.int(u64);
+                const rsp_path = "tmp" ++ s ++ std.fmt.hex(rand_int) ++ ".rsp";
+
+                const rsp_file = try comp.dirs.local_cache.handle.createFileZ(rsp_path, .{});
+                defer comp.dirs.local_cache.handle.deleteFileZ(rsp_path) catch |err|
+                    log.warn("failed to delete response file {s}: {s}", .{ rsp_path, @errorName(err) });
+                {
+                    defer rsp_file.close();
+                    var rsp_buf = std.io.bufferedWriter(rsp_file.writer());
+                    const rsp_writer = rsp_buf.writer();
+                    for (argv[2..]) |arg| {
+                        try rsp_writer.writeByte('"');
+                        for (arg) |c| {
+                            switch (c) {
+                                '\"', '\\' => try rsp_writer.writeByte('\\'),
+                                else => {},
+                            }
+                            try rsp_writer.writeByte(c);
+                        }
+                        try rsp_writer.writeByte('"');
+                        try rsp_writer.writeByte('\n');
+                    }
+                    try rsp_buf.flush();
+                }
+
+                var rsp_child = std.process.Child.init(&.{ argv[0], argv[1], try std.fmt.allocPrint(
+                    arena,
+                    "@{s}",
+                    .{try comp.dirs.local_cache.join(arena, &.{rsp_path})},
+                ) }, arena);
+                if (comp.clang_passthrough_mode) {
+                    rsp_child.stdin_behavior = .Inherit;
+                    rsp_child.stdout_behavior = .Inherit;
+                    rsp_child.stderr_behavior = .Inherit;
+
+                    break :term rsp_child.spawnAndWait() catch |err| break :err err;
+                } else {
+                    rsp_child.stdin_behavior = .Ignore;
+                    rsp_child.stdout_behavior = .Ignore;
+                    rsp_child.stderr_behavior = .Pipe;
+
+                    rsp_child.spawn() catch |err| break :err err;
+                    stderr = try rsp_child.stderr.?.reader().readAllAlloc(comp.gpa, std.math.maxInt(usize));
+                    break :term rsp_child.wait() catch |err| break :err err;
+                }
+            },
+            else => first_err,
+        };
+        log.err("unable to spawn LLD {s}: {s}", .{ argv[0], @errorName(err) });
+        return error.UnableToSpawnSelf;
+    };
+
+    const diags = &comp.link_diags;
+    switch (term) {
+        .Exited => |code| if (code != 0) {
+            if (comp.clang_passthrough_mode) std.process.exit(code);
+            diags.lockAndParseLldStderr(argv[1], stderr);
+            return error.LinkFailure;
+        },
+        else => {
+            if (comp.clang_passthrough_mode) std.process.abort();
+            return diags.fail("{s} terminated with stderr:\n{s}", .{ argv[0], stderr });
+        },
+    }
+
+    if (stderr.len > 0) log.warn("unexpected LLD stderr:\n{s}", .{stderr});
+}
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const Cache = std.Build.Cache;
+const allocPrint = std.fmt.allocPrint;
+const assert = std.debug.assert;
+const fs = std.fs;
+const log = std.log.scoped(.link);
+const mem = std.mem;
+
+const Compilation = @import("../Compilation.zig");
+const Zcu = @import("../Zcu.zig");
+const dev = @import("../dev.zig");
+const freebsd = @import("../libs/freebsd.zig");
+const glibc = @import("../libs/glibc.zig");
+const netbsd = @import("../libs/netbsd.zig");
+const wasi_libc = @import("../libs/wasi_libc.zig");
+const link = @import("../link.zig");
+const lldMain = @import("../main.zig").lldMain;
+const target_util = @import("../target.zig");
+const trace = @import("../tracy.zig").trace;
+const Lld = @This();
src/link/MachO.zig
@@ -194,7 +194,6 @@ pub fn createEmpty(
             .stack_size = options.stack_size orelse 16777216,
             .allow_shlib_undefined = allow_shlib_undefined,
             .file = null,
-            .disable_lld_caching = options.disable_lld_caching,
             .build_id = options.build_id,
         },
         .rpath_list = options.rpath_list,
@@ -227,7 +226,7 @@ pub fn createEmpty(
     self.base.file = try emit.root_dir.handle.createFile(emit.sub_path, .{
         .truncate = true,
         .read = true,
-        .mode = link.File.determineMode(false, output_mode, link_mode),
+        .mode = link.File.determineMode(output_mode, link_mode),
     });
 
     // Append null file
@@ -341,15 +340,6 @@ pub fn flush(
     arena: Allocator,
     tid: Zcu.PerThread.Id,
     prog_node: std.Progress.Node,
-) link.File.FlushError!void {
-    try self.flushZcu(arena, tid, prog_node);
-}
-
-pub fn flushZcu(
-    self: *MachO,
-    arena: Allocator,
-    tid: Zcu.PerThread.Id,
-    prog_node: std.Progress.Node,
 ) link.File.FlushError!void {
     const tracy = trace(@src());
     defer tracy.end();
@@ -373,7 +363,7 @@ pub fn flushZcu(
     // --verbose-link
     if (comp.verbose_link) try self.dumpArgv(comp);
 
-    if (self.getZigObject()) |zo| try zo.flushZcu(self, tid);
+    if (self.getZigObject()) |zo| try zo.flush(self, tid);
     if (self.base.isStaticLib()) return relocatable.flushStaticLib(self, comp, module_obj_path);
     if (self.base.isObject()) return relocatable.flushObject(self, comp, module_obj_path);
 
@@ -617,7 +607,7 @@ pub fn flushZcu(
         error.LinkFailure => return error.LinkFailure,
         else => |e| return diags.fail("failed to calculate and write uuid: {s}", .{@errorName(e)}),
     };
-    if (self.getDebugSymbols()) |dsym| dsym.flushZcu(self) catch |err| switch (err) {
+    if (self.getDebugSymbols()) |dsym| dsym.flush(self) catch |err| switch (err) {
         error.OutOfMemory => return error.OutOfMemory,
         else => |e| return diags.fail("failed to get debug symbols: {s}", .{@errorName(e)}),
     };
src/link/Plan9.zig
@@ -301,7 +301,6 @@ pub fn createEmpty(
             .stack_size = options.stack_size orelse 16777216,
             .allow_shlib_undefined = options.allow_shlib_undefined orelse false,
             .file = null,
-            .disable_lld_caching = options.disable_lld_caching,
             .build_id = options.build_id,
         },
         .sixtyfour_bit = sixtyfour_bit,
@@ -494,7 +493,7 @@ fn updateFinish(self: *Plan9, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index
     // write the symbol
     // we already have the got index
     const sym: aout.Sym = .{
-        .value = undefined, // the value of stuff gets filled in in flushZcu
+        .value = undefined, // the value of stuff gets filled in in flush
         .type = atom.type,
         .name = try gpa.dupe(u8, nav.name.toSlice(ip)),
     };
@@ -527,25 +526,6 @@ fn allocateGotIndex(self: *Plan9) usize {
     }
 }
 
-pub fn flush(
-    self: *Plan9,
-    arena: Allocator,
-    tid: Zcu.PerThread.Id,
-    prog_node: std.Progress.Node,
-) link.File.FlushError!void {
-    const comp = self.base.comp;
-    const diags = &comp.link_diags;
-    const use_lld = build_options.have_llvm and comp.config.use_lld;
-    assert(!use_lld);
-
-    switch (link.File.effectiveOutputMode(use_lld, comp.config.output_mode)) {
-        .Exe => {},
-        .Obj => return diags.fail("writing plan9 object files unimplemented", .{}),
-        .Lib => return diags.fail("writing plan9 lib files unimplemented", .{}),
-    }
-    return self.flushZcu(arena, tid, prog_node);
-}
-
 pub fn changeLine(l: *std.ArrayList(u8), delta_line: i32) !void {
     if (delta_line > 0 and delta_line < 65) {
         const toappend = @as(u8, @intCast(delta_line));
@@ -586,7 +566,7 @@ fn atomCount(self: *Plan9) usize {
     return data_nav_count + fn_nav_count + lazy_atom_count + extern_atom_count + uav_atom_count;
 }
 
-pub fn flushZcu(
+pub fn flush(
     self: *Plan9,
     arena: Allocator,
     /// TODO: stop using this
@@ -607,10 +587,16 @@ pub fn flushZcu(
     const gpa = comp.gpa;
     const target = comp.root_mod.resolved_target.result;
 
+    switch (comp.config.output_mode) {
+        .Exe => {},
+        .Obj => return diags.fail("writing plan9 object files unimplemented", .{}),
+        .Lib => return diags.fail("writing plan9 lib files unimplemented", .{}),
+    }
+
     const sub_prog_node = prog_node.start("Flush Module", 0);
     defer sub_prog_node.end();
 
-    log.debug("flushZcu", .{});
+    log.debug("flush", .{});
 
     defer assert(self.hdr.entry != 0x0);
 
@@ -1039,7 +1025,7 @@ pub fn getOrCreateAtomForLazySymbol(self: *Plan9, pt: Zcu.PerThread, lazy_sym: F
     const atom = atom_ptr.*;
     _ = try self.getAtomPtr(atom).getOrCreateSymbolTableEntry(self);
     _ = self.getAtomPtr(atom).getOrCreateOffsetTableEntry(self);
-    // anyerror needs to be deferred until flushZcu
+    // anyerror needs to be deferred until flush
     if (lazy_sym.ty != .anyerror_type) try self.updateLazySymbolAtom(pt, lazy_sym, atom);
     return atom;
 }
@@ -1182,11 +1168,7 @@ pub fn open(
 
     const file = try emit.root_dir.handle.createFile(emit.sub_path, .{
         .read = true,
-        .mode = link.File.determineMode(
-            use_lld,
-            comp.config.output_mode,
-            comp.config.link_mode,
-        ),
+        .mode = link.File.determineMode(comp.config.output_mode, comp.config.link_mode),
     });
     errdefer file.close();
     self.base.file = file;
src/link/SpirV.zig
@@ -17,7 +17,7 @@
 //! All regular functions.
 
 // Because SPIR-V requires re-compilation anyway, and so hot swapping will not work
-// anyway, we simply generate all the code in flushZcu. This keeps
+// anyway, we simply generate all the code in flush. This keeps
 // things considerably simpler.
 
 const SpirV = @This();
@@ -83,7 +83,6 @@ pub fn createEmpty(
             .stack_size = options.stack_size orelse 0,
             .allow_shlib_undefined = options.allow_shlib_undefined orelse false,
             .file = null,
-            .disable_lld_caching = options.disable_lld_caching,
             .build_id = options.build_id,
         },
         .object = codegen.Object.init(gpa, comp.getTarget()),
@@ -193,18 +192,14 @@ pub fn updateExports(
     // TODO: Export regular functions, variables, etc using Linkage attributes.
 }
 
-pub fn flush(self: *SpirV, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
-    return self.flushZcu(arena, tid, prog_node);
-}
-
-pub fn flushZcu(
+pub fn flush(
     self: *SpirV,
     arena: Allocator,
     tid: Zcu.PerThread.Id,
     prog_node: std.Progress.Node,
 ) link.File.FlushError!void {
     // The goal is to never use this because it's only needed if we need to
-    // write to InternPool, but flushZcu is too late to be writing to the
+    // write to InternPool, but flush is too late to be writing to the
     // InternPool.
     _ = tid;
 
src/link/Wasm.zig
@@ -40,7 +40,6 @@ const Zcu = @import("../Zcu.zig");
 const codegen = @import("../codegen.zig");
 const dev = @import("../dev.zig");
 const link = @import("../link.zig");
-const lldMain = @import("../main.zig").lldMain;
 const trace = @import("../tracy.zig").trace;
 const wasi_libc = @import("../libs/wasi_libc.zig");
 const Value = @import("../Value.zig");
@@ -74,8 +73,6 @@ global_base: ?u64,
 initial_memory: ?u64,
 /// When defined, sets the maximum memory size of the memory.
 max_memory: ?u64,
-/// When true, will import the function table from the host environment.
-import_table: bool,
 /// When true, will export the function table to the host environment.
 export_table: bool,
 /// Output name of the file
@@ -2935,17 +2932,14 @@ pub fn createEmpty(
     const target = comp.root_mod.resolved_target.result;
     assert(target.ofmt == .wasm);
 
-    const use_lld = build_options.have_llvm and comp.config.use_lld;
     const use_llvm = comp.config.use_llvm;
     const output_mode = comp.config.output_mode;
     const wasi_exec_model = comp.config.wasi_exec_model;
 
-    // If using LLD to link, this code should produce an object file so that it
-    // can be passed to LLD.
     // If using LLVM to generate the object file for the zig compilation unit,
     // we need a place to put the object file so that it can be subsequently
     // handled.
-    const zcu_object_sub_path = if (!use_lld and !use_llvm)
+    const zcu_object_sub_path = if (!use_llvm)
         null
     else
         try std.fmt.allocPrint(arena, "{s}.o", .{emit.sub_path});
@@ -2970,13 +2964,11 @@ pub fn createEmpty(
             },
             .allow_shlib_undefined = options.allow_shlib_undefined orelse false,
             .file = null,
-            .disable_lld_caching = options.disable_lld_caching,
             .build_id = options.build_id,
         },
         .name = undefined,
         .string_table = .empty,
         .string_bytes = .empty,
-        .import_table = options.import_table,
         .export_table = options.export_table,
         .import_symbols = options.import_symbols,
         .export_symbol_names = options.export_symbol_names,
@@ -3004,17 +2996,7 @@ pub fn createEmpty(
         .named => |name| (try wasm.internString(name)).toOptional(),
     };
 
-    if (use_lld and (use_llvm or !comp.config.have_zcu)) {
-        // LLVM emits the object file (if any); LLD links it into the final product.
-        return wasm;
-    }
-
-    // What path should this Wasm linker code output to?
-    // If using LLD to link, this code should produce an object file so that it
-    // can be passed to LLD.
-    const sub_path = if (use_lld) zcu_object_sub_path.? else emit.sub_path;
-
-    wasm.base.file = try emit.root_dir.handle.createFile(sub_path, .{
+    wasm.base.file = try emit.root_dir.handle.createFile(emit.sub_path, .{
         .truncate = true,
         .read = true,
         .mode = if (fs.has_executable_bit)
@@ -3025,7 +3007,7 @@ pub fn createEmpty(
         else
             0,
     });
-    wasm.name = sub_path;
+    wasm.name = emit.sub_path;
 
     return wasm;
 }
@@ -3367,21 +3349,6 @@ pub fn loadInput(wasm: *Wasm, input: link.Input) !void {
     }
 }
 
-pub fn flush(wasm: *Wasm, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
-    const comp = wasm.base.comp;
-    const use_lld = build_options.have_llvm and comp.config.use_lld;
-    const diags = &comp.link_diags;
-
-    if (use_lld) {
-        return wasm.linkWithLLD(arena, tid, prog_node) catch |err| switch (err) {
-            error.OutOfMemory => return error.OutOfMemory,
-            error.LinkFailure => return error.LinkFailure,
-            else => |e| return diags.fail("failed to link with LLD: {s}", .{@errorName(e)}),
-        };
-    }
-    return wasm.flushZcu(arena, tid, prog_node);
-}
-
 pub fn prelink(wasm: *Wasm, prog_node: std.Progress.Node) link.File.FlushError!void {
     const tracy = trace(@src());
     defer tracy.end();
@@ -3773,14 +3740,14 @@ fn markTable(wasm: *Wasm, i: ObjectTableIndex) link.File.FlushError!void {
     try wasm.tables.put(wasm.base.comp.gpa, .fromObjectTable(i), {});
 }
 
-pub fn flushZcu(
+pub fn flush(
     wasm: *Wasm,
     arena: Allocator,
     tid: Zcu.PerThread.Id,
     prog_node: std.Progress.Node,
 ) link.File.FlushError!void {
     // The goal is to never use this because it's only needed if we need to
-    // write to InternPool, but flushZcu is too late to be writing to the
+    // write to InternPool, but flush is too late to be writing to the
     // InternPool.
     _ = tid;
     const comp = wasm.base.comp;
@@ -3832,436 +3799,6 @@ pub fn flushZcu(
     };
 }
 
-fn linkWithLLD(wasm: *Wasm, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) !void {
-    dev.check(.lld_linker);
-
-    const tracy = trace(@src());
-    defer tracy.end();
-
-    const comp = wasm.base.comp;
-    const diags = &comp.link_diags;
-    const shared_memory = comp.config.shared_memory;
-    const export_memory = comp.config.export_memory;
-    const import_memory = comp.config.import_memory;
-    const target = comp.root_mod.resolved_target.result;
-
-    const gpa = comp.gpa;
-
-    const directory = wasm.base.emit.root_dir; // Just an alias to make it shorter to type.
-    const full_out_path = try directory.join(arena, &[_][]const u8{wasm.base.emit.sub_path});
-
-    // If there is no Zig code to compile, then we should skip flushing the output file because it
-    // will not be part of the linker line anyway.
-    const module_obj_path: ?[]const u8 = if (comp.zcu) |zcu| blk: {
-        if (zcu.llvm_object == null) {
-            try wasm.flushZcu(arena, tid, prog_node);
-        } else {
-            // `Compilation.flush` has already made LLVM emit this object file for us.
-        }
-
-        if (fs.path.dirname(full_out_path)) |dirname| {
-            break :blk try fs.path.join(arena, &.{ dirname, wasm.base.zcu_object_sub_path.? });
-        } else {
-            break :blk wasm.base.zcu_object_sub_path.?;
-        }
-    } else null;
-
-    const sub_prog_node = prog_node.start("LLD Link", 0);
-    defer sub_prog_node.end();
-
-    const is_obj = comp.config.output_mode == .Obj;
-    const compiler_rt_path: ?Path = blk: {
-        if (comp.compiler_rt_lib) |lib| break :blk lib.full_object_path;
-        if (comp.compiler_rt_obj) |obj| break :blk obj.full_object_path;
-        break :blk null;
-    };
-    const ubsan_rt_path: ?Path = blk: {
-        if (comp.ubsan_rt_lib) |lib| break :blk lib.full_object_path;
-        if (comp.ubsan_rt_obj) |obj| break :blk obj.full_object_path;
-        break :blk null;
-    };
-
-    const id_symlink_basename = "lld.id";
-
-    var man: Cache.Manifest = undefined;
-    defer if (!wasm.base.disable_lld_caching) man.deinit();
-
-    var digest: [Cache.hex_digest_len]u8 = undefined;
-
-    if (!wasm.base.disable_lld_caching) {
-        man = comp.cache_parent.obtain();
-
-        // We are about to obtain this lock, so here we give other processes a chance first.
-        wasm.base.releaseLock();
-
-        comptime assert(Compilation.link_hash_implementation_version == 14);
-
-        try link.hashInputs(&man, comp.link_inputs);
-        for (comp.c_object_table.keys()) |key| {
-            _ = try man.addFilePath(key.status.success.object_path, null);
-        }
-        try man.addOptionalFile(module_obj_path);
-        try man.addOptionalFilePath(compiler_rt_path);
-        try man.addOptionalFilePath(ubsan_rt_path);
-        man.hash.addOptionalBytes(wasm.entry_name.slice(wasm));
-        man.hash.add(wasm.base.stack_size);
-        man.hash.add(wasm.base.build_id);
-        man.hash.add(import_memory);
-        man.hash.add(export_memory);
-        man.hash.add(wasm.import_table);
-        man.hash.add(wasm.export_table);
-        man.hash.addOptional(wasm.initial_memory);
-        man.hash.addOptional(wasm.max_memory);
-        man.hash.add(shared_memory);
-        man.hash.addOptional(wasm.global_base);
-        man.hash.addListOfBytes(wasm.export_symbol_names);
-        // strip does not need to go into the linker hash because it is part of the hash namespace
-
-        // We don't actually care whether it's a cache hit or miss; we just need the digest and the lock.
-        _ = try man.hit();
-        digest = man.final();
-
-        var prev_digest_buf: [digest.len]u8 = undefined;
-        const prev_digest: []u8 = Cache.readSmallFile(
-            directory.handle,
-            id_symlink_basename,
-            &prev_digest_buf,
-        ) catch |err| blk: {
-            log.debug("WASM LLD new_digest={s} error: {s}", .{ std.fmt.fmtSliceHexLower(&digest), @errorName(err) });
-            // Handle this as a cache miss.
-            break :blk prev_digest_buf[0..0];
-        };
-        if (mem.eql(u8, prev_digest, &digest)) {
-            log.debug("WASM LLD digest={s} match - skipping invocation", .{std.fmt.fmtSliceHexLower(&digest)});
-            // Hot diggity dog! The output binary is already there.
-            wasm.base.lock = man.toOwnedLock();
-            return;
-        }
-        log.debug("WASM LLD prev_digest={s} new_digest={s}", .{ std.fmt.fmtSliceHexLower(prev_digest), std.fmt.fmtSliceHexLower(&digest) });
-
-        // We are about to change the output file to be different, so we invalidate the build hash now.
-        directory.handle.deleteFile(id_symlink_basename) catch |err| switch (err) {
-            error.FileNotFound => {},
-            else => |e| return e,
-        };
-    }
-
-    if (is_obj) {
-        // LLD's WASM driver does not support the equivalent of `-r` so we do a simple file copy
-        // here. TODO: think carefully about how we can avoid this redundant operation when doing
-        // build-obj. See also the corresponding TODO in linkAsArchive.
-        const the_object_path = blk: {
-            if (link.firstObjectInput(comp.link_inputs)) |obj| break :blk obj.path;
-
-            if (comp.c_object_table.count() != 0)
-                break :blk comp.c_object_table.keys()[0].status.success.object_path;
-
-            if (module_obj_path) |p|
-                break :blk Path.initCwd(p);
-
-            // TODO I think this is unreachable. Audit this situation when solving the above TODO
-            // regarding eliding redundant object -> object transformations.
-            return error.NoObjectsToLink;
-        };
-        try fs.Dir.copyFile(
-            the_object_path.root_dir.handle,
-            the_object_path.sub_path,
-            directory.handle,
-            wasm.base.emit.sub_path,
-            .{},
-        );
-    } else {
-        // Create an LLD command line and invoke it.
-        var argv = std.ArrayList([]const u8).init(gpa);
-        defer argv.deinit();
-        // 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.
-        const linker_command = "wasm-ld";
-        try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, linker_command });
-        try argv.append("--error-limit=0");
-
-        if (comp.config.lto != .none) {
-            switch (comp.root_mod.optimize_mode) {
-                .Debug => {},
-                .ReleaseSmall => try argv.append("-O2"),
-                .ReleaseFast, .ReleaseSafe => try argv.append("-O3"),
-            }
-        }
-
-        if (import_memory) {
-            try argv.append("--import-memory");
-        }
-
-        if (export_memory) {
-            try argv.append("--export-memory");
-        }
-
-        if (wasm.import_table) {
-            assert(!wasm.export_table);
-            try argv.append("--import-table");
-        }
-
-        if (wasm.export_table) {
-            assert(!wasm.import_table);
-            try argv.append("--export-table");
-        }
-
-        // For wasm-ld we only need to specify '--no-gc-sections' when the user explicitly
-        // specified it as garbage collection is enabled by default.
-        if (!wasm.base.gc_sections) {
-            try argv.append("--no-gc-sections");
-        }
-
-        if (comp.config.debug_format == .strip) {
-            try argv.append("-s");
-        }
-
-        if (wasm.initial_memory) |initial_memory| {
-            const arg = try std.fmt.allocPrint(arena, "--initial-memory={d}", .{initial_memory});
-            try argv.append(arg);
-        }
-
-        if (wasm.max_memory) |max_memory| {
-            const arg = try std.fmt.allocPrint(arena, "--max-memory={d}", .{max_memory});
-            try argv.append(arg);
-        }
-
-        if (shared_memory) {
-            try argv.append("--shared-memory");
-        }
-
-        if (wasm.global_base) |global_base| {
-            const arg = try std.fmt.allocPrint(arena, "--global-base={d}", .{global_base});
-            try argv.append(arg);
-        } else {
-            // We prepend it by default, so when a stack overflow happens the runtime will trap correctly,
-            // rather than silently overwrite all global declarations. See https://github.com/ziglang/zig/issues/4496
-            //
-            // The user can overwrite this behavior by setting the global-base
-            try argv.append("--stack-first");
-        }
-
-        // Users are allowed to specify which symbols they want to export to the wasm host.
-        for (wasm.export_symbol_names) |symbol_name| {
-            const arg = try std.fmt.allocPrint(arena, "--export={s}", .{symbol_name});
-            try argv.append(arg);
-        }
-
-        if (comp.config.rdynamic) {
-            try argv.append("--export-dynamic");
-        }
-
-        if (wasm.entry_name.slice(wasm)) |entry_name| {
-            try argv.appendSlice(&.{ "--entry", entry_name });
-        } else {
-            try argv.append("--no-entry");
-        }
-
-        try argv.appendSlice(&.{
-            "-z",
-            try std.fmt.allocPrint(arena, "stack-size={d}", .{wasm.base.stack_size}),
-        });
-
-        switch (wasm.base.build_id) {
-            .none => try argv.append("--build-id=none"),
-            .fast, .uuid, .sha1 => try argv.append(try std.fmt.allocPrint(arena, "--build-id={s}", .{
-                @tagName(wasm.base.build_id),
-            })),
-            .hexstring => |hs| try argv.append(try std.fmt.allocPrint(arena, "--build-id=0x{s}", .{
-                std.fmt.fmtSliceHexLower(hs.toSlice()),
-            })),
-            .md5 => {},
-        }
-
-        if (wasm.import_symbols) {
-            try argv.append("--allow-undefined");
-        }
-
-        if (comp.config.output_mode == .Lib and comp.config.link_mode == .dynamic) {
-            try argv.append("--shared");
-        }
-        if (comp.config.pie) {
-            try argv.append("--pie");
-        }
-
-        try argv.appendSlice(&.{ "-o", full_out_path });
-
-        if (target.cpu.arch == .wasm64) {
-            try argv.append("-mwasm64");
-        }
-
-        const is_exe_or_dyn_lib = comp.config.output_mode == .Exe or
-            (comp.config.output_mode == .Lib and comp.config.link_mode == .dynamic);
-
-        if (comp.config.link_libc and is_exe_or_dyn_lib) {
-            if (target.os.tag == .wasi) {
-                for (comp.wasi_emulated_libs) |crt_file| {
-                    try argv.append(try comp.crtFileAsString(
-                        arena,
-                        wasi_libc.emulatedLibCRFileLibName(crt_file),
-                    ));
-                }
-
-                try argv.append(try comp.crtFileAsString(
-                    arena,
-                    wasi_libc.execModelCrtFileFullName(comp.config.wasi_exec_model),
-                ));
-                try argv.append(try comp.crtFileAsString(arena, "libc.a"));
-            }
-
-            if (comp.zigc_static_lib) |zigc| {
-                try argv.append(try zigc.full_object_path.toString(arena));
-            }
-
-            if (comp.config.link_libcpp) {
-                try argv.append(try comp.libcxx_static_lib.?.full_object_path.toString(arena));
-                try argv.append(try comp.libcxxabi_static_lib.?.full_object_path.toString(arena));
-            }
-        }
-
-        // Positional arguments to the linker such as object files.
-        var whole_archive = false;
-        for (comp.link_inputs) |link_input| switch (link_input) {
-            .object, .archive => |obj| {
-                if (obj.must_link and !whole_archive) {
-                    try argv.append("-whole-archive");
-                    whole_archive = true;
-                } else if (!obj.must_link and whole_archive) {
-                    try argv.append("-no-whole-archive");
-                    whole_archive = false;
-                }
-                try argv.append(try obj.path.toString(arena));
-            },
-            .dso => |dso| {
-                try argv.append(try dso.path.toString(arena));
-            },
-            .dso_exact => unreachable,
-            .res => unreachable,
-        };
-        if (whole_archive) {
-            try argv.append("-no-whole-archive");
-            whole_archive = false;
-        }
-
-        for (comp.c_object_table.keys()) |key| {
-            try argv.append(try key.status.success.object_path.toString(arena));
-        }
-        if (module_obj_path) |p| {
-            try argv.append(p);
-        }
-
-        if (compiler_rt_path) |p| {
-            try argv.append(try p.toString(arena));
-        }
-
-        if (ubsan_rt_path) |p| {
-            try argv.append(try p.toStringZ(arena));
-        }
-
-        if (comp.verbose_link) {
-            // Skip over our own name so that the LLD linker name is the first argv item.
-            Compilation.dump_argv(argv.items[1..]);
-        }
-
-        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
-            var child = std.process.Child.init(argv.items, arena);
-            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("failed to spawn (passthrough mode) LLD {s}: {s}", .{ argv.items[0], @errorName(err) });
-                    return error.UnableToSpawnWasm;
-                };
-                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, std.math.maxInt(usize));
-
-                const term = child.wait() catch |err| {
-                    log.err("failed to spawn LLD {s}: {s}", .{ argv.items[0], @errorName(err) });
-                    return error.UnableToSpawnWasm;
-                };
-
-                switch (term) {
-                    .Exited => |code| {
-                        if (code != 0) {
-                            diags.lockAndParseLldStderr(linker_command, stderr);
-                            return error.LinkFailure;
-                        }
-                    },
-                    else => {
-                        return diags.fail("{s} terminated with stderr:\n{s}", .{ argv.items[0], stderr });
-                    },
-                }
-
-                if (stderr.len != 0) {
-                    log.warn("unexpected LLD stderr:\n{s}", .{stderr});
-                }
-            }
-        } else {
-            const exit_code = try lldMain(arena, argv.items, false);
-            if (exit_code != 0) {
-                if (comp.clang_passthrough_mode) {
-                    std.process.exit(exit_code);
-                } else {
-                    return diags.fail("{s} returned exit code {d}:\n{s}", .{ argv.items[0], exit_code });
-                }
-            }
-        }
-
-        // Give +x to the .wasm file if it is an executable and the OS is WASI.
-        // Some systems may be configured to execute such binaries directly. Even if that
-        // is not the case, it means we will get "exec format error" when trying to run
-        // it, and then can react to that in the same way as trying to run an ELF file
-        // from a foreign CPU architecture.
-        if (fs.has_executable_bit and target.os.tag == .wasi and
-            comp.config.output_mode == .Exe)
-        {
-            // TODO: what's our strategy for reporting linker errors from this function?
-            // report a nice error here with the file path if it fails instead of
-            // just returning the error code.
-            // chmod does not interact with umask, so we use a conservative -rwxr--r-- here.
-            std.posix.fchmodat(fs.cwd().fd, full_out_path, 0o744, 0) catch |err| switch (err) {
-                error.OperationNotSupported => unreachable, // Not a symlink.
-                else => |e| return e,
-            };
-        }
-    }
-
-    if (!wasm.base.disable_lld_caching) {
-        // Update the file with the digest. If it fails we can continue; it only
-        // means that the next invocation will have an unnecessary cache miss.
-        Cache.writeSmallFile(directory.handle, id_symlink_basename, &digest) catch |err| {
-            log.warn("failed to save linking hash digest symlink: {s}", .{@errorName(err)});
-        };
-        // Again failure here only means an unnecessary cache miss.
-        man.writeManifest() catch |err| {
-            log.warn("failed to write cache manifest when linking: {s}", .{@errorName(err)});
-        };
-        // We hang on to this lock so that the output file path can be used without
-        // other processes clobbering it.
-        wasm.base.lock = man.toOwnedLock();
-    }
-}
-
 fn defaultEntrySymbolName(
     preloaded_strings: *const PreloadedStrings,
     wasi_exec_model: std.builtin.WasiExecModel,
src/link/Xcoff.zig
@@ -46,7 +46,6 @@ pub fn createEmpty(
             .stack_size = options.stack_size orelse 0,
             .allow_shlib_undefined = options.allow_shlib_undefined orelse false,
             .file = null,
-            .disable_lld_caching = options.disable_lld_caching,
             .build_id = options.build_id,
         },
     };
@@ -105,10 +104,6 @@ pub fn updateExports(
 }
 
 pub fn flush(self: *Xcoff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
-    return self.flushZcu(arena, tid, prog_node);
-}
-
-pub fn flushZcu(self: *Xcoff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) link.File.FlushError!void {
     _ = self;
     _ = arena;
     _ = tid;
src/Compilation.zig
@@ -1592,9 +1592,9 @@ pub const CreateOptions = struct {
     linker_tsaware: bool = false,
     linker_nxcompat: bool = false,
     linker_dynamicbase: bool = true,
-    linker_compress_debug_sections: ?link.File.Elf.CompressDebugSections = null,
+    linker_compress_debug_sections: ?link.File.Lld.Elf.CompressDebugSections = null,
     linker_module_definition_file: ?[]const u8 = null,
-    linker_sort_section: ?link.File.Elf.SortSection = null,
+    linker_sort_section: ?link.File.Lld.Elf.SortSection = null,
     major_subsystem_version: ?u16 = null,
     minor_subsystem_version: ?u16 = null,
     clang_passthrough_mode: bool = false,
@@ -1616,7 +1616,7 @@ pub const CreateOptions = struct {
     /// building such dependencies themselves, this flag must be set to avoid
     /// infinite recursion.
     skip_linker_dependencies: bool = false,
-    hash_style: link.File.Elf.HashStyle = .both,
+    hash_style: link.File.Lld.Elf.HashStyle = .both,
     entry: Entry = .default,
     force_undefined_symbols: std.StringArrayHashMapUnmanaged(void) = .empty,
     stack_size: ?u64 = null,
src/link.zig
@@ -19,7 +19,6 @@ const Zcu = @import("Zcu.zig");
 const InternPool = @import("InternPool.zig");
 const Type = @import("Type.zig");
 const Value = @import("Value.zig");
-const lldMain = @import("main.zig").lldMain;
 const Package = @import("Package.zig");
 const dev = @import("dev.zig");
 const ThreadSafeQueue = @import("ThreadSafeQueue.zig").ThreadSafeQueue;
@@ -388,7 +387,6 @@ pub const File = struct {
     /// When linking with LLD, this linker code will output an object file only at
     /// this location, and then this path can be placed on the LLD linker line.
     zcu_object_sub_path: ?[]const u8 = null,
-    disable_lld_caching: bool,
     gc_sections: bool,
     print_gc_sections: bool,
     build_id: std.zig.BuildId,
@@ -424,7 +422,7 @@ pub const File = struct {
         tsaware: bool,
         nxcompat: bool,
         dynamicbase: bool,
-        compress_debug_sections: Elf.CompressDebugSections,
+        compress_debug_sections: Lld.Elf.CompressDebugSections,
         bind_global_refs_locally: bool,
         import_symbols: bool,
         import_table: bool,
@@ -436,8 +434,8 @@ pub const File = struct {
         global_base: ?u64,
         build_id: std.zig.BuildId,
         disable_lld_caching: bool,
-        hash_style: Elf.HashStyle,
-        sort_section: ?Elf.SortSection,
+        hash_style: Lld.Elf.HashStyle,
+        sort_section: ?Lld.Elf.SortSection,
         major_subsystem_version: ?u16,
         minor_subsystem_version: ?u16,
         gc_sections: ?bool,
@@ -521,12 +519,20 @@ pub const File = struct {
         emit: Path,
         options: OpenOptions,
     ) !*File {
+        if (comp.config.use_lld) {
+            dev.check(.lld_linker);
+            assert(comp.zcu == null or comp.config.use_llvm);
+            // LLD does not support incremental linking.
+            const lld: *Lld = try .createEmpty(arena, comp, emit, options);
+            return &lld.base;
+        }
         switch (Tag.fromObjectFormat(comp.root_mod.resolved_target.result.ofmt)) {
             inline else => |tag| {
                 dev.check(tag.devFeature());
                 const ptr = try tag.Type().open(arena, comp, emit, options);
                 return &ptr.base;
             },
+            .lld => unreachable, // not known from ofmt
         }
     }
 
@@ -536,12 +542,19 @@ pub const File = struct {
         emit: Path,
         options: OpenOptions,
     ) !*File {
+        if (comp.config.use_lld) {
+            dev.check(.lld_linker);
+            assert(comp.zcu == null or comp.config.use_llvm);
+            const lld: *Lld = try .createEmpty(arena, comp, emit, options);
+            return &lld.base;
+        }
         switch (Tag.fromObjectFormat(comp.root_mod.resolved_target.result.ofmt)) {
             inline else => |tag| {
                 dev.check(tag.devFeature());
                 const ptr = try tag.Type().createEmpty(arena, comp, emit, options);
                 return &ptr.base;
             },
+            .lld => unreachable, // not known from ofmt
         }
     }
 
@@ -554,6 +567,7 @@ pub const File = struct {
         const comp = base.comp;
         const gpa = comp.gpa;
         switch (base.tag) {
+            .lld => assert(base.file == null),
             .coff, .elf, .macho, .plan9, .wasm, .goff, .xcoff => {
                 if (base.file != null) return;
                 dev.checkAny(&.{ .coff_linker, .elf_linker, .macho_linker, .plan9_linker, .wasm_linker, .goff_linker, .xcoff_linker });
@@ -586,13 +600,12 @@ pub const File = struct {
                         }
                     }
                 }
-                const use_lld = build_options.have_llvm and comp.config.use_lld;
                 const output_mode = comp.config.output_mode;
                 const link_mode = comp.config.link_mode;
                 base.file = try emit.root_dir.handle.createFile(emit.sub_path, .{
                     .truncate = false,
                     .read = true,
-                    .mode = determineMode(use_lld, output_mode, link_mode),
+                    .mode = determineMode(output_mode, link_mode),
                 });
             },
             .c, .spirv => dev.checkAny(&.{ .c_linker, .spirv_linker }),
@@ -618,7 +631,6 @@ pub const File = struct {
         const comp = base.comp;
         const output_mode = comp.config.output_mode;
         const link_mode = comp.config.link_mode;
-        const use_lld = build_options.have_llvm and comp.config.use_lld;
 
         switch (output_mode) {
             .Obj => return,
@@ -629,13 +641,9 @@ pub const File = struct {
             .Exe => {},
         }
         switch (base.tag) {
+            .lld => assert(base.file == null),
             .elf => if (base.file) |f| {
                 dev.check(.elf_linker);
-                if (base.zcu_object_sub_path != null and use_lld) {
-                    // The file we have open is not the final file that we want to
-                    // make executable, so we don't have to close it.
-                    return;
-                }
                 f.close();
                 base.file = null;
 
@@ -650,11 +658,6 @@ pub const File = struct {
             },
             .coff, .macho, .plan9, .wasm, .goff, .xcoff => if (base.file) |f| {
                 dev.checkAny(&.{ .coff_linker, .macho_linker, .plan9_linker, .wasm_linker, .goff_linker, .xcoff_linker });
-                if (base.zcu_object_sub_path != null) {
-                    // The file we have open is not the final file that we want to
-                    // make executable, so we don't have to close it.
-                    return;
-                }
                 f.close();
                 base.file = null;
 
@@ -692,6 +695,7 @@ pub const File = struct {
     pub fn getGlobalSymbol(base: *File, name: []const u8, lib_name: ?[]const u8) UpdateNavError!u32 {
         log.debug("getGlobalSymbol '{s}' (expected in '{?s}')", .{ name, lib_name });
         switch (base.tag) {
+            .lld => unreachable,
             .plan9 => unreachable,
             .spirv => unreachable,
             .c => unreachable,
@@ -709,6 +713,7 @@ pub const File = struct {
         const nav = pt.zcu.intern_pool.getNav(nav_index);
         assert(nav.status == .fully_resolved);
         switch (base.tag) {
+            .lld => unreachable,
             inline else => |tag| {
                 dev.check(tag.devFeature());
                 return @as(*tag.Type(), @fieldParentPtr("base", base)).updateNav(pt, nav_index);
@@ -726,6 +731,7 @@ pub const File = struct {
     fn updateContainerType(base: *File, pt: Zcu.PerThread, ty: InternPool.Index) UpdateContainerTypeError!void {
         assert(base.comp.zcu.?.llvm_object == null);
         switch (base.tag) {
+            .lld => unreachable,
             else => {},
             inline .elf => |tag| {
                 dev.check(tag.devFeature());
@@ -746,6 +752,7 @@ pub const File = struct {
     ) UpdateNavError!void {
         assert(base.comp.zcu.?.llvm_object == null);
         switch (base.tag) {
+            .lld => unreachable,
             inline else => |tag| {
                 dev.check(tag.devFeature());
                 return @as(*tag.Type(), @fieldParentPtr("base", base)).updateFunc(pt, func_index, air, liveness);
@@ -772,6 +779,7 @@ pub const File = struct {
         }
 
         switch (base.tag) {
+            .lld => unreachable,
             .spirv => {},
             .goff, .xcoff => {},
             inline else => |tag| {
@@ -811,8 +819,7 @@ pub const File = struct {
         OutOfMemory,
     };
 
-    /// Commit pending changes and write headers. Takes into account final output mode
-    /// and `use_lld`, not only `effectiveOutputMode`.
+    /// Commit pending changes and write headers. Takes into account final output mode.
     /// `arena` has the lifetime of the call to `Compilation.update`.
     pub fn flush(base: *File, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) FlushError!void {
         const comp = base.comp;
@@ -834,15 +841,7 @@ pub const File = struct {
             };
             return;
         }
-
         assert(base.post_prelink);
-
-        const use_lld = build_options.have_llvm and comp.config.use_lld;
-        const output_mode = comp.config.output_mode;
-        const link_mode = comp.config.link_mode;
-        if (use_lld and output_mode == .Lib and link_mode == .static) {
-            return base.linkAsArchive(arena, tid, prog_node);
-        }
         switch (base.tag) {
             inline else => |tag| {
                 dev.check(tag.devFeature());
@@ -851,19 +850,6 @@ pub const File = struct {
         }
     }
 
-    /// Commit pending changes and write headers. Works based on `effectiveOutputMode`
-    /// rather than final output mode.
-    /// Never called when LLVM is codegenning the ZCU.
-    fn flushZcu(base: *File, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) FlushError!void {
-        assert(base.comp.zcu.?.llvm_object == null);
-        switch (base.tag) {
-            inline else => |tag| {
-                dev.check(tag.devFeature());
-                return @as(*tag.Type(), @fieldParentPtr("base", base)).flushZcu(arena, tid, prog_node);
-            },
-        }
-    }
-
     pub const UpdateExportsError = error{
         OutOfMemory,
         AnalysisFail,
@@ -882,6 +868,7 @@ pub const File = struct {
     ) UpdateExportsError!void {
         assert(base.comp.zcu.?.llvm_object == null);
         switch (base.tag) {
+            .lld => unreachable,
             inline else => |tag| {
                 dev.check(tag.devFeature());
                 return @as(*tag.Type(), @fieldParentPtr("base", base)).updateExports(pt, exported, export_indices);
@@ -911,6 +898,7 @@ pub const File = struct {
     pub fn getNavVAddr(base: *File, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index, reloc_info: RelocInfo) !u64 {
         assert(base.comp.zcu.?.llvm_object == null);
         switch (base.tag) {
+            .lld => unreachable,
             .c => unreachable,
             .spirv => unreachable,
             .wasm => unreachable,
@@ -932,6 +920,7 @@ pub const File = struct {
     ) !codegen.GenResult {
         assert(base.comp.zcu.?.llvm_object == null);
         switch (base.tag) {
+            .lld => unreachable,
             .c => unreachable,
             .spirv => unreachable,
             .wasm => unreachable,
@@ -947,6 +936,7 @@ pub const File = struct {
     pub fn getUavVAddr(base: *File, decl_val: InternPool.Index, reloc_info: RelocInfo) !u64 {
         assert(base.comp.zcu.?.llvm_object == null);
         switch (base.tag) {
+            .lld => unreachable,
             .c => unreachable,
             .spirv => unreachable,
             .wasm => unreachable,
@@ -966,6 +956,8 @@ pub const File = struct {
     ) void {
         assert(base.comp.zcu.?.llvm_object == null);
         switch (base.tag) {
+            .lld => unreachable,
+
             .plan9,
             .spirv,
             .goff,
@@ -981,6 +973,7 @@ pub const File = struct {
 
     /// Opens a path as an object file and parses it into the linker.
     fn openLoadObject(base: *File, path: Path) anyerror!void {
+        if (base.tag == .lld) return;
         const diags = &base.comp.link_diags;
         const input = try openObjectInput(diags, path);
         errdefer input.object.file.close();
@@ -990,6 +983,7 @@ pub const File = struct {
     /// Opens a path as a static library and parses it into the linker.
     /// If `query` is non-null, allows GNU ld scripts.
     fn openLoadArchive(base: *File, path: Path, opt_query: ?UnresolvedInput.Query) anyerror!void {
+        if (base.tag == .lld) return;
         if (opt_query) |query| {
             const archive = try openObject(path, query.must_link, query.hidden);
             errdefer archive.file.close();
@@ -1012,6 +1006,7 @@ pub const File = struct {
     /// Opens a path as a shared library and parses it into the linker.
     /// Handles GNU ld scripts.
     fn openLoadDso(base: *File, path: Path, query: UnresolvedInput.Query) anyerror!void {
+        if (base.tag == .lld) return;
         const dso = try openDso(path, query.needed, query.weak, query.reexport);
         errdefer dso.file.close();
         loadInput(base, .{ .dso = dso }) catch |err| switch (err) {
@@ -1064,8 +1059,7 @@ pub const File = struct {
     }
 
     pub fn loadInput(base: *File, input: Input) anyerror!void {
-        const use_lld = build_options.have_llvm and base.comp.config.use_lld;
-        if (use_lld) return;
+        if (base.tag == .lld) return;
         switch (base.tag) {
             inline .elf, .wasm => |tag| {
                 dev.check(tag.devFeature());
@@ -1079,8 +1073,6 @@ pub const File = struct {
     /// this, `loadInput` will not be called anymore.
     pub fn prelink(base: *File, prog_node: std.Progress.Node) FlushError!void {
         assert(!base.post_prelink);
-        const use_lld = build_options.have_llvm and base.comp.config.use_lld;
-        if (use_lld) return;
 
         // In this case, an object file is created by the LLVM backend, so
         // there is no prelink phase. The Zig code is linked as a standard
@@ -1096,170 +1088,6 @@ pub const File = struct {
         }
     }
 
-    fn linkAsArchive(base: *File, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) FlushError!void {
-        dev.check(.lld_linker);
-
-        const tracy = trace(@src());
-        defer tracy.end();
-
-        const comp = base.comp;
-        const diags = &comp.link_diags;
-
-        return linkAsArchiveInner(base, arena, tid, prog_node) catch |err| switch (err) {
-            error.OutOfMemory => return error.OutOfMemory,
-            error.LinkFailure => return error.LinkFailure,
-            else => |e| return diags.fail("failed to link as archive: {s}", .{@errorName(e)}),
-        };
-    }
-
-    fn linkAsArchiveInner(base: *File, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) !void {
-        const comp = base.comp;
-
-        const directory = base.emit.root_dir; // Just an alias to make it shorter to type.
-        const full_out_path = try directory.join(arena, &[_][]const u8{base.emit.sub_path});
-        const full_out_path_z = try arena.dupeZ(u8, full_out_path);
-        const opt_zcu = comp.zcu;
-
-        // If there is no Zig code to compile, then we should skip flushing the output file
-        // because it will not be part of the linker line anyway.
-        const zcu_obj_path: ?[]const u8 = if (opt_zcu) |zcu| blk: {
-            if (zcu.llvm_object == null) {
-                try base.flushZcu(arena, tid, prog_node);
-            } else {
-                // `Compilation.flush` has already made LLVM emit this object file for us.
-            }
-            const dirname = fs.path.dirname(full_out_path_z) orelse ".";
-            break :blk try fs.path.join(arena, &.{ dirname, base.zcu_object_sub_path.? });
-        } else null;
-
-        log.debug("zcu_obj_path={s}", .{if (zcu_obj_path) |s| s else "(null)"});
-
-        const compiler_rt_path: ?Path = if (comp.compiler_rt_strat == .obj)
-            comp.compiler_rt_obj.?.full_object_path
-        else
-            null;
-
-        const ubsan_rt_path: ?Path = if (comp.ubsan_rt_strat == .obj)
-            comp.ubsan_rt_obj.?.full_object_path
-        else
-            null;
-
-        // This function follows the same pattern as link.Elf.linkWithLLD so if you want some
-        // insight as to what's going on here you can read that function body which is more
-        // well-commented.
-
-        const id_symlink_basename = "llvm-ar.id";
-
-        var man: Cache.Manifest = undefined;
-        defer if (!base.disable_lld_caching) man.deinit();
-
-        const link_inputs = comp.link_inputs;
-
-        var digest: [Cache.hex_digest_len]u8 = undefined;
-
-        if (!base.disable_lld_caching) {
-            man = comp.cache_parent.obtain();
-
-            // We are about to obtain this lock, so here we give other processes a chance first.
-            base.releaseLock();
-
-            try hashInputs(&man, link_inputs);
-
-            for (comp.c_object_table.keys()) |key| {
-                _ = try man.addFilePath(key.status.success.object_path, null);
-            }
-            for (comp.win32_resource_table.keys()) |key| {
-                _ = try man.addFile(key.status.success.res_path, null);
-            }
-            try man.addOptionalFile(zcu_obj_path);
-            try man.addOptionalFilePath(compiler_rt_path);
-            try man.addOptionalFilePath(ubsan_rt_path);
-
-            // We don't actually care whether it's a cache hit or miss; we just need the digest and the lock.
-            _ = try man.hit();
-            digest = man.final();
-
-            var prev_digest_buf: [digest.len]u8 = undefined;
-            const prev_digest: []u8 = Cache.readSmallFile(
-                directory.handle,
-                id_symlink_basename,
-                &prev_digest_buf,
-            ) catch |err| b: {
-                log.debug("archive new_digest={s} readFile error: {s}", .{ std.fmt.fmtSliceHexLower(&digest), @errorName(err) });
-                break :b prev_digest_buf[0..0];
-            };
-            if (mem.eql(u8, prev_digest, &digest)) {
-                log.debug("archive digest={s} match - skipping invocation", .{std.fmt.fmtSliceHexLower(&digest)});
-                base.lock = man.toOwnedLock();
-                return;
-            }
-
-            // We are about to change the output file to be different, so we invalidate the build hash now.
-            directory.handle.deleteFile(id_symlink_basename) catch |err| switch (err) {
-                error.FileNotFound => {},
-                else => |e| return e,
-            };
-        }
-
-        var object_files: std.ArrayListUnmanaged([*:0]const u8) = .empty;
-
-        try object_files.ensureUnusedCapacity(arena, link_inputs.len);
-        for (link_inputs) |input| {
-            object_files.appendAssumeCapacity(try input.path().?.toStringZ(arena));
-        }
-
-        try object_files.ensureUnusedCapacity(arena, comp.c_object_table.count() +
-            comp.win32_resource_table.count() + 2);
-
-        for (comp.c_object_table.keys()) |key| {
-            object_files.appendAssumeCapacity(try key.status.success.object_path.toStringZ(arena));
-        }
-        for (comp.win32_resource_table.keys()) |key| {
-            object_files.appendAssumeCapacity(try arena.dupeZ(u8, key.status.success.res_path));
-        }
-        if (zcu_obj_path) |p| object_files.appendAssumeCapacity(try arena.dupeZ(u8, p));
-        if (compiler_rt_path) |p| object_files.appendAssumeCapacity(try p.toStringZ(arena));
-        if (ubsan_rt_path) |p| object_files.appendAssumeCapacity(try p.toStringZ(arena));
-
-        if (comp.verbose_link) {
-            std.debug.print("ar rcs {s}", .{full_out_path_z});
-            for (object_files.items) |arg| {
-                std.debug.print(" {s}", .{arg});
-            }
-            std.debug.print("\n", .{});
-        }
-
-        const llvm_bindings = @import("codegen/llvm/bindings.zig");
-        const llvm = @import("codegen/llvm.zig");
-        const target = comp.root_mod.resolved_target.result;
-        llvm.initializeLLVMTarget(target.cpu.arch);
-        const bad = llvm_bindings.WriteArchive(
-            full_out_path_z,
-            object_files.items.ptr,
-            object_files.items.len,
-            switch (target.os.tag) {
-                .aix => .AIXBIG,
-                .windows => .COFF,
-                else => if (target.os.tag.isDarwin()) .DARWIN else .GNU,
-            },
-        );
-        if (bad) return error.UnableToWriteArchive;
-
-        if (!base.disable_lld_caching) {
-            Cache.writeSmallFile(directory.handle, id_symlink_basename, &digest) catch |err| {
-                log.warn("failed to save archive hash digest file: {s}", .{@errorName(err)});
-            };
-
-            if (man.have_exclusive_lock) {
-                man.writeManifest() catch |err| {
-                    log.warn("failed to write cache manifest when archiving: {s}", .{@errorName(err)});
-                };
-            }
-
-            base.lock = man.toOwnedLock();
-        }
-    }
-
     pub const Tag = enum {
         coff,
         elf,
@@ -1270,6 +1098,7 @@ pub const File = struct {
         plan9,
         goff,
         xcoff,
+        lld,
 
         pub fn Type(comptime tag: Tag) type {
             return switch (tag) {
@@ -1282,10 +1111,11 @@ pub const File = struct {
                 .plan9 => Plan9,
                 .goff => Goff,
                 .xcoff => Xcoff,
+                .lld => Lld,
             };
         }
 
-        pub fn fromObjectFormat(ofmt: std.Target.ObjectFormat) Tag {
+        fn fromObjectFormat(ofmt: std.Target.ObjectFormat) Tag {
             return switch (ofmt) {
                 .coff => .coff,
                 .elf => .elf,
@@ -1313,15 +1143,7 @@ pub const File = struct {
         ty: InternPool.Index,
     };
 
-    pub fn effectiveOutputMode(
-        use_lld: bool,
-        output_mode: std.builtin.OutputMode,
-    ) std.builtin.OutputMode {
-        return if (use_lld) .Obj else output_mode;
-    }
-
     pub fn determineMode(
-        use_lld: bool,
         output_mode: std.builtin.OutputMode,
         link_mode: std.builtin.LinkMode,
     ) fs.File.Mode {
@@ -1330,7 +1152,7 @@ pub const File = struct {
         // more leniently. As another data point, C's fopen seems to open files with the
         // 666 mode.
         const executable_mode = if (builtin.target.os.tag == .windows) 0 else 0o777;
-        switch (effectiveOutputMode(use_lld, output_mode)) {
+        switch (output_mode) {
             .Lib => return switch (link_mode) {
                 .dynamic => executable_mode,
                 .static => fs.File.default_mode,
@@ -1378,6 +1200,7 @@ pub const File = struct {
         return base.comp.zcu.?.codegenFail(nav_index, format, args);
     }
 
+    pub const Lld = @import("link/Lld.zig");
     pub const C = @import("link/C.zig");
     pub const Coff = @import("link/Coff.zig");
     pub const Plan9 = @import("link/Plan9.zig");
@@ -1685,116 +1508,6 @@ pub fn doTask(comp: *Compilation, tid: usize, task: Task) void {
     }
 }
 
-pub fn spawnLld(
-    comp: *Compilation,
-    arena: Allocator,
-    argv: []const []const u8,
-) !void {
-    if (comp.verbose_link) {
-        // Skip over our own name so that the LLD linker name is the first argv item.
-        Compilation.dump_argv(argv[1..]);
-    }
-
-    // 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
-    if (!std.process.can_spawn) {
-        const exit_code = try lldMain(arena, argv, false);
-        if (exit_code == 0) return;
-        if (comp.clang_passthrough_mode) std.process.exit(exit_code);
-        return error.LinkFailure;
-    }
-
-    var stderr: []u8 = &.{};
-    defer comp.gpa.free(stderr);
-
-    var child = std.process.Child.init(argv, arena);
-    const term = (if (comp.clang_passthrough_mode) term: {
-        child.stdin_behavior = .Inherit;
-        child.stdout_behavior = .Inherit;
-        child.stderr_behavior = .Inherit;
-
-        break :term child.spawnAndWait();
-    } else term: {
-        child.stdin_behavior = .Ignore;
-        child.stdout_behavior = .Ignore;
-        child.stderr_behavior = .Pipe;
-
-        child.spawn() catch |err| break :term err;
-        stderr = try child.stderr.?.reader().readAllAlloc(comp.gpa, std.math.maxInt(usize));
-        break :term child.wait();
-    }) catch |first_err| term: {
-        const err = switch (first_err) {
-            error.NameTooLong => err: {
-                const s = fs.path.sep_str;
-                const rand_int = std.crypto.random.int(u64);
-                const rsp_path = "tmp" ++ s ++ std.fmt.hex(rand_int) ++ ".rsp";
-
-                const rsp_file = try comp.dirs.local_cache.handle.createFileZ(rsp_path, .{});
-                defer comp.dirs.local_cache.handle.deleteFileZ(rsp_path) catch |err|
-                    log.warn("failed to delete response file {s}: {s}", .{ rsp_path, @errorName(err) });
-                {
-                    defer rsp_file.close();
-                    var rsp_buf = std.io.bufferedWriter(rsp_file.writer());
-                    const rsp_writer = rsp_buf.writer();
-                    for (argv[2..]) |arg| {
-                        try rsp_writer.writeByte('"');
-                        for (arg) |c| {
-                            switch (c) {
-                                '\"', '\\' => try rsp_writer.writeByte('\\'),
-                                else => {},
-                            }
-                            try rsp_writer.writeByte(c);
-                        }
-                        try rsp_writer.writeByte('"');
-                        try rsp_writer.writeByte('\n');
-                    }
-                    try rsp_buf.flush();
-                }
-
-                var rsp_child = std.process.Child.init(&.{ argv[0], argv[1], try std.fmt.allocPrint(
-                    arena,
-                    "@{s}",
-                    .{try comp.dirs.local_cache.join(arena, &.{rsp_path})},
-                ) }, arena);
-                if (comp.clang_passthrough_mode) {
-                    rsp_child.stdin_behavior = .Inherit;
-                    rsp_child.stdout_behavior = .Inherit;
-                    rsp_child.stderr_behavior = .Inherit;
-
-                    break :term rsp_child.spawnAndWait() catch |err| break :err err;
-                } else {
-                    rsp_child.stdin_behavior = .Ignore;
-                    rsp_child.stdout_behavior = .Ignore;
-                    rsp_child.stderr_behavior = .Pipe;
-
-                    rsp_child.spawn() catch |err| break :err err;
-                    stderr = try rsp_child.stderr.?.reader().readAllAlloc(comp.gpa, std.math.maxInt(usize));
-                    break :term rsp_child.wait() catch |err| break :err err;
-                }
-            },
-            else => first_err,
-        };
-        log.err("unable to spawn LLD {s}: {s}", .{ argv[0], @errorName(err) });
-        return error.UnableToSpawnSelf;
-    };
-
-    const diags = &comp.link_diags;
-    switch (term) {
-        .Exited => |code| if (code != 0) {
-            if (comp.clang_passthrough_mode) std.process.exit(code);
-            diags.lockAndParseLldStderr(argv[1], stderr);
-            return error.LinkFailure;
-        },
-        else => {
-            if (comp.clang_passthrough_mode) std.process.abort();
-            return diags.fail("{s} terminated with stderr:\n{s}", .{ argv[0], stderr });
-        },
-    }
-
-    if (stderr.len > 0) log.warn("unexpected LLD stderr:\n{s}", .{stderr});
-}
-
 /// Provided by the CLI, processed into `LinkInput` instances at the start of
 /// the compilation pipeline.
 pub const UnresolvedInput = union(enum) {
src/main.zig
@@ -867,9 +867,9 @@ fn buildOutputType(
     var linker_allow_undefined_version: bool = false;
     var linker_enable_new_dtags: ?bool = null;
     var disable_c_depfile = false;
-    var linker_sort_section: ?link.File.Elf.SortSection = null;
+    var linker_sort_section: ?link.File.Lld.Elf.SortSection = null;
     var linker_gc_sections: ?bool = null;
-    var linker_compress_debug_sections: ?link.File.Elf.CompressDebugSections = null;
+    var linker_compress_debug_sections: ?link.File.Lld.Elf.CompressDebugSections = null;
     var linker_allow_shlib_undefined: ?bool = null;
     var allow_so_scripts: bool = false;
     var linker_bind_global_refs_locally: ?bool = null;
@@ -921,7 +921,7 @@ fn buildOutputType(
     var debug_compiler_runtime_libs = false;
     var opt_incremental: ?bool = null;
     var install_name: ?[]const u8 = null;
-    var hash_style: link.File.Elf.HashStyle = .both;
+    var hash_style: link.File.Lld.Elf.HashStyle = .both;
     var entitlements: ?[]const u8 = null;
     var pagezero_size: ?u64 = null;
     var lib_search_strategy: link.UnresolvedInput.SearchStrategy = .paths_first;
@@ -1196,11 +1196,11 @@ fn buildOutputType(
                         install_name = args_iter.nextOrFatal();
                     } else if (mem.startsWith(u8, arg, "--compress-debug-sections=")) {
                         const param = arg["--compress-debug-sections=".len..];
-                        linker_compress_debug_sections = std.meta.stringToEnum(link.File.Elf.CompressDebugSections, param) orelse {
+                        linker_compress_debug_sections = std.meta.stringToEnum(link.File.Lld.Elf.CompressDebugSections, param) orelse {
                             fatal("expected --compress-debug-sections=[none|zlib|zstd], found '{s}'", .{param});
                         };
                     } else if (mem.eql(u8, arg, "--compress-debug-sections")) {
-                        linker_compress_debug_sections = link.File.Elf.CompressDebugSections.zlib;
+                        linker_compress_debug_sections = link.File.Lld.Elf.CompressDebugSections.zlib;
                     } else if (mem.eql(u8, arg, "-pagezero_size")) {
                         const next_arg = args_iter.nextOrFatal();
                         pagezero_size = std.fmt.parseUnsigned(u64, eatIntPrefix(next_arg, 16), 16) catch |err| {
@@ -2368,7 +2368,7 @@ fn buildOutputType(
                         if (it.only_arg.len == 0) {
                             linker_compress_debug_sections = .zlib;
                         } else {
-                            linker_compress_debug_sections = std.meta.stringToEnum(link.File.Elf.CompressDebugSections, it.only_arg) orelse {
+                            linker_compress_debug_sections = std.meta.stringToEnum(link.File.Lld.Elf.CompressDebugSections, it.only_arg) orelse {
                                 fatal("expected [none|zlib|zstd] after --compress-debug-sections, found '{s}'", .{it.only_arg});
                             };
                         }
@@ -2505,7 +2505,7 @@ fn buildOutputType(
                     linker_print_map = true;
                 } else if (mem.eql(u8, arg, "--sort-section")) {
                     const arg1 = linker_args_it.nextOrFatal();
-                    linker_sort_section = std.meta.stringToEnum(link.File.Elf.SortSection, arg1) orelse {
+                    linker_sort_section = std.meta.stringToEnum(link.File.Lld.Elf.SortSection, arg1) orelse {
                         fatal("expected [name|alignment] after --sort-section, found '{s}'", .{arg1});
                     };
                 } else if (mem.eql(u8, arg, "--allow-shlib-undefined") or
@@ -2551,7 +2551,7 @@ fn buildOutputType(
                     try linker_export_symbol_names.append(arena, linker_args_it.nextOrFatal());
                 } else if (mem.eql(u8, arg, "--compress-debug-sections")) {
                     const arg1 = linker_args_it.nextOrFatal();
-                    linker_compress_debug_sections = std.meta.stringToEnum(link.File.Elf.CompressDebugSections, arg1) orelse {
+                    linker_compress_debug_sections = std.meta.stringToEnum(link.File.Lld.Elf.CompressDebugSections, arg1) orelse {
                         fatal("expected [none|zlib|zstd] after --compress-debug-sections, found '{s}'", .{arg1});
                     };
                 } else if (mem.startsWith(u8, arg, "-z")) {
@@ -2764,7 +2764,7 @@ fn buildOutputType(
                     mem.eql(u8, arg, "--hash-style"))
                 {
                     const next_arg = linker_args_it.nextOrFatal();
-                    hash_style = std.meta.stringToEnum(link.File.Elf.HashStyle, next_arg) orelse {
+                    hash_style = std.meta.stringToEnum(link.File.Lld.Elf.HashStyle, next_arg) orelse {
                         fatal("expected [sysv|gnu|both] after --hash-style, found '{s}'", .{
                             next_arg,
                         });