Vendor Binaries with nix-ld and FHS Envs

Updated

September 12, 2026

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.2

Output (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 interpreter

Case 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' | head

Those 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-fhs

You 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-toolbox

Case 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 from ldd. Match arch.
  • Vendor installers that need /usr: buildFHSEnv or steam-run, not a fake /usr on the host.
  • Never a global LD_LIBRARY_PATH or a profile NIX_LD_LIBRARY_PATH.
  • autoPatchelfHook when 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 autoPatchelfHook later.

Try this

  1. On a NixOS VM, enable Case 1, download a random upstream rg static/dynamic release (not from nixpkgs), file it, and run it. If it is static, nix-ld is unused (fine). If dynamic, ldd it.
  2. ldd $(readlink -f $(which curl)) on a Nix curl — every line should already be a store path. nix-ld must not be involved.
  3. Enter the FHS env from Case 3, run ls /usr/bin | head and ldd /usr/bin/bash. Exit and confirm /usr/bin/bash on the host is the NixOS one (or absent), not the bubble.
  4. Add one library to nix-ld.libraries, switch, and nix why-depends /run/current-system pkgs.openssl to see that the system closure grew. That is the cost of a long list.