use std::collections::hash_map::DefaultHasher; use std::fmt::Write as _; use std::hash::{Hash, Hasher}; use std::process::Command; use std::{fs, path::Path}; fn main() { // Set GIT_HASH env var for compile-time inclusion via option_env!() println!("cargo::rustc-env=GIT_HASH={}", git_hash()); // Compile the TypeScript frontend to static/dist/ (best-effort, see fn). build_frontend(); // The three generated stylesheets. Spacing comes from makeover-geometry, // time from makeover-timing and composition from makeover-webview, the // same way colour already comes from makeover through theming.rs. Written // into static/ rather than a bundle directory because the server serves // its stylesheets; all three are gitignored, since the crates are the // source and a checked-in copy would drift from the pin. // // No explicit touch selector: density hangs off (hover: none), // (pointer: coarse) alone, because the server has no mode class to key it // on. GO passes `.ui-mode-mobile` because it has one. // The four scripts quasi-webview ships, written where the shell already says // they are served from. Each is behaviour a description cannot state and a // renderer therefore owns: how many rows are ticked and that a control over // none of them should not be pressable (`quasi-selection.js`), a readout // reckoned against the current time that keeps moving (`quasi-clock.js`), // an htmx answer that has to become a saved file rather than page text // (`quasi-download.js`), and a chosen value that lands in the box the // reader is typing in (`quasi-fill.js`). // `Shell::under("/static")` has named these addresses since each script // existed and only two of the files were being written, so every page has // been linking a script that 404s -- which is what the loop is for: a name // added to the renderer and forgotten here is a feature that silently does // nothing. // // Written from the crate's own constants for the reason they are constants: // the script reads hooks the emitter writes, so a copy checked in here goes // stale against the next bump in silence. Gitignored, like the stylesheets. for (name, source) in GENERATED_STATIC_JS { // `write_if_changed`, not `fs::write`, and this is load-bearing rather // than a micro-optimisation. `markup_files` globs `static/*.js`, and // `check_vocabulary_use` emits a `rerun-if-changed` for every file it // reads, so these four are files this script both writes AND watches. // An unconditional write moves the mtime on every run, cargo then sees // a watched file newer than the build script's own output, and the // crate recompiles on every cargo invocation forever. That is what put // a second full 5m25s compile inside every Sando `cargo_test` gate // (task 4200580f); cargo named this exact file as the stale one. write_if_changed(Path::new(&format!("static/{name}")), source); } makeover_build::geometry_css("static/geometry.css", None); // The time axis, beside the spacing one and for the same reason: the // durations are makeover-timing's answer, and a number typed into a // hand-written sheet here is the drift that crate exists to end. makeover_build::timing_css("static/timing.css"); makeover_build::layout_css("static/layout.css", &makeover_build::Emit::default()); // The embeds get their own copy of the spacing layer, because they are // served into third-party iframes and cannot link a stylesheet. Pointer // density only, and no `@media` block: an embed body is `height: 100vh` // inside an iframe the host page sized, so growing the gaps on a coarse // pointer clips rather than reflows, and the host author never sees it // happen. Revisit if embeds ever gain a resize protocol. fs::write( "static/embed-geometry.css", makeover_geometry::geometry_css_vars(makeover_geometry::Density::Pointer), ) .expect("write static/embed-geometry.css"); // The typography layer, and the faces it fetches. // // Two halves that have to agree: makeover_build writes the `@font-face` // rules and the tokens, and `cut_house_faces` puts the woff2 files where // those rules point. Both name the file through // `makeover::WEBFONT_*_FILE`, so the agreement is a constant rather than a // string typed twice. // // Young Serif is the third token, and it is here rather than in style.css // because that is what layer 0 is for. It used to be a hand-written // `@font-face` plus a `--font-heading` nothing outside this repository had // heard of; declared as an override it is the same shape as the two house // slots, and `--font-display` is the name every product uses for its brand // tier. The face itself is committed rather than cut: it is upstream's, // unmodified, and quasi-type has no part in it. makeover_build::typography_css_from( "static/typography.css", &makeover_build::Typography::house("/static/fonts").with_override( makeover_build::FontOverride::new( makeover_build::FontSlot::Display, "\"Young Serif\", serif", ) .with_face(makeover_build::FontFace::new( "Young Serif", ["ysrf.woff2", "ysrf.ttf"], )), ), ); // The embeds get the house layer WITHOUT the override, for the same reason // they get their own spacing layer: different context, different answer. // An embed carries no brand face by decision — `--font-display` is left // undefined so the `var(--font-display, Georgia, serif)` fallback in each // template renders — and the alternative is 21 KB of a serif face on // somebody else's page to style one card title. The two files are // generated from one `Typography` vocabulary, so they cannot disagree // about the house slots the way two hand-written sheets did. makeover_build::typography_css_from( "static/embed-typography.css", &makeover_build::Typography::house("/static/fonts"), ); cut_house_faces(); println!("cargo::rerun-if-changed=build.rs"); // The landing shots' own dimensions, so the description can reserve their // space. Generated rather than written down; see `shot_dimensions`. let shots = shot_dimensions(); let mut table = String::from( "/// Intrinsic size of each landing screenshot, by URL path.\n\ /// Generated by build.rs from the files themselves.\n\ pub static SHOT_DIMENSIONS: &[(&str, u32, u32)] = &[\n", ); for (path, w, h) in &shots { let _ = writeln!(table, " ({path:?}, {w}, {h}),"); } table.push_str("];\n"); fs::write( Path::new(&std::env::var("OUT_DIR").expect("OUT_DIR")).join("shot_dimensions.rs"), table, ) .expect("write shot_dimensions.rs"); // The same check goingson and balanced_breakfast run, out of // makeover-build rather than a third copy of it. The named-list form: // static/ holds the generated stylesheets and a bundler's output beside // the hand-written ones, so there is no directory to scan. No tuning // widths -- every threshold here is a shell boundary. makeover_build::check_breakpoints_files(&HAND_WRITTEN_CSS, &[]); makeover_build::check_vocabulary_files( &HAND_WRITTEN_CSS, &makeover_build::Emit::default(), REVIEWED_OVERLAPS, REVIEWED_ELEMENT_OVERLAPS, ); makeover_build::check_vocabulary_use( &markup_files(), &makeover_build::Emit::default(), DEAD_VOCABULARY_HIGH_WATER, ); // --- Static asset fingerprinting --- // Hash the content of key static files to produce a version suffix. // When any watched file changes, URLs in templates get a new ?v= param, // busting browser caches automatically. let static_files = [ "static/style.css", // The two per-page sheets. Linked from page templates rather than // from the head, which is why they were outside the fingerprint and // served stale to any browser holding a cached copy. "static/wizard.css", "static/media-player.css", // Same case again, found 2026-08-15: linked from a