Skip to main content

max / alloy

install: let each unit answer for VT1 itself 7b977b2 fixed installed machines and broke the installer medium. Dropping `Conflicts=` meant nothing kept the greeter off the live ISO, and the ExecStartPre that replaced it cannot do that job: greetd is Wanted by graphical.target, so stopping it once does not keep it stopped. Booting the rebuilt ISO landed on tuigreet instead of `alloy install`, with the wizard keystrokes going into a login prompt. Both failed arrangements were the same shape, the installer reaching across to stop the greeter, and they failed at opposite ends of the boot: `Conflicts=` fires when the job is enqueued, before the installer's condition is checked; `ExecStartPre` fires after graphical.target is free to start greetd again. So neither unit reaches across now. greetd gets the negation of the installer's own condition, in a drop-in, and the pair is the whole mechanism: one kernel flag, set by the ISO's GRUB entries and nowhere else, decides which of the two owns VT1. getty@tty1 keeps the ExecStartPre stop, which is sound for it in a way it was not for greetd: nothing restarts a getty once it is stopped. Verified in QEMU in both directions, because each was broken in turn: - Live ISO reaches `alloy install`, step 1 of 4, greeter absent. - A machine installed from it reaches tuigreet, with the journal showing `alloy-installer.service ... skipped, unmet condition check` followed by `Started greetd.service`. The swayosd work from c1cd25e is confirmed at runtime on the same install: swayosd-libinput-backend.service starts, and pressing Caps Lock draws the "Caps Lock On" overlay in the Alloy palette. That bind had never worked.
Co-Authored-By
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-22 03:48 UTC
Signed with PGP, not checked
Commit: 59aec95dee8f48bc8cfa702e52891cde9d2e2e07
Parent: c1cd25e
2 files changed, +38 insertions, -19 deletions
@@ -6,26 +6,22 @@
6 6 # machine is a way to lose a disk. `alloy.installer` is set only by the
7 7 # GRUB entries in build/build-iso.sh, so this unit is inert everywhere else.
8 8 #
9 - # greetd and getty@tty1 have to be out of the way, because all three want
10 - # VT1. On the live medium there is no account to log into yet, so the
11 - # greeter has nothing to offer and would only race the installer for the
12 - # terminal. They are stopped from ExecStartPre rather than declared in
13 - # `Conflicts=`, and that is not a style choice:
9 + # Three things want VT1: this, greetd, and getty@tty1. The kernel flag above
10 + # settles it, and each of the three answers for itself.
14 11 #
15 - # `Conflicts=` is applied when the start job is ENQUEUED, not when the unit
16 - # activates. The condition above is checked later, when the job runs. So on
17 - # an installed machine multi-user.target would pull this unit in, systemd
18 - # would queue stop jobs for greetd and getty@tty1, and only then would the
19 - # condition fail and skip the installer. The two units it displaced never
20 - # start, nothing owns VT1, and the machine boots to a black screen with a
21 - # cursor and no way to log in. That is what shipped: found 2026-07-21 by
22 - # installing from the ISO in QEMU and booting the result, where the journal
23 - # showed graphical.target reached, no failed units, and zero mentions of
24 - # greetd anywhere. Masking this one unit brought tuigreet straight back.
12 + # greetd stays away by its own condition, the negation of this one, in
13 + # etc/systemd/system/greetd.service.d/10-installer-medium.conf. That file
14 + # carries the history of the two arrangements that did not work, both of
15 + # which were versions of this unit reaching across to stop the greeter.
25 16 #
26 - # ExecStartPre runs only after the condition has passed, so an installed
27 - # machine never touches either unit. The leading `-` keeps a stop of a unit
28 - # that is not running from failing the install before it starts.
17 + # getty@tty1 is stopped here, from ExecStartPre rather than `Conflicts=`.
18 + # `Conflicts=` is applied when the start job is ENQUEUED, before the
19 + # condition above is checked, so on an installed machine it would displace
20 + # getty even though the installer never runs. ExecStartPre runs only after
21 + # the condition has passed. Unlike greetd, getty@tty1 is not pulled by a
22 + # target that would restart it, so stopping it once is enough. The leading
23 + # `-` keeps stopping a unit that is not running from failing the install
24 + # before it starts.
29 25 [Unit]
30 26 Description=Alloy installer
31 27 Documentation=https://git.sr.ht/~maxmj/alloy
@@ -34,7 +30,7 @@
34 30 Before=getty.target
35 31
36 32 [Service]
37 - ExecStartPre=-/usr/bin/systemctl stop getty@tty1.service greetd.service
33 + ExecStartPre=-/usr/bin/systemctl stop getty@tty1.service
38 34 # Type=idle so the boot's own console output has finished before a
39 35 # full-screen TUI takes the terminal; without it the first frame draws
40 36 # over service status lines and the user sees a torn screen.
@@ -1,0 +1,23 @@
1 + # Keep the greeter off the installer medium.
2 + #
3 + # greetd and `alloy install` both want VT1, and on the live medium there is
4 + # no account to log into yet, so a greeter there has nothing to offer and
5 + # only races the installer for the terminal.
6 + #
7 + # This is the counterpart to alloy-installer.service's own
8 + # ConditionKernelCommandLine, and the pair is the whole mechanism: one flag,
9 + # set by the ISO's GRUB entries and nowhere else, decides which of the two
10 + # owns VT1. Each unit answers for itself rather than one reaching across to
11 + # stop the other.
12 + #
13 + # The stopping-the-other-one version was tried and does not work. As
14 + # `Conflicts=`, it fires when the start job is ENQUEUED, before the
15 + # installer's condition is checked, so it killed the greeter on installed
16 + # machines where the installer never ran and left them with a black VT1
17 + # (7b977b2). Moved into the installer's ExecStartPre, it stopped racing the
18 + # wrong way instead: greetd is Wanted by graphical.target, so stopping it
19 + # once does not keep it from being started again, and the live medium came
20 + # up at a login prompt instead of the installer. Both were found by booting
21 + # the thing rather than by reading it.
22 + [Unit]
23 + ConditionKernelCommandLine=!alloy.installer