From 457d979d8ac7525c78bd08dc6fe130d528e1b1e3 Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Fri, 12 Dec 2025 13:05:28 +1100 Subject: [PATCH] add `--z-dump-linker` flag --- src/cli/cli_args.zig | 7 ++- src/cli/linker.zig | 59 ++++++++++++++++--- src/cli/main.zig | 134 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+), 10 deletions(-) diff --git a/src/cli/cli_args.zig b/src/cli/cli_args.zig index 44ac12541c..5cd6813940 100644 --- a/src/cli/cli_args.zig +++ b/src/cli/cli_args.zig @@ -92,6 +92,7 @@ pub const BuildArgs = struct { wasm_stack_size: ?usize = null, // stack size for WASM targets (default: 8MB) z_bench_tokenize: ?[]const u8 = null, // benchmark tokenizer on a file or directory z_bench_parse: ?[]const u8 = null, // benchmark parser on a file or directory + z_dump_linker: bool = false, // dump linker inputs to temp directory for debugging }; /// Arguments for `roc test` @@ -271,6 +272,7 @@ fn parseBuild(args: []const []const u8) CliArgs { var wasm_stack_size: ?usize = null; var z_bench_tokenize: ?[]const u8 = null; var z_bench_parse: ?[]const u8 = null; + var z_dump_linker: bool = false; for (args) |arg| { if (isHelpFlag(arg)) { return CliArgs{ .help = @@ -291,6 +293,7 @@ fn parseBuild(args: []const []const u8) CliArgs { \\ --wasm-stack-size= Stack size for WASM targets in bytes (default: 8388608 = 8MB) \\ --z-bench-tokenize= Benchmark tokenizer on a file or directory \\ --z-bench-parse= Benchmark parser on a file or directory + \\ --z-dump-linker Dump linker inputs to temp directory for debugging \\ -h, --help Print help \\ }; @@ -348,6 +351,8 @@ fn parseBuild(args: []const []const u8) CliArgs { } else { return CliArgs{ .problem = CliProblem{ .missing_flag_value = .{ .flag = "--wasm-stack-size" } } }; } + } else if (mem.eql(u8, arg, "--z-dump-linker")) { + z_dump_linker = true; } else { if (path != null) { return CliArgs{ .problem = CliProblem{ .unexpected_argument = .{ .cmd = "build", .arg = arg } } }; @@ -355,7 +360,7 @@ fn parseBuild(args: []const []const u8) CliArgs { path = arg; } } - return CliArgs{ .build = BuildArgs{ .path = path orelse "main.roc", .opt = opt, .target = target, .output = output, .debug = debug, .allow_errors = allow_errors, .wasm_memory = wasm_memory, .wasm_stack_size = wasm_stack_size, .z_bench_tokenize = z_bench_tokenize, .z_bench_parse = z_bench_parse } }; + return CliArgs{ .build = BuildArgs{ .path = path orelse "main.roc", .opt = opt, .target = target, .output = output, .debug = debug, .allow_errors = allow_errors, .wasm_memory = wasm_memory, .wasm_stack_size = wasm_stack_size, .z_bench_tokenize = z_bench_tokenize, .z_bench_parse = z_bench_parse, .z_dump_linker = z_dump_linker } }; } fn parseBundle(alloc: mem.Allocator, args: []const []const u8) std.mem.Allocator.Error!CliArgs { diff --git a/src/cli/linker.zig b/src/cli/linker.zig index 90fa37cab1..de59a4fb83 100644 --- a/src/cli/linker.zig +++ b/src/cli/linker.zig @@ -119,13 +119,10 @@ pub const LinkError = error{ WindowsSDKNotFound, } || std.zig.system.DetectError; -/// Link object files into an executable using LLD -pub fn link(allocs: *Allocators, config: LinkConfig) LinkError!void { - // Check if LLVM is available at compile time - if (comptime !llvm_available) { - return LinkError.LLVMNotAvailable; - } - +/// Build the linker command arguments for the given configuration. +/// Returns the args array that would be passed to LLD. +/// This is used both by link() and formatLinkCommand(). +fn buildLinkArgs(allocs: *Allocators, config: LinkConfig) LinkError!std.array_list.Managed([]const u8) { // Use arena allocator for all temporary allocations // Pre-allocate capacity to avoid reallocations (typical command has 20-40 args) var args = std.array_list.Managed([]const u8).initCapacity(allocs.arena, 64) catch return LinkError.OutOfMemory; @@ -356,6 +353,18 @@ pub fn link(allocs: *Allocators, config: LinkConfig) LinkError!void { try args.append(extra_arg); } + return args; +} + +/// Link object files into an executable using LLD +pub fn link(allocs: *Allocators, config: LinkConfig) LinkError!void { + // Check if LLVM is available at compile time + if (comptime !llvm_available) { + return LinkError.LLVMNotAvailable; + } + + const args = try buildLinkArgs(allocs, config); + // Debug: Print the linker command std.log.debug("Linker command:", .{}); for (args.items) |arg| { @@ -371,7 +380,7 @@ pub fn link(allocs: *Allocators, config: LinkConfig) LinkError!void { } // Call appropriate LLD function based on target format - const success = if (comptime llvm_available) switch (config.target_format) { + const success = switch (config.target_format) { .elf => llvm_externs.ZigLLDLinkELF( @intCast(c_args.len), c_args.ptr, @@ -396,13 +405,45 @@ pub fn link(allocs: *Allocators, config: LinkConfig) LinkError!void { config.can_exit_early, config.disable_output, ), - } else false; + }; if (!success) { return LinkError.LinkFailed; } } +/// Format link configuration as a shell command string for manual reproduction. +/// Useful for debugging linking issues by allowing users to run the linker manually. +pub fn formatLinkCommand(allocs: *Allocators, config: LinkConfig) LinkError![]const u8 { + const args = try buildLinkArgs(allocs, config); + + // Join args with spaces, quoting paths that contain spaces or special chars + var result = std.array_list.Managed(u8).init(allocs.arena); + + for (args.items, 0..) |arg, i| { + if (i > 0) result.append(' ') catch return LinkError.OutOfMemory; + + // Quote if contains spaces or shell metacharacters + const needs_quoting = std.mem.indexOfAny(u8, arg, " \t'\"\\$`") != null; + if (needs_quoting) { + result.append('\'') catch return LinkError.OutOfMemory; + // Escape single quotes within the string + for (arg) |c| { + if (c == '\'') { + result.appendSlice("'\\''") catch return LinkError.OutOfMemory; + } else { + result.append(c) catch return LinkError.OutOfMemory; + } + } + result.append('\'') catch return LinkError.OutOfMemory; + } else { + result.appendSlice(arg) catch return LinkError.OutOfMemory; + } + } + + return result.toOwnedSlice() catch return LinkError.OutOfMemory; +} + /// Convenience function to link two object files into an executable pub fn linkTwoObjects(allocs: *Allocators, obj1: []const u8, obj2: []const u8, output: []const u8) LinkError!void { if (comptime !llvm_available) { diff --git a/src/cli/main.zig b/src/cli/main.zig index e3cf176a24..4e1af5ba17 100644 --- a/src/cli/main.zig +++ b/src/cli/main.zig @@ -3816,6 +3816,11 @@ fn rocBuildEmbedded(allocs: *Allocators, args: cli_args.BuildArgs) !void { .wasm_stack_size = args.wasm_stack_size orelse linker_mod.DEFAULT_WASM_STACK_SIZE, }; + // Dump linker inputs to temp directory if requested + if (args.z_dump_linker) { + try dumpLinkerInputs(allocs, link_config); + } + try linker_mod.link(allocs, link_config); const output_type = switch (link_type) { @@ -3826,6 +3831,135 @@ fn rocBuildEmbedded(allocs: *Allocators, args: cli_args.BuildArgs) !void { std.log.info("Successfully built {s}: {s}", .{ output_type, final_output_path }); } +/// Dump linker inputs to a temp directory for debugging linking issues. +/// Creates a directory with all input files copied and a README with the linker command. +fn dumpLinkerInputs(allocs: *Allocators, link_config: linker.LinkConfig) !void { + const stderr = stderrWriter(); + defer stderr.flush() catch {}; + + // Create temp directory with unique name based on timestamp + const timestamp = std.time.timestamp(); + const dir_name = try std.fmt.allocPrint(allocs.arena, "roc-linker-debug-{d}", .{timestamp}); + const dump_dir = try std.fs.path.join(allocs.arena, &.{ "/tmp", dir_name }); + + std.fs.cwd().makePath(dump_dir) catch |err| { + try stderr.print("Failed to create debug dump directory '{s}': {}\n", .{ dump_dir, err }); + return err; + }; + + // Track copied files for the README + var copied_files = try std.array_list.Managed(CopiedFile).initCapacity(allocs.arena, 16); + + // Copy platform_files_pre + for (link_config.platform_files_pre, 0..) |src, i| { + const basename = std.fs.path.basename(src); + const dest_name = try std.fmt.allocPrint(allocs.arena, "pre_{d}_{s}", .{ i, basename }); + const dest_path = try std.fs.path.join(allocs.arena, &.{ dump_dir, dest_name }); + std.fs.cwd().copyFile(src, std.fs.cwd(), dest_path, .{}) catch |err| { + try stderr.print("Warning: Failed to copy '{s}': {}\n", .{ src, err }); + continue; + }; + try copied_files.append(.{ .name = dest_name, .original = src, .category = "platform (pre-link)" }); + } + + // Copy object_files + for (link_config.object_files, 0..) |src, i| { + const basename = std.fs.path.basename(src); + const dest_name = try std.fmt.allocPrint(allocs.arena, "obj_{d}_{s}", .{ i, basename }); + const dest_path = try std.fs.path.join(allocs.arena, &.{ dump_dir, dest_name }); + std.fs.cwd().copyFile(src, std.fs.cwd(), dest_path, .{}) catch |err| { + try stderr.print("Warning: Failed to copy '{s}': {}\n", .{ src, err }); + continue; + }; + try copied_files.append(.{ .name = dest_name, .original = src, .category = "object file" }); + } + + // Copy platform_files_post + for (link_config.platform_files_post, 0..) |src, i| { + const basename = std.fs.path.basename(src); + const dest_name = try std.fmt.allocPrint(allocs.arena, "post_{d}_{s}", .{ i, basename }); + const dest_path = try std.fs.path.join(allocs.arena, &.{ dump_dir, dest_name }); + std.fs.cwd().copyFile(src, std.fs.cwd(), dest_path, .{}) catch |err| { + try stderr.print("Warning: Failed to copy '{s}': {}\n", .{ src, err }); + continue; + }; + try copied_files.append(.{ .name = dest_name, .original = src, .category = "platform (post-link)" }); + } + + // Generate the linker command string + const link_cmd = linker.formatLinkCommand(allocs, link_config) catch |err| { + try stderr.print("Warning: Failed to format linker command: {}\n", .{err}); + return; + }; + + // Build the file list for README + var file_list = std.array_list.Managed(u8).init(allocs.arena); + for (copied_files.items) |file| { + try file_list.writer().print(" {s}\n <- {s} ({s})\n", .{ file.name, file.original, file.category }); + } + + // Write README.txt with instructions + const readme_content = try std.fmt.allocPrint(allocs.arena, + \\Roc Linker Debug Dump + \\===================== + \\ + \\Target format: {s} + \\Target OS: {s} + \\Target arch: {s} + \\Output: {s} + \\ + \\Files ({d} copied): + \\{s} + \\ + \\To manually reproduce the link step: + \\ + \\ {s} + \\ + \\Note: The command above uses original file paths. The copied files + \\in this directory preserve original filenames for inspection. + \\ + , .{ + @tagName(link_config.target_format), + if (link_config.target_os) |os| @tagName(os) else "native", + if (link_config.target_arch) |arch| @tagName(arch) else "native", + link_config.output_path, + copied_files.items.len, + file_list.items, + link_cmd, + }); + + const readme_path = try std.fs.path.join(allocs.arena, &.{ dump_dir, "README.txt" }); + const readme_file = std.fs.cwd().createFile(readme_path, .{}) catch |err| { + try stderr.print("Warning: Failed to create README.txt: {}\n", .{err}); + return; + }; + defer readme_file.close(); + readme_file.writeAll(readme_content) catch |err| { + try stderr.print("Warning: Failed to write README.txt: {}\n", .{err}); + }; + + // Print summary to stderr + try stderr.print( + \\ + \\=== Linker debug dump === + \\Directory: {s} + \\Files: {d} copied + \\ + \\To reproduce: + \\ {s} + \\ + \\See {s}/README.txt for details + \\========================= + \\ + , .{ dump_dir, copied_files.items.len, link_cmd, dump_dir }); +} + +const CopiedFile = struct { + name: []const u8, + original: []const u8, + category: []const u8, +}; + /// Information about a test (expect statement) to be evaluated const ExpectTest = struct { expr_idx: can.CIR.Expr.Idx,