- Published on June 5, 2020 by Tuan Le
- Tags:
Table of contents
Goals
Our goal is to seamlessly build any Haskell programs to run on a Raspberry Pi, whatever the building platform is: a Raspberry Pi, a classic Desktop computer or any other platform that can run GHC.
Haskell.nix is still evolving but should be quite stable at the time of writing. I’ll try to keep this content up-to-date. Also some of these instructions don’t necessarily reflect the documentation: I’ve added some specific parts to suit my needs.
Objectives
Reading through this post, we will learn to:
- Produce a Haskell program binary that can be executed on a Raspberry Pi from any building computer
- Retain the capability of compiling and running the same Haskell program on the building computer (tests…)
The instructions in this post should be enough to achieve our objectives. The sample code used here is available on one of my GitHub repsitory.
Pre-requisites
You only need basic knowledges in Haskell, GHC and Nix to go through this post. More specifically, you need to:
- Understand how a Stack or Cabal Haskell project basically works, in case something goes wrong
- Know how to use the basic nix commands such as
nix-shell
andnix build
, in case something goes wrong
To actually build our programs, we will need the following:
- NixOS
- An Internet connection (to download the required packages and binaries)
- An existing Haskell Stack or Cabal project without external libraries (those will be handled in another post)
To test our cross-compiled program, we will need:
- A Raspberry Pi
- QEMU (for the targeted platform, in our case
qemu-arm
)
Overall idea
The compilation process will actually be performed by Nix. The Nix package manager will allow us to build our programs in a consistent and reproducible way using Nix code.
We will also use Haskell.nix to download and run GHC to compile our program for us. Haskell.nix is a Nix build architecture that can automatically convert Stack or Cabal project and its dependencies into Nix code. This code can then be used to produce a Haskell binary, including a cross-compiled one targeting the Raspberry Pi architectures.
There are two ARM architectures of interest for the Raspberry Pis:
armv6l
: Raspberry Pi, Raspberry Pi Zero. Also works with the Raspberry Pi 2, 3…armv7l
: Raspberry Pi 2, 3…
Choosing armv6l
should work for most people.
We shall configure Nix to achieve the followings:
- Build the Haskell program to run on the building computer, allowing us to do tests and validation
- Build the Haskell program to run on the Raspberry Pi
A small note however on cross-compilation with Nix: the cross-compiled program will be built to actually run on the target platform with Nix. Most notably, the program will be hardcoded with a Nix LD interpreter. If you target a non-Nix OS, you will need to patch the generated program to use the default program interpreter. Fortunately, this task can also be automated with Nix code.
Step-by-step
Create a common configuration file
First, create the nix/pkgs.nix
file with the following content:
let
# (1)
haskellNix = import (builtins.fetchTarball https://github.com/input-output-hk/haskell.nix/archive/21a6d6090a64ef5956c9625bcf00a15045d65042.tar.gz) {};
# (2)
nixpkgsSrc = haskellNix.sources.nixpkgs-2003;
nixpkgsArgs = haskellNix.nixpkgsArgs;
# (3)
native = import nixpkgsSrc nixpkgsArgs;
crossRpi = import nixpkgsSrc (nixpkgsArgs // {
crossSystem = native.lib.systems.examples.raspberryPi;
});
crossArmv7l = import nixpkgsSrc (nixpkgsArgs // {
crossSystem = native.lib.systems.examples.raspberryPi;
});
in {
# (4)
inherit haskellNix;
inherit nixpkgsSrc nixpkgsArgs;
inherit native crossRpi crossArmv7l;
}
This file serves different purposes:
- Centralize the version of Haskell.nix. Here, we are using a pinned version for this post. But you can use the lastest Git version of Haskell.nix with the following URL:
https://github.com/input-output-hk/haskell.nix/archive/master.tar.gz
. - Centralize the version of Nixpkgs. We will especially use the pinned version by Haskell.nix, in order to use the dedicated binary cache. We will use the 20.03 version of Nixpkgs.
- Define the cross-compilation configuration to each target architectures. Here, we are targeting the Raspberry Pi
(
armv6l
) and some of the latest Raspberry Pis (armv7l
). - Export those configurations.
You can try and target a new (GHC-compatible) platform by defining in (3) a Nix cross-compilation configuration:
import nixpkgsSrc (nixpkgsArgs // {
crossCustom = # Define your target platform below
crossSystem = anotherPlatform;
});
Once our basic dependencies have been defined, we can go on and define our Haskell project.
Use Haskell.nix for compilation
Create the default.nix
file with the following content:
let
pkgsNix = import ./nix/pkgs.nix;
in
# (1)
{ pkgs ? pkgsNix.native
}:
# (2)
-nix.project {
pkgs.haskellsrc = pkgs.haskell-nix.haskellLib.cleanGit { name = "cross-haskell-app"; src = ./.; };
}
This file defines our current Haskell project (2) in terms of the current environment (1).
In our example, the environment is the building computer by default. Being a parameter, it can be changed, say to a cross-compiling environment.
As it is, we can already use Haskell.nix to build and run our program. However, we are targeting the Raspberry Pi, so there is still one bit of configuration to make.
Configure cross-compilation
Next, create the release.nix
file with the following content:
let
# (1)
pkgsNix = import ./nix/pkgs.nix;
pkgsNative = pkgsNix.native;
pkgsRaspberryPi = pkgsNix.crossRpi;
pkgsArmv7l = pkgsNix.crossArmv7l;
# (2)
hsApp = import ./default.nix;
appNative = hsApp { pkgs = pkgsNative; };
appCrossRaspberryPi = hsApp { pkgs = pkgsRaspberryPi; };
appCrossArmv7l = hsApp { pkgs = pkgsArmv7l; };
in {
# (3)
native = appNative.cross-haskell-app.components.exes.cross-haskell-app-exe;
raspberry-pi = appCrossRaspberryPi.cross-haskell-app.components.exes.cross-haskell-app-exe;
armv7l = appCrossArmv7l.cross-haskell-app.components.exes.cross-haskell-app-exe;
}
There are several sections defined here:
- We start by defining the different building environment we are interested in.
pkgsNative
represents our building computer.pkgsRaspberryPi
represents a Raspberry Pi (armv6l
) cross-compiling environment.pkgsArmv7l
represents a Raspberry Pi (armv7l
) cross-compiling environment.
- We define our Haskell project to compile for each building environment.
- We define each Haskell executable to build per environment. In our example, we only have a single executable in our project.
If you have added another target platform, you have to add the target environment in (1) and the corresponding Haskell project in (2).
pkgsCustom = pkgsNix.crossCustom;
{ pkgs = pkgsCustom; }; appCrossCustom = hsApp
And then, you have to declare in (3) the buildable executables:
-haskell-app.components.exes.cross-haskell-app-exe; custom = appCrossCustom.cross
Build / Cross-compile
To build a Haskell executable, we now have different options:
- Build for the current platform:
nix build -f release.nix native
- Cross-compile for the Raspberry Pi (
armv6l
):nix build -f release.nix raspberry-pi
- Cross-compile for the Raspberry Pi (
armv7l
):nix build -f release.nix armv7l
And to cross-compile to your custom target platform: nix build -f release.nix custom
The compiled executable will then be available in result/bin
.
result
├── bin
│ └── cross-haskell-app-exe
└── share
└── doc
└── arm-linux-ghc-8.6.5
└── cross-haskell-app-0.1.0.0
└── LICENSE
Run the cross-compiled program (QEMU)
Running the compiled program is as just easy as invoking it when native building. When cross-compiling though, the program has to be invoked through the appropriate qemu program. For our Raspberry Pi example, invoking the program can be achieved with the following command:
qemu-arm result/bin/cross-haskell-app-exe
Targeting another platform will require the appropriate emulator.
Patch for non-Nix OSes
As mentionned earlier, compiling (or cross-compiling) a program will generate a new executable targeting a Nix(OS) environment. It is however possible to target another environment, given the right informations written into the generated program.
In our case, the only issue is that the program interpreter for our targeted ARM system is not located wherever Nix
would expect. For one of my personal project with a heavily customized Linux distribution, it was located at
/lib/ld-linux-armhf.so.3
. We must then patch our program to use that path for the interpreter. This can be done with
Nix with the following configuration:
let
pkgsNix = import ./nix/pkgs.nix;
pkgsNative = pkgsNix.native;
pkgsRaspberryPi = pkgsNix.crossRpi;
hsApp = import ./default.nix;
appCrossRaspberryPi = hsApp { pkgs = pkgsRaspberryPi; };
# (1)
patchForNotNixLinux = {app, name}:
"${app.name}-patched" { } ''
pkgsNative.runCommand set -eu
cp ${app}/bin/${name} $out
chmod +w $out
${pkgsNative.patchelf}/bin/patchelf --set-interpreter /lib/ld-linux-armhf.so.3 --set-rpath /lib:/usr/lib $out
chmod -w $out
'';
in {
# (2)
raspberry-pi-patched = patchForNotNixLinux {
app = appCrossRaspberryPi.cross-haskell-app.components.exes.cross-haskell-app-exe;
name ="cross-haskell-app-exe";
};
}
In the previous code snippet:
- This part just instructs Nix how to build a patched program by running a predefined script. This script would call
patchelf
to change the interpreter path to match the interpreter in our target OS, and then would also change the default path to find any library our program needs. - This part indicates how to build the patched executable (with
patchForNotNixLinux
) from an existing executable (appCrossRaspberryPi.cross-haskell-app.components.exes.cross-haskell-app-exe
).
Shell (bonus)
A development shell is available using the following command:
nix-shell -A shellFor
It is especially useful to run an Haskell executable or tests.
Conclusion
We have used Haskell.nix to cross-compile our Haskell source code for the Raspberry Pi. The program can be run on a board running NixOS, or any other distribution when patched.
An example program can be found on one of my GitHub repository.
Next, we can focus on using external libraries, especially libraries that can only run natively on the Raspberry Pi.