add --z-dump-linker flag

This commit is contained in:
Luke Boswell 2025-12-12 13:05:28 +11:00
parent 3db5635c24
commit 457d979d8a
No known key found for this signature in database
GPG key ID: 54A7324B1B975757
3 changed files with 190 additions and 10 deletions

View file

@ -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=<bytes> Stack size for WASM targets in bytes (default: 8388608 = 8MB)
\\ --z-bench-tokenize=<path> Benchmark tokenizer on a file or directory
\\ --z-bench-parse=<path> 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 {

View file

@ -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) {

View file

@ -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,