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
-
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.
-
Dependency tracking: Ninja’s dependency tracking is precise. Once the DAG is defined, it handles incremental rebuilds correctly.
-
Tool discovery:
find_program()withrequired: falsegracefully degrades when optional tools (netlify, aws, vale) are missing, with clear configure-time messages. -
Configure-time summary: The
summary()block at the end ofmeson.buildgives a clear picture of what’s available at configure time. -
Structured loop generation: The
foreachoverhugo_environmentsis 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
-
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-runmeson setup --reconfigurebefore building. Make re-evaluates$(shell find ...)every invocation. This is the single biggest usability regression. -
EXIF pattern rules: Make’s
%.exifstripped: | %elegantly handles any number of images with one rule. Meson forces us to either: (a) create onecustom_targetper image (bloatsbuild.ninja), or (b) wrap everything in a single shell script that loses per-file granularity. We chose (b). -
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. -
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. -
No lazy evaluation: Make variables like
DEVHOSTuse=(recursive) evaluation — they run at use time. Meson evaluates everything at configure time. TheDEVHOSTTailscale lookup would need to happen at configure time and would be stale if the network changes. -
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_targetwith stamp files works but is semantically wrong — Ninja might skip a deploy if the stamp file exists, even if the remote state has changed. -
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. -
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## commentpattern. Useninja -C builddir -t targetsto 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.