At TwoSix, we needed to build a GCC cross toolchain for the obscure OpenRisc architecture in a repeatable way, and we did it with Nix. But before I delve into how, let’s talk about…
Why OpenRisc?
To answer the question of why, we first need a brief history lesson…
Little known fact, but there is actually OpenRisc silicon in the wild. In fact, before RISC-V went mainstream, OpenRisc was one of the go to license-free ISAs, perhaps along with Lattice Semiconductor’s ISA in the LatticeMico32.
A bit of an aside, making one’s own ISA is pretty straightforward. Adding GCC, LLVM, binutils, glibc, and Linux kernel support are much much trickier. Both OpenRisc and LatticeMico have C toolchain software support for GCC, LLVM, and the Linux Kernel.
Reasons to include OpenRisc or LatticeMico IP in hard silicon before RISC-V went mainstream probably went something like this:
At [insert company name here], we need a PMC(power management controller) in our chip but can’t really be bothered to poke ARM for yet another CPU license. Furthermore, since the PMC has only one job and is effectively software-isolated from the rest of the system, we can afford to use a non-standard ISA.
Back to the why of OpenRisc, which basically boils down to the fact that a particular project at TwoSix makes use of the very popular Allwinner A64 chip. The A64 has an OpenRisc PMC that can boot a software payload packaged into U-Boot. Building this payload requires the OpenRisc toolchain.
Challenges of Building A GCC Cross Compiler
Building a GCC cross compiler can be tricky for a number of reasons.
- The cross compiler has to be built in a number of stages in the right order.
- The cross compiler has to be built with the right version of GCC as future versions may have meaningful differences such as problematic Werror flags set by the Linux distro.
- The cross compiler has to be built with the right libc, binutils, and Linux kernel headers, and unfortunately, FSF doesn’t maintain a compatibility matrix.
Enter Nix
I’ve been using Nix to achieve repeatable builds as Nix allows me to pin down the exact environment in which a build occurs. Nix is better than Docker as Nix forces nearly purely functional builds. That is, Nix does its best to ensure that a build script will always produce identical outputs. In particular Nix does the following:
- Requires a “sources” stage for sources to be used when building a package. The sources can be fetched or can be local. Nix hashes the sources and only proceeds with the build if the sha256 source hashes are what the Nix recipe(derivation) said they would be.
- Performs builds with repeatable input state. Nix goes so far as to perform derivation builds in an offline chroot environment. As many sources of variation as possible are removed such included time(Nix fixes time at build to the Unix Epoch). The one thing Nix can’t really control for are race conditions in multithreaded builds that could potentially affect build outcomes. I suppose Nix could enforce only one core is allowed to perform builds(but that could be unusably slow). Furthermore, if the build relies on sources of entropy such as variations in memory access times, Nix can’t do much to enforce repeatability there.
- Stores successful builds in hashed paths. For example, say I want to spin up a Nix environment with gcc, bison, and verilator on my path, I would invoke
nix-shell --pure -p gcc bison verilator, and my path might look something like the following(path redacted to save space):
$ nix-shell --pure -p gcc bison verilator [nix-shell:~/git/uboot-2024-pine64]$ echo $PATH /nix/store/a11996pij79myrp43myn4zycdlzh9jsw-gcc-wrapper-13.3.0/bin:/nix/store/mcmyjzgqhfq6ycm0hz2482f454gd7ipc-gcc-13.3.0/bin:/nix/store/cv5w5wlyhfx1246dk4mdigj6x33w8jm2-glibc-2.39-52-bin/bin:/nix/store/y8yv3c6bnz15aqadfbkgs30xq88qwskm-verilator-5.022/bin:/nix/store/84yg60swk80b04apprb1432kir41bvzj-coreutils-9.5/bin
- Nix uses hashed stored builds as a cache for future builds. If Nix sees a recipe for a build that matches a hash of a build it has already stored/performed in that past, Nix will simple fetch that build store. Likewise, Nix can be pointed to build stores on other machines. The default Nix installation points to the upstream nixpkgs build store, but this is merely a matter of convenience. In principle, Nix can be pointed to any build store.
With that in mind, I can substantiate my claim of Nix being better than Docker – mainly because I’ve had issues in the past where old Ubuntu Docker containers responsible for building a python package might start to fail once upstream PyPi upgrades it certs. Nix mitigates this by fetching all the resources needed to build a given package ahead of time so that Nix builds always will work offline. This has some implications:
- Many upstream packages such as those written in packages have been patched to work with Nix offline build strategy such as Rust’s Cargo, Haskell’s Cabal, and even pip for Python(I think). This usually involves Nix computing the dependencies of a Rust crate for example, and then generating a nix script that will fetch and hash them.
- A Nix build doesn’t have to be dockerized – although it can be! Nix, once it has performed an initial build of a package will never again need internet access to perform subsequent builds provided that the Nix store cache has not been deleted(i.e. – you didn’t manually invoke nix garbage collection).
A GCC Cross Toolchain Buildscript in Nix
With this in mind, I feel confident in saying I have truly converged upon a repeatable GCC Cross toolchain builder nix derivation package. This Nix derivation should succeed where others have failed!
There are many GCC build scripts online that no longer work because the host GCC version on a modern distro is to different from the GCC that the script originally target – but no problem for Nix! The following derivation freezes the version of NixPkgs used, and by extension, every single dependency used to build gcc(and any other tool used in the derivation) forever.
Here is that glorious build script. If you want to just use the resulting OpenRisc toolchain, create both files below and then invoke nix-shell. After that, you should be able to invoke or1k-linux-gcc.
Lastly, this script should be able to be modified to support other architecture by changing the target variable’s triple. You may also have to fiddle around with mach_opts.
or1k-toolchain.nix
{ pkgs ? import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/24.05.tar.gz";
}) {} }:
let
binutilsVersion = "2.36.1";
gccVersion = "11.2.0";
linuxKernelVersion = "6.6.30";
glibcVersion = "2.35";
target = "or1k-linux";
in
pkgs.stdenv.mkDerivation {
name = "or1k-toolchain";
version = "1.0";
srcs = [
( pkgs.fetchurl {
url = "http://ftpmirror.gnu.org/binutils/binutils-${binutilsVersion}.tar.gz";
sha256 = "sha256-5o7equtsqWh7bcuu3Rs3ZQa6rS1I3iaohfxatqy4Odo=";
})
( pkgs.fetchurl {
url = "http://ftpmirror.gnu.org/gcc/gcc-${gccVersion}/gcc-${gccVersion}.tar.gz";
sha256 = "sha256-8IN/G/gkSlzCO9lv9jZnEqeRz64B344lsTdpisom78E=";
})
( pkgs.fetchurl {
url = "https://www.kernel.org/pub/linux/kernel/v6.x/linux-${linuxKernelVersion}.tar.xz";
sha256 = "sha256-tmpbhjsPhmlEi3TKg71kGoVvFksplW5Tm7y1/e6rnMY=";
})
( pkgs.fetchurl {
url = "http://ftpmirror.gnu.org/glibc/glibc-${glibcVersion}.tar.xz";
sha256 = "sha256-USNzL2tnzNMZMF79OZlx1YWSEivMKmUYob0lEN0M9S4=";
})
];
buildInputs = [
pkgs.gcc
pkgs.wget
pkgs.which
pkgs.rsync
pkgs.gmp
pkgs.libmpc
pkgs.mpfr
pkgs.python3
pkgs.bison
pkgs.flex
];
# I believe the following prevents gcc from treating "-Werror=format-security"
# warnings as errors
hardeningDisable = [ "format" ];
sourceRoot = ".";
buildPhase = ''
echo $PWD
ls -lah .
# binutils
mkdir build-binutils
cd build-binutils
../binutils-${binutilsVersion}/configure \
--prefix=$out \
--with-sysroot=$out/sysroot \
--target=${target}
make -j$(nproc)
make install
cd ../linux-${linuxKernelVersion}
make ARCH=openrisc INSTALL_HDR_PATH=$out/sysroot/usr headers_install
cd ..
# gcc stage 1
mkdir build-gcc
cd build-gcc
../gcc-${gccVersion}/configure \
--target=${target} \
--prefix=$out \
--with-sysroot=$out/sysroot \
--enable-languages=c,c++ \
--disable-threads \
--disable-multilib \
--disable-libssp \
--disable-nls \
--disable-shared \
--with-gmp=${pkgs.gmp} \
--with-mpfr=${pkgs.mpfr} \
--with-mpc=${pkgs.libmpc}
make -j$(nproc) all-gcc
make install-gcc
cd ../
# build glibc headers
echo line 113
mkdir build-glibc
cd build-glibc
rm -rf *
echo "libc_cv_forced_unwind=yes" > config.cache
echo "libc_cv_c_cleanup=yes" >> config.cache
../glibc-${glibcVersion}/configure \
--host=${target} \
--prefix=/usr \
--with-headers=$out/sysroot/usr/include \
--config-cache \
--enable-add-ons=nptl \
--enable-kernel=${linuxKernelVersion}
make install_root=$out/sysroot install-headers
cd ..
# gcc stage 1.5
mkdir -p $out/${target}/include/gnu
touch $out/${target}/include/gnu/stubs.h
cd build-gcc
make -j$(nproc) all-target-libgcc
make install-target-libgcc
cd ..
# build glibc
export PATH=$out/bin:$PATH
echo line 138
cd build-glibc
rm -rf *
echo "libc_cv_forced_unwind=yes" > config.cache
echo "libc_cv_c_cleanup=yes" >> config.cache
mach_opts="-mcmov -mror -mrori -msfimm -mshftimm -msext -mhard-float"
export CC="${target}-gcc $mach_opts"
export AR="${target}-ar"
export RANLIB="${target}-ranlib"
export OBJCOPY="${target}-objcopy"
../glibc-${glibcVersion}/configure \
--host=${target} \
--prefix=/usr \
--libexecdir=/usr/lib/glibc \
--with-binutils=$out/bin \
--with-headers=$out/sysroot/usr/include \
--disable-werror \
--config-cache \
--enable-add-ons=nptl \
--enable-kernel=${linuxKernelVersion}
make -j$(nproc)
make install_root=$out/sysroot install
cd ..
# build glibc-final
cd build-gcc
rm -rf *
export CC="gcc"
export AR="ar"
export RANLIB="ranlib"
export OBJCOPY="objcopy"
../gcc-${gccVersion}/configure \
--with-gnu-ld \
--with-gnu-as \
--disable-nls \
--disable-libssp \
--with-multilib-list=mcmov,mhard-float \
--enable-languages=c,c++ \
--target=${target} \
--prefix=$out \
--with-sysroot=$out/sysroot
make -j$(nproc)
make install
cd ..
mkdir -p $out/${target}/lib
mkdir -p $out/sysroot/lib
rsync -a $out/${target}/lib/ $out/sysroot/lib
'';
meta = {
description = "Cross-compilation toolchain for OpenRISC architecture";
homepage = "https://openrisc.io";
license = pkgs.lib.licenses.gpl2;
maintainers = with pkgs.stdenv.lib.maintainers; [ ];
};
}
shell.nix
{ pkgs ? import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/24.05.tar.gz";
}) {} }:
let
or1k-toolchain = pkgs.callPackage ./or1k-toolchain.nix {};
in
pkgs.mkShell {
buildInputs = [
or1k-toolchain
pkgs.which
];
}


