Vendor Binaries with nix-ld and FHS Envs
Vendor Binaries with nix-ld and FHS Envs
Nix binaries look for ld-linux and libssl.so under /nix/store. A vendor AppImage, a JetBrains extractor, or a curl | tar toolchain looks for /lib64/ld-linux-x86-64.so.2. The boring default is: programs.nix-ld for random ELF files, buildFHSEnv for programs that require an FHS tree, and never a global LD_LIBRARY_PATH.
Mental model
Three kinds of program live on a desk:
| Kind | Loader | Fix |
|---|---|---|
| Nix package | Store ld-linux + RUNPATH |
None. Already correct. |
| Vendor ELF | Hard-coded /lib64/ld-linux… |
nix-ld intercepts that path |
“Must see /usr/lib” installer |
FHS layout | buildFHSEnv / steam-run |
nix-ld installs a compatibility loader at the path vendor binaries request. That loader then finds libraries from a Nix-provided list (programs.nix-ld.libraries). The vendor file stays where you downloaded it; it does not enter /nix/store unless you package it.
buildFHSEnv builds a bubblewrapped /usr with the packages you listed. The process sees libc.so in the usual places. Use it when nix-ld is not enough (scripts that dlopen("/usr/lib/libfoo.so")).
vendor binary
requested interpreter: /lib64/ld-linux-x86-64.so.2
│
▼
nix-ld stub at that path
│
▼
real libraries from programs.nix-ld.libraries (store paths)
Worked examples
Case 1: Enable nix-ld on NixOS
Save as nix-ld.nix:
# nix-ld.nix
{ pkgs, ... }:
{
programs.nix-ld.enable = true;
programs.nix-ld.libraries = with pkgs; [
stdenv.cc.cc
zlib
openssl
curl
icu
nss
nspr
libxml2
libGL
xorg.libX11
];
}sudo nixos-rebuild switch
ls -l /lib64/ld-linux-x86-64.so.2Output (shape):
lrwxrwxrwx 1 root root … /lib64/ld-linux-x86-64.so.2 -> /nix/store/…-nix-ld-…/lib/ld-linux-x86-64.so.2
A vendor ./desk-toolbox that died with No such file or directory (missing interpreter) should now start — or fail on a named missing library, which you add to libraries.
On aarch64 the stub is /lib/ld-linux-aarch64.so.1 (not /lib64/ld-linux-x86-64.so.2). file ./desk-toolbox must match the host. An x86_64 AppImage on ARM is qemu, not nix-ld.
file ./desk-toolbox
readelf -l ./desk-toolbox | grep interpreterCase 2: See what is still missing
ldd ./desk-toolbox | grep 'not found'Output (shape):
libfoo.so.1 => not found
Find it in nixpkgs, add pkgs.foo to programs.nix-ld.libraries, switch, retry. Do not export LD_LIBRARY_PATH=/nix/store/…/lib.
NIX_LD / NIX_LD_LIBRARY_PATH are what the stub loader consults. nix-ld sets them from programs.nix-ld.libraries via environment.sessionVariables. A user export of NIX_LD_LIBRARY_PATH in .bashrc overrides the module list and is the same class of bug as LD_LIBRARY_PATH. Keep it in the module.
echo "$NIX_LD"
echo "$NIX_LD_LIBRARY_PATH" | tr ':' '\n' | headThose paths must be store paths from programs.nix-ld.libraries, not ~/.local/lib.
Case 3: buildFHSEnv for an installer that shells out
Save as fhs.nix:
# fhs.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.buildFHSEnv {
name = "desk-fhs";
targetPkgs = pkgs: with pkgs; [
stdenv.cc.cc
zlib
openssl
curl
bash
coreutils
gnused
gawk
];
runScript = "bash";
}nix-build fhs.nix
./result/bin/desk-fhsYou are now in a shell where /usr/bin/env and libc.so.6 exist in FHS locations. Run the vendor installer inside this shell. Exit, and the host is unchanged.
steam-run is a pre-built FHS env in nixpkgs for the same trick:
nix run nixpkgs#steam-run -- ./desk-toolboxCase 4: Package the vendor blob only when you must
If the team will run this for years, write a derivation with autoPatchelfHook instead of asking every laptop to enable nix-ld for it. That is the packaging part. nix-ld is the escape hatch for binaries you will not package this week.
# vendor-patchelf.nix fragment
{
nativeBuildInputs = [ autoPatchelfHook ];
buildInputs = [ stdenv.cc.cc zlib openssl ];
}autoPatchelfHook rewrites the interpreter and RUNPATH in the store copy. The original download on disk is unchanged. That is the opposite of nix-ld (loader stays /lib64, libraries from the module). Pick one per binary.
Case 5: Foreign Linux
programs.nix-ld is a NixOS module. On Ubuntu, the equivalent is the nix-ld package plus a loader in /lib64, which fights the distro. Prefer buildFHSEnv / steam-run / nix develop on foreign distros, or run the vendor app in a container. Do not overwrite Ubuntu’s ld-linux.
The trap
The trap is export LD_LIBRARY_PATH=$HOME/libs so one AppImage starts. Every Nix binary in that terminal now prefers $HOME/libs/libssl.so. The next day, curl speaks a surprise TLS stack.
The other trap is adding pkgs.linuxPackages.kernel to nix-ld.libraries “to have everything.” Keep the list short. Add libraries when ldd names them.
A third: NIX_LD_LIBRARY_PATH in Home Manager home.sessionVariables fighting programs.nix-ld. A fourth: nix-ld and autoPatchelfHook on the same blob so RUNPATH and the stub disagree.
The boring rule
- Nix packages stay Nix packages. Do not wrap them in FHS.
- Vendor ELFs on NixOS:
programs.nix-ld+ a short library list fromldd. Match arch. - Vendor installers that need
/usr:buildFHSEnvorsteam-run, not a fake/usron the host. - Never a global
LD_LIBRARY_PATHor a profileNIX_LD_LIBRARY_PATH. autoPatchelfHookwhen the blob is worth a derivation; nix-ld is the week-one hatch. One or the other.- If you run it daily, package it with
autoPatchelfHooklater.
Try this
- On a NixOS VM, enable Case 1, download a random upstream
rgstatic/dynamic release (not from nixpkgs),fileit, and run it. If it is static, nix-ld is unused (fine). If dynamic,lddit. ldd $(readlink -f $(which curl))on a Nix curl — every line should already be a store path. nix-ld must not be involved.- Enter the FHS env from Case 3, run
ls /usr/bin | headandldd /usr/bin/bash. Exit and confirm/usr/bin/bashon the host is the NixOS one (or absent), not the bubble. - Add one library to
nix-ld.libraries, switch, andnix why-depends /run/current-system pkgs.opensslto see that the system closure grew. That is the cost of a long list.