diff --git a/README.md b/README.md index 6f18bcd9..286cce64 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ You will need latest Zig in your path. You can get nightly binaries from [here]( $ zig build ``` -This will create the `zld` binary in `zig-out/bin/zld`. You can then use it like you'd use a standard linker. +This will create the `ld.zld` (Elf), `ld64.zld` (MachO) and `link-zld` (Coff) binaries in `zig-out/bin/`. +You can then use it like you'd use a standard linker. ``` $ cat < hello.c @@ -30,8 +31,11 @@ $ clang -c hello.c # Or, create .o using zig cc $ zig cc -c hello.c -# Link away! -$ ./zig-out/bin/zld hello.o -o hello +# On macOS +$ ./zig-out/bin/ld64.zld hello.o -o hello + +# On Linux +$ ./zig-out/bin/ld.zld hello.o -o hello # Run! $ ./hello diff --git a/build.zig b/build.zig index 52a31781..7804606b 100644 --- a/build.zig +++ b/build.zig @@ -1,14 +1,16 @@ -const Builder = @import("std").build.Builder; +const std = @import("std"); +const fs = std.fs; +const log = std.log; + +const Allocator = std.mem.Allocator; +const Builder = std.build.Builder; +const FileSource = std.build.FileSource; +const LibExeObjStep = std.build.LibExeObjStep; +const InstallDir = std.build.InstallDir; +const Step = std.build.Step; pub fn build(b: *Builder) void { - // Standard target options allows the person running `zig build` to choose - // what target to build for. Here we do not override the defaults, which - // means any target is allowed, and the default is native. Other options - // for restricting supported target set are available. const target = b.standardTargetOptions(.{}); - - // Standard release options allow the person running `zig build` to select - // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. const mode = b.standardReleaseOptions(); const enable_logging = b.option(bool, "log", "Whether to enable logging") orelse false; @@ -27,17 +29,13 @@ pub fn build(b: *Builder) void { const exe_opts = b.addOptions(); exe.addOptions("build_options", exe_opts); exe_opts.addOption(bool, "enable_logging", enable_logging); - exe.install(); - const run_cmd = exe.run(); - run_cmd.step.dependOn(b.getInstallStep()); - if (b.args) |args| { - run_cmd.addArgs(args); - } + const elf_symlink = symlink(exe, "ld.zld"); + elf_symlink.step.dependOn(&exe.step); - const run_step = b.step("run", "Run the app"); - run_step.dependOn(&run_cmd.step); + const macho_symlink = symlink(exe, "ld64.zld"); + macho_symlink.step.dependOn(&exe.step); const tests = b.addTest("src/test.zig"); tests.setBuildMode(mode); @@ -47,8 +45,52 @@ pub fn build(b: *Builder) void { const test_opts = b.addOptions(); tests.addOptions("build_options", test_opts); test_opts.addOption(bool, "enable_qemu", is_qemu_enabled); + test_opts.addOption(bool, "enable_logging", enable_logging); const test_step = b.step("test", "Run library and end-to-end tests"); - test_step.dependOn(&exe.step); test_step.dependOn(&tests.step); } + +fn symlink(exe: *LibExeObjStep, name: []const u8) *CreateSymlinkStep { + const step = CreateSymlinkStep.create(exe.builder, exe.getOutputSource(), name); + exe.builder.getInstallStep().dependOn(&step.step); + return step; +} + +const CreateSymlinkStep = struct { + pub const base_id = .custom; + + step: Step, + builder: *Builder, + source: FileSource, + target: []const u8, + + pub fn create( + builder: *Builder, + source: FileSource, + target: []const u8, + ) *CreateSymlinkStep { + const self = builder.allocator.create(CreateSymlinkStep) catch unreachable; + self.* = CreateSymlinkStep{ + .builder = builder, + .step = Step.init(.log, builder.fmt("symlink {s} -> {s}", .{ + source.getDisplayName(), + target, + }), builder.allocator, make), + .source = source, + .target = builder.dupe(target), + }; + return self; + } + + fn make(step: *Step) anyerror!void { + const self = @fieldParentPtr(CreateSymlinkStep, "step", step); + const rel_source = fs.path.basename(self.source.getPath(self.builder)); + const source_path = self.builder.getInstallPath(.bin, rel_source); + const target_path = self.builder.getInstallPath(.bin, self.target); + fs.atomicSymLink(self.builder.allocator, source_path, target_path) catch |err| { + log.err("Unable to symlink {s} -> {s}", .{ source_path, target_path }); + return err; + }; + } +}; diff --git a/src/Coff.zig b/src/Coff.zig index 17cf3eee..eabb07bc 100644 --- a/src/Coff.zig +++ b/src/Coff.zig @@ -10,15 +10,17 @@ const mem = std.mem; const Allocator = mem.Allocator; const Object = @import("Coff/Object.zig"); +pub const Options = @import("Coff/Options.zig"); const Zld = @import("Zld.zig"); pub const base_tag = Zld.Tag.coff; base: Zld, +options: Options, objects: std.ArrayListUnmanaged(Object) = .{}, -pub fn openPath(allocator: Allocator, options: Zld.Options) !*Coff { +pub fn openPath(allocator: Allocator, options: Options) !*Coff { const file = try options.emit.directory.createFile(options.emit.sub_path, .{ .truncate = true, .read = true, @@ -34,24 +36,22 @@ pub fn openPath(allocator: Allocator, options: Zld.Options) !*Coff { return self; } -fn createEmpty(gpa: Allocator, options: Zld.Options) !*Coff { +fn createEmpty(gpa: Allocator, options: Options) !*Coff { const self = try gpa.create(Coff); self.* = .{ .base = .{ .tag = .coff, - .options = options, .allocator = gpa, .file = undefined, }, + .options = options, }; return self; } pub fn deinit(self: *Coff) void { - self.closeFiles(); - for (self.objects.items) |*object| { object.deinit(self.base.allocator); } @@ -59,7 +59,7 @@ pub fn deinit(self: *Coff) void { self.objects.deinit(self.base.allocator); } -pub fn closeFiles(self: Coff) void { +pub fn closeFiles(self: *const Coff) void { for (self.objects.items) |object| { object.file.close(); } @@ -70,9 +70,9 @@ pub fn flush(self: *Coff) !void { var positionals = std.ArrayList([]const u8).init(gpa); defer positionals.deinit(); - try positionals.ensureTotalCapacity(self.base.options.positionals.len); + try positionals.ensureTotalCapacity(self.options.positionals.len); - for (self.base.options.positionals) |obj| { + for (self.options.positionals) |obj| { positionals.appendAssumeCapacity(obj.path); } @@ -110,7 +110,7 @@ fn parseObject(self: *Coff, path: []const u8) !bool { .file = file, }; - object.parse(self.base.allocator, self.base.options.target) catch |err| switch (err) { + object.parse(self.base.allocator, self.options.target.cpu_arch.?) catch |err| switch (err) { error.EndOfStream => { object.deinit(self.base.allocator); return false; diff --git a/src/Coff/Object.zig b/src/Coff/Object.zig index e845bf21..5cc3a00f 100644 --- a/src/Coff/Object.zig +++ b/src/Coff/Object.zig @@ -96,10 +96,10 @@ pub const IMAGE_SYM_CLASS_SECTION = 104; pub const IMAGE_SYM_CLASS_WEAK_EXTERNAL = 105; pub const IMAGE_SYM_CLASS_CLR_TOKEN = 107; -// comptime { -// assert(@sizeOf(Symbol) == 18); -// assert(@sizeOf(CoffHeader) == 20); -// } +comptime { + assert(@sizeOf(Symbol) == 18); + assert(@sizeOf(CoffHeader) == 20); +} pub fn deinit(self: *Object, allocator: Allocator) void { self.symtab.deinit(allocator); @@ -108,7 +108,7 @@ pub fn deinit(self: *Object, allocator: Allocator) void { allocator.free(self.name); } -pub fn parse(self: *Object, allocator: Allocator, target: std.Target) !void { +pub fn parse(self: *Object, allocator: Allocator, cpu_arch: std.Target.Cpu.Arch) !void { const reader = self.file.reader(); const header = try reader.readStruct(CoffHeader); @@ -117,10 +117,10 @@ pub fn parse(self: *Object, allocator: Allocator, target: std.Target) !void { return error.NotObject; } - if (header.machine != @enumToInt(target.cpu.arch.toCoffMachine())) { + if (header.machine != @enumToInt(cpu_arch.toCoffMachine())) { log.debug("Invalid architecture {any}, expected {any}", .{ header.machine, - target.cpu.arch.toCoffMachine(), + cpu_arch.toCoffMachine(), }); return error.InvalidCpuArch; } diff --git a/src/Coff/Options.zig b/src/Coff/Options.zig new file mode 100644 index 00000000..012b14ce --- /dev/null +++ b/src/Coff/Options.zig @@ -0,0 +1,91 @@ +const Options = @This(); + +const std = @import("std"); +const builtin = @import("builtin"); +const io = std.io; +const mem = std.mem; +const process = std.process; + +const Allocator = mem.Allocator; +const CrossTarget = std.zig.CrossTarget; +const Coff = @import("../Coff.zig"); +const Zld = @import("../Zld.zig"); + +const usage = + \\Usage: link-zld [files...] + \\ + \\General Options: + \\-l[name] Specify library to link against + \\-L[path] Specify library search dir + \\-o [path] Specify output path for the final artifact + \\-h, --help Print this help and exit + \\--debug-log [scope] Turn on debugging logs for [scope] (requires zld compiled with -Dlog) +; + +emit: Zld.Emit, +output_mode: Zld.OutputMode, +target: CrossTarget, +positionals: []const Zld.LinkObject, +libs: std.StringArrayHashMap(Zld.SystemLib), +lib_dirs: []const []const u8, + +pub fn parseArgs(arena: Allocator, ctx: Zld.MainCtx) !Options { + if (ctx.args.len == 0) { + ctx.printSuccess("{s}", .{usage}); + } + + var positionals = std.ArrayList(Zld.LinkObject).init(arena); + var libs = std.StringArrayHashMap(Zld.SystemLib).init(arena); + var lib_dirs = std.ArrayList([]const u8).init(arena); + var out_path: ?[]const u8 = null; + + const Iterator = struct { + args: []const []const u8, + i: usize = 0, + fn next(it: *@This()) ?[]const u8 { + if (it.i >= it.args.len) { + return null; + } + defer it.i += 1; + return it.args[it.i]; + } + }; + var args_iter = Iterator{ .args = ctx.args }; + + while (args_iter.next()) |arg| { + if (mem.eql(u8, arg, "--help") or mem.eql(u8, arg, "-h")) { + ctx.printSuccess("{s}", .{usage}); + } else if (mem.eql(u8, arg, "--debug-log")) { + const scope = args_iter.next() orelse ctx.printFailure("Expected log scope after {s}", .{arg}); + try ctx.log_scopes.append(scope); + } else if (mem.startsWith(u8, arg, "-l")) { + try libs.put(arg[2..], .{}); + } else if (mem.startsWith(u8, arg, "-L")) { + try lib_dirs.append(arg[2..]); + } else if (mem.eql(u8, arg, "-o")) { + out_path = args_iter.next() orelse + ctx.printFailure("Expected output path after {s}", .{arg}); + } else { + try positionals.append(.{ + .path = arg, + .must_link = true, + }); + } + } + + if (positionals.items.len == 0) { + ctx.printFailure("Expected at least one input .o file", .{}); + } + + return Options{ + .emit = .{ + .directory = std.fs.cwd(), + .sub_path = out_path orelse "a.out", + }, + .target = CrossTarget.fromTarget(builtin.target), + .output_mode = .exe, + .positionals = positionals.items, + .libs = libs, + .lib_dirs = lib_dirs.items, + }; +} diff --git a/src/Elf.zig b/src/Elf.zig index d06f3575..e8bba41e 100644 --- a/src/Elf.zig +++ b/src/Elf.zig @@ -12,12 +12,14 @@ const Allocator = mem.Allocator; const Archive = @import("Elf/Archive.zig"); const Atom = @import("Elf/Atom.zig"); const Object = @import("Elf/Object.zig"); +pub const Options = @import("Elf/Options.zig"); const StringTable = @import("strtab.zig").StringTable; const Zld = @import("Zld.zig"); pub const base_tag = Zld.Tag.elf; base: Zld, +options: Options, archives: std.ArrayListUnmanaged(Archive) = .{}, objects: std.ArrayListUnmanaged(Object) = .{}, @@ -90,7 +92,7 @@ pub const SymbolWithLoc = struct { file: ?u32, }; -pub fn openPath(allocator: Allocator, options: Zld.Options) !*Elf { +pub fn openPath(allocator: Allocator, options: Options) !*Elf { const file = try options.emit.directory.createFile(options.emit.sub_path, .{ .truncate = true, .read = true, @@ -108,24 +110,22 @@ pub fn openPath(allocator: Allocator, options: Zld.Options) !*Elf { return self; } -fn createEmpty(gpa: Allocator, options: Zld.Options) !*Elf { +fn createEmpty(gpa: Allocator, options: Options) !*Elf { const self = try gpa.create(Elf); self.* = .{ .base = .{ .tag = .elf, - .options = options, .allocator = gpa, .file = undefined, }, + .options = options, }; return self; } pub fn deinit(self: *Elf) void { - self.closeFiles(); - self.atoms.deinit(self.base.allocator); for (self.managed_atoms.items) |atom| { atom.deinit(self.base.allocator); @@ -154,7 +154,7 @@ pub fn deinit(self: *Elf) void { self.archives.deinit(self.base.allocator); } -fn closeFiles(self: Elf) void { +pub fn closeFiles(self: *const Elf) void { for (self.objects.items) |object| { object.file.close(); } @@ -192,10 +192,8 @@ pub fn flush(self: *Elf) !void { defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); - const gc_sections = self.base.options.gc_sections orelse false; - var lib_dirs = std.ArrayList([]const u8).init(arena); - for (self.base.options.lib_dirs) |dir| { + for (self.options.lib_dirs) |dir| { // Verify that search path actually exists var tmp = fs.cwd().openDir(dir, .{}) catch |err| switch (err) { error.FileNotFound => continue, @@ -208,10 +206,10 @@ pub fn flush(self: *Elf) !void { var libs = std.StringArrayHashMap(Zld.SystemLib).init(arena); var lib_not_found = false; - for (self.base.options.libs.keys()) |lib_name| { + for (self.options.libs.keys()) |lib_name| { for (&[_][]const u8{ ".dylib", ".a" }) |ext| { if (try resolveLib(arena, lib_dirs.items, lib_name, ext)) |full_path| { - try libs.put(full_path, self.base.options.libs.get(lib_name).?); + try libs.put(full_path, self.options.libs.get(lib_name).?); break; } } else { @@ -227,8 +225,8 @@ pub fn flush(self: *Elf) !void { } var positionals = std.ArrayList([]const u8).init(arena); - try positionals.ensureTotalCapacity(self.base.options.positionals.len); - for (self.base.options.positionals) |obj| { + try positionals.ensureTotalCapacity(self.options.positionals.len); + for (self.options.positionals) |obj| { positionals.appendAssumeCapacity(obj.path); } @@ -257,7 +255,7 @@ pub fn flush(self: *Elf) !void { try object.parseIntoAtoms(self.base.allocator, @intCast(u16, object_id), self); } - if (gc_sections) { + if (self.options.gc_sections) { try self.gcAtoms(); } @@ -320,11 +318,11 @@ fn populateMetadata(self: *Elf) !void { if (self.header == null) { var header = elf.Elf64_Ehdr{ .e_ident = undefined, - .e_type = switch (self.base.options.output_mode) { + .e_type = switch (self.options.output_mode) { .exe => elf.ET.EXEC, .lib => elf.ET.DYN, }, - .e_machine = self.base.options.target.cpu.arch.toElfMachine(), + .e_machine = self.options.target.cpu_arch.?.toElfMachine(), .e_version = 1, .e_entry = 0, .e_phoff = @sizeOf(elf.Elf64_Ehdr), @@ -1251,7 +1249,7 @@ fn parseObject(self: *Elf, path: []const u8) !bool { .file = file, }; - object.parse(self.base.allocator, self.base.options.target) catch |err| switch (err) { + object.parse(self.base.allocator, self.options.target.cpu_arch.?) catch |err| switch (err) { error.EndOfStream, error.NotObject => { object.deinit(self.base.allocator); return false; @@ -1385,7 +1383,11 @@ fn resolveSymbolsInArchives(self: *Elf) !void { const object_id = @intCast(u16, self.objects.items.len); const object = try self.objects.addOne(self.base.allocator); - object.* = try archive.parseObject(self.base.allocator, self.base.options.target, offsets.items[0]); + object.* = try archive.parseObject( + self.base.allocator, + self.options.target.cpu_arch.?, + offsets.items[0], + ); try self.resolveSymbolsInObject(object_id); continue :loop; @@ -1829,7 +1831,7 @@ fn writeAtoms(self: *Elf) !void { } fn setEntryPoint(self: *Elf) !void { - if (self.base.options.output_mode != .exe) return; + if (self.options.output_mode != .exe) return; const global = self.globals.get("_start") orelse return error.DefaultEntryPointNotFound; const object = self.objects.items[global.file.?]; const sym = object.symtab.items[global.sym_index]; @@ -1837,7 +1839,7 @@ fn setEntryPoint(self: *Elf) !void { } fn setStackSize(self: *Elf) !void { - const stack_size = self.base.options.stack_size_override orelse return; + const stack_size = self.options.stack_size orelse return; const gnu_stack_phdr_index = self.gnu_stack_phdr_index orelse blk: { const gnu_stack_phdr_index = @intCast(u16, self.phdrs.items.len); try self.phdrs.append(self.base.allocator, .{ diff --git a/src/Elf/Archive.zig b/src/Elf/Archive.zig index ed3e9146..a59b4c41 100644 --- a/src/Elf/Archive.zig +++ b/src/Elf/Archive.zig @@ -172,7 +172,7 @@ fn getExtName(self: Archive, off: u32) []const u8 { return mem.sliceTo(@ptrCast([*:'\n']const u8, self.extnames_strtab.items.ptr + off), 0); } -pub fn parseObject(self: Archive, allocator: Allocator, target: std.Target, offset: u32) !Object { +pub fn parseObject(self: Archive, allocator: Allocator, cpu_arch: std.Target.Cpu.Arch, offset: u32) !Object { const reader = self.file.reader(); try reader.context.seekTo(offset); @@ -201,7 +201,7 @@ pub fn parseObject(self: Archive, allocator: Allocator, target: std.Target, offs .file_offset = @intCast(u32, try reader.context.getPos()), }; - try object.parse(allocator, target); + try object.parse(allocator, cpu_arch); try reader.context.seekTo(0); return object; diff --git a/src/Elf/Object.zig b/src/Elf/Object.zig index 5e9b6c54..9ae5fa53 100644 --- a/src/Elf/Object.zig +++ b/src/Elf/Object.zig @@ -45,7 +45,7 @@ pub fn deinit(self: *Object, allocator: Allocator) void { allocator.free(self.name); } -pub fn parse(self: *Object, allocator: Allocator, target: std.Target) !void { +pub fn parse(self: *Object, allocator: Allocator, cpu_arch: std.Target.Cpu.Arch) !void { const reader = self.file.reader(); if (self.file_offset) |offset| { try reader.context.seekTo(offset); @@ -72,10 +72,10 @@ pub fn parse(self: *Object, allocator: Allocator, target: std.Target) !void { log.debug("Invalid file type {any}, expected ET.REL", .{header.e_type}); return error.NotObject; } - if (header.e_machine != target.cpu.arch.toElfMachine()) { + if (header.e_machine != cpu_arch.toElfMachine()) { log.debug("Invalid architecture {any}, expected {any}", .{ header.e_machine, - target.cpu.arch.toElfMachine(), + cpu_arch.toElfMachine(), }); return error.InvalidCpuArch; } @@ -163,7 +163,14 @@ pub fn parseIntoAtoms(self: *Object, allocator: Allocator, object_id: u16, elf_f log.debug("parsing '{s}' into atoms", .{self.name}); var symbols_by_shndx = std.AutoHashMap(u16, std.ArrayList(u32)).init(allocator); - defer symbols_by_shndx.deinit(); + defer { + var it = symbols_by_shndx.valueIterator(); + while (it.next()) |value| { + value.deinit(); + } + symbols_by_shndx.deinit(); + } + for (self.sections.items) |ndx| { try symbols_by_shndx.putNoClobber(ndx, std.ArrayList(u32).init(allocator)); } diff --git a/src/Elf/Options.zig b/src/Elf/Options.zig new file mode 100644 index 00000000..501b575d --- /dev/null +++ b/src/Elf/Options.zig @@ -0,0 +1,141 @@ +const Options = @This(); + +const std = @import("std"); +const builtin = @import("builtin"); +const io = std.io; +const mem = std.mem; +const process = std.process; + +const Allocator = mem.Allocator; +const CrossTarget = std.zig.CrossTarget; +const Elf = @import("../Elf.zig"); +const Zld = @import("../Zld.zig"); + +const usage = + \\Usage: ld.zld [files...] + \\ + \\General Options: + \\--entry=[name] Set name of the entry point symbol + \\--gc-sections Force removal of functions and data that are unreachable by the entry point or exported symbols + \\-l[name] Specify library to link against + \\-L[path] Specify library search dir + \\--rpath=[path], -R [path] Specify runtime path + \\--shared Create dynamic library + \\-o [path] Specify output path for the final artifact + \\-z [arg] Set linker extension flags + \\ stack-size=[value] Override default stack size + \\-h, --help Print this help and exit + \\--debug-log [scope] Turn on debugging logs for [scope] (requires zld compiled with -Dlog) + \\ + \\ld.zld: supported targets: elf64-x86-64 +; + +emit: Zld.Emit, +output_mode: Zld.OutputMode, +target: CrossTarget, +positionals: []const Zld.LinkObject, +libs: std.StringArrayHashMap(Zld.SystemLib), +lib_dirs: []const []const u8, +rpath_list: []const []const u8, +stack_size: ?u64 = null, +strip: bool = false, +entry: ?[]const u8 = null, +gc_sections: bool = false, + +pub fn parseArgs(arena: Allocator, ctx: Zld.MainCtx) !Options { + if (ctx.args.len == 0) { + ctx.printSuccess("{s}", .{usage}); + } + + var positionals = std.ArrayList(Zld.LinkObject).init(arena); + var libs = std.StringArrayHashMap(Zld.SystemLib).init(arena); + var lib_dirs = std.ArrayList([]const u8).init(arena); + var rpath_list = std.ArrayList([]const u8).init(arena); + var out_path: ?[]const u8 = null; + var stack_size: ?u64 = null; + var shared: bool = false; + var gc_sections: bool = false; + var entry: ?[]const u8 = null; + + const Iterator = struct { + args: []const []const u8, + i: usize = 0, + fn next(it: *@This()) ?[]const u8 { + if (it.i >= it.args.len) { + return null; + } + defer it.i += 1; + return it.args[it.i]; + } + }; + var args_iter = Iterator{ .args = ctx.args }; + + while (args_iter.next()) |arg| { + if (mem.eql(u8, arg, "--help") or mem.eql(u8, arg, "-h")) { + ctx.printSuccess("{s}", .{usage}); + } else if (mem.eql(u8, arg, "--debug-log")) { + const scope = args_iter.next() orelse ctx.printFailure("Expected log scope after {s}", .{arg}); + try ctx.log_scopes.append(scope); + } else if (mem.startsWith(u8, arg, "-l")) { + try libs.put(arg[2..], .{}); + } else if (mem.startsWith(u8, arg, "-L")) { + try lib_dirs.append(arg[2..]); + } else if (mem.eql(u8, arg, "-o")) { + out_path = args_iter.next() orelse + ctx.printFailure("Expected output path after {s}", .{arg}); + } else if (mem.eql(u8, arg, "-z")) { + const z_arg = args_iter.next() orelse + ctx.printFailure("Expected another argument after {s}", .{arg}); + if (mem.startsWith(u8, z_arg, "stack-size=")) { + stack_size = try std.fmt.parseInt(u64, z_arg["stack-size=".len..], 10); + } else { + std.log.warn("TODO unhandled argument '-z {s}'", .{z_arg}); + } + } else if (mem.startsWith(u8, arg, "-z")) { + std.log.warn("TODO unhandled argument '-z {s}'", .{arg["-z".len..]}); + } else if (mem.eql(u8, arg, "--gc-sections")) { + gc_sections = true; + } else if (mem.eql(u8, arg, "--as-needed")) { + std.log.warn("TODO unhandled argument '--as-needed'", .{}); + } else if (mem.eql(u8, arg, "--allow-shlib-undefined")) { + std.log.warn("TODO unhandled argument '--allow-shlib-undefined'", .{}); + } else if (mem.startsWith(u8, arg, "-O")) { + std.log.warn("TODO unhandled argument '-O{s}'", .{arg["-O".len..]}); + } else if (mem.eql(u8, arg, "--shared")) { + shared = true; + } else if (mem.startsWith(u8, arg, "--rpath=")) { + try rpath_list.append(arg["--rpath=".len..]); + } else if (mem.eql(u8, arg, "-R")) { + const rpath = args_iter.next() orelse + ctx.printFailure("Expected path after {s}", .{arg}); + try rpath_list.append(rpath); + } else if (mem.startsWith(u8, arg, "--entry=")) { + entry = arg["--entry=".len..]; + } else { + try positionals.append(.{ + .path = arg, + .must_link = true, + }); + } + } + + if (positionals.items.len == 0) { + ctx.printFailure("Expected at least one input .o file", .{}); + } + + return Options{ + .emit = .{ + .directory = std.fs.cwd(), + .sub_path = out_path orelse "a.out", + }, + .target = CrossTarget.fromTarget(builtin.target), + .output_mode = if (shared) .lib else .exe, + .positionals = positionals.items, + .libs = libs, + .lib_dirs = lib_dirs.items, + .rpath_list = rpath_list.items, + .stack_size = stack_size, + .gc_sections = gc_sections, + .entry = entry, + }; +} diff --git a/src/MachO.zig b/src/MachO.zig index 949f962c..8409feec 100644 --- a/src/MachO.zig +++ b/src/MachO.zig @@ -23,6 +23,7 @@ const Atom = @import("MachO/Atom.zig"); const CodeSignature = @import("MachO/CodeSignature.zig"); const Dylib = @import("MachO/Dylib.zig"); const Object = @import("MachO/Object.zig"); +pub const Options = @import("MachO/Options.zig"); const LibStub = @import("tapi.zig").LibStub; const LoadCommand = macho.LoadCommand; const SegmentCommand = macho.SegmentCommand; @@ -32,14 +33,10 @@ const Zld = @import("Zld.zig"); pub const base_tag = Zld.Tag.macho; -pub const SearchStrategy = enum { - paths_first, - dylibs_first, -}; - pub const N_DESC_GCED: u16 = @bitCast(u16, @as(i16, -1)); base: Zld, +options: Options, /// Page size is dependent on the target cpu architecture. /// For x86_64 that's 4KB, whereas for aarch64, that's 16KB. @@ -190,7 +187,7 @@ const default_pagezero_vmsize: u64 = 0x100000000; /// potential future extensions. const default_headerpad_size: u32 = 0x1000; -pub fn openPath(allocator: Allocator, options: Zld.Options) !*MachO { +pub fn openPath(allocator: Allocator, options: Options) !*MachO { const file = try options.emit.directory.createFile(options.emit.sub_path, .{ .truncate = true, .read = true, @@ -206,11 +203,11 @@ pub fn openPath(allocator: Allocator, options: Zld.Options) !*MachO { return self; } -fn createEmpty(gpa: Allocator, options: Zld.Options) !*MachO { +fn createEmpty(gpa: Allocator, options: Options) !*MachO { const self = try gpa.create(MachO); - const cpu_arch = options.target.cpu.arch; - const os_tag = options.target.os.tag; - const abi = options.target.abi; + const cpu_arch = options.target.cpu_arch.?; + const os_tag = options.target.os_tag.?; + const abi = options.target.abi.?; const page_size: u16 = if (cpu_arch == .aarch64) 0x4000 else 0x1000; // Adhoc code signature is required when targeting aarch64-macos either directly or indirectly via the simulator // ABI such as aarch64-ios-simulator, etc. @@ -219,10 +216,10 @@ fn createEmpty(gpa: Allocator, options: Zld.Options) !*MachO { self.* = .{ .base = .{ .tag = .macho, - .options = options, .allocator = gpa, .file = undefined, }, + .options = options, .page_size = page_size, .code_signature = if (requires_adhoc_codesig) CodeSignature.init(page_size) @@ -239,8 +236,7 @@ pub fn flush(self: *MachO) !void { defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); - const syslibroot = self.base.options.syslibroot; - const gc_sections = self.base.options.gc_sections orelse false; + const syslibroot = self.options.syslibroot; try self.strtab.buffer.append(gpa, 0); try self.populateMetadata(); @@ -250,51 +246,42 @@ pub fn flush(self: *MachO) !void { // Positional arguments to the linker such as object files and static archives. var positionals = std.ArrayList([]const u8).init(arena); - try positionals.ensureUnusedCapacity(self.base.options.positionals.len); + try positionals.ensureUnusedCapacity(self.options.positionals.len); var must_link_archives = std.StringArrayHashMap(void).init(arena); - try must_link_archives.ensureUnusedCapacity(self.base.options.positionals.len); + try must_link_archives.ensureUnusedCapacity(self.options.positionals.len); - // TODO implement must-link archives - for (self.base.options.positionals) |obj| { - positionals.appendAssumeCapacity(obj.path); + for (self.options.positionals) |obj| { + if (must_link_archives.contains(obj.path)) continue; + if (obj.must_link) { + _ = must_link_archives.getOrPutAssumeCapacity(obj.path); + } else { + positionals.appendAssumeCapacity(obj.path); + } } - // for (self.base.options.positionals) |obj| { - // if (must_link_archives.contains(obj.path)) continue; - // if (obj.must_link) { - // _ = must_link_archives.getOrPutAssumeCapacity(obj.path); - // } else { - // _ = positionals.appendAssumeCapacity(obj.path); - // } - // } // Shared and static libraries passed via `-l` flag. var lib_dirs = std.ArrayList([]const u8).init(arena); - for (self.base.options.lib_dirs) |dir| { + for (self.options.lib_dirs) |dir| { if (try resolveSearchDir(arena, dir, syslibroot)) |search_dir| { try lib_dirs.append(search_dir); } else { log.warn("directory not found for '-L{s}'", .{dir}); } } - if (builtin.os.tag == .macos) { - if (try resolveSearchDir(arena, "/usr/lib", syslibroot)) |search_dir| { - try lib_dirs.append(search_dir); - } - } var libs = std.StringArrayHashMap(Zld.SystemLib).init(arena); // Assume ld64 default -search_paths_first if no strategy specified. - const search_strategy = self.base.options.search_strategy orelse .paths_first; - outer: for (self.base.options.libs.keys()) |lib_name| { + const search_strategy = self.options.search_strategy orelse .paths_first; + outer: for (self.options.libs.keys()) |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.put(full_path, self.base.options.libs.get(lib_name).?); + try libs.put(full_path, self.options.libs.get(lib_name).?); continue :outer; } } @@ -308,13 +295,13 @@ pub fn flush(self: *MachO) !void { for (lib_dirs.items) |dir| { for (&[_][]const u8{ ".tbd", ".dylib" }) |ext| { if (try resolveLib(arena, dir, lib_name, ext)) |full_path| { - try libs.put(full_path, self.base.options.libs.get(lib_name).?); + try libs.put(full_path, self.options.libs.get(lib_name).?); continue :outer; } } } else for (lib_dirs.items) |dir| { if (try resolveLib(arena, dir, lib_name, ".a")) |full_path| { - try libs.put(full_path, self.base.options.libs.get(lib_name).?); + try libs.put(full_path, self.options.libs.get(lib_name).?); } else { log.warn("library not found for '-l{s}'", .{lib_name}); lib_not_found = true; @@ -331,13 +318,9 @@ pub fn flush(self: *MachO) !void { } } - if (builtin.os.tag == .macos) { - try resolveLibSystem(arena, lib_dirs.items, &libs); - } - // frameworks var framework_dirs = std.ArrayList([]const u8).init(arena); - for (self.base.options.framework_dirs) |dir| { + for (self.options.framework_dirs) |dir| { if (try resolveSearchDir(arena, dir, syslibroot)) |search_dir| { try framework_dirs.append(search_dir); } else { @@ -345,17 +328,11 @@ pub fn flush(self: *MachO) !void { } } - if (builtin.os.tag == .macos and self.base.options.frameworks.count() > 0) { - if (try resolveSearchDir(arena, "/System/Library/Frameworks", syslibroot)) |search_dir| { - try framework_dirs.append(search_dir); - } - } - - outer: for (self.base.options.frameworks.keys()) |f_name| { + outer: for (self.options.frameworks.keys()) |f_name| { for (framework_dirs.items) |dir| { for (&[_][]const u8{ ".tbd", ".dylib", "" }) |ext| { if (try resolveFramework(arena, dir, f_name, ext)) |full_path| { - const info = self.base.options.frameworks.get(f_name).?; + const info = self.options.frameworks.get(f_name).?; try libs.put(full_path, .{ .needed = info.needed, .weak = info.weak, @@ -378,7 +355,7 @@ pub fn flush(self: *MachO) !void { // rpaths var rpath_table = std.StringArrayHashMap(void).init(arena); - for (self.base.options.rpath_list) |rpath| { + for (self.options.rpath_list) |rpath| { if (rpath_table.contains(rpath)) continue; const cmdsize = @intCast(u32, mem.alignForwardGeneric( u64, @@ -397,14 +374,14 @@ pub fn flush(self: *MachO) !void { } // code signature and entitlements - if (self.base.options.entitlements) |path| { + if (self.options.entitlements) |path| { if (self.code_signature) |*csig| { try csig.addEntitlements(gpa, path); - csig.code_directory.ident = self.base.options.emit.sub_path; + csig.code_directory.ident = self.options.emit.sub_path; } else { var csig = CodeSignature.init(self.page_size); try csig.addEntitlements(gpa, path); - csig.code_directory.ident = self.base.options.emit.sub_path; + csig.code_directory.ident = self.options.emit.sub_path; self.code_signature = csig; } } @@ -449,7 +426,7 @@ pub fn flush(self: *MachO) !void { try object.splitIntoAtoms(self, @intCast(u32, object_id)); } - if (gc_sections) { + if (self.options.dead_strip) { try dead_strip.gcAtoms(self); } @@ -459,11 +436,11 @@ pub fn flush(self: *MachO) !void { try self.allocateSpecialSymbols(); - // if (build_options.enable_logging) { - // self.logSymtab(); - // self.logSectionOrdinals(); - // self.logAtoms(); - // } + if (build_options.enable_logging) { + self.logSymtab(); + self.logSectionOrdinals(); + self.logAtoms(); + } try self.writeAtoms(); @@ -480,7 +457,7 @@ pub fn flush(self: *MachO) !void { if (self.code_signature) |*csig| { csig.clear(gpa); - csig.code_directory.ident = self.base.options.emit.sub_path; + csig.code_directory.ident = self.options.emit.sub_path; // Preallocate space for the code signature. // We need to do this at this stage so that we have the load commands with proper values // written out to the file. @@ -494,34 +471,12 @@ pub fn flush(self: *MachO) !void { if (self.code_signature) |*csig| { try self.writeCodeSignature(csig); // code signing always comes last - const dir = self.base.options.emit.directory; - const path = self.base.options.emit.sub_path; + const dir = self.options.emit.directory; + const path = self.options.emit.sub_path; try dir.copyFile(path, dir, path, .{}); } } -fn resolveLibSystem(arena: Allocator, search_dirs: []const []const u8, out_libs: anytype) !void { - // Try stub file first. If we hit it, then we're done as the stub file - // re-exports every single symbol definition. - for (search_dirs) |dir| { - if (try resolveLib(arena, dir, "System", ".tbd")) |full_path| { - try out_libs.put(full_path, .{ .needed = true }); - return; - } - } - // 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. - for (search_dirs) |dir| { - if (try resolveLib(arena, dir, "System", ".dylib")) |libsystem_path| { - if (try resolveLib(arena, dir, "c", ".dylib")) |libc_path| { - try out_libs.put(libsystem_path, .{ .needed = true }); - try out_libs.put(libc_path, .{ .needed = true }); - return; - } - } - } -} - fn resolveSearchDir( arena: Allocator, dir: []const u8, @@ -634,7 +589,7 @@ fn parseObject(self: *MachO, path: []const u8) !bool { .mtime = mtime, }; - object.parse(self.base.allocator, self.base.options.target) catch |err| switch (err) { + object.parse(self.base.allocator, self.options.target.cpu_arch.?) catch |err| switch (err) { error.EndOfStream, error.NotObject => { object.deinit(self.base.allocator); return false; @@ -662,7 +617,7 @@ fn parseArchive(self: *MachO, path: []const u8, force_load: bool) !bool { .file = file, }; - archive.parse(self.base.allocator, self.base.options.target) catch |err| switch (err) { + archive.parse(self.base.allocator, self.options.target.cpu_arch.?) catch |err| switch (err) { error.EndOfStream, error.NotArchive => { archive.deinit(self.base.allocator); return false; @@ -682,7 +637,7 @@ fn parseArchive(self: *MachO, path: []const u8, force_load: bool) !bool { } for (offsets.keys()) |off| { const object = try self.objects.addOne(self.base.allocator); - object.* = try archive.parseObject(self.base.allocator, self.base.options.target, off); + object.* = try archive.parseObject(self.base.allocator, self.options.target.cpu_arch.?, off); } } else { try self.archives.append(self.base.allocator, archive); @@ -730,7 +685,7 @@ pub fn parseDylib( dylib.parse( self.base.allocator, - self.base.options.target, + self.options.target.cpu_arch.?, dylib_id, dependent_libs, ) catch |err| switch (err) { @@ -745,7 +700,7 @@ pub fn parseDylib( try dylib.parseFromStub( self.base.allocator, - self.base.options.target, + self.options.target, lib_stub, dylib_id, dependent_libs, @@ -767,11 +722,16 @@ pub fn parseDylib( } } + const gop = try self.dylibs_map.getOrPut(self.base.allocator, dylib.id.?.name); + if (gop.found_existing) { + dylib.deinit(self.base.allocator); + return true; + } + gop.value_ptr.* = dylib_id; try self.dylibs.append(self.base.allocator, dylib); - try self.dylibs_map.putNoClobber(self.base.allocator, dylib.id.?.name, dylib_id); const should_link_dylib_even_if_unreachable = blk: { - if (self.base.options.dead_strip_dylibs and !opts.needed) break :blk false; + if (self.options.dead_strip_dylibs and !opts.needed) break :blk false; break :blk !(opts.dependent or self.referenced_dylibs.contains(dylib_id)); }; @@ -1594,7 +1554,7 @@ pub fn createGotAtom(self: *MachO, target: SymbolWithLoc) !*Atom { .subtractor = null, .pcrel = false, .length = 3, - .@"type" = switch (self.base.options.target.cpu.arch) { + .@"type" = switch (self.options.target.cpu_arch.?) { .aarch64 => @enumToInt(macho.reloc_type_arm64.ARM64_RELOC_UNSIGNED), .x86_64 => @enumToInt(macho.reloc_type_x86_64.X86_64_RELOC_UNSIGNED), else => unreachable, @@ -1687,13 +1647,13 @@ fn createStubHelperPreambleAtom(self: *MachO) !void { if (self.stub_helper_preamble_atom != null) return; const gpa = self.base.allocator; - const arch = self.base.options.target.cpu.arch; - const size: u64 = switch (arch) { + const cpu_arch = self.options.target.cpu_arch.?; + const size: u64 = switch (cpu_arch) { .x86_64 => 15, .aarch64 => 6 * @sizeOf(u32), else => unreachable, }; - const alignment: u32 = switch (arch) { + const alignment: u32 = switch (cpu_arch) { .x86_64 => 0, .aarch64 => 2, else => unreachable, @@ -1708,7 +1668,7 @@ fn createStubHelperPreambleAtom(self: *MachO) !void { }); const atom = try MachO.createEmptyAtom(gpa, sym_index, size, alignment); const dyld_private_sym_index = self.dyld_private_atom.?.sym_index; - switch (arch) { + switch (cpu_arch) { .x86_64 => { try atom.relocs.ensureUnusedCapacity(self.base.allocator, 2); // lea %r11, [rip + disp] @@ -1815,13 +1775,13 @@ fn createStubHelperPreambleAtom(self: *MachO) !void { pub fn createStubHelperAtom(self: *MachO) !*Atom { const gpa = self.base.allocator; - const arch = self.base.options.target.cpu.arch; - const stub_size: u4 = switch (arch) { + const cpu_arch = self.options.target.cpu_arch.?; + const stub_size: u4 = switch (cpu_arch) { .x86_64 => 10, .aarch64 => 3 * @sizeOf(u32), else => unreachable, }; - const alignment: u2 = switch (arch) { + const alignment: u2 = switch (cpu_arch) { .x86_64 => 0, .aarch64 => 2, else => unreachable, @@ -1837,7 +1797,7 @@ pub fn createStubHelperAtom(self: *MachO) !*Atom { const atom = try MachO.createEmptyAtom(gpa, sym_index, stub_size, alignment); try atom.relocs.ensureTotalCapacity(gpa, 1); - switch (arch) { + switch (cpu_arch) { .x86_64 => { // pushq atom.code.items[0] = 0x68; @@ -1909,7 +1869,7 @@ pub fn createLazyPointerAtom(self: *MachO, stub_sym_index: u32, target: SymbolWi .subtractor = null, .pcrel = false, .length = 3, - .@"type" = switch (self.base.options.target.cpu.arch) { + .@"type" = switch (self.options.target.cpu_arch.?) { .aarch64 => @enumToInt(macho.reloc_type_arm64.ARM64_RELOC_UNSIGNED), .x86_64 => @enumToInt(macho.reloc_type_x86_64.X86_64_RELOC_UNSIGNED), else => unreachable, @@ -1936,13 +1896,13 @@ pub fn createLazyPointerAtom(self: *MachO, stub_sym_index: u32, target: SymbolWi pub fn createStubAtom(self: *MachO, laptr_sym_index: u32) !*Atom { const gpa = self.base.allocator; - const arch = self.base.options.target.cpu.arch; - const alignment: u2 = switch (arch) { + const cpu_arch = self.options.target.cpu_arch.?; + const alignment: u2 = switch (cpu_arch) { .x86_64 => 0, .aarch64 => 2, else => unreachable, // unhandled architecture type }; - const stub_size: u4 = switch (arch) { + const stub_size: u4 = switch (cpu_arch) { .x86_64 => 6, .aarch64 => 3 * @sizeOf(u32), else => unreachable, // unhandled architecture type @@ -1956,7 +1916,7 @@ pub fn createStubAtom(self: *MachO, laptr_sym_index: u32) !*Atom { .n_value = 0, }); const atom = try MachO.createEmptyAtom(gpa, sym_index, stub_size, alignment); - switch (arch) { + switch (cpu_arch) { .x86_64 => { // jmp atom.code.items[0] = 0xff; @@ -2132,7 +2092,11 @@ fn resolveSymbolsInArchives(self: *MachO) !void { const object_id = @intCast(u16, self.objects.items.len); const object = try self.objects.addOne(self.base.allocator); - object.* = try archive.parseObject(self.base.allocator, self.base.options.target, offsets.items[0]); + object.* = try archive.parseObject( + self.base.allocator, + self.options.target.cpu_arch.?, + offsets.items[0], + ); try self.resolveSymbolsInObject(object, object_id); continue :loop; @@ -2178,9 +2142,6 @@ fn resolveSymbolsInDylibs(self: *MachO) !void { } fn resolveSymbolsAtLoading(self: *MachO) !void { - const is_lib = self.base.options.output_mode == .lib; - const allow_undef = is_lib and (self.base.options.allow_shlib_undefined orelse false); - var next_sym: usize = 0; while (next_sym < self.unresolved.count()) { const global_index = self.unresolved.keys()[next_sym]; @@ -2198,7 +2159,7 @@ fn resolveSymbolsAtLoading(self: *MachO) !void { }; _ = self.unresolved.swapRemove(global_index); continue; - } else if (allow_undef) { + } else if (self.options.allow_undef) { const n_desc = @bitCast( u16, macho.BIND_SPECIAL_DYLIB_FLAT_LOOKUP * @intCast(i16, macho.N_SYMBOL_RESOLVER), @@ -2220,7 +2181,7 @@ fn resolveSymbolsAtLoading(self: *MachO) !void { } fn createMhExecuteHeaderSymbol(self: *MachO) !void { - if (self.base.options.output_mode != .exe) return; + if (self.options.output_mode != .exe) return; if (self.globals.get("__mh_execute_header")) |global| { const sym = self.getSymbol(global); if (!sym.undf() and !(sym.pext() or sym.weakDef())) return; @@ -2405,14 +2366,14 @@ fn addCodeSignatureLC(self: *MachO) !void { } fn setEntryPoint(self: *MachO) !void { - if (self.base.options.output_mode != .exe) return; + if (self.options.output_mode != .exe) return; const seg = self.load_commands.items[self.text_segment_cmd_index.?].segment; const global = try self.getEntryPoint(); const sym = self.getSymbol(global); const ec = &self.load_commands.items[self.main_cmd_index.?].main; ec.entryoff = @intCast(u32, sym.n_value - seg.inner.vmaddr); - ec.stacksize = self.base.options.stack_size_override orelse 0; + ec.stacksize = self.options.stack_size orelse 0; } pub fn deinit(self: *MachO) void { @@ -2469,7 +2430,7 @@ pub fn deinit(self: *MachO) void { } } -fn closeFiles(self: MachO) void { +pub fn closeFiles(self: *const MachO) void { for (self.objects.items) |object| { object.file.close(); } @@ -2482,12 +2443,12 @@ fn closeFiles(self: MachO) void { } fn populateMetadata(self: *MachO) !void { - const cpu_arch = self.base.options.target.cpu.arch; - const pagezero_vmsize = self.base.options.pagezero_size orelse default_pagezero_vmsize; + const cpu_arch = self.options.target.cpu_arch.?; + const pagezero_vmsize = self.options.pagezero_size orelse default_pagezero_vmsize; const aligned_pagezero_vmsize = mem.alignBackwardGeneric(u64, pagezero_vmsize, self.page_size); if (self.pagezero_segment_cmd_index == null) blk: { - if (self.base.options.output_mode == .lib) break :blk; + if (self.options.output_mode == .lib) break :blk; if (aligned_pagezero_vmsize == 0) break :blk; if (aligned_pagezero_vmsize != pagezero_vmsize) { log.warn("requested __PAGEZERO size (0x{x}) is not page aligned", .{pagezero_vmsize}); @@ -2744,7 +2705,7 @@ fn populateMetadata(self: *MachO) !void { try self.load_commands.append(self.base.allocator, .{ .dylinker = dylinker_cmd }); } - if (self.main_cmd_index == null and self.base.options.output_mode == .exe) { + if (self.main_cmd_index == null and self.options.output_mode == .exe) { self.main_cmd_index = @intCast(u16, self.load_commands.items.len); try self.load_commands.append(self.base.allocator, .{ .main = .{ @@ -2755,12 +2716,12 @@ fn populateMetadata(self: *MachO) !void { }); } - if (self.dylib_id_cmd_index == null and self.base.options.output_mode == .lib) { + if (self.dylib_id_cmd_index == null and self.options.output_mode == .lib) { self.dylib_id_cmd_index = @intCast(u16, self.load_commands.items.len); - const install_name = self.base.options.install_name orelse self.base.options.emit.sub_path; - const current_version = self.base.options.version orelse + const install_name = self.options.install_name orelse self.options.emit.sub_path; + const current_version = self.options.current_version orelse std.builtin.Version{ .major = 1, .minor = 0, .patch = 0 }; - const compat_version = self.base.options.compatibility_version orelse + const compat_version = self.options.compatibility_version orelse std.builtin.Version{ .major = 1, .minor = 0, .patch = 0 }; var dylib_cmd = try macho.createLoadDylibCommand( self.base.allocator, @@ -2792,14 +2753,19 @@ fn populateMetadata(self: *MachO) !void { @sizeOf(u64), )); const platform_version = blk: { - const ver = self.base.options.target.os.version_range.semver.min; + const ver = self.options.platform_version; const platform_version = ver.major << 16 | ver.minor << 8; break :blk platform_version; }; - const is_simulator_abi = self.base.options.target.abi == .simulator; + const sdk_version = blk: { + const ver = self.options.sdk_version; + const sdk_version = ver.major << 16 | ver.minor << 8; + break :blk sdk_version; + }; + const is_simulator_abi = self.options.target.abi.? == .simulator; var cmd = macho.emptyGenericCommandWithData(macho.build_version_command{ .cmdsize = cmdsize, - .platform = switch (self.base.options.target.os.tag) { + .platform = switch (self.options.target.os_tag.?) { .macos => .MACOS, .ios => if (is_simulator_abi) macho.PLATFORM.IOSSIMULATOR else macho.PLATFORM.IOS, .watchos => if (is_simulator_abi) macho.PLATFORM.WATCHOSSIMULATOR else macho.PLATFORM.WATCHOS, @@ -2807,7 +2773,7 @@ fn populateMetadata(self: *MachO) !void { else => unreachable, }, .minos = platform_version, - .sdk = platform_version, + .sdk = sdk_version, .ntools = 1, }); const ld_ver = macho.build_tool_version{ @@ -2862,10 +2828,10 @@ fn calcMinHeaderpad(self: *MachO) u64 { sizeofcmds += lc.cmdsize(); } - var padding: u32 = sizeofcmds + (self.base.options.headerpad_size orelse 0); + var padding: u32 = sizeofcmds + (self.options.headerpad orelse 0); log.debug("minimum requested headerpad size 0x{x}", .{padding + @sizeOf(macho.mach_header_64)}); - if (self.base.options.headerpad_max_install_names) { + if (self.options.headerpad_max_install_names) { var min_headerpad_size: u32 = 0; for (self.load_commands.items) |lc| switch (lc.cmd()) { .ID_DYLIB, @@ -3278,7 +3244,7 @@ fn writeDyldInfoData(self: *MachO) !void { const text_segment = self.load_commands.items[self.text_segment_cmd_index.?].segment; const base_address = text_segment.inner.vmaddr; - if (self.base.options.output_mode == .exe) { + if (self.options.output_mode == .exe) { for (&[_]SymbolWithLoc{ try self.getEntryPoint(), self.globals.get("__mh_execute_header").?, @@ -3293,7 +3259,7 @@ fn writeDyldInfoData(self: *MachO) !void { }); } } else { - assert(self.base.options.output_mode == .lib); + assert(self.options.output_mode == .lib); for (self.globals.values()) |global| { const sym = self.getSymbol(global); @@ -3473,7 +3439,7 @@ fn populateLazyBindOffsetsInStubHelper(self: *MachO, buffer: []const u8) !void { .seg = text_segment_cmd_index, .sect = stub_helper_section_index, }); - const stub_offset: u4 = switch (self.base.options.target.cpu.arch) { + const stub_offset: u4 = switch (self.options.target.cpu_arch.?) { .x86_64 => 1, .aarch64 => 2 * @sizeOf(u32), else => unreachable, @@ -3669,7 +3635,7 @@ fn writeSymtab(self: *MachO) !void { try locals.append(out_sym); } - if (!self.base.options.strip) { + if (!self.options.strip) { try self.generateSymbolStabs(object, &locals); } } @@ -3864,7 +3830,7 @@ fn writeCodeSignature(self: *MachO, code_sig: *CodeSignature) !void { .exec_seg_base = seg.inner.fileoff, .exec_seg_limit = seg.inner.filesize, .code_sig_cmd = code_sig_cmd, - .output_mode = self.base.options.output_mode, + .output_mode = self.options.output_mode, }, buffer.writer()); assert(buffer.items.len == code_sig.size()); @@ -3905,7 +3871,7 @@ fn writeHeader(self: *MachO) !void { var header: macho.mach_header_64 = .{}; header.flags = macho.MH_NOUNDEFS | macho.MH_DYLDLINK | macho.MH_PIE | macho.MH_TWOLEVEL; - switch (self.base.options.target.cpu.arch) { + switch (self.options.target.cpu_arch.?) { .aarch64 => { header.cputype = macho.CPU_TYPE_ARM64; header.cpusubtype = macho.CPU_SUBTYPE_ARM_ALL; @@ -3917,7 +3883,7 @@ fn writeHeader(self: *MachO) !void { else => return error.UnsupportedCpuArchitecture, } - switch (self.base.options.output_mode) { + switch (self.options.output_mode) { .exe => { header.filetype = macho.MH_EXECUTE; }, @@ -4052,7 +4018,7 @@ pub fn getTlvPtrAtomForSymbol(self: *MachO, sym_with_loc: SymbolWithLoc) ?*Atom /// Returns symbol location corresponding to the set entrypoint. /// Asserts output mode is executable. pub fn getEntryPoint(self: MachO) error{MissingMainEntrypoint}!SymbolWithLoc { - const entry_name = self.base.options.entry orelse "_main"; + const entry_name = self.options.entry orelse "_main"; const global = self.globals.get(entry_name) orelse { log.err("entrypoint '{s}' not found", .{entry_name}); return error.MissingMainEntrypoint; @@ -4144,13 +4110,14 @@ pub fn generateSymbolStabs( object: Object, locals: *std.ArrayList(macho.nlist_64), ) !void { - assert(!self.base.options.strip); + assert(!self.options.strip); const gpa = self.base.allocator; log.debug("parsing debug info in '{s}'", .{object.name}); var debug_info = (try DebugInfo.parse(gpa, object)) orelse return; + defer debug_info.deinit(gpa); // We assume there is only one CU. const compile_unit = debug_info.inner.findCompileUnit(0x0) catch |err| switch (err) { diff --git a/src/MachO/Archive.zig b/src/MachO/Archive.zig index b662108a..ed6cdcb7 100644 --- a/src/MachO/Archive.zig +++ b/src/MachO/Archive.zig @@ -103,9 +103,9 @@ pub fn deinit(self: *Archive, allocator: Allocator) void { allocator.free(self.name); } -pub fn parse(self: *Archive, allocator: Allocator, target: std.Target) !void { +pub fn parse(self: *Archive, allocator: Allocator, cpu_arch: std.Target.Cpu.Arch) !void { const reader = self.file.reader(); - self.library_offset = try fat.getLibraryOffset(reader, target); + self.library_offset = try fat.getLibraryOffset(reader, cpu_arch); try self.file.seekTo(self.library_offset); const magic = try reader.readBytesNoEof(SARMAG); @@ -188,7 +188,7 @@ fn parseTableOfContents(self: *Archive, allocator: Allocator, reader: anytype) ! } } -pub fn parseObject(self: Archive, allocator: Allocator, target: std.Target, offset: u32) !Object { +pub fn parseObject(self: Archive, allocator: Allocator, cpu_arch: std.Target.Cpu.Arch, offset: u32) !Object { const reader = self.file.reader(); try reader.context.seekTo(offset + self.library_offset); @@ -217,7 +217,7 @@ pub fn parseObject(self: Archive, allocator: Allocator, target: std.Target, offs .mtime = try self.header.?.date(), }; - try object.parse(allocator, target); + try object.parse(allocator, cpu_arch); try reader.context.seekTo(0); return object; diff --git a/src/MachO/Atom.zig b/src/MachO/Atom.zig index dc4004e5..751a9357 100644 --- a/src/MachO/Atom.zig +++ b/src/MachO/Atom.zig @@ -95,7 +95,7 @@ pub const Relocation = struct { pub fn getTargetAtom(self: Relocation, macho_file: *MachO) ?*Atom { const is_via_got = got: { - switch (macho_file.base.options.target.cpu.arch) { + switch (macho_file.options.target.cpu_arch.?) { .aarch64 => break :got switch (@intToEnum(macho.reloc_type_arm64, self.@"type")) { .ARM64_RELOC_GOT_LOAD_PAGE21, .ARM64_RELOC_GOT_LOAD_PAGEOFF12, @@ -183,7 +183,7 @@ const RelocContext = struct { pub fn parseRelocs(self: *Atom, relocs: []const macho.relocation_info, context: RelocContext) !void { const gpa = context.macho_file.base.allocator; - const arch = context.macho_file.base.options.target.cpu.arch; + const arch = context.macho_file.options.target.cpu_arch.?; var addend: i64 = 0; var subtractor: ?SymbolWithLoc = null; @@ -494,7 +494,7 @@ pub fn resolveRelocs(self: *Atom, macho_file: *MachO) !void { log.debug("ATOM(%{d}, '{s}')", .{ self.sym_index, self.getName(macho_file) }); for (self.relocs.items) |rel| { - const arch = macho_file.base.options.target.cpu.arch; + const arch = macho_file.options.target.cpu_arch.?; switch (arch) { .aarch64 => { log.debug(" RELA({s}) @ {x} => %{d} in object({d})", .{ diff --git a/src/MachO/Dylib.zig b/src/MachO/Dylib.zig index 6d4369ba..5d9fd14b 100644 --- a/src/MachO/Dylib.zig +++ b/src/MachO/Dylib.zig @@ -11,6 +11,7 @@ const mem = std.mem; const fat = @import("fat.zig"); const Allocator = mem.Allocator; +const CrossTarget = std.zig.CrossTarget; const LibStub = @import("../tapi.zig").LibStub; const LoadCommand = macho.LoadCommand; const MachO = @import("../MachO.zig"); @@ -146,13 +147,13 @@ pub fn deinit(self: *Dylib, allocator: Allocator) void { pub fn parse( self: *Dylib, allocator: Allocator, - target: std.Target, + cpu_arch: std.Target.Cpu.Arch, dylib_id: u16, dependent_libs: anytype, ) !void { log.debug("parsing shared library '{s}'", .{self.name}); - self.library_offset = try fat.getLibraryOffset(self.file.reader(), target); + self.library_offset = try fat.getLibraryOffset(self.file.reader(), cpu_arch); try self.file.seekTo(self.library_offset); @@ -166,8 +167,8 @@ pub fn parse( const this_arch: std.Target.Cpu.Arch = try fat.decodeArch(self.header.?.cputype, true); - if (this_arch != target.cpu.arch) { - log.err("mismatched cpu architecture: expected {s}, found {s}", .{ target.cpu.arch, this_arch }); + if (this_arch != cpu_arch) { + log.err("mismatched cpu architecture: expected {s}, found {s}", .{ cpu_arch, this_arch }); return error.MismatchedCpuArchitecture; } @@ -279,23 +280,24 @@ fn addSymbol(self: *Dylib, allocator: Allocator, sym_name: []const u8) !void { const TargetMatcher = struct { allocator: Allocator, - target: std.Target, + target: CrossTarget, target_strings: std.ArrayListUnmanaged([]const u8) = .{}, - fn init(allocator: Allocator, target: std.Target) !TargetMatcher { + fn init(allocator: Allocator, target: CrossTarget) !TargetMatcher { var self = TargetMatcher{ .allocator = allocator, .target = target, }; try self.target_strings.append(allocator, try targetToAppleString(allocator, target)); - if (target.abi == .simulator) { + const abi = target.abi orelse .none; + if (abi == .simulator) { // For Apple simulator targets, linking gets tricky as we need to link against the simulator // hosts dylibs too. - const host_target = try targetToAppleString(allocator, (std.zig.CrossTarget{ - .cpu_arch = target.cpu.arch, + const host_target = try targetToAppleString(allocator, .{ + .cpu_arch = target.cpu_arch.?, .os_tag = .macos, - }).toTarget()); + }); try self.target_strings.append(allocator, host_target); } @@ -309,23 +311,24 @@ const TargetMatcher = struct { self.target_strings.deinit(self.allocator); } - fn targetToAppleString(allocator: Allocator, target: std.Target) ![]const u8 { - const arch = switch (target.cpu.arch) { + fn targetToAppleString(allocator: Allocator, target: CrossTarget) ![]const u8 { + const cpu_arch = switch (target.cpu_arch.?) { .aarch64 => "arm64", .x86_64 => "x86_64", else => unreachable, }; - const os = @tagName(target.os.tag); - const abi: ?[]const u8 = switch (target.abi) { + const os_tag = @tagName(target.os_tag.?); + const target_abi = target.abi orelse .none; + const abi: ?[]const u8 = switch (target_abi) { .none => null, .simulator => "simulator", .macabi => "maccatalyst", else => unreachable, }; if (abi) |x| { - return std.fmt.allocPrint(allocator, "{s}-{s}-{s}", .{ arch, os, x }); + return std.fmt.allocPrint(allocator, "{s}-{s}-{s}", .{ cpu_arch, os_tag, x }); } - return std.fmt.allocPrint(allocator, "{s}-{s}", .{ arch, os }); + return std.fmt.allocPrint(allocator, "{s}-{s}", .{ cpu_arch, os_tag }); } fn hasValue(stack: []const []const u8, needle: []const u8) bool { @@ -343,14 +346,14 @@ const TargetMatcher = struct { } fn matchesArch(self: TargetMatcher, archs: []const []const u8) bool { - return hasValue(archs, @tagName(self.target.cpu.arch)); + return hasValue(archs, @tagName(self.target.cpu_arch.?)); } }; pub fn parseFromStub( self: *Dylib, allocator: Allocator, - target: std.Target, + target: CrossTarget, lib_stub: LibStub, dylib_id: u16, dependent_libs: anytype, diff --git a/src/MachO/Object.zig b/src/MachO/Object.zig index 6684bbd9..f9e0dfdb 100644 --- a/src/MachO/Object.zig +++ b/src/MachO/Object.zig @@ -66,6 +66,7 @@ pub fn deinit(self: *Object, gpa: Allocator) void { } self.load_commands.deinit(gpa); gpa.free(self.contents); + self.symtab.deinit(gpa); self.sections_as_symbols.deinit(gpa); self.atom_by_index_table.deinit(gpa); @@ -78,7 +79,7 @@ pub fn deinit(self: *Object, gpa: Allocator) void { gpa.free(self.name); } -pub fn parse(self: *Object, allocator: Allocator, target: std.Target) !void { +pub fn parse(self: *Object, allocator: Allocator, cpu_arch: std.Target.Cpu.Arch) !void { const file_stat = try self.file.stat(); const file_size = math.cast(usize, file_stat.size) orelse return error.Overflow; self.contents = try self.file.readToEndAlloc(allocator, file_size); @@ -108,8 +109,8 @@ pub fn parse(self: *Object, allocator: Allocator, target: std.Target) !void { return error.UnsupportedCpuArchitecture; }, }; - if (this_arch != target.cpu.arch) { - log.err("mismatched cpu architecture: expected {s}, found {s}", .{ target.cpu.arch, this_arch }); + if (this_arch != cpu_arch) { + log.err("mismatched cpu architecture: expected {s}, found {s}", .{ cpu_arch, this_arch }); return error.MismatchedCpuArchitecture; } @@ -345,7 +346,7 @@ pub fn splitIntoAtoms(self: *Object, macho_file: *MachO, object_id: u32) !void { macho_file.getSection(match).sectName(), }); - const arch = macho_file.base.options.target.cpu.arch; + const cpu_arch = macho_file.options.target.cpu_arch.?; const is_zerofill = blk: { const section_type = sect.type_(); break :blk section_type == macho.S_ZEROFILL or section_type == macho.S_THREAD_LOCAL_ZEROFILL; @@ -460,7 +461,7 @@ pub fn splitIntoAtoms(self: *Object, macho_file: *MachO, object_id: u32) !void { sect, ); - if (arch == .x86_64 and addr == sect.addr) { + if (cpu_arch == .x86_64 and addr == sect.addr) { // In x86_64 relocs, it can so happen that the compiler refers to the same // atom by both the actual assigned symbol and the start of the section. In this // case, we need to link the two together so add an alias. diff --git a/src/MachO/Options.zig b/src/MachO/Options.zig new file mode 100644 index 00000000..c964cab1 --- /dev/null +++ b/src/MachO/Options.zig @@ -0,0 +1,483 @@ +const Options = @This(); + +const std = @import("std"); +const builtin = @import("builtin"); +const io = std.io; +const macho = std.macho; +const mem = std.mem; +const process = std.process; + +const Allocator = mem.Allocator; +const CrossTarget = std.zig.CrossTarget; +const MachO = @import("../MachO.zig"); +const Zld = @import("../Zld.zig"); + +pub const SearchStrategy = enum { + paths_first, + dylibs_first, +}; + +const usage = + \\Usage: ld64.zld [files...] + \\ + \\General Options: + \\ + \\-arch [name] + \\ Specifies which architecture the output file should be + \\ + \\-current_version [value] + \\ Specifies the current version number of the library + \\ + \\-compatibility_version [value] + \\ Specifies the compatibility version number of the library + \\ + \\-dead_strip + \\ Remove functions and data that are unreachable by the entry point or exported symbols + \\ + \\-dead_strip_dylibs + \\ Remove dylibs that were unreachable by the entry point or exported symbols + \\ + \\-dylib + \\ Create dynamic library + \\ + \\-dynamic + \\ Perform dynamic linking + \\ + \\-e [name] + \\ Specifies the entry point of main executable + \\ + \\-force_load [path] + \\ Loads all members of the specified static archive library + \\ + \\-framework [name] + \\ Link against framework + \\ + \\-F[path] + \\ Add search path for frameworks + \\ + \\-headerpad [value] + \\ Set minimum space for future expansion of the load commands in hexadecimal notation + \\ + \\-headerpad_max_install_names + \\ Set enough space as if all paths were MAXPATHLEN + \\ + \\-install_name + \\ Add dylib's install name + \\ + \\-l[name] + \\ Link against library + \\ + \\-L[path] + \\ Add search path for libraries + \\ + \\-needed_framework [name] + \\ Link against framework (even if unused) + \\ + \\-needed-l[name] + \\ Alias of -needed_library + \\ + \\-needed_library [name] + \\ Link against library (even if unused) + \\ + \\-rpath [path] + \\ Specify runtime path + \\ + \\-pagezero_size [value] + \\ Size of the __PAGEZERO segment in hexademical notation + \\ + \\-platform_version [platform] [min_version] [sdk_version] + \\ Sets the platform, oldest supported version of that platform and the SDK it was built against + \\ + \\-S + \\ Do not put debug information (STABS or DWARF) in the output file + \\ + \\-search_paths_first + \\ Search each dir in library search paths for `libx.dylib` then `libx.a` + \\ + \\-search_dylibs_first + \\ Search `libx.dylib` in each dir in library search paths, then `libx.a` + \\ + \\-stack_size [value] + \\ Size of the default stack in hexadecimal notation + \\ + \\-syslibroot [path] + \\ Specify the syslibroot + \\ + \\-undefined [value] + \\ Specifies how undefined symbols are to be treated: error (default), warning, suppress, or dynamic_lookup. + \\ + \\-weak_framework [name] + \\ Link against framework and mark it and all referenced symbols as weak + \\ + \\-weak-l[name] + \\ Alias of -weak_library + \\ + \\-weak_library [name] + \\ Link against library and mark it and all referenced symbols as weak + \\ + \\--entitlements + \\ (Linker extension) add path to entitlements file for embedding in code signature + \\ + \\-o [path] + \\ Specify output path for the final artifact + \\ + \\-h, --help + \\ Print this help and exit + \\ + \\--debug-log [scope] + \\ Turn on debugging logs for [scope] (requires zld compiled with -Dlog) +; + +emit: Zld.Emit, +output_mode: Zld.OutputMode, +target: CrossTarget, +platform_version: std.builtin.Version, +sdk_version: std.builtin.Version, +positionals: []const Zld.LinkObject, +libs: std.StringArrayHashMap(Zld.SystemLib), +frameworks: std.StringArrayHashMap(Zld.SystemLib), +lib_dirs: []const []const u8, +framework_dirs: []const []const u8, +rpath_list: []const []const u8, +dynamic: bool = false, +syslibroot: ?[]const u8 = null, +stack_size: ?u64 = null, +strip: bool = false, +entry: ?[]const u8 = null, +current_version: ?std.builtin.Version = null, +compatibility_version: ?std.builtin.Version = null, +install_name: ?[]const u8 = null, +entitlements: ?[]const u8 = null, +pagezero_size: ?u64 = null, +search_strategy: ?SearchStrategy = null, +headerpad: ?u32 = null, +headerpad_max_install_names: bool = false, +dead_strip: bool = false, +dead_strip_dylibs: bool = false, +allow_undef: bool = false, + +pub fn parseArgs(arena: Allocator, ctx: Zld.MainCtx) !Options { + if (ctx.args.len == 0) { + ctx.printSuccess("{s}", .{usage}); + } + + var positionals = std.ArrayList(Zld.LinkObject).init(arena); + var libs = std.StringArrayHashMap(Zld.SystemLib).init(arena); + var lib_dirs = std.ArrayList([]const u8).init(arena); + var frameworks = std.StringArrayHashMap(Zld.SystemLib).init(arena); + var framework_dirs = std.ArrayList([]const u8).init(arena); + var rpath_list = std.ArrayList([]const u8).init(arena); + var out_path: ?[]const u8 = null; + var syslibroot: ?[]const u8 = null; + var stack_size: ?u64 = null; + var dynamic: bool = false; + var dylib: bool = false; + var install_name: ?[]const u8 = null; + var current_version: ?std.builtin.Version = null; + var compatibility_version: ?std.builtin.Version = null; + var headerpad: ?u32 = null; + var headerpad_max_install_names: bool = false; + var pagezero_size: ?u64 = null; + var dead_strip: bool = false; + var dead_strip_dylibs: bool = false; + var entry: ?[]const u8 = null; + var strip: bool = false; + var allow_undef: bool = false; + + var target: ?CrossTarget = if (comptime builtin.target.isDarwin()) + CrossTarget.fromTarget(builtin.target) + else + null; + var platform_version: ?std.builtin.Version = if (comptime builtin.target.isDarwin()) + builtin.target.os.version_range.semver.min + else + null; + var sdk_version: ?std.builtin.Version = if (comptime builtin.target.isDarwin()) + builtin.target.os.version_range.semver.min + else + null; + + const Iterator = struct { + args: []const []const u8, + i: usize = 0, + fn next(it: *@This()) ?[]const u8 { + if (it.i >= it.args.len) { + return null; + } + defer it.i += 1; + return it.args[it.i]; + } + }; + var args_iter = Iterator{ .args = ctx.args }; + + while (args_iter.next()) |arg| { + if (mem.eql(u8, arg, "--help") or mem.eql(u8, arg, "-h")) { + ctx.printSuccess("{s}", .{usage}); + } else if (mem.eql(u8, arg, "--debug-log")) { + const scope = args_iter.next() orelse ctx.printFailure("Expected log scope after {s}", .{arg}); + try ctx.log_scopes.append(scope); + } else if (mem.eql(u8, arg, "-syslibroot")) { + syslibroot = args_iter.next() orelse ctx.printFailure("Expected path after {s}", .{arg}); + } else if (mem.startsWith(u8, arg, "-l")) { + try libs.put(arg[2..], .{}); + } else if (mem.startsWith(u8, arg, "-L")) { + try lib_dirs.append(arg[2..]); + } else if (mem.eql(u8, arg, "-framework") or mem.eql(u8, arg, "-weak_framework")) { + const name = args_iter.next() orelse ctx.printFailure("Expected framework name after {s}", .{arg}); + try frameworks.put(name, .{}); + } else if (mem.startsWith(u8, arg, "-F")) { + try framework_dirs.append(arg[2..]); + } else if (mem.startsWith(u8, arg, "-needed-l")) { + try libs.put(arg["-needed-l".len..], .{ .needed = true }); + } else if (mem.eql(u8, arg, "-needed_library")) { + const name = args_iter.next() orelse ctx.printFailure("Expected library name after {s}", .{arg}); + try libs.put(name, .{ .needed = true }); + } else if (mem.eql(u8, arg, "-needed_framework")) { + const name = args_iter.next() orelse ctx.printFailure("Expected framework name after {s}", .{arg}); + try frameworks.put(name, .{ .needed = true }); + } else if (mem.startsWith(u8, arg, "-weak-l")) { + try libs.put(arg["-weak-l".len..], .{ .weak = true }); + } else if (mem.eql(u8, arg, "-weak_library")) { + const name = args_iter.next() orelse ctx.printFailure("Expected library name after {s}", .{arg}); + try libs.put(name, .{ .weak = true }); + } else if (mem.eql(u8, arg, "-weak_framework")) { + const name = args_iter.next() orelse ctx.printFailure("Expected framework name after {s}", .{arg}); + try frameworks.put(name, .{ .weak = true }); + } else if (mem.eql(u8, arg, "-o")) { + out_path = args_iter.next() orelse ctx.printFailure("Expected output path after {s}", .{arg}); + } else if (mem.eql(u8, arg, "-stack_size")) { + const stack_s = args_iter.next() orelse + ctx.printFailure("Expected stack size value after {s}", .{arg}); + stack_size = std.fmt.parseUnsigned(u64, eatIntPrefix(stack_s, 16), 16) catch |err| { + ctx.printFailure("Unable to parse '{s}': {s}", .{ arg, @errorName(err) }); + }; + } else if (mem.eql(u8, arg, "-dylib")) { + dylib = true; + } else if (mem.eql(u8, arg, "-dynamic")) { + dynamic = true; + } else if (mem.eql(u8, arg, "-static")) { + dynamic = false; + } else if (mem.eql(u8, arg, "-rpath")) { + const rpath = args_iter.next() orelse ctx.printFailure("Expected path after {s}", .{arg}); + try rpath_list.append(rpath); + } else if (mem.eql(u8, arg, "-compatibility_version")) { + const raw = args_iter.next() orelse ctx.printFailure("Expected version after {s}", .{arg}); + compatibility_version = std.builtin.Version.parse(raw) catch |err| { + ctx.printFailure("Unable to parse {s} {s}: {s}", .{ arg, raw, @errorName(err) }); + }; + } else if (mem.eql(u8, arg, "-current_version")) { + const raw = args_iter.next() orelse ctx.printFailure("Expected version after {s}", .{arg}); + current_version = std.builtin.Version.parse(raw) catch |err| { + ctx.printFailure("Unable to parse {s} {s}: {s}", .{ arg, raw, @errorName(err) }); + }; + } else if (mem.eql(u8, arg, "-install_name")) { + install_name = args_iter.next() orelse ctx.printFailure("Expected argument after {s}", .{arg}); + } else if (mem.eql(u8, arg, "-headerpad")) { + const headerpad_s = args_iter.next() orelse + ctx.printFailure("Expected headerpad size value after {s}", .{arg}); + headerpad = std.fmt.parseUnsigned(u32, eatIntPrefix(headerpad_s, 16), 16) catch |err| { + ctx.printFailure("Unable to parse '{s}': {s}", .{ arg, @errorName(err) }); + }; + } else if (mem.eql(u8, arg, "-headerpad_max_install_names")) { + headerpad_max_install_names = true; + } else if (mem.eql(u8, arg, "-pagezero_size")) { + const pagezero_s = args_iter.next() orelse + ctx.printFailure("Expected pagezero size value after {s}", .{arg}); + pagezero_size = std.fmt.parseUnsigned(u64, eatIntPrefix(pagezero_s, 16), 16) catch |err| { + ctx.printFailure("Unable to parse '{s}': {s}", .{ arg, @errorName(err) }); + }; + } else if (mem.eql(u8, arg, "-dead_strip")) { + dead_strip = true; + } else if (mem.eql(u8, arg, "-dead_strip_dylibs")) { + dead_strip_dylibs = true; + } else if (mem.eql(u8, arg, "-e")) { + entry = args_iter.next() orelse ctx.printFailure("Expected symbol name after {s}", .{arg}); + } else if (mem.eql(u8, arg, "-S")) { + strip = true; + } else if (mem.eql(u8, arg, "-force_load")) { + const path = args_iter.next() orelse ctx.printFailure("Expected path after {s}", .{arg}); + try positionals.append(.{ + .path = path, + .must_link = true, + }); + } else if (mem.eql(u8, arg, "-arch")) { + const arch_s = args_iter.next() orelse + ctx.printFailure("Expected architecture name after {s}", .{arg}); + if (mem.eql(u8, arch_s, "arm64")) { + target.?.cpu_arch = .aarch64; + } else if (mem.eql(u8, arch_s, "x86_64")) { + target.?.cpu_arch = .x86_64; + } else { + ctx.printFailure("Failed to parse CPU architecture from '{s}'", .{arch_s}); + } + } else if (mem.eql(u8, arg, "-platform_version")) { + const platform = args_iter.next() orelse + ctx.printFailure("Expected platform name after {s}", .{arg}); + const min_v = args_iter.next() orelse + ctx.printFailure("Expected minimum platform version after {s} {s}", .{ arg, platform }); + const sdk_v = args_iter.next() orelse + ctx.printFailure("Expected SDK version after {s} {s} {s}", .{ arg, platform, min_v }); + + var tmp_target = CrossTarget{}; + + // First, try parsing platform as a numeric value. + if (std.fmt.parseUnsigned(u32, platform, 10)) |ord| { + switch (@intToEnum(macho.PLATFORM, ord)) { + .MACOS => tmp_target = .{ + .os_tag = .macos, + .abi = .none, + }, + .IOS => tmp_target = .{ + .os_tag = .ios, + .abi = .none, + }, + .TVOS => tmp_target = .{ + .os_tag = .tvos, + .abi = .none, + }, + .WATCHOS => tmp_target = .{ + .os_tag = .watchos, + .abi = .none, + }, + .IOSSIMULATOR => tmp_target = .{ + .os_tag = .ios, + .abi = .simulator, + }, + .TVOSSIMULATOR => tmp_target = .{ + .os_tag = .tvos, + .abi = .simulator, + }, + .WATCHOSSIMULATOR => tmp_target = .{ + .os_tag = .watchos, + .abi = .simulator, + }, + else => |x| ctx.printFailure("Unsupported Apple OS: {s}", .{@tagName(x)}), + } + } else |_| { + if (mem.eql(u8, platform, "macos")) { + tmp_target = .{ + .os_tag = .macos, + .abi = .none, + }; + } else if (mem.eql(u8, platform, "ios")) { + tmp_target = .{ + .os_tag = .ios, + .abi = .none, + }; + } else if (mem.eql(u8, platform, "tvos")) { + tmp_target = .{ + .os_tag = .tvos, + .abi = .none, + }; + } else if (mem.eql(u8, platform, "watchos")) { + tmp_target = .{ + .os_tag = .watchos, + .abi = .none, + }; + } else if (mem.eql(u8, platform, "ios-simulator")) { + tmp_target = .{ + .os_tag = .ios, + .abi = .simulator, + }; + } else if (mem.eql(u8, platform, "tvos-simulator")) { + tmp_target = .{ + .os_tag = .tvos, + .abi = .simulator, + }; + } else if (mem.eql(u8, platform, "watchos-simulator")) { + tmp_target = .{ + .os_tag = .watchos, + .abi = .simulator, + }; + } else { + ctx.printFailure("Unsupported Apple OS: {s}", .{platform}); + } + } + + if (target) |*tt| { + tt.os_tag = tmp_target.os_tag; + tt.abi = tmp_target.abi; + } + + platform_version = std.builtin.Version.parse(min_v) catch |err| { + ctx.printFailure("Failed to parse min_version '{s}': {s}", .{ min_v, @errorName(err) }); + }; + sdk_version = std.builtin.Version.parse(sdk_v) catch |err| { + ctx.printFailure("Failed to parse sdk_version '{s}': {s}", .{ sdk_v, @errorName(err) }); + }; + } else if (mem.eql(u8, arg, "-undefined")) { + const treatment = args_iter.next() orelse ctx.printFailure("Expected value after {s}", .{arg}); + if (mem.eql(u8, treatment, "error")) { + allow_undef = false; + } else if (mem.eql(u8, treatment, "warning") or mem.eql(u8, treatment, "suppress")) { + ctx.printFailure("TODO unimplemented -undefined {s} option", .{treatment}); + } else if (mem.eql(u8, treatment, "dynamic_lookup")) { + allow_undef = true; + } else { + ctx.printFailure("Unknown option -undefined {s}", .{treatment}); + } + } else { + try positionals.append(.{ + .path = arg, + .must_link = false, + }); + } + } + + if (positionals.items.len == 0) { + ctx.printFailure("Expected at least one input .o file", .{}); + } + if (target == null or target.?.cpu_arch == null) { + ctx.printFailure("Missing -arch when cross-linking", .{}); + } + if (target.?.os_tag == null) { + ctx.printFailure("Missing -platform_version when cross-linking", .{}); + } + + // Add some defaults + try lib_dirs.append("/usr/lib"); + try framework_dirs.append("/System/Library/Frameworks"); + + return Options{ + .emit = .{ + .directory = std.fs.cwd(), + .sub_path = out_path orelse "a.out", + }, + .dynamic = dynamic, + .target = target.?, + .platform_version = platform_version.?, + .sdk_version = sdk_version.?, + .output_mode = if (dylib) .lib else .exe, + .syslibroot = syslibroot, + .positionals = positionals.items, + .libs = libs, + .frameworks = frameworks, + .lib_dirs = lib_dirs.items, + .framework_dirs = framework_dirs.items, + .rpath_list = rpath_list.items, + .stack_size = stack_size, + .install_name = install_name, + .current_version = current_version, + .compatibility_version = compatibility_version, + .dead_strip = dead_strip, + .dead_strip_dylibs = dead_strip_dylibs, + .headerpad = headerpad, + .headerpad_max_install_names = headerpad_max_install_names, + .pagezero_size = pagezero_size, + .entry = entry, + .strip = strip, + .allow_undef = allow_undef, + }; +} + +fn eatIntPrefix(arg: []const u8, radix: u8) []const u8 { + if (arg.len > 2 and arg[0] == '0') { + switch (std.ascii.toLower(arg[1])) { + 'b' => if (radix == 2) return arg[2..], + 'o' => if (radix == 8) return arg[2..], + 'x' => if (radix == 16) return arg[2..], + else => {}, + } + } + return arg; +} diff --git a/src/MachO/dead_strip.zig b/src/MachO/dead_strip.zig index 3bf784c8..d390722e 100644 --- a/src/MachO/dead_strip.zig +++ b/src/MachO/dead_strip.zig @@ -50,7 +50,7 @@ fn removeAtomFromSection(atom: *Atom, match: MatchingSection, macho_file: *MachO } fn collectRoots(roots: *std.AutoHashMap(*Atom, void), macho_file: *MachO) !void { - const output_mode = macho_file.base.options.output_mode; + const output_mode = macho_file.options.output_mode; switch (output_mode) { .exe => { diff --git a/src/MachO/fat.zig b/src/MachO/fat.zig index 7ce05081..c86a0f81 100644 --- a/src/MachO/fat.zig +++ b/src/MachO/fat.zig @@ -6,7 +6,7 @@ const mem = std.mem; const native_endian = builtin.target.cpu.arch.endian(); pub fn decodeArch(cputype: macho.cpu_type_t, comptime logError: bool) !std.Target.Cpu.Arch { - const arch: std.Target.Cpu.Arch = switch (cputype) { + const cpu_arch: std.Target.Cpu.Arch = switch (cputype) { macho.CPU_TYPE_ARM64 => .aarch64, macho.CPU_TYPE_X86_64 => .x86_64, else => { @@ -16,7 +16,7 @@ pub fn decodeArch(cputype: macho.cpu_type_t, comptime logError: bool) !std.Targe return error.UnsupportedCpuArchitecture; }, }; - return arch; + return cpu_arch; } fn readFatStruct(reader: anytype, comptime T: type) !T { @@ -29,7 +29,7 @@ fn readFatStruct(reader: anytype, comptime T: type) !T { return res; } -pub fn getLibraryOffset(reader: anytype, target: std.Target) !u64 { +pub fn getLibraryOffset(reader: anytype, cpu_arch: std.Target.Cpu.Arch) !u64 { const fat_header = try readFatStruct(reader, macho.fat_header); if (fat_header.magic != macho.FAT_MAGIC) return 0; @@ -42,12 +42,12 @@ pub fn getLibraryOffset(reader: anytype, target: std.Target) !u64 { error.UnsupportedCpuArchitecture => continue, else => |e| return e, }; - if (lib_arch == target.cpu.arch) { + if (lib_arch == cpu_arch) { // We have found a matching architecture! return fat_arch.offset; } } else { - log.err("Could not find matching cpu architecture in fat library: expected {s}", .{target.cpu.arch}); + log.err("Could not find matching cpu architecture in fat library: expected {s}", .{cpu_arch}); return error.MismatchedCpuArchitecture; } } diff --git a/src/Zld.zig b/src/Zld.zig index 7b94aa9c..86183562 100644 --- a/src/Zld.zig +++ b/src/Zld.zig @@ -7,6 +7,7 @@ const mem = std.mem; const process = std.process; const Allocator = mem.Allocator; +const CrossTarget = std.zig.CrossTarget; const Elf = @import("Elf.zig"); const MachO = @import("MachO.zig"); const Coff = @import("Coff.zig"); @@ -14,7 +15,6 @@ const Coff = @import("Coff.zig"); tag: Tag, allocator: Allocator, file: fs.File, -options: Options, pub const Tag = enum { coff, @@ -42,56 +42,53 @@ pub const LinkObject = struct { must_link: bool = false, }; -pub const Options = struct { - emit: Emit, - dynamic: bool, - output_mode: OutputMode, - target: std.Target, - syslibroot: ?[]const u8, - positionals: []const LinkObject, - libs: std.StringArrayHashMap(SystemLib), - frameworks: std.StringArrayHashMap(SystemLib), - lib_dirs: []const []const u8, - framework_dirs: []const []const u8, - rpath_list: []const []const u8, - stack_size_override: ?u64 = null, - gc_sections: ?bool = null, - allow_shlib_undefined: ?bool = null, - strip: bool = false, - entry: ?[]const u8 = null, - verbose: bool = false, - - version: ?std.builtin.Version = null, - compatibility_version: ?std.builtin.Version = null, - - /// (Darwin) Install name for the dylib - install_name: ?[]const u8 = null, - - /// (Darwin) Path to entitlements file - entitlements: ?[]const u8 = null, - - /// (Darwin) size of the __PAGEZERO segment - pagezero_size: ?u64 = null, - - /// (Darwin) search strategy for system libraries - search_strategy: ?MachO.SearchStrategy = null, - - /// (Darwin) set minimum space for future expansion of the load commands - headerpad_size: ?u32 = null, - - /// (Darwin) set enough space as if all paths were MATPATHLEN - headerpad_max_install_names: bool = false, - - /// (Darwin) remove dylibs that are unreachable by the entry point or exported symbols - dead_strip_dylibs: bool = false, +pub const Options = union { + elf: Elf.Options, + macho: MachO.Options, + coff: Coff.Options, }; -pub fn openPath(allocator: Allocator, options: Options) !*Zld { - return switch (options.target.os.tag) { - .linux => &(try Elf.openPath(allocator, options)).base, - .macos => &(try MachO.openPath(allocator, options)).base, - .windows => &(try Coff.openPath(allocator, options)).base, - else => error.Unimplemented, +pub const MainCtx = struct { + gpa: Allocator, + args: []const []const u8, + log_scopes: *std.ArrayList([]const u8), + + pub fn printSuccess(ctx: MainCtx, comptime format: []const u8, args: anytype) noreturn { + ret: { + const msg = std.fmt.allocPrint(ctx.gpa, format, args) catch break :ret; + std.io.getStdOut().writeAll(msg) catch {}; + } + std.process.exit(0); + } + + pub fn printFailure(ctx: MainCtx, comptime format: []const u8, args: anytype) noreturn { + ret: { + const msg = std.fmt.allocPrint(ctx.gpa, format, args) catch break :ret; + std.io.getStdErr().writeAll(msg) catch {}; + } + std.process.exit(1); + } +}; + +pub fn main(tag: Tag, ctx: MainCtx) !void { + var arena_allocator = std.heap.ArenaAllocator.init(ctx.gpa); + defer arena_allocator.deinit(); + const arena = arena_allocator.allocator(); + const opts: Options = switch (tag) { + .elf => .{ .elf = try Elf.Options.parseArgs(arena, ctx) }, + .macho => .{ .macho = try MachO.Options.parseArgs(arena, ctx) }, + .coff => .{ .coff = try Coff.Options.parseArgs(arena, ctx) }, + }; + const zld = try openPath(ctx.gpa, tag, opts); + defer zld.deinit(); + try zld.flush(); +} + +pub fn openPath(allocator: Allocator, tag: Tag, options: Options) !*Zld { + return switch (tag) { + .macho => &(try MachO.openPath(allocator, options.macho)).base, + .elf => &(try Elf.openPath(allocator, options.elf)).base, + .coff => &(try Coff.openPath(allocator, options.coff)).base, }; } @@ -101,79 +98,23 @@ pub fn deinit(base: *Zld) void { .macho => @fieldParentPtr(MachO, "base", base).deinit(), .coff => @fieldParentPtr(Coff, "base", base).deinit(), } + base.allocator.destroy(base); } pub fn flush(base: *Zld) !void { - const gpa = base.allocator; - var arena_allocator = std.heap.ArenaAllocator.init(gpa); - defer arena_allocator.deinit(); - const arena = arena_allocator.allocator(); - - if (base.options.verbose) { - var argv = std.ArrayList([]const u8).init(arena); - try argv.append("zld"); - - if (base.options.dynamic) { - try argv.append("-dynamic"); - } else { - try argv.append("-static"); - } - - if (base.options.syslibroot) |path| { - try argv.append("-syslibroot"); - try argv.append(path); - } - if (base.options.output_mode == .lib) { - try argv.append("-shared"); - } - if (base.options.stack_size_override) |st| { - switch (base.options.target.getObjectFormat()) { - .elf => try argv.append(try std.fmt.allocPrint(arena, "-z stack-size={d}", .{st})), - .macho => { - try argv.append("-stack"); - try argv.append(try std.fmt.allocPrint(arena, "{d}", .{st})); - }, - else => {}, - } - } - try argv.append("-o"); - try argv.append(base.options.emit.sub_path); - for (base.options.libs.keys()) |lib| { - try argv.append(try std.fmt.allocPrint(arena, "-l{s}", .{lib})); - } - for (base.options.lib_dirs) |dir| { - try argv.append(try std.fmt.allocPrint(arena, "-L{s}", .{dir})); - } - for (base.options.frameworks.keys()) |fw| { - try argv.append("-framework"); - try argv.append(fw); - } - for (base.options.framework_dirs) |dir| { - try argv.append(try std.fmt.allocPrint(arena, "-F{s}", .{dir})); - } - for (base.options.rpath_list) |rpath| { - try argv.append("-rpath"); - try argv.append(rpath); - } - if (base.options.gc_sections) |gc_sections| { - if (gc_sections) { - try argv.append("--gc-sections"); - } else { - try argv.append("--no-gc-sections"); - } - } - for (base.options.positionals) |obj| { - try argv.append(obj.path); - } - try argv.append("\n"); - - try io.getStdOut().writeAll(try mem.join(arena, " ", argv.items)); - } - switch (base.tag) { .elf => try @fieldParentPtr(Elf, "base", base).flush(), .macho => try @fieldParentPtr(MachO, "base", base).flush(), .coff => try @fieldParentPtr(Coff, "base", base).flush(), } + base.closeFiles(); +} + +fn closeFiles(base: *const Zld) void { + switch (base.tag) { + .elf => @fieldParentPtr(Elf, "base", base).closeFiles(), + .macho => @fieldParentPtr(MachO, "base", base).closeFiles(), + .coff => @fieldParentPtr(Coff, "base", base).closeFiles(), + } base.file.close(); } diff --git a/src/main.zig b/src/main.zig index b0eabd2e..e1f569c7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,60 +1,20 @@ const std = @import("std"); const builtin = @import("builtin"); const build_options = @import("build_options"); -const io = std.io; const mem = std.mem; -const process = std.process; const Allocator = mem.Allocator; -const CrossTarget = std.zig.CrossTarget; const Zld = @import("Zld.zig"); var gpa_allocator = std.heap.GeneralPurposeAllocator(.{}){}; +const gpa = gpa_allocator.allocator(); const usage = - \\Usage: zld [files...] - \\ - \\Commands: - \\ [files...] (default) Generate final executable artifact 'a.out' from input '.o' files - \\ - \\General Options: - \\-h, --help Print this help and exit - \\--verbose Print the full invocation - \\-dynamic Perform dynamic linking - \\-dylib Create dynamic library - \\-shared Create dynamic library - \\-syslibroot [path] Specify the syslibroot - \\-l[name] Specify library to link against - \\-L[path] Specify library search dir - \\-framework [name] Specify framework to link against - \\-weak_framework [name] Specify weak framework to link against - \\-F[path] Specify framework search dir - \\-rpath [path] Specify runtime path - \\-stack Override default stack size - \\-o [path] Specify output path for the final artifact - \\--debug-log [name] Turn on debugging for [name] backend (requires zld to be compiled with -Dlog) + \\zld is a generic linker driver. + \\Call ld.zld (ELF), ld64.zld (MachO), link-zld (COFF). ; -fn printHelpAndExit() noreturn { - io.getStdOut().writeAll(usage) catch {}; - process.exit(0); -} - -fn fatal(comptime format: []const u8, args: anytype) noreturn { - ret: { - const msg = std.fmt.allocPrint(gpa_allocator.allocator(), format, args) catch break :ret; - io.getStdErr().writeAll(msg) catch {}; - } - process.exit(1); -} - -pub const log_level: std.log.Level = switch (builtin.mode) { - .Debug => .debug, - .ReleaseSafe, .ReleaseFast => .err, - .ReleaseSmall => .crit, -}; - -var log_scopes: std.ArrayListUnmanaged([]const u8) = .{}; +var log_scopes: std.ArrayList([]const u8) = std.ArrayList([]const u8).init(gpa); pub fn log( comptime level: std.log.Level, @@ -72,8 +32,7 @@ pub fn log( const scope_name = @tagName(scope); for (log_scopes.items) |log_scope| { - if (mem.eql(u8, log_scope, scope_name)) - break; + if (mem.eql(u8, log_scope, scope_name)) break; } else return; } @@ -91,149 +50,26 @@ pub fn log( std.debug.print(prefix1 ++ prefix2 ++ format ++ "\n", args); } -pub fn main() anyerror!void { - var gpa = gpa_allocator.allocator(); - var arena_allocator = std.heap.ArenaAllocator.init(gpa); - defer arena_allocator.deinit(); - const arena = arena_allocator.allocator(); - - const all_args = try process.argsAlloc(gpa); - defer process.argsFree(gpa, all_args); - - const args = all_args[1..]; - if (args.len == 0) { - printHelpAndExit(); - } - - var positionals = std.ArrayList(Zld.LinkObject).init(arena); - var libs = std.StringArrayHashMap(Zld.SystemLib).init(arena); - var lib_dirs = std.ArrayList([]const u8).init(arena); - var frameworks = std.StringArrayHashMap(Zld.SystemLib).init(arena); - var framework_dirs = std.ArrayList([]const u8).init(arena); - var rpath_list = std.ArrayList([]const u8).init(arena); - var out_path: ?[]const u8 = null; - var syslibroot: ?[]const u8 = null; - var stack: ?u64 = null; - var dynamic: bool = false; - var verbose: bool = false; - var dylib: bool = false; - var shared: bool = false; - var version: ?std.builtin.Version = null; - var compatibility_version: ?std.builtin.Version = null; - var gc_sections: bool = false; - - var i: usize = 0; - while (i < args.len) : (i += 1) { - const arg = args[i]; - if (mem.eql(u8, arg, "--help") or mem.eql(u8, arg, "-h")) { - printHelpAndExit(); - } else if (mem.eql(u8, arg, "--debug-log")) { - if (i + 1 >= args.len) fatal("Expected parameter after {s}", .{arg}); - i += 1; - try log_scopes.append(arena, args[i]); - } else if (mem.eql(u8, arg, "-syslibroot")) { - if (i + 1 >= args.len) fatal("Expected path after {s}", .{arg}); - i += 1; - syslibroot = args[i]; - } else if (mem.startsWith(u8, arg, "-l")) { - try libs.put(args[i][2..], .{}); - } else if (mem.startsWith(u8, arg, "-L")) { - try lib_dirs.append(args[i][2..]); - } else if (mem.eql(u8, arg, "-framework") or mem.eql(u8, arg, "-weak_framework")) { - if (i + 1 >= args.len) fatal("Expected framework name after {s}", .{arg}); - i += 1; - try frameworks.put(args[i], .{}); - } else if (mem.startsWith(u8, arg, "-F")) { - try framework_dirs.append(args[i][2..]); - } else if (mem.eql(u8, arg, "-o")) { - if (i + 1 >= args.len) fatal("Expected output path after {s}", .{arg}); - i += 1; - out_path = args[i]; - } else if (mem.eql(u8, arg, "-stack")) { - if (i + 1 >= args.len) fatal("Expected stack size value after {s}", .{arg}); - i += 1; - stack = try std.fmt.parseInt(u64, args[i], 10); - } else if (mem.eql(u8, arg, "-z")) { - if (i + 1 >= args.len) fatal("Expected another argument after {s}", .{arg}); - i += 1; - if (mem.startsWith(u8, args[i], "stack-size=")) { - stack = try std.fmt.parseInt(u64, args[i]["stack-size=".len..], 10); - } else { - std.log.warn("TODO unhandled argument '-z {s}'", .{args[i]}); - } - } else if (mem.startsWith(u8, arg, "-z")) { - std.log.warn("TODO unhandled argument '-z {s}'", .{args[i]["-z".len..]}); - } else if (mem.eql(u8, arg, "--gc-sections")) { - gc_sections = true; - } else if (mem.eql(u8, arg, "--as-needed")) { - std.log.warn("TODO unhandled argument '--as-needed'", .{}); - } else if (mem.eql(u8, arg, "--allow-shlib-undefined")) { - std.log.warn("TODO unhandled argument '--allow-shlib-undefined'", .{}); - } else if (mem.startsWith(u8, arg, "-O")) { - std.log.warn("TODO unhandled argument '-O{s}'", .{args[i]["-O".len..]}); - } else if (mem.eql(u8, arg, "-dylib")) { - dylib = true; - } else if (mem.eql(u8, arg, "-shared")) { - shared = true; - } else if (mem.eql(u8, arg, "-dynamic")) { - dynamic = true; - } else if (mem.eql(u8, arg, "-static")) { - dynamic = false; - } else if (mem.eql(u8, arg, "-rpath")) { - if (i + 1 >= args.len) fatal("Expected path after {s}", .{arg}); - i += 1; - try rpath_list.append(args[i]); - } else if (mem.eql(u8, arg, "-compatibility_version")) { - if (i + 1 >= args.len) fatal("Expected version after {s}", .{arg}); - i += 1; - compatibility_version = std.builtin.Version.parse(args[i]) catch |err| { - fatal("Unable to parse {s} {s}: {s}", .{ arg, args[i], @errorName(err) }); - }; - } else if (mem.eql(u8, arg, "-current_version")) { - if (i + 1 >= args.len) fatal("Expected version after {s}", .{arg}); - i += 1; - version = std.builtin.Version.parse(args[i]) catch |err| { - fatal("Unable to parse {s} {s}: {s}", .{ arg, args[i], @errorName(err) }); - }; - } else if (mem.eql(u8, arg, "--verbose")) { - verbose = true; +pub fn main() !void { + const all_args = try std.process.argsAlloc(gpa); + defer std.process.argsFree(gpa, all_args); + + const cmd = std.fs.path.basename(all_args[0]); + const tag: Zld.Tag = blk: { + if (mem.eql(u8, cmd, "ld.zld")) { + break :blk .elf; + } else if (mem.eql(u8, cmd, "ld64.zld")) { + break :blk .macho; + } else if (mem.eql(u8, cmd, "link-zld")) { + break :blk .coff; } else { - try positionals.append(.{ - .path = arg, - .must_link = true, - }); + std.io.getStdOut().writeAll(usage) catch {}; + std.process.exit(0); } - } - - if (positionals.items.len == 0) { - fatal("Expected at least one input .o file", .{}); - } - - // TODO allow for non-native targets - const target = builtin.target; - - var zld = try Zld.openPath(gpa, .{ - .emit = .{ - .directory = std.fs.cwd(), - .sub_path = out_path orelse "a.out", - }, - .dynamic = dynamic, - .target = target, - .output_mode = if (dylib or shared) .lib else .exe, - .syslibroot = syslibroot, - .positionals = positionals.items, - .libs = libs, - .frameworks = frameworks, - .lib_dirs = lib_dirs.items, - .framework_dirs = framework_dirs.items, - .rpath_list = rpath_list.items, - .stack_size_override = stack, - .version = version, - .compatibility_version = compatibility_version, - .gc_sections = gc_sections, - .verbose = verbose, + }; + return Zld.main(tag, .{ + .gpa = gpa, + .args = all_args[1..], + .log_scopes = &log_scopes, }); - defer zld.deinit(); - - try zld.flush(); } diff --git a/src/test.zig b/src/test.zig index 9257b94c..11dd91f8 100644 --- a/src/test.zig +++ b/src/test.zig @@ -6,16 +6,14 @@ const testing = std.testing; const process = std.process; const log = std.log.scoped(.tests); +const Allocator = mem.Allocator; const ChildProcess = std.ChildProcess; const Target = std.Target; const CrossTarget = std.zig.CrossTarget; const tmpDir = testing.tmpDir; const Zld = @import("Zld.zig"); -var gpa = std.heap.GeneralPurposeAllocator(.{}){}; -const allocator = gpa.allocator(); -// TODO fix memory leaks in std.dwarf -// const allocator = testing.allocator(); +const gpa = testing.allocator; test "unit" { _ = @import("Zld.zig"); @@ -56,7 +54,7 @@ pub const TestContext = struct { contents: []const u8, /// Caller own the memory. - fn getFilename(self: InputFile) ![]u8 { + fn getFilename(self: InputFile, allocator: Allocator) ![]u8 { const ext = switch (self.filetype) { .Header => ".h", .C => ".c", @@ -67,7 +65,7 @@ pub const TestContext = struct { } }; - pub fn init(name: []const u8, target: CrossTarget) Case { + pub fn init(allocator: Allocator, name: []const u8, target: CrossTarget) Case { var input_files = std.ArrayList(InputFile).init(allocator); return .{ .name = name, @@ -115,7 +113,7 @@ pub const TestContext = struct { }; pub fn init() TestContext { - var cases = std.ArrayList(Case).init(allocator); + var cases = std.ArrayList(Case).init(gpa); return .{ .cases = cases }; } @@ -128,45 +126,38 @@ pub const TestContext = struct { pub fn addCase(self: *TestContext, name: []const u8, target: CrossTarget) !*Case { const idx = self.cases.items.len; - try self.cases.append(Case.init(name, target)); + try self.cases.append(Case.init(gpa, name, target)); return &self.cases.items[idx]; } pub fn run(self: *TestContext) !void { + var arena_allocator = std.heap.ArenaAllocator.init(gpa); + defer arena_allocator.deinit(); + const arena = arena_allocator.allocator(); + for (self.cases.items) |case| { var tmp = tmpDir(.{}); defer tmp.cleanup(); - const cwd = try std.fs.path.join(allocator, &[_][]const u8{ + const cwd = try std.fs.path.join(arena, &[_][]const u8{ "zig-cache", "tmp", &tmp.sub_path, }); - defer allocator.free(cwd); - var objects = std.ArrayList(Zld.LinkObject).init(allocator); - defer { - for (objects.items) |obj| { - allocator.free(obj.path); - } - objects.deinit(); - } + var objects = std.ArrayList(Zld.LinkObject).init(arena); - const target_triple = try std.fmt.allocPrint(allocator, "{s}-{s}-{s}", .{ + const target_triple = try std.fmt.allocPrint(arena, "{s}-{s}-{s}", .{ @tagName(case.target.cpu_arch.?), @tagName(case.target.os_tag.?), @tagName(case.target.abi.?), }); - defer allocator.free(target_triple); var requires_crts: bool = true; for (case.input_files.items) |input_file| { - const input_filename = try input_file.getFilename(); - defer allocator.free(input_filename); + const input_filename = try input_file.getFilename(arena); try tmp.dir.writeFile(input_filename, input_file.contents); - var argv = std.ArrayList([]const u8).init(allocator); - defer argv.deinit(); - + var argv = std.ArrayList([]const u8).init(arena); try argv.append("zig"); switch (input_file.filetype) { @@ -190,28 +181,23 @@ pub const TestContext = struct { try argv.append(input_filename); - const output_filename = try std.fmt.allocPrint(allocator, "{s}.o", .{input_file.basename}); - defer allocator.free(output_filename); + const output_filename = try std.fmt.allocPrint(arena, "{s}.o", .{input_file.basename}); if (input_file.filetype != .Zig) { try argv.append("-o"); try argv.append(output_filename); } - const output_file_path = try std.fs.path.join(allocator, &[_][]const u8{ + const output_file_path = try std.fs.path.join(arena, &[_][]const u8{ cwd, output_filename, }); - try objects.append(.{ .path = output_file_path, .must_link = true }); + try objects.append(.{ .path = output_file_path, .must_link = false }); const result = try std.ChildProcess.exec(.{ - .allocator = allocator, + .allocator = arena, .argv = argv.items, .cwd = cwd, }); - defer { - allocator.free(result.stdout); - allocator.free(result.stderr); - } if (result.stdout.len != 0) { log.warn("unexpected compiler stdout: {s}", .{result.stdout}); } @@ -226,7 +212,7 @@ pub const TestContext = struct { } // compiler_rt - const compiler_rt_path = try std.fs.path.join(allocator, &[_][]const u8{ + const compiler_rt_path = try std.fs.path.join(arena, &[_][]const u8{ "test", "assets", target_triple, "libcompiler_rt.a", }); try objects.append(.{ .path = compiler_rt_path, .must_link = false }); @@ -234,72 +220,112 @@ pub const TestContext = struct { if (case.target.getAbi() == .musl) { if (requires_crts) { // crt1 - const crt1_path = try std.fs.path.join(allocator, &[_][]const u8{ + const crt1_path = try std.fs.path.join(arena, &[_][]const u8{ "test", "assets", target_triple, "crt1.o", }); try objects.append(.{ .path = crt1_path, .must_link = true }); // crti - const crti_path = try std.fs.path.join(allocator, &[_][]const u8{ + const crti_path = try std.fs.path.join(arena, &[_][]const u8{ "test", "assets", target_triple, "crti.o", }); try objects.append(.{ .path = crti_path, .must_link = true }); // crtn - const crtn_path = try std.fs.path.join(allocator, &[_][]const u8{ + const crtn_path = try std.fs.path.join(arena, &[_][]const u8{ "test", "assets", target_triple, "crtn.o", }); try objects.append(.{ .path = crtn_path, .must_link = true }); } // libc - const libc_path = try std.fs.path.join(allocator, &[_][]const u8{ + const libc_path = try std.fs.path.join(arena, &[_][]const u8{ "test", "assets", target_triple, "libc.a", }); try objects.append(.{ .path = libc_path, .must_link = false }); } - const output_path = try std.fs.path.join(allocator, &[_][]const u8{ + const output_path = try std.fs.path.join(arena, &[_][]const u8{ "zig-cache", "tmp", &tmp.sub_path, "a.out", }); - defer allocator.free(output_path); - - var libs = std.StringArrayHashMap(Zld.SystemLib).init(allocator); - defer libs.deinit(); - - var frameworks = std.StringArrayHashMap(Zld.SystemLib).init(allocator); - defer frameworks.deinit(); - - const host = try std.zig.system.NativeTargetInfo.detect(allocator, .{}); - const target_info = try std.zig.system.NativeTargetInfo.detect(allocator, case.target); - const syslibroot = blk: { - if (case.target.getOsTag() == .macos and host.target.os.tag == .macos) { - if (!std.zig.system.darwin.isDarwinSDKInstalled(allocator)) break :blk null; - const sdk = std.zig.system.darwin.getDarwinSDK(allocator, host.target) orelse - break :blk null; - break :blk sdk.path; + + var libs = std.StringArrayHashMap(Zld.SystemLib).init(arena); + var lib_dirs = std.ArrayList([]const u8).init(arena); + var frameworks = std.StringArrayHashMap(Zld.SystemLib).init(arena); + var framework_dirs = std.ArrayList([]const u8).init(arena); + + const host = try std.zig.system.NativeTargetInfo.detect(arena, .{}); + const target_info = try std.zig.system.NativeTargetInfo.detect(arena, case.target); + var syslibroot: ?[]const u8 = null; + + if (case.target.isDarwin()) { + try libs.put("System", .{}); + try lib_dirs.append("/usr/lib"); + try framework_dirs.append("/System/Library/Frameworks"); + + if (std.zig.system.darwin.isDarwinSDKInstalled(arena)) { + if (std.zig.system.darwin.getDarwinSDK(arena, host.target)) |sdk| { + syslibroot = sdk.path; + } } - break :blk null; + } + + const tag: Zld.Tag = switch (case.target.os_tag.?) { + .macos, + .ios, + .watchos, + .tvos, + => .macho, + .linux => .elf, + .windows => .coff, + else => unreachable, }; - var zld = try Zld.openPath(allocator, .{ - .emit = .{ - .directory = std.fs.cwd(), - .sub_path = output_path, - }, - .dynamic = true, - .target = case.target.toTarget(), - .output_mode = .exe, - .syslibroot = syslibroot, - .positionals = objects.items, - .libs = libs, - .frameworks = frameworks, - .lib_dirs = &[0][]const u8{}, - .framework_dirs = &[0][]const u8{}, - .rpath_list = &[0][]const u8{}, - .gc_sections = true, - }); + var opts: Zld.Options = switch (tag) { + .macho => .{ .macho = .{ + .emit = .{ + .directory = std.fs.cwd(), + .sub_path = output_path, + }, + .dynamic = true, + .target = case.target, + .platform_version = target_info.target.os.version_range.semver.min, + .sdk_version = target_info.target.os.version_range.semver.min, + .output_mode = .exe, + .syslibroot = syslibroot, + .positionals = objects.items, + .libs = libs, + .frameworks = frameworks, + .lib_dirs = lib_dirs.items, + .framework_dirs = framework_dirs.items, + .rpath_list = &[0][]const u8{}, + .dead_strip = true, + } }, + .elf => .{ .elf = .{ + .emit = .{ + .directory = std.fs.cwd(), + .sub_path = output_path, + }, + .target = case.target, + .output_mode = .exe, + .positionals = objects.items, + .libs = libs, + .lib_dirs = lib_dirs.items, + .rpath_list = &[0][]const u8{}, + .gc_sections = true, + } }, + .coff => .{ .coff = .{ + .emit = .{ + .directory = std.fs.cwd(), + .sub_path = output_path, + }, + .target = case.target, + .output_mode = .exe, + .positionals = objects.items, + .libs = libs, + .lib_dirs = &[0][]const u8{}, + } }, + }; + const zld = try Zld.openPath(gpa, tag, opts); defer zld.deinit(); - var argv = std.ArrayList([]const u8).init(allocator); - defer argv.deinit(); - + var argv = std.ArrayList([]const u8).init(arena); outer: { switch (host.getExternalExecutor(target_info, .{})) { .native => { @@ -320,14 +346,10 @@ pub const TestContext = struct { } const result = try std.ChildProcess.exec(.{ - .allocator = allocator, + .allocator = arena, .argv = argv.items, .cwd = cwd, }); - defer { - allocator.free(result.stdout); - allocator.free(result.stderr); - } if (case.expected_out.stdout != null or case.expected_out.stderr != null) { if (case.expected_out.stderr) |err| { @@ -359,7 +381,7 @@ pub const TestContext = struct { }; fn printInvocation(argv: []const []const u8) !void { - const full_inv = try std.mem.join(allocator, " ", argv); - defer allocator.free(full_inv); + const full_inv = try std.mem.join(gpa, " ", argv); + defer gpa.free(full_inv); log.err("The following command failed:\n{s}", .{full_inv}); }