tools: preview the plymouth splash unprivileged; #137 attempt 1 reverted
All checks were successful
Check / eval (push) Successful in 3m49s

Answering "how do I give you root": you don't. tools/plymouth-preview.sh
renders the splash — LUKS dialog included — into X11 windows in ~10s with
no reboot, no root and no DRM:

- unshare -rm = user + mount namespace; every mount dies with it.
- /run/plymouth is BOTH the socket dir and the compiled-in plugin path
  (`strings plymouthd` → /run/plymouth/plugins/), so we bind a writable dir
  over it AND recreate its symlinks — miss those and plymouth can't find
  x11.so, silently skips X11 and reaches for a tty.
- never pass --tty (main.c:2133 takes the X11 path only while default_tty
  is unset); /dev/dri is masked so a preview physically cannot mode-set a
  real output — which matters on a docked session (#127).
- PLY_CREATE_FAKE_MULTI_HEAD_SETUP fakes 800x600 + 640x480 heads.
- debug wants plymouth.debug=stream: — `file:` buffers and yields nothing.

What it proved immediately: with a STABLE two-head canvas the current theme
centres correctly on both heads. So the geometry is fine and #137 really is
the timing bug (positions frozen at parse time vs a canvas that resizes
when a head arrives) — recorded in the item.

And it caught my own fix: hoisting positions into layout() rendered the
background on both heads and nothing else — no logo, no bar — with NO parse
error logged. Reverted rather than shipped, because that script draws the
passphrase box on an encrypted machine. #137 carries the failure, the prime
suspect (plymouth script scope: assignment lands in `local` unless the name
already exists) and the fast edit/render loop for the next attempt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 12:42:31 +01:00
parent 3e6cc6fe84
commit 0cd6212b9c
3 changed files with 118 additions and 13 deletions

View File

@@ -214,20 +214,48 @@ the existing `refresh_callback` (already running at 50 fps) whenever
included), entry, lock, any live bullets, progress box/bar. Keep it a included), entry, lock, any live bullets, progress box/bar. Keep it a
canvas-change check, not an every-frame recompute. canvas-change check, not an every-frame recompute.
**Testing — the rig exists, and it needs root.** nixpkgs' plymouth ships **The rig works, needs no root, and is in `scratchpad/ply-rig.sh`** (recipe
`x11.so`, and plymouth has a built-in fake dual-head mode: below — rebuild it, it is ~20 lines). Rendered on the dev box 2026-07-16:
`PLY_CREATE_FAKE_MULTI_HEAD_SETUP=1` (`renderers/x11/plugin.c:294`) creates
800×600 + 640×480 heads, so the splash can be rendered in a window with no
DRM and no reboot. Caveat: that fake path builds both heads up front, so it
proves *rendering and centring*, not the canvas-change bug itself — for that,
plug/unplug while the splash is up, or compare a boot with and without the
external. `plymouthd` wants root and its theme comes from
`/etc/plymouth/plymouthd.conf`, so this needs a sudo session.
**Do not ship this blind:** the same script draws the **LUKS passphrase box**, - `unshare -rm` gives a user + mount namespace: root *in there* only, every
this machine is encrypted, and a script that fails to parse leaves a prompt you mount invisible to the real system and gone when it exits.
cannot see. Pass = with the rig, a two-head render centred on both; then a real - `/run/plymouth` is BOTH the daemon's socket dir **and** its compiled-in
docked boot + shutdown, undocked unchanged. plugin/theme path (`strings plymouthd``/run/plymouth/plugins/`). Bind a
writable dir over it **and recreate its three symlinks** (`plugins`,
`themes`, `plymouthd.defaults``/etc/plymouth/…`) or plymouth cannot find
`x11.so`, silently skips X11 and reaches for a tty.
- **Do not pass `--tty`**: `main.c:2133` only takes the X11 path while
`default_tty` is unset. Bind an empty dir over `/dev/dri` too — with X11
first in the renderer order (`ply-renderer.c:283`) DRM is never used, and the
mask makes that a guarantee rather than a hope.
- `PLY_CREATE_FAKE_MULTI_HEAD_SETUP=1` → 800×600 + 640×480 windows.
`plymouth show-splash`, then `plymouth ask-for-password` renders the LUKS
dialog. Debug: `--kernel-command-line="plymouth.debug=stream:/path splash"`
(**`stream:`**, not `file:``file:` buffers and you get nothing).
- Fast loop: copy the theme into the rig's `themes/` and sed its `.plymouth`
`ScriptFile=`/`ImageDir=` to the copy — then editing the script is a 10s
render instead of a 3min `nixos-rebuild`.
**What the rig already proved (2026-07-16):** with a *stable* two-head canvas
the **current** theme centres correctly on both heads — logo and progress bar
each in their own head's middle. So the geometry is not the bug, which is the
evidence for the timing diagnosis above. The fake path builds both heads up
front, so it cannot reproduce a canvas *change*: for that, plug/unplug while
the splash is up, or compare a boot with and without the external.
**Attempt 1 failed, and the rig caught it — that is the point.** Hoisting the
positions into a `layout()` called from `refresh_callback` rendered the
background on both heads and **nothing else**: no logo, no progress bar. The
script plugin logs `starting boot animation` and **no parse error**, so it
fails *silently* — reverted rather than shipped (that script draws the LUKS box
on an encrypted machine). Next session: bisect with the fast loop. Prime
suspect is scope — plymouth script assigns into `local` unless the name already
exists, so a `logo.image = …` inside a function may not be the `logo` the rest
of the script sees, even though the same pattern works in `update_progress_bar`.
Test that hypothesis *first*, in isolation, before rewriting anything.
Pass = with the rig, a two-head render centred on both **plus the password
dialog visible**; then a real docked boot + shutdown, undocked unchanged.
### 143. Rofi keeps the selected *row number* when the search changes, so the highlight lands on an unrelated entry ### 143. Rofi keeps the selected *row number* when the search changes, so the highlight lands on an unrelated entry

View File

@@ -108,6 +108,19 @@ unattended env it uses is in the script, and the same flow works
interactively from the live terminal. If the desktop comes up unthemed, interactively from the live terminal. If the desktop comes up unthemed,
read `/var/log/nomarchy-hm-preactivate.log` on the installed system. read `/var/log/nomarchy-hm-preactivate.log` on the installed system.
`tools/plymouth-preview.sh` renders the **boot splash** — including the LUKS
password dialog — into X11 windows in about ten seconds: no reboot, no root,
and no DRM. It runs plymouthd inside `unshare -rm` (a user + mount namespace),
binds a writable dir over `/run/plymouth` (which is both the daemon's socket
dir *and* its compiled-in plugin path), masks `/dev/dri` so a preview can never
mode-set a real output, and sets `PLY_CREATE_FAKE_MULTI_HEAD_SETUP` for plymouth's
built-in fake dual head. Pass it a theme store path to iterate on the script
without a rebuild. Use it before touching `modules/nixos/plymouth/`: that script
draws the passphrase box, a failure there is silent (the plugin logs
`starting boot animation` and no error), and "type your LUKS password blind" is
a bad way to find out. Its limit: the fake heads exist from the start, so it
cannot reproduce a head arriving *mid-splash* (#137).
Two full-desktop VM harnesses (softGL Hyprland, too heavy for `checks.*`) Two full-desktop VM harnesses (softGL Hyprland, too heavy for `checks.*`)
live next to the scripts: `tools/theme-shot.nix` (themed-desktop live next to the scripts: `tools/theme-shot.nix` (themed-desktop
screenshots) and `tools/monitor-fallback.nix` (KVM display transitions: screenshots) and `tools/monitor-fallback.nix` (KVM display transitions:

64
tools/plymouth-preview.sh Executable file
View File

@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Render the Plymouth splash in X11 windows — no root, no reboot, no DRM (#137).
#
# tools/plymouth-preview.sh [<theme-store-path>]
#
# Plymouth is otherwise untestable without rebooting, which is a poor loop for
# a script that draws the LUKS passphrase box on an encrypted machine. This
# gives you a real render in seconds:
#
# * `unshare -rm` = user + mount namespace. We are root only in here, and
# every mount below dies with the process.
# * /run/plymouth is both the daemon's socket dir AND its compiled-in plugin
# path (`strings plymouthd` → /run/plymouth/plugins/). So we bind a
# writable dir over it and recreate its symlinks — miss those and plymouth
# cannot find x11.so, silently skips X11, and goes looking for a tty.
# * X11 is first in plymouth's renderer order (ply-renderer.c:283) and is
# taken only while default_tty is unset — so never pass --tty. /dev/dri is
# masked as well: a preview must not be able to mode-set the real outputs.
# * PLY_CREATE_FAKE_MULTI_HEAD_SETUP (renderers/x11/plugin.c) fakes two heads
# (800x600 + 640x480) — the multi-monitor case, on a single-monitor desk.
#
# Caveat: the fake heads are created up front, so this proves rendering and
# centring, NOT #137's canvas-change bug (a head arriving mid-splash).
set -u
THEME="${1:-}"
PLY=$(nix build --no-link --print-out-paths nixpkgs#plymouth 2>/dev/null | head -1)
[ -n "$PLY" ] || { echo "could not build plymouth" >&2; exit 1; }
[ -n "${DISPLAY:-}" ] || { echo "no DISPLAY — the x11 renderer needs Xwayland" >&2; exit 1; }
export PLY THEME
unshare -rm -- bash -euc '
rm -rf /tmp/plyrig; mkdir -p /tmp/plyrig/run /tmp/plyrig/dri /tmp/plyrig/themes
if [ -n "$THEME" ]; then
cp -rL "$THEME/share/plymouth/themes/nomarchy" /tmp/plyrig/themes/
# The .plymouth bakes absolute store paths at build time; point them at the
# copy so the script here is the one that renders (edit + rerun = seconds).
sed -i "s|^ScriptFile=.*|ScriptFile=/tmp/plyrig/themes/nomarchy/nomarchy.script|;
s|^ImageDir=.*|ImageDir=/tmp/plyrig/themes/nomarchy|" \
/tmp/plyrig/themes/nomarchy/nomarchy.plymouth
ln -s /tmp/plyrig/themes /tmp/plyrig/run/themes
else
ln -s /etc/plymouth/themes /tmp/plyrig/run/themes
fi
ln -s /etc/plymouth/plugins /tmp/plyrig/run/plugins
ln -s /etc/plymouth/plymouthd.defaults /tmp/plyrig/run/plymouthd.defaults
mount --bind /tmp/plyrig/run /run/plymouth
mount --bind /tmp/plyrig/dri /dev/dri 2>/dev/null || true
export PLY_CREATE_FAKE_MULTI_HEAD_SETUP=1
"$PLY/sbin/plymouthd" --no-daemon --mode=boot --no-boot-log \
--kernel-command-line="plymouth.debug=stream:/tmp/plyrig/dbg.log splash" \
>/dev/null 2>&1 &
d=$!
sleep 2
"$PLY/bin/plymouth" show-splash >/dev/null 2>&1
echo "splash up — two windows (800x600, 640x480). Ctrl+C or wait 20s."
sleep 8
"$PLY/bin/plymouth" ask-for-password --prompt="Unlock" --number-of-tries=1 >/dev/null 2>&1 &
echo "password dialog requested (the LUKS path)"
sleep 10
"$PLY/bin/plymouth" quit >/dev/null 2>&1; sleep 1; kill $d 2>/dev/null
cp /tmp/plyrig/dbg.log /tmp/plymouth-preview.log 2>/dev/null
echo "log: /tmp/plymouth-preview.log"
'