Skip to main content

max / alloy

Settle where the components get layered, and record what was tried GO d866e125 subtask 7, first half. Max chose the shape: the RPMs travel on the ISO rather than over the network, so the installer copies our repo onto the target and the machine layers from file:///var/lib/alloy/rpm. It is the only shape that survives an offline install, and it keeps the no-phone-home position intact for a machine whose owner has not consented to anything yet. That left one sub-decision, and it was worth a measurement because the better answer removes a whole moving part: layering during the install needs no first-boot unit, no stamp file, no extra reboot in front of someone who just finished installing, and no ordering against greetd. installtime.sh measures it, and it does not work from here. `rpm-ostree install` is a D-Bus client and --sysroot does not make it standalone, so against an unbooted target it failed three ways in a row: no system bus, then a bus without activation, then a hand-started daemon that never registered the object. Abandoned there rather than pushed further, because the first-boot shape needs no new mechanism at all — it is the ordinary layering the existing cases already measure — and the difference between them is one reboot. Kept rather than deleted, and written up as not-proven rather than impossible. The failures are a container with no systemd fighting D-Bus activation, and the real installer runs inside the live ISO, which has systemd and a bus. What stays genuinely open is whether rpm-ostreed will operate on a sysroot other than its own. Someone who wants the reboot back will want that answer, and the script says where to get it. Also fixes a latent bug the run exposed, in both scripts: `rpm -q` exits 1 for a package that is not installed, which is a result rather than a failure. The liveness check treated it as one and reported "guest did not come back" about a guest that came back and answered the question.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-14 23:39 UTC
Signed with PGP, not checked
Commit: 145b0fa4d1110613b4ba31881b4b149de1b0db10
Parent: ad2958a
3 files changed, +184 insertions, -1 deletion
@@ -110,6 +110,28 @@
110 110 would work here. Reset drops every layered package including ones the user
111 111 layered themselves, and those are not ours to remove.
112 112
113 + ## How an installed machine gets the components, 2026-08-14
114 +
115 + Decided by Max: **they travel on the ISO**, not over the network. The installer
116 + copies our repo from the medium onto the target, and the machine layers from
117 + `file:///var/lib/alloy/rpm`. That is the only shape that survives an offline
118 + install, and it keeps the no-phone-home position intact for a machine whose
119 + owner has not consented to anything yet.
120 +
121 + **Layering happens at first boot, not during the install.** `installtime.sh`
122 + measured the alternative and it is unproven: `rpm-ostree install` is a D-Bus
123 + client and `--sysroot` does not make it standalone, so against an unbooted
124 + target it failed three ways in a row (no bus, then a bus without activation,
125 + then a hand-started daemon that never registered the object). The first-boot
126 + shape needs no new mechanism at all, since it is the ordinary layering the
127 + cases above already measure. The difference between them is one reboot.
128 +
129 + That is not a proof that install-time layering cannot work. The failures are a
130 + container with no systemd fighting D-Bus activation, and the real installer
131 + runs inside the live ISO, which has systemd and a bus. The open part is whether
132 + rpm-ostreed will operate on a sysroot other than its own. `installtime.sh`
133 + records where to pick that up if the extra reboot ever becomes worth removing.
134 +
113 135 ## Use
114 136
115 137 Needs `qemu-system-x86_64` with KVM, OVMF, podman, and about 25 GB free. No
@@ -82,9 +82,14 @@
82 82 sleep 45
83 83
84 84 say "--- booted ---"
85 + # `|| true` on the last command: `rpm -q` exits 1 for a package that is not
86 + # installed, and that is a result rather than a failure. Without it a case that
87 + # legitimately ends with nothing layered reports "guest did not come back"
88 + # about a guest that came back and answered the question.
85 89 "$HERE/sshx" 'echo -n "base-mark: "; cat /usr/share/base-mark; \
86 90 echo -n "binary: "; alloy-demo 2>&1 || echo "(absent)"; \
87 - echo -n "rpm -q: "; rpm -q alloy-demo 2>&1' || die "guest did not come back"
91 + echo -n "rpm -q: "; rpm -q alloy-demo 2>&1 || true' \
92 + || die "guest did not come back"
88 93 "$HERE/sshx" 'rpm-ostree status --json' | python3 "$HERE/readstate.py"
89 94
90 95 # Whether the machine can still take the next one. A wedge does not show up in
@@ -1,0 +1,156 @@
1 + #!/usr/bin/env bash
2 + #
3 + # installtime.sh — can the installer layer a package into the target it just
4 + # installed, or does that have to wait for the machine's first boot?
5 + #
6 + # installtime.sh
7 + #
8 + # The question this answers. Alloy's hotfixable components cannot ship in the
9 + # base image (see README), so an installed machine has to acquire them some
10 + # other way, and Max chose 2026-08-14 that they travel on the ISO rather than
11 + # over the network. That leaves one sub-decision: whether `alloy install` lays
12 + # them down while the target is still mounted, or whether a first-boot service
13 + # does it on the installed machine.
14 + #
15 + # Layering at install time is worth a measurement because it removes a whole
16 + # moving part: no first-boot unit, no stamp file to decide whether it already
17 + # ran, no extra reboot in front of a user who just finished installing, and no
18 + # ordering constraint against greetd. `rpm-ostree install` takes --sysroot, so
19 + # it is at least plausible. Whether it works against a target that has never
20 + # been booted is the thing to find out.
21 + #
22 + # What it does: install n1 (a base carrying nothing), mount the result, put a
23 + # repo and its .repo file inside the target, run rpm-ostree against that
24 + # sysroot from a container, unmount, boot it, and see whether the package is
25 + # there on the very first boot.
26 + #
27 + # ## RESULT 2026-08-14: NOT PROVEN, AND FIRST BOOT WAS CHOSEN INSTEAD
28 + #
29 + # It does not work from this harness, and the reason is D-Bus rather than
30 + # anything about ostree. `rpm-ostree install` is a client that talks to
31 + # rpm-ostreed over the system bus, and --sysroot does not make it standalone.
32 + # Three attempts, each getting one step further:
33 + #
34 + # no bus at all error: Loading sysroot: Connecting to system bus:
35 + # Could not connect: No such file or directory
36 + # dbus-daemon --system error: Failed to invoke RegisterClient:
37 + # GDBus.Error...Spawn.ChildExited: Launch helper
38 + # exited with unknown return code 1
39 + # daemon started by error: Object does not exist at path "/"
40 + # hand on that sysroot
41 + #
42 + # The measurement was abandoned there rather than pushed further, because the
43 + # first-boot shape was already proven by the ordinary cases in README.md and
44 + # the difference between them is one reboot.
45 + #
46 + # **Do not read this as "install-time layering is impossible."** The failures
47 + # above are a container with no systemd fighting D-Bus activation. The real
48 + # `alloy install` runs inside the ISO's live environment, which is a full
49 + # Fedora system with systemd, a system bus and rpm-ostree already present, so
50 + # the thing that blocked this harness may simply not be there. What stays
51 + # genuinely unproven is whether rpm-ostreed will operate on a sysroot that is
52 + # not the one it is running from. If the first-boot reboot ever becomes worth
53 + # removing, that is the question to answer, and the right place to answer it is
54 + # from a booted live ISO rather than from here.
55 + set -euo pipefail
56 +
57 + # shellcheck source=build/layertest/common.sh
58 + . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh"
59 +
60 + # shellcheck source=build/privilege.sh
61 + . "$HERE/../privilege.sh"
62 +
63 + MNT="$STATE/target"
64 +
65 + cleanup() {
66 + privc umount "$MNT" 2>/dev/null || true
67 + [ -n "${LOOP:-}" ] && privc losetup -d "$LOOP" 2>/dev/null
68 + true
69 + }
70 + trap cleanup EXIT
71 +
72 + "$HERE/install-disk.sh" n1
73 +
74 + # -------------------------------------------------------------- mount it
75 + # -P so the partition table is read and /dev/loopNp* appear. bootc's generic
76 + # ext4 layout is ESP, boot, root; the root partition is the last and the big
77 + # one, and it is the one carrying ostree/.
78 + say ""
79 + say "mounting the installed target"
80 + LOOP="$(privc losetup --find --show -P "$STATE/disk.raw")"
81 + privc mkdir -p "$MNT"
82 +
83 + ROOT=""
84 + for part in "$LOOP"p*; do
85 + privc mount "$part" "$MNT" 2>/dev/null || continue
86 + if privc test -d "$MNT/ostree"; then ROOT="$part"; break; fi
87 + privc umount "$MNT"
88 + done
89 + [ -n "$ROOT" ] || die "no partition on $LOOP carries an ostree sysroot"
90 + say "sysroot is $ROOT"
91 +
92 + # --------------------------------------------- put the repo inside the target
93 + # /var, not /usr: /usr is the immutable ostree tree and belongs to the image,
94 + # while /var is the machine's own writable state and survives every upgrade.
95 + # This is where the ISO's copy of our RPMs would land during a real install.
96 + #
97 + # The deployment's /etc is where rpm-ostree reads repo config from, and under
98 + # ostree that is a per-deployment directory rather than a symlink into /var.
99 + DEPLOY="$(privc sh -c "ls -d $MNT/ostree/deploy/*/deploy/*/ | head -1")"
100 + [ -n "$DEPLOY" ] || die "no deployment under $MNT/ostree/deploy"
101 + say "deployment is ${DEPLOY#"$MNT"}"
102 +
103 + privc mkdir -p "$MNT/ostree/deploy/default/var/lib/alloy" 2>/dev/null || true
104 + VAR="$(privc sh -c "ls -d $MNT/ostree/deploy/*/var | head -1")"
105 + privc mkdir -p "$VAR/lib/alloy"
106 + privc cp -r "$STATE/repo" "$VAR/lib/alloy/rpm"
107 +
108 + # file:// and not http://, which is the whole point: no network at install time
109 + # and none at first boot either.
110 + privc tee "$DEPLOY/etc/yum.repos.d/alloy-local.repo" >/dev/null <<'EOF'
111 + [alloy-local]
112 + name=Alloy components, from the installer medium
113 + baseurl=file:///var/lib/alloy/rpm
114 + enabled=1
115 + gpgcheck=0
116 + EOF
117 +
118 + # ------------------------------------------------------ layer into the target
119 + # In a container because the dev host has no rpm-ostree. --privileged and the
120 + # host mount namespace, because it is about to operate on a mounted filesystem
121 + # and drive device nodes underneath it.
122 + say ""
123 + say "=== rpm-ostree install --sysroot against the unbooted target ==="
124 + set +e
125 + privc podman run --rm --privileged --pid=host \
126 + --security-opt label=type:unconfined_t \
127 + -v /dev:/dev \
128 + -v "$MNT:/target" \
129 + registry.fedoraproject.org/fedora:43 bash -c '
130 + dnf -y install rpm-ostree >/dev/null 2>&1
131 + rpm-ostree --sysroot=/target install alloy-demo 2>&1 | tail -20
132 + exit "${PIPESTATUS[0]}"
133 + '
134 + RESULT=$?
135 + set -e
136 + say "rpm-ostree exit: $RESULT"
137 +
138 + cleanup
139 + trap - EXIT
140 +
141 + # ------------------------------------------------------------- boot and look
142 + say ""
143 + say "booting the target"
144 + nohup "$HERE/vm.sh" >"$STATE/qemu.log" 2>&1 &
145 + sleep 50
146 +
147 + say "--- first boot, never booted before this ---"
148 + # `|| true` on the last command, not decoration. `rpm -q` exits 1 for a package
149 + # that is not installed, which is a perfectly good answer here and is exactly
150 + # the answer this script expects to see; without it the absent case reports
151 + # "guest did not come back" about a guest that came back and said so.
152 + "$HERE/sshx" 'echo -n "base-mark: "; cat /usr/share/base-mark; \
153 + echo -n "binary: "; alloy-demo 2>&1 || echo "(absent)"; \
154 + echo -n "rpm -q: "; rpm -q alloy-demo 2>&1 || true' \
155 + || die "guest did not come back"
156 + "$HERE/sshx" 'rpm-ostree status --json' | python3 "$HERE/readstate.py"