Skip to main content

max / alloy

9.5 KB · 209 lines History Blame Raw
1 # 2. Building your image
2
3 Alloy publishes source and never publishes a binary image. You build the
4 installer ISO yourself, on a machine you already own.
5
6 That is not an interim state waiting on infrastructure. It is the distribution
7 model: what you boot is the thing you configured, and Make Creative hosts no
8 registry, signs no images, and redistributes nobody's binaries.
9
10 ## What you need
11
12 - **A Linux machine with podman.** The build runs rootful.
13 - **Disk.** The image, the container cache, and the ISO working directory
14 together want tens of gigabytes free.
15 - **Time.** Around fifteen minutes on a current laptop with a fast mirror,
16 most of it in three roughly equal parts: compiling the console and the
17 terminal, pulling and installing around 600 packages, and assembling the
18 ISO. A slow connection moves the second part and dominates the rest.
19
20 There is no path to an Alloy ISO from macOS or Windows. The build needs Linux
21 with podman, and nothing emulates that cheaply enough to recommend.
22
23 ## Get the source
24
25 git clone https://makenot.work/git/max/alloy
26 cd alloy
27
28 ## Build the ISO
29
30 build/build-iso.sh
31
32 That builds the bootc image (`localhost/alloy:local`), then wraps it into an
33 installer ISO under `output/`. The ISO carries the image twice: once as the
34 live system you boot, and once as the source that gets installed to the disk.
35
36 Useful flags:
37
38 | Flag | What it does |
39 |---|---|
40 | `--skip-build` | Reuse the image already in container storage |
41 | `--fast` | Reuse the image and compress cheaply. For iterating on the boot chain |
42 | `--skip-source` | Omit the install source. The ISO boots but cannot install |
43 | `--update-target host:5000/alloy:43` | Bake an update registry into the medium (development builds only) |
44
45 Without `--update-target`, machines installed from the medium never fetch
46 updates. That is the intended state for anything but a development build.
47
48 ## Write it to a USB stick
49
50 Any of the usual tools work on the ISO in `output/`. If you would rather have
51 the write checked for you, both build scripts take the same two flags:
52
53 build/build-iso.sh --write-only --write /dev/sdX
54
55 It refuses partitions, refuses anything with a mounted filesystem, demands an
56 explicit device path plus a typed confirmation, and verifies the result with
57 `cmp` before claiming success.
58
59 ## Building a disk image instead
60
61 `build/build-image.sh` produces raw or qcow2 disk images through
62 bootc-image-builder, which is what you want for a VM:
63
64 build/build-image.sh --type qcow2
65
66 It refuses to build ISOs on purpose. Every ISO type that tool offers composes
67 an Anaconda installer, and an Anaconda install of Alloy leaves a machine with
68 no account and a locked root. The Alloy installer ISO comes from
69 `build/build-iso.sh` and nowhere else.
70
71 ## Baking in a key and a hostname
72
73 A medium can carry the public key and the machine name the install will use,
74 which is what makes a headless install possible: you reach the installer over
75 SSH rather than typing at a console the machine does not have.
76
77 build/build-iso.sh \
78 --build-arg ALLOY_SSH_KEY="$(cat ~/.ssh/id_ed25519.pub)" \
79 --build-arg ALLOY_HOSTNAME=workshop
80
81 `ALLOY_SSH_KEY` takes the key itself and not a path to it, because the build
82 container cannot see your filesystem. The build refuses anything that is not a
83 public key, and refuses outright if it looks like a private one.
84
85 The key lands at `/usr/lib/alloy/authorized_keys` on the medium and does two
86 jobs from there. It is the credential for the `installer` account, which exists
87 only on the live medium and drops you straight into `alloy install`. And the
88 installer's account screen offers it back to you as the key for the machine
89 being installed, so the credential you connect with is the one that ends up on
90 the finished system unless you replace it.
91
92 `alloy image` is the same thing with a form around it: it asks for a profile, a
93 browser, languages, a database, what to trim, a hostname and a path to a public
94 key, and runs the script above with what you chose.
95
96 Chapter 3 covers the install itself, and what the SSH posture means once the
97 machine is up.
98
99 ## Changing what goes in
100
101 The Containerfile at the repo root is the whole package list, and it is meant
102 to be read and edited. Adding a package is a line in the right group; removing
103 one is deleting a line. Two cautions:
104
105 - The build asserts a number of things it depends on (that `satty`, `grim`,
106 `slurp`, `jq` and `notify-send` exist for the screenshot script, that the
107 fontconfig picks resolve, that the polkit rules name actions that exist).
108 Removing a package can trip an assertion, which is the build telling you the
109 removal has consequences somewhere else.
110 - Anything you add that Alloy ships should get a line in
111 `crates/alloy/credits.toml`, which is what the installer's credits page
112 reads. It is hand-curated on purpose, so nothing adds itself.
113
114 ## Carrying a database
115
116 `DB` decides whether the image ships PostgreSQL, and it defaults to `none`. A
117 stock mint carries no database and nothing about it changes by this dial
118 existing.
119
120 podman build --build-arg DB=postgres16 ...
121
122 `postgres16` installs `postgresql16` and `postgresql16-server` from `updates`:
123 three packages, 9 MiB of download and 38 MiB installed. It is deliberately not
124 Fedora's own `postgresql-server`, which is 18.4. The build host runs Sando's
125 scratch cluster to test MNW against what production runs, and production is on
126 16, so a cluster two majors ahead would test a database nobody deploys.
127
128 The arm puts the binaries on `PATH` and stops. No `systemctl enable`, no
129 `initdb`: a cluster is state belonging to the machine that runs it rather than
130 to the image.
131
132 ## When the build cannot reach the fonts
133
134 Alloy cuts its two faces during the build from a pinned upstream base, and the
135 base is fetched from raw.githubusercontent.com, which rate-limits by IP. A busy
136 network or a shared address can earn an HTTP 429. The fetch retries five times
137 before it gives up, and curl retries a 429 like any other transient answer, so
138 what gets through to you is a host that stayed unwilling.
139
140 One build asks that host three times: the two faces the image cuts, and a third
141 that the terminal's own build script cuts while `cargo install shop` runs. The
142 seed below covers the first two. It does not cover the terminal's, which is the
143 fetch that failed in August, because the terminal's build script names its own
144 cache directory and nothing in the image build can point it elsewhere.
145
146 What covers all three is a mirror, and the build takes one by default:
147 `QUASI_TYPE_MIRROR` is set to `https://makenot.work/static/bases`, where the
148 pinned files are served under the sha256 each pin already carries. The tool
149 tries the mirror first and falls back to the pinned upstream url, and it checks
150 the bytes against that digest either way, so a mirror can serve the pinned file
151 or nothing. A build with no access to makenot.work is a slower first build and
152 nothing more; to switch it off,
153
154 build/build-iso.sh --build-arg QUASI_TYPE_MIRROR=
155
156 The build log says which way it went. Warming the cargo target directory still
157 covers the terminal's fetch on a host that has built once.
158
159 To seed the two the image cuts, put the four files in `build/base-cache/` (its
160 README names them and says where to copy them from). They are carried into the
161 build and used in place of a download. To make the cut refuse to reach out at
162 all:
163
164 build/build-iso.sh --build-arg QUASI_BASES=sealed
165
166 If the seed is short of a file the pins name, that fails before the tool is
167 built, listing what is missing, rather than at the cut. It seals the cut and
168 nothing else: the build still clones two repositories, resolves crates, installs
169 packages, and cuts the terminal's face.
170
171 Keeping the bases outside the repo works too, with `podman build -v
172 <dir>:/base-cache:ro`, but neither wrapper script forwards a `-v`, and a bare
173 `podman build` produces an image with no build stamp (see docs/IMAGE.md). Copy
174 the files in instead unless you know you want that.
175
176 ## What the base ships that you do not need
177
178 The Fedora base is a general-purpose server image, and three of the things it
179 carries cannot be reached from an Alloy install: an AWS SDK, eighteen
180 architectures of `qemu-user-static`, and toolbox, whose job distrobox already
181 does. The build drops them.
182
183 build/build-iso.sh --build-arg TRIM=keep
184
185 keeps them, and the one capability that comes back with them is running
186 containers built for a foreign architecture.
187
188 `TRIM` decides one more thing, and it is the one you are likelier to notice.
189 A trimmed build installs no translations beyond English and no documentation,
190 which is about 263 MB and the largest cut in the image. Documentation includes
191 man pages, because that is the only way rpm files them, so `man` on a default
192 Alloy has nothing to read for anything installed after the image was built.
193 `TRIM=keep` brings back every language and every page. Chapter 7 has the
194 day-to-day version of this.
195
196 Do not expect a smaller ISO from this. Removing a base package takes 304 MB
197 out of `/usr` and 3.8 MB off the image, because the base hardlinks its `/usr`
198 into an ostree repo that ships in the image and that the removal cannot prune.
199 The packages are gone from the installed system, which is where the saving
200 should land; the medium you write is the same size either way.
201
202 Firmware is never trimmed. A medium you build has to boot hardware nobody
203 asked about when it was built, so every firmware package the base ships stays
204 in, and the build fails rather than producing an image that lost one.
205
206 Two builds a week apart can differ: the RPM set is not pinned to a snapshot
207 yet. Fonts and the Rust dependency tree are pinned; the Fedora packages are
208 not.
209