| 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 |
|
| 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 |
|