Skip to main content

max / alloy

Stamp the build profile into os-release VARIANT fields
Author: Max Johnson <me@maxj.phd> · 2026-08-25 20:42 UTC
Signed with PGP, not checked
Commit: 7b42fc27cd83fd76d8c2e6ec31a8fca665703bbe
Parent: 90b0b11
4 files changed, +62 insertions, -3 deletions
@@ -3653,6 +3653,38 @@
3653 3653 test "$(cat /usr/lib/alloy/profile)" = "$PROFILE" \
3654 3654 || { echo "the profile marker did not land" >&2; exit 1; }
3655 3655
3656 + # The profile, recorded where every os-release reader can see it.
3657 + #
3658 + # The committed file carries `<profile>` on both fields, the same
3659 + # placeholder idiom as `build <n>`: a value that never got rewritten reads
3660 + # as obviously broken rather than as a plausible wrong answer. What stood
3661 + # there before was `VARIANT_ID=base`, which every image of both profiles
3662 + # shipped, so anything keying off it (a dotfile, a support question, a
3663 + # `systemd-analyze` dump) was told nothing.
3664 + #
3665 + # Downstream of the PROFILE `case` near the top, so the value is already
3666 + # known to be one of the two words and the display half can be written by
3667 + # hand rather than derived.
3668 + #
3669 + # The profile alone, never the tag variants. `firewall-server` and
3670 + # `usbgate-server` are image tags chosen by whoever runs the build; the
3671 + # Containerfile is never told which one it is making, so there is nothing
3672 + # here to stamp them from.
3673 + RUN set -eu; \
3674 + case "$PROFILE" in \
3675 + client) variant="Client" ;; \
3676 + server) variant="Server" ;; \
3677 + esac; \
3678 + sed -i "s/^VARIANT=.*/VARIANT=\"$variant\"/; \
3679 + s/^VARIANT_ID=.*/VARIANT_ID=$PROFILE/" /usr/lib/os-release; \
3680 + grep -q "^VARIANT_ID=$PROFILE$" /usr/lib/os-release \
3681 + || { echo "the profile did not land in os-release" >&2; exit 1; }; \
3682 + grep -q "^VARIANT=\"$variant\"$" /usr/lib/os-release \
3683 + || { echo "the profile display name did not land in os-release" >&2; exit 1; }; \
3684 + if grep -q '<profile>' /usr/lib/os-release; then \
3685 + echo "the profile placeholder is still in os-release" >&2; exit 1; \
3686 + fi
3687 +
3656 3688 RUN systemctl preset-all
3657 3689
3658 3690 # =====================================================================
M docs/IMAGE.md +1 -1
@@ -103,7 +103,7 @@
103 103 5. **Config tree:** the tree at `etc/skel/.config/*` (new-user defaults), `etc/*` (system-wide), and `usr/*`, including `usr/share/polkit-1/rules.d/*` (which system settings the console may change without a prompt) in the repo maps 1:1 into the image. The polkit rule is asserted at build time against the actions the image actually defines, since a grant naming a renamed action is inert and silent about it.
104 104 6. **Rendered tree:** everything in the image that carries a color is not in the repo as a finished file. `templates/` holds it with the palette left as tokens, and `skelgen` renders it against the two Akari themes into a second tree that mirrors `/` the same way the config tree does. Themed skeleton files render twice: the light one lands at `etc/skel/<rel>`, the dark one at `usr/share/alloy/skel-night/<rel>`, and `alloy theme apply` copies whichever the user's mode file names into `$HOME` at login. The build asserts the two trees are a bijection and that they do not overlap the repo's own `etc/skel`, because a themed file that quietly loses its dark render leaves a light sway border on a dark desktop and nothing else.
105 105 7. **Systemd presets:** which services are enabled by default (syncthing off by default, gammastep off until enrolled, alloy-hinged conditionally on FW12, etc.).
106 - 8. **Branding:** os-release, plymouth splash. The build stamps the image's build number into os-release here; see [Version fields](#version-fields).
106 + 8. **Branding:** os-release, plymouth splash. The build stamps the image's build number into os-release here; see [Version fields](#version-fields). It stamps the profile in the same way, rewriting the committed `VARIANT="<profile>"` and `VARIANT_ID=<profile>` placeholders to `Client`/`client` or `Server`/`server`, and failing if either placeholder survives. Only the profile: the tag variants (`firewall-server`, `usbgate-server`) are names chosen by whoever runs the build, and the Containerfile is never told which one it is making.
107 107 9. **Validation:** `bootc container lint` runs at build.
108 108
109 109 **A package added here gets a line in `crates/alloy/credits.toml`.** The installer's last screen names the projects Alloy ships and their licenses, off a hand-curated manifest rather than a generated closure, so nothing adds itself. The manifest is embedded in the console binary with `include_str!`, which means the page cannot go missing on installer media or a read-only deployment and also means a manifest edit needs a rebuild. Its own header says which license to record: for anything packaged out of Rust or Go, Fedora's `%{LICENSE}` is the whole vendored closure rather than the project's own terms, so read upstream's LICENSE for those and use `rpm -q --qf '%{LICENSE}'` only for the C packages.
@@ -5,8 +5,8 @@
5 5 ALLOY_BASE="43"
6 6 ID=alloy
7 7 ID_LIKE=fedora
8 - VARIANT="Base"
9 - VARIANT_ID=base
8 + VARIANT="<profile>"
9 + VARIANT_ID=<profile>
10 10 ANSI_COLOR="0;38;2;138;69;48"
11 11 LOGO=alloy
12 12 DEFAULT_HOSTNAME=alloy
@@ -217,6 +217,33 @@
217 217 // there would be a plausible-looking number on any machine where the stamping
218 218 // stopped working, which is worse than an absent field: `alloy --version` drops
219 219 // the build clause and says so by saying less.
220 + // `VARIANT`/`VARIANT_ID` are the same shape of rewrite and failed the same
221 + // way for longer: the committed file said `base` on both, no build step ever
222 + // touched them, and every image of both profiles shipped claiming to be a
223 + // variant that does not exist. A placeholder makes the unrewritten state
224 + // visible, and this pairs the two halves so an edit that drops either one
225 + // fails here rather than in an image nobody inspects.
226 + #[test]
227 + fn the_committed_os_release_carries_the_profile_placeholder() {
228 + let os_release = read("usr/lib/os-release");
229 + for line in ["VARIANT=\"<profile>\"", "VARIANT_ID=<profile>"] {
230 + assert!(
231 + os_release.lines().any(|candidate| candidate == line),
232 + "os-release has no `{line}` for the profile step to rewrite",
233 + );
234 + }
235 +
236 + // The other half: a placeholder with nothing rewriting it is worse than
237 + // the wrong answer it replaced.
238 + let containerfile = read("Containerfile");
239 + for field in ["VARIANT", "VARIANT_ID"] {
240 + assert!(
241 + containerfile.contains(&format!("s/^{field}=.*/")),
242 + "nothing in the Containerfile rewrites {field}, so the placeholder would ship",
243 + );
244 + }
245 + }
246 +
220 247 #[test]
221 248 fn the_committed_os_release_carries_what_the_build_stamp_rewrites() {
222 249 let os_release = read("usr/lib/os-release");