Four builds, four hashes, and the one file that was doing it
cljd-deps was not reproducible, and the reason turned out to be pub's own bookkeeping. It records which project directories use the cache under active_roots/, sharded by a hash of the path — and $NIX_BUILD_TOP is a new path on every build, so every run left a differently-named note to itself. The package sources under hosted/ were byte-identical the whole time. The cleanup below already sweeps everything a tool writes about a run rather than about a dependency. active_roots was simply missing from the list, presumably added by a newer pub than the one this was written for. Two runs in one container now agree, so the recorded hash is a value that can hold rather than one machine's souvenir. The old one predated the 2026-09-12 ClojureDart bump, which is why gitlibs and m2 looked wrong too — they were not: they were stable across every build and only this machine's store copy was old. The stub gets pubspec.lock now. That fixed nothing that was broken — resolution was already agreeing — but a lock is the difference between agreeing today and agreeing after pub.dev moves. And the diagnostic prints the path rather than the basename, which is the change that made any of this findable. `cljd-deps pub 09` names nothing; `cljd-deps pub pub-cache/active_roots/09` names the bug. `#flutter-appimage` rides along: the Flutter desktop GUI squashed into one file the way `#appimage` does libcosmic's, since a store path cannot come home without its closure. It wraps flutter-desktop rather than the unwrapped binary, so the bundle carries the Mesa nixGL puts the host driver in front of. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
b3b3cf6 parent: 972ab37 modified
flake.nix +31 -4 | @@ -465,6 +465,18 @@ | ||
| 465 | 465 | appimage = |
| 466 | 466 | nix-appimage.bundlers.${pkgs.stdenv.hostPlatform.system}.default frq; |
| 467 | 467 | |
| 468 | + # The other desktop GUI, squashed the same way. `flutter-desktop` is | |
| 469 | + # already the nixGL-wrapped launcher rather than the raw Flutter | |
| 470 | + # bundle, so this carries the same store Mesa for the same reason — | |
| 471 | + # and it is the whole point here, since a host with Flutter's | |
| 472 | + # runtime deps but no Nix is exactly who wants one file. | |
| 473 | + # | |
| 474 | + # Named by its backend, the way the outputs it wraps are: `appimage` | |
| 475 | + # is libcosmic's and this is Flutter's, and neither is the default. | |
| 476 | + flutter-appimage = | |
| 477 | + nix-appimage.bundlers.${pkgs.stdenv.hostPlatform.system}.default | |
| 478 | + self.packages.${pkgs.stdenv.hostPlatform.system}.flutter-desktop; | |
| 479 | + | |
| 468 | 480 | # Everything `clojure -M:cljd compile` would otherwise reach the |
| 469 | 481 | # network for, fetched once and hashed. |
| 470 | 482 | # |
| @@ -530,7 +542,12 @@ | ||
| 530 | 542 | cp ${./common/deps.edn} "$NIX_BUILD_TOP/common/deps.edn" |
| 531 | 543 | cp ${./flutter/deps.edn} "$proj/deps.edn" |
| 532 | 544 | cp ${./flutter/pubspec.yaml} "$proj/pubspec.yaml" |
| 533 | - chmod u+w "$proj/deps.edn" "$proj/pubspec.yaml" | |
| 545 | + # The lock, or `pub get` resolves against pub.dev and takes | |
| 546 | + # whatever satisfies the ranges today. Every build then fetches | |
| 547 | + # a slightly different set and the fixed-output hash is a | |
| 548 | + # promise nothing can keep. | |
| 549 | + cp ${./flutter/pubspec.lock} "$proj/pubspec.lock" | |
| 550 | + chmod u+w "$proj/deps.edn" "$proj/pubspec.yaml" "$proj/pubspec.lock" | |
| 534 | 551 | cat > "$proj/src/stub/main.cljd" <<'EOF' |
| 535 | 552 | (ns stub.main) |
| 536 | 553 | (defn main [] nil) |
| @@ -573,9 +590,16 @@ | ||
| 573 | 590 | # rewrites its resolution metadata on every resolve, and |
| 574 | 591 | # tools.gitlibs keeps bare clones it only needs in order to |
| 575 | 592 | # make a checkout. None of it is read offline. |
| 593 | + # | |
| 594 | + # active_roots is the one that was actually breaking this. Pub | |
| 595 | + # records the project directories using the cache, sharded by | |
| 596 | + # a hash of the path, and $NIX_BUILD_TOP is different on every | |
| 597 | + # run -- so two builds whose hosted/ trees were byte-identical | |
| 598 | + # still disagreed, purely over which directory had asked. Four | |
| 599 | + # builds gave four hashes until this went. | |
| 576 | 600 | rm -rf "$out/pub-cache/log" "$out/pub-cache/_temp" \ |
| 577 | 601 | "$out/pub-cache/git" "$out/pub-cache/global_packages" \ |
| 578 | - "$out/pub-cache/bin" | |
| 602 | + "$out/pub-cache/bin" "$out/pub-cache/active_roots" | |
| 579 | 603 | |
| 580 | 604 | # tools.gitlibs keeps a bare clone per URL under _repos/, and a |
| 581 | 605 | # bare clone is packfiles — which two runs of the same fetch do |
| @@ -630,7 +654,10 @@ | ||
| 630 | 654 | done |
| 631 | 655 | for d in "$out"/pub-cache/*/*; do |
| 632 | 656 | [ -d "$d" ] || continue |
| 633 | - echo "cljd-deps pub $(basename "$d") $( (cd "$d" && find . -type f \ | |
| 657 | + # The path relative to $out, not the basename: two runs that | |
| 658 | + # disagree here disagree about *which* directory exists, and | |
| 659 | + # a bare `09` names nothing you can go and look at. | |
| 660 | + echo "cljd-deps pub ''${d#$out/} $( (cd "$d" && find . -type f \ | |
| 634 | 661 | -exec sha256sum {} + | sort -k2 | sha256sum) )" >&2 |
| 635 | 662 | done |
| 636 | 663 | ''; |
| @@ -639,7 +666,7 @@ | ||
| 639 | 666 | outputHashAlgo = "sha256"; |
| 640 | 667 | # Moves when flutter/deps.edn or flutter/pubspec.yaml move, and |
| 641 | 668 | # not when frq's own source does — see the stub above. |
| 642 | - outputHash = "sha256-gfJGlKCPaJsKcXfCWOJY1089XEfzTndEx0LVf3JOXfs="; | |
| 669 | + outputHash = "sha256-qSGx7WFdVyV7yu4R+EjiQHZcLqwDjYSlohiZcXB43DY="; | |
| 643 | 670 | }; |
| 644 | 671 | |
| 645 | 672 | # The Flutter desktop GUI, built rather than run out of the tree. |
| @@ -465,6 +465,18 @@ | |||
| 465 | appimage = | 465 | appimage = |
| 466 | nix-appimage.bundlers.${pkgs.stdenv.hostPlatform.system}.default frq; | 466 | nix-appimage.bundlers.${pkgs.stdenv.hostPlatform.system}.default frq; |
| 467 | 467 | ||
| 468 | + # The other desktop GUI, squashed the same way. `flutter-desktop` is | ||
| 469 | + # already the nixGL-wrapped launcher rather than the raw Flutter | ||
| 470 | + # bundle, so this carries the same store Mesa for the same reason — | ||
| 471 | + # and it is the whole point here, since a host with Flutter's | ||
| 472 | + # runtime deps but no Nix is exactly who wants one file. | ||
| 473 | + # | ||
| 474 | + # Named by its backend, the way the outputs it wraps are: `appimage` | ||
| 475 | + # is libcosmic's and this is Flutter's, and neither is the default. | ||
| 476 | + flutter-appimage = | ||
| 477 | + nix-appimage.bundlers.${pkgs.stdenv.hostPlatform.system}.default | ||
| 478 | + self.packages.${pkgs.stdenv.hostPlatform.system}.flutter-desktop; | ||
| 479 | + | ||
| 468 | # Everything `clojure -M:cljd compile` would otherwise reach the | 480 | # Everything `clojure -M:cljd compile` would otherwise reach the |
| 469 | # network for, fetched once and hashed. | 481 | # network for, fetched once and hashed. |
| 470 | # | 482 | # |
| @@ -530,7 +542,12 @@ | |||
| 530 | cp ${./common/deps.edn} "$NIX_BUILD_TOP/common/deps.edn" | 542 | cp ${./common/deps.edn} "$NIX_BUILD_TOP/common/deps.edn" |
| 531 | cp ${./flutter/deps.edn} "$proj/deps.edn" | 543 | cp ${./flutter/deps.edn} "$proj/deps.edn" |
| 532 | cp ${./flutter/pubspec.yaml} "$proj/pubspec.yaml" | 544 | cp ${./flutter/pubspec.yaml} "$proj/pubspec.yaml" |
| 533 | - chmod u+w "$proj/deps.edn" "$proj/pubspec.yaml" | 545 | + # The lock, or `pub get` resolves against pub.dev and takes |
| 546 | + # whatever satisfies the ranges today. Every build then fetches | ||
| 547 | + # a slightly different set and the fixed-output hash is a | ||
| 548 | + # promise nothing can keep. | ||
| 549 | + cp ${./flutter/pubspec.lock} "$proj/pubspec.lock" | ||
| 550 | + chmod u+w "$proj/deps.edn" "$proj/pubspec.yaml" "$proj/pubspec.lock" | ||
| 534 | cat > "$proj/src/stub/main.cljd" <<'EOF' | 551 | cat > "$proj/src/stub/main.cljd" <<'EOF' |
| 535 | (ns stub.main) | 552 | (ns stub.main) |
| 536 | (defn main [] nil) | 553 | (defn main [] nil) |
| @@ -573,9 +590,16 @@ | |||
| 573 | # rewrites its resolution metadata on every resolve, and | 590 | # rewrites its resolution metadata on every resolve, and |
| 574 | # tools.gitlibs keeps bare clones it only needs in order to | 591 | # tools.gitlibs keeps bare clones it only needs in order to |
| 575 | # make a checkout. None of it is read offline. | 592 | # make a checkout. None of it is read offline. |
| 593 | + # | ||
| 594 | + # active_roots is the one that was actually breaking this. Pub | ||
| 595 | + # records the project directories using the cache, sharded by | ||
| 596 | + # a hash of the path, and $NIX_BUILD_TOP is different on every | ||
| 597 | + # run -- so two builds whose hosted/ trees were byte-identical | ||
| 598 | + # still disagreed, purely over which directory had asked. Four | ||
| 599 | + # builds gave four hashes until this went. | ||
| 576 | rm -rf "$out/pub-cache/log" "$out/pub-cache/_temp" \ | 600 | rm -rf "$out/pub-cache/log" "$out/pub-cache/_temp" \ |
| 577 | "$out/pub-cache/git" "$out/pub-cache/global_packages" \ | 601 | "$out/pub-cache/git" "$out/pub-cache/global_packages" \ |
| 578 | - "$out/pub-cache/bin" | 602 | + "$out/pub-cache/bin" "$out/pub-cache/active_roots" |
| 579 | 603 | ||
| 580 | # tools.gitlibs keeps a bare clone per URL under _repos/, and a | 604 | # tools.gitlibs keeps a bare clone per URL under _repos/, and a |
| 581 | # bare clone is packfiles — which two runs of the same fetch do | 605 | # bare clone is packfiles — which two runs of the same fetch do |
| @@ -630,7 +654,10 @@ | |||
| 630 | done | 654 | done |
| 631 | for d in "$out"/pub-cache/*/*; do | 655 | for d in "$out"/pub-cache/*/*; do |
| 632 | [ -d "$d" ] || continue | 656 | [ -d "$d" ] || continue |
| 633 | - echo "cljd-deps pub $(basename "$d") $( (cd "$d" && find . -type f \ | 657 | + # The path relative to $out, not the basename: two runs that |
| 658 | + # disagree here disagree about *which* directory exists, and | ||
| 659 | + # a bare `09` names nothing you can go and look at. | ||
| 660 | + echo "cljd-deps pub ''${d#$out/} $( (cd "$d" && find . -type f \ | ||
| 634 | -exec sha256sum {} + | sort -k2 | sha256sum) )" >&2 | 661 | -exec sha256sum {} + | sort -k2 | sha256sum) )" >&2 |
| 635 | done | 662 | done |
| 636 | ''; | 663 | ''; |
| @@ -639,7 +666,7 @@ | |||
| 639 | outputHashAlgo = "sha256"; | 666 | outputHashAlgo = "sha256"; |
| 640 | # Moves when flutter/deps.edn or flutter/pubspec.yaml move, and | 667 | # Moves when flutter/deps.edn or flutter/pubspec.yaml move, and |
| 641 | # not when frq's own source does — see the stub above. | 668 | # not when frq's own source does — see the stub above. |
| 642 | - outputHash = "sha256-gfJGlKCPaJsKcXfCWOJY1089XEfzTndEx0LVf3JOXfs="; | 669 | + outputHash = "sha256-qSGx7WFdVyV7yu4R+EjiQHZcLqwDjYSlohiZcXB43DY="; |
| 643 | }; | 670 | }; |
| 644 | 671 | ||
| 645 | # The Flutter desktop GUI, built rather than run out of the tree. | 672 | # The Flutter desktop GUI, built rather than run out of the tree. |