nandi/frqpublic Fork 0
6ca9b5a
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

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>
nandi committed 2026-09-12T07:05:11-07:00 Browse files
6ca9b5a parent: 484abf8
modified flake.nix +291 -0
@@ -464,6 +464,297 @@
464464 # nixGL needs a store Mesa to put the host's driver in front of.
465465 appimage =
466466 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+ '';
467758 });
468759
469760 # 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 it760 # Where `just run` runs, and — because entering it realises what it