1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
[container]
name = "frq"
description = "nix build .#appimage -- the cosmic GUI, in one file"
base = "arch-nix"
# A Sandbox, not a Function: runs on a real VM, and the command is
# the sandbox's own process so it dies when the command does.
runtime = "sandbox"
[build]
# The container lives inside the repo it builds, so the copy is rooted two
# levels up and `.` is the whole tree -- uncommitted edits included, which is
# the point of copying rather than fetching.
context = "../.."
include = ["."]
# The build state a local checkout carries: 395MB of a 441MB repo, uploaded on
# every start and wanted by nothing out there. Flutter builds into a volume of
# its own, and the jolt and clojure caches are this machine's.
ignore = [
"flutter/build", "flutter/.home", "flutter/.dart_tool",
"flutter/.clojuredart", "flutter/.cpcache",
".jolt", ".cpcache", "result", "build", ".git",
]
# The Modal Volume that makes a second build cheap. Mounted at run time, which
# is when the build happens here -- nothing is written to it while the image is
# built, because a volume mount is not part of the resulting image.
[volumes]
nix-cache = "/nix-cache"
[resources]
cpu = 8
memory = 16384
# libjoltcosmic's tree is the long pole even when most of it substitutes.
timeout = 3600
[run]
workdir = "/app"
# The build, not the window: `#appimage` bundles `#frq` -- the cosmic GUI,
# whose frqScript runs `-m frq.cosmic` -- and nothing here tries to open it.
# There is no GL and no display on a build box, which is why this only builds.
#
# `#appimage` rather than `#frq` because of how the result gets home. A store
# path is only useful to a machine that can take its whole closure, which
# means walking ~200 narinfos out of the volume one call at a time and then
# importing them. The AppImage is that same closure squashed into one file:
# one `modal volume get`, no store import, and it runs off NixOS because the
# bundle carries the Mesa that nixGL puts the host driver in front of.
#
# `path:/app` and not `.`: a checkout copied in here brings its `.git` with
# it, and in a worktree that is a *file* naming a gitdir back on the host.
# Nix believes it, tries to open a repository that is not there, and fails
# before it evaluates anything. `path:` says plain directory and means it.
#
# The cache is read as a substituter and written with `nix copy --all`, but
# only when this run actually produced something the cache has not got. That
# test is the difference between a nine-minute run and a two-minute one: on a
# full hit `--all` still interrogates every one of the cache's thousands of
# paths to discover it has nothing to write, and that interrogation cost more
# than the build it exists to avoid. Asking whether the result's own narinfo
# is already there answers the same question in one stat.
#
# `--max-jobs auto` on the command line and not only in nix.conf: the base
# image carries that setting now, but only from the next `modal run
# arch_nix.py` onwards, and the flag costs nothing once it is redundant.
# Nix's default is 1 -- the whole graph end to end, one derivation at a time.
#
# `set -e` earns its place: the last command here is a `ls`, so without it a
# failed `nix build` would still leave the sandbox exiting 0 and the run would
# report success. The substituter test is an `if` rather than `&&` for the same
# reason -- under `set -e` a false `&&` would abort the whole script on the
# first run, when there is legitimately nothing in the cache yet.
command = """
set -e
mkdir -p /nix-cache/artifacts
subs=""
if [ -f /nix-cache/nix-cache-info ]; then
subs="--extra-substituters file:///nix-cache"
echo "cache: reading from /nix-cache"
else
echo "cache: empty, this run fills it"
fi
# One setting per invocation -- `nix config show a b` is an argument error,
# and under `set -e` that kills the run before it builds anything.
nix config show max-jobs
nix config show cores
out=$(nix build path:/app#appimage --accept-flake-config $subs \
--max-jobs auto --cores 0 --print-out-paths --print-build-logs)
echo "built: $out"
cp -L "$out" /nix-cache/artifacts/frq.AppImage
chmod +x /nix-cache/artifacts/frq.AppImage
ls -la /nix-cache/artifacts/frq.AppImage
hash=$(basename "$out" | cut -d- -f1)
if [ -f "/nix-cache/$hash.narinfo" ]; then
echo "cache: $hash already held, nothing to write"
else
echo "cache: writing the store back"
nix copy --no-check-sigs --all --to file:///nix-cache
fi
"""
env = { }
[nix]
# Every nix command in the container reads the mounted cache, including one
# typed by hand in a shell. Passing --extra-substituters per command only ever
# covered the scripts.
substituters = ["file:///nix-cache"]
# No devShell: we want `nix build`, not a shell to run something inside, so
# there is nothing to warm at build time and no reason for the ptyshim. The
# build happens at run time, in the Sandbox, on a real VM -- which is the
# whole reason it can realise a derivation at all.
flake = false
shim = false
# [experimental] overrides the sandbox default of vm_runtime = true.
# Setting it here turns that default off, so leave it alone unless you
# want gVisor.
|