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>
0450c08 parent: 1451151 modified
containers/frq/container.toml +36 -25 | @@ -1,6 +1,6 @@ | ||
| 1 | 1 | [container] |
| 2 | 2 | name = "frq" |
| 3 | -description = "nix build .#frq -- the cosmic GUI" | |
| 3 | +description = "nix build .#appimage -- the cosmic GUI, in one file" | |
| 4 | 4 | base = "arch-nix" |
| 5 | 5 | # A Sandbox, not a Function: runs on a real VM, and the command is |
| 6 | 6 | # the sandbox's own process so it dies when the command does. |
| @@ -27,43 +27,43 @@ timeout = 3600 | ||
| 27 | 27 | |
| 28 | 28 | [run] |
| 29 | 29 | 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. | |
| 33 | 40 | # |
| 34 | 41 | # `path:/app` and not `.`: a checkout copied in here brings its `.git` with |
| 35 | 42 | # it, and in a worktree that is a *file* naming a gitdir back on the host. |
| 36 | 43 | # Nix believes it, tries to open a repository that is not there, and fails |
| 37 | 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 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. | |
| 53 | 53 | # |
| 54 | 54 | # `--max-jobs auto` on the command line and not only in nix.conf: the base |
| 55 | 55 | # image carries that setting now, but only from the next `modal run |
| 56 | 56 | # arch_nix.py` onwards, and the flag costs nothing once it is redundant. |
| 57 | 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 a | |
| 59 | +# `set -e` earns its place: the last command here is a `ls`, so without it a | |
| 60 | 60 | # failed `nix build` would still leave the sandbox exiting 0 and the run would |
| 61 | 61 | # report success. The substituter test is an `if` rather than `&&` for the same |
| 62 | 62 | # reason -- under `set -e` a false `&&` would abort the whole script on the |
| 63 | 63 | # first run, when there is legitimately nothing in the cache yet. |
| 64 | 64 | command = """ |
| 65 | 65 | set -e |
| 66 | -mkdir -p /nix-cache | |
| 66 | +mkdir -p /nix-cache/artifacts | |
| 67 | 67 | subs="" |
| 68 | 68 | if [ -f /nix-cache/nix-cache-info ]; then |
| 69 | 69 | subs="--extra-substituters file:///nix-cache" |
| @@ -75,11 +75,22 @@ fi | ||
| 75 | 75 | # and under `set -e` that kills the run before it builds anything. |
| 76 | 76 | nix config show max-jobs |
| 77 | 77 | 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 | |
| 83 | 94 | """ |
| 84 | 95 | env = { } |
| 85 | 96 | |
| @@ -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 is | 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. | 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 runs | 30 | +# 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 and | 31 | +# 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` with | 41 | # `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 fails | 43 | # 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 to | 46 | +# 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 substituter | 47 | +# only when this run actually produced something the cache has not got. That |
| 41 | -# unconditionally would break the first run -- a directory with no | 48 | +# 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 reason | 49 | +# 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, not | 52 | +# 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 base | 54 | # `--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 run | 55 | # 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 a | 59 | +# `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 would | 60 | # 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 same | 61 | # 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 the | 62 | # 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 -e | 65 | set -e |
| 66 | -mkdir -p /nix-cache | 66 | +mkdir -p /nix-cache/artifacts |
| 67 | subs="" | 67 | subs="" |
| 68 | if [ -f /nix-cache/nix-cache-info ]; then | 68 | 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-jobs | 76 | nix config show max-jobs |
| 77 | nix config show cores | 77 | 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-logs | 80 | + --max-jobs auto --cores 0 --print-out-paths --print-build-logs) |
| 81 | -nix copy --no-check-sigs --all --to file:///nix-cache | 81 | +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 | ||