Using the Ninja build system to generate this site
This static website is generated with a Python script that does not
produce HTML files, but
produces a build.ninja
file, which is then
processed by the Ninja build system to generate the final HTML files.
I don't implement all the source/target dependency graph logic myself, it's Ninja's job to take care of it (and build systems in general), and it knows how to do it right and efficiently, i.e., only (re)generating needed targets, leveraging parallelism to speed up the build time.
Ninja files are intended to be generated by a program rather than hand-written by a human. Ninja rules are very simple, beacuse the business logic decisions have been made by the program generating the Ninja build file: deciding what to map and how to map the source files to the destination HTML files, how to name each of them, how to process each of them. That's essentially what my static generator does.
Due to its nature,
the Ninja build file may change often, but it's fine because Ninja
maintains a log (.ninja_log
file) so it knows how to rebuild targets correctly
and efficienlty across multiple regenerations of the build.ninja
file.
This is the job of my site generator:
- Scan the source directory to identify and specify the targets (final HTML pages)
- (Re)Write the
build.ninja
file with the rules and directives, describing the target and its dependencies (source article, but also the header and footer HTML files typically) - Invoke command
ninja
and let it do the heavy lifting.
build
-directive for each target it wants:
# Code generated. DO NOT EDIT.
rule justhtml
command = cat $in | sed 's/:TITLE/$title/g' > $out
description = $in -> $out
build www/colophon/index.html: justhtml header.html src/colophon.html footer.html
title = Colophon
build www/contact/index.html: justhtml header.html src/contact.html footer.html
title = Contact
build www/legal/index.html: justhtml header.html src/legal.html footer.html
title = Mentions légales
...
...
...
The work Ninja does when nothing changed:
$ ./build.py
ninja: no work to do.
After a new article is created or updated:
$ ./build.py
[1/1] header.html src/notes/site-generator-ninja.html footer.html -> www/notes/site-generator-ninja/index.html
After the global header has chaged (I have 11 pages generated in parallel on my 12-core machine, Ninja only displays one line):
./build.py
[11/11] header.html src/index.html footer.html -> www/index.html
The generator is this one-file Python script (build.py
), for now, because my current needs are so simple.
#!/usr/bin/env python3
import os
import re
from pathlib import Path
src = 'src'
dest = 'www'
header = 'header.html'
footer = 'footer.html'
default_title = "Lu's Website"
pattern = re.compile(r'<h1>(.+)</h1>')
with open('build.ninja', 'w') as f:
f.write(f'''# Code generated. DO NOT EDIT.
rule justhtml
command = cat $in | sed 's/:TITLE/$title/g' > $out
description = $in -> $out
''')
for hfile in Path(src).glob('**/*.html'):
stem = hfile.parent / hfile.stem
stem = str(stem).removeprefix(f'{src}/')
if stem == 'index':
continue
# Fetch title (first h1)
content = hfile.read_text(encoding='utf-8')
m = pattern.findall(content)
if not m:
title = default_title
else:
title = m[0].strip()
f.write(f'build {dest}/{stem}/index.html: justhtml {header} {hfile} {footer}\n title = {title}\n')
indexfile = Path(src) / 'index.html'
if indexfile.is_file():
f.write(f'build {dest}/index.html: justhtml {header} {indexfile} {footer}\n title = Home\n')
os.system('ninja')
Okay, I have to admit: my needs are very basic (I don't even use Markdown!), so I can afford to toy around with a build system. I'll see if Ninja still make sense in a future where my needs may evolve.
Nevertheless, I use it for a year now, and it's fine for my purposes so far.