/blog/

0001 0101 Meson Build System Translation

This directory contains a Meson build system translation of the project’s root Makefile.

Usage

# From the repo root:
meson setup builddir buildsys/meson
ninja -C builddir <target>

# Examples:
ninja -C builddir prod-obverse          # Build prod-obverse Hugo site
ninja -C builddir build-all             # Build all four production sites
ninja -C builddir deploy-prod-obverse   # Deploy prod-obverse to Netlify
ninja -C builddir check-all             # Run all pre-build checks
ninja -C builddir dev                   # Start dev servers

# Reconfigure (e.g., after adding new content files):
meson setup --reconfigure builddir buildsys/meson

How This Differs from Make

Fundamental Architecture Differences

Aspect Make Meson+Ninja
Execution model Single tool reads Makefile, builds dependency graph, executes commands Two-phase: Meson generates build.ninja at configure time; Ninja executes at build time
Shell evaluation $(shell ...) runs at parse time per invocation run_command() runs once at configure time; results are baked in
Pattern rules %.exifstripped: | % — one rule covers any matching file No equivalent; must enumerate all targets at configure time
Macros define hugobuild ... endef — inline code reuse with arguments No macros; use foreach loops or external scripts
Phony targets .PHONY: target — always runs No direct equivalent; custom_target with build_by_default: false + stamp files
Build directory Outputs go wherever the rules say (in-source public/) Ninja-tracked outputs go to a separate build directory; Hugo still writes to public/ in-source

What Works Well in Meson

  1. Parallel builds: Ninja is significantly faster than Make at scheduling parallel work. For a build with many independent Hugo environments, Ninja will build them concurrently automatically.

  2. Dependency tracking: Ninja’s dependency tracking is precise. Once the DAG is defined, it handles incremental rebuilds correctly.

  3. Tool discovery: find_program() with required: false gracefully degrades when optional tools (netlify, aws, vale) are missing, with clear configure-time messages.

  4. Configure-time summary: The summary() block at the end of meson.build gives a clear picture of what’s available at configure time.

  5. Structured loop generation: The foreach over hugo_environments is cleaner than repeating Make rules for each environment. Similarly, the test CFN targets use a loop over a list of dictionaries, which is more structured than the Makefile’s copy-paste approach.

What Doesn’t Work Well / Trade-offs

  1. Stale source lists (biggest issue): Hugo source files are discovered at configure time via run_command(find ...). If you add, rename, or delete content files, you must re-run meson setup --reconfigure before building. Make re-evaluates $(shell find ...) every invocation. This is the single biggest usability regression.

  2. EXIF pattern rules: Make’s %.exifstripped: | % elegantly handles any number of images with one rule. Meson forces us to either: (a) create one custom_target per image (bloats build.ninja), or (b) wrap everything in a single shell script that loses per-file granularity. We chose (b).

  3. In-source outputs: Hugo writes to public/ in the source tree. Meson’s model assumes all build outputs go to the build directory. We use stamp files in the build directory for Ninja tracking, but the actual Hugo output is in-source. This dual-location model is confusing.

  4. Complex shell commands: Many targets require multi-line shell commands with variable interpolation. Meson’s string formatting (.format()) with positional @N@ placeholders is harder to read than Make’s $(VAR) substitution, especially for the CloudFormation targets.

  5. No lazy evaluation: Make variables like DEVHOST use = (recursive) evaluation — they run at use time. Meson evaluates everything at configure time. The DEVHOST Tailscale lookup would need to happen at configure time and would be stale if the network changes.

  6. Deploy targets as build artifacts: Meson is designed for producing build artifacts. Deploy operations (netlify deploy, aws cloudformation deploy, rsync) are side effects, not artifacts. Modeling them as custom_target with stamp files works but is semantically wrong — Ninja might skip a deploy if the stamp file exists, even if the remote state has changed.

  7. No equivalent of make -j1: Some deploys must run sequentially (e.g., CFN base before distribution). Meson handles this through explicit dependency edges, which is correct, but means you must be very careful to declare all dependencies. Make’s approach of just listing prerequisites is more forgiving.

  8. macOS-specific targets omitted: The macos-dev-* targets (launchctl, macOS app generation) are not translated. They are highly platform-specific and would require conditional logic that doesn’t add clarity to this comparison.

Missing Targets

The following Makefile targets are not translated:

  • help — Meson has no built-in self-documenting help system like Make’s ## comment pattern. Use ninja -C builddir -t targets to list available targets.
  • macos-dev-* — macOS-specific launch agent management, not relevant to the build system comparison.
  • list-pagerefs, list-shortcodes — Simple grep commands better run directly.
  • build-all-docker — Docker-based build wrapper; trivial to add but not interesting for comparison.
  • deploy-all, deploy-s3 — Aggregate deploy targets are not generated because their sub-targets are conditionally defined. Run individual deploy targets instead.

Verdict

Meson+Ninja is a poor fit for this project. The Makefile is primarily orchestrating shell commands (hugo, rsync, aws, netlify) with file-based dependency tracking. Meson’s strengths — cross-platform compilation, dependency resolution for C/C++/Rust libraries, pkg-config integration — are irrelevant here.

The translation works, and Ninja’s parallelism is a genuine advantage, but the configure-time/build-time split creates friction (stale source lists, no dynamic shell evaluation), and the lack of pattern rules makes EXIF checking awkward.

If choosing a Make alternative for this project, consider:

  • Just (https://github.com/casey/just) — simpler command runner, no dependency tracking but cleaner syntax
  • Task (https://taskfile.dev) — YAML-based, supports file-based dependencies
  • Tup — file-based build system with automatic dependency detection, closer to Make’s model
  • Or just keep using Make — it’s the right tool for this job.

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