fix(build): stop a stale or missing keepsake viewer from shipping silently

Three ways the compiled-in viewer could be wrong, none of which anything would
have reported. Found by mutation-testing the guard added below — it failed
when it should have passed, and the reason was the second bullet.

* `include_dir!` registers NO rebuild dependency. Run `npm run build` in
  frontend/export-viewer, then `cargo build`, and cargo sees no source change
  and reuses the cached binary — carrying the PREVIOUS index.html. The file on
  disk and the file in the binary disagree, git is clean, every check passes,
  and Memories.zip ships a stale viewer. Confirmed empirically: after replacing
  the artifact the compiled-in copy did not change until a source file was
  touched. A build.rs now declares `rerun-if-changed` for
  `static/export-viewer` AND `migrations` — sqlx::migrate!() embeds its
  directory the same way, and there the stale snapshot is worse still: the
  binary boots against a database that already ran a newer migration and
  crash-loops with VersionMissing.

* `emptyOutDir: true` deleted the committed artifact BEFORE generating. That
  was safe while the build could not fail; it no longer is, because
  `inlineThemeFonts` now calls `this.error` on a keepsake that is not
  self-contained. A failed build left the directory empty — and include_dir!
  over an empty directory compiles fine, while `write_viewer_with_data`
  iterates zero files and returns Ok. The result is a valid archive with every
  photo and no viewer. The output is one overwritten file, so nothing
  accumulates without the wipe.

* Nothing asserted the viewer was there at all. Now asserted at the point of
  use (bail rather than write a viewer-less keepsake) and in a test that checks
  presence, plausible size, and that no `url(/...)` survived inlining — the
  three ways it can be present but useless.

The Dockerfile copies build.rs with the sources rather than with Cargo.toml, so
the dependency-cache layer stays byte-identical and the dummy build does not
run it.
This commit is contained in:
fabi
2026-08-12 20:51:51 +02:00
parent 8af8c4fab7
commit 010bcc0e3c
4 changed files with 83 additions and 1 deletions

View File

@@ -107,7 +107,17 @@ export default defineConfig({
},
build: {
outDir: fileURLToPath(new URL('../../backend/static/export-viewer', import.meta.url)),
emptyOutDir: true,
// NOT `true`. Vite empties outDir BEFORE generating, so a build that fails late — which is
// now a real possibility, since `inlineThemeFonts` calls `this.error` on a keepsake that is
// not self-contained — left the directory EMPTY. `include_dir!` over an empty directory
// compiles perfectly happily, and `write_viewer_with_data` iterates zero files and returns
// Ok, so the next `cargo build` produced a binary whose Memories.zip has the photos and no
// viewer at all. Before the guard existed the build could not fail, so neither could this.
//
// The only output is a single `index.html`, overwritten on every successful build, so
// there is nothing to accumulate — and a failed build now leaves the last good artifact in
// place instead of deleting it.
emptyOutDir: false,
target: 'es2020'
}
});