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 }