From e9ecd840c5fe8c29d12570dd201f72cdfba66f68 Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Tue, 16 Dec 2025 17:16:13 +1100 Subject: [PATCH] fix parallel build race condition with content-hashed shim files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The platform shim generation was writing to fixed filenames (platform_shim.bc and platform_shim.o) in the shared cache directory, causing race conditions when multiple builds ran in parallel. Fix: Include a CRC32 hash of the serialized module content in the shim filenames. Each unique module content now gets its own shim files, allowing parallel builds to proceed without interfering with each other. Also: - Remove obsolete wasm-elem test platform from build.zig - Update CLI tests to run in parallel instead of sequentially 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- build.zig | 32 +++++++++----------------------- src/cli/main.zig | 20 ++++++++++++++++++-- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/build.zig b/build.zig index b96f35b3ce..96cf176221 100644 --- a/build.zig +++ b/build.zig @@ -1443,22 +1443,6 @@ fn setupTestPlatforms( clear_cache_step.dependOn(©_step.step); } - // Build the wasm-elem test platform host for wasm32-freestanding (tests parameterized opaque types with requires clause) - { - const wasm_target = b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .freestanding, .abi = .none }); - const copy_step = buildAndCopyTestPlatformHostLib( - b, - "wasm-elem", - wasm_target, - "wasm32", - optimize, - roc_modules, - strip, - omit_frame_pointer, - ); - clear_cache_step.dependOn(©_step.step); - } - b.getInstallStep().dependOn(clear_cache_step); test_platforms_step.dependOn(clear_cache_step); } @@ -1680,8 +1664,7 @@ pub fn build(b: *std.Build) void { b.installArtifact(test_runner_exe); // CLI integration tests - run actual roc programs like CI does - // NOTE: These tests must run sequentially to avoid race conditions on the Roc cache - // Order: test_runner int -> test_runner str -> roc_subcommands_test + // These tests can run in parallel since each build uses content-hashed shim files if (!no_bin) { const install = b.addInstallArtifact(roc_exe, .{}); const install_runner = b.addInstallArtifact(test_runner_exe, .{}); @@ -1694,16 +1677,19 @@ pub fn build(b: *std.Build) void { run_int_tests.step.dependOn(&install.step); run_int_tests.step.dependOn(&install_runner.step); run_int_tests.step.dependOn(test_platforms_step); + test_cli_step.dependOn(&run_int_tests.step); // Test str platform (native mode only for now) - // Run after int tests to avoid cache race conditions const run_str_tests = b.addRunArtifact(test_runner_exe); run_str_tests.addArg("zig-out/bin/roc"); run_str_tests.addArg("str"); run_str_tests.addArg("--mode=native"); - run_str_tests.step.dependOn(&run_int_tests.step); + run_str_tests.step.dependOn(&install.step); + run_str_tests.step.dependOn(&install_runner.step); + run_str_tests.step.dependOn(test_platforms_step); + test_cli_step.dependOn(&run_str_tests.step); - // Roc subcommands integration test - runs after test_runner tests + // Roc subcommands integration test const roc_subcommands_test = b.addTest(.{ .name = "roc_subcommands_test", .root_module = b.createModule(.{ @@ -1718,8 +1704,8 @@ pub fn build(b: *std.Build) void { if (run_args.len != 0) { run_roc_subcommands_test.addArgs(run_args); } - run_roc_subcommands_test.step.dependOn(&run_str_tests.step); // Run after test_runner - + run_roc_subcommands_test.step.dependOn(&install.step); + run_roc_subcommands_test.step.dependOn(test_platforms_step); test_cli_step.dependOn(&run_roc_subcommands_test.step); } diff --git a/src/cli/main.zig b/src/cli/main.zig index 48a3381e77..35075c15e7 100644 --- a/src/cli/main.zig +++ b/src/cli/main.zig @@ -827,12 +827,28 @@ fn generatePlatformHostShim(allocs: *Allocators, cache_dir: []const u8, entrypoi }; // Generate paths for temporary files - const bitcode_path = std.fs.path.join(allocs.arena, &.{ cache_dir, "platform_shim.bc" }) catch |err| { + // Use a hash of the serialized module content to avoid race conditions when multiple + // builds run in parallel. Each unique module content gets its own shim files. + const content_hash = if (serialized_module) |module_bytes| + std.hash.Crc32.hash(module_bytes) + else + 0; // For IPC mode (roc run), use a fixed name since there's no embedded data + + const bitcode_filename = std.fmt.allocPrint(allocs.arena, "platform_shim_{x}.bc", .{content_hash}) catch |err| { + std.log.err("Failed to create bitcode filename: {}", .{err}); + return err; + }; + const object_filename = std.fmt.allocPrint(allocs.arena, "platform_shim_{x}.o", .{content_hash}) catch |err| { + std.log.err("Failed to create object filename: {}", .{err}); + return err; + }; + + const bitcode_path = std.fs.path.join(allocs.arena, &.{ cache_dir, bitcode_filename }) catch |err| { std.log.err("Failed to create bitcode path: {}", .{err}); return err; }; - const object_path = std.fs.path.join(allocs.arena, &.{ cache_dir, "platform_shim.o" }) catch |err| { + const object_path = std.fs.path.join(allocs.arena, &.{ cache_dir, object_filename }) catch |err| { std.log.err("Failed to create object path: {}", .{err}); return err; };