... field note

Pin your build targets

date May 30, 2026 author Kevin

A minor version bump in OpenTUI — from 0.2.6 to 0.2.7 — broke the library for anyone running Linux with GLIBC 2.28 or older. That’s RHEL, CentOS, AlmaLinux 8, Ubuntu 18.04 and several other enterprise distributions still in active support.

The error:

/lib64/libm.so.6: version `GLIBC_2.29' not found

The TUI wouldn’t even start. Downgrading was the only workaround.

The Investigation

OpenTUI is a TypeScript library built on a native Zig core. The native binary is cross-compiled for six platforms and distributed as an optional npm dependency. When I compared the two releases, the binary nearly doubled in size — from 4.5MB to 7.8MB.

Three things changed in 0.2.7:

  1. Native audio support using the miniaudio C library
  2. Zig target triplets changed from x86_64-linux (musl) to x86_64-linux-gnu (glibc)
  3. linkLibC() was called to link the C audio library

The audio library itself wasn’t the problem — miniaudio loads ALSA and PulseAudio via dlopen() at runtime. The culprit was the target triplet change.

When Zig targets x86_64-linux-gnu, it links against the host system’s glibc. The CI runs on Ubuntu 24.04, which ships GLIBC 2.39. The resulting binary referenced math symbols that didn’t exist until GLIBC 2.29 (September 2018).

Once I had a clear understanding of the problem I opened an issue on Github.

The Dead End

My first instinct was to revert to musl targets. That’s what worked before 0.2.7, and Zig bundles musl statically — zero glibc dependency.

But addCSourceFile() in Zig’s build system always links libc dynamically, even on musl targets. The binary would reference libc.so, which doesn’t exist on glibc systems (they only have libc.so.6). Static linking the C library helped, but it felt like fighting the toolchain.

The Fix

The solution was to pin:

// Before (broken)
.{ .zig_target = "x86_64-linux-gnu", ... },

// After (fixed)
.{ .zig_target = "x86_64-linux-gnu.2.28", ... },

That .2.28 suffix tells Zig to pin the minimum GLIBC version. The compiler will only emit symbols available in GLIBC 2.28 or earlier. The binary dynamically links, but it’s compatible with anything from RHEL 8 onward.

This is the industry standard. Electron, VS Code, Node.js native modules, and sharp all do the same thing — usually pinning to GLIBC 2.17 (the RHEL 7 baseline).

Once I had a working fix, I opened a pull request on github.

The Lesson

When you change a build target from musl to glibc, you inherit the host system’s GLIBC version as a hidden dependency. There’s no warning, no error, and no way to detect it until someone on an older system tries to run your binary.

The fix was trivial. Finding it took an hour of reading Zig build system internals, testing in Docker containers, and learning why addCSourceFile() behaves the way it does.

Open source rocks!