max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+100 insertions,
-6 deletions
| @@ -38,12 +38,7 @@ | |||
| 38 | 38 | // Written from the crate's own constants for the reason they are constants: | |
| 39 | 39 | // the script reads hooks the emitter writes, so a copy checked in here goes | |
| 40 | 40 | // stale against the next bump in silence. Gitignored, like the stylesheets. | |
| 41 | - | for (name, source) in [ | |
| 42 | - | ("quasi-selection.js", quasi_webview::SELECTION_JS), | |
| 43 | - | ("quasi-clock.js", quasi_webview::CLOCK_JS), | |
| 44 | - | ("quasi-download.js", quasi_webview::DOWNLOAD_JS), | |
| 45 | - | ("quasi-fill.js", quasi_webview::FILL_JS), | |
| 46 | - | ] { | |
| 41 | + | for (name, source) in GENERATED_STATIC_JS { | |
| 47 | 42 | // `write_if_changed`, not `fs::write`, and this is load-bearing rather | |
| 48 | 43 | // than a micro-optimisation. `markup_files` globs `static/*.js`, and | |
| 49 | 44 | // `check_vocabulary_use` emits a `rerun-if-changed` for every file it | |
| @@ -501,11 +496,45 @@ | |||
| 501 | 496 | /// It tightens as screens convert, exactly as the lines above did. | |
| 502 | 497 | const DEAD_VOCABULARY_HIGH_WATER: usize = 44; | |
| 503 | 498 | ||
| 499 | + | /// The scripts this build script generates into `static/`. | |
| 500 | + | /// | |
| 501 | + | /// One list, two consumers, and that is the whole point of it being a const. | |
| 502 | + | /// The write loop iterates it, and [`markup_files`] excludes it. Those two have | |
| 503 | + | /// to agree or the build recompiles on every cargo invocation forever. | |
| 504 | + | /// | |
| 505 | + | /// # Why a generated file may never reach `markup_files` | |
| 506 | + | /// | |
| 507 | + | /// `markup_files` globs `static/*.js`, and `makeover_build::check_vocabulary_use` | |
| 508 | + | /// emits a `cargo::rerun-if-changed` for every file it reads. So a generated file | |
| 509 | + | /// under `static/` is a file this script both **writes and watches**: the write | |
| 510 | + | /// moves its mtime, cargo then sees a watched file newer than the build script's | |
| 511 | + | /// own output, and the fingerprint can never settle. Nothing fails, nothing warns; | |
| 512 | + | /// the crate just recompiles every time, which cost a second full 5m25s compile | |
| 513 | + | /// inside every Sando `cargo_test` gate for five days. | |
| 514 | + | /// | |
| 515 | + | /// That has now happened twice through two different doors — `bd448cbe` left a | |
| 516 | + | /// deleted file in the watched list, and `a52ec579` added generated ones to a | |
| 517 | + | /// globbed directory the day after the first was fixed. `write_if_changed` fixes | |
| 518 | + | /// an instance. This list plus the assertion in `markup_files` closes the door. | |
| 519 | + | /// | |
| 520 | + | /// **Adding a generated file under `static/`? Add it here too.** | |
| 521 | + | const GENERATED_STATIC_JS: [(&str, &str); 4] = [ | |
| 522 | + | ("quasi-selection.js", quasi_webview::SELECTION_JS), | |
| 523 | + | ("quasi-clock.js", quasi_webview::CLOCK_JS), | |
| 524 | + | ("quasi-download.js", quasi_webview::DOWNLOAD_JS), | |
| 525 | + | ("quasi-fill.js", quasi_webview::FILL_JS), | |
| 526 | + | ]; | |
| 527 | + | ||
| 504 | 528 | /// Every file that can carry a class name. | |
| 505 | 529 | /// | |
| 506 | 530 | /// Sorted within each group, so two machines read the same set in the same | |
| 507 | 531 | /// order. It makes no difference to the count and every difference to reading a | |
| 508 | 532 | /// diff of the warning. | |
| 533 | + | /// | |
| 534 | + | /// Generated files are excluded: see [`GENERATED_STATIC_JS`] for why watching one | |
| 535 | + | /// is a build that never settles. These four are also quasi-webview's emitted | |
| 536 | + | /// output rather than markup this repo writes, so measuring them against this | |
| 537 | + | /// server's own vocabulary seal was answering a question about another crate. | |
| 509 | 538 | fn markup_files() -> Vec<std::path::PathBuf> { | |
| 510 | 539 | let mut files = Vec::new(); | |
| 511 | 540 | for (dir, extension) in [ | |
| @@ -519,6 +548,71 @@ | |||
| 519 | 548 | found.sort(); | |
| 520 | 549 | files.extend(found); | |
| 521 | 550 | } | |
| 551 | + | ||
| 552 | + | // The door, closed. Excluding by file name rather than by full path because | |
| 553 | + | // `collect` walks recursively and the generated ones sit at the root of | |
| 554 | + | // `static/`; a name collision deeper in the tree would be a second file | |
| 555 | + | // called `quasi-clock.js`, which is its own problem. | |
| 556 | + | let before = files.len(); | |
| 557 | + | files.retain(|path| { | |
| 558 | + | !path | |
| 559 | + | .file_name() | |
| 560 | + | .and_then(|name| name.to_str()) | |
| 561 | + | .is_some_and(|name| GENERATED_STATIC_JS.iter().any(|(g, _)| *g == name)) | |
| 562 | + | }); | |
| 563 | + | ||
| 564 | + | // Catches a listed script that `markup_files` cannot see: an entry given a | |
| 565 | + | // name `collect` does not glob, such as a `.mjs` or one written somewhere | |
| 566 | + | // other than `static/`. It would be excluded from nothing while still being | |
| 567 | + | // written and watched. | |
| 568 | + | // | |
| 569 | + | // It deliberately does NOT catch a deleted or renamed file, and the comment | |
| 570 | + | // here used to claim it did. It cannot: the write loop iterates this same | |
| 571 | + | // const and regenerates every entry before this function runs, so a missing | |
| 572 | + | // one is recreated rather than absent. Verified by deleting | |
| 573 | + | // `static/quasi-fill.js` and watching the build stay green. The `.gitignore` | |
| 574 | + | // cross-check below is the half that does the real work. | |
| 575 | + | let excluded = before - files.len(); | |
| 576 | + | assert_eq!( | |
| 577 | + | excluded, | |
| 578 | + | GENERATED_STATIC_JS.len(), | |
| 579 | + | "markup_files excluded {excluded} generated scripts but GENERATED_STATIC_JS \ | |
| 580 | + | names {}. A generated file under static/ that reaches markup_files is one \ | |
| 581 | + | this build script both writes and watches, which recompiles the crate on \ | |
| 582 | + | every cargo invocation. Update GENERATED_STATIC_JS to match what is written.", | |
| 583 | + | GENERATED_STATIC_JS.len(), | |
| 584 | + | ); | |
| 585 | + | ||
| 586 | + | // The other half of the guard, and the one that catches the likelier | |
| 587 | + | // mistake. The assertion above catches a listed script that stopped | |
| 588 | + | // existing; this catches a NEW generated script nobody added to the list. | |
| 589 | + | // | |
| 590 | + | // `.gitignore` is the cross-check because it is the other place a generated | |
| 591 | + | // file has to be named: it is not committed, so it either gets an entry or | |
| 592 | + | // it shows up as untracked in every `git status` until someone adds one. | |
| 593 | + | // Read rather than asked of `git`, so this costs no subprocess and still | |
| 594 | + | // works where there is no git at all -- and where the file is unreadable it | |
| 595 | + | // fails open, which is the right direction for a guard rather than a gate. | |
| 596 | + | let ignored = fs::read_to_string("../.gitignore").unwrap_or_default(); | |
| 597 | + | for name in ignored | |
| 598 | + | .lines() | |
| 599 | + | .map(str::trim) | |
| 600 | + | .filter_map(|line| line.strip_prefix("server/static/")) | |
| 601 | + | .filter(|name| { | |
| 602 | + | Path::new(name) | |
| 603 | + | .extension() | |
| 604 | + | .is_some_and(|ext| ext.eq_ignore_ascii_case("js")) | |
| 605 | + | }) | |
| 606 | + | { | |
| 607 | + | assert!( | |
| 608 | + | GENERATED_STATIC_JS.iter().any(|(g, _)| *g == name), | |
| 609 | + | "`.gitignore` names generated script static/{name}, which \ | |
| 610 | + | GENERATED_STATIC_JS does not. markup_files therefore watches a file \ | |
| 611 | + | this build script writes, and the crate will recompile on every cargo \ | |
| 612 | + | invocation with no other symptom. Add it to GENERATED_STATIC_JS.", | |
| 613 | + | ); | |
| 614 | + | } | |
| 615 | + | ||
| 522 | 616 | files | |
| 523 | 617 | } | |
| 524 | 618 |