4. Adopt a Modular Makefile Architecture
Date: 2024-03-01
Status
Accepted
Context
Rhiza provides Makefile-based task automation as a core part of its template offering.
As the number of supported features grew (testing, documentation, Docker, Marimo
notebooks, presentations, CI helpers, release management), maintaining a single
monolithic Makefile became unworkable:
- Adding a new feature required editing a shared file, creating merge conflicts during template synchronization.
- Downstream projects that only wanted a subset of features still received all targets,
cluttering
make helpoutput. - Large Makefiles are hard to navigate and understand.
- The root
Makefileis typically owned by the downstream project; Rhiza cannot safely update it without overwriting project-specific customizations.
The core challenge is that the Makefile is simultaneously a Rhiza-managed file (it
needs to include Rhiza's logic) and a user-owned file (it is the natural place for
project-specific targets).
Decision
We will implement a modular Makefile architecture where:
- Thin root
Makefile: The rootMakefilecontains only a singleincludestatement pointing to.rhiza/rhiza.mk. Project-specific targets are added above this include line. - Core logic in
.rhiza/rhiza.mk: All Rhiza infrastructure lives in.rhiza/rhiza.mk, which is synced from the template repository. - Auto-loading extensions:
rhiza.mkends with-include .rhiza/make.d/*.mk, which automatically loads all.mkfiles in themake.d/directory in lexicographic order. - Numbered prefixes for ordering: Makefile modules use numeric prefixes to control
load order (e.g.,
00-19for configuration,20-79for tasks,80-99for hooks). - Feature isolation: Each feature domain lives in its own
.mkfile (test.mk,docker.mk,marimo.mk, etc.), making it easy to add or remove features. - Hook system: Double-colon targets (
pre-install::,post-install::) allow downstream projects to inject behaviour before and after standard targets. local.mkescape hatch: An optionallocal.mkfile (never synced) provides a place for developer-local shortcuts without affecting the shared configuration.
Consequences
Positive
- Clean separation: Rhiza infrastructure is isolated in
.rhiza/make.d/; project customizations live in the rootMakefileorlocal.mk. Template sync never overwrites user code. - Selective feature adoption: Adding a bundle to
.rhiza/template.ymldrops in the corresponding.mkfile, automatically registering new targets. - Easy to navigate: Each
.mkfile is focused on a single domain. Contributors can find relevant targets without searching a large monolithic file. - Extensible: New features can be added to Rhiza without touching existing files—
just add a new
.mkfile. make helpstays organized: Section headers (##@) in each module group related targets, keeping the help output readable as more modules are added.
Neutral
- Auto-loading behaviour: The glob include (
*.mk) loads files in alphabetical order. Module authors must be aware of this when one module depends on variables defined in another. - Hidden complexity: New contributors may be surprised to discover that
make helptargets come from multiple files. TheARCHITECTURE.mdand module READMEs document this.
Negative
- Debugging Make: Tracing the origin of a target or variable requires knowing the
module structure.
make --print-data-basecan assist but is verbose. - Filename collisions: Two modules defining the same target name will silently interact (or produce an error). Module authors must coordinate target naming.