From 95eb37a198271373b9c4918ce885bb959418e67e Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 31 May 2026 08:21:13 +0100 Subject: [PATCH 1/5] Compilation.Config: do not default to '-flld' if '-fnew-linker' given This causes a confusing CLI error. --- src/Compilation/Config.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Compilation/Config.zig b/src/Compilation/Config.zig index 2ce5c9abbe998e1f9841309004a262fa96cd2c42..62dfca1e4ba78962110220f42002d6effdb5af6a 100644 --- a/src/Compilation/Config.zig +++ b/src/Compilation/Config.zig @@ -409,6 +409,10 @@ pub fn resolve(options: Options) ResolveError!Config { if (options.use_lld) |x| break :b x; + // If the user didn't specify whether to use LLD but did specify to use the new linker, + // assume no LLD. + if (options.use_new_linker == true) break :b false; + // If we have no zig code to compile, no need for the self-hosted linker. if (!options.have_zcu) break :b true; -- 2.54.0 From 72f0ae7ec46c24d59aef2fae793275bede59ac8c Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 31 May 2026 08:21:59 +0100 Subject: [PATCH 2/5] Compilation.Config: improve error reporting for '-fnew-linker' ...and allow using `Elf2` with backends other than self-hosted x86_64. The new ELF linker is currently opt-in, so there's no harm in allowing backends which are not yet "officially" supported. (And also I'm going to make the LLVM backend work with `Elf2` in the next commit because that seems pretty trivial.) --- src/Compilation/Config.zig | 2 +- src/main.zig | 2 +- src/target.zig | 8 +++----- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Compilation/Config.zig b/src/Compilation/Config.zig index 62dfca1e4ba78962110220f42002d6effdb5af6a..ea9929bbcd70986deacc74d28f94660b060dd217 100644 --- a/src/Compilation/Config.zig +++ b/src/Compilation/Config.zig @@ -441,7 +441,7 @@ pub fn resolve(options: Options) ResolveError!Config { break :b false; } - if (!target_util.hasNewLinkerSupport(target.ofmt, backend)) { + if (!target_util.hasNewLinker(target.ofmt)) { if (options.use_new_linker == true) return error.NewLinkerIncompatibleObjectFormat; break :b false; } diff --git a/src/main.zig b/src/main.zig index 1e674077e461dc63c5be1c6a25dc0d0ca1056f12..7dc632c7b184298e3d4b7f5242cd69281ef5c334 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4137,8 +4137,8 @@ fn createModule( error.LldUnavailable => fatal("zig was compiled without LLD libraries", .{}), error.ClangUnavailable => fatal("zig was compiled without Clang libraries", .{}), error.DllExportFnsRequiresWindows => fatal("only Windows OS targets support DLLs", .{}), - error.NewLinkerIncompatibleObjectFormat => fatal("using the new linker to link {s} files is unsupported", .{@tagName(target.ofmt)}), error.NewLinkerIncompatibleWithLld => fatal("using the new linker is incompatible with using lld", .{}), + error.NewLinkerIncompatibleObjectFormat => fatal("no new linker available for '{t}' files", .{target.ofmt}), }; } diff --git a/src/target.zig b/src/target.zig index 56c429a13496f37f2f66c16a97d1c497992476f3..997dbb89453bf30bcbc5cb172000684c27d75ba6 100644 --- a/src/target.zig +++ b/src/target.zig @@ -275,12 +275,10 @@ pub fn hasLldSupport(ofmt: std.Target.ObjectFormat) bool { }; } -pub fn hasNewLinkerSupport(ofmt: std.Target.ObjectFormat, backend: std.lang.CompilerBackend) 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, .coff => switch (backend) { - .stage2_x86_64 => true, - else => false, - }, + .elf => true, else => false, }; } -- 2.54.0 From 41a617990513a0b6a0ad1f10ca8b78f9ea69cb28 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 31 May 2026 08:43:48 +0100 Subject: [PATCH 3/5] compiler: linker-agnostic handling of LLVM object This started as a small diff to `Elf2` to make it support the LLVM backend, but as I was writing it, I felt that I was just writing logic shared by every linker implementation. The object file emitted by the LLVM backend is a totally normal link input as far as the linker implementations are concerned---so, why not treat it as one? The `zcu_object_basename` field is removed from `link.File`, instead moved to `llvm.Object`. Logic in `link.Queue` avoids calling `prelink` when using the LLVM backend, because it knows we will receive another link input later. In `Compilation.flush`, after LLVM emits the ZCU object, we call `link.runPrelinkTask` to process that input, and then perform the deferred `prelink` call. This means that linker implementations which integrate with prelink don't need to be aware of LLVM whatsoever! (Aside from perhaps checking `use_llvm` to know if they are going to receive any ZCU tasks.) The `MachO` and `Lld` linker implementations still have specific handling for the ZCU object file from LLVM, because they do not currently integrate with `prelink`---but the `Coff`, `Elf`, `Wasm`, and `Elf2` implementations no longer need to handle this case specially. Note that the self-hosted linkers currently do not support incremental compilation with the LLVM backend---this is an existing issue, but I thought I'd mention it here because I wasn't previously aware of this bug. That is tracked by https://codeberg.org/ziglang/zig/issues/32053. --- src/Compilation.zig | 23 +++++++++++++++++------ src/codegen/llvm.zig | 14 ++++++++++++++ src/link.zig | 28 +++++++++++++++------------- src/link/Coff.zig | 2 +- src/link/Elf.zig | 33 ++------------------------------- src/link/Elf2.zig | 44 ++++++++++++++++++++++++-------------------- src/link/Lld.zig | 22 ++++++++-------------- src/link/MachO.zig | 20 ++++++++++---------- src/link/Queue.zig | 32 +++++++++++++++++++------------- src/link/Wasm.zig | 28 +--------------------------- 10 files changed, 111 insertions(+), 135 deletions(-) diff --git a/src/Compilation.zig b/src/Compilation.zig index 84b2febd6f05ccf76423196310bba22459a7f387..d50586581e3a530b39309573da3dffb933f988ec 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -3344,15 +3344,15 @@ fn flush(comp: *Compilation, arena: Allocator, tid: Zcu.PerThread.Id) (Io.Cancel comp.time_report.?.stats.real_ns_llvm_emit = ns; }; + const zcu_obj_path: ?Cache.Path = if (comp.bin_file != null) p: { + break :p try comp.resolveEmitPathFlush(arena, .temp, llvm_object.out_bin_basename); + } else null; + llvm_object.emit(pt, .{ .pre_ir_path = comp.verbose_llvm_ir, .pre_bc_path = comp.verbose_llvm_bc, - .bin_path = p: { - const lf = comp.bin_file orelse break :p null; - const p = try comp.resolveEmitPathFlush(arena, .temp, lf.zcu_object_basename.?); - break :p try p.toStringZ(arena); - }, + .bin_path = if (zcu_obj_path) |p| try p.toStringZ(arena) else null, .asm_path = p: { const raw = comp.emit_asm orelse break :p null; const p = try comp.resolveEmitPathFlush(arena, .artifact, raw); @@ -3379,6 +3379,17 @@ fn flush(comp: *Compilation, arena: Allocator, tid: Zcu.PerThread.Id) (Io.Cancel error.AlreadyReported => {}, error.OutOfMemory => |e| return e, }; + + if (zcu_obj_path) |path| { + // Tell the linker backend about the ZCU object emitted by LLVM. + link.doPrelinkTask(comp, .{ .load_object = path }); + // `link.Queue` has not called `prelink` because it knew we would want to send that + // final link input. It is *our* responsibility to call `prelink` now we're done. + comp.bin_file.?.prelink() catch |err| switch (err) { + error.AlreadyReported => return, + else => |e| return e, + }; + } } } if (comp.bin_file) |lf| { @@ -3390,7 +3401,7 @@ fn flush(comp: *Compilation, arena: Allocator, tid: Zcu.PerThread.Id) (Io.Cancel }; // This is needed before reading the error flags. lf.flush(arena, tid, comp.link_prog_node) catch |err| switch (err) { - error.AlreadyReported => {}, + error.AlreadyReported => return, error.OutOfMemory, error.Canceled => |e| return e, }; } diff --git a/src/codegen/llvm.zig b/src/codegen/llvm.zig index 831b44a052c25eb93976c2034961c70f00096a2d..3de01b1e6516d05af12c0ec131290d567b279bd9 100644 --- a/src/codegen/llvm.zig +++ b/src/codegen/llvm.zig @@ -518,6 +518,12 @@ pub const Object = struct { gpa: Allocator, builder: Builder, + /// The basename of the object file which will emitted by LLVM for the ZCU. Once it it emitted, + /// this object file is passed to the active linker implementation as an ordinary link input. + /// + /// For the full path, use `Compilation.resolveEmitPath` with `kind == .temp`. + out_bin_basename: []const u8, + /// This pool contains only types (and not `@as(type, undefined)`). It has two purposes: /// /// * Lazily tracking ABI alignment of types, so that `@"align"` attributes can be set to a @@ -657,6 +663,14 @@ pub const Object = struct { obj.* = .{ .gpa = gpa, .builder = builder, + .out_bin_basename = try std.zig.binNameAlloc(arena, .{ + .root_name = try std.fmt.allocPrint(arena, "{s}_zcu", .{comp.root_name}), + .cpu_arch = target.cpu.arch, + .os_tag = target.os.tag, + .ofmt = target.ofmt, + .abi = target.abi, + .output_mode = .Obj, + }), .type_pool = .empty, .lazy_abi_aligns = .empty, .debug_compile_unit = debug_compile_unit, diff --git a/src/link.zig b/src/link.zig index 3148795d700440cb13a39ea3abd3366da14ce83e..12d12a959bbd626d505f544a9972bc044e6efab4 100644 --- a/src/link.zig +++ b/src/link.zig @@ -402,11 +402,6 @@ pub const File = struct { emit: Path, file: ?Io.File, - /// When using the LLVM backend, the emitted object is written to a file with this name. This - /// object file then becomes a normal link input to LLD or a self-hosted linker. - /// - /// To convert this to an actual path, see `Compilation.resolveEmitPath` (with `kind == .temp`). - zcu_object_basename: ?[]const u8 = null, gc_sections: bool, print_gc_sections: bool, build_id: std.zig.BuildId, @@ -1196,20 +1191,22 @@ pub const File = struct { /// 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 { - assert(!base.post_prelink); - - // In this case, an object file is created by the LLVM backend, so - // there is no prelink phase. The Zig code is linked as a standard - // object along with the others. - if (base.zcu_object_basename != null) return; + // The guard on this assertion is a temporary hack to make the LLVM backend with LLD work with + // `-fincremental`. This works only because `File.Lld` does nothing in prelink. + // Related: https://codeberg.org/ziglang/zig/issues/32081 + if (base.tag != .lld) { + assert(!base.post_prelink); + } switch (base.tag) { inline .elf2, .coff2, .wasm => |tag| { dev.check(tag.devFeature()); - return @as(*tag.Type(), @fieldParentPtr("base", base)).prelink(base.comp.link_prog_node); + try @as(*tag.Type(), @fieldParentPtr("base", base)).prelink(base.comp.link_prog_node); }, else => {}, } + + base.post_prelink = true; } /// Legacy function for old linker code @@ -1407,7 +1404,12 @@ pub fn doPrelinkTask(comp: *Compilation, task: PrelinkTask) void { return; }; - assert(!base.post_prelink); + // The guard on this assertion is a temporary hack to make the LLVM backend with LLD work with + // `-fincremental`. This works only because `File.Lld` does nothing in prelink. + // Related: https://codeberg.org/ziglang/zig/issues/32081 + if (base.tag != .lld) { + assert(!base.post_prelink); + } var timer = comp.startTimer(); defer if (timer.finish(io)) |ns| { diff --git a/src/link/Coff.zig b/src/link/Coff.zig index 6eb98a7799cd09a243ae294b81dcfacd63683894..2d41c3bc582ccd62799e8b8fa1ec6ec178d8c460 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -1524,7 +1524,7 @@ pub fn addReloc( target.target_relocs = ri; } -pub fn prelink(coff: *Coff, prog_node: std.Progress.Node) void { +pub fn prelink(coff: *Coff, prog_node: std.Progress.Node) link.Error!void { _ = coff; _ = prog_node; } diff --git a/src/link/Elf.zig b/src/link/Elf.zig index 25ab18fd9954a033f0e445737e52f009ea3ae13b..4c50caf8f2b6d363a37c3f6189b0975b8bdd1451 100644 --- a/src/link/Elf.zig +++ b/src/link/Elf.zig @@ -260,10 +260,6 @@ pub fn createEmpty( .tag = .elf, .comp = comp, .emit = emit, - .zcu_object_basename = if (use_llvm) - try std.fmt.allocPrint(arena, "{s}_zcu.o", .{fs.path.stem(emit.sub_path)}) - else - null, .gc_sections = options.gc_sections orelse (optimize_mode != .Debug and output_mode != .Obj), .print_gc_sections = options.print_gc_sections, .stack_size = options.stack_size orelse 16777216, @@ -762,18 +758,14 @@ pub fn flush(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std } fn flushInner(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id) !void { + _ = arena; + const comp = self.base.comp; const gpa = comp.gpa; const diags = &comp.link_diags; - const zcu_obj_path: ?Path = if (self.base.zcu_object_basename) |raw| p: { - break :p try comp.resolveEmitPathFlush(arena, .temp, raw); - } else null; - if (self.zigObjectPtr()) |zig_object| try zig_object.flush(self, tid); - if (zcu_obj_path) |path| openParseObjectReportingFailure(self, path); - switch (comp.config.output_mode) { .Obj => return relocatable.flushObject(self, comp), .Lib => switch (comp.config.link_mode) { @@ -1046,27 +1038,6 @@ fn dumpArgvInit(self: *Elf, arena: Allocator) !void { } } -pub fn openParseObjectReportingFailure(self: *Elf, path: Path) void { - const comp = self.base.comp; - const io = comp.io; - const diags = &comp.link_diags; - const obj = link.openObject(io, path, false, false) catch |err| { - switch (diags.failParse(path, "failed to open object: {t}", .{err})) { - error.AlreadyReported => return, - } - }; - self.parseObjectReportingFailure(obj); -} - -fn parseObjectReportingFailure(self: *Elf, obj: link.Input.Object) void { - const comp = self.base.comp; - const diags = &comp.link_diags; - self.parseObject(obj) catch |err| switch (err) { - error.AlreadyReported => return, // already reported - else => |e| diags.addParseError(obj.path, "failed to parse object: {t}", .{e}), - }; -} - fn parseObject(self: *Elf, obj: link.Input.Object) !void { const tracy = trace(@src()); defer tracy.end(); diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index c72270bd2e2429553c929bec8b4ffce6f373e59f..b16e726d21213f6ba5ccd2c6aa6c22c4690ef914 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -4825,25 +4825,29 @@ pub fn prelink(elf: *Elf, prog_node: std.Progress.Node) link.Error!void { fn prelinkInner(elf: *Elf) Error!void { const comp = elf.base.comp; const gpa = comp.gpa; - try elf.ensureUnusedSymbolCapacity(1, .all_local); - try elf.inputs.ensureUnusedCapacity(gpa, 1); - const zcu_name = try std.fmt.allocPrint(gpa, "{s}_zcu", .{ - std.fs.path.stem(elf.base.emit.sub_path), - }); - defer gpa.free(zcu_name); - const zcu_file_symbol = elf.addLocalSymbolAssumeCapacity(.{ - .node = .none, - .name = try elf.string(.strtab, zcu_name), - .value = 0, - .size = 0, - .type = .FILE, - .shndx = .ABS, - }); - elf.inputs.addOneAssumeCapacity().* = .{ - .path = elf.base.emit, - .member = null, - .file_symbol = zcu_file_symbol, - }; + + if (comp.zcu != null and !comp.config.use_llvm) { + // We're use self-hosted codegen---add an input representing the Zig "object". + try elf.ensureUnusedSymbolCapacity(1, .all_local); + try elf.inputs.ensureUnusedCapacity(gpa, 1); + const zcu_name = try std.fmt.allocPrint(gpa, "{s}_zcu", .{ + std.fs.path.stem(elf.base.emit.sub_path), + }); + defer gpa.free(zcu_name); + const zcu_file_symbol = elf.addLocalSymbolAssumeCapacity(.{ + .node = .none, + .name = try elf.string(.strtab, zcu_name), + .value = 0, + .size = 0, + .type = .FILE, + .shndx = .ABS, + }); + elf.inputs.addOneAssumeCapacity().* = .{ + .path = elf.base.emit, + .member = null, + .file_symbol = zcu_file_symbol, + }; + } if (elf.shndx.dynamic != .UNDEF) switch (elf.identClass()) { .NONE, _ => unreachable, @@ -5859,8 +5863,8 @@ pub fn flush( ) link.Error!void { const comp = elf.base.comp; const diags = &comp.link_diags; + _ = prog_node; _ = arena; - _ = prog_node; if (comp.config.output_mode == .Exe) { var any_undef = false; diff --git a/src/link/Lld.zig b/src/link/Lld.zig index bc1b7d8f6052cf84e5d79023826c6ececd8e5ca6..3f20d15ebd7e8c9890b2fab69d57dc5779715579 100644 --- a/src/link/Lld.zig +++ b/src/link/Lld.zig @@ -207,11 +207,6 @@ pub fn createEmpty( const output_mode = comp.config.output_mode; const optimize_mode = comp.root_mod.optimize_mode; - const obj_file_ext: []const u8 = switch (target.ofmt) { - .coff => "obj", - .elf, .wasm => "o", - else => unreachable, - }; const gc_sections: bool = options.gc_sections orelse switch (target.ofmt) { .coff => optimize_mode != .Debug, .elf => optimize_mode != .Debug and output_mode != .Obj, @@ -230,7 +225,6 @@ pub fn createEmpty( .tag = .lld, .comp = comp, .emit = emit, - .zcu_object_basename = try allocPrint(arena, "{s}_zcu.{s}", .{ fs.path.stem(emit.sub_path), obj_file_ext }), .gc_sections = gc_sections, .print_gc_sections = options.print_gc_sections, .stack_size = stack_size, @@ -290,8 +284,8 @@ fn linkAsArchive(lld: *Lld, arena: Allocator) !void { const full_out_path_z = try arena.dupeSentinel(u8, full_out_path, 0); const opt_zcu = comp.zcu; - const zcu_obj_path: ?Cache.Path = if (opt_zcu != null) p: { - break :p try comp.resolveEmitPathFlush(arena, .temp, base.zcu_object_basename.?); + const zcu_obj_path: ?Cache.Path = if (opt_zcu) |zcu| p: { + break :p try comp.resolveEmitPathFlush(arena, .temp, zcu.llvm_object.?.out_bin_basename); } else null; log.debug("zcu_obj_path={?f}", .{zcu_obj_path}); @@ -376,8 +370,8 @@ fn coffLink(lld: *Lld, arena: Allocator) !void { const directory = base.emit.root_dir; // Just an alias to make it shorter to type. const full_out_path = try directory.join(arena, &[_][]const u8{base.emit.sub_path}); - const zcu_obj_path: ?Cache.Path = if (comp.zcu != null) p: { - break :p try comp.resolveEmitPathFlush(arena, .temp, base.zcu_object_basename.?); + const zcu_obj_path: ?Cache.Path = if (comp.zcu) |zcu| p: { + break :p try comp.resolveEmitPathFlush(arena, .temp, zcu.llvm_object.?.out_bin_basename); } else null; const is_lib = comp.config.output_mode == .Lib; @@ -766,8 +760,8 @@ fn elfLink(lld: *Lld, arena: Allocator) !void { const directory = base.emit.root_dir; // Just an alias to make it shorter to type. const full_out_path = try directory.join(arena, &[_][]const u8{base.emit.sub_path}); - const zcu_obj_path: ?Cache.Path = if (comp.zcu != null) p: { - break :p try comp.resolveEmitPathFlush(arena, .temp, base.zcu_object_basename.?); + const zcu_obj_path: ?Cache.Path = if (comp.zcu) |zcu| p: { + break :p try comp.resolveEmitPathFlush(arena, .temp, zcu.llvm_object.?.out_bin_basename); } else null; const output_mode = comp.config.output_mode; @@ -1364,8 +1358,8 @@ fn wasmLink(lld: *Lld, arena: Allocator) !void { const directory = base.emit.root_dir; // Just an alias to make it shorter to type. const full_out_path = try directory.join(arena, &[_][]const u8{base.emit.sub_path}); - const zcu_obj_path: ?Cache.Path = if (comp.zcu != null) p: { - break :p try comp.resolveEmitPathFlush(arena, .temp, base.zcu_object_basename.?); + const zcu_obj_path: ?Cache.Path = if (comp.zcu) |zcu| p: { + break :p try comp.resolveEmitPathFlush(arena, .temp, zcu.llvm_object.?.out_bin_basename); } else null; const is_obj = comp.config.output_mode == .Obj; diff --git a/src/link/MachO.zig b/src/link/MachO.zig index 5295ae8ea779d334fc75cf64370830917550cd7a..8783781a4ced69514353f2696587a9f9a1516b23 100644 --- a/src/link/MachO.zig +++ b/src/link/MachO.zig @@ -181,10 +181,6 @@ pub fn createEmpty( .tag = .macho, .comp = comp, .emit = emit, - .zcu_object_basename = if (use_llvm) - try std.fmt.allocPrint(arena, "{s}_zcu.o", .{fs.path.stem(emit.sub_path)}) - else - null, .gc_sections = options.gc_sections orelse (optimize_mode != .Debug), .print_gc_sections = options.print_gc_sections, .stack_size = options.stack_size orelse 16777216, @@ -353,9 +349,11 @@ pub fn flush( const sub_prog_node = prog_node.start("MachO Flush", 0); defer sub_prog_node.end(); - const zcu_obj_path: ?Path = if (self.base.zcu_object_basename) |raw| p: { - break :p try comp.resolveEmitPathFlush(arena, .temp, raw); - } else null; + 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); @@ -630,10 +628,12 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void { const directory = self.base.emit.root_dir; const full_out_path = try directory.join(arena, &[_][]const u8{self.base.emit.sub_path}); - const zcu_obj_path: ?[]const u8 = if (self.base.zcu_object_basename) |raw| p: { - const p = try comp.resolveEmitPathFlush(arena, .temp, raw); + const zcu_obj_path: ?[]const u8 = p: { + const zcu = comp.zcu orelse break :p null; + const llvm_object = zcu.llvm_object orelse break :p null; + const p = try comp.resolveEmitPathFlush(arena, .temp, llvm_object.out_bin_basename); break :p try p.toString(arena); - } else null; + }; var argv = std.array_list.Managed([]const u8).init(arena); diff --git a/src/link/Queue.zig b/src/link/Queue.zig index cc30c762c2b51fd7b6f30d0d22d95d28c56d65e5..9e05b0b99ef870fb90f800d091124269c0f39ca0 100644 --- a/src/link/Queue.zig +++ b/src/link/Queue.zig @@ -130,14 +130,17 @@ pub fn finishPrelinkQueue(q: *Queue, comp: *Compilation) Io.Cancelable!void { prelink: { const lf = comp.bin_file orelse break :prelink; if (lf.post_prelink) break :prelink; + if (comp.zcu != null and comp.zcu.?.llvm_object != null) { + // Don't call `prelink` just yet. It will be the frontend's responsibility instead, + // after it sends the ZCU object emitted by LLVM as the final link input. + break :prelink; + } - if (lf.prelink()) |_| { - lf.post_prelink = true; - } else |err| switch (err) { + lf.prelink() catch |err| switch (err) { error.OutOfMemory => comp.link_diags.setAllocFailure(), error.AlreadyReported => {}, error.Canceled => |e| return e, - } + }; } } @@ -171,16 +174,19 @@ fn runLinkTasks(q: *Queue, comp: *Compilation) void { } // We've finished the prelink tasks, so run prelink if necessary. - if (comp.bin_file) |lf| { - if (!lf.post_prelink) { - if (lf.prelink()) |_| { - lf.post_prelink = true; - } else |err| switch (err) { - error.OutOfMemory => comp.link_diags.setAllocFailure(), - error.Canceled => @panic("TODO"), - error.AlreadyReported => {}, - } + prelink: { + const lf = comp.bin_file orelse break :prelink; + if (lf.post_prelink) break :prelink; + if (comp.zcu != null and comp.zcu.?.llvm_object != null) { + // Don't call `prelink` just yet. It will be the frontend's responsibility instead, + // after it sends the ZCU object emitted by LLVM as the final link input. + break :prelink; } + lf.prelink() catch |err| switch (err) { + error.OutOfMemory => comp.link_diags.setAllocFailure(), + error.Canceled => @panic("TODO"), + error.AlreadyReported => {}, + }; } zcu_tasks: while (true) { diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index ff70f0e647efc6009a1fd72227b75f8365c30348..88502d8329e81c37e3dd3a7f8c48b7cd7bfd29a9 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -2944,7 +2944,6 @@ pub fn createEmpty( const target = &comp.root_mod.resolved_target.result; assert(target.ofmt == .wasm); - const use_llvm = comp.config.use_llvm; const output_mode = comp.config.output_mode; const wasi_exec_model = comp.config.wasi_exec_model; @@ -2954,10 +2953,6 @@ pub fn createEmpty( .tag = .wasm, .comp = comp, .emit = emit, - .zcu_object_basename = if (use_llvm) - try std.fmt.allocPrint(arena, "{s}_zcu.o", .{fs.path.stem(emit.sub_path)}) - else - null, // Garbage collection is so crucial to WebAssembly that we design // the linker around the assumption that it will be on in the vast // majority of cases, and therefore express "no garbage collection" @@ -3021,22 +3016,6 @@ pub fn createEmpty( return wasm; } -fn openParseObjectReportingFailure(wasm: *Wasm, path: Path) void { - const comp = wasm.base.comp; - const io = comp.io; - const diags = &comp.link_diags; - const obj = link.openObject(io, path, false, false) catch |err| { - switch (diags.failParse(path, "failed to open object: {t}", .{err})) { - error.AlreadyReported => return, - } - }; - wasm.parseObject(obj) catch |err| { - switch (diags.failParse(path, "failed to parse object: {t}", .{err})) { - error.AlreadyReported => return, - } - }; -} - fn parseObject(wasm: *Wasm, obj: link.Input.Object) !void { log.debug("parseObject {f}", .{obj.path}); const gpa = wasm.base.comp.gpa; @@ -3822,6 +3801,7 @@ pub fn flush( tid: Zcu.PerThread.Id, prog_node: std.Progress.Node, ) link.Error!void { + _ = arena; // The goal is to never use this because it's only needed if we need to // write to InternPool, but flush is too late to be writing to the // InternPool. @@ -3833,12 +3813,6 @@ pub fn flush( if (comp.verbose_link) try Compilation.dumpArgv(io, wasm.dump_argv_list.items); - if (wasm.base.zcu_object_basename) |raw| { - const zcu_obj_path: Path = try comp.resolveEmitPathFlush(arena, .temp, raw); - openParseObjectReportingFailure(wasm, zcu_obj_path); - try prelink(wasm, prog_node); - } - const tracy = trace(@src()); defer tracy.end(); -- 2.54.0 From 4a2a9e9d24b71bd0bea80aaf88205039ae77c8df Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Tue, 2 Jun 2026 07:27:10 +0100 Subject: [PATCH 4/5] Compilation.Config: rename an error The old error name didn't align with the actual message we emitted, and kept confusing me. --- src/Compilation/Config.zig | 10 +++++----- src/main.zig | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Compilation/Config.zig b/src/Compilation/Config.zig index ea9929bbcd70986deacc74d28f94660b060dd217..ce180f9ead8c4f545f4e5e8ea2a6ecc3c7b33179 100644 --- a/src/Compilation/Config.zig +++ b/src/Compilation/Config.zig @@ -130,7 +130,7 @@ pub const ResolveError = error{ ZigLacksTargetSupport, EmittingBinaryRequiresLlvmLibrary, LldIncompatibleObjectFormat, - LldCannotIncrementallyLink, + LldIncompatibleWithSelfHostedBackend, LtoRequiresLld, SanitizeThreadRequiresLibCpp, LibCRequiresLibUnwind, @@ -400,10 +400,10 @@ pub fn resolve(options: Options) ResolveError!Config { break :b true; } - // If there's no ZCU we aren't using the LLVM backend but - // it shouldn't influence which linker we pick - if (!use_llvm and options.have_zcu) { - if (options.use_lld == true) return error.LldCannotIncrementallyLink; + // If we have Zig code (i.e. a ZCU) and are compiling with a self-hosted backend, then we + // also need to use a self-hosted linker. + if (options.have_zcu and !use_llvm) { + if (options.use_lld == true) return error.LldIncompatibleWithSelfHostedBackend; break :b false; } diff --git a/src/main.zig b/src/main.zig index 7dc632c7b184298e3d4b7f5242cd69281ef5c334..e55100f89171452f0d1d9e457d8595c8756d8319 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4115,7 +4115,7 @@ fn createModule( error.ZigLacksTargetSupport => fatal("compiler backend unavailable for the specified target", .{}), error.EmittingBinaryRequiresLlvmLibrary => fatal("producing machine code via LLVM requires using the LLVM library", .{}), error.LldIncompatibleObjectFormat => fatal("using LLD to link {s} files is unsupported", .{@tagName(target.ofmt)}), - error.LldCannotIncrementallyLink => fatal("self-hosted backends do not support linking with LLD", .{}), + error.LldIncompatibleWithSelfHostedBackend => fatal("self-hosted backends do not support linking with LLD", .{}), error.LtoRequiresLld => fatal("LTO requires using LLD", .{}), error.SanitizeThreadRequiresLibCpp => fatal("thread sanitization is (for now) implemented in C++, so it requires linking libc++", .{}), error.LibCRequiresLibUnwind => fatal("libc of the specified target requires linking libunwind", .{}), -- 2.54.0 From 82e8ed66ecaa3fc88267bfcb2850c50fe9a4b955 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Tue, 2 Jun 2026 09:41:22 +0100 Subject: [PATCH 5/5] Elf2: add basic standalone test coverage This is just a temporary measure before we have better test coverage set up for this. I figure that in the name of incremental improvements, it makes sense to get at least a little bit of coverage in now to prevent basic use cases from regressing---because right now the old coverage of Elf2 in CI is the incremental tests, which only cover compiling for x86_64-linux without libc. In the future, this standalone test should be replaced with both of: * entries in the main test target matrix * linker tests (related: https://codeberg.org/ziglang/zig/pulls/32065) --- test/standalone/build.zig.zon | 3 ++ test/standalone/elf2/build.zig | 51 ++++++++++++++++++++++++++++++++++ test/standalone/elf2/hello.zig | 6 ++++ 3 files changed, 60 insertions(+) create mode 100644 test/standalone/elf2/build.zig create mode 100644 test/standalone/elf2/hello.zig diff --git a/test/standalone/build.zig.zon b/test/standalone/build.zig.zon index 072bcd0309cf4decce854badf096994606a1e550..0314bc23ef96091c368cdcb20649c5567aedd92c 100644 --- a/test/standalone/build.zig.zon +++ b/test/standalone/build.zig.zon @@ -193,6 +193,9 @@ .debug_io_color = .{ .path = "debug_io_color", }, + .elf2 = .{ + .path = "elf2", + }, }, .paths = .{ "build.zig", diff --git a/test/standalone/elf2/build.zig b/test/standalone/elf2/build.zig new file mode 100644 index 0000000000000000000000000000000000000000..d25c04eb3a446a8cb5db8d07222105775e126da5 --- /dev/null +++ b/test/standalone/elf2/build.zig @@ -0,0 +1,51 @@ +pub fn build(b: *Build) void { + const test_step = b.step("test", "Test the new ELF linker"); + b.default_step = test_step; + + if (b.graph.host.result.cpu.arch == .x86_64 and b.graph.host.result.os.tag == .linux) { + addOne(b, test_step, b.graph.host, false, .static, "elf2-hello-native-selfhosted-static"); + addOne(b, test_step, b.graph.host, false, .dynamic, "elf2-hello-native-selfhosted-dynamic"); + addOne(b, test_step, b.graph.host, true, .static, "elf2-hello-native-llvm-static"); + addOne(b, test_step, b.graph.host, true, .dynamic, "elf2-hello-native-llvm-dynamic"); + } + + const x86_64_linux_target: Build.ResolvedTarget = b.resolveTargetQuery(.{ + .cpu_arch = .x86_64, + .os_tag = .linux, + }); + addOne(b, test_step, x86_64_linux_target, false, .static, "elf2-hello-selfhosted-static"); + addOne(b, test_step, x86_64_linux_target, true, .static, "elf2-hello-llvm-static"); +} + +fn addOne( + b: *Build, + test_step: *Build.Step, + target: Build.ResolvedTarget, + use_llvm: bool, + link_mode: std.lang.LinkMode, + name: []const u8, +) void { + const mod = b.createModule(.{ + .root_source_file = b.path("hello.zig"), + .target = target, + .optimize = .Debug, + .link_libc = link_mode == .dynamic, + }); + const exe = b.addExecutable(.{ + .name = name, + .root_module = mod, + .linkage = link_mode, + }); + exe.use_new_linker = true; + exe.use_llvm = use_llvm; + + const run = b.addRunArtifact(exe); + run.expectExitCode(0); + run.expectStdOutEqual("Hello, World!\n"); + run.skip_foreign_checks = true; + + test_step.dependOn(&run.step); +} + +const std = @import("std"); +const Build = std.Build; diff --git a/test/standalone/elf2/hello.zig b/test/standalone/elf2/hello.zig new file mode 100644 index 0000000000000000000000000000000000000000..8ac9329446e86ca6711af740bd8213be0b89bb21 --- /dev/null +++ b/test/standalone/elf2/hello.zig @@ -0,0 +1,6 @@ +pub fn main(init: std.process.Init) !void { + const stdout: std.Io.File = .stdout(); + try stdout.writeStreamingAll(init.io, "Hello, World!\n"); +} + +const std = @import("std"); -- 2.54.0