Skip to main content

max / alloy

8.0 KB · 145 lines History Blame Raw
1 # Lay down Alloy's own components on a machine that has just been installed.
2 #
3 # The console and shop cannot ship inside the image. A component the base
4 # carries cannot be replaced client-side — `rpm-ostree install` refuses to
5 # depsolve against it, and `override replace` records a request that never
6 # activates — so an image that carried them would be an image whose console
7 # could never be fixed without rebuilding an ISO and writing a drive. They are
8 # layered packages instead, and stay layered. Measured in build/layertest.
9 #
10 # The repo they come from rides inside the image, at /usr/share/alloy/rpm, as
11 # RPM files that are never installed at build time. So this needs no network,
12 # and it needs nothing copied off the medium either: the ISO carries the repo
13 # because the ISO is the image, and this machine has it for the same reason.
14 # Deliberate twice over: an offline install has to produce a working machine,
15 # and a machine whose owner has not consented to anything yet must not reach
16 # out. See docs/STACK.md, "Updates" and "Hotfixes".
17 #
18 # Carrying the RPMs is not the same as carrying the component, and the
19 # difference is the whole design. What cannot be replaced client-side is an
20 # installed package or a file at the path being layered over. An uninstalled
21 # .rpm sitting in /usr/share is neither: @System does not hold the component,
22 # /usr/bin/alloy does not exist, and the layer goes down cleanly over both.
23 # Measured, `c1`/`c2` in build/layertest.
24 #
25 # ## Why this reboots, and why that is not a regression
26 #
27 # /usr is a read-only ostree tree, so a layered package arrives as a new
28 # deployment and a new deployment needs a boot. There is no version of this
29 # that ends with the console usable in the session it ran from.
30 #
31 # Layering during the install instead would avoid it, and was measured rather
32 # than assumed: `rpm-ostree install --sysroot` against an unbooted target is a
33 # D-Bus client with no daemon it can reach, and did not work. Not proven
34 # impossible, and build/layertest/installtime.sh records exactly where to pick
35 # that up. Until then, one reboot.
36 #
37 # ## Ordering
38 #
39 # Before greetd, because the session wrapper it launches calls `alloy theme
40 # apply` and the greeter is the first thing a person sees. A boot that reached
41 # the greeter first would show a login that leads to a session that cannot
42 # start, which is a worse failure than a visible one-time setup step.
43 #
44 # After network-online is deliberately NOT set. Nothing here uses the network,
45 # and ordering after something that may never arrive is how a machine with no
46 # link sits at a blank console for ninety seconds.
47
48 [Unit]
49 Description=Install Alloy's own components from the installer medium
50 Documentation=https://makenot.work/git/max/alloy
51 # Default dependencies deliberately left ON. The early-boot idiom
52 # (DefaultDependencies=no, Conflicts=shutdown.target) is wrong here: rpm-ostree
53 # is a D-Bus client and rpm-ostreed is D-Bus activated, so a unit that ordered
54 # itself before the bus would fail on a machine where everything is fine.
55 After=dbus.service
56 Wants=dbus.service
57 Before=greetd.service getty.target
58
59 # The gate, and it is two conditions rather than a stamp file on purpose.
60 #
61 # The repo has to be there: an image built before this existed, or one somebody
62 # stripped, has nothing to install from and this unit is not its business.
63 ConditionPathExists=/usr/share/alloy/rpm/repodata/repomd.xml
64 # And the console has to be absent. This is the "already ran" test, and reading
65 # it off the filesystem rather than off a stamp means it answers correctly
66 # after a rollback, after a user uninstalls the layer deliberately, and on a
67 # machine somebody imaged from another one. A stamp file would say "done" in
68 # all three cases and leave the machine without a console.
69 ConditionPathExists=!/usr/bin/alloy
70
71 [Service]
72 Type=oneshot
73 RemainAfterExit=yes
74 # The notice comes first, because the install is the long part and the blank
75 # screen is what it is explaining. `quiet loglevel=3` on the cmdline plus no
76 # plymouth means this boot shows nothing at all otherwise, including this
77 # unit's own Description. See usr/bin/alloy-layer-notice.
78 #
79 # `-` on both notice lines: a prefix that makes systemd ignore the exit
80 # status. Telling the user is worth less than installing the console, so a
81 # broken notice must not be what stops the machine getting one. The script
82 # takes the same care internally; this is the belt outside it.
83 ExecStartPre=-/usr/bin/alloy-layer-notice start
84
85 # Then the repo fence, and it is what makes an offline first boot work at all.
86 # rpm-ostree refreshes every enabled repo before it depsolves, so with Fedora's
87 # four enabled this unit needed name resolution to install packages sitting on
88 # the disk, and a machine that could not resolve came up with no console.
89 # Measured 2026-08-25; usr/bin/alloy-layer-repos carries the detail and the two
90 # flags that cannot do this instead.
91 #
92 # `on` before `off` so a first boot interrupted between them heals here rather
93 # than leaving the repos quietly disabled, and `on` again in ExecStopPost on
94 # every path. No `-` on the `off` line: if the fence cannot be raised, the
95 # install that follows is the one that leaves the machine without a console,
96 # and failing here retries on the next boot with /etc intact.
97 ExecStartPre=-/usr/bin/alloy-layer-repos on
98 ExecStartPre=/usr/bin/alloy-layer-repos off
99
100 # By bare name, not by NEVRA, and it matters later rather than here. rpm-ostree
101 # records a request under the string it was given, so a package layered by full
102 # name-version-release cannot afterwards be removed by its bare name — it
103 # answers "not currently requested" and leaves the package in place, which
104 # reads as nothing-to-do rather than as a failure. `alloy update` has to drop
105 # and re-add these on every upgrade (it is the only thing that makes a layered
106 # component follow the image it shipped with), so installing them by name here
107 # is what keeps that safe.
108 #
109 # -y because rpm-ostree prompts on a tty it does not have, and a unit blocked
110 # forever on a prompt nobody can see is indistinguishable from a hang.
111 # --idempotent so a re-run after a partial failure is not itself an error.
112 #
113 # The package list is read from the image rather than written here. One unit
114 # file serves both profiles, and `server` has no compositor, so laying a Wayland
115 # terminal down there would install something that cannot run. The image knows
116 # which profile it is and writes the list at build time; this unit installs what
117 # it is told. A missing or empty list leaves rpm-ostree with no arguments, which
118 # fails loudly — the right outcome, since the alternative is a machine that
119 # quietly comes up with no console.
120 ExecStart=/bin/sh -c 'exec /usr/bin/rpm-ostree install --idempotent -y $(cat /usr/share/alloy/components)'
121 ExecStart=/usr/bin/systemctl reboot
122 # Only on failure, which is the state the user cannot get out of on their own:
123 # no console and no terminal, and the session they would fix it from is the one
124 # that will not start. $SERVICE_RESULT is systemd's, and it is `success` on the
125 # ordinary path where the reboot above is already under way.
126 ExecStopPost=-/bin/sh -c '[ "$SERVICE_RESULT" = success ] || /usr/bin/alloy-layer-notice fail'
127 # And /etc back, on every path including the successful one, before the reboot
128 # above takes effect. The staged deployment does not inherit the disabled files
129 # because ostree merges /etc as it is left here, which is measured rather than
130 # assumed: rebooting into the result shows the ordinary enabled set.
131 ExecStopPost=-/usr/bin/alloy-layer-repos on
132
133 # A failure here leaves a machine with no console, which the user cannot fix
134 # from the session they cannot start. Failing loudly is the only honest option:
135 # the journal names the unit, the console says so through the notice above, and
136 # the next boot retries because the conditions at the top are both still true.
137 #
138 # No Restart=, though. A unit whose second ExecStart is `reboot` and that
139 # restarts on failure is a boot loop, and a boot loop on the one machine class
140 # this project claims to support is worse than a machine that boots to a
141 # console and says what went wrong.
142
143 [Install]
144 WantedBy=multi-user.target
145