This is a CMake translation of the project’s Makefile, configured to use Ninja as the build generator.
Quick Start
# From the repository root:
cmake -G Ninja -S . -B buildsys/cmake/build -DREPO_ROOT="$(pwd)"
ninja -C buildsys/cmake/build build-prod-obverse
# Or with a separate build directory:
mkdir -p /tmp/site-build && cd /tmp/site-build
cmake -G Ninja -S /path/to/repo -DREPO_ROOT=/path/to/repo
ninja build-all
The -DREPO_ROOT=... flag tells CMake where the actual repository lives (with content/, config/, etc.). This is necessary because CMakeLists.txt lives in buildsys/cmake/, not at the repo root.
Available Targets
Run ninja help or ninja -t targets depth 1 to see all targets.
Build targets: build-dev-obverse, build-prod-obverse, build-prod-reverse, build-test-obverse, build-test-reverse, build-onion-obverse, build-onion-reverse, build-all
Deploy targets: deploy-prod-obverse, deploy-prod-reverse, deploy-test-obverse, deploy-test-reverse, deploy-onion-obverse, deploy-onion-reverse, deploy-all, deploy-s3
Check targets: check-all, check-symlinks, check-exifstripped, spellcheck-content, spellcheck-prod-obverse
Other: deploy-clean, go-mod, install-hedgerules, me.micahrl.com-builder
Configuration
Override variables on the cmake command line:
cmake -G Ninja -S . -B build \
-DREPO_ROOT="$(pwd)" \
-DCFN_REGION=us-west-2 \
-DONIONSERVER=myserver.example \
-DVALE_HTML_CONFIG=.vale.custom.ini
How This Differs from Make
Source file discovery is at configure time, not build time
Make: HUGOSOURCES = $(shell find ... -type f) runs every time you invoke make, so new/deleted files are picked up automatically.
CMake: file(GLOB_RECURSE ...) runs only when you run cmake (the configure step). If you add or remove source files (new blog post, new layout, etc.), you must re-run cmake before building:
cmake -G Ninja -S . -B buildsys/cmake/build -DREPO_ROOT="$(pwd)"
ninja -C buildsys/cmake/build build-prod-obverse
This is the single most impactful difference for day-to-day use. CMake’s own documentation warns against using GLOB for source files for this reason, but for a Hugo site where the “source files” are content and templates, it’s the closest equivalent.
No order-only prerequisites
Make: %.exifstripped: | % uses order-only prerequisites – the exifstripped target is built if it doesn’t exist, but NOT rebuilt when the image file changes. This is Make-specific syntax.
CMake: Has no equivalent concept. The check-exifstripped target instead runs the exif check on all files every time. This is slightly less efficient but functionally equivalent.
No pattern rules
Make: %.exifstripped: | % defines a pattern rule applying to all matching files.
CMake: Has no pattern rule equivalent. We handle EXIF checking as a single target that iterates over all images, rather than individual per-file rules.
Macro/function differences
Make: define hugobuild is a text-expansion macro using $(call hugobuild,env-name). The $(eval) inside it runs shell commands at Make parse time.
CMake: We use a function(hugobuild ENV_NAME) that creates add_custom_command rules. The git status/commit logic runs at build time via bash -c, which is actually more correct than Make’s approach (Make’s $(eval $(shell ...)) runs at rule parse time, which can be surprising with parallel builds).
Two-phase build model
Make: Single invocation: make build-all parses the Makefile and runs rules.
CMake: Two phases:
- Configure:
cmake -G Ninja ...reads CMakeLists.txt, discovers files, generatesbuild.ninja - Build:
ninja build-allexecutes the generated rules
This adds overhead but gives Ninja full visibility into the dependency graph upfront, enabling better parallelism.
Phony target semantics
Make: .PHONY: target means the target always runs.
CMake: add_custom_target(name ...) always runs (closest to .PHONY). add_custom_command(OUTPUT ...) only runs when outputs are missing or inputs are newer.
Target naming
Make targets like public/prod-obverse/.sentinel are invoked by their path. CMake targets have names, so we use build-prod-obverse instead. The sentinel file is still produced as the output artifact.
What Works Well
- Parallel builds: Ninja excels at parallel execution. Independent environment builds (e.g., prod-obverse and prod-reverse) run simultaneously automatically.
- Dependency graph visualization:
cmake --graphviz=deps.dot .generates a visual dependency graph. - Build progress: Ninja shows
[X/Y]progress counters. - Minimal rebuilds: When sentinel files exist and sources haven’t changed, Ninja skips the build.
- Configuration management: CMake’s
CACHEvariables provide typed, documented configuration with defaults.
What Doesn’t Work Well
- File discovery lag: New content files aren’t picked up until you re-run
cmake. This is the biggest usability gap vs Make. - No dev server: The
make devtarget (hugo server with live reload) isn’t translated. CMake is a build system, not a process manager. Just runhugo serverdirectly. - No macOS launchd targets: The
macos-dev-*targets manage macOS launch agents. These are imperative system administration commands, not build rules, and are better served by a shell script or Makefile. - Verbosity: CMake’s syntax is significantly more verbose than Make for this use case. The CMakeLists.txt is roughly 3x the size of the equivalent Makefile sections.
- Overkill for non-compiled projects: CMake’s strengths (compiler detection, platform abstraction, find_package) are irrelevant for a Hugo site. The added complexity of two-phase builds doesn’t pay for itself here.
- String interpolation in shell commands: Embedding shell scripts in CMake requires awkward bracket-quoting (
[=[ ... ]=]) to avoid conflicts between CMake and shell variable syntax.
Targets NOT Translated
These Makefile targets are omitted because they don’t fit CMake’s build-system model:
dev: Runs twohugo serverprocesses. Usehugo serverdirectly.macos-dev-*: macOS launchd service management (install, clean, restart).list-pagerefs,list-shortcodes: One-liner grep commands. Just run them directly.build-all-docker: Docker wrapper aroundmake build-all. Would need its own CMake-in-Docker setup.