The Haskell Cabal and Cross Compilation
Once you want to tap into the vast ecosystem of haskell libraries, you will run into cabal and hackage in one way or the other (stack and stackage build upon cabal as well). Over the last few days we set up the Raspberry Pi, built the Raspbian SDK and the Haskell cross compiler. Today we will look at what cabal is, and how to use it for cross compilation.
Cabal, cabal-install, cabal and Hackage
The Common Architecture for Building Applications and Libraries consists of Cabal the library. Cabal the library contains the logic how to build a haskell package from a .cabal
file. cabal-install is the cabal package, that provides the cabal
command. Hackage finally is the haskell package repository.
As an end user you will mostly deal with cabal
the command line interface. This will also take care of downloading dependencies and building them as required.
Cross Compiling with cabal
cabal is not yet very cross compilation agnostic. As cross compilation has been mostly a niche, cross compiling packages with cabal needs some hand-holding.
By default cabal
will use a non-prefixed toolchain, which results in the library being compiled for the build machine. In the cross compilation setting, we want to compile for the host machine. That is the machine that will host the final binary, and not the machine that builds the binary.
Luckily cabal
provides the necessary arguments to pass in the toolchain we want to use (I’ve seen this first in the ghc-ios-scripts).
For our arm-linux-gnueabihf-ghc
we built yesterday we want to effectively call cabal
with the following arguments:
--builddir=dist/arm-linux-gnueabihf
to put any build product into a separate directory for each architecture. And
--with-ghc=arm-linux-gnueabihf-ghc
--with-ghc-pkg=arm-linux-gnueabihf-ghc-pkg
--with-gcc=arm-linux-gnueabihf-clang
--with-ld=arm-linux-gnueabihf-ld
to teach cabal about our toolchain. cabal may call hsc2hs
as well, passing
--hsc2hs-options=--cross-compile
when compiling ensures that hsc2hs
operates in cross compilation mode.
Finally cabal
may invoke configure
. Therefore we pass
--configure-option=--host=arm-linux-gnueabihf
as well, when running cabal configure
or cabal install
, so that configure scripts can rely on the proper --host
flag.
Passing all this by hand is truly annoying. Therefore let us create a wrapper script for this (again credit for this goes to ghc-ios-scripts, where I saw this approach first — we will use a slightly modified version here).
Let us create a script called cabal-wrapper
with the following content:
#!/bin/bash
name=${0##*/}
cmd=${name##*-}
target=${name%-*}
fcommon="--builddir=dist/${target}"
fcompile=" --with-ghc=${target}-ghc"
fcompile+=" --with-ghc-pkg=${target}-ghc-pkg"
fcompile+=" --with-gcc=${target}-clang"
fcompile+=" --with-ld=${target}-ld"
fcompile+=" --hsc2hs-options=--cross-compile"
fconfig="--disable-shared --configure-option=--host=${target}"
case $1 in
configure|install) flags="${fcommon} ${fcompile} ${fconfig}" ;;
build) flags="${fcommon} ${fcompile}" ;;
list|info|update) flags="" ;;
"") flags="" ;;
*) flags=$fcommon ;;
esac
exec $cmd $flags "$@"
Note: for simplicity we do not wrap cabal
’s new nix-style local build commands like new-build
, and new-install
.
Simply creating a symbolic link
ln -s cabal-wrapper arm-linux-gnueabihf-cabal
provides us with arm-linux-gnueabihf-cabal
which nicely fits in with the rest of the toolchain we built so far. And we can use arm-linux-gnueabihf-cabal
just like cabal
to build or install haskell packages to use with the cross compiler.
Be Aware of build-type: Custom
Cabal provides an escape hatch for to support custom build types, for when the build-type: Simple
is not sufficient. Unfortunately this requires the Setup.hs
to be built by cabal and run, and cabal default to using the ghc
is knows about to compile the Setup.hs
. This then leads to a Setup.hs
that is compiled with the cross compiling ghc (here: arm-linux-gnueabihf-ghc
), and can only be run on the host, but not on the build machine. Ideally cabal would be cross compilation aware and compile the Setup.hs
with the compiler that targets the build machine.
As quite a few packages use build-type: Custom
for the purpose of supporting doctest
or haddock
. Often times just rewriting build-type: Custom
to build-type: Simple
can make a package succeed to compile for cross compilation. (An alternative solution, that tries to respect the build-type: Custom
, can be found in the cabal-custom script, again from the ghc-ios-scripts)
Be Aware of Template Haskell
Template Haskell provides interesting challenges for cross compilation. For a long time Template Haskell was simply not an option for cross compilation. While there are evil splicer and ZeroTH, which try to work around Template Haskell, there has been no proper Template Haskell support. However GHCJS does support Template Haskell and is a cross compiler as well, and the same approach can be taken with GHC. Cross compiling and Template Haskell will the the main topic for next week.