Hwaro::Core::Build::Cache
Build cache manager for tracking file changes
Constants
Default cache filename - uses dot prefix to hide from directory listings and 'hwaro_' prefix to identify it as project-specific cache
Racy-git: an mtime only stands in for a file's bytes once it is
safely older than the moment the stamp was taken. Filesystems stamp
with a coarse clock (ext4 uses the kernel tick, 1-10ms; HFS+ 1s,
FAT 2s), so a same-size rewrite inside the tick a stamp was taken
in keeps the stamped mtime, and the next warm build served the old
output (#857: two builds in one tick, as specs, serve and scripts
run them). A stamp that is not stable_mtime? is not kept; the
next build re-hashes the file instead.
hwaro serve --cache keeps its own cache, inside the .hwaro/
workspace next to the .hwaro/serve tree it describes. A cache
remembers ONE output directory, and a --cache build whose cache
names another one takes the cold path — so a shared file made every
hwaro build --cache after a serve session wipe public/ and
regenerate every OG image and resized image variant.
Stored in place of an mtime that was not stable when recorded. Never a real mtime, so the next check re-hashes the file.
Constructors
Class methods
Compute a checksum for the effective (env-merged, env-substituted)
config plus the active env name and resolved base_url. Hashing the
parsed config.raw rather than the raw config.toml bytes means an
env override file (config.<env>.toml), changed ${ENV_VAR}
substitutions, or a --base-url override all invalidate the per-page
cache — none of which the file-bytes hash above can detect — while a
formatting-only edit to config.toml no longer forces a full rebuild.
Fingerprint the CLI options that change what a page RENDERS TO.
Every other cache hash covers files (sources, templates, config,
data). None of them move when the same sources are built with a
different flag, so build --cache --minify after a plain
build --cache kept serving the unminified HTML it had already
rendered — for as long as the pages stayed untouched. Flags that
only affect which pages are built (--drafts, --include-future)
or how fast the build runs (--jobs, --stream, --no-parallel)
are deliberately excluded: they never change a rendered page's
bytes, and folding them in would force needless cold rebuilds.
Compute a combined checksum for a set of template files. Fields are length-prefixed (see DigestUtils) so adjacent name/content pairs can't produce the same byte stream across boundaries, which would have failed to invalidate.
A build WITHOUT --cache rewrites output_dir and never touches the
cache file, which keeps describing the tree as it was: the next
--cache build then keeps the new output (drafts, minified or
other-base-url pages) and skips every page whose hashes still match.
Drop the file when it describes this output directory; a cache for
another one is none of this build's business.
Workspace-independent identity for an output directory: relative to
the project root when it lives inside it, absolute otherwise. Makes
-o dist vs -o preview distinguishable while /ci/a/dist and
/ci/b/dist compare equal.
The cache file a build with these settings reads and writes.
Instance methods
Check if a file has changed since last build.
template_hash is the page's template closure fingerprint; nil
skips the comparison (non-page entries, or dependency tracking off).
extra_outputs are secondary sibling output files (see [outputs])
that must also still exist on disk — a manually deleted index.json
forces a rebuild just like a deleted index.html does.
Compare the current render-inputs digest (computed over
render_input_keys) with the last build's. A change invalidates
every entry, like a config edit: the reads are global, and a cached
page cannot say which of them it made. Returns true when it did.
MD5 checksum of a file's content (streamed by Digest#file)
May a copy whose size and mtime equal its source's skip the byte
compare? Not while the source mtime is racy (#857), nor on the
first check after a copy was made inside that window (#859): the
copy carries the source mtime, so a same-size rewrite in that tick
matches it exactly. That check takes dest off the list, since the
caller then compares the bytes or copies again.
Every output file the entries that SURVIVED this build record. The Finalize phase subtracts it from the stale list: a path harvested from a discarded entry and then written again is not stale at all.
Files this page wrote BESIDES its own output on the last build — its
aliases redirect stubs and a section's /page/N/ pagination pages
(see CacheEntry#derived_paths). They are produced only by a render,
so a caller that needs to know whether something else has since
overwritten one has to ask for them by name.
Write-ahead invalidation: the file on disk stops describing the
output directory the moment a build starts rewriting it, and only a
successful Finalize save (forced by the dirty flag) vouches for it
again. A build that dies mid-render otherwise leaves the previous
build's hashes next to half-rewritten pages, and reverting the edit
that broke it makes every hash match again — a warm build then
trusts output it never produced.
Secondary output files (beyond the primary HTML output) recorded for
file_path on the last build, or [] when the entry has none (or
doesn't exist). Used to detect a manually deleted sibling format
file and to locate stale files when a page's source is removed
(see Builder#stale_outputs_for_removed).
Has the global page set (content page metadata that listings render — path/url/title/date/weight/draft/section) changed since last build?
The source-less generated outputs the LAST build recorded, and the setter the current build's Finalize phase persists through. Paths are stored relative to the output directory; callers rejoin them.
Drop every entry whose source path is NOT in live_paths and return
the output files those entries recorded.
A --cache build keeps the output directory between runs (a cold
build wipes it), and nothing else notices that a page went away: the
render phase only walks pages that still exist. So rm content/posts/old.md && hwaro build --cache left /posts/old/ in
public/ — still served, still deployed — and the same held for a
renamed source, a post flipped to draft, one that passed its
expires date, and render = false. Each entry knows the primary
file it wrote plus its [outputs] siblings, so the entries no live
page claims name exactly the files to remove.
Callers filter the result against the outputs the CURRENT site claims before deleting: a source renamed onto the same output URL (foo.md -> foo/index.md) leaves an orphan entry pointing at a file this very build rewrote.
Persist this build's render-inputs digest and the read keys it covers; marks the cache dirty only when either moved.
Record the current page/section-set fingerprints so the next build can detect a change; marks the cache dirty when either value moves.
The render reads the last build recorded (see
CacheMetadata#render_input_keys).
Save cache to disk using atomic write (temp file + rename) to prevent corruption from partial writes (e.g. disk full, crash). No-op when nothing changed since load — a warm all-hits build otherwise re-serializes every entry just to write identical bytes.
Has the section set (section metadata that nav/menus render) changed?
Set the current build's template and config checksums.
A config change always invalidates all entries. A template change
invalidates all entries only when invalidate_on_template_change
is true — with template dependency tracking active, the builder
passes false and per-page closure hashes (see changed?) decide
which pages a template edit actually affects.
Output files recorded by an entry this build then replaced — see
@orphaned_outputs. Drained: a second call returns nothing.
Does this cache describe what is sitting in output_dir right now?
A --cache build keeps the output directory and prunes only what
the cache remembers writing there, so it is only as good as that
memory. With no usable record — no cache file, a corrupt one (whose
warning promises a rebuild from scratch), a legacy one, or one last
written for a DIFFERENT output directory — a page deleted since the
tree was written stayed published, with its alias stubs and tag
pages, on every later warm build. The Initialize phase takes the
cold path instead when this is false. Asked before
set_global_checksums, which re-stamps output_dir.
Update cache entry for a file.
template_hash is the page's template closure fingerprint; nil
stores the global templates checksum (non-page entries).
output_paths are the secondary sibling output files this page
emitted (see [outputs]); empty when the feature isn't in use.
source_read is the page's Page#source_read: the entry then
describes the bytes the render read, not whatever the file holds by
now (#859). A racy read stores its own digest; a file whose stamp
moved since the read stores a hash that never matches, so the next
build re-renders it.
Thread-safe: protected by mutex for concurrent parallel builds.