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

Bring the window home in one file, and stop asking the cache what it knows

Two things the runs taught.

`#appimage` rather than `#frq`. A store path is only useful to a machine
willing to take its whole closure, and taking it means walking some two
hundred narinfos out of the volume one call at a time and then importing
them, signatures and trusted-user and all. The AppImage is the same closure
squashed into one file: one `modal volume get`, thirty seconds, 532MB, and
it runs off NixOS because the bundle carries the Mesa that nixGL wants to
put the host driver in front of. It lands in /nix-cache/artifacts/ so there
is something to fetch.

And `nix copy --all` now only runs when the result is not already cached.
A warm run built nothing, wrote nothing, and still spent nine hundred
seconds -- against a hundred-second build -- because `--all` interrogates
every path the cache holds before it can discover there is nothing to send.
One stat of the result's own narinfo answers that. Counting store paths
before and against after would not: a container starts from the image's
store, so the count always climbs, hit or miss.

The fat cache itself stays. It is what took the build from 1092
derivations to none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-13T21:43:06-07:00 Browse files
0450c08 parent: 1451151
modified containers/frq/container.toml +36 -25
@@ -1,6 +1,6 @@
11 [container]
22 name = "frq"
3-description = "nix build .#frq -- the cosmic GUI"
3+description = "nix build .#appimage -- the cosmic GUI, in one file"
44 base = "arch-nix"
55 # A Sandbox, not a Function: runs on a real VM, and the command is
66 # the sandbox's own process so it dies when the command does.
@@ -27,43 +27,43 @@ timeout = 3600
2727
2828 [run]
2929 workdir = "/app"
30-# The build, not the window: `#frq` is the cosmic GUI (its frqScript runs
31-# `-m frq.cosmic`), and nothing here tries to open it -- there is no GL and
32-# no display on a build box, which is exactly why this only builds.
30+# The build, not the window: `#appimage` bundles `#frq` -- the cosmic GUI,
31+# whose frqScript runs `-m frq.cosmic` -- and nothing here tries to open it.
32+# There is no GL and no display on a build box, which is why this only builds.
33+#
34+# `#appimage` rather than `#frq` because of how the result gets home. A store
35+# path is only useful to a machine that can take its whole closure, which
36+# means walking ~200 narinfos out of the volume one call at a time and then
37+# importing them. The AppImage is that same closure squashed into one file:
38+# one `modal volume get`, no store import, and it runs off NixOS because the
39+# bundle carries the Mesa that nixGL puts the host driver in front of.
3340 #
3441 # `path:/app` and not `.`: a checkout copied in here brings its `.git` with
3542 # it, and in a worktree that is a *file* naming a gitdir back on the host.
3643 # Nix believes it, tries to open a repository that is not there, and fails
3744 # before it evaluates anything. `path:` says plain directory and means it.
3845 #
39-# Two halves. The build reads from /nix-cache when there is something there to
40-# read, and `nix copy` writes what it produced back. Naming the substituter
41-# unconditionally would break the first run -- a directory with no
42-# nix-cache-info is not a binary cache yet -- so the test for it is the reason
43-# this is a script rather than a line.
44-#
45-# `--all`, deliberately fat: every path in the container's store goes up, not
46-# just the result's runtime closure. The closure alone is the cheap cache, and
47-# it only pays off while nothing changes -- nix substitutes libjoltcosmic and
48-# never looks at its inputs. Change a dependency, though, and the intermediate
49-# outputs are gone: crane's deps-only artifact, the toolchain, the vendored
50-# crates, all rebuilt from nothing. Those are exactly the paths a fat cache
51-# keeps, so a partial invalidation costs a partial rebuild rather than a whole
52-# one. It buys that with volume size and with upload time on every run.
46+# The cache is read as a substituter and written with `nix copy --all`, but
47+# only when this run actually produced something the cache has not got. That
48+# test is the difference between a nine-minute run and a two-minute one: on a
49+# full hit `--all` still interrogates every one of the cache's thousands of
50+# paths to discover it has nothing to write, and that interrogation cost more
51+# than the build it exists to avoid. Asking whether the result's own narinfo
52+# is already there answers the same question in one stat.
5353 #
5454 # `--max-jobs auto` on the command line and not only in nix.conf: the base
5555 # image carries that setting now, but only from the next `modal run
5656 # arch_nix.py` onwards, and the flag costs nothing once it is redundant.
5757 # Nix's default is 1 -- the whole graph end to end, one derivation at a time.
5858 #
59-# `set -e` earns its place: the last command here is `du`, so without it a
59+# `set -e` earns its place: the last command here is a `ls`, so without it a
6060 # failed `nix build` would still leave the sandbox exiting 0 and the run would
6161 # report success. The substituter test is an `if` rather than `&&` for the same
6262 # reason -- under `set -e` a false `&&` would abort the whole script on the
6363 # first run, when there is legitimately nothing in the cache yet.
6464 command = """
6565 set -e
66-mkdir -p /nix-cache
66+mkdir -p /nix-cache/artifacts
6767 subs=""
6868 if [ -f /nix-cache/nix-cache-info ]; then
6969 subs="--extra-substituters file:///nix-cache"
@@ -75,11 +75,22 @@ fi
7575 # and under `set -e` that kills the run before it builds anything.
7676 nix config show max-jobs
7777 nix config show cores
78-nix build path:/app#frq --accept-flake-config $subs \
79- --max-jobs auto --cores 0 \
80- --print-out-paths --print-build-logs
81-nix copy --no-check-sigs --all --to file:///nix-cache
82-echo "cache size: $(du -sh /nix-cache | cut -f1)"
78+
79+out=$(nix build path:/app#appimage --accept-flake-config $subs \
80+ --max-jobs auto --cores 0 --print-out-paths --print-build-logs)
81+echo "built: $out"
82+
83+cp -L "$out" /nix-cache/artifacts/frq.AppImage
84+chmod +x /nix-cache/artifacts/frq.AppImage
85+ls -la /nix-cache/artifacts/frq.AppImage
86+
87+hash=$(basename "$out" | cut -d- -f1)
88+if [ -f "/nix-cache/$hash.narinfo" ]; then
89+ echo "cache: $hash already held, nothing to write"
90+else
91+ echo "cache: writing the store back"
92+ nix copy --no-check-sigs --all --to file:///nix-cache
93+fi
8394 """
8495 env = { }
8596
@@ -1,6 +1,6 @@
1 [container]1 [container]
2 name = "frq"2 name = "frq"
3-description = "nix build .#frq -- the cosmic GUI"3+description = "nix build .#appimage -- the cosmic GUI, in one file"
4 base = "arch-nix"4 base = "arch-nix"
5 # A Sandbox, not a Function: runs on a real VM, and the command is5 # 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.6 # the sandbox's own process so it dies when the command does.
@@ -27,43 +27,43 @@ timeout = 3600
27 27
28 [run]28 [run]
29 workdir = "/app"29 workdir = "/app"
30-# The build, not the window: `#frq` is the cosmic GUI (its frqScript runs30+# The build, not the window: `#appimage` bundles `#frq` -- the cosmic GUI,
31-# `-m frq.cosmic`), and nothing here tries to open it -- there is no GL and31+# whose frqScript runs `-m frq.cosmic` -- and nothing here tries to open it.
32-# no display on a build box, which is exactly why this only builds.32+# There is no GL and no display on a build box, which is why this only builds.
33+#
34+# `#appimage` rather than `#frq` because of how the result gets home. A store
35+# path is only useful to a machine that can take its whole closure, which
36+# means walking ~200 narinfos out of the volume one call at a time and then
37+# importing them. The AppImage is that same closure squashed into one file:
38+# one `modal volume get`, no store import, and it runs off NixOS because the
39+# bundle carries the Mesa that nixGL puts the host driver in front of.
33 #40 #
34 # `path:/app` and not `.`: a checkout copied in here brings its `.git` with41 # `path:/app` and not `.`: a checkout copied in here brings its `.git` with
35 # it, and in a worktree that is a *file* naming a gitdir back on the host.42 # it, and in a worktree that is a *file* naming a gitdir back on the host.
36 # Nix believes it, tries to open a repository that is not there, and fails43 # Nix believes it, tries to open a repository that is not there, and fails
37 # before it evaluates anything. `path:` says plain directory and means it.44 # before it evaluates anything. `path:` says plain directory and means it.
38 #45 #
39-# Two halves. The build reads from /nix-cache when there is something there to46+# The cache is read as a substituter and written with `nix copy --all`, but
40-# read, and `nix copy` writes what it produced back. Naming the substituter47+# only when this run actually produced something the cache has not got. That
41-# unconditionally would break the first run -- a directory with no48+# test is the difference between a nine-minute run and a two-minute one: on a
42-# nix-cache-info is not a binary cache yet -- so the test for it is the reason49+# full hit `--all` still interrogates every one of the cache's thousands of
43-# this is a script rather than a line.50+# paths to discover it has nothing to write, and that interrogation cost more
44-#51+# than the build it exists to avoid. Asking whether the result's own narinfo
45-# `--all`, deliberately fat: every path in the container's store goes up, not52+# is already there answers the same question in one stat.
46-# just the result's runtime closure. The closure alone is the cheap cache, and
47-# it only pays off while nothing changes -- nix substitutes libjoltcosmic and
48-# never looks at its inputs. Change a dependency, though, and the intermediate
49-# outputs are gone: crane's deps-only artifact, the toolchain, the vendored
50-# crates, all rebuilt from nothing. Those are exactly the paths a fat cache
51-# keeps, so a partial invalidation costs a partial rebuild rather than a whole
52-# one. It buys that with volume size and with upload time on every run.
53 #53 #
54 # `--max-jobs auto` on the command line and not only in nix.conf: the base54 # `--max-jobs auto` on the command line and not only in nix.conf: the base
55 # image carries that setting now, but only from the next `modal run55 # image carries that setting now, but only from the next `modal run
56 # arch_nix.py` onwards, and the flag costs nothing once it is redundant.56 # arch_nix.py` onwards, and the flag costs nothing once it is redundant.
57 # Nix's default is 1 -- the whole graph end to end, one derivation at a time.57 # Nix's default is 1 -- the whole graph end to end, one derivation at a time.
58 #58 #
59-# `set -e` earns its place: the last command here is `du`, so without it a59+# `set -e` earns its place: the last command here is a `ls`, so without it a
60 # failed `nix build` would still leave the sandbox exiting 0 and the run would60 # failed `nix build` would still leave the sandbox exiting 0 and the run would
61 # report success. The substituter test is an `if` rather than `&&` for the same61 # report success. The substituter test is an `if` rather than `&&` for the same
62 # reason -- under `set -e` a false `&&` would abort the whole script on the62 # reason -- under `set -e` a false `&&` would abort the whole script on the
63 # first run, when there is legitimately nothing in the cache yet.63 # first run, when there is legitimately nothing in the cache yet.
64 command = """64 command = """
65 set -e65 set -e
66-mkdir -p /nix-cache66+mkdir -p /nix-cache/artifacts
67 subs=""67 subs=""
68 if [ -f /nix-cache/nix-cache-info ]; then68 if [ -f /nix-cache/nix-cache-info ]; then
69 subs="--extra-substituters file:///nix-cache"69 subs="--extra-substituters file:///nix-cache"
@@ -75,11 +75,22 @@ fi
75 # and under `set -e` that kills the run before it builds anything.75 # and under `set -e` that kills the run before it builds anything.
76 nix config show max-jobs76 nix config show max-jobs
77 nix config show cores77 nix config show cores
78-nix build path:/app#frq --accept-flake-config $subs \78+
79- --max-jobs auto --cores 0 \79+out=$(nix build path:/app#appimage --accept-flake-config $subs \
80- --print-out-paths --print-build-logs80+ --max-jobs auto --cores 0 --print-out-paths --print-build-logs)
81-nix copy --no-check-sigs --all --to file:///nix-cache81+echo "built: $out"
82-echo "cache size: $(du -sh /nix-cache | cut -f1)"82+
83+cp -L "$out" /nix-cache/artifacts/frq.AppImage
84+chmod +x /nix-cache/artifacts/frq.AppImage
85+ls -la /nix-cache/artifacts/frq.AppImage
86+
87+hash=$(basename "$out" | cut -d- -f1)
88+if [ -f "/nix-cache/$hash.narinfo" ]; then
89+ echo "cache: $hash already held, nothing to write"
90+else
91+ echo "cache: writing the store back"
92+ nix copy --no-check-sigs --all --to file:///nix-cache
93+fi
83 """94 """
84 env = { }95 env = { }
85 96