diff --git a/lib/std/build.zig b/lib/std/build.zig index 57304242cd..0b49087747 100644 --- a/lib/std/build.zig +++ b/lib/std/build.zig @@ -1586,6 +1586,13 @@ pub const LibExeObjStep = struct { /// (Darwin) Size of the pagezero segment. pagezero_size: ?u64 = null, + /// (Darwin) Search strategy for searching system libraries. Either `paths_first` or `dylibs_first`. + /// The former lowers to `-search_paths_first` linker option, while the latter to `-search_dylibs_first` + /// option. + /// By default, if no option is specified, the linker assumes `paths_first` as the default + /// search strategy. + search_strategy: ?enum { paths_first, dylibs_first } = null, + /// Position Independent Code force_pic: ?bool = null, @@ -2650,6 +2657,10 @@ pub const LibExeObjStep = struct { const size = try std.fmt.allocPrint(builder.allocator, "{x}", .{pagezero_size}); try zig_args.appendSlice(&[_][]const u8{ "-pagezero_size", size }); } + if (self.search_strategy) |strat| switch (strat) { + .paths_first => try zig_args.append("-search_paths_first"), + .dylibs_first => try zig_args.append("-search_dylibs_first"), + }; if (self.bundle_compiler_rt) |x| { if (x) { diff --git a/src/Compilation.zig b/src/Compilation.zig index 8cb93b5473..ccaad7c8c4 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -905,6 +905,8 @@ pub const InitOptions = struct { entitlements: ?[]const u8 = null, /// (Darwin) size of the __PAGEZERO segment pagezero_size: ?u64 = null, + /// (Darwin) search strategy for system libraries + search_strategy: ?link.File.MachO.SearchStrategy = null, }; fn addPackageTableToCacheHash( @@ -1745,6 +1747,7 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation { .install_name = options.install_name, .entitlements = options.entitlements, .pagezero_size = options.pagezero_size, + .search_strategy = options.search_strategy, }); errdefer bin_file.destroy(); comp.* = .{ @@ -2360,7 +2363,7 @@ fn prepareWholeEmitSubPath(arena: Allocator, opt_emit: ?EmitLoc) error{OutOfMemo /// to remind the programmer to update multiple related pieces of code that /// are in different locations. Bump this number when adding or deleting /// anything from the link cache manifest. -pub const link_hash_implementation_version = 4; +pub const link_hash_implementation_version = 5; fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifest) !void { const gpa = comp.gpa; @@ -2370,7 +2373,7 @@ fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifes defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); - comptime assert(link_hash_implementation_version == 4); + comptime assert(link_hash_implementation_version == 5); if (comp.bin_file.options.module) |mod| { const main_zig_file = try mod.main_pkg.root_src_directory.join(arena, &[_][]const u8{ @@ -2476,6 +2479,7 @@ fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifes man.hash.addListOfBytes(comp.bin_file.options.frameworks); try man.addOptionalFile(comp.bin_file.options.entitlements); man.hash.addOptional(comp.bin_file.options.pagezero_size); + man.hash.addOptional(comp.bin_file.options.search_strategy); // COFF specific stuff man.hash.addOptional(comp.bin_file.options.subsystem); diff --git a/src/link.zig b/src/link.zig index c78fb8c2c5..cfcb112e9b 100644 --- a/src/link.zig +++ b/src/link.zig @@ -190,6 +190,9 @@ pub const Options = struct { /// (Darwin) size of the __PAGEZERO segment pagezero_size: ?u64 = null, + /// (Darwin) search strategy for system libraries + search_strategy: ?File.MachO.SearchStrategy = null, + pub fn effectiveOutputMode(options: Options) std.builtin.OutputMode { return if (options.use_lld) .Obj else options.output_mode; } diff --git a/src/link/Coff.zig b/src/link/Coff.zig index 6b4ff26bce..3418a206df 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -969,7 +969,7 @@ fn linkWithLLD(self: *Coff, comp: *Compilation, prog_node: *std.Progress.Node) ! man = comp.cache_parent.obtain(); self.base.releaseLock(); - comptime assert(Compilation.link_hash_implementation_version == 4); + comptime assert(Compilation.link_hash_implementation_version == 5); for (self.base.options.objects) |obj| { _ = try man.addFile(obj.path, null); diff --git a/src/link/Elf.zig b/src/link/Elf.zig index 0bcde06f33..1051c0eb78 100644 --- a/src/link/Elf.zig +++ b/src/link/Elf.zig @@ -1298,7 +1298,7 @@ fn linkWithLLD(self: *Elf, comp: *Compilation, prog_node: *std.Progress.Node) !v // We are about to obtain this lock, so here we give other processes a chance first. self.base.releaseLock(); - comptime assert(Compilation.link_hash_implementation_version == 4); + comptime assert(Compilation.link_hash_implementation_version == 5); try man.addOptionalFile(self.base.options.linker_script); try man.addOptionalFile(self.base.options.version_script); diff --git a/src/link/MachO.zig b/src/link/MachO.zig index 0c44901d29..2bbd2e7a72 100644 --- a/src/link/MachO.zig +++ b/src/link/MachO.zig @@ -47,6 +47,11 @@ pub const DebugSymbols = @import("MachO/DebugSymbols.zig"); pub const base_tag: File.Tag = File.Tag.macho; +pub const SearchStrategy = enum { + paths_first, + dylibs_first, +}; + base: File, /// If this is not null, an object file is created by LLVM and linked with LLD afterwards. @@ -536,7 +541,7 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No // We are about to obtain this lock, so here we give other processes a chance first. self.base.releaseLock(); - comptime assert(Compilation.link_hash_implementation_version == 4); + comptime assert(Compilation.link_hash_implementation_version == 5); for (self.base.options.objects) |obj| { _ = try man.addFile(obj.path, null); @@ -550,6 +555,7 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No // installation sources because they are always a product of the compiler version + target information. man.hash.add(stack_size); man.hash.addOptional(self.base.options.pagezero_size); + man.hash.addOptional(self.base.options.search_strategy); man.hash.addListOfBytes(self.base.options.lib_dirs); man.hash.addListOfBytes(self.base.options.framework_dirs); man.hash.addListOfBytes(self.base.options.frameworks); @@ -784,18 +790,43 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No } var libs = std.ArrayList([]const u8).init(arena); - for (search_lib_names.items) |lib_name| { - // Assume ld64 default: -search_paths_first - // Look in each directory for a dylib (stub first), and then for archive - // TODO implement alternative: -search_dylibs_first - for (&[_][]const u8{ ".tbd", ".dylib", ".a" }) |ext| { - if (try resolveLib(arena, lib_dirs.items, lib_name, ext)) |full_path| { - try libs.append(full_path); - break; - } - } else { - log.warn("library not found for '-l{s}'", .{lib_name}); - lib_not_found = true; + + // Assume ld64 default -search_paths_first if no strategy specified. + const search_strategy = self.base.options.search_strategy orelse .paths_first; + outer: for (search_lib_names.items) |lib_name| { + switch (search_strategy) { + .paths_first => { + // Look in each directory for a dylib (stub first), and then for archive + for (lib_dirs.items) |dir| { + for (&[_][]const u8{ ".tbd", ".dylib", ".a" }) |ext| { + if (try resolveLib(arena, dir, lib_name, ext)) |full_path| { + try libs.append(full_path); + continue :outer; + } + } + } else { + log.warn("library not found for '-l{s}'", .{lib_name}); + lib_not_found = true; + } + }, + .dylibs_first => { + // First, look for a dylib in each search dir + for (lib_dirs.items) |dir| { + for (&[_][]const u8{ ".tbd", ".dylib" }) |ext| { + if (try resolveLib(arena, dir, lib_name, ext)) |full_path| { + try libs.append(full_path); + continue :outer; + } + } + } else for (lib_dirs.items) |dir| { + if (try resolveLib(arena, dir, lib_name, ".a")) |full_path| { + try libs.append(full_path); + } else { + log.warn("library not found for '-l{s}'", .{lib_name}); + lib_not_found = true; + } + } + }, } } @@ -811,19 +842,23 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No if (self.base.options.sysroot != null) blk: { // Try stub file first. If we hit it, then we're done as the stub file // re-exports every single symbol definition. - if (try resolveLib(arena, lib_dirs.items, "System", ".tbd")) |full_path| { - try libs.append(full_path); - libsystem_available = true; - break :blk; + for (lib_dirs.items) |dir| { + if (try resolveLib(arena, dir, "System", ".tbd")) |full_path| { + try libs.append(full_path); + libsystem_available = true; + break :blk; + } } // If we didn't hit the stub file, try .dylib next. However, libSystem.dylib // doesn't export libc.dylib which we'll need to resolve subsequently also. - if (try resolveLib(arena, lib_dirs.items, "System", ".dylib")) |libsystem_path| { - if (try resolveLib(arena, lib_dirs.items, "c", ".dylib")) |libc_path| { - try libs.append(libsystem_path); - try libs.append(libc_path); - libsystem_available = true; - break :blk; + for (lib_dirs.items) |dir| { + if (try resolveLib(arena, dir, "System", ".dylib")) |libsystem_path| { + if (try resolveLib(arena, dir, "c", ".dylib")) |libc_path| { + try libs.append(libsystem_path); + try libs.append(libc_path); + libsystem_available = true; + break :blk; + } } } } @@ -847,11 +882,13 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No } } - for (self.base.options.frameworks) |framework| { - for (&[_][]const u8{ ".tbd", ".dylib", "" }) |ext| { - if (try resolveFramework(arena, framework_dirs.items, framework, ext)) |full_path| { - try libs.append(full_path); - break; + outer: for (self.base.options.frameworks) |framework| { + for (framework_dirs.items) |dir| { + for (&[_][]const u8{ ".tbd", ".dylib", "" }) |ext| { + if (try resolveFramework(arena, dir, framework, ext)) |full_path| { + try libs.append(full_path); + continue :outer; + } } } else { log.warn("framework not found for '-framework {s}'", .{framework}); @@ -934,12 +971,36 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No try argv.append(try std.fmt.allocPrint(arena, "0x{x}", .{pagezero_size})); } + if (self.base.options.search_strategy) |strat| switch (strat) { + .paths_first => try argv.append("-search_paths_first"), + .dylibs_first => try argv.append("-search_dylibs_first"), + }; + if (self.base.options.entry) |entry| { try argv.append("-e"); try argv.append(entry); } - try argv.appendSlice(positionals.items); + for (self.base.options.objects) |obj| { + try argv.append(obj.path); + } + + for (comp.c_object_table.keys()) |key| { + try argv.append(key.status.success.object_path); + } + + if (module_obj_path) |p| { + try argv.append(p); + } + + if (comp.compiler_rt_lib) |lib| { + try argv.append(lib.full_object_path); + } + + if (self.base.options.link_libcpp) { + try argv.append(comp.libcxxabi_static_lib.?.full_object_path); + try argv.append(comp.libcxx_static_lib.?.full_object_path); + } try argv.append("-o"); try argv.append(full_out_path); @@ -947,7 +1008,7 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No try argv.append("-lSystem"); try argv.append("-lc"); - for (search_lib_names.items) |l_name| { + for (self.base.options.system_libs.keys()) |l_name| { try argv.append(try std.fmt.allocPrint(arena, "-l{s}", .{l_name})); } @@ -1183,51 +1244,41 @@ fn resolveSearchDir( fn resolveLib( arena: Allocator, - search_dirs: []const []const u8, + search_dir: []const u8, name: []const u8, ext: []const u8, ) !?[]const u8 { const search_name = try std.fmt.allocPrint(arena, "lib{s}{s}", .{ name, ext }); + const full_path = try fs.path.join(arena, &[_][]const u8{ search_dir, search_name }); - for (search_dirs) |dir| { - const full_path = try fs.path.join(arena, &[_][]const u8{ dir, search_name }); + // Check if the file exists. + const tmp = fs.cwd().openFile(full_path, .{}) catch |err| switch (err) { + error.FileNotFound => return null, + else => |e| return e, + }; + defer tmp.close(); - // Check if the file exists. - const tmp = fs.cwd().openFile(full_path, .{}) catch |err| switch (err) { - error.FileNotFound => continue, - else => |e| return e, - }; - defer tmp.close(); - - return full_path; - } - - return null; + return full_path; } fn resolveFramework( arena: Allocator, - search_dirs: []const []const u8, + search_dir: []const u8, name: []const u8, ext: []const u8, ) !?[]const u8 { const search_name = try std.fmt.allocPrint(arena, "{s}{s}", .{ name, ext }); const prefix_path = try std.fmt.allocPrint(arena, "{s}.framework", .{name}); + const full_path = try fs.path.join(arena, &[_][]const u8{ search_dir, prefix_path, search_name }); - for (search_dirs) |dir| { - const full_path = try fs.path.join(arena, &[_][]const u8{ dir, prefix_path, search_name }); + // Check if the file exists. + const tmp = fs.cwd().openFile(full_path, .{}) catch |err| switch (err) { + error.FileNotFound => return null, + else => |e| return e, + }; + defer tmp.close(); - // Check if the file exists. - const tmp = fs.cwd().openFile(full_path, .{}) catch |err| switch (err) { - error.FileNotFound => continue, - else => |e| return e, - }; - defer tmp.close(); - - return full_path; - } - - return null; + return full_path; } fn parseObject(self: *MachO, path: []const u8) !bool { diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index 4357a588a1..c654fbe719 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -2481,7 +2481,7 @@ fn linkWithLLD(self: *Wasm, comp: *Compilation, prog_node: *std.Progress.Node) ! // We are about to obtain this lock, so here we give other processes a chance first. self.base.releaseLock(); - comptime assert(Compilation.link_hash_implementation_version == 4); + comptime assert(Compilation.link_hash_implementation_version == 5); for (self.base.options.objects) |obj| { _ = try man.addFile(obj.path, null); diff --git a/src/main.zig b/src/main.zig index 6942d0ba49..3d65875975 100644 --- a/src/main.zig +++ b/src/main.zig @@ -448,6 +448,8 @@ const usage_build_generic = \\ -install_name=[value] (Darwin) add dylib's install name \\ --entitlements [path] (Darwin) add path to entitlements file for embedding in code signature \\ -pagezero_size [value] (Darwin) size of the __PAGEZERO segment in hexadecimal notation + \\ -search_paths_first (Darwin) search each dir in library search paths for `libx.dylib` then `libx.a` + \\ -search_dylibs_first (Darwin) search `libx.dylib` in each dir in library search paths, then `libx.a` \\ --import-memory (WebAssembly) import memory from the environment \\ --import-table (WebAssembly) import function table from the host environment \\ --export-table (WebAssembly) export function table to the host environment @@ -696,6 +698,7 @@ fn buildOutputType( var hash_style: link.HashStyle = .both; var entitlements: ?[]const u8 = null; var pagezero_size: ?u64 = null; + var search_strategy: ?link.File.MachO.SearchStrategy = null; // e.g. -m3dnow or -mno-outline-atomics. They correspond to std.Target llvm cpu feature names. // This array is populated by zig cc frontend and then has to be converted to zig-style @@ -917,6 +920,10 @@ fn buildOutputType( pagezero_size = std.fmt.parseUnsigned(u64, eatIntPrefix(next_arg, 16), 16) catch |err| { fatal("unable to parse '{s}': {s}", .{ arg, @errorName(err) }); }; + } else if (mem.eql(u8, arg, "-search_paths_first")) { + search_strategy = .paths_first; + } else if (mem.eql(u8, arg, "-search_dylibs_first")) { + search_strategy = .dylibs_first; } else if (mem.eql(u8, arg, "-T") or mem.eql(u8, arg, "--script")) { linker_script = args_iter.next() orelse { fatal("expected parameter after {s}", .{arg}); @@ -1475,6 +1482,10 @@ fn buildOutputType( mem.eql(u8, linker_arg, "-static")) { force_static_libs = true; + } else if (mem.eql(u8, linker_arg, "-search_paths_first")) { + search_strategy = .paths_first; + } else if (mem.eql(u8, linker_arg, "-search_dylibs_first")) { + search_strategy = .dylibs_first; } else { try linker_args.append(linker_arg); } @@ -2141,6 +2152,7 @@ fn buildOutputType( } for (lib_dirs.items) |lib_dir_path| { + if (cross_target.isDarwin()) break; // Targeting Darwin we let the linker resolve the libraries in the correct order test_path.clearRetainingCapacity(); try test_path.writer().print("{s}" ++ sep ++ "{s}{s}{s}", .{ lib_dir_path, @@ -2782,6 +2794,7 @@ fn buildOutputType( .install_name = install_name, .entitlements = entitlements, .pagezero_size = pagezero_size, + .search_strategy = search_strategy, }) catch |err| switch (err) { error.LibCUnavailable => { const target = target_info.target; diff --git a/test/link.zig b/test/link.zig index 42ce12f73c..62bdcff4b0 100644 --- a/test/link.zig +++ b/test/link.zig @@ -60,5 +60,9 @@ pub fn addCases(cases: *tests.StandaloneContext) void { cases.addBuildFile("test/link/macho/stack_size/build.zig", .{ .build_modes = true, }); + + cases.addBuildFile("test/link/macho/search_strategy/build.zig", .{ + .build_modes = true, + }); } } diff --git a/test/link/macho/search_strategy/a.c b/test/link/macho/search_strategy/a.c new file mode 100644 index 0000000000..199b31e1a0 --- /dev/null +++ b/test/link/macho/search_strategy/a.c @@ -0,0 +1,7 @@ +#include + +char world[] = "world"; + +char* hello() { + return "Hello"; +} diff --git a/test/link/macho/search_strategy/build.zig b/test/link/macho/search_strategy/build.zig new file mode 100644 index 0000000000..c5e867c8d0 --- /dev/null +++ b/test/link/macho/search_strategy/build.zig @@ -0,0 +1,68 @@ +const std = @import("std"); +const Builder = std.build.Builder; +const LibExeObjectStep = std.build.LibExeObjStep; + +pub fn build(b: *Builder) void { + const mode = b.standardReleaseOptions(); + + const test_step = b.step("test", "Test"); + test_step.dependOn(b.getInstallStep()); + + { + // -search_dylibs_first + const exe = createScenario(b, mode); + exe.search_strategy = .dylibs_first; + + const check = exe.checkObject(.macho); + check.checkStart("cmd LOAD_DYLIB"); + check.checkNext("name @rpath/liba.dylib"); + + test_step.dependOn(&check.step); + + const run = exe.run(); + run.cwd = b.pathFromRoot("."); + run.expectStdOutEqual("Hello world"); + test_step.dependOn(&run.step); + } + + { + // -search_paths_first + const exe = createScenario(b, mode); + exe.search_strategy = .paths_first; + + const run = exe.run(); + run.cwd = b.pathFromRoot("."); + run.expectStdOutEqual("Hello world"); + test_step.dependOn(&run.step); + } +} + +fn createScenario(b: *Builder, mode: std.builtin.Mode) *LibExeObjectStep { + const static = b.addStaticLibrary("a", null); + static.setBuildMode(mode); + static.addCSourceFile("a.c", &.{}); + static.linkLibC(); + static.override_dest_dir = std.build.InstallDir{ + .custom = "static", + }; + static.install(); + + const dylib = b.addSharedLibrary("a", null, b.version(1, 0, 0)); + dylib.setBuildMode(mode); + dylib.addCSourceFile("a.c", &.{}); + dylib.linkLibC(); + dylib.override_dest_dir = std.build.InstallDir{ + .custom = "dynamic", + }; + dylib.install(); + + const exe = b.addExecutable("main", null); + exe.setBuildMode(mode); + exe.addCSourceFile("main.c", &.{}); + exe.linkSystemLibraryName("a"); + exe.linkLibC(); + exe.addLibraryPath(b.pathFromRoot("zig-out/static")); + exe.addLibraryPath(b.pathFromRoot("zig-out/dynamic")); + exe.addRPath(b.pathFromRoot("zig-out/dynamic")); + return exe; +} diff --git a/test/link/macho/search_strategy/main.c b/test/link/macho/search_strategy/main.c new file mode 100644 index 0000000000..be1647ddad --- /dev/null +++ b/test/link/macho/search_strategy/main.c @@ -0,0 +1,9 @@ +#include + +char* hello(); +extern char world[]; + +int main() { + printf("%s %s", hello(), world); + return 0; +}