Blog
[measured] Zig resolves -target x86_64-linux to musl. A job added to catch platform-blind verification had been compiling every migrated file against a libc no real build of the project uses.

A CI job in trinity-fpga compiles every file that has been migrated to Zig 0.16, one at a time, on Linux. It exists for a specific reason: an earlier version of the same check passed on a macOS laptop and failed on the runner, because macOS links libc implicitly and Linux does not. So the target was pinned rather than left native, and the comment beside it said the pin was "a no-op on this runner."
The pin was x86_64-linux. That is not the runner's libc. Zig resolves a Linux target with no ABI suffix to musl, and the runner — and every real build of this project — is glibc.
Reproduce it with a file that does nothing. Zig 0.16.0, cross-compiling from macOS aarch64, one empty main and -lc:
printf 'pub fn main() void {}\n' > t.zig
for t in x86_64-linux x86_64-linux-gnu x86_64-linux-musl; do
zig build-exe t.zig -target $t -lc -femit-bin=out_$t
file out_$t
done
| -target | file(1) reports |
|---|---|
| x86_64-linux | statically linked |
| x86_64-linux-gnu | dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2 |
| x86_64-linux-musl | statically linked |
The first row and the third row behave the same way and the middle one does not. Static linkage with no interpreter is the musl build; the glibc build carries a dynamic loader in its program headers. The unsuffixed target sits with musl, and the only target triple that appears as a string anywhere in the unsuffixed binary is x86_64-linux-musl — Zig records the resolved triple, not the one you typed.
Whether that difference can reach a real file is a question about the standard library. In the 0.16.0 install used here, std/c.zig contains eleven isGnu() or isMusl() call sites:
grep -cE '\.isGnu\(\)|\.isMusl\(\)' "$(dirname "$(readlink -f "$(which zig)")")/../lib/zig/std/c.zig"
11
A file that reaches one of those switches on the musl side compiles cleanly under x86_64-linux and fails under x86_64-linux-gnu. The job would have gone green and the real build would have gone red — which is the exact failure the job was created to prevent, moved one layer down. A check written against platform-blind verification was itself verifying against a platform nobody ships.
The fix is one suffix. The pin is now x86_64-linux-gnu, with the measurement written into the comment so the three characters do not read as decoration to whoever edits the line next. The false claim was corrected in place rather than deleted: "this used to be wrong and here is why" survives a later reader better than silence does.
The pull request describes the musl-suffixed build as "byte-identical" to the unsuffixed one. That is not checkable this way, and the reason is worth more than the claim. Building the identical target three times, into three separate directories with the identical output name, gives three different binaries:
| build of -target x86_64-linux | size (bytes) | sha256 prefix |
|---|---|---|
| first | 12,731,133 | 468ce3a1 |
| second | 12,731,154 | 6998eff5 |
| third | 12,731,133 | 1d387f61 |
Three hashes, two sizes, from one command run three times. So the hash cannot discriminate between targets here at all — it does not even discriminate a target from itself, and neither does the size. What survives that control is the linkage reported by file(1) and the resolved triple recorded in the binary. Those two are the evidence; the hash never was.
This does not weaken the correction. It replaces a strong-sounding piece of support with a weaker piece that is actually true, which is the direction that costs nothing later.
The workflow file has exactly three commits, and two of them are corrections to the first:
| PR | merged (UTC) | what was wrong |
|---|---|---|
| #746 | 2026-09-05 18:38:29 | the job is introduced |
| #747 | 2026-09-05 18:57:37 | its predicate tested the type, not the entry point |
| #749 | 2026-09-05 19:48:52 | its target resolved to musl, not glibc |
Seventy minutes and twenty-three seconds from introduction to the second correction. Neither defect was visible in the YAML; both were found by extracting the step body and running it verbatim against the tree. Reading a gate tells you what it was meant to check. Running it tells you what it checks.
No file in this tree was shown to depend on a musl-only symbol. The finding is that the gate could not have caught one, not that one is there. The target resolution was measured on one host cross-compiling, not on the runner; the runner is inferred to behave the same because it fetches the same Zig version. And the job compiles files — it does not run them. A file that links against the right libc has not thereby been shown to work.
Work with me
I audit RTL and build independent, bit-exact models, then take the result through synthesis and, when useful, onto an Artix-7 board. The first conformance module is free.