nandi/frqpublic Fork 0
c20643a
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.

The window opens, and a devShell that remembers what it built

Two things, one of which is the Flutter GUI actually running.

buildFlutterApplication's wrapper appends a bare `/lib` to
LD_LIBRARY_PATH — the host's, not the bundle's — and off NixOS that is a
foreign library directory in front of nothing. First it died on
`__pointer_chk_guard`, the host glibc under the store's loader; putting
the store's glibc ahead of it only moved the seam, and it died again on
the host's libglib wanting GLIBC_2.43. Ordering cannot fix a mixture, so
the entry is removed rather than outranked: a sed'd copy of the generated
wrapper, with an assertion that fails the build if the edit stops
matching. `#flutter-appimage` now opens a window off NixOS.

And `flutter-desktop-unwrapped` took the whole repo as `src`, so editing
flake.nix — or CLAUDE.md, or the jolt half in src/ — invalidated the
entire Dart compile. It reads `flutter/` and `common/` and nothing else,
so that is what it gets now. Matched on the path and not the basename:
`src` as a basename would have excluded `flutter/src` too.

`.modal/flutter-dev` is the other half of the same problem. `nix build`
cannot be incremental — a derivation is all-or-nothing, so any edit is a
fresh sandbox and a fresh compile of everything — so this container does
not use it. The devShell supplies the toolchain, `just flutter-desktop`
runs unmodified, and the build tree lives on a volume: flutter/build,
.home and .clojuredart survive, and the next run compiles what changed.
Nix for the dependencies, the ordinary toolchain for the loop.

The volume is `devshell` and shared. Each shell gets a directory named
for itself — this one is frq-flutter-desktop — because Modal Volumes have
no locking and directories are the only thing keeping two of them apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-13T23:17:05-07:00 Browse files
c20643a parent: b3b3cf6
added .modal/flutter-dev/README.md +16 -0
new file mode 100644
@@ -0,0 +1,16 @@
1+# `flutter-dev`
2+
3+ scripts/deploy flutter-dev
4+ modal run .modal/flutter-dev/container.py
5+
6+Defined by `container.toml`; see `../spec.md` for the keys.
7+Built on the published `arch-nix` image.
8+
9+Runs as a Sandbox on a real VM (kernel 6.x, not gVisor). The command
10+is the sandbox's own process, so it dies when the command exits --
11+no idle window and nothing to tear down. Note the VM restrictions:
12+no GPU, and memory is exactly what `[resources] memory` asks for.
13+
14+No nix at run time: nothing is substituted at build time and nothing
15+is evaluated at start. Add a `flake.nix` and set `[nix] flake`/`shim`
16+together if you want a devShell, knowing what it costs.
new file mode 100644
@@ -0,0 +1,16 @@
1+# `flutter-dev`
2+
3+ scripts/deploy flutter-dev
4+ modal run .modal/flutter-dev/container.py
5+
6+Defined by `container.toml`; see `../spec.md` for the keys.
7+Built on the published `arch-nix` image.
8+
9+Runs as a Sandbox on a real VM (kernel 6.x, not gVisor). The command
10+is the sandbox's own process, so it dies when the command exits --
11+no idle window and nothing to tear down. Note the VM restrictions:
12+no GPU, and memory is exactly what `[resources] memory` asks for.
13+
14+No nix at run time: nothing is substituted at build time and nothing
15+is evaluated at start. Add a `flake.nix` and set `[nix] flake`/`shim`
16+together if you want a devShell, knowing what it costs.
added .modal/flutter-dev/container.py +45 -0
new file mode 100644
@@ -0,0 +1,45 @@
1+"""Generated stub -- the container is defined by container.toml.
2+
3+Edit container.toml, not this file.
4+"""
5+
6+import os
7+import sys
8+
9+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
10+
11+from _loader import Container # noqa: E402
12+
13+c = Container.from_toml(__file__)
14+image, app = c.image, c.app
15+
16+
17+# No @app.function here: a Sandbox runs its command as its own process and
18+# nothing of this module is imported into it. Registering a Function would be
19+# dead weight, and its kwargs are where vm_runtime would be wrongly applied.
20+@app.local_entrypoint()
21+def main(command: str = ""):
22+ c.run_sandbox(command)
23+
24+
25+@app.local_entrypoint()
26+def shell():
27+ """Leave a Sandbox running and say how to get into it.
28+
29+ `modal shell --image` only takes registry references, so it cannot be
30+ pointed at a published Modal image like arch-nix. Attaching to a running
31+ Sandbox can, and that Sandbox is this container: same image, same volumes,
32+ same resources. It outlives this process, so it also has to be killed.
33+ """
34+ sb = c.open_sandbox()
35+ print(f"sandbox {sb.object_id} up, with {', '.join(c.volumes) or 'no volumes'}")
36+ print(f" attach: modal shell {sb.object_id} (from another terminal)")
37+ print("Ctrl-C here takes it down.")
38+ # Blocks on `sleep infinity`, which is the point: an ephemeral app stops
39+ # when its local entrypoint returns, and stopping the app terminates the
40+ # Sandbox with it -- so returning here would leave nothing to attach to.
41+ try:
42+ sb.wait()
43+ except KeyboardInterrupt:
44+ print("terminating")
45+ sb.terminate()
new file mode 100644
@@ -0,0 +1,45 @@
1+"""Generated stub -- the container is defined by container.toml.
2+
3+Edit container.toml, not this file.
4+"""
5+
6+import os
7+import sys
8+
9+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
10+
11+from _loader import Container # noqa: E402
12+
13+c = Container.from_toml(__file__)
14+image, app = c.image, c.app
15+
16+
17+# No @app.function here: a Sandbox runs its command as its own process and
18+# nothing of this module is imported into it. Registering a Function would be
19+# dead weight, and its kwargs are where vm_runtime would be wrongly applied.
20+@app.local_entrypoint()
21+def main(command: str = ""):
22+ c.run_sandbox(command)
23+
24+
25+@app.local_entrypoint()
26+def shell():
27+ """Leave a Sandbox running and say how to get into it.
28+
29+ `modal shell --image` only takes registry references, so it cannot be
30+ pointed at a published Modal image like arch-nix. Attaching to a running
31+ Sandbox can, and that Sandbox is this container: same image, same volumes,
32+ same resources. It outlives this process, so it also has to be killed.
33+ """
34+ sb = c.open_sandbox()
35+ print(f"sandbox {sb.object_id} up, with {', '.join(c.volumes) or 'no volumes'}")
36+ print(f" attach: modal shell {sb.object_id} (from another terminal)")
37+ print("Ctrl-C here takes it down.")
38+ # Blocks on `sleep infinity`, which is the point: an ephemeral app stops
39+ # when its local entrypoint returns, and stopping the app terminates the
40+ # Sandbox with it -- so returning here would leave nothing to attach to.
41+ try:
42+ sb.wait()
43+ except KeyboardInterrupt:
44+ print("terminating")
45+ sb.terminate()
added .modal/flutter-dev/container.toml +125 -0
new file mode 100644
@@ -0,0 +1,125 @@
1+[container]
2+name = "frq-flutter-dev"
3+description = "the Flutter desktop build, incremental, in a nix devShell"
4+base = "arch-nix"
5+# A Sandbox, not a Function: runs on a real VM, and the command is
6+# the sandbox's own process so it dies when the command does.
7+runtime = "sandbox"
8+
9+[build]
10+# The container lives inside the repo it builds, so the copy is rooted two
11+# levels up and `.` is the whole tree.
12+context = "../.."
13+include = ["."]
14+
15+# Two volumes doing two different jobs. `nix-cache` is the binary cache every
16+# container here reads from and writes back to. `devshell` is the working
17+# state of a `nix develop` loop, and it is shared by every container that has
18+# one -- each gets its own directory under it, named for the devShell it
19+# belongs to, so two projects (or two shells of one project) never write the
20+# same tree. Modal Volumes have no locking, so the directories are the only
21+# thing keeping them apart, and two runs of the *same* devshell must not
22+# overlap.
23+[volumes]
24+nix-cache = "/nix-cache"
25+devshell = "/devshell"
26+
27+[resources]
28+cpu = 8
29+memory = 16384
30+timeout = 3600
31+
32+[run]
33+workdir = "/app"
34+# Nix for the dependencies, the ordinary toolchain for the build. `nix build`
35+# cannot do this: a derivation is all-or-nothing, so any edit is a fresh
36+# sandbox and a fresh compile of everything. Here the devShell supplies the
37+# compiler and the libraries, and `flutter build` decides what is stale --
38+# which is the whole reason `just flutter-desktop` exists as the working-tree
39+# loop rather than as another `nix build`.
40+#
41+# rsync and not cp, with --checksum and not mtimes: Modal copies the source in
42+# with fresh timestamps on every run, so a plain copy would look entirely new
43+# to Flutter and rebuild the lot. --checksum compares content, leaves the
44+# unchanged files' timestamps alone, and lets the incremental build work.
45+#
46+# The excludes are the state that must NOT be overwritten from /app -- it is
47+# what we are here to keep. `just flutter-desktop` seeds those caches only
48+# when they are missing, so finding them warm is all it takes.
49+command = """
50+set -e
51+# This container's own directory on the shared devshell volume, named for the
52+# devShell it keeps the state of. Anything else using this volume picks its
53+# own name and the two never meet.
54+SHELL_DIR=/devshell/frq-flutter-desktop
55+mkdir -p "$SHELL_DIR"
56+
57+# A worktree's `.git` is a *file* naming a gitdir back on the machine that
58+# copied it in, and nix believes it and goes looking for a path that is not
59+# here. It has to go before any flake reference to /app.
60+rm -rf /app/.git
61+
62+echo "sync: /app -> $SHELL_DIR"
63+# `nix shell --command` and not `nix profile install`: a profile install puts
64+# rsync in ~/.nix-profile/bin, which is not on the PATH of the shell already
65+# running, so the very next line said `rsync: command not found`.
66+#
67+# rsync and not cp, with --checksum and not mtimes: Modal copies the source in
68+# with fresh timestamps every run, so a plain copy looks entirely new to
69+# Flutter and rebuilds the lot. --checksum compares content and leaves the
70+# unchanged files' timestamps alone, which is the whole basis of the
71+# incremental build.
72+#
73+# The excludes are the state we are here to keep -- overwriting them from /app
74+# would defeat the volume. `just flutter-desktop` seeds those caches only when
75+# they are missing, so finding them warm is all it takes.
76+nix shell nixpkgs#rsync --accept-flake-config \
77+ --extra-substituters file:///nix-cache --command \
78+ rsync -a --checksum --delete \
79+ --exclude 'flutter/.home/' \
80+ --exclude 'flutter/.clojuredart/' \
81+ --exclude 'flutter/build/' \
82+ --exclude 'flutter/.dart_tool/' \
83+ --exclude '.git' \
84+ /app/ "$SHELL_DIR/"
85+
86+cd "$SHELL_DIR"
87+echo "state carried over:"
88+du -sh flutter/.home flutter/.clojuredart flutter/build 2>/dev/null \
89+ || echo " (none yet -- first run)"
90+
91+# Nix for the dependencies, the ordinary toolchain for the build. `nix build`
92+# cannot do this: a derivation is all-or-nothing, so any edit is a fresh
93+# sandbox and a fresh compile of everything. Here the devShell supplies the
94+# compiler and the libraries and `flutter build` decides what is stale.
95+# Evaluated from /app and built in the volume. Both halves matter: /app is the
96+# pristine copy, so nix stores a source tree of the repo rather than one
97+# carrying gigabytes of flutter/build, while the recipe still runs where the
98+# state it reuses lives -- `just -f` is what puts it there, since the recipe
99+# cds to its own justfile's directory.
100+nix develop /app#flutter-desktop --accept-flake-config \
101+ --extra-substituters file:///nix-cache \
102+ --max-jobs auto --command just -f "$SHELL_DIR/justfile" flutter-desktop
103+
104+echo "built:"
105+du -sh flutter/build
106+
107+# The devShell's closure is gigabytes of Flutter, Dart, clang and GTK, and the
108+# store it landed in belongs to the image rather than to a volume -- so
109+# without this every run re-fetches it from upstream. Written back, the next
110+# run substitutes it from file:///nix-cache instead.
111+if [ -f /nix-cache/nix-cache-info ]; then
112+ echo "cache: writing the devShell closure back"
113+ nix copy --no-check-sigs --all --to file:///nix-cache
114+fi
115+"""
116+env = { }
117+
118+[nix]
119+# No devShell warming at image build time: this enters `nix develop` at run
120+# time, on the VM, where the cache answers for its closure. The ptyshim that
121+# warming would need under gVisor is deprecated and does not come back.
122+flake = false
123+shim = false
124+
125+# [experimental] overrides the sandbox default of vm_runtime = true.
new file mode 100644
@@ -0,0 +1,125 @@
1+[container]
2+name = "frq-flutter-dev"
3+description = "the Flutter desktop build, incremental, in a nix devShell"
4+base = "arch-nix"
5+# A Sandbox, not a Function: runs on a real VM, and the command is
6+# the sandbox's own process so it dies when the command does.
7+runtime = "sandbox"
8+
9+[build]
10+# The container lives inside the repo it builds, so the copy is rooted two
11+# levels up and `.` is the whole tree.
12+context = "../.."
13+include = ["."]
14+
15+# Two volumes doing two different jobs. `nix-cache` is the binary cache every
16+# container here reads from and writes back to. `devshell` is the working
17+# state of a `nix develop` loop, and it is shared by every container that has
18+# one -- each gets its own directory under it, named for the devShell it
19+# belongs to, so two projects (or two shells of one project) never write the
20+# same tree. Modal Volumes have no locking, so the directories are the only
21+# thing keeping them apart, and two runs of the *same* devshell must not
22+# overlap.
23+[volumes]
24+nix-cache = "/nix-cache"
25+devshell = "/devshell"
26+
27+[resources]
28+cpu = 8
29+memory = 16384
30+timeout = 3600
31+
32+[run]
33+workdir = "/app"
34+# Nix for the dependencies, the ordinary toolchain for the build. `nix build`
35+# cannot do this: a derivation is all-or-nothing, so any edit is a fresh
36+# sandbox and a fresh compile of everything. Here the devShell supplies the
37+# compiler and the libraries, and `flutter build` decides what is stale --
38+# which is the whole reason `just flutter-desktop` exists as the working-tree
39+# loop rather than as another `nix build`.
40+#
41+# rsync and not cp, with --checksum and not mtimes: Modal copies the source in
42+# with fresh timestamps on every run, so a plain copy would look entirely new
43+# to Flutter and rebuild the lot. --checksum compares content, leaves the
44+# unchanged files' timestamps alone, and lets the incremental build work.
45+#
46+# The excludes are the state that must NOT be overwritten from /app -- it is
47+# what we are here to keep. `just flutter-desktop` seeds those caches only
48+# when they are missing, so finding them warm is all it takes.
49+command = """
50+set -e
51+# This container's own directory on the shared devshell volume, named for the
52+# devShell it keeps the state of. Anything else using this volume picks its
53+# own name and the two never meet.
54+SHELL_DIR=/devshell/frq-flutter-desktop
55+mkdir -p "$SHELL_DIR"
56+
57+# A worktree's `.git` is a *file* naming a gitdir back on the machine that
58+# copied it in, and nix believes it and goes looking for a path that is not
59+# here. It has to go before any flake reference to /app.
60+rm -rf /app/.git
61+
62+echo "sync: /app -> $SHELL_DIR"
63+# `nix shell --command` and not `nix profile install`: a profile install puts
64+# rsync in ~/.nix-profile/bin, which is not on the PATH of the shell already
65+# running, so the very next line said `rsync: command not found`.
66+#
67+# rsync and not cp, with --checksum and not mtimes: Modal copies the source in
68+# with fresh timestamps every run, so a plain copy looks entirely new to
69+# Flutter and rebuilds the lot. --checksum compares content and leaves the
70+# unchanged files' timestamps alone, which is the whole basis of the
71+# incremental build.
72+#
73+# The excludes are the state we are here to keep -- overwriting them from /app
74+# would defeat the volume. `just flutter-desktop` seeds those caches only when
75+# they are missing, so finding them warm is all it takes.
76+nix shell nixpkgs#rsync --accept-flake-config \
77+ --extra-substituters file:///nix-cache --command \
78+ rsync -a --checksum --delete \
79+ --exclude 'flutter/.home/' \
80+ --exclude 'flutter/.clojuredart/' \
81+ --exclude 'flutter/build/' \
82+ --exclude 'flutter/.dart_tool/' \
83+ --exclude '.git' \
84+ /app/ "$SHELL_DIR/"
85+
86+cd "$SHELL_DIR"
87+echo "state carried over:"
88+du -sh flutter/.home flutter/.clojuredart flutter/build 2>/dev/null \
89+ || echo " (none yet -- first run)"
90+
91+# Nix for the dependencies, the ordinary toolchain for the build. `nix build`
92+# cannot do this: a derivation is all-or-nothing, so any edit is a fresh
93+# sandbox and a fresh compile of everything. Here the devShell supplies the
94+# compiler and the libraries and `flutter build` decides what is stale.
95+# Evaluated from /app and built in the volume. Both halves matter: /app is the
96+# pristine copy, so nix stores a source tree of the repo rather than one
97+# carrying gigabytes of flutter/build, while the recipe still runs where the
98+# state it reuses lives -- `just -f` is what puts it there, since the recipe
99+# cds to its own justfile's directory.
100+nix develop /app#flutter-desktop --accept-flake-config \
101+ --extra-substituters file:///nix-cache \
102+ --max-jobs auto --command just -f "$SHELL_DIR/justfile" flutter-desktop
103+
104+echo "built:"
105+du -sh flutter/build
106+
107+# The devShell's closure is gigabytes of Flutter, Dart, clang and GTK, and the
108+# store it landed in belongs to the image rather than to a volume -- so
109+# without this every run re-fetches it from upstream. Written back, the next
110+# run substitutes it from file:///nix-cache instead.
111+if [ -f /nix-cache/nix-cache-info ]; then
112+ echo "cache: writing the devShell closure back"
113+ nix copy --no-check-sigs --all --to file:///nix-cache
114+fi
115+"""
116+env = { }
117+
118+[nix]
119+# No devShell warming at image build time: this enters `nix develop` at run
120+# time, on the VM, where the cache answers for its closure. The ptyshim that
121+# warming would need under gVisor is deprecated and does not come back.
122+flake = false
123+shim = false
124+
125+# [experimental] overrides the sandbox default of vm_runtime = true.
modified flake.nix +54 -9
@@ -699,14 +699,31 @@
699699
700700 src = lib.cleanSourceWith {
701701 src = ./.;
702- # Build trees and caches, which are large, machine-specific and
703- # would make every one of them a new store path.
704- filter = path: type:
705- let base = baseNameOf path; in
706- !(builtins.elem base [
707- "build" ".home" ".clojuredart" ".cpcache" "cljd-out"
708- ".dart_tool" "result" ".git" ".jolt" "buck-out"
709- ]);
702+ # Two trees, and only two: `flutter/` is the app and `common/`
703+ # is the screens its deps.edn puts on the classpath. The root is
704+ # still the source root because of that `../common`, but letting
705+ # the *whole* root in means every file in the repo is an input —
706+ # so editing flake.nix, or CLAUDE.md, or the jolt half in `src/`,
707+ # invalidated the entire Dart compile and paid ten minutes for a
708+ # change the Flutter build cannot even see.
709+ #
710+ # Matched on the path relative to the root rather than on
711+ # basename: `src` as a basename would also exclude `flutter/src`
712+ # and `common/src`, which is everything that matters.
713+ filter =
714+ let root = toString ./.; in
715+ path: type:
716+ let
717+ rel = lib.removePrefix (root + "/") (toString path);
718+ inTree = d: rel == d || lib.hasPrefix (d + "/") rel;
719+ in
720+ (inTree "flutter" || inTree "common")
721+ # Build trees and caches, which are large, machine-specific
722+ # and would make every one of them a new store path.
723+ && !(builtins.elem (baseNameOf path) [
724+ "build" ".home" ".clojuredart" ".cpcache" "cljd-out"
725+ ".dart_tool" "result" ".git" ".jolt" "buck-out"
726+ ]);
710727 };
711728 sourceRoot = "source/flutter";
712729
@@ -766,10 +783,38 @@
766783 let
767784 unwrapped =
768785 self.packages.${pkgs.stdenv.hostPlatform.system}.flutter-desktop-unwrapped;
786+
787+ # buildFlutterApplication's own wrapper appends a bare `/lib` to
788+ # LD_LIBRARY_PATH — the host's, not the bundle's. Off NixOS that
789+ # is a foreign library directory in front of nothing, and the app
790+ # dies in the loader before main: first
791+ #
792+ # /lib/libc.so.6: undefined symbol: __pointer_chk_guard
793+ #
794+ # and, once the store's glibc is put ahead of it,
795+ #
796+ # libc.so.6: version `GLIBC_2.43' not found
797+ # (required by /lib/libglib-2.0.so.0)
798+ #
799+ # which is the same bug wearing the other hat: the host's glib
800+ # against the store's glibc. Ordering cannot fix a mixture, so
801+ # the entry goes rather than moves. The wrapper is generated, so
802+ # this edits a copy and asserts the edit landed — a silent miss
803+ # here is a runtime failure on someone else's machine.
804+ fixed = pkgs.runCommand "frq-flutter-wrapper" { } ''
805+ mkdir -p "$out/bin"
806+ sed "s|'/lib'||g" ${unwrapped}/bin/frq > "$out/bin/frq"
807+ chmod +x "$out/bin/frq"
808+ if grep -q "'/lib'" "$out/bin/frq"; then
809+ echo "the /lib entry outlived the edit; look at the wrapper" >&2
810+ exit 1
811+ fi
812+ '';
813+
769814 script = pkgs.writeShellScript "frq" ''
770815 runner=""
771816 [ -e /run/current-system ] || runner="${nixGLFor pkgs}/bin/nixGLIntel"
772- exec ''${runner} ${unwrapped}/bin/frq "$@"
817+ exec ''${runner} ${fixed}/bin/frq "$@"
773818 '';
774819 in
775820 pkgs.runCommand "frq-flutter-0.1.0"
@@ -699,14 +699,31 @@
699 699
700 src = lib.cleanSourceWith {700 src = lib.cleanSourceWith {
701 src = ./.;701 src = ./.;
702- # Build trees and caches, which are large, machine-specific and702+ # Two trees, and only two: `flutter/` is the app and `common/`
703- # would make every one of them a new store path.703+ # is the screens its deps.edn puts on the classpath. The root is
704- filter = path: type:704+ # still the source root because of that `../common`, but letting
705- let base = baseNameOf path; in705+ # the *whole* root in means every file in the repo is an input —
706- !(builtins.elem base [706+ # so editing flake.nix, or CLAUDE.md, or the jolt half in `src/`,
707- "build" ".home" ".clojuredart" ".cpcache" "cljd-out"707+ # invalidated the entire Dart compile and paid ten minutes for a
708- ".dart_tool" "result" ".git" ".jolt" "buck-out"708+ # change the Flutter build cannot even see.
709- ]);709+ #
710+ # Matched on the path relative to the root rather than on
711+ # basename: `src` as a basename would also exclude `flutter/src`
712+ # and `common/src`, which is everything that matters.
713+ filter =
714+ let root = toString ./.; in
715+ path: type:
716+ let
717+ rel = lib.removePrefix (root + "/") (toString path);
718+ inTree = d: rel == d || lib.hasPrefix (d + "/") rel;
719+ in
720+ (inTree "flutter" || inTree "common")
721+ # Build trees and caches, which are large, machine-specific
722+ # and would make every one of them a new store path.
723+ && !(builtins.elem (baseNameOf path) [
724+ "build" ".home" ".clojuredart" ".cpcache" "cljd-out"
725+ ".dart_tool" "result" ".git" ".jolt" "buck-out"
726+ ]);
710 };727 };
711 sourceRoot = "source/flutter";728 sourceRoot = "source/flutter";
712 729
@@ -766,10 +783,38 @@
766 let783 let
767 unwrapped =784 unwrapped =
768 self.packages.${pkgs.stdenv.hostPlatform.system}.flutter-desktop-unwrapped;785 self.packages.${pkgs.stdenv.hostPlatform.system}.flutter-desktop-unwrapped;
786+
787+ # buildFlutterApplication's own wrapper appends a bare `/lib` to
788+ # LD_LIBRARY_PATH — the host's, not the bundle's. Off NixOS that
789+ # is a foreign library directory in front of nothing, and the app
790+ # dies in the loader before main: first
791+ #
792+ # /lib/libc.so.6: undefined symbol: __pointer_chk_guard
793+ #
794+ # and, once the store's glibc is put ahead of it,
795+ #
796+ # libc.so.6: version `GLIBC_2.43' not found
797+ # (required by /lib/libglib-2.0.so.0)
798+ #
799+ # which is the same bug wearing the other hat: the host's glib
800+ # against the store's glibc. Ordering cannot fix a mixture, so
801+ # the entry goes rather than moves. The wrapper is generated, so
802+ # this edits a copy and asserts the edit landed — a silent miss
803+ # here is a runtime failure on someone else's machine.
804+ fixed = pkgs.runCommand "frq-flutter-wrapper" { } ''
805+ mkdir -p "$out/bin"
806+ sed "s|'/lib'||g" ${unwrapped}/bin/frq > "$out/bin/frq"
807+ chmod +x "$out/bin/frq"
808+ if grep -q "'/lib'" "$out/bin/frq"; then
809+ echo "the /lib entry outlived the edit; look at the wrapper" >&2
810+ exit 1
811+ fi
812+ '';
813+
769 script = pkgs.writeShellScript "frq" ''814 script = pkgs.writeShellScript "frq" ''
770 runner=""815 runner=""
771 [ -e /run/current-system ] || runner="${nixGLFor pkgs}/bin/nixGLIntel"816 [ -e /run/current-system ] || runner="${nixGLFor pkgs}/bin/nixGLIntel"
772- exec ''${runner} ${unwrapped}/bin/frq "$@"817+ exec ''${runner} ${fixed}/bin/frq "$@"
773 '';818 '';
774 in819 in
775 pkgs.runCommand "frq-flutter-0.1.0"820 pkgs.runCommand "frq-flutter-0.1.0"