Cross-libc Packages
Linux has two major libc families: glibc (Debian, Fedora, Arch, most distros) and musl (Alpine, Void-musl, static builds). Binaries built against one cannot run on the other without bundled pieces, because:
- The dynamic linker path differs. A musl ELF has
PT_INTERP = /lib/ld-musl-x86_64.so.1. A glibc host doesn't have that file. - Libraries linked against one libc can't satisfy the other's symbols. Glibc's fortify
__printf_chkdoesn't exist in musl, for example.
onelf handles both concerns automatically.
What happens at pack time
When bundle-libs sees a musl binary (PT_INTERP matches ld-musl-*), it:
- Bundles the musl libc under both names it might be called: the actual filename (
libc.musl-x86_64.so.1) and a symlink (ld-musl-x86_64.so.1) matching thePT_INTERP. - Skips glibc libc / loader entries that would show up transitively (e.g. from
libgcc_s.so.1). Those are wrong-family and would break things. - With
--strict-libc, refuses to bundle other libraries that link against the wrong libc at all.
What happens at runtime
bundle-libs rewrites each ELF's PT_INTERP to a path relative to the AppDir root (like lib/ld-musl-x86_64.so.1 or lib/ld-linux-x86-64.so.2). At exec time the runtime chdirs into the AppDir and the kernel resolves the relative PT_INTERP against the bundled loader. The host's own loader is never consulted, so a musl binary runs on a glibc host and vice versa without any host-level setup.
For PIE (ET_DYN) binaries the runtime can also use userland-execve to map the bundled loader directly and skip the kernel loader entirely. Non-PIE (ET_EXEC) binaries always go through the kernel + patched PT_INTERP path.
When the replacement PT_INTERP overflows the original slot (e.g. a deeply nested binary like 5.0/python/bin/python3.11 needing ../../../lib/ld-linux-x86-64.so.2), bundle-libs appends the new string to the end of the file and rewrites the PT_INTERP program header to point at it. Kernel reads PT_INTERP by file offset, so the string does not need PT_LOAD coverage.
Packaging musl apps on glibc hosts
The host's libdrm, libgcc_s, libpthread, etc. are all glibc-linked, so bundle-libs can't use them directly. You need musl-built versions.
With Nix
export MUSL_LIBDRM=$(nix build nixpkgs#pkgsCross.musl64.libdrm.out --no-link --print-out-paths)/lib
export MUSL_GCC=$(nix eval --raw nixpkgs#pkgsCross.musl64.stdenv.cc.cc.lib)/x86_64-unknown-linux-musl/lib
onelf bundle-libs ./app --strict-libc \
--search-path "$MUSL_LIBDRM" \
--search-path "$MUSL_GCC"With a musl toolchain
If you built the app against a musl sysroot, point at that:
onelf bundle-libs ./app --strict-libc \
--search-path $MUSL_SYSROOT/usr/lib \
--search-path $MUSL_SYSROOT/libRecipe
[bundle]
search-paths = ["${MUSL_LIBDRM}", "${MUSL_GCC}"]
strict-libc = trueExport the env vars in your CI or direnv and onelf build does the rest.
Verifying the bundle is clean
After bundle-libs, watch for warnings:
warning: libfoo.so.1 links against Glibc libc but target is Musl;
this bundle may not work at runtimeWith --strict-libc those are skipped and listed under "Not found":
Not found (1)
libfoo.so.1 (needed by bin/app (libfoo.so.1 links against Glibc libc but target is Musl))Either way, the warning is your signal that you need a musl-built replacement on the --search-path.
Limitations
- We can't cross-libc arbitrary system calls. A binary that directly uses glibc-specific APIs (like
obstack_*) won't magically work on musl. - nss / nsswitch libraries can be fiddly. If your app does DNS or user lookups via dlopen'd nsswitch modules, you may need to bundle them too.