diff --git a/README.md b/README.md index b4fe5f0..eaa9184 100644 --- a/README.md +++ b/README.md @@ -403,10 +403,9 @@ bootloader: ```yaml plymouth: - theme: spinner # passed to plymouth-set-default-theme + theme: spinner # a built-in archinstall theme (see below) grub: - cmdline_extra: "quiet splash" # appended to GRUB_CMDLINE_LINUX_DEFAULT theme: source: vinceliuice # vinceliuice | url | none name: tela # vinceliuice theme: tela|stylish|vimix|whitesur|slaze @@ -415,6 +414,13 @@ grub: # url: https://example.com/theme.tar.gz # used when source: url ``` +Plymouth is installed and configured by **archinstall in Phase A**: it adds the initramfs hook, +appends `quiet splash` to the kernel cmdline, sets the theme and regenerates the initramfs. `theme` +must be one of archinstall's built-in themes — `bgrt`, `fade-in`, `glow`, `script`, `solar`, +`spinner`, `spinfinity`, `tribar`, `text`, `details` — because archinstall aborts the install on an +unknown value; **custom Plymouth themes are not supported** via this path. Leave `plymouth` unset for +no boot splash. + For `source: vinceliuice`, `screen` maps to the installer's `-s` flag and `background` supplies a custom image (the installer uses a `background.jpg` in its checkout). `background` may be a local path or an `http(s)` URL. **Heads-up on ordering:** the `grub-theme` stage (60) runs *before* @@ -650,14 +656,13 @@ name **or** number. | # | Stage | Phase | What it does | |---|-------|-------|--------------| | 0 | `preflight` | A | UEFI + config + archinstall version checks (warns, doesn't block) | -| 10 | `archinstall` | A | reflector → probe geometry → render JSON → `archinstall --silent` → chroot: repos + kernels → stage the binary for Phase B | +| 10 | `archinstall` | A | reflector → probe geometry → render JSON (incl. `plymouth` boot splash) → `archinstall --silent` → chroot: repos + kernels → stage the binary for Phase B | | 10 | `yay` | B | install the AUR helper (`aur_helper`) | | 20 | `packages` | B | `pacman -S --needed` the official/custom-repo packages | | 25 | `snapper` | B | provision Snapper (only when btrfs + `snapshots: snapper`) | | 30 | `flatpak` | B | register `flatpak_remotes`, install `flatpaks` | | 40 | `aur` | B | build/install the `aur` list via the helper | -| 50 | `plymouth` | B | set the boot splash theme | -| 60 | `grub-theme` | B | apply the GRUB theme + cmdline extras | +| 60 | `grub-theme` | B | apply the GRUB theme | | 70 | `kde` | B | KDE global theme via `LookAndFeelPackage` in kdeglobals (no-op for other DEs) | | 80 | `dotfiles` | B | apply dotfiles via the configured manager | | 85 | `setup` | B | run the ordered `setup.steps` (clones/commands) | diff --git a/config.example.yaml b/config.example.yaml index b14392d..b877093 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -213,11 +213,14 @@ bootloader: kind: grub # grub | systemd-boot # --- boot splash / theming -------------------------------------------------- +# Plymouth is installed and configured by archinstall in Phase A: it adds the +# initramfs hook, appends "quiet splash" to the kernel cmdline and sets the theme. +# theme must be one of archinstall's built-in themes (custom themes unsupported): +# bgrt | fade-in | glow | script | solar | spinner | spinfinity | tribar | text | details plymouth: - theme: spinner # name passed to plymouth-set-default-theme + theme: spinner grub: - cmdline_extra: "quiet splash" # appended to GRUB_CMDLINE_LINUX_DEFAULT theme: source: vinceliuice # vinceliuice | url | none name: tela # vinceliuice theme name (tela|stylish|vimix|whitesur|slaze) diff --git a/docs/bugs/plymouth-bootctl-update-fails-systemd-boot.md b/docs/bugs/plymouth-bootctl-update-fails-systemd-boot.md index a6c9538..13e8d1a 100644 --- a/docs/bugs/plymouth-bootctl-update-fails-systemd-boot.md +++ b/docs/bugs/plymouth-bootctl-update-fails-systemd-boot.md @@ -1,6 +1,11 @@ # Bug: Phase B `regenerateBootConfig` runs `bootctl update` on systemd-boot and fails, aborting bootstrap (via the always-on plymouth stage) -**Status:** open — found by the automated VM e2e harness (`test/e2e/vm/`, descriptor `sdboot-lvm`), 2026-06-23 +**Status:** resolved 2026-06-30 — Plymouth moved to archinstall (Phase A) on the 4.4 bump. +The Phase B `plymouth` stage, `regenerateBootConfig`/`ensureKernelParam` helpers, and the +`bootctl update --graceful` workaround described below were all removed; archinstall now installs +and configures Plymouth (hook, `quiet splash`, theme) during the install, so no Phase B step ever +runs `bootctl update`. The failure path no longer exists. (Originally found by the automated VM e2e +harness `test/e2e/vm/`, descriptor `sdboot-lvm`, 2026-06-23.) **Area:** `internal/stages/helpers.go` (`regenerateBootConfig`); surfaced via `internal/stages/plymouth.go` **Severity:** high — Phase B `bootstrap` **fails on any systemd-boot system** at the plymouth stage (and any other stage that regenerates boot config), even with a default config. diff --git a/docs/vm-validation.md b/docs/vm-validation.md index 681cbb6..ad70615 100644 --- a/docs/vm-validation.md +++ b/docs/vm-validation.md @@ -68,7 +68,8 @@ Each shape was reverse-engineered; the harness has now exercised them all. | Snapper | timer-unit + `set-config` key names | ✅ confirmed — `btrfs-snapper` installs snapper + green | | Btrfs | subvolume JSON `{name, mountpoint}` | ❌ **bug** — shape parses, but archinstall installs to the **top-level** subvolume; the configured `@` is created but unused as root → [`btrfs-subvolume-not-used-as-root.md`](bugs/btrfs-subvolume-not-used-as-root.md) | | LVM | multi-volume "rest of VG" sizing (fixed root + remainder `/home`) | ❌ **bug** — the PV partition renders with an empty `fs_type`; archinstall aborts Phase A before sizing is reached → [`lvm-multivolume-pv-fstype-empty.md`](bugs/lvm-multivolume-pv-fstype-empty.md) | -| systemd-boot | loader-entry default + `bootctl update` cmdline-refresh path | ⚠️ install + boot **work**; the Phase-B `bootctl update` refresh path **fails** (nonzero when already current), reached via the always-on plymouth stage → [`plymouth-bootctl-update-fails-systemd-boot.md`](bugs/plymouth-bootctl-update-fails-systemd-boot.md) | +| systemd-boot | loader-entry default | ⚠️ install + boot **work**. The Phase-B `bootctl update` refresh path that previously failed is **gone**: the always-on plymouth stage that triggered it was removed when Plymouth moved to archinstall (Phase A) — see [`plymouth-bootctl-update-fails-systemd-boot.md`](bugs/plymouth-bootctl-update-fails-systemd-boot.md) (resolved). | +| Plymouth | `bootloader_config.plymouth` (archinstall 4.4 built-in theme) | ⚠️ **not yet VM-validated** — archinstall installs/configures the splash in Phase A; confirm the `spinner` theme is set and quiet/splash reach the kernel cmdline on a real run. | (A fourth bug unrelated to a disk shape — the flatpak stage hangs on a polkit prompt — is in [`flatpak-system-remote-add-polkit-hang.md`](bugs/flatpak-system-remote-add-polkit-hang.md).) diff --git a/internal/archinstall/archinstall.go b/internal/archinstall/archinstall.go index c40afe3..233a0bd 100644 --- a/internal/archinstall/archinstall.go +++ b/internal/archinstall/archinstall.go @@ -37,7 +37,7 @@ import ( ) // Version is the archinstall release this schema was modelled against. -const Version = "4.3" +const Version = "4.4" // newObjID generates the unique obj_id values archinstall uses to cross-reference // partitions from lvm_pvs. It is a package var solely so golden render tests can @@ -167,10 +167,19 @@ type DiskConfig struct { // (args.py BootloaderConfiguration). It replaces the deprecated bare // `bootloader: "Grub"` string. Bootloader is one of "Grub", "Systemd-boot", // "Efistub", "Limine", "Refind", "No bootloader"; uki/removable default false. +// +// Plymouth, added in archinstall 4.4, is one of archinstall's built-in +// PlymouthTheme values (e.g. "spinner", "bgrt"). When set, archinstall installs +// plymouth, inserts the initramfs hook, appends "quiet splash" to the kernel +// cmdline, runs plymouth-set-default-theme and regenerates the initramfs — all in +// Phase A. omitempty keeps the unconfigured render byte-identical to before, and a +// non-built-in value is rejected (archinstall sys.exit(1)s on unknown themes, so +// config validation restricts Plymouth.Theme to the same built-in set). type BootloaderConfig struct { Bootloader string `json:"bootloader"` UKI bool `json:"uki"` Removable bool `json:"removable"` + Plymouth string `json:"plymouth,omitempty"` } // archBootloader maps our config bootloader kind ("grub"/"systemd-boot") to @@ -293,7 +302,7 @@ func Build(cfg *config.Config, geom Geometry, password string) (*Config, *Creds, pkgs := append([]string(nil), cfg.Pacstrap...) c := &Config{ Lang: "English", - BootloaderConfig: BootloaderConfig{Bootloader: archBootloader(cfg.Bootloader.EffectiveKind()), UKI: false, Removable: false}, + BootloaderConfig: BootloaderConfig{Bootloader: archBootloader(cfg.Bootloader.EffectiveKind()), UKI: false, Removable: false, Plymouth: cfg.Plymouth.Theme}, Kernels: kernelBase(cfg), Hostname: cfg.System.Hostname, Packages: pkgs, diff --git a/internal/archinstall/bootloader_render_test.go b/internal/archinstall/bootloader_render_test.go index c51a422..af43d3f 100644 --- a/internal/archinstall/bootloader_render_test.go +++ b/internal/archinstall/bootloader_render_test.go @@ -70,3 +70,22 @@ func TestBuild_BootloaderSystemdBoot(t *testing.T) { t.Errorf("systemd-boot bootloader = %q, want %q", got, "Systemd-boot") } } + +// A configured plymouth theme is rendered into bootloader_config.plymouth so +// archinstall installs and configures the splash in Phase A (replacing the old +// Phase B plymouth stage). The theme string passes through verbatim. +func TestBuild_PlymouthThemeRendered(t *testing.T) { + out := buildForBootloader(t, "plymouth:\n theme: spinner\n") + if got := out.BootloaderConfig.Plymouth; got != "spinner" { + t.Errorf("plymouth theme = %q, want %q", got, "spinner") + } +} + +// With no plymouth theme the field is omitted (omitempty), keeping the rendered +// bootloader_config byte-identical to the pre-4.4 output the golden files assert. +func TestBuild_PlymouthOmittedWhenUnset(t *testing.T) { + out := buildForBootloader(t, "") + if got := out.BootloaderConfig.Plymouth; got != "" { + t.Errorf("plymouth theme = %q, want empty", got) + } +} diff --git a/internal/archinstall/testdata/golden/btrfs-subvols.config.json b/internal/archinstall/testdata/golden/btrfs-subvols.config.json index fed8f37..5d157fd 100644 --- a/internal/archinstall/testdata/golden/btrfs-subvols.config.json +++ b/internal/archinstall/testdata/golden/btrfs-subvols.config.json @@ -114,6 +114,6 @@ "lvm_config": null, "disk_encryption": null }, - "version": "4.3", - "config_version": "4.3" + "version": "4.4", + "config_version": "4.4" } diff --git a/internal/archinstall/testdata/golden/ext4-sata.config.json b/internal/archinstall/testdata/golden/ext4-sata.config.json index 9ab97a5..3a42f6a 100644 --- a/internal/archinstall/testdata/golden/ext4-sata.config.json +++ b/internal/archinstall/testdata/golden/ext4-sata.config.json @@ -128,6 +128,6 @@ }, "disk_encryption": null }, - "version": "4.3", - "config_version": "4.3" + "version": "4.4", + "config_version": "4.4" } diff --git a/internal/archinstall/testdata/golden/lvm-volumes.config.json b/internal/archinstall/testdata/golden/lvm-volumes.config.json index 1eed940..bd3e55f 100644 --- a/internal/archinstall/testdata/golden/lvm-volumes.config.json +++ b/internal/archinstall/testdata/golden/lvm-volumes.config.json @@ -145,6 +145,6 @@ }, "disk_encryption": null }, - "version": "4.3", - "config_version": "4.3" + "version": "4.4", + "config_version": "4.4" } diff --git a/internal/archinstall/testdata/golden/lvm-zram.config.json b/internal/archinstall/testdata/golden/lvm-zram.config.json index 430a54f..df60122 100644 --- a/internal/archinstall/testdata/golden/lvm-zram.config.json +++ b/internal/archinstall/testdata/golden/lvm-zram.config.json @@ -128,6 +128,6 @@ }, "disk_encryption": null }, - "version": "4.3", - "config_version": "4.3" + "version": "4.4", + "config_version": "4.4" } diff --git a/internal/archinstall/testdata/golden/multi-disk-lvm.config.json b/internal/archinstall/testdata/golden/multi-disk-lvm.config.json index d4168f4..ac01bee 100644 --- a/internal/archinstall/testdata/golden/multi-disk-lvm.config.json +++ b/internal/archinstall/testdata/golden/multi-disk-lvm.config.json @@ -196,6 +196,6 @@ }, "disk_encryption": null }, - "version": "4.3", - "config_version": "4.3" + "version": "4.4", + "config_version": "4.4" } diff --git a/internal/archinstall/testdata/golden/single-disk-lvm.config.json b/internal/archinstall/testdata/golden/single-disk-lvm.config.json index ed36980..4d4b716 100644 --- a/internal/archinstall/testdata/golden/single-disk-lvm.config.json +++ b/internal/archinstall/testdata/golden/single-disk-lvm.config.json @@ -128,6 +128,6 @@ }, "disk_encryption": null }, - "version": "4.3", - "config_version": "4.3" + "version": "4.4", + "config_version": "4.4" } diff --git a/internal/config/config.go b/internal/config/config.go index 3e6a4fa..95f899c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -77,13 +77,17 @@ type Config struct { Multilib bool `yaml:"multilib"` } `yaml:"pacman"` + // Plymouth selects the boot splash theme. Theme must be one of archinstall's + // built-in PlymouthTheme values: archinstall installs and configures plymouth + // in Phase A (it appends "quiet splash" to the kernel cmdline itself), and it + // sys.exit(1)s on a theme outside this set, so the enum is enforced here too. + // Custom (non-built-in) themes are unsupported by this path. Empty = no splash. Plymouth struct { - Theme string `yaml:"theme"` + Theme string `yaml:"theme" validate:"omitempty,oneof=bgrt fade-in glow script solar spinner spinfinity tribar text details"` } `yaml:"plymouth"` GRUB struct { - CmdlineExtra string `yaml:"cmdline_extra"` - Theme struct { + Theme struct { Source string `yaml:"source" validate:"omitempty,oneof=vinceliuice url none"` Name string `yaml:"name"` URL string `yaml:"url" validate:"omitempty,url"` diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f467d51..7de1d4e 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -53,7 +53,6 @@ aur: [1password] plymouth: theme: bgrt grub: - cmdline_extra: "quiet splash" theme: source: vinceliuice name: tela @@ -203,6 +202,11 @@ func TestValidate_Errors(t *testing.T) { yaml: strings.Replace(validYAML, "pvs: [/dev/nvme0n1p3, /dev/sda]", "pvs: []", 1), want: []string{"disks.lvm.pvs must have at least 1 item(s)"}, }, + { + name: "non-built-in plymouth theme", + yaml: strings.Replace(validYAML, "theme: bgrt", "theme: my-custom-theme", 1), + want: []string{"plymouth.theme must be one of: bgrt fade-in glow script solar spinner spinfinity tribar text details"}, + }, { name: "bad grub theme source", yaml: strings.Replace(validYAML, "source: vinceliuice", "source: bananas", 1), diff --git a/internal/stages/bootloader_test.go b/internal/stages/bootloader_test.go deleted file mode 100644 index d29545a..0000000 --- a/internal/stages/bootloader_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package stages - -import "testing" - -// TestPlan_PlymouthBootConfig proves the C3 refactor is behaviour-preserving: the -// plymouth stage still emits the kernel-param sed (now via the shared, bootloader- -// aware ensureKernelParam helper) and the GRUB regeneration (now via -// regenerateBootConfig) into the recorded .Plan. Self-contained config so it does -// not depend on the shared testYAML fixture. -func TestPlan_PlymouthBootConfig(t *testing.T) { - plan := planForCfg(t, Bootstrap, "plymouth", ` -plymouth: - theme: bgrt -grub: - cmdline_extra: "quiet splash" -`) - mustContain(t, plan, - // each cmdline token appended to GRUB_CMDLINE_LINUX_DEFAULT, idempotently - `grep -qE 'GRUB_CMDLINE_LINUX_DEFAULT="[^"]*\bquiet\b' /etc/default/grub || `+ - `sed -i -E 's/(GRUB_CMDLINE_LINUX_DEFAULT=")/\1quiet /' /etc/default/grub`, - `grep -qE 'GRUB_CMDLINE_LINUX_DEFAULT="[^"]*\bsplash\b' /etc/default/grub || `+ - `sed -i -E 's/(GRUB_CMDLINE_LINUX_DEFAULT=")/\1splash /' /etc/default/grub`, - // bootloader config regenerated after the cmdline/theme change - "grub-mkconfig -o /boot/grub/grub.cfg", - ) -} diff --git a/internal/stages/fromto_test.go b/internal/stages/fromto_test.go index 3329eed..4742244 100644 --- a/internal/stages/fromto_test.go +++ b/internal/stages/fromto_test.go @@ -27,12 +27,12 @@ func TestWithin(t *testing.T) { { name: "from only by name is inclusive lower bound", from: "flatpak", to: "", - want: []string{"flatpak", "aur", "plymouth", "grub-theme", "kde", "dotfiles", "setup", "services"}, + want: []string{"flatpak", "aur", "grub-theme", "kde", "dotfiles", "setup", "services"}, }, { name: "from only by number resolves same as name", from: "30", to: "", - want: []string{"flatpak", "aur", "plymouth", "grub-theme", "kde", "dotfiles", "setup", "services"}, + want: []string{"flatpak", "aur", "grub-theme", "kde", "dotfiles", "setup", "services"}, }, { name: "to only by name is inclusive upper bound", @@ -46,13 +46,13 @@ func TestWithin(t *testing.T) { }, { name: "both bounds inclusive on each end", - from: "packages", to: "plymouth", - want: []string{"packages", "snapper", "flatpak", "aur", "plymouth"}, + from: "packages", to: "aur", + want: []string{"packages", "snapper", "flatpak", "aur"}, }, { name: "both bounds mixing name and number", from: "20", to: "kde", - want: []string{"packages", "snapper", "flatpak", "aur", "plymouth", "grub-theme", "kde"}, + want: []string{"packages", "snapper", "flatpak", "aur", "grub-theme", "kde"}, }, { name: "single-stage window when from equals to", @@ -61,7 +61,7 @@ func TestWithin(t *testing.T) { }, { name: "inverted bounds is an error", - from: "plymouth", + from: "aur", to: "packages", wantErr: "from", }, @@ -105,12 +105,12 @@ func TestWithin(t *testing.T) { // TestWithin_ComposesWithSkip confirms the filter operates on whatever Select // already produced: a skipped stage stays absent even when inside the window. func TestWithin_ComposesWithSkip(t *testing.T) { - in := Select(Bootstrap, "", []string{"aur"}, nil) - got, err := Within(in, "packages", "plymouth") + in := Select(Bootstrap, "", []string{"snapper"}, nil) + got, err := Within(in, "packages", "aur") if err != nil { t.Fatalf("unexpected error: %v", err) } - want := []string{"packages", "snapper", "flatpak", "plymouth"} // aur removed by --skip + want := []string{"packages", "flatpak", "aur"} // snapper removed by --skip if g := names(got); !equalStrings(g, want) { t.Fatalf("Within over skipped input = %v, want %v", g, want) } @@ -118,7 +118,7 @@ func TestWithin_ComposesWithSkip(t *testing.T) { // TestWithin_EmptyInput is a no-op pass-through, never a panic. func TestWithin_EmptyInput(t *testing.T) { - got, err := Within(nil, "packages", "plymouth") + got, err := Within(nil, "packages", "aur") if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/internal/stages/helpers.go b/internal/stages/helpers.go index db1f423..2c0c167 100644 --- a/internal/stages/helpers.go +++ b/internal/stages/helpers.go @@ -20,54 +20,6 @@ func ensureTool(ctx *Context, bin, pkg string) error { return ctx.R.Root("pacman", "-S", "--needed", "--noconfirm", pkg) } -// ensureKernelParam adds a single kernel command-line token, idempotently. It is -// the bootloader-aware seam for kernel-cmdline edits, keyed off the configured -// bootloader: -// - grub: appends tok to GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub -// (grep-guarded sed) — UNCHANGED from before, byte-identical command. -// - systemd-boot: appends tok to /etc/kernel/cmdline (the single-line cmdline -// consumed by kernel-install when generating loader entries), grep-guarded so -// re-runs don't duplicate the token. The file is created if missing. -func ensureKernelParam(ctx *Context, tok string) error { - if ctx.Cfg.Bootloader.EffectiveKind() == "systemd-boot" { - // /etc/kernel/cmdline is a single space-separated line. Add tok only if its - // word boundary isn't already present; create the file if it doesn't exist. - // The whole pipeline runs as root via RootShell, so no inner per-command sudo. - return ctx.R.RootShell(fmt.Sprintf( - `grep -qE '\b%[1]s\b' /etc/kernel/cmdline 2>/dev/null || `+ - `{ touch /etc/kernel/cmdline && `+ - `sed -i -E '$ s/$/ %[1]s/' /etc/kernel/cmdline; }`, - tok)) - } - return ctx.R.RootShell(fmt.Sprintf( - `grep -qE 'GRUB_CMDLINE_LINUX_DEFAULT="[^"]*\b%[1]s\b' /etc/default/grub || `+ - `sed -i -E 's/(GRUB_CMDLINE_LINUX_DEFAULT=")/\1%[1]s /' /etc/default/grub`, - tok)) -} - -// regenerateBootConfig regenerates the bootloader's generated configuration after a -// kernel-cmdline or theme change. It is the bootloader-aware seam for the -// "apply the config" step, keyed off the configured bootloader: -// - grub: runs grub-mkconfig -o /boot/grub/grub.cfg through Root — UNCHANGED from -// before, byte-identical command. -// - systemd-boot: there is no grub.cfg to regenerate. We refresh the installed -// boot loader binary on the ESP with `bootctl update --graceful`; loader entries -// are regenerated separately by kernel-install when the cmdline changes. The -// `--graceful` flag makes bootctl exit 0 when there is nothing to do (the loader -// archinstall just installed is already current) instead of failing, and the -// whole step is best-effort via TryRoot so a nonzero exit can never abort the -// stage — the loader was already installed by archinstall and our cmdline edits -// live in /etc/kernel/cmdline, which systemd-boot reads directly. (See -// docs/bugs/plymouth-bootctl-update-fails-systemd-boot.md.) -func regenerateBootConfig(ctx *Context) error { - if ctx.Cfg.Bootloader.EffectiveKind() == "systemd-boot" { - // Best-effort + graceful: never fail the stage on "already current". - ctx.R.TryRoot("bootctl", "update", "--graceful") - return nil - } - return ctx.R.Root("grub-mkconfig", "-o", "/boot/grub/grub.cfg") -} - // cloneBuild git-clones a repo into a temp dir, runs inCheckout from within it, // and removes the temp dir — the shared "clone, build, clean" idiom. cloneArgs is // the git-clone argument string (any flags plus the URL); inCheckout is the shell diff --git a/internal/stages/plymouth.go b/internal/stages/plymouth.go deleted file mode 100644 index 58887e1..0000000 --- a/internal/stages/plymouth.go +++ /dev/null @@ -1,65 +0,0 @@ -package stages - -import ( - "strings" - - "github.com/AdamJHall/archwright/internal/ui" -) - -// plymouth is the Phase B 50-plymouth equivalent: install Plymouth, wire its -// initramfs hook, set the 'quiet splash' kernel params, set the theme, and -// regenerate grub.cfg. -type plymouth struct{} - -func init() { register(plymouth{}) } - -func (plymouth) Order() int { return 50 } -func (plymouth) Name() string { return "plymouth" } -func (plymouth) Phase() Phase { return Bootstrap } - -func (plymouth) Run(ctx *Context) error { - // Gate off when unconfigured: with no plymouth.theme set the stage is a clean - // no-op so a default config never installs plymouth or touches the bootloader - // (see docs/bugs/plymouth-bootctl-update-fails-systemd-boot.md). - theme := ctx.Cfg.Plymouth.Theme - if theme == "" { - ui.Warn("no plymouth theme in config — skipping") - return nil - } - cmdline := ctx.Cfg.GRUB.CmdlineExtra - if cmdline == "" { - cmdline = "quiet splash" - } - - if err := ensureTool(ctx, "plymouth", "plymouth"); err != nil { - return err - } - - // Add the plymouth hook after 'udev' (fallback 'systemd'), idempotently. - if err := ctx.R.Shell( - `grep -qE '^HOOKS=.*\bplymouth\b' /etc/mkinitcpio.conf || ` + - `sudo sed -i -E 's/^(HOOKS=.*)\budev\b/\1udev plymouth/; t; s/^(HOOKS=.*)\bsystemd\b/\1systemd plymouth/' /etc/mkinitcpio.conf`, - ); err != nil { - return err - } - - // Ensure each kernel param is present in GRUB_CMDLINE_LINUX_DEFAULT. - for _, tok := range strings.Fields(cmdline) { - if err := ensureKernelParam(ctx, tok); err != nil { - return err - } - } - - // -R rebuilds the initramfs, picking up the new HOOKS line too. - if err := ctx.R.Root("plymouth-set-default-theme", "-R", theme); err != nil { - return err - } - // Regenerating the bootloader config is GRUB's concern, not Plymouth's: route - // it through the shared bootloader-aware helper (same emitted command). - if err := regenerateBootConfig(ctx); err != nil { - return err - } - - ui.OK("plymouth configured") - return nil -} diff --git a/internal/stages/plymouth_test.go b/internal/stages/plymouth_test.go deleted file mode 100644 index f122411..0000000 --- a/internal/stages/plymouth_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package stages - -import "testing" - -// Regression coverage for docs/bugs/plymouth-bootctl-update-fails-systemd-boot.md: -// the plymouth stage must (a) be a clean no-op when no plymouth theme is configured -// and (b) refresh systemd-boot via the graceful, best-effort `bootctl update -// --graceful` rather than a plain `bootctl update` that aborts bootstrap when the -// loader is already current. The grub path stays byte-identical. Self-contained -// inline configs — no shared fixtures touched. - -// With no plymouth: block the stage installs nothing and never touches the -// bootloader, even on a systemd-boot config (the default-config case that broke -// every systemd-boot bootstrap). -func TestPlan_PlymouthUnconfiguredIsNoop(t *testing.T) { - plan := planForCfg(t, Bootstrap, "plymouth", ` -bootloader: - kind: systemd-boot -`) - if len(plan) != 0 { - t.Errorf("plymouth stage should be a no-op when unconfigured, got plan:\n%v", plan) - } - mustNotContain(t, plan, - "plymouth", - "bootctl", - "grub-mkconfig", - "/etc/kernel/cmdline", - "plymouth-set-default-theme", - ) -} - -// A configured theme on systemd-boot regenerates boot config via the graceful, -// best-effort invocation and never the plain failing form. -func TestPlan_PlymouthSystemdBootGraceful(t *testing.T) { - plan := planForCfg(t, Bootstrap, "plymouth", ` -bootloader: - kind: systemd-boot -plymouth: - theme: bgrt -`) - // best-effort step still goes through Root semantics (sudo in Phase B). - mustContain(t, plan, - "sudo bootctl update --graceful", - ) - // the plain, non-graceful form (which exits non-zero when already current and - // aborted bootstrap) must be gone, as must any grub regeneration. - if hasExact(plan, "sudo bootctl update") || hasExact(plan, "bootctl update") { - t.Errorf("plan must not contain a plain `bootctl update`; plan:\n%v", plan) - } - mustNotContain(t, plan, "grub-mkconfig -o /boot/grub/grub.cfg") -} - -// A configured theme on a grub config regenerates grub.cfg unchanged. -func TestPlan_PlymouthGrubUnchanged(t *testing.T) { - plan := planForCfg(t, Bootstrap, "plymouth", ` -plymouth: - theme: bgrt -`) - mustContain(t, plan, "grub-mkconfig -o /boot/grub/grub.cfg") - mustNotContain(t, plan, "bootctl") -} - -// hasExact reports whether any recorded plan line equals line exactly (as opposed -// to mustContain's substring match, which `bootctl update --graceful` would -// satisfy for the substring `bootctl update`). -func hasExact(plan []string, line string) bool { - for _, p := range plan { - if p == line { - return true - } - } - return false -} diff --git a/internal/stages/stages_test.go b/internal/stages/stages_test.go index 83f7f06..eda2175 100644 --- a/internal/stages/stages_test.go +++ b/internal/stages/stages_test.go @@ -63,7 +63,6 @@ aur: [1password, 1password-cli] plymouth: theme: bgrt grub: - cmdline_extra: "quiet splash" theme: source: vinceliuice name: tela @@ -148,8 +147,8 @@ func TestRegistry(t *testing.T) { []string{"preflight", "archinstall"}, []int{0, 10}) check(Bootstrap, - []string{"yay", "packages", "snapper", "flatpak", "aur", "plymouth", "grub-theme", "kde", "dotfiles", "setup", "services"}, - []int{10, 20, 25, 30, 40, 50, 60, 70, 80, 85, 90}) + []string{"yay", "packages", "snapper", "flatpak", "aur", "grub-theme", "kde", "dotfiles", "setup", "services"}, + []int{10, 20, 25, 30, 40, 60, 70, 80, 85, 90}) } func TestPlan_Archinstall(t *testing.T) { @@ -213,13 +212,6 @@ func TestPlan_Flatpak(t *testing.T) { ) } -func TestPlan_Plymouth(t *testing.T) { - mustContain(t, planFor(t, Bootstrap, "plymouth"), - "plymouth-set-default-theme -R bgrt", - "grub-mkconfig -o /boot/grub/grub.cfg", - ) -} - func TestPlan_GrubTheme(t *testing.T) { mustContain(t, planFor(t, Bootstrap, "grub-theme"), "git clone --depth 1 https://github.com/vinceliuice/grub2-themes", diff --git a/internal/stages/systemd_boot_test.go b/internal/stages/systemd_boot_test.go deleted file mode 100644 index 62a98af..0000000 --- a/internal/stages/systemd_boot_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package stages - -import "testing" - -// TestPlan_PlymouthSystemdBoot proves the A4 systemd-boot branch: with -// bootloader.kind: systemd-boot, the plymouth stage's ensureKernelParam edits -// /etc/kernel/cmdline (not /etc/default/grub) and regenerateBootConfig runs -// `bootctl update` (not grub-mkconfig). Self-contained inline config. -func TestPlan_PlymouthSystemdBoot(t *testing.T) { - plan := planForCfg(t, Bootstrap, "plymouth", ` -bootloader: - kind: systemd-boot -plymouth: - theme: bgrt -grub: - cmdline_extra: "quiet splash" -`) - mustContain(t, plan, - // each cmdline token appended to /etc/kernel/cmdline, idempotently. The - // whole pipeline runs as root via RootShell, so no inner per-command sudo. - `grep -qE '\bquiet\b' /etc/kernel/cmdline 2>/dev/null || `+ - `{ touch /etc/kernel/cmdline && `+ - `sed -i -E '$ s/$/ quiet/' /etc/kernel/cmdline; }`, - `grep -qE '\bsplash\b' /etc/kernel/cmdline 2>/dev/null || `+ - `{ touch /etc/kernel/cmdline && `+ - `sed -i -E '$ s/$/ splash/' /etc/kernel/cmdline; }`, - // systemd-boot refresh, no grub.cfg regeneration - "bootctl update", - ) - mustNotContain(t, plan, - "grub-mkconfig -o /boot/grub/grub.cfg", - `GRUB_CMDLINE_LINUX_DEFAULT`, - ) -} - -// TestPlan_PlymouthGrubDefault proves the default (empty bootloader) path still -// emits the GRUB sed + grub-mkconfig, byte-identical to today. -func TestPlan_PlymouthGrubDefault(t *testing.T) { - plan := planForCfg(t, Bootstrap, "plymouth", ` -plymouth: - theme: bgrt -grub: - cmdline_extra: "quiet splash" -`) - mustContain(t, plan, - `grep -qE 'GRUB_CMDLINE_LINUX_DEFAULT="[^"]*\bquiet\b' /etc/default/grub || `+ - `sed -i -E 's/(GRUB_CMDLINE_LINUX_DEFAULT=")/\1quiet /' /etc/default/grub`, - `grep -qE 'GRUB_CMDLINE_LINUX_DEFAULT="[^"]*\bsplash\b' /etc/default/grub || `+ - `sed -i -E 's/(GRUB_CMDLINE_LINUX_DEFAULT=")/\1splash /' /etc/default/grub`, - "grub-mkconfig -o /boot/grub/grub.cfg", - ) - mustNotContain(t, plan, - "bootctl update", - "/etc/kernel/cmdline", - ) -}