max / alloy_tui
- Co-Authored-By
- Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 files changed,
+161 insertions,
-32 deletions
| @@ -6,7 +6,59 @@ | |||
| 6 | 6 | # Layer order optimizes rebuild speed: repos first (rarely change), | |
| 7 | 7 | # then package installs (change with STACK.md), then config tree | |
| 8 | 8 | # (changes most often, so lives at the tail). | |
| 9 | + | # | |
| 10 | + | # Multi-stage: two cargo-install stages produce Rust binaries not | |
| 11 | + | # packaged in Fedora, then the final bootc stage COPYs them in. | |
| 12 | + | # Keeps the Rust toolchain out of the runtime image. | |
| 9 | 13 | ||
| 14 | + | # ===================================================================== | |
| 15 | + | # rust-build: ironbar, anyrun, wl-screenrec | |
| 16 | + | # ===================================================================== | |
| 17 | + | # All three are small crates. Grouped in one stage so cargo's registry | |
| 18 | + | # cache is amortized across them. | |
| 19 | + | FROM registry.fedoraproject.org/fedora:42 AS rust-build | |
| 20 | + | RUN dnf install -y \ | |
| 21 | + | rust cargo git \ | |
| 22 | + | gcc make pkgconf-pkg-config \ | |
| 23 | + | gtk3-devel gtk-layer-shell-devel \ | |
| 24 | + | gtk4-devel gtk4-layer-shell-devel \ | |
| 25 | + | wayland-devel libdrm-devel \ | |
| 26 | + | ffmpeg-free-devel \ | |
| 27 | + | openssl-devel \ | |
| 28 | + | && dnf clean all | |
| 29 | + | ENV CARGO_HOME=/build/cargo \ | |
| 30 | + | CARGO_TARGET_DIR=/build/target | |
| 31 | + | # ironbar and wl-screenrec are on crates.io; anyrun is git-only. | |
| 32 | + | RUN cargo install --locked --root /build/out ironbar && \ | |
| 33 | + | cargo install --locked --root /build/out wl-screenrec && \ | |
| 34 | + | cargo install --locked --root /build/out \ | |
| 35 | + | --git https://github.com/anyrun-org/anyrun anyrun | |
| 36 | + | ||
| 37 | + | # ===================================================================== | |
| 38 | + | # gram-build: Zed fork (graphical editor) | |
| 39 | + | # ===================================================================== | |
| 40 | + | # Heavier than the trio above (LLVM, wasm, vulkan headers), so its | |
| 41 | + | # own stage keeps rust-build fast when only the small crates change. | |
| 42 | + | # Source: codeberg.org/GramEditor/gram (GPLv3). | |
| 43 | + | FROM registry.fedoraproject.org/fedora:42 AS gram-build | |
| 44 | + | RUN dnf install -y \ | |
| 45 | + | rust cargo git \ | |
| 46 | + | gcc make cmake clang mold \ | |
| 47 | + | pkgconf-pkg-config \ | |
| 48 | + | wayland-devel libxkbcommon-devel \ | |
| 49 | + | openssl-devel fontconfig-devel freetype-devel \ | |
| 50 | + | alsa-lib-devel \ | |
| 51 | + | vulkan-loader-devel vulkan-headers \ | |
| 52 | + | && dnf clean all | |
| 53 | + | ENV CARGO_HOME=/build/cargo \ | |
| 54 | + | CARGO_TARGET_DIR=/build/target | |
| 55 | + | RUN git clone --depth 1 https://codeberg.org/GramEditor/gram /build/gram | |
| 56 | + | WORKDIR /build/gram | |
| 57 | + | RUN cargo build --release --locked --bin gram | |
| 58 | + | ||
| 59 | + | # ===================================================================== | |
| 60 | + | # Runtime image — the bootable container itself. | |
| 61 | + | # ===================================================================== | |
| 10 | 62 | FROM quay.io/fedora/fedora-bootc:42 | |
| 11 | 63 | ||
| 12 | 64 | # ===================================================================== | |
| @@ -56,7 +108,6 @@ | |||
| 56 | 108 | niri \ | |
| 57 | 109 | xdg-desktop-portal xdg-desktop-portal-gtk xdg-desktop-portal-gnome \ | |
| 58 | 110 | # Bar, notifications, screenshot, wallpaper | |
| 59 | - | # (ironbar, anyrun still to source — see cargo-install stage below) | |
| 60 | 111 | mako \ | |
| 61 | 112 | satty \ | |
| 62 | 113 | swww \ | |
| @@ -77,7 +128,6 @@ | |||
| 77 | 128 | bottom \ | |
| 78 | 129 | dua-cli \ | |
| 79 | 130 | # Wayland session glue | |
| 80 | - | # (wl-screenrec still to source — see cargo-install stage) | |
| 81 | 131 | cliphist \ | |
| 82 | 132 | swayosd \ | |
| 83 | 133 | playerctl \ | |
| @@ -94,10 +144,19 @@ | |||
| 94 | 144 | adw-gtk3-theme \ | |
| 95 | 145 | # Screenshot region-select helper | |
| 96 | 146 | slurp \ | |
| 97 | - | # Flatpak for shipping Floorp and Rnote | |
| 147 | + | # Flatpak client (Flathub + Floorp/Rnote install at first boot) | |
| 98 | 148 | flatpak \ | |
| 99 | 149 | # Font tools | |
| 100 | 150 | unzip fontconfig \ | |
| 151 | + | # Runtime libs for the cargo-built binaries copied in below: | |
| 152 | + | # ironbar → gtk4 + gtk4-layer-shell | |
| 153 | + | # anyrun → gtk3 + gtk-layer-shell | |
| 154 | + | # wl-screenrec → ffmpeg-free + libdrm | |
| 155 | + | # gram → vulkan-loader + mesa-vulkan-drivers + fontconfig | |
| 156 | + | gtk3 gtk-layer-shell \ | |
| 157 | + | gtk4 gtk4-layer-shell \ | |
| 158 | + | ffmpeg-free libdrm \ | |
| 159 | + | vulkan-loader mesa-vulkan-drivers \ | |
| 101 | 160 | && dnf clean all | |
| 102 | 161 | ||
| 103 | 162 | # ===================================================================== | |
| @@ -119,16 +178,15 @@ | |||
| 119 | 178 | fc-cache -fv | |
| 120 | 179 | ||
| 121 | 180 | # ===================================================================== | |
| 122 | - | # TODO(follow-up): multi-stage cargo build for the three Rust tools | |
| 123 | - | # without published Fedora packages — ironbar (bar), anyrun (launcher), | |
| 124 | - | # wl-screenrec (screen recorder). Adds a `FROM fedora:42 as rust-build` | |
| 125 | - | # stage that runs `cargo install`, then copies the binaries to | |
| 126 | - | # /usr/local/bin. Keeps toolchain out of the runtime image. | |
| 127 | - | # | |
| 128 | - | # For now, these are missing from the image; users get niri without a | |
| 129 | - | # bar, anyrun-plugin-less, and no wl-screenrec. Restoring them is the | |
| 130 | - | # next Containerfile change after the base image builds green. | |
| 181 | + | # Rust binaries not packaged in Fedora — pulled from the multi-stage | |
| 182 | + | # builds above. Landing under /usr/local/bin keeps them out of dnf's | |
| 183 | + | # view of /usr/bin, which is what the atomic tooling expects for | |
| 184 | + | # hand-installed binaries. | |
| 131 | 185 | # ===================================================================== | |
| 186 | + | COPY --from=rust-build /build/out/bin/ironbar /usr/local/bin/ironbar | |
| 187 | + | COPY --from=rust-build /build/out/bin/anyrun /usr/local/bin/anyrun | |
| 188 | + | COPY --from=rust-build /build/out/bin/wl-screenrec /usr/local/bin/wl-screenrec | |
| 189 | + | COPY --from=gram-build /build/target/release/gram /usr/local/bin/gram | |
| 132 | 190 | ||
| 133 | 191 | # ===================================================================== | |
| 134 | 192 | # Package removals — stock desktop pieces Alloy replaces | |
| @@ -143,16 +201,21 @@ | |||
| 143 | 201 | || true | |
| 144 | 202 | ||
| 145 | 203 | # ===================================================================== | |
| 146 | - | # Flathub + Floorp + Rnote | |
| 204 | + | # Flathub + Floorp + Rnote — deferred to first boot | |
| 147 | 205 | # ===================================================================== | |
| 148 | - | # Floorp is Flatpak-only. Rnote is available via Fedora but the | |
| 149 | - | # Flatpak track is what upstream recommends for consistent behavior | |
| 150 | - | # under Wayland. | |
| 151 | - | RUN flatpak remote-add --if-not-exists flathub \ | |
| 152 | - | https://dl.flathub.org/repo/flathub.flatpakrepo && \ | |
| 153 | - | flatpak install --system --noninteractive flathub \ | |
| 154 | - | one.ablaze.floorp \ | |
| 155 | - | com.github.flxzt.rnote | |
| 206 | + | # bootc prefers /var empty at build time; `flatpak remote-add` and | |
| 207 | + | # `flatpak install --system` both write to /var/lib/flatpak, which | |
| 208 | + | # tripped `bootc container lint` on the first build. The provisioning | |
| 209 | + | # now runs via alloy-flatpak-firstboot.service (see | |
| 210 | + | # usr/lib/systemd/system/), enabled by the system preset. | |
| 211 | + | ||
| 212 | + | # ===================================================================== | |
| 213 | + | # System user for greetd. greetd drops privileges to this account | |
| 214 | + | # before spawning tuigreet; without it greetd exits with | |
| 215 | + | # "configured default session user 'greeter' not found". Fedora | |
| 216 | + | # bootc's minimal user database does not include it. | |
| 217 | + | # ===================================================================== | |
| 218 | + | RUN useradd -M -r -s /sbin/nologin greeter | |
| 156 | 219 | ||
| 157 | 220 | # ===================================================================== | |
| 158 | 221 | # Config tree — the etc/ and usr/ trees in the repo map 1:1 into | |
| @@ -168,7 +231,13 @@ | |||
| 168 | 231 | # in the config tree above. Split across system-preset (greetd, | |
| 169 | 232 | # tailscaled) and user-preset (swayosd, syncthing, gammastep, | |
| 170 | 233 | # alloy-hinged). See docs/CONTINUITY.md for rationale. | |
| 234 | + | # | |
| 235 | + | # The preset files declare the intended enable/disable state; they | |
| 236 | + | # do not by themselves create the wants/ symlinks. `systemctl | |
| 237 | + | # preset-all` reads every preset file and applies it — this must | |
| 238 | + | # run after the config tree is in place, so it lives here. | |
| 171 | 239 | # ===================================================================== | |
| 240 | + | RUN systemctl preset-all | |
| 172 | 241 | ||
| 173 | 242 | # ===================================================================== | |
| 174 | 243 | # Branding |
| @@ -15,12 +15,35 @@ | |||
| 15 | 15 | ||
| 16 | 16 | ## Follow-up build work | |
| 17 | 17 | ||
| 18 | - | - [ ] Multi-stage cargo-install build for Rust tools without published Fedora packages: `ironbar` (bar), `anyrun` (launcher), `wl-screenrec` (screen recorder), and **Gram** (graphical editor — Zed fork, larger build). New `FROM fedora:42 as rust-build` stage runs `cargo install`, then copies binaries to `/usr/local/bin` in the runtime stage. Gram may deserve its own stage due to build weight. | |
| 19 | - | - [ ] Address bootc lint's `/var` warnings — Flatpak install writes to `/var/lib/flatpak` at build time, but bootc prefers `/var` empty at build so systemd-tmpfiles populates at first boot. Options: (a) accept the warnings, (b) move Flatpak installs to a first-boot systemd oneshot instead of build-time, (c) `bootc container lint`'s official escape hatch if there is one. | |
| 20 | - | - [ ] Boot the image in a VM (`bootc-image-builder` → qcow2 or ISO) to verify it actually runs as an OS beyond just linting clean. | |
| 18 | + | - [x] Multi-stage cargo-install build for Rust tools without published Fedora packages: `ironbar`, `anyrun`, `wl-screenrec` in one `rust-build` stage; **Gram** (Zed fork) in its own `gram-build` stage per todo's build-weight note. Both stages land on `registry.fedoraproject.org/fedora:42`; binaries copied to `/usr/local/bin` in the runtime stage. Runtime libs (gtk3/gtk4 + layer-shell, ffmpeg-free, vulkan-loader/mesa-vulkan-drivers) added to the runtime dnf install. anyrun pulled `--git` (not on crates.io); ironbar and wl-screenrec pulled from crates.io. First rebuild will validate; if wl-screenrec turns out to be git-only, flip that one line to `--git https://github.com/russelltg/wl-screenrec`. | |
| 19 | + | - [x] Address bootc lint's `/var` warnings — moved Flatpak provisioning (flathub remote-add + Floorp/Rnote install) to `usr/lib/systemd/system/alloy-flatpak-firstboot.service`, a first-boot oneshot with `ConditionPathExists=!/var/lib/alloy/flatpak-firstboot.done`. Enabled via `etc/systemd/system-preset/50-alloy.preset`. Build-time Flatpak lines removed from the Containerfile. | |
| 20 | + | - [x] Boot the image in a VM (`bootc-image-builder` → qcow2 or ISO) to verify it actually runs as an OS beyond just linting clean. Booted 2026-07-04 on fw13 via `podman run … bootc-image-builder:latest build --type qcow2 --rootfs xfs …`, then `qemu-system-x86_64 -enable-kvm -machine q35` with OVMF. Kernel + initrd + systemd all clean; tuigreet reached VT1 after the fixes below were applied by hand in the running VM. Punch list captured under "Punch list from first QEMU boot" below. | |
| 21 | 21 | - [x] Package Alloy-authored config files that STACK.md references but the Containerfile doesn't yet ship. Shipped 2026-07-05 under `etc/skel/.config/`: `niri/config.kdl`, `ironbar/{config.toml,style.css}`, `anyrun/{config.ron,style.css}`, `rio/config.toml`, `yazi/{yazi.toml,theme.toml}`, `mako/config`, `satty/config.toml`, `swaylock/config`, `starship.toml`. All applied Alloy light-mode palette per docs/TOKENS.md and IosevkaTerm Nerd Font per docs/STACK.md#fonts. Live-system verification pending — see the follow-up item below. | |
| 22 | 22 | - [ ] Live-system verification of the packaged configs. Test on Framework 13 (or QEMU with hardware passthrough where feasible): rio → yazi kitty-graphics preview, Niri Fn-key binds → swayosd overlays, Print → screenshot to `~/Pictures/Screenshots/`, Mod+Print → satty on latest, Mod+Ctrl+L → swaylock with palette, ironbar workspaces reflecting Niri IPC, anyrun launcher, mako urgency colors, starship prompt on nu login. Any config that breaks on real hardware gets fixed and re-shipped as a follow-up. | |
| 23 | 23 | ||
| 24 | + | ## Punch list from first QEMU boot (2026-07-04) | |
| 25 | + | ||
| 26 | + | All five items are Containerfile bugs uncovered by actually booting the image. Reproducer for each is "boot the vanilla `alloy:local` qcow2, observe." All landed and verified 2026-07-04 by rebuilding `alloy:local`, regenerating the qcow2 with no CLI overrides, and booting: greetd is `enabled` from first boot, `active (running)`, PAM session opens for `greeter`, tuigreet renders on the Alloy palette with no kernel printk overlay. | |
| 27 | + | ||
| 28 | + | - [x] **Declare a default root filesystem.** bootc-image-builder aborts with `missing required info: DefaultRootFs` unless `--rootfs` is passed. Shipped as `usr/lib/bootc/install/00-alloy.toml` with `[install.filesystem.root] type = "xfs"`. Verified by rebuilding the qcow2 without `--rootfs` on the CLI. | |
| 29 | + | - [x] **Apply presets at build time.** `50-alloy.preset` is copied into `/etc/systemd/system-preset/` but nothing was running `systemctl preset-all` during the build, so greetd stayed disabled at first boot even though the preset said enable. Fix: `RUN systemctl preset-all` in the Containerfile immediately after `COPY etc/` / `COPY usr/`. | |
| 30 | + | - [x] **Single-line `etc/greetd/config.toml` command.** The multi-line basic string with backslash continuations tripped greetd's TOML parser (`expected equals sign on line, but found none`). Rewritten as a single-line `command = "tuigreet …"` with the Alloy theme preserved on the same line. Verified by observing the correct palette in the second-boot screenshot. | |
| 31 | + | - [x] **Create the `greeter` system user.** Greetd exits `error: configured default session user 'greeter' not found` without it — Fedora bootc's minimal user database doesn't include it. Added `RUN useradd -M -r -s /sbin/nologin greeter` to the Containerfile. Landed as a system user (uid < 1000). The `video` group also isn't in the base image; not needed for the greetd path, revisit when niri actually needs GPU nodes. | |
| 32 | + | - [x] **Silence kernel `printk` on the greeter VT.** Audit lines from `systemd-hostnamed` and `user-runtime-dir` were overwriting the greeter UI. Shipped `usr/lib/bootc/kargs.d/10-alloy.toml` with `kargs = ["quiet", "loglevel=3"]`. Verified — `/proc/cmdline` in the fresh image includes both flags, and the second-boot screenshot has a clean greeter frame. | |
| 33 | + | ||
| 34 | + | ## New findings from the second QEMU boot (2026-07-04) | |
| 35 | + | ||
| 36 | + | Surfaced during verification of the fixes above; separate work items. | |
| 37 | + | ||
| 38 | + | - [x] **Ship a default `/etc/hostname`.** Shipped as `etc/hostname` with `alloy`. Static for now; if per-install randomization becomes desirable later, replace with a first-boot systemd unit that writes a randomized suffix. | |
| 39 | + | ||
| 40 | + | ## Meta findings from the same session | |
| 41 | + | ||
| 42 | + | Not Containerfile issues, but worth capturing so the next person doesn't rediscover them. | |
| 43 | + | ||
| 44 | + | - **bootc-image-builder must find the source image in the same podman storage it uses.** `sudo podman run … bootc-image-builder … localhost/alloy:local` fails with `image not known` if the image was built rootless. Either build the image as root (`sudo podman build`), or shuttle it across with `podman save localhost/alloy:local | sudo podman load`. The current builder image no longer pulls sources; the error message says so explicitly. | |
| 45 | + | - **First QEMU launch parameters that worked**, for reference: `qemu-system-x86_64 -enable-kvm -cpu host -smp 4 -m 4096 -machine q35 -drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd -drive if=pflash,format=raw,file=OVMF_VARS.fd -drive if=virtio,file=disk.qcow2,format=qcow2 -netdev user,id=n0,hostfwd=tcp:127.0.0.1:2222-:22 -device virtio-net-pci,netdev=n0 -vga virtio -vnc 127.0.0.1:1 -serial mon:stdio`. UEFI + q35 is required (bootc images do not have a legacy BIOS path). `hostfwd` for port 22 makes remote diagnosis trivial once sshd is up. | |
| 46 | + | ||
| 24 | 47 | ## When distribution starts | |
| 25 | 48 | ||
| 26 | 49 | Deferred until Alloy has something worth pulling. Re-add `sign` and `push` tasks to `.builds/alloy-image.yml` and populate the sr.ht secrets when doing this. |
| @@ -2,18 +2,15 @@ | |||
| 2 | 2 | # | |
| 3 | 3 | # Boots into tuigreet on VT1. tuigreet prompts for user and password, | |
| 4 | 4 | # then execs niri-session. Alloy palette applied via tuigreet's --theme. | |
| 5 | + | # | |
| 6 | + | # Command must stay on a single line: greetd's TOML parser rejects the | |
| 7 | + | # multi-line basic-string / backslash-continuation form ("expected | |
| 8 | + | # equals sign on line, but found none"). | |
| 5 | 9 | ||
| 6 | 10 | [terminal] | |
| 7 | 11 | vt = 1 | |
| 8 | 12 | switch = true | |
| 9 | 13 | ||
| 10 | 14 | [default_session] | |
| 11 | - | command = """ | |
| 12 | - | tuigreet \ | |
| 13 | - | --time \ | |
| 14 | - | --remember \ | |
| 15 | - | --asterisks \ | |
| 16 | - | --cmd niri-session \ | |
| 17 | - | --theme 'container=white;border=dark_gray;text=black;time=dark_gray;prompt=yellow;input=black;action=dark_gray;button=yellow' | |
| 18 | - | """ | |
| 15 | + | command = "tuigreet --time --remember --asterisks --cmd niri-session --theme 'container=white;border=dark_gray;text=black;time=dark_gray;prompt=yellow;input=black;action=dark_gray;button=yellow'" | |
| 19 | 16 | user = "greeter" |
| @@ -7,10 +7,15 @@ | |||
| 7 | 7 | # - tailscaled: opt-in only. Enrollment happens via `alloy tail` | |
| 8 | 8 | # (see docs/CONTINUITY.md — Alloy does not phone home | |
| 9 | 9 | # before the user says yes). | |
| 10 | + | # - alloy-flatpak-firstboot: adds flathub and installs Floorp + Rnote | |
| 11 | + | # the first time the machine reaches multi-user with | |
| 12 | + | # network. Kept out of the build so /var stays empty | |
| 13 | + | # at image-build time (bootc lint requirement). | |
| 10 | 14 | # | |
| 11 | 15 | # Format: `enable|disable <unit>`. First match wins; entries without | |
| 12 | 16 | # a match fall through to /usr/lib/systemd/system-preset/ defaults. | |
| 13 | 17 | ||
| 14 | 18 | enable greetd.service | |
| 19 | + | enable alloy-flatpak-firstboot.service | |
| 15 | 20 | ||
| 16 | 21 | disable tailscaled.service |
| @@ -1,0 +1,1 @@ | |||
| 1 | + | alloy |
| @@ -1,0 +1,9 @@ | |||
| 1 | + | # Alloy bootc install configuration. | |
| 2 | + | # | |
| 3 | + | # Declares the root filesystem type used when the image is installed | |
| 4 | + | # to disk. Without this, bootc-image-builder fails with | |
| 5 | + | # "missing required info: DefaultRootFs" and any consumer has to pass | |
| 6 | + | # --rootfs on the CLI. xfs matches Fedora bootc's own default. | |
| 7 | + | ||
| 8 | + | [install.filesystem.root] | |
| 9 | + | type = "xfs" |
| @@ -1,0 +1,8 @@ | |||
| 1 | + | # Alloy default kernel cmdline additions. | |
| 2 | + | # | |
| 3 | + | # `quiet` + `loglevel=3` suppresses kernel/audit printk on tty1 so the | |
| 4 | + | # greetd/tuigreet UI isn't overwritten by systemd-hostnamed and | |
| 5 | + | # user-runtime-dir audit messages. Anyone debugging boot problems can | |
| 6 | + | # still get the full log via `journalctl -k` or `dmesg`. | |
| 7 | + | ||
| 8 | + | kargs = ["quiet", "loglevel=3"] |
| @@ -1,0 +1,17 @@ | |||
| 1 | + | [Unit] | |
| 2 | + | Description=Alloy first-boot Flatpak provisioning (Floorp, Rnote) | |
| 3 | + | Documentation=file:///usr/share/alloy/README | |
| 4 | + | After=network-online.target | |
| 5 | + | Wants=network-online.target | |
| 6 | + | ConditionPathExists=!/var/lib/alloy/flatpak-firstboot.done | |
| 7 | + | ||
| 8 | + | [Service] | |
| 9 | + | Type=oneshot | |
| 10 | + | RemainAfterExit=yes | |
| 11 | + | ExecStart=/usr/bin/flatpak remote-add --system --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo | |
| 12 | + | ExecStart=/usr/bin/flatpak install --system --noninteractive --or-update flathub one.ablaze.floorp com.github.flxzt.rnote | |
| 13 | + | ExecStart=/usr/bin/mkdir -p /var/lib/alloy | |
| 14 | + | ExecStart=/usr/bin/touch /var/lib/alloy/flatpak-firstboot.done | |
| 15 | + | ||
| 16 | + | [Install] | |
| 17 | + | WantedBy=multi-user.target |