I recently stumbled upon The Lost Art of the Makefile. It was a timely find, in that I too had been circling back to make
and the article expresses some of my opinions. It’s a tool that is not only still relevant, but, I argue, more enjoyable and apt for many scenarios than the modern tools that get chosen by default or vice.
Shortly before finding the article, I’d taken a little Sunday excursion to write yet another static website generator. It was intended to be notoriously simple, not just technically, but conceptually; perhaps reacting to a perceived cognitive bloat of today’s Web, I’d decided that a generated website could be nothing more than an index and individual article pages. Articles should feature no automated page-to-page navigation, no metadata. Initially, even the style was kept to a single line constraining the maximum width of the page — it was then refined1, but only minimally.
The result is a twenty-eight-line Makefile. I won’t break it down, since the aforementioned article is a good introduction to the subject. That said, a few highlights of the unimaginatively named makeweb
are in order.
Jump to the coda if you’d like to see the site generated by the Makefile.
Highlights
Deep dependency awareness
The terseness of the target-based rules is something that I’ve missed from my University years, where Makefiles were commonplace, along with other traditional development practices. There is nothing surprising about this system if not for how much make
provides out of the box without language-specific knowledge. The reader is invited to modify different files of the project and run make
to see what gets rebuilt. Succinctly:
File edited | Files regenerated |
---|---|
pages/article-1.md | public/article-1.html, public/index.html |
pages/article-2.html | public/article-2.html, public/index.html |
templates/header.html | public/*.html |
templates/index-begin.html | public/index.html |
make
tracks changes in the dependency chain of each artefact in order to only run the targets (i.e. recompile) what is needed. This means that building a large site, even perhaps a deeply nested one with nested Makefiles, can be very performant. The setup–payoff ratio is refreshingly high, in an age of convoluted NPM-webpack-foobar build systems.
Multiple source formats
Despite its size, the makeweb
Makefile can accommodate both HTML and Markdown as content sources. More interestingly, dependency awareness isn’t lost, thanks to the concept of optional dependencies2, i.e. if attempting to build public/a.html
, the tool knows to look for pages/a.html
or pages/a.md
and check their modification dates to avoid unnecessary recompilation, but it doesn’t need both source files. Since Markdown allows some HTML formatting, this accommodation is probably moot from a product standpoint, but the technical question stood nevertheless — different source formats could trivially be adopted later. I left this feature there for the sake of example, perhaps hypocritically, as it goes against the premise of simplicity of this exercise.
Simple convention over sophistication
One might be tempted to introduce the notion of page metadata (title, date, author, locale…) and dedicated parsing of said data. That was my choice in a larger site-generator project in 2009, in which source files were Markdown-powered YAML:
- date: 2009-06-28
config:
locale : 'en_US.UTF-8'
date_format : '%A, %B %d, %Y'
keyword: sample test
title: Test Page
body: |
This is a sample article. It'd be nice if it could be recognized as _Markdown_. […]
The idea had merit, but why bother? What is really needed, anyway? Thus, a page is expected to sport a h1
tag containing its title. Similarly, its filename is expected to start with a date in YYYY-MM-DD
format3, currently only used for sorting the index. Finally, locale can be expressed via the lang
HTML attribute, whether globally (in templates/header.html
) or on a per-page basis, and author and other such data could be provided with meta
elements.
These conventions allow things like title extraction for the index to be performed trivially as a shell operation inside the Makefile:
for p in $(subst public/,,$(filter-out templates/%,$^)) ; do \
t=$$(grep -m2 '<h1>' public/$$p | tail -n1 \
| awk 'match($$0,">[^<]+<"){print substr($$0,RSTART+1,RLENGTH-2)}') ; \
echo "<li><a href="$$p">$$t</a></li>" >> $@ ; \
done
On correctness. The makeweb
Makefile presents concessions made in the name of brevity. For instance: strictly speaking, targets all
and clean
should be declared .PHONY
targets, since running those targets doesn’t generate files by those two names. However, that omission should never be an issue given the intended use of the Makefile and given implicit conventions. The .PHONY
declaration takes just one extra line, so cost of implementation is not an issue. Instead, the point here was to highlight that a different mindset is allowed when we treat the software we write as a minimal tool that will be used in its entirety — as opposed to via opaque interfaces — by someone else. Similarly, the code that looks for a page’s title is naïve, but the point is that the user of the tool will be working with the tool, and that we don’t need to guard against every hypothetical failure. This can be very liberating.
No configuration as a feature
Allowing no configuration or extension mechanisms encourages the Makefile to stay small. Indeed, at this scale, tools can be readily understood, and thus forked and specifically tweaked for each usage. In this scenario, config files and plugin systems are hard to justify: if one needs an automated extension solution, systematic patch application is an apt solution. Consider the following example:
Suppose a different Markdown processor — or a different set of processing rules — is desired for some reason. Rather than having the Makefile proper offer that option, which could look like:
# Offer a default processor
MARKDOWN ?= python3 -m markdown
public/%.html:
cat templates/header.html > tmpfile
$(MARKDOWN) $> tmpfile
cat templates/footer.html > tmpfile
one instead keeps a patch:
diff --git a/Makefile b/Makefile
index 7509045..d96cf95 100644
--- a/Makefile
+++ b/Makefile
@@ -18,7 +18,7 @@ public/index.html: $(DST_PAGES) templates/*
public/%.html: $$(wildcard pages/$$*.html) $$(wildcard pages/$$*.md) templates/header.html templates/footer.html
@echo $@
@if [[ '$(findstring .md,$<)' ]]; then \
- python3 -m markdown $ tmpfile ; \
+ my-markdown-processor $ tmpfile ; \
else \
cat $ tmpfile ; \
fi
First off, why not offer MARKDOWN
as an environment variable? It’s a reasonable request. But the underlying question is: where should the line be drawn? Should custom flags for the Markdown processor also be configurable? Should control flow within the Makefile also be configurable for user-defined error handling?
Instead, this patch, along with other possible patches, can be collected and reapplied to the original Makefile at any time. If this sounds farfetched, know that it can be a very simple and pleasant workflow, with the right tooling (perhaps make
itself). The software school of suckless.org uses this pattern. I can say it was a joy to customize dwm
with community patches and my own settings and have this process handled by Arch Linux’s excellent makepkg
tool.
Coda
UNIX-dō
All these little tools may seem intimidating at first, with their cryptic names and options, when compared to ready-made alternatives. This paragraph is a short éloge of the small, the simple and the composable. When reading about make
, grep
, cut
, awk
, bash
, one may come across the terms GNU toolchain or the UNIX way. Behind these terms lies the idea that, rather than gravitating towards specific tooling ecosystems depending on one’s chosen technology (should technology matter, or shouldn’t indeed the nature of a problem be more determinant?), one should find specialized tools and techniques that recognize and celebrate the universality of text and files. For this reason, shell scripting and deep editing skills — proficiency in Vi(m), Emacs, or comparable — are as relevant today as thirty years ago. Finally, once acquired, these skills will be useful throughout an individual’s computing life.
Parva sed apta
A website generated with makeweb
can be visited at lambda.surge.sh. It does, shows and weighs less; what remains is the content — the value of its substance is a different topic. That website isn’t meant to replace my actual one, but it may shape it.
Next
As Gutenberg — a project orders of magnitude more complex than any Markdown-powered site generator — is reaching merge proposal state for inclusion in core WordPress, these pseudo-philosophical detours are an opportunity to remember what the ultimate goals for WordPress are: writing, publishing, building one’s home on the Web.
Gutenberg semantically augments the content so as to help writers write and readers read; it absorbs disparate concepts of WordPress content and presentation (shortcodes, widgets, menus, embeds) into a single concept of the block; and, while any tool can be misused, there is potential in the block paradigm to reclaim content as the central entity of the Web, and to simplify our little virtual homes.