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

container.toml · 233 lines · 11.2 KBTOML Blame HistoryRaw
A third target, and the seam that was already waiting for it f54ca45 nandi 2d ago1[container]
2name = "frq-flutter-web"
3description = "the Flutter web build, incremental, in a nix devShell"
4base = "arch-nix"
5# A Sandbox, not a Function: it runs on a real VM, the command is the
6# sandbox's own process so it dies when the command does, and only a Sandbox
7# can hold open a tunnel -- which is the whole of `serve`.
8runtime = "sandbox"
9
10[build]
11# The container lives inside the repo it builds, so the copy is rooted two
12# levels up and `.` is the whole tree.
13context = "../.."
14include = ["."]
15# The devShell, baked in rather than entered -- `flutter-dev`'s trick and for
16# its reasons. `print-dev-env` writes the whole environment out as shell and
17# realises its inputs on the way, so the closure becomes an image layer
18# instead of a fetch every container pays for; sourcing it from .bashrc means
19# a shell attached to this container *is* the devShell.
20#
21# `dev` stays for the case where the baked env is stale against a flake edit.
22# The toolchain layer, and what it is allowed to depend on.
23#
24# `warm` is copied before `setup` runs and is deliberately six files: the
25# flake and its lock, plus the four `cljd-deps` actually reads -- it does
26# `cp ${./common/deps.edn}`, `${./flutter/deps.edn}`, `${./flutter/pubspec.yaml}`
27# and `${./flutter/pubspec.lock}` and nothing else. That is the whole of what
28# `nix develop .#flutter-web` needs to evaluate, so this layer moves when a
29# dependency moves and not when a line of ClojureDart does.
30#
31# The point of the split: `[build] commands` run after the full source copy,
32# so editing `flutter/src/frq/net/web.cljd` used to invalidate them and spend
33# minutes re-warming a devShell that had not changed. Here the image is built
34# from the toolchain and the program is built in the sandbox, which is where
35# it was always going to happen anyway.
36warm = [
37 "flake.nix", "flake.lock",
38 "common/deps.edn",
39 "flutter/deps.edn", "flutter/pubspec.yaml", "flutter/pubspec.lock",
40]
41# The devShell, baked in rather than entered. `print-dev-env` writes the whole
42# environment out as shell and realises its inputs on the way, so the closure
43# becomes an image layer instead of a fetch every container pays for; sourcing
44# it from .bashrc means a shell attached to this container *is* the devShell.
45#
46# Best-effort, and the reason is gVisor. An image build is a Function
47# underneath, so it cannot *build* a derivation -- and `print-dev-env` realises
48# the shell's inputs, which here includes `flutter-wrapped-…-sdk-links.drv`,
49# in no binary cache and therefore built. Under gVisor that dies with
50# `unexpected EOF reading a line`, and the ptyshim that papered over it is
51# deprecated. Nothing here is load-bearing: [run] enters `nix develop` itself,
52# on the VM, where a real pty makes the same build work.
53setup = [
54 "nix print-dev-env /app#flutter-web --accept-flake-config --extra-substituters file:///nix-cache > /etc/devshell.sh || rm -f /etc/devshell.sh",
55 "echo '[ -s /etc/devshell.sh ] && . /etc/devshell.sh' >> /root/.bashrc",
56 "printf '#!/bin/sh\\nexec nix develop /app#flutter-web \"$@\"\\n' > /usr/local/bin/dev && chmod +x /usr/local/bin/dev",
57]
58# The build state a local checkout carries, wanted by nothing out here: this
59# container builds into a volume of its own, and the jolt and clojure caches
60# are the laptop's.
61ignore = [
62 "flutter/build", "flutter/.home", "flutter/.dart_tool",
63 "flutter/.clojuredart", "flutter/.cpcache",
64 ".jolt", ".cpcache", "result", "build", ".git",
65]
66
67# `nix-cache` is the binary cache every container here reads from and writes
68# back to. `devshell` is the working state of a `nix develop` loop, shared by
69# every container that has one -- each gets its own directory under it, named
70# for the devShell it belongs to, so `flutter-web` and `flutter-desktop` never
71# write the same tree even though they share a `.home`-shaped cache layout.
72# Modal Volumes have no locking, so those directory names are the only thing
73# keeping them apart, and two runs of the *same* devshell must not overlap.
74[volumes]
75nix-cache = "/nix-cache"
76devshell = "/devshell"
77
78[resources]
79cpu = 8
80memory = 16384
81timeout = 3600
82
83# The port `just flutter-web serve` listens on, tunnelled out. Nothing is
84# served unless the command asks for it -- a plain build exits and the tunnel
85# closes with the sandbox -- but the port has to be declared at create time,
86# so it is declared once here and `modal run --command` decides whether
87# anything ever binds it.
88[network]
89ports = [8080]
90
91[run]
92workdir = "/app"
93# Nix for the dependencies, the ordinary toolchain for the build -- the
94# `flutter-dev` argument, unchanged: a derivation is all-or-nothing, so any
95# edit under `nix build` is a fresh sandbox and a fresh compile of everything.
96# Here the devShell supplies dart2js and the engine artifacts and `flutter
97# build web` decides what is stale.
98command = """
99set -e
100# This container's own directory on the shared devshell volume, named for the
101# devShell whose state it keeps. `flutter-desktop` has its own beside it.
102SHELL_DIR=/devshell/frq-flutter-web
103mkdir -p "$SHELL_DIR" "$SHELL_DIR/.cache"
104
105# A worktree's `.git` is a *file* naming a gitdir back on the machine that
106# copied it in, and nix believes it and goes looking for a path that is not
107# here. It has to go before any flake reference to /app.
108rm -rf /app/.git
109
110echo "sync: /app -> $SHELL_DIR"
111# `nix shell --command` and not `nix profile install`: a profile install puts
112# rsync in ~/.nix-profile/bin, which is not on the PATH of the shell already
113# running.
114#
115# rsync and not cp, with --checksum and not mtimes: Modal copies the source in
116# with fresh timestamps every run, so a plain copy looks entirely new to
117# Flutter and rebuilds the lot. --checksum compares content and leaves the
118# unchanged files' timestamps alone, which is the whole basis of the
119# incremental build.
120#
121# The excludes are the state we are here to keep -- overwriting them from /app
122# would defeat the volume. `flutter/web/` is NOT on the list: it is committed
123# now, because the OAuth client keeps a script there, so it has to arrive from
124# /app like any other source.
125nix shell nixpkgs#rsync --accept-flake-config \
126 --extra-substituters file:///nix-cache --command \
127 rsync -a --checksum --delete \
128 --exclude 'flutter/.home/' \
129 --exclude 'flutter/.clojuredart/' \
130 --exclude 'flutter/build/' \
131 --exclude 'flutter/.dart_tool/' \
132 --exclude '.git' \
133 /app/ "$SHELL_DIR/"
134
135cd "$SHELL_DIR"
136echo "state carried over:"
137du -sh flutter/.home flutter/.clojuredart flutter/build 2>/dev/null \
138 || echo " (none yet -- first run)"
139
140# Evaluated from /app and built in the volume, as `flutter-dev` does and for
141# the same two reasons: /app is the pristine copy, so nix stores a source tree
142# of the repo rather than one carrying gigabytes of flutter/build, while the
143# recipe still runs where the state it reuses lives. `just -f` is what puts it
144# there, since the recipe cds to its own justfile's directory.
145# The baked devShell if there is one, and `nix develop` if there is not.
146#
147# This is the difference between a two-minute rebuild and a three-minute one.
148# `nix develop /app#flutter-web` re-copies the whole repo into the nix store
149# and re-evaluates the flake on every run -- the flake's source is the tree,
150# so any edit makes it a new source -- and all of that to arrive at an
151# environment the image already computed with `print-dev-env` and wrote to
152# /etc/devshell.sh. Sourcing it is the same PATH and the same variables with
153# no evaluation at all.
154#
155# The fallback is not decoration: that setup step is best-effort, because
156# realising the devShell under gVisor can fail (see [build] setup), and a
157# container whose bake did not happen still has to build.
158# The devShell environment, computed once and kept on the volume.
159#
160# `nix develop /app#flutter-web` re-copies the whole repo into the nix store
161# and re-evaluates the flake on every run -- the flake's source IS the tree,
162# so any edit makes it a new source -- to arrive at an environment that has
163# not changed. `print-dev-env` writes that environment out as shell, and
164# sourcing it is the same PATH and the same variables with no evaluation.
165#
166# Here and not in [build] setup, which is where it used to be: an image build
167# is a Function under gVisor, where nix cannot realise a derivation, and this
168# devShell's closure contains one no cache can answer for
169# (flutter-wrapped-...-sdk-links). That bake failed every time and was made
170# non-fatal, which meant it silently never happened. On the VM it works, and
171# the volume is what makes it worth doing once.
172#
173# Regenerated when flake.lock's CONTENT changes -- by hash and never by mtime.
174# Modal copies /app in with fresh timestamps on every run, so `-nt` says the
175# lock is newer every single time and the cache never hits. That is the same
176# trap the rsync above documents and works around with --checksum; it catches
177# anything here that asks a file when it changed.
178# Beside the working tree and NOT inside it: the rsync above runs with
179# --delete, so anything under $SHELL_DIR that is not in /app is removed on
180# every run. Kept in there, this cache was deleted moments before it was
181# consulted, which is why it recomputed every time while claiming to be a
182# cache.
183ENV_DIR="$SHELL_DIR.env"
184mkdir -p "$ENV_DIR"
185ENV_SH="$ENV_DIR/devshell.sh"
186STAMP="$ENV_DIR/devshell.lock"
187WANT="$(sha256sum /app/flake.lock | cut -d' ' -f1)"
188if [ ! -s "$ENV_SH" ] || [ "$(cat "$STAMP" 2>/dev/null)" != "$WANT" ]; then
189 echo "devshell: computing (first run, or flake.lock changed)"
190 nix print-dev-env /app#flutter-web --accept-flake-config \
191 --extra-substituters file:///nix-cache > "$ENV_SH.tmp"
192 mv "$ENV_SH.tmp" "$ENV_SH"
193 echo "$WANT" > "$STAMP"
194else
195 echo "devshell: reusing the computed env"
196fi
197
198# A subshell, so what the env sets does not leak into the `nix copy` below --
199# that wants the container's own nix, not the shell's. `set +u` because a
200# printed dev env references variables that need not be set.
201( set +u; . "$ENV_SH"; set -u
202 just -f "$SHELL_DIR/justfile" flutter-web )
203
204echo "built:"
205du -sh flutter/build/web
206
207# The devShell's closure is gigabytes of Flutter and Dart, and the store it
208# landed in belongs to the image rather than to a volume -- so without this
209# every run re-fetches it from upstream. Written back, the next run
210# substitutes it from file:///nix-cache instead.
211if [ -f /nix-cache/nix-cache-info ]; then
212 echo "cache: writing the devShell closure back"
213 nix copy --no-check-sigs --all --to file:///nix-cache
214fi
215"""
216# Nix's own cache, on the volume rather than in the container. Without it
217# every Sandbox starts empty and `nix develop` re-clones the flake's git
218# inputs, because flake.lock pins which revision to fetch and not whether it
219# is already on disk. Set here rather than in the command so an interactive
220# shell into this container gets it too.
221env = { XDG_CACHE_HOME = "/devshell/frq-flutter-web/.cache" }
222
223[nix]
224# Every nix command in the container reads the mounted cache, including one
225# typed by hand in a shell.
226substituters = ["file:///nix-cache"]
227# No devShell warming at image build time: this enters `nix develop` at run
228# time, on the VM, where the cache answers for its closure. The ptyshim that
229# warming would need under gVisor is deprecated and does not come back.
230flake = false
231shim = false
232
233# [experimental] overrides the sandbox default of vm_runtime = true.