/blog/

0001 0101 Shake Build System for me.micahrl.com

A Shake (Haskell) translation of the project’s Makefile.

Running

Three ways to use it, from simplest to most optimized:

1. Interpreted (no compilation needed)

# Requires: GHC + shake package installed (cabal install shake)
cd /path/to/repo
runghc buildsys/shake/Shakefile.hs build-all

2. Compiled (faster repeated builds)

cd buildsys/shake
ghc -O2 -threaded Shakefile.hs -o ../../build
cd ../..
./build build-all

3. Via Cabal (manages dependencies automatically)

cd buildsys/shake
cabal run shakefile -- build-all

Available Targets

All targets from the original Makefile are supported:

Target Description
help Show available targets (default)
build-all Build all four production sites
deploy-all Deploy all production sites
deploy-prod-obverse Deploy production site to Netlify
deploy-prod-reverse Deploy production mirror to Netlify
deploy-onion-obverse Deploy onion site via rsync
deploy-onion-reverse Deploy onion mirror via rsync
deploy-test-obverse Deploy test site (S3 + CloudFormation)
deploy-test-reverse Deploy test mirror (S3 + CloudFormation)
deploy-s3 Deploy both test sites
check-all Run all pre-build checks
check-symlinks Check for symlinks in content/
check-exifstripped Verify EXIF data stripped from images
spellcheck-content Spell check content/ files
deploy-clean Clean generated files and Go module cache
dev Run Hugo dev servers (obverse + reverse)
install-hedgerules Install the Hedgerules CLI tool
me.micahrl.com-builder Build the CI Docker container

Why Shake is an Excellent Fit for This Project

Of all alternative build systems, Shake is arguably the best match for this project’s needs. Here’s why:

1. Content-Hash Tracking Instead of Timestamps

Make uses file modification timestamps to decide what to rebuild. Shake tracks file content hashes by default. This matters hugely for a Hugo site with ~2500 source files:

  • Checking out a different git branch and switching back won’t trigger unnecessary rebuilds (timestamps change, content doesn’t).
  • touching a file without changing it won’t cause a rebuild.
  • CI builds from clean checkouts behave correctly without needing make clean first.

2. True Parallel Builds

Shake’s need function declares dependencies, and Shake automatically parallelizes independent work. When you run build-all, all four Hugo environments build simultaneously on available cores. The Makefile supports make -j but Shake’s parallelism is more robust:

  • No race conditions from implicit parallelism.
  • Shake’s thread pool is managed properly, not just forking processes.
  • Progress reporting works correctly even with parallel builds.

3. Pattern Rules That Actually Work

The Makefile has to repeat the hugobuild macro for every environment. Shake’s pattern rule:

"public//*/.sentinel" %> \out -> do
    let env = takeDirectory1 $ dropDirectory1 out
    hugoBuild env

…handles all environments with a single rule. Adding a new environment requires zero build system changes — just need its sentinel file.

4. Parameterized CloudFormation Rules

The Makefile copy-pastes ~60 lines for test-obverse and test-reverse CloudFormation rules (differing only in stack names, domains, and bucket names). Shake uses a TestEnvConfig record type to parameterize everything, making it impossible for the two environments to drift apart. Adding a third test environment would be a 10-line config addition.

5. Dynamic Dependencies

The EXIF checking workflow demonstrates Shake’s strength: scripts/exif all produces a dynamic list of image files that need checking. Shake handles this naturally:

Stdout imagesRaw <- cmd "scripts/exif" "all"
let sentinels = map (++ ".exifstripped") (lines imagesRaw)
need sentinels

Make requires $(shell ...) evaluation at parse time, which can’t depend on other build outputs. Shake computes dependencies during the build, enabling patterns that are impossible in Make.

6. Proper Error Messages

Shake provides clear error messages with dependency chains when something fails. Make’s errors often reference line numbers in the Makefile with little context about why a rule ran.

7. Monadic Build Rules with Full Haskell

Build logic is written in Haskell, giving access to:

  • Real string manipulation (vs Make’s fragile $(shell) + $(eval) hacks)
  • Type checking catches mistakes at compile time
  • Proper data structures for configuration (TestEnvConfig)
  • Standard library for file operations, JSON parsing, etc.

Trade-offs vs Make

Advantages

  • Correctness: Content-hash tracking avoids both unnecessary rebuilds and missed rebuilds
  • Parallelism: Automatic, safe parallelism from dependency declarations
  • DRY: Parameterized rules eliminate copy-paste (especially for CloudFormation)
  • Expressiveness: Full Haskell for complex logic (git dirty detection, dynamic file lists)
  • Rebuild database: Shake maintains _build/ with build history; --report generates HTML build profiles

Disadvantages

  • Haskell dependency: Requires GHC and the shake package installed (vs Make which is ubiquitous)
  • Learning curve: Team members need basic Haskell literacy to modify build rules
  • CI setup: Docker/CI containers need Haskell toolchain added (larger image, slower cold builds)
  • Compilation step: For best performance, the Shakefile should be compiled — adds a bootstrap step
  • Ecosystem: Make has universal IDE support, syntax highlighting, and documentation; Shake is niche

When Make is Better

  • Simple projects where make target is all you need
  • Teams without Haskell experience
  • CI environments where adding GHC is impractical

When Shake Wins

  • Projects with complex dependency graphs (like this one with CloudFormation → extract → hedgerules → distribution chains)
  • Projects where correctness matters (content-hash tracking)
  • Projects with repeated patterns that benefit from parameterization
  • Build systems that need to compute dependencies dynamically

This project sits squarely in Shake’s sweet spot: it has complex multi-stage deployments, repeated patterns across environments, dynamic file dependencies, and benefits significantly from correct incremental builds.

Responses

Webmentions

Hosted on remote sites, and collected here via Webmention.io (thanks!).

Comments

Comments are hosted on this site and powered by Remark42 (thanks!).