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.
- To my knowledge they are not able to build just a single file and its dependencies. In contrast,
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"
No comments:
Post a Comment