/blog/

0001 0101 CMake + Ninja Build System Translation

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:

  1. Configure: cmake -G Ninja ... reads CMakeLists.txt, discovers files, generates build.ninja
  2. Build: ninja build-all executes 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 CACHE variables 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 dev target (hugo server with live reload) isn’t translated. CMake is a build system, not a process manager. Just run hugo server directly.
  • 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 two hugo server processes. Use hugo server directly.
  • 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 around make build-all. Would need its own CMake-in-Docker setup.

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!).