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 *C*ommon *A*rchitecture for *B*uilding *A*pplications and *L*ibraries 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 "[email protected]"

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.