Setting up NixOS to run RISC-V binaries

Since most RISC-V boards are quite slow, it is often useful to do initial development and testing on a fast x86-64 or ARM64 machine. QEMU user mode emulation has you covered here – it can run binaries for another CPU using emulation without setting up a complete VM.

Running QEMU user mode emulation by hand gets a bit tedious over time, but you can set up linux binfmt_misc to register QEMU as a handler for RISC-V binaries. In NixOS, you can enable this by adding this line to your system configuration:

boot.binfmt.emulatedSystems = [ "riscv64-linux" ];

Then you go from:

$ nix run nixpkgs#pkgsCross.riscv64.hello
error: unable to execute '/nix/store/2476cpnkgbl5cdhrln3mlbhl9dslad97-hello-riscv64-unknown-linux-gnu-2.12.2/bin/hello': Exec format error

to

$ nix run nixpkgs#pkgsCross.riscv64.hello
Hello, world!

after switching to the new system configuration.

Cross-compilation

You can easily create a cross-compiler with nixpkgs using the crossSystem option. The nicest way to do this is to create a flake that creates a cross-compilation environment as a development shell:

{
  description = "RISC-V cross-compiler";
 
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };
 
  outputs =
    {
      self,
      nixpkgs,
      flake-utils,
    }:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        cross = import nixpkgs {
          crossSystem = {
            config = "riscv64-unknown-linux-gnu";
            # Or if you want to build against MUSL:
            # config = "riscv64-unknown-linux-musl";
          };
          inherit system;
        };
      in
      {
        devShells.default =
          with cross; mkShell {
            # RISC-V dependencies if you need them.
            buildInputs = [ zlib ];
          };
      }
    );
}

You can then cross-compile your C or C++ programs in the development shell:

$ nix develop
$ riscv64-unknown-linux-gnu-gcc -o hello hello.c ; file hello ; ./hello
hello: ELF 64-bit LSB pie executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), dynamically linked, interpreter /nix/store/vggr947f2zwmzvnjv9ikkk5kcsqhpgml-glibc-riscv64-unknown-linux-gnu-2.40-66/lib/ld-linux-riscv64-lp64d.so.1, for GNU/Linux 4.15.0, not stripped
Hello world!

If you need any additional C/C++ dependencies, you can add them to buildInputs.