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 cleanfirst.
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;--reportgenerates 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 targetis 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.