Build the Flutter desktop GUI with nix, sandbox and all
`nix build .#flutter-desktop`, beside `.#frq`. It is the first output here
that builds purely — the APK cannot, because Gradle fetches as it goes.
The Dart does not exist until ClojureDart writes it, so this is two stages.
`cljd-deps` is a fixed-output derivation holding everything the compiler
would otherwise reach the network for, and `flutter-desktop` runs the
compiler out of those caches with `--offline` before handing the result to
nixpkgs' buildFlutterApplication.
Three things about that FOD are worth the comments they carry, because each
was a failed build first:
* It never sees frq's source. A three-line stub project is compiled against
the same deps.edn and pubspec.yaml, so the hash moves when a dependency
moves and not when a screen changes.
* A fixed-output derivation may not reference a store path, and a resolved
pub project is nothing but store paths. The analyzer project ships
unresolved and `pub get --offline` resolves it at build time, where
naming the store is allowed.
* Two runs have to agree. pub's version listings record when they were
fetched and tools.gitlibs keeps bare clones, which are packfiles; both
are dropped. The bare repos cannot simply go, since `procure` calls
`ensure-git-dir` before it looks at anything else — but they need only
exist, so they are re-initialised empty.
The GL launcher is the same one `frq` has and for the same reason: off NixOS
the driver is the host's, and without nixGL in front the store build dies
with "No provider of eglGetPlatformDisplayEXT found". It wraps the built
application from outside, because buildFlutterApplication's own dartFixupHook
runs after postFixup and rewrites bin/frq.
Release, not debug — it is the artefact rather than the loop, and that is
most of why the build is nine minutes to `just flutter-desktop`'s forty
seconds: AOT compiles cljd.core and the framework into libapp.so, single
threaded, with every cache cold.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>6ca9b5a parent: 484abf8 modified
flake.nix +291 -0 | @@ -464,6 +464,297 @@ | ||
| 464 | 464 | # nixGL needs a store Mesa to put the host's driver in front of. |
| 465 | 465 | appimage = |
| 466 | 466 | nix-appimage.bundlers.${pkgs.stdenv.hostPlatform.system}.default frq; |
| 467 | + | |
| 468 | + # Everything `clojure -M:cljd compile` would otherwise reach the | |
| 469 | + # network for, fetched once and hashed. | |
| 470 | + # | |
| 471 | + # The compile needs three caches, and the reason this is one | |
| 472 | + # derivation rather than three is that only one of them is obvious. | |
| 473 | + # Maven and gitlibs are the ordinary tools.deps pair. The third is | |
| 474 | + # ClojureDart's own: `ensure-cljd-analyzer!` writes a *second*, whole | |
| 475 | + # pub project to `.clojuredart/cache/<cljd sha>/cljd_helper`, runs | |
| 476 | + # `pub add analyzer` in it, and then runs `bin/analyzer.dart` out of | |
| 477 | + # it for the duration of the compile — so a sandbox needs that | |
| 478 | + # project already resolved, not just the app's dependencies. | |
| 479 | + # | |
| 480 | + # Fixed-output, so it is allowed the network the rest of the build is | |
| 481 | + # not. What that costs is a hash to maintain, and the thing worth | |
| 482 | + # being exact about is *when*: this derivation never sees frq's | |
| 483 | + # source. It compiles a three-line throwaway project against the same | |
| 484 | + # `flutter/deps.edn` and the same `flutter/pubspec.yaml`, so the hash | |
| 485 | + # moves when a dependency moves and not when a screen changes. A | |
| 486 | + # stub, rather than `-P` and a hand-built analyzer dir, because | |
| 487 | + # running the real compiler once is the only way to be sure the | |
| 488 | + # caches are the ones it actually wants. | |
| 489 | + # | |
| 490 | + # PUB_CACHE lands in $out on purpose. The package_config.json inside | |
| 491 | + # cljd_helper carries absolute paths to whatever resolved it, so | |
| 492 | + # resolving into a build directory would bake in paths that stop | |
| 493 | + # existing the moment this derivation finishes. Pointed at $out they | |
| 494 | + # are store paths, and still true. | |
| 495 | + cljd-deps = | |
| 496 | + let | |
| 497 | + flutterPkg = pkgs.flutter; | |
| 498 | + in | |
| 499 | + pkgs.stdenvNoCC.mkDerivation { | |
| 500 | + name = "frq-cljd-deps"; | |
| 501 | + dontUnpack = true; | |
| 502 | + | |
| 503 | + nativeBuildInputs = [ | |
| 504 | + pkgs.clojure | |
| 505 | + pkgs.jdk17 | |
| 506 | + flutterPkg | |
| 507 | + pkgs.git | |
| 508 | + pkgs.cacert | |
| 509 | + ]; | |
| 510 | + | |
| 511 | + buildCommand = '' | |
| 512 | + export HOME="$NIX_BUILD_TOP/home" | |
| 513 | + # Resolved in the build directory and copied to $out at the | |
| 514 | + # end, never written there directly. A fixed-output derivation | |
| 515 | + # may not reference a store path and its own output is a store | |
| 516 | + # path, so pub writing its cache's absolute location into its | |
| 517 | + # own metadata is enough to fail the check. | |
| 518 | + cache="$NIX_BUILD_TOP/cache" | |
| 519 | + export PUB_CACHE="$cache/pub-cache" | |
| 520 | + export GITLIBS="$cache/gitlibs" | |
| 521 | + export SSL_CERT_FILE="${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" | |
| 522 | + mkdir -p "$HOME" "$PUB_CACHE" "$GITLIBS" "$cache/m2" | |
| 523 | + | |
| 524 | + # The stub: our dependency files, nothing of our source. `:paths` | |
| 525 | + # still names ../common, so that has to exist for tools.deps to | |
| 526 | + # build a classpath — empty is enough. | |
| 527 | + proj="$NIX_BUILD_TOP/stub" | |
| 528 | + mkdir -p "$proj/src/stub" "$NIX_BUILD_TOP/common" | |
| 529 | + cp ${./flutter/deps.edn} "$proj/deps.edn" | |
| 530 | + cp ${./flutter/pubspec.yaml} "$proj/pubspec.yaml" | |
| 531 | + chmod u+w "$proj/deps.edn" "$proj/pubspec.yaml" | |
| 532 | + cat > "$proj/src/stub/main.cljd" <<'EOF' | |
| 533 | + (ns stub.main) | |
| 534 | + (defn main [] nil) | |
| 535 | + EOF | |
| 536 | + | |
| 537 | + cd "$proj" | |
| 538 | + # `:main` has to name the stub, or the compiler goes looking for | |
| 539 | + # frq.main in a tree that is not here. | |
| 540 | + sed -i 's/:main frq\.main/:main stub.main/' deps.edn | |
| 541 | + | |
| 542 | + flutter config --no-analytics &>/dev/null || true | |
| 543 | + flutter config --enable-linux-desktop >/dev/null || true | |
| 544 | + | |
| 545 | + clojure -Sdeps '{:mvn/local-repo "'"$cache"'/m2"}' -M:cljd compile | |
| 546 | + | |
| 547 | + # What the compile left behind, and only that. The analyzer | |
| 548 | + # project is keyed by the ClojureDart sha, so the directory | |
| 549 | + # under cache/ is copied wholesale rather than named here. | |
| 550 | + mkdir -p "$out/clojuredart" | |
| 551 | + cp -r .clojuredart/cache "$out/clojuredart/cache" | |
| 552 | + cp -r "$cache/m2" "$out/m2" | |
| 553 | + cp -r "$cache/gitlibs" "$out/gitlibs" | |
| 554 | + cp -r "$PUB_CACHE" "$out/pub-cache" | |
| 555 | + | |
| 556 | + # A fixed-output derivation may not reference a store path, and | |
| 557 | + # a resolved pub project is nothing but store paths: | |
| 558 | + # package_config.json names the Flutter SDK and every package | |
| 559 | + # in the cache by absolute path. So the analyzer project ships | |
| 560 | + # *unresolved* — its pubspec and its analyzer.dart and nothing | |
| 561 | + # else — and `flutter pub get --offline` re-resolves it against | |
| 562 | + # this cache at build time, where naming the store is allowed. | |
| 563 | + find "$out" \( -name '.dart_tool' -o -name '.flutter-plugins' \ | |
| 564 | + -o -name '.flutter-plugins-dependencies' \) -prune -exec rm -rf {} + | |
| 565 | + find "$out" -name '.packages' -delete | |
| 566 | + | |
| 567 | + | |
| 568 | + # A fixed-output hash is a promise that two runs agree, so | |
| 569 | + # everything a tool writes *about* a run rather than about a | |
| 570 | + # dependency has to go: pub's log carries timestamps, Maven | |
| 571 | + # rewrites its resolution metadata on every resolve, and | |
| 572 | + # tools.gitlibs keeps bare clones it only needs in order to | |
| 573 | + # make a checkout. None of it is read offline. | |
| 574 | + rm -rf "$out/pub-cache/log" "$out/pub-cache/_temp" \ | |
| 575 | + "$out/pub-cache/git" "$out/pub-cache/global_packages" \ | |
| 576 | + "$out/pub-cache/bin" | |
| 577 | + | |
| 578 | + # tools.gitlibs keeps a bare clone per URL under _repos/, and a | |
| 579 | + # bare clone is packfiles — which two runs of the same fetch do | |
| 580 | + # not have to produce byte for byte. It cannot simply be | |
| 581 | + # deleted, because `procure` calls `ensure-git-dir` before it | |
| 582 | + # looks at anything else and would clone it again, over a | |
| 583 | + # network this has and the build that uses it does not. | |
| 584 | + # | |
| 585 | + # It does not need the objects, though. `procure` finds the sha | |
| 586 | + # with `match-exact` against the checkout already in libs/, so | |
| 587 | + # the bare repo only has to exist. Emptied and re-initialised, | |
| 588 | + # it is a fixed handful of files from the pinned git and the | |
| 589 | + # same on every run. | |
| 590 | + find "$out/gitlibs/_repos" -name HEAD | while read -r head; do | |
| 591 | + repo="$(dirname "$head")" | |
| 592 | + rm -rf "$repo" | |
| 593 | + git init --bare -q "$repo" | |
| 594 | + # The sample hooks are shell scripts, so they carry a | |
| 595 | + # `#!/nix/store/.../bash` line — which is exactly the kind of | |
| 596 | + # store reference a fixed-output derivation may not hold. An | |
| 597 | + # empty bare repo nothing ever runs has no use for them. | |
| 598 | + rm -rf "$repo/hooks" | |
| 599 | + done | |
| 600 | + # pub's version listings, which record when they were fetched. | |
| 601 | + # This is the one that actually moved between two runs of this | |
| 602 | + # derivation: the package sources under hosted/ were identical | |
| 603 | + # and the listings beside them were not. Nothing offline reads | |
| 604 | + # them — a resolution that already has every package on disk | |
| 605 | + # never asks pub.dev what versions exist. | |
| 606 | + find "$out/pub-cache" -name '.cache' -type d -prune -exec rm -rf {} + | |
| 607 | + find "$out/m2" \( -name '*.lastUpdated' -o -name '_remote.repositories' \ | |
| 608 | + -o -name 'resolver-status.properties' -o -name '*.part' \ | |
| 609 | + -o -name 'maven-metadata-*.xml*' \) -delete | |
| 610 | + find "$out" \( -name '.DS_Store' -o -name '*.log' -o -name '.git' \) \ | |
| 611 | + -prune -exec rm -rf {} + | |
| 612 | + find "$out" -type d -empty -delete | |
| 613 | + chmod -R u+w "$out" | |
| 614 | + | |
| 615 | + # Last, after every cleanup above: anything still naming the | |
| 616 | + # store fails the fixed-output check, and the error names one | |
| 617 | + # path out of thousands of files. This names the files. | |
| 618 | + if refs="$(grep -rlI /nix/store "$out" 2>/dev/null)" && [ -n "$refs" ]; then | |
| 619 | + echo "cljd-deps: these still reference the store:" >&2 | |
| 620 | + echo "$refs" | head -20 >&2 | |
| 621 | + fi | |
| 622 | + | |
| 623 | + # If two runs disagree, this says which half to look in. Cheap, | |
| 624 | + # and the alternative is a hash mismatch with nothing attached. | |
| 625 | + for d in "$out"/*; do | |
| 626 | + echo "cljd-deps subtree $(basename "$d") $( (cd "$d" && find . -type f \ | |
| 627 | + -exec sha256sum {} + | sort -k2 | sha256sum) )" >&2 | |
| 628 | + done | |
| 629 | + for d in "$out"/pub-cache/*/*; do | |
| 630 | + [ -d "$d" ] || continue | |
| 631 | + echo "cljd-deps pub $(basename "$d") $( (cd "$d" && find . -type f \ | |
| 632 | + -exec sha256sum {} + | sort -k2 | sha256sum) )" >&2 | |
| 633 | + done | |
| 634 | + ''; | |
| 635 | + | |
| 636 | + outputHashMode = "recursive"; | |
| 637 | + outputHashAlgo = "sha256"; | |
| 638 | + # Moves when flutter/deps.edn or flutter/pubspec.yaml move, and | |
| 639 | + # not when frq's own source does — see the stub above. | |
| 640 | + outputHash = "sha256-gfJGlKCPaJsKcXfCWOJY1089XEfzTndEx0LVf3JOXfs="; | |
| 641 | + }; | |
| 642 | + | |
| 643 | + # The Flutter desktop GUI, built rather than run out of the tree. | |
| 644 | + # | |
| 645 | + # `just flutter-desktop` is the working-tree loop and this is its | |
| 646 | + # opposite number, the same way `nix build .#frq` is `just run`'s: the | |
| 647 | + # source is the flake's, the output is a store path, and the build is | |
| 648 | + # a sandbox with no network. It is the first thing here that builds | |
| 649 | + # purely — the APK cannot, because Gradle fetches as it goes. | |
| 650 | + # | |
| 651 | + # Two stages, because the Dart does not exist until ClojureDart writes | |
| 652 | + # it. `preBuild` runs the compiler over `flutter/src` and `common/` | |
| 653 | + # with `--offline`, out of the caches `cljd-deps` fetched; everything | |
| 654 | + # after that is an ordinary Flutter application as far as nixpkgs is | |
| 655 | + # concerned. | |
| 656 | + # | |
| 657 | + # The caches are copied in rather than used where they lie. Maven, | |
| 658 | + # tools.gitlibs and pub all expect to be able to write to their own | |
| 659 | + # cache — a lock file, a resolved marker — and the store is read-only, | |
| 660 | + # so pointing them at $out of a fixed-output derivation fails in three | |
| 661 | + # different ways at three different depths. | |
| 662 | + # | |
| 663 | + # `src` is the whole tree and not `flutter/`: `flutter/deps.edn` puts | |
| 664 | + # `../common` on the classpath, which is the entire point of that | |
| 665 | + # directory, and a source root of `flutter/` would leave the screens | |
| 666 | + # outside it. | |
| 667 | + flutter-desktop-unwrapped = pkgs.flutter.buildFlutterApplication rec { | |
| 668 | + pname = "frq-flutter"; | |
| 669 | + version = "0.1.0"; | |
| 670 | + | |
| 671 | + src = lib.cleanSourceWith { | |
| 672 | + src = ./.; | |
| 673 | + # Build trees and caches, which are large, machine-specific and | |
| 674 | + # would make every one of them a new store path. | |
| 675 | + filter = path: type: | |
| 676 | + let base = baseNameOf path; in | |
| 677 | + !(builtins.elem base [ | |
| 678 | + "build" ".home" ".clojuredart" ".cpcache" "cljd-out" | |
| 679 | + ".dart_tool" "result" ".git" ".jolt" "buck-out" | |
| 680 | + ]); | |
| 681 | + }; | |
| 682 | + sourceRoot = "source/flutter"; | |
| 683 | + | |
| 684 | + # Read at eval time, so the lock in git is the lock that is built. | |
| 685 | + autoPubspecLock = ./flutter/pubspec.lock; | |
| 686 | + | |
| 687 | + # git, because tools.deps resolves the ClojureDart dependency through | |
| 688 | + # tools.gitlibs even when every byte of it is already on disk — see | |
| 689 | + # the _repos note in cljd-deps. | |
| 690 | + nativeBuildInputs = [ pkgs.clojure pkgs.jdk17 pkgs.git ]; | |
| 691 | + | |
| 692 | + preBuild = '' | |
| 693 | + export PUB_CACHE="$NIX_BUILD_TOP/pub-cache" | |
| 694 | + export GITLIBS="$NIX_BUILD_TOP/gitlibs" | |
| 695 | + cp -r ${self.packages.${pkgs.stdenv.hostPlatform.system}.cljd-deps}/pub-cache "$PUB_CACHE" | |
| 696 | + cp -r ${self.packages.${pkgs.stdenv.hostPlatform.system}.cljd-deps}/gitlibs "$GITLIBS" | |
| 697 | + cp -r ${self.packages.${pkgs.stdenv.hostPlatform.system}.cljd-deps}/m2 "$NIX_BUILD_TOP/m2" | |
| 698 | + mkdir -p .clojuredart | |
| 699 | + cp -r ${self.packages.${pkgs.stdenv.hostPlatform.system}.cljd-deps}/clojuredart/cache .clojuredart/cache | |
| 700 | + chmod -R u+w "$PUB_CACHE" "$GITLIBS" "$NIX_BUILD_TOP/m2" .clojuredart | |
| 701 | + | |
| 702 | + # Resolve the analyzer project here rather than in cljd-deps, | |
| 703 | + # which was not allowed to name the store. Offline, out of the | |
| 704 | + # cache that derivation did fetch. ClojureDart only reaches for | |
| 705 | + # the network when `bin/analyzer.dart` is missing, and it is not. | |
| 706 | + for helper in .clojuredart/cache/*/cljd_helper; do | |
| 707 | + ( cd "$helper" && flutter pub get --offline ) | |
| 708 | + done | |
| 709 | + | |
| 710 | + # --offline is what keeps `pub get` out of a sandbox that has no | |
| 711 | + # network; the analyzer project it would otherwise resolve is | |
| 712 | + # already in .clojuredart, put there by cljd-deps. | |
| 713 | + clojure -Sdeps "{:mvn/local-repo \"$NIX_BUILD_TOP/m2\"}" \ | |
| 714 | + -M:cljd compile --offline | |
| 715 | + ''; | |
| 716 | + | |
| 717 | + meta = { | |
| 718 | + description = "frq's screens on Flutter's Linux target (no GL launcher)"; | |
| 719 | + mainProgram = "frq"; | |
| 720 | + platforms = systems; | |
| 721 | + }; | |
| 722 | + }; | |
| 723 | + | |
| 724 | + # The same shape as `frq` above: a launcher, and a package that is a | |
| 725 | + # symlink to it. The reason is the same one `frqScript` gives — on | |
| 726 | + # NixOS the store's Mesa is the system's and the window opens, and | |
| 727 | + # anywhere else the real driver is the host's, so the process is | |
| 728 | + # handed to nixGL. Without it the store build dies on a distrobox | |
| 729 | + # Arch with "No provider of eglGetPlatformDisplayEXT found", which is | |
| 730 | + # that failure wearing an EGL hat. | |
| 731 | + # | |
| 732 | + # A wrapper *around* the built application rather than a `postFixup` | |
| 733 | + # inside it, because buildFlutterApplication's own dartFixupHook runs | |
| 734 | + # after postFixup and rewrites `bin/frq` — so anything done to that | |
| 735 | + # path from inside is undone on the way out. | |
| 736 | + flutter-desktop = | |
| 737 | + let | |
| 738 | + unwrapped = | |
| 739 | + self.packages.${pkgs.stdenv.hostPlatform.system}.flutter-desktop-unwrapped; | |
| 740 | + script = pkgs.writeShellScript "frq" '' | |
| 741 | + runner="" | |
| 742 | + [ -e /run/current-system ] || runner="${nixGLFor pkgs}/bin/nixGLIntel" | |
| 743 | + exec ''${runner} ${unwrapped}/bin/frq "$@" | |
| 744 | + ''; | |
| 745 | + in | |
| 746 | + pkgs.runCommand "frq-flutter-0.1.0" | |
| 747 | + { | |
| 748 | + meta = { | |
| 749 | + description = "frq's screens on Flutter's Linux target"; | |
| 750 | + mainProgram = "frq"; | |
| 751 | + platforms = systems; | |
| 752 | + }; | |
| 753 | + } | |
| 754 | + '' | |
| 755 | + mkdir -p "$out/bin" | |
| 756 | + ln -s ${script} "$out/bin/frq" | |
| 757 | + ''; | |
| 467 | 758 | }); |
| 468 | 759 | |
| 469 | 760 | # Where `just run` runs, and — because entering it realises what it |
| @@ -464,6 +464,297 @@ | |||
| 464 | # nixGL needs a store Mesa to put the host's driver in front of. | 464 | # nixGL needs a store Mesa to put the host's driver in front of. |
| 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 | + | ||
| 468 | + # Everything `clojure -M:cljd compile` would otherwise reach the | ||
| 469 | + # network for, fetched once and hashed. | ||
| 470 | + # | ||
| 471 | + # The compile needs three caches, and the reason this is one | ||
| 472 | + # derivation rather than three is that only one of them is obvious. | ||
| 473 | + # Maven and gitlibs are the ordinary tools.deps pair. The third is | ||
| 474 | + # ClojureDart's own: `ensure-cljd-analyzer!` writes a *second*, whole | ||
| 475 | + # pub project to `.clojuredart/cache/<cljd sha>/cljd_helper`, runs | ||
| 476 | + # `pub add analyzer` in it, and then runs `bin/analyzer.dart` out of | ||
| 477 | + # it for the duration of the compile — so a sandbox needs that | ||
| 478 | + # project already resolved, not just the app's dependencies. | ||
| 479 | + # | ||
| 480 | + # Fixed-output, so it is allowed the network the rest of the build is | ||
| 481 | + # not. What that costs is a hash to maintain, and the thing worth | ||
| 482 | + # being exact about is *when*: this derivation never sees frq's | ||
| 483 | + # source. It compiles a three-line throwaway project against the same | ||
| 484 | + # `flutter/deps.edn` and the same `flutter/pubspec.yaml`, so the hash | ||
| 485 | + # moves when a dependency moves and not when a screen changes. A | ||
| 486 | + # stub, rather than `-P` and a hand-built analyzer dir, because | ||
| 487 | + # running the real compiler once is the only way to be sure the | ||
| 488 | + # caches are the ones it actually wants. | ||
| 489 | + # | ||
| 490 | + # PUB_CACHE lands in $out on purpose. The package_config.json inside | ||
| 491 | + # cljd_helper carries absolute paths to whatever resolved it, so | ||
| 492 | + # resolving into a build directory would bake in paths that stop | ||
| 493 | + # existing the moment this derivation finishes. Pointed at $out they | ||
| 494 | + # are store paths, and still true. | ||
| 495 | + cljd-deps = | ||
| 496 | + let | ||
| 497 | + flutterPkg = pkgs.flutter; | ||
| 498 | + in | ||
| 499 | + pkgs.stdenvNoCC.mkDerivation { | ||
| 500 | + name = "frq-cljd-deps"; | ||
| 501 | + dontUnpack = true; | ||
| 502 | + | ||
| 503 | + nativeBuildInputs = [ | ||
| 504 | + pkgs.clojure | ||
| 505 | + pkgs.jdk17 | ||
| 506 | + flutterPkg | ||
| 507 | + pkgs.git | ||
| 508 | + pkgs.cacert | ||
| 509 | + ]; | ||
| 510 | + | ||
| 511 | + buildCommand = '' | ||
| 512 | + export HOME="$NIX_BUILD_TOP/home" | ||
| 513 | + # Resolved in the build directory and copied to $out at the | ||
| 514 | + # end, never written there directly. A fixed-output derivation | ||
| 515 | + # may not reference a store path and its own output is a store | ||
| 516 | + # path, so pub writing its cache's absolute location into its | ||
| 517 | + # own metadata is enough to fail the check. | ||
| 518 | + cache="$NIX_BUILD_TOP/cache" | ||
| 519 | + export PUB_CACHE="$cache/pub-cache" | ||
| 520 | + export GITLIBS="$cache/gitlibs" | ||
| 521 | + export SSL_CERT_FILE="${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" | ||
| 522 | + mkdir -p "$HOME" "$PUB_CACHE" "$GITLIBS" "$cache/m2" | ||
| 523 | + | ||
| 524 | + # The stub: our dependency files, nothing of our source. `:paths` | ||
| 525 | + # still names ../common, so that has to exist for tools.deps to | ||
| 526 | + # build a classpath — empty is enough. | ||
| 527 | + proj="$NIX_BUILD_TOP/stub" | ||
| 528 | + mkdir -p "$proj/src/stub" "$NIX_BUILD_TOP/common" | ||
| 529 | + cp ${./flutter/deps.edn} "$proj/deps.edn" | ||
| 530 | + cp ${./flutter/pubspec.yaml} "$proj/pubspec.yaml" | ||
| 531 | + chmod u+w "$proj/deps.edn" "$proj/pubspec.yaml" | ||
| 532 | + cat > "$proj/src/stub/main.cljd" <<'EOF' | ||
| 533 | + (ns stub.main) | ||
| 534 | + (defn main [] nil) | ||
| 535 | + EOF | ||
| 536 | + | ||
| 537 | + cd "$proj" | ||
| 538 | + # `:main` has to name the stub, or the compiler goes looking for | ||
| 539 | + # frq.main in a tree that is not here. | ||
| 540 | + sed -i 's/:main frq\.main/:main stub.main/' deps.edn | ||
| 541 | + | ||
| 542 | + flutter config --no-analytics &>/dev/null || true | ||
| 543 | + flutter config --enable-linux-desktop >/dev/null || true | ||
| 544 | + | ||
| 545 | + clojure -Sdeps '{:mvn/local-repo "'"$cache"'/m2"}' -M:cljd compile | ||
| 546 | + | ||
| 547 | + # What the compile left behind, and only that. The analyzer | ||
| 548 | + # project is keyed by the ClojureDart sha, so the directory | ||
| 549 | + # under cache/ is copied wholesale rather than named here. | ||
| 550 | + mkdir -p "$out/clojuredart" | ||
| 551 | + cp -r .clojuredart/cache "$out/clojuredart/cache" | ||
| 552 | + cp -r "$cache/m2" "$out/m2" | ||
| 553 | + cp -r "$cache/gitlibs" "$out/gitlibs" | ||
| 554 | + cp -r "$PUB_CACHE" "$out/pub-cache" | ||
| 555 | + | ||
| 556 | + # A fixed-output derivation may not reference a store path, and | ||
| 557 | + # a resolved pub project is nothing but store paths: | ||
| 558 | + # package_config.json names the Flutter SDK and every package | ||
| 559 | + # in the cache by absolute path. So the analyzer project ships | ||
| 560 | + # *unresolved* — its pubspec and its analyzer.dart and nothing | ||
| 561 | + # else — and `flutter pub get --offline` re-resolves it against | ||
| 562 | + # this cache at build time, where naming the store is allowed. | ||
| 563 | + find "$out" \( -name '.dart_tool' -o -name '.flutter-plugins' \ | ||
| 564 | + -o -name '.flutter-plugins-dependencies' \) -prune -exec rm -rf {} + | ||
| 565 | + find "$out" -name '.packages' -delete | ||
| 566 | + | ||
| 567 | + | ||
| 568 | + # A fixed-output hash is a promise that two runs agree, so | ||
| 569 | + # everything a tool writes *about* a run rather than about a | ||
| 570 | + # dependency has to go: pub's log carries timestamps, Maven | ||
| 571 | + # rewrites its resolution metadata on every resolve, and | ||
| 572 | + # tools.gitlibs keeps bare clones it only needs in order to | ||
| 573 | + # make a checkout. None of it is read offline. | ||
| 574 | + rm -rf "$out/pub-cache/log" "$out/pub-cache/_temp" \ | ||
| 575 | + "$out/pub-cache/git" "$out/pub-cache/global_packages" \ | ||
| 576 | + "$out/pub-cache/bin" | ||
| 577 | + | ||
| 578 | + # tools.gitlibs keeps a bare clone per URL under _repos/, and a | ||
| 579 | + # bare clone is packfiles — which two runs of the same fetch do | ||
| 580 | + # not have to produce byte for byte. It cannot simply be | ||
| 581 | + # deleted, because `procure` calls `ensure-git-dir` before it | ||
| 582 | + # looks at anything else and would clone it again, over a | ||
| 583 | + # network this has and the build that uses it does not. | ||
| 584 | + # | ||
| 585 | + # It does not need the objects, though. `procure` finds the sha | ||
| 586 | + # with `match-exact` against the checkout already in libs/, so | ||
| 587 | + # the bare repo only has to exist. Emptied and re-initialised, | ||
| 588 | + # it is a fixed handful of files from the pinned git and the | ||
| 589 | + # same on every run. | ||
| 590 | + find "$out/gitlibs/_repos" -name HEAD | while read -r head; do | ||
| 591 | + repo="$(dirname "$head")" | ||
| 592 | + rm -rf "$repo" | ||
| 593 | + git init --bare -q "$repo" | ||
| 594 | + # The sample hooks are shell scripts, so they carry a | ||
| 595 | + # `#!/nix/store/.../bash` line — which is exactly the kind of | ||
| 596 | + # store reference a fixed-output derivation may not hold. An | ||
| 597 | + # empty bare repo nothing ever runs has no use for them. | ||
| 598 | + rm -rf "$repo/hooks" | ||
| 599 | + done | ||
| 600 | + # pub's version listings, which record when they were fetched. | ||
| 601 | + # This is the one that actually moved between two runs of this | ||
| 602 | + # derivation: the package sources under hosted/ were identical | ||
| 603 | + # and the listings beside them were not. Nothing offline reads | ||
| 604 | + # them — a resolution that already has every package on disk | ||
| 605 | + # never asks pub.dev what versions exist. | ||
| 606 | + find "$out/pub-cache" -name '.cache' -type d -prune -exec rm -rf {} + | ||
| 607 | + find "$out/m2" \( -name '*.lastUpdated' -o -name '_remote.repositories' \ | ||
| 608 | + -o -name 'resolver-status.properties' -o -name '*.part' \ | ||
| 609 | + -o -name 'maven-metadata-*.xml*' \) -delete | ||
| 610 | + find "$out" \( -name '.DS_Store' -o -name '*.log' -o -name '.git' \) \ | ||
| 611 | + -prune -exec rm -rf {} + | ||
| 612 | + find "$out" -type d -empty -delete | ||
| 613 | + chmod -R u+w "$out" | ||
| 614 | + | ||
| 615 | + # Last, after every cleanup above: anything still naming the | ||
| 616 | + # store fails the fixed-output check, and the error names one | ||
| 617 | + # path out of thousands of files. This names the files. | ||
| 618 | + if refs="$(grep -rlI /nix/store "$out" 2>/dev/null)" && [ -n "$refs" ]; then | ||
| 619 | + echo "cljd-deps: these still reference the store:" >&2 | ||
| 620 | + echo "$refs" | head -20 >&2 | ||
| 621 | + fi | ||
| 622 | + | ||
| 623 | + # If two runs disagree, this says which half to look in. Cheap, | ||
| 624 | + # and the alternative is a hash mismatch with nothing attached. | ||
| 625 | + for d in "$out"/*; do | ||
| 626 | + echo "cljd-deps subtree $(basename "$d") $( (cd "$d" && find . -type f \ | ||
| 627 | + -exec sha256sum {} + | sort -k2 | sha256sum) )" >&2 | ||
| 628 | + done | ||
| 629 | + for d in "$out"/pub-cache/*/*; do | ||
| 630 | + [ -d "$d" ] || continue | ||
| 631 | + echo "cljd-deps pub $(basename "$d") $( (cd "$d" && find . -type f \ | ||
| 632 | + -exec sha256sum {} + | sort -k2 | sha256sum) )" >&2 | ||
| 633 | + done | ||
| 634 | + ''; | ||
| 635 | + | ||
| 636 | + outputHashMode = "recursive"; | ||
| 637 | + outputHashAlgo = "sha256"; | ||
| 638 | + # Moves when flutter/deps.edn or flutter/pubspec.yaml move, and | ||
| 639 | + # not when frq's own source does — see the stub above. | ||
| 640 | + outputHash = "sha256-gfJGlKCPaJsKcXfCWOJY1089XEfzTndEx0LVf3JOXfs="; | ||
| 641 | + }; | ||
| 642 | + | ||
| 643 | + # The Flutter desktop GUI, built rather than run out of the tree. | ||
| 644 | + # | ||
| 645 | + # `just flutter-desktop` is the working-tree loop and this is its | ||
| 646 | + # opposite number, the same way `nix build .#frq` is `just run`'s: the | ||
| 647 | + # source is the flake's, the output is a store path, and the build is | ||
| 648 | + # a sandbox with no network. It is the first thing here that builds | ||
| 649 | + # purely — the APK cannot, because Gradle fetches as it goes. | ||
| 650 | + # | ||
| 651 | + # Two stages, because the Dart does not exist until ClojureDart writes | ||
| 652 | + # it. `preBuild` runs the compiler over `flutter/src` and `common/` | ||
| 653 | + # with `--offline`, out of the caches `cljd-deps` fetched; everything | ||
| 654 | + # after that is an ordinary Flutter application as far as nixpkgs is | ||
| 655 | + # concerned. | ||
| 656 | + # | ||
| 657 | + # The caches are copied in rather than used where they lie. Maven, | ||
| 658 | + # tools.gitlibs and pub all expect to be able to write to their own | ||
| 659 | + # cache — a lock file, a resolved marker — and the store is read-only, | ||
| 660 | + # so pointing them at $out of a fixed-output derivation fails in three | ||
| 661 | + # different ways at three different depths. | ||
| 662 | + # | ||
| 663 | + # `src` is the whole tree and not `flutter/`: `flutter/deps.edn` puts | ||
| 664 | + # `../common` on the classpath, which is the entire point of that | ||
| 665 | + # directory, and a source root of `flutter/` would leave the screens | ||
| 666 | + # outside it. | ||
| 667 | + flutter-desktop-unwrapped = pkgs.flutter.buildFlutterApplication rec { | ||
| 668 | + pname = "frq-flutter"; | ||
| 669 | + version = "0.1.0"; | ||
| 670 | + | ||
| 671 | + src = lib.cleanSourceWith { | ||
| 672 | + src = ./.; | ||
| 673 | + # Build trees and caches, which are large, machine-specific and | ||
| 674 | + # would make every one of them a new store path. | ||
| 675 | + filter = path: type: | ||
| 676 | + let base = baseNameOf path; in | ||
| 677 | + !(builtins.elem base [ | ||
| 678 | + "build" ".home" ".clojuredart" ".cpcache" "cljd-out" | ||
| 679 | + ".dart_tool" "result" ".git" ".jolt" "buck-out" | ||
| 680 | + ]); | ||
| 681 | + }; | ||
| 682 | + sourceRoot = "source/flutter"; | ||
| 683 | + | ||
| 684 | + # Read at eval time, so the lock in git is the lock that is built. | ||
| 685 | + autoPubspecLock = ./flutter/pubspec.lock; | ||
| 686 | + | ||
| 687 | + # git, because tools.deps resolves the ClojureDart dependency through | ||
| 688 | + # tools.gitlibs even when every byte of it is already on disk — see | ||
| 689 | + # the _repos note in cljd-deps. | ||
| 690 | + nativeBuildInputs = [ pkgs.clojure pkgs.jdk17 pkgs.git ]; | ||
| 691 | + | ||
| 692 | + preBuild = '' | ||
| 693 | + export PUB_CACHE="$NIX_BUILD_TOP/pub-cache" | ||
| 694 | + export GITLIBS="$NIX_BUILD_TOP/gitlibs" | ||
| 695 | + cp -r ${self.packages.${pkgs.stdenv.hostPlatform.system}.cljd-deps}/pub-cache "$PUB_CACHE" | ||
| 696 | + cp -r ${self.packages.${pkgs.stdenv.hostPlatform.system}.cljd-deps}/gitlibs "$GITLIBS" | ||
| 697 | + cp -r ${self.packages.${pkgs.stdenv.hostPlatform.system}.cljd-deps}/m2 "$NIX_BUILD_TOP/m2" | ||
| 698 | + mkdir -p .clojuredart | ||
| 699 | + cp -r ${self.packages.${pkgs.stdenv.hostPlatform.system}.cljd-deps}/clojuredart/cache .clojuredart/cache | ||
| 700 | + chmod -R u+w "$PUB_CACHE" "$GITLIBS" "$NIX_BUILD_TOP/m2" .clojuredart | ||
| 701 | + | ||
| 702 | + # Resolve the analyzer project here rather than in cljd-deps, | ||
| 703 | + # which was not allowed to name the store. Offline, out of the | ||
| 704 | + # cache that derivation did fetch. ClojureDart only reaches for | ||
| 705 | + # the network when `bin/analyzer.dart` is missing, and it is not. | ||
| 706 | + for helper in .clojuredart/cache/*/cljd_helper; do | ||
| 707 | + ( cd "$helper" && flutter pub get --offline ) | ||
| 708 | + done | ||
| 709 | + | ||
| 710 | + # --offline is what keeps `pub get` out of a sandbox that has no | ||
| 711 | + # network; the analyzer project it would otherwise resolve is | ||
| 712 | + # already in .clojuredart, put there by cljd-deps. | ||
| 713 | + clojure -Sdeps "{:mvn/local-repo \"$NIX_BUILD_TOP/m2\"}" \ | ||
| 714 | + -M:cljd compile --offline | ||
| 715 | + ''; | ||
| 716 | + | ||
| 717 | + meta = { | ||
| 718 | + description = "frq's screens on Flutter's Linux target (no GL launcher)"; | ||
| 719 | + mainProgram = "frq"; | ||
| 720 | + platforms = systems; | ||
| 721 | + }; | ||
| 722 | + }; | ||
| 723 | + | ||
| 724 | + # The same shape as `frq` above: a launcher, and a package that is a | ||
| 725 | + # symlink to it. The reason is the same one `frqScript` gives — on | ||
| 726 | + # NixOS the store's Mesa is the system's and the window opens, and | ||
| 727 | + # anywhere else the real driver is the host's, so the process is | ||
| 728 | + # handed to nixGL. Without it the store build dies on a distrobox | ||
| 729 | + # Arch with "No provider of eglGetPlatformDisplayEXT found", which is | ||
| 730 | + # that failure wearing an EGL hat. | ||
| 731 | + # | ||
| 732 | + # A wrapper *around* the built application rather than a `postFixup` | ||
| 733 | + # inside it, because buildFlutterApplication's own dartFixupHook runs | ||
| 734 | + # after postFixup and rewrites `bin/frq` — so anything done to that | ||
| 735 | + # path from inside is undone on the way out. | ||
| 736 | + flutter-desktop = | ||
| 737 | + let | ||
| 738 | + unwrapped = | ||
| 739 | + self.packages.${pkgs.stdenv.hostPlatform.system}.flutter-desktop-unwrapped; | ||
| 740 | + script = pkgs.writeShellScript "frq" '' | ||
| 741 | + runner="" | ||
| 742 | + [ -e /run/current-system ] || runner="${nixGLFor pkgs}/bin/nixGLIntel" | ||
| 743 | + exec ''${runner} ${unwrapped}/bin/frq "$@" | ||
| 744 | + ''; | ||
| 745 | + in | ||
| 746 | + pkgs.runCommand "frq-flutter-0.1.0" | ||
| 747 | + { | ||
| 748 | + meta = { | ||
| 749 | + description = "frq's screens on Flutter's Linux target"; | ||
| 750 | + mainProgram = "frq"; | ||
| 751 | + platforms = systems; | ||
| 752 | + }; | ||
| 753 | + } | ||
| 754 | + '' | ||
| 755 | + mkdir -p "$out/bin" | ||
| 756 | + ln -s ${script} "$out/bin/frq" | ||
| 757 | + ''; | ||
| 467 | }); | 758 | }); |
| 468 | 759 | ||
| 469 | # Where `just run` runs, and — because entering it realises what it | 760 | # Where `just run` runs, and — because entering it realises what it |