{"slug": "a-caching-problem-with-0-17", "title": "A caching problem with 0.17", "summary": "A Zig 0.17 caching bug causes stale executables when a C project's header changes after a library-only relink, according to a forum report from a developer building a JUCE C++ project on an M2 Mac running Tahoe 26.6.2. The report states that when Zig relinks without compiling sources it rewrites the build step's cache note with source files only and no headers, so a later header change is reported as \"cached\" and the old binary persists until .zig-cache is deleted. The developer published a minimal reproduction in /tmp/zig-header-bug and could not file a bug report due to Zig's no-AI bug report policy.", "body_md": "Hi,\n\nI use zig to build a c++ JUCE project on a Mac M2 Silicon running Tahoe 26.6.2 and recently updated to Zig 0.17.\n\nToday I had the following problem: I build the project but the executable didn’t get updated. After some research, Claude claimed that the problem lies in Zig.\n\nI am not so proficient in Zig to judge for myself if the problem was indeed in Zig and I cannot send a bug report due to the no AI bug report policy, which is fine.\n\nBecause I benefit greatly from using Zig daily, I still wanted to provide some feedback, so I chose this forum. I asked for a minimal project that can recreate the problem, it is here. The problem was real, I am unsure about the cause.\n\nKind regards.\n\nZig keeps two kinds of cache notes for a C program:\n\n`main.c` and `value.h`. This note is correct.` cached` and does no work. It never opens the per-file notes.\nThe bug is in the second note. Sometimes zig must redo a step without compiling any of its sources, for example when a library it links has changed, so it only re-links. Zig then rewrites the step’s note with the source files only, and no headers. The header list comes from compiling, and nothing was compiled.\n\nAfter that, the step’s note no longer mentions `value.h`. When `value.h` changes, zig checks the note, sees no listed file changed, reports `cached`, and keeps the old binary. The binary stays stale on every later build until `.zig-cache` is deleted.\n\n| File | What it does | \n|---|---|\n| `value.h` | Defines `VALUE` | \n| `main.c` | Includes `value.h` and prints`VALUE` | \n| `other.c` | A static library with one function that returns a number. `main.c` prints its result too. | \n| `build.zig` | Builds the library and an executable that links it | \n\nIt creates the project in `/tmp/zig-header-bug` and runs two cases. It touches nothing outside `/tmp`.\n\n```\n(\nd=/tmp/zig-header-bug\nrm -rf \"$d\"\nmkdir -p \"$d\"\ncd \"$d\" || exit 1\n\ncat > build.zig <<'EOF'\nconst std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n    const target = b.standardTargetOptions(.{});\n    const optimize = b.standardOptimizeOption(.{});\n\n    const lib_mod = b.createModule(.{ .target = target, .optimize = optimize, .link_libc = true });\n    lib_mod.addCSourceFile(.{ .file = b.path(\"other.c\") });\n    const lib = b.addLibrary(.{ .name = \"other\", .root_module = lib_mod, .linkage = .static });\n\n    const exe_mod = b.createModule(.{ .target = target, .optimize = optimize, .link_libc = true });\n    exe_mod.addCSourceFile(.{ .file = b.path(\"main.c\") });\n    exe_mod.linkLibrary(lib);\n    const exe = b.addExecutable(.{ .name = \"repro\", .root_module = exe_mod });\n    b.installArtifact(exe);\n}\nEOF\n\ncat > main.c <<'EOF'\n#include <stdio.h>\n#include \"value.h\"\nint other(void);\nint main(void)\n{\n    printf(\"header=%d library=%d\\n\", VALUE, other());\n    return 0;\n}\nEOF\n\nsetHeader() { printf '#define VALUE %s\\n' \"$1\" > value.h; }\nsetLib() { printf 'int other(void) { return %s; }\\n' \"$1\" > other.c; }\nrun() {\n    sleep 1\n    zig build --summary all 2>&1 | grep -E 'compile exe repro|error'\n    printf '  output: '\n    ./zig-out/bin/repro\n}\n\necho \"zig $(zig version)\"\n\necho '=== CONTROL: header change only ==='\nrm -rf .zig-cache zig-out\nsetHeader 1; setLib 1\necho '-- first build (expect header=1 library=1)'; run\nsetHeader 2\necho '-- header 1 -> 2 (expect header=2 library=1)'; run\n\necho '=== BUG: library change, then header change ==='\nrm -rf .zig-cache zig-out\nsetHeader 1; setLib 1\necho '-- first build (expect header=1 library=1)'; run\nsetLib 2\necho '-- library 1 -> 2, program only re-linked (expect header=1 library=2)'; run\nsetHeader 2\necho '-- header 1 -> 2 (expect header=2 library=2)'; run\necho '-- the same build again, nothing changed (expect header=2 library=2)'; run\n) 2>&1 | tee /tmp/zig-header-bug.txt\n```\n\n`(` to the last line, and press Return.`/tmp/zig-header-bug.txt`.\nThe `sleep 1` before each build keeps file times clearly apart, so the result cannot be a timing effect.\n\nThe same steps by hand, in the project folder:\n\n`zig build`, then `./zig-out/bin/repro`. It prints `header=1 library=1`.` other.c` to return 2, and build again. Zig re-links only, and the program prints `header=1 library=2`.`#define VALUE 2`, and build again. Zig reports `header=1`.\n| Step | Expected | Actual | \n|---|---|---|\n| Control: change the header only | Recompile, `header=2` | Recompile, `header=2` (correct) | \n| Bug: change the library | Re-link, `header=1 library=2` | Re-link, `header=1 library=2` (correct) | \n| Bug: then change the header | Recompile, `header=2 library=2` | **`cached`, `header=1 library=2`** | \n| Bug: build again, no change | `header=2 library=2` | **`cached`, `header=1 library=2`** | \n\n```\nzig 0.17.0\n=== CONTROL: header change only ===\n-- first build (expect header=1 library=1)\n   +- compile exe repro debug native success 125ms MaxRSS:54M\n  output: header=1 library=1\n-- header 1 -> 2 (expect header=2 library=1)\n   +- compile exe repro debug native success 133ms MaxRSS:54M\n  output: header=2 library=1\n=== BUG: library change, then header change ===\n-- first build (expect header=1 library=1)\n   +- compile exe repro debug native success 118ms MaxRSS:54M\n  output: header=1 library=1\n-- library 1 -> 2, program only re-linked (expect header=1 library=2)\n   +- compile exe repro debug native success 81ms MaxRSS:49M\n  output: header=1 library=2\n-- header 1 -> 2 (expect header=2 library=2)\n   +- compile exe repro debug native cached 62ms MaxRSS:30M\n  output: header=1 library=2\n-- the same build again, nothing changed (expect header=2 library=2)\n   +- compile exe repro debug native cached 62ms MaxRSS:30M\n  output: header=1 library=2\n```\n\nThe build reports success and the output looks fine, but it is built from old headers. In a real project, the common trigger is a change in a linked library while the program’s own sources stay the same. A later header change then ships silently as old code.\n\n`.zig-cache` before a build that must be correct.", "url": "https://wpnews.pro/news/a-caching-problem-with-0-17", "canonical_source": "https://ziggit.dev/t/a-caching-problem-with-0-17/17929#post_1", "published_at": "2026-10-12 20:00:55+00:00", "updated_at": "2026-10-12 20:22:33.217414+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Zig", "Zig 0.17", "JUCE", "Claude", "Apple M2"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/a-caching-problem-with-0-17", "markdown": "https://wpnews.pro/news/a-caching-problem-with-0-17.md", "text": "https://wpnews.pro/news/a-caching-problem-with-0-17.txt", "jsonld": "https://wpnews.pro/news/a-caching-problem-with-0-17.jsonld"}}