Saturday, 26 January 2019

Build Haskell projects using Make


Make has been around for a long time. It does, however, have a moderate learning curve. Once you understand and overcome some of its idiosyncrasies, it is a very powerful generic build tool. Below are my variants for building Haskell projects using make. Most of my projects now use Stack. Even so, having a Makefile helps to coordinate building project artefacts. It becomes an organising script for the project.

A full description on building modules can be found in the chapter Using Make in the GHC documentation. For a single target, you can use make to build a Haskell project.

Make is whitespace sensitive. You must use tabs. The default is to use a tab setting of 8 spaces.

GHC

Below is a version using just GHC for compilation. We are defining a custom pattern to build from Haskell source (.hs) to an object (.o). This rule is cheating, as the end result is actually a executable, but as I didn't want to add a .exe extension, we are 'faking' it with testing just the object.

The build is dependent upon three other targets:
  1. build tags for use in editors like Vim and Emacs
  2. style to ensure consistent source code formatting
  3. lint to check code for bad code smells

Makefile
#!/usr/bin/env make
 
.SUFFIXES:
.SUFFIXES: .o .hs
 
%:%.hs
        -ghc -Wall -O2 -threaded --make $<
 
.DEFAULT: build
 
SRCS    := $(wildcard *.hs)
TGTS    := $(patsubst %.hs, %, $(SRCS))
 
.PHONY: build
build:  check $(TGTS)
 
.PHONY: check
check:  tags style lint
 
.PHONY: all
all:    cleanall build
 
style:  $(SRCS)
        -stylish-haskell --config=.stylish-haskell.yaml --inplace $(SRCS)
 
lint:   $(SRCS)
        -hlint --color --show $(SRCS)
 
tags:   $(SRCS)
        -hasktags --ctags $(SRCS)
 
.PHONY: clean
clean:
        # replaces hs with o / hi in list of sources
        -$(RM) $(patsubst %.hs, %.o, $(SRCS))
        -$(RM) $(patsubst %.hs, %.hi, $(SRCS))
 
.PHONY: cleanall
cleanall: clean
        -$(RM) $(TGTS)

Cabal

Below we are using Cabal for compilation. This is essentially the same as for the build for GHC, but Cabal includes targets for checking, unit testing, performance benchmarks, documentation and execution. There is some setup involved to do all this, but that is described in the Cabal documentation. Cabal is useful if your project is using packages beyond the Prelude.

Makefile
#!/usr/bin/env make
 
TARGET := example
SUBS := $(wildcard */)
SRCS := $(wildcard $(addsuffix *.hs, $(SUBS)))
 
ARGS ?= -h
 
build: check
 @cabal build
 
.PHONY: all
all: check build test bench doc tags
 
.PHONY: style
style:
 @stylish-haskell -c .stylish-haskell.yaml -i $(SRCS)
 
.PHONY: lint
lint:
 @hlint --color $(SRCS)
 
.PHONY: check
check: lint style
 @cabal check
 
.PHONY: exec
exec: # Example:  make ARGS="112 12" exec
 @cabal exec $(TARGET) -- $(ARGS)
 
.PHONY: test
test:
 @cabal test
 
.PHONY: bench
bench:
 @cabal bench
 
.PHONY: tags
tags:
 -hasktags --ctags $(SRCS)
 
.PHONY: doc
doc:
 @cabal haddock
 
.PHONY: ghci
ghci:
 -ghci -Wno-type-defaults
 
.PHONY: clean
clean:
 @cabal clean

Stack

Stack is a more complete project management tool, built on top of Cabal. Where Cabal was used in the Makefile previously, it has now been replaced by calls to Stack:

Makefile
#!/usr/bin/env make
 
TARGET := example
SUBS := $(wildcard */)
SRCS := $(wildcard $(addsuffix *.hs, $(SUBS)))
 
ARGS ?= -h
 
build: check
 @stack build
 
.PHONY: all
all: check build test bench doc tags
 
.PHONY: style
style:
 @stylish-haskell -c .stylish-haskell.yaml -i $(SRCS)
 
.PHONY: lint
lint:
 @hlint --color $(SRCS)
 
.PHONY: check
check: lint style
 
.PHONY: exec
exec: # Example:  make ARGS="-h" exec
 @stack exec $(TARGET) -- $(ARGS)
 
.PHONY: test
test:
 @stack test
 
.PHONY: bench
bench:
 @stack bench
 
.PHONY: tags
tags:
 @hasktags --ctags $(SRCS)
 
.PHONY: install
install:
 @stack install
 
.PHONY: doc
doc:
 @stack haddock
 
.PHONY: ghci
ghci:
 @stack ghci --ghci-options -Wno-type-defaults
 
.PHONY: clean
clean:
 @stack clean
 
.PHONY: cleanall
cleanall: clean
 @$(RM) -rf .stack-work/

Other Examples

I hope these examples are a useful starting point. They could be extended. For example, by including a setup task. This would bootstrap a project, ensuring dependencies are met. Other tasks would be to package or deploy a target. 
You can find other Haskell project examples using make on my GitHub pages here