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>
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 @@ | ||
| 699 | 699 | |
| 700 | 700 | src = lib.cleanSourceWith { |
| 701 | 701 | 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 | + ]); | |
| 710 | 727 | }; |
| 711 | 728 | sourceRoot = "source/flutter"; |
| 712 | 729 | |
| @@ -766,10 +783,38 @@ | ||
| 766 | 783 | let |
| 767 | 784 | unwrapped = |
| 768 | 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 | 814 | script = pkgs.writeShellScript "frq" '' |
| 770 | 815 | runner="" |
| 771 | 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 | 819 | in |
| 775 | 820 | 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 and | 702 | + # 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; in | 705 | + # 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 | let | 783 | 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 | in | 819 | in |
| 775 | pkgs.runCommand "frq-flutter-0.1.0" | 820 | pkgs.runCommand "frq-flutter-0.1.0" |