Commit e567abb339

Andrew Kelley <andrew@ziglang.org>
2024-10-16 21:14:19
rework linker inputs
* Compilation.objects changes to Compilation.link_inputs which stores objects, archives, windows resources, shared objects, and strings intended to be put directly into the dynamic section. Order is now preserved between all of these kinds of linker inputs. If it is determined the order does not matter for a particular kind of linker input, that item should be moved to a different array. * rename system_libs to windows_libs * untangle library lookup from CLI types * when doing library lookup, instead of using access syscalls, go ahead and open the files and keep the handles around for passing to the cache system and the linker. * during library lookup and cache file hashing, use positioned reads to avoid affecting the file seek position. * library directories are opened in the CLI and converted to Directory objects, warnings emitted for those that cannot be opened.
1 parent 4706ec8
lib/std/Build/Cache.zig
@@ -142,6 +142,9 @@ pub const hasher_init: Hasher = Hasher.init(&[_]u8{
 pub const File = struct {
     prefixed_path: PrefixedPath,
     max_file_size: ?usize,
+    /// Populated if the user calls `addOpenedFile`.
+    /// The handle is not owned here.
+    handle: ?fs.File,
     stat: Stat,
     bin_digest: BinDigest,
     contents: ?[]const u8,
@@ -173,6 +176,11 @@ pub const File = struct {
         const new = new_max_size orelse return;
         file.max_file_size = if (file.max_file_size) |old| @max(old, new) else new;
     }
+
+    pub fn updateHandle(file: *File, new_handle: ?fs.File) void {
+        const handle = new_handle orelse return;
+        file.handle = handle;
+    }
 };
 
 pub const HashHelper = struct {
@@ -363,15 +371,20 @@ pub const Manifest = struct {
     /// var file_contents = cache_hash.files.keys()[file_index].contents.?;
     /// ```
     pub fn addFilePath(m: *Manifest, file_path: Path, max_file_size: ?usize) !usize {
+        return addOpenedFile(m, file_path, null, max_file_size);
+    }
+
+    /// Same as `addFilePath` except the file has already been opened.
+    pub fn addOpenedFile(m: *Manifest, path: Path, handle: ?fs.File, max_file_size: ?usize) !usize {
         const gpa = m.cache.gpa;
         try m.files.ensureUnusedCapacity(gpa, 1);
         const resolved_path = try fs.path.resolve(gpa, &.{
-            file_path.root_dir.path orelse ".",
-            file_path.subPathOrDot(),
+            path.root_dir.path orelse ".",
+            path.subPathOrDot(),
         });
         errdefer gpa.free(resolved_path);
         const prefixed_path = try m.cache.findPrefixResolved(resolved_path);
-        return addFileInner(m, prefixed_path, max_file_size);
+        return addFileInner(m, prefixed_path, handle, max_file_size);
     }
 
     /// Deprecated; use `addFilePath`.
@@ -383,13 +396,14 @@ pub const Manifest = struct {
         const prefixed_path = try self.cache.findPrefix(file_path);
         errdefer gpa.free(prefixed_path.sub_path);
 
-        return addFileInner(self, prefixed_path, max_file_size);
+        return addFileInner(self, prefixed_path, null, max_file_size);
     }
 
-    fn addFileInner(self: *Manifest, prefixed_path: PrefixedPath, max_file_size: ?usize) !usize {
+    fn addFileInner(self: *Manifest, prefixed_path: PrefixedPath, handle: ?fs.File, max_file_size: ?usize) usize {
         const gop = self.files.getOrPutAssumeCapacityAdapted(prefixed_path, FilesAdapter{});
         if (gop.found_existing) {
             gop.key_ptr.updateMaxSize(max_file_size);
+            gop.key_ptr.updateHandle(handle);
             return gop.index;
         }
         gop.key_ptr.* = .{
@@ -398,6 +412,7 @@ pub const Manifest = struct {
             .max_file_size = max_file_size,
             .stat = undefined,
             .bin_digest = undefined,
+            .handle = handle,
         };
 
         self.hash.add(prefixed_path.prefix);
@@ -565,6 +580,7 @@ pub const Manifest = struct {
                             },
                             .contents = null,
                             .max_file_size = null,
+                            .handle = null,
                             .stat = .{
                                 .size = stat_size,
                                 .inode = stat_inode,
@@ -708,12 +724,19 @@ pub const Manifest = struct {
     }
 
     fn populateFileHash(self: *Manifest, ch_file: *File) !void {
-        const pp = ch_file.prefixed_path;
-        const dir = self.cache.prefixes()[pp.prefix].handle;
-        const file = try dir.openFile(pp.sub_path, .{});
-        defer file.close();
+        if (ch_file.handle) |handle| {
+            return populateFileHashHandle(self, ch_file, handle);
+        } else {
+            const pp = ch_file.prefixed_path;
+            const dir = self.cache.prefixes()[pp.prefix].handle;
+            const handle = try dir.openFile(pp.sub_path, .{});
+            defer handle.close();
+            return populateFileHashHandle(self, ch_file, handle);
+        }
+    }
 
-        const actual_stat = try file.stat();
+    fn populateFileHashHandle(self: *Manifest, ch_file: *File, handle: fs.File) !void {
+        const actual_stat = try handle.stat();
         ch_file.stat = .{
             .size = actual_stat.size,
             .mtime = actual_stat.mtime,
@@ -739,8 +762,7 @@ pub const Manifest = struct {
             var hasher = hasher_init;
             var off: usize = 0;
             while (true) {
-                // give me everything you've got, captain
-                const bytes_read = try file.read(contents[off..]);
+                const bytes_read = try handle.pread(contents[off..], off);
                 if (bytes_read == 0) break;
                 hasher.update(contents[off..][0..bytes_read]);
                 off += bytes_read;
@@ -749,7 +771,7 @@ pub const Manifest = struct {
 
             ch_file.contents = contents;
         } else {
-            try hashFile(file, &ch_file.bin_digest);
+            try hashFile(handle, &ch_file.bin_digest);
         }
 
         self.hash.hasher.update(&ch_file.bin_digest);
@@ -813,6 +835,7 @@ pub const Manifest = struct {
         gop.key_ptr.* = .{
             .prefixed_path = prefixed_path,
             .max_file_size = null,
+            .handle = null,
             .stat = undefined,
             .bin_digest = undefined,
             .contents = null,
@@ -851,6 +874,7 @@ pub const Manifest = struct {
         new_file.* = .{
             .prefixed_path = prefixed_path,
             .max_file_size = null,
+            .handle = null,
             .stat = stat,
             .bin_digest = undefined,
             .contents = null,
@@ -1067,6 +1091,7 @@ pub const Manifest = struct {
             gop.key_ptr.* = .{
                 .prefixed_path = prefixed_path,
                 .max_file_size = file.max_file_size,
+                .handle = file.handle,
                 .stat = file.stat,
                 .bin_digest = file.bin_digest,
                 .contents = null,
@@ -1103,14 +1128,14 @@ pub fn writeSmallFile(dir: fs.Dir, sub_path: []const u8, data: []const u8) !void
 
 fn hashFile(file: fs.File, bin_digest: *[Hasher.mac_length]u8) !void {
     var buf: [1024]u8 = undefined;
-
     var hasher = hasher_init;
+    var off: u64 = 0;
     while (true) {
-        const bytes_read = try file.read(&buf);
+        const bytes_read = try file.pread(&buf, off);
         if (bytes_read == 0) break;
         hasher.update(buf[0..bytes_read]);
+        off += bytes_read;
     }
-
     hasher.final(bin_digest);
 }
 
src/link/Coff/lld.zig
@@ -8,6 +8,7 @@ const log = std.log.scoped(.link);
 const mem = std.mem;
 const Cache = std.Build.Cache;
 const Path = std.Build.Cache.Path;
+const Directory = std.Build.Cache.Directory;
 
 const mingw = @import("../../mingw.zig");
 const link = @import("../../link.zig");
@@ -74,10 +75,7 @@ pub fn linkWithLLD(self: *Coff, arena: Allocator, tid: Zcu.PerThread.Id, prog_no
 
         comptime assert(Compilation.link_hash_implementation_version == 14);
 
-        for (comp.objects) |obj| {
-            _ = try man.addFilePath(obj.path, null);
-            man.hash.add(obj.must_link);
-        }
+        try link.hashInputs(&man, comp.link_inputs);
         for (comp.c_object_table.keys()) |key| {
             _ = try man.addFilePath(key.status.success.object_path, null);
         }
@@ -88,7 +86,10 @@ pub fn linkWithLLD(self: *Coff, arena: Allocator, tid: Zcu.PerThread.Id, prog_no
         man.hash.addOptionalBytes(entry_name);
         man.hash.add(self.base.stack_size);
         man.hash.add(self.image_base);
-        man.hash.addListOfBytes(self.lib_dirs);
+        {
+            // TODO remove this, libraries must instead be resolved by the frontend.
+            for (self.lib_directories) |lib_directory| man.hash.addOptionalBytes(lib_directory.path);
+        }
         man.hash.add(comp.skip_linker_dependencies);
         if (comp.config.link_libc) {
             man.hash.add(comp.libc_installation != null);
@@ -100,7 +101,7 @@ pub fn linkWithLLD(self: *Coff, arena: Allocator, tid: Zcu.PerThread.Id, prog_no
                 }
             }
         }
-        try link.hashAddSystemLibs(&man, comp.system_libs);
+        man.hash.addListOfBytes(comp.windows_libs.keys());
         man.hash.addListOfBytes(comp.force_undefined_symbols.keys());
         man.hash.addOptional(self.subsystem);
         man.hash.add(comp.config.is_test);
@@ -148,8 +149,7 @@ pub fn linkWithLLD(self: *Coff, arena: Allocator, tid: Zcu.PerThread.Id, prog_no
         // here. TODO: think carefully about how we can avoid this redundant operation when doing
         // build-obj. See also the corresponding TODO in linkAsArchive.
         const the_object_path = blk: {
-            if (comp.objects.len != 0)
-                break :blk comp.objects[0].path;
+            if (link.firstObjectInput(comp.link_inputs)) |obj| break :blk obj.path;
 
             if (comp.c_object_table.count() != 0)
                 break :blk comp.c_object_table.keys()[0].status.success.object_path;
@@ -266,18 +266,24 @@ pub fn linkWithLLD(self: *Coff, arena: Allocator, tid: Zcu.PerThread.Id, prog_no
             }
         }
 
-        for (self.lib_dirs) |lib_dir| {
-            try argv.append(try allocPrint(arena, "-LIBPATH:{s}", .{lib_dir}));
+        for (self.lib_directories) |lib_directory| {
+            try argv.append(try allocPrint(arena, "-LIBPATH:{s}", .{lib_directory.path orelse "."}));
         }
 
-        try argv.ensureUnusedCapacity(comp.objects.len);
-        for (comp.objects) |obj| {
-            if (obj.must_link) {
-                argv.appendAssumeCapacity(try allocPrint(arena, "-WHOLEARCHIVE:{}", .{@as(Path, obj.path)}));
-            } else {
-                argv.appendAssumeCapacity(try obj.path.toString(arena));
-            }
-        }
+        try argv.ensureUnusedCapacity(comp.link_inputs.len);
+        for (comp.link_inputs) |link_input| switch (link_input) {
+            .dso_exact => unreachable, // not applicable to PE/COFF
+            inline .dso, .res => |x| {
+                argv.appendAssumeCapacity(try x.path.toString(arena));
+            },
+            .object, .archive => |obj| {
+                if (obj.must_link) {
+                    argv.appendAssumeCapacity(try allocPrint(arena, "-WHOLEARCHIVE:{}", .{@as(Path, obj.path)}));
+                } else {
+                    argv.appendAssumeCapacity(try obj.path.toString(arena));
+                }
+            },
+        };
 
         for (comp.c_object_table.keys()) |key| {
             try argv.append(try key.status.success.object_path.toString(arena));
@@ -484,20 +490,20 @@ pub fn linkWithLLD(self: *Coff, arena: Allocator, tid: Zcu.PerThread.Id, prog_no
             if (comp.compiler_rt_lib) |lib| try argv.append(try lib.full_object_path.toString(arena));
         }
 
-        try argv.ensureUnusedCapacity(comp.system_libs.count());
-        for (comp.system_libs.keys()) |key| {
+        try argv.ensureUnusedCapacity(comp.windows_libs.count());
+        for (comp.windows_libs.keys()) |key| {
             const lib_basename = try allocPrint(arena, "{s}.lib", .{key});
             if (comp.crt_files.get(lib_basename)) |crt_file| {
                 argv.appendAssumeCapacity(try crt_file.full_object_path.toString(arena));
                 continue;
             }
-            if (try findLib(arena, lib_basename, self.lib_dirs)) |full_path| {
+            if (try findLib(arena, lib_basename, self.lib_directories)) |full_path| {
                 argv.appendAssumeCapacity(full_path);
                 continue;
             }
             if (target.abi.isGnu()) {
                 const fallback_name = try allocPrint(arena, "lib{s}.dll.a", .{key});
-                if (try findLib(arena, fallback_name, self.lib_dirs)) |full_path| {
+                if (try findLib(arena, fallback_name, self.lib_directories)) |full_path| {
                     argv.appendAssumeCapacity(full_path);
                     continue;
                 }
@@ -530,14 +536,13 @@ pub fn linkWithLLD(self: *Coff, arena: Allocator, tid: Zcu.PerThread.Id, prog_no
     }
 }
 
-fn findLib(arena: Allocator, name: []const u8, lib_dirs: []const []const u8) !?[]const u8 {
-    for (lib_dirs) |lib_dir| {
-        const full_path = try fs.path.join(arena, &.{ lib_dir, name });
-        fs.cwd().access(full_path, .{}) catch |err| switch (err) {
+fn findLib(arena: Allocator, name: []const u8, lib_directories: []const Directory) !?[]const u8 {
+    for (lib_directories) |lib_directory| {
+        lib_directory.handle.access(name, .{}) catch |err| switch (err) {
             error.FileNotFound => continue,
             else => |e| return e,
         };
-        return full_path;
+        return try lib_directory.join(arena, &.{name});
     }
     return null;
 }
src/link/Elf/relocatable.zig
@@ -2,13 +2,13 @@ pub fn flushStaticLib(elf_file: *Elf, comp: *Compilation, module_obj_path: ?Path
     const gpa = comp.gpa;
     const diags = &comp.link_diags;
 
-    for (comp.objects) |obj| {
-        switch (Compilation.classifyFileExt(obj.path.sub_path)) {
-            .object => parseObjectStaticLibReportingFailure(elf_file, obj.path),
-            .static_library => parseArchiveStaticLibReportingFailure(elf_file, obj.path),
-            else => diags.addParseError(obj.path, "unrecognized file extension", .{}),
-        }
-    }
+    for (comp.link_inputs) |link_input| switch (link_input) {
+        .object => |obj| parseObjectStaticLibReportingFailure(elf_file, obj.path),
+        .archive => |obj| parseArchiveStaticLibReportingFailure(elf_file, obj.path),
+        .dso_exact => unreachable,
+        .res => unreachable,
+        .dso => unreachable,
+    };
 
     for (comp.c_object_table.keys()) |key| {
         parseObjectStaticLibReportingFailure(elf_file, key.status.success.object_path);
@@ -153,18 +153,18 @@ pub fn flushStaticLib(elf_file: *Elf, comp: *Compilation, module_obj_path: ?Path
 pub fn flushObject(elf_file: *Elf, comp: *Compilation, module_obj_path: ?Path) link.File.FlushError!void {
     const diags = &comp.link_diags;
 
-    for (comp.objects) |obj| {
-        elf_file.parseInputReportingFailure(obj.path, false, obj.must_link);
+    for (comp.link_inputs) |link_input| {
+        elf_file.parseInputReportingFailure(link_input);
     }
 
     // This is a set of object files emitted by clang in a single `build-exe` invocation.
     // For instance, the implicit `a.o` as compiled by `zig build-exe a.c` will end up
     // in this set.
     for (comp.c_object_table.keys()) |key| {
-        elf_file.parseObjectReportingFailure(key.status.success.object_path);
+        elf_file.openParseObjectReportingFailure(key.status.success.object_path);
     }
 
-    if (module_obj_path) |path| elf_file.parseObjectReportingFailure(path);
+    if (module_obj_path) |path| elf_file.openParseObjectReportingFailure(path);
 
     if (diags.hasErrors()) return error.FlushFailure;
 
src/link/MachO/relocatable.zig
@@ -3,16 +3,16 @@ pub fn flushObject(macho_file: *MachO, comp: *Compilation, module_obj_path: ?Pat
     const diags = &macho_file.base.comp.link_diags;
 
     // TODO: "positional arguments" is a CLI concept, not a linker concept. Delete this unnecessary array list.
-    var positionals = std.ArrayList(Compilation.LinkObject).init(gpa);
+    var positionals = std.ArrayList(link.Input).init(gpa);
     defer positionals.deinit();
-    try positionals.ensureUnusedCapacity(comp.objects.len);
-    positionals.appendSliceAssumeCapacity(comp.objects);
+    try positionals.ensureUnusedCapacity(comp.link_inputs.len);
+    positionals.appendSliceAssumeCapacity(comp.link_inputs);
 
     for (comp.c_object_table.keys()) |key| {
-        try positionals.append(.{ .path = key.status.success.object_path });
+        try positionals.append(try link.openObjectInput(diags, key.status.success.object_path));
     }
 
-    if (module_obj_path) |path| try positionals.append(.{ .path = path });
+    if (module_obj_path) |path| try positionals.append(try link.openObjectInput(diags, path));
 
     if (macho_file.getZigObject() == null and positionals.items.len == 1) {
         // Instead of invoking a full-blown `-r` mode on the input which sadly will strip all
@@ -20,7 +20,7 @@ pub fn flushObject(macho_file: *MachO, comp: *Compilation, module_obj_path: ?Pat
         // the *only* input file over.
         // TODO: in the future, when we implement `dsymutil` alternative directly in the Zig
         // compiler, investigate if we can get rid of this `if` prong here.
-        const path = positionals.items[0].path;
+        const path = positionals.items[0].path().?;
         const in_file = try path.root_dir.handle.openFile(path.sub_path, .{});
         const stat = try in_file.stat();
         const amt = try in_file.copyRangeAll(0, macho_file.base.file.?, 0, stat.size);
@@ -28,9 +28,9 @@ pub fn flushObject(macho_file: *MachO, comp: *Compilation, module_obj_path: ?Pat
         return;
     }
 
-    for (positionals.items) |obj| {
-        macho_file.classifyInputFile(obj.path, .{ .path = obj.path }, obj.must_link) catch |err|
-            diags.addParseError(obj.path, "failed to read input file: {s}", .{@errorName(err)});
+    for (positionals.items) |link_input| {
+        macho_file.classifyInputFile(link_input) catch |err|
+            diags.addParseError(link_input.path().?, "failed to read input file: {s}", .{@errorName(err)});
     }
 
     if (diags.hasErrors()) return error.FlushFailure;
@@ -72,25 +72,25 @@ pub fn flushStaticLib(macho_file: *MachO, comp: *Compilation, module_obj_path: ?
     const gpa = comp.gpa;
     const diags = &macho_file.base.comp.link_diags;
 
-    var positionals = std.ArrayList(Compilation.LinkObject).init(gpa);
+    var positionals = std.ArrayList(link.Input).init(gpa);
     defer positionals.deinit();
 
-    try positionals.ensureUnusedCapacity(comp.objects.len);
-    positionals.appendSliceAssumeCapacity(comp.objects);
+    try positionals.ensureUnusedCapacity(comp.link_inputs.len);
+    positionals.appendSliceAssumeCapacity(comp.link_inputs);
 
     for (comp.c_object_table.keys()) |key| {
-        try positionals.append(.{ .path = key.status.success.object_path });
+        try positionals.append(try link.openObjectInput(diags, key.status.success.object_path));
     }
 
-    if (module_obj_path) |path| try positionals.append(.{ .path = path });
+    if (module_obj_path) |path| try positionals.append(try link.openObjectInput(diags, path));
 
     if (comp.include_compiler_rt) {
-        try positionals.append(.{ .path = comp.compiler_rt_obj.?.full_object_path });
+        try positionals.append(try link.openObjectInput(diags, comp.compiler_rt_obj.?.full_object_path));
     }
 
-    for (positionals.items) |obj| {
-        macho_file.classifyInputFile(obj.path, .{ .path = obj.path }, obj.must_link) catch |err|
-            diags.addParseError(obj.path, "failed to read input file: {s}", .{@errorName(err)});
+    for (positionals.items) |link_input| {
+        macho_file.classifyInputFile(link_input) catch |err|
+            diags.addParseError(link_input.path().?, "failed to read input file: {s}", .{@errorName(err)});
     }
 
     if (diags.hasErrors()) return error.FlushFailure;
@@ -745,20 +745,15 @@ fn writeHeader(macho_file: *MachO, ncmds: usize, sizeofcmds: usize) !void {
     try macho_file.base.file.?.pwriteAll(mem.asBytes(&header), 0);
 }
 
+const std = @import("std");
+const Path = std.Build.Cache.Path;
+const WaitGroup = std.Thread.WaitGroup;
 const assert = std.debug.assert;
-const build_options = @import("build_options");
-const eh_frame = @import("eh_frame.zig");
-const fat = @import("fat.zig");
-const link = @import("../../link.zig");
-const load_commands = @import("load_commands.zig");
 const log = std.log.scoped(.link);
 const macho = std.macho;
 const math = std.math;
 const mem = std.mem;
 const state_log = std.log.scoped(.link_state);
-const std = @import("std");
-const trace = @import("../../tracy.zig").trace;
-const Path = std.Build.Cache.Path;
 
 const Archive = @import("Archive.zig");
 const Atom = @import("Atom.zig");
@@ -767,3 +762,9 @@ const File = @import("file.zig").File;
 const MachO = @import("../MachO.zig");
 const Object = @import("Object.zig");
 const Symbol = @import("Symbol.zig");
+const build_options = @import("build_options");
+const eh_frame = @import("eh_frame.zig");
+const fat = @import("fat.zig");
+const link = @import("../../link.zig");
+const load_commands = @import("load_commands.zig");
+const trace = @import("../../tracy.zig").trace;
src/link/Coff.zig
@@ -16,7 +16,7 @@ dynamicbase: bool,
 /// default or populated together. They should not be separate fields.
 major_subsystem_version: u16,
 minor_subsystem_version: u16,
-lib_dirs: []const []const u8,
+lib_directories: []const Directory,
 entry: link.File.OpenOptions.Entry,
 entry_addr: ?u32,
 module_definition_file: ?[]const u8,
@@ -297,7 +297,7 @@ pub fn createEmpty(
         .dynamicbase = options.dynamicbase,
         .major_subsystem_version = options.major_subsystem_version orelse 6,
         .minor_subsystem_version = options.minor_subsystem_version orelse 0,
-        .lib_dirs = options.lib_dirs,
+        .lib_directories = options.lib_directories,
         .entry_addr = math.cast(u32, options.entry_addr orelse 0) orelse
             return error.EntryAddressTooBig,
         .module_definition_file = options.module_definition_file,
@@ -2727,6 +2727,7 @@ const mem = std.mem;
 
 const Allocator = std.mem.Allocator;
 const Path = std.Build.Cache.Path;
+const Directory = std.Build.Cache.Directory;
 
 const codegen = @import("../codegen.zig");
 const link = @import("../link.zig");
src/link/Elf.zig
@@ -796,44 +796,55 @@ pub fn flushModule(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_nod
     const csu = try comp.getCrtPaths(arena);
 
     // csu prelude
-    if (csu.crt0) |path| parseObjectReportingFailure(self, path);
-    if (csu.crti) |path| parseObjectReportingFailure(self, path);
-    if (csu.crtbegin) |path| parseObjectReportingFailure(self, path);
-
-    for (comp.objects) |obj| {
-        parseInputReportingFailure(self, obj.path, obj.needed, obj.must_link);
-    }
+    if (csu.crt0) |path| openParseObjectReportingFailure(self, path);
+    if (csu.crti) |path| openParseObjectReportingFailure(self, path);
+    if (csu.crtbegin) |path| openParseObjectReportingFailure(self, path);
+
+    // objects and archives
+    for (comp.link_inputs) |link_input| switch (link_input) {
+        .object, .archive => parseInputReportingFailure(self, link_input),
+        .dso_exact => @panic("TODO"),
+        .dso => continue, // handled below
+        .res => unreachable,
+    };
 
     // This is a set of object files emitted by clang in a single `build-exe` invocation.
     // For instance, the implicit `a.o` as compiled by `zig build-exe a.c` will end up
     // in this set.
     for (comp.c_object_table.keys()) |key| {
-        parseObjectReportingFailure(self, key.status.success.object_path);
+        openParseObjectReportingFailure(self, key.status.success.object_path);
     }
 
-    if (module_obj_path) |path| parseObjectReportingFailure(self, path);
+    if (module_obj_path) |path| openParseObjectReportingFailure(self, path);
 
-    if (comp.config.any_sanitize_thread) parseCrtFileReportingFailure(self, comp.tsan_lib.?);
-    if (comp.config.any_fuzz) parseCrtFileReportingFailure(self, comp.fuzzer_lib.?);
+    if (comp.config.any_sanitize_thread)
+        openParseArchiveReportingFailure(self, comp.tsan_lib.?.full_object_path);
+
+    if (comp.config.any_fuzz)
+        openParseArchiveReportingFailure(self, comp.fuzzer_lib.?.full_object_path);
 
     // libc
     if (!comp.skip_linker_dependencies and !comp.config.link_libc) {
-        if (comp.libc_static_lib) |lib| parseCrtFileReportingFailure(self, lib);
+        if (comp.libc_static_lib) |lib|
+            openParseArchiveReportingFailure(self, lib.full_object_path);
     }
 
-    for (comp.system_libs.values()) |lib_info| {
-        parseInputReportingFailure(self, lib_info.path.?, lib_info.needed, false);
-    }
+    // dynamic libraries
+    for (comp.link_inputs) |link_input| switch (link_input) {
+        .object, .archive, .dso_exact => continue, // handled above
+        .dso => parseInputReportingFailure(self, link_input),
+        .res => unreachable,
+    };
 
     // libc++ dep
     if (comp.config.link_libcpp) {
-        parseInputReportingFailure(self, comp.libcxxabi_static_lib.?.full_object_path, false, false);
-        parseInputReportingFailure(self, comp.libcxx_static_lib.?.full_object_path, false, false);
+        openParseArchiveReportingFailure(self, comp.libcxxabi_static_lib.?.full_object_path);
+        openParseArchiveReportingFailure(self, comp.libcxx_static_lib.?.full_object_path);
     }
 
     // libunwind dep
     if (comp.config.link_libunwind) {
-        parseInputReportingFailure(self, comp.libunwind_static_lib.?.full_object_path, false, false);
+        openParseArchiveReportingFailure(self, comp.libunwind_static_lib.?.full_object_path);
     }
 
     // libc dep
@@ -853,7 +864,10 @@ pub fn flushModule(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_nod
                     lc.crt_dir.?, lib_name, suffix,
                 });
                 const resolved_path = Path.initCwd(lib_path);
-                parseInputReportingFailure(self, resolved_path, false, false);
+                switch (comp.config.link_mode) {
+                    .static => openParseArchiveReportingFailure(self, resolved_path),
+                    .dynamic => openParseDsoReportingFailure(self, resolved_path),
+                }
             }
         } else if (target.isGnuLibC()) {
             for (glibc.libs) |lib| {
@@ -864,15 +878,19 @@ pub fn flushModule(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_nod
                 const lib_path = Path.initCwd(try std.fmt.allocPrint(arena, "{s}{c}lib{s}.so.{d}", .{
                     comp.glibc_so_files.?.dir_path, fs.path.sep, lib.name, lib.sover,
                 }));
-                parseInputReportingFailure(self, lib_path, false, false);
+                openParseDsoReportingFailure(self, lib_path);
             }
-            parseInputReportingFailure(self, try comp.get_libc_crt_file(arena, "libc_nonshared.a"), false, false);
+            const crt_file_path = try comp.get_libc_crt_file(arena, "libc_nonshared.a");
+            openParseArchiveReportingFailure(self, crt_file_path);
         } else if (target.isMusl()) {
             const path = try comp.get_libc_crt_file(arena, switch (link_mode) {
                 .static => "libc.a",
                 .dynamic => "libc.so",
             });
-            parseInputReportingFailure(self, path, false, false);
+            switch (link_mode) {
+                .static => openParseArchiveReportingFailure(self, path),
+                .dynamic => openParseDsoReportingFailure(self, path),
+            }
         } else {
             diags.flags.missing_libc = true;
         }
@@ -884,14 +902,14 @@ pub fn flushModule(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_nod
     // to be after the shared libraries, so they are picked up from the shared
     // libraries, not libcompiler_rt.
     if (comp.compiler_rt_lib) |crt_file| {
-        parseInputReportingFailure(self, crt_file.full_object_path, false, false);
+        openParseArchiveReportingFailure(self, crt_file.full_object_path);
     } else if (comp.compiler_rt_obj) |crt_file| {
-        parseObjectReportingFailure(self, crt_file.full_object_path);
+        openParseObjectReportingFailure(self, crt_file.full_object_path);
     }
 
     // csu postlude
-    if (csu.crtend) |path| parseObjectReportingFailure(self, path);
-    if (csu.crtn) |path| parseObjectReportingFailure(self, path);
+    if (csu.crtend) |path| openParseObjectReportingFailure(self, path);
+    if (csu.crtn) |path| openParseObjectReportingFailure(self, path);
 
     if (diags.hasErrors()) return error.FlushFailure;
 
@@ -1087,9 +1105,15 @@ fn dumpArgv(self: *Elf, comp: *Compilation) !void {
     try argv.append(full_out_path);
 
     if (self.base.isRelocatable()) {
-        for (comp.objects) |obj| {
-            try argv.append(try obj.path.toString(arena));
-        }
+        for (self.base.comp.link_inputs) |link_input| switch (link_input) {
+            .res => unreachable,
+            .dso => |dso| try argv.append(try dso.path.toString(arena)),
+            .object, .archive => |obj| try argv.append(try obj.path.toString(arena)),
+            .dso_exact => |dso_exact| {
+                assert(dso_exact.name[0] == ':');
+                try argv.appendSlice(&.{ "-l", dso_exact.name });
+            },
+        };
 
         for (comp.c_object_table.keys()) |key| {
             try argv.append(try key.status.success.object_path.toString(arena));
@@ -1186,20 +1210,26 @@ fn dumpArgv(self: *Elf, comp: *Compilation) !void {
         }
 
         var whole_archive = false;
-        for (comp.objects) |obj| {
-            if (obj.must_link and !whole_archive) {
-                try argv.append("-whole-archive");
-                whole_archive = true;
-            } else if (!obj.must_link and whole_archive) {
-                try argv.append("-no-whole-archive");
-                whole_archive = false;
-            }
 
-            if (obj.loption) {
-                try argv.append("-l");
-            }
-            try argv.append(try obj.path.toString(arena));
-        }
+        for (self.base.comp.link_inputs) |link_input| switch (link_input) {
+            .res => unreachable,
+            .dso => continue,
+            .object, .archive => |obj| {
+                if (obj.must_link and !whole_archive) {
+                    try argv.append("-whole-archive");
+                    whole_archive = true;
+                } else if (!obj.must_link and whole_archive) {
+                    try argv.append("-no-whole-archive");
+                    whole_archive = false;
+                }
+                try argv.append(try obj.path.toString(arena));
+            },
+            .dso_exact => |dso_exact| {
+                assert(dso_exact.name[0] == ':');
+                try argv.appendSlice(&.{ "-l", dso_exact.name });
+            },
+        };
+
         if (whole_archive) {
             try argv.append("-no-whole-archive");
             whole_archive = false;
@@ -1231,25 +1261,28 @@ fn dumpArgv(self: *Elf, comp: *Compilation) !void {
         // Shared libraries.
         // Worst-case, we need an --as-needed argument for every lib, as well
         // as one before and one after.
-        try argv.ensureUnusedCapacity(self.base.comp.system_libs.keys().len * 2 + 2);
         argv.appendAssumeCapacity("--as-needed");
         var as_needed = true;
 
-        for (self.base.comp.system_libs.values()) |lib_info| {
-            const lib_as_needed = !lib_info.needed;
-            switch ((@as(u2, @intFromBool(lib_as_needed)) << 1) | @intFromBool(as_needed)) {
-                0b00, 0b11 => {},
-                0b01 => {
-                    argv.appendAssumeCapacity("--no-as-needed");
-                    as_needed = false;
-                },
-                0b10 => {
-                    argv.appendAssumeCapacity("--as-needed");
-                    as_needed = true;
-                },
-            }
-            argv.appendAssumeCapacity(try lib_info.path.?.toString(arena));
-        }
+        for (self.base.comp.link_inputs) |link_input| switch (link_input) {
+            .object, .archive, .dso_exact => continue,
+            .dso => |dso| {
+                const lib_as_needed = !dso.needed;
+                switch ((@as(u2, @intFromBool(lib_as_needed)) << 1) | @intFromBool(as_needed)) {
+                    0b00, 0b11 => {},
+                    0b01 => {
+                        try argv.append("--no-as-needed");
+                        as_needed = false;
+                    },
+                    0b10 => {
+                        try argv.append("--as-needed");
+                        as_needed = true;
+                    },
+                }
+                argv.appendAssumeCapacity(try dso.path.toString(arena));
+            },
+            .res => unreachable,
+        };
 
         if (!as_needed) {
             argv.appendAssumeCapacity("--as-needed");
@@ -1321,59 +1354,51 @@ pub const ParseError = error{
     UnknownFileType,
 } || fs.Dir.AccessError || fs.File.SeekError || fs.File.OpenError || fs.File.ReadError;
 
-fn parseCrtFileReportingFailure(self: *Elf, crt_file: Compilation.CrtFile) void {
-    parseInputReportingFailure(self, crt_file.full_object_path, false, false);
-}
-
-pub fn parseInputReportingFailure(self: *Elf, path: Path, needed: bool, must_link: bool) void {
+pub fn parseInputReportingFailure(self: *Elf, input: link.Input) void {
     const gpa = self.base.comp.gpa;
     const diags = &self.base.comp.link_diags;
     const target = self.getTarget();
 
-    switch (Compilation.classifyFileExt(path.sub_path)) {
-        .object => parseObjectReportingFailure(self, path),
-        .shared_library => parseSharedObject(gpa, diags, .{
-            .path = path,
-            .needed = needed,
-        }, &self.shared_objects, &self.files, target) catch |err| switch (err) {
-            error.LinkFailure => return, // already reported
-            error.BadMagic, error.UnexpectedEndOfFile => {
-                var notes = diags.addErrorWithNotes(2) catch return diags.setAllocFailure();
-                notes.addMsg("failed to parse shared object: {s}", .{@errorName(err)}) catch return diags.setAllocFailure();
-                notes.addNote("while parsing {}", .{path}) catch return diags.setAllocFailure();
-                notes.addNote("{s}", .{@as([]const u8, "the file may be a GNU ld script, in which case it is not an ELF file but a text file referencing other libraries to link. In this case, avoid depending on the library, convince your system administrators to refrain from using this kind of file, or pass -fallow-so-scripts to force the compiler to check every shared library in case it is an ld script.")}) catch return diags.setAllocFailure();
-            },
-            else => |e| diags.addParseError(path, "failed to parse shared object: {s}", .{@errorName(e)}),
-        },
-        .static_library => parseArchive(self, path, must_link) catch |err| switch (err) {
-            error.LinkFailure => return, // already reported
-            else => |e| diags.addParseError(path, "failed to parse archive: {s}", .{@errorName(e)}),
-        },
-        else => diags.addParseError(path, "unrecognized file type", .{}),
+    switch (input) {
+        .res => unreachable,
+        .dso_exact => unreachable,
+        .object => |obj| parseObjectReportingFailure(self, obj),
+        .archive => |obj| parseArchiveReportingFailure(self, obj),
+        .dso => |dso| parseDsoReportingFailure(gpa, diags, dso, &self.shared_objects, &self.files, target),
     }
 }
 
-pub fn parseObjectReportingFailure(self: *Elf, path: Path) void {
+pub fn openParseObjectReportingFailure(self: *Elf, path: Path) void {
+    const diags = &self.base.comp.link_diags;
+    const obj = link.openObject(path, false, false) catch |err| {
+        switch (diags.failParse(path, "failed to open object {}: {s}", .{ path, @errorName(err) })) {
+            error.LinkFailure => return,
+        }
+    };
+    self.parseObjectReportingFailure(obj);
+}
+
+pub fn parseObjectReportingFailure(self: *Elf, obj: link.Input.Object) void {
     const diags = &self.base.comp.link_diags;
-    self.parseObject(path) catch |err| switch (err) {
+    self.parseObject(obj) catch |err| switch (err) {
         error.LinkFailure => return, // already reported
-        else => |e| diags.addParseError(path, "unable to parse object: {s}", .{@errorName(e)}),
+        else => |e| diags.addParseError(obj.path, "failed to parse object: {s}", .{@errorName(e)}),
     };
 }
 
-fn parseObject(self: *Elf, path: Path) ParseError!void {
+fn parseObject(self: *Elf, obj: link.Input.Object) ParseError!void {
     const tracy = trace(@src());
     defer tracy.end();
 
     const gpa = self.base.comp.gpa;
-    const handle = try path.root_dir.handle.openFile(path.sub_path, .{});
+    const handle = obj.file;
     const fh = try self.addFileHandle(handle);
 
     const index: File.Index = @intCast(try self.files.addOne(gpa));
     self.files.set(index, .{ .object = .{
         .path = .{
-            .root_dir = path.root_dir,
-            .sub_path = try gpa.dupe(u8, path.sub_path),
+            .root_dir = obj.path.root_dir,
+            .sub_path = try gpa.dupe(u8, obj.path.sub_path),
         },
         .file_handle = fh,
         .index = index,
@@ -1384,17 +1409,35 @@ fn parseObject(self: *Elf, path: Path) ParseError!void {
     try object.parse(self);
 }
 
-fn parseArchive(self: *Elf, path: Path, must_link: bool) ParseError!void {
+pub fn openParseArchiveReportingFailure(self: *Elf, path: Path) void {
+    const diags = &self.base.comp.link_diags;
+    const obj = link.openObject(path, false, false) catch |err| {
+        switch (diags.failParse(path, "failed to open archive {}: {s}", .{ path, @errorName(err) })) {
+            error.LinkFailure => return,
+        }
+    };
+    parseArchiveReportingFailure(self, obj);
+}
+
+pub fn parseArchiveReportingFailure(self: *Elf, obj: link.Input.Object) void {
+    const diags = &self.base.comp.link_diags;
+    self.parseArchive(obj) catch |err| switch (err) {
+        error.LinkFailure => return, // already reported
+        else => |e| diags.addParseError(obj.path, "failed to parse archive: {s}", .{@errorName(e)}),
+    };
+}
+
+fn parseArchive(self: *Elf, obj: link.Input.Object) ParseError!void {
     const tracy = trace(@src());
     defer tracy.end();
 
     const gpa = self.base.comp.gpa;
-    const handle = try path.root_dir.handle.openFile(path.sub_path, .{});
+    const handle = obj.file;
     const fh = try self.addFileHandle(handle);
 
     var archive: Archive = .{};
     defer archive.deinit(gpa);
-    try archive.parse(self, path, fh);
+    try archive.parse(self, obj.path, fh);
 
     const objects = try archive.objects.toOwnedSlice(gpa);
     defer gpa.free(objects);
@@ -1404,16 +1447,48 @@ fn parseArchive(self: *Elf, path: Path, must_link: bool) ParseError!void {
         self.files.set(index, .{ .object = extracted });
         const object = &self.files.items(.data)[index].object;
         object.index = index;
-        object.alive = must_link;
+        object.alive = obj.must_link;
         try object.parse(self);
         try self.objects.append(gpa, index);
     }
 }
 
-fn parseSharedObject(
+fn openParseDsoReportingFailure(self: *Elf, path: Path) void {
+    const diags = &self.base.comp.link_diags;
+    const target = self.getTarget();
+    const dso = link.openDso(path, false, false, false) catch |err| {
+        switch (diags.failParse(path, "failed to open shared object {}: {s}", .{ path, @errorName(err) })) {
+            error.LinkFailure => return,
+        }
+    };
+    const gpa = self.base.comp.gpa;
+    parseDsoReportingFailure(gpa, diags, dso, &self.shared_objects, &self.files, target);
+}
+
+fn parseDsoReportingFailure(
+    gpa: Allocator,
+    diags: *Diags,
+    dso: link.Input.Dso,
+    shared_objects: *std.StringArrayHashMapUnmanaged(File.Index),
+    files: *std.MultiArrayList(File.Entry),
+    target: std.Target,
+) void {
+    parseDso(gpa, diags, dso, shared_objects, files, target) catch |err| switch (err) {
+        error.LinkFailure => return, // already reported
+        error.BadMagic, error.UnexpectedEndOfFile => {
+            var notes = diags.addErrorWithNotes(2) catch return diags.setAllocFailure();
+            notes.addMsg("failed to parse shared object: {s}", .{@errorName(err)}) catch return diags.setAllocFailure();
+            notes.addNote("while parsing {}", .{dso.path}) catch return diags.setAllocFailure();
+            notes.addNote("{s}", .{@as([]const u8, "the file may be a GNU ld script, in which case it is not an ELF file but a text file referencing other libraries to link. In this case, avoid depending on the library, convince your system administrators to refrain from using this kind of file, or pass -fallow-so-scripts to force the compiler to check every shared library in case it is an ld script.")}) catch return diags.setAllocFailure();
+        },
+        else => |e| diags.addParseError(dso.path, "failed to parse shared object: {s}", .{@errorName(e)}),
+    };
+}
+
+fn parseDso(
     gpa: Allocator,
     diags: *Diags,
-    lib: SystemLib,
+    dso: link.Input.Dso,
     shared_objects: *std.StringArrayHashMapUnmanaged(File.Index),
     files: *std.MultiArrayList(File.Entry),
     target: std.Target,
@@ -1421,14 +1496,14 @@ fn parseSharedObject(
     const tracy = trace(@src());
     defer tracy.end();
 
-    const handle = try lib.path.root_dir.handle.openFile(lib.path.sub_path, .{});
+    const handle = dso.file;
     defer handle.close();
 
     const stat = Stat.fromFs(try handle.stat());
-    var header = try SharedObject.parseHeader(gpa, diags, lib.path, handle, stat, target);
+    var header = try SharedObject.parseHeader(gpa, diags, dso.path, handle, stat, target);
     defer header.deinit(gpa);
 
-    const soname = header.soname() orelse lib.path.basename();
+    const soname = header.soname() orelse dso.path.basename();
 
     const gop = try shared_objects.getOrPut(gpa, soname);
     if (gop.found_existing) {
@@ -1446,8 +1521,8 @@ fn parseSharedObject(
     errdefer parsed.deinit(gpa);
 
     const duped_path: Path = .{
-        .root_dir = lib.path.root_dir,
-        .sub_path = try gpa.dupe(u8, lib.path.sub_path),
+        .root_dir = dso.path.root_dir,
+        .sub_path = try gpa.dupe(u8, dso.path.sub_path),
     };
     errdefer gpa.free(duped_path.sub_path);
 
@@ -1456,8 +1531,8 @@ fn parseSharedObject(
             .parsed = parsed,
             .path = duped_path,
             .index = index,
-            .needed = lib.needed,
-            .alive = lib.needed,
+            .needed = dso.needed,
+            .alive = dso.needed,
             .aliases = null,
             .symbols = .empty,
             .symbols_extra = .empty,
@@ -1824,11 +1899,7 @@ fn linkWithLLD(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: s
         try man.addOptionalFile(self.version_script);
         man.hash.add(self.allow_undefined_version);
         man.hash.addOptional(self.enable_new_dtags);
-        for (comp.objects) |obj| {
-            _ = try man.addFilePath(obj.path, null);
-            man.hash.add(obj.must_link);
-            man.hash.add(obj.loption);
-        }
+        try link.hashInputs(&man, comp.link_inputs);
         for (comp.c_object_table.keys()) |key| {
             _ = try man.addFilePath(key.status.success.object_path, null);
         }
@@ -1875,7 +1946,6 @@ fn linkWithLLD(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: s
         }
         man.hash.addOptionalBytes(self.soname);
         man.hash.addOptional(comp.version);
-        try link.hashAddSystemLibs(&man, comp.system_libs);
         man.hash.addListOfBytes(comp.force_undefined_symbols.keys());
         man.hash.add(self.base.allow_shlib_undefined);
         man.hash.add(self.bind_global_refs_locally);
@@ -1922,8 +1992,7 @@ fn linkWithLLD(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: s
         // here. TODO: think carefully about how we can avoid this redundant operation when doing
         // build-obj. See also the corresponding TODO in linkAsArchive.
         const the_object_path = blk: {
-            if (comp.objects.len != 0)
-                break :blk comp.objects[0].path;
+            if (link.firstObjectInput(comp.link_inputs)) |obj| break :blk obj.path;
 
             if (comp.c_object_table.count() != 0)
                 break :blk comp.c_object_table.keys()[0].status.success.object_path;
@@ -2178,21 +2247,26 @@ fn linkWithLLD(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: s
 
         // Positional arguments to the linker such as object files.
         var whole_archive = false;
-        for (comp.objects) |obj| {
-            if (obj.must_link and !whole_archive) {
-                try argv.append("-whole-archive");
-                whole_archive = true;
-            } else if (!obj.must_link and whole_archive) {
-                try argv.append("-no-whole-archive");
-                whole_archive = false;
-            }
 
-            if (obj.loption) {
-                assert(obj.path.sub_path[0] == ':');
-                try argv.append("-l");
-            }
-            try argv.append(try obj.path.toString(arena));
-        }
+        for (self.base.comp.link_inputs) |link_input| switch (link_input) {
+            .res => unreachable, // Windows-only
+            .dso => continue,
+            .object, .archive => |obj| {
+                if (obj.must_link and !whole_archive) {
+                    try argv.append("-whole-archive");
+                    whole_archive = true;
+                } else if (!obj.must_link and whole_archive) {
+                    try argv.append("-no-whole-archive");
+                    whole_archive = false;
+                }
+                try argv.append(try obj.path.toString(arena));
+            },
+            .dso_exact => |dso_exact| {
+                assert(dso_exact.name[0] == ':');
+                try argv.appendSlice(&.{ "-l", dso_exact.name });
+            },
+        };
+
         if (whole_archive) {
             try argv.append("-no-whole-archive");
             whole_archive = false;
@@ -2228,35 +2302,35 @@ fn linkWithLLD(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: s
 
         // Shared libraries.
         if (is_exe_or_dyn_lib) {
-            const system_libs = comp.system_libs.keys();
-            const system_libs_values = comp.system_libs.values();
-
             // Worst-case, we need an --as-needed argument for every lib, as well
             // as one before and one after.
-            try argv.ensureUnusedCapacity(system_libs.len * 2 + 2);
-            argv.appendAssumeCapacity("--as-needed");
+            try argv.append("--as-needed");
             var as_needed = true;
 
-            for (system_libs_values) |lib_info| {
-                const lib_as_needed = !lib_info.needed;
-                switch ((@as(u2, @intFromBool(lib_as_needed)) << 1) | @intFromBool(as_needed)) {
-                    0b00, 0b11 => {},
-                    0b01 => {
-                        argv.appendAssumeCapacity("--no-as-needed");
-                        as_needed = false;
-                    },
-                    0b10 => {
-                        argv.appendAssumeCapacity("--as-needed");
-                        as_needed = true;
-                    },
-                }
+            for (self.base.comp.link_inputs) |link_input| switch (link_input) {
+                .res => unreachable, // Windows-only
+                .object, .archive, .dso_exact => continue,
+                .dso => |dso| {
+                    const lib_as_needed = !dso.needed;
+                    switch ((@as(u2, @intFromBool(lib_as_needed)) << 1) | @intFromBool(as_needed)) {
+                        0b00, 0b11 => {},
+                        0b01 => {
+                            argv.appendAssumeCapacity("--no-as-needed");
+                            as_needed = false;
+                        },
+                        0b10 => {
+                            argv.appendAssumeCapacity("--as-needed");
+                            as_needed = true;
+                        },
+                    }
 
-                // By this time, we depend on these libs being dynamically linked
-                // libraries and not static libraries (the check for that needs to be earlier),
-                // but they could be full paths to .so files, in which case we
-                // want to avoid prepending "-l".
-                argv.appendAssumeCapacity(try lib_info.path.?.toString(arena));
-            }
+                    // By this time, we depend on these libs being dynamically linked
+                    // libraries and not static libraries (the check for that needs to be earlier),
+                    // but they could be full paths to .so files, in which case we
+                    // want to avoid prepending "-l".
+                    argv.appendAssumeCapacity(try dso.path.toString(arena));
+                },
+            };
 
             if (!as_needed) {
                 argv.appendAssumeCapacity("--as-needed");
src/link/MachO.zig
@@ -1,3 +1,7 @@
+pub const Atom = @import("MachO/Atom.zig");
+pub const DebugSymbols = @import("MachO/DebugSymbols.zig");
+pub const Relocation = @import("MachO/Relocation.zig");
+
 base: link.File,
 
 rpath_list: []const []const u8,
@@ -114,8 +118,8 @@ headerpad_max_install_names: bool,
 dead_strip_dylibs: bool,
 /// Treatment of undefined symbols
 undefined_treatment: UndefinedTreatment,
-/// Resolved list of library search directories
-lib_dirs: []const []const u8,
+/// TODO: delete this, libraries need to be resolved by the frontend instead
+lib_directories: []const Directory,
 /// Resolved list of framework search directories
 framework_dirs: []const []const u8,
 /// List of input frameworks
@@ -213,7 +217,8 @@ pub fn createEmpty(
         .platform = Platform.fromTarget(target),
         .sdk_version = if (options.darwin_sdk_layout) |layout| inferSdkVersion(comp, layout) else null,
         .undefined_treatment = if (allow_shlib_undefined) .dynamic_lookup else .@"error",
-        .lib_dirs = options.lib_dirs,
+        // TODO delete this, directories must instead be resolved by the frontend
+        .lib_directories = options.lib_directories,
         .framework_dirs = options.framework_dirs,
         .force_load_objc = options.force_load_objc,
     };
@@ -371,48 +376,44 @@ pub fn flushModule(self: *MachO, arena: Allocator, tid: Zcu.PerThread.Id, prog_n
     if (self.base.isStaticLib()) return relocatable.flushStaticLib(self, comp, module_obj_path);
     if (self.base.isObject()) return relocatable.flushObject(self, comp, module_obj_path);
 
-    var positionals = std.ArrayList(Compilation.LinkObject).init(gpa);
+    var positionals = std.ArrayList(link.Input).init(gpa);
     defer positionals.deinit();
 
-    try positionals.ensureUnusedCapacity(comp.objects.len);
-    positionals.appendSliceAssumeCapacity(comp.objects);
+    try positionals.ensureUnusedCapacity(comp.link_inputs.len);
+
+    for (comp.link_inputs) |link_input| switch (link_input) {
+        .dso => continue, // handled below
+        .object, .archive => positionals.appendAssumeCapacity(link_input),
+        .dso_exact => @panic("TODO"),
+        .res => unreachable,
+    };
 
     // This is a set of object files emitted by clang in a single `build-exe` invocation.
     // For instance, the implicit `a.o` as compiled by `zig build-exe a.c` will end up
     // in this set.
     try positionals.ensureUnusedCapacity(comp.c_object_table.keys().len);
     for (comp.c_object_table.keys()) |key| {
-        positionals.appendAssumeCapacity(.{ .path = key.status.success.object_path });
+        positionals.appendAssumeCapacity(try link.openObjectInput(diags, key.status.success.object_path));
     }
 
-    if (module_obj_path) |path| try positionals.append(.{ .path = path });
+    if (module_obj_path) |path| try positionals.append(try link.openObjectInput(diags, path));
 
     if (comp.config.any_sanitize_thread) {
-        try positionals.append(.{ .path = comp.tsan_lib.?.full_object_path });
+        try positionals.append(try link.openObjectInput(diags, comp.tsan_lib.?.full_object_path));
     }
 
     if (comp.config.any_fuzz) {
-        try positionals.append(.{ .path = comp.fuzzer_lib.?.full_object_path });
+        try positionals.append(try link.openObjectInput(diags, comp.fuzzer_lib.?.full_object_path));
     }
 
-    for (positionals.items) |obj| {
-        self.classifyInputFile(obj.path, .{ .path = obj.path }, obj.must_link) catch |err|
-            diags.addParseError(obj.path, "failed to read input file: {s}", .{@errorName(err)});
+    for (positionals.items) |link_input| {
+        self.classifyInputFile(link_input) catch |err|
+            diags.addParseError(link_input.path().?, "failed to read input file: {s}", .{@errorName(err)});
     }
 
     var system_libs = std.ArrayList(SystemLib).init(gpa);
     defer system_libs.deinit();
 
-    // libs
-    try system_libs.ensureUnusedCapacity(comp.system_libs.values().len);
-    for (comp.system_libs.values()) |info| {
-        system_libs.appendAssumeCapacity(.{
-            .needed = info.needed,
-            .weak = info.weak,
-            .path = info.path.?,
-        });
-    }
-
     // frameworks
     try system_libs.ensureUnusedCapacity(self.frameworks.len);
     for (self.frameworks) |info| {
@@ -436,20 +437,24 @@ pub fn flushModule(self: *MachO, arena: Allocator, tid: Zcu.PerThread.Id, prog_n
         else => |e| return e, // TODO: convert into an error
     };
 
-    for (system_libs.items) |lib| {
-        self.classifyInputFile(lib.path, lib, false) catch |err|
-            diags.addParseError(lib.path, "failed to parse input file: {s}", .{@errorName(err)});
-    }
+    for (comp.link_inputs) |link_input| switch (link_input) {
+        .object, .archive, .dso_exact => continue,
+        .res => unreachable,
+        .dso => {
+            self.classifyInputFile(link_input) catch |err|
+                diags.addParseError(link_input.path().?, "failed to parse input file: {s}", .{@errorName(err)});
+        },
+    };
 
     // Finally, link against compiler_rt.
-    const compiler_rt_path: ?Path = blk: {
-        if (comp.compiler_rt_lib) |x| break :blk x.full_object_path;
-        if (comp.compiler_rt_obj) |x| break :blk x.full_object_path;
-        break :blk null;
-    };
-    if (compiler_rt_path) |path| {
-        self.classifyInputFile(path, .{ .path = path }, false) catch |err|
-            diags.addParseError(path, "failed to parse input file: {s}", .{@errorName(err)});
+    if (comp.compiler_rt_lib) |crt_file| {
+        const path = crt_file.full_object_path;
+        self.classifyInputFile(try link.openArchiveInput(diags, path)) catch |err|
+            diags.addParseError(path, "failed to parse archive: {s}", .{@errorName(err)});
+    } else if (comp.compiler_rt_obj) |crt_file| {
+        const path = crt_file.full_object_path;
+        self.classifyInputFile(try link.openObjectInput(diags, path)) catch |err|
+            diags.addParseError(path, "failed to parse archive: {s}", .{@errorName(err)});
     }
 
     try self.parseInputFiles();
@@ -596,9 +601,12 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void {
     }
 
     if (self.base.isRelocatable()) {
-        for (comp.objects) |obj| {
-            try argv.append(try obj.path.toString(arena));
-        }
+        for (comp.link_inputs) |link_input| switch (link_input) {
+            .object, .archive => |obj| try argv.append(try obj.path.toString(arena)),
+            .res => |res| try argv.append(try res.path.toString(arena)),
+            .dso => |dso| try argv.append(try dso.path.toString(arena)),
+            .dso_exact => |dso_exact| try argv.appendSlice(&.{ "-l", dso_exact.name }),
+        };
 
         for (comp.c_object_table.keys()) |key| {
             try argv.append(try key.status.success.object_path.toString(arena));
@@ -678,13 +686,15 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void {
             try argv.append("dynamic_lookup");
         }
 
-        for (comp.objects) |obj| {
-            // TODO: verify this
-            if (obj.must_link) {
-                try argv.append("-force_load");
-            }
-            try argv.append(try obj.path.toString(arena));
-        }
+        for (comp.link_inputs) |link_input| switch (link_input) {
+            .dso => continue, // handled below
+            .res => unreachable, // windows only
+            .object, .archive => |obj| {
+                if (obj.must_link) try argv.append("-force_load"); // TODO: verify this
+                try argv.append(try obj.path.toString(arena));
+            },
+            .dso_exact => |dso_exact| try argv.appendSlice(&.{ "-l", dso_exact.name }),
+        };
 
         for (comp.c_object_table.keys()) |key| {
             try argv.append(try key.status.success.object_path.toString(arena));
@@ -703,21 +713,25 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void {
             try argv.append(try comp.fuzzer_lib.?.full_object_path.toString(arena));
         }
 
-        for (self.lib_dirs) |lib_dir| {
-            const arg = try std.fmt.allocPrint(arena, "-L{s}", .{lib_dir});
+        for (self.lib_directories) |lib_directory| {
+            // TODO delete this, directories must instead be resolved by the frontend
+            const arg = try std.fmt.allocPrint(arena, "-L{s}", .{lib_directory.path orelse "."});
             try argv.append(arg);
         }
 
-        for (comp.system_libs.keys()) |l_name| {
-            const info = comp.system_libs.get(l_name).?;
-            const arg = if (info.needed)
-                try std.fmt.allocPrint(arena, "-needed-l{s}", .{l_name})
-            else if (info.weak)
-                try std.fmt.allocPrint(arena, "-weak-l{s}", .{l_name})
-            else
-                try std.fmt.allocPrint(arena, "-l{s}", .{l_name});
-            try argv.append(arg);
-        }
+        for (comp.link_inputs) |link_input| switch (link_input) {
+            .object, .archive, .dso_exact => continue, // handled above
+            .res => unreachable, // windows only
+            .dso => |dso| {
+                if (dso.needed) {
+                    try argv.appendSlice(&.{ "-needed-l", try dso.path.toString(arena) });
+                } else if (dso.weak) {
+                    try argv.appendSlice(&.{ "-weak-l", try dso.path.toString(arena) });
+                } else {
+                    try argv.appendSlice(&.{ "-l", try dso.path.toString(arena) });
+                }
+            },
+        };
 
         for (self.framework_dirs) |f_dir| {
             try argv.append("-F");
@@ -751,6 +765,7 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void {
     Compilation.dump_argv(argv.items);
 }
 
+/// TODO delete this, libsystem must be resolved when setting up the compilationt pipeline
 pub fn resolveLibSystem(
     self: *MachO,
     arena: Allocator,
@@ -774,8 +789,8 @@ pub fn resolveLibSystem(
             },
         };
 
-        for (self.lib_dirs) |dir| {
-            if (try accessLibPath(arena, &test_path, &checked_paths, dir, "System")) break :success;
+        for (self.lib_directories) |directory| {
+            if (try accessLibPath(arena, &test_path, &checked_paths, directory.path orelse ".", "System")) break :success;
         }
 
         diags.addMissingLibraryError(checked_paths.items, "unable to find libSystem system library", .{});
@@ -789,13 +804,14 @@ pub fn resolveLibSystem(
     });
 }
 
-pub fn classifyInputFile(self: *MachO, path: Path, lib: SystemLib, must_link: bool) !void {
+pub fn classifyInputFile(self: *MachO, input: link.Input) !void {
     const tracy = trace(@src());
     defer tracy.end();
 
+    const path, const file = input.pathAndFile().?;
+    // TODO don't classify now, it's too late. The input file has already been classified
     log.debug("classifying input file {}", .{path});
 
-    const file = try path.root_dir.handle.openFile(path.sub_path, .{});
     const fh = try self.addFileHandle(file);
     var buffer: [Archive.SARMAG]u8 = undefined;
 
@@ -806,17 +822,17 @@ pub fn classifyInputFile(self: *MachO, path: Path, lib: SystemLib, must_link: bo
         if (h.magic != macho.MH_MAGIC_64) break :blk;
         switch (h.filetype) {
             macho.MH_OBJECT => try self.addObject(path, fh, offset),
-            macho.MH_DYLIB => _ = try self.addDylib(lib, true, fh, offset),
+            macho.MH_DYLIB => _ = try self.addDylib(.fromLinkInput(input), true, fh, offset),
             else => return error.UnknownFileType,
         }
         return;
     }
     if (readArMagic(file, offset, &buffer) catch null) |ar_magic| blk: {
         if (!mem.eql(u8, ar_magic, Archive.ARMAG)) break :blk;
-        try self.addArchive(lib, must_link, fh, fat_arch);
+        try self.addArchive(input.archive, fh, fat_arch);
         return;
     }
-    _ = try self.addTbd(lib, true, fh);
+    _ = try self.addTbd(.fromLinkInput(input), true, fh);
 }
 
 fn parseFatFile(self: *MachO, file: std.fs.File, path: Path) !?fat.Arch {
@@ -903,7 +919,7 @@ fn parseInputFileWorker(self: *MachO, file: File) void {
     };
 }
 
-fn addArchive(self: *MachO, lib: SystemLib, must_link: bool, handle: File.HandleIndex, fat_arch: ?fat.Arch) !void {
+fn addArchive(self: *MachO, lib: link.Input.Object, handle: File.HandleIndex, fat_arch: ?fat.Arch) !void {
     const tracy = trace(@src());
     defer tracy.end();
 
@@ -918,7 +934,7 @@ fn addArchive(self: *MachO, lib: SystemLib, must_link: bool, handle: File.Handle
         self.files.set(index, .{ .object = unpacked });
         const object = &self.files.items(.data)[index].object;
         object.index = index;
-        object.alive = must_link or lib.needed; // TODO: or self.options.all_load;
+        object.alive = lib.must_link; // TODO: or self.options.all_load;
         object.hidden = lib.hidden;
         try self.objects.append(gpa, index);
     }
@@ -993,6 +1009,7 @@ fn isHoisted(self: *MachO, install_name: []const u8) bool {
     return false;
 }
 
+/// TODO delete this, libraries must be instead resolved when instantiating the compilation pipeline
 fn accessLibPath(
     arena: Allocator,
     test_path: *std.ArrayList(u8),
@@ -1051,9 +1068,11 @@ fn parseDependentDylibs(self: *MachO) !void {
     if (self.dylibs.items.len == 0) return;
 
     const gpa = self.base.comp.gpa;
-    const lib_dirs = self.lib_dirs;
     const framework_dirs = self.framework_dirs;
 
+    // TODO delete this, directories must instead be resolved by the frontend
+    const lib_directories = self.lib_directories;
+
     var arena_alloc = std.heap.ArenaAllocator.init(gpa);
     defer arena_alloc.deinit();
     const arena = arena_alloc.allocator();
@@ -1094,9 +1113,9 @@ fn parseDependentDylibs(self: *MachO) !void {
 
                     // Library
                     const lib_name = eatPrefix(stem, "lib") orelse stem;
-                    for (lib_dirs) |dir| {
+                    for (lib_directories) |lib_directory| {
                         test_path.clearRetainingCapacity();
-                        if (try accessLibPath(arena, &test_path, &checked_paths, dir, lib_name)) break :full_path test_path.items;
+                        if (try accessLibPath(arena, &test_path, &checked_paths, lib_directory.path orelse ".", lib_name)) break :full_path test_path.items;
                     }
                 }
 
@@ -4366,6 +4385,24 @@ const SystemLib = struct {
     hidden: bool = false,
     reexport: bool = false,
     must_link: bool = false,
+
+    fn fromLinkInput(link_input: link.Input) SystemLib {
+        return switch (link_input) {
+            .dso_exact => unreachable,
+            .res => unreachable,
+            .object, .archive => |obj| .{
+                .path = obj.path,
+                .must_link = obj.must_link,
+                .hidden = obj.hidden,
+            },
+            .dso => |dso| .{
+                .path = dso.path,
+                .needed = dso.needed,
+                .weak = dso.weak,
+                .reexport = dso.reexport,
+            },
+        };
+    }
 };
 
 pub const SdkLayout = std.zig.LibCDirs.DarwinSdkLayout;
@@ -5303,17 +5340,16 @@ const Air = @import("../Air.zig");
 const Alignment = Atom.Alignment;
 const Allocator = mem.Allocator;
 const Archive = @import("MachO/Archive.zig");
-pub const Atom = @import("MachO/Atom.zig");
 const AtomicBool = std.atomic.Value(bool);
 const Bind = bind.Bind;
 const Cache = std.Build.Cache;
-const Path = Cache.Path;
 const CodeSignature = @import("MachO/CodeSignature.zig");
 const Compilation = @import("../Compilation.zig");
 const DataInCode = synthetic.DataInCode;
-pub const DebugSymbols = @import("MachO/DebugSymbols.zig");
+const Directory = Cache.Directory;
 const Dylib = @import("MachO/Dylib.zig");
 const ExportTrie = @import("MachO/dyld_info/Trie.zig");
+const Path = Cache.Path;
 const File = @import("MachO/file.zig").File;
 const GotSection = synthetic.GotSection;
 const Hash = std.hash.Wyhash;
@@ -5329,7 +5365,6 @@ const Md5 = std.crypto.hash.Md5;
 const Zcu = @import("../Zcu.zig");
 const InternPool = @import("../InternPool.zig");
 const Rebase = @import("MachO/dyld_info/Rebase.zig");
-pub const Relocation = @import("MachO/Relocation.zig");
 const StringTable = @import("StringTable.zig");
 const StubsSection = synthetic.StubsSection;
 const StubsHelperSection = synthetic.StubsHelperSection;
src/link/Wasm.zig
@@ -637,14 +637,6 @@ fn createSyntheticSymbolOffset(wasm: *Wasm, name_offset: u32, tag: Symbol.Tag) !
     return loc;
 }
 
-fn parseInputFiles(wasm: *Wasm, files: []const []const u8) !void {
-    for (files) |path| {
-        if (try wasm.parseObjectFile(path)) continue;
-        if (try wasm.parseArchive(path, false)) continue; // load archives lazily
-        log.warn("Unexpected file format at path: '{s}'", .{path});
-    }
-}
-
 /// Parses the object file from given path. Returns true when the given file was an object
 /// file and parsed successfully. Returns false when file is not an object file.
 /// May return an error instead when parsing failed.
@@ -2522,7 +2514,7 @@ pub fn flushModule(wasm: *Wasm, arena: Allocator, tid: Zcu.PerThread.Id, prog_no
     // Positional arguments to the linker such as object files and static archives.
     // TODO: "positional arguments" is a CLI concept, not a linker concept. Delete this unnecessary array list.
     var positionals = std.ArrayList([]const u8).init(arena);
-    try positionals.ensureUnusedCapacity(comp.objects.len);
+    try positionals.ensureUnusedCapacity(comp.link_inputs.len);
 
     const target = comp.root_mod.resolved_target.result;
     const output_mode = comp.config.output_mode;
@@ -2566,9 +2558,12 @@ pub fn flushModule(wasm: *Wasm, arena: Allocator, tid: Zcu.PerThread.Id, prog_no
         try positionals.append(path);
     }
 
-    for (comp.objects) |object| {
-        try positionals.append(try object.path.toString(arena));
-    }
+    for (comp.link_inputs) |link_input| switch (link_input) {
+        .object, .archive => |obj| try positionals.append(try obj.path.toString(arena)),
+        .dso => |dso| try positionals.append(try dso.path.toString(arena)),
+        .dso_exact => unreachable, // forbidden by frontend
+        .res => unreachable, // windows only
+    };
 
     for (comp.c_object_table.keys()) |c_object| {
         try positionals.append(try c_object.status.success.object_path.toString(arena));
@@ -2577,7 +2572,11 @@ pub fn flushModule(wasm: *Wasm, arena: Allocator, tid: Zcu.PerThread.Id, prog_no
     if (comp.compiler_rt_lib) |lib| try positionals.append(try lib.full_object_path.toString(arena));
     if (comp.compiler_rt_obj) |obj| try positionals.append(try obj.full_object_path.toString(arena));
 
-    try wasm.parseInputFiles(positionals.items);
+    for (positionals.items) |path| {
+        if (try wasm.parseObjectFile(path)) continue;
+        if (try wasm.parseArchive(path, false)) continue; // load archives lazily
+        log.warn("Unexpected file format at path: '{s}'", .{path});
+    }
 
     if (wasm.zig_object_index != .null) {
         try wasm.resolveSymbolsInObject(wasm.zig_object_index);
@@ -3401,10 +3400,7 @@ fn linkWithLLD(wasm: *Wasm, arena: Allocator, tid: Zcu.PerThread.Id, prog_node:
 
         comptime assert(Compilation.link_hash_implementation_version == 14);
 
-        for (comp.objects) |obj| {
-            _ = try man.addFilePath(obj.path, null);
-            man.hash.add(obj.must_link);
-        }
+        try link.hashInputs(&man, comp.link_inputs);
         for (comp.c_object_table.keys()) |key| {
             _ = try man.addFilePath(key.status.success.object_path, null);
         }
@@ -3458,8 +3454,7 @@ fn linkWithLLD(wasm: *Wasm, arena: Allocator, tid: Zcu.PerThread.Id, prog_node:
         // here. TODO: think carefully about how we can avoid this redundant operation when doing
         // build-obj. See also the corresponding TODO in linkAsArchive.
         const the_object_path = blk: {
-            if (comp.objects.len != 0)
-                break :blk comp.objects[0].path;
+            if (link.firstObjectInput(comp.link_inputs)) |obj| break :blk obj.path;
 
             if (comp.c_object_table.count() != 0)
                 break :blk comp.c_object_table.keys()[0].status.success.object_path;
@@ -3621,16 +3616,23 @@ fn linkWithLLD(wasm: *Wasm, arena: Allocator, tid: Zcu.PerThread.Id, prog_node:
 
         // Positional arguments to the linker such as object files.
         var whole_archive = false;
-        for (comp.objects) |obj| {
-            if (obj.must_link and !whole_archive) {
-                try argv.append("-whole-archive");
-                whole_archive = true;
-            } else if (!obj.must_link and whole_archive) {
-                try argv.append("-no-whole-archive");
-                whole_archive = false;
-            }
-            try argv.append(try obj.path.toString(arena));
-        }
+        for (comp.link_inputs) |link_input| switch (link_input) {
+            .object, .archive => |obj| {
+                if (obj.must_link and !whole_archive) {
+                    try argv.append("-whole-archive");
+                    whole_archive = true;
+                } else if (!obj.must_link and whole_archive) {
+                    try argv.append("-no-whole-archive");
+                    whole_archive = false;
+                }
+                try argv.append(try obj.path.toString(arena));
+            },
+            .dso => |dso| {
+                try argv.append(try dso.path.toString(arena));
+            },
+            .dso_exact => unreachable,
+            .res => unreachable,
+        };
         if (whole_archive) {
             try argv.append("-no-whole-archive");
             whole_archive = false;
src/Compilation.zig
@@ -76,12 +76,13 @@ implib_emit: ?Path,
 docs_emit: ?Path,
 root_name: [:0]const u8,
 include_compiler_rt: bool,
-objects: []Compilation.LinkObject,
+/// Resolved into known paths, any GNU ld scripts already resolved.
+link_inputs: []const link.Input,
 /// Needed only for passing -F args to clang.
 framework_dirs: []const []const u8,
-/// These are *always* dynamically linked. Static libraries will be
-/// provided as positional arguments.
-system_libs: std.StringArrayHashMapUnmanaged(SystemLib),
+/// These are only for DLLs dependencies fulfilled by the `.def` files shipped
+/// with Zig. Static libraries are provided as `link.Input` values.
+windows_libs: std.StringArrayHashMapUnmanaged(void),
 version: ?std.SemanticVersion,
 libc_installation: ?*const LibCInstallation,
 skip_linker_dependencies: bool,
@@ -384,7 +385,7 @@ const Job = union(enum) {
     /// one of WASI libc static objects
     wasi_libc_crt_file: wasi_libc.CrtFile,
 
-    /// The value is the index into `system_libs`.
+    /// The value is the index into `windows_libs`.
     windows_import_lib: usize,
 
     const Tag = @typeInfo(Job).@"union".tag_type.?;
@@ -999,25 +1000,6 @@ const CacheUse = union(CacheMode) {
     }
 };
 
-pub const LinkObject = struct {
-    path: Path,
-    must_link: bool = false,
-    needed: bool = false,
-    weak: bool = false,
-    /// When the library is passed via a positional argument, it will be
-    /// added as a full path. If it's `-l<lib>`, then just the basename.
-    ///
-    /// Consistent with `withLOption` variable name in lld ELF driver.
-    loption: bool = false,
-
-    pub fn isObject(lo: LinkObject) bool {
-        return switch (classifyFileExt(lo.path.sub_path)) {
-            .object => true,
-            else => false,
-        };
-    }
-};
-
 pub const CreateOptions = struct {
     zig_lib_directory: Directory,
     local_cache_directory: Directory,
@@ -1065,18 +1047,17 @@ pub const CreateOptions = struct {
     /// This field is intended to be removed.
     /// The ELF implementation no longer uses this data, however the MachO and COFF
     /// implementations still do.
-    lib_dirs: []const []const u8 = &[0][]const u8{},
+    lib_directories: []const Directory = &.{},
     rpath_list: []const []const u8 = &[0][]const u8{},
     symbol_wrap_set: std.StringArrayHashMapUnmanaged(void) = .empty,
     c_source_files: []const CSourceFile = &.{},
     rc_source_files: []const RcSourceFile = &.{},
     manifest_file: ?[]const u8 = null,
     rc_includes: RcIncludes = .any,
-    link_objects: []LinkObject = &[0]LinkObject{},
+    link_inputs: []const link.Input = &.{},
     framework_dirs: []const []const u8 = &[0][]const u8{},
     frameworks: []const Framework = &.{},
-    system_lib_names: []const []const u8 = &.{},
-    system_lib_infos: []const SystemLib = &.{},
+    windows_lib_names: []const []const u8 = &.{},
     /// These correspond to the WASI libc emulated subcomponents including:
     /// * process clocks
     /// * getpid
@@ -1459,12 +1440,8 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
         };
         errdefer if (opt_zcu) |zcu| zcu.deinit();
 
-        var system_libs = try std.StringArrayHashMapUnmanaged(SystemLib).init(
-            gpa,
-            options.system_lib_names,
-            options.system_lib_infos,
-        );
-        errdefer system_libs.deinit(gpa);
+        var windows_libs = try std.StringArrayHashMapUnmanaged(void).init(gpa, options.windows_lib_names, &.{});
+        errdefer windows_libs.deinit(gpa);
 
         comp.* = .{
             .gpa = gpa,
@@ -1526,11 +1503,11 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
             .libcxx_abi_version = options.libcxx_abi_version,
             .root_name = root_name,
             .sysroot = sysroot,
-            .system_libs = system_libs,
+            .windows_libs = windows_libs,
             .version = options.version,
             .libc_installation = libc_dirs.libc_installation,
             .include_compiler_rt = include_compiler_rt,
-            .objects = options.link_objects,
+            .link_inputs = options.link_inputs,
             .framework_dirs = options.framework_dirs,
             .llvm_opt_bisect_limit = options.llvm_opt_bisect_limit,
             .skip_linker_dependencies = options.skip_linker_dependencies,
@@ -1568,7 +1545,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
             .z_max_page_size = options.linker_z_max_page_size,
             .darwin_sdk_layout = libc_dirs.darwin_sdk_layout,
             .frameworks = options.frameworks,
-            .lib_dirs = options.lib_dirs,
+            .lib_directories = options.lib_directories,
             .framework_dirs = options.framework_dirs,
             .rpath_list = options.rpath_list,
             .symbol_wrap_set = options.symbol_wrap_set,
@@ -1851,17 +1828,12 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
             });
 
             // When linking mingw-w64 there are some import libs we always need.
-            for (mingw.always_link_libs) |name| {
-                try comp.system_libs.put(comp.gpa, name, .{
-                    .needed = false,
-                    .weak = false,
-                    .path = null,
-                });
-            }
+            try comp.windows_libs.ensureUnusedCapacity(gpa, mingw.always_link_libs.len);
+            for (mingw.always_link_libs) |name| comp.windows_libs.putAssumeCapacity(name, {});
         }
         // Generate Windows import libs.
         if (target.os.tag == .windows) {
-            const count = comp.system_libs.count();
+            const count = comp.windows_libs.count();
             for (0..count) |i| {
                 try comp.queueJob(.{ .windows_import_lib = i });
             }
@@ -1930,7 +1902,7 @@ pub fn destroy(comp: *Compilation) void {
     comp.embed_file_work_queue.deinit();
 
     const gpa = comp.gpa;
-    comp.system_libs.deinit(gpa);
+    comp.windows_libs.deinit(gpa);
 
     {
         var it = comp.crt_files.iterator();
@@ -2563,13 +2535,7 @@ fn addNonIncrementalStuffToCacheManifest(
         cache_helpers.addModule(&man.hash, comp.root_mod);
     }
 
-    for (comp.objects) |obj| {
-        _ = try man.addFilePath(obj.path, null);
-        man.hash.add(obj.must_link);
-        man.hash.add(obj.needed);
-        man.hash.add(obj.weak);
-        man.hash.add(obj.loption);
-    }
+    try link.hashInputs(man, comp.link_inputs);
 
     for (comp.c_object_table.keys()) |key| {
         _ = try man.addFile(key.src.src_path, null);
@@ -2606,7 +2572,7 @@ fn addNonIncrementalStuffToCacheManifest(
     man.hash.add(comp.rc_includes);
     man.hash.addListOfBytes(comp.force_undefined_symbols.keys());
     man.hash.addListOfBytes(comp.framework_dirs);
-    try link.hashAddSystemLibs(man, comp.system_libs);
+    man.hash.addListOfBytes(comp.windows_libs.keys());
 
     cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_asm);
     cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_llvm_ir);
@@ -2625,12 +2591,16 @@ fn addNonIncrementalStuffToCacheManifest(
     man.hash.addOptional(opts.image_base);
     man.hash.addOptional(opts.gc_sections);
     man.hash.add(opts.emit_relocs);
-    man.hash.addListOfBytes(opts.lib_dirs);
+    const target = comp.root_mod.resolved_target.result;
+    if (target.ofmt == .macho or target.ofmt == .coff) {
+        // TODO remove this, libraries need to be resolved by the frontend. this is already
+        // done by ELF.
+        for (opts.lib_directories) |lib_directory| man.hash.addOptionalBytes(lib_directory.path);
+    }
     man.hash.addListOfBytes(opts.rpath_list);
     man.hash.addListOfBytes(opts.symbol_wrap_set.keys());
     if (comp.config.link_libc) {
         man.hash.add(comp.libc_installation != null);
-        const target = comp.root_mod.resolved_target.result;
         if (comp.libc_installation) |libc_installation| {
             man.hash.addOptionalBytes(libc_installation.crt_dir);
             if (target.abi == .msvc or target.abi == .itanium) {
@@ -3798,7 +3768,7 @@ fn processOneJob(tid: usize, comp: *Compilation, job: Job, prog_node: std.Progre
             const named_frame = tracy.namedFrame("windows_import_lib");
             defer named_frame.end();
 
-            const link_lib = comp.system_libs.keys()[index];
+            const link_lib = comp.windows_libs.keys()[index];
             mingw.buildImportLib(comp, link_lib) catch |err| {
                 // TODO Surface more error details.
                 comp.lockAndSetMiscFailure(
@@ -4711,7 +4681,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr
     // file and building an object we need to link them together, but with just one it should go
     // directly to the output file.
     const direct_o = comp.c_source_files.len == 1 and comp.zcu == null and
-        comp.config.output_mode == .Obj and comp.objects.len == 0;
+        comp.config.output_mode == .Obj and !link.anyObjectInputs(comp.link_inputs);
     const o_basename_noext = if (direct_o)
         comp.root_name
     else
@@ -6516,24 +6486,14 @@ pub fn addLinkLib(comp: *Compilation, lib_name: []const u8) !void {
     // then when we create a sub-Compilation for zig libc, it also tries to
     // build kernel32.lib.
     if (comp.skip_linker_dependencies) return;
+    const target = comp.root_mod.resolved_target.result;
+    if (target.os.tag != .windows or target.ofmt == .c) return;
 
     // This happens when an `extern "foo"` function is referenced.
     // If we haven't seen this library yet and we're targeting Windows, we need
     // to queue up a work item to produce the DLL import library for this.
-    const gop = try comp.system_libs.getOrPut(comp.gpa, lib_name);
-    if (!gop.found_existing) {
-        gop.value_ptr.* = .{
-            .needed = true,
-            .weak = false,
-            .path = null,
-        };
-        const target = comp.root_mod.resolved_target.result;
-        if (target.os.tag == .windows and target.ofmt != .c) {
-            try comp.queueJob(.{
-                .windows_import_lib = comp.system_libs.count() - 1,
-            });
-        }
-    }
+    const gop = try comp.windows_libs.getOrPut(comp.gpa, lib_name);
+    if (!gop.found_existing) try comp.queueJob(.{ .windows_import_lib = comp.windows_libs.count() - 1 });
 }
 
 /// This decides the optimization mode for all zig-provided libraries, including
src/link.zig
@@ -12,6 +12,7 @@ const Air = @import("Air.zig");
 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 Liveness = @import("Liveness.zig");
@@ -26,19 +27,6 @@ const dev = @import("dev.zig");
 
 pub const LdScript = @import("link/LdScript.zig");
 
-/// When adding a new field, remember to update `hashAddSystemLibs`.
-/// These are *always* dynamically linked. Static libraries will be
-/// provided as positional arguments.
-pub const SystemLib = struct {
-    needed: bool,
-    weak: bool,
-    /// This can be null in two cases right now:
-    /// 1. Windows DLLs that zig ships such as advapi32.
-    /// 2. extern "foo" fn declarations where we find out about libraries too late
-    /// TODO: make this non-optional and resolve those two cases somehow.
-    path: ?Path,
-};
-
 pub const Diags = struct {
     /// Stored here so that function definitions can distinguish between
     /// needing an allocator for things besides error reporting.
@@ -355,19 +343,6 @@ pub const Diags = struct {
     }
 };
 
-pub fn hashAddSystemLibs(
-    man: *Cache.Manifest,
-    hm: std.StringArrayHashMapUnmanaged(SystemLib),
-) !void {
-    const keys = hm.keys();
-    man.hash.addListOfBytes(keys);
-    for (hm.values()) |value| {
-        man.hash.add(value.needed);
-        man.hash.add(value.weak);
-        if (value.path) |p| _ = try man.addFilePath(p, null);
-    }
-}
-
 pub const producer_string = if (builtin.is_test) "zig test" else "zig " ++ build_options.version;
 
 pub const File = struct {
@@ -455,7 +430,7 @@ pub const File = struct {
         compatibility_version: ?std.SemanticVersion,
 
         // TODO: remove this. libraries are resolved by the frontend.
-        lib_dirs: []const []const u8,
+        lib_directories: []const Directory,
         framework_dirs: []const []const u8,
         rpath_list: []const []const u8,
 
@@ -1027,7 +1002,6 @@ pub const File = struct {
         defer tracy.end();
 
         const comp = base.comp;
-        const gpa = comp.gpa;
 
         const directory = base.emit.root_dir; // Just an alias to make it shorter to type.
         const full_out_path = try directory.join(arena, &[_][]const u8{base.emit.sub_path});
@@ -1059,7 +1033,7 @@ pub const File = struct {
         var man: Cache.Manifest = undefined;
         defer if (!base.disable_lld_caching) man.deinit();
 
-        const objects = comp.objects;
+        const link_inputs = comp.link_inputs;
 
         var digest: [Cache.hex_digest_len]u8 = undefined;
 
@@ -1069,11 +1043,8 @@ pub const File = struct {
             // We are about to obtain this lock, so here we give other processes a chance first.
             base.releaseLock();
 
-            for (objects) |obj| {
-                _ = try man.addFilePath(obj.path, null);
-                man.hash.add(obj.must_link);
-                man.hash.add(obj.loption);
-            }
+            try hashInputs(&man, link_inputs);
+
             for (comp.c_object_table.keys()) |key| {
                 _ = try man.addFilePath(key.status.success.object_path, null);
             }
@@ -1109,26 +1080,24 @@ pub const File = struct {
             };
         }
 
-        const win32_resource_table_len = comp.win32_resource_table.count();
-        const num_object_files = objects.len + comp.c_object_table.count() + win32_resource_table_len + 2;
-        var object_files = try std.ArrayList([*:0]const u8).initCapacity(gpa, num_object_files);
-        defer object_files.deinit();
+        var object_files: std.ArrayListUnmanaged([*:0]const u8) = .empty;
 
-        for (objects) |obj| {
-            object_files.appendAssumeCapacity(try obj.path.toStringZ(arena));
+        try object_files.ensureUnusedCapacity(arena, link_inputs.len);
+        for (link_inputs) |input| {
+            object_files.appendAssumeCapacity(try input.path().?.toStringZ(arena));
         }
+
+        try object_files.ensureUnusedCapacity(arena, comp.c_object_table.count() +
+            comp.win32_resource_table.count() + 2);
+
         for (comp.c_object_table.keys()) |key| {
             object_files.appendAssumeCapacity(try key.status.success.object_path.toStringZ(arena));
         }
         for (comp.win32_resource_table.keys()) |key| {
             object_files.appendAssumeCapacity(try arena.dupeZ(u8, key.status.success.res_path));
         }
-        if (zcu_obj_path) |p| {
-            object_files.appendAssumeCapacity(try arena.dupeZ(u8, p));
-        }
-        if (compiler_rt_path) |p| {
-            object_files.appendAssumeCapacity(try p.toStringZ(arena));
-        }
+        if (zcu_obj_path) |p| object_files.appendAssumeCapacity(try arena.dupeZ(u8, p));
+        if (compiler_rt_path) |p| object_files.appendAssumeCapacity(try p.toStringZ(arena));
 
         if (comp.verbose_link) {
             std.debug.print("ar rcs {s}", .{full_out_path_z});
@@ -1404,3 +1373,676 @@ pub fn spawnLld(
 
     if (stderr.len > 0) log.warn("unexpected LLD stderr:\n{s}", .{stderr});
 }
+
+/// Provided by the CLI, processed into `LinkInput` instances at the start of
+/// the compilation pipeline.
+pub const UnresolvedInput = union(enum) {
+    /// 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,
+
+    ///// Relocatable.
+    //object: Input.Object,
+    ///// Static library.
+    //archive: Input.Object,
+    ///// Windows resource file.
+    //winres: Path,
+
+    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.builtin.LinkMode,
+        search_strategy: SearchStrategy,
+
+        fn fallbackMode(q: Query) std.builtin.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: fs.File,
+        must_link: bool,
+        hidden: bool,
+    };
+
+    pub const Res = struct {
+        path: Path,
+        file: fs.File,
+    };
+
+    pub const Dso = struct {
+        path: Path,
+        file: fs.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, fs.File } {
+        return switch (input) {
+            .object, .archive => |obj| .{ obj.path, obj.file },
+            inline .res, .dso => |x| .{ x.path, x.file },
+            .dso_exact => null,
+        };
+    }
+};
+
+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,
+    target: std.Target,
+    /// This function mutates this array but does not take ownership.
+    /// Allocated with `gpa`.
+    unresolved_inputs: *std.ArrayListUnmanaged(UnresolvedInput),
+    /// Allocated with `gpa`.
+    resolved_inputs: *std.ArrayListUnmanaged(Input),
+    lib_directories: []const Cache.Directory,
+    color: std.zig.Color,
+) Allocator.Error!void {
+    var checked_paths: std.ArrayListUnmanaged(u8) = .empty;
+    defer checked_paths.deinit(gpa);
+
+    var ld_script_bytes: std.ArrayListUnmanaged(u8) = .empty;
+    defer ld_script_bytes.deinit(gpa);
+
+    var failed_libs: std.ArrayListUnmanaged(struct {
+        name: []const u8,
+        strategy: UnresolvedInput.SearchStrategy,
+        checked_paths: []const u8,
+        preferred_mode: std.builtin.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.popOrNull()) |unresolved_input| {
+        const name_query: UnresolvedInput.NameQuery = switch (unresolved_input) {
+            .name_query => |nq| nq,
+            .ambiguous_name => |an| an: {
+                const lib_name, const link_mode = stripLibPrefixAndSuffix(an.name, target) orelse {
+                    try resolvePathInput(gpa, arena, unresolved_inputs, resolved_inputs, &ld_script_bytes, target, .{
+                        .path = Path.initCwd(an.name),
+                        .query = an.query,
+                    }, color);
+                    continue;
+                };
+                break :an .{
+                    .name = lib_name,
+                    .query = .{
+                        .needed = an.query.needed,
+                        .weak = an.query.weak,
+                        .reexport = an.query.reexport,
+                        .must_link = an.query.must_link,
+                        .hidden = an.query.hidden,
+                        .preferred_mode = link_mode,
+                        .search_strategy = .no_fallback,
+                    },
+                };
+            },
+            .path_query => |pq| {
+                try resolvePathInput(gpa, arena, unresolved_inputs, resolved_inputs, &ld_script_bytes, target, pq, color);
+                continue;
+            },
+            .dso_exact => |dso_exact| {
+                try resolved_inputs.append(gpa, .{ .dso_exact = dso_exact });
+                continue;
+            },
+        };
+        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,
+                    unresolved_inputs,
+                    resolved_inputs,
+                    &checked_paths,
+                    &ld_script_bytes,
+                    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,
+                    unresolved_inputs,
+                    resolved_inputs,
+                    &checked_paths,
+                    &ld_script_bytes,
+                    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,
+                        unresolved_inputs,
+                        resolved_inputs,
+                        &checked_paths,
+                        &ld_script_bytes,
+                        lib_directory,
+                        name_query,
+                        target,
+                        query.preferred_mode,
+                        color,
+                    )) {
+                        .ok => continue :syslib,
+                        .no_match => {},
+                    }
+
+                    // check for fallback mode
+                    switch (try resolveLibInput(
+                        gpa,
+                        arena,
+                        unresolved_inputs,
+                        resolved_inputs,
+                        &checked_paths,
+                        &ld_script_bytes,
+                        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;
+            },
+        }
+        @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 AccessLibPathResult = enum { ok, no_match };
+const fatal = std.process.fatal;
+
+fn resolveLibInput(
+    gpa: Allocator,
+    arena: Allocator,
+    /// Allocated via `gpa`.
+    unresolved_inputs: *std.ArrayListUnmanaged(UnresolvedInput),
+    /// Allocated via `gpa`.
+    resolved_inputs: *std.ArrayListUnmanaged(Input),
+    /// Allocated via `gpa`.
+    checked_paths: *std.ArrayListUnmanaged(u8),
+    /// Allocated via `gpa`.
+    ld_script_bytes: *std.ArrayListUnmanaged(u8),
+    lib_directory: Directory,
+    name_query: UnresolvedInput.NameQuery,
+    target: std.Target,
+    link_mode: std.builtin.LinkMode,
+    color: std.zig.Color,
+) Allocator.Error!AccessLibPathResult {
+    try resolved_inputs.ensureUnusedCapacity(gpa, 1);
+
+    const lib_name = name_query.name;
+
+    if (target.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.writer(gpa).print("\n  {}", .{test_path});
+        var file = test_path.root_dir.handle.openFile(test_path.sub_path, .{}) catch |err| switch (err) {
+            error.FileNotFound => break :tbd,
+            else => |e| fatal("unable to search for tbd library '{}': {s}", .{ test_path, @errorName(e) }),
+        };
+        errdefer file.close();
+        return finishAccessLibPath(resolved_inputs, 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.writer(gpa).print("\n  {}", .{test_path});
+        switch (try resolvePathInputLib(gpa, arena, unresolved_inputs, resolved_inputs, ld_script_bytes, 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.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.writer(gpa).print("\n  {}", .{test_path});
+        var file = test_path.root_dir.handle.openFile(test_path.sub_path, .{}) catch |err| switch (err) {
+            error.FileNotFound => break :so,
+            else => |e| fatal("unable to search for so library '{}': {s}", .{
+                test_path, @errorName(e),
+            }),
+        };
+        errdefer file.close();
+        return finishAccessLibPath(resolved_inputs, 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.writer(gpa).print("\n  {}", .{test_path});
+        var file = test_path.root_dir.handle.openFile(test_path.sub_path, .{}) catch |err| switch (err) {
+            error.FileNotFound => break :mingw,
+            else => |e| fatal("unable to search for static library '{}': {s}", .{ test_path, @errorName(e) }),
+        };
+        errdefer file.close();
+        return finishAccessLibPath(resolved_inputs, test_path, file, link_mode, name_query.query);
+    }
+
+    return .no_match;
+}
+
+fn finishAccessLibPath(
+    resolved_inputs: *std.ArrayListUnmanaged(Input),
+    path: Path,
+    file: std.fs.File,
+    link_mode: std.builtin.LinkMode,
+    query: UnresolvedInput.Query,
+) AccessLibPathResult {
+    switch (link_mode) {
+        .static => 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,
+    /// Allocated with `gpa`.
+    unresolved_inputs: *std.ArrayListUnmanaged(UnresolvedInput),
+    /// Allocated with `gpa`.
+    resolved_inputs: *std.ArrayListUnmanaged(Input),
+    /// Allocated via `gpa`.
+    ld_script_bytes: *std.ArrayListUnmanaged(u8),
+    target: std.Target,
+    pq: UnresolvedInput.PathQuery,
+    color: std.zig.Color,
+) Allocator.Error!void {
+    switch (switch (Compilation.classifyFileExt(pq.path.sub_path)) {
+        .static_library => try resolvePathInputLib(gpa, arena, unresolved_inputs, resolved_inputs, ld_script_bytes, target, pq, .static, color),
+        .shared_library => try resolvePathInputLib(gpa, arena, unresolved_inputs, resolved_inputs, ld_script_bytes, target, pq, .dynamic, color),
+        .object => {
+            var file = pq.path.root_dir.handle.openFile(pq.path.sub_path, .{}) catch |err|
+                fatal("failed to open object {}: {s}", .{ pq.path, @errorName(err) });
+            errdefer file.close();
+            try resolved_inputs.append(gpa, .{ .object = .{
+                .path = pq.path,
+                .file = file,
+                .must_link = pq.query.must_link,
+                .hidden = pq.query.hidden,
+            } });
+            return;
+        },
+        .res => {
+            var file = pq.path.root_dir.handle.openFile(pq.path.sub_path, .{}) catch |err|
+                fatal("failed to open windows resource {}: {s}", .{ pq.path, @errorName(err) });
+            errdefer file.close();
+            try resolved_inputs.append(gpa, .{ .res = .{
+                .path = pq.path,
+                .file = file,
+            } });
+            return;
+        },
+        else => fatal("{}: unrecognized file extension", .{pq.path}),
+    }) {
+        .ok => {},
+        .no_match => fatal("{}: file not found", .{pq.path}),
+    }
+}
+
+fn resolvePathInputLib(
+    gpa: Allocator,
+    arena: Allocator,
+    /// Allocated with `gpa`.
+    unresolved_inputs: *std.ArrayListUnmanaged(UnresolvedInput),
+    /// Allocated with `gpa`.
+    resolved_inputs: *std.ArrayListUnmanaged(Input),
+    /// Allocated via `gpa`.
+    ld_script_bytes: *std.ArrayListUnmanaged(u8),
+    target: std.Target,
+    pq: UnresolvedInput.PathQuery,
+    link_mode: std.builtin.LinkMode,
+    color: std.zig.Color,
+) Allocator.Error!AccessLibPathResult {
+    const test_path: Path = pq.path;
+    // In the case of .so files, they might actually be "linker scripts"
+    // that contain references to other libraries.
+    if (pq.query.allow_so_scripts and target.ofmt == .elf and mem.endsWith(u8, test_path.sub_path, ".so")) {
+        var file = test_path.root_dir.handle.openFile(test_path.sub_path, .{}) catch |err| switch (err) {
+            error.FileNotFound => return .no_match,
+            else => |e| fatal("unable to search for {s} library '{'}': {s}", .{
+                @tagName(link_mode), test_path, @errorName(e),
+            }),
+        };
+        errdefer file.close();
+        try ld_script_bytes.resize(gpa, @sizeOf(std.elf.Elf64_Ehdr));
+        const n = file.preadAll(ld_script_bytes.items, 0) catch |err| fatal("failed to read '{'}': {s}", .{
+            test_path, @errorName(err),
+        });
+        elf_file: {
+            if (n != ld_script_bytes.items.len) break :elf_file;
+            if (!mem.eql(u8, ld_script_bytes.items[0..4], "\x7fELF")) break :elf_file;
+            // Appears to be an ELF file.
+            return finishAccessLibPath(resolved_inputs, test_path, file, link_mode, pq.query);
+        }
+        const stat = file.stat() catch |err|
+            fatal("failed to stat {}: {s}", .{ test_path, @errorName(err) });
+        const size = std.math.cast(u32, stat.size) orelse
+            fatal("{}: linker script too big", .{test_path});
+        try ld_script_bytes.resize(gpa, size);
+        const buf = ld_script_bytes.items[n..];
+        const n2 = file.preadAll(buf, n) catch |err|
+            fatal("failed to read {}: {s}", .{ test_path, @errorName(err) });
+        if (n2 != buf.len) fatal("failed to read {}: unexpected end of file", .{test_path});
+        var diags = Diags.init(gpa);
+        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);
+
+            var error_bundle = try wip_errors.toOwnedBundle("");
+            defer error_bundle.deinit(gpa);
+
+            error_bundle.renderToStdErr(color.renderOptions());
+
+            std.process.exit(1);
+        }
+
+        var ld_script = ld_script_result catch |err|
+            fatal("{}: failed to parse linker script: {s}", .{ test_path, @errorName(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();
+        return .ok;
+    }
+
+    var file = test_path.root_dir.handle.openFile(test_path.sub_path, .{}) catch |err| switch (err) {
+        error.FileNotFound => return .no_match,
+        else => |e| fatal("unable to search for {s} library {}: {s}", .{
+            @tagName(link_mode), test_path, @errorName(e),
+        }),
+    };
+    errdefer file.close();
+    return finishAccessLibPath(resolved_inputs, test_path, file, link_mode, pq.query);
+}
+
+pub fn openObject(path: Path, must_link: bool, hidden: bool) !Input.Object {
+    var file = try path.root_dir.handle.openFile(path.sub_path, .{});
+    errdefer file.close();
+    return .{
+        .path = path,
+        .file = file,
+        .must_link = must_link,
+        .hidden = hidden,
+    };
+}
+
+pub fn openDso(path: Path, needed: bool, weak: bool, reexport: bool) !Input.Dso {
+    var file = try path.root_dir.handle.openFile(path.sub_path, .{});
+    errdefer file.close();
+    return .{
+        .path = path,
+        .file = file,
+        .needed = needed,
+        .weak = weak,
+        .reexport = reexport,
+    };
+}
+
+pub fn openObjectInput(diags: *Diags, path: Path) error{LinkFailure}!Input {
+    return .{ .object = openObject(path, false, false) catch |err| {
+        return diags.failParse(path, "failed to open {}: {s}", .{ path, @errorName(err) });
+    } };
+}
+
+pub fn openArchiveInput(diags: *Diags, path: Path) error{LinkFailure}!Input {
+    return .{ .archive = openObject(path, false, false) catch |err| {
+        return diags.failParse(path, "failed to open {}: {s}", .{ path, @errorName(err) });
+    } };
+}
+
+fn stripLibPrefixAndSuffix(path: []const u8, target: std.Target) ?struct { []const u8, std.builtin.LinkMode } {
+    const prefix = target.libPrefix();
+    const static_suffix = target.staticLibSuffix();
+    const dynamic_suffix = target.dynamicLibSuffix();
+    const basename = fs.path.basename(path);
+    const unlibbed = if (mem.startsWith(u8, basename, prefix)) basename[prefix.len..] else return null;
+    if (mem.endsWith(u8, unlibbed, static_suffix)) return .{
+        unlibbed[0 .. unlibbed.len - static_suffix.len], .static,
+    };
+    if (mem.endsWith(u8, unlibbed, dynamic_suffix)) return .{
+        unlibbed[0 .. unlibbed.len - dynamic_suffix.len], .dynamic,
+    };
+    return null;
+}
+
+/// 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;
+}
src/main.zig
@@ -15,6 +15,7 @@ const cleanExit = std.process.cleanExit;
 const native_os = builtin.os.tag;
 const Cache = std.Build.Cache;
 const Path = std.Build.Cache.Path;
+const Directory = std.Build.Cache.Directory;
 const EnvVar = std.zig.EnvVar;
 const LibCInstallation = std.zig.LibCInstallation;
 const AstGen = std.zig.AstGen;
@@ -55,7 +56,7 @@ pub fn wasi_cwd() std.os.wasi.fd_t {
     return cwd_fd;
 }
 
-fn getWasiPreopen(name: []const u8) Compilation.Directory {
+fn getWasiPreopen(name: []const u8) Directory {
     return .{
         .path = name,
         .handle = .{
@@ -768,27 +769,6 @@ const ArgsIterator = struct {
     }
 };
 
-/// In contrast to `link.SystemLib`, this stores arguments that may need to be
-/// resolved into static libraries so that we can pass only dynamic libraries
-/// as system libs to `Compilation`.
-const SystemLib = struct {
-    needed: bool,
-    weak: bool,
-
-    preferred_mode: std.builtin.LinkMode,
-    search_strategy: SearchStrategy,
-
-    const SearchStrategy = enum { paths_first, mode_first, no_fallback };
-
-    fn fallbackMode(this: SystemLib) std.builtin.LinkMode {
-        assert(this.search_strategy != .no_fallback);
-        return switch (this.preferred_mode) {
-            .dynamic => .static,
-            .static => .dynamic,
-        };
-    }
-};
-
 /// Similar to `link.Framework` except it doesn't store yet unresolved
 /// path to the framework.
 const Framework = struct {
@@ -869,6 +849,7 @@ fn buildOutputType(
     var linker_gc_sections: ?bool = null;
     var linker_compress_debug_sections: ?link.File.Elf.CompressDebugSections = null;
     var linker_allow_shlib_undefined: ?bool = null;
+    var allow_so_scripts: bool = false;
     var linker_bind_global_refs_locally: ?bool = null;
     var linker_import_symbols: bool = false;
     var linker_import_table: bool = false;
@@ -921,7 +902,7 @@ fn buildOutputType(
     var hash_style: link.File.Elf.HashStyle = .both;
     var entitlements: ?[]const u8 = null;
     var pagezero_size: ?u64 = null;
-    var lib_search_strategy: SystemLib.SearchStrategy = .paths_first;
+    var lib_search_strategy: link.UnresolvedInput.SearchStrategy = .paths_first;
     var lib_preferred_mode: std.builtin.LinkMode = .dynamic;
     var headerpad_size: ?u32 = null;
     var headerpad_max_install_names: bool = false;
@@ -985,8 +966,10 @@ fn buildOutputType(
         // Populated in the call to `createModule` for the root module.
         .resolved_options = undefined,
 
-        .system_libs = .{},
-        .resolved_system_libs = .{},
+        .cli_link_inputs = .empty,
+        .windows_libs = .empty,
+        .link_inputs = .empty,
+
         .wasi_emulated_libs = .{},
 
         .c_source_files = .{},
@@ -994,7 +977,7 @@ fn buildOutputType(
 
         .llvm_m_args = .{},
         .sysroot = null,
-        .lib_dirs = .{}, // populated by createModule()
+        .lib_directories = .{}, // populated by createModule()
         .lib_dir_args = .{}, // populated from CLI arg parsing
         .libc_installation = null,
         .want_native_include_dirs = false,
@@ -1003,9 +986,7 @@ fn buildOutputType(
         .rpath_list = .{},
         .each_lib_rpath = null,
         .libc_paths_file = try EnvVar.ZIG_LIBC.get(arena),
-        .link_objects = .{},
         .native_system_include_paths = &.{},
-        .allow_so_scripts = false,
     };
 
     // before arg parsing, check for the NO_COLOR and CLICOLOR_FORCE environment variables
@@ -1240,30 +1221,42 @@ fn buildOutputType(
                         // We don't know whether this library is part of libc
                         // or libc++ until we resolve the target, so we append
                         // to the list for now.
-                        try create_module.system_libs.put(arena, args_iter.nextOrFatal(), .{
-                            .needed = false,
-                            .weak = false,
-                            .preferred_mode = lib_preferred_mode,
-                            .search_strategy = lib_search_strategy,
-                        });
+                        try create_module.cli_link_inputs.append(arena, .{ .name_query = .{
+                            .name = args_iter.nextOrFatal(),
+                            .query = .{
+                                .needed = false,
+                                .weak = false,
+                                .preferred_mode = lib_preferred_mode,
+                                .search_strategy = lib_search_strategy,
+                                .allow_so_scripts = allow_so_scripts,
+                            },
+                        } });
                     } else if (mem.eql(u8, arg, "--needed-library") or
                         mem.eql(u8, arg, "-needed-l") or
                         mem.eql(u8, arg, "-needed_library"))
                     {
                         const next_arg = args_iter.nextOrFatal();
-                        try create_module.system_libs.put(arena, next_arg, .{
-                            .needed = true,
-                            .weak = false,
-                            .preferred_mode = lib_preferred_mode,
-                            .search_strategy = lib_search_strategy,
-                        });
+                        try create_module.cli_link_inputs.append(arena, .{ .name_query = .{
+                            .name = next_arg,
+                            .query = .{
+                                .needed = true,
+                                .weak = false,
+                                .preferred_mode = lib_preferred_mode,
+                                .search_strategy = lib_search_strategy,
+                                .allow_so_scripts = allow_so_scripts,
+                            },
+                        } });
                     } else if (mem.eql(u8, arg, "-weak_library") or mem.eql(u8, arg, "-weak-l")) {
-                        try create_module.system_libs.put(arena, args_iter.nextOrFatal(), .{
-                            .needed = false,
-                            .weak = true,
-                            .preferred_mode = lib_preferred_mode,
-                            .search_strategy = lib_search_strategy,
-                        });
+                        try create_module.cli_link_inputs.append(arena, .{ .name_query = .{
+                            .name = args_iter.nextOrFatal(),
+                            .query = .{
+                                .needed = false,
+                                .weak = true,
+                                .preferred_mode = lib_preferred_mode,
+                                .search_strategy = lib_search_strategy,
+                                .allow_so_scripts = allow_so_scripts,
+                            },
+                        } });
                     } else if (mem.eql(u8, arg, "-D")) {
                         try cc_argv.appendSlice(arena, &.{ arg, args_iter.nextOrFatal() });
                     } else if (mem.eql(u8, arg, "-I")) {
@@ -1577,9 +1570,9 @@ fn buildOutputType(
                     } else if (mem.eql(u8, arg, "-fno-allow-shlib-undefined")) {
                         linker_allow_shlib_undefined = false;
                     } else if (mem.eql(u8, arg, "-fallow-so-scripts")) {
-                        create_module.allow_so_scripts = true;
+                        allow_so_scripts = true;
                     } else if (mem.eql(u8, arg, "-fno-allow-so-scripts")) {
-                        create_module.allow_so_scripts = false;
+                        allow_so_scripts = false;
                     } else if (mem.eql(u8, arg, "-z")) {
                         const z_arg = args_iter.nextOrFatal();
                         if (mem.eql(u8, z_arg, "nodelete")) {
@@ -1687,26 +1680,38 @@ fn buildOutputType(
                         // We don't know whether this library is part of libc
                         // or libc++ until we resolve the target, so we append
                         // to the list for now.
-                        try create_module.system_libs.put(arena, arg["-l".len..], .{
-                            .needed = false,
-                            .weak = false,
-                            .preferred_mode = lib_preferred_mode,
-                            .search_strategy = lib_search_strategy,
-                        });
+                        try create_module.cli_link_inputs.append(arena, .{ .name_query = .{
+                            .name = arg["-l".len..],
+                            .query = .{
+                                .needed = false,
+                                .weak = false,
+                                .preferred_mode = lib_preferred_mode,
+                                .search_strategy = lib_search_strategy,
+                                .allow_so_scripts = allow_so_scripts,
+                            },
+                        } });
                     } else if (mem.startsWith(u8, arg, "-needed-l")) {
-                        try create_module.system_libs.put(arena, arg["-needed-l".len..], .{
-                            .needed = true,
-                            .weak = false,
-                            .preferred_mode = lib_preferred_mode,
-                            .search_strategy = lib_search_strategy,
-                        });
+                        try create_module.cli_link_inputs.append(arena, .{ .name_query = .{
+                            .name = arg["-needed-l".len..],
+                            .query = .{
+                                .needed = true,
+                                .weak = false,
+                                .preferred_mode = lib_preferred_mode,
+                                .search_strategy = lib_search_strategy,
+                                .allow_so_scripts = allow_so_scripts,
+                            },
+                        } });
                     } else if (mem.startsWith(u8, arg, "-weak-l")) {
-                        try create_module.system_libs.put(arena, arg["-weak-l".len..], .{
-                            .needed = false,
-                            .weak = true,
-                            .preferred_mode = lib_preferred_mode,
-                            .search_strategy = lib_search_strategy,
-                        });
+                        try create_module.cli_link_inputs.append(arena, .{ .name_query = .{
+                            .name = arg["-weak-l".len..],
+                            .query = .{
+                                .needed = false,
+                                .weak = true,
+                                .preferred_mode = lib_preferred_mode,
+                                .search_strategy = lib_search_strategy,
+                                .allow_so_scripts = allow_so_scripts,
+                            },
+                        } });
                     } else if (mem.startsWith(u8, arg, "-D")) {
                         try cc_argv.append(arena, arg);
                     } else if (mem.startsWith(u8, arg, "-I")) {
@@ -1731,15 +1736,28 @@ fn buildOutputType(
                         fatal("unrecognized parameter: '{s}'", .{arg});
                     }
                 } else switch (file_ext orelse Compilation.classifyFileExt(arg)) {
-                    .shared_library => {
-                        try create_module.link_objects.append(arena, .{ .path = Path.initCwd(arg) });
-                        create_module.opts.any_dyn_libs = true;
-                    },
-                    .object, .static_library => {
-                        try create_module.link_objects.append(arena, .{ .path = Path.initCwd(arg) });
+                    .shared_library, .object, .static_library => {
+                        try create_module.cli_link_inputs.append(arena, .{ .path_query = .{
+                            .path = Path.initCwd(arg),
+                            .query = .{
+                                .preferred_mode = lib_preferred_mode,
+                                .search_strategy = lib_search_strategy,
+                                .allow_so_scripts = allow_so_scripts,
+                            },
+                        } });
+                        // We do not set `any_dyn_libs` yet because a .so file
+                        // may actually resolve to a GNU ld script which ends
+                        // up being a static library.
                     },
                     .res => {
-                        try create_module.link_objects.append(arena, .{ .path = Path.initCwd(arg) });
+                        try create_module.cli_link_inputs.append(arena, .{ .path_query = .{
+                            .path = Path.initCwd(arg),
+                            .query = .{
+                                .preferred_mode = lib_preferred_mode,
+                                .search_strategy = lib_search_strategy,
+                                .allow_so_scripts = allow_so_scripts,
+                            },
+                        } });
                         contains_res_file = true;
                     },
                     .manifest => {
@@ -1792,6 +1810,7 @@ fn buildOutputType(
             // some functionality that depend on it, such as C++ exceptions and
             // DWARF-based stack traces.
             link_eh_frame_hdr = true;
+            allow_so_scripts = true;
 
             const COutMode = enum {
                 link,
@@ -1851,24 +1870,32 @@ fn buildOutputType(
                                 .ext = file_ext, // duped while parsing the args.
                             });
                         },
-                        .shared_library => {
-                            try create_module.link_objects.append(arena, .{
-                                .path = Path.initCwd(it.only_arg),
-                                .must_link = must_link,
-                            });
-                            create_module.opts.any_dyn_libs = true;
-                        },
-                        .unknown, .object, .static_library => {
-                            try create_module.link_objects.append(arena, .{
+                        .unknown, .object, .static_library, .shared_library => {
+                            try create_module.cli_link_inputs.append(arena, .{ .path_query = .{
                                 .path = Path.initCwd(it.only_arg),
-                                .must_link = must_link,
-                            });
+                                .query = .{
+                                    .must_link = must_link,
+                                    .needed = needed,
+                                    .preferred_mode = lib_preferred_mode,
+                                    .search_strategy = lib_search_strategy,
+                                    .allow_so_scripts = allow_so_scripts,
+                                },
+                            } });
+                            // We do not set `any_dyn_libs` yet because a .so file
+                            // may actually resolve to a GNU ld script which ends
+                            // up being a static library.
                         },
                         .res => {
-                            try create_module.link_objects.append(arena, .{
+                            try create_module.cli_link_inputs.append(arena, .{ .path_query = .{
                                 .path = Path.initCwd(it.only_arg),
-                                .must_link = must_link,
-                            });
+                                .query = .{
+                                    .must_link = must_link,
+                                    .needed = needed,
+                                    .preferred_mode = lib_preferred_mode,
+                                    .search_strategy = lib_search_strategy,
+                                    .allow_so_scripts = allow_so_scripts,
+                                },
+                            } });
                             contains_res_file = true;
                         },
                         .manifest => {
@@ -1900,19 +1927,21 @@ fn buildOutputType(
                             // -l :path/to/filename is used when callers need
                             // more control over what's in the resulting
                             // binary: no extra rpaths and DSO filename exactly
-                            // as provided. Hello, Go.
-                            try create_module.link_objects.append(arena, .{
-                                .path = Path.initCwd(it.only_arg),
-                                .must_link = must_link,
-                                .loption = true,
-                            });
+                            // as provided. CGo compilation depends on this.
+                            try create_module.cli_link_inputs.append(arena, .{ .dso_exact = .{
+                                .name = it.only_arg,
+                            } });
                         } else {
-                            try create_module.system_libs.put(arena, it.only_arg, .{
-                                .needed = needed,
-                                .weak = false,
-                                .preferred_mode = lib_preferred_mode,
-                                .search_strategy = lib_search_strategy,
-                            });
+                            try create_module.cli_link_inputs.append(arena, .{ .name_query = .{
+                                .name = it.only_arg,
+                                .query = .{
+                                    .needed = needed,
+                                    .weak = false,
+                                    .preferred_mode = lib_preferred_mode,
+                                    .search_strategy = lib_search_strategy,
+                                    .allow_so_scripts = allow_so_scripts,
+                                },
+                            } });
                         }
                     },
                     .ignore => {},
@@ -2181,12 +2210,16 @@ fn buildOutputType(
                     },
                     .force_load_objc => force_load_objc = true,
                     .mingw_unicode_entry_point => mingw_unicode_entry_point = true,
-                    .weak_library => try create_module.system_libs.put(arena, it.only_arg, .{
-                        .needed = false,
-                        .weak = true,
-                        .preferred_mode = lib_preferred_mode,
-                        .search_strategy = lib_search_strategy,
-                    }),
+                    .weak_library => try create_module.cli_link_inputs.append(arena, .{ .name_query = .{
+                        .name = it.only_arg,
+                        .query = .{
+                            .needed = false,
+                            .weak = true,
+                            .preferred_mode = lib_preferred_mode,
+                            .search_strategy = lib_search_strategy,
+                            .allow_so_scripts = allow_so_scripts,
+                        },
+                    } }),
                     .weak_framework => try create_module.frameworks.put(arena, it.only_arg, .{ .weak = true }),
                     .headerpad_max_install_names => headerpad_max_install_names = true,
                     .compress_debug_sections => {
@@ -2489,26 +2522,38 @@ fn buildOutputType(
                 } else if (mem.eql(u8, arg, "-needed_framework")) {
                     try create_module.frameworks.put(arena, linker_args_it.nextOrFatal(), .{ .needed = true });
                 } else if (mem.eql(u8, arg, "-needed_library")) {
-                    try create_module.system_libs.put(arena, linker_args_it.nextOrFatal(), .{
-                        .weak = false,
-                        .needed = true,
-                        .preferred_mode = lib_preferred_mode,
-                        .search_strategy = lib_search_strategy,
-                    });
+                    try create_module.cli_link_inputs.append(arena, .{ .name_query = .{
+                        .name = linker_args_it.nextOrFatal(),
+                        .query = .{
+                            .weak = false,
+                            .needed = true,
+                            .preferred_mode = lib_preferred_mode,
+                            .search_strategy = lib_search_strategy,
+                            .allow_so_scripts = allow_so_scripts,
+                        },
+                    } });
                 } else if (mem.startsWith(u8, arg, "-weak-l")) {
-                    try create_module.system_libs.put(arena, arg["-weak-l".len..], .{
-                        .weak = true,
-                        .needed = false,
-                        .preferred_mode = lib_preferred_mode,
-                        .search_strategy = lib_search_strategy,
-                    });
+                    try create_module.cli_link_inputs.append(arena, .{ .name_query = .{
+                        .name = arg["-weak-l".len..],
+                        .query = .{
+                            .weak = true,
+                            .needed = false,
+                            .preferred_mode = lib_preferred_mode,
+                            .search_strategy = lib_search_strategy,
+                            .allow_so_scripts = allow_so_scripts,
+                        },
+                    } });
                 } else if (mem.eql(u8, arg, "-weak_library")) {
-                    try create_module.system_libs.put(arena, linker_args_it.nextOrFatal(), .{
-                        .weak = true,
-                        .needed = false,
-                        .preferred_mode = lib_preferred_mode,
-                        .search_strategy = lib_search_strategy,
-                    });
+                    try create_module.cli_link_inputs.append(arena, .{ .name_query = .{
+                        .name = linker_args_it.nextOrFatal(),
+                        .query = .{
+                            .weak = true,
+                            .needed = false,
+                            .preferred_mode = lib_preferred_mode,
+                            .search_strategy = lib_search_strategy,
+                            .allow_so_scripts = allow_so_scripts,
+                        },
+                    } });
                 } else if (mem.eql(u8, arg, "-compatibility_version")) {
                     const compat_version = linker_args_it.nextOrFatal();
                     compatibility_version = std.SemanticVersion.parse(compat_version) catch |err| {
@@ -2539,10 +2584,14 @@ fn buildOutputType(
                 } else if (mem.eql(u8, arg, "-install_name")) {
                     install_name = linker_args_it.nextOrFatal();
                 } else if (mem.eql(u8, arg, "-force_load")) {
-                    try create_module.link_objects.append(arena, .{
+                    try create_module.cli_link_inputs.append(arena, .{ .path_query = .{
                         .path = Path.initCwd(linker_args_it.nextOrFatal()),
-                        .must_link = true,
-                    });
+                        .query = .{
+                            .must_link = true,
+                            .preferred_mode = .static,
+                            .search_strategy = .no_fallback,
+                        },
+                    } });
                 } else if (mem.eql(u8, arg, "-hash-style") or
                     mem.eql(u8, arg, "--hash-style"))
                 {
@@ -2672,7 +2721,7 @@ fn buildOutputType(
                 },
             }
             if (create_module.c_source_files.items.len == 0 and
-                create_module.link_objects.items.len == 0 and
+                !link.anyObjectInputs(create_module.link_inputs.items) and
                 root_src_file == null)
             {
                 // For example `zig cc` and no args should print the "no input files" message.
@@ -2714,8 +2763,9 @@ fn buildOutputType(
             if (create_module.c_source_files.items.len >= 1)
                 break :b create_module.c_source_files.items[0].src_path;
 
-            if (create_module.link_objects.items.len >= 1)
-                break :b create_module.link_objects.items[0].path.sub_path;
+            for (create_module.link_inputs.items) |link_input| {
+                if (link_input.path()) |path| break :b path.sub_path;
+            }
 
             if (emit_bin == .yes)
                 break :b emit_bin.yes;
@@ -2801,7 +2851,7 @@ fn buildOutputType(
             fatal("unable to find zig self exe path: {s}", .{@errorName(err)});
         };
 
-    var zig_lib_directory: Compilation.Directory = d: {
+    var zig_lib_directory: Directory = d: {
         if (override_lib_dir) |unresolved_lib_dir| {
             const lib_dir = try introspect.resolvePath(arena, unresolved_lib_dir);
             break :d .{
@@ -2822,7 +2872,7 @@ fn buildOutputType(
     };
     defer zig_lib_directory.handle.close();
 
-    var global_cache_directory: Compilation.Directory = l: {
+    var global_cache_directory: Directory = l: {
         if (override_global_cache_dir) |p| {
             break :l .{
                 .handle = try fs.cwd().makeOpenPath(p, .{}),
@@ -2852,7 +2902,7 @@ fn buildOutputType(
 
     var builtin_modules: std.StringHashMapUnmanaged(*Package.Module) = .empty;
     // `builtin_modules` allocated into `arena`, so no deinit
-    const main_mod = try createModule(gpa, arena, &create_module, 0, null, zig_lib_directory, &builtin_modules);
+    const main_mod = try createModule(gpa, arena, &create_module, 0, null, zig_lib_directory, &builtin_modules, color);
     for (create_module.modules.keys(), create_module.modules.values()) |key, cli_mod| {
         if (cli_mod.resolved == null)
             fatal("module '{s}' declared but not used", .{key});
@@ -2946,7 +2996,6 @@ fn buildOutputType(
         }
     }
 
-    // We now repeat part of the process for frameworks.
     var resolved_frameworks = std.ArrayList(Compilation.Framework).init(arena);
 
     if (create_module.frameworks.keys().len > 0) {
@@ -3003,7 +3052,7 @@ fn buildOutputType(
         const total_obj_count = create_module.c_source_files.items.len +
             @intFromBool(root_src_file != null) +
             create_module.rc_source_files.items.len +
-            create_module.link_objects.items.len;
+            link.countObjectInputs(create_module.link_inputs.items);
         if (total_obj_count > 1) {
             fatal("{s} does not support linking multiple objects into one", .{@tagName(target.ofmt)});
         }
@@ -3219,7 +3268,7 @@ fn buildOutputType(
     var cleanup_local_cache_dir: ?fs.Dir = null;
     defer if (cleanup_local_cache_dir) |*dir| dir.close();
 
-    var local_cache_directory: Compilation.Directory = l: {
+    var local_cache_directory: Directory = l: {
         if (override_local_cache_dir) |local_cache_dir_path| {
             const dir = try fs.cwd().makeOpenPath(local_cache_dir_path, .{});
             cleanup_local_cache_dir = dir;
@@ -3356,7 +3405,7 @@ fn buildOutputType(
         .emit_llvm_bc = emit_llvm_bc_resolved.data,
         .emit_docs = emit_docs_resolved.data,
         .emit_implib = emit_implib_resolved.data,
-        .lib_dirs = create_module.lib_dirs.items,
+        .lib_directories = create_module.lib_directories.items,
         .rpath_list = create_module.rpath_list.items,
         .symbol_wrap_set = symbol_wrap_set,
         .c_source_files = create_module.c_source_files.items,
@@ -3364,11 +3413,10 @@ fn buildOutputType(
         .manifest_file = manifest_file,
         .rc_includes = rc_includes,
         .mingw_unicode_entry_point = mingw_unicode_entry_point,
-        .link_objects = create_module.link_objects.items,
+        .link_inputs = create_module.link_inputs.items,
         .framework_dirs = create_module.framework_dirs.items,
         .frameworks = resolved_frameworks.items,
-        .system_lib_names = create_module.resolved_system_libs.items(.name),
-        .system_lib_infos = create_module.resolved_system_libs.items(.lib),
+        .windows_lib_names = create_module.windows_libs.keys(),
         .wasi_emulated_libs = create_module.wasi_emulated_libs.items,
         .want_compiler_rt = want_compiler_rt,
         .hash_style = hash_style,
@@ -3625,28 +3673,6 @@ fn buildOutputType(
     return cleanExit();
 }
 
-const LinkerInput = union(enum) {
-    /// An argument like: -l[name]
-    named: Named,
-    /// When a file path is provided.
-    path: struct {
-        path: Path,
-        /// We still need all this info because the path may point to a .so
-        /// file which may actually be a "linker script" that references
-        /// library names which need to be resolved.
-        info: SystemLib,
-    },
-    /// Put exactly this string in the dynamic section, no rpath.
-    exact: struct {
-        name: []const u8,
-    },
-
-    const Named = struct {
-        name: []const u8,
-        info: SystemLib,
-    };
-};
-
 const CreateModule = struct {
     global_cache_directory: Cache.Directory,
     modules: std.StringArrayHashMapUnmanaged(CliModule),
@@ -3659,12 +3685,14 @@ const CreateModule = struct {
     /// This one is used while collecting CLI options. The set of libs is used
     /// directly after computing the target and used to compute link_libc,
     /// link_libcpp, and then the libraries are filtered into
-    /// `external_system_libs` and `resolved_system_libs`.
-    system_libs: std.StringArrayHashMapUnmanaged(SystemLib),
-    resolved_system_libs: std.MultiArrayList(struct {
-        name: []const u8,
-        lib: Compilation.SystemLib,
-    }),
+    /// `unresolved_linker_inputs` and `windows_libs`.
+    cli_link_inputs: std.ArrayListUnmanaged(link.UnresolvedInput),
+    windows_libs: std.StringArrayHashMapUnmanaged(void),
+    /// The local variable `unresolved_link_inputs` is fed into library
+    /// resolution, mutating the input array, and producing this data as
+    /// output. Allocated with gpa.
+    link_inputs: std.ArrayListUnmanaged(link.Input),
+
     wasi_emulated_libs: std.ArrayListUnmanaged(wasi_libc.CrtFile),
 
     c_source_files: std.ArrayListUnmanaged(Compilation.CSourceFile),
@@ -3675,7 +3703,7 @@ const CreateModule = struct {
     /// CPU features.
     llvm_m_args: std.ArrayListUnmanaged([]const u8),
     sysroot: ?[]const u8,
-    lib_dirs: std.ArrayListUnmanaged([]const u8),
+    lib_directories: std.ArrayListUnmanaged(Directory),
     lib_dir_args: std.ArrayListUnmanaged([]const u8),
     libc_installation: ?LibCInstallation,
     want_native_include_dirs: bool,
@@ -3685,8 +3713,6 @@ const CreateModule = struct {
     rpath_list: std.ArrayListUnmanaged([]const u8),
     each_lib_rpath: ?bool,
     libc_paths_file: ?[]const u8,
-    link_objects: std.ArrayListUnmanaged(Compilation.LinkObject),
-    allow_so_scripts: bool,
 };
 
 fn createModule(
@@ -3697,6 +3723,7 @@ fn createModule(
     parent: ?*Package.Module,
     zig_lib_directory: Cache.Directory,
     builtin_modules: *std.StringHashMapUnmanaged(*Package.Module),
+    color: std.zig.Color,
 ) Allocator.Error!*Package.Module {
     const cli_mod = &create_module.modules.values()[index];
     if (cli_mod.resolved) |m| return m;
@@ -3790,82 +3817,101 @@ fn createModule(
         // First, remove libc, libc++, and compiler_rt libraries from the system libraries list.
         // We need to know whether the set of system libraries contains anything besides these
         // to decide whether to trigger native path detection logic.
-        var external_linker_inputs: std.ArrayListUnmanaged(LinkerInput) = .empty;
-        for (create_module.system_libs.keys(), create_module.system_libs.values()) |lib_name, info| {
-            if (std.zig.target.isLibCLibName(target, lib_name)) {
-                create_module.opts.link_libc = true;
-                continue;
-            }
-            if (std.zig.target.isLibCxxLibName(target, lib_name)) {
-                create_module.opts.link_libcpp = true;
-                continue;
-            }
-            switch (target_util.classifyCompilerRtLibName(target, lib_name)) {
-                .none => {},
-                .only_libunwind, .both => {
-                    create_module.opts.link_libunwind = true;
+        // Preserves linker input order.
+        var unresolved_link_inputs: std.ArrayListUnmanaged(link.UnresolvedInput) = .empty;
+        try unresolved_link_inputs.ensureUnusedCapacity(arena, create_module.cli_link_inputs.items.len);
+        var any_name_queries_remaining = false;
+        for (create_module.cli_link_inputs.items) |cli_link_input| switch (cli_link_input) {
+            .name_query => |nq| {
+                const lib_name = nq.name;
+                if (std.zig.target.isLibCLibName(target, lib_name)) {
+                    create_module.opts.link_libc = true;
                     continue;
-                },
-                .only_compiler_rt => {
-                    warn("ignoring superfluous library '{s}': this dependency is fulfilled instead by compiler-rt which zig unconditionally provides", .{lib_name});
+                }
+                if (std.zig.target.isLibCxxLibName(target, lib_name)) {
+                    create_module.opts.link_libcpp = true;
                     continue;
-                },
-            }
+                }
+                switch (target_util.classifyCompilerRtLibName(target, lib_name)) {
+                    .none => {},
+                    .only_libunwind, .both => {
+                        create_module.opts.link_libunwind = true;
+                        continue;
+                    },
+                    .only_compiler_rt => {
+                        warn("ignoring superfluous library '{s}': this dependency is fulfilled instead by compiler-rt which zig unconditionally provides", .{lib_name});
+                        continue;
+                    },
+                }
 
-            if (target.isMinGW()) {
-                const exists = mingw.libExists(arena, target, zig_lib_directory, lib_name) catch |err| {
-                    fatal("failed to check zig installation for DLL import libs: {s}", .{
-                        @errorName(err),
-                    });
-                };
-                if (exists) {
-                    try create_module.resolved_system_libs.append(arena, .{
-                        .name = lib_name,
-                        .lib = .{
-                            .needed = true,
-                            .weak = false,
-                            .path = null,
-                        },
-                    });
-                    continue;
+                if (target.isMinGW()) {
+                    const exists = mingw.libExists(arena, target, zig_lib_directory, lib_name) catch |err| {
+                        fatal("failed to check zig installation for DLL import libs: {s}", .{
+                            @errorName(err),
+                        });
+                    };
+                    if (exists) {
+                        try create_module.windows_libs.put(arena, lib_name, {});
+                        continue;
+                    }
                 }
-            }
 
-            if (fs.path.isAbsolute(lib_name)) {
-                fatal("cannot use absolute path as a system library: {s}", .{lib_name});
-            }
+                if (fs.path.isAbsolute(lib_name)) {
+                    fatal("cannot use absolute path as a system library: {s}", .{lib_name});
+                }
 
-            if (target.os.tag == .wasi) {
-                if (wasi_libc.getEmulatedLibCrtFile(lib_name)) |crt_file| {
-                    try create_module.wasi_emulated_libs.append(arena, crt_file);
-                    continue;
+                if (target.os.tag == .wasi) {
+                    if (wasi_libc.getEmulatedLibCrtFile(lib_name)) |crt_file| {
+                        try create_module.wasi_emulated_libs.append(arena, crt_file);
+                        continue;
+                    }
                 }
-            }
+                unresolved_link_inputs.appendAssumeCapacity(cli_link_input);
+                any_name_queries_remaining = true;
+            },
+            else => {
+                unresolved_link_inputs.appendAssumeCapacity(cli_link_input);
+            },
+        }; // After this point, unresolved_link_inputs is used instead of cli_link_inputs.
 
-            try external_linker_inputs.append(arena, .{ .named = .{
-                .name = lib_name,
-                .info = info,
-            } });
-        }
-        // After this point, external_linker_inputs is used instead of system_libs.
-        if (external_linker_inputs.items.len != 0)
-            create_module.want_native_include_dirs = true;
+        if (any_name_queries_remaining) create_module.want_native_include_dirs = true;
 
         // Resolve the library path arguments with respect to sysroot.
+        try create_module.lib_directories.ensureUnusedCapacity(arena, create_module.lib_dir_args.items.len);
         if (create_module.sysroot) |root| {
-            try create_module.lib_dirs.ensureUnusedCapacity(arena, create_module.lib_dir_args.items.len * 2);
-            for (create_module.lib_dir_args.items) |dir| {
-                if (fs.path.isAbsolute(dir)) {
-                    const stripped_dir = dir[fs.path.diskDesignator(dir).len..];
+            for (create_module.lib_dir_args.items) |lib_dir_arg| {
+                if (fs.path.isAbsolute(lib_dir_arg)) {
+                    const stripped_dir = lib_dir_arg[fs.path.diskDesignator(lib_dir_arg).len..];
                     const full_path = try fs.path.join(arena, &[_][]const u8{ root, stripped_dir });
-                    create_module.lib_dirs.appendAssumeCapacity(full_path);
+                    create_module.lib_directories.appendAssumeCapacity(.{
+                        .handle = fs.cwd().openDir(full_path, .{}) catch |err| {
+                            warn("unable to open library directory {s}: {s}", .{ full_path, @errorName(err) });
+                            continue;
+                        },
+                        .path = full_path,
+                    });
+                } else {
+                    create_module.lib_directories.appendAssumeCapacity(.{
+                        .handle = fs.cwd().openDir(lib_dir_arg, .{}) catch |err| {
+                            warn("unable to open library directory {s}: {s}", .{ lib_dir_arg, @errorName(err) });
+                            continue;
+                        },
+                        .path = lib_dir_arg,
+                    });
                 }
-                create_module.lib_dirs.appendAssumeCapacity(dir);
             }
         } else {
-            create_module.lib_dirs = create_module.lib_dir_args;
+            for (create_module.lib_dir_args.items) |lib_dir_arg| {
+                create_module.lib_directories.appendAssumeCapacity(.{
+                    .handle = fs.cwd().openDir(lib_dir_arg, .{}) catch |err| {
+                        warn("unable to open library directory {s}: {s}", .{ lib_dir_arg, @errorName(err) });
+                        continue;
+                    },
+                    .path = lib_dir_arg,
+                });
+            }
         }
-        create_module.lib_dir_args = undefined; // From here we use lib_dirs instead.
+        create_module.lib_dir_args = undefined; // From here we use lib_directories instead.
 
         if (resolved_target.is_native_os and target.isDarwin()) {
             // If we want to link against frameworks, we need system headers.
@@ -3874,7 +3920,10 @@ fn createModule(
         }
 
         if (create_module.each_lib_rpath orelse resolved_target.is_native_os) {
-            try create_module.rpath_list.appendSlice(arena, create_module.lib_dirs.items);
+            try create_module.rpath_list.ensureUnusedCapacity(arena, create_module.lib_directories.items.len);
+            for (create_module.lib_directories.items) |lib_directory| {
+                create_module.rpath_list.appendAssumeCapacity(lib_directory.path.?);
+            }
         }
 
         // Trigger native system library path detection if necessary.
@@ -3892,8 +3941,18 @@ fn createModule(
             create_module.native_system_include_paths = try paths.include_dirs.toOwnedSlice(arena);
 
             try create_module.framework_dirs.appendSlice(arena, paths.framework_dirs.items);
-            try create_module.lib_dirs.appendSlice(arena, paths.lib_dirs.items);
             try create_module.rpath_list.appendSlice(arena, paths.rpaths.items);
+
+            try create_module.lib_directories.ensureUnusedCapacity(arena, paths.lib_dirs.items.len);
+            for (paths.lib_dirs.items) |lib_dir| {
+                create_module.lib_directories.appendAssumeCapacity(.{
+                    .handle = fs.cwd().openDir(lib_dir, .{}) catch |err| {
+                        warn("unable to open library directory {s}: {s}", .{ lib_dir, @errorName(err) });
+                        continue;
+                    },
+                    .path = lib_dir,
+                });
+            }
         }
 
         if (create_module.libc_paths_file) |paths_file| {
@@ -3905,7 +3964,7 @@ fn createModule(
         }
 
         if (builtin.target.os.tag == .windows and (target.abi == .msvc or target.abi == .itanium) and
-            external_linker_inputs.items.len != 0)
+            any_name_queries_remaining)
         {
             if (create_module.libc_installation == null) {
                 create_module.libc_installation = LibCInstallation.findNative(.{
@@ -3916,204 +3975,32 @@ fn createModule(
                     fatal("unable to find native libc installation: {s}", .{@errorName(err)});
                 };
 
-                try create_module.lib_dirs.appendSlice(arena, &.{
+                try create_module.lib_directories.appendSlice(arena, &.{
                     create_module.libc_installation.?.msvc_lib_dir.?,
                     create_module.libc_installation.?.kernel32_lib_dir.?,
                 });
             }
         }
 
-        // If any libs in this list are statically provided, we omit them from the
-        // resolved list and populate the link_objects array instead.
-        {
-            var test_path: std.ArrayListUnmanaged(u8) = .empty;
-            defer test_path.deinit(gpa);
-
-            var checked_paths: std.ArrayListUnmanaged(u8) = .empty;
-            defer checked_paths.deinit(gpa);
-
-            var ld_script_bytes: std.ArrayListUnmanaged(u8) = .empty;
-            defer ld_script_bytes.deinit(gpa);
-
-            var failed_libs: std.ArrayListUnmanaged(struct {
-                name: []const u8,
-                strategy: SystemLib.SearchStrategy,
-                checked_paths: []const u8,
-                preferred_mode: std.builtin.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(LinkerInput, external_linker_inputs.items);
-
-            syslib: while (external_linker_inputs.popOrNull()) |external_linker_input| {
-                const external_system_lib: LinkerInput.Named = switch (external_linker_input) {
-                    .named => |named| named,
-                    .path => |p| p: {
-                        if (fs.path.isAbsolute(p.path.sub_path)) {
-                            try create_module.link_objects.append(arena, .{
-                                .path = p.path,
-                                .needed = p.info.needed,
-                                .weak = p.info.weak,
-                            });
-                            continue;
-                        }
-                        const lib_name, const link_mode = stripLibPrefixAndSuffix(p.path.sub_path, target);
-                        break :p .{
-                            .name = lib_name,
-                            .info = .{
-                                .needed = p.info.needed,
-                                .weak = p.info.weak,
-                                .preferred_mode = link_mode,
-                                .search_strategy = .no_fallback,
-                            },
-                        };
-                    },
-                    .exact => |exact| {
-                        try create_module.link_objects.append(arena, .{
-                            .path = Path.initCwd(exact.name),
-                            .loption = true,
-                        });
-                        continue;
-                    },
-                };
-                const lib_name = external_system_lib.name;
-                const info = external_system_lib.info;
-
-                // Checked in the first pass above while looking for libc libraries.
-                assert(!fs.path.isAbsolute(lib_name));
-
-                checked_paths.clearRetainingCapacity();
-
-                switch (info.search_strategy) {
-                    .mode_first, .no_fallback => {
-                        // check for preferred mode
-                        for (create_module.lib_dirs.items) |lib_dir_path| switch (try accessLibPath(
-                            gpa,
-                            arena,
-                            &test_path,
-                            &checked_paths,
-                            &external_linker_inputs,
-                            create_module,
-                            &ld_script_bytes,
-                            lib_dir_path,
-                            lib_name,
-                            target,
-                            info.preferred_mode,
-                            info,
-                        )) {
-                            .ok => continue :syslib,
-                            .no_match => {},
-                        };
-                        // check for fallback mode
-                        if (info.search_strategy == .no_fallback) {
-                            try failed_libs.append(arena, .{
-                                .name = lib_name,
-                                .strategy = info.search_strategy,
-                                .checked_paths = try arena.dupe(u8, checked_paths.items),
-                                .preferred_mode = info.preferred_mode,
-                            });
-                            continue :syslib;
-                        }
-                        for (create_module.lib_dirs.items) |lib_dir_path| switch (try accessLibPath(
-                            gpa,
-                            arena,
-                            &test_path,
-                            &checked_paths,
-                            &external_linker_inputs,
-                            create_module,
-                            &ld_script_bytes,
-                            lib_dir_path,
-                            lib_name,
-                            target,
-                            info.fallbackMode(),
-                            info,
-                        )) {
-                            .ok => continue :syslib,
-                            .no_match => {},
-                        };
-                        try failed_libs.append(arena, .{
-                            .name = lib_name,
-                            .strategy = info.search_strategy,
-                            .checked_paths = try arena.dupe(u8, checked_paths.items),
-                            .preferred_mode = info.preferred_mode,
-                        });
-                        continue :syslib;
-                    },
-                    .paths_first => {
-                        for (create_module.lib_dirs.items) |lib_dir_path| {
-                            // check for preferred mode
-                            switch (try accessLibPath(
-                                gpa,
-                                arena,
-                                &test_path,
-                                &checked_paths,
-                                &external_linker_inputs,
-                                create_module,
-                                &ld_script_bytes,
-                                lib_dir_path,
-                                lib_name,
-                                target,
-                                info.preferred_mode,
-                                info,
-                            )) {
-                                .ok => continue :syslib,
-                                .no_match => {},
-                            }
-
-                            // check for fallback mode
-                            switch (try accessLibPath(
-                                gpa,
-                                arena,
-                                &test_path,
-                                &checked_paths,
-                                &external_linker_inputs,
-                                create_module,
-                                &ld_script_bytes,
-                                lib_dir_path,
-                                lib_name,
-                                target,
-                                info.fallbackMode(),
-                                info,
-                            )) {
-                                .ok => continue :syslib,
-                                .no_match => {},
-                            }
-                        }
-                        try failed_libs.append(arena, .{
-                            .name = lib_name,
-                            .strategy = info.search_strategy,
-                            .checked_paths = try arena.dupe(u8, checked_paths.items),
-                            .preferred_mode = info.preferred_mode,
-                        });
-                        continue :syslib;
-                    },
-                }
-                @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,
-                    });
-                }
-                process.exit(1);
-            }
-        }
-        // After this point, create_module.resolved_system_libs is used instead
-        // of external_linker_inputs.
-
-        if (create_module.resolved_system_libs.len != 0)
-            create_module.opts.any_dyn_libs = true;
+        // Destructively mutates but does not transfer ownership of `unresolved_link_inputs`.
+        link.resolveInputs(
+            gpa,
+            arena,
+            target,
+            &unresolved_link_inputs,
+            &create_module.link_inputs,
+            create_module.lib_directories.items,
+            color,
+        ) catch |err| fatal("failed to resolve link inputs: {s}", .{@errorName(err)});
+
+        if (create_module.windows_libs.count() != 0) create_module.opts.any_dyn_libs = true;
+        if (!create_module.opts.any_dyn_libs) for (create_module.link_inputs.items) |item| switch (item) {
+            .dso, .dso_exact => {
+                create_module.opts.any_dyn_libs = true;
+                break;
+            },
+            else => {},
+        };
 
         create_module.resolved_options = Compilation.Config.resolve(create_module.opts) catch |err| switch (err) {
             error.WasiExecModelRequiresWasi => fatal("only WASI OS targets support execution model", .{}),
@@ -4181,7 +4068,7 @@ fn createModule(
     for (cli_mod.deps) |dep| {
         const dep_index = create_module.modules.getIndex(dep.value) orelse
             fatal("module '{s}' depends on non-existent module '{s}'", .{ name, dep.key });
-        const dep_mod = try createModule(gpa, arena, create_module, dep_index, mod, zig_lib_directory, builtin_modules);
+        const dep_mod = try createModule(gpa, arena, create_module, dep_index, mod, zig_lib_directory, builtin_modules, color);
         try mod.deps.put(arena, dep.key, dep_mod);
     }
 
@@ -5046,7 +4933,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
 
     process.raiseFileDescriptorLimit();
 
-    var zig_lib_directory: Compilation.Directory = if (override_lib_dir) |lib_dir| .{
+    var zig_lib_directory: Directory = if (override_lib_dir) |lib_dir| .{
         .path = lib_dir,
         .handle = fs.cwd().openDir(lib_dir, .{}) catch |err| {
             fatal("unable to open zig lib directory from 'zig-lib-dir' argument: '{s}': {s}", .{ lib_dir, @errorName(err) });
@@ -5065,7 +4952,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
     });
     child_argv.items[argv_index_build_file] = build_root.directory.path orelse cwd_path;
 
-    var global_cache_directory: Compilation.Directory = l: {
+    var global_cache_directory: Directory = l: {
         const p = override_global_cache_dir orelse try introspect.resolveGlobalCacheDir(arena);
         break :l .{
             .handle = try fs.cwd().makeOpenPath(p, .{}),
@@ -5076,7 +4963,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
 
     child_argv.items[argv_index_global_cache_dir] = global_cache_directory.path orelse cwd_path;
 
-    var local_cache_directory: Compilation.Directory = l: {
+    var local_cache_directory: Directory = l: {
         if (override_local_cache_dir) |local_cache_dir_path| {
             break :l .{
                 .handle = try fs.cwd().makeOpenPath(local_cache_dir_path, .{}),
@@ -5510,7 +5397,7 @@ fn jitCmd(
     const override_lib_dir: ?[]const u8 = try EnvVar.ZIG_LIB_DIR.get(arena);
     const override_global_cache_dir: ?[]const u8 = try EnvVar.ZIG_GLOBAL_CACHE_DIR.get(arena);
 
-    var zig_lib_directory: Compilation.Directory = if (override_lib_dir) |lib_dir| .{
+    var zig_lib_directory: Directory = if (override_lib_dir) |lib_dir| .{
         .path = lib_dir,
         .handle = fs.cwd().openDir(lib_dir, .{}) catch |err| {
             fatal("unable to open zig lib directory from 'zig-lib-dir' argument: '{s}': {s}", .{ lib_dir, @errorName(err) });
@@ -5520,7 +5407,7 @@ fn jitCmd(
     };
     defer zig_lib_directory.handle.close();
 
-    var global_cache_directory: Compilation.Directory = l: {
+    var global_cache_directory: Directory = l: {
         const p = override_global_cache_dir orelse try introspect.resolveGlobalCacheDir(arena);
         break :l .{
             .handle = try fs.cwd().makeOpenPath(p, .{}),
@@ -6907,197 +6794,6 @@ const ClangSearchSanitizer = struct {
     };
 };
 
-const AccessLibPathResult = enum { ok, no_match };
-
-fn accessLibPath(
-    gpa: Allocator,
-    arena: Allocator,
-    /// Allocated via `gpa`.
-    test_path: *std.ArrayListUnmanaged(u8),
-    /// Allocated via `gpa`.
-    checked_paths: *std.ArrayListUnmanaged(u8),
-    /// Allocated via `arena`.
-    external_linker_inputs: *std.ArrayListUnmanaged(LinkerInput),
-    create_module: *CreateModule,
-    /// Allocated via `gpa`.
-    ld_script_bytes: *std.ArrayListUnmanaged(u8),
-    lib_dir_path: []const u8,
-    lib_name: []const u8,
-    target: std.Target,
-    link_mode: std.builtin.LinkMode,
-    parent: SystemLib,
-) Allocator.Error!AccessLibPathResult {
-    const sep = fs.path.sep_str;
-
-    if (target.isDarwin() and link_mode == .dynamic) tbd: {
-        // Prefer .tbd over .dylib.
-        test_path.clearRetainingCapacity();
-        try test_path.writer(gpa).print("{s}" ++ sep ++ "lib{s}.tbd", .{ lib_dir_path, lib_name });
-        try checked_paths.writer(gpa).print("\n  {s}", .{test_path.items});
-        fs.cwd().access(test_path.items, .{}) catch |err| switch (err) {
-            error.FileNotFound => break :tbd,
-            else => |e| fatal("unable to search for tbd library '{s}': {s}", .{
-                test_path.items, @errorName(e),
-            }),
-        };
-        return finishAccessLibPath(arena, create_module, test_path, link_mode, parent, lib_name);
-    }
-
-    main_check: {
-        test_path.clearRetainingCapacity();
-        try test_path.writer(gpa).print("{s}" ++ sep ++ "{s}{s}{s}", .{
-            lib_dir_path,
-            target.libPrefix(),
-            lib_name,
-            switch (link_mode) {
-                .static => target.staticLibSuffix(),
-                .dynamic => target.dynamicLibSuffix(),
-            },
-        });
-        try checked_paths.writer(gpa).print("\n  {s}", .{test_path.items});
-
-        // In the case of .so files, they might actually be "linker scripts"
-        // that contain references to other libraries.
-        if (create_module.allow_so_scripts and target.ofmt == .elf and mem.endsWith(u8, test_path.items, ".so")) {
-            var file = fs.cwd().openFile(test_path.items, .{}) catch |err| switch (err) {
-                error.FileNotFound => break :main_check,
-                else => |e| fatal("unable to search for {s} library '{s}': {s}", .{
-                    @tagName(link_mode), test_path.items, @errorName(e),
-                }),
-            };
-            defer file.close();
-            try ld_script_bytes.resize(gpa, @sizeOf(std.elf.Elf64_Ehdr));
-            const n = file.readAll(ld_script_bytes.items) catch |err| fatal("failed to read {s}: {s}", .{
-                test_path.items, @errorName(err),
-            });
-            elf_file: {
-                if (n != ld_script_bytes.items.len) break :elf_file;
-                if (!mem.eql(u8, ld_script_bytes.items[0..4], "\x7fELF")) break :elf_file;
-                // Appears to be an ELF file.
-                return finishAccessLibPath(arena, create_module, test_path, link_mode, parent, lib_name);
-            }
-            const stat = file.stat() catch |err|
-                fatal("failed to stat {s}: {s}", .{ test_path.items, @errorName(err) });
-            const size = std.math.cast(u32, stat.size) orelse
-                fatal("{s}: linker script too big", .{test_path.items});
-            try ld_script_bytes.resize(gpa, size);
-            const buf = ld_script_bytes.items[n..];
-            const n2 = file.readAll(buf) catch |err|
-                fatal("failed to read {s}: {s}", .{ test_path.items, @errorName(err) });
-            if (n2 != buf.len) fatal("failed to read {s}: unexpected end of file", .{test_path.items});
-            var diags = link.Diags.init(gpa);
-            defer diags.deinit();
-            const ld_script_result = link.LdScript.parse(gpa, &diags, Path.initCwd(test_path.items), 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);
-
-                var error_bundle = try wip_errors.toOwnedBundle("");
-                defer error_bundle.deinit(gpa);
-
-                const color: Color = .auto;
-                error_bundle.renderToStdErr(color.renderOptions());
-
-                process.exit(1);
-            }
-
-            var ld_script = ld_script_result catch |err|
-                fatal("{s}: failed to parse linker script: {s}", .{ test_path.items, @errorName(err) });
-            defer ld_script.deinit(gpa);
-
-            try external_linker_inputs.ensureUnusedCapacity(arena, ld_script.args.len);
-            for (ld_script.args) |arg| {
-                const syslib: SystemLib = .{
-                    .needed = arg.needed or parent.needed,
-                    .weak = parent.weak,
-                    .preferred_mode = parent.preferred_mode,
-                    .search_strategy = parent.search_strategy,
-                };
-                if (mem.startsWith(u8, arg.path, "-l")) {
-                    external_linker_inputs.appendAssumeCapacity(.{ .named = .{
-                        .name = try arena.dupe(u8, arg.path["-l".len..]),
-                        .info = syslib,
-                    } });
-                } else {
-                    external_linker_inputs.appendAssumeCapacity(.{ .path = .{
-                        .path = Path.initCwd(try arena.dupe(u8, arg.path)),
-                        .info = syslib,
-                    } });
-                }
-            }
-            return .ok;
-        }
-
-        fs.cwd().access(test_path.items, .{}) catch |err| switch (err) {
-            error.FileNotFound => break :main_check,
-            else => |e| fatal("unable to search for {s} library '{s}': {s}", .{
-                @tagName(link_mode), test_path.items, @errorName(e),
-            }),
-        };
-        return finishAccessLibPath(arena, create_module, test_path, link_mode, parent, lib_name);
-    }
-
-    // In the case of Darwin, the main check will be .dylib, so here we
-    // additionally check for .so files.
-    if (target.isDarwin() and link_mode == .dynamic) so: {
-        test_path.clearRetainingCapacity();
-        try test_path.writer(gpa).print("{s}" ++ sep ++ "lib{s}.so", .{ lib_dir_path, lib_name });
-        try checked_paths.writer(gpa).print("\n  {s}", .{test_path.items});
-        fs.cwd().access(test_path.items, .{}) catch |err| switch (err) {
-            error.FileNotFound => break :so,
-            else => |e| fatal("unable to search for so library '{s}': {s}", .{
-                test_path.items, @errorName(e),
-            }),
-        };
-        return finishAccessLibPath(arena, create_module, test_path, link_mode, parent, lib_name);
-    }
-
-    // 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: {
-        test_path.clearRetainingCapacity();
-        try test_path.writer(gpa).print("{s}" ++ sep ++ "lib{s}.a", .{
-            lib_dir_path, lib_name,
-        });
-        try checked_paths.writer(gpa).print("\n  {s}", .{test_path.items});
-        fs.cwd().access(test_path.items, .{}) catch |err| switch (err) {
-            error.FileNotFound => break :mingw,
-            else => |e| fatal("unable to search for static library '{s}': {s}", .{
-                test_path.items, @errorName(e),
-            }),
-        };
-        return finishAccessLibPath(arena, create_module, test_path, link_mode, parent, lib_name);
-    }
-
-    return .no_match;
-}
-
-fn finishAccessLibPath(
-    arena: Allocator,
-    create_module: *CreateModule,
-    test_path: *std.ArrayListUnmanaged(u8),
-    link_mode: std.builtin.LinkMode,
-    parent: SystemLib,
-    lib_name: []const u8,
-) Allocator.Error!AccessLibPathResult {
-    const path = Path.initCwd(try arena.dupe(u8, test_path.items));
-    switch (link_mode) {
-        .static => try create_module.link_objects.append(arena, .{ .path = path }),
-        .dynamic => try create_module.resolved_system_libs.append(arena, .{
-            .name = lib_name,
-            .lib = .{
-                .needed = parent.needed,
-                .weak = parent.weak,
-                .path = path,
-            },
-        }),
-    }
-    return .ok;
-}
-
 fn accessFrameworkPath(
     test_path: *std.ArrayList(u8),
     checked_paths: *std.ArrayList(u8),
@@ -7218,7 +6914,7 @@ fn cmdFetch(
     });
     defer root_prog_node.end();
 
-    var global_cache_directory: Compilation.Directory = l: {
+    var global_cache_directory: Directory = l: {
         const p = override_global_cache_dir orelse try introspect.resolveGlobalCacheDir(arena);
         break :l .{
             .handle = try fs.cwd().makeOpenPath(p, .{}),
@@ -7795,18 +7491,3 @@ fn handleModArg(
     c_source_files_owner_index.* = create_module.c_source_files.items.len;
     rc_source_files_owner_index.* = create_module.rc_source_files.items.len;
 }
-
-fn stripLibPrefixAndSuffix(path: []const u8, target: std.Target) struct { []const u8, std.builtin.LinkMode } {
-    const prefix = target.libPrefix();
-    const static_suffix = target.staticLibSuffix();
-    const dynamic_suffix = target.dynamicLibSuffix();
-    const basename = fs.path.basename(path);
-    const unlibbed = if (mem.startsWith(u8, basename, prefix)) basename[prefix.len..] else basename;
-    if (mem.endsWith(u8, unlibbed, static_suffix)) return .{
-        unlibbed[0 .. unlibbed.len - static_suffix.len], .static,
-    };
-    if (mem.endsWith(u8, unlibbed, dynamic_suffix)) return .{
-        unlibbed[0 .. unlibbed.len - dynamic_suffix.len], .dynamic,
-    };
-    fatal("unrecognized library path: {s}", .{path});
-}
src/Sema.zig
@@ -9595,7 +9595,7 @@ fn resolveGenericBody(
 }
 
 /// Given a library name, examines if the library name should end up in
-/// `link.File.Options.system_libs` table (for example, libc is always
+/// `link.File.Options.windows_libs` table (for example, libc is always
 /// specified via dedicated flag `link_libc` instead),
 /// and puts it there if it doesn't exist.
 /// It also dupes the library name which can then be saved as part of the