From e4d1bd610534c9a6ed01a17481bc4b6dc5b7d8dc Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 2 Aug 2026 14:32:26 +0100 Subject: [PATCH 01/10] compiler: fix missed `Coff2` renames --- src/codegen.zig | 2 +- src/codegen/x86_64/Emit.zig | 16 ++++++++-------- src/dev.zig | 4 +--- src/link.zig | 32 ++++++++++++++++---------------- src/link/Coff.zig | 2 +- 5 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/codegen.zig b/src/codegen.zig index a71f2e9d13710379a141de0b7969e31e92156cb4..61932ba4f1ddf7d921156053c9c38cb785f69c41 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -862,7 +862,7 @@ pub fn genNavRef( if (is_threadlocal) zo.symbols.items[sym_index].flags.tlv = true; return @fromBackingInt(@intCast(sym_index)); } - } else if (lf.cast(.coff2)) |coff| { + } else if (lf.cast(.coff)) |coff| { return @fromBackingInt(@intCast(@backingInt(try coff.navSymbol(zcu, nav_index)))); } else { std.debug.panic("TODO genNavRef for '{t}'", .{lf.tag}); diff --git a/src/codegen/x86_64/Emit.zig b/src/codegen/x86_64/Emit.zig index eddc1f1b9341485ac0802140bbe0f1efbd08efa7..6dcaf2173d2f1fb36cf8940a0146881de02cfb93 100644 --- a/src/codegen/x86_64/Emit.zig +++ b/src/codegen/x86_64/Emit.zig @@ -142,7 +142,7 @@ pub fn emitMir(emit: *Emit) Error!void { else if (emit.bin_file.cast(.macho)) |macho_file| @fromBackingInt(@intCast(macho_file.getZigObject().?.getOrCreateMetadataForLazySymbol(macho_file, emit.pt, lazy_sym) catch |err| return emit.fail("{s} creating lazy symbol", .{@errorName(err)}))) - else if (emit.bin_file.cast(.coff2)) |coff| + else if (emit.bin_file.cast(.coff)) |coff| @fromBackingInt(@intCast(@backingInt(try coff.lazySymbol(lazy_sym)))) else return emit.fail("lazy symbols unimplemented for {s}", .{@tagName(emit.bin_file.tag)}), @@ -157,7 +157,7 @@ pub fn emitMir(emit: *Emit) Error!void { .type = .FUNC, }) else if (emit.bin_file.cast(.macho)) |macho_file| @fromBackingInt(@intCast(try macho_file.getGlobalSymbol(extern_func.toSlice(&emit.lower.mir).?, null))) - else if (emit.bin_file.cast(.coff2)) |coff| @fromBackingInt(@intCast(@backingInt(try coff.globalSymbol(.{ + else if (emit.bin_file.cast(.coff)) |coff| @fromBackingInt(@intCast(@backingInt(try coff.globalSymbol(.{ .name = extern_func.toSlice(&emit.lower.mir).?, })))) else return emit.fail("external symbol unimplemented for {s}", .{@tagName(emit.bin_file.tag)}), .is_extern = true, @@ -171,7 +171,7 @@ pub fn emitMir(emit: *Emit) Error!void { switch (lowered_inst.encoding.mnemonic) { .call => { reloc.target = .{ .branch = target }; - if (target.is_dll_import and emit.bin_file.cast(.coff2) != null) { + if (target.is_dll_import and emit.bin_file.cast(.coff) != null) { try emit.encodeInst(try .new(.none, .call, &.{ .{ .mem = .initRip(.ptr, 0) }, }, emit.lower.target), reloc_info); @@ -252,7 +252,7 @@ pub fn emitMir(emit: *Emit) Error!void { }, emit.lower.target), reloc_info), else => unreachable, } - } else if (emit.bin_file.cast(.coff2)) |_| { + } else if (emit.bin_file.cast(.coff)) |_| { if (target.is_dll_import) switch (lowered_inst.encoding.mnemonic) { .lea => try emit.encodeInst(try .new(.none, .mov, &.{ lowered_inst.ops[0], @@ -372,7 +372,7 @@ pub fn emitMir(emit: *Emit) Error!void { }, emit.lower.target), &.{}); }, else => unreachable, - } else if (emit.bin_file.cast(.coff2)) |coff| { + } else if (emit.bin_file.cast(.coff)) |coff| { switch (emit.lower.target.cpu.arch) { else => unreachable, .x86 => { @@ -845,7 +845,7 @@ fn encodeInst(emit: *Emit, lowered_inst: Instruction, reloc_info: []const RelocI if (target.is_extern and !target.force_pcrel_direct) break :rt .GOTPCREL; break :rt .PC32; } }, - ) else if (emit.bin_file.cast(.coff2)) |coff| try coff.addReloc( + ) else if (emit.bin_file.cast(.coff)) |coff| try coff.addReloc( @fromBackingInt(@intCast(@backingInt(emit.atom_id))), end_offset - 4, @fromBackingInt(@intCast(@backingInt(target.symbol))), @@ -883,7 +883,7 @@ fn encodeInst(emit: *Emit, lowered_inst: Instruction, reloc_info: []const RelocI .symbolnum = @intCast(@backingInt(target.symbol)), }, }); - } else if (emit.bin_file.cast(.coff2)) |coff| try coff.addReloc( + } else if (emit.bin_file.cast(.coff)) |coff| try coff.addReloc( @fromBackingInt(@intCast(@backingInt(emit.atom_id))), end_offset - 4, @fromBackingInt(@intCast(@backingInt(target.symbol))), @@ -941,7 +941,7 @@ fn encodeInst(emit: *Emit, lowered_inst: Instruction, reloc_info: []const RelocI .symbolnum = @intCast(@backingInt(target.symbol)), }, }); - } else if (emit.bin_file.cast(.coff2)) |coff| try coff.addReloc( + } else if (emit.bin_file.cast(.coff)) |coff| try coff.addReloc( @fromBackingInt(@intCast(@backingInt(emit.atom_id))), end_offset - 4, @fromBackingInt(@intCast(@backingInt(target.symbol))), diff --git a/src/dev.zig b/src/dev.zig index e8b62bd420f5d6e9472252cd021db4f122971f1d..683824812a06bb09bb45a25bd43dcf7ecdb6f372 100644 --- a/src/dev.zig +++ b/src/dev.zig @@ -109,7 +109,6 @@ pub const Env = enum { .spork8_backend, .lld_linker, .coff_linker, - .coff2_linker, .elf_linker, .elf2_linker, .macho_linker, @@ -231,7 +230,7 @@ pub const Env = enum { .incremental, .legalize, .x86_64_backend, - .coff2_linker, + .coff_linker, => true, else => Env.sema.supports(feature), }, @@ -321,7 +320,6 @@ pub const Feature = enum { lld_linker, coff_linker, - coff2_linker, elf_linker, elf2_linker, macho_linker, diff --git a/src/link.zig b/src/link.zig index 6b2d2eecdf7f644ddaa94b71a701de983173fda5..0e2d6e2ff5c54184fb473a25dee0a2488e3e746f 100644 --- a/src/link.zig +++ b/src/link.zig @@ -587,7 +587,7 @@ pub const File = struct { pub fn startProgress(base: *File, prog_node: std.Progress.Node) void { switch (base.tag) { else => {}, - inline .elf2, .coff2 => |tag| { + inline .elf2, .coff => |tag| { dev.check(tag.devFeature()); return @as(*tag.Type(), @fieldParentPtr("base", base)).startProgress(prog_node); }, @@ -597,7 +597,7 @@ pub const File = struct { pub fn endProgress(base: *File) void { switch (base.tag) { else => {}, - inline .elf2, .coff2 => |tag| { + inline .elf2, .coff => |tag| { dev.check(tag.devFeature()); return @as(*tag.Type(), @fieldParentPtr("base", base)).endProgress(); }, @@ -651,10 +651,10 @@ pub const File = struct { } base.file = try emit.root_dir.handle.openFile(io, emit.sub_path, .{ .mode = .read_write }); }, - .elf2, .coff2 => if (base.file == null) { + .elf2, .coff => if (base.file == null) { const mf = if (base.cast(.elf2)) |elf| &elf.mf - else if (base.cast(.coff2)) |coff| + else if (base.cast(.coff)) |coff| &coff.mf else unreachable; @@ -740,10 +740,10 @@ pub const File = struct { } } }, - .elf2, .coff2 => if (base.file) |f| { + .elf2, .coff => if (base.file) |f| { const mf = if (base.cast(.elf2)) |elf| &elf.mf - else if (base.cast(.coff2)) |coff| + else if (base.cast(.coff)) |coff| &coff.mf else unreachable; @@ -841,7 +841,7 @@ pub const File = struct { switch (base.tag) { .lld => unreachable, else => {}, - inline .elf, .elf2, .c, .coff2 => |tag| { + inline .elf, .elf2, .c, .coff => |tag| { dev.check(tag.devFeature()); return @as(*tag.Type(), @fieldParentPtr("base", base)).updateContainerType(pt, ty, success); }, @@ -899,7 +899,7 @@ pub const File = struct { .lld => unreachable, .plan9 => unreachable, .spirv => {}, - .coff2 => {}, + .coff => {}, inline else => |tag| { dev.check(tag.devFeature()); return @as(*tag.Type(), @fieldParentPtr("base", base)).updateLineNumber(pt, ti_id, line); @@ -951,7 +951,7 @@ pub const File = struct { pub fn idle(base: *File, tid: Zcu.PerThread.Id) Error!bool { switch (base.tag) { else => return false, - inline .elf2, .coff2 => |tag| { + inline .elf2, .coff => |tag| { dev.check(tag.devFeature()); return @as(*tag.Type(), @fieldParentPtr("base", base)).idle(tid); }, @@ -961,7 +961,7 @@ pub const File = struct { pub fn updateErrorData(base: *File, pt: Zcu.PerThread) Error!void { switch (base.tag) { else => {}, - inline .elf2, .coff2 => |tag| { + inline .elf2, .coff => |tag| { dev.check(tag.devFeature()); return @as(*tag.Type(), @fieldParentPtr("base", base)).updateErrorData(pt); }, @@ -1242,7 +1242,7 @@ pub const File = struct { assert(!base.post_prelink); switch (base.tag) { - inline .coff2, .elf, .elf2, .wasm, .spirv => |tag| { + inline .coff, .elf, .elf2, .wasm, .spirv => |tag| { dev.check(tag.devFeature()); return @as(*tag.Type(), @fieldParentPtr("base", base)).loadInput(input); }, @@ -1261,7 +1261,7 @@ pub const File = struct { } switch (base.tag) { - inline .elf2, .coff2, .wasm, .c => |tag| { + inline .elf2, .coff, .wasm, .c => |tag| { dev.check(tag.devFeature()); try @as(*tag.Type(), @fieldParentPtr("base", base)).prelink(base.comp.link_prog_node); }, @@ -1302,7 +1302,7 @@ pub const File = struct { } pub const Tag = enum { - coff2, + coff, elf, elf2, macho, @@ -1315,7 +1315,7 @@ pub const File = struct { pub fn Type(comptime tag: Tag) type { return switch (tag) { - .coff2 => Coff2, + .coff => Coff, .elf => Elf, .elf2 => Elf2, .macho => MachO, @@ -1330,7 +1330,7 @@ pub const File = struct { fn fromObjectFormat(ofmt: std.Target.ObjectFormat, use_new_linker: bool) Tag { return switch (ofmt) { - .coff => .coff2, + .coff => .coff, .elf => if (use_new_linker) .elf2 else .elf, .macho => .macho, .wasm => .wasm, @@ -1420,7 +1420,7 @@ pub const File = struct { pub const Lld = @import("link/Lld.zig"); pub const C = @import("link/C.zig"); - pub const Coff2 = @import("link/Coff.zig"); + pub const Coff = @import("link/Coff.zig"); pub const Spork8 = @import("link/Spork8.zig"); pub const Elf = @import("link/Elf.zig"); pub const Elf2 = @import("link/Elf2.zig"); diff --git a/src/link/Coff.zig b/src/link/Coff.zig index 250dbd388658148ff196567923b5e0de593f72eb..1aa577aa45026387561fc81794ce7e43f42b71be 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -1607,7 +1607,7 @@ fn create( errdefer file.close(io); coff.* = .{ .base = .{ - .tag = .coff2, + .tag = .coff, .comp = comp, .emit = path, -- 2.54.0 From 02b67d7e19455cfc069daf9e665fe28d4d01a9f2 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 2 Aug 2026 16:02:49 +0100 Subject: [PATCH 02/10] link: simplify public API There was a bunch of legacy cruft in the linker's public API which was duplicating functionality, particularly regarding symbols and relocations. Move towards standardising on an interface based on `link.File.SymbolId`. The `navSymbol` and `uavSymbol` functions return `link.File.SymbolId`s representing a given NAV or UAV; then, with that `SymbolId`, codegen can call `relocSymAddr` to relocate that symbol's absolute address. The caller is responsible for writing placeholder bytes (probably zeroes) to be overwritten by that relocation. Previously the API told you an address to write in, but this was entirely redundant with the added relocation. --- src/codegen.zig | 86 +++++---------------------------- src/codegen/aarch64/Mir.zig | 8 +-- src/codegen/loongarch/Mir.zig | 15 ++---- src/codegen/riscv64/CodeGen.zig | 2 +- src/codegen/x86_64/Emit.zig | 12 ++--- src/link.zig | 28 ++++------- src/link/Coff.zig | 47 +++++------------- src/link/Elf.zig | 16 +++--- src/link/Elf/ZigObject.zig | 80 +++++++++++++----------------- src/link/Elf2.zig | 36 ++------------ src/link/MachO.zig | 16 +++--- src/link/MachO/ZigObject.zig | 74 +++++++++++----------------- 12 files changed, 126 insertions(+), 294 deletions(-) diff --git a/src/codegen.zig b/src/codegen.zig index 61932ba4f1ddf7d921156053c9c38cb785f69c41..2213e7ee506d2156be699188d8d666c1c22d4e7b 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -754,23 +754,15 @@ fn lowerUavRef( } const uav_align = Type.fromInterned(uav.orig_ty).ptrAlignment(zcu); - _ = try lf.lowerUav(pt, uav_val, uav_align); - const vaddr = lf.getUavVAddr(uav_val, .{ + try lf.relocSymAddr(.{ .parent = reloc_parent, .offset = w.end, + .target = try lf.uavSymbol(pt, uav_val, uav_align), .addend = @intCast(offset), - }) catch |err| switch (err) { - error.OutOfMemory => |e| return e, - else => |e| std.debug.panic("TODO rework lowerUav. internal error: {t}", .{e}), - }; - const endian = target.cpu.arch.endian(); - switch (ptr_width_bytes) { - 2 => try w.writeInt(u16, @intCast(vaddr), endian), - 4 => try w.writeInt(u32, @intCast(vaddr), endian), - 8 => try w.writeInt(u64, vaddr, endian), - else => unreachable, - } + }); + + try w.splatByteAll(0, ptr_width_bytes); // overwritten by relocation } fn lowerNavRef( @@ -805,68 +797,14 @@ fn lowerNavRef( else => {}, } - const vaddr = lf.getNavVAddr(pt, nav_index, .{ + try lf.relocSymAddr(.{ .parent = reloc_parent, .offset = w.end, + .target = try lf.navSymbol(nav_index), .addend = @intCast(offset), - }) catch @panic("TODO rework getNavVAddr"); - const endian = target.cpu.arch.endian(); - switch (ptr_width_bytes) { - 2 => try w.writeInt(u16, @intCast(vaddr), endian), - 4 => try w.writeInt(u32, @intCast(vaddr), endian), - 8 => try w.writeInt(u64, vaddr, endian), - else => unreachable, - } -} + }); -pub fn genNavRef( - lf: *link.File, - pt: Zcu.PerThread, - nav_index: InternPool.Nav.Index, -) Error!link.File.SymbolId { - const zcu = pt.zcu; - const ip = &zcu.intern_pool; - const nav = ip.getNav(nav_index); - log.debug("genNavRef({f})", .{nav.fqn.fmt(ip)}); - - const is_threadlocal = nav.resolved.?.@"threadlocal" and zcu.comp.config.any_non_single_threaded; - if (lf.cast(.elf)) |elf_file| { - const zo = elf_file.zigObjectPtr().?; - if (nav.getExtern(ip)) |e| { - const sym_index = try elf_file.getGlobalSymbol(nav.name.toSlice(ip), e.lib_name.toSlice(ip)); - switch (e.linkage) { - .strong => {}, - .weak => zo.symbol(sym_index).flags.weak = true, - } - if (is_threadlocal) zo.symbol(sym_index).flags.is_tls = true; - return @fromBackingInt(@intCast(sym_index)); - } else { - const sym_index = try zo.getOrCreateMetadataForNav(zcu, nav_index); - if (is_threadlocal) zo.symbol(sym_index).flags.is_tls = true; - return @fromBackingInt(@intCast(sym_index)); - } - } else if (lf.cast(.elf2)) |elf| { - return elf.navSymbol(nav_index); - } else if (lf.cast(.macho)) |macho_file| { - const zo = macho_file.getZigObject().?; - if (nav.getExtern(ip)) |e| { - const sym_index = try macho_file.getGlobalSymbol(nav.name.toSlice(ip), e.lib_name.toSlice(ip)); - switch (e.linkage) { - .strong => {}, - .weak => zo.symbols.items[sym_index].flags.weak = true, - } - if (is_threadlocal) zo.symbols.items[sym_index].flags.tlv = true; - return @fromBackingInt(@intCast(sym_index)); - } else { - const sym_index = try zo.getOrCreateMetadataForNav(macho_file, nav_index); - if (is_threadlocal) zo.symbols.items[sym_index].flags.tlv = true; - return @fromBackingInt(@intCast(sym_index)); - } - } else if (lf.cast(.coff)) |coff| { - return @fromBackingInt(@intCast(@backingInt(try coff.navSymbol(zcu, nav_index)))); - } else { - std.debug.panic("TODO genNavRef for '{t}'", .{lf.tag}); - } + try w.splatByteAll(0, ptr_width_bytes); // overwritten by relocation } /// deprecated legacy type @@ -908,13 +846,13 @@ pub fn genTypedValue( .none => .none, .undef => .undef, .immediate => |imm| .{ .immediate = imm }, - .lea_nav => |nav| .{ .lea_symbol = try genNavRef(lf, pt, nav) }, - .load_uav => |uav| .{ .load_symbol = try lf.lowerUav( + .lea_nav => |nav| .{ .lea_symbol = try lf.navSymbol(nav) }, + .load_uav => |uav| .{ .load_symbol = try lf.uavSymbol( pt, uav.val, Type.fromInterned(uav.orig_ty).ptrAlignment(pt.zcu), ) }, - .lea_uav => |uav| .{ .lea_symbol = try lf.lowerUav( + .lea_uav => |uav| .{ .lea_symbol = try lf.uavSymbol( pt, uav.val, Type.fromInterned(uav.orig_ty).ptrAlignment(pt.zcu), diff --git a/src/codegen/aarch64/Mir.zig b/src/codegen/aarch64/Mir.zig index ad40e86902d4273b2b558fa1b1b97add12b29345..1233a8180db0e726d2d2904eb2246734ad18ab56 100644 --- a/src/codegen/aarch64/Mir.zig +++ b/src/codegen/aarch64/Mir.zig @@ -93,11 +93,7 @@ pub fn emit( lf, zcu, atom_index, - try @import("../../codegen.zig").genNavRef( - lf, - pt, - nav_reloc.nav, - ), + try lf.navSymbol(nav_reloc.nav), mir.body[nav_reloc.reloc.label], body_end - Instruction.size * (1 + nav_reloc.reloc.label), nav_reloc.reloc.addend, @@ -107,7 +103,7 @@ pub fn emit( lf, zcu, atom_index, - try lf.lowerUav( + try lf.uavSymbol( pt, uav_reloc.uav.val, ZigType.fromInterned(uav_reloc.uav.orig_ty).ptrAlignment(zcu), diff --git a/src/codegen/loongarch/Mir.zig b/src/codegen/loongarch/Mir.zig index f81843ff4e24ce3c395cd250844e1bed9bb368cb..e107984767b547ccb82c9ac1ab869c4ee4d02e24 100644 --- a/src/codegen/loongarch/Mir.zig +++ b/src/codegen/loongarch/Mir.zig @@ -83,11 +83,7 @@ pub fn emit( lf, zcu, atom_index, - try @import("../../codegen.zig").genNavRef( - lf, - pt, - nav_reloc.nav, - ), + try lf.navSymbol(nav_reloc.nav), nav_reloc.reloc.type, body_end - @sizeOf(Instruction) * (1 + nav_reloc.reloc.label), nav_reloc.reloc.addend, @@ -97,7 +93,7 @@ pub fn emit( lf, zcu, atom_index, - try lf.lowerUav( + try lf.uavSymbol( pt, uav_reloc.uav.val, ZigType.fromInterned(uav_reloc.uav.orig_ty).ptrAlignment(zcu), @@ -142,16 +138,11 @@ pub fn emit( ) catch |err| return zcu.codegenFail(func.owner_nav, "emit reloc failed: {t}", .{err}); - const func_nav = try @import("../../codegen.zig").genNavRef( - lf, - pt, - func.owner_nav, - ); for (mir.internal_relocs) |internal_reloc| emitReloc( lf, zcu, atom_index, - func_nav, + try lf.navSymbol(func.owner_nav), internal_reloc.reloc.type, body_end - @sizeOf(Instruction) * (1 + internal_reloc.reloc.label), @sizeOf(Instruction) * (@as(i64, @intCast(mir.prologue.len + mir.body.len - internal_reloc.target))), diff --git a/src/codegen/riscv64/CodeGen.zig b/src/codegen/riscv64/CodeGen.zig index 1db4dde0627e2ee00b6f65f9c48db0b53b29f8c2..aeb1dde892769fe1270542079b778ed6487d0cdf 100644 --- a/src/codegen/riscv64/CodeGen.zig +++ b/src/codegen/riscv64/CodeGen.zig @@ -8160,7 +8160,7 @@ fn genTypedValue(func: *Func, val: Value) InnerError!MCValue { const lf = func.bin_file; const result: codegen.MCValue = if (val.isUndef(pt.zcu)) - .{ .load_symbol = try lf.lowerUav(pt, val.toIntern(), .none) } + .{ .load_symbol = try lf.uavSymbol(pt, val.toIntern(), .none) } else try codegen.genTypedValue(lf, pt, val, func.target); const mcv: MCValue = switch (result) { diff --git a/src/codegen/x86_64/Emit.zig b/src/codegen/x86_64/Emit.zig index 6dcaf2173d2f1fb36cf8940a0146881de02cfb93..a82ae415ac502309854af8e9ca44636ee3c8ab43 100644 --- a/src/codegen/x86_64/Emit.zig +++ b/src/codegen/x86_64/Emit.zig @@ -100,11 +100,7 @@ pub fn emitMir(emit: *Emit) Error!void { .inst => |inst| .{ .inst = inst }, .table => .table, .nav => |nav| { - const symbol_id = try codegen.genNavRef( - emit.bin_file, - emit.pt, - nav, - ); + const symbol_id = try emit.bin_file.navSymbol(nav); const target_symbol: RelocInfo.Target.Symbol = if (ip.getNav(nav).getExtern(ip)) |@"extern"| .{ .symbol = symbol_id, .is_extern = switch (@"extern".visibility) { @@ -124,7 +120,7 @@ pub fn emitMir(emit: *Emit) Error!void { } }, .uav => |uav| .{ .symbol = .{ - .symbol = try emit.bin_file.lowerUav( + .symbol = try emit.bin_file.uavSymbol( emit.pt, uav.val, Type.fromInterned(uav.orig_ty).ptrAlignment(emit.pt.zcu), @@ -597,12 +593,12 @@ pub fn emitMir(emit: *Emit) Error!void { .nav => |nav| @unionInit( DwarfLoc, addr_loc, - try codegen.genNavRef(emit.bin_file, emit.pt, nav), + try emit.bin_file.navSymbol(nav), ), .uav => |uav| @unionInit( DwarfLoc, addr_loc, - try emit.bin_file.lowerUav( + try emit.bin_file.uavSymbol( emit.pt, uav.val, Type.fromInterned(uav.orig_ty) diff --git a/src/link.zig b/src/link.zig index 0e2d6e2ff5c54184fb473a25dee0a2488e3e746f..c72a65a888eb4489153569c6971c9eec6d63f217 100644 --- a/src/link.zig +++ b/src/link.zig @@ -1039,6 +1039,7 @@ pub const File = struct { pub const RelocInfo = struct { parent: Parent, offset: u64, + target: SymbolId, addend: u32, pub const Parent = union(enum) { @@ -1048,16 +1049,9 @@ pub const File = struct { }; }; - /// Get allocated `Nav`'s address in virtual memory. - /// The linker is passed information about the containing atom, `parent_atom_index`, and offset within it's - /// memory buffer, `offset`, so that it can make a note of potential relocation sites, should the - /// `Nav`'s address was not yet resolved, or the containing atom gets moved in virtual memory. - /// May be called before or after updateFunc/updateNav therefore it is up to the linker to allocate - /// the block/atom. /// Never called when LLVM is codegenning the ZCU. - pub fn getNavVAddr(base: *File, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index, reloc_info: RelocInfo) Error!u64 { - assert(pt.zcu.llvm_object == null); - + pub fn relocSymAddr(base: *File, reloc_info: RelocInfo) Error!void { + assert(base.comp.zcu.?.llvm_object == null); switch (base.tag) { .lld => unreachable, .c => unreachable, @@ -1067,20 +1061,19 @@ pub const File = struct { .spork8 => unreachable, inline else => |tag| { dev.check(tag.devFeature()); - return @as(*tag.Type(), @fieldParentPtr("base", base)).getNavVAddr(pt, nav_index, reloc_info); + return @as(*tag.Type(), @fieldParentPtr("base", base)).relocSymAddr(reloc_info); }, } } /// Never called when LLVM is codegenning the ZCU. - pub fn lowerUav( + pub fn uavSymbol( base: *File, pt: Zcu.PerThread, - decl_val: InternPool.Index, - decl_align: InternPool.Alignment, + uav_val: InternPool.Index, + uav_align: InternPool.Alignment, ) Error!SymbolId { assert(pt.zcu.llvm_object == null); - switch (base.tag) { .lld => unreachable, .c => unreachable, @@ -1090,15 +1083,14 @@ pub const File = struct { .spork8 => unreachable, inline else => |tag| { dev.check(tag.devFeature()); - return @as(*tag.Type(), @fieldParentPtr("base", base)).lowerUav(pt, decl_val, decl_align); + return @as(*tag.Type(), @fieldParentPtr("base", base)).uavSymbol(pt, uav_val, uav_align); }, } } /// Never called when LLVM is codegenning the ZCU. - pub fn getUavVAddr(base: *File, decl_val: InternPool.Index, reloc_info: RelocInfo) Error!u64 { + pub fn navSymbol(base: *File, nav: InternPool.Nav.Index) Error!SymbolId { assert(base.comp.zcu.?.llvm_object == null); - switch (base.tag) { .lld => unreachable, .c => unreachable, @@ -1108,7 +1100,7 @@ pub const File = struct { .spork8 => unreachable, inline else => |tag| { dev.check(tag.devFeature()); - return @as(*tag.Type(), @fieldParentPtr("base", base)).getUavVAddr(decl_val, reloc_info); + return @as(*tag.Type(), @fieldParentPtr("base", base)).navSymbol(nav); }, } } diff --git a/src/link/Coff.zig b/src/link/Coff.zig index 1aa577aa45026387561fc81794ce7e43f42b71be..dbdea4966de0b5ba91b7e2b026a7b80c419d5854 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -2935,18 +2935,19 @@ fn navMapIndex(coff: *Coff, zcu: *Zcu, nav_index: InternPool.Nav.Index) !Node.Na if (!sym_gop.found_existing) sym_gop.value_ptr.* = coff.addSymbolAssumeCapacity(); return @fromBackingInt(@intCast(sym_gop.index)); } -pub fn navSymbol(coff: *Coff, zcu: *Zcu, nav_index: InternPool.Nav.Index) !Symbol.Index { +pub fn navSymbol(coff: *Coff, nav_index: InternPool.Nav.Index) link.Error!link.File.SymbolId { + const zcu = coff.base.comp.zcu.?; const ip = &zcu.intern_pool; const nav = ip.getNav(nav_index); - if (nav.getExtern(ip)) |@"extern"| return coff.globalSymbol(.{ + if (nav.getExtern(ip)) |@"extern"| return @bitCast(try coff.globalSymbol(.{ .name = @"extern".name.toSlice(ip), .lib_name = @"extern".lib_name.toSlice(ip), // TODO: Threadlocal as well? .type = if (ip.isFunctionType(nav.resolved.?.type)) .code else .data, .dll_storage_class = if (@"extern".is_dll_import) .dllimport else .default, - }); + })); const nmi = try coff.navMapIndex(zcu, nav_index); - return nmi.symbol(coff); + return @bitCast(nmi.symbol(coff)); } fn uavMapIndex(coff: *Coff, uav_val: InternPool.Index) !Node.UavMapIndex { @@ -2956,10 +2957,6 @@ fn uavMapIndex(coff: *Coff, uav_val: InternPool.Index) !Node.UavMapIndex { if (!sym_gop.found_existing) sym_gop.value_ptr.* = coff.addSymbolAssumeCapacity(); return @fromBackingInt(@intCast(sym_gop.index)); } -pub fn uavSymbol(coff: *Coff, uav_val: InternPool.Index) !Symbol.Index { - const umi = try coff.uavMapIndex(uav_val); - return umi.symbol(coff); -} pub fn lazySymbol(coff: *Coff, lazy: link.File.LazySymbol) !Symbol.Index { const gpa = coff.base.comp.gpa; @@ -2972,24 +2969,8 @@ pub fn lazySymbol(coff: *Coff, lazy: link.File.LazySymbol) !Symbol.Index { return sym_gop.value_ptr.*; } -pub fn getNavVAddr( - coff: *Coff, - pt: Zcu.PerThread, - nav: InternPool.Nav.Index, - reloc_info: link.File.RelocInfo, -) link.Error!u64 { - return coff.getVAddr(reloc_info, try coff.navSymbol(pt.zcu, nav)); -} - -pub fn getUavVAddr( - coff: *Coff, - uav: InternPool.Index, - reloc_info: link.File.RelocInfo, -) link.Error!u64 { - return coff.getVAddr(reloc_info, try coff.uavSymbol(uav)); -} - -pub fn getVAddr(coff: *Coff, reloc_info: link.File.RelocInfo, target_si: Symbol.Index) link.Error!u64 { +pub fn relocSymAddr(coff: *Coff, reloc_info: link.File.RelocInfo) link.Error!void { + const target_si: Symbol.Index = @bitCast(reloc_info.target); try coff.addReloc( @fromBackingInt(@intCast(@backingInt(reloc_info.parent.atom_index))), reloc_info.offset, @@ -3001,10 +2982,6 @@ pub fn getVAddr(coff: *Coff, reloc_info: link.File.RelocInfo, target_si: Symbol. .I386 => .{ .I386 = .DIR32 }, }, ); - - var vaddr: u64 = target_si.get(coff).rva; - if (coff.isImage()) vaddr += coff.optionalHeaderField(.image_base); - return vaddr; } /// Caller guarantees there is capacity for one member and two nodes @@ -5580,13 +5557,15 @@ pub fn updateContainerType( }; } -pub fn lowerUav( +pub fn uavSymbol( coff: *Coff, pt: Zcu.PerThread, uav_val: InternPool.Index, uav_align: InternPool.Alignment, ) link.Error!link.File.SymbolId { - const zcu = pt.zcu; + _ = pt; + + const zcu = coff.base.comp.zcu.?; const gpa = zcu.gpa; try coff.pending_uavs.ensureUnusedCapacity(gpa, 1); @@ -7471,8 +7450,8 @@ fn updateExportInner( try coff.symbols.ensureUnusedCapacity(gpa, 1); const exported_si: Symbol.Index = switch (exp.exported) { - .nav => |nav| try coff.navSymbol(zcu, nav), - .uav => |uav| @fromBackingInt(@intCast(@backingInt(try coff.lowerUav( + .nav => |nav| @bitCast(try coff.navSymbol(nav)), + .uav => |uav| @fromBackingInt(@intCast(@backingInt(try coff.uavSymbol( pt, uav, Type.fromInterned(ip.typeOf(uav)).abiAlignment(zcu), diff --git a/src/link/Elf.zig b/src/link/Elf.zig index b0e188390f24c3000d382aae4b76646fd6306644..3cbcc53361033b74e8ea1c69189f6909d3be7ad6 100644 --- a/src/link/Elf.zig +++ b/src/link/Elf.zig @@ -463,21 +463,21 @@ pub fn deinit(self: *Elf) void { self.dump_argv_list.deinit(gpa); } -pub fn getNavVAddr(self: *Elf, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index, reloc_info: link.File.RelocInfo) !u64 { - return self.zigObjectPtr().?.getNavVAddr(self, pt, nav_index, reloc_info); +pub fn navSymbol(self: *Elf, nav: InternPool.Nav.Index) link.Error!link.File.SymbolId { + return self.zigObjectPtr().?.navSymbol(self, nav); } -pub fn lowerUav( +pub fn relocSymAddr(self: *Elf, reloc_info: link.File.RelocInfo) !void { + return self.zigObjectPtr().?.relocSymAddr(self, reloc_info); +} + +pub fn uavSymbol( self: *Elf, pt: Zcu.PerThread, uav: InternPool.Index, explicit_alignment: InternPool.Alignment, ) !link.File.SymbolId { - return self.zigObjectPtr().?.lowerUav(self, pt, uav, explicit_alignment); -} - -pub fn getUavVAddr(self: *Elf, uav: InternPool.Index, reloc_info: link.File.RelocInfo) !u64 { - return self.zigObjectPtr().?.getUavVAddr(self, uav, reloc_info); + return self.zigObjectPtr().?.uavSymbol(self, pt, uav, explicit_alignment); } /// Returns end pos of collision, if any. diff --git a/src/link/Elf/ZigObject.zig b/src/link/Elf/ZigObject.zig index ea092197086be61f652433464a06f31a481e7ebe..551f4c626ddb3a0e819eb158a4290c271d5d34be 100644 --- a/src/link/Elf/ZigObject.zig +++ b/src/link/Elf/ZigObject.zig @@ -917,24 +917,42 @@ pub fn codeAlloc(self: *ZigObject, elf_file: *Elf, atom_index: Atom.Index) ![]u8 return code; } -pub fn getNavVAddr( - self: *ZigObject, +pub fn navSymbol( + zo: *ZigObject, elf_file: *Elf, - pt: Zcu.PerThread, nav_index: InternPool.Nav.Index, - reloc_info: link.File.RelocInfo, -) !u64 { - const zcu = pt.zcu; +) link.Error!link.File.SymbolId { + const zcu = elf_file.base.comp.zcu.?; const ip = &zcu.intern_pool; const nav = ip.getNav(nav_index); - log.debug("getNavVAddr {f}({d})", .{ nav.fqn.fmt(ip), nav_index }); - const this_sym_index = if (nav.getExtern(ip)) |@"extern"| try self.getGlobalSymbol( - elf_file, - nav.name.toSlice(ip), - @"extern".lib_name.toSlice(ip), - ) else try self.getOrCreateMetadataForNav(zcu, nav_index); - const this_sym = self.symbol(this_sym_index); - const vaddr = this_sym.address(.{}, elf_file); + if (nav.getExtern(ip)) |@"extern"| { + const sym_index = try zo.getGlobalSymbol( + elf_file, + nav.name.toSlice(ip), + @"extern".lib_name.toSlice(ip), + ); + if (@"extern".linkage == .weak) { + zo.symbol(sym_index).flags.weak = true; + } + if (nav.resolved.?.@"threadlocal") { + zo.symbol(sym_index).flags.is_tls = true; + } + return @fromBackingInt(sym_index); + } else { + const sym_index = try zo.getOrCreateMetadataForNav(zcu, nav_index); + if (nav.resolved.?.@"threadlocal") { + zo.symbol(sym_index).flags.is_tls = true; + } + return @fromBackingInt(sym_index); + } +} + +pub fn relocSymAddr( + self: *ZigObject, + elf_file: *Elf, + reloc_info: link.File.RelocInfo, +) !void { + const this_sym_index = @backingInt(reloc_info.target); switch (reloc_info.parent) { .none => unreachable, .atom_index => |atom_index| { @@ -952,39 +970,9 @@ pub fn getNavVAddr( .target_off = reloc_info.addend, }), } - return @intCast(vaddr); } -pub fn getUavVAddr( - self: *ZigObject, - elf_file: *Elf, - uav: InternPool.Index, - reloc_info: link.File.RelocInfo, -) !u64 { - const sym_index = self.uavs.get(uav).?.symbol_index; - const sym = self.symbol(sym_index); - const vaddr = sym.address(.{}, elf_file); - switch (reloc_info.parent) { - .none => unreachable, - .atom_index => |atom_index| { - const parent_atom = self.symbol(@backingInt(atom_index)).atom(elf_file).?; - const r_type = relocation.encode(.abs, elf_file.getTarget().cpu.arch); - try parent_atom.addReloc(elf_file.base.comp.gpa, .{ - .r_offset = reloc_info.offset, - .r_info = (@as(u64, @intCast(sym_index)) << 32) | r_type, - .r_addend = reloc_info.addend, - }, self); - }, - .debug_output => |debug_output| try debug_output.dwarf.infoExternalReloc(.{ - .source_off = @intCast(reloc_info.offset), - .target_sym = @fromBackingInt(@intCast(sym_index)), - .target_off = reloc_info.addend, - }), - } - return @intCast(vaddr); -} - -pub fn lowerUav( +pub fn uavSymbol( self: *ZigObject, elf_file: *Elf, pt: Zcu.PerThread, @@ -1889,7 +1877,7 @@ pub fn updateExports( break :blk self.navs.getPtr(nav).?; }, .uav => |uav| self.uavs.getPtr(uav) orelse blk: { - _ = try self.lowerUav(elf_file, pt, uav, .none); + _ = try self.uavSymbol(elf_file, pt, uav, .none); break :blk self.uavs.getPtr(uav).?; }, }; diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index 10043990722a7482f2a537aa8191b6c81020787f..5967cfe48e03369cd10f620035179e99b4f41904 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -4324,36 +4324,7 @@ pub fn navSymbol(elf: *Elf, nav_index: InternPool.Nav.Index) link.Error!link.Fil const s: Symbol.Id = .local(nmi.symbol(elf)); return s.toTypeErased(); } -pub fn uavSymbol( - elf: *Elf, - uav_val: InternPool.Index, - uav_align: InternPool.Alignment, -) link.Error!link.File.SymbolId { - const diags = &elf.base.comp.link_diags; - const umi = elf.uavMapIndex(uav_val, uav_align) catch |err| switch (err) { - else => |e| return e, - error.MappedFileIo => return diags.fail("failed to write output file: {t}", .{elf.mf.io_err.?}), - }; - const s: Symbol.Id = .local(umi.symbol(elf)); - return s.toTypeErased(); -} -pub fn getNavVAddr( - elf: *Elf, - pt: Zcu.PerThread, - nav: InternPool.Nav.Index, - reloc_info: link.File.RelocInfo, -) link.Error!u64 { - _ = pt; - return elf.getVAddr(reloc_info, try elf.navSymbol(nav)); -} -pub fn getUavVAddr( - elf: *Elf, - uav_val: InternPool.Index, - reloc_info: link.File.RelocInfo, -) link.Error!u64 { - return elf.getVAddr(reloc_info, try elf.uavSymbol(uav_val, .none)); -} -pub fn getVAddr(elf: *Elf, reloc_info: link.File.RelocInfo, target: link.File.SymbolId) link.Error!u64 { +pub fn relocSymAddr(elf: *Elf, reloc_info: link.File.RelocInfo) link.Error!void { try elf.addReloc( switch (reloc_info.parent) { .none => unreachable, @@ -4361,13 +4332,12 @@ pub fn getVAddr(elf: *Elf, reloc_info: link.File.RelocInfo, target: link.File.Sy .debug_output => |debug_output| Node.toAtom(debug_output.dwarf2.info_writer.ni), }, reloc_info.offset, - target, + reloc_info.target, reloc_info.addend, .absAddr(elf), ); - return Symbol.Id.fromTypeErased(target).value(elf); } -pub fn lowerUav( +pub fn uavSymbol( elf: *Elf, pt: Zcu.PerThread, uav_val: InternPool.Index, diff --git a/src/link/MachO.zig b/src/link/MachO.zig index d321d45939cd883d40a3dadd32462071b442fe67..dbabc88a512ff37af5fd0caa9ece4562b8e06378 100644 --- a/src/link/MachO.zig +++ b/src/link/MachO.zig @@ -3124,21 +3124,21 @@ pub fn freeNav(self: *MachO, nav: InternPool.Nav.Index) void { return self.getZigObject().?.freeNav(nav); } -pub fn getNavVAddr(self: *MachO, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index, reloc_info: link.File.RelocInfo) !u64 { - return self.getZigObject().?.getNavVAddr(self, pt, nav_index, reloc_info); +pub fn navSymbol(self: *MachO, nav: InternPool.Nav.Index) link.Error!link.File.SymbolId { + return self.getZigObject().?.navSymbol(self, nav); } -pub fn lowerUav( +pub fn relocSymAddr(self: *MachO, reloc_info: link.File.RelocInfo) link.Error!void { + return self.getZigObject().?.relocSymAddr(self, reloc_info); +} + +pub fn uavSymbol( self: *MachO, pt: Zcu.PerThread, uav: InternPool.Index, explicit_alignment: InternPool.Alignment, ) !link.File.SymbolId { - return self.getZigObject().?.lowerUav(self, pt, uav, explicit_alignment); -} - -pub fn getUavVAddr(self: *MachO, uav: InternPool.Index, reloc_info: link.File.RelocInfo) !u64 { - return self.getZigObject().?.getUavVAddr(self, uav, reloc_info); + return self.getZigObject().?.uavSymbol(self, pt, uav, explicit_alignment); } pub fn getGlobalSymbol(self: *MachO, name: []const u8, lib_name: ?[]const u8) !u32 { diff --git a/src/link/MachO/ZigObject.zig b/src/link/MachO/ZigObject.zig index 6d7f9d19d3a63a102ec137c4a29f896e03b83572..a44846211834c8b96a52324ce218c7ea479de48f 100644 --- a/src/link/MachO/ZigObject.zig +++ b/src/link/MachO/ZigObject.zig @@ -611,60 +611,43 @@ pub fn flush(self: *ZigObject, macho_file: *MachO, tid: Zcu.PerThread.Id) link.E assert(!self.debug_strtab_dirty); } -pub fn getNavVAddr( - self: *ZigObject, +pub fn navSymbol( + zo: *ZigObject, macho_file: *MachO, - pt: Zcu.PerThread, nav_index: InternPool.Nav.Index, - reloc_info: link.File.RelocInfo, -) !u64 { - const zcu = pt.zcu; +) link.Error!link.File.SymbolId { + const zcu = macho_file.base.comp.zcu.?; const ip = &zcu.intern_pool; const nav = ip.getNav(nav_index); - log.debug("getNavVAddr {f}({d})", .{ nav.fqn.fmt(ip), nav_index }); - const sym_index = if (nav.getExtern(ip)) |@"extern"| try self.getGlobalSymbol( - macho_file, - nav.name.toSlice(ip), - @"extern".lib_name.toSlice(ip), - ) else try self.getOrCreateMetadataForNav(macho_file, nav_index); - const sym = self.symbols.items[sym_index]; - const vaddr = sym.getAddress(.{}, macho_file); - switch (reloc_info.parent) { - .none => unreachable, - .atom_index => |atom_index| { - const parent_atom = self.symbols.items[@backingInt(atom_index)].getAtom(macho_file).?; - try parent_atom.addReloc(macho_file, .{ - .tag = .@"extern", - .offset = @intCast(reloc_info.offset), - .target = sym_index, - .addend = reloc_info.addend, - .type = .unsigned, - .meta = .{ - .pcrel = false, - .has_subtractor = false, - .length = 3, - .symbolnum = @intCast(sym.nlist_idx), - }, - }); - }, - .debug_output => |debug_output| try debug_output.dwarf.infoExternalReloc(.{ - .source_off = @intCast(reloc_info.offset), - .target_sym = @fromBackingInt(@intCast(sym_index)), - .target_off = reloc_info.addend, - }), + if (nav.getExtern(ip)) |@"extern"| { + const sym_index = try zo.getGlobalSymbol( + macho_file, + nav.name.toSlice(ip), + @"extern".lib_name.toSlice(ip), + ); + if (@"extern".linkage == .weak) { + zo.symbols.items[sym_index].flags.weak = true; + } + if (nav.resolved.?.@"threadlocal") { + zo.symbols.items[sym_index].flags.tlv = true; + } + return @fromBackingInt(sym_index); + } else { + const sym_index = try zo.getOrCreateMetadataForNav(macho_file, nav_index); + if (nav.resolved.?.@"threadlocal") { + zo.symbols.items[sym_index].flags.tlv = true; + } + return @fromBackingInt(sym_index); } - return vaddr; } -pub fn getUavVAddr( +pub fn relocSymAddr( self: *ZigObject, macho_file: *MachO, - uav: InternPool.Index, reloc_info: link.File.RelocInfo, -) !u64 { - const sym_index = self.uavs.get(uav).?.symbol_index; +) !void { + const sym_index = @backingInt(reloc_info.target); const sym = self.symbols.items[sym_index]; - const vaddr = sym.getAddress(.{}, macho_file); switch (reloc_info.parent) { .none => unreachable, .atom_index => |atom_index| { @@ -689,10 +672,9 @@ pub fn getUavVAddr( .target_off = reloc_info.addend, }), } - return vaddr; } -pub fn lowerUav( +pub fn uavSymbol( self: *ZigObject, macho_file: *MachO, pt: Zcu.PerThread, @@ -1281,7 +1263,7 @@ pub fn updateExports( break :blk self.navs.getPtr(nav).?; }, .uav => |uav| self.uavs.getPtr(uav) orelse blk: { - _ = try self.lowerUav(macho_file, pt, uav, .none); + _ = try self.uavSymbol(macho_file, pt, uav, .none); break :blk self.uavs.getPtr(uav).?; }, }; -- 2.54.0 From 4019d010007294de58acd5370cc879f14d40e506 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 2 Aug 2026 16:11:55 +0100 Subject: [PATCH 03/10] MachO2: stub new linker implementation --- src/dev.zig | 2 + src/link.zig | 26 +++-- src/link/MachO2.zig | 275 ++++++++++++++++++++++++++++++++++++++++++++ src/target.zig | 2 +- 4 files changed, 295 insertions(+), 10 deletions(-) create mode 100644 src/link/MachO2.zig diff --git a/src/dev.zig b/src/dev.zig index 683824812a06bb09bb45a25bd43dcf7ecdb6f372..6472f65f518bcc909f72ce3863c72dd0c6a2c4fc 100644 --- a/src/dev.zig +++ b/src/dev.zig @@ -112,6 +112,7 @@ pub const Env = enum { .elf_linker, .elf2_linker, .macho_linker, + .macho2_linker, .c_linker, .wasm_linker, .spirv_linker, @@ -323,6 +324,7 @@ pub const Feature = enum { elf_linker, elf2_linker, macho_linker, + macho2_linker, c_linker, wasm_linker, spirv_linker, diff --git a/src/link.zig b/src/link.zig index c72a65a888eb4489153569c6971c9eec6d63f217..62751df5dd2d619f09764d5dd3aa47ba24aa9840 100644 --- a/src/link.zig +++ b/src/link.zig @@ -587,7 +587,7 @@ pub const File = struct { pub fn startProgress(base: *File, prog_node: std.Progress.Node) void { switch (base.tag) { else => {}, - inline .elf2, .coff => |tag| { + inline .elf2, .coff, .macho2 => |tag| { dev.check(tag.devFeature()); return @as(*tag.Type(), @fieldParentPtr("base", base)).startProgress(prog_node); }, @@ -597,7 +597,7 @@ pub const File = struct { pub fn endProgress(base: *File) void { switch (base.tag) { else => {}, - inline .elf2, .coff => |tag| { + inline .elf2, .coff, .macho2 => |tag| { dev.check(tag.devFeature()); return @as(*tag.Type(), @fieldParentPtr("base", base)).endProgress(); }, @@ -651,11 +651,13 @@ pub const File = struct { } base.file = try emit.root_dir.handle.openFile(io, emit.sub_path, .{ .mode = .read_write }); }, - .elf2, .coff => if (base.file == null) { + .elf2, .coff, .macho2 => if (base.file == null) { const mf = if (base.cast(.elf2)) |elf| &elf.mf else if (base.cast(.coff)) |coff| &coff.mf + else if (base.cast(.macho2)) |macho| + &macho.mf else unreachable; mf.memory_map.file = try base.emit.root_dir.handle.openFile(io, base.emit.sub_path, .{ @@ -740,11 +742,13 @@ pub const File = struct { } } }, - .elf2, .coff => if (base.file) |f| { + .elf2, .coff, .macho2 => if (base.file) |f| { const mf = if (base.cast(.elf2)) |elf| &elf.mf else if (base.cast(.coff)) |coff| &coff.mf + else if (base.cast(.macho2)) |macho| + &macho.mf else unreachable; mf.unmap(); @@ -900,6 +904,7 @@ pub const File = struct { .plan9 => unreachable, .spirv => {}, .coff => {}, + .macho2 => {}, inline else => |tag| { dev.check(tag.devFeature()); return @as(*tag.Type(), @fieldParentPtr("base", base)).updateLineNumber(pt, ti_id, line); @@ -951,7 +956,7 @@ pub const File = struct { pub fn idle(base: *File, tid: Zcu.PerThread.Id) Error!bool { switch (base.tag) { else => return false, - inline .elf2, .coff => |tag| { + inline .elf2, .coff, .macho2 => |tag| { dev.check(tag.devFeature()); return @as(*tag.Type(), @fieldParentPtr("base", base)).idle(tid); }, @@ -961,7 +966,7 @@ pub const File = struct { pub fn updateErrorData(base: *File, pt: Zcu.PerThread) Error!void { switch (base.tag) { else => {}, - inline .elf2, .coff => |tag| { + inline .elf2, .coff, .macho2 => |tag| { dev.check(tag.devFeature()); return @as(*tag.Type(), @fieldParentPtr("base", base)).updateErrorData(pt); }, @@ -1234,7 +1239,7 @@ pub const File = struct { assert(!base.post_prelink); switch (base.tag) { - inline .coff, .elf, .elf2, .wasm, .spirv => |tag| { + inline .coff, .elf, .elf2, .macho2, .wasm, .spirv => |tag| { dev.check(tag.devFeature()); return @as(*tag.Type(), @fieldParentPtr("base", base)).loadInput(input); }, @@ -1253,7 +1258,7 @@ pub const File = struct { } switch (base.tag) { - inline .elf2, .coff, .wasm, .c => |tag| { + inline .elf2, .coff, .macho2, .wasm, .c => |tag| { dev.check(tag.devFeature()); try @as(*tag.Type(), @fieldParentPtr("base", base)).prelink(base.comp.link_prog_node); }, @@ -1298,6 +1303,7 @@ pub const File = struct { elf, elf2, macho, + macho2, c, wasm, spirv, @@ -1311,6 +1317,7 @@ pub const File = struct { .elf => Elf, .elf2 => Elf2, .macho => MachO, + .macho2 => MachO2, .c => C, .wasm => Wasm, .spirv => Spirv, @@ -1324,7 +1331,7 @@ pub const File = struct { return switch (ofmt) { .coff => .coff, .elf => if (use_new_linker) .elf2 else .elf, - .macho => .macho, + .macho => if (use_new_linker) .macho2 else .macho, .wasm => .wasm, .plan9 => .plan9, .c => .c, @@ -1417,6 +1424,7 @@ pub const File = struct { pub const Elf = @import("link/Elf.zig"); pub const Elf2 = @import("link/Elf2.zig"); pub const MachO = @import("link/MachO.zig"); + pub const MachO2 = @import("link/MachO2.zig"); pub const Spirv = @import("link/Spirv.zig"); pub const Wasm = @import("link/Wasm.zig"); pub const Dwarf = @import("link/Dwarf.zig"); diff --git a/src/link/MachO2.zig b/src/link/MachO2.zig new file mode 100644 index 0000000000000000000000000000000000000000..c01ed3cf0b62d1a22555792236c32ffea0fb098d --- /dev/null +++ b/src/link/MachO2.zig @@ -0,0 +1,275 @@ +const MachO = @This(); + +base: link.File, +options: link.File.OpenOptions, +mf: MappedFile, +nodes: std.MultiArrayList(Node), + +const_prog_node: std.Progress.Node, +synth_prog_node: std.Progress.Node, +input_prog_node: std.Progress.Node, + +const Node = union(enum) { + macho, +}; +fn getNode(macho: *MachO, ni: MappedFile.Node.Index) Node { + return macho.nodes.get(@backingInt(ni)); +} + +pub fn open( + arena: std.mem.Allocator, + comp: *Compilation, + path: std.Build.Cache.Path, + options: link.File.OpenOptions, +) !*MachO { + return create(arena, comp, path, options); +} +pub fn createEmpty( + arena: std.mem.Allocator, + comp: *Compilation, + path: std.Build.Cache.Path, + options: link.File.OpenOptions, +) !*MachO { + return create(arena, comp, path, options); +} +fn create( + arena: std.mem.Allocator, + comp: *Compilation, + path: std.Build.Cache.Path, + options: link.File.OpenOptions, +) !*MachO { + const io = comp.io; + + const macho = try arena.create(MachO); + const file = try path.root_dir.handle.createFile(io, path.sub_path, .{ + .read = true, + .permissions = link.File.determinePermissions(comp.config.output_mode, comp.config.link_mode), + }); + + macho.* = .{ + .base = .{ + .tag = .macho2, + + .comp = comp, + .emit = path, + + .file = file, + .gc_sections = false, + .print_gc_sections = false, + .build_id = .none, + .allow_shlib_undefined = false, + .stack_size = 0, + }, + .options = options, + .mf = try .init(file, comp.gpa, io), + .nodes = .empty, + .const_prog_node = .none, + .synth_prog_node = .none, + .input_prog_node = .none, + }; + errdefer macho.deinit(); + + try macho.initInner(); + + return macho; +} +fn initInner(macho: *MachO) !void { + const comp = macho.base.comp; + const gpa = comp.gpa; + + try macho.nodes.append(gpa, .macho); // file root node + + @panic("TODO"); +} + +pub fn deinit(macho: *MachO) void { + const gpa = macho.base.comp.gpa; + macho.mf.deinit(gpa); + macho.nodes.deinit(gpa); + macho.* = undefined; +} + +pub fn startProgress(macho: *MachO, prog_node: std.Progress.Node) void { + prog_node.increaseEstimatedTotalItems(4); + macho.const_prog_node = prog_node.start("Constants", 0); + macho.synth_prog_node = prog_node.start("Synthetics", 0); + macho.input_prog_node = prog_node.start("Inputs", 0); + macho.mf.update_prog_node = prog_node.start("Relocations", macho.mf.updates.items.len); +} +pub fn endProgress(macho: *MachO) void { + macho.const_prog_node.end(); + macho.const_prog_node = .none; + macho.synth_prog_node.end(); + macho.synth_prog_node = .none; + macho.input_prog_node.end(); + macho.input_prog_node = .none; + macho.mf.update_prog_node.end(); + macho.mf.update_prog_node = .none; +} + +pub fn navSymbol(macho: *MachO, nav_id: InternPool.Nav.Index) link.Error!link.File.SymbolId { + _ = macho; + _ = nav_id; + @panic("TODO"); +} +pub fn uavSymbol( + macho: *MachO, + pt: Zcu.PerThread, + uav_val: InternPool.Index, + uav_align: InternPool.Alignment, +) link.Error!link.File.SymbolId { + _ = macho; + _ = pt; + _ = uav_val; + _ = uav_align; + @panic("TODO"); +} +pub fn relocSymAddr( + macho: *MachO, + reloc_info: link.File.RelocInfo, +) link.Error!void { + _ = macho; + _ = reloc_info; + @panic("TODO"); +} + +pub fn loadInput(macho: *MachO, input: link.Input) link.Error!void { + _ = macho; + _ = input; + @panic("TODO"); +} +pub fn prelink(macho: *MachO, prog_node: std.Progress.Node) link.Error!void { + _ = macho; + _ = prog_node; + @panic("TODO"); +} +pub fn flush( + macho: *MachO, + arena: std.mem.Allocator, + tid: Zcu.PerThread.Id, + prog_node: std.Progress.Node, +) link.Error!void { + _ = macho; + _ = arena; + _ = tid; + _ = prog_node; + @panic("TODO"); +} +pub fn updateErrorData(macho: *MachO, pt: Zcu.PerThread) link.Error!void { + _ = macho; + _ = pt; + @panic("TODO"); +} +pub fn idle(macho: *MachO, tid: Zcu.PerThread.Id) link.Error!bool { + _ = macho; + _ = tid; + @panic("TODO"); +} + +pub fn updateFunc( + macho: *MachO, + pt: Zcu.PerThread, + func_index: InternPool.Index, + mir: *const codegen.AnyMir, +) link.Error!void { + _ = macho; + _ = pt; + _ = func_index; + _ = mir; + @panic("TODO"); +} +pub fn updateNav(macho: *MachO, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) link.Error!void { + _ = macho; + _ = pt; + _ = nav_index; + @panic("TODO"); +} +pub fn updateExports( + macho: *MachO, + pt: Zcu.PerThread, + export_indices: []const Zcu.Export.Index, +) link.Error!void { + _ = macho; + _ = pt; + _ = export_indices; + @panic("TODO"); +} + +pub fn dump(macho: *MachO, w: *Io.Writer, tid: Zcu.PerThread.Id) !link.File.DumpResult { + if (macho.options.enable_link_snapshots) { + try macho.printNode(tid, w, .root, 0); + return .enabled; + } + return .disabled; +} +fn printNode( + macho: *MachO, + tid: Zcu.PerThread.Id, + w: *Io.Writer, + ni: MappedFile.Node.Index, + indent: usize, +) Io.Writer.Error!void { + const node = macho.getNode(ni); + try w.splatByteAll(' ', indent); + switch (node) { + .macho => try w.writeAll("macho"), + } + { + const mf_node = &macho.mf.nodes.items[@backingInt(ni)]; + const off, const size = mf_node.location().resolve(&macho.mf); + try w.print(" index={d} offset=0x{x} size=0x{x} align=0x{x} {t}{s}{s}{s}{s}{s}{s}\n", .{ + @backingInt(ni), + off, + size, + mf_node.flags.alignment.toByteUnits(), + mf_node.flags.position, + if (mf_node.flags.bubbles_moved) " bubbles_moved" else "", + if (mf_node.flags.resized) " moved" else "", + if (mf_node.flags.resized) " resized" else "", + if (mf_node.flags.enable_next_moved) " enable_next_moved" else "", + if (mf_node.flags.next_moved) " next_moved" else "", + if (mf_node.flags.has_content) " has_content" else "", + }); + } + if (ni.first(&macho.mf).unwrap()) |first_ni| { + // non-leaf, just print children + var child_ni = first_ni; + while (true) { + try macho.printNode(tid, w, child_ni, indent + 1); + child_ni = child_ni.next(&macho.mf).unwrap() orelse break; + } + return; + } + const file_loc = ni.fileLocation(&macho.mf, false); + var address = file_loc.offset; + if (file_loc.size == 0) { + try w.splatByteAll(' ', indent + 1); + try w.print("{x:0>8}\n", .{address}); + return; + } + const line_len = 0x10; + var line_it = std.mem.window( + u8, + macho.mf.memory_map.memory[@intCast(file_loc.offset)..][0..@intCast(file_loc.size)], + line_len, + line_len, + ); + while (line_it.next()) |line_bytes| : (address += line_len) { + try w.splatByteAll(' ', indent + 1); + try w.print("{x:0>8} ", .{address}); + for (line_bytes) |byte| try w.print("{x:0>2} ", .{byte}); + try w.splatByteAll(' ', 3 * (line_len - line_bytes.len) + 1); + for (line_bytes) |byte| try w.writeByte(if (std.ascii.isPrint(byte)) byte else '.'); + try w.writeByte('\n'); + } +} + +const std = @import("std"); +const Io = std.Io; + +const link = @import("../link.zig"); +const codegen = @import("../codegen.zig"); +const Compilation = @import("../Compilation.zig"); +const InternPool = @import("../InternPool.zig"); +const MappedFile = @import("MappedFile.zig"); +const Zcu = @import("../Zcu.zig"); diff --git a/src/target.zig b/src/target.zig index 81d39c22fa4d81129016bd58b04c97a6e3e7a1b6..5e36be5fd4f7c7ad799f619e775bcc611747003f 100644 --- a/src/target.zig +++ b/src/target.zig @@ -299,7 +299,7 @@ pub fn preferNewLinkerOverLld(target: *const std.Target) bool { /// Returns `true` if `ofmt` has two linker implementations, so `-fnew-linker` is meaningful. pub fn hasNewLinker(ofmt: std.Target.ObjectFormat) bool { return switch (ofmt) { - .elf => true, + .elf, .macho => true, else => false, }; } -- 2.54.0 From e7502d1474724335116751b87a2b0b0f78b43299 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Wed, 23 Sep 2026 21:14:09 +0100 Subject: [PATCH 04/10] link: don't call `updateErrorData` when using LLVM This function is only applicable when a self-hosted backend is being used to codegen the ZCU. --- src/link.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/link.zig b/src/link.zig index 62751df5dd2d619f09764d5dd3aa47ba24aa9840..c6d077c7772a51836a9d331f0120299745f8b542 100644 --- a/src/link.zig +++ b/src/link.zig @@ -964,6 +964,7 @@ pub const File = struct { } pub fn updateErrorData(base: *File, pt: Zcu.PerThread) Error!void { + if (base.comp.zcu.?.llvm_object != null) return; switch (base.tag) { else => {}, inline .elf2, .coff, .macho2 => |tag| { -- 2.54.0 From c1456afca0aaf0e9e2cdb29865766d8e017b7f06 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Wed, 23 Sep 2026 21:15:07 +0100 Subject: [PATCH 05/10] MappedFile: fix `Node.Index.hasMoved` Before, this would always report `false` for nodes with `bubbles_moved` set to `false`, but that is incorrect behavior; `bubbles_moved` is only relevant when considering *parent* nodes. --- src/link/MappedFile.zig | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/link/MappedFile.zig b/src/link/MappedFile.zig index 22b6f529accf3ba22eac77e9574677140260bfcd..5db69a96c00af9a0a6ad9fc0f987d5a37fab3b9e 100644 --- a/src/link/MappedFile.zig +++ b/src/link/MappedFile.zig @@ -495,12 +495,12 @@ pub const Node = extern struct { } pub fn hasMoved(ni: Node.Index, mf: *const MappedFile) bool { - var parent_ni = ni; - while (parent_ni != .root) { - const parent_node = parent_ni.get(mf); - if (!parent_node.flags.bubbles_moved) break; - if (parent_node.flags.moved) return true; - parent_ni = parent_node.parent.unwrap().?; + var cur_ni = ni; + while (cur_ni != .root) { + if (cur_ni.get(mf).flags.moved) return true; + const parent_ni = cur_ni.parent(mf).unwrap().?; + if (!parent_ni.get(mf).flags.bubbles_moved) break; + cur_ni = parent_ni; } return false; } -- 2.54.0 From c1ce1a2da70b1f3667df16212ace4c227aeaced1 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Thu, 24 Sep 2026 00:12:25 +0100 Subject: [PATCH 06/10] compiler: move darwin libSystem resolution to the frontend Native libSystem detection happens via `std.zig.LibCInstallation`. In that type, the `crt_dir` field is now mandatory on Darwin (bringing Darwin into alignment with other OSes): it holds the directory containing `libSystem.dylib`/`libSystem.tbd`. (This is also the directory containing `crt1.o`, not that we need that.) In the compiler, a new field is added to `link.Input` to represent a tbd file. These are essentially text-based representations of a dylib's ABI, so the `tbd` field contains very similar information to the `dso` field. The handling for non-native libSystem is trivial compared to most cross libs, because we don't actually need to build anything (the linker can directly consume the tbd file we ship!), so the logic in `Compilation` just directly queues a single `.load_tbd` prelink task. Frameworks are not yet integrated with the frontend link input logic. The legacy Mach-O linker is now at least vaguely integrated with prelink: instead of building its own list of link inputs, it implements `loadInput` by just adding the `link.Input` to an `ArrayList` which it consumes in `flush`. This just helped to avoid duplicating code with the frontend. --- lib/std/zig/LibCInstallation.zig | 20 ++-- src/Compilation.zig | 12 +- src/link.zig | 169 +++++++++++++++++++-------- src/link/Coff.zig | 1 + src/link/Elf.zig | 2 + src/link/Elf2.zig | 3 +- src/link/Lld.zig | 6 +- src/link/MachO.zig | 192 +++++-------------------------- src/link/MachO/relocatable.zig | 75 +++++------- src/link/Wasm.zig | 2 + src/target.zig | 2 +- 11 files changed, 211 insertions(+), 273 deletions(-) diff --git a/lib/std/zig/LibCInstallation.zig b/lib/std/zig/LibCInstallation.zig index fe44e3a307ccaa56671b2dbfe50b25afc4f4f935..a96bbaea26e8cdabad6aba221584916ee4c7d9cf 100644 --- a/lib/std/zig/LibCInstallation.zig +++ b/lib/std/zig/LibCInstallation.zig @@ -96,6 +96,10 @@ pub fn parse(allocator: Allocator, io: Io, libc_file: []const u8, target: *const log.err("sys_include_dir may not be empty", .{}); return error.ParseError; } + if (self.crt_dir == null) { + log.err("crt_dir may not be empty", .{}); + return error.ParseError; + } const os_tag = target.os.tag; @@ -103,12 +107,6 @@ pub fn parse(allocator: Allocator, io: Io, libc_file: []const u8, target: *const log.err("cc_dir may not be empty for {s}", .{@tagName(os_tag)}); return error.ParseError; } - - if (self.crt_dir == null and !target.os.tag.isDarwin()) { - log.err("crt_dir may not be empty for {s}", .{@tagName(os_tag)}); - return error.ParseError; - } - if (self.msvc_lib_dir == null and os_tag == .windows and (target.abi == .msvc or target.abi == .itanium)) { log.err("msvc_lib_dir may not be empty for {s}-{s}", .{ @tagName(os_tag), @@ -153,7 +151,6 @@ pub fn render(self: LibCInstallation, out: *std.Io.Writer) !void { \\ \\# The directory that contains `crt1.o` or `crt2.o`. \\# On POSIX, can be found with `cc -print-file-name=crt1.o`. - \\# Not needed when targeting macOS. \\crt_dir={s} \\ \\# The directory that contains `vcruntime.lib`. @@ -193,12 +190,9 @@ pub fn findNative(gpa: Allocator, io: Io, args: FindNativeOptions) FindError!Lib return error.DarwinSdkNotFound; defer gpa.free(sdk); - self.include_dir = try fs.path.join(gpa, &.{ - sdk, "usr/include", - }); - self.sys_include_dir = try fs.path.join(gpa, &.{ - sdk, "usr/include", - }); + self.include_dir = try fs.path.join(gpa, &.{ sdk, "usr/include" }); + self.sys_include_dir = try fs.path.join(gpa, &.{ sdk, "usr/include" }); + self.crt_dir = try fs.path.join(gpa, &.{ sdk, "usr/lib" }); return self; } else if (is_windows) { const sdk = std.zig.WindowsSdk.find(gpa, io, args.target.cpu.arch, args.environ_map) catch |err| switch (err) { diff --git a/src/Compilation.zig b/src/Compilation.zig index d73e142ffdad6067e611eb702072b4e77f6ef143..7bdc84b74d4fd56c2dc21be583691de40de3989f 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -2429,11 +2429,7 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, // If linking against host libc installation, instead queue up jobs // for loading those files in the linker. if (comp.config.link_libc and is_exe_or_dyn_lib) { - // If the "is darwin" check is moved below the libc_installation check below, - // error.LibCInstallationMissingCrtDir is returned from lci.resolveCrtPaths(). - if (target.isDarwinLibC()) { - // TODO delete logic from MachO flush() and queue up tasks here instead. - } else if (comp.libc_installation) |lci| { + if (comp.libc_installation) |lci| { const basenames = LibCInstallation.CrtBasenames.get(.{ .target = target, .link_libc = comp.config.link_libc, @@ -2456,6 +2452,12 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, } // Loads the libraries provided by `target_util.libcFullLinkFlags(target)`. comp.oneshot_prelink_tasks.appendAssumeCapacity(.load_host_libc); + } else if (target.isDarwinLibC()) { + try comp.oneshot_prelink_tasks.append(gpa, .{ .load_tbd = .{ + .root_dir = comp.dirs.zig_lib, + .sub_path = "libc" ++ fs.path.sep_str ++ "darwin" ++ fs.path.sep_str ++ "libSystem.tbd", + } }); + // TODO delete logic from MachO flush() and queue up tasks here instead. } else if (target.isMuslLibC()) { if (!std.zig.target.canBuildLibC(target)) return diag.fail(.cross_libc_unavailable); diff --git a/src/link.zig b/src/link.zig index c6d077c7772a51836a9d331f0120299745f8b542..247ca5d4b9250e2dd94408d34d2e12786483376a 100644 --- a/src/link.zig +++ b/src/link.zig @@ -1236,15 +1236,15 @@ pub const File = struct { } pub fn loadInput(base: *File, input: Input) anyerror!void { - if (base.tag == .lld) return; - assert(!base.post_prelink); - + if (base.tag != .lld) { + assert(!base.post_prelink); + } switch (base.tag) { - inline .coff, .elf, .elf2, .macho2, .wasm, .spirv => |tag| { + inline else => |tag| { dev.check(tag.devFeature()); return @as(*tag.Type(), @fieldParentPtr("base", base)).loadInput(input); }, - else => {}, + .c, .spork8, .plan9, .lld => {}, } } @@ -1450,6 +1450,7 @@ pub const PrelinkTask = union(enum) { /// Tells the linker to load a shared library, possibly one that is a /// GNU ld script. load_dso: Path, + load_tbd: Path, }; pub const ZcuTask = union(enum) { /// Sent once per update, as the very first `ZcuTask` in the update. Indicates that all per-file @@ -1504,6 +1505,7 @@ pub fn doPrelinkTask(comp: *Compilation, task: PrelinkTask) void { error.AlreadyReported => return, // error reported via diags else => |e| switch (input) { .dso => |dso| diags.addParseError(dso.path, "failed to parse shared library: {t}", .{e}), + .tbd => |tbd| diags.addParseError(tbd.path, "failed to parse tbd: {t}", .{e}), .object => |obj| diags.addParseError(obj.path, "failed to parse object: {t}", .{e}), .archive => |obj| diags.addParseError(obj.path, "failed to parse archive: {t}", .{e}), .res => |res| diags.addParseError(res.path, "failed to parse Windows resource: {t}", .{e}), @@ -1525,34 +1527,72 @@ pub fn doPrelinkTask(comp: *Compilation, task: PrelinkTask) void { assert(mem.startsWith(u8, flag, "-l")); const lib_name = flag["-l".len..]; switch (comp.config.link_mode) { - .dynamic => { - const dso_path = Path.initCwd( - std.fmt.allocPrint(comp.arena, "{s}" ++ sep ++ "{s}{s}{s}", .{ - crt_dir, target.libPrefix(), lib_name, target.dynamicLibSuffix(), - }) catch return diags.setAllocFailure(), - ); - base.openLoadDso(dso_path, .{ + .dynamic => loaded: { + if (target.os.tag.isDarwin()) { + // Prefer .tbd over .dylib. + const tbd_path: Path = .initCwd(std.fmt.allocPrint( + comp.arena, + "{s}" ++ sep ++ "{s}{s}.tbd", + .{ crt_dir, target.libPrefix(), lib_name }, + ) catch return diags.setAllocFailure()); + if (tbd_path.root_dir.handle.openFile(io, tbd_path.sub_path, .{})) |file| { + errdefer file.close(io); + base.loadInput(.{ .tbd = .{ + .path = tbd_path, + .file = file, + .needed = false, + .weak = false, + .reexport = false, + } }) catch |err| switch (err) { + error.AlreadyReported => return, + error.Canceled => io.recancel(), + else => |e| diags.addParseError(tbd_path, "failed to parse tbd: {t}", .{e}), + }; + break :loaded; + } else |err| switch (err) { + error.FileNotFound => {}, + error.Canceled => io.recancel(), + else => |e| diags.addParseError(tbd_path, "failed to parse tbd: {t}", .{e}), + } + } + + // Try dynamic library. + const dso_path: Path = .initCwd(std.fmt.allocPrint( + comp.arena, + "{s}" ++ sep ++ "{s}{s}{s}", + .{ crt_dir, target.libPrefix(), lib_name, target.dynamicLibSuffix() }, + ) catch return diags.setAllocFailure()); + if (base.openLoadDso(dso_path, .{ .preferred_mode = .dynamic, .search_strategy = .paths_first, - }) catch |err| switch (err) { - error.FileNotFound => { - // Also try static. - const archive_path = Path.initCwd( - std.fmt.allocPrint(comp.arena, "{s}" ++ sep ++ "{s}{s}{s}", .{ - crt_dir, target.libPrefix(), lib_name, target.staticLibSuffix(), - }) catch return diags.setAllocFailure(), - ); - base.openLoadArchiveQuery(archive_path, .{ - .preferred_mode = .dynamic, - .search_strategy = .paths_first, - }) catch |archive_err| switch (archive_err) { - error.AlreadyReported => return, // error reported via diags - else => |e| diags.addParseError(dso_path, "failed to parse archive {f}: {s}", .{ archive_path, @errorName(e) }), - }; - }, - error.AlreadyReported => return, // error reported via diags + })) { + break :loaded; + } else |err| switch (err) { + error.FileNotFound => {}, + error.AlreadyReported => return, + error.Canceled => io.recancel(), else => |e| diags.addParseError(dso_path, "failed to parse shared library: {s}", .{@errorName(e)}), - }; + } + + // Also try static. + const archive_path = Path.initCwd( + std.fmt.allocPrint(comp.arena, "{s}" ++ sep ++ "{s}{s}{s}", .{ + crt_dir, target.libPrefix(), lib_name, target.staticLibSuffix(), + }) catch return diags.setAllocFailure(), + ); + if (base.openLoadArchiveQuery(archive_path, .{ + .preferred_mode = .dynamic, + .search_strategy = .paths_first, + })) { + break :loaded; + } else |archive_err| switch (archive_err) { + error.FileNotFound => {}, + error.AlreadyReported => return, + error.Canceled => io.recancel(), + else => |e| diags.addParseError(archive_path, "failed to parse archive {f}: {s}", .{ archive_path, @errorName(e) }), + } + + diags.addError("failed to find library '{s}'", .{lib_name}); }, .static => { const path = Path.initCwd( @@ -1647,6 +1687,28 @@ pub fn doPrelinkTask(comp: *Compilation, task: PrelinkTask) void { else => |e| diags.addParseError(path, "failed to parse shared library: {s}", .{@errorName(e)}), }; }, + .load_tbd => |path| { + const prog_node = comp.link_prog_node.start("Parse Shared Library Stub", 0); + defer prog_node.end(); + if (path.root_dir.handle.openFile(io, path.sub_path, .{})) |file| { + errdefer file.close(io); + base.loadInput(.{ .tbd = .{ + .path = path, + .file = file, + .needed = false, + .weak = false, + .reexport = false, + } }) catch |err| switch (err) { + error.AlreadyReported => return, + error.Canceled => io.recancel(), + else => |e| diags.addParseError(path, "failed to parse tbd: {t}", .{e}), + }; + } else |err| switch (err) { + error.FileNotFound => {}, + error.Canceled => io.recancel(), + else => |e| diags.addParseError(path, "failed to parse tbd: {t}", .{e}), + } + }, } } pub fn doZcuTask(comp: *Compilation, tid: Zcu.PerThread.Id, task: ZcuTask) void { @@ -1873,6 +1935,8 @@ pub const Input = union(enum) { /// May not be a GNU ld script. Those are resolved when converting from /// `UnresolvedInput` to `Input` values. dso: Dso, + /// Only possible when targeting Darwin. + tbd: Tbd, pub const Object = struct { path: Path, @@ -1897,24 +1961,32 @@ pub const Input = union(enum) { pub const FallbackSoname = enum { basename, full_path }; }; + pub const Tbd = struct { + path: Path, + file: Io.File, + needed: bool, + weak: bool, + reexport: bool, + }; + pub fn path(input: Input) Path { return switch (input) { .object, .archive => |obj| obj.path, - inline .res, .dso => |x| x.path, + inline .res, .dso, .tbd => |x| x.path, }; } pub fn pathAndFile(input: Input) struct { Path, Io.File } { return switch (input) { .object, .archive => |obj| .{ obj.path, obj.file }, - inline .res, .dso => |x| .{ x.path, x.file }, + inline .res, .dso, .tbd => |x| .{ x.path, x.file }, }; } pub fn taskName(input: Input) []const u8 { return switch (input) { .object, .archive => |obj| obj.path.basename(), - inline .res, .dso => |x| x.path.basename(), + inline .res, .dso, .tbd => |x| x.path.basename(), }; } }; @@ -1947,6 +2019,15 @@ pub fn hashInputs(man: *Cache.Manifest, link_inputs: []const Input) !void { man.hash.add(dso.reexport); man.hash.add(dso.fallback_soname); }, + .tbd => |tbd| { + _ = try man.addInputPath(tbd.path, .{ + .handle = .{ .file = tbd.file }, + .request_handle = true, + }); + man.hash.add(tbd.needed); + man.hash.add(tbd.weak); + man.hash.add(tbd.reexport); + }, } } } @@ -2236,16 +2317,14 @@ fn resolveLibInput( else => |e| fatal("searching for tbd library {qf}: {t}", .{ test_path, e }), }; errdefer file.close(io); - return finishResolveLibInput( - io, - resolved_inputs, - archive_dedup, - test_path, - file, - link_mode, - name_query.query, - .basename, - ); + resolved_inputs.appendAssumeCapacity(.{ .tbd = .{ + .path = test_path, + .file = file, + .needed = name_query.query.needed, + .weak = name_query.query.weak, + .reexport = name_query.query.reexport, + } }); + return .ok; } { @@ -2700,7 +2779,7 @@ pub fn anyObjectInputs(inputs: []const Input) bool { pub fn countObjectInputs(inputs: []const Input) usize { var count: usize = 0; for (inputs) |input| switch (input) { - .dso => continue, + .dso, .tbd => continue, .res, .object, .archive => count += 1, }; return count; @@ -2710,7 +2789,7 @@ pub fn countObjectInputs(inputs: []const Input) usize { pub fn firstObjectInput(inputs: []const Input) ?Input.Object { for (inputs) |input| switch (input) { .object, .archive => |obj| return obj, - .res, .dso => continue, + .res, .dso, .tbd => continue, }; return null; } diff --git a/src/link/Coff.zig b/src/link/Coff.zig index dbdea4966de0b5ba91b7e2b026a7b80c419d5854..754c3ad9a830078489427c9794455642527b7f20 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -3912,6 +3912,7 @@ pub fn loadInput(coff: *Coff, input: link.Input) link.Error!void { coff.loadDll(dso.path, &fr) catch |err| return coff.failLoadInput(err, &fr, dso.path); }, + .tbd => unreachable, } } diff --git a/src/link/Elf.zig b/src/link/Elf.zig index 3cbcc53361033b74e8ea1c69189f6909d3be7ad6..5a3aa2648a58c61e59be964f0058525efa650fd9 100644 --- a/src/link/Elf.zig +++ b/src/link/Elf.zig @@ -722,6 +722,7 @@ pub fn loadInput(self: *Elf, input: link.Input) !void { const argv = &self.dump_argv_list; switch (input) { .res => unreachable, + .tbd => unreachable, .object, .archive => |obj| try argv.append(gpa, try obj.path.toString(comp.arena)), .dso => |dso| try argv.append(gpa, try dso.path.toString(comp.arena)), } @@ -729,6 +730,7 @@ pub fn loadInput(self: *Elf, input: link.Input) !void { switch (input) { .res => unreachable, + .tbd => unreachable, .object => |obj| try parseObject(self, obj), .archive => |obj| if (self.base.isStaticLib()) { // Ignore static library inputs when generating a static library. diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index 5967cfe48e03369cd10f620035179e99b4f41904..26d3d346f62289221cf549306e04f98b74c0809e 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -7146,7 +7146,6 @@ fn loadInputInner(elf: *Elf, input: link.Input) (Error || error{BadMagic})!void }, }; }, - .res => unreachable, .dso => |dso| { try elf.needed.ensureUnusedCapacity(elf.base.comp.gpa, 1); var fr = dso.file.reader(io, &buf); @@ -7163,6 +7162,8 @@ fn loadInputInner(elf: *Elf, input: link.Input) (Error || error{BadMagic})!void }, }; }, + .res => unreachable, + .tbd => unreachable, } } fn loadArchive(elf: *Elf, path: std.Build.Cache.Path, fr: *Io.File.Reader) (LoadParseInputError || error{BadMagic})!void { diff --git a/src/link/Lld.zig b/src/link/Lld.zig index c56557dfb02336a36ab88ec32170b705a64d5e53..64a9aeeebf2e94889cd258ba0edc877a4518c1f4 100644 --- a/src/link/Lld.zig +++ b/src/link/Lld.zig @@ -309,7 +309,7 @@ fn linkAsArchive(lld: *Lld, arena: Allocator) link.Error!void { try object_files.ensureUnusedCapacity(arena, comp.link_inputs.len); for (comp.link_inputs) |input| switch (input) { - .dso, .archive => {}, // static archives should not contain shared libraries or other static archives + .dso, .tbd, .archive => {}, // static archives should not contain shared libraries or other static archives .res, .object => { const path = try input.path().toStringZ(arena); object_files.appendAssumeCapacity(path); @@ -561,6 +561,7 @@ fn coffLink(lld: *Lld, arena: Allocator) !void { argv.appendAssumeCapacity(try obj.path.toString(arena)); } }, + .tbd => unreachable, }; for (comp.c_objects.items) |c_object| { @@ -1102,6 +1103,7 @@ fn elfLink(lld: *Lld, arena: Allocator) !void { for (base.comp.link_inputs) |link_input| switch (link_input) { .res => unreachable, // Windows-only + .tbd => unreachable, // Darwin-only .dso => continue, .object, .archive => |obj| { if (obj.must_link and !whole_archive) { @@ -1163,6 +1165,7 @@ fn elfLink(lld: *Lld, arena: Allocator) !void { for (base.comp.link_inputs) |link_input| switch (link_input) { .res => unreachable, // Windows-only + .tbd => unreachable, // Darwin-only .object, .archive => continue, .dso => |dso| { const lib_as_needed = !dso.needed; @@ -1596,6 +1599,7 @@ fn wasmLink(lld: *Lld, arena: Allocator) !void { .dso => |dso| { try argv.append(try dso.path.toString(arena)); }, + .tbd => unreachable, .res => unreachable, }; if (whole_archive) { diff --git a/src/link/MachO.zig b/src/link/MachO.zig index dbabc88a512ff37af5fd0caa9ece4562b8e06378..db7d1733859235990e5cc60fcd840b5fa8cd30dc 100644 --- a/src/link/MachO.zig +++ b/src/link/MachO.zig @@ -4,6 +4,8 @@ pub const Relocation = @import("MachO/Relocation.zig"); base: link.File, +all_inputs: std.ArrayList(link.Input), + rpath_list: []const []const u8, /// Debug symbols bundle (or dSym). @@ -190,6 +192,7 @@ pub fn createEmpty( .file = null, .build_id = options.build_id, }, + .all_inputs = .empty, .rpath_list = options.rpath_list, .pagezero_size = options.pagezero_size, .headerpad_size = options.headerpad_size, @@ -271,6 +274,8 @@ pub fn deinit(self: *MachO) void { const gpa = comp.gpa; const io = comp.io; + self.all_inputs.deinit(gpa); + if (self.d_sym) |*d_sym| { d_sym.deinit(); } @@ -334,6 +339,11 @@ pub fn deinit(self: *MachO) void { self.thunks.deinit(gpa); } +pub fn loadInput(self: *MachO, input: link.Input) link.Error!void { + const gpa = self.base.comp.gpa; + try self.all_inputs.append(gpa, input); +} + pub fn flush( self: *MachO, arena: Allocator, @@ -343,6 +353,8 @@ pub fn flush( const tracy = trace(@src()); defer tracy.end(); + _ = arena; + const comp = self.base.comp; const gpa = comp.gpa; const io = comp.io; @@ -351,148 +363,47 @@ pub fn flush( const sub_prog_node = prog_node.start("MachO Flush", 0); defer sub_prog_node.end(); - const zcu_obj_path: ?Path = p: { - const zcu = comp.zcu orelse break :p null; - const llvm_object = zcu.llvm_object orelse break :p null; - break :p try comp.resolveEmitPathFlush(arena, .temp, llvm_object.out_bin_basename); - }; - // --verbose-link if (comp.verbose_link) try self.dumpArgv(comp); if (self.getZigObject()) |zo| try zo.flush(self, tid); - if (self.base.isStaticLib()) return relocatable.flushStaticLib(self, comp, zcu_obj_path); - if (self.base.isObject()) return relocatable.flushObject(self, comp, zcu_obj_path); + if (self.base.isStaticLib()) return relocatable.flushStaticLib(self, comp); + if (self.base.isObject()) return relocatable.flushObject(self, comp); - var positionals = std.array_list.Managed(link.Input).init(gpa); - defer positionals.deinit(); - - try positionals.ensureUnusedCapacity(comp.link_inputs.len); - - for (comp.link_inputs) |link_input| switch (link_input) { - .dso => continue, // handled below - .object, .archive => positionals.appendAssumeCapacity(link_input), - .res => unreachable, - }; - - // This is a set of object files emitted by clang in a single `build-exe` invocation. - // For instance, the implicit `a.o` as compiled by `zig build-exe a.c` will end up - // in this set. - try positionals.ensureUnusedCapacity(comp.c_objects.items.len); - for (comp.c_objects.items) |c_object| { - positionals.appendAssumeCapacity(try link.openObjectInput(io, diags, c_object.status.success.object_path)); + for (self.all_inputs.items) |input| { + self.classifyInputFile(input) catch |err| + diags.addParseError(input.path(), "failed to read input file: {t}", .{err}); } - if (zcu_obj_path) |path| try positionals.append(try link.openObjectInput(io, diags, path)); - - if (comp.config.any_sanitize_thread) { - try positionals.append(try link.openObjectInput(io, diags, comp.tsan_lib.?.full_object_path)); - } - - if (comp.config.any_fuzz) { - try positionals.append(try link.openArchiveInput(io, diags, comp.fuzzer_lib.?.full_object_path, false, false)); - } - - if (comp.ubsan_rt_lib) |crt_file| { - const path = crt_file.full_object_path; - self.classifyInputFile(try link.openArchiveInput(io, diags, path, false, false)) catch |err| - diags.addParseError(path, "failed to parse archive: {s}", .{@errorName(err)}); - } else if (comp.ubsan_rt_obj) |crt_file| { - const path = crt_file.full_object_path; - self.classifyInputFile(try link.openObjectInput(io, diags, path)) catch |err| - diags.addParseError(path, "failed to parse archive: {s}", .{@errorName(err)}); - } - - for (positionals.items) |link_input| { - self.classifyInputFile(link_input) catch |err| - diags.addParseError(link_input.path(), "failed to read input file: {t}", .{err}); - } - - var system_libs = std.array_list.Managed(SystemLib).init(gpa); - defer system_libs.deinit(); - // frameworks - try system_libs.ensureUnusedCapacity(self.frameworks.len); - for (self.frameworks) |info| { - system_libs.appendAssumeCapacity(.{ - .needed = info.needed, - .weak = info.weak, - .path = info.path, - }); - } - - // libc++ dep - if (comp.config.link_libcpp) { - try system_libs.ensureUnusedCapacity(2); - system_libs.appendAssumeCapacity(.{ .path = comp.libcxxabi_static_lib.?.full_object_path }); - system_libs.appendAssumeCapacity(.{ .path = comp.libcxx_static_lib.?.full_object_path }); - } - - const is_exe_or_dyn_lib = comp.config.output_mode == .Exe or - (comp.config.output_mode == .Lib and comp.config.link_mode == .dynamic); - - if (comp.config.link_libc and is_exe_or_dyn_lib) { - if (comp.zigc_static_lib) |zigc| { - const path = zigc.full_object_path; - self.classifyInputFile(try link.openArchiveInput(io, diags, path, false, false)) catch |err| - diags.addParseError(path, "failed to parse archive: {s}", .{@errorName(err)}); - } - } - - // libc/libSystem dep - self.resolveLibSystem(arena, comp, &system_libs) catch |err| switch (err) { - error.MissingLibSystem => {}, // already reported - else => |e| return diags.fail("failed to resolve libSystem: {s}", .{@errorName(e)}), - }; - - for (comp.link_inputs) |link_input| switch (link_input) { - .object, .archive => continue, - .res => unreachable, - .dso => { - self.classifyInputFile(link_input) catch |err| - diags.addParseError(link_input.path(), "failed to parse input file: {t}", .{err}); - }, - }; - - for (system_libs.items) |lib| { - switch (Compilation.classifyFileExt(lib.path.sub_path)) { + for (self.frameworks) |framework| { + switch (Compilation.classifyFileExt(framework.path.sub_path)) { .shared_library => { - const dso_input = try link.openDsoInput(io, diags, lib.path, lib.needed, lib.weak, lib.reexport); + const dso_input = try link.openDsoInput(io, diags, framework.path, framework.needed, framework.weak, false); self.classifyInputFile(dso_input) catch |err| - diags.addParseError(lib.path, "failed to parse input file: {s}", .{@errorName(err)}); + diags.addParseError(framework.path, "failed to parse input file: {s}", .{@errorName(err)}); }, .static_library => { - const archive_input = try link.openArchiveInput(io, diags, lib.path, lib.must_link, lib.hidden); + const archive_input = try link.openArchiveInput(io, diags, framework.path, false, false); self.classifyInputFile(archive_input) catch |err| - diags.addParseError(lib.path, "failed to parse input file: {s}", .{@errorName(err)}); + diags.addParseError(framework.path, "failed to parse input file: {s}", .{@errorName(err)}); }, else => { dso: { - const dso_input = link.openDsoInput(io, diags, lib.path, lib.needed, lib.weak, lib.reexport) catch break :dso; + const dso_input = link.openDsoInput(io, diags, framework.path, framework.needed, framework.weak, false) catch break :dso; self.classifyInputFile(dso_input) catch break :dso; continue; } ar: { - const archive_input = link.openArchiveInput(io, diags, lib.path, lib.must_link, lib.hidden) catch break :ar; + const archive_input = link.openArchiveInput(io, diags, framework.path, false, false) catch break :ar; self.classifyInputFile(archive_input) catch break :ar; continue; } - diags.addParseError(lib.path, "unknown file extension", .{}); + diags.addParseError(framework.path, "unknown file extension", .{}); }, } } - // Finally, link against compiler_rt. - if (comp.compiler_rt_lib) |crt_file| { - const path = crt_file.full_object_path; - self.classifyInputFile(try link.openArchiveInput(io, diags, path, false, false)) catch |err| - diags.addParseError(path, "failed to parse archive: {s}", .{@errorName(err)}); - } else if (comp.compiler_rt_obj) |crt_file| { - const path = crt_file.full_object_path; - self.classifyInputFile(try link.openObjectInput(io, diags, path)) catch |err| - diags.addParseError(path, "failed to parse archive: {s}", .{@errorName(err)}); - } - try self.parseInputFiles(); self.parseDependentDylibs() catch |err| { switch (err) { @@ -666,9 +577,8 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void { for (comp.link_inputs) |link_input| switch (link_input) { .object, .archive => |obj| try argv.append(try obj.path.toString(arena)), .res => |res| try argv.append(try res.path.toString(arena)), - .dso => |dso| { - try argv.append(try dso.path.toString(arena)); - }, + .dso => |dso| try argv.append(try dso.path.toString(arena)), + .tbd => |tbd| try argv.append(try tbd.path.toString(arena)), }; for (comp.c_objects.items) |c_object| { @@ -754,7 +664,7 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void { } for (comp.link_inputs) |link_input| switch (link_input) { - .dso => continue, // handled below + .dso, .tbd => continue, // handled below .res => unreachable, // windows only .object, .archive => |obj| { if (obj.must_link) try argv.append("-force_load"); // TODO: verify this @@ -788,7 +698,7 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void { for (comp.link_inputs) |link_input| switch (link_input) { .object, .archive => continue, // handled above .res => unreachable, // windows only - .dso => |dso| { + inline .dso, .tbd => |dso| { try argv.ensureUnusedCapacity(2); if (dso.needed) { argv.appendAssumeCapacity("-needed-l"); @@ -836,46 +746,6 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void { try Compilation.dumpArgv(io, argv.items); } -/// TODO delete this, libsystem must be resolved when setting up the compilation pipeline -pub fn resolveLibSystem( - self: *MachO, - arena: Allocator, - comp: *Compilation, - out_libs: anytype, -) !void { - const io = comp.io; - const diags = &comp.link_diags; - - var test_path = std.array_list.Managed(u8).init(arena); - var checked_paths = std.array_list.Managed([]const u8).init(arena); - - success: { - if (self.sdk_layout) |sdk_layout| switch (sdk_layout) { - .sdk => { - const dir = try fs.path.join(arena, &.{ comp.sysroot.?, "usr", "lib" }); - if (try accessLibPath(arena, io, &test_path, &checked_paths, dir, "System")) break :success; - }, - .vendored => { - const dir = try comp.dirs.zig_lib.join(arena, &.{ "libc", "darwin" }); - if (try accessLibPath(arena, io, &test_path, &checked_paths, dir, "System")) break :success; - }, - }; - - for (self.lib_directories) |directory| { - if (try accessLibPath(arena, io, &test_path, &checked_paths, directory.path orelse ".", "System")) break :success; - } - - diags.addMissingLibraryError(checked_paths.items, "unable to find libSystem system library", .{}); - return error.MissingLibSystem; - } - - const libsystem_path = Path.initCwd(try arena.dupe(u8, test_path.items)); - try out_libs.append(.{ - .needed = true, - .path = libsystem_path, - }); -} - pub fn classifyInputFile(self: *MachO, input: link.Input) !void { const tracy = trace(@src()); defer tracy.end(); @@ -4497,7 +4367,7 @@ const SystemLib = struct { .must_link = obj.must_link, .hidden = obj.hidden, }, - .dso => |dso| .{ + inline .dso, .tbd => |dso| .{ .path = dso.path, .needed = dso.needed, .weak = dso.weak, diff --git a/src/link/MachO/relocatable.zig b/src/link/MachO/relocatable.zig index 38420414382928237d93b16b5c8648e4c91fb6f5..1b679cc9058932f34ea8098032ef75af49295059 100644 --- a/src/link/MachO/relocatable.zig +++ b/src/link/MachO/relocatable.zig @@ -1,25 +1,25 @@ -pub fn flushObject(macho_file: *MachO, comp: *Compilation, module_obj_path: ?Path) link.Error!void { - const gpa = comp.gpa; +pub fn flushObject(macho_file: *MachO, comp: *Compilation) link.Error!void { const io = comp.io; const diags = &comp.link_diags; - // TODO: "positional arguments" is a CLI concept, not a linker concept. Delete this unnecessary array list. - var positionals = std.array_list.Managed(link.Input).init(gpa); - defer positionals.deinit(); - try positionals.ensureUnusedCapacity(comp.link_inputs.len); - positionals.appendSliceAssumeCapacity(comp.link_inputs); - - for (comp.c_objects.items) |c_object| { - try positionals.append(try link.openObjectInput(io, diags, c_object.status.success.object_path)); - } - - if (module_obj_path) |path| try positionals.append(try link.openObjectInput(io, diags, path)); - - if (macho_file.getZigObject() == null and positionals.items.len == 1) { + one_input: { + if (macho_file.getZigObject() != null) { + break :one_input; + } + var only_input: ?link.Input = null; + for (macho_file.all_inputs.items) |input| switch (input) { + .res => unreachable, + .dso, .tbd => {}, + .object, .archive => if (only_input == null) { + only_input = input; + } else { + break :one_input; + }, + }; // Instead of invoking a full-blown `-r` mode on the input which sadly will strip all // debug info segments/sections (this is apparently by design by Apple), we copy // the *only* input file over. - const path = positionals.items[0].path(); + const path = (only_input orelse break :one_input).path(); const in_file = path.root_dir.handle.openFile(io, path.sub_path, .{}) catch |err| return diags.fail("failed to open {f}: {s}", .{ path, @errorName(err) }); const stat = in_file.stat(io) catch |err| @@ -29,10 +29,12 @@ pub fn flushObject(macho_file: *MachO, comp: *Compilation, module_obj_path: ?Pat return; } - for (positionals.items) |link_input| { - macho_file.classifyInputFile(link_input) catch |err| - diags.addParseError(link_input.path(), "failed to read input file: {t}", .{err}); - } + for (macho_file.all_inputs.items) |input| switch (input) { + .res => unreachable, + .dso, .tbd => {}, + .object, .archive => macho_file.classifyInputFile(input) catch |err| + diags.addParseError(input.path(), "failed to read input file: {t}", .{err}), + }; if (diags.hasErrors()) return error.AlreadyReported; @@ -75,35 +77,16 @@ pub fn flushObject(macho_file: *MachO, comp: *Compilation, module_obj_path: ?Pat try writeHeader(macho_file, ncmds, sizeofcmds); } -pub fn flushStaticLib(macho_file: *MachO, comp: *Compilation, module_obj_path: ?Path) link.Error!void { +pub fn flushStaticLib(macho_file: *MachO, comp: *Compilation) link.Error!void { const gpa = comp.gpa; - const io = comp.io; const diags = &macho_file.base.comp.link_diags; - var positionals = std.array_list.Managed(link.Input).init(gpa); - defer positionals.deinit(); - - try positionals.ensureUnusedCapacity(comp.link_inputs.len); - positionals.appendSliceAssumeCapacity(comp.link_inputs); - - for (comp.c_objects.items) |c_object| { - try positionals.append(try link.openObjectInput(io, diags, c_object.status.success.object_path)); - } - - if (module_obj_path) |path| try positionals.append(try link.openObjectInput(io, diags, path)); - - if (comp.compiler_rt_strat == .obj) { - try positionals.append(try link.openObjectInput(io, diags, comp.compiler_rt_obj.?.full_object_path)); - } - - if (comp.ubsan_rt_strat == .obj) { - try positionals.append(try link.openObjectInput(io, diags, comp.ubsan_rt_obj.?.full_object_path)); - } - - for (positionals.items) |link_input| { - macho_file.classifyInputFile(link_input) catch |err| - diags.addParseError(link_input.path(), "failed to read input file: {t}", .{err}); - } + for (macho_file.all_inputs.items) |input| switch (input) { + .res => unreachable, + .dso, .tbd => {}, + .object, .archive => macho_file.classifyInputFile(input) catch |err| + diags.addParseError(input.path(), "failed to read input file: {t}", .{err}), + }; if (diags.hasErrors()) return error.AlreadyReported; diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index e83fa056802e10d9c961ebdd3f77ea9355de6739..ea00a2f31b74610c151499948658af9e85002903 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -3783,6 +3783,7 @@ pub fn loadInput(wasm: *Wasm, input: link.Input) !void { switch (input) { .res => unreachable, .dso => unreachable, + .tbd => unreachable, .object, .archive => |obj| { try argv.append(gpa, try obj.path.toString(comp.arena)); }, @@ -3792,6 +3793,7 @@ pub fn loadInput(wasm: *Wasm, input: link.Input) !void { switch (input) { .res => unreachable, .dso => unreachable, + .tbd => unreachable, .object => |obj| try parseObject(wasm, obj), .archive => |obj| try parseArchive(wasm, obj), } diff --git a/src/target.zig b/src/target.zig index 5e36be5fd4f7c7ad799f619e775bcc611747003f..6234c11a146b18c45b6b36ecf12138edebb99a5c 100644 --- a/src/target.zig +++ b/src/target.zig @@ -510,7 +510,7 @@ pub fn libcFullLinkFlags(target: *const std.Target) []const []const u8 { }, // On SerenityOS libc includes libm, libpthread, libdl, and libssp. .serenity => &.{"-lc"}, - else => &.{}, + else => if (target.os.tag.isDarwin()) &.{"-lSystem"} else &.{}, }; return result; } -- 2.54.0 From 2a5e70202b11982bde5cadb287cdb1abdcd1a49b Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Thu, 24 Sep 2026 11:02:41 +0100 Subject: [PATCH 07/10] compiler: move Darwin SDK version resolution to the frontend The SDK version is determined by parsing the `SDKSettings.json` file in the Darwin SDK (or in `lib/libc/darwin/` when using our vendored libs). (The old Mach-O linker also had a fallback strategy of determining the version from the SDK path, but that seems unnecessary nowadays.) Moving that parsing to `src/link.zig`, and calling into it at the same time as handling libc (libSystem), turns out to simplify this logic quite a bit! --- lib/std/zig/LibCDirs.zig | 30 +----- lib/std/zig/LibCInstallation.zig | 33 ++++--- src/Compilation.zig | 16 ++-- src/link.zig | 152 ++++++++++++++++++++++++++++++- src/link/MachO.zig | 126 ++++++------------------- src/link/MachO/load_commands.zig | 19 ++-- src/link/MachO2.zig | 4 + 7 files changed, 220 insertions(+), 160 deletions(-) diff --git a/lib/std/zig/LibCDirs.zig b/lib/std/zig/LibCDirs.zig index 84002241bf9ea4fb1dbf5f8702a4723c05a01694..0c8873479420e6aca1db1e27e02f79600ee10950 100644 --- a/lib/std/zig/LibCDirs.zig +++ b/lib/std/zig/LibCDirs.zig @@ -11,15 +11,6 @@ libc_include_dir_list: []const []const u8, libc_installation: ?*const LibCInstallation, libc_framework_dir_list: []const []const u8, sysroot: ?[]const u8, -darwin_sdk_layout: ?DarwinSdkLayout, - -/// The filesystem layout of darwin SDK elements. -pub const DarwinSdkLayout = enum { - /// macOS SDK layout: TOP { /usr/include, /usr/lib, /System/Library/Frameworks }. - sdk, - /// Shipped libc layout: TOP { /lib/libc/include, /lib/libc/darwin, }. - vendored, -}; pub fn detect( arena: Allocator, @@ -37,7 +28,6 @@ pub fn detect( .libc_installation = null, .libc_framework_dir_list = &.{}, .sysroot = null, - .darwin_sdk_layout = null, }; } @@ -102,13 +92,11 @@ pub fn detect( .libc_installation = null, .libc_framework_dir_list = &.{}, .sysroot = null, - .darwin_sdk_layout = null, }; } fn detectFromInstallation(arena: Allocator, target: *const std.Target, lci: *const LibCInstallation) !LibCDirs { var list = try std.array_list.Managed([]const u8).initCapacity(arena, 5); - var framework_list = std.array_list.Managed([]const u8).init(arena); list.appendAssumeCapacity(lci.include_dir.?); @@ -149,21 +137,15 @@ fn detectFromInstallation(arena: Allocator, target: *const std.Target, lci: *con } } - var sysroot: ?[]const u8 = null; - - if (target.os.tag.isDarwin()) d: { - const down1 = std.fs.path.dirname(lci.sys_include_dir.?) orelse break :d; - const down2 = std.fs.path.dirname(down1) orelse break :d; - try framework_list.append(try std.fs.path.join(arena, &.{ down2, "System", "Library", "Frameworks" })); - sysroot = down2; - } + const frameworks: []const []const u8 = if (target.os.tag.isDarwin()) try arena.dupe([]const u8, &.{ + try std.fs.path.join(arena, &.{ lci.darwin_sdk_dir.?, "System", "Library", "Frameworks" }), + }) else &.{}; return .{ .libc_include_dir_list = list.items, .libc_installation = lci, - .libc_framework_dir_list = framework_list.items, - .sysroot = sysroot, - .darwin_sdk_layout = if (sysroot == null) null else .sdk, + .libc_framework_dir_list = frameworks, + .sysroot = if (target.os.tag.isDarwin()) lci.darwin_sdk_dir.? else null, }; } @@ -178,7 +160,6 @@ pub fn detectFromBuilding(arena: Allocator, zig_lib_dir: Path, target: *const st .libc_installation = null, .libc_framework_dir_list = &.{}, .sysroot = null, - .darwin_sdk_layout = .vendored, }; } @@ -230,7 +211,6 @@ pub fn detectFromBuilding(arena: Allocator, zig_lib_dir: Path, target: *const st .libc_installation = null, .libc_framework_dir_list = &.{}, .sysroot = null, - .darwin_sdk_layout = .vendored, }; } diff --git a/lib/std/zig/LibCInstallation.zig b/lib/std/zig/LibCInstallation.zig index a96bbaea26e8cdabad6aba221584916ee4c7d9cf..5191bb323b5873465c17749502c6930cd336f630 100644 --- a/lib/std/zig/LibCInstallation.zig +++ b/lib/std/zig/LibCInstallation.zig @@ -23,6 +23,7 @@ crt_dir: ?[]const u8 = null, msvc_lib_dir: ?[]const u8 = null, kernel32_lib_dir: ?[]const u8 = null, cc_dir: ?[]const u8 = null, +darwin_sdk_dir: ?[]const u8 = null, pub const FindError = error{ OutOfMemory, @@ -121,19 +122,16 @@ pub fn parse(allocator: Allocator, io: Io, libc_file: []const u8, target: *const }); return error.ParseError; } + if (self.darwin_sdk_dir == null and os_tag.isDarwin()) { + log.err("darwin_sdk_dir may not be empty for {t}", .{os_tag}); + return error.ParseError; + } return self; } pub fn render(self: LibCInstallation, out: *std.Io.Writer) !void { @setEvalBranchQuota(4000); - const include_dir = self.include_dir orelse ""; - const sys_include_dir = self.sys_include_dir orelse ""; - const crt_dir = self.crt_dir orelse ""; - const msvc_lib_dir = self.msvc_lib_dir orelse ""; - const kernel32_lib_dir = self.kernel32_lib_dir orelse ""; - const cc_dir = self.cc_dir orelse ""; - try out.print( \\# The directory that contains `stdlib.h`. \\# On POSIX, can be found with: `cc -E -Wp,-v -xc /dev/null` @@ -161,13 +159,18 @@ pub fn render(self: LibCInstallation, out: *std.Io.Writer) !void { \\# Only needed when targeting MSVC on Windows. \\kernel32_lib_dir={s} \\ + \\# The directory that contains `SDKSettings.json` + \\# Only needed when targeting Darwin. + \\darwin_sdk_dir={s} + \\ , .{ - include_dir, - sys_include_dir, - crt_dir, - msvc_lib_dir, - kernel32_lib_dir, - cc_dir, + self.include_dir orelse "", + self.sys_include_dir orelse "", + self.cc_dir orelse "", + self.crt_dir orelse "", + self.msvc_lib_dir orelse "", + self.kernel32_lib_dir orelse "", + self.darwin_sdk_dir orelse "", }); } @@ -186,10 +189,10 @@ pub fn findNative(gpa: Allocator, io: Io, args: FindNativeOptions) FindError!Lib if (is_darwin and args.target.os.tag.isDarwin()) { if (!std.zig.system.darwin.isSdkInstalled(gpa, io)) return error.DarwinSdkNotFound; - const sdk = std.zig.system.darwin.getSdk(gpa, io, args.target) orelse + self.darwin_sdk_dir = std.zig.system.darwin.getSdk(gpa, io, args.target) orelse return error.DarwinSdkNotFound; - defer gpa.free(sdk); + const sdk = self.darwin_sdk_dir.?; self.include_dir = try fs.path.join(gpa, &.{ sdk, "usr/include" }); self.sys_include_dir = try fs.path.join(gpa, &.{ sdk, "usr/include" }); self.crt_dir = try fs.path.join(gpa, &.{ sdk, "usr/lib" }); diff --git a/src/Compilation.zig b/src/Compilation.zig index 7bdc84b74d4fd56c2dc21be583691de40de3989f..f40fad7fe376d9f1dba6665af1a07d02664c5f1b 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -2208,7 +2208,6 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, .z_relro = options.linker_z_relro, .z_common_page_size = options.linker_z_common_page_size, .z_max_page_size = options.linker_z_max_page_size, - .darwin_sdk_layout = libc_dirs.darwin_sdk_layout, .frameworks = options.frameworks, .lib_directories = options.lib_directories, .framework_dirs = options.framework_dirs, @@ -2453,10 +2452,16 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, // Loads the libraries provided by `target_util.libcFullLinkFlags(target)`. comp.oneshot_prelink_tasks.appendAssumeCapacity(.load_host_libc); } else if (target.isDarwinLibC()) { - try comp.oneshot_prelink_tasks.append(gpa, .{ .load_tbd = .{ - .root_dir = comp.dirs.zig_lib, - .sub_path = "libc" ++ fs.path.sep_str ++ "darwin" ++ fs.path.sep_str ++ "libSystem.tbd", - } }); + try comp.oneshot_prelink_tasks.appendSlice(gpa, &.{ + .{ .load_tbd = .{ + .root_dir = comp.dirs.zig_lib, + .sub_path = "libc" ++ fs.path.sep_str ++ "darwin" ++ fs.path.sep_str ++ "libSystem.tbd", + } }, + .{ .load_darwin_sdk_settings = .{ + .root_dir = comp.dirs.zig_lib, + .sub_path = "libc" ++ fs.path.sep_str ++ "darwin" ++ fs.path.sep_str ++ "SDKSettings.json", + } }, + }); // TODO delete logic from MachO flush() and queue up tasks here instead. } else if (target.isMuslLibC()) { if (!std.zig.target.canBuildLibC(target)) return diag.fail(.cross_libc_unavailable); @@ -3487,7 +3492,6 @@ fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifes man.hash.add(opts.discard_local_symbols); man.hash.addOptional(opts.compatibility_version); man.hash.addOptionalBytes(opts.install_name); - man.hash.addOptional(opts.darwin_sdk_layout); // COFF specific stuff man.hash.addOptional(opts.subsystem); diff --git a/src/link.zig b/src/link.zig index 247ca5d4b9250e2dd94408d34d2e12786483376a..34b020b677cce80c7f541c158276a507d031331e 100644 --- a/src/link.zig +++ b/src/link.zig @@ -502,7 +502,6 @@ pub const File = struct { /// Remove dylibs that are unreachable by the entry point or exported symbols dead_strip_dylibs: bool, frameworks: []const MachO.Framework, - darwin_sdk_layout: ?MachO.SdkLayout, /// Force load all members of static archives that implement an /// Objective-C class or category force_load_objc: bool, @@ -1248,6 +1247,59 @@ pub const File = struct { } } + fn loadDarwinSdkSettings(base: *File, sdk_settings_path: Path) Error!void { + const comp = base.comp; + const io = comp.io; + const arena = comp.arena; + const diags = &comp.link_diags; + + const contents = sdk_settings_path.root_dir.handle.readFileAlloc( + io, + sdk_settings_path.sub_path, + arena, + .limited(1024 * 1024 * 256), + ) catch |err| switch (err) { + error.OutOfMemory, error.Canceled => |e| return e, + else => |e| return diags.failParse(sdk_settings_path, "failed to parse Darwin SDK settings: {t}", .{e}), + }; + + const parsed = std.json.parseFromSlice(std.json.Value, arena, contents, .{}) catch |err| switch (err) { + error.OutOfMemory => |e| return e, + else => |e| return diags.failParse(sdk_settings_path, "failed to parse Darwin SDK settings: {t}", .{e}), + }; + const parsed_object = switch (parsed.value) { + .object => |obj| obj, + else => return diags.failParse(sdk_settings_path, "failed to parse Darwin SDK settings: file is not a JSON object", .{}), + }; + + const version_json = parsed_object.get("MinimalDisplayName") orelse return diags.failParse( + sdk_settings_path, + "failed to parse Darwin SDK settings: 'MinimalDisplayName' missing", + .{}, + ); + const version_str: []const u8 = switch (version_json) { + .string => |str| str, + else => return diags.failParse( + sdk_settings_path, + "failed to parse Darwin SDK settings: 'MinimalDisplayName' not a string", + .{}, + ), + }; + const version: DarwinSdkVersion = try .parse(diags, sdk_settings_path, version_str); + try base.setDarwinSdkVersion(version); + } + + fn setDarwinSdkVersion(base: *File, version: DarwinSdkVersion) Error!void { + assert(!base.post_prelink); + switch (base.tag) { + inline .macho, .macho2 => |tag| { + dev.check(tag.devFeature()); + return @as(*tag.Type(), @fieldParentPtr("base", base)).setDarwinSdkVersion(version); + }, + else => unreachable, + } + } + /// Called when all linker inputs have been sent via `loadInput`. After /// this, `loadInput` will not be called anymore. pub fn prelink(base: *File) Error!void { @@ -1451,6 +1503,7 @@ pub const PrelinkTask = union(enum) { /// GNU ld script. load_dso: Path, load_tbd: Path, + load_darwin_sdk_settings: Path, }; pub const ZcuTask = union(enum) { /// Sent once per update, as the very first `ZcuTask` in the update. Indicates that all per-file @@ -1589,7 +1642,7 @@ pub fn doPrelinkTask(comp: *Compilation, task: PrelinkTask) void { error.FileNotFound => {}, error.AlreadyReported => return, error.Canceled => io.recancel(), - else => |e| diags.addParseError(archive_path, "failed to parse archive {f}: {s}", .{ archive_path, @errorName(e) }), + else => |e| diags.addParseError(archive_path, "failed to parse archive: {s}", .{@errorName(e)}), } diags.addError("failed to find library '{s}'", .{lib_name}); @@ -1612,6 +1665,19 @@ pub fn doPrelinkTask(comp: *Compilation, task: PrelinkTask) void { } } + if (target.os.tag.isDarwin()) { + const sdk_settings_path: Path = .initCwd(std.fmt.allocPrint( + comp.arena, + "{s}" ++ sep ++ "SDKSettings.json", + .{libc_installation.darwin_sdk_dir.?}, + ) catch return diags.setAllocFailure()); + base.loadDarwinSdkSettings(sdk_settings_path) catch |err| switch (err) { + error.OutOfMemory => return diags.setAllocFailure(), + error.Canceled => return io.recancel(), + error.AlreadyReported => return, + }; + } + if (target.os.tag == .windows and target.abi == .msvc) { const inputs: []const struct { dir: enum { crt, msvc_lib, kernel32_lib }, @@ -1709,6 +1775,15 @@ pub fn doPrelinkTask(comp: *Compilation, task: PrelinkTask) void { else => |e| diags.addParseError(path, "failed to parse tbd: {t}", .{e}), } }, + .load_darwin_sdk_settings => |path| { + const prog_node = comp.link_prog_node.start("Parse SDK Settings", 0); + defer prog_node.end(); + base.loadDarwinSdkSettings(path) catch |err| switch (err) { + error.OutOfMemory => return diags.setAllocFailure(), + error.Canceled => return io.recancel(), + error.AlreadyReported => return, + }; + }, } } pub fn doZcuTask(comp: *Compilation, tid: Zcu.PerThread.Id, task: ZcuTask) void { @@ -2793,3 +2868,76 @@ pub fn firstObjectInput(inputs: []const Input) ?Input.Object { }; return null; } + +pub const DarwinSdkVersion = packed struct(u32) { + patch: u8, + minor: u8, + major: u16, + + fn parse( + diags: *Diags, + /// Passed to `Diags.failParse` on error. + sdk_settings_path: Path, + version_str: []const u8, + ) error{AlreadyReported}!DarwinSdkVersion { + var it = std.mem.splitScalar(u8, version_str, '.'); + + const major = try parseComponent( + .major, + diags, + sdk_settings_path, + version_str, + it.first(), + ); + const minor = if (it.next()) |component_str| try parseComponent( + .minor, + diags, + sdk_settings_path, + version_str, + component_str, + ) else return diags.failParse( + sdk_settings_path, + "failed to parse Darwin SDK version {q}: missing minor version", + .{version_str}, + ); + const patch = if (it.next()) |component_str| try parseComponent( + .patch, + diags, + sdk_settings_path, + version_str, + component_str, + ) else 0; // Apple sometimes omit the patch version + + if (it.next() != null) return diags.failParse( + sdk_settings_path, + "failed to parse Darwin SDK version {q}: too many version components", + .{version_str}, + ); + + return .{ .major = major, .minor = minor, .patch = patch }; + } + + fn parseComponent( + comptime component: enum { major, minor, patch }, + diags: *Diags, + /// Passed to `Diags.failParse` on error. + sdk_settings_path: Path, + /// Only used for error messages. + version_str: []const u8, + component_str: []const u8, + ) error{AlreadyReported}!@FieldType(DarwinSdkVersion, @tagName(component)) { + const Int = @FieldType(DarwinSdkVersion, @tagName(component)); + return std.fmt.parseInt(Int, component_str, 10) catch |err| switch (err) { + error.Overflow => return diags.failParse( + sdk_settings_path, + "failed to parse Darwin SDK version {q}: {t} version {q} too large", + .{ version_str, component, component_str }, + ), + error.InvalidCharacter => return diags.failParse( + sdk_settings_path, + "failed to parse Darwin SDK version {q}: invalid {t} version {q}", + .{ version_str, component, component_str }, + ), + }; + } +}; diff --git a/src/link/MachO.zig b/src/link/MachO.zig index db7d1733859235990e5cc60fcd840b5fa8cd30dc..03c350c210f3410994028f77a0cf97ae0ef30ae1 100644 --- a/src/link/MachO.zig +++ b/src/link/MachO.zig @@ -106,9 +106,7 @@ has_tlv: AtomicBool = AtomicBool.init(false), binds_to_weak: AtomicBool = AtomicBool.init(false), weak_defines: AtomicBool = AtomicBool.init(false), -/// Options -/// SDK layout -sdk_layout: ?SdkLayout, +// Options /// Size of the __PAGEZERO segment. pagezero_size: ?u64, /// Minimum space for future expansion of the load commands. @@ -134,7 +132,7 @@ compatibility_version: ?std.SemanticVersion, /// Entry name entry_name: ?[]const u8, platform: Platform, -sdk_version: ?std.SemanticVersion, +sdk_version: ?link.DarwinSdkVersion, /// When set to true, the linker will hoist all dylibs including system dependent dylibs. no_implicit_dylibs: bool = false, /// Whether the linker should parse and always force load objects containing ObjC in archives. @@ -198,7 +196,6 @@ pub fn createEmpty( .headerpad_size = options.headerpad_size, .headerpad_max_install_names = options.headerpad_max_install_names, .dead_strip_dylibs = options.dead_strip_dylibs, - .sdk_layout = options.darwin_sdk_layout, .frameworks = options.frameworks, .install_name = options.install_name, .entitlements = options.entitlements, @@ -210,7 +207,7 @@ pub fn createEmpty( .named => |name| name, }, .platform = Platform.fromTarget(target), - .sdk_version = if (options.darwin_sdk_layout) |layout| inferSdkVersion(comp, layout) else null, + .sdk_version = null, .undefined_treatment = if (allow_shlib_undefined) .dynamic_lookup else .@"error", // TODO delete this, directories must instead be resolved by the frontend .lib_directories = options.lib_directories, @@ -604,12 +601,20 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void { try argv.append("-platform_version"); try argv.append(@tagName(self.platform.os_tag)); - try argv.append(try std.fmt.allocPrint(arena, "{f}", .{self.platform.version})); + try argv.append(try std.fmt.allocPrint(arena, "{d}.{d}.{d}", .{ + self.platform.version.major, + self.platform.version.minor, + self.platform.version.patch, + })); if (self.sdk_version) |ver| { try argv.append(try std.fmt.allocPrint(arena, "{d}.{d}", .{ ver.major, ver.minor })); } else { - try argv.append(try std.fmt.allocPrint(arena, "{f}", .{self.platform.version})); + try argv.append(try std.fmt.allocPrint(arena, "{d}.{d}.{d}", .{ + self.platform.version.major, + self.platform.version.minor, + self.platform.version.patch, + })); } if (comp.sysroot) |syslibroot| { @@ -4101,7 +4106,7 @@ pub const null_sym = macho.nlist_64{ pub const Platform = struct { os_tag: std.Target.Os.Tag, abi: std.Target.Abi, - version: std.SemanticVersion, + version: link.DarwinSdkVersion, /// Using Apple's ld64 as our blueprint, `min_version` as well as `sdk_version` are set to /// the extracted minimum platform version. @@ -4128,7 +4133,7 @@ pub const Platform = struct { => .simulator, else => .none, }, - .version = appleVersionToSemanticVersion(cmd.minos), + .version = @fromBackingInt(cmd.minos), }; }, .VERSION_MIN_IPHONEOS, @@ -4147,7 +4152,7 @@ pub const Platform = struct { else => unreachable, }, .abi = .none, - .version = appleVersionToSemanticVersion(cmd.version), + .version = @fromBackingInt(cmd.version), }; }, else => unreachable, @@ -4155,17 +4160,18 @@ pub const Platform = struct { } pub fn fromTarget(target: *const std.Target) Platform { + const semver = target.os.version_range.semver.min; return .{ .os_tag = target.os.tag, .abi = target.abi, - .version = target.os.version_range.semver.min, + .version = .{ + .major = @intCast(semver.major), + .minor = @intCast(semver.minor), + .patch = @intCast(semver.patch), + }, }; } - pub fn toAppleVersion(plat: Platform) u32 { - return semanticVersionToAppleVersion(plat.version); - } - pub fn toApplePlatform(plat: Platform) macho.PLATFORM { return switch (plat.os_tag) { .driverkit => .DRIVERKIT, @@ -4182,7 +4188,7 @@ pub const Platform = struct { pub fn isBuildVersionCompatible(plat: Platform) bool { inline for (supported_platforms) |sup_plat| { if (sup_plat[0] == plat.os_tag and sup_plat[1] == plat.abi) { - return sup_plat[2] <= plat.toAppleVersion(); + return sup_plat[2] <= @backingInt(plat.version); } } return false; @@ -4191,7 +4197,7 @@ pub const Platform = struct { pub fn isVersionMinCompatible(plat: Platform) bool { inline for (supported_platforms) |sup_plat| { if (sup_plat[0] == plat.os_tag and sup_plat[1] == plat.abi) { - return sup_plat[3] <= plat.toAppleVersion(); + return sup_plat[3] <= @backingInt(plat.version); } } return false; @@ -4250,86 +4256,8 @@ const supported_platforms = [_]SupportedPlatforms{ }; // zig fmt: on -pub inline fn semanticVersionToAppleVersion(version: std.SemanticVersion) u32 { - const major = version.major; - const minor = version.minor; - const patch = version.patch; - return (@as(u32, @intCast(major)) << 16) | (@as(u32, @intCast(minor)) << 8) | @as(u32, @intCast(patch)); -} - -pub inline fn appleVersionToSemanticVersion(version: u32) std.SemanticVersion { - return .{ - .major = @as(u16, @truncate(version >> 16)), - .minor = @as(u8, @truncate(version >> 8)), - .patch = @as(u8, @truncate(version)), - }; -} - -fn inferSdkVersion(comp: *Compilation, sdk_layout: SdkLayout) ?std.SemanticVersion { - const gpa = comp.gpa; - - var arena_allocator = std.heap.ArenaAllocator.init(gpa); - defer arena_allocator.deinit(); - const arena = arena_allocator.allocator(); - - const io = comp.io; - - const sdk_dir = switch (sdk_layout) { - .sdk => comp.sysroot.?, - .vendored => fs.path.join(arena, &.{ comp.dirs.zig_lib.path.?, "libc", "darwin" }) catch return null, - }; - if (readSdkVersionFromSettings(arena, io, sdk_dir)) |ver| { - return parseSdkVersion(ver); - } else |_| { - // Read from settings should always succeed when vendored. - // TODO: convert to fatal linker error - if (sdk_layout == .vendored) @panic("zig installation bug: unable to parse SDK version"); - } - - // infer from pathname - const stem = fs.path.stem(sdk_dir); - const start = for (stem, 0..) |c, i| { - if (std.ascii.isDigit(c)) break i; - } else stem.len; - const end = for (stem[start..], start..) |c, i| { - if (std.ascii.isDigit(c) or c == '.') continue; - break i; - } else stem.len; - return parseSdkVersion(stem[start..end]); -} - -// Official Apple SDKs ship with a `SDKSettings.json` located at the top of SDK fs layout. -// Use property `MinimalDisplayName` to determine version. -// The file/property is also available with vendored libc. -fn readSdkVersionFromSettings(arena: Allocator, io: Io, dir: []const u8) ![]const u8 { - const sdk_path = try fs.path.join(arena, &.{ dir, "SDKSettings.json" }); - const contents = try Io.Dir.cwd().readFileAlloc(io, sdk_path, arena, .limited(std.math.maxInt(u16))); - const parsed = try std.json.parseFromSlice(std.json.Value, arena, contents, .{}); - if (parsed.value.object.get("MinimalDisplayName")) |ver| return ver.string; - return error.SdkVersionFailure; -} - -// Versions reported by Apple aren't exactly semantically valid as they usually omit -// the patch component, so we parse SDK value by hand. -fn parseSdkVersion(raw: []const u8) ?std.SemanticVersion { - var parsed: std.SemanticVersion = .{ - .major = 0, - .minor = 0, - .patch = 0, - }; - - const parseNext = struct { - fn parseNext(it: anytype) ?u16 { - const nn = it.next() orelse return null; - return std.fmt.parseInt(u16, nn, 10) catch null; - } - }.parseNext; - - var it = std.mem.splitAny(u8, raw, "."); - parsed.major = parseNext(&it) orelse return null; - parsed.minor = parseNext(&it) orelse return null; - parsed.patch = parseNext(&it) orelse 0; - return parsed; +pub fn setDarwinSdkVersion(self: *MachO, version: link.DarwinSdkVersion) link.Error!void { + self.sdk_version = version; } /// When allocating, the ideal_capacity is calculated by @@ -4377,8 +4305,6 @@ const SystemLib = struct { } }; -pub const SdkLayout = std.zig.LibCDirs.DarwinSdkLayout; - const UndefinedTreatment = enum { @"error", warn, diff --git a/src/link/MachO/load_commands.zig b/src/link/MachO/load_commands.zig index 62bbc751cb58e82aee019f07065eabf1b8cf8917..f6b97406849f031a235a51f31c64cb8e3b65b545 100644 --- a/src/link/MachO/load_commands.zig +++ b/src/link/MachO/load_commands.zig @@ -9,6 +9,7 @@ const Allocator = std.mem.Allocator; const DebugSymbols = @import("DebugSymbols.zig"); const Dylib = @import("Dylib.zig"); const MachO = @import("../MachO.zig"); +const link = @import("../../link.zig"); pub const default_dyld_path: [*:0]const u8 = "/usr/lib/dyld"; @@ -296,7 +297,7 @@ pub fn writeRpathLC(rpath: []const u8, writer: *Writer) !void { } } -pub fn writeVersionMinLC(platform: MachO.Platform, sdk_version: ?std.SemanticVersion, writer: *Writer) !void { +pub fn writeVersionMinLC(platform: MachO.Platform, sdk_version: ?link.DarwinSdkVersion, writer: *Writer) !void { const cmd: macho.LC = switch (platform.os_tag) { .macos => .VERSION_MIN_MACOSX, .ios, .maccatalyst => .VERSION_MIN_IPHONEOS, @@ -306,24 +307,18 @@ pub fn writeVersionMinLC(platform: MachO.Platform, sdk_version: ?std.SemanticVer }; try writer.writeAll(mem.asBytes(&macho.version_min_command{ .cmd = cmd, - .version = platform.toAppleVersion(), - .sdk = if (sdk_version) |ver| - MachO.semanticVersionToAppleVersion(ver) - else - platform.toAppleVersion(), + .version = @backingInt(platform.version), + .sdk = @backingInt(sdk_version orelse platform.version), })); } -pub fn writeBuildVersionLC(platform: MachO.Platform, sdk_version: ?std.SemanticVersion, writer: *Writer) !void { +pub fn writeBuildVersionLC(platform: MachO.Platform, sdk_version: ?link.DarwinSdkVersion, writer: *Writer) !void { const cmdsize = @sizeOf(macho.build_version_command) + @sizeOf(macho.build_tool_version); try writer.writeStruct(@as(macho.build_version_command, .{ .cmdsize = cmdsize, .platform = platform.toApplePlatform(), - .minos = platform.toAppleVersion(), - .sdk = if (sdk_version) |ver| - MachO.semanticVersionToAppleVersion(ver) - else - platform.toAppleVersion(), + .minos = @backingInt(platform.version), + .sdk = @backingInt(sdk_version orelse platform.version), .ntools = 1, }), .little); try writer.writeAll(mem.asBytes(&macho.build_tool_version{ diff --git a/src/link/MachO2.zig b/src/link/MachO2.zig index c01ed3cf0b62d1a22555792236c32ffea0fb098d..47e97ba57d1ef1313bcfa48bb244b7d8730e8536 100644 --- a/src/link/MachO2.zig +++ b/src/link/MachO2.zig @@ -138,6 +138,10 @@ pub fn loadInput(macho: *MachO, input: link.Input) link.Error!void { _ = input; @panic("TODO"); } +pub fn setDarwinSdkVersion(macho: *MachO, version: link.DarwinSdkVersion) link.Error!void { + _ = macho; + std.log.err("MLUGG TODO: sdk version is '{d}.{d}.{d}'", .{ version.major, version.minor, version.patch }); +} pub fn prelink(macho: *MachO, prog_node: std.Progress.Node) link.Error!void { _ = macho; _ = prog_node; -- 2.54.0 From ed833ab31c93a937c2dafe14346a8cf928da474e Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Thu, 24 Sep 2026 13:07:38 +0100 Subject: [PATCH 08/10] compiler: move Darwin framework resolution to the frontend There's not much to say about this; it's essentially just another way of specifying link libraries. Framework directories are handled nearly identically to library directories, and frameworks are resolved in `link.resolveInputs` just like other link libraries. The new function `link.resolveFrameworkInput` is responsible for trying to find the framework's tbd or dylib file. As in 41d3b9a3e, if the framework contains a file with no extension, we will attempt to figure out whether it is a dylib, static archive, or tbd---this logic is quite similar to how we handle GNU ld scripts. --- lib/std/macho.zig | 24 ++- src/Compilation.zig | 18 +- src/link.zig | 340 ++++++++++++++++++++++++--------- src/link/MachO.zig | 68 +------ src/link/MachO/Archive.zig | 19 +- src/link/MachO/relocatable.zig | 4 +- src/main.zig | 276 ++++++++++++-------------- 7 files changed, 411 insertions(+), 338 deletions(-) diff --git a/lib/std/macho.zig b/lib/std/macho.zig index 48804cf7b1ccb51e8a37052686d0a9afe670d116..1e49fb5ce8a8e749ce3b79df01974c58585b8f61 100644 --- a/lib/std/macho.zig +++ b/lib/std/macho.zig @@ -1145,16 +1145,16 @@ pub const LC = enum(u32) { }; /// the mach magic number -pub const MH_MAGIC = 0xfeedface; +pub const MH_MAGIC: u32 = 0xfeedface; /// NXSwapInt(MH_MAGIC) -pub const MH_CIGAM = 0xcefaedfe; +pub const MH_CIGAM: u32 = 0xcefaedfe; /// the 64-bit mach magic number -pub const MH_MAGIC_64 = 0xfeedfacf; +pub const MH_MAGIC_64: u32 = 0xfeedfacf; /// NXSwapInt(MH_MAGIC_64) -pub const MH_CIGAM_64 = 0xcffaedfe; +pub const MH_CIGAM_64: u32 = 0xcffaedfe; /// relocatable object file pub const MH_OBJECT = 0x1; @@ -1281,16 +1281,16 @@ pub const MH_DYLIB_IN_CACHE = 0x80000000; // Constants for the flags field of the fat_header /// the fat magic number -pub const FAT_MAGIC = 0xcafebabe; +pub const FAT_MAGIC: u32 = 0xcafebabe; /// NXSwapLong(FAT_MAGIC) -pub const FAT_CIGAM = 0xbebafeca; +pub const FAT_CIGAM: u32 = 0xbebafeca; /// the 64-bit fat magic number -pub const FAT_MAGIC_64 = 0xcafebabf; +pub const FAT_MAGIC_64: u32 = 0xcafebabf; /// NXSwapLong(FAT_MAGIC_64) -pub const FAT_CIGAM_64 = 0xbfbafeca; +pub const FAT_CIGAM_64: u32 = 0xbfbafeca; /// Segment flags /// The file contents for this segment is for the high part of the VM space, the low part @@ -2212,3 +2212,11 @@ pub const CompactUnwindEncoding = packed struct(u32) { has_lsda: u1, start: u1, }; + +// Archive files start with the ARMAG identifying string. Then follows a +// `struct ar_hdr', and as many bytes of member file data as its `ar_size' +// member indicates, for each member file. +/// String that begins an archive file. +pub const ARMAG = "!\n"; +/// String in ar_fmag at the end of each header. +pub const ARFMAG = "`\n"; diff --git a/src/Compilation.zig b/src/Compilation.zig index f40fad7fe376d9f1dba6665af1a07d02664c5f1b..823763cef6f0af734eb86fcd1de4a1dd316e0025 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -83,7 +83,7 @@ zigc_strat: RtStrat, /// Resolved into known paths, any GNU ld scripts already resolved. link_inputs: []const link.Input, /// Needed only for passing -F args to clang. -framework_dirs: []const []const u8, +framework_dirs: []const Cache.Directory, /// These are only for DLLs dependencies fulfilled by the `.def` files shipped /// with Zig. Static libraries are provided as `link.Input` values. windows_libs: std.array_hash_map.String(void), @@ -1350,7 +1350,6 @@ pub const ClangPreprocessorMode = enum { version, }; -pub const Framework = link.File.MachO.Framework; pub const SystemLib = link.SystemLib; pub const CacheMode = enum { @@ -1487,6 +1486,7 @@ pub const CreateOptions = struct { /// The ELF implementation no longer uses this data, however the MachO and COFF /// implementations still do. lib_directories: []const Cache.Directory = &.{}, + framework_directories: []const Cache.Directory = &.{}, rpath_list: []const []const u8 = &[0][]const u8{}, symbol_wrap_set: std.array_hash_map.String(void) = .empty, c_source_files: []const CSourceFile = &.{}, @@ -1494,8 +1494,6 @@ pub const CreateOptions = struct { manifest_file: ?[]const u8 = null, rc_includes: std.zig.RcIncludes = .any, link_inputs: []const link.Input = &.{}, - framework_dirs: []const []const u8 = &[0][]const u8{}, - frameworks: []const Framework = &.{}, windows_lib_names: []const []const u8 = &.{}, /// This means that if the output mode is an executable it will be a /// Position Independent Executable. If the output mode is not an @@ -2146,7 +2144,7 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, .ubsan_rt_strat = ubsan_rt_strat, .zigc_strat = zigc_strat, .link_inputs = options.link_inputs, - .framework_dirs = options.framework_dirs, + .framework_dirs = options.framework_directories, .llvm_opt_bisect_limit = options.llvm_opt_bisect_limit, .skip_linker_dependencies = options.skip_linker_dependencies, .queued_jobs = .{}, @@ -2208,9 +2206,7 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, .z_relro = options.linker_z_relro, .z_common_page_size = options.linker_z_common_page_size, .z_max_page_size = options.linker_z_max_page_size, - .frameworks = options.frameworks, .lib_directories = options.lib_directories, - .framework_dirs = options.framework_dirs, .rpath_list = options.rpath_list, .symbol_wrap_set = options.symbol_wrap_set, .repro = options.linker_repro orelse (options.root_mod.optimize_mode != .debug), @@ -3416,7 +3412,10 @@ fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifes man.hash.add(comp.zigc_strat); man.hash.add(comp.rc_includes); man.hash.addListOfBytes(comp.force_undefined_symbols.keys()); - man.hash.addListOfBytes(comp.framework_dirs); + man.hash.add(comp.framework_dirs.len); + for (comp.framework_dirs) |framework_dir| { + man.hash.addBytes(framework_dir.path orelse "."); + } man.hash.addListOfBytes(comp.windows_libs.keys()); man.hash.addListOfBytes(comp.global_cc_argv); @@ -3482,7 +3481,6 @@ fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifes man.hash.add(opts.growable_table); // Mach-O specific stuff - try link.File.MachO.hashAddFrameworks(man, opts.frameworks); try man.addInputPathOptional(opts.entitlements, .{}); man.hash.addOptional(opts.pagezero_size); man.hash.addOptional(opts.headerpad_size); @@ -6461,7 +6459,7 @@ fn addCommonCCArgs( try argv.ensureUnusedCapacity(comp.framework_dirs.len * 2); for (comp.framework_dirs) |framework_dir| { - try argv.appendSlice(&.{ "-F", framework_dir }); + try argv.appendSlice(&.{ "-F", framework_dir.path orelse "." }); } } } diff --git a/src/link.zig b/src/link.zig index 34b020b677cce80c7f541c158276a507d031331e..c1ad89e817611968fa3c61b12cd5b19ad317fdfe 100644 --- a/src/link.zig +++ b/src/link.zig @@ -481,7 +481,6 @@ pub const File = struct { // TODO: remove this. libraries are resolved by the frontend. lib_directories: []const Directory, - framework_dirs: []const []const u8, rpath_list: []const []const u8, /// Zig compiler development linker flags. @@ -501,7 +500,6 @@ pub const File = struct { headerpad_max_install_names: bool, /// Remove dylibs that are unreachable by the entry point or exported symbols dead_strip_dylibs: bool, - frameworks: []const MachO.Framework, /// Force load all members of static archives that implement an /// Objective-C class or category force_load_objc: bool, @@ -1958,6 +1956,7 @@ pub const UnresolvedInput = union(enum) { /// Strings that come from GNU ld scripts. Is it a filename? Is it a path? /// Who knows! Fuck around and find out. ambiguous_name: AmbiguousNameQuery, + framework_query: FrameworkQuery, pub const NameQuery = struct { name: []const u8, @@ -1996,6 +1995,12 @@ pub const UnresolvedInput = union(enum) { } }; + pub const FrameworkQuery = struct { + name: []const u8, + needed: bool, + weak: bool, + }; + pub const SearchStrategy = enum { paths_first, mode_first, @@ -2118,6 +2123,7 @@ pub fn resolveInputs( /// Allocated with `gpa`. resolved_inputs: *std.ArrayList(Input), lib_directories: []const Cache.Directory, + framework_directories: []const Cache.Directory, color: std.zig.Color, ) Allocator.Error!void { var checked_paths: std.ArrayList(u8) = .empty; @@ -2129,6 +2135,7 @@ pub fn resolveInputs( var archive_dedup: ArchiveDedupMap = .empty; defer archive_dedup.deinit(gpa); + // Allocated with `arena`. var failed_libs: std.ArrayList(struct { name: []const u8, strategy: UnresolvedInput.SearchStrategy, @@ -2136,6 +2143,12 @@ pub fn resolveInputs( preferred_mode: std.lang.LinkMode, }) = .empty; + // Allocated with `arena`. + var failed_frameworks: std.ArrayList(struct { + name: []const u8, + checked_paths: []const u8, + }) = .empty; + // Convert external system libs into a stack so that items can be // pushed to it. // @@ -2337,17 +2350,47 @@ pub fn resolveInputs( } continue; }, + .framework_query => |framework_query| { + checked_paths.clearRetainingCapacity(); + for (framework_directories) |framework_directory| { + switch (try resolveFrameworkInput( + gpa, + arena, + io, + resolved_inputs, + &checked_paths, + &ld_script_bytes, + &archive_dedup, + framework_directory, + framework_query, + )) { + .ok => continue :syslib, + .no_match => {}, + } + } + try failed_frameworks.append(arena, .{ + .name = framework_query.name, + .checked_paths = try arena.dupe(u8, checked_paths.items), + }); + continue :syslib; + }, } comptime unreachable; } - if (failed_libs.items.len > 0) { + if (failed_libs.items.len > 0 or failed_frameworks.items.len > 0) { for (failed_libs.items) |f| { const searched_paths = if (f.checked_paths.len == 0) " none" else f.checked_paths; std.log.err("unable to find {t} system library {q} using strategy {t}. searched paths:{s}", .{ f.preferred_mode, f.name, f.strategy, searched_paths, }); } + for (failed_frameworks.items) |f| { + const searched_paths = if (f.checked_paths.len == 0) " none" else f.checked_paths; + std.log.err("unable to find system framework {q}. searched paths:{s}", .{ + f.name, searched_paths, + }); + } std.process.exit(1); } } @@ -2437,16 +2480,15 @@ fn resolveLibInput( else => |e| fatal("unable to search for so library {qf}: {t}", .{ test_path, e }), }; errdefer file.close(io); - return finishResolveLibInput( - io, - resolved_inputs, - archive_dedup, - test_path, - file, - link_mode, - name_query.query, - .basename, - ); + resolved_inputs.appendAssumeCapacity(.{ .dso = .{ + .path = test_path, + .file = file, + .needed = name_query.query.needed, + .weak = name_query.query.weak, + .reexport = name_query.query.reexport, + .fallback_soname = .basename, + } }); + return .ok; } // In the case of MinGW, the main check will be .lib but we also need to @@ -2462,16 +2504,13 @@ fn resolveLibInput( else => |e| fatal("unable to search for static library {qf}: {t}", .{ test_path, e }), }; errdefer file.close(io); - return finishResolveLibInput( - io, - resolved_inputs, - archive_dedup, - test_path, - file, - link_mode, - name_query.query, - .basename, - ); + addResolvedStaticLibInput(io, resolved_inputs, archive_dedup, .{ + .path = test_path, + .file = file, + .must_link = name_query.query.must_link, + .hidden = name_query.query.hidden, + }); + return .ok; } // In the case of OpenBSD, dynamic libraries are always versioned, without @@ -2533,6 +2572,145 @@ fn resolveLibInput( return .no_match; } +fn resolveFrameworkInput( + gpa: Allocator, + arena: Allocator, + io: Io, + /// Allocated via `gpa`. + resolved_inputs: *std.ArrayList(Input), + /// Allocated via `gpa`. + checked_paths: *std.ArrayList(u8), + /// Allocated via `gpa`. + ld_script_bytes: *std.ArrayList(u8), + /// Allocated via `gpa`. + archive_dedup: *ArchiveDedupMap, + framework_directory: Directory, + framework_query: UnresolvedInput.FrameworkQuery, +) Allocator.Error!ResolveLibInputResult { + const sep = std.fs.path.sep_str; + + tbd: { + const test_path: Path = .{ + .root_dir = framework_directory, + .sub_path = try std.fmt.allocPrint( + arena, + "{s}.framework" ++ sep ++ "{s}.tbd", + .{ framework_query.name, framework_query.name }, + ), + }; + try checked_paths.print(gpa, "\n {f}", .{test_path}); + var file = test_path.root_dir.handle.openFile(io, test_path.sub_path, .{}) catch |err| switch (err) { + error.FileNotFound => break :tbd, + else => |e| fatal("unable to search for tbd library {qf}: {t}", .{ test_path, e }), + }; + errdefer file.close(io); + resolved_inputs.appendAssumeCapacity(.{ .tbd = .{ + .path = test_path, + .file = file, + .needed = framework_query.needed, + .weak = framework_query.weak, + .reexport = false, + } }); + return .ok; + } + + dylib: { + const test_path: Path = .{ + .root_dir = framework_directory, + .sub_path = try std.fmt.allocPrint( + arena, + "{s}.framework" ++ sep ++ "{s}.dylib", + .{ framework_query.name, framework_query.name }, + ), + }; + try checked_paths.print(gpa, "\n {f}", .{test_path}); + var file = test_path.root_dir.handle.openFile(io, test_path.sub_path, .{}) catch |err| switch (err) { + error.FileNotFound => break :dylib, + else => |e| fatal("unable to search for dynamic library {qf}: {t}", .{ test_path, e }), + }; + errdefer file.close(io); + resolved_inputs.appendAssumeCapacity(.{ .dso = .{ + .path = test_path, + .file = file, + .needed = framework_query.needed, + .weak = framework_query.weak, + .reexport = false, + .fallback_soname = .basename, + } }); + return .ok; + } + + ambiguous: { + const test_path: Path = .{ + .root_dir = framework_directory, + .sub_path = try std.fmt.allocPrint( + arena, + "{s}.framework" ++ sep ++ "{s}", + .{ framework_query.name, framework_query.name }, + ), + }; + try checked_paths.print(gpa, "\n {f}", .{test_path}); + var file = test_path.root_dir.handle.openFile(io, test_path.sub_path, .{}) catch |err| switch (err) { + error.FileNotFound => break :ambiguous, + else => |e| fatal("unable to search for framework library {qf}: {t}", .{ test_path, e }), + }; + errdefer file.close(io); + + const macho_magics: []const [4]u8 = &.{ + @bitCast(std.macho.MH_MAGIC), + @bitCast(std.macho.MH_CIGAM), + @bitCast(std.macho.MH_MAGIC_64), + @bitCast(std.macho.MH_CIGAM_64), + @bitCast(std.macho.FAT_MAGIC), + @bitCast(std.macho.FAT_CIGAM), + @bitCast(std.macho.FAT_MAGIC_64), + @bitCast(std.macho.FAT_CIGAM_64), + }; + try ld_script_bytes.resize(gpa, @max(4, std.macho.ARMAG.len)); + const n = file.readPositionalAll(io, ld_script_bytes.items, 0) catch |err| + fatal("failed to read {qf}: {t}", .{ test_path, err }); + const buf = ld_script_bytes.items[0..n]; + + for (macho_magics) |*macho_magic| { + if (mem.startsWith(u8, buf, macho_magic)) { + // Appears to be a Mach-O file, so a dylib. + resolved_inputs.appendAssumeCapacity(.{ .dso = .{ + .path = test_path, + .file = file, + .needed = framework_query.needed, + .weak = framework_query.weak, + .reexport = false, + .fallback_soname = .basename, + } }); + return .ok; + } + } + + if (mem.startsWith(u8, buf, std.macho.ARMAG)) { + // Appears to be an archive file. + addResolvedStaticLibInput(io, resolved_inputs, archive_dedup, .{ + .path = test_path, + .file = file, + .must_link = false, + .hidden = false, + }); + return .ok; + } + + // It doesn't look like a dylib or archive, so assume it's a tbd. + resolved_inputs.appendAssumeCapacity(.{ .tbd = .{ + .path = test_path, + .file = file, + .needed = framework_query.needed, + .weak = framework_query.weak, + .reexport = false, + } }); + return .ok; + } + + return .no_match; +} + /// Deduplicates static archive link inputs based on their path. This is done for efficiency, so /// that linker implementations do not need to open and scan the archive just to determine that they /// need not extract any objects. At the time of writing, it also helps avoid "multiple definitions @@ -2556,45 +2734,21 @@ const ArchiveDedupAdapter = struct { } }; -fn finishResolveLibInput( +fn addResolvedStaticLibInput( io: Io, resolved_inputs: *std.ArrayList(Input), archive_dedup: *ArchiveDedupMap, - path: Path, - file: Io.File, - link_mode: std.lang.LinkMode, - query: UnresolvedInput.Query, - fallback_soname: Input.Dso.FallbackSoname, -) ResolveLibInputResult { - switch (link_mode) { - .static => { - const ctx: ArchiveDedupAdapter = .{ .resolved_inputs = resolved_inputs.items }; - const gop = archive_dedup.getOrPutAssumeCapacityAdapted(path, ctx); - if (gop.found_existing) { - // Ignore duplicate archive input - file.close(io); - return .ok; - } - gop.key_ptr.* = @intCast(resolved_inputs.items.len); - resolved_inputs.appendAssumeCapacity(.{ .archive = .{ - .path = path, - .file = file, - .must_link = query.must_link, - .hidden = query.hidden, - } }); - }, - .dynamic => resolved_inputs.appendAssumeCapacity(.{ - .dso = .{ - .path = path, - .file = file, - .needed = query.needed, - .weak = query.weak, - .reexport = query.reexport, - .fallback_soname = fallback_soname, - }, - }), + archive: Input.Object, +) void { + const ctx: ArchiveDedupAdapter = .{ .resolved_inputs = resolved_inputs.items }; + const gop = archive_dedup.getOrPutAssumeCapacityAdapted(archive.path, ctx); + if (gop.found_existing) { + // Ignore duplicate archive input + archive.file.close(io); + } else { + gop.key_ptr.* = @intCast(resolved_inputs.items.len); + resolved_inputs.appendAssumeCapacity(.{ .archive = archive }); } - return .ok; } fn resolvePathInput( @@ -2690,19 +2844,24 @@ fn resolvePathInputLib( try archive_dedup.ensureUnusedCapacity(gpa, 1); const test_path: Path = pq.path; + + var file = test_path.root_dir.handle.openFile(io, test_path.sub_path, .{}) catch |err| switch (err) { + error.FileNotFound => return .no_match, + else => |e| fatal("unable to search for {t} library {qf}: {t}", .{ + link_mode, std.fmt.alt(test_path, .formatEscapeChar), e, + }), + }; + errdefer file.close(io); + // In the case of shared libraries, they might actually be "linker scripts" // that contain references to other libraries. - if (pq.query.allow_so_scripts and target.ofmt == .elf and switch (Compilation.classifyFileExt(test_path.sub_path)) { - .static_library, .shared_library => true, - else => false, - }) { - var file = test_path.root_dir.handle.openFile(io, test_path.sub_path, .{}) catch |err| switch (err) { - error.FileNotFound => return .no_match, - else => |e| fatal("unable to search for {t} library {qf}: {t}", .{ - link_mode, std.fmt.alt(test_path, .formatEscapeChar), e, - }), - }; - errdefer file.close(io); + ld_script: { + if (!pq.query.allow_so_scripts) break :ld_script; + if (target.ofmt != .elf) break :ld_script; + switch (Compilation.classifyFileExt(test_path.sub_path)) { + .static_library, .shared_library => {}, + else => break :ld_script, + } try ld_script_bytes.resize(gpa, @max(std.elf.MAGIC.len, std.elf.ARMAG.len)); const n = file.readPositionalAll(io, ld_script_bytes.items, 0) catch |err| fatal("failed to read {qf}: {t}", .{ test_path, err }); @@ -2712,16 +2871,7 @@ fn resolvePathInputLib( mem.startsWith(u8, buf, std.elf.ARMAG_THIN)) { // Appears to be an ELF or archive file. - return finishResolveLibInput( - io, - resolved_inputs, - archive_dedup, - test_path, - file, - link_mode, - pq.query, - fallback_soname, - ); + break :ld_script; } const stat = file.stat(io) catch |err| fatal("failed to stat {f}: {t}", .{ test_path, err }); @@ -2785,21 +2935,23 @@ fn resolvePathInputLib( return .ok; } - var file = test_path.root_dir.handle.openFile(io, test_path.sub_path, .{}) catch |err| switch (err) { - error.FileNotFound => return .no_match, - else => |e| fatal("unable to search for {t} library {f}: {t}", .{ link_mode, test_path, e }), - }; - errdefer file.close(io); - return finishResolveLibInput( - io, - resolved_inputs, - archive_dedup, - test_path, - file, - link_mode, - pq.query, - fallback_soname, - ); + switch (link_mode) { + .dynamic => resolved_inputs.appendAssumeCapacity(.{ .dso = .{ + .path = test_path, + .file = file, + .needed = pq.query.needed, + .weak = pq.query.weak, + .reexport = pq.query.reexport, + .fallback_soname = fallback_soname, + } }), + .static => addResolvedStaticLibInput(io, resolved_inputs, archive_dedup, .{ + .path = test_path, + .file = file, + .must_link = pq.query.must_link, + .hidden = pq.query.hidden, + }), + } + return .ok; } pub fn openObject(io: Io, path: Path, must_link: bool, hidden: bool) !Input.Object { diff --git a/src/link/MachO.zig b/src/link/MachO.zig index 03c350c210f3410994028f77a0cf97ae0ef30ae1..b2b90f4d5091feee962fe301dfe6346692faa1b1 100644 --- a/src/link/MachO.zig +++ b/src/link/MachO.zig @@ -119,10 +119,6 @@ dead_strip_dylibs: bool, undefined_treatment: UndefinedTreatment, /// TODO: delete this, libraries need to be resolved by the frontend instead lib_directories: []const Directory, -/// Resolved list of framework search directories -framework_dirs: []const []const u8, -/// List of input frameworks -frameworks: []const Framework, /// Install name for the dylib. /// TODO: unify with soname install_name: ?[]const u8, @@ -196,7 +192,6 @@ pub fn createEmpty( .headerpad_size = options.headerpad_size, .headerpad_max_install_names = options.headerpad_max_install_names, .dead_strip_dylibs = options.dead_strip_dylibs, - .frameworks = options.frameworks, .install_name = options.install_name, .entitlements = options.entitlements, .compatibility_version = options.compatibility_version, @@ -211,7 +206,6 @@ pub fn createEmpty( .undefined_treatment = if (allow_shlib_undefined) .dynamic_lookup else .@"error", // TODO delete this, directories must instead be resolved by the frontend .lib_directories = options.lib_directories, - .framework_dirs = options.framework_dirs, .force_load_objc = options.force_load_objc, .discard_local_symbols = options.discard_local_symbols, }; @@ -372,35 +366,6 @@ pub fn flush( diags.addParseError(input.path(), "failed to read input file: {t}", .{err}); } - // frameworks - for (self.frameworks) |framework| { - switch (Compilation.classifyFileExt(framework.path.sub_path)) { - .shared_library => { - const dso_input = try link.openDsoInput(io, diags, framework.path, framework.needed, framework.weak, false); - self.classifyInputFile(dso_input) catch |err| - diags.addParseError(framework.path, "failed to parse input file: {s}", .{@errorName(err)}); - }, - .static_library => { - const archive_input = try link.openArchiveInput(io, diags, framework.path, false, false); - self.classifyInputFile(archive_input) catch |err| - diags.addParseError(framework.path, "failed to parse input file: {s}", .{@errorName(err)}); - }, - else => { - dso: { - const dso_input = link.openDsoInput(io, diags, framework.path, framework.needed, framework.weak, false) catch break :dso; - self.classifyInputFile(dso_input) catch break :dso; - continue; - } - ar: { - const archive_input = link.openArchiveInput(io, diags, framework.path, false, false) catch break :ar; - self.classifyInputFile(archive_input) catch break :ar; - continue; - } - diags.addParseError(framework.path, "unknown file extension", .{}); - }, - } - } - try self.parseInputFiles(); self.parseDependentDylibs() catch |err| { switch (err) { @@ -716,20 +681,9 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void { }, }; - for (self.framework_dirs) |f_dir| { + for (comp.framework_dirs) |f_dir| { try argv.append("-F"); - try argv.append(f_dir); - } - - for (self.frameworks) |framework| { - const name = framework.path.stem(); - const arg = if (framework.needed) - try std.fmt.allocPrint(arena, "-needed_framework {s}", .{name}) - else if (framework.weak) - try std.fmt.allocPrint(arena, "-weak_framework {s}", .{name}) - else - try std.fmt.allocPrint(arena, "-framework {s}", .{name}); - try argv.append(arg); + try argv.append(f_dir.path orelse "."); } if (comp.config.link_libcpp) { @@ -763,7 +717,7 @@ pub fn classifyInputFile(self: *MachO, input: link.Input) !void { log.debug("classifying input file {f}", .{path}); const fh = try self.addFileHandle(file); - var buffer: [Archive.SARMAG]u8 = undefined; + var buffer: [macho.ARMAG.len]u8 = undefined; const fat_arch: ?fat.Arch = try self.parseFatFile(file, path); const offset = if (fat_arch) |fa| fa.offset else 0; @@ -778,7 +732,7 @@ pub fn classifyInputFile(self: *MachO, input: link.Input) !void { return; } if (readArMagic(io, file, offset, &buffer) catch null) |ar_magic| blk: { - if (!mem.eql(u8, ar_magic, Archive.ARMAG)) break :blk; + if (!mem.eql(u8, ar_magic, macho.ARMAG)) break :blk; try self.addArchive(input.archive, fh, fat_arch); return; } @@ -808,10 +762,10 @@ pub fn readMachHeader(io: Io, file: Io.File, offset: usize) !macho.mach_header_6 return hdr; } -pub fn readArMagic(io: Io, file: Io.File, offset: usize, buffer: *[Archive.SARMAG]u8) ![]const u8 { +pub fn readArMagic(io: Io, file: Io.File, offset: usize, buffer: *[macho.ARMAG.len]u8) ![]const u8 { const nread = try file.readPositionalAll(io, buffer, offset); if (nread != buffer.len) return error.InputOutput; - return buffer[0..Archive.SARMAG]; + return buffer[0..macho.ARMAG.len]; } fn addObject(self: *MachO, path: Path, handle_index: File.HandleIndex, offset: u64) !void { @@ -1034,10 +988,6 @@ fn parseDependentDylibs(self: *MachO) !void { const comp = self.base.comp; const gpa = comp.gpa; const io = comp.io; - const framework_dirs = self.framework_dirs; - - // TODO delete this, directories must instead be resolved by the frontend - const lib_directories = self.lib_directories; var arena_alloc = std.heap.ArenaAllocator.init(gpa); defer arena_alloc.deinit(); @@ -1072,14 +1022,14 @@ fn parseDependentDylibs(self: *MachO) !void { const stem = fs.path.stem(id.name); // Framework - for (framework_dirs) |dir| { + for (comp.framework_dirs) |dir| { test_path.clearRetainingCapacity(); - if (try accessFrameworkPath(arena, io, &test_path, &checked_paths, dir, stem)) break :full_path test_path.items; + if (try accessFrameworkPath(arena, io, &test_path, &checked_paths, dir.path orelse ".", stem)) break :full_path test_path.items; } // Library const lib_name = eatPrefix(stem, "lib") orelse stem; - for (lib_directories) |lib_directory| { + for (self.lib_directories) |lib_directory| { test_path.clearRetainingCapacity(); if (try accessLibPath(arena, io, &test_path, &checked_paths, lib_directory.path orelse ".", lib_name)) break :full_path test_path.items; } diff --git a/src/link/MachO/Archive.zig b/src/link/MachO/Archive.zig index 91860f45986b106bed46fbf37e9ac35133ff5b49..4c2ad4187a01830b5d93b7cc2ff8ddc68de3c6e3 100644 --- a/src/link/MachO/Archive.zig +++ b/src/link/MachO/Archive.zig @@ -17,7 +17,7 @@ pub fn unpack(self: *Archive, macho_file: *MachO, path: Path, handle_index: File const offset = if (fat_arch) |ar| ar.offset else 0; const end_pos = if (fat_arch) |ar| offset + ar.size else (try handle.stat(io)).size; - var pos: usize = offset + SARMAG; + var pos: usize = offset + macho.ARMAG.len; while (true) { if (pos >= end_pos) break; if (!mem.isAligned(pos, 2)) pos += 1; @@ -30,9 +30,9 @@ pub fn unpack(self: *Archive, macho_file: *MachO, path: Path, handle_index: File const hdr = @as(*align(1) const ar_hdr, @ptrCast(&hdr_buffer)).*; pos += @sizeOf(ar_hdr); - if (!mem.eql(u8, &hdr.ar_fmag, ARFMAG)) { + if (!mem.eql(u8, &hdr.ar_fmag, macho.ARFMAG)) { return diags.failParse(path, "invalid header delimiter: expected '{f}', found '{f}'", .{ - std.ascii.hexEscape(ARFMAG, .lower), std.ascii.hexEscape(&hdr.ar_fmag, .lower), + std.ascii.hexEscape(macho.ARFMAG, .lower), std.ascii.hexEscape(&hdr.ar_fmag, .lower), }); } @@ -116,17 +116,6 @@ pub fn writeHeader( } } -// Archive files start with the ARMAG identifying string. Then follows a -// `struct ar_hdr', and as many bytes of member file data as its `ar_size' -// member indicates, for each member file. -/// String that begins an archive file. -pub const ARMAG: *const [SARMAG:0]u8 = "!\n"; -/// Size of that string. -pub const SARMAG: u4 = 8; - -/// String in ar_fmag at the end of each header. -const ARFMAG: *const [2:0]u8 = "`\n"; - pub const SYMDEF = "__.SYMDEF"; pub const SYMDEF64 = "__.SYMDEF_64"; pub const SYMDEF_SORTED = "__.SYMDEF SORTED"; @@ -146,7 +135,7 @@ pub const ar_hdr = extern struct { /// File size, in ASCII decimal. ar_size: [10]u8 = "0\x20\x20\x20\x20\x20\x20\x20\x20\x20".*, /// Always contains ARFMAG. - ar_fmag: [2]u8 = ARFMAG.*, + ar_fmag: [2]u8 = macho.ARFMAG.*, fn date(self: ar_hdr) !u64 { const value = mem.trimEnd(u8, &self.ar_date, &[_]u8{@as(u8, 0x20)}); diff --git a/src/link/MachO/relocatable.zig b/src/link/MachO/relocatable.zig index 1b679cc9058932f34ea8098032ef75af49295059..4f3cc6c66cebd98a9d79dcaf5c5818f39cce00ce 100644 --- a/src/link/MachO/relocatable.zig +++ b/src/link/MachO/relocatable.zig @@ -151,7 +151,7 @@ pub fn flushStaticLib(macho_file: *MachO, comp: *Compilation) link.Error!void { // Update file offsets of contributing objects const total_size: usize = blk: { - var pos: usize = Archive.SARMAG; + var pos: usize = macho.ARMAG.len; pos += @sizeOf(Archive.ar_hdr); pos += Archive.SYMDEF.len + 1; pos = mem.alignForward(usize, pos, 8); @@ -194,7 +194,7 @@ pub fn flushStaticLib(macho_file: *MachO, comp: *Compilation) link.Error!void { var writer: Writer = .fixed(buffer); // Write magic - writer.writeAll(Archive.ARMAG) catch unreachable; + writer.writeAll(macho.ARMAG) catch unreachable; // Write symtab ar_symtab.write(format, macho_file, &writer) catch |err| diff --git a/src/main.zig b/src/main.zig index 5ad21b548e12b1453aa4e1345c74fa7c17af5036..34ff983b0328f7d603ae0ad8e456ab5d0b993634 100644 --- a/src/main.zig +++ b/src/main.zig @@ -893,13 +893,6 @@ const ArgsIterator = struct { } }; -/// Similar to `link.Framework` except it doesn't store yet unresolved -/// path to the framework. -const Framework = struct { - needed: bool = false, - weak: bool = false, -}; - const CliModule = struct { root_path: []const u8, root_src_path: []const u8, @@ -1117,12 +1110,12 @@ fn buildOutputType( .llvm_m_args = .empty, .sysroot = null, - .lib_directories = .empty, // populated by createModule() .lib_dir_args = .empty, // populated from CLI arg parsing + .lib_directories = .empty, // populated by createModule() + .framework_dir_args = .empty, // populated from CLI arg parsing + .framework_directories = .empty, // populated by createModule() .libc_installation = null, .want_native_include_dirs = false, - .frameworks = .empty, - .framework_dirs = .empty, .rpath_list = .empty, .each_lib_rpath = null, .libc_paths_file = EnvVar.ZIG_LIBC.get(environ_map), @@ -1275,13 +1268,25 @@ fn buildOutputType( } else if (mem.eql(u8, arg, "--library-directory") or mem.eql(u8, arg, "-L")) { try create_module.lib_dir_args.append(arena, args_iter.nextOrFatal()); } else if (mem.eql(u8, arg, "-F")) { - try create_module.framework_dirs.append(arena, args_iter.nextOrFatal()); + try create_module.framework_dir_args.append(arena, args_iter.nextOrFatal()); } else if (mem.eql(u8, arg, "-framework")) { - try create_module.frameworks.put(arena, args_iter.nextOrFatal(), .{}); + try create_module.cli_link_inputs.append(arena, .{ .framework_query = .{ + .name = args_iter.nextOrFatal(), + .needed = false, + .weak = false, + } }); } else if (mem.eql(u8, arg, "-weak_framework")) { - try create_module.frameworks.put(arena, args_iter.nextOrFatal(), .{ .weak = true }); + try create_module.cli_link_inputs.append(arena, .{ .framework_query = .{ + .name = args_iter.nextOrFatal(), + .needed = false, + .weak = true, + } }); } else if (mem.eql(u8, arg, "-needed_framework")) { - try create_module.frameworks.put(arena, args_iter.nextOrFatal(), .{ .needed = true }); + try create_module.cli_link_inputs.append(arena, .{ .framework_query = .{ + .name = args_iter.nextOrFatal(), + .needed = true, + .weak = false, + } }); } else if (mem.eql(u8, arg, "-install_name")) { install_name = args_iter.nextOrFatal(); } else if (mem.cutPrefix(u8, arg, "--compress-debug-sections=")) |param| { @@ -1396,11 +1401,11 @@ fn buildOutputType( } else if (mem.eql(u8, arg, "-iframework")) { const path = args_iter.nextOrFatal(); try cssan.addIncludePath(arena, &cc_argv, .iframework, arg, path, false); - try create_module.framework_dirs.append(arena, path); // Forward to the backend as -F + try create_module.framework_dir_args.append(arena, path); // Forward to the backend as -F } else if (mem.eql(u8, arg, "-iframeworkwithsysroot")) { const path = args_iter.nextOrFatal(); try cssan.addIncludePath(arena, &cc_argv, .iframeworkwithsysroot, arg, path, false); - try create_module.framework_dirs.append(arena, path); // Forward to the backend as -F + try create_module.framework_dir_args.append(arena, path); // Forward to the backend as -F } else if (mem.eql(u8, arg, "--version")) { const next_arg = args_iter.nextOrFatal(); version = std.SemanticVersion.parse(next_arg) catch |err| { @@ -1828,7 +1833,7 @@ fn buildOutputType( } else if (mem.cutPrefix(u8, arg, "-L")) |rest| { try create_module.lib_dir_args.append(arena, rest); } else if (mem.cutPrefix(u8, arg, "-F")) |rest| { - try create_module.framework_dirs.append(arena, rest); + try create_module.framework_dir_args.append(arena, rest); } else if (mem.cutPrefix(u8, arg, "-l")) |name| { // We don't know whether this library is part of libc // or libc++ until we resolve the target, so we append @@ -2532,8 +2537,12 @@ fn buildOutputType( disable_c_depfile = true; try cc_argv.appendSlice(arena, it.other_args); }, - .framework_dir => try create_module.framework_dirs.append(arena, it.only_arg), - .framework => try create_module.frameworks.put(arena, it.only_arg, .{}), + .framework_dir => try create_module.framework_dir_args.append(arena, it.only_arg), + .framework => try create_module.cli_link_inputs.append(arena, .{ .framework_query = .{ + .name = it.only_arg, + .needed = false, + .weak = false, + } }), .nostdlibinc => create_module.want_native_include_dirs = false, .strip => mod_opts.strip = true, .exec_model => { @@ -2561,7 +2570,11 @@ fn buildOutputType( }, .name_done = false, } }), - .weak_framework => try create_module.frameworks.put(arena, it.only_arg, .{ .weak = true }), + .weak_framework => try create_module.cli_link_inputs.append(arena, .{ .framework_query = .{ + .name = it.only_arg, + .needed = false, + .weak = true, + } }), .headerpad_max_install_names => headerpad_max_install_names = true, .compress_debug_sections => { if (it.only_arg.len == 0) { @@ -2914,11 +2927,23 @@ fn buildOutputType( fatal("unable to parse minor subsystem version {q}: {t}", .{ minor, err }); }; } else if (mem.eql(u8, arg, "-framework")) { - try create_module.frameworks.put(arena, linker_args_it.nextOrFatal(), .{}); + try create_module.cli_link_inputs.append(arena, .{ .framework_query = .{ + .name = linker_args_it.nextOrFatal(), + .needed = false, + .weak = false, + } }); } else if (mem.eql(u8, arg, "-weak_framework")) { - try create_module.frameworks.put(arena, linker_args_it.nextOrFatal(), .{ .weak = true }); + try create_module.cli_link_inputs.append(arena, .{ .framework_query = .{ + .name = linker_args_it.nextOrFatal(), + .needed = false, + .weak = true, + } }); } else if (mem.eql(u8, arg, "-needed_framework")) { - try create_module.frameworks.put(arena, linker_args_it.nextOrFatal(), .{ .needed = true }); + try create_module.cli_link_inputs.append(arena, .{ .framework_query = .{ + .name = linker_args_it.nextOrFatal(), + .needed = true, + .weak = false, + } }); } else if (mem.eql(u8, arg, "-needed_library")) { try create_module.cli_link_inputs.append(arena, .{ .name_query = .{ .name = linker_args_it.nextOrFatal(), @@ -3400,59 +3425,6 @@ fn buildOutputType( } } - var resolved_frameworks = std.array_list.Managed(Compilation.Framework).init(arena); - - if (create_module.frameworks.keys().len > 0) { - var test_path = std.array_list.Managed(u8).init(gpa); - defer test_path.deinit(); - - var checked_paths = std.array_list.Managed(u8).init(gpa); - defer checked_paths.deinit(); - - var failed_frameworks = std.array_list.Managed(struct { - name: []const u8, - checked_paths: []const u8, - }).init(arena); - - framework: for (create_module.frameworks.keys(), create_module.frameworks.values()) |framework_name, info| { - checked_paths.clearRetainingCapacity(); - - for (create_module.framework_dirs.items) |framework_dir_path| { - if (try accessFrameworkPath( - io, - &test_path, - &checked_paths, - framework_dir_path, - framework_name, - )) { - const path = Path.initCwd(try arena.dupe(u8, test_path.items)); - try resolved_frameworks.append(.{ - .needed = info.needed, - .weak = info.weak, - .path = path, - }); - continue :framework; - } - } - - try failed_frameworks.append(.{ - .name = framework_name, - .checked_paths = try arena.dupe(u8, checked_paths.items), - }); - } - - if (failed_frameworks.items.len > 0) { - for (failed_frameworks.items) |f| { - const searched_paths = if (f.checked_paths.len == 0) " none" else f.checked_paths; - std.log.err("unable to find framework {q}. searched paths: {s}", .{ - f.name, searched_paths, - }); - } - process.exit(1); - } - } - // After this point, resolved_frameworks is used instead of frameworks. - if (create_module.resolved_options.output_mode == .Obj and target.ofmt == .coff) { const total_obj_count = create_module.c_source_files.items.len + @intFromBool(root_src_file != null) + @@ -3689,6 +3661,7 @@ fn buildOutputType( .emit_docs = emit_docs_resolved, .emit_implib = emit_implib_resolved, .lib_directories = create_module.lib_directories.items, + .framework_directories = create_module.framework_directories.items, .rpath_list = create_module.rpath_list.items, .symbol_wrap_set = symbol_wrap_set, .c_source_files = create_module.c_source_files.items, @@ -3697,8 +3670,6 @@ fn buildOutputType( .rc_includes = rc_includes, .mingw_unicode_entry_point = mingw_unicode_entry_point, .link_inputs = create_module.link_inputs.items, - .framework_dirs = create_module.framework_dirs.items, - .frameworks = resolved_frameworks.items, .windows_lib_names = create_module.windows_libs.keys(), .want_compiler_rt = if (zig_cc_explicitly_link_compiler_rt) true else want_compiler_rt, .want_ubsan_rt = want_ubsan_rt, @@ -4041,13 +4012,13 @@ const CreateModule = struct { /// CPU features. llvm_m_args: std.ArrayList([]const u8), sysroot: ?[]const u8, - lib_directories: std.ArrayList(Directory), lib_dir_args: std.ArrayList([]const u8), + lib_directories: std.ArrayList(Directory), + framework_dir_args: std.ArrayList([]const u8), + framework_directories: std.ArrayList(Directory), libc_installation: ?LibCInstallation, want_native_include_dirs: bool, - frameworks: std.array_hash_map.String(Framework), native_system_include_paths: []const []const u8, - framework_dirs: std.ArrayList([]const u8), rpath_list: std.ArrayList([]const u8), each_lib_rpath: ?bool, libc_paths_file: ?[]const u8, @@ -4158,7 +4129,7 @@ fn createModule( var unresolved_link_inputs: std.ArrayList(link.UnresolvedInput) = .empty; defer unresolved_link_inputs.deinit(gpa); try unresolved_link_inputs.ensureUnusedCapacity(gpa, create_module.cli_link_inputs.items.len); - var any_name_queries_remaining = false; + var any_named_library_queries = false; for (create_module.cli_link_inputs.items) |cli_link_input| switch (cli_link_input) { .name_query => |nq| { const lib_name = nq.name; @@ -4199,39 +4170,52 @@ fn createModule( } unresolved_link_inputs.appendAssumeCapacity(cli_link_input); - any_name_queries_remaining = true; + any_named_library_queries = true; + }, + .framework_query => { + unresolved_link_inputs.appendAssumeCapacity(cli_link_input); + any_named_library_queries = true; }, else => { unresolved_link_inputs.appendAssumeCapacity(cli_link_input); }, }; // After this point, unresolved_link_inputs is used instead of cli_link_inputs. - if (any_name_queries_remaining) create_module.want_native_include_dirs = true; + if (any_named_library_queries) create_module.want_native_include_dirs = true; // Resolve the library path arguments with respect to sysroot. try create_module.lib_directories.ensureUnusedCapacity(arena, create_module.lib_dir_args.items.len); - if (create_module.sysroot) |root| { - for (create_module.lib_dir_args.items) |lib_dir_arg| { - if (fs.path.isAbsolute(lib_dir_arg)) { - const stripped_dir = lib_dir_arg[fs.path.parsePath(lib_dir_arg).root.len..]; - const full_path = try fs.path.join(arena, &[_][]const u8{ root, stripped_dir }); - addLibDirectoryWarn(io, &create_module.lib_directories, full_path); - } else { - addLibDirectoryWarn(io, &create_module.lib_directories, lib_dir_arg); + for (create_module.lib_dir_args.items) |dir_arg| { + const path: []const u8 = path: { + if (fs.path.isAbsolute(dir_arg)) { + if (create_module.sysroot) |sysroot| { + // Change the path root to the given sysroot. + const stripped_dir = dir_arg[fs.path.parsePath(dir_arg).root.len..]; + break :path try fs.path.join(arena, &.{ sysroot, stripped_dir }); + } } - } - } else { - for (create_module.lib_dir_args.items) |lib_dir_arg| { - addLibDirectoryWarn(io, &create_module.lib_directories, lib_dir_arg); - } + break :path dir_arg; + }; + appendLibDirOrWarn(io, &create_module.lib_directories, path); } create_module.lib_dir_args = undefined; // From here we use lib_directories instead. - if (resolved_target.is_native_os and target.os.tag.isDarwin()) { - // If we want to link against frameworks, we need system headers. - if (create_module.frameworks.count() > 0) - create_module.want_native_include_dirs = true; + // Likewise for framework path arguments. + try create_module.framework_directories.ensureUnusedCapacity(arena, create_module.framework_dir_args.items.len); + for (create_module.framework_dir_args.items) |dir_arg| { + const path: []const u8 = path: { + if (fs.path.isAbsolute(dir_arg)) { + if (create_module.sysroot) |sysroot| { + // Change the path root to the given sysroot. + const stripped_dir = dir_arg[fs.path.parsePath(dir_arg).root.len..]; + break :path try fs.path.join(arena, &.{ sysroot, stripped_dir }); + } + } + break :path dir_arg; + }; + appendLibDirOrWarn(io, &create_module.framework_directories, path); } + create_module.framework_dir_args = undefined; // From here we use framework_directories instead. if (create_module.each_lib_rpath orelse resolved_target.is_native_os) { try create_module.rpath_list.ensureUnusedCapacity(arena, create_module.lib_directories.items.len); @@ -4253,11 +4237,17 @@ fn createModule( create_module.native_system_include_paths = try paths.include_dirs.toOwnedSlice(arena); - try create_module.framework_dirs.appendSlice(arena, paths.framework_dirs.items); try create_module.rpath_list.appendSlice(arena, paths.rpaths.items); try create_module.lib_directories.ensureUnusedCapacity(arena, paths.lib_dirs.items.len); - for (paths.lib_dirs.items) |path| addLibDirectoryWarn2(io, &create_module.lib_directories, path, true); + for (paths.lib_dirs.items) |path| { + appendLibDirOrWarnAllowMissing(io, &create_module.lib_directories, path); + } + + try create_module.framework_directories.ensureUnusedCapacity(arena, paths.framework_dirs.items.len); + for (paths.framework_dirs.items) |path| { + appendLibDirOrWarnAllowMissing(io, &create_module.framework_directories, path); + } } if (create_module.libc_paths_file) |paths_file| { @@ -4266,7 +4256,7 @@ fn createModule( } if (target.os.tag == .windows and (target.abi == .msvc or target.abi == .itanium) and - any_name_queries_remaining) + any_named_library_queries) { if (create_module.libc_installation == null) { create_module.libc_installation = LibCInstallation.findNative(arena, io, .{ @@ -4277,9 +4267,19 @@ fn createModule( fatal("unable to find native libc installation: {t}", .{err}); }; } + try create_module.lib_directories.ensureUnusedCapacity(arena, 2); - addLibDirectoryWarn(io, &create_module.lib_directories, create_module.libc_installation.?.msvc_lib_dir.?); - addLibDirectoryWarn(io, &create_module.lib_directories, create_module.libc_installation.?.kernel32_lib_dir.?); + + appendLibDirOrWarn( + io, + &create_module.lib_directories, + create_module.libc_installation.?.msvc_lib_dir.?, + ); + appendLibDirOrWarn( + io, + &create_module.lib_directories, + create_module.libc_installation.?.kernel32_lib_dir.?, + ); } // Destructively mutates but does not transfer ownership of `unresolved_link_inputs`. @@ -4291,6 +4291,7 @@ fn createModule( &unresolved_link_inputs, &create_module.link_inputs, create_module.lib_directories.items, + create_module.framework_directories.items, color, ) catch |err| fatal("failed to resolve link inputs: {t}", .{err}); @@ -6085,33 +6086,6 @@ const ClangSearchSanitizer = struct { }; }; -fn accessFrameworkPath( - io: Io, - test_path: *std.array_list.Managed(u8), - checked_paths: *std.array_list.Managed(u8), - framework_dir_path: []const u8, - framework_name: []const u8, -) !bool { - const sep = fs.path.sep_str; - - for (&[_][]const u8{ ".tbd", ".dylib", "" }) |ext| { - test_path.clearRetainingCapacity(); - try test_path.print("{s}" ++ sep ++ "{s}.framework" ++ sep ++ "{s}{s}", .{ - framework_dir_path, framework_name, framework_name, ext, - }); - try checked_paths.print("\n {s}", .{test_path.items}); - Io.Dir.cwd().access(io, test_path.items, .{}) catch |err| switch (err) { - error.FileNotFound => continue, - else => |e| fatal("unable to search for {s} framework {q}: {t}", .{ - ext, test_path.items, e, - }), - }; - return true; - } - - return false; -} - fn parseRcIncludes(arg: []const u8) std.zig.RcIncludes { return stringToEnum(std.zig.RcIncludes, arg) orelse fatal("unsupported rc includes type: {q}", .{arg}); @@ -6222,24 +6196,26 @@ fn anyObjectLinkInputs(link_inputs: []const link.UnresolvedInput) bool { return false; } -fn addLibDirectoryWarn(io: Io, lib_directories: *std.ArrayList(Directory), path: []const u8) void { - return addLibDirectoryWarn2(io, lib_directories, path, false); +fn appendLibDirOrWarn(io: Io, dirs: *std.ArrayList(Directory), path: []const u8) void { + if (Io.Dir.cwd().openDir(io, path, .{})) |handle| { + dirs.appendAssumeCapacity(.{ + .path = path, + .handle = handle, + }); + } else |err| { + warn("unable to open library directory {q}: {t}", .{ path, err }); + } } - -fn addLibDirectoryWarn2( - io: Io, - lib_directories: *std.ArrayList(Directory), - path: []const u8, - ignore_not_found: bool, -) void { - lib_directories.appendAssumeCapacity(.{ - .handle = Io.Dir.cwd().openDir(io, path, .{}) catch |err| { - if (err == error.FileNotFound and ignore_not_found) return; - warn("unable to open library directory {q}: {t}", .{ path, err }); - return; - }, - .path = path, - }); +fn appendLibDirOrWarnAllowMissing(io: Io, dirs: *std.ArrayList(Directory), path: []const u8) void { + if (Io.Dir.cwd().openDir(io, path, .{})) |handle| { + dirs.appendAssumeCapacity(.{ + .path = path, + .handle = handle, + }); + } else |err| switch (err) { + error.FileNotFound => {}, // ignore + else => |e| warn("unable to open library directory {q}: {t}", .{ path, e }), + } } const IoImpl = switch (build_options.io_mode) { -- 2.54.0 From 80c140ea31a625acc480122613f7d477173d3ebd Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Tue, 22 Sep 2026 22:49:29 +0100 Subject: [PATCH 09/10] MachO2: emit a valid executable We're still not doing anything useful, but we now emit a valid Mach-O file, with a few basic load commands. This is enough that the following command now emits a binary which will run on a macOS system configured to allow unsigned executables: ``` zig build-exe -target aarch64-macos -fno-compiler-rt -fno-sanitize-c -fnew-linker -femit-bin=empty ``` Of course, the binary crashes immediately since it does not contain any instructions, but just getting something to run without being killed by the kernel or dyld is progress. --- src/link/Elf2.zig | 2 +- src/link/MachO2.zig | 1164 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 1122 insertions(+), 44 deletions(-) diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index 26d3d346f62289221cf549306e04f98b74c0809e..37d7dfe0499aeb8a9b80e983e6eb8785739eb75e 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -12355,7 +12355,7 @@ pub fn printNode( mf_node.flags.alignment.toByteUnits(), mf_node.flags.position, if (mf_node.flags.bubbles_moved) " bubbles_moved" else "", - if (mf_node.flags.resized) " moved" else "", + if (mf_node.flags.moved) " moved" else "", if (mf_node.flags.resized) " resized" else "", if (mf_node.flags.enable_next_moved) " enable_next_moved" else "", if (mf_node.flags.next_moved) " next_moved" else "", diff --git a/src/link/MachO2.zig b/src/link/MachO2.zig index 47e97ba57d1ef1313bcfa48bb244b7d8730e8536..f5583cb7915a24b4a658636d35a77f76e7923cfb 100644 --- a/src/link/MachO2.zig +++ b/src/link/MachO2.zig @@ -2,22 +2,223 @@ const MachO = @This(); base: link.File, options: link.File.OpenOptions, -mf: MappedFile, -nodes: std.MultiArrayList(Node), const_prog_node: std.Progress.Node, synth_prog_node: std.Progress.Node, input_prog_node: std.Progress.Node, +mf: MappedFile, +nodes: std.MultiArrayList(Node), + +node: struct { + macho: MappedFile.Node.Index, + header: MappedFile.Node.Index, + symtab: MappedFile.Node.Index, + strtab: MappedFile.Node.Index, +}, + +lc: struct { + offsets: std.ArrayList(u32), + pagezero: LoadCommand, + symtab: LoadCommand, + entry_point: ?LoadCommand, +}, +/// Does not contain an entry for the `__PAGEZERO` segment. +segments: std.MultiArrayList(struct { + lc: LoadCommand, + node: MappedFile.Node.Index, +}), +sections: std.MultiArrayList(struct { + lc: LoadCommand, + node: MappedFile.Node.Index, + first_symbol: Symbol.Index.Optional, + index: u8, +}), +/// Accessed with `Segment.NameAdapter` using `[]const u8` keys. +segments_by_name: std.array_hash_map.Custom(void, void, void, true), +/// Accessed with `Section.NameAdapter` using `Section.NameAdapter.SegmentAndSection`. +sections_by_name: std.array_hash_map.Custom(void, void, void, true), + +/// Accessed with `String.Adapter` using `[]const u8` keys. +strtab: std.array_hash_map.Custom(String, void, void, true), + +symtab: std.ArrayList(Symbol), + +sections_changed_index: std.array_hash_map.Auto(Section, void), + +const Error = link.Error || error{MappedFileIo}; + +/// Stable reference to a particular load command (or `std.macho.section_64`, despite those not +/// technically being load commands). Underlying value is an index into `MachO.lc.offsets`. +const LoadCommand = enum(u32) { + _, + fn offset(lc: LoadCommand, macho: *const MachO) u32 { + return macho.lc.offsets.items[@backingInt(lc)]; + } + fn ptrConst(lc: LoadCommand, macho: *const MachO, comptime Cmd: type) *align(8) const Cmd { + return @ptrCast(@alignCast( + macho.node.header.sliceConst(&macho.mf)[lc.offset(macho)..][0..@sizeOf(Cmd)], + )); + } + fn ptr(lc: LoadCommand, macho: *MachO, comptime Cmd: type) *align(8) Cmd { + return @ptrCast(@alignCast( + macho.node.header.slice(&macho.mf)[lc.offset(macho)..][0..@sizeOf(Cmd)], + )); + } +}; + +/// Stable reference to a segment defined by a `std.macho.segment_command_64` load command. +/// +/// This type is backed by `u8` instead of a larger type because Mach-O itself uses 8-bit indices to +/// refer to sections, so the limitation exists regardless. +const Segment = enum(u8) { + _, + + fn lc(seg: Segment, macho: *const MachO) LoadCommand { + return macho.segments.items(.lc)[@backingInt(seg)]; + } + fn lcPtrConst(seg: Segment, macho: *const MachO) *const std.macho.segment_command_64 { + return seg.lc(macho).ptrConst(macho, std.macho.segment_command_64); + } + fn lcPtr(seg: Segment, macho: *MachO) *std.macho.segment_command_64 { + return seg.lc(macho).ptr(macho, std.macho.segment_command_64); + } + + fn name(seg: Segment, macho: *const MachO) []const u8 { + return std.mem.sliceTo(&seg.lcPtrConst(macho).segname, 0); + } + + fn vaddr(seg: Segment, macho: *const MachO) u64 { + return macho.targetLoad(&seg.lcPtrConst(macho).vmaddr); + } + + fn node(seg: Segment, macho: *const MachO) MappedFile.Node.Index { + return macho.segments.items(.node)[@backingInt(seg)]; + } + + const NameAdapter = struct { + macho: *const MachO, + pub fn eql(ctx: NameAdapter, a_seg_name: []const u8, _: void, b_seg_raw: usize) bool { + const b_seg: Segment = @fromBackingInt(@intCast(b_seg_raw)); + return std.mem.eql(u8, a_seg_name, b_seg.name(ctx.macho)); + } + pub fn hash(ctx: NameAdapter, a_seg_name: []const u8) u32 { + _ = ctx; + return std.array_hash_map.hashString(a_seg_name); + } + }; +}; + +/// Stable reference to a section defined by a `std.macho.section_64` in the load commands. +/// +/// This type is backed by `u8` instead of a larger type because Mach-O itself uses 8-bit indices to +/// refer to sections, so the limitation exists regardless. +/// +/// However, the backing integer of a `Section` is *not* the same as the section index, because +/// section indices can change. For the index of a section, see `Section.index`. +const Section = enum(u8) { + _, + + fn lc(sect: Section, macho: *const MachO) LoadCommand { + return macho.sections.items(.lc)[@backingInt(sect)]; + } + fn lcPtrConst(sect: Section, macho: *const MachO) *const std.macho.section_64 { + return sect.lc(macho).ptrConst(macho, std.macho.section_64); + } + fn lcPtr(sect: Section, macho: *MachO) *std.macho.section_64 { + return sect.lc(macho).ptr(macho, std.macho.section_64); + } + + fn name(sect: Section, macho: *const MachO) []const u8 { + return std.mem.sliceTo(§.lcPtrConst(macho).sectname, 0); + } + + fn segmentName(sect: Section, macho: *const MachO) []const u8 { + return std.mem.sliceTo(§.lcPtrConst(macho).segname, 0); + } + + fn node(sect: Section, macho: *const MachO) MappedFile.Node.Index { + return macho.sections.items(.node)[@backingInt(sect)]; + } + + /// Returns this section's index in the Mach-O file. Section indices are implicitly assigned + /// based on the order of load commands, so this value may change in response to segments or + /// sections being added. When this value changes for a given `Section`, it is added to + /// `MachO.sections_changed_index`, so that references to the section index can be updated. + fn index(sect: Section, macho: *const MachO) u8 { + return macho.sections.items(.index)[@backingInt(sect)]; + } + + /// Returns the head of a linked list of symbols defined relative to this section. + fn firstSymbol(sect: Section, macho: *MachO) *Symbol.Index.Optional { + return &macho.sections.items(.first_symbol)[@backingInt(sect)]; + } + + const NameAdapter = struct { + macho: *const MachO, + const SegmentAndSection = struct { + segment: []const u8, + section: []const u8, + }; + pub fn eql(ctx: NameAdapter, a_name: SegmentAndSection, _: void, b_sect_raw: usize) bool { + const b_sect: Section = @fromBackingInt(@intCast(b_sect_raw)); + return std.mem.eql(u8, a_name.segment, b_sect.segmentName(ctx.macho)) and + std.mem.eql(u8, a_name.section, b_sect.name(ctx.macho)); + } + pub fn hash(ctx: NameAdapter, a_name: SegmentAndSection) u32 { + _ = ctx; + var h: std.hash.Wyhash = .init(a_name.section.len); + h.update(a_name.section); + h.update(a_name.segment); + return @truncate(h.final()); + } + }; +}; + +const Symbol = struct { + next_in_section: Symbol.Index.Optional, + + const Index = enum(u32) { + _, + fn ptr(i: Index, macho: *MachO) *Symbol { + return &macho.symtab.items[@backingInt(i)]; + } + fn nlist(i: Index, macho: *MachO) *std.macho.nlist_64 { + return &macho.nlistSlice()[@backingInt(i)]; + } + const Optional = enum(u32) { + none = std.math.maxInt(u32), + _, + fn unwrap(o: Optional) ?Index { + return switch (o) { + .none => null, + _ => @fromBackingInt(@backingInt(o)), + }; + } + fn wrap(i: Index) Optional { + return @bitCast(i); + } + }; + }; +}; + const Node = union(enum) { macho, + /// Contains the `mach_header_64` structure and all load commands. Inside of the `.segment` node for the `__TEXT` segment. + macho_header, + + symtab, + strtab, + + segment: Segment, + section: Section, }; -fn getNode(macho: *MachO, ni: MappedFile.Node.Index) Node { +fn getNode(macho: *const MachO, ni: MappedFile.Node.Index) Node { return macho.nodes.get(@backingInt(ni)); } pub fn open( - arena: std.mem.Allocator, + arena: Allocator, comp: *Compilation, path: std.Build.Cache.Path, options: link.File.OpenOptions, @@ -25,7 +226,7 @@ pub fn open( return create(arena, comp, path, options); } pub fn createEmpty( - arena: std.mem.Allocator, + arena: Allocator, comp: *Compilation, path: std.Build.Cache.Path, options: link.File.OpenOptions, @@ -33,7 +234,7 @@ pub fn createEmpty( return create(arena, comp, path, options); } fn create( - arena: std.mem.Allocator, + arena: Allocator, comp: *Compilation, path: std.Build.Cache.Path, options: link.File.OpenOptions, @@ -61,11 +262,30 @@ fn create( .stack_size = 0, }, .options = options, - .mf = try .init(file, comp.gpa, io), - .nodes = .empty, + .const_prog_node = .none, .synth_prog_node = .none, .input_prog_node = .none, + + .mf = try .init(file, comp.gpa, io), + .nodes = .empty, + + .node = undefined, + .lc = .{ + .offsets = .empty, + .pagezero = undefined, + .symtab = undefined, + .entry_point = null, + }, + .segments = .empty, + .sections = .empty, + .segments_by_name = .empty, + .sections_by_name = .empty, + + .strtab = .empty, + .symtab = .empty, + + .sections_changed_index = .empty, }; errdefer macho.deinit(); @@ -77,15 +297,679 @@ fn initInner(macho: *MachO) !void { const comp = macho.base.comp; const gpa = comp.gpa; - try macho.nodes.append(gpa, .macho); // file root node + macho.node.macho = .root; + try macho.nodes.append(gpa, .macho); - @panic("TODO"); + const arch: enum { aarch64, x86_64 } = switch (comp.getTarget().cpu.arch) { + .aarch64 => .aarch64, + .x86_64 => .x86_64, + else => |arch| std.debug.panic("TODO: unsupported target arch {t}", .{arch}), + }; + + const mach_header_64 = std.macho.mach_header_64; + + // The `__TEXT` segment is special because it contains the mach header and load commands, so we + // must create the text segment node before we can actually call `addSegment`. + const text_seg_node = try macho.node.macho.addOnlyHeaderChild(gpa, &macho.mf, .{ + .alignment = macho.mf.flags.block_size.max(macho.targetPageAlign()), + .moved = true, // required by `addSegment` + .bubbles_moved = false, + }); + try macho.nodes.append(gpa, .{ .segment = undefined }); // populated by `addSegment` + + macho.node.header = try text_seg_node.addOnlyHeaderChild(gpa, &macho.mf, .{ + .size = @sizeOf(mach_header_64), + .alignment = .@"8", + }); + try macho.nodes.append(gpa, .macho_header); + + const mach_header: *mach_header_64 = @ptrCast(@alignCast( + macho.node.header.slice(&macho.mf)[0..@sizeOf(mach_header_64)], + )); + mach_header.* = .{ + .cputype = switch (arch) { + .aarch64 => std.macho.CPU_TYPE_ARM64, + .x86_64 => std.macho.CPU_TYPE_X86_64, + }, + .cpusubtype = switch (arch) { + .aarch64 => std.macho.CPU_SUBTYPE_ARM_ALL, + .x86_64 => std.macho.CPU_SUBTYPE_X86_64_ALL, + }, + .filetype = switch (comp.config.output_mode) { + .Exe => std.macho.MH_EXECUTE, + .Lib => switch (comp.config.link_mode) { + .static => std.macho.MH_OBJECT, + .dynamic => std.macho.MH_DYLIB, + }, + .Obj => std.macho.MH_OBJECT, + }, + .ncmds = 0, + .sizeofcmds = 0, + .flags = std.macho.MH_DYLDLINK | std.macho.MH_PIE, + }; + if (macho.targetEndian() != std.lang.Endian.native) { + std.mem.byteSwapAllFields(mach_header_64, mach_header); + } + + // The "__LINKEDIT" segment is special in that it needs to be at the end of the Mach-O file, so + // like "__TEXT", we'll construct its node here instead of in `addSegment`. + const linkedit_seg_node = try macho.node.macho.addFooterChildBefore(gpa, &macho.mf, .none, .{ + .alignment = macho.mf.flags.block_size.max(macho.targetPageAlign()), + .moved = true, // required by `addSegment` + .bubbles_moved = false, + }); + try macho.nodes.append(gpa, .{ .segment = undefined }); // populated by `addSegment` + + // The "__PAGEZERO" segment must come before any other segment load commands. It is inaccessible + // and occupies 4 GiB (apparently to catch invalid 32-bit pointer accesses). + macho.lc.pagezero = try macho.appendSimpleLoadCommand(std.macho.segment_command_64, .{ + .cmdsize = @sizeOf(std.macho.segment_command_64), + .segname = "__PAGEZERO\x00\x00\x00\x00\x00\x00".*, + .vmaddr = 0, + .vmsize = 0x1_0000_0000, + .fileoff = 0, + .filesize = 0, + .maxprot = .{}, + .initprot = .{}, + .nsects = 0, + .flags = 0, + }); + const text_seg = try macho.addSegment(.{ + .name = "__TEXT", + .prot = .{ .READ = true, .EXEC = true }, + .existing_node = text_seg_node, + }); + const data_seg = try macho.addSegment(.{ + .name = "__DATA", + .prot = .{ .READ = true, .WRITE = true }, + }); + const linkedit_seg = try macho.addSegment(.{ + .name = "__LINKEDIT", + .prot = .{ .READ = true }, + .existing_node = linkedit_seg_node, + }); + + _ = try macho.addSection(.{ + .segment = text_seg, + .name = "__text", + .@"align" = .@"4", + .flags = .{ .some_instructions = true }, + }); + _ = try macho.addSection(.{ + .segment = text_seg, + .name = "__const", + .@"align" = .@"2", + .flags = .{}, + }); + _ = try macho.addSection(.{ + .segment = data_seg, + .name = "__data", + .@"align" = .@"2", + .flags = .{}, + }); + + macho.node.symtab = try linkedit_seg.node(macho).addFloatingChild(gpa, &macho.mf, .{ + .alignment = macho.mf.flags.block_size.max(.of(std.macho.nlist_64)), + .moved = true, // so that `symoff` in the symtab load command will be set + }); + try macho.nodes.append(gpa, .symtab); + + macho.node.strtab = try linkedit_seg.node(macho).addFloatingChild(gpa, &macho.mf, .{ + .alignment = macho.mf.flags.block_size, + .moved = true, // so that `stroff` in the symtab load command will be set + }); + try macho.nodes.append(gpa, .strtab); + + try macho.node.strtab.ensureMinimumSize(gpa, &macho.mf, 1); + macho.node.strtab.slice(&macho.mf)[0] = 0; + + _ = try macho.appendSimpleLoadCommand(std.macho.dyld_info_command, .{}); + + macho.lc.symtab = try macho.appendSimpleLoadCommand(std.macho.symtab_command, .{ + .symoff = 0, + .nsyms = 0, + .stroff = 0, + .strsize = 1, // empty string (null terminator) + }); + + const want_dyld_path: bool = switch (comp.config.output_mode) { + .Obj => false, + .Lib => switch (comp.config.link_mode) { + .static => false, + .dynamic => comp.root_mod.resolved_target.is_explicit_dynamic_linker, + }, + .Exe => true, + }; + if (want_dyld_path) { + if (comp.getTarget().dynamic_linker.get()) |dyld_path| { + const cmd = try macho.appendLoadCommand(std.macho.dylinker_command, .{ + .cmd = .LOAD_DYLINKER, + .cmdsize = std.mem.alignForward(u32, @intCast( + @sizeOf(std.macho.dylinker_command) + dyld_path.len + 1, + ), 8), + .name = @sizeOf(std.macho.dylinker_command), + }); + @memcpy(cmd.trailing[0..dyld_path.len], dyld_path); + @memset(cmd.trailing[dyld_path.len..], 0); + } + } + + switch (comp.config.output_mode) { + .Obj, .Lib => {}, + .Exe => macho.lc.entry_point = try macho.appendSimpleLoadCommand(std.macho.entry_point_command, .{ + .entryoff = 0, + .stacksize = macho.options.stack_size orelse 0, + }), + } + + _ = try macho.appendSimpleLoadCommand(std.macho.uuid_command, .{ + .uuid = @splat(0), + }); + + try macho.addSimpleSymbol("some_function", 0x1234, "__TEXT", "__text"); + try macho.addSimpleSymbol("my_cool_data", 0x5678, "__DATA", "__data"); + + try macho.updateSegmentLoadAddresses(); +} +/// TODO: this function doesn't actually do anything particularly useful, it's just a placeholder +/// for actual symbol creation logic so that we can test adding stuff to the symtab. +fn addSimpleSymbol( + macho: *MachO, + name: []const u8, + value: u64, + segment_name: []const u8, + section_name: []const u8, +) Error!void { + const gpa = macho.base.comp.gpa; + try macho.symtab.ensureUnusedCapacity(gpa, 1); + try macho.node.symtab.ensureMinimumSize(gpa, &macho.mf, (macho.symtab.items.len + 1) * @sizeOf(std.macho.nlist_64)); + + const strx = try macho.string(name); + const section = macho.sectionByName(segment_name, section_name).?; + + const symtab_ptr = macho.lc.symtab.ptr(macho, std.macho.symtab_command); + assert(macho.targetLoad(&symtab_ptr.nsyms) == macho.symtab.items.len); + macho.targetStore(&symtab_ptr.nsyms, @intCast(macho.symtab.items.len + 1)); + + const sym_index: Symbol.Index = @fromBackingInt(@intCast(macho.symtab.items.len)); + macho.symtab.appendAssumeCapacity(.{ + .next_in_section = section.firstSymbol(macho).*, + }); + section.firstSymbol(macho).* = .wrap(sym_index); + + sym_index.nlist(macho).* = .{ + .n_strx = @backingInt(strx), + .n_type = .{ .bits = .{ + .ext = true, + .type = .sect, + .pext = false, + .is_stab = 0, + } }, + .n_sect = section.index(macho), + .n_desc = .{ + .arm_thumb_def = false, + .referenced_dynamically = false, + .discarded_or_no_dead_strip = false, + .weak_ref = false, + .weak_def_or_ref_to_weak = false, + .symbol_resolver = false, + .alt_entry = false, + }, + .n_value = value, + }; + if (macho.targetEndian() != std.lang.Endian.native) { + std.mem.byteSwapAllFields(std.macho.nlist_64, sym_index.nlist(macho)); + } +} + +fn addSegment(macho: *MachO, opts: struct { + name: []const u8, + prot: std.macho.vm_prot_t, + /// The caller is allowed to create the section's node themselves, in which case it is passed in + /// here. The caller must initialize the `MachO.nodes` entry to `.{ .segment = undefined }`, and + /// the node must be created with the option `.moved = true`. + existing_node: ?MappedFile.Node.Index = null, +}) Error!Segment { + const gpa = macho.base.comp.gpa; + + const segment_command_64 = std.macho.segment_command_64; + + // We limit the number of segments to 255, because there can only be 255 *sections*, so there's + // no need to allow more. See corresponding check in `addSection`. + if (macho.segments.len == 255) { + return macho.base.comp.link_diags.fail("maximum segment count exceeded", .{}); + } + + try macho.nodes.ensureUnusedCapacity(gpa, 1); + try macho.segments.ensureUnusedCapacity(gpa, 1); + try macho.segments_by_name.ensureUnusedCapacity(gpa, 1); + + const root_ni: MappedFile.Node.Index = .root; + + const lc = try macho.appendSimpleLoadCommand(std.macho.segment_command_64, .{ + .cmdsize = @sizeOf(segment_command_64), + .segname = name: { + var name: [16]u8 = undefined; + @memcpy(name[0..opts.name.len], opts.name); + @memset(name[opts.name.len..], 0); + break :name name; + }, + .vmaddr = 0, + .vmsize = 0, + .fileoff = 0, // to be set by `flushMoved` + .filesize = 0, + .maxprot = opts.prot, + .initprot = opts.prot, + .nsects = 0, + .flags = 0, + }); + + const segment: Segment = @fromBackingInt(@intCast(macho.segments.len)); + const segment_node: MappedFile.Node.Index = node: { + if (opts.existing_node) |node| { + assert(node.hasMoved(&macho.mf)); // so that `fileoff` will be set + assert(macho.nodes.items(.tags)[@backingInt(node)] == .segment); + macho.nodes.set(@backingInt(node), .{ .segment = segment }); + break :node node; + } + const node = try root_ni.addFloatingChild(gpa, &macho.mf, .{ + .alignment = macho.mf.flags.block_size.max(macho.targetPageAlign()), + .moved = true, // so that `fileoff` will be set + .bubbles_moved = false, + }); + macho.nodes.appendAssumeCapacity(.{ .segment = segment }); + break :node node; + }; + macho.segments.appendAssumeCapacity(.{ + .lc = lc, + .node = segment_node, + }); + + { + const gop = macho.segments_by_name.getOrPutAssumeCapacityAdapted( + opts.name, + @as(Segment.NameAdapter, .{ .macho = macho }), + ); + assert(!gop.found_existing); + assert(gop.index == @backingInt(segment)); + } + + return segment; +} + +const String = enum(u32) { + empty = 0, + _, + fn slice(s: String, macho: *const MachO) [:0]const u8 { + const overlong = macho.node.strtab.slice(&macho.mf)[@backingInt(s)..]; + return overlong[0..std.mem.findScalar(u8, overlong, 0).? :0]; + } + + const Adapter = struct { + macho: *const MachO, + pub fn eql(ctx: Adapter, lhs_slice: []const u8, rhs: String, _: usize) bool { + const macho = ctx.macho; + return std.mem.eql(u8, lhs_slice, rhs.slice(macho)); + } + pub fn hash(ctx: Adapter, s: []const u8) u32 { + _ = ctx; + return std.array_hash_map.hashString(s); + } + }; +}; + +fn string(macho: *MachO, slice: []const u8) Error!String { + const gpa = macho.base.comp.gpa; + + const old_strsize = macho.targetLoad( + &macho.lc.symtab.ptr(macho, std.macho.symtab_command).strsize, + ); + try macho.node.strtab.ensureMinimumSize(gpa, &macho.mf, old_strsize + slice.len + 1); + + try macho.strtab.ensureUnusedCapacity(gpa, 1); + + errdefer comptime unreachable; + + const adapter: String.Adapter = .{ .macho = macho }; + const gop = macho.strtab.getOrPutAssumeCapacityAdapted(slice, adapter); + if (!gop.found_existing) { + macho.targetStore( + &macho.lc.symtab.ptr(macho, std.macho.symtab_command).strsize, + @intCast(old_strsize + slice.len + 1), + ); + gop.key_ptr.* = @fromBackingInt(old_strsize); + const dest_slice = macho.node.strtab.slice(&macho.mf)[old_strsize..]; + @memcpy(dest_slice[0..slice.len], slice); + dest_slice[slice.len] = 0; + } + return gop.key_ptr.*; +} + +fn nlistSlice(macho: *MachO) []std.macho.nlist_64 { + const nsyms = macho.targetLoad( + &macho.lc.symtab.ptr(macho, std.macho.symtab_command).nsyms, + ); + return @ptrCast(@alignCast( + macho.node.symtab.slice(&macho.mf)[0 .. nsyms * @sizeOf(std.macho.nlist_64)], + )); +} + +fn updateSegmentLoadAddresses(macho: *MachO) Allocator.Error!void { + // TODO: this segment load address allocation logic is bad because it moves segments quite + // frequently. We should avoid that by introducing padding between segments so that they have + // space to grow in-place. We could also consider allowing a segment which can't fit to jump + // over another segment, like in `Elf2.allocateSegmentLoadAddress`. + var cur_addr: u64 = start_addr: { + const pagezero_ptr = macho.lc.pagezero.ptr(macho, std.macho.segment_command_64); + assert(macho.targetLoad(&pagezero_ptr.vmaddr) == 0); + break :start_addr macho.targetLoad(&pagezero_ptr.vmsize); + }; + for (0..macho.segments.len) |segment_raw| { + const segment: Segment = @fromBackingInt(@intCast(segment_raw)); + + _, const size: u64 = segment.node(macho).location(&macho.mf).resolve(&macho.mf); + const cmd_ptr = segment.lcPtr(macho); + + macho.targetStore(&cmd_ptr.vmsize, size); + + const old_addr = macho.targetLoad(&cmd_ptr.vmaddr); + if (old_addr >= cur_addr) { + cur_addr = old_addr; + } else { + macho.targetStore(&cmd_ptr.vmaddr, cur_addr); + try segment.node(macho).childrenMoved(macho.base.comp.gpa, &macho.mf); + } + + cur_addr = macho.targetPageAlign().forward(cur_addr + size); + } +} + +/// Like `appendLoadCommand`, but asserts that `cmd.cmdsize` exactly equals `@sizeOf(Cmd)` and does +/// not return the (empty) trailing slice. +fn appendSimpleLoadCommand( + macho: *MachO, + comptime Cmd: type, + cmd: Cmd, +) Error!LoadCommand { + comptime assert(@sizeOf(Cmd) % 8 == 0); + assert(cmd.cmdsize == @sizeOf(Cmd)); + return (try macho.appendLoadCommand(Cmd, cmd)).lc; +} + +/// Asserts that the first two fields of `Cmd` are `cmd: LC` and `cmdsize: u32`, and that the value +/// of `lc.cmdsize` is 8-byte aligned. +fn appendLoadCommand( + macho: *MachO, + comptime Cmd: type, + cmd: Cmd, +) Error!struct { + lc: LoadCommand, + trailing: []u8, +} { + const gpa = macho.base.comp.gpa; + + try macho.lc.offsets.ensureUnusedCapacity(gpa, 1); + + comptime { + const s = @typeInfo(Cmd).@"struct"; + assert(s.layout == .@"extern"); + // First field of load command should be `cmd: LC` + assert(std.mem.eql(u8, s.field_names[0], "cmd")); + assert(s.field_types[0] == std.macho.LC); + // Second field of load command should be `cmdsize: u32` + assert(std.mem.eql(u8, s.field_names[1], "cmdsize")); + assert(s.field_types[1] == u32); + } + + const cmd_size = @as(*const std.macho.load_command, @ptrCast(&cmd)).cmdsize; + assert(cmd_size % 8 == 0); + + const mach_header_64 = std.macho.mach_header_64; + const old_cmds_size: u32 = size: { + const mach_header: *const mach_header_64 = @ptrCast(@alignCast( + macho.node.header.sliceConst(&macho.mf)[0..@sizeOf(mach_header_64)], + )); + break :size macho.targetLoad(&mach_header.sizeofcmds); + }; + const new_cmds_size = old_cmds_size + cmd_size; + try macho.node.header.ensureMinimumSize(gpa, &macho.mf, @sizeOf(mach_header_64) + new_cmds_size); + + const header_slice = macho.node.header.slice(&macho.mf); + const mach_header: *mach_header_64 = @ptrCast(@alignCast( + header_slice[0..@sizeOf(mach_header_64)], + )); + macho.targetStore(&mach_header.sizeofcmds, new_cmds_size); + macho.targetStore(&mach_header.ncmds, macho.targetLoad(&mach_header.ncmds) + 1); + + const cmd_slice: []align(8) u8 = @alignCast( + header_slice[@sizeOf(mach_header_64) + old_cmds_size ..][0..cmd_size], + ); + + const cmd_ptr: *Cmd = @ptrCast(cmd_slice[0..@sizeOf(Cmd)]); + cmd_ptr.* = cmd; + if (macho.targetEndian() != std.lang.Endian.native) { + std.mem.byteSwapAllFields(Cmd, cmd_ptr); + } + + const lc: LoadCommand = @fromBackingInt(@intCast(macho.lc.offsets.items.len)); + macho.lc.offsets.appendAssumeCapacity(@sizeOf(mach_header_64) + old_cmds_size); + + return .{ .lc = lc, .trailing = cmd_slice[@sizeOf(Cmd)..] }; +} + +fn addSection(macho: *MachO, opts: struct { + name: []const u8, + segment: Segment, + @"align": MappedFile.Alignment, + /// This matches the layout of `std.macho.section_64.flags`. + flags: packed struct(u32) { + _unused0: u10 = 0, + some_instructions: bool = false, + _unused1: u21 = 0, + }, +}) Error!Section { + const gpa = macho.base.comp.gpa; + + const mach_header_64 = std.macho.mach_header_64; + const segment_command_64 = std.macho.segment_command_64; + const section_64 = std.macho.section_64; + + // Check if we can actually fit another section. The limit is 255 because Mach-O implicitly + // assigns 8-bit indices to sections starting from 1 (0 is reserved). + if (macho.sections.len == 255) { + return macho.base.comp.link_diags.fail("maximum section count exceeded", .{}); + } + + const old_cmds_size: u32 = size: { + const mach_header: *const mach_header_64 = @ptrCast(@alignCast( + macho.node.header.sliceConst(&macho.mf)[0..@sizeOf(mach_header_64)], + )); + break :size macho.targetLoad(&mach_header.sizeofcmds); + }; + const new_cmds_size = old_cmds_size + @sizeOf(section_64); + try macho.node.header.ensureMinimumSize(gpa, &macho.mf, @sizeOf(mach_header_64) + new_cmds_size); + + try macho.nodes.ensureUnusedCapacity(gpa, 1); + try macho.lc.offsets.ensureUnusedCapacity(gpa, 1); + try macho.sections.ensureUnusedCapacity(gpa, 1); + try macho.sections_by_name.ensureUnusedCapacity(gpa, 1); + + const section: Section = @fromBackingInt(@intCast(macho.sections.len)); + const section_node = try opts.segment.node(macho).addFloatingChild(gpa, &macho.mf, .{ + .alignment = opts.@"align", + .moved = true, // so that `offset` will be set + }); + macho.nodes.appendAssumeCapacity(.{ .section = section }); + macho.sections.appendAssumeCapacity(.{ + .lc = @fromBackingInt(@intCast(macho.lc.offsets.items.len)), + .node = section_node, + .first_symbol = .none, + .index = undefined, // populated later + }); + + { + const adapter: Section.NameAdapter = .{ .macho = macho }; + const key: Section.NameAdapter.SegmentAndSection = .{ + .segment = opts.segment.name(macho), + .section = opts.name, + }; + const gop = macho.sections_by_name.getOrPutAssumeCapacityAdapted(key, adapter); + assert(!gop.found_existing); + assert(gop.index == @backingInt(section)); + } + + const header_slice = macho.node.header.slice(&macho.mf); + + { + const mach_header: *mach_header_64 = @ptrCast(@alignCast( + header_slice[0..@sizeOf(mach_header_64)], + )); + macho.targetStore(&mach_header.sizeofcmds, new_cmds_size); + } + + const seg_cmd = opts.segment.lcPtr(macho); + const old_nsects = macho.targetLoad(&seg_cmd.nsects); + const old_cmdsize = macho.targetLoad(&seg_cmd.cmdsize); + assert(old_cmdsize == @sizeOf(segment_command_64) + old_nsects * @sizeOf(section_64)); + const new_cmdsize = old_cmdsize + @sizeOf(section_64); + macho.targetStore(&seg_cmd.cmdsize, new_cmdsize); + macho.targetStore(&seg_cmd.nsects, old_nsects + 1); + + const seg_cmd_off = opts.segment.lc(macho).offset(macho); + const offset = seg_cmd_off + old_cmdsize; + + // Shift forward every load command which comes after this segment command, to make space + // for the new `section_64` (and update the offsets in `macho.lc.offsets` accordingly). + @memmove( + header_slice[seg_cmd_off + new_cmdsize .. @sizeOf(mach_header_64) + new_cmds_size], + header_slice[seg_cmd_off + old_cmdsize .. @sizeOf(mach_header_64) + old_cmds_size], + ); + for (macho.lc.offsets.items) |*other_off| { + if (other_off.* >= offset) { + other_off.* += @sizeOf(section_64); + } + } + // Only now do we append our *own* offset (otherwise the above loop would have changed it!). + macho.lc.offsets.appendAssumeCapacity(offset); + + // Increment the index of every section after where we've inserted ourselves. + { + var new_section_index: u8 = 1; + try macho.sections_changed_index.ensureUnusedCapacity(gpa, macho.sections.len); + for (0..macho.sections.len) |other_section_raw| { + const other_section: Section = @fromBackingInt(@intCast(other_section_raw)); + if (other_section == section) continue; + if (other_section.lc(macho).offset(macho) > offset) { + macho.sections.items(.index)[@backingInt(other_section)] += 1; + if (other_section.firstSymbol(macho).* != .none) { + macho.sections_changed_index.putAssumeCapacity(other_section, {}); + } + } else { + new_section_index += 1; + } + } + macho.sections.items(.index)[@backingInt(section)] = new_section_index; + } + + // Write the new `section_64`. + const section_cmd: *section_64 = @ptrCast(@alignCast( + header_slice[offset..][0..@sizeOf(section_64)], + )); + section_cmd.* = .{ + .sectname = name: { + var name: [16]u8 = undefined; + @memcpy(name[0..opts.name.len], opts.name); + @memset(name[opts.name.len..], 0); + break :name name; + }, + .segname = seg_cmd.segname, + .addr = 0, + .size = 0, + .offset = 0, + .@"align" = opts.@"align".toLog2Units(), + .reloff = 0, + .nreloc = 0, + .flags = @bitCast(opts.flags), + }; + if (macho.targetEndian() != std.lang.Endian.native) { + std.mem.byteSwapAllFields(section_64, section_cmd); + } + + return section; +} + +fn segmentByName(macho: *const MachO, name: []const u8) ?Segment { + const adapter: Segment.NameAdapter = .{ .macho = macho }; + if (macho.segments_by_name.getIndexAdapted(name, adapter)) |segment_raw| { + return @fromBackingInt(@intCast(segment_raw)); + } else { + return null; + } +} + +fn sectionByName(macho: *const MachO, segment_name: []const u8, section_name: []const u8) ?Section { + const adapter: Section.NameAdapter = .{ .macho = macho }; + const key: Section.NameAdapter.SegmentAndSection = .{ + .segment = segment_name, + .section = section_name, + }; + if (macho.sections_by_name.getIndexAdapted(key, adapter)) |section_raw| { + return @fromBackingInt(@intCast(section_raw)); + } else { + return null; + } +} + +fn targetEndian(macho: *const MachO) std.lang.Endian { + _ = macho; + return .little; +} +fn targetPageAlign(macho: *const MachO) MappedFile.Alignment { + _ = macho; + return .fromByteUnits(0x4000); +} +fn targetLoad(macho: *const MachO, ptr: anytype) @typeInfo(@TypeOf(ptr)).pointer.child { + const pointer_ty = @typeInfo(@TypeOf(ptr)).pointer; + const Child = pointer_ty.child; + const alignment = pointer_ty.attrs.@"align" orelse @alignOf(Child); + return switch (@typeInfo(Child)) { + else => @compileError(@typeName(Child)), + .int => std.mem.toNative(Child, ptr.*, macho.targetEndian()), + .@"enum" => |@"enum"| @fromBackingInt(macho.targetLoad(@as(*align(alignment) const @"enum".tag_type, @ptrCast(ptr)))), + .@"struct" => |@"struct"| @bitCast( + macho.targetLoad(@as(*align(alignment) @"struct".backing_integer.?, @ptrCast(ptr))), + ), + }; +} +fn targetStore(macho: *const MachO, ptr: anytype, val: @typeInfo(@TypeOf(ptr)).pointer.child) void { + const pointer_ty = @typeInfo(@TypeOf(ptr)).pointer; + const Child = pointer_ty.child; + const alignment = pointer_ty.attrs.@"align" orelse @alignOf(Child); + return switch (@typeInfo(Child)) { + else => @compileError(@typeName(Child)), + .int => ptr.* = std.mem.nativeTo(Child, val, macho.targetEndian()), + .@"enum" => |@"enum"| macho.targetStore( + @as(*align(alignment) @"enum".tag_type, @ptrCast(ptr)), + @backingInt(val), + ), + .@"struct" => |@"struct"| macho.targetStore( + @as(*align(alignment) @"struct".backing_integer.?, @ptrCast(ptr)), + @bitCast(val), + ), + }; } pub fn deinit(macho: *MachO) void { const gpa = macho.base.comp.gpa; macho.mf.deinit(gpa); macho.nodes.deinit(gpa); + macho.segments.deinit(gpa); + macho.sections.deinit(gpa); + macho.segments_by_name.deinit(gpa); + macho.sections_by_name.deinit(gpa); + macho.strtab.deinit(gpa); + macho.symtab.deinit(gpa); + macho.sections_changed_index.deinit(gpa); macho.* = undefined; } @@ -134,30 +1018,110 @@ pub fn relocSymAddr( } pub fn loadInput(macho: *MachO, input: link.Input) link.Error!void { - _ = macho; - _ = input; - @panic("TODO"); + const diags = &macho.base.comp.link_diags; + macho.loadInputInner(input) catch |err| switch (err) { + error.MappedFileIo => return diags.fail("failed to write output file: {t}", .{macho.mf.io_err.?}), + else => |e| return e, + }; } -pub fn setDarwinSdkVersion(macho: *MachO, version: link.DarwinSdkVersion) link.Error!void { - _ = macho; - std.log.err("MLUGG TODO: sdk version is '{d}.{d}.{d}'", .{ version.major, version.minor, version.patch }); +fn loadInputInner(macho: *MachO, input: link.Input) Error!void { + switch (input) { + .res => unreachable, + .object => @panic("TODO(MachO2): load object"), + .archive => @panic("TODO(MachO2): load archive"), + .dso => @panic("TODO(MachO2): load dso"), + .tbd => |tbd| { + log.debug("load tbd {qf}", .{tbd.path.fmtEscapeString()}); + + // TODO: obviously this is completely wrong, we need to parse the tbd to determine the + // dylib install name; but for now this lets us link libSystem + const prefix = "/usr/lib/"; + const name = std.fs.path.stem(tbd.path.sub_path); + const suffix = ".dylib"; + + const cmd = try macho.appendLoadCommand(std.macho.dylib_command, .{ + .cmd = .LOAD_DYLIB, + .cmdsize = std.mem.alignForward(u32, @intCast( + @sizeOf(std.macho.dylib_command) + prefix.len + name.len + suffix.len + 1, + ), 8), + .dylib = .{ + .name = @sizeOf(std.macho.dylib_command), + .timestamp = 0, + .current_version = 0, + .compatibility_version = 0, + }, + }); + @memcpy(cmd.trailing[0..prefix.len], prefix); + @memcpy(cmd.trailing[prefix.len..][0..name.len], name); + @memcpy(cmd.trailing[prefix.len + name.len ..][0..suffix.len], suffix); + @memset(cmd.trailing[prefix.len + name.len + suffix.len ..], 0); + }, + } +} +pub fn setDarwinSdkVersion(macho: *MachO, sdk_version: link.DarwinSdkVersion) link.Error!void { + const comp = macho.base.comp; + const diags = &comp.link_diags; + const target = comp.getTarget(); + + const os_min_version: link.DarwinSdkVersion = .{ + .major = @intCast(target.os.version_range.semver.min.major), + .minor = @intCast(target.os.version_range.semver.min.minor), + .patch = @intCast(target.os.version_range.semver.min.patch), + }; + + _ = macho.appendSimpleLoadCommand(std.macho.version_min_command, .{ + .cmd = switch (target.os.tag) { + .macos => .VERSION_MIN_MACOSX, + .ios => .VERSION_MIN_IPHONEOS, + .tvos => .VERSION_MIN_TVOS, + .watchos => .VERSION_MIN_WATCHOS, + .maccatalyst, .driverkit, .visionos => |os_tag| std.debug.panic( + "TODO(MachO2): sdk version for '{t}'", + .{os_tag}, + ), + else => unreachable, + }, + .version = @backingInt(os_min_version), + .sdk = @backingInt(sdk_version), + }) catch |err| switch (err) { + error.MappedFileIo => return diags.fail("failed to write output file: {t}", .{macho.mf.io_err.?}), + else => |e| return e, + }; } pub fn prelink(macho: *MachO, prog_node: std.Progress.Node) link.Error!void { _ = macho; _ = prog_node; - @panic("TODO"); } pub fn flush( macho: *MachO, - arena: std.mem.Allocator, + arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node, ) link.Error!void { - _ = macho; + const diags = &macho.base.comp.link_diags; _ = arena; - _ = tid; _ = prog_node; - @panic("TODO"); + + try macho.updateSegmentLoadAddresses(); + while (try macho.idle(tid)) {} + + if (macho.lc.entry_point) |entry_point_lc| { + // TODO: actually find the entry point. + // For now, we just set the entry point to the start of the "__TEXT,__text" section. + const entry_node = macho.sectionByName("__TEXT", "__text").?.node(macho); + const entry_file_off: u64, _ = entry_node.location(&macho.mf).resolve(&macho.mf); + const entry_point_ptr = entry_point_lc.ptr(macho, std.macho.entry_point_command); + macho.targetStore(&entry_point_ptr.entryoff, @intCast(entry_file_off)); + } + + macho.mf.flush() catch |err| switch (err) { + error.MappedFileIo => return diags.fail("failed to write output file: {t}", .{macho.mf.io_err.?}), + else => |e| return e, + }; + + if (macho.options.enable_link_snapshots) + macho.dumpStderr(tid) catch |err| + return diags.fail("dumping link snapshot failed: {t}", .{err}); } pub fn updateErrorData(macho: *MachO, pt: Zcu.PerThread) link.Error!void { _ = macho; @@ -165,9 +1129,99 @@ pub fn updateErrorData(macho: *MachO, pt: Zcu.PerThread) link.Error!void { @panic("TODO"); } pub fn idle(macho: *MachO, tid: Zcu.PerThread.Id) link.Error!bool { - _ = macho; _ = tid; - @panic("TODO"); + + macho.mf.nodes_lock.lock(); + defer macho.mf.nodes_lock.unlock(); + + task: { + if (macho.sections_changed_index.pop()) |kv| { + const section = kv.key; + var sym_it = section.firstSymbol(macho).*; + while (sym_it.unwrap()) |sym| : (sym_it = sym.ptr(macho).next_in_section) { + macho.targetStore(&sym.nlist(macho).n_sect, section.index(macho)); + } + break :task; + } + + while (macho.mf.updates.pop()) |ni| { + const clean_moved = ni.cleanMoved(&macho.mf); + const clean_resized = ni.cleanResized(&macho.mf); + const clean_next_moved = ni.cleanNextMoved(&macho.mf); + if (!clean_moved and !clean_resized and !clean_next_moved) continue; + + const sub_prog_node = macho.mf.update_prog_node.start(@tagName(macho.getNode(ni)), 0); + defer sub_prog_node.end(); + + if (clean_moved) try macho.flushMoved(ni); + if (clean_resized) try macho.flushResized(ni); + + break :task; + } + } + + return macho.sections_changed_index.count() > 0 or + macho.mf.updates.items.len > 0; +} +fn flushMoved(macho: *MachO, ni: MappedFile.Node.Index) link.Error!void { + macho.flushMachOFileOffset(ni); + switch (macho.getNode(ni)) { + .macho => unreachable, + .macho_header => {}, + .segment => return, // segments don't bubble moved + .section => |section| { + const segment_vaddr: u64 = macho.getNode(ni.parent(&macho.mf).unwrap().?).segment.vaddr(macho); + const section_offset = ni.location(&macho.mf).resolve(&macho.mf)[0]; + macho.targetStore(§ion.lcPtr(macho).addr, segment_vaddr + section_offset); + }, + .symtab => {}, + .strtab => {}, + } + try ni.childrenMoved(macho.base.comp.gpa, &macho.mf); +} +fn flushMachOFileOffset(macho: *MachO, ni: MappedFile.Node.Index) void { + const offset = ni.fileLocation(&macho.mf, false).offset; + switch (macho.getNode(ni)) { + .macho => unreachable, + .macho_header => { + assert(offset == 0); + }, + .segment => |segment| { + macho.targetStore(&segment.lcPtr(macho).fileoff, @intCast(offset)); + var child_oni = ni.first(&macho.mf); + while (child_oni.unwrap()) |child_ni| : (child_oni = child_ni.next(&macho.mf)) { + macho.flushMachOFileOffset(child_ni); + } + }, + .section => |section| { + macho.targetStore(§ion.lcPtr(macho).offset, @intCast(offset)); + }, + .symtab => { + const symtab_cmd = macho.lc.symtab.ptr(macho, std.macho.symtab_command); + macho.targetStore(&symtab_cmd.symoff, @intCast(offset)); + }, + .strtab => { + const symtab_cmd = macho.lc.symtab.ptr(macho, std.macho.symtab_command); + macho.targetStore(&symtab_cmd.stroff, @intCast(offset)); + }, + } +} +fn flushResized(macho: *MachO, ni: MappedFile.Node.Index) link.Error!void { + const size = ni.location(&macho.mf).resolve(&macho.mf)[1]; + switch (macho.getNode(ni)) { + .macho => {}, + .macho_header => {}, + .segment => |segment| { + macho.targetStore(&segment.lcPtr(macho).filesize, size); + }, + .section => |section| { + macho.targetStore(§ion.lcPtr(macho).size, size); + }, + .symtab, .strtab => { + // Don't modify the symtab load command's `nsyms` or `strsize` field here; we only do + // that when we actually add strings or symbols. + }, + } } pub fn updateFunc( @@ -199,6 +1253,18 @@ pub fn updateExports( @panic("TODO"); } +fn dumpStderr(macho: *MachO, tid: Zcu.PerThread.Id) Io.File.Writer.Error!void { + const comp = macho.base.comp; + const io = comp.io; + var buffer: [512]u8 = undefined; + const stderr = try io.lockStderr(&buffer, null); + defer io.unlockStderr(); + const w = &stderr.file_writer.interface; + _ = macho.dump(w, tid) catch |err| switch (err) { + error.WriteFailed => return stderr.file_writer.err.?, + }; +} + pub fn dump(macho: *MachO, w: *Io.Writer, tid: Zcu.PerThread.Id) !link.File.DumpResult { if (macho.options.enable_link_snapshots) { try macho.printNode(tid, w, .root, 0); @@ -207,7 +1273,7 @@ pub fn dump(macho: *MachO, w: *Io.Writer, tid: Zcu.PerThread.Id) !link.File.Dump return .disabled; } fn printNode( - macho: *MachO, + macho: *const MachO, tid: Zcu.PerThread.Id, w: *Io.Writer, ni: MappedFile.Node.Index, @@ -217,6 +1283,11 @@ fn printNode( try w.splatByteAll(' ', indent); switch (node) { .macho => try w.writeAll("macho"), + .macho_header => try w.writeAll("macho_header"), + .section => |section| try w.print("section({q})", .{section.name(macho)}), + .segment => |segment| try w.print("segment({q})", .{segment.name(macho)}), + .symtab => try w.writeAll("symtab"), + .strtab => try w.writeAll("strtab"), } { const mf_node = &macho.mf.nodes.items[@backingInt(ni)]; @@ -228,7 +1299,7 @@ fn printNode( mf_node.flags.alignment.toByteUnits(), mf_node.flags.position, if (mf_node.flags.bubbles_moved) " bubbles_moved" else "", - if (mf_node.flags.resized) " moved" else "", + if (mf_node.flags.moved) " moved" else "", if (mf_node.flags.resized) " resized" else "", if (mf_node.flags.enable_next_moved) " enable_next_moved" else "", if (mf_node.flags.next_moved) " next_moved" else "", @@ -244,31 +1315,38 @@ fn printNode( } return; } - const file_loc = ni.fileLocation(&macho.mf, false); - var address = file_loc.offset; - if (file_loc.size == 0) { - try w.splatByteAll(' ', indent + 1); - try w.print("{x:0>8}\n", .{address}); - return; - } + const start_address: usize, const end_address: usize = file_loc: { + const file_loc = ni.fileLocation(&macho.mf, false); + break :file_loc .{ @intCast(file_loc.offset), @intCast(file_loc.offset + file_loc.size) }; + }; + var address = start_address; const line_len = 0x10; - var line_it = std.mem.window( - u8, - macho.mf.memory_map.memory[@intCast(file_loc.offset)..][0..@intCast(file_loc.size)], - line_len, - line_len, - ); - while (line_it.next()) |line_bytes| : (address += line_len) { + while (true) : (address = @min(std.mem.alignForward(usize, address + 1, line_len), end_address)) { try w.splatByteAll(' ', indent + 1); - try w.print("{x:0>8} ", .{address}); - for (line_bytes) |byte| try w.print("{x:0>2} ", .{byte}); - try w.splatByteAll(' ', 3 * (line_len - line_bytes.len) + 1); - for (line_bytes) |byte| try w.writeByte(if (std.ascii.isPrint(byte)) byte else '.'); + try w.print("{x:0>8}", .{address}); + if (address == end_address) break try w.writeByte('\n'); + try w.splatByteAll(' ', 2); + const start_byte_address = std.mem.alignBackward(usize, address, line_len); + const end_byte_address = start_byte_address + line_len; + for (start_byte_address..end_byte_address) |byte_address| + if (byte_address < start_address or byte_address >= end_address) + try w.splatByteAll(' ', 3) + else + try w.print("{x:0>2} ", .{macho.mf.memory_map.memory[byte_address]}); + try w.writeByte(' '); + for (start_byte_address..@min(end_address, end_byte_address)) |byte_address| + try w.writeByte(if (byte_address < start_address or byte_address >= end_address) ' ' else char: { + const byte = macho.mf.memory_map.memory[byte_address]; + break :char if (std.ascii.isPrint(byte)) byte else '.'; + }); try w.writeByte('\n'); } } const std = @import("std"); +const assert = std.debug.assert; +const log = std.log.scoped(.link); +const Allocator = std.mem.Allocator; const Io = std.Io; const link = @import("../link.zig"); -- 2.54.0 From 6418840f90d71a3c991bbb207ccdde4c9dc21d17 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Thu, 24 Sep 2026 21:36:36 +0100 Subject: [PATCH 10/10] link.MachO: work around non-deterministic link input order This commit should be reverted when https://codeberg.org/ziglang/zig/issues/36946 is fixed. --- src/link/MachO.zig | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/link/MachO.zig b/src/link/MachO.zig index b2b90f4d5091feee962fe301dfe6346692faa1b1..eb3e6d05432ca0c7af4a86b2468041229bccf414 100644 --- a/src/link/MachO.zig +++ b/src/link/MachO.zig @@ -354,6 +354,30 @@ pub fn flush( const sub_prog_node = prog_node.start("MachO Flush", 0); defer sub_prog_node.end(); + // TODO: due to https://codeberg.org/ziglang/zig/issues/36946, we receive link inputs in a + // non-deterministic order. To ensure that this linker behaves deterministically, sort the + // inputs into a consistent order now. + { + const InputSortContext = struct { + fn lessThan(ctx: @This(), lhs: link.Input, rhs: link.Input) bool { + _ = ctx; + // We can't sort by full path because that could vary between runs where we still + // want reproducibility (e.g. the path is affected by cwd). So instead, let's sort + // by basename---that's good enough for our purposes since we never link two + // vendored libraries/objects with the same name. + const lhs_name = fs.path.basename(lhs.path().sub_path); + const rhs_name = fs.path.basename(rhs.path().sub_path); + return std.mem.order(u8, lhs_name, rhs_name).compare(.lt); + } + }; + std.mem.sort( + link.Input, + self.all_inputs.items, + @as(InputSortContext, .{}), + InputSortContext.lessThan, + ); + } + // --verbose-link if (comp.verbose_link) try self.dumpArgv(comp); -- 2.54.0