const builtin = @import("builtin");
const build_options = @import("build_options");

const std = @import("std");
const Io = std.Io;
const assert = std.debug.assert;
const fs = std.fs;
const mem = std.mem;
const log = std.log.scoped(.link);
const Allocator = std.mem.Allocator;
const Cache = std.Build.Cache;
const Path = std.Build.Cache.Path;
const Directory = std.Build.Cache.Directory;
const Compilation = @import("Compilation.zig");
const LibCInstallation = std.zig.LibCInstallation;

const trace = @import("tracy.zig").trace;
const wasi_libc = @import("libs/wasi_libc.zig");

const Zcu = @import("Zcu.zig");
const InternPool = @import("InternPool.zig");
const Type = @import("Type.zig");
const Value = @import("Value.zig");
const Package = @import("Package.zig");
const dev = @import("dev.zig");
const target_util = @import("target.zig");
const codegen = @import("codegen.zig");

pub const aarch64 = @import("link/aarch64.zig");
pub const LdScript = @import("link/LdScript.zig");
pub const Queue = @import("link/Queue.zig");
pub const ConstPool = @import("link/ConstPool.zig");

pub const Error = Allocator.Error || Io.Cancelable || error{
    /// An error message has already been stored in persistent state on `Compilation` or `Zcu`, for
    /// instance in `Compilation.link_diags`.
    AlreadyReported,
};

pub const Diags = struct {
    /// Stored here so that function definitions can distinguish between
    /// needing an allocator for things besides error reporting.
    gpa: Allocator,
    io: Io,
    mutex: Io.Mutex,
    msgs: std.ArrayList(Msg),
    flags: Flags,
    lld: std.ArrayList(Lld),

    pub const SourceLocation = union(enum) {
        none,
        wasm: File.Wasm.SourceLocation,
    };

    pub const Flags = packed struct {
        no_entry_point_found: bool = false,
        missing_libc: bool = false,
        alloc_failure_occurred: bool = false,

        const Int = blk: {
            const bits = @typeInfo(@This()).@"struct".field_names.len;
            break :blk @Int(.unsigned, bits);
        };

        pub fn anySet(ef: Flags) bool {
            return @as(Int, @bitCast(ef)) > 0;
        }
    };

    pub const Lld = struct {
        /// Allocated with gpa.
        msg: []const u8,
        context_lines: []const []const u8 = &.{},

        pub fn deinit(self: *Lld, gpa: Allocator) void {
            for (self.context_lines) |line| gpa.free(line);
            gpa.free(self.context_lines);
            gpa.free(self.msg);
            self.* = undefined;
        }
    };

    pub const Msg = struct {
        source_location: SourceLocation = .none,
        msg: []const u8,
        notes: []Msg = &.{},

        fn string(
            msg: *const Msg,
            bundle: *std.zig.ErrorBundle.Wip,
            base: ?*File,
        ) Allocator.Error!std.zig.ErrorBundle.String {
            return switch (msg.source_location) {
                .none => try bundle.addString(msg.msg),
                .wasm => |sl| {
                    dev.check(.wasm_linker);
                    const wasm = base.?.cast(.wasm).?;
                    return sl.string(msg.msg, bundle, wasm);
                },
            };
        }

        pub fn deinit(self: *Msg, gpa: Allocator) void {
            for (self.notes) |*note| note.deinit(gpa);
            gpa.free(self.notes);
            gpa.free(self.msg);
        }
    };

    pub const ErrorWithNotes = struct {
        diags: *Diags,
        /// Allocated index in diags.msgs array.
        index: usize,
        /// Next available note slot.
        note_slot: usize = 0,

        pub fn addMsg(
            err: ErrorWithNotes,
            comptime format: []const u8,
            args: anytype,
        ) Allocator.Error!void {
            const gpa = err.diags.gpa;
            const err_msg = &err.diags.msgs.items[err.index];
            err_msg.msg = try std.fmt.allocPrint(gpa, format, args);
        }

        pub fn addNote(err: *ErrorWithNotes, comptime format: []const u8, args: anytype) void {
            const gpa = err.diags.gpa;
            const msg = std.fmt.allocPrint(gpa, format, args) catch return err.diags.setAllocFailure();
            const err_msg = &err.diags.msgs.items[err.index];
            assert(err.note_slot < err_msg.notes.len);
            err_msg.notes[err.note_slot] = .{ .msg = msg };
            err.note_slot += 1;
        }
    };

    pub fn init(gpa: Allocator, io: Io) Diags {
        return .{
            .gpa = gpa,
            .io = io,
            .mutex = .init,
            .msgs = .empty,
            .flags = .{},
            .lld = .empty,
        };
    }

    pub fn deinit(diags: *Diags) void {
        const gpa = diags.gpa;

        for (diags.msgs.items) |*item| item.deinit(gpa);
        diags.msgs.deinit(gpa);

        for (diags.lld.items) |*item| item.deinit(gpa);
        diags.lld.deinit(gpa);

        diags.* = undefined;
    }

    pub fn hasErrors(diags: *Diags) bool {
        return diags.msgs.items.len > 0 or diags.flags.anySet();
    }

    pub fn lockAndParseLldStderr(diags: *Diags, prefix: []const u8, stderr: []const u8) void {
        const io = diags.io;

        diags.mutex.lockUncancelable(io);
        defer diags.mutex.unlock(io);

        diags.parseLldStderr(prefix, stderr) catch diags.setAllocFailure();
    }

    fn parseLldStderr(
        diags: *Diags,
        prefix: []const u8,
        stderr: []const u8,
    ) Allocator.Error!void {
        const gpa = diags.gpa;

        var context_lines: std.ArrayList([]const u8) = .empty;
        defer context_lines.deinit(gpa);

        var current_err: ?*Lld = null;
        var lines = mem.splitSequence(u8, stderr, if (builtin.os.tag == .windows) "\r\n" else "\n");
        while (lines.next()) |line| {
            if (line.len > prefix.len + ":".len and
                mem.eql(u8, line[0..prefix.len], prefix) and line[prefix.len] == ':')
            {
                if (current_err) |err| {
                    err.context_lines = try context_lines.toOwnedSlice(gpa);
                }

                var split = mem.splitSequence(u8, line, "error: ");
                _ = split.first();

                try diags.lld.ensureUnusedCapacity(gpa, 1);

                const duped_msg = try std.fmt.allocPrint(gpa, "{s}: {s}", .{ prefix, split.rest() });

                current_err = diags.lld.addOneAssumeCapacity();
                current_err.?.* = .{ .msg = duped_msg };
            } else if (current_err != null) {
                const context_prefix = ">>> ";
                var trimmed = mem.trimEnd(u8, line, &std.ascii.whitespace);
                if (mem.startsWith(u8, trimmed, context_prefix)) {
                    trimmed = trimmed[context_prefix.len..];
                }

                if (trimmed.len > 0) {
                    try context_lines.ensureUnusedCapacity(gpa, 1);
                    context_lines.appendAssumeCapacity(try gpa.dupe(u8, trimmed));
                }
            }
        }

        if (current_err) |err| {
            err.context_lines = try context_lines.toOwnedSlice(gpa);
        }
    }

    pub fn fail(diags: *Diags, comptime format: []const u8, args: anytype) error{AlreadyReported} {
        @branchHint(.cold);
        addError(diags, format, args);
        return error.AlreadyReported;
    }

    pub fn failSourceLocation(diags: *Diags, sl: SourceLocation, comptime format: []const u8, args: anytype) error{AlreadyReported} {
        @branchHint(.cold);
        addErrorSourceLocation(diags, sl, format, args);
        return error.AlreadyReported;
    }

    pub fn addError(diags: *Diags, comptime format: []const u8, args: anytype) void {
        @branchHint(.cold);
        return addErrorSourceLocation(diags, .none, format, args);
    }

    pub fn addErrorSourceLocation(diags: *Diags, sl: SourceLocation, comptime format: []const u8, args: anytype) void {
        @branchHint(.cold);
        const gpa = diags.gpa;
        const io = diags.io;
        const eu_main_msg = std.fmt.allocPrint(gpa, format, args);
        diags.mutex.lockUncancelable(io);
        defer diags.mutex.unlock(io);
        addErrorLockedFallible(diags, sl, eu_main_msg) catch |err| switch (err) {
            error.OutOfMemory => diags.setAllocFailureLocked(),
        };
    }

    fn addErrorLockedFallible(diags: *Diags, sl: SourceLocation, eu_main_msg: Allocator.Error![]u8) Allocator.Error!void {
        const gpa = diags.gpa;
        const main_msg = try eu_main_msg;
        errdefer gpa.free(main_msg);
        try diags.msgs.append(gpa, .{
            .msg = main_msg,
            .source_location = sl,
        });
    }

    pub fn addErrorWithNotes(diags: *Diags, note_count: usize) Allocator.Error!ErrorWithNotes {
        @branchHint(.cold);
        const gpa = diags.gpa;
        const io = diags.io;
        diags.mutex.lockUncancelable(io);
        defer diags.mutex.unlock(io);
        try diags.msgs.ensureUnusedCapacity(gpa, 1);
        return addErrorWithNotesAssumeCapacity(diags, note_count);
    }

    pub fn addErrorWithNotesAssumeCapacity(diags: *Diags, note_count: usize) Allocator.Error!ErrorWithNotes {
        @branchHint(.cold);
        const gpa = diags.gpa;
        const index = diags.msgs.items.len;
        const err = diags.msgs.addOneAssumeCapacity();
        err.* = .{
            .msg = undefined,
            .notes = try gpa.alloc(Msg, note_count),
        };
        return .{
            .diags = diags,
            .index = index,
        };
    }

    pub fn addMissingLibraryError(
        diags: *Diags,
        checked_paths: []const []const u8,
        comptime format: []const u8,
        args: anytype,
    ) void {
        @branchHint(.cold);
        const gpa = diags.gpa;
        const io = diags.io;
        const eu_main_msg = std.fmt.allocPrint(gpa, format, args);
        diags.mutex.lockUncancelable(io);
        defer diags.mutex.unlock(io);
        addMissingLibraryErrorLockedFallible(diags, checked_paths, eu_main_msg) catch |err| switch (err) {
            error.OutOfMemory => diags.setAllocFailureLocked(),
        };
    }

    fn addMissingLibraryErrorLockedFallible(
        diags: *Diags,
        checked_paths: []const []const u8,
        eu_main_msg: Allocator.Error![]u8,
    ) Allocator.Error!void {
        const gpa = diags.gpa;
        const main_msg = try eu_main_msg;
        errdefer gpa.free(main_msg);
        try diags.msgs.ensureUnusedCapacity(gpa, 1);
        const notes = try gpa.alloc(Msg, checked_paths.len);
        errdefer gpa.free(notes);
        for (checked_paths, notes) |path, *note| {
            note.* = .{ .msg = try std.fmt.allocPrint(gpa, "tried {s}", .{path}) };
        }
        diags.msgs.appendAssumeCapacity(.{
            .msg = main_msg,
            .notes = notes,
        });
    }

    pub fn addParseError(
        diags: *Diags,
        path: Path,
        comptime format: []const u8,
        args: anytype,
    ) void {
        @branchHint(.cold);
        const gpa = diags.gpa;
        const io = diags.io;
        const eu_main_msg = std.fmt.allocPrint(gpa, format, args);
        diags.mutex.lockUncancelable(io);
        defer diags.mutex.unlock(io);
        addParseErrorLockedFallible(diags, path, eu_main_msg) catch |err| switch (err) {
            error.OutOfMemory => diags.setAllocFailureLocked(),
        };
    }

    fn addParseErrorLockedFallible(diags: *Diags, path: Path, m: Allocator.Error![]u8) Allocator.Error!void {
        const gpa = diags.gpa;
        const main_msg = try m;
        errdefer gpa.free(main_msg);
        try diags.msgs.ensureUnusedCapacity(gpa, 1);
        const note = try std.fmt.allocPrint(gpa, "while parsing {f}", .{path});
        errdefer gpa.free(note);
        const notes = try gpa.create([1]Msg);
        errdefer gpa.destroy(notes);
        notes.* = .{.{ .msg = note }};
        diags.msgs.appendAssumeCapacity(.{
            .msg = main_msg,
            .notes = notes,
        });
    }

    pub fn failParse(
        diags: *Diags,
        path: Path,
        comptime format: []const u8,
        args: anytype,
    ) error{AlreadyReported} {
        @branchHint(.cold);
        addParseError(diags, path, format, args);
        return error.AlreadyReported;
    }

    pub fn setAllocFailure(diags: *Diags) void {
        @branchHint(.cold);
        const io = diags.io;
        diags.mutex.lockUncancelable(io);
        defer diags.mutex.unlock(io);
        setAllocFailureLocked(diags);
    }

    fn setAllocFailureLocked(diags: *Diags) void {
        log.debug("memory allocation failure", .{});
        diags.flags.alloc_failure_occurred = true;
    }

    pub fn addMessagesToBundle(diags: *const Diags, bundle: *std.zig.ErrorBundle.Wip, base: ?*File) Allocator.Error!void {
        for (diags.msgs.items) |link_err| {
            try bundle.addRootErrorMessage(.{
                .msg = try link_err.string(bundle, base),
                .notes_len = @intCast(link_err.notes.len),
            });
            const notes_start = try bundle.reserveNotes(@intCast(link_err.notes.len));
            for (link_err.notes, 0..) |note, i| {
                bundle.extra.items[notes_start + i] = @intFromEnum(try bundle.addErrorMessage(.{
                    .msg = try note.string(bundle, base),
                }));
            }
        }
    }
};

pub const producer_string = if (builtin.is_test) "zig test" else "zig " ++ build_options.version;

pub const File = struct {
    tag: Tag,

    /// The owner of this output File.
    comp: *Compilation,
    emit: Path,

    file: ?Io.File,
    gc_sections: bool,
    print_gc_sections: bool,
    build_id: std.zig.BuildId,
    allow_shlib_undefined: bool,
    stack_size: u64,
    post_prelink: bool = false,

    /// Prevents other processes from clobbering files in the output directory
    /// of this linking operation.
    lock: ?Cache.Lock = null,
    child_pid: ?std.process.Child.Id = null,

    pub const OpenOptions = struct {
        symbol_count_hint: u64 = 32,
        program_code_size_hint: u64 = 256 * 1024,

        /// This may depend on what symbols are found during the linking process.
        entry: Entry,
        /// Virtual address of the entry point procedure relative to image base.
        entry_addr: ?u64,
        stack_size: ?u64,
        image_base: ?u64,
        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,
        tsaware: bool,
        nxcompat: bool,
        dynamicbase: bool,
        compress_debug_sections: std.zig.CompressDebugSections,
        bind_global_refs_locally: bool,
        import_symbols: bool,
        import_table: bool,
        export_table: bool,
        initial_memory: ?u64,
        max_memory: ?u64,
        object_host_name: ?[]const u8,
        export_symbol_names: []const []const u8,
        global_base: ?u64,
        build_id: std.zig.BuildId,
        hash_style: Lld.Elf.HashStyle,
        sort_section: ?Lld.Elf.SortSection,
        major_subsystem_version: ?u16,
        minor_subsystem_version: ?u16,
        gc_sections: ?bool,
        repro: bool,
        allow_shlib_undefined: ?bool,
        allow_undefined_version: bool,
        enable_new_dtags: ?bool,
        subsystem: ?std.zig.Subsystem,
        linker_script: ?[]const u8,
        version_script: ?[]const u8,
        soname: ?[]const u8,
        print_gc_sections: bool,
        print_icf_sections: bool,
        print_map: bool,

        /// Use a wrapper function for symbol. Any undefined reference to symbol
        /// will be resolved to __wrap_symbol. Any undefined reference to
        /// __real_symbol will be resolved to symbol. This can be used to provide a
        /// wrapper for a system function. The wrapper function should be called
        /// __wrap_symbol. If it wishes to call the system function, it should call
        /// __real_symbol.
        symbol_wrap_set: std.array_hash_map.String(void),

        compatibility_version: ?std.SemanticVersion,

        // TODO: remove this. libraries are resolved by the frontend.
        lib_directories: []const Directory,
        framework_dirs: []const []const u8,
        rpath_list: []const []const u8,

        /// Zig compiler development linker flags.
        /// Enable dumping of linker's state as JSON.
        enable_link_snapshots: bool,

        /// Darwin-specific linker flags:
        /// Install name for the dylib
        install_name: ?[]const u8,
        /// Path to entitlements file
        entitlements: ?[]const u8,
        /// size of the __PAGEZERO segment
        pagezero_size: ?u64,
        /// Set minimum space for future expansion of the load commands
        headerpad_size: ?u32,
        /// Set enough space as if all paths were MATPATHLEN
        headerpad_max_install_names: bool,
        /// Remove dylibs that are unreachable by the entry point or exported symbols
        dead_strip_dylibs: bool,
        frameworks: []const MachO.Framework,
        darwin_sdk_layout: ?MachO.SdkLayout,
        /// Force load all members of static archives that implement an
        /// Objective-C class or category
        force_load_objc: bool,
        /// Whether local symbols should be discarded from the symbol table.
        discard_local_symbols: bool,

        /// Windows-specific linker flags:
        /// PDB source path prefix to instruct the linker how to resolve relative
        /// paths when consolidating CodeView streams into a single PDB file.
        pdb_source_path: ?[]const u8,
        /// PDB output path
        pdb_out_path: ?[]const u8,
        /// .def file to specify when linking
        module_definition_file: ?[]const u8,

        pub const Entry = union(enum) {
            default,
            disabled,
            enabled,
            named: []const u8,
        };
    };

    pub const OpenError = @typeInfo(@typeInfo(@TypeOf(open)).@"fn".return_type.?).error_union.error_set;

    /// Attempts incremental linking, if the file already exists. If
    /// incremental linking fails, falls back to truncating the file and
    /// rewriting it. A malicious file is detected as incremental link failure
    /// and does not cause Illegal Behavior. This operation is not atomic.
    /// `arena` is used for allocations with the same lifetime as the created File.
    pub fn open(
        arena: Allocator,
        comp: *Compilation,
        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, comp.config.use_new_linker)) {
            .plan9 => return error.UnsupportedObjectFormat,
            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
        }
    }

    pub fn createEmpty(
        arena: Allocator,
        comp: *Compilation,
        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, comp.config.use_new_linker)) {
            .plan9 => return error.UnsupportedObjectFormat,
            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
        }
    }

    pub fn cast(base: *File, comptime tag: Tag) if (dev.env.supports(tag.devFeature())) ?*tag.Type() else ?noreturn {
        return if (dev.env.supports(tag.devFeature()) and base.tag == tag) @fieldParentPtr("base", base) else null;
    }

    pub fn startProgress(base: *File, prog_node: std.Progress.Node) void {
        switch (base.tag) {
            else => {},
            inline .elf2, .coff2 => |tag| {
                dev.check(tag.devFeature());
                return @as(*tag.Type(), @fieldParentPtr("base", base)).startProgress(prog_node);
            },
        }
    }

    pub fn endProgress(base: *File) void {
        switch (base.tag) {
            else => {},
            inline .elf2, .coff2 => |tag| {
                dev.check(tag.devFeature());
                return @as(*tag.Type(), @fieldParentPtr("base", base)).endProgress();
            },
        }
    }

    pub fn makeWritable(base: *File) !void {
        dev.check(.make_writable);
        const comp = base.comp;
        const gpa = comp.gpa;
        const io = comp.io;
        switch (base.tag) {
            .lld => assert(base.file == null),
            .elf, .macho, .wasm => {
                dev.checkAny(&.{ .coff_linker, .elf_linker, .macho_linker, .plan9_linker, .wasm_linker });
                if (base.file != null) return;
                const emit = base.emit;
                if (base.child_pid) |pid| {
                    if (builtin.os.tag == .windows) {
                        return error.HotSwapUnavailableOnHostOperatingSystem;
                    } else {
                        // If we try to open the output file in write mode while it is running,
                        // it will return ETXTBSY. So instead, we copy the file, atomically rename it
                        // over top of the exe path, and then proceed normally. This changes the inode,
                        // avoiding the error.
                        const random_integer = r: {
                            var x: u32 = undefined;
                            io.random(@ptrCast(&x));
                            break :r x;
                        };
                        const tmp_sub_path = try std.fmt.allocPrint(gpa, "{s}-{x}", .{
                            emit.sub_path, random_integer,
                        });
                        defer gpa.free(tmp_sub_path);
                        try emit.root_dir.handle.copyFile(emit.sub_path, emit.root_dir.handle, tmp_sub_path, io, .{});
                        try emit.root_dir.handle.rename(tmp_sub_path, emit.root_dir.handle, emit.sub_path, io);
                        switch (builtin.os.tag) {
                            .linux => std.posix.ptrace(std.os.linux.PTRACE.ATTACH, pid, 0, 0) catch |err| {
                                log.warn("ptrace failure: {t}", .{err});
                            },
                            .maccatalyst, .macos => {
                                const macho_file = base.cast(.macho).?;
                                macho_file.ptraceAttach(pid) catch |err| {
                                    log.warn("attaching failed with error: {t}", .{err});
                                };
                            },
                            .windows => unreachable,
                            else => return error.HotSwapUnavailableOnHostOperatingSystem,
                        }
                    }
                }
                base.file = try emit.root_dir.handle.openFile(io, emit.sub_path, .{ .mode = .read_write });
            },
            .elf2, .coff2 => if (base.file == null) {
                dev.checkAny(&.{ .elf2_linker, .coff2_linker });
                const mf = if (base.cast(.elf2)) |elf|
                    &elf.mf
                else if (base.cast(.coff2)) |coff|
                    &coff.mf
                else
                    unreachable;
                mf.memory_map.file = try base.emit.root_dir.handle.openFile(io, base.emit.sub_path, .{
                    .mode = .read_write,
                });
                base.file = mf.memory_map.file;
                try mf.ensureTotalCapacity(@intCast(mf.nodes.items[0].location().resolve(mf)[1]));
            },
            .c, .spirv => if (base.file == null) {
                dev.checkAny(&.{ .c_linker, .spirv_linker });
                base.file = try base.emit.root_dir.handle.openFile(io, base.emit.sub_path, .{
                    .mode = .write_only,
                });
            },
            .plan9 => unreachable,
        }
    }

    /// Some linkers create a separate file for debug info, which we might need to temporarily close
    /// when moving the compilation result directory due to the host OS not allowing moving a
    /// file/directory while a handle remains open.
    /// Returns `true` if a debug info file was closed. In that case, `reopenDebugInfo` may be called.
    pub fn closeDebugInfo(base: *File) bool {
        const macho = base.cast(.macho) orelse return false;
        return macho.closeDebugInfo();
    }

    pub fn reopenDebugInfo(base: *File) !void {
        const macho = base.cast(.macho).?;
        return macho.reopenDebugInfo();
    }

    pub fn makeExecutable(base: *File) !void {
        dev.check(.make_executable);
        const comp = base.comp;
        const io = comp.io;
        switch (comp.config.output_mode) {
            .Obj => return,
            .Lib => switch (comp.config.link_mode) {
                .static => return,
                .dynamic => {},
            },
            .Exe => {},
        }
        switch (base.tag) {
            .lld => assert(base.file == null),
            .elf => if (base.file) |f| {
                dev.check(.elf_linker);
                f.close(io);
                base.file = null;

                if (base.child_pid) |pid| {
                    switch (builtin.os.tag) {
                        .linux => std.posix.ptrace(std.os.linux.PTRACE.DETACH, pid, 0, 0) catch |err| {
                            log.warn("ptrace failure: {s}", .{@errorName(err)});
                        },
                        else => return error.HotSwapUnavailableOnHostOperatingSystem,
                    }
                }
            },
            .macho, .wasm => if (base.file) |f| {
                dev.checkAny(&.{ .coff_linker, .macho_linker, .plan9_linker, .wasm_linker });
                f.close(io);
                base.file = null;

                if (base.child_pid) |pid| {
                    switch (builtin.os.tag) {
                        .maccatalyst, .macos => {
                            const macho_file = base.cast(.macho).?;
                            macho_file.ptraceDetach(pid) catch |err| {
                                log.warn("detaching failed with error: {s}", .{@errorName(err)});
                            };
                        },
                        else => return error.HotSwapUnavailableOnHostOperatingSystem,
                    }
                }
            },
            .elf2, .coff2 => if (base.file) |f| {
                const mf = if (base.cast(.elf2)) |elf|
                    &elf.mf
                else if (base.cast(.coff2)) |coff|
                    &coff.mf
                else
                    unreachable;
                mf.unmap();
                assert(mf.memory_map.file.handle == f.handle);
                mf.memory_map.file.close(io);
                mf.memory_map.file = undefined;
                base.file = null;
            },
            .c, .spirv => dev.checkAny(&.{ .c_linker, .spirv_linker }),
            .plan9 => unreachable,
        }
    }

    pub const DebugInfoOutput = union(enum) {
        dwarf: *Dwarf.WipNav,
        none,
    };
    pub const UpdateDebugInfoError = Dwarf.UpdateError;

    /// Opaque identifier for a function currently being emitted.
    ///
    /// The function may be an interned function with a NAV, or it may be a lazy function.
    ///
    /// This type exists for type-safe interaction between codegen and link.
    pub const AtomId = enum(u32) { _ };

    /// Opaque identifier for some symbol in the output binary.
    ///
    /// This type exists for type-safe interaction between codegen and link.
    pub const SymbolId = enum(u32) { _ };

    /// Called from within CodeGen to retrieve the symbol index of a global symbol.
    /// If no symbol exists yet with this name, a new undefined global symbol will
    /// be created. This symbol may get resolved once all relocatables are (re-)linked.
    /// Optionally, it is possible to specify where to expect the symbol defined if it
    /// is an import.
    pub fn getGlobalSymbol(base: *File, name: []const u8, lib_name: ?[]const u8) Error!SymbolId {
        log.debug("getGlobalSymbol '{s}' (expected in '{?s}')", .{ name, lib_name });
        switch (base.tag) {
            .lld => unreachable,
            .spirv => unreachable,
            .c => unreachable,
            inline else => |tag| {
                dev.check(tag.devFeature());
                return @as(*tag.Type(), @fieldParentPtr("base", base)).getGlobalSymbol(name, lib_name);
            },
        }
    }

    /// May be called before or after updateExports for any given Nav.
    /// Asserts that the ZCU is not using the LLVM backend.
    fn updateNav(base: *File, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) Error!void {
        assert(base.comp.zcu.?.llvm_object == null);
        const nav = pt.zcu.intern_pool.getNav(nav_index);
        assert(nav.resolved.?.value != .none);
        switch (base.tag) {
            .lld => unreachable,
            .plan9 => unreachable,
            inline else => |tag| {
                dev.check(tag.devFeature());
                return @as(*tag.Type(), @fieldParentPtr("base", base)).updateNav(pt, nav_index);
            },
        }
    }

    /// Never called when LLVM is codegenning the ZCU.
    fn updateContainerType(base: *File, pt: Zcu.PerThread, ty: InternPool.Index, success: bool) Error!void {
        assert(base.comp.zcu.?.llvm_object == null);
        switch (base.tag) {
            .lld => unreachable,
            else => {},
            inline .elf, .c => |tag| {
                dev.check(tag.devFeature());
                return @as(*tag.Type(), @fieldParentPtr("base", base)).updateContainerType(pt, ty, success);
            },
        }
    }

    /// Never called when LLVM is codegenning the ZCU.
    fn clearContainerType(base: *File, pt: Zcu.PerThread, ty: InternPool.Index) Error!void {
        assert(base.comp.zcu.?.llvm_object == null);
        switch (base.tag) {
            .lld => unreachable,
            else => {},
            inline .elf => |tag| {
                dev.check(tag.devFeature());
                return @as(*tag.Type(), @fieldParentPtr("base", base)).clearContainerType(pt, ty);
            },
        }
    }

    /// May be called before or after updateExports for any given Decl.
    /// The active tag of `mir` is determined by the backend used for the module this function is in.
    /// Never called when LLVM is codegenning the ZCU.
    fn updateFunc(
        base: *File,
        pt: Zcu.PerThread,
        func_index: InternPool.Index,
        /// This is owned by the caller, but the callee is permitted to mutate it provided
        /// that `mir.deinit` remains legal for the caller. For instance, the callee can
        /// take ownership of an embedded slice and replace it with `&.{}` in `mir`.
        mir: *codegen.AnyMir,
    ) Error!void {
        assert(base.comp.zcu.?.llvm_object == null);
        switch (base.tag) {
            .lld => unreachable,
            .spirv => unreachable, // see corresponding special case in `Zcu.PerThread.runCodegenInner`
            .plan9 => unreachable,
            inline else => |tag| {
                dev.check(tag.devFeature());
                return @as(*tag.Type(), @fieldParentPtr("base", base)).updateFunc(pt, func_index, mir);
            },
        }
    }

    /// On an incremental update, fixup the line number of all `Nav`s at the given `TrackedInst`, because
    /// its line number has changed. The ZIR instruction `ti_id` has tag `.declaration`.
    /// Never called when LLVM is codegenning the ZCU.
    fn updateLineNumber(base: *File, pt: Zcu.PerThread, ti_id: InternPool.TrackedInst.Index) Error!void {
        assert(base.comp.zcu.?.llvm_object == null);
        {
            const ti = ti_id.resolveFull(&pt.zcu.intern_pool).?;
            const file = pt.zcu.fileByIndex(ti.file);
            const inst = file.zir.?.instructions.get(@intFromEnum(ti.inst));
            assert(inst.tag == .declaration);
        }

        switch (base.tag) {
            .lld => unreachable,
            .spirv => {},
            .plan9 => unreachable,
            .elf2, .coff2 => {},
            inline else => |tag| {
                dev.check(tag.devFeature());
                return @as(*tag.Type(), @fieldParentPtr("base", base)).updateLineNumber(pt, ti_id);
            },
        }
    }

    pub fn releaseLock(base: *File) void {
        const comp = base.comp;
        const io = comp.io;
        if (base.lock) |*lock| {
            lock.release(io);
            base.lock = null;
        }
    }

    pub fn toOwnedLock(self: *File) Cache.Lock {
        const lock = self.lock.?;
        self.lock = null;
        return lock;
    }

    pub fn destroy(base: *File) void {
        const io = base.comp.io;
        base.releaseLock();
        if (base.file) |f| f.close(io);
        switch (base.tag) {
            .plan9 => unreachable,
            inline else => |tag| {
                dev.check(tag.devFeature());
                @as(*tag.Type(), @fieldParentPtr("base", base)).deinit();
            },
        }
    }

    pub fn idle(base: *File, tid: Zcu.PerThread.Id) Error!bool {
        switch (base.tag) {
            else => return false,
            inline .elf2, .coff2 => |tag| {
                dev.check(tag.devFeature());
                return @as(*tag.Type(), @fieldParentPtr("base", base)).idle(tid);
            },
        }
    }

    pub fn updateErrorData(base: *File, pt: Zcu.PerThread) Error!void {
        switch (base.tag) {
            else => {},
            inline .elf2, .coff2 => |tag| {
                dev.check(tag.devFeature());
                return @as(*tag.Type(), @fieldParentPtr("base", base)).updateErrorData(pt);
            },
        }
    }

    /// 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) Error!void {
        const comp = base.comp;
        const io = comp.io;
        if (comp.clang_preprocessor_mode == .yes or comp.clang_preprocessor_mode == .pch) {
            dev.check(.clang_command);
            const emit = base.emit;
            // TODO: avoid extra link step when it's just 1 object file (the `zig cc -c` case)
            // Until then, we do `lld -r -o output.o input.o` even though the output is the same
            // as the input. For the preprocessing case (`zig cc -E -o foo`) we copy the file
            // to the final location. See also the corresponding TODO in Coff linking.
            assert(comp.c_object_table.count() == 1);
            const the_key = comp.c_object_table.keys()[0];
            const cached_pp_file_path = the_key.status.success.object_path;
            Io.Dir.copyFile(
                cached_pp_file_path.root_dir.handle,
                cached_pp_file_path.sub_path,
                emit.root_dir.handle,
                emit.sub_path,
                io,
                .{},
            ) catch |err| {
                const diags = &base.comp.link_diags;
                return diags.fail("failed to copy '{f}' to '{f}': {t}", .{
                    std.fmt.alt(@as(Path, cached_pp_file_path), .formatEscapeChar),
                    std.fmt.alt(@as(Path, emit), .formatEscapeChar),
                    err,
                });
            };
            return;
        }
        assert(base.post_prelink);
        switch (base.tag) {
            .plan9 => unreachable,
            inline else => |tag| {
                dev.check(tag.devFeature());
                return @as(*tag.Type(), @fieldParentPtr("base", base)).flush(arena, tid, prog_node);
            },
        }
    }

    /// This is called for every exported thing. `exports` is almost always
    /// a list of size 1, meaning that `exported` is exported once. However, it is possible
    /// to export the same thing with multiple different symbol names (aliases).
    /// May be called before or after updateDecl for any given Decl.
    /// Never called when LLVM is codegenning the ZCU.
    pub fn updateExports(
        base: *File,
        pt: Zcu.PerThread,
        exported: Zcu.Exported,
        export_indices: []const Zcu.Export.Index,
    ) Error!void {
        assert(base.comp.zcu.?.llvm_object == null);
        switch (base.tag) {
            .lld => unreachable,
            .plan9 => unreachable,
            inline else => |tag| {
                dev.check(tag.devFeature());
                return @as(*tag.Type(), @fieldParentPtr("base", base)).updateExports(pt, exported, export_indices);
            },
        }
    }

    pub const RelocInfo = struct {
        parent: Parent,
        offset: u64,
        addend: u32,

        pub const Parent = union(enum) {
            none,
            atom_index: AtomId,
            debug_output: DebugInfoOutput,
        };
    };

    /// Get allocated `Nav`'s address in virtual memory.
    /// The linker is passed information about the containing atom, `parent_atom_index`, and offset within it's
    /// memory buffer, `offset`, so that it can make a note of potential relocation sites, should the
    /// `Nav`'s address was not yet resolved, or the containing atom gets moved in virtual memory.
    /// May be called before or after updateFunc/updateNav therefore it is up to the linker to allocate
    /// the block/atom.
    /// Never called when LLVM is codegenning the ZCU.
    pub fn getNavVAddr(base: *File, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index, reloc_info: RelocInfo) Error!u64 {
        assert(base.comp.zcu.?.llvm_object == null);
        switch (base.tag) {
            .lld => unreachable,
            .c => unreachable,
            .spirv => unreachable,
            .wasm => unreachable,
            .plan9 => unreachable,
            inline else => |tag| {
                dev.check(tag.devFeature());
                return @as(*tag.Type(), @fieldParentPtr("base", base)).getNavVAddr(pt, nav_index, reloc_info);
            },
        }
    }

    /// Never called when LLVM is codegenning the ZCU.
    pub fn lowerUav(
        base: *File,
        pt: Zcu.PerThread,
        decl_val: InternPool.Index,
        decl_align: InternPool.Alignment,
    ) Error!SymbolId {
        assert(base.comp.zcu.?.llvm_object == null);
        switch (base.tag) {
            .lld => unreachable,
            .c => unreachable,
            .spirv => unreachable,
            .wasm => unreachable,
            .plan9 => unreachable,
            inline else => |tag| {
                dev.check(tag.devFeature());
                return @as(*tag.Type(), @fieldParentPtr("base", base)).lowerUav(pt, decl_val, decl_align);
            },
        }
    }

    /// Never called when LLVM is codegenning the ZCU.
    pub fn getUavVAddr(base: *File, decl_val: InternPool.Index, reloc_info: RelocInfo) Error!u64 {
        assert(base.comp.zcu.?.llvm_object == null);
        switch (base.tag) {
            .lld => unreachable,
            .c => unreachable,
            .spirv => unreachable,
            .wasm => unreachable,
            .plan9 => unreachable,
            inline else => |tag| {
                dev.check(tag.devFeature());
                return @as(*tag.Type(), @fieldParentPtr("base", base)).getUavVAddr(decl_val, reloc_info);
            },
        }
    }

    /// Never called when LLVM is codegenning the ZCU.
    pub fn deleteExport(
        base: *File,
        exported: Zcu.Exported,
        name: InternPool.NullTerminatedString,
    ) void {
        assert(base.comp.zcu.?.llvm_object == null);
        switch (base.tag) {
            .lld => unreachable,
            .plan9 => unreachable,

            .spirv,
            => {},

            inline else => |tag| {
                dev.check(tag.devFeature());
                return @as(*tag.Type(), @fieldParentPtr("base", base)).deleteExport(exported, name);
            },
        }
    }

    /// 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 io = base.comp.io;
        const diags = &base.comp.link_diags;
        const input = try openObjectInput(io, diags, path);
        errdefer input.object.file.close(io);
        try loadInput(base, input);
    }

    /// Opens a path as a static library and parses it into the linker.
    fn openLoadArchive(base: *File, path: Path, must_link: bool) anyerror!void {
        if (base.tag == .lld) return;
        const io = base.comp.io;
        const archive = try openObject(io, path, must_link, false);
        errdefer archive.file.close(io);
        try loadInput(base, .{ .archive = archive });
    }

    /// Opens a path as a static library and parses it into the linker. Allows GNU ld scripts.
    fn openLoadArchiveQuery(base: *File, path: Path, query: UnresolvedInput.Query) anyerror!void {
        if (base.tag == .lld) return;
        const io = base.comp.io;
        const archive = try openObject(io, path, query.must_link, query.hidden);
        errdefer archive.file.close(io);
        loadInput(base, .{ .archive = archive }) catch |err| switch (err) {
            error.BadMagic, error.UnexpectedEndOfFile => {
                if (base.tag != .elf and base.tag != .elf2) return err;
                try loadGnuLdScript(base, path, query, archive.file);
                archive.file.close(io);
                return;
            },
            else => return err,
        };
    }

    /// 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 io = base.comp.io;
        const dso = try openDso(io, path, query.needed, query.weak, query.reexport);
        errdefer dso.file.close(io);
        loadInput(base, .{ .dso = dso }) catch |err| switch (err) {
            error.BadMagic, error.UnexpectedEndOfFile => {
                if (base.tag != .elf and base.tag != .elf2) return err;
                try loadGnuLdScript(base, path, query, dso.file);
                dso.file.close(io);
                return;
            },
            else => return err,
        };
    }

    fn loadGnuLdScript(base: *File, path: Path, parent_query: UnresolvedInput.Query, file: Io.File) anyerror!void {
        const comp = base.comp;
        const io = comp.io;
        const diags = &comp.link_diags;
        const gpa = comp.gpa;
        const stat = try file.stat(io);
        const size = std.math.cast(u32, stat.size) orelse return error.FileTooBig;
        const buf = try gpa.alloc(u8, size);
        defer gpa.free(buf);
        const n = try file.readPositionalAll(io, buf, 0);
        if (buf.len != n) return error.UnexpectedEndOfFile;
        var ld_script = try LdScript.parse(gpa, diags, path, buf);
        defer ld_script.deinit(gpa);
        for (ld_script.args) |arg| {
            const query: UnresolvedInput.Query = .{
                .needed = arg.needed or parent_query.needed,
                .weak = parent_query.weak,
                .reexport = parent_query.reexport,
                .preferred_mode = parent_query.preferred_mode,
                .search_strategy = parent_query.search_strategy,
                .allow_so_scripts = parent_query.allow_so_scripts,
            };
            if (mem.startsWith(u8, arg.path, "-l")) {
                @panic("TODO");
            } else {
                if (fs.path.isAbsolute(arg.path)) {
                    const new_path = Path.initCwd(path: {
                        comp.mutex.lockUncancelable(io);
                        defer comp.mutex.unlock(io);
                        break :path try comp.arena.dupe(u8, arg.path);
                    });
                    switch (Compilation.classifyFileExt(arg.path)) {
                        .shared_library => try openLoadDso(base, new_path, query),
                        .object => try openLoadObject(base, new_path),
                        .static_library => try openLoadArchiveQuery(base, new_path, query),
                        else => diags.addParseError(path, "GNU ld script references file with unrecognized extension: {s}", .{arg.path}),
                    }
                } else {
                    @panic("TODO");
                }
            }
        }
    }

    pub fn loadInput(base: *File, input: Input) anyerror!void {
        if (base.tag == .lld) return;
        assert(!base.post_prelink);
        switch (base.tag) {
            inline .elf, .elf2, .wasm => |tag| {
                dev.check(tag.devFeature());
                return @as(*tag.Type(), @fieldParentPtr("base", base)).loadInput(input);
            },
            else => {},
        }
    }

    /// Called when all linker inputs have been sent via `loadInput`. After
    /// this, `loadInput` will not be called anymore.
    pub fn prelink(base: *File) Error!void {
        // The guard on this assertion is a temporary hack to make the LLVM backend with LLD work with
        // `-fincremental`. This works only because `File.Lld` does nothing in prelink.
        // Related: https://codeberg.org/ziglang/zig/issues/32081
        if (base.tag != .lld) {
            assert(!base.post_prelink);
        }

        switch (base.tag) {
            inline .elf2, .coff2, .wasm => |tag| {
                dev.check(tag.devFeature());
                try @as(*tag.Type(), @fieldParentPtr("base", base)).prelink(base.comp.link_prog_node);
            },
            else => {},
        }

        base.post_prelink = true;
    }

    /// Legacy function for old linker code
    pub fn copyRangeAll(base: *File, old_offset: u64, new_offset: u64, size: u64) !void {
        const comp = base.comp;
        const io = comp.io;
        const file = base.file.?;
        return copyRangeAll2(io, file, file, old_offset, new_offset, size);
    }

    /// Legacy function for old linker code
    pub fn copyRangeAll2(io: Io, src_file: Io.File, dst_file: Io.File, old_offset: u64, new_offset: u64, size: u64) !void {
        var write_buffer: [2048]u8 = undefined;
        var file_reader = src_file.reader(io, &.{});
        file_reader.pos = old_offset;
        var file_writer = dst_file.writer(io, &write_buffer);
        file_writer.pos = new_offset;
        const size_u = std.math.cast(usize, size) orelse return error.Overflow;
        const n = file_writer.interface.sendFileAll(&file_reader, .limited(size_u)) catch |err| switch (err) {
            error.ReadFailed => switch (file_reader.err.?) {
                error.ConnectionResetByPeer => return error.Unexpected, // not a socket
                error.SocketUnconnected => return error.Unexpected, // not a socket
                else => |e| return e,
            },
            error.WriteFailed => return file_writer.err.?,
        };
        assert(n == size_u);
        file_writer.interface.flush() catch |err| switch (err) {
            error.WriteFailed => return file_writer.err.?,
        };
    }

    pub const Tag = enum {
        coff2,
        elf,
        elf2,
        macho,
        c,
        wasm,
        spirv,
        plan9,
        lld,

        pub fn Type(comptime tag: Tag) type {
            return switch (tag) {
                .coff2 => Coff2,
                .elf => Elf,
                .elf2 => Elf2,
                .macho => MachO,
                .c => C,
                .wasm => Wasm,
                .spirv => SpirV,
                .lld => Lld,
                .plan9 => comptime unreachable,
            };
        }

        fn fromObjectFormat(ofmt: std.Target.ObjectFormat, use_new_linker: bool) Tag {
            return switch (ofmt) {
                .coff => .coff2,
                .elf => if (use_new_linker) .elf2 else .elf,
                .macho => .macho,
                .wasm => .wasm,
                .plan9 => .plan9,
                .c => .c,
                .spirv => .spirv,
                .hex => @panic("TODO implement hex object format"),
                .raw => @panic("TODO implement raw object format"),
            };
        }

        pub fn devFeature(tag: Tag) dev.Feature {
            return @field(dev.Feature, @tagName(tag) ++ "_linker");
        }
    };

    pub const LazySymbol = struct {
        pub const Kind = enum { code, const_data };

        kind: Kind,
        ty: InternPool.Index,
    };

    pub fn determinePermissions(
        output_mode: std.lang.OutputMode,
        link_mode: std.lang.LinkMode,
    ) Io.File.Permissions {
        // On common systems with a 0o022 umask, 0o777 will still result in a file created
        // with 0o755 permissions, but it works appropriately if the system is configured
        // more leniently. As another data point, C's fopen seems to open files with the
        // 666 mode.
        const executable_mode: Io.File.Permissions = if (builtin.target.os.tag == .windows)
            .default_file
        else
            .fromMode(0o777);

        switch (output_mode) {
            .Lib => return switch (link_mode) {
                .dynamic => executable_mode,
                .static => .default_file,
            },
            .Exe => return executable_mode,
            .Obj => return .default_file,
        }
    }

    pub fn isStatic(self: File) bool {
        return self.comp.config.link_mode == .static;
    }

    pub fn isObject(self: File) bool {
        const output_mode = self.comp.config.output_mode;
        return output_mode == .Obj;
    }

    pub fn isExe(self: File) bool {
        const output_mode = self.comp.config.output_mode;
        return output_mode == .Exe;
    }

    pub fn isStaticLib(self: File) bool {
        const output_mode = self.comp.config.output_mode;
        return output_mode == .Lib and self.isStatic();
    }

    pub fn isRelocatable(self: File) bool {
        return self.isObject() or self.isStaticLib();
    }

    pub fn isDynLib(self: File) bool {
        const output_mode = self.comp.config.output_mode;
        return output_mode == .Lib and !self.isStatic();
    }

    pub fn cgFail(
        base: *File,
        nav_index: InternPool.Nav.Index,
        comptime format: []const u8,
        args: anytype,
    ) Zcu.CodegenFailError {
        @branchHint(.cold);
        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 Coff2 = @import("link/Coff.zig");
    pub const Elf = @import("link/Elf.zig");
    pub const Elf2 = @import("link/Elf2.zig");
    pub const MachO = @import("link/MachO.zig");
    pub const SpirV = @import("link/SpirV.zig");
    pub const Wasm = @import("link/Wasm.zig");
    pub const Dwarf = @import("link/Dwarf.zig");
};

pub const PrelinkTask = union(enum) {
    /// Loads the objects, shared objects, and archives that are already
    /// known from the command line.
    load_explicitly_provided,
    /// Loads the shared objects and archives by resolving
    /// `target_util.libcFullLinkFlags()` against the host libc
    /// installation.
    load_host_libc,
    /// Tells the linker to load an object file by path.
    load_object: Path,
    /// Tells the linker to load a static library by path.
    load_archive: struct {
        path: Path,
        must_link: bool,
    },
    /// Tells the linker to load a shared library, possibly one that is a
    /// GNU ld script.
    load_dso: Path,
};
pub const ZcuTask = union(enum) {
    /// Write the constant value for a Decl to the output file.
    link_nav: InternPool.Nav.Index,
    /// Write the machine code for a function to the output file.
    link_func: Zcu.CodegenTaskPool.Index,
    /// This struct/union/enum type has finished type resolution (successfully or otherwise), so the
    /// linker can now lower debug information for this type (and any structural types which depend
    /// on it, such as `?T`, `struct { T }`, `[2]T`, etc).
    debug_update_container_type: struct {
        ty: InternPool.Index,
        success: bool,
    },
    debug_update_line_number: InternPool.TrackedInst.Index,
};

pub fn doPrelinkTask(comp: *Compilation, task: PrelinkTask) void {
    const io = comp.io;
    const diags = &comp.link_diags;
    const base = comp.bin_file orelse {
        comp.link_prog_node.completeOne();
        return;
    };

    // The guard on this assertion is a temporary hack to make the LLVM backend with LLD work with
    // `-fincremental`. This works only because `File.Lld` does nothing in prelink.
    // Related: https://codeberg.org/ziglang/zig/issues/32081
    if (base.tag != .lld) {
        assert(!base.post_prelink);
    }

    var timer = comp.startTimer();
    defer if (timer.finish(io)) |ns| {
        comp.mutex.lockUncancelable(io);
        defer comp.mutex.unlock(io);
        comp.time_report.?.stats.cpu_ns_link += ns;
    };

    switch (task) {
        .load_explicitly_provided => {
            const prog_node = comp.link_prog_node.start("Parse Inputs", comp.link_inputs.len);
            defer prog_node.end();
            for (comp.link_inputs) |input| {
                base.loadInput(input) catch |err| switch (err) {
                    error.AlreadyReported => return, // error reported via diags
                    else => |e| switch (input) {
                        .dso => |dso| diags.addParseError(dso.path, "failed to parse shared library: {s}", .{@errorName(e)}),
                        .object => |obj| diags.addParseError(obj.path, "failed to parse object: {s}", .{@errorName(e)}),
                        .archive => |obj| diags.addParseError(obj.path, "failed to parse archive: {s}", .{@errorName(e)}),
                        .res => |res| diags.addParseError(res.path, "failed to parse Windows resource: {s}", .{@errorName(e)}),
                        .dso_exact => diags.addError("failed to handle dso_exact: {s}", .{@errorName(e)}),
                    },
                };
                prog_node.completeOne();
            }
        },
        .load_host_libc => {
            const prog_node = comp.link_prog_node.start("Parse Host libc", 0);
            defer prog_node.end();

            const target = &comp.root_mod.resolved_target.result;
            const flags = target_util.libcFullLinkFlags(target);
            const crt_dir = comp.libc_installation.?.crt_dir.?;
            const sep = std.fs.path.sep_str;
            for (flags) |flag| {
                assert(mem.startsWith(u8, flag, "-l"));
                const lib_name = flag["-l".len..];
                switch (comp.config.link_mode) {
                    .dynamic => {
                        const dso_path = Path.initCwd(
                            std.fmt.allocPrint(comp.arena, "{s}" ++ sep ++ "{s}{s}{s}", .{
                                crt_dir, target.libPrefix(), lib_name, target.dynamicLibSuffix(),
                            }) catch return diags.setAllocFailure(),
                        );
                        base.openLoadDso(dso_path, .{
                            .preferred_mode = .dynamic,
                            .search_strategy = .paths_first,
                        }) catch |err| switch (err) {
                            error.FileNotFound => {
                                // Also try static.
                                const archive_path = Path.initCwd(
                                    std.fmt.allocPrint(comp.arena, "{s}" ++ sep ++ "{s}{s}{s}", .{
                                        crt_dir, target.libPrefix(), lib_name, target.staticLibSuffix(),
                                    }) catch return diags.setAllocFailure(),
                                );
                                base.openLoadArchiveQuery(archive_path, .{
                                    .preferred_mode = .dynamic,
                                    .search_strategy = .paths_first,
                                }) catch |archive_err| switch (archive_err) {
                                    error.AlreadyReported => return, // error reported via diags
                                    else => |e| diags.addParseError(dso_path, "failed to parse archive {f}: {s}", .{ archive_path, @errorName(e) }),
                                };
                            },
                            error.AlreadyReported => return, // error reported via diags
                            else => |e| diags.addParseError(dso_path, "failed to parse shared library: {s}", .{@errorName(e)}),
                        };
                    },
                    .static => {
                        const path = Path.initCwd(
                            std.fmt.allocPrint(comp.arena, "{s}" ++ sep ++ "{s}{s}{s}", .{
                                crt_dir, target.libPrefix(), lib_name, target.staticLibSuffix(),
                            }) catch return diags.setAllocFailure(),
                        );
                        // glibc sometimes makes even archive files GNU ld scripts.
                        base.openLoadArchiveQuery(path, .{
                            .preferred_mode = .static,
                            .search_strategy = .no_fallback,
                        }) catch |err| switch (err) {
                            error.AlreadyReported => return, // error reported via diags
                            else => |e| diags.addParseError(path, "failed to parse archive: {s}", .{@errorName(e)}),
                        };
                    },
                }
            }
        },
        .load_object => |path| {
            const prog_node = comp.link_prog_node.start("Parse Object", 0);
            defer prog_node.end();
            base.openLoadObject(path) catch |err| switch (err) {
                error.AlreadyReported => return, // error reported via diags
                else => |e| diags.addParseError(path, "failed to parse object: {s}", .{@errorName(e)}),
            };
        },
        .load_archive => |load_archive| {
            const prog_node = comp.link_prog_node.start("Parse Archive", 0);
            defer prog_node.end();
            base.openLoadArchive(load_archive.path, load_archive.must_link) catch |err| switch (err) {
                error.AlreadyReported => return, // error reported via link_diags
                else => |e| diags.addParseError(load_archive.path, "failed to parse archive: {s}", .{@errorName(e)}),
            };
        },
        .load_dso => |path| {
            const prog_node = comp.link_prog_node.start("Parse Shared Library", 0);
            defer prog_node.end();
            base.openLoadDso(path, .{
                .preferred_mode = .dynamic,
                .search_strategy = .paths_first,
            }) catch |err| switch (err) {
                error.AlreadyReported => return, // error reported via link_diags
                else => |e| diags.addParseError(path, "failed to parse shared library: {s}", .{@errorName(e)}),
            };
        },
    }
}
pub fn doZcuTask(comp: *Compilation, tid: Zcu.PerThread.Id, task: ZcuTask) void {
    const io = comp.io;
    const diags = &comp.link_diags;
    const zcu = comp.zcu.?;
    const ip = &zcu.intern_pool;
    const active = zcu.activate(tid);
    defer active.deactivate();
    const pt = active.pt;

    var timer = comp.startTimer();

    const maybe_nav: ?InternPool.Nav.Index = switch (task) {
        .link_nav => |nav_index| nav: {
            const fqn_slice = ip.getNav(nav_index).fqn.toSlice(ip);
            const nav_prog_node = comp.link_prog_node.start(fqn_slice, 0);
            defer nav_prog_node.end();
            if (zcu.llvm_object) |llvm_object| {
                llvm_object.updateNav(pt, nav_index) catch |err| switch (err) {
                    error.OutOfMemory => diags.setAllocFailure(),
                };
            } else if (comp.bin_file) |lf| {
                lf.updateNav(pt, nav_index) catch |err| switch (err) {
                    error.Canceled => io.recancel(),
                    error.AlreadyReported => return,
                    error.OutOfMemory => diags.setAllocFailure(),
                };
            }
            break :nav nav_index;
        },
        .link_func => |codegen_task| nav: {
            timer.pause(io);
            const func, var mir = codegen_task.wait(&zcu.codegen_task_pool, zcu) catch |err| switch (err) {
                error.Canceled, error.AlreadyReported => {
                    comp.link_prog_node.completeOne();
                    return;
                },
            };
            defer mir.deinit(zcu);
            timer.@"resume"(io);

            const nav = zcu.funcInfo(func).owner_nav;
            const fqn_slice = ip.getNav(nav).fqn.toSlice(ip);

            const nav_prog_node = comp.link_prog_node.start(fqn_slice, 0);
            defer nav_prog_node.end();

            assert(zcu.llvm_object == null); // LLVM codegen doesn't produce MIR
            if (comp.bin_file) |lf| {
                lf.updateFunc(pt, func, &mir) catch |err| switch (err) {
                    error.Canceled => io.recancel(),
                    error.AlreadyReported => return,
                    error.OutOfMemory => return diags.setAllocFailure(),
                };
            }
            break :nav ip.indexToKey(func).func.owner_nav;
        },
        .debug_update_container_type => |container_update| nav: {
            const name = Type.fromInterned(container_update.ty).containerTypeName(ip).toSlice(ip);
            const ty_prog_node = comp.link_prog_node.start(name, 0);
            defer ty_prog_node.end();
            if (zcu.llvm_object) |llvm_object| {
                llvm_object.updateContainerType(pt, container_update.ty, container_update.success) catch |err| switch (err) {
                    error.OutOfMemory => diags.setAllocFailure(),
                };
            } else {
                if (comp.bin_file) |lf| {
                    lf.updateContainerType(pt, container_update.ty, container_update.success) catch |err| switch (err) {
                        error.OutOfMemory => diags.setAllocFailure(),
                        error.Canceled => io.recancel(),
                        error.AlreadyReported => {},
                    };
                }
            }
            break :nav null;
        },
        .debug_update_line_number => |ti| nav: {
            const nav_prog_node = comp.link_prog_node.start("Update line number", 0);
            defer nav_prog_node.end();
            if (pt.zcu.llvm_object == null) {
                if (comp.bin_file) |lf| {
                    lf.updateLineNumber(pt, ti) catch |err| switch (err) {
                        error.OutOfMemory => diags.setAllocFailure(),
                        else => |e| log.err("update line number failed: {s}", .{@errorName(e)}),
                    };
                }
            }
            break :nav null;
        },
    };

    if (timer.finish(io)) |ns_link| report_time: {
        comp.mutex.lockUncancelable(io);
        defer comp.mutex.unlock(io);
        const tr = &zcu.comp.time_report.?;
        tr.stats.cpu_ns_link += ns_link;
        if (maybe_nav) |nav| {
            const zir_decl = ip.getNav(nav).srcInst(ip);
            const gop = tr.decl_link_ns.getOrPut(zcu.gpa, zir_decl) catch |err| switch (err) {
                error.OutOfMemory => {
                    zcu.comp.setAllocFailure();
                    break :report_time;
                },
            };
            if (!gop.found_existing) gop.value_ptr.* = 0;
            gop.value_ptr.* += ns_link;
        }
    }
}
pub fn doIdleTask(comp: *Compilation, tid: Zcu.PerThread.Id) Error!bool {
    return if (comp.bin_file) |lf| lf.idle(tid) else false;
}
/// After the main pipeline is done, but before flush, the compilation may need to link one final
/// `Nav` into the binary: the `builtin.test_functions` value. Since the link thread isn't running
/// by then, we expose this function which can be called directly.
pub fn linkTestFunctionsNav(pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) void {
    const zcu = pt.zcu;
    const comp = zcu.comp;
    const diags = &comp.link_diags;
    if (zcu.llvm_object) |llvm_object| {
        llvm_object.updateNav(pt, nav_index) catch |err| switch (err) {
            error.OutOfMemory => diags.setAllocFailure(),
        };
    } else if (comp.bin_file) |lf| {
        lf.updateNav(pt, nav_index) catch |err| switch (err) {
            error.Canceled => comp.io.recancel(),
            error.AlreadyReported => return,
            error.OutOfMemory => diags.setAllocFailure(),
        };
    }
}
pub fn updateErrorData(pt: Zcu.PerThread) void {
    const comp = pt.zcu.comp;
    if (comp.bin_file) |lf| lf.updateErrorData(pt) catch |err| switch (err) {
        error.OutOfMemory => comp.link_diags.setAllocFailure(),
        error.Canceled => comp.io.recancel(),
        error.AlreadyReported => {},
    };
}

/// Provided by the CLI, processed into `LinkInput` instances at the start of
/// the compilation pipeline.
pub const UnresolvedInput = union(enum) {
    /// A library name that could potentially be dynamic or static depending on
    /// query parameters, resolved according to library directories.
    /// This could potentially resolve to a GNU ld script, resulting in more
    /// library dependencies.
    name_query: NameQuery,
    /// When a file path is provided, query info is still needed because the
    /// path may point to a .so file which may actually be a GNU ld script that
    /// references library names which need to be resolved.
    path_query: PathQuery,
    /// Strings that come from GNU ld scripts. Is it a filename? Is it a path?
    /// Who knows! Fuck around and find out.
    ambiguous_name: NameQuery,
    /// Put exactly this string in the dynamic section, no rpath.
    dso_exact: Input.DsoExact,

    pub const NameQuery = struct {
        name: []const u8,
        query: Query,
    };

    pub const PathQuery = struct {
        path: Path,
        query: Query,
    };

    pub const Query = struct {
        needed: bool = false,
        weak: bool = false,
        reexport: bool = false,
        must_link: bool = false,
        hidden: bool = false,
        allow_so_scripts: bool = false,
        preferred_mode: std.lang.LinkMode,
        search_strategy: SearchStrategy,

        fn fallbackMode(q: Query) std.lang.LinkMode {
            assert(q.search_strategy != .no_fallback);
            return switch (q.preferred_mode) {
                .dynamic => .static,
                .static => .dynamic,
            };
        }
    };

    pub const SearchStrategy = enum {
        paths_first,
        mode_first,
        no_fallback,
    };
};

pub const Input = union(enum) {
    object: Object,
    archive: Object,
    res: Res,
    /// May not be a GNU ld script. Those are resolved when converting from
    /// `UnresolvedInput` to `Input` values.
    dso: Dso,
    dso_exact: DsoExact,

    pub const Object = struct {
        path: Path,
        file: Io.File,
        must_link: bool,
        hidden: bool,
    };

    pub const Res = struct {
        path: Path,
        file: Io.File,
    };

    pub const Dso = struct {
        path: Path,
        file: Io.File,
        needed: bool,
        weak: bool,
        reexport: bool,
    };

    pub const DsoExact = struct {
        /// Includes the ":" prefix. This is intended to be put into the DSO
        /// section verbatim with no corresponding rpaths.
        name: []const u8,
    };

    /// Returns `null` in the case of `dso_exact`.
    pub fn path(input: Input) ?Path {
        return switch (input) {
            .object, .archive => |obj| obj.path,
            inline .res, .dso => |x| x.path,
            .dso_exact => null,
        };
    }

    /// Returns `null` in the case of `dso_exact`.
    pub fn pathAndFile(input: Input) ?struct { Path, Io.File } {
        return switch (input) {
            .object, .archive => |obj| .{ obj.path, obj.file },
            inline .res, .dso => |x| .{ x.path, x.file },
            .dso_exact => null,
        };
    }

    pub fn taskName(input: Input) []const u8 {
        return switch (input) {
            .object, .archive => |obj| obj.path.basename(),
            inline .res, .dso => |x| x.path.basename(),
            .dso_exact => "dso_exact",
        };
    }
};

pub fn hashInputs(man: *Cache.Manifest, link_inputs: []const Input) !void {
    for (link_inputs) |link_input| {
        man.hash.add(@as(@typeInfo(Input).@"union".tag_type.?, link_input));
        switch (link_input) {
            .object, .archive => |obj| {
                _ = try man.addOpenedFile(obj.path, obj.file, null);
                man.hash.add(obj.must_link);
                man.hash.add(obj.hidden);
            },
            .res => |res| {
                _ = try man.addOpenedFile(res.path, res.file, null);
            },
            .dso => |dso| {
                _ = try man.addOpenedFile(dso.path, dso.file, null);
                man.hash.add(dso.needed);
                man.hash.add(dso.weak);
                man.hash.add(dso.reexport);
            },
            .dso_exact => |dso_exact| {
                man.hash.addBytes(dso_exact.name);
            },
        }
    }
}

pub fn resolveInputs(
    gpa: Allocator,
    arena: Allocator,
    io: Io,
    target: *const std.Target,
    /// This function mutates this array but does not take ownership.
    /// Allocated with `gpa`.
    unresolved_inputs: *std.ArrayList(UnresolvedInput),
    /// Allocated with `gpa`.
    resolved_inputs: *std.ArrayList(Input),
    lib_directories: []const Cache.Directory,
    color: std.zig.Color,
) Allocator.Error!void {
    var checked_paths: std.ArrayList(u8) = .empty;
    defer checked_paths.deinit(gpa);

    var ld_script_bytes: std.ArrayList(u8) = .empty;
    defer ld_script_bytes.deinit(gpa);

    var archive_dedup: ArchiveDedupMap = .empty;
    defer archive_dedup.deinit(gpa);

    var failed_libs: std.ArrayList(struct {
        name: []const u8,
        strategy: UnresolvedInput.SearchStrategy,
        checked_paths: []const u8,
        preferred_mode: std.lang.LinkMode,
    }) = .empty;

    // Convert external system libs into a stack so that items can be
    // pushed to it.
    //
    // This is necessary because shared objects might turn out to be
    // "linker scripts" that in fact resolve to one or more other
    // external system libs, including parameters such as "needed".
    //
    // Unfortunately, such files need to be detected immediately, so
    // that this library search logic can be applied to them.
    mem.reverse(UnresolvedInput, unresolved_inputs.items);

    syslib: while (unresolved_inputs.pop()) |unresolved_input| {
        switch (unresolved_input) {
            .name_query => |name_query| {
                const query = name_query.query;

                // Checked in the first pass above while looking for libc libraries.
                assert(!fs.path.isAbsolute(name_query.name));

                checked_paths.clearRetainingCapacity();

                switch (query.search_strategy) {
                    .mode_first, .no_fallback => {
                        // check for preferred mode
                        for (lib_directories) |lib_directory| switch (try resolveLibInput(
                            gpa,
                            arena,
                            io,
                            unresolved_inputs,
                            resolved_inputs,
                            &checked_paths,
                            &ld_script_bytes,
                            &archive_dedup,
                            lib_directory,
                            name_query,
                            target,
                            query.preferred_mode,
                            color,
                        )) {
                            .ok => continue :syslib,
                            .no_match => {},
                        };
                        // check for fallback mode
                        if (query.search_strategy == .no_fallback) {
                            try failed_libs.append(arena, .{
                                .name = name_query.name,
                                .strategy = query.search_strategy,
                                .checked_paths = try arena.dupe(u8, checked_paths.items),
                                .preferred_mode = query.preferred_mode,
                            });
                            continue :syslib;
                        }
                        for (lib_directories) |lib_directory| switch (try resolveLibInput(
                            gpa,
                            arena,
                            io,
                            unresolved_inputs,
                            resolved_inputs,
                            &checked_paths,
                            &ld_script_bytes,
                            &archive_dedup,
                            lib_directory,
                            name_query,
                            target,
                            query.fallbackMode(),
                            color,
                        )) {
                            .ok => continue :syslib,
                            .no_match => {},
                        };
                        try failed_libs.append(arena, .{
                            .name = name_query.name,
                            .strategy = query.search_strategy,
                            .checked_paths = try arena.dupe(u8, checked_paths.items),
                            .preferred_mode = query.preferred_mode,
                        });
                        continue :syslib;
                    },
                    .paths_first => {
                        for (lib_directories) |lib_directory| {
                            // check for preferred mode
                            switch (try resolveLibInput(
                                gpa,
                                arena,
                                io,
                                unresolved_inputs,
                                resolved_inputs,
                                &checked_paths,
                                &ld_script_bytes,
                                &archive_dedup,
                                lib_directory,
                                name_query,
                                target,
                                query.preferred_mode,
                                color,
                            )) {
                                .ok => continue :syslib,
                                .no_match => {},
                            }

                            // check for fallback mode
                            switch (try resolveLibInput(
                                gpa,
                                arena,
                                io,
                                unresolved_inputs,
                                resolved_inputs,
                                &checked_paths,
                                &ld_script_bytes,
                                &archive_dedup,
                                lib_directory,
                                name_query,
                                target,
                                query.fallbackMode(),
                                color,
                            )) {
                                .ok => continue :syslib,
                                .no_match => {},
                            }
                        }
                        try failed_libs.append(arena, .{
                            .name = name_query.name,
                            .strategy = query.search_strategy,
                            .checked_paths = try arena.dupe(u8, checked_paths.items),
                            .preferred_mode = query.preferred_mode,
                        });
                        continue :syslib;
                    },
                }
            },
            .ambiguous_name => |an| {
                // First check the path relative to the current working directory.
                // If the file is a library and is not found there, check the library search paths as well.
                // This is consistent with the behavior of GNU ld.
                if (try resolvePathInput(
                    gpa,
                    arena,
                    io,
                    unresolved_inputs,
                    resolved_inputs,
                    &ld_script_bytes,
                    &archive_dedup,
                    target,
                    .{
                        .path = Path.initCwd(an.name),
                        .query = an.query,
                    },
                    color,
                )) |lib_result| {
                    switch (lib_result) {
                        .ok => continue :syslib,
                        .no_match => {
                            for (lib_directories) |lib_directory| {
                                switch ((try resolvePathInput(
                                    gpa,
                                    arena,
                                    io,
                                    unresolved_inputs,
                                    resolved_inputs,
                                    &ld_script_bytes,
                                    &archive_dedup,
                                    target,
                                    .{
                                        .path = .{
                                            .root_dir = lib_directory,
                                            .sub_path = an.name,
                                        },
                                        .query = an.query,
                                    },
                                    color,
                                )).?) {
                                    .ok => continue :syslib,
                                    .no_match => {},
                                }
                            }
                            fatal("{s}: file listed in linker script not found", .{an.name});
                        },
                    }
                }
                continue;
            },
            .path_query => |pq| {
                if (try resolvePathInput(
                    gpa,
                    arena,
                    io,
                    unresolved_inputs,
                    resolved_inputs,
                    &ld_script_bytes,
                    &archive_dedup,
                    target,
                    pq,
                    color,
                )) |lib_result| {
                    switch (lib_result) {
                        .ok => {},
                        .no_match => fatal("{f}: file not found", .{pq.path}),
                    }
                }
                continue;
            },
            .dso_exact => |dso_exact| {
                try resolved_inputs.append(gpa, .{ .dso_exact = dso_exact });
                continue;
            },
        }
        @compileError("unreachable");
    }

    if (failed_libs.items.len > 0) {
        for (failed_libs.items) |f| {
            const searched_paths = if (f.checked_paths.len == 0) " none" else f.checked_paths;
            std.log.err("unable to find {s} system library '{s}' using strategy '{s}'. searched paths:{s}", .{
                @tagName(f.preferred_mode), f.name, @tagName(f.strategy), searched_paths,
            });
        }
        std.process.exit(1);
    }
}

const ResolveLibInputResult = enum { ok, no_match };
const fatal = std.process.fatal;

fn resolveLibInput(
    gpa: Allocator,
    arena: Allocator,
    io: Io,
    /// Allocated via `gpa`.
    unresolved_inputs: *std.ArrayList(UnresolvedInput),
    /// Allocated via `gpa`.
    resolved_inputs: *std.ArrayList(Input),
    /// Allocated via `gpa`.
    checked_paths: *std.ArrayList(u8),
    /// Allocated via `gpa`.
    ld_script_bytes: *std.ArrayList(u8),
    /// Allocated via `gpa`.
    archive_dedup: *ArchiveDedupMap,
    lib_directory: Directory,
    name_query: UnresolvedInput.NameQuery,
    target: *const std.Target,
    link_mode: std.lang.LinkMode,
    color: std.zig.Color,
) Allocator.Error!ResolveLibInputResult {
    try resolved_inputs.ensureUnusedCapacity(gpa, 1);
    try archive_dedup.ensureUnusedCapacity(gpa, 1);

    const lib_name = name_query.name;

    if (target.os.tag.isDarwin() and link_mode == .dynamic) tbd: {
        // Prefer .tbd over .dylib.
        const test_path: Path = .{
            .root_dir = lib_directory,
            .sub_path = try std.fmt.allocPrint(arena, "lib{s}.tbd", .{lib_name}),
        };
        try checked_paths.print(gpa, "\n  {f}", .{test_path});
        var file = test_path.root_dir.handle.openFile(io, test_path.sub_path, .{}) catch |err| switch (err) {
            error.FileNotFound => break :tbd,
            else => |e| fatal("unable to search for tbd library '{f}': {s}", .{ test_path, @errorName(e) }),
        };
        errdefer file.close(io);
        return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, name_query.query);
    }

    {
        const test_path: Path = .{
            .root_dir = lib_directory,
            .sub_path = try std.fmt.allocPrint(arena, "{s}{s}{s}", .{
                target.libPrefix(), lib_name,
                switch (link_mode) {
                    .static => target.staticLibSuffix(),
                    .dynamic => target.dynamicLibSuffix(),
                },
            }),
        };
        try checked_paths.print(gpa, "\n  {f}", .{test_path});
        switch (try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, archive_dedup, target, .{
            .path = test_path,
            .query = name_query.query,
        }, link_mode, color)) {
            .no_match => {},
            .ok => return .ok,
        }
    }

    // In the case of Darwin, the main check will be .dylib, so here we
    // additionally check for .so files.
    if (target.os.tag.isDarwin() and link_mode == .dynamic) so: {
        const test_path: Path = .{
            .root_dir = lib_directory,
            .sub_path = try std.fmt.allocPrint(arena, "lib{s}.so", .{lib_name}),
        };
        try checked_paths.print(gpa, "\n  {f}", .{test_path});
        var file = test_path.root_dir.handle.openFile(io, test_path.sub_path, .{}) catch |err| switch (err) {
            error.FileNotFound => break :so,
            else => |e| fatal("unable to search for so library '{f}': {s}", .{
                test_path, @errorName(e),
            }),
        };
        errdefer file.close(io);
        return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, name_query.query);
    }

    // In the case of MinGW, the main check will be .lib but we also need to
    // look for `libfoo.a`.
    if (target.isMinGW() and link_mode == .static) mingw: {
        const test_path: Path = .{
            .root_dir = lib_directory,
            .sub_path = try std.fmt.allocPrint(arena, "lib{s}.a", .{lib_name}),
        };
        try checked_paths.print(gpa, "\n  {f}", .{test_path});
        var file = test_path.root_dir.handle.openFile(io, test_path.sub_path, .{}) catch |err| switch (err) {
            error.FileNotFound => break :mingw,
            else => |e| fatal("unable to search for static library '{f}': {s}", .{ test_path, @errorName(e) }),
        };
        errdefer file.close(io);
        return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, name_query.query);
    }

    return .no_match;
}

/// Deduplicates static archive link inputs based on their path. This is done for efficiency, so
/// that linker implementations do not need to open and scan the archive just to determine that they
/// need not extract any objects. At the time of writing, it also helps avoid "multiple definitions
/// of symbol" errors in incomplete linker implementations.
///
/// Key is index into `resolved_inputs` of an `Input.archive`.
///
/// Accessed through `ArchiveDedupAdapter`.
///
const ArchiveDedupMap = std.array_hash_map.Custom(u32, void, void, true);
/// Adapter for accessing `ArchiveDedupMap` with an effective key type of `Path`.
const ArchiveDedupAdapter = struct {
    resolved_inputs: []const Input,
    pub fn hash(ctx: ArchiveDedupAdapter, path: Path) u32 {
        _ = ctx;
        return Path.TableAdapter.hash(.{}, path);
    }
    pub fn eql(ctx: ArchiveDedupAdapter, a_path: Path, b_input_index: u32, _: usize) bool {
        const b_path = ctx.resolved_inputs[b_input_index].archive.path;
        return a_path.eql(b_path);
    }
};

fn finishResolveLibInput(
    io: Io,
    resolved_inputs: *std.ArrayList(Input),
    archive_dedup: *ArchiveDedupMap,
    path: Path,
    file: Io.File,
    link_mode: std.lang.LinkMode,
    query: UnresolvedInput.Query,
) ResolveLibInputResult {
    switch (link_mode) {
        .static => {
            const ctx: ArchiveDedupAdapter = .{ .resolved_inputs = resolved_inputs.items };
            const gop = archive_dedup.getOrPutAssumeCapacityAdapted(path, ctx);
            if (gop.found_existing) {
                // Ignore duplicate archive input
                file.close(io);
                return .ok;
            }
            gop.key_ptr.* = @intCast(resolved_inputs.items.len);
            resolved_inputs.appendAssumeCapacity(.{ .archive = .{
                .path = path,
                .file = file,
                .must_link = query.must_link,
                .hidden = query.hidden,
            } });
        },
        .dynamic => resolved_inputs.appendAssumeCapacity(.{ .dso = .{
            .path = path,
            .file = file,
            .needed = query.needed,
            .weak = query.weak,
            .reexport = query.reexport,
        } }),
    }
    return .ok;
}

fn resolvePathInput(
    gpa: Allocator,
    arena: Allocator,
    io: Io,
    /// Allocated with `gpa`.
    unresolved_inputs: *std.ArrayList(UnresolvedInput),
    /// Allocated with `gpa`.
    resolved_inputs: *std.ArrayList(Input),
    /// Allocated via `gpa`.
    ld_script_bytes: *std.ArrayList(u8),
    /// Allocated via `gpa`.
    archive_dedup: *ArchiveDedupMap,
    target: *const std.Target,
    pq: UnresolvedInput.PathQuery,
    color: std.zig.Color,
) Allocator.Error!?ResolveLibInputResult {
    switch (Compilation.classifyFileExt(pq.path.sub_path)) {
        .static_library => return try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, archive_dedup, target, pq, .static, color),
        .shared_library => return try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, archive_dedup, target, pq, .dynamic, color),
        .object => {
            var file = pq.path.root_dir.handle.openFile(io, pq.path.sub_path, .{}) catch |err|
                fatal("failed to open object {f}: {s}", .{ pq.path, @errorName(err) });
            errdefer file.close(io);
            try resolved_inputs.append(gpa, .{ .object = .{
                .path = pq.path,
                .file = file,
                .must_link = pq.query.must_link,
                .hidden = pq.query.hidden,
            } });
            return null;
        },
        .res => {
            var file = pq.path.root_dir.handle.openFile(io, pq.path.sub_path, .{}) catch |err|
                fatal("failed to open windows resource {f}: {s}", .{ pq.path, @errorName(err) });
            errdefer file.close(io);
            try resolved_inputs.append(gpa, .{ .res = .{
                .path = pq.path,
                .file = file,
            } });
            return null;
        },
        else => fatal("{f}: unrecognized file extension", .{pq.path}),
    }
}

fn resolvePathInputLib(
    gpa: Allocator,
    arena: Allocator,
    io: Io,
    /// Allocated with `gpa`.
    unresolved_inputs: *std.ArrayList(UnresolvedInput),
    /// Allocated with `gpa`.
    resolved_inputs: *std.ArrayList(Input),
    /// Allocated via `gpa`.
    ld_script_bytes: *std.ArrayList(u8),
    /// Allocated via `gpa`.
    archive_dedup: *ArchiveDedupMap,
    target: *const std.Target,
    pq: UnresolvedInput.PathQuery,
    link_mode: std.lang.LinkMode,
    color: std.zig.Color,
) Allocator.Error!ResolveLibInputResult {
    try resolved_inputs.ensureUnusedCapacity(gpa, 1);
    try archive_dedup.ensureUnusedCapacity(gpa, 1);

    const test_path: Path = pq.path;
    // In the case of shared libraries, they might actually be "linker scripts"
    // that contain references to other libraries.
    if (pq.query.allow_so_scripts and target.ofmt == .elf and switch (Compilation.classifyFileExt(test_path.sub_path)) {
        .static_library, .shared_library => true,
        else => false,
    }) {
        var file = test_path.root_dir.handle.openFile(io, test_path.sub_path, .{}) catch |err| switch (err) {
            error.FileNotFound => return .no_match,
            else => |e| fatal("unable to search for {t} library '{f}': {t}", .{
                link_mode, std.fmt.alt(test_path, .formatEscapeChar), e,
            }),
        };
        errdefer file.close(io);
        try ld_script_bytes.resize(gpa, @max(std.elf.MAGIC.len, std.elf.ARMAG.len));
        const n = file.readPositionalAll(io, ld_script_bytes.items, 0) catch |err|
            fatal("failed to read '{f}': {t}", .{ std.fmt.alt(test_path, .formatEscapeChar), err });
        const buf = ld_script_bytes.items[0..n];
        if (mem.startsWith(u8, buf, std.elf.MAGIC) or
            mem.startsWith(u8, buf, std.elf.ARMAG) or
            mem.startsWith(u8, buf, std.elf.ARMAG_THIN))
        {
            // Appears to be an ELF or archive file.
            return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, pq.query);
        }
        const stat = file.stat(io) catch |err|
            fatal("failed to stat {f}: {t}", .{ test_path, err });
        const size = std.math.cast(u32, stat.size) orelse
            fatal("{f}: linker script too big", .{test_path});
        try ld_script_bytes.resize(gpa, size);
        const buf2 = ld_script_bytes.items[n..];
        const n2 = file.readPositionalAll(io, buf2, n) catch |err|
            fatal("failed to read {f}: {t}", .{ test_path, err });
        if (n2 != buf2.len) fatal("failed to read {f}: unexpected end of file", .{test_path});

        // This `Io` is only used for a mutex, and we know we aren't doing anything async/concurrent.
        var threaded: Io.Threaded = .init_single_threaded;
        defer threaded.deinit();
        var diags: Diags = .init(gpa, threaded.io());
        defer diags.deinit();

        const ld_script_result = LdScript.parse(gpa, &diags, test_path, ld_script_bytes.items);
        if (diags.hasErrors()) {
            var wip_errors: std.zig.ErrorBundle.Wip = undefined;
            try wip_errors.init(gpa);
            defer wip_errors.deinit();

            try diags.addMessagesToBundle(&wip_errors, null);

            var error_bundle = try wip_errors.toOwnedBundle("");
            defer error_bundle.deinit(gpa);

            error_bundle.renderToStderr(io, .{}, color) catch {};
            std.process.exit(1);
        }

        var ld_script = ld_script_result catch |err|
            fatal("{f}: failed to parse linker script: {t}", .{ test_path, err });
        defer ld_script.deinit(gpa);

        try unresolved_inputs.ensureUnusedCapacity(gpa, ld_script.args.len);
        for (ld_script.args) |arg| {
            const query: UnresolvedInput.Query = .{
                .needed = arg.needed or pq.query.needed,
                .weak = pq.query.weak,
                .reexport = pq.query.reexport,
                .preferred_mode = pq.query.preferred_mode,
                .search_strategy = pq.query.search_strategy,
                .allow_so_scripts = pq.query.allow_so_scripts,
            };
            if (mem.startsWith(u8, arg.path, "-l")) {
                unresolved_inputs.appendAssumeCapacity(.{ .name_query = .{
                    .name = try arena.dupe(u8, arg.path["-l".len..]),
                    .query = query,
                } });
            } else {
                unresolved_inputs.appendAssumeCapacity(.{ .ambiguous_name = .{
                    .name = try arena.dupe(u8, arg.path),
                    .query = query,
                } });
            }
        }
        file.close(io);
        return .ok;
    }

    var file = test_path.root_dir.handle.openFile(io, test_path.sub_path, .{}) catch |err| switch (err) {
        error.FileNotFound => return .no_match,
        else => |e| fatal("unable to search for {s} library {f}: {s}", .{
            @tagName(link_mode), test_path, @errorName(e),
        }),
    };
    errdefer file.close(io);
    return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, pq.query);
}

pub fn openObject(io: Io, path: Path, must_link: bool, hidden: bool) !Input.Object {
    var file = try path.root_dir.handle.openFile(io, path.sub_path, .{});
    errdefer file.close(io);
    return .{
        .path = path,
        .file = file,
        .must_link = must_link,
        .hidden = hidden,
    };
}

pub fn openDso(io: Io, path: Path, needed: bool, weak: bool, reexport: bool) !Input.Dso {
    var file = try path.root_dir.handle.openFile(io, path.sub_path, .{});
    errdefer file.close(io);
    return .{
        .path = path,
        .file = file,
        .needed = needed,
        .weak = weak,
        .reexport = reexport,
    };
}

pub fn openObjectInput(io: Io, diags: *Diags, path: Path) error{AlreadyReported}!Input {
    return .{ .object = openObject(io, path, false, false) catch |err| {
        return diags.failParse(path, "failed to open {f}: {s}", .{ path, @errorName(err) });
    } };
}

pub fn openArchiveInput(io: Io, diags: *Diags, path: Path, must_link: bool, hidden: bool) error{AlreadyReported}!Input {
    return .{ .archive = openObject(io, path, must_link, hidden) catch |err| {
        return diags.failParse(path, "failed to open {f}: {s}", .{ path, @errorName(err) });
    } };
}

pub fn openDsoInput(io: Io, diags: *Diags, path: Path, needed: bool, weak: bool, reexport: bool) error{AlreadyReported}!Input {
    return .{ .dso = openDso(io, path, needed, weak, reexport) catch |err| {
        return diags.failParse(path, "failed to open {f}: {s}", .{ path, @errorName(err) });
    } };
}

/// Returns true if and only if there is at least one input of type object,
/// archive, or Windows resource file.
pub fn anyObjectInputs(inputs: []const Input) bool {
    return countObjectInputs(inputs) != 0;
}

/// Returns the number of inputs of type object, archive, or Windows resource file.
pub fn countObjectInputs(inputs: []const Input) usize {
    var count: usize = 0;
    for (inputs) |input| switch (input) {
        .dso, .dso_exact => continue,
        .res, .object, .archive => count += 1,
    };
    return count;
}

/// Returns the first input of type object or archive.
pub fn firstObjectInput(inputs: []const Input) ?Input.Object {
    for (inputs) |input| switch (input) {
        .object, .archive => |obj| return obj,
        .res, .dso, .dso_exact => continue,
    };
    return null;
}
