lib/tldr/src/cli.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const builtin = @import("builtin");
   3 const pretty = @import("pretty");
   4 const pretty_usage = @import("pretty_usage");
   5 const sys = @import("sys");
   6 const tldr = @import("tldr");
   7 
   8 const Allocator = std.mem.Allocator;
   9 const elf_object = tldr.formats.elf.object;
  10 
  11 test "tldr cli package namespace" {
  12     std.testing.refAllDecls(@This());
  13 }
  14 
  15 const max_response_bytes = 16 * 1024 * 1024;
  16 const max_object_bytes = 512 * 1024 * 1024;
  17 const prefault_input_threshold = 64 * 1024 * 1024;
  18 
  19 const usage_text =
  20     \\usage: tldr-link [-o PATH] [-e SYMBOL] [-L PATH] [-l LIB] [-pie] [-dynamic-linker PATH] [--eh-frame-hdr] [--build-id[=fast|sha1]] [--hash-style=gnu] [--gc-sections] [--strip-debug] [--icf=all|off] [--incremental=off|prepare|relink] [--incremental-manifest PATH] [--emit-link-map PATH] [-Map PATH] [--emit-manifest PATH] OBJECT...
  21 ;
  22 
  23 const ParsedArgs = struct {
  24     output_path: []const u8 = "a.out",
  25     entry_symbol: []const u8 = "_start",
  26     output_kind: tldr.model.OutputKind = .executable,
  27     pie: bool = false,
  28     dynamic_linker: ?[]const u8 = null,
  29     soname: ?[]const u8 = null,
  30     export_dynamic: bool = false,
  31     eh_frame_header: bool = false,
  32     gc_sections: bool = false,
  33     strip_debug: bool = false,
  34     build_id: tldr.model.BuildIdMode = .none,
  35     icf: tldr.model.IcfMode = .off,
  36     incremental_mode: tldr.model.IncrementalMode = .off,
  37     incremental_manifest_path: ?[]const u8 = null,
  38     link_map_path: ?[]const u8 = null,
  39     manifest_path: ?[]const u8 = null,
  40     input_paths: []const []const u8,
  41 };
  42 
  43 const LoadedInputs = struct {
  44     files: std.ArrayListUnmanaged(LoadedInputFile) = .empty,
  45     inputs: std.ArrayListUnmanaged(tldr.Input) = .empty,
  46     prefault: ?sys.thread.JoinHandle = null,
  47     prefault_stop: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
  48 
  49     fn deinit(self: *LoadedInputs, allocator: Allocator) void {
  50         if (self.prefault) |handle| {
  51             self.prefault_stop.store(true, .release);
  52             handle.join();
  53         }
  54         for (self.files.items) |*file| file.deinit();
  55         self.files.deinit(allocator);
  56         self.inputs.deinit(allocator);
  57     }
  58 
  59     fn startPrefault(self: *LoadedInputs) void {
  60         if (self.prefault != null) return;
  61         if (!prefaultInputs(self.totalBytes())) return;
  62         self.prefault_stop.store(false, .release);
  63         self.prefault = sys.thread.spawn(prefaultMappedInputs, .{self}) catch null;
  64     }
  65 
  66     fn totalBytes(self: *const LoadedInputs) usize {
  67         var total: usize = 0;
  68         for (self.inputs.items) |input| total += input.bytes.len;
  69         return total;
  70     }
  71 };
  72 
  73 const LoadedInputFile = struct {
  74     bytes: []const u8 = "",
  75     mapping: ?[]align(std.heap.page_size_min) u8 = null,
  76     identity: ?tldr.InputIdentity = null,
  77 
  78     fn deinit(self: *LoadedInputFile) void {
  79         if (self.mapping) |mapping| sys.memory.unmap(mapping);
  80         self.* = .{};
  81     }
  82 };
  83 
  84 const MappedOutput = struct {
  85     path: []const u8,
  86     output_path: []const u8,
  87     sideline_path: []const u8,
  88     file: std.Io.File,
  89     file_open: bool = true,
  90     active: bool = true,
  91     delivered: bool = false,
  92     sideline: ?Sideline = null,
  93 
  94     const Sideline = struct {
  95         unlink_thread: ?sys.thread.JoinHandle = null,
  96     };
  97 
  98     fn init(allocator: Allocator, cwd: std.Io.Dir, output_path: []const u8) !MappedOutput {
  99         const path = try mappedOutputTempPath(allocator, output_path);
 100         const sideline_path = try mappedOutputSidelinePath(allocator, output_path);
 101         const file = try cwd.createFile(sys.fs.debugIo(), path, .{
 102             .read = true,
 103             .truncate = true,
 104         });
 105         return .{
 106             .path = path,
 107             .output_path = output_path,
 108             .sideline_path = sideline_path,
 109             .file = file,
 110         };
 111     }
 112 
 113     fn commitObserver(self: *MappedOutput) tldr.LinkCommitObserver {
 114         return .{ .context = self, .committed = onLinkCommitted };
 115     }
 116 
 117     fn onLinkCommitted(context: *anyopaque) void {
 118         const self: *MappedOutput = @ptrCast(@alignCast(context));
 119         if (self.sideline != null) return;
 120         const cwd = sys.fs.cwd();
 121         const stat = cwd.statFile(sys.fs.debugIo(), self.output_path, .{ .follow_symlinks = false }) catch return;
 122         switch (stat.kind) {
 123             .file, .sym_link => {},
 124             else => return,
 125         }
 126         cwd.rename(self.output_path, cwd, self.sideline_path, sys.fs.debugIo()) catch return;
 127         self.sideline = .{
 128             .unlink_thread = sys.thread.spawn(unlinkSidelineFile, .{self.sideline_path}) catch null,
 129         };
 130     }
 131 
 132     fn finish(self: *MappedOutput, cwd: std.Io.Dir) !void {
 133         if (self.file_open) {
 134             try self.file.setPermissions(sys.fs.debugIo(), .executable_file);
 135             self.file.close(sys.fs.debugIo());
 136             self.file_open = false;
 137         }
 138         try cwd.rename(self.path, cwd, self.output_path, sys.fs.debugIo());
 139         self.active = false;
 140         self.deliver(cwd);
 141     }
 142 
 143     fn deliver(self: *MappedOutput, cwd: std.Io.Dir) void {
 144         self.delivered = true;
 145         self.resolveSideline(cwd);
 146     }
 147 
 148     fn resolveSideline(self: *MappedOutput, cwd: std.Io.Dir) void {
 149         const sideline = self.sideline orelse return;
 150         self.sideline = null;
 151         if (sideline.unlink_thread) |thread| {
 152             thread.join();
 153             return;
 154         }
 155         if (self.delivered) {
 156             cwd.deleteFile(sys.fs.debugIo(), self.sideline_path) catch {};
 157         } else {
 158             cwd.rename(self.sideline_path, cwd, self.output_path, sys.fs.debugIo()) catch {};
 159         }
 160     }
 161 
 162     fn deinit(self: *MappedOutput, cwd: std.Io.Dir) void {
 163         self.resolveSideline(cwd);
 164         if (self.file_open) {
 165             self.file.close(sys.fs.debugIo());
 166             self.file_open = false;
 167         }
 168         if (self.active) {
 169             cwd.deleteFile(sys.fs.debugIo(), self.path) catch {};
 170             self.active = false;
 171         }
 172     }
 173 };
 174 
 175 pub fn main(init: sys.process.Init) !void {
 176     if (builtin.mode == .debug) {
 177         var debug_allocator: std.heap.DebugAllocator(.{}) = .{};
 178         const exit_code = try mainExitCode(init, debug_allocator.allocator());
 179         if (debug_allocator.deinit() == .leak) sys.process.exit(1);
 180         sys.process.exit(exit_code);
 181     }
 182 
 183     const exit_code = try mainExitCode(init, sys.allocator.processAllocator());
 184     sys.process.exit(exit_code);
 185 }
 186 
 187 fn unlinkSidelineFile(path: []const u8) void {
 188     sys.fs.cwd().deleteFile(sys.fs.debugIo(), path) catch {};
 189 }
 190 
 191 fn mappedOutputTempPath(allocator: Allocator, output_path: []const u8) ![]u8 {
 192     const stamp: u64 = @truncate(@as(u128, @bitCast(sys.time.realNanoTimestamp())));
 193     return try std.fmt.allocPrint(allocator, "{s}.tldr-tmp-{x}", .{ output_path, stamp });
 194 }
 195 
 196 fn mappedOutputSidelinePath(allocator: Allocator, output_path: []const u8) ![]u8 {
 197     const stamp: u64 = @truncate(@as(u128, @bitCast(sys.time.realNanoTimestamp())));
 198     return try std.fmt.allocPrint(allocator, "{s}.tldr-old-{x}", .{ output_path, stamp });
 199 }
 200 
 201 fn mainExitCode(init: sys.process.Init, allocator: Allocator) !u8 {
 202     const exit_code: u8 = blk: {
 203         var args_arena = std.heap.ArenaAllocator.init(allocator);
 204         defer args_arena.deinit();
 205         const argv = try init.minimal.args.toSlice(args_arena.allocator());
 206 
 207         const stderr_file = sys.stdio.stderr();
 208         const options = prettyOptions(stderr_file);
 209         var stderr_buffer: [4096]u8 = undefined;
 210         var stderr_writer = stderr_file.writer(sys.stdio.debugIo(), &stderr_buffer);
 211         var stderr_text_buffer: [4096]u8 = undefined;
 212         var stderr_text = pretty.TextWriter.init(
 213             &stderr_writer.interface,
 214             &stderr_text_buffer,
 215             options,
 216         );
 217         const stderr = &stderr_text.writer;
 218         const code = run(allocator, argv[1..], stderr, options) catch |err| code_blk: {
 219             try pretty_usage.writeErrorText(
 220                 allocator,
 221                 stderr,
 222                 "tldr-link",
 223                 @errorName(err),
 224                 .{ .layout = options },
 225             );
 226             break :code_blk 1;
 227         };
 228         try stderr.flush();
 229         try stderr_writer.interface.flush();
 230         break :blk code;
 231     };
 232 
 233     return exit_code;
 234 }
 235 
 236 pub fn run(
 237     allocator: Allocator,
 238     args: []const []const u8,
 239     stderr: *std.Io.Writer,
 240     options: pretty.LayoutOptions,
 241 ) !u8 {
 242     return try runNamed(allocator, args, stderr, options, "tldr-link");
 243 }
 244 
 245 pub fn runNamed(
 246     allocator: Allocator,
 247     args: []const []const u8,
 248     stderr: *std.Io.Writer,
 249     options: pretty.LayoutOptions,
 250     command_name: []const u8,
 251 ) !u8 {
 252     var arena_state = std.heap.ArenaAllocator.init(allocator);
 253     defer arena_state.deinit();
 254     const arena = arena_state.allocator();
 255 
 256     if (pretty_usage.hasHelpArg(args)) {
 257         try writeUsageNamed(allocator, stderr, options, command_name);
 258         return 0;
 259     }
 260 
 261     const parsed = parseArgs(arena, args) catch |err| switch (err) {
 262         error.InvalidArguments => {
 263             try writeUsageNamed(allocator, stderr, options, command_name);
 264             return 2;
 265         },
 266         else => return err,
 267     };
 268     if (parsed.input_paths.len == 0) {
 269         try writeUsageNamed(allocator, stderr, options, command_name);
 270         return 2;
 271     }
 272     if (parsed.incremental_mode == .relink and parsed.incremental_manifest_path == null) {
 273         try writeUsageNamed(allocator, stderr, options, command_name);
 274         return 2;
 275     }
 276 
 277     const inputs_read_at_ns = std.math.lossyCast(i64, sys.time.realNanoTimestamp());
 278     var loaded_inputs = try loadInputs(allocator, parsed.input_paths);
 279     defer loaded_inputs.deinit(allocator);
 280     if (parsed.incremental_mode != .relink) loaded_inputs.startPrefault();
 281 
 282     var diagnostics: tldr.Diagnostics = .{};
 283     var link_options: tldr.LinkOptions = .{
 284         .inputs_read_at_ns = inputs_read_at_ns,
 285         .output_kind = parsed.output_kind,
 286         .entry_symbol = parsed.entry_symbol,
 287         .pie = parsed.pie,
 288         .dynamic_linker = parsed.dynamic_linker,
 289         .soname = parsed.soname,
 290         .export_dynamic = parsed.export_dynamic,
 291         .eh_frame_header = parsed.eh_frame_header,
 292         .gc_sections = parsed.gc_sections,
 293         .strip_debug = parsed.strip_debug,
 294         .build_id = parsed.build_id,
 295         .icf = parsed.icf,
 296         .incremental_mode = parsed.incremental_mode,
 297         .diagnostics = &diagnostics,
 298     };
 299     const timing_phases = beginPhaseTimings();
 300     if (parsed.incremental_mode == .relink) {
 301         const relink_start = sys.time.nanoTimestamp();
 302         const status = try runRelink(arena, parsed, &loaded_inputs, link_options, stderr, options, &diagnostics);
 303         if (timing_phases) {
 304             const elapsed = sys.time.nanoTimestamp() - relink_start;
 305             finishPhaseTimings(stderr, if (elapsed < 0) 0 else @intCast(elapsed));
 306         }
 307         return status;
 308     }
 309 
 310     const link_start = sys.time.nanoTimestamp();
 311     var mapped_output = try MappedOutput.init(arena, sys.fs.cwd(), parsed.output_path);
 312     defer mapped_output.deinit(sys.fs.cwd());
 313     link_options.mapped_output_file = mapped_output.file;
 314     link_options.commit_observer = mapped_output.commitObserver();
 315     var linked = tldr.link(arena, loaded_inputs.inputs.items, link_options) catch |err| {
 316         try writeLinkFailure(arena, stderr, options, diagnostics.linkFailure(err));
 317         return 1;
 318     };
 319     if (timing_phases) {
 320         const elapsed = sys.time.nanoTimestamp() - link_start;
 321         finishPhaseTimings(stderr, if (elapsed < 0) 0 else @intCast(elapsed));
 322     }
 323     defer linked.deinit(arena);
 324 
 325     if (linked.isMaterialized()) {
 326         try mapped_output.finish(sys.fs.cwd());
 327     } else {
 328         try writeExecutableFile(sys.fs.cwd(), parsed.output_path, linked.bytes);
 329         mapped_output.deliver(sys.fs.cwd());
 330     }
 331 
 332     var prepared_state: ?tldr.incremental.PreparedState = if (parsed.incremental_mode == .prepare)
 333         try linked.prepareIncrementalState(arena)
 334     else
 335         null;
 336     defer if (prepared_state) |*state| state.deinit(arena);
 337     const manifest = if (prepared_state) |*state| state.manifest else linked.manifest;
 338     if (parsed.link_map_path) |link_map_path| {
 339         const link_map = try manifest.formatTextAlloc(arena);
 340         try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{
 341             .sub_path = link_map_path,
 342             .data = link_map,
 343             .flags = .{ .truncate = true },
 344         });
 345     }
 346     if (parsed.manifest_path) |manifest_path| {
 347         const manifest_binary = try manifest.formatBinaryAlloc(arena);
 348         try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{
 349             .sub_path = manifest_path,
 350             .data = manifest_binary,
 351             .flags = .{ .truncate = true },
 352         });
 353     }
 354     return 0;
 355 }
 356 
 357 const phase_timings_env = "TLDR_PHASE_TIMINGS";
 358 
 359 var phase_timers: tldr.trace.PhaseTimers = .{};
 360 
 361 fn phaseTimingsRequested() bool {
 362     const text = sys.env.get(phase_timings_env) orelse return false;
 363     return !std.mem.eql(u8, text, "0");
 364 }
 365 
 366 fn beginPhaseTimings() bool {
 367     if (!phaseTimingsRequested()) return false;
 368     tldr.trace.beginPhaseTiming(&phase_timers, &sys.time.nanoTimestamp);
 369     return true;
 370 }
 371 
 372 fn finishPhaseTimings(stderr: *std.Io.Writer, total_ns: u64) void {
 373     tldr.trace.endPhaseTiming();
 374     stderr.print("tldr phase timings (total {d:.3}ms):\n", .{@as(f64, @floatFromInt(total_ns)) / 1_000_000.0}) catch return;
 375     for (tldr.model.product_stage_order) |stage| {
 376         if (phase_timers.callCount(stage) == 0) continue;
 377         const stage_ns = phase_timers.totalNs(stage);
 378         const pct = if (total_ns == 0) 0.0 else @as(f64, @floatFromInt(stage_ns)) * 100.0 / @as(f64, @floatFromInt(total_ns));
 379         stderr.print(
 380             "  {s:<28} {d:>9.3}ms  {d:>5.1}%  calls={d}\n",
 381             .{ stage.label(), @as(f64, @floatFromInt(stage_ns)) / 1_000_000.0, pct, phase_timers.callCount(stage) },
 382         ) catch return;
 383     }
 384     stderr.flush() catch {};
 385 }
 386 
 387 fn loadInputs(allocator: Allocator, paths: []const []const u8) !LoadedInputs {
 388     var loaded: LoadedInputs = .{};
 389     errdefer loaded.deinit(allocator);
 390     var first_cached_archive_path: ?[]const u8 = null;
 391     var first_cached_archive_index: usize = 0;
 392     var cached_archive_paths: std.StringHashMapUnmanaged(usize) = .empty;
 393     defer cached_archive_paths.deinit(allocator);
 394 
 395     try loaded.files.ensureTotalCapacity(allocator, paths.len);
 396     try loaded.inputs.ensureTotalCapacity(allocator, paths.len);
 397     for (paths) |path| {
 398         if (cacheableInputPath(path)) {
 399             if (first_cached_archive_path) |first_path| {
 400                 if (std.mem.eql(u8, path, first_path)) {
 401                     appendCachedInput(&loaded, path, first_cached_archive_index);
 402                     continue;
 403                 }
 404                 if (cached_archive_paths.count() == 0) {
 405                     try cached_archive_paths.ensureTotalCapacity(allocator, 4);
 406                     cached_archive_paths.putAssumeCapacityNoClobber(first_path, first_cached_archive_index);
 407                 }
 408                 const gop = try cached_archive_paths.getOrPut(allocator, path);
 409                 if (gop.found_existing) {
 410                     appendCachedInput(&loaded, path, gop.value_ptr.*);
 411                     continue;
 412                 }
 413                 gop.value_ptr.* = loaded.files.items.len;
 414             } else {
 415                 first_cached_archive_path = path;
 416                 first_cached_archive_index = loaded.files.items.len;
 417             }
 418         }
 419         const file = try loadInputFile(path);
 420         loaded.files.appendAssumeCapacity(file);
 421         loaded.inputs.appendAssumeCapacity(.{
 422             .name = path,
 423             .bytes = file.bytes,
 424             .identity = file.identity,
 425         });
 426     }
 427     return loaded;
 428 }
 429 
 430 fn appendCachedInput(loaded: *LoadedInputs, path: []const u8, file_index: usize) void {
 431     const file = loaded.files.items[file_index];
 432     loaded.inputs.appendAssumeCapacity(.{
 433         .name = path,
 434         .bytes = file.bytes,
 435         .identity = file.identity,
 436     });
 437 }
 438 
 439 fn cacheableInputPath(path: []const u8) bool {
 440     return path.len >= 2 and path[path.len - 2] == '.' and path[path.len - 1] == 'a';
 441 }
 442 
 443 fn prefaultInputs(total_bytes: usize) bool {
 444     return sys.thread.threadsSupported() and total_bytes >= prefault_input_threshold;
 445 }
 446 
 447 fn prefaultMappedInputs(loaded: *LoadedInputs) void {
 448     const page_size = sys.memory.pageSize();
 449     var sink: u8 = 0;
 450     outer: for (loaded.files.items) |file| {
 451         const bytes = file.bytes;
 452         if (bytes.len == 0) continue;
 453         var offset: usize = 0;
 454         var pages: usize = 0;
 455         while (offset < bytes.len) : (offset += page_size) {
 456             if (loaded.prefault_stop.load(.acquire)) break :outer;
 457             const byte: *const volatile u8 = @ptrCast(&bytes[offset]);
 458             sink ^= byte.*;
 459             pages += 1;
 460             if ((pages & 0xff) == 0) {
 461                 if (loaded.prefault_stop.load(.acquire)) break :outer;
 462                 sys.thread.yield();
 463             }
 464         }
 465         if (loaded.prefault_stop.load(.acquire)) break;
 466         const last: *const volatile u8 = @ptrCast(&bytes[bytes.len - 1]);
 467         sink ^= last.*;
 468     }
 469     std.mem.doNotOptimizeAway(sink);
 470 }
 471 
 472 fn loadInputFile(path: []const u8) !LoadedInputFile {
 473     var file = if (std.fs.path.isAbsolute(path))
 474         try sys.fs.openAbsoluteFile(path, .{})
 475     else
 476         try sys.fs.cwd().openFile(sys.fs.debugIo(), path, .{});
 477     defer file.close(sys.fs.debugIo());
 478 
 479     const stat = try file.stat(sys.fs.debugIo());
 480     const size = std.math.cast(usize, stat.size) orelse return error.FileTooBig;
 481     if (size > max_object_bytes) return error.FileTooBig;
 482     const identity: ?tldr.InputIdentity = if (std.math.cast(i64, stat.mtime.nanoseconds)) |mtime_ns| .{
 483         .mtime_ns = mtime_ns,
 484         .inode = stat.inode,
 485     } else null;
 486     if (size == 0) return .{ .identity = identity };
 487 
 488     const mapping = try sys.memory.mapPrivateFile(file.handle, size, .{ .read = true }, 0);
 489     return .{
 490         .bytes = mapping[0..size],
 491         .mapping = mapping,
 492         .identity = identity,
 493     };
 494 }
 495 
 496 fn runRelink(
 497     allocator: Allocator,
 498     parsed: ParsedArgs,
 499     loaded: *LoadedInputs,
 500     options: tldr.LinkOptions,
 501     stderr: *std.Io.Writer,
 502     output_options: pretty.LayoutOptions,
 503     diagnostics: *tldr.Diagnostics,
 504 ) !u8 {
 505     const inputs = loaded.inputs.items;
 506     const manifest_input_path = parsed.incremental_manifest_path.?;
 507     const cwd = sys.fs.cwd();
 508     const decode_phase = tldr.trace.product(.relink_manifest_decode);
 509     var manifest_source = try MappedManifest.open(cwd, manifest_input_path);
 510     defer manifest_source.deinit();
 511     const previous_manifest_bytes = manifest_source.bytes;
 512 
 513     const previous_manifest = tldr.incremental.Manifest.fromBinary(allocator, previous_manifest_bytes) catch |err| switch (err) {
 514         error.UnsupportedManifestProtocol, error.InvalidManifestBinary => {
 515             decode_phase.end();
 516             loaded.startPrefault();
 517             return try runRelinkFullLink(allocator, parsed, inputs, options, stderr, output_options, diagnostics, manifest_input_path);
 518         },
 519         else => return err,
 520     };
 521     var state = try tldr.incremental.PreparedState.fromOwnedManifest(allocator, previous_manifest);
 522     defer state.deinit(allocator);
 523     decode_phase.end();
 524 
 525     var output_present = true;
 526     cwd.access(sys.fs.debugIo(), parsed.output_path, .{}) catch |err| switch (err) {
 527         error.FileNotFound => output_present = false,
 528         else => return err,
 529     };
 530     const reuse = detect_reuse: {
 531         const change_phase = tldr.trace.product(.relink_change_detection);
 532         defer change_phase.end();
 533         break :detect_reuse output_present and state.canReuseFor(options, inputs);
 534     };
 535     if (reuse) {
 536         try writeRelinkSidecars(allocator, cwd, parsed, state.manifest, manifest_input_path, false);
 537         return 0;
 538     }
 539 
 540     const input_change_limits = tldr.incremental.InputChangeStorage.Limits.inspect(
 541         tldr.incremental.RecordedInputs.fromManifest(&state.manifest),
 542         inputs,
 543     );
 544     const input_change_capacity = try tldr.incremental.InputChangeStorage.Capacity.derive(
 545         input_change_limits,
 546     );
 547     const input_change_bytes = try allocator.alignedAlloc(
 548         u8,
 549         .fromByteUnits(tldr.incremental.InputChangeStorage.storage_alignment),
 550         input_change_capacity.storage_bytes,
 551     );
 552     defer allocator.free(input_change_bytes);
 553     var input_change_storage = try tldr.incremental.InputChangeStorage.init(
 554         input_change_bytes,
 555         input_change_limits,
 556     );
 557     defer _ = input_change_storage.deinit();
 558     input_change_storage.activate();
 559     const input_changes = classify: {
 560         const change_phase = tldr.trace.product(.relink_change_detection);
 561         defer change_phase.end();
 562         break :classify try state.classifyInputs(
 563             &input_change_storage,
 564             inputs,
 565         );
 566     };
 567 
 568     var replacements: []const tldr.incremental.ReplacementContribution = &.{};
 569     var replacements_owned = false;
 570     defer if (replacements_owned) tldr.incremental.freeReplacementContributions(allocator, replacements);
 571 
 572     var relink_plan = state.planChangedInputRelinkFromInputChanges(options, input_changes, replacements);
 573     var direct_evidence: tldr.incremental.DirectRelinkEvidence = .{};
 574     defer direct_evidence.deinit(allocator);
 575     var direct_inputs_proven = false;
 576     if (relinkNeedsCandidateReplacements(relink_plan, input_changes)) {
 577         const evidence_phase = tldr.trace.product(.relink_direct_evidence);
 578         defer evidence_phase.end();
 579         direct_evidence = tldr.directIncrementalEvidenceAlloc(
 580             allocator,
 581             state.manifest,
 582             inputs,
 583             input_changes,
 584             options,
 585         ) catch |err| switch (err) {
 586             error.OutOfMemory => return error.OutOfMemory,
 587             else => .{},
 588         };
 589         replacements = direct_evidence.replacements;
 590         if (replacements.len != 0) {
 591             if (state.ensureReplacementIndex(allocator, replacements)) {
 592                 relink_plan = state.planChangedInputRelinkFromInputChanges(options, input_changes, replacements);
 593             } else |err| switch (err) {
 594                 error.OutOfMemory => return error.OutOfMemory,
 595                 error.DuplicateContribution => {},
 596             }
 597         }
 598         direct_inputs_proven = direct_evidence.inputs_proven;
 599     }
 600 
 601     var use_in_place = false;
 602     var manifest_updates: ?tldr.incremental.PreparedState.RecordUpdates = null;
 603     defer if (manifest_updates) |*updates| updates.deinit(allocator);
 604     var patch_target: ?MappedPatchOutput = null;
 605     defer if (patch_target) |*target| target.deinit();
 606     var patched_ranges: std.ArrayListUnmanaged(tldr.incremental.FileRange) = .empty;
 607     defer patched_ranges.deinit(allocator);
 608     var candidate: ?tldr.LinkedImage = null;
 609     defer if (candidate) |*linked| linked.deinit(allocator);
 610     var mapped_output: ?MappedOutput = null;
 611     defer if (mapped_output) |*output| output.deinit(cwd);
 612 
 613     if (relink_plan.decision == .in_place and direct_inputs_proven) {
 614         {
 615             const io_phase = tldr.trace.product(.relink_output_io);
 616             defer io_phase.end();
 617             patch_target = try MappedPatchOutput.open(cwd, parsed.output_path);
 618         }
 619         if (patch_target) |target| {
 620             const patch_phase = tldr.trace.product(.relink_patch_apply);
 621             defer patch_phase.end();
 622             const application = try state.applyAcceptedChangedInputRelink(
 623                 target.mapping,
 624                 replacements,
 625                 relink_plan,
 626             );
 627             if (application.plan.decision == .in_place) {
 628                 use_in_place = try finishInPlaceDirectRelink(
 629                     &state,
 630                     target.mapping,
 631                     options,
 632                     inputs,
 633                     input_changes,
 634                     replacements,
 635                     direct_evidence.member_updates,
 636                     relink_plan,
 637                 );
 638             }
 639             if (use_in_place) {
 640                 try appendReplacementRanges(allocator, &patched_ranges, &state, replacements);
 641                 if (try tldr.incrementalBuildIdNoteRange(target.mapping, options)) |range| {
 642                     try patched_ranges.append(allocator, .{ .offset = range.offset, .len = range.len });
 643                 }
 644                 manifest_updates = try state.acceptedRelinkRecordUpdates(
 645                     allocator,
 646                     inputs,
 647                     input_changes,
 648                     replacements,
 649                     direct_evidence.member_updates,
 650                 );
 651             }
 652         }
 653         if (!use_in_place) {
 654             if (patch_target) |*target| target.deinit();
 655             patch_target = null;
 656         }
 657     }
 658 
 659     if (!use_in_place) {
 660         loaded.startPrefault();
 661         mapped_output = try MappedOutput.init(allocator, cwd, parsed.output_path);
 662         var candidate_options = options;
 663         candidate_options.mapped_output_file = mapped_output.?.file;
 664         candidate = tldr.link(allocator, inputs, candidate_options) catch |err| {
 665             try writeLinkFailure(allocator, stderr, output_options, diagnostics.linkFailure(err));
 666             return 1;
 667         };
 668 
 669         if (relinkNeedsCandidateReplacements(relink_plan, input_changes)) {
 670             if (replacements_owned) {
 671                 tldr.incremental.freeReplacementContributions(allocator, replacements);
 672                 replacements_owned = false;
 673             }
 674             replacements = try tldr.incremental.replacementContributionsFromImage(
 675                 allocator,
 676                 candidate.?.bytes,
 677                 candidate.?.manifest,
 678                 input_changes,
 679             );
 680             replacements_owned = replacements.len != 0;
 681             if (state.ensureReplacementIndex(allocator, replacements)) {
 682                 relink_plan = state.planChangedInputRelinkFromInputChanges(options, input_changes, replacements);
 683             } else |err| switch (err) {
 684                 error.OutOfMemory => return error.OutOfMemory,
 685                 error.DuplicateContribution => {},
 686             }
 687         }
 688 
 689         if (relink_plan.decision != .full_link) {
 690             {
 691                 const io_phase = tldr.trace.product(.relink_output_io);
 692                 defer io_phase.end();
 693                 patch_target = try MappedPatchOutput.open(cwd, parsed.output_path);
 694             }
 695             if (patch_target) |target| {
 696                 const patch_phase = tldr.trace.product(.relink_patch_apply);
 697                 defer patch_phase.end();
 698                 const application = try state.applyAcceptedChangedInputRelink(
 699                     target.mapping,
 700                     replacements,
 701                     relink_plan,
 702                 );
 703                 if (application.plan.decision == .in_place) {
 704                     use_in_place = try finishInPlaceCandidateRelink(
 705                         allocator,
 706                         &state,
 707                         target.mapping,
 708                         candidate.?.bytes,
 709                         candidate.?.manifest,
 710                         options,
 711                         inputs,
 712                         input_changes,
 713                         replacements,
 714                         relink_plan,
 715                     );
 716                 }
 717                 if (use_in_place) {
 718                     try appendReplacementRanges(allocator, &patched_ranges, &state, replacements);
 719                     if (try tldr.incrementalMetadataPatchRange(target.mapping, options)) |range| {
 720                         try patched_ranges.append(allocator, .{ .offset = range.offset, .len = range.len });
 721                     }
 722                     if (try tldr.incrementalBuildIdNoteRange(target.mapping, options)) |range| {
 723                         try patched_ranges.append(allocator, .{ .offset = range.offset, .len = range.len });
 724                     }
 725                 }
 726             }
 727             if (!use_in_place) {
 728                 if (patch_target) |*target| target.deinit();
 729                 patch_target = null;
 730             }
 731         }
 732     }
 733     const final_manifest = if (use_in_place) state.manifest else candidate.?.manifest;
 734 
 735     if (use_in_place) {
 736         const io_phase = tldr.trace.product(.relink_output_io);
 737         defer io_phase.end();
 738         try writeMappedRanges(cwd, parsed.output_path, patch_target.?.mapping, patched_ranges.items);
 739         if (manifest_updates) |updates| {
 740             if (try writeRelinkManifestPatches(
 741                 allocator,
 742                 cwd,
 743                 parsed,
 744                 state.manifest,
 745                 previous_manifest_bytes,
 746                 manifest_input_path,
 747                 updates,
 748             )) {
 749                 try writeRelinkLinkMap(allocator, cwd, parsed, state.manifest);
 750                 return 0;
 751             }
 752         }
 753     } else if (candidate.?.isMaterialized()) {
 754         if (mapped_output) |*output| {
 755             try output.finish(cwd);
 756         } else {
 757             return error.OutputFileError;
 758         }
 759     } else {
 760         try writeExecutableFile(cwd, parsed.output_path, candidate.?.bytes);
 761     }
 762     try writeRelinkSidecars(allocator, cwd, parsed, final_manifest, manifest_input_path, true);
 763     return 0;
 764 }
 765 
 766 fn runRelinkFullLink(
 767     allocator: Allocator,
 768     parsed: ParsedArgs,
 769     inputs: []const tldr.Input,
 770     options: tldr.LinkOptions,
 771     stderr: *std.Io.Writer,
 772     output_options: pretty.LayoutOptions,
 773     diagnostics: *tldr.Diagnostics,
 774     manifest_input_path: []const u8,
 775 ) !u8 {
 776     const cwd = sys.fs.cwd();
 777     var mapped_output = try MappedOutput.init(allocator, cwd, parsed.output_path);
 778     defer mapped_output.deinit(cwd);
 779     var candidate_options = options;
 780     candidate_options.mapped_output_file = mapped_output.file;
 781     var linked = tldr.link(allocator, inputs, candidate_options) catch |err| {
 782         try writeLinkFailure(allocator, stderr, output_options, diagnostics.linkFailure(err));
 783         return 1;
 784     };
 785     defer linked.deinit(allocator);
 786 
 787     if (linked.isMaterialized()) {
 788         try mapped_output.finish(cwd);
 789     } else {
 790         try writeExecutableFile(cwd, parsed.output_path, linked.bytes);
 791     }
 792     try writeRelinkSidecars(allocator, cwd, parsed, linked.manifest, manifest_input_path, true);
 793     return 0;
 794 }
 795 
 796 const MappedManifest = struct {
 797     bytes: []u8 = &.{},
 798     mapping: ?[]align(std.heap.page_size_min) u8 = null,
 799 
 800     fn open(cwd: std.Io.Dir, path: []const u8) !MappedManifest {
 801         var file = if (std.fs.path.isAbsolute(path))
 802             try sys.fs.openAbsoluteFile(path, .{})
 803         else
 804             try cwd.openFile(sys.fs.debugIo(), path, .{});
 805         defer file.close(sys.fs.debugIo());
 806 
 807         const stat = try file.stat(sys.fs.debugIo());
 808         const size = std.math.cast(usize, stat.size) orelse return error.FileTooBig;
 809         if (size > max_object_bytes) return error.FileTooBig;
 810         if (size == 0) return .{};
 811 
 812         const mapping = try sys.memory.mapPrivateFile(file.handle, size, .{ .read = true, .write = true }, 0);
 813         return .{
 814             .bytes = mapping[0..size],
 815             .mapping = mapping,
 816         };
 817     }
 818 
 819     fn deinit(self: *MappedManifest) void {
 820         if (self.mapping) |mapping| sys.memory.unmap(mapping);
 821         self.* = .{};
 822     }
 823 };
 824 
 825 const MappedPatchOutput = struct {
 826     raw: []align(std.heap.page_size_min) u8,
 827     mapping: []u8,
 828 
 829     fn open(cwd: std.Io.Dir, path: []const u8) !?MappedPatchOutput {
 830         var file = cwd.openFile(sys.fs.debugIo(), path, .{}) catch |err| switch (err) {
 831             error.FileNotFound => return null,
 832             else => return err,
 833         };
 834         defer file.close(sys.fs.debugIo());
 835         const stat = try file.stat(sys.fs.debugIo());
 836         const size = std.math.cast(usize, stat.size) orelse return error.FileTooBig;
 837         if (size == 0) return null;
 838         const raw = try sys.memory.mapPrivateFile(file.handle, size, .{ .read = true, .write = true }, 0);
 839         return .{ .raw = raw, .mapping = raw[0..size] };
 840     }
 841 
 842     fn deinit(self: *MappedPatchOutput) void {
 843         sys.memory.unmap(self.raw);
 844         self.* = undefined;
 845     }
 846 };
 847 
 848 fn appendReplacementRanges(
 849     allocator: Allocator,
 850     ranges: *std.ArrayListUnmanaged(tldr.incremental.FileRange),
 851     state: *const tldr.incremental.PreparedState,
 852     replacements: []const tldr.incremental.ReplacementContribution,
 853 ) !void {
 854     for (replacements) |replacement| {
 855         if (state.replacementFileRange(replacement)) |range| {
 856             try ranges.append(allocator, range);
 857         }
 858     }
 859 }
 860 
 861 fn writeMappedRanges(
 862     cwd: std.Io.Dir,
 863     path: []const u8,
 864     image: []const u8,
 865     ranges: []const tldr.incremental.FileRange,
 866 ) !void {
 867     var file = try cwd.openFile(sys.fs.debugIo(), path, .{ .mode = .write_only });
 868     defer file.close(sys.fs.debugIo());
 869     for (ranges) |range| {
 870         if (range.offset >= image.len) continue;
 871         const end = @min(range.offset + range.len, image.len);
 872         if (range.offset >= end) continue;
 873         try sys.fs.writeHandleAt(file, image[range.offset..end], range.offset);
 874     }
 875     try cwd.setFilePermissions(sys.fs.debugIo(), path, .executable_file, .{});
 876 }
 877 
 878 fn writeExecutableFile(cwd: std.Io.Dir, path: []const u8, bytes: []const u8) !void {
 879     try cwd.writeFile(sys.fs.debugIo(), .{
 880         .sub_path = path,
 881         .data = bytes,
 882         .flags = .{ .truncate = true },
 883     });
 884     try cwd.setFilePermissions(sys.fs.debugIo(), path, .executable_file, .{});
 885 }
 886 
 887 fn relinkNeedsCandidateReplacements(
 888     plan: tldr.incremental.RelinkPlan,
 889     input_changes: tldr.incremental.InputChanges,
 890 ) bool {
 891     if (plan.decision == .full_link and plan.blocker == .replacement_missing) return true;
 892     return plan.decision == .in_place and input_changes.summary.changed != 0;
 893 }
 894 
 895 fn finishInPlaceDirectRelink(
 896     state: *tldr.incremental.PreparedState,
 897     output: []u8,
 898     options: tldr.LinkOptions,
 899     inputs: []const tldr.Input,
 900     input_changes: tldr.incremental.InputChanges,
 901     replacements: []const tldr.incremental.ReplacementContribution,
 902     member_updates: []const tldr.incremental.MemberHashUpdate,
 903     relink_plan: tldr.incremental.RelinkPlan,
 904 ) Allocator.Error!bool {
 905     tldr.finishDirectIncrementalMetadataPatch(output, options) catch return false;
 906     state.updateManifestForAcceptedChangedInputRelinkFromInputChanges(
 907         inputs,
 908         input_changes,
 909         replacements,
 910         member_updates,
 911         options.inputs_read_at_ns,
 912         relink_plan,
 913     ) catch |err| switch (err) {
 914         error.OutOfMemory => return error.OutOfMemory,
 915         error.RelinkUpdateRequiresInPlacePlan,
 916         error.ReplacementMissing,
 917         error.MemberMissing,
 918         error.SectionLayoutChanged,
 919         => return false,
 920     };
 921     return true;
 922 }
 923 
 924 fn writeRelinkLinkMap(
 925     allocator: Allocator,
 926     cwd: std.Io.Dir,
 927     parsed: ParsedArgs,
 928     manifest: tldr.incremental.Manifest,
 929 ) !void {
 930     if (parsed.link_map_path) |link_map_path| {
 931         const link_map = try manifest.formatTextAlloc(allocator);
 932         try cwd.writeFile(sys.fs.debugIo(), .{
 933             .sub_path = link_map_path,
 934             .data = link_map,
 935             .flags = .{ .truncate = true },
 936         });
 937     }
 938 }
 939 
 940 fn writeRelinkManifestPatches(
 941     allocator: Allocator,
 942     cwd: std.Io.Dir,
 943     parsed: ParsedArgs,
 944     manifest: tldr.incremental.Manifest,
 945     encoded: []const u8,
 946     manifest_input_path: []const u8,
 947     updates: tldr.incremental.PreparedState.RecordUpdates,
 948 ) !bool {
 949     const manifest_output_path = parsed.manifest_path orelse manifest_input_path;
 950     if (!std.mem.eql(u8, manifest_output_path, manifest_input_path)) return false;
 951     const patches = manifest.scalarPatchesAlloc(
 952         allocator,
 953         encoded,
 954         updates.input_indexes.items,
 955         updates.contribution_indexes.items,
 956         updates.archive_member_indexes.items,
 957     ) catch |err| switch (err) {
 958         error.OutOfMemory => return error.OutOfMemory,
 959         else => return false,
 960     };
 961     defer allocator.free(patches);
 962     var file = cwd.openFile(sys.fs.debugIo(), manifest_input_path, .{ .mode = .write_only }) catch |err| switch (err) {
 963         error.FileNotFound => return false,
 964         else => return err,
 965     };
 966     defer file.close(sys.fs.debugIo());
 967     for (patches) |patch| {
 968         try sys.fs.writeHandleAt(file, patch.slice(), patch.offset);
 969     }
 970     return true;
 971 }
 972 
 973 fn writeRelinkSidecars(
 974     allocator: Allocator,
 975     cwd: std.Io.Dir,
 976     parsed: ParsedArgs,
 977     manifest: tldr.incremental.Manifest,
 978     manifest_input_path: []const u8,
 979     manifest_dirty: bool,
 980 ) !void {
 981     try writeRelinkLinkMap(allocator, cwd, parsed, manifest);
 982 
 983     if (parsed.incremental_manifest_path != null or parsed.manifest_path != null) {
 984         const manifest_output_path = parsed.manifest_path orelse if (parsed.incremental_manifest_path != null) manifest_input_path else null;
 985         if (manifest_output_path) |path| {
 986             if (!manifest_dirty and std.mem.eql(u8, path, manifest_input_path)) return;
 987             const manifest_binary = try manifest.formatBinaryAlloc(allocator);
 988             try cwd.writeFile(sys.fs.debugIo(), .{
 989                 .sub_path = path,
 990                 .data = manifest_binary,
 991                 .flags = .{ .truncate = true },
 992             });
 993         }
 994     }
 995 }
 996 
 997 fn finishInPlaceCandidateRelink(
 998     allocator: Allocator,
 999     state: *tldr.incremental.PreparedState,
1000     previous_output: []u8,
1001     candidate_bytes: []const u8,
1002     candidate_manifest: tldr.incremental.Manifest,
1003     options: tldr.LinkOptions,
1004     inputs: []const tldr.Input,
1005     input_changes: tldr.incremental.InputChanges,
1006     replacements: []const tldr.incremental.ReplacementContribution,
1007     relink_plan: tldr.incremental.RelinkPlan,
1008 ) Allocator.Error!bool {
1009     tldr.applyIncrementalMetadataPatch(previous_output, candidate_bytes, options) catch return false;
1010     state.updateManifestForAcceptedCandidateRelinkFromInputChanges(
1011         allocator,
1012         inputs,
1013         input_changes,
1014         replacements,
1015         candidate_manifest,
1016         relink_plan,
1017     ) catch |err| switch (err) {
1018         error.OutOfMemory => return error.OutOfMemory,
1019         error.RelinkUpdateRequiresInPlacePlan,
1020         error.ReplacementMissing,
1021         error.MemberMissing,
1022         error.SectionLayoutChanged,
1023         => return false,
1024     };
1025     return true;
1026 }
1027 
1028 fn writeUsage(
1029     allocator: Allocator,
1030     stderr: *std.Io.Writer,
1031     options: pretty.LayoutOptions,
1032 ) !void {
1033     try writeUsageNamed(allocator, stderr, options, "tldr-link");
1034 }
1035 
1036 fn writeUsageNamed(
1037     allocator: Allocator,
1038     stderr: *std.Io.Writer,
1039     options: pretty.LayoutOptions,
1040     command_name: []const u8,
1041 ) !void {
1042     const text = try usageTextAlloc(allocator, command_name);
1043     defer allocator.free(text);
1044     try pretty_usage.writeUsageText(allocator, stderr, text, .{ .layout = options });
1045 }
1046 
1047 fn usageTextAlloc(allocator: Allocator, command_name: []const u8) ![]const u8 {
1048     if (std.mem.eql(u8, command_name, "tldr-link")) return try allocator.dupe(u8, usage_text);
1049     const prefix = try std.fmt.allocPrint(allocator, "usage: {s}", .{command_name});
1050     defer allocator.free(prefix);
1051     return try std.mem.replaceOwned(u8, allocator, usage_text, "usage: tldr-link", prefix);
1052 }
1053 
1054 fn writeLinkFailure(
1055     allocator: Allocator,
1056     stderr: *std.Io.Writer,
1057     options: pretty.LayoutOptions,
1058     failure: tldr.Diagnostics.LinkFailure,
1059 ) !void {
1060     const rendered = try renderLinkFailureAlloc(allocator, options, failure);
1061     defer allocator.free(rendered);
1062     try stderr.writeAll(rendered);
1063 }
1064 
1065 fn renderLinkFailureAlloc(
1066     allocator: Allocator,
1067     options: pretty.LayoutOptions,
1068     failure: tldr.Diagnostics.LinkFailure,
1069 ) ![]u8 {
1070     var arena_state = std.heap.ArenaAllocator.init(allocator);
1071     defer arena_state.deinit();
1072     const builder = pretty.Builder.init(arena_state.allocator());
1073     const doc = try linkFailureDocAlloc(builder, failure);
1074     return try pretty.renderAlloc(allocator, doc, options);
1075 }
1076 
1077 fn linkFailureDocAlloc(
1078     builder: pretty.Builder,
1079     failure: tldr.Diagnostics.LinkFailure,
1080 ) !pretty.Doc {
1081     return switch (failure) {
1082         .undefined_symbols => |undefined_symbols| try undefinedSymbolsDocAlloc(builder, undefined_symbols),
1083         .undefined_symbol => |undefined_symbol| try lineWithPrefixDoc(
1084             builder,
1085             "TLDR link failed",
1086             .danger,
1087             try builder.concat(&.{
1088                 builder.text("undefined symbol"),
1089                 pretty.softline,
1090                 try builder.styledText(.name, undefined_symbol.name),
1091                 pretty.softline,
1092                 builder.text("referenced by"),
1093                 pretty.softline,
1094                 try builder.styledText(.source, undefined_symbol.input_name),
1095             }),
1096         ),
1097         .unsupported_relocation => |relocation| try unsupportedRelocationDocAlloc(builder, relocation),
1098         .unsupported_input_format => |unsupported| try unsupportedInputFormatDocAlloc(builder, unsupported),
1099         .generic => |err| try lineWithPrefixDoc(
1100             builder,
1101             "TLDR link failed",
1102             .danger,
1103             builder.text(@errorName(err)),
1104         ),
1105     };
1106 }
1107 
1108 fn undefinedSymbolsDocAlloc(
1109     builder: pretty.Builder,
1110     undefined_symbols: tldr.Diagnostics.UndefinedSymbols,
1111 ) !pretty.Doc {
1112     var parts: std.ArrayListUnmanaged(pretty.Doc) = .empty;
1113     try parts.append(builder.allocator, try lineWithPrefixDoc(
1114         builder,
1115         "TLDR link failed",
1116         .danger,
1117         builder.text("undefined symbols"),
1118     ));
1119     for (undefined_symbols.symbols) |undefined_symbol| {
1120         try parts.append(builder.allocator, try builder.spaces(2));
1121         try parts.append(builder.allocator, try builder.group(try builder.nest(2, try builder.concat(&.{
1122             try builder.styledText(.name, undefined_symbol.name),
1123             pretty.softline,
1124             builder.text("referenced by"),
1125             pretty.softline,
1126             try builder.styledText(.source, undefined_symbol.input_name),
1127         }))));
1128         try parts.append(builder.allocator, pretty.hardline);
1129     }
1130     if (undefined_symbols.hidden_count != 0) {
1131         try parts.append(builder.allocator, try builder.spaces(2));
1132         try parts.append(builder.allocator, try builder.styledText(.muted, "... and "));
1133         try parts.append(builder.allocator, try builder.styledFmt(.number, "{d}", .{undefined_symbols.hidden_count}));
1134         try parts.append(builder.allocator, try builder.styledText(.muted, " more"));
1135         try parts.append(builder.allocator, pretty.hardline);
1136     }
1137     return try builder.concat(parts.items);
1138 }
1139 
1140 fn unsupportedRelocationDocAlloc(
1141     builder: pretty.Builder,
1142     relocation: tldr.Diagnostics.UnsupportedRelocation,
1143 ) !pretty.Doc {
1144     var parts: std.ArrayListUnmanaged(pretty.Doc) = .empty;
1145     try parts.append(builder.allocator, builder.text("unsupported relocation"));
1146     try parts.append(builder.allocator, pretty.softline);
1147     try parts.append(builder.allocator, try builder.styledFmt(.number, "{d}", .{relocation.relocation_type}));
1148     try parts.append(builder.allocator, pretty.softline);
1149     try parts.append(builder.allocator, builder.text("in"));
1150     try parts.append(builder.allocator, pretty.softline);
1151     try parts.append(builder.allocator, try builder.styledText(.source, relocation.input_name));
1152     try parts.append(builder.allocator, try builder.punct(":"));
1153     try parts.append(builder.allocator, try builder.styledText(.source, relocation.section_name));
1154     if (relocation.symbol_name.len != 0) {
1155         try parts.append(builder.allocator, pretty.softline);
1156         try parts.append(builder.allocator, builder.text("against"));
1157         try parts.append(builder.allocator, pretty.softline);
1158         try parts.append(builder.allocator, try builder.styledText(.name, relocation.symbol_name));
1159     }
1160     return try lineWithPrefixDoc(
1161         builder,
1162         "TLDR link failed",
1163         .danger,
1164         try builder.concat(parts.items),
1165     );
1166 }
1167 
1168 fn unsupportedInputFormatDocAlloc(
1169     builder: pretty.Builder,
1170     unsupported: tldr.Diagnostics.UnsupportedInputFormat,
1171 ) !pretty.Doc {
1172     var parts: std.ArrayListUnmanaged(pretty.Doc) = .empty;
1173     try parts.append(builder.allocator, builder.text("unsupported input format"));
1174     try parts.append(builder.allocator, pretty.softline);
1175     try parts.append(builder.allocator, try builder.styledText(.type_name, @tagName(unsupported.input_format)));
1176     try parts.append(builder.allocator, pretty.softline);
1177     try parts.append(builder.allocator, builder.text("in"));
1178     try parts.append(builder.allocator, pretty.softline);
1179     try parts.append(builder.allocator, try builder.styledText(.source, unsupported.input_name));
1180     if (unsupported.member_name) |member_name| {
1181         try parts.append(builder.allocator, try builder.punct("("));
1182         try parts.append(builder.allocator, try builder.styledText(.source, member_name));
1183         try parts.append(builder.allocator, try builder.punct(")"));
1184     }
1185     try parts.append(builder.allocator, pretty.softline);
1186     try parts.append(builder.allocator, builder.text("for"));
1187     try parts.append(builder.allocator, pretty.softline);
1188     try parts.append(builder.allocator, try builder.styledText(.type_name, @tagName(unsupported.target_format)));
1189     try parts.append(builder.allocator, pretty.softline);
1190     try parts.append(builder.allocator, builder.text("target"));
1191     return try lineWithPrefixDoc(
1192         builder,
1193         "TLDR link failed",
1194         .danger,
1195         try builder.concat(parts.items),
1196     );
1197 }
1198 
1199 fn lineWithPrefixDoc(
1200     builder: pretty.Builder,
1201     prefix: []const u8,
1202     style: pretty.Style,
1203     message: pretty.Doc,
1204 ) !pretty.Doc {
1205     return try builder.concat(&.{
1206         try builder.group(try builder.concat(&.{
1207             try builder.styledText(style, prefix),
1208             try builder.punct(":"),
1209             try builder.nest(prefix.len + 2, try builder.concat(&.{
1210                 pretty.softline,
1211                 message,
1212             })),
1213         })),
1214         pretty.hardline,
1215     });
1216 }
1217 
1218 fn prettyOptions(file: std.Io.File) pretty.LayoutOptions {
1219     return pretty_usage.layoutOptions(file, .{});
1220 }
1221 
1222 fn parseArgs(allocator: Allocator, args: []const []const u8) !ParsedArgs {
1223     const expanded = try expandArgs(allocator, args);
1224     var input_paths: std.ArrayListUnmanaged([]const u8) = .empty;
1225     var library_search_paths: std.ArrayListUnmanaged([]const u8) = .empty;
1226     var parsed: ParsedArgs = .{ .input_paths = &.{} };
1227     var static_library_search = false;
1228 
1229     var index: usize = 0;
1230     while (index < expanded.len) : (index += 1) {
1231         const arg = expanded[index];
1232         if (std.mem.eql(u8, arg, "-o") or std.mem.eql(u8, arg, "--output")) {
1233             index += 1;
1234             if (index >= expanded.len) return error.InvalidArguments;
1235             parsed.output_path = expanded[index];
1236             continue;
1237         }
1238         if (std.mem.startsWith(u8, arg, "-o") and arg.len > 2) {
1239             parsed.output_path = arg[2..];
1240             continue;
1241         }
1242         if (std.mem.startsWith(u8, arg, "--output=")) {
1243             parsed.output_path = arg["--output=".len..];
1244             continue;
1245         }
1246         if (std.mem.eql(u8, arg, "-e") or std.mem.eql(u8, arg, "--entry")) {
1247             index += 1;
1248             if (index >= expanded.len) return error.InvalidArguments;
1249             parsed.entry_symbol = expanded[index];
1250             continue;
1251         }
1252         if (std.mem.startsWith(u8, arg, "-e") and arg.len > 2) {
1253             parsed.entry_symbol = arg[2..];
1254             continue;
1255         }
1256         if (std.mem.startsWith(u8, arg, "--entry=")) {
1257             parsed.entry_symbol = arg["--entry=".len..];
1258             continue;
1259         }
1260         if (std.mem.eql(u8, arg, "--gc-sections")) {
1261             parsed.gc_sections = true;
1262             continue;
1263         }
1264         if (std.mem.eql(u8, arg, "--no-gc-sections")) {
1265             parsed.gc_sections = false;
1266             continue;
1267         }
1268         if (std.mem.eql(u8, arg, "--strip-debug")) {
1269             parsed.strip_debug = true;
1270             continue;
1271         }
1272         if (std.mem.eql(u8, arg, "--no-strip-debug")) {
1273             parsed.strip_debug = false;
1274             continue;
1275         }
1276         if (std.mem.eql(u8, arg, "-pie") or std.mem.eql(u8, arg, "--pie")) {
1277             parsed.pie = true;
1278             continue;
1279         }
1280         if (std.mem.eql(u8, arg, "-no-pie") or std.mem.eql(u8, arg, "--no-pie")) {
1281             parsed.pie = false;
1282             continue;
1283         }
1284         if (std.mem.eql(u8, arg, "-static-pie")) {
1285             parsed.pie = true;
1286             static_library_search = true;
1287             continue;
1288         }
1289         if (std.mem.eql(u8, arg, "-shared") or std.mem.eql(u8, arg, "--shared")) {
1290             parsed.output_kind = .shared_library;
1291             continue;
1292         }
1293         if (std.mem.eql(u8, arg, "-dynamic-linker") or std.mem.eql(u8, arg, "--dynamic-linker")) {
1294             index += 1;
1295             if (index >= expanded.len or expanded[index].len == 0) return error.InvalidArguments;
1296             parsed.dynamic_linker = expanded[index];
1297             continue;
1298         }
1299         if (std.mem.startsWith(u8, arg, "-dynamic-linker=")) {
1300             const linker = arg["-dynamic-linker=".len..];
1301             if (linker.len == 0) return error.InvalidArguments;
1302             parsed.dynamic_linker = linker;
1303             continue;
1304         }
1305         if (std.mem.startsWith(u8, arg, "--dynamic-linker=")) {
1306             const linker = arg["--dynamic-linker=".len..];
1307             if (linker.len == 0) return error.InvalidArguments;
1308             parsed.dynamic_linker = linker;
1309             continue;
1310         }
1311         if (std.mem.eql(u8, arg, "-soname") or std.mem.eql(u8, arg, "--soname")) {
1312             index += 1;
1313             if (index >= expanded.len or expanded[index].len == 0) return error.InvalidArguments;
1314             parsed.soname = expanded[index];
1315             continue;
1316         }
1317         if (std.mem.startsWith(u8, arg, "-soname=")) {
1318             const soname = arg["-soname=".len..];
1319             if (soname.len == 0) return error.InvalidArguments;
1320             parsed.soname = soname;
1321             continue;
1322         }
1323         if (std.mem.startsWith(u8, arg, "--soname=")) {
1324             const soname = arg["--soname=".len..];
1325             if (soname.len == 0) return error.InvalidArguments;
1326             parsed.soname = soname;
1327             continue;
1328         }
1329         if (std.mem.eql(u8, arg, "--export-dynamic") or std.mem.eql(u8, arg, "-export-dynamic") or std.mem.eql(u8, arg, "-E")) {
1330             parsed.export_dynamic = true;
1331             continue;
1332         }
1333         if (std.mem.eql(u8, arg, "--eh-frame-hdr")) {
1334             parsed.eh_frame_header = true;
1335             continue;
1336         }
1337         if (std.mem.eql(u8, arg, "--no-eh-frame-hdr")) {
1338             parsed.eh_frame_header = false;
1339             continue;
1340         }
1341         if (std.mem.eql(u8, arg, "-L") or std.mem.eql(u8, arg, "--library-path")) {
1342             index += 1;
1343             if (index >= expanded.len or expanded[index].len == 0) return error.InvalidArguments;
1344             try library_search_paths.append(allocator, expanded[index]);
1345             continue;
1346         }
1347         if (std.mem.startsWith(u8, arg, "-L") and arg.len > 2) {
1348             try library_search_paths.append(allocator, arg[2..]);
1349             continue;
1350         }
1351         if (std.mem.startsWith(u8, arg, "--library-path=")) {
1352             const path = arg["--library-path=".len..];
1353             if (path.len == 0) return error.InvalidArguments;
1354             try library_search_paths.append(allocator, path);
1355             continue;
1356         }
1357         if (std.mem.eql(u8, arg, "-l") or std.mem.eql(u8, arg, "--library")) {
1358             index += 1;
1359             if (index >= expanded.len or expanded[index].len == 0) return error.InvalidArguments;
1360             try tldr.library.appendResolvedInputPaths(allocator, &input_paths, library_search_paths.items, expanded[index], static_library_search);
1361             continue;
1362         }
1363         if (std.mem.startsWith(u8, arg, "-l") and arg.len > 2) {
1364             try tldr.library.appendResolvedInputPaths(allocator, &input_paths, library_search_paths.items, arg[2..], static_library_search);
1365             continue;
1366         }
1367         if (std.mem.startsWith(u8, arg, "--library=")) {
1368             const library = arg["--library=".len..];
1369             if (library.len == 0) return error.InvalidArguments;
1370             try tldr.library.appendResolvedInputPaths(allocator, &input_paths, library_search_paths.items, library, static_library_search);
1371             continue;
1372         }
1373         if (std.mem.eql(u8, arg, "-static") or std.mem.eql(u8, arg, "--static") or std.mem.eql(u8, arg, "-Bstatic")) {
1374             static_library_search = true;
1375             continue;
1376         }
1377         if (std.mem.eql(u8, arg, "-Bdynamic") or std.mem.eql(u8, arg, "-bdynamic")) {
1378             static_library_search = false;
1379             continue;
1380         }
1381         if (std.mem.eql(u8, arg, "--start-group") or std.mem.eql(u8, arg, "--end-group") or std.mem.eql(u8, arg, "-(") or std.mem.eql(u8, arg, "-)")) continue;
1382         if (std.mem.eql(u8, arg, "--as-needed") or std.mem.eql(u8, arg, "--no-as-needed")) continue;
1383         if (std.mem.eql(u8, arg, "--enable-new-dtags") or std.mem.eql(u8, arg, "--disable-new-dtags")) continue;
1384         if (std.mem.eql(u8, arg, "-m")) {
1385             index += 1;
1386             if (index >= expanded.len or expanded[index].len == 0) return error.InvalidArguments;
1387             continue;
1388         }
1389         if (std.mem.startsWith(u8, arg, "-m") and arg.len > 2) continue;
1390         if (std.mem.eql(u8, arg, "-z")) {
1391             index += 1;
1392             if (index >= expanded.len or expanded[index].len == 0) return error.InvalidArguments;
1393             if (!supportedIgnoredZPolicy(expanded[index])) return error.InvalidArguments;
1394             continue;
1395         }
1396         if (std.mem.startsWith(u8, arg, "-z") and arg.len > 2) {
1397             if (!supportedIgnoredZPolicy(arg[2..])) return error.InvalidArguments;
1398             continue;
1399         }
1400         if (std.mem.eql(u8, arg, "-rpath") or std.mem.eql(u8, arg, "--rpath")) {
1401             index += 1;
1402             if (index >= expanded.len or expanded[index].len == 0) return error.InvalidArguments;
1403             continue;
1404         }
1405         if (std.mem.startsWith(u8, arg, "-rpath=") or std.mem.startsWith(u8, arg, "--rpath=")) continue;
1406         if (std.mem.eql(u8, arg, "--build-id")) {
1407             parsed.build_id = .fast;
1408             continue;
1409         }
1410         if (std.mem.startsWith(u8, arg, "--build-id=")) {
1411             parsed.build_id = try parseBuildIdMode(arg["--build-id=".len..]);
1412             continue;
1413         }
1414         if (std.mem.eql(u8, arg, "--no-build-id")) {
1415             parsed.build_id = .none;
1416             continue;
1417         }
1418         if (std.mem.eql(u8, arg, "--hash-style")) {
1419             index += 1;
1420             if (index >= expanded.len or !supportedStaticHashStyle(expanded[index])) return error.InvalidArguments;
1421             continue;
1422         }
1423         if (std.mem.startsWith(u8, arg, "--hash-style=")) {
1424             if (!supportedStaticHashStyle(arg["--hash-style=".len..])) return error.InvalidArguments;
1425             continue;
1426         }
1427         if (std.mem.eql(u8, arg, "--icf")) {
1428             index += 1;
1429             if (index >= expanded.len) return error.InvalidArguments;
1430             parsed.icf = try parseIcf(expanded[index]);
1431             continue;
1432         }
1433         if (std.mem.startsWith(u8, arg, "--icf=")) {
1434             parsed.icf = try parseIcf(arg["--icf=".len..]);
1435             continue;
1436         }
1437         if (std.mem.eql(u8, arg, "--incremental")) {
1438             index += 1;
1439             if (index >= expanded.len) return error.InvalidArguments;
1440             parsed.incremental_mode = try parseIncrementalMode(expanded[index]);
1441             continue;
1442         }
1443         if (std.mem.startsWith(u8, arg, "--incremental=")) {
1444             parsed.incremental_mode = try parseIncrementalMode(arg["--incremental=".len..]);
1445             continue;
1446         }
1447         if (std.mem.eql(u8, arg, "--incremental-manifest")) {
1448             index += 1;
1449             if (index >= expanded.len) return error.InvalidArguments;
1450             parsed.incremental_manifest_path = expanded[index];
1451             continue;
1452         }
1453         if (std.mem.startsWith(u8, arg, "--incremental-manifest=")) {
1454             parsed.incremental_manifest_path = arg["--incremental-manifest=".len..];
1455             continue;
1456         }
1457         if (std.mem.eql(u8, arg, "--emit-link-map")) {
1458             index += 1;
1459             if (index >= expanded.len) return error.InvalidArguments;
1460             parsed.link_map_path = expanded[index];
1461             continue;
1462         }
1463         if (std.mem.startsWith(u8, arg, "--emit-link-map=")) {
1464             parsed.link_map_path = arg["--emit-link-map=".len..];
1465             continue;
1466         }
1467         if (std.mem.eql(u8, arg, "-Map") or std.mem.eql(u8, arg, "--Map") or std.mem.eql(u8, arg, "--map")) {
1468             index += 1;
1469             if (index >= expanded.len) return error.InvalidArguments;
1470             parsed.link_map_path = expanded[index];
1471             continue;
1472         }
1473         if (std.mem.startsWith(u8, arg, "-Map=")) {
1474             parsed.link_map_path = arg["-Map=".len..];
1475             continue;
1476         }
1477         if (std.mem.startsWith(u8, arg, "--Map=")) {
1478             parsed.link_map_path = arg["--Map=".len..];
1479             continue;
1480         }
1481         if (std.mem.startsWith(u8, arg, "--map=")) {
1482             parsed.link_map_path = arg["--map=".len..];
1483             continue;
1484         }
1485         if (std.mem.eql(u8, arg, "--emit-manifest")) {
1486             index += 1;
1487             if (index >= expanded.len) return error.InvalidArguments;
1488             parsed.manifest_path = expanded[index];
1489             continue;
1490         }
1491         if (std.mem.startsWith(u8, arg, "--emit-manifest=")) {
1492             parsed.manifest_path = arg["--emit-manifest=".len..];
1493             continue;
1494         }
1495         if (std.mem.eql(u8, arg, "--no-fork")) continue;
1496         if (std.mem.startsWith(u8, arg, "-")) return error.InvalidArguments;
1497         try input_paths.append(allocator, arg);
1498     }
1499 
1500     parsed.input_paths = try input_paths.toOwnedSlice(allocator);
1501     return parsed;
1502 }
1503 
1504 fn supportedIgnoredZPolicy(value: []const u8) bool {
1505     return std.mem.eql(u8, value, "relro") or
1506         std.mem.eql(u8, value, "now") or
1507         std.mem.eql(u8, value, "noexecstack") or
1508         std.mem.eql(u8, value, "execstack");
1509 }
1510 
1511 fn parseBuildIdMode(value: []const u8) !tldr.model.BuildIdMode {
1512     if (std.mem.eql(u8, value, "none")) return .none;
1513     if (std.mem.eql(u8, value, "fast")) return .fast;
1514     if (std.mem.eql(u8, value, "sha1")) return .sha1;
1515     return error.InvalidArguments;
1516 }
1517 
1518 fn supportedStaticHashStyle(value: []const u8) bool {
1519     return std.mem.eql(u8, value, "gnu");
1520 }
1521 
1522 fn parseIcf(value: []const u8) !tldr.model.IcfMode {
1523     if (std.mem.eql(u8, value, "all")) return .all;
1524     if (std.mem.eql(u8, value, "off") or std.mem.eql(u8, value, "none")) return .off;
1525     return error.InvalidArguments;
1526 }
1527 
1528 fn parseIncrementalMode(value: []const u8) !tldr.model.IncrementalMode {
1529     if (std.mem.eql(u8, value, "off")) return .off;
1530     if (std.mem.eql(u8, value, "prepare")) return .prepare;
1531     if (std.mem.eql(u8, value, "relink")) return .relink;
1532     return error.InvalidArguments;
1533 }
1534 
1535 fn expandArgs(allocator: Allocator, args: []const []const u8) ![]const []const u8 {
1536     var expanded: std.ArrayListUnmanaged([]const u8) = .empty;
1537     for (args) |arg| {
1538         if (arg.len > 1 and arg[0] == '@') {
1539             const bytes = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), arg[1..], allocator, .limited(max_response_bytes));
1540             try appendResponseTokens(allocator, &expanded, bytes);
1541         } else {
1542             try expanded.append(allocator, arg);
1543         }
1544     }
1545     return try expanded.toOwnedSlice(allocator);
1546 }
1547 
1548 fn appendResponseTokens(
1549     allocator: Allocator,
1550     tokens: *std.ArrayListUnmanaged([]const u8),
1551     bytes: []const u8,
1552 ) !void {
1553     var index: usize = 0;
1554     while (true) {
1555         while (index < bytes.len and std.ascii.isWhitespace(bytes[index])) index += 1;
1556         if (index >= bytes.len) return;
1557 
1558         var token: std.ArrayListUnmanaged(u8) = .empty;
1559         while (index < bytes.len and !std.ascii.isWhitespace(bytes[index])) {
1560             const byte = bytes[index];
1561             if (byte == '"' or byte == '\'') {
1562                 const quote = byte;
1563                 index += 1;
1564                 while (index < bytes.len and bytes[index] != quote) : (index += 1) {
1565                     if (bytes[index] == '\\' and index + 1 < bytes.len) index += 1;
1566                     try token.append(allocator, bytes[index]);
1567                 }
1568                 if (index >= bytes.len) return error.InvalidArguments;
1569                 index += 1;
1570                 continue;
1571             }
1572             if (byte == '\\' and index + 1 < bytes.len) {
1573                 index += 1;
1574                 try token.append(allocator, bytes[index]);
1575                 index += 1;
1576                 continue;
1577             }
1578             try token.append(allocator, byte);
1579             index += 1;
1580         }
1581         try tokens.append(allocator, try token.toOwnedSlice(allocator));
1582     }
1583 }
1584 
1585 test "response tokenizer preserves quoted tokens" {
1586     const allocator = std.testing.allocator;
1587     var tokens: std.ArrayListUnmanaged([]const u8) = .empty;
1588     defer {
1589         for (tokens.items) |token| allocator.free(token);
1590         tokens.deinit(allocator);
1591     }
1592 
1593     try appendResponseTokens(allocator, &tokens, "-o \"linked image\" 'input object.o'");
1594 
1595     try std.testing.expectEqual(@as(usize, 3), tokens.items.len);
1596     try std.testing.expectEqualStrings("-o", tokens.items[0]);
1597     try std.testing.expectEqualStrings("linked image", tokens.items[1]);
1598     try std.testing.expectEqualStrings("input object.o", tokens.items[2]);
1599 }
1600 
1601 test "usage renders through pretty plain and colored output" {
1602     const plain = try pretty_usage.renderUsageTextAlloc(std.testing.allocator, usage_text, .{ .layout = .{ .width = 120 } });
1603     defer std.testing.allocator.free(plain);
1604     try std.testing.expect(std.mem.indexOf(u8, plain, "\x1b[") == null);
1605     try std.testing.expect(std.mem.startsWith(u8, plain, "usage: tldr-link"));
1606 
1607     const colored = try pretty_usage.renderUsageTextAlloc(std.testing.allocator, usage_text, .{ .layout = .{ .width = 120, .color = .ansi } });
1608     defer std.testing.allocator.free(colored);
1609     try std.testing.expect(std.mem.indexOf(u8, colored, "\x1b[") != null);
1610     try std.testing.expect(std.mem.indexOf(u8, colored, "tldr-link") != null);
1611 }
1612 
1613 test "usage wraps long option list at terminal width" {
1614     const rendered = try pretty_usage.renderUsageTextAlloc(std.testing.allocator, usage_text, .{ .layout = .{ .width = 48 } });
1615     defer std.testing.allocator.free(rendered);
1616     try std.testing.expect(std.mem.indexOf(u8, rendered, "\n  ") != null);
1617     try std.testing.expect(std.mem.indexOf(u8, rendered, "[-L PATH]") != null);
1618     try std.testing.expect(std.mem.indexOf(u8, rendered, "[--gc-sections]") != null);
1619 }
1620 
1621 test "command errors render through pretty plain and colored output" {
1622     const plain = try pretty_usage.renderErrorTextAlloc(std.testing.allocator, "tldr-link", "OutOfMemory", .{ .layout = .{ .width = 88 } });
1623     defer std.testing.allocator.free(plain);
1624     try std.testing.expectEqualStrings("tldr-link: OutOfMemory\n", plain);
1625 
1626     const colored = try pretty_usage.renderErrorTextAlloc(std.testing.allocator, "tldr-link", "OutOfMemory", .{ .layout = .{ .width = 88, .color = .ansi } });
1627     defer std.testing.allocator.free(colored);
1628     try std.testing.expect(std.mem.indexOf(u8, colored, "\x1b[") != null);
1629     try std.testing.expect(std.mem.indexOf(u8, colored, "OutOfMemory") != null);
1630 }
1631 
1632 test "link failures render through pretty plain and colored output" {
1633     var diagnostics: tldr.Diagnostics = .{};
1634     diagnostics.recordUndefinedSymbol("tiny.o", "__tiny_make_integer");
1635     diagnostics.recordUndefinedSymbol("libtinyrt.a", "memcpy");
1636 
1637     const plain = try renderLinkFailureAlloc(
1638         std.testing.allocator,
1639         .{ .width = 120 },
1640         diagnostics.linkFailure(error.UndefinedSymbol),
1641     );
1642     defer std.testing.allocator.free(plain);
1643     try std.testing.expectEqualStrings(
1644         \\TLDR link failed: undefined symbols
1645         \\  __tiny_make_integer referenced by tiny.o
1646         \\  memcpy referenced by libtinyrt.a
1647         \\
1648     , plain);
1649 
1650     const colored = try renderLinkFailureAlloc(
1651         std.testing.allocator,
1652         .{ .width = 120, .color = .ansi },
1653         diagnostics.linkFailure(error.UndefinedSymbol),
1654     );
1655     defer std.testing.allocator.free(colored);
1656     try std.testing.expect(std.mem.indexOf(u8, colored, "\x1b[") != null);
1657     try std.testing.expect(std.mem.indexOf(u8, colored, "undefined symbols") != null);
1658 }
1659 
1660 test "link failure details wrap at terminal width" {
1661     var diagnostics: tldr.Diagnostics = .{};
1662     diagnostics.recordUnsupportedArchiveMemberFormat("libmixed.a", "needed.obj", .coff, .elf);
1663 
1664     const rendered = try renderLinkFailureAlloc(
1665         std.testing.allocator,
1666         .{ .width = 48 },
1667         diagnostics.linkFailure(error.UnsupportedFormat),
1668     );
1669     defer std.testing.allocator.free(rendered);
1670     try std.testing.expect(std.mem.indexOf(u8, rendered, "TLDR link failed:\n") != null);
1671     try std.testing.expect(std.mem.indexOf(u8, rendered, "unsupported input format") != null);
1672 }
1673 
1674 test "CLI input loader maps file bytes" {
1675     const allocator = std.testing.allocator;
1676     var tmp = std.testing.tmpDir(.{});
1677     defer tmp.cleanup();
1678 
1679     try tmp.dir.writeFile(sys.fs.debugIo(), .{
1680         .sub_path = "input.o",
1681         .data = "mapped object",
1682     });
1683     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
1684     defer allocator.free(root);
1685     const object_path = try std.fs.path.join(allocator, &.{ root, "input.o" });
1686     defer allocator.free(object_path);
1687 
1688     var loaded = try loadInputs(allocator, &.{object_path});
1689     defer loaded.deinit(allocator);
1690 
1691     try std.testing.expectEqual(@as(usize, 1), loaded.inputs.items.len);
1692     try std.testing.expectEqualStrings("mapped object", loaded.inputs.items[0].bytes);
1693     try std.testing.expect(loaded.files.items[0].mapping != null);
1694 }
1695 
1696 test "CLI input loader reuses duplicate archive paths" {
1697     const allocator = std.testing.allocator;
1698     var tmp = std.testing.tmpDir(.{});
1699     defer tmp.cleanup();
1700 
1701     try tmp.dir.writeFile(sys.fs.debugIo(), .{
1702         .sub_path = "libinput.a",
1703         .data = "mapped archive",
1704     });
1705     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
1706     defer allocator.free(root);
1707     const archive_path = try std.fs.path.join(allocator, &.{ root, "libinput.a" });
1708     defer allocator.free(archive_path);
1709 
1710     var loaded = try loadInputs(allocator, &.{ archive_path, archive_path });
1711     defer loaded.deinit(allocator);
1712 
1713     try std.testing.expectEqual(@as(usize, 2), loaded.inputs.items.len);
1714     try std.testing.expectEqual(@as(usize, 1), loaded.files.items.len);
1715     try std.testing.expectEqual(loaded.inputs.items[0].bytes.ptr, loaded.inputs.items[1].bytes.ptr);
1716 }
1717 
1718 test "CLI input prefault threshold protects small links" {
1719     try std.testing.expect(!prefaultInputs(prefault_input_threshold - 1));
1720     if (sys.thread.threadsSupported()) {
1721         try std.testing.expect(prefaultInputs(prefault_input_threshold));
1722     }
1723 }
1724 
1725 test "CLI input prefault touches mapped bytes" {
1726     const allocator = std.testing.allocator;
1727     const bytes = try allocator.alloc(u8, sys.memory.pageSize() * 2 + 1);
1728     defer allocator.free(bytes);
1729     @memset(bytes, 0x5a);
1730 
1731     var loaded: LoadedInputs = .{};
1732     defer loaded.files.deinit(allocator);
1733     try loaded.files.append(allocator, .{ .bytes = bytes });
1734     prefaultMappedInputs(&loaded);
1735 }
1736 
1737 test "CLI executable output writer sets executable permissions" {
1738     if (comptime !sys.fs.FilePermissions.has_executable_bit) return error.SkipZigTest;
1739 
1740     const allocator = std.testing.allocator;
1741     var tmp = std.testing.tmpDir(.{});
1742     defer tmp.cleanup();
1743 
1744     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
1745     defer allocator.free(root);
1746     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
1747     defer allocator.free(output_path);
1748 
1749     try writeExecutableFile(sys.fs.cwd(), output_path, std.elf.MAGIC);
1750 
1751     const stat = try sys.fs.statFile(output_path);
1752     try std.testing.expect((stat.permissions.toMode() & 0o111) != 0);
1753 }
1754 
1755 test "CLI links a response-file ELF object" {
1756     const allocator = std.testing.allocator;
1757     var tmp = std.testing.tmpDir(.{});
1758     defer tmp.cleanup();
1759 
1760     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
1761     defer allocator.free(root);
1762     const object_path = try std.fs.path.join(allocator, &.{ root, "start.o" });
1763     defer allocator.free(object_path);
1764     const response_path = try std.fs.path.join(allocator, &.{ root, "response.txt" });
1765     defer allocator.free(response_path);
1766     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
1767     defer allocator.free(output_path);
1768 
1769     const text = [_]u8{
1770         0xb8, 0x3c, 0x00, 0x00, 0x00,
1771         0x31, 0xff, 0x0f, 0x05,
1772     };
1773     const object = try elf_object.build(allocator, .{
1774         .sections = &.{elf_object.Section.progbits(".text", &text, std.elf.SHF_EXECINSTR, 16)},
1775         .symbols = &.{
1776             elf_object.Symbol.section(1),
1777             elf_object.Symbol.function("_start", 1, 0, text.len),
1778         },
1779     });
1780     defer allocator.free(object);
1781 
1782     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = object_path, .data = object });
1783     const response = try std.fmt.allocPrint(allocator, "-o {s} -e _start {s}\n", .{ output_path, object_path });
1784     defer allocator.free(response);
1785     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = response_path, .data = response });
1786 
1787     var stderr_buffer: [512]u8 = undefined;
1788     var stderr = std.Io.Writer.fixed(&stderr_buffer);
1789     const response_arg = try std.fmt.allocPrint(allocator, "@{s}", .{response_path});
1790     defer allocator.free(response_arg);
1791     const code = try run(allocator, &.{response_arg}, &stderr, .{ .width = 88 });
1792     try std.testing.expectEqual(@as(u8, 0), code);
1793 
1794     const linked = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), output_path, allocator, .limited(4096));
1795     defer allocator.free(linked);
1796     try std.testing.expectEqualSlices(u8, std.elf.MAGIC, linked[0..4]);
1797     try std.testing.expectEqual(@as(u64, text.len), try linkedSectionSize(linked, ".text"));
1798 }
1799 
1800 fn expectNoOutputLitter(allocator: Allocator, root: []const u8) !void {
1801     const entries = try sys.fs.listDirAlloc(allocator, root);
1802     defer {
1803         for (entries) |entry| entry.deinit(allocator);
1804         allocator.free(entries);
1805     }
1806     for (entries) |entry| {
1807         try std.testing.expect(std.mem.indexOf(u8, entry.name, ".tldr-tmp-") == null);
1808         try std.testing.expect(std.mem.indexOf(u8, entry.name, ".tldr-old-") == null);
1809     }
1810 }
1811 
1812 test "CLI relinking over an existing output leaves no litter" {
1813     const allocator = std.testing.allocator;
1814     var tmp = std.testing.tmpDir(.{});
1815     defer tmp.cleanup();
1816 
1817     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
1818     defer allocator.free(root);
1819     const object_path = try std.fs.path.join(allocator, &.{ root, "start.o" });
1820     defer allocator.free(object_path);
1821     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
1822     defer allocator.free(output_path);
1823 
1824     const text = [_]u8{
1825         0xb8, 0x3c, 0x00, 0x00, 0x00,
1826         0x31, 0xff, 0x0f, 0x05,
1827     };
1828     const object = try elf_object.build(allocator, .{
1829         .sections = &.{elf_object.Section.progbits(".text", &text, std.elf.SHF_EXECINSTR, 16)},
1830         .symbols = &.{
1831             elf_object.Symbol.section(1),
1832             elf_object.Symbol.function("_start", 1, 0, text.len),
1833         },
1834     });
1835     defer allocator.free(object);
1836     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = object_path, .data = object });
1837 
1838     var stderr_buffer: [512]u8 = undefined;
1839     var stderr = std.Io.Writer.fixed(&stderr_buffer);
1840     const args = [_][]const u8{ "-o", output_path, "-e", "_start", object_path };
1841     try std.testing.expectEqual(@as(u8, 0), try run(allocator, &args, &stderr, .{ .width = 88 }));
1842     try std.testing.expectEqual(@as(u8, 0), try run(allocator, &args, &stderr, .{ .width = 88 }));
1843 
1844     const linked = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), output_path, allocator, .limited(4096));
1845     defer allocator.free(linked);
1846     try std.testing.expectEqualSlices(u8, std.elf.MAGIC, linked[0..4]);
1847     try expectNoOutputLitter(allocator, root);
1848 }
1849 
1850 test "CLI failed links preserve the previous output" {
1851     const allocator = std.testing.allocator;
1852     var tmp = std.testing.tmpDir(.{});
1853     defer tmp.cleanup();
1854 
1855     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
1856     defer allocator.free(root);
1857     const object_path = try std.fs.path.join(allocator, &.{ root, "start.o" });
1858     defer allocator.free(object_path);
1859     const broken_path = try std.fs.path.join(allocator, &.{ root, "broken.o" });
1860     defer allocator.free(broken_path);
1861     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
1862     defer allocator.free(output_path);
1863 
1864     const text = [_]u8{
1865         0xb8, 0x3c, 0x00, 0x00, 0x00,
1866         0x31, 0xff, 0x0f, 0x05,
1867     };
1868     const object = try elf_object.build(allocator, .{
1869         .sections = &.{elf_object.Section.progbits(".text", &text, std.elf.SHF_EXECINSTR, 16)},
1870         .symbols = &.{
1871             elf_object.Symbol.section(1),
1872             elf_object.Symbol.function("_start", 1, 0, text.len),
1873         },
1874     });
1875     defer allocator.free(object);
1876     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = object_path, .data = object });
1877 
1878     const call = [_]u8{ 0xe8, 0, 0, 0, 0, 0xc3 };
1879     const broken_object = try elf_object.build(allocator, .{
1880         .sections = &.{elf_object.Section.progbits(".text", &call, std.elf.SHF_EXECINSTR, 16)},
1881         .symbols = &.{
1882             elf_object.Symbol.section(1),
1883             elf_object.Symbol.function("_start", 1, 0, call.len),
1884             elf_object.Symbol.undefinedFunction("missing"),
1885         },
1886         .relocations = &.{elf_object.Relocation.x86_64(1, 1, 3, .PLT32, -4)},
1887     });
1888     defer allocator.free(broken_object);
1889     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = broken_path, .data = broken_object });
1890 
1891     var stderr_buffer: [2048]u8 = undefined;
1892     var stderr = std.Io.Writer.fixed(&stderr_buffer);
1893     const good_args = [_][]const u8{ "-o", output_path, "-e", "_start", object_path };
1894     try std.testing.expectEqual(@as(u8, 0), try run(allocator, &good_args, &stderr, .{ .width = 88 }));
1895     const before = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), output_path, allocator, .limited(4096));
1896     defer allocator.free(before);
1897 
1898     const broken_args = [_][]const u8{ "-o", output_path, "-e", "_start", broken_path };
1899     try std.testing.expectEqual(@as(u8, 1), try run(allocator, &broken_args, &stderr, .{ .width = 88 }));
1900 
1901     const after = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), output_path, allocator, .limited(4096));
1902     defer allocator.free(after);
1903     try std.testing.expectEqualSlices(u8, before, after);
1904     try expectNoOutputLitter(allocator, root);
1905 }
1906 
1907 test "CLI accepts strip-debug for DWARF payloads" {
1908     const allocator = std.testing.allocator;
1909     var tmp = std.testing.tmpDir(.{});
1910     defer tmp.cleanup();
1911 
1912     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
1913     defer allocator.free(root);
1914     const object_path = try std.fs.path.join(allocator, &.{ root, "debug.o" });
1915     defer allocator.free(object_path);
1916     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
1917     defer allocator.free(output_path);
1918 
1919     const text = [_]u8{0xc3};
1920     const debug = [_]u8{ 1, 2, 3, 4 };
1921     const object = try elf_object.build(allocator, .{
1922         .sections = &.{
1923             elf_object.Section.progbits(".text", &text, std.elf.SHF_EXECINSTR, 16),
1924             elf_object.Section.nonAlloc(".debug_info", &debug, std.elf.SHT_PROGBITS, 1),
1925         },
1926         .symbols = &.{
1927             elf_object.Symbol.section(1),
1928             elf_object.Symbol.section(2),
1929             elf_object.Symbol.function("_start", 1, 0, text.len),
1930         },
1931     });
1932     defer allocator.free(object);
1933 
1934     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = object_path, .data = object });
1935 
1936     var stderr_buffer: [512]u8 = undefined;
1937     var stderr = std.Io.Writer.fixed(&stderr_buffer);
1938     const code = try run(allocator, &.{ "--strip-debug", "-o", output_path, object_path }, &stderr, .{ .width = 88 });
1939     try std.testing.expectEqual(@as(u8, 0), code);
1940 
1941     const linked = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), output_path, allocator, .limited(4096));
1942     defer allocator.free(linked);
1943     try std.testing.expectEqualSlices(u8, std.elf.MAGIC, linked[0..4]);
1944     try std.testing.expectError(error.MissingSection, linkedSectionSize(linked, ".debug_info"));
1945 }
1946 
1947 test "CLI resolves static library search inputs" {
1948     const allocator = std.testing.allocator;
1949     var tmp = std.testing.tmpDir(.{});
1950     defer tmp.cleanup();
1951 
1952     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
1953     defer allocator.free(root);
1954     const library_dir = try std.fs.path.join(allocator, &.{ root, "lib" });
1955     defer allocator.free(library_dir);
1956     const library_path = try std.fs.path.join(allocator, &.{ library_dir, "libtiny.a" });
1957     defer allocator.free(library_path);
1958     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
1959     defer allocator.free(output_path);
1960 
1961     try sys.fs.createDirPath(library_dir);
1962 
1963     const text = [_]u8{
1964         0xb8, 0x3c, 0x00, 0x00, 0x00,
1965         0x31, 0xff, 0x0f, 0x05,
1966     };
1967     const object = try elf_object.build(allocator, .{
1968         .sections = &.{elf_object.Section.progbits(".text", &text, std.elf.SHF_EXECINSTR, 16)},
1969         .symbols = &.{
1970             elf_object.Symbol.section(1),
1971             elf_object.Symbol.function("_start", 1, 0, text.len),
1972         },
1973     });
1974     defer allocator.free(object);
1975     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = library_path, .data = object });
1976 
1977     var stderr_buffer: [512]u8 = undefined;
1978     var stderr = std.Io.Writer.fixed(&stderr_buffer);
1979     const search_arg = try std.fmt.allocPrint(allocator, "-L{s}", .{library_dir});
1980     defer allocator.free(search_arg);
1981     const code = try run(
1982         allocator,
1983         &.{
1984             "-m",
1985             "elf_x86_64",
1986             "--as-needed",
1987             "-static",
1988             "--build-id",
1989             "--hash-style=gnu",
1990             "-z",
1991             "relro",
1992             "--start-group",
1993             search_arg,
1994             "-ltiny",
1995             "--end-group",
1996             "-o",
1997             output_path,
1998         },
1999         &stderr,
2000         .{ .width = 88 },
2001     );
2002     try std.testing.expectEqual(@as(u8, 0), code);
2003 
2004     const linked = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), output_path, allocator, .limited(4096));
2005     defer allocator.free(linked);
2006     try std.testing.expectEqualSlices(u8, std.elf.MAGIC, linked[0..4]);
2007     try std.testing.expectEqual(@as(u64, text.len), try linkedSectionSize(linked, ".text"));
2008     try std.testing.expectEqual(@as(u64, 24), try linkedSectionSize(linked, ".note.gnu.build-id"));
2009 }
2010 
2011 test "CLI expands static linker script group inputs" {
2012     const allocator = std.testing.allocator;
2013     var tmp = std.testing.tmpDir(.{});
2014     defer tmp.cleanup();
2015 
2016     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
2017     defer allocator.free(root);
2018     const library_dir = try std.fs.path.join(allocator, &.{ root, "lib" });
2019     defer allocator.free(library_dir);
2020     const object_path = try std.fs.path.join(allocator, &.{ library_dir, "tiny.o" });
2021     defer allocator.free(object_path);
2022     const script_path = try std.fs.path.join(allocator, &.{ library_dir, "libtiny.a" });
2023     defer allocator.free(script_path);
2024     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
2025     defer allocator.free(output_path);
2026 
2027     try sys.fs.createDirPath(library_dir);
2028 
2029     const text = [_]u8{
2030         0xb8, 0x3c, 0x00, 0x00, 0x00,
2031         0x31, 0xff, 0x0f, 0x05,
2032     };
2033     const object = try elf_object.build(allocator, .{
2034         .sections = &.{elf_object.Section.progbits(".text", &text, std.elf.SHF_EXECINSTR, 16)},
2035         .symbols = &.{
2036             elf_object.Symbol.section(1),
2037             elf_object.Symbol.function("_start", 1, 0, text.len),
2038         },
2039     });
2040     defer allocator.free(object);
2041     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = object_path, .data = object });
2042 
2043     const script = try std.fmt.allocPrint(
2044         allocator,
2045         "/* GNU ld script */\nOUTPUT_FORMAT(elf64-x86-64)\nGROUP ( tiny.o )\n",
2046         .{},
2047     );
2048     defer allocator.free(script);
2049     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = script_path, .data = script });
2050 
2051     var stderr_buffer: [512]u8 = undefined;
2052     var stderr = std.Io.Writer.fixed(&stderr_buffer);
2053     const search_arg = try std.fmt.allocPrint(allocator, "-L{s}", .{library_dir});
2054     defer allocator.free(search_arg);
2055     const code = try run(
2056         allocator,
2057         &.{
2058             "-static",
2059             search_arg,
2060             "-ltiny",
2061             "-o",
2062             output_path,
2063         },
2064         &stderr,
2065         .{ .width = 88 },
2066     );
2067     try std.testing.expectEqual(@as(u8, 0), code);
2068 
2069     const linked = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), output_path, allocator, .limited(4096));
2070     defer allocator.free(linked);
2071     try std.testing.expectEqualSlices(u8, std.elf.MAGIC, linked[0..4]);
2072     try std.testing.expectEqual(@as(u64, text.len), try linkedSectionSize(linked, ".text"));
2073 }
2074 
2075 test "CLI parses incremental preparation mode" {
2076     var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
2077     defer arena_state.deinit();
2078     const allocator = arena_state.allocator();
2079 
2080     const compact = try parseArgs(allocator, &.{"input.o"});
2081     try std.testing.expectEqual(tldr.model.IncrementalMode.off, compact.incremental_mode);
2082 
2083     const prepared = try parseArgs(allocator, &.{ "--incremental=prepare", "input.o" });
2084     try std.testing.expectEqual(tldr.model.IncrementalMode.prepare, prepared.incremental_mode);
2085 
2086     const stripped = try parseArgs(allocator, &.{ "--strip-debug", "input.o" });
2087     try std.testing.expect(stripped.strip_debug);
2088 
2089     const relink = try parseArgs(allocator, &.{ "--incremental=relink", "--incremental-manifest=linked.manifest", "input.o" });
2090     try std.testing.expectEqual(tldr.model.IncrementalMode.relink, relink.incremental_mode);
2091     try std.testing.expectEqualStrings("linked.manifest", relink.incremental_manifest_path.?);
2092 
2093     const link_map = try parseArgs(allocator, &.{ "--emit-link-map=linked.map", "input.o" });
2094     try std.testing.expectEqualStrings("linked.map", link_map.link_map_path.?);
2095 
2096     const gnu_link_map = try parseArgs(allocator, &.{ "-Map=linked.gnu.map", "input.o" });
2097     try std.testing.expectEqualStrings("linked.gnu.map", gnu_link_map.link_map_path.?);
2098 
2099     const gnu_link_map_separate = try parseArgs(allocator, &.{ "-Map", "linked.gnu.separate.map", "input.o" });
2100     try std.testing.expectEqualStrings("linked.gnu.separate.map", gnu_link_map_separate.link_map_path.?);
2101 
2102     const long_gnu_link_map = try parseArgs(allocator, &.{ "--Map=linked.long.map", "input.o" });
2103     try std.testing.expectEqualStrings("linked.long.map", long_gnu_link_map.link_map_path.?);
2104 
2105     const manifest = try parseArgs(allocator, &.{ "--emit-manifest=linked.manifest", "input.o" });
2106     try std.testing.expectEqualStrings("linked.manifest", manifest.manifest_path.?);
2107 }
2108 
2109 test "CLI accepts static output metadata policy flags narrowly" {
2110     var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
2111     defer arena_state.deinit();
2112     const allocator = arena_state.allocator();
2113 
2114     const build_id = try parseArgs(allocator, &.{ "--build-id", "input.o" });
2115     try std.testing.expectEqual(tldr.model.BuildIdMode.fast, build_id.build_id);
2116 
2117     const build_id_sha1 = try parseArgs(allocator, &.{ "--build-id=sha1", "input.o" });
2118     try std.testing.expectEqual(tldr.model.BuildIdMode.sha1, build_id_sha1.build_id);
2119 
2120     const build_id_fast = try parseArgs(allocator, &.{ "--build-id=fast", "--hash-style=gnu", "input.o" });
2121     try std.testing.expectEqual(tldr.model.BuildIdMode.fast, build_id_fast.build_id);
2122 
2123     const build_id_none = try parseArgs(allocator, &.{ "--build-id=none", "input.o" });
2124     try std.testing.expectEqual(tldr.model.BuildIdMode.none, build_id_none.build_id);
2125 
2126     try std.testing.expectError(error.InvalidArguments, parseArgs(allocator, &.{ "--build-id=uuid", "input.o" }));
2127     try std.testing.expectError(error.InvalidArguments, parseArgs(allocator, &.{ "--hash-style=both", "input.o" }));
2128     try std.testing.expectError(error.InvalidArguments, parseArgs(allocator, &.{ "--hash-style", "sysv", "input.o" }));
2129     try std.testing.expectError(error.InvalidArguments, parseArgs(allocator, &.{ "-z", "max-page-size=0x10000", "input.o" }));
2130 }
2131 
2132 test "CLI parses dynamic output policy flags" {
2133     var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
2134     defer arena_state.deinit();
2135     const allocator = arena_state.allocator();
2136 
2137     const parsed = try parseArgs(allocator, &.{ "--eh-frame-hdr", "-pie", "-dynamic-linker", "/lib64/ld-linux-x86-64.so.2", "--export-dynamic", "-soname=libtiny.so", "input.o" });
2138     try std.testing.expect(parsed.eh_frame_header);
2139     try std.testing.expect(parsed.pie);
2140     try std.testing.expectEqualStrings("/lib64/ld-linux-x86-64.so.2", parsed.dynamic_linker.?);
2141     try std.testing.expect(parsed.export_dynamic);
2142     try std.testing.expectEqualStrings("libtiny.so", parsed.soname.?);
2143     try std.testing.expectEqual(tldr.model.OutputKind.executable, parsed.output_kind);
2144 
2145     const shared = try parseArgs(allocator, &.{ "--shared", "--dynamic-linker=/lib64/ld-linux-x86-64.so.2", "input.o" });
2146     try std.testing.expectEqual(tldr.model.OutputKind.shared_library, shared.output_kind);
2147     try std.testing.expectEqualStrings("/lib64/ld-linux-x86-64.so.2", shared.dynamic_linker.?);
2148 
2149     try std.testing.expectError(error.InvalidArguments, parseArgs(allocator, &.{"-dynamic-linker"}));
2150     try std.testing.expectError(error.InvalidArguments, parseArgs(allocator, &.{ "--dynamic-linker", "" }));
2151     try std.testing.expectError(error.InvalidArguments, parseArgs(allocator, &.{ "-soname=", "input.o" }));
2152 }
2153 
2154 test "CLI reports dynamic output as linker failure" {
2155     const allocator = std.testing.allocator;
2156     var tmp = std.testing.tmpDir(.{});
2157     defer tmp.cleanup();
2158 
2159     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
2160     defer allocator.free(root);
2161     const object_path = try std.fs.path.join(allocator, &.{ root, "start.o" });
2162     defer allocator.free(object_path);
2163     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
2164     defer allocator.free(output_path);
2165 
2166     const text = [_]u8{0xc3};
2167     const object = try elf_object.build(allocator, .{
2168         .sections = &.{elf_object.Section.progbits(".text", &text, std.elf.SHF_EXECINSTR, 16)},
2169         .symbols = &.{
2170             elf_object.Symbol.section(1),
2171             elf_object.Symbol.function("_start", 1, 0, text.len),
2172         },
2173     });
2174     defer allocator.free(object);
2175     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = object_path, .data = object });
2176 
2177     var stderr_buffer: [512]u8 = undefined;
2178     var stderr = std.Io.Writer.fixed(&stderr_buffer);
2179     const code = try run(allocator, &.{ "-pie", "-dynamic-linker", "/lib64/ld-linux-x86-64.so.2", "-o", output_path, object_path }, &stderr, .{ .width = 88 });
2180     try std.testing.expectEqual(@as(u8, 1), code);
2181     try std.testing.expect(std.mem.indexOf(u8, stderr.buffered(), "UnsupportedDynamicLinking") != null);
2182     try std.testing.expect(std.mem.indexOf(u8, stderr.buffered(), "usage: tldr-link") == null);
2183 }
2184 
2185 test "CLI emits prepared manifest link map" {
2186     const allocator = std.testing.allocator;
2187     var tmp = std.testing.tmpDir(.{});
2188     defer tmp.cleanup();
2189 
2190     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
2191     defer allocator.free(root);
2192     const object_path = try std.fs.path.join(allocator, &.{ root, "start.o" });
2193     defer allocator.free(object_path);
2194     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
2195     defer allocator.free(output_path);
2196     const link_map_path = try std.fs.path.join(allocator, &.{ root, "linked.map" });
2197     defer allocator.free(link_map_path);
2198     const link_map_arg = try std.fmt.allocPrint(allocator, "-Map={s}", .{link_map_path});
2199     defer allocator.free(link_map_arg);
2200 
2201     const text = [_]u8{0xc3};
2202     const object = try elf_object.build(allocator, .{
2203         .sections = &.{elf_object.Section.progbits(".text", &text, std.elf.SHF_EXECINSTR, 16)},
2204         .symbols = &.{
2205             elf_object.Symbol.section(1),
2206             elf_object.Symbol.function("_start", 1, 0, text.len),
2207         },
2208     });
2209     defer allocator.free(object);
2210 
2211     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = object_path, .data = object });
2212 
2213     var stderr_buffer: [512]u8 = undefined;
2214     var stderr = std.Io.Writer.fixed(&stderr_buffer);
2215     const code = try run(
2216         allocator,
2217         &.{
2218             "--incremental=prepare",
2219             link_map_arg,
2220             "-o",
2221             output_path,
2222             "-e",
2223             "_start",
2224             object_path,
2225         },
2226         &stderr,
2227         .{ .width = 88 },
2228     );
2229     try std.testing.expectEqual(@as(u8, 0), code);
2230 
2231     const link_map = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), link_map_path, allocator, .limited(4096));
2232     defer allocator.free(link_map);
2233     try std.testing.expect(std.mem.indexOf(u8, link_map, "tldr link map\n") != null);
2234     try std.testing.expect(std.mem.indexOf(u8, link_map, "hash=0x") != null);
2235     try std.testing.expect(std.mem.indexOf(u8, link_map, "contributions (1):") != null);
2236 }
2237 
2238 test "CLI emits prepared private manifest" {
2239     const allocator = std.testing.allocator;
2240     var tmp = std.testing.tmpDir(.{});
2241     defer tmp.cleanup();
2242 
2243     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
2244     defer allocator.free(root);
2245     const object_path = try std.fs.path.join(allocator, &.{ root, "start.o" });
2246     defer allocator.free(object_path);
2247     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
2248     defer allocator.free(output_path);
2249     const manifest_path = try std.fs.path.join(allocator, &.{ root, "linked.manifest" });
2250     defer allocator.free(manifest_path);
2251 
2252     const text = [_]u8{0xc3};
2253     const object = try elf_object.build(allocator, .{
2254         .sections = &.{elf_object.Section.progbits(".text", &text, std.elf.SHF_EXECINSTR, 16)},
2255         .symbols = &.{
2256             elf_object.Symbol.section(1),
2257             elf_object.Symbol.function("_start", 1, 0, text.len),
2258         },
2259     });
2260     defer allocator.free(object);
2261 
2262     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = object_path, .data = object });
2263 
2264     var stderr_buffer: [512]u8 = undefined;
2265     var stderr = std.Io.Writer.fixed(&stderr_buffer);
2266     const code = try run(
2267         allocator,
2268         &.{
2269             "--incremental=prepare",
2270             "--emit-manifest",
2271             manifest_path,
2272             "-o",
2273             output_path,
2274             "-e",
2275             "_start",
2276             object_path,
2277         },
2278         &stderr,
2279         .{ .width = 88 },
2280     );
2281     try std.testing.expectEqual(@as(u8, 0), code);
2282 
2283     const manifest_bytes = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), manifest_path, allocator, .limited(4096));
2284     defer allocator.free(manifest_bytes);
2285     var parsed_manifest = try tldr.incremental.Manifest.fromBinary(allocator, manifest_bytes);
2286     defer parsed_manifest.deinit(allocator);
2287 
2288     try std.testing.expect(parsed_manifest.canReuseFor(
2289         .{ .entry_symbol = "_start", .incremental_mode = .relink },
2290         &.{.{ .name = object_path, .bytes = object }},
2291     ));
2292     try std.testing.expectEqual(@as(usize, 1), parsed_manifest.contributions.len);
2293 }
2294 
2295 test "CLI relinks build id output from prepared private manifest" {
2296     const allocator = std.testing.allocator;
2297     var tmp = std.testing.tmpDir(.{});
2298     defer tmp.cleanup();
2299 
2300     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
2301     defer allocator.free(root);
2302     const object_path = try std.fs.path.join(allocator, &.{ root, "start.o" });
2303     defer allocator.free(object_path);
2304     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
2305     defer allocator.free(output_path);
2306     const manifest_path = try std.fs.path.join(allocator, &.{ root, "linked.manifest" });
2307     defer allocator.free(manifest_path);
2308 
2309     const old_text = [_]u8{0xc3};
2310     const old_object = try elf_object.build(allocator, .{
2311         .sections = &.{
2312             elf_object.Section.progbits(".text", &old_text, std.elf.SHF_EXECINSTR, 16),
2313         },
2314         .symbols = &.{
2315             elf_object.Symbol.section(1),
2316             elf_object.Symbol.function("_start", 1, 0, old_text.len),
2317         },
2318     });
2319     defer allocator.free(old_object);
2320     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = object_path, .data = old_object });
2321 
2322     var stderr_buffer: [512]u8 = undefined;
2323     var stderr = std.Io.Writer.fixed(&stderr_buffer);
2324     const prepare_code = try run(
2325         allocator,
2326         &.{
2327             "--incremental=prepare",
2328             "--build-id",
2329             "--emit-manifest",
2330             manifest_path,
2331             "-o",
2332             output_path,
2333             "-e",
2334             "_start",
2335             object_path,
2336         },
2337         &stderr,
2338         .{ .width = 88 },
2339     );
2340     try std.testing.expectEqual(@as(u8, 0), prepare_code);
2341 
2342     const new_text = [_]u8{ 0x90, 0xc3 };
2343     const new_object = try elf_object.build(allocator, .{
2344         .sections = &.{
2345             elf_object.Section.progbits(".text", &new_text, std.elf.SHF_EXECINSTR, 16),
2346         },
2347         .symbols = &.{
2348             elf_object.Symbol.section(1),
2349             elf_object.Symbol.function("_start", 1, 0, new_text.len),
2350         },
2351     });
2352     defer allocator.free(new_object);
2353     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = object_path, .data = new_object });
2354 
2355     var candidate = try tldr.link(
2356         allocator,
2357         &.{.{ .name = object_path, .bytes = new_object }},
2358         .{ .entry_symbol = "_start", .incremental_mode = .relink, .build_id = .fast },
2359     );
2360     defer candidate.deinit(allocator);
2361 
2362     stderr = std.Io.Writer.fixed(&stderr_buffer);
2363     const relink_code = try run(
2364         allocator,
2365         &.{
2366             "--incremental=relink",
2367             "--build-id",
2368             "--incremental-manifest",
2369             manifest_path,
2370             "-o",
2371             output_path,
2372             "-e",
2373             "_start",
2374             object_path,
2375         },
2376         &stderr,
2377         .{ .width = 88 },
2378     );
2379     try std.testing.expectEqual(@as(u8, 0), relink_code);
2380 
2381     const relinked = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), output_path, allocator, .limited(4096));
2382     defer allocator.free(relinked);
2383     try std.testing.expectEqualSlices(u8, candidate.bytes, relinked);
2384 
2385     const manifest_bytes = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), manifest_path, allocator, .limited(4096));
2386     defer allocator.free(manifest_bytes);
2387     var parsed_manifest = try tldr.incremental.Manifest.fromBinary(allocator, manifest_bytes);
2388     defer parsed_manifest.deinit(allocator);
2389 
2390     try std.testing.expect(parsed_manifest.canReuseFor(
2391         .{ .entry_symbol = "_start", .incremental_mode = .relink, .build_id = .fast },
2392         &.{.{ .name = object_path, .bytes = new_object }},
2393     ));
2394 }
2395 
2396 test "CLI relinks from prepared private manifest" {
2397     const allocator = std.testing.allocator;
2398     var tmp = std.testing.tmpDir(.{});
2399     defer tmp.cleanup();
2400 
2401     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
2402     defer allocator.free(root);
2403     const object_path = try std.fs.path.join(allocator, &.{ root, "start.o" });
2404     defer allocator.free(object_path);
2405     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
2406     defer allocator.free(output_path);
2407     const manifest_path = try std.fs.path.join(allocator, &.{ root, "linked.manifest" });
2408     defer allocator.free(manifest_path);
2409 
2410     const old_text = [_]u8{0xc3};
2411     const old_object = try elf_object.build(allocator, .{
2412         .sections = &.{
2413             elf_object.Section.progbits(".text", &old_text, std.elf.SHF_EXECINSTR, 16),
2414         },
2415         .symbols = &.{
2416             elf_object.Symbol.section(1),
2417             elf_object.Symbol.function("_start", 1, 0, old_text.len),
2418         },
2419     });
2420     defer allocator.free(old_object);
2421 
2422     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = object_path, .data = old_object });
2423 
2424     var stderr_buffer: [512]u8 = undefined;
2425     var stderr = std.Io.Writer.fixed(&stderr_buffer);
2426     const prepare_code = try run(
2427         allocator,
2428         &.{
2429             "--incremental=prepare",
2430             "--emit-manifest",
2431             manifest_path,
2432             "-o",
2433             output_path,
2434             "-e",
2435             "_start",
2436             object_path,
2437         },
2438         &stderr,
2439         .{ .width = 88 },
2440     );
2441     try std.testing.expectEqual(@as(u8, 0), prepare_code);
2442 
2443     const new_text = [_]u8{ 0x90, 0xc3 };
2444     const new_object = try elf_object.build(allocator, .{
2445         .sections = &.{
2446             elf_object.Section.progbits(".text", &new_text, std.elf.SHF_EXECINSTR, 16),
2447         },
2448         .symbols = &.{
2449             elf_object.Symbol.section(1),
2450             elf_object.Symbol.function("_start", 1, 0, new_text.len),
2451         },
2452     });
2453     defer allocator.free(new_object);
2454     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = object_path, .data = new_object });
2455 
2456     stderr = std.Io.Writer.fixed(&stderr_buffer);
2457     const relink_code = try run(
2458         allocator,
2459         &.{
2460             "--incremental=relink",
2461             "--incremental-manifest",
2462             manifest_path,
2463             "-o",
2464             output_path,
2465             "-e",
2466             "_start",
2467             object_path,
2468         },
2469         &stderr,
2470         .{ .width = 88 },
2471     );
2472     try std.testing.expectEqual(@as(u8, 0), relink_code);
2473 
2474     const relinked = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), output_path, allocator, .limited(4096));
2475     defer allocator.free(relinked);
2476     try std.testing.expect(std.mem.indexOf(u8, relinked, &new_text) != null);
2477 
2478     const manifest_bytes = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), manifest_path, allocator, .limited(4096));
2479     defer allocator.free(manifest_bytes);
2480     var parsed_manifest = try tldr.incremental.Manifest.fromBinary(allocator, manifest_bytes);
2481     defer parsed_manifest.deinit(allocator);
2482 
2483     try std.testing.expect(parsed_manifest.canReuseFor(
2484         .{ .entry_symbol = "_start", .incremental_mode = .relink },
2485         &.{.{ .name = object_path, .bytes = new_object }},
2486     ));
2487     try std.testing.expectEqual(@as(u64, new_text.len), parsed_manifest.contributions[0].size);
2488 }
2489 
2490 test "CLI relink full-links over a stale manifest protocol" {
2491     const allocator = std.testing.allocator;
2492     var tmp = std.testing.tmpDir(.{});
2493     defer tmp.cleanup();
2494 
2495     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
2496     defer allocator.free(root);
2497     const start_path = try std.fs.path.join(allocator, &.{ root, "start.o" });
2498     defer allocator.free(start_path);
2499     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
2500     defer allocator.free(output_path);
2501     const manifest_path = try std.fs.path.join(allocator, &.{ root, "linked.manifest" });
2502     defer allocator.free(manifest_path);
2503 
2504     const start_text = [_]u8{0xc3};
2505     const start_object = try elf_object.build(allocator, .{
2506         .sections = &.{
2507             elf_object.Section.progbits(".text.start", &start_text, std.elf.SHF_EXECINSTR, 16),
2508         },
2509         .symbols = &.{
2510             elf_object.Symbol.section(1),
2511             elf_object.Symbol.function("_start", 1, 0, start_text.len),
2512         },
2513     });
2514     defer allocator.free(start_object);
2515     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = start_path, .data = start_object });
2516 
2517     var stderr_buffer: [512]u8 = undefined;
2518     var stderr = std.Io.Writer.fixed(&stderr_buffer);
2519     const prepare_code = try run(
2520         allocator,
2521         &.{ "--incremental=prepare", "--emit-manifest", manifest_path, "-o", output_path, "-e", "_start", start_path },
2522         &stderr,
2523         .{ .width = 88 },
2524     );
2525     try std.testing.expectEqual(@as(u8, 0), prepare_code);
2526 
2527     const manifest_bytes = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), manifest_path, allocator, .limited(1 << 20));
2528     defer allocator.free(manifest_bytes);
2529     const protocol_index = std.mem.indexOf(u8, manifest_bytes, tldr.incremental.manifest_binary_protocol) orelse
2530         return error.MissingManifestProtocol;
2531     const stale_bytes = try allocator.dupe(u8, manifest_bytes);
2532     defer allocator.free(stale_bytes);
2533     stale_bytes[protocol_index + tldr.incremental.manifest_binary_protocol.len - 1] = '0';
2534     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = manifest_path, .data = stale_bytes, .flags = .{ .truncate = true } });
2535 
2536     stderr = std.Io.Writer.fixed(&stderr_buffer);
2537     const relink_code = try run(
2538         allocator,
2539         &.{ "--incremental=relink", "--incremental-manifest", manifest_path, "-o", output_path, "-e", "_start", start_path },
2540         &stderr,
2541         .{ .width = 88 },
2542     );
2543     try std.testing.expectEqual(@as(u8, 0), relink_code);
2544 
2545     const healed_bytes = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), manifest_path, allocator, .limited(1 << 20));
2546     defer allocator.free(healed_bytes);
2547     var healed = try tldr.incremental.Manifest.fromBinary(allocator, healed_bytes);
2548     defer healed.deinit(allocator);
2549     try std.testing.expect(healed.canReuseFor(
2550         .{ .entry_symbol = "_start", .incremental_mode = .relink },
2551         &.{.{ .name = start_path, .bytes = start_object }},
2552     ));
2553 }
2554 
2555 test "CLI relink full-links over duplicate manifest contributions" {
2556     const allocator = std.testing.allocator;
2557     var tmp = std.testing.tmpDir(.{});
2558     defer tmp.cleanup();
2559 
2560     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
2561     defer allocator.free(root);
2562     const start_path = try std.fs.path.join(allocator, &.{ root, "start.o" });
2563     defer allocator.free(start_path);
2564     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
2565     defer allocator.free(output_path);
2566     const manifest_path = try std.fs.path.join(allocator, &.{ root, "linked.manifest" });
2567     defer allocator.free(manifest_path);
2568 
2569     const start_text = [_]u8{ 0xc3, 0x90, 0x90 };
2570     const start_object = try elf_object.build(allocator, .{
2571         .sections = &.{
2572             elf_object.Section.progbits(".text.start", &start_text, std.elf.SHF_EXECINSTR, 16),
2573         },
2574         .symbols = &.{
2575             elf_object.Symbol.section(1),
2576             elf_object.Symbol.function("_start", 1, 0, start_text.len),
2577         },
2578     });
2579     defer allocator.free(start_object);
2580     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = start_path, .data = start_object });
2581 
2582     var stderr_buffer: [512]u8 = undefined;
2583     var stderr = std.Io.Writer.fixed(&stderr_buffer);
2584     const prepare_code = try run(
2585         allocator,
2586         &.{ "--incremental=prepare", "--emit-manifest", manifest_path, "-o", output_path, "-e", "_start", start_path },
2587         &stderr,
2588         .{ .width = 88 },
2589     );
2590     try std.testing.expectEqual(@as(u8, 0), prepare_code);
2591 
2592     const manifest_bytes = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), manifest_path, allocator, .limited(1 << 20));
2593     defer allocator.free(manifest_bytes);
2594     var duplicated = try tldr.incremental.Manifest.fromBinary(allocator, manifest_bytes);
2595     defer duplicated.deinit(allocator);
2596     try std.testing.expect(duplicated.contributions.len != 0);
2597     const previous_contributions = duplicated.contributions;
2598     const contributions = try allocator.alloc(tldr.incremental.ContributionRecord, previous_contributions.len + 1);
2599     @memcpy(contributions[0..previous_contributions.len], previous_contributions);
2600     contributions[previous_contributions.len] = previous_contributions[0];
2601     duplicated.contributions = contributions;
2602     duplicated.owned.contributions = true;
2603     const duplicated_bytes = try duplicated.formatBinaryAlloc(allocator);
2604     defer allocator.free(duplicated_bytes);
2605     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = manifest_path, .data = duplicated_bytes, .flags = .{ .truncate = true } });
2606 
2607     const changed_text = [_]u8{ 0xc3, 0x90, 0xcc };
2608     const changed_object = try elf_object.build(allocator, .{
2609         .sections = &.{
2610             elf_object.Section.progbits(".text.start", &changed_text, std.elf.SHF_EXECINSTR, 16),
2611         },
2612         .symbols = &.{
2613             elf_object.Symbol.section(1),
2614             elf_object.Symbol.function("_start", 1, 0, changed_text.len),
2615         },
2616     });
2617     defer allocator.free(changed_object);
2618     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = start_path, .data = changed_object, .flags = .{ .truncate = true } });
2619 
2620     stderr = std.Io.Writer.fixed(&stderr_buffer);
2621     const relink_code = try run(
2622         allocator,
2623         &.{ "--incremental=relink", "--incremental-manifest", manifest_path, "-o", output_path, "-e", "_start", start_path },
2624         &stderr,
2625         .{ .width = 88 },
2626     );
2627     try std.testing.expectEqual(@as(u8, 0), relink_code);
2628 
2629     const healed_bytes = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), manifest_path, allocator, .limited(1 << 20));
2630     defer allocator.free(healed_bytes);
2631     var healed = try tldr.incremental.Manifest.fromBinary(allocator, healed_bytes);
2632     defer healed.deinit(allocator);
2633     var healed_state = try tldr.incremental.PreparedState.fromOwnedManifest(allocator, healed.take());
2634     defer healed_state.deinit(allocator);
2635     try healed_state.ensureReplacementIndex(allocator, &.{.{
2636         .input_name = start_path,
2637         .input_index = 0,
2638         .kind = .section,
2639         .name = ".text.start",
2640         .ordinal = 0,
2641         .size = changed_text.len,
2642         .alignment = 16,
2643     }});
2644     try std.testing.expect(healed_state.canReuseFor(
2645         .{ .entry_symbol = "_start", .incremental_mode = .relink },
2646         &.{.{ .name = start_path, .bytes = changed_object }},
2647     ));
2648 }
2649 
2650 test "CLI relinks same-shape direct object payloads" {
2651     const allocator = std.testing.allocator;
2652     var tmp = std.testing.tmpDir(.{});
2653     defer tmp.cleanup();
2654 
2655     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
2656     defer allocator.free(root);
2657     const start_path = try std.fs.path.join(allocator, &.{ root, "start.o" });
2658     defer allocator.free(start_path);
2659     const patch_path = try std.fs.path.join(allocator, &.{ root, "patch.o" });
2660     defer allocator.free(patch_path);
2661     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
2662     defer allocator.free(output_path);
2663     const manifest_path = try std.fs.path.join(allocator, &.{ root, "linked.manifest" });
2664     defer allocator.free(manifest_path);
2665 
2666     const start_text = [_]u8{0xc3};
2667     const start_object = try elf_object.build(allocator, .{
2668         .sections = &.{
2669             elf_object.Section.progbits(".text.start", &start_text, std.elf.SHF_EXECINSTR, 16),
2670         },
2671         .symbols = &.{
2672             elf_object.Symbol.section(1),
2673             elf_object.Symbol.function("_start", 1, 0, start_text.len),
2674         },
2675     });
2676     defer allocator.free(start_object);
2677     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = start_path, .data = start_object });
2678 
2679     const old_patch = "old!";
2680     const old_patch_object = try elf_object.build(allocator, .{
2681         .sections = &.{elf_object.Section.progbits(".rodata.patch", old_patch, 0, 4)},
2682         .symbols = &.{elf_object.Symbol.section(1)},
2683     });
2684     defer allocator.free(old_patch_object);
2685     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = patch_path, .data = old_patch_object });
2686 
2687     var stderr_buffer: [512]u8 = undefined;
2688     var stderr = std.Io.Writer.fixed(&stderr_buffer);
2689     const prepare_code = try run(
2690         allocator,
2691         &.{
2692             "--incremental=prepare",
2693             "--emit-manifest",
2694             manifest_path,
2695             "-o",
2696             output_path,
2697             "-e",
2698             "_start",
2699             start_path,
2700             patch_path,
2701         },
2702         &stderr,
2703         .{ .width = 88 },
2704     );
2705     try std.testing.expectEqual(@as(u8, 0), prepare_code);
2706 
2707     const new_patch = "new!";
2708     const new_patch_object = try elf_object.build(allocator, .{
2709         .sections = &.{elf_object.Section.progbits(".rodata.patch", new_patch, 0, 4)},
2710         .symbols = &.{elf_object.Symbol.section(1)},
2711     });
2712     defer allocator.free(new_patch_object);
2713     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = patch_path, .data = new_patch_object });
2714 
2715     stderr = std.Io.Writer.fixed(&stderr_buffer);
2716     const relink_code = try run(
2717         allocator,
2718         &.{
2719             "--incremental=relink",
2720             "--incremental-manifest",
2721             manifest_path,
2722             "-o",
2723             output_path,
2724             "-e",
2725             "_start",
2726             start_path,
2727             patch_path,
2728         },
2729         &stderr,
2730         .{ .width = 88 },
2731     );
2732     try std.testing.expectEqual(@as(u8, 0), relink_code);
2733 
2734     const relinked = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), output_path, allocator, .limited(4096));
2735     defer allocator.free(relinked);
2736     try std.testing.expect(std.mem.indexOf(u8, relinked, new_patch) != null);
2737     try std.testing.expect(std.mem.indexOf(u8, relinked, old_patch) == null);
2738 
2739     const manifest_bytes = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), manifest_path, allocator, .limited(4096));
2740     defer allocator.free(manifest_bytes);
2741     var parsed_manifest = try tldr.incremental.Manifest.fromBinary(allocator, manifest_bytes);
2742     defer parsed_manifest.deinit(allocator);
2743 
2744     try std.testing.expect(parsed_manifest.canReuseFor(
2745         .{ .entry_symbol = "_start", .incremental_mode = .relink },
2746         &.{
2747             .{ .name = start_path, .bytes = start_object },
2748             .{ .name = patch_path, .bytes = new_patch_object },
2749         },
2750     ));
2751     try std.testing.expectEqual(tldr.incremental.hashBytes(new_patch_object), parsed_manifest.inputs[1].hash);
2752 }
2753 
2754 test "CLI relinks same-shape archive member payloads" {
2755     const allocator = std.testing.allocator;
2756     var tmp = std.testing.tmpDir(.{});
2757     defer tmp.cleanup();
2758 
2759     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
2760     defer allocator.free(root);
2761     const start_path = try std.fs.path.join(allocator, &.{ root, "start.o" });
2762     defer allocator.free(start_path);
2763     const archive_path = try std.fs.path.join(allocator, &.{ root, "libpatch.a" });
2764     defer allocator.free(archive_path);
2765     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
2766     defer allocator.free(output_path);
2767     const manifest_path = try std.fs.path.join(allocator, &.{ root, "linked.manifest" });
2768     defer allocator.free(manifest_path);
2769 
2770     const start_text = [_]u8{0xc3};
2771     const ptr_bytes = @as([8]u8, @splat(0));
2772     const start_object = try elf_object.build(allocator, .{
2773         .sections = &.{
2774             elf_object.Section.progbits(".text.start", &start_text, std.elf.SHF_EXECINSTR, 16),
2775             elf_object.Section.progbits(".data.ptr", &ptr_bytes, std.elf.SHF_WRITE, 8),
2776         },
2777         .symbols = &.{
2778             elf_object.Symbol.section(1),
2779             elf_object.Symbol.section(2),
2780             elf_object.Symbol.function("_start", 1, 0, start_text.len),
2781             elf_object.Symbol.undefinedObject("patch_data"),
2782         },
2783         .relocations = &.{elf_object.Relocation.x86_64(2, 0, 4, .@"64", 0)},
2784     });
2785     defer allocator.free(start_object);
2786     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = start_path, .data = start_object });
2787 
2788     const old_patch = "old!";
2789     const old_patch_object = try elf_object.build(allocator, .{
2790         .sections = &.{elf_object.Section.progbits(".rodata.patch", old_patch, 0, 4)},
2791         .symbols = &.{
2792             elf_object.Symbol.section(1),
2793             elf_object.Symbol.object("patch_data", 1, 0, old_patch.len),
2794         },
2795     });
2796     defer allocator.free(old_patch_object);
2797     const old_archive = try tldr.archive.build(allocator, &.{.{ .name = "patch.o", .bytes = old_patch_object, .symbols = &.{"patch_data"} }});
2798     defer allocator.free(old_archive);
2799     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = archive_path, .data = old_archive });
2800 
2801     var stderr_buffer: [512]u8 = undefined;
2802     var stderr = std.Io.Writer.fixed(&stderr_buffer);
2803     const prepare_code = try run(
2804         allocator,
2805         &.{
2806             "--incremental=prepare",
2807             "--emit-manifest",
2808             manifest_path,
2809             "-o",
2810             output_path,
2811             "-e",
2812             "_start",
2813             start_path,
2814             archive_path,
2815         },
2816         &stderr,
2817         .{ .width = 88 },
2818     );
2819     try std.testing.expectEqual(@as(u8, 0), prepare_code);
2820 
2821     const new_patch = "new!";
2822     const new_patch_object = try elf_object.build(allocator, .{
2823         .sections = &.{elf_object.Section.progbits(".rodata.patch", new_patch, 0, 4)},
2824         .symbols = &.{
2825             elf_object.Symbol.section(1),
2826             elf_object.Symbol.object("patch_data", 1, 0, new_patch.len),
2827         },
2828     });
2829     defer allocator.free(new_patch_object);
2830     const new_archive = try tldr.archive.build(allocator, &.{.{ .name = "patch.o", .bytes = new_patch_object, .symbols = &.{"patch_data"} }});
2831     defer allocator.free(new_archive);
2832     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = archive_path, .data = new_archive });
2833 
2834     stderr = std.Io.Writer.fixed(&stderr_buffer);
2835     const relink_code = try run(
2836         allocator,
2837         &.{
2838             "--incremental=relink",
2839             "--incremental-manifest",
2840             manifest_path,
2841             "-o",
2842             output_path,
2843             "-e",
2844             "_start",
2845             start_path,
2846             archive_path,
2847         },
2848         &stderr,
2849         .{ .width = 88 },
2850     );
2851     try std.testing.expectEqual(@as(u8, 0), relink_code);
2852 
2853     const relinked = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), output_path, allocator, .limited(4096));
2854     defer allocator.free(relinked);
2855     try std.testing.expect(std.mem.indexOf(u8, relinked, new_patch) != null);
2856     try std.testing.expect(std.mem.indexOf(u8, relinked, old_patch) == null);
2857 
2858     const manifest_bytes = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), manifest_path, allocator, .limited(4096));
2859     defer allocator.free(manifest_bytes);
2860     var parsed_manifest = try tldr.incremental.Manifest.fromBinary(allocator, manifest_bytes);
2861     defer parsed_manifest.deinit(allocator);
2862 
2863     try std.testing.expect(parsed_manifest.canReuseFor(
2864         .{ .entry_symbol = "_start", .incremental_mode = .relink },
2865         &.{
2866             .{ .name = start_path, .bytes = start_object },
2867             .{ .name = archive_path, .bytes = new_archive },
2868         },
2869     ));
2870     try std.testing.expectEqual(tldr.incremental.hashBytes(new_archive), parsed_manifest.inputs[1].hash);
2871     try std.testing.expectEqual(@as(usize, 1), parsed_manifest.archive_members.len);
2872     try std.testing.expectEqual(tldr.incremental.hashBytes(new_patch_object), parsed_manifest.archive_members[0].hash);
2873 
2874     const third_patch = "two!";
2875     const third_patch_object = try elf_object.build(allocator, .{
2876         .sections = &.{elf_object.Section.progbits(".rodata.patch", third_patch, 0, 4)},
2877         .symbols = &.{
2878             elf_object.Symbol.section(1),
2879             elf_object.Symbol.object("patch_data", 1, 0, third_patch.len),
2880         },
2881     });
2882     defer allocator.free(third_patch_object);
2883     const third_archive = try tldr.archive.build(allocator, &.{.{ .name = "patch.o", .bytes = third_patch_object, .symbols = &.{"patch_data"} }});
2884     defer allocator.free(third_archive);
2885     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = archive_path, .data = third_archive });
2886 
2887     stderr = std.Io.Writer.fixed(&stderr_buffer);
2888     const second_relink_code = try run(
2889         allocator,
2890         &.{
2891             "--incremental=relink",
2892             "--incremental-manifest",
2893             manifest_path,
2894             "-o",
2895             output_path,
2896             "-e",
2897             "_start",
2898             start_path,
2899             archive_path,
2900         },
2901         &stderr,
2902         .{ .width = 88 },
2903     );
2904     try std.testing.expectEqual(@as(u8, 0), second_relink_code);
2905 
2906     const second_relinked = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), output_path, allocator, .limited(4096));
2907     defer allocator.free(second_relinked);
2908     try std.testing.expect(std.mem.indexOf(u8, second_relinked, third_patch) != null);
2909     try std.testing.expect(std.mem.indexOf(u8, second_relinked, new_patch) == null);
2910 
2911     const second_manifest_bytes = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), manifest_path, allocator, .limited(4096));
2912     defer allocator.free(second_manifest_bytes);
2913     var second_manifest = try tldr.incremental.Manifest.fromBinary(allocator, second_manifest_bytes);
2914     defer second_manifest.deinit(allocator);
2915 
2916     try std.testing.expect(second_manifest.canReuseFor(
2917         .{ .entry_symbol = "_start", .incremental_mode = .relink },
2918         &.{
2919             .{ .name = start_path, .bytes = start_object },
2920             .{ .name = archive_path, .bytes = third_archive },
2921         },
2922     ));
2923     try std.testing.expectEqual(tldr.incremental.hashBytes(third_archive), second_manifest.inputs[1].hash);
2924     try std.testing.expectEqual(tldr.incremental.hashBytes(third_patch_object), second_manifest.archive_members[0].hash);
2925 }
2926 
2927 test "CLI relink full-links when changed input gains contribution" {
2928     const allocator = std.testing.allocator;
2929     var tmp = std.testing.tmpDir(.{});
2930     defer tmp.cleanup();
2931 
2932     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
2933     defer allocator.free(root);
2934     const start_path = try std.fs.path.join(allocator, &.{ root, "start.o" });
2935     defer allocator.free(start_path);
2936     const feature_path = try std.fs.path.join(allocator, &.{ root, "feature.o" });
2937     defer allocator.free(feature_path);
2938     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
2939     defer allocator.free(output_path);
2940     const manifest_path = try std.fs.path.join(allocator, &.{ root, "linked.manifest" });
2941     defer allocator.free(manifest_path);
2942 
2943     const start_text = [_]u8{0xc3};
2944     const start_object = try elf_object.build(allocator, .{
2945         .sections = &.{
2946             elf_object.Section.progbits(".text", &start_text, std.elf.SHF_EXECINSTR, 16),
2947         },
2948         .symbols = &.{
2949             elf_object.Symbol.section(1),
2950             elf_object.Symbol.function("_start", 1, 0, start_text.len),
2951         },
2952     });
2953     defer allocator.free(start_object);
2954     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = start_path, .data = start_object });
2955 
2956     const old_feature_object = try elf_object.build(allocator, .{ .sections = &.{} });
2957     defer allocator.free(old_feature_object);
2958     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = feature_path, .data = old_feature_object });
2959 
2960     var stderr_buffer: [512]u8 = undefined;
2961     var stderr = std.Io.Writer.fixed(&stderr_buffer);
2962     const prepare_code = try run(
2963         allocator,
2964         &.{
2965             "--incremental=prepare",
2966             "--emit-manifest",
2967             manifest_path,
2968             "-o",
2969             output_path,
2970             "-e",
2971             "_start",
2972             start_path,
2973             feature_path,
2974         },
2975         &stderr,
2976         .{ .width = 88 },
2977     );
2978     try std.testing.expectEqual(@as(u8, 0), prepare_code);
2979 
2980     const feature_text = [_]u8{0x90};
2981     const new_feature_object = try elf_object.build(allocator, .{
2982         .sections = &.{
2983             elf_object.Section.progbits(".text.feature", &feature_text, std.elf.SHF_EXECINSTR, 16),
2984         },
2985         .symbols = &.{
2986             elf_object.Symbol.section(1),
2987             elf_object.Symbol.function("feature", 1, 0, feature_text.len),
2988         },
2989     });
2990     defer allocator.free(new_feature_object);
2991     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = feature_path, .data = new_feature_object });
2992 
2993     var candidate = try tldr.link(
2994         allocator,
2995         &.{
2996             .{ .name = start_path, .bytes = start_object },
2997             .{ .name = feature_path, .bytes = new_feature_object },
2998         },
2999         .{ .entry_symbol = "_start", .incremental_mode = .relink },
3000     );
3001     defer candidate.deinit(allocator);
3002 
3003     stderr = std.Io.Writer.fixed(&stderr_buffer);
3004     const relink_code = try run(
3005         allocator,
3006         &.{
3007             "--incremental=relink",
3008             "--incremental-manifest",
3009             manifest_path,
3010             "-o",
3011             output_path,
3012             "-e",
3013             "_start",
3014             start_path,
3015             feature_path,
3016         },
3017         &stderr,
3018         .{ .width = 88 },
3019     );
3020     try std.testing.expectEqual(@as(u8, 0), relink_code);
3021 
3022     const relinked = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), output_path, allocator, .limited(4096));
3023     defer allocator.free(relinked);
3024     try std.testing.expectEqualSlices(u8, candidate.bytes, relinked);
3025 
3026     const manifest_bytes = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), manifest_path, allocator, .limited(4096));
3027     defer allocator.free(manifest_bytes);
3028     var parsed_manifest = try tldr.incremental.Manifest.fromBinary(allocator, manifest_bytes);
3029     defer parsed_manifest.deinit(allocator);
3030 
3031     try std.testing.expect(parsed_manifest.canReuseFor(
3032         .{ .entry_symbol = "_start", .incremental_mode = .relink },
3033         &.{
3034             .{ .name = start_path, .bytes = start_object },
3035             .{ .name = feature_path, .bytes = new_feature_object },
3036         },
3037     ));
3038     try std.testing.expectEqual(@as(usize, 2), parsed_manifest.contributions.len);
3039 }
3040 
3041 test "CLI relink reuses unchanged output without rewriting" {
3042     const allocator = std.testing.allocator;
3043     var tmp = std.testing.tmpDir(.{});
3044     defer tmp.cleanup();
3045 
3046     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
3047     defer allocator.free(root);
3048     const object_path = try std.fs.path.join(allocator, &.{ root, "start.o" });
3049     defer allocator.free(object_path);
3050     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
3051     defer allocator.free(output_path);
3052     const manifest_path = try std.fs.path.join(allocator, &.{ root, "linked.manifest" });
3053     defer allocator.free(manifest_path);
3054     const link_map_path = try std.fs.path.join(allocator, &.{ root, "linked.map" });
3055     defer allocator.free(link_map_path);
3056 
3057     const text = [_]u8{0xc3};
3058     const object = try elf_object.build(allocator, .{
3059         .sections = &.{elf_object.Section.progbits(".text", &text, std.elf.SHF_EXECINSTR, 16)},
3060         .symbols = &.{
3061             elf_object.Symbol.section(1),
3062             elf_object.Symbol.function("_start", 1, 0, text.len),
3063         },
3064     });
3065     defer allocator.free(object);
3066 
3067     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = object_path, .data = object });
3068 
3069     var stderr_buffer: [512]u8 = undefined;
3070     var stderr = std.Io.Writer.fixed(&stderr_buffer);
3071     const prepare_code = try run(
3072         allocator,
3073         &.{
3074             "--incremental=prepare",
3075             "--emit-manifest",
3076             manifest_path,
3077             "-o",
3078             output_path,
3079             "-e",
3080             "_start",
3081             object_path,
3082         },
3083         &stderr,
3084         .{ .width = 88 },
3085     );
3086     try std.testing.expectEqual(@as(u8, 0), prepare_code);
3087 
3088     const sentinel = "cached-output-sentinel";
3089     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = output_path, .data = sentinel });
3090 
3091     stderr = std.Io.Writer.fixed(&stderr_buffer);
3092     const relink_code = try run(
3093         allocator,
3094         &.{
3095             "--incremental=relink",
3096             "--incremental-manifest",
3097             manifest_path,
3098             "--emit-link-map",
3099             link_map_path,
3100             "-o",
3101             output_path,
3102             "-e",
3103             "_start",
3104             object_path,
3105         },
3106         &stderr,
3107         .{ .width = 88 },
3108     );
3109     try std.testing.expectEqual(@as(u8, 0), relink_code);
3110 
3111     const relinked = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), output_path, allocator, .limited(4096));
3112     defer allocator.free(relinked);
3113     try std.testing.expectEqualSlices(u8, sentinel, relinked);
3114 
3115     const link_map = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), link_map_path, allocator, .limited(4096));
3116     defer allocator.free(link_map);
3117     try std.testing.expect(std.mem.indexOf(u8, link_map, "summary: inputs=1 sections=1") != null);
3118 
3119     const manifest_bytes = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), manifest_path, allocator, .limited(4096));
3120     defer allocator.free(manifest_bytes);
3121     var parsed_manifest = try tldr.incremental.Manifest.fromBinary(allocator, manifest_bytes);
3122     defer parsed_manifest.deinit(allocator);
3123 
3124     try std.testing.expect(parsed_manifest.canReuseFor(
3125         .{ .entry_symbol = "_start", .incremental_mode = .relink },
3126         &.{.{ .name = object_path, .bytes = object }},
3127     ));
3128 }
3129 
3130 test "CLI relink full-links reordered inputs" {
3131     const allocator = std.testing.allocator;
3132     var tmp = std.testing.tmpDir(.{});
3133     defer tmp.cleanup();
3134 
3135     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
3136     defer allocator.free(root);
3137     const start_path = try std.fs.path.join(allocator, &.{ root, "start.o" });
3138     defer allocator.free(start_path);
3139     const helper_path = try std.fs.path.join(allocator, &.{ root, "helper.o" });
3140     defer allocator.free(helper_path);
3141     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
3142     defer allocator.free(output_path);
3143     const manifest_path = try std.fs.path.join(allocator, &.{ root, "linked.manifest" });
3144     defer allocator.free(manifest_path);
3145 
3146     const start_text = [_]u8{0xc3};
3147     const start_object = try elf_object.build(allocator, .{
3148         .sections = &.{
3149             elf_object.Section.progbits(".text.start", &start_text, std.elf.SHF_EXECINSTR, 16),
3150         },
3151         .symbols = &.{
3152             elf_object.Symbol.section(1),
3153             elf_object.Symbol.function("_start", 1, 0, start_text.len),
3154         },
3155     });
3156     defer allocator.free(start_object);
3157     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = start_path, .data = start_object });
3158 
3159     const helper_text = [_]u8{ 0x90, 0xc3 };
3160     const helper_object = try elf_object.build(allocator, .{
3161         .sections = &.{
3162             elf_object.Section.progbits(".text.helper", &helper_text, std.elf.SHF_EXECINSTR, 16),
3163         },
3164         .symbols = &.{
3165             elf_object.Symbol.section(1),
3166             elf_object.Symbol.function("helper", 1, 0, helper_text.len),
3167         },
3168     });
3169     defer allocator.free(helper_object);
3170     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = helper_path, .data = helper_object });
3171 
3172     var stderr_buffer: [512]u8 = undefined;
3173     var stderr = std.Io.Writer.fixed(&stderr_buffer);
3174     const prepare_code = try run(
3175         allocator,
3176         &.{
3177             "--incremental=prepare",
3178             "--emit-manifest",
3179             manifest_path,
3180             "-o",
3181             output_path,
3182             "-e",
3183             "_start",
3184             start_path,
3185             helper_path,
3186         },
3187         &stderr,
3188         .{ .width = 88 },
3189     );
3190     try std.testing.expectEqual(@as(u8, 0), prepare_code);
3191 
3192     const sentinel = "stale-reordered-output";
3193     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = output_path, .data = sentinel });
3194 
3195     var candidate = try tldr.link(
3196         allocator,
3197         &.{
3198             .{ .name = helper_path, .bytes = helper_object },
3199             .{ .name = start_path, .bytes = start_object },
3200         },
3201         .{ .entry_symbol = "_start", .incremental_mode = .relink },
3202     );
3203     defer candidate.deinit(allocator);
3204 
3205     stderr = std.Io.Writer.fixed(&stderr_buffer);
3206     const relink_code = try run(
3207         allocator,
3208         &.{
3209             "--incremental=relink",
3210             "--incremental-manifest",
3211             manifest_path,
3212             "-o",
3213             output_path,
3214             "-e",
3215             "_start",
3216             helper_path,
3217             start_path,
3218         },
3219         &stderr,
3220         .{ .width = 88 },
3221     );
3222     try std.testing.expectEqual(@as(u8, 0), relink_code);
3223 
3224     const relinked = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), output_path, allocator, .limited(4096));
3225     defer allocator.free(relinked);
3226     try std.testing.expectEqualSlices(u8, candidate.bytes, relinked);
3227     try std.testing.expect(std.mem.indexOf(u8, relinked, sentinel) == null);
3228 
3229     const manifest_bytes = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), manifest_path, allocator, .limited(4096));
3230     defer allocator.free(manifest_bytes);
3231     var parsed_manifest = try tldr.incremental.Manifest.fromBinary(allocator, manifest_bytes);
3232     defer parsed_manifest.deinit(allocator);
3233 
3234     try std.testing.expect(parsed_manifest.canReuseFor(
3235         .{ .entry_symbol = "_start", .incremental_mode = .relink },
3236         &.{
3237             .{ .name = helper_path, .bytes = helper_object },
3238             .{ .name = start_path, .bytes = start_object },
3239         },
3240     ));
3241 }
3242 
3243 test "CLI relink full-link fallback does not require previous output" {
3244     const allocator = std.testing.allocator;
3245     var tmp = std.testing.tmpDir(.{});
3246     defer tmp.cleanup();
3247 
3248     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
3249     defer allocator.free(root);
3250     const object_path = try std.fs.path.join(allocator, &.{ root, "start.o" });
3251     defer allocator.free(object_path);
3252     const extra_path = try std.fs.path.join(allocator, &.{ root, "extra.o" });
3253     defer allocator.free(extra_path);
3254     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
3255     defer allocator.free(output_path);
3256     const manifest_path = try std.fs.path.join(allocator, &.{ root, "linked.manifest" });
3257     defer allocator.free(manifest_path);
3258 
3259     const start_text = [_]u8{0xc3};
3260     const start_object = try elf_object.build(allocator, .{
3261         .sections = &.{
3262             elf_object.Section.progbits(".text", &start_text, std.elf.SHF_EXECINSTR, 16),
3263         },
3264         .symbols = &.{
3265             elf_object.Symbol.section(1),
3266             elf_object.Symbol.function("_start", 1, 0, start_text.len),
3267         },
3268     });
3269     defer allocator.free(start_object);
3270     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = object_path, .data = start_object });
3271 
3272     var stderr_buffer: [512]u8 = undefined;
3273     var stderr = std.Io.Writer.fixed(&stderr_buffer);
3274     const prepare_code = try run(
3275         allocator,
3276         &.{
3277             "--incremental=prepare",
3278             "--emit-manifest",
3279             manifest_path,
3280             "-o",
3281             output_path,
3282             "-e",
3283             "_start",
3284             object_path,
3285         },
3286         &stderr,
3287         .{ .width = 88 },
3288     );
3289     try std.testing.expectEqual(@as(u8, 0), prepare_code);
3290     try sys.fs.cwd().deleteFile(sys.fs.debugIo(), output_path);
3291 
3292     const extra_text = [_]u8{ 0x90, 0xc3 };
3293     const extra_object = try elf_object.build(allocator, .{
3294         .sections = &.{
3295             elf_object.Section.progbits(".text.extra", &extra_text, std.elf.SHF_EXECINSTR, 16),
3296         },
3297         .symbols = &.{
3298             elf_object.Symbol.section(1),
3299             elf_object.Symbol.function("extra", 1, 0, extra_text.len),
3300         },
3301     });
3302     defer allocator.free(extra_object);
3303     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = extra_path, .data = extra_object });
3304 
3305     var candidate = try tldr.link(
3306         allocator,
3307         &.{
3308             .{ .name = object_path, .bytes = start_object },
3309             .{ .name = extra_path, .bytes = extra_object },
3310         },
3311         .{ .entry_symbol = "_start", .incremental_mode = .relink },
3312     );
3313     defer candidate.deinit(allocator);
3314 
3315     stderr = std.Io.Writer.fixed(&stderr_buffer);
3316     const relink_code = try run(
3317         allocator,
3318         &.{
3319             "--incremental=relink",
3320             "--incremental-manifest",
3321             manifest_path,
3322             "-o",
3323             output_path,
3324             "-e",
3325             "_start",
3326             object_path,
3327             extra_path,
3328         },
3329         &stderr,
3330         .{ .width = 88 },
3331     );
3332     try std.testing.expectEqual(@as(u8, 0), relink_code);
3333 
3334     const relinked = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), output_path, allocator, .limited(4096));
3335     defer allocator.free(relinked);
3336     try std.testing.expectEqualSlices(u8, candidate.bytes, relinked);
3337 
3338     const manifest_bytes = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), manifest_path, allocator, .limited(4096));
3339     defer allocator.free(manifest_bytes);
3340     var parsed_manifest = try tldr.incremental.Manifest.fromBinary(allocator, manifest_bytes);
3341     defer parsed_manifest.deinit(allocator);
3342 
3343     try std.testing.expect(parsed_manifest.canReuseFor(
3344         .{ .entry_symbol = "_start", .incremental_mode = .relink },
3345         &.{
3346             .{ .name = object_path, .bytes = start_object },
3347             .{ .name = extra_path, .bytes = extra_object },
3348         },
3349     ));
3350 }
3351 
3352 test "CLI relink falls back when candidate metadata layout changes" {
3353     const allocator = std.testing.allocator;
3354     var tmp = std.testing.tmpDir(.{});
3355     defer tmp.cleanup();
3356 
3357     const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
3358     defer allocator.free(root);
3359     const object_path = try std.fs.path.join(allocator, &.{ root, "start.o" });
3360     defer allocator.free(object_path);
3361     const output_path = try std.fs.path.join(allocator, &.{ root, "linked" });
3362     defer allocator.free(output_path);
3363     const manifest_path = try std.fs.path.join(allocator, &.{ root, "linked.manifest" });
3364     defer allocator.free(manifest_path);
3365 
3366     const text = [_]u8{0xc3};
3367     const old_object = try elf_object.build(allocator, .{
3368         .sections = &.{elf_object.Section.progbits(".text", &text, std.elf.SHF_EXECINSTR, 16)},
3369         .symbols = &.{
3370             elf_object.Symbol.section(1),
3371             elf_object.Symbol.function("_start", 1, 0, text.len),
3372         },
3373     });
3374     defer allocator.free(old_object);
3375 
3376     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = object_path, .data = old_object });
3377 
3378     var stderr_buffer: [512]u8 = undefined;
3379     var stderr = std.Io.Writer.fixed(&stderr_buffer);
3380     const prepare_code = try run(
3381         allocator,
3382         &.{
3383             "--incremental=prepare",
3384             "--emit-manifest",
3385             manifest_path,
3386             "-o",
3387             output_path,
3388             "-e",
3389             "_start",
3390             object_path,
3391         },
3392         &stderr,
3393         .{ .width = 88 },
3394     );
3395     try std.testing.expectEqual(@as(u8, 0), prepare_code);
3396 
3397     const new_object = try elf_object.build(allocator, .{
3398         .sections = &.{elf_object.Section.progbits(".text", &text, std.elf.SHF_EXECINSTR, 16)},
3399         .symbols = &.{
3400             elf_object.Symbol.section(1),
3401             elf_object.Symbol.function("_start", 1, 0, text.len),
3402             elf_object.Symbol.function("__tldr_incremental_metadata_growth_probe", 1, 0, 0),
3403         },
3404     });
3405     defer allocator.free(new_object);
3406     try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = object_path, .data = new_object });
3407 
3408     var candidate = try tldr.link(
3409         allocator,
3410         &.{.{ .name = object_path, .bytes = new_object }},
3411         .{ .entry_symbol = "_start", .incremental_mode = .relink },
3412     );
3413     defer candidate.deinit(allocator);
3414 
3415     stderr = std.Io.Writer.fixed(&stderr_buffer);
3416     const relink_code = try run(
3417         allocator,
3418         &.{
3419             "--incremental=relink",
3420             "--incremental-manifest",
3421             manifest_path,
3422             "-o",
3423             output_path,
3424             "-e",
3425             "_start",
3426             object_path,
3427         },
3428         &stderr,
3429         .{ .width = 88 },
3430     );
3431     try std.testing.expectEqual(@as(u8, 0), relink_code);
3432 
3433     const relinked = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), output_path, allocator, .limited(4096));
3434     defer allocator.free(relinked);
3435     try std.testing.expectEqualSlices(u8, candidate.bytes, relinked);
3436 
3437     const manifest_bytes = try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), manifest_path, allocator, .limited(4096));
3438     defer allocator.free(manifest_bytes);
3439     var parsed_manifest = try tldr.incremental.Manifest.fromBinary(allocator, manifest_bytes);
3440     defer parsed_manifest.deinit(allocator);
3441 
3442     try std.testing.expect(parsed_manifest.canReuseFor(
3443         .{ .entry_symbol = "_start", .incremental_mode = .relink },
3444         &.{.{ .name = object_path, .bytes = new_object }},
3445     ));
3446 }
3447 
3448 test "candidate replacement extraction covers changed in-place plans" {
3449     try std.testing.expect(relinkNeedsCandidateReplacements(
3450         .{ .decision = .full_link, .blocker = .replacement_missing },
3451         .{ .summary = .{ .changed = 1 } },
3452     ));
3453     try std.testing.expect(relinkNeedsCandidateReplacements(
3454         .{ .decision = .in_place },
3455         .{ .summary = .{ .changed = 1 } },
3456     ));
3457     try std.testing.expect(!relinkNeedsCandidateReplacements(
3458         .{ .decision = .reuse_output },
3459         .{ .summary = .{ .unchanged = 1 } },
3460     ));
3461     try std.testing.expect(!relinkNeedsCandidateReplacements(
3462         .{ .decision = .full_link, .blocker = .input_added },
3463         .{ .summary = .{ .added = 1 } },
3464     ));
3465 }
3466 
3467 fn linkedSectionSize(image: []const u8, target_name: []const u8) !u64 {
3468     if (image.len < 64 or !std.mem.eql(u8, image[0..4], std.elf.MAGIC)) return error.InvalidElfHeader;
3469     const shoff = readElfU64(image, 40);
3470     const shentsize = readElfU16(image, 58);
3471     const shnum = readElfU16(image, 60);
3472     const shstrndx = readElfU16(image, 62);
3473     if (shentsize < 64 or shnum == 0 or shstrndx >= shnum) return error.InvalidElfHeader;
3474 
3475     const shstr_start = try sectionHeaderStart(image, shoff, shentsize, shstrndx);
3476     const shstr_offset = readElfU64(image, shstr_start + 24);
3477     const shstr_size = readElfU64(image, shstr_start + 32);
3478     if (shstr_offset > image.len or shstr_offset + shstr_size > image.len) return error.InvalidElfHeader;
3479     const shstrtab = image[@intCast(shstr_offset)..@intCast(shstr_offset + shstr_size)];
3480 
3481     var section_index: u16 = 0;
3482     while (section_index < shnum) : (section_index += 1) {
3483         const section_start = try sectionHeaderStart(image, shoff, shentsize, section_index);
3484         const name = try linkedSectionName(shstrtab, readElfU32(image, section_start));
3485         if (std.mem.eql(u8, name, target_name)) return readElfU64(image, section_start + 32);
3486     }
3487     return error.MissingSection;
3488 }
3489 
3490 fn linkedSectionName(shstrtab: []const u8, offset: u32) ![]const u8 {
3491     const start: usize = @intCast(offset);
3492     if (start >= shstrtab.len) return error.InvalidElfHeader;
3493     const end_offset = std.mem.indexOfScalar(u8, shstrtab[start..], 0) orelse return error.InvalidElfHeader;
3494     return shstrtab[start .. start + end_offset];
3495 }
3496 
3497 fn sectionHeaderStart(image: []const u8, shoff: u64, shentsize: u16, index: u16) !usize {
3498     const start = shoff + @as(u64, shentsize) * @as(u64, index);
3499     if (start > image.len or start + 64 > image.len) return error.InvalidElfHeader;
3500     return @intCast(start);
3501 }
3502 
3503 fn readElfU16(image: []const u8, offset: usize) u16 {
3504     return std.mem.readInt(u16, image[offset..][0..2], .little);
3505 }
3506 
3507 fn readElfU32(image: []const u8, offset: usize) u32 {
3508     return std.mem.readInt(u32, image[offset..][0..4], .little);
3509 }
3510 
3511 fn readElfU64(image: []const u8, offset: usize) u64 {
3512     return std.mem.readInt(u64, image[offset..][0..8], .little);
3513 }