Thursday, August 3, 2017

Building Haskell projects with GHC

For some reason, building Haskell projects with GHC/GHCi is usually much faster than using Stack or cabal-install. This document explains how to do it.

UPDATE: A more accurate title would be: Building Haskell projects with ghc --make and cabal-cargs.

UPDATE: A more accurate description of the problem is:

  • By default, Stack and cabal-install will compile with optimizations and linking, etc. to produce efficient libraries or executables. If we are only interested in checking the code or simple testing without performance concerns, we can speed up the process by turning off these things when calling GHC.
  • Even if we modify the Cabal file with ghc-options to turn off optimizations, etc., two problems remain with Stack/cabal-install:
    • To my knowledge they are not able to build just a single file and its dependencies. In contrast, ghc --make can target a single file just fine.
    • When a package consists of a main library and targets such as test suites that depend on the library, Stack/cabal-install have to build and install the whole main library before starting to build the test suite. In contrast, ghc --make can view the whole project as a single unit and only compile the necessary files.

Prerequisites

Install the command line tool cabal-cargs.

Building without linking

GHC can be used as a fast tool for checking syntax/types/etc. of a file in the project. Here is one way to do that:

ghc --make -O0 -no-link -dynamic -hidir .ghc-temp -odir .ghc-temp `cabal-cargs` file.hs

Some things to note:

  • We are not interested in generating an optimized executable, so we turn off optimizations and linking by passing -O0 -no-link to GHC.
  • We put temporary files in the directory .ghc-temp to avoid sprinkling temporary files in the project directory.
  • We use cabal-cargs to get all required additional flags from the project’s Cabal file.

The first time the above command is issued for file.hs, GHC will also build the modules on which file.hs depends. In sub-sequent runs, only dependencies that have changed will be rebuilt.

The way cabal-cargs is used in the above command, it will set the flags for the whole project, including all targets (library, test suites, executables, etc.) In order to only build files from a a particular target, pass the flag --sourcefile to cabal-cargs:

ghc --make -O0 -no-link -dynamic -hidir .ghc-temp -odir .ghc-temp `cabal-cargs --sourcefile=test/file.hs` test/file.hs

This option allows cabal-cargs to find the target that includes test/file.hs and only set the flags for this target. Note that if, for example, test/file.hs is a part of a test suite that depends on the main library, you need to first install the main library before running the above command.

Using GHCi

In order to load a project file in GHCi, we just change some flags to GHC:

ghc --interactive -hidir .ghc-temp -odir .ghc-temp `cabal-cargs` file.hs

Note that by keeping the -hidir and -odir flags, we allow GHCi to make use of previously generated intermediate files. This means that it only has to load (and interpret) files that have changed since they were previously built. Thus, using ghc --make before ghc --interactive can significantly improve the time it takes for the interactive mode to load.

Interaction with Stack and Cabal sandboxes

Users of Stack or Cabal sandboxes will need to set the environment that tells GHC where to find installed packages before running the above commands. A convenient way to do this is to use stack exec or cabal exec; for example:

stack exec -- ghc --make -O0 -no-link -dynamic -hidir .ghc-temp -odir .ghc-temp `cabal-cargs` file.hs

or

cabal exec -- ghc --make -O0 -no-link -dynamic -hidir .ghc-temp -odir .ghc-temp `cabal-cargs` file.hs

Convenient wrapper scripts

I use the following Bash scripts as convenient wrappers around the commands shown in this text:

$ cat ghcc
#!/bin/bash
exec ghc --make -O0 -no-link -dynamic -hidir .ghc-temp -odir .ghc-temp `cabal-cargs` "$@"

$ cat ghcci
#!/bin/bash
exec ghc --interactive -hidir .ghc-temp -odir .ghc-temp `cabal-cargs` "$@"

$ cat ghcc1
#!/bin/bash
exec ghc --make -O0 -no-link -dynamic -hidir .ghc-temp -odir .ghc-temp `cabal-cargs --sourcefile="$1"` "$1"

$ cat ghcci1
#!/bin/bash
exec ghc --interactive -hidir .ghc-temp -odir .ghc-temp `cabal-cargs --sourcefile="$1"` "$1"