Cross Compiling Yesod to Raspberry Pi

As we have seen we can now cross compile Template Haskell. The Yesod Web Framework is one of many haskell web frameworks. By default it makes use of shakespeare templates, which happen to use Template Haskell.

This is supposed to be a case study of cross compiling haskell packages to the Raspberry Pi. We will study the issues that can arise with this rather recent development and how to mitigate them. It will involve updating packages to make use of the new Template Haskell facilities and lifting build constraints.

The Sample Yesod Application

The application we are trying to compile is a rather simple one from the Yesod book. You can follow along with the source from the cross-compiling-yesod subfolder in the zw3rk/medium repository.

git clone https://github.com/zw3rk/medium.git
cd medium/cross-compiling-yesod

The src/Main.hs looks as follows

{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE QuasiQuotes           #-}
{-# LANGUAGE TemplateHaskell       #-}
{-# LANGUAGE TypeFamilies          #-}
import           Yesod
data HelloWorld = HelloWorld
mkYesod "HelloWorld" [parseRoutes|
/ HomeR GET
/page1 Page1R GET
/page2 Page2R GET
|]
instance Yesod HelloWorld
getHomeR :: Handler Html
getHomeR  = defaultLayout
            [whamlet|Hello World! <a href=@{Page1R}>Go to page 1!|]
getPage1R = defaultLayout
            [whamlet|Page 1 <a href=@{Page2R}>Go to page 2!|]
getPage2R = defaultLayout
            [whamlet|Page 2 <a href=@{HomeR}>Go home!|]
main :: IO ()
main = warp 3000 HelloWorld

This small example uses Template Haskell with two custom QuasiQuotes parseRoutes and whamlet.

Cross Compiling to Raspberry Pi

We will use the The Haskell Cross Compilation toolchain with Raspbery Pi Slave process as set up in Cross Compiling Template Haskell post.

Let’s start simple and see how far we get by installing the dependencies:

arm-linux-gnueabihf-cabal install --dependencies-only --allow-newer

We need the --allow-newer as we are building with a development compiler.

Build-Type: Custom

After a little while this will fail with

Failed to install entropy-0.3.7
Build log ( ~/.cabal/logs/entropy-0.3.7.log ):
cabal: Entering directory 'entropy-0.3.7'
[1 of 1] Compiling Main             ( entropy-0.3.7/dist/arm-linux-gnueabihf/setup/setup.hs, entropy-0.3.7/dist/arm-linux-gnueabihf/setup/Main.o )
Linking entropy-0.3.7/dist/arm-linux-gnueabihf/setup/setup ...
entropy-0.3.7/dist/arm-linux-gnueabihf/setup/setup: entropy-0.3.7/dist/arm-linux-gnueabihf/setup/setup: cannot execute binary file
cabal: Leaving directory 'entropy-0.3.7'

for entropy-0.3.7 and stm-chans-3.0.0.4. This is the result of built-type: custom as discussed in the The Haskell Cabal and Cross Compilation post.

A quick inspection of entropy/Setup.hs reveals that the setup is required for haddock and detecting HAVE_RDRAND. It is probably ok to try and compile this without the custom setup. stm-chans/Setup.hs does some haddock setup as well, which we do not need right now.

cabal get entropy
cd entropy-*
sed -i.bak -E 's/([Bb]uild-[Tt]ype:[ ]+)Custom/\1Simple/g' *.cabal
arm-linux-gnueabihf-cabal install --allow-newer
cabal get stm-chans
cd stm-chans-*
sed -i.bak -E 's/([Bb]uild-[Tt]ype:[ ]+)Custom/\1Simple/g' *.cabal
arm-linux-gnueabihf-cabal install --allow-newer

hsc2hs: unsupported directive in cross compilation

We will also see the zlib package to fail with:

Configuring zlib-0.6.1.2...
Building zlib-0.6.1.2...
Preprocessing library zlib-0.6.1.2...
Codec/Compression/Zlib/Stream.hsc:1023
   directive const_str cannot be handled in cross-compilation mode
cabal: Leaving directory 'zlib-0.6.1.2'

The GHC user guide section for the hsc2hs utility lists const_str as not supported in cross-compilation mode as well.

Reid Barton kindly pointed out #9689, which provides a proper patch to zlib, which is much better than this hack to enable const_str in hsc2hs.

Sadly though it looks like the patch has never made it into zlib; I’ve opened pull/#14 on behalf of the original authors.

git clone https://github.com/mobilehaskell/zlib
(cd zlib && arm-linux-gnueabihf-cabal install --allow-newer)

Should make the zlib package available.

Requires External Interpreter

A few other packages (th-lift-instances, math-functions, will have failed with

Failed to install th-lift-instances-0.1.11
Build log ( ~/.cabal/logs/th-lift-instances-0.1.11.log ):
cabal: Entering directory 'th-lift-instances-0.1.11'
Configuring th-lift-instances-0.1.11...
Building th-lift-instances-0.1.11...
Preprocessing library th-lift-instances-0.1.11...
[1 of 1] Compiling Instances.TH.Lift ( src/Instances/TH/Lift.hs, dist/arm-linux-gnueabihf/build/Instances/TH/Lift.o )
ghc: this operation requires -fexternal-interpreter
cabal: Leaving directory 'th-lift-instances-0.1.11'

This indicates some Template Haskell compilation is needed. With GHCSlave started on the Raspberry Pi at 10.0.1.1, we retry this with

arm-linux-gnueabihf-cabal install --allow-newer \
   --ghc-options="-fexternal-interpreter \
                  -pgmi $HOME/.cabal/bin/iserv-proxy \
                  -opti10.0.1.1 -opti5000"

Requires File or Process IO

Next the shakespeare package will fail with

Preprocessing library shakespeare-2.0.13...
[ 1 of 18] Compiling Text.IndentToBrace ( Text/IndentToBrace.hs, dist/arm-linux-gnueabihf/build/Text/IndentToBrace.o )
[ 2 of 18] Compiling Text.MkSizeType  ( Text/MkSizeType.hs, dist/arm-linux-gnueabihf/build/Text/MkSizeType.o )
Text/MkSizeType.hs:7:1: warning: [-Wunused-imports]
    The import of ‘Language.Haskell.TH’ is redundant
      except perhaps to import instances from ‘Language.Haskell.TH’
    To import instances alone, use: import Language.Haskell.TH()
  |
7 | import Language.Haskell.TH (conT)
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[ 3 of 18] Compiling Text.Shakespeare.Base ( Text/Shakespeare/Base.hs, dist/arm-linux-gnueabihf/build/Text/Shakespeare/Base.o )
[ 4 of 18] Compiling Text.Shakespeare ( Text/Shakespeare.hs, dist/arm-linux-gnueabihf/build/Text/Shakespeare.o )
Text/Shakespeare.hs:237:24: error:
    Ambiguous occurrence ‘readProcessWithExitCode’
    It could refer to either
       ‘Language.Haskell.TH.Syntax.readProcessWithExitCode’,
    imported from
       ‘Language.Haskell.TH.Syntax’ at Text/Shakespeare.hs:43:1-33
    or ‘System.Process.readProcessWithExitCode’,
    imported from ‘System.Process’ at Text/Shakespeare.hs:63:24-46
    |
237 |   (ex, output, err) <- readProcessWithExitCode cmd args input
    |                        ^^^^^^^^^^^^^^^^^^^^^^^
Text/Shakespeare.hs:468:23: error:
    Ambiguous occurrence ‘getModificationTime’
    It could refer to either
       ‘Language.Haskell.TH.Syntax.getModificationTime’,
    imported from
       ‘Language.Haskell.TH.Syntax’ at Text/Shakespeare.hs:43:1-33
    or ‘System.Directory.getModificationTime’,
    imported from ‘System.Directory’ at Text/Shakespeare.hs:54:26-44
    |
468 |     mtime <- qRunIO $ getModificationTime fp
    |                       ^^^^^^^^^^^^^^^^^^^

This is due to the deliberate naming of process and file IO functions identical to their System.Process and System.Directory counterparts. This way packages that use process or file IO, which likely import Language.Haskell.TH.Syntax as well, will fail and can be inspected for adjustments.

After a minor adjustment to the shakespeare package, similar to the adjustments made to file-embed and gitrev the other day. We can install shakespeare as well:

git clone https://github.com/mobilehaskell/shakespeare.git
cd shakespeare
arm-linux-gnueabihf-cabal install --allow-newer \
   --ghc-options="-fexternal-interpreter \
                  -pgmi $HOME/.cabal/bin/iserv-proxy \
                  -opti10.0.1.1 -opti5000"

The Final Stretch

Returning to compile the dependencies for our Yesod app:

arm-linux-gnueabihf-cabal install --dependencies-only --allow-newer

The http-api-data package will fail with a build-type: custom failure. And the yesod package will fail with ambiguous occurrences, which require yet another small adjustment, and install just fine after that.

git clone https://github.com/mobilehaskell/yesod.git
cd yesod/yesod
arm-linux-gnueabihf-cabal install --allow-newer \
   --ghc-options="-fexternal-interpreter \
                  -pgmi $HOME/.cabal/bin/iserv-proxy \
                  -opti10.0.1.1 -opti5000"

Building and Running the Yesod Application on Raspberry Pi

Finally we are in the position to build out Yesod app now, while this was clearly a bit involved, over time (and with patches being upstreamed) this will become much easier.

Building the app now is as simple as

arm-linux-gnueabihf-cabal configure
arm-linux-gnueabihf-cabal build \
   --ghc-options="-fexternal-interpreter \
                  -pgmi $HOME/.cabal/bin/iserv-proxy \
                  -opti10.0.1.1 -opti5000"

Transferring the app binary to the Raspberry Pi and running it:

scp dist/arm-linux-gnueabihf/build/app/app pi@raspberrypi:
ssh pi@raspberrypi 'LD_LIBRARY_PATH=. ./app'

Opening http://raspberrypi:3000 shows the running Yesod instance

Figure 1: The Haskell Yesod Web Framework running on Raspberry Pi

Figure 1: The Haskell Yesod Web Framework running on Raspberry Pi

Conclusion

We have seen that cross compiling Haskell to Raspberry Pi with Template Haskell support is possible. It currently still does require a few manual interventions. Those will become unnecessary over time, when patches are integrated into the libraries. There has also been some progress on lessening the need for build-type: custom when it is just needed for doctest. If packages can be freed from needing custom build types for haddock and doctest, they would be easier to cross compile.

Finally you might have noticed that I moved all the patched packages to github.com/mobilehaskell. I’d like to invite you to join that organisation and publish patched forks there, so that we all can benefit, and be able to find the patched packages in a central location.