Yes, Nixpkgs 23.05 still works* on CentOS 6
Published on
During recent years, I’ve encountered more times than I should with HPC systems stuck on CentOS 6 or even CentOS 5. One of them in a corporation, three of them in academic settings. Installing software as a normal user on them without root have always been a pain due to how ancient these systems are, and it didn’t help that some of these systems are offline for security requirements.
It occurred to me that maybe I can stretch Nixpkgs’s support to these old, offline systems—and it turned out to be easier than I imagined. It did require some thought and wrestling, but in the end I’ve succeeded bootstrapping Nixpkgs for the x86_64-unknown-linux-musl
system up to a fully functional Emacs 28.2 on an offline CentOS 6 machine without root.
“Why musl, and why bootstrap from source?” you might ask. To begin with, I have three special constraints imposed here:
- CentOS 6 comes with kernel 2.6.32.
- HPC users usually don’t have root access.
- Some HPC’s are offline.
Consequently, through the logic of this flow chart, I arrived at the conclusion that the easiest path is to bootstrap Nixpkgs for native musl libc on the offline machine.
Some explanations:
- Modern versions of glibc requires at least kernel 3.2 to function properly, but Nixpkgs comes with a fairly recent version of glibc.
- The default installation method of Nix requires the creation of
/nix
, which is not an option for unprivileged HPC users. nix-user-chroot
, the alternative way to install Nix without root permissions, relies on the user namespaces feature that isn’t available until kernel 3.8. It wasn’t until RHEL 8 that this feature was enabled by default.- Thus, the only 1 way to use Nix is to use a custom store path that’s not
/nix
and thus writable by me as an unprivileged user. Using a custom store path means that I have to bootstrap Nixpkgs from source, because the official binary cache is built for/nix
. Bootstrapping requires quite a lot of compute, but on HPC systems this is less of an issue. - The musl libc specifically states that it supports kernel 2.6 or later, so it’s more suitable for our usecase than the glibc in Nixpkgs.
- Finally, as the machine is offline, I must prefetch and transfer the “source closure” required to build the derivations I want, hence the “FOD prefetch” task.
The rest of this post documents how I bootstrapped Nixpkgs for localSystem = "x86_64-unknown-linux-musl"
up to the emacs
package on an offline machine running CentOS 6 as an unprivileged user with a custom store location at /home/kotatsu/.nix/
. From now on, I’ll refer to the target system as “native musl” to avoid spelling out the host triple (quadruple?) repeatedly.
Prefetch the source closure for offline bootstrap
The information on how to prefetch source tarballs for offline build is pretty sparse on the Internet, and all I could find was this thread from 2021 asking for “source closure”, hence the heading of this section. The gist is to list all the derivations required for building a particular derivation (which was Emacs in my case), filter the list to retain all the fixed-output derivations, and finally build all the fixed-output derivations from a machine with access to the Internet.
In my case, there’s another problem—I can’t take advantage of the binary cache for fetching the source closure, because
- I’m targeting a custom store location, and also because
- I’m targeting native musl,
which are both not cached officially. Trying to fetch anything that requires pkgs.fetchgit
or even just pkgs.fetchurl
from Nixpkgs results in bootstrapping the whole stdenv up to git and curl so that the fetchers can be run, which I do not want to do on my local systems, because it takes way too long to bootstrap on my local systems compared to the HPC system.
My approach to this problem is to write some dirty Python scripts that perform these steps. Effectively, the scripts fetch the fixed-output derivations using the x86_64-unknown-linux
(-glibc) Nixpkgs powered by the official binary cache, and then transfer the resulting store paths from the /nix
store to the custom store.
- Evaluate the installable I want to build (e.g. Emacs) to get the closure of all dependencies by
nix path-info --recursive --derivation <installable>
. This step is done on the custom store with native musl, but no bootstrapping is required because I only evaluate, not build, the derivations. - Get the JSON representation of each derivation
nix derivation show /path/to/the/derivation.drv^*
to determine whether it’s a fixed-output derivation that should be prefetched. If a derivation is fixed-output, record the derivation. This step is also done on the custom store. - On the
/nix
store, recreate and build the fixed-output derivations. Because this is done on the/nix
store with glibc, even the ones that require bootstrapping many things, such asbuildCargoTarball
that requires building Cargo, can be built with ease thanks to the binary cache. - Copy each store path from the
/nix
store back to the custom store bynix store add-file
ornix store add-path
depending on whether the output hash mode of the fixed-output derivation is flat or recursive, respectively.
The store paths copied into the custom store in the final step can then be copied to a local binary cache store by nix copy --to file:///tmp/binary-cache <store paths>
and transferred to the offline machine.
In the step 2 above, how do I preserve the information to recreate the fixed-output derivations themselves on a different instance of Nixpkgs targeting glibc? Well, I made some modifications to Nixpkgs to preserve the function arguments to each fetch*
fetcher function, and read those arguments from the JSON representation of each derivation in my Python scripts. For example, in the fetchgit
function I added these “metadata attributes” such as fod-fetch-variant
and fod-fetch-inputs
.
{ url, rev ? "HEAD", md5 ? "", sha256 ? "",
# More function inputs omitted here...
}:
.mkDerivation {
stdenvNoCC# Preserve function name (fetchgit) and inputs for the script
# to recreate the derivation.
fod-fetch-variant = "fetchgit";
fod-fetch-inputs = builtins.toJSON {
inherit url rev sha256 hash
All other inputs preserved here...
# ;
};
}
I’m not sure if this method of injecting stuff to the derivations is the best solution, but this got me as far as building Emacs without problems.
Fix the build failures for native musl on kernel 2.6
After prefetching and transferring the source closure to the offline HPC, I built the derivation for musl by nix build
. To build Emacs, I had to fix or workaround the following build issues. In practice, this task is no different from packaging software with Nix for the conventional glibc platform.
- Some tests in
coreutils
fail due togetrandom()
being unavailable on kernel 2.6. I disabled those specific tests. libgcrypt
fails to build due to the lack ofgetrandom()
again. Luckily, it provides an--enable-random=linux
flag for enabling a fallback mechanism that uses/dev/random
instead.m17n_db
fails to find the “charmaps” during its build, because while only glibc exposes the charmaps, Nixpkgs specifies a path under${stdenv.cc.libc}
, which is musl libc in my native musl case. I edited it to unpack and use charmaps from the glibc source instead.- Some tests in
libseccomp
fail for reasons beyond my debugging skills. I disabled those specific tests. - One of the tests in
gnutls
hangs for more than an hour. I disabled all tests ongnutls
altogether by settingdoCheck = false
because I was lazy. systemd-minimal
fails to apply a patch for reasons beyond my debugging skills. As I do not need SystemD support for dbus on the HPC, I disabled the feature.
And that’s all I did. Ignoring the failed attempts, the whole build process completes in a few hours on my test VM with 6 cores of an i7-8700 allocated.
Is this practical?
Yes, the setup is practical. Here’s a screenshot of the Emacs 28.2 bootstrapped from source.

This copy of Emacs is built with all the optional dependencies such as freetype, harfbuzz, and libgccjit for native compilation, without the need to learn to configure and build each of the dependencies and their transitive dependencies. Before this post, I used to “bootstrap” software from scratch by referring to the BLFS book such as this page for Emacs on HPC systems, but the upgradability of software installed in this ad-hoc way is a mess. Using Nix makes the process of bootstrapping and patching software much less tedious, and it comes with the benefits of being reproducible and easily transferrable.
Other alternatives to my approach
There are some alternatives that were either tried, discarded, or overlooked when I did this post. The alternatives for the general task “installing new software on old, offline HPC” are listed here. If you went down any of the rabbit holes in the list, please write about your experience and let me know!
Build on an online machine to avoid my prefetch drama. This is preferable if powerful yet online machines are available, but I don’t.
Patch Nixpkgs to use an older glibc. I think this is more painful than using musl, because it’s equivalent to maintaining a different set of stdenv bootstrap outside upstream Nixpkgs. However, someone on NixOS discourse actually did it for the nix-bundle project.
Use Nixpkgs with modern glibc on old kernel anyways. I tried this, and it did not build—see the caveats section.
Use Nixpkgs’
pkgMusl
instead of instantiating Nixpkgs forx86_64-unknown-linux-musl
. I tried this, and it did not work, becausepkgsMusl
means that while thecrossSystem
is musl, thelocalSystem
is still glibc. Building the glibc stdenv fails as per the previous item.Use a fake HTTP/HTTPS/FTP proxy to intercept all outgoing requests from Nix on the offline machine, and then transfer those requests to another machine for fetching.
This is potentially a great alternative to my prefetching scripts, but I don’t know if there’re some software for this, and I have a truckload of questions on the idea. What if Nix makes more requests after the first failing ones? Do I have to do multiple passes back and forth until all required files are on the offline machine? Will the fake proxy work for Git and CVS dependencies?
If you come up with a proxy-based solution, I’d like to hear from you.
Use conda. The idea of using conda has been disturbing to me as a NixOS user, since conda relies on the FHS system, which is why I overlooked this easy option. As of November 2023, conda-forge still supports CentOS 6, even though some packages are built for CentOS 7 or above these days. If you just want to install Emacs, I think conda would suffice. It seems like CentOS 6 is unsupported by versions beyond 2020.11, as per their documentation.
Use Homebrew. I used Homebrew a few years ago on HPC systems, but as for 2023 its requirements listed for Linux specifically mentions kernel 3.2, so it likely won’t work.
Use a Gentoo Prefix. On Gentoo wiki, there’s a list of tested installations contributed by users of Gentoo Prefix, but the last entry with kernel 2.6.32 was from 2017.
Use Guix. I have never used it before. I assume that it’s tied to glibc, but I have trouble finding information about this due to my unfamiliarity with it.
Caveats
These are some caveats that didn’t end up in the main prose.
Modern glibc on kernel 2.6?
What happens when glibc is built on kernel 2.6, anyways? Well, the dynamic linker fails to load libc.so
:
mkdir -p -- /home/kotatsu/.nix/store/dg8d4rfn2ykhnf32a00hjgkw8j95db87-glibc-2.38-23/lib/locale
C.UTF-8.../tmp/nix-build-glibc-2.38-23.drv-0/build/locale/localedef: error while loading shared libraries: libc.so.
make[2]: *** [Makefile:475: install-archive-C.UTF-8/UTF-8] Error 127
make[2]: Leaving directory '/tmp/nix-build-glibc-2.38-23.drv-0/glibc-2.38/localedata'
make[1]: *** [Makefile:749: localedata/install-locales] Error 2
make[1]: Leaving directory '/tmp/nix-build-glibc-2.38-23.drv-0/glibc-2.38' make: *** [Makefile:9: localedata/install-locales] Error 2
Running strace /path/to/localedef
reveals the cause. The dynamic linker from modern glibc somehow uses AT_EMPTY_PATH
, which is not available on kernel 2.6.32. Glibc really meant it when they dropped support for kernel under version 3.2 ;)
newfstatat(3, "", 0x7fffffffa780, AT_EMPTY_PATH) = -1 EINVAL (Invalid argument)
I only learned about the Nix HPC channel at #hpc:nixos.org
after going through the trouble of new glibc on an old kernel, and there was a report of this exact EINVAL
problem from November 2022. Oops.
nix-shell
builds glibc on native musl
nix-shell
does not work by default, because it tries to build glibc stdenv, because it wants bash from <nixpkgs>
, which is the glibc version by default.
To make nix-shell
work, specify a musl-based shell using the NIX_BUILD_SHELL
environment variable. I haven’t found a way to do the same thing for nix develop
, but I think it can be done by patching Nixpkgs to use prefer musl over glibc when running on Linux.
Other caveats
- The
x86_64-unknown-linux-musl
system is not exposed by the Nixpkgs flake underlegacyPackages
. The only way to get an instance of Nixpkgs with such system seems to beimport nixpkgs { localSystem = "..."; }
. - The
git
command on CentOS 6 is too old for Nix to properly function. Thus, before bootstrapping a fresh copy ofgit
from Nixpkgs, I had to refer to flakes aspath:/path/to/flake#package
to avoid errors.
There’s another tool called
proot
that intercepts system calls usingptrace()
to implement the functionalities ofchroot
, but in practice I foundproot
’s overhead to be problematic.↩︎