From 8d59cc4f0651bc280860e518932c6bcf9fdd48c0 Mon Sep 17 00:00:00 2001 From: fi3w0 Date: Sat, 3 Oct 2026 05:51:38 +0200 Subject: [PATCH] Complete optional setup and boot restoration workflow --- README.md | 24 ++- assets/images/Arch.png | Bin 0 -> 55501 bytes assets/images/Gentoo.png | Bin 0 -> 64814 bytes cmd/dots/main.go | 146 ++++++++++--- cmd/dots/main_test.go | 16 ++ docs/assets.md | 10 + docs/boot.md | 96 +++++++-- docs/scripts.md | 29 +++ docs/setup.md | 168 +++++++++------ lib/boot.py | 431 +++++++++++++++++++++++++++++++++++++++ lib/rice.py | 80 +++++++- lib/setup.py | 174 ++++++++++++++++ presets/fiw-ryzen.json | 4 +- presets/stock.json | 4 +- tests/test_setup.py | 142 +++++++++++++ 15 files changed, 1204 insertions(+), 120 deletions(-) create mode 100644 assets/images/Arch.png create mode 100644 assets/images/Gentoo.png create mode 100644 docs/scripts.md create mode 100644 lib/boot.py create mode 100644 lib/setup.py create mode 100644 tests/test_setup.py diff --git a/README.md b/README.md index 0c77eb7..9661485 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@
+Gentoo logo + # Fiw-Gentoo-Dots **My Gentoo setup, ready to make home feel like home again.** @@ -29,7 +31,7 @@ anything is applied. - **Keep your edits:** conflict prompts, backups and `.new` proposals on updates. **Work in progress:** configuration restoration and the TUI are ready for review. -Package installation needs a supervised first run on a fresh Gentoo system. +Package and boot execution need a supervised first run on a fresh Gentoo system. The existing installation has not been changed by creating this repo. ## Start @@ -41,10 +43,12 @@ cd Fiw-Gentoo-Dots ``` The Bubble Tea interface offers a preset, package sections, individual app -configs, kernel/bootloader choices, extras, and a final preview. Enter at the -preview saves a selection without applying it. `a` restores configs; `i` -starts the package installation workflow with sudo. Go 1.24+ builds the TUI; -Python 3.11+ runs the backend. Go dependencies are recorded in go.mod/go.sum. +configs, kernel/bootloader choices, individual Flatpaks, optional services and +a final preview. Enter saves without applying. `r` runs the full restore; +`a` restores configs, `i` installs Portage packages, `f` installs Flatpaks, +`v` enables selected services and `b` deploys the selected bootloader. +Go 1.24+ builds the TUI; Python 3.11+ runs the backend. Go dependencies are +recorded in go.mod/go.sum. Saved selections are reused on subsequent launches. For a preview without building the TUI: @@ -85,9 +89,11 @@ CachyOS 7.2.7-1 patch. Refresh it explicitly when updating the snapshot. | [tidewm](packages/tidewm.list) | Optional compositor and supporting utilities | Filelight, NVIDIA drivers and Btrfs tools are separate extras. Ext4 does not -require Btrfs tools. LocalSend is an optional Flatpak in the apps section. +require Btrfs tools. LocalSend and Sober are individually optional Flatpaks +in the TUI, grouped with apps and gaming respectively. Limine and GRUB are optional alternatives; retaining the current loader is -the default. Kernel choice is independent of bootloader choice. +the default. Kernel choice is independent of bootloader choice. Deployment +previews the target ESP and firmware choices; see [boot setup](docs/boot.md). ## Configs @@ -136,6 +142,10 @@ python3 -m unittest discover -s tests go test ./... ``` +Missing selected overlays are staged before package resolution. Optional +service presets enable audio, networking, Bluetooth and power profiles for +the next boot/login. See [setup](docs/setup.md) and the [helper list](docs/scripts.md). + Review captures before committing. `local/` and built binaries are ignored. This repo starts with fresh history. Third-party theme/cursor assets retain their licence notices; overlay ebuilds retain their original notices. diff --git a/assets/images/Arch.png b/assets/images/Arch.png new file mode 100644 index 0000000000000000000000000000000000000000..b39010d937cd94e26d41ae3557d4762c6a019c40 GIT binary patch literal 55501 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

;lM7()xE{AG*G#d(&L+dW&+Z=Z5pD|> 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 literal 0 HcmV?d00001 diff --git a/assets/images/Gentoo.png b/assets/images/Gentoo.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd820ad7316fb7d94a95fb21b3a14d70c3963bc GIT binary patch literal 64814 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�oe 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 PpJPW)9Z5Un^z8our|hH} literal 0 HcmV?d00001 diff --git a/cmd/dots/main.go b/cmd/dots/main.go index a8dd413..9ba9bc8 100644 --- a/cmd/dots/main.go +++ b/cmd/dots/main.go @@ -22,17 +22,27 @@ type selection struct { Extras []string `json:"extras"` Firefox bool `json:"firefox_privacy"` Spotify bool `json:"spotify_custom"` + Flatpaks []string `json:"flatpaks"` + Services []string `json:"services"` } type config struct { Label string `json:"label"` Group string `json:"group"` } +type setupOption struct { + Label string `json:"label"` + Group string `json:"group"` + Scope string `json:"scope"` +} + type catalog struct { - Groups []string `json:"groups"` - Configs map[string]config `json:"configs"` - Extras map[string]string `json:"extras"` - Stock selection `json:"stock"` - Ryzen selection `json:"ryzen"` + Groups []string `json:"groups"` + Configs map[string]config `json:"configs"` + Extras map[string]string `json:"extras"` + Flatpaks map[string]setupOption `json:"flatpaks"` + Services map[string]setupOption `json:"services"` + Stock selection `json:"stock"` + Ryzen selection `json:"ryzen"` } type row struct { id, label string @@ -102,6 +112,21 @@ func (m *model) prepare() { m.rows = append(m.rows, row{id, m.catalog.Extras[id], contains(m.selection.Extras, id)}) } m.rows = append(m.rows, row{"autostart", "Optional app autostart", contains(m.selection.Configs, "autostart")}, row{"firefox", "Optional Firefox-Privacy setup", m.selection.Firefox}, row{"spotify", "Optional Spotify customization script (stock by default)", m.selection.Spotify}) + case 4: + flatpaks := make([]string, 0, len(m.catalog.Flatpaks)) + for id := range m.catalog.Flatpaks { + flatpaks = append(flatpaks, id) + } + sort.Strings(flatpaks) + for _, id := range flatpaks { + option := m.catalog.Flatpaks[id] + m.rows = append(m.rows, row{"flatpak:" + id, "Flatpak: " + option.Label + " (" + option.Group + ")", contains(m.selection.Flatpaks, id)}) + } + for _, id := range []string{"audio", "network", "bluetooth", "power"} { + if option, ok := m.catalog.Services[id]; ok { + m.rows = append(m.rows, row{"service:" + id, option.Label, contains(m.selection.Services, id)}) + } + } } } func (m *model) remember() { @@ -155,6 +180,19 @@ func (m *model) remember() { } } m.selection.Configs = configNames + case 4: + m.selection.Flatpaks, m.selection.Services = []string{}, []string{} + for _, r := range m.rows { + if !r.checked { + continue + } + if strings.HasPrefix(r.id, "flatpak:") { + m.selection.Flatpaks = append(m.selection.Flatpaks, strings.TrimPrefix(r.id, "flatpak:")) + } + if strings.HasPrefix(r.id, "service:") { + m.selection.Services = append(m.selection.Services, strings.TrimPrefix(r.id, "service:")) + } + } } } func (m model) makePreview() tea.Cmd { @@ -195,7 +233,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.loading { return m, nil } - if m.stage == 4 { + if m.stage == 5 { switch key { case "up", "k": if m.scroll > 0 { @@ -208,7 +246,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "esc": m.stage-- m.prepare() - case "enter", "s", "a", "i": + case "enter", "s", "a", "i", "f", "v", "b", "r": if m.errorText == "" { m.done = true m.action = key @@ -267,7 +305,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.remember() m.stage++ - if m.stage == 4 { + if m.stage == 5 { m.loading = true m.errorText = "" return m, m.makePreview() @@ -287,13 +325,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m model) View() string { const purple = "\033[38;2;157;143;217m" const reset = "\033[0m" - titles := []string{"Preset", "Package sections", "Optional app configs", "Kernel, bootloader and extras", "Final preview"} + titles := []string{"Preset", "Package sections", "Optional app configs", "Kernel, bootloader and extras", "Flatpaks and optional services", "Final preview"} text := purple + " Fiw-Gentoo-Dots\n" + reset + " " + titles[m.stage] + " · " + m.selection.Name + "\n\n" budget := m.height - 10 if budget < 5 { budget = 5 } - if m.stage == 4 { + if m.stage == 5 { if m.loading { return text + " Preparing preview…\n" } @@ -305,7 +343,7 @@ func (m model) View() string { for _, line := range lines[m.scroll:end] { text += " " + line + "\n" } - text += "\n ↑/↓ scroll · Esc back · Enter save plan · a restore configs · i install packages\n q cancel\n" + text += "\n ↑/↓ scroll · Esc back · Enter save · r full restore · a configs · i packages\n f Flatpaks · v services · b bootloader · q cancel\n" if m.errorText != "" { text += " " + m.errorText + "\n" } @@ -355,6 +393,14 @@ func main() { os.Exit(1) } m := model{repo: repo, catalog: c, selection: c.Stock, height: 28} + savedPath := filepath.Join(repo, "local", "selection.json") + if saved, readErr := os.ReadFile(savedPath); readErr == nil { + var previous selection + if json.Unmarshal(saved, &previous) == nil && (previous.Profile == "stock" || previous.Profile == "fiw-ryzen") { + m.selection = previous + m.stage = 1 + } + } m.prepare() result, err := tea.NewProgram(m, tea.WithAltScreen()).Run() if err != nil { @@ -377,27 +423,75 @@ func main() { } fmt.Println(final.preview) fmt.Println("\nSaved selection:", path) - args := []string{filepath.Join(repo, "lib", "rice.py"), "--selection", path} + run := func(action string, root bool) error { + args := []string{filepath.Join(repo, "lib", "rice.py"), "--selection", path, action} + program := "python3" + if root { + program = "sudo" + args = append([]string{"python3"}, args...) + } + cmd := exec.Command(program, args...) + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + return cmd.Run() + } + services := func() error { + system, user := false, false + for _, id := range final.selection.Services { + if c.Services[id].Scope == "system" { + system = true + } + if c.Services[id].Scope == "user" { + user = true + } + } + if system { + if err := run("--enable-services", true); err != nil { + return err + } + } + if user { + return run("--enable-services", false) + } + if !system { + fmt.Println("No services selected.") + } + return nil + } + var actionErr error switch final.action { case "a": - args = append(args, "--apply-configs") + actionErr = run("--apply-configs", false) case "i": - args = append([]string{"python3"}, args...) - args = append(args, "--install-packages") + actionErr = run("--install-packages", true) + case "f": + actionErr = run("--install-flatpaks", false) + case "v": + actionErr = services() + case "b": + actionErr = run("--deploy-bootloader", true) + case "r": + actionErr = run("--check-configs", false) + if actionErr == nil && (len(final.selection.Groups) > 0 || len(final.selection.Extras) > 0 || final.selection.Bootloader != "keep" || len(final.selection.Flatpaks) > 0 || len(final.selection.Services) > 0) { + actionErr = run("--install-packages", true) + } + if actionErr == nil && len(final.selection.Flatpaks) > 0 { + actionErr = run("--install-flatpaks", false) + } + if actionErr == nil && len(final.selection.Services) > 0 { + actionErr = services() + } + if actionErr == nil && len(final.selection.Configs) > 0 { + actionErr = run("--apply-configs", false) + } + if actionErr == nil && final.selection.Bootloader != "keep" { + actionErr = run("--deploy-bootloader", true) + } default: - fmt.Println("Review the setup notes in docs/setup.md before applying.") + fmt.Println("Saved. Choose an action in the TUI or use the commands in docs/setup.md.") return } - program := "python3" - if final.action == "i" { - program = "sudo" - } - cmd := exec.Command(program, args...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - fmt.Fprintln(os.Stderr, err) + if actionErr != nil { + fmt.Fprintln(os.Stderr, actionErr) os.Exit(1) } } diff --git a/cmd/dots/main_test.go b/cmd/dots/main_test.go index 9f7752c..50baff2 100644 --- a/cmd/dots/main_test.go +++ b/cmd/dots/main_test.go @@ -50,3 +50,19 @@ func TestConfigsSelectableWithoutPackageGroups(t *testing.T) { t.Fatal("configs were hidden or deselected by package choices") } } + +func TestSetupCheckboxesSurviveFinalPreview(t *testing.T) { + m := model{stage: 4, catalog: catalog{ + Flatpaks: map[string]setupOption{"org.vinegarhq.Sober": {Label: "Sober", Group: "gaming"}}, + Services: map[string]setupOption{"audio": {Label: "Audio", Scope: "user"}}, + }, selection: selection{}} + m.prepare() + for i := range m.rows { + m.rows[i].checked = true + } + next, command := m.Update(tea.KeyMsg{Type: tea.KeyEnter}) + result := next.(model) + if result.stage != 5 || command == nil || !contains(result.selection.Flatpaks, "org.vinegarhq.Sober") || !contains(result.selection.Services, "audio") { + t.Fatalf("setup selections lost before preview: %+v", result.selection) + } +} diff --git a/docs/assets.md b/docs/assets.md index fc02366..bd34171 100644 --- a/docs/assets.md +++ b/docs/assets.md @@ -21,3 +21,13 @@ theme metadata and colour-scheme notices. It still scans those files for other private-data patterns. A changed fingerprint requires review before updating tools/public-attribution.json. The fingerprinted upstream kernel patch similarly retains public source/test data rather than being rewritten. + +## Distribution logos + +The user-supplied Gentoo and Arch images are kept in `assets/images`. The +Gentoo image is used in the README; the Arch image is retained as supplied. +These distribution marks are separate from this project's original MIT code. +Upstream references: [Gentoo artwork](https://www.gentoo.org/inside-gentoo/artwork/) +and [name/logo guidelines](https://www.gentoo.org/inside-gentoo/foundation/name-logo-guidelines.html); +[Arch artwork](https://archlinux.org/art/) and +[trademark policy](https://terms.archlinux.org/docs/trademark-policy/). diff --git a/docs/boot.md b/docs/boot.md index 16ee0f1..905aafa 100644 --- a/docs/boot.md +++ b/docs/boot.md @@ -1,20 +1,84 @@ -# Optional bootloader choices +# Optional bootloader deployment -`keep` is the default. `limine` and `grub` select separate package lists. -The draft does not install firmware entries or replace the active bootloader. -Target-specific deployment still needs implementation and a reviewed target -ESP/root layout. Selecting a kernel does not select a bootloader. +`keep` is the default. Limine and GRUB have separate package choices; kernel +selection remains independent. Selecting a loader does not itself deploy it. +Use the final preview's **b** action or include it in **r** full restore. -GRUB integration must use the target system's installkernel configuration -and generate that system's grub.cfg. Limine integration must derive root and -ESP identifiers on the target and produce the selected custom/generic entries. -Fiw's current dual-boot paths and UUIDs do not belong in generic templates. +## Preview and apply -The retained `optional/fiw-limine-splice` refreshes the Gentoo block in an -existing Limine configuration and preserves other entries. It is specific -to the current marker/snippet design and is not installed by the default -rice preset or by the GRUB variant. +```sh +./install --selection local/selection.json --boot-plan +sudo ./install --selection local/selection.json --deploy-bootloader +# Specify a mounted ESP when automatic discovery does not find yours: +sudo ./install --selection local/selection.json --deploy-bootloader --esp /boot/efi +``` -`optional/fiw-cachy-patch` supports the custom kernel, not a bootloader. It -generates a CachyOS patch and updates the version guard/name. It requires -root and matching upstream source releases; review before running. +Discovery checks mounted `/efi`, `/boot/efi` and `/boot` candidates for the GPT +ESP type and FAT filesystem. The plan shows the selected partition, root +filesystem, kernel arguments, installed kernel/initramfs candidates and +proposed loader config or commands. UUIDs are derived on the target and kept +in its local state; they are not shipped in the repo. + +Firmware registration has three choices: + +| Choice | Effect | +|---|---| +| keep | Leave current firmware entries and BootOrder unchanged | +| add | Create an entry without adding it to BootOrder | +| default | Register the selected loader first in BootOrder | + +The final deployment confirmation follows the target and firmware preview. +Existing matching firmware entries are reused when their partition and EFI +path match. Other entries are retained. No reboot is performed. + +## Limine + +The loader goes to `EFI/Fiw-Gentoo/BOOTX64.EFI`, with `limine.conf` beside it. +Versioned kernels and initramfs files are copied below that directory so +Limine can read them from the ESP. Custom Fiw/Cachy kernels appear first, +followed by generic kernels, including an installed binary fallback. + +A marked block contains the generated Gentoo entries. Entries outside that +block are preserved. Locally edited managed entries receive a `.new` proposal +instead of being overwritten. ESP free space is checked before copying kernel +artifacts. The protocol and paths follow the upstream +[Limine configuration reference](https://github.com/limine-bootloader/limine/blob/v12.x/CONFIG.md). + +## GRUB + +`grub-install` uses `--target=x86_64-efi`, a dedicated `Fiw-Gentoo` EFI ID, +`/boot/fiw-gentoo` as its boot directory and `--no-nvram`. Firmware changes are +handled separately by the selected registration choice. + +The generated menu lives in `/boot/fiw-gentoo/grub/grub.cfg`. A temporary output +is generated before replacing it. If the previous menu was edited locally, +the new menu becomes a `.new` proposal. Existing `/etc/default/grub` settings +are used by `grub-mkconfig`. + +## Kernel updates and backups + +Deployment installs a standalone refresh helper at +`/usr/local/libexec/fiw-dots-boot`, a systemd kernel-install hook and a +traditional Gentoo installkernel post-install hook. The active installkernel +configuration uses the compat layout and Dracut. Missing initramfs files are +generated when needed. Each refresh verifies the ESP is still mounted and +updates only the selected loader's managed menu. + +The systemd remove hook excludes the removed kernel from its generated menu. +The legacy post-install hook refreshes after additions. Deployment saves +local boot state under `/etc/fiw-gentoo-dots/boot.json`; backup files and reports +live under `/var/lib/Fiw-Gentoo-Dots`. No existing disk partition is formatted. + +Automatic deployment currently supports amd64 UEFI, GPT ESPs, plain ext4/Btrfs +roots and unsigned boot. Encrypted, UKI-only, BIOS and signing workflows can +keep their existing loader. Secure Boot being active stops unsigned deployment +before changes. Full boot execution on a fresh installation remains unverified. + +## Retained helpers + +`optional/fiw-limine-splice` is the earlier helper for Fiw's marker/snippet +layout. It is retained for manual use and is not installed by this deployment +or by the GRUB variant. The new refresh helper manages its own separate config. + +`optional/fiw-cachy-patch` refreshes the custom kernel patch; it does not select +a bootloader. See the [helper list](scripts.md) before using legacy helpers. diff --git a/docs/scripts.md b/docs/scripts.md new file mode 100644 index 0000000..ac71254 --- /dev/null +++ b/docs/scripts.md @@ -0,0 +1,29 @@ +# Helpers and what they do + +| Helper | Purpose | How it is used | +|---|---|---| +| `install` | Builds/opens the Bubble Tea chooser, or forwards CLI options to the Python backend | Main entry point | +| `lib/rice.py` | Validates selections, previews plans, restores configs and installs Portage packages | Called by the entry point/TUI | +| `lib/setup.py` | Stages missing overlays, installs selected user Flatpaks and enables selected service units | Called by the backend | +| `lib/boot.py` | Detects the target ESP/root, previews/deploys Limine or GRUB, refreshes kernel menus | Boot action; copied as `fiw-dots-boot` on deployment | +| `fiw-shot` | Captures a region or monitor using Spectacle, copies it to the clipboard and offers Save/Edit through a notification | Installed with KDE shortcuts; `region` or `screen` argument | +| `tools/capture.py` | Recaptures the explicit preference allowlist without copying full personal profiles | Maintenance command; review the diff | +| `tools/shortcuts.py` | Regenerates the readable shortcut reference from captured repo files | Run after changing/capturing shortcuts | +| `tools/check-private.py` | Scans publishable files for personal paths, identifiers and credentials; keeps fingerprinted upstream attribution | Run before committing | +| `optional/fiw-cachy-patch` | Downloads matching vanilla/CachyOS kernel sources, generates a patch and adjusts the installed guard/name config | Explicit manual root operation; not part of ordinary restoration | +| `optional/fiw-limine-splice` | Replaces the old Gentoo snippet block in an existing Limine menu and orders its entries | Explicit manual use with an ESP argument; specific to the earlier snippet design | +| `optional/spotify/spotify-patch` | Runs the retained SpotX/Spicetify customization workflow for the Gentoo Spotify client | Explicit optional user operation; stock Spotify is the default | +| `optional/spotify/spicetify-snapshotfix` | Copies missing Marketplace fragments into the captured Spotify UI snapshot format | Called by the optional customization workflow | + +The kernel refresh helper installed on a target runs with `--refresh`. Its +kernel hooks are created only when boot deployment is explicitly confirmed; +they are not enabled by the default `keep` choice. + +The older Cachy patch helper predates the snapshot guard shipped by this +repo. It does not update the repo's `snapshot.json`, checksum or version mask; +review/update those together when preparing a new supported kernel snapshot. +It is retained as a reference, not run automatically. + +For the optional Spotify scripts, see [the setup and risk note](spotify.md). +Unofficial Spotify modifications are optional and used at your own risk. +This project does not endorse bypassing paid features. diff --git a/docs/setup.md b/docs/setup.md index 8a8c7dc..e69dbba 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -1,83 +1,131 @@ -# Setup and current draft boundaries +# Restore the setup Start with an installed amd64 Gentoo system using systemd and a multilib -Plasma profile. This repo does not partition disks or install the stage3. -Plasma Login Manager currently requires systemd. It is the default greeter -when installing the KDE section. +Plasma profile, with Git and Python 3.11+ available. Go 1.24+ builds the TUI. +This repo does not partition disks or install a stage3. -1. Run `./install`, make selections and review the final preview. -2. Save the selection. It lives in the ignored `local/selection.json`. -3. Export for inspection if desired: - `./install --selection local/selection.json --export local/stage`. -4. Install packages via the TUI or - `sudo ./install --selection local/selection.json --install-packages`. - Resolution happens in a temporary Portage config before live files change. - Missing binaries prompt compile/skip; final installation requires confirmation. -5. Apply user configs as the target user: - `./install --selection local/selection.json --apply-configs`. - Log out of Plasma and use a TTY for KDE configs; running KDE can overwrite - edited config files. Portable app configs can be restored separately. +## Terminal interface -For updates, review the Git changes, then use the saved selection with -`--apply-configs --update`. There is no automatic git pull or upstream upgrade. -Backups and reports live under `~/.local/state/Fiw-Gentoo-Dots`. Package reports -and system backups live under `/var/lib/Fiw-Gentoo-Dots`. +Run `./install`. Choose a preset, package sections, individual configs, +kernel/bootloader options, Flatpaks and optional services. Previous selections +are loaded from the ignored `local/selection.json`; press Esc to return to +preset selection when you want to change profiles. -## Explicit setup steps +At the final preview: -Root package installation enables `plasmalogin.service` for selected KDE -installs without restarting the running greeter. After the desired packages -are installed, enable the services you use: +| Key | Action | +|---|---| +| Enter / s | Save the selection without applying it | +| r | Full restore: packages, Flatpaks, services, configs, selected bootloader | +| i | Install Portage packages | +| f | Install selected user Flatpaks | +| v | Enable selected services | +| a | Restore selected user configs | +| b | Preview and deploy the selected bootloader | + +Full restore checks the KDE session before making package changes. Log out of +Plasma and run from a TTY when KDE configs are selected. Each operation also +has a command for resuming a saved selection: ```sh -sudo systemctl enable NetworkManager bluetooth power-profiles-daemon -systemctl --user enable pipewire.socket pipewire-pulse.socket wireplumber.service +./install --selection local/selection.json --plan +sudo ./install --selection local/selection.json --install-packages +./install --selection local/selection.json --install-flatpaks +sudo ./install --selection local/selection.json --enable-services +./install --selection local/selection.json --enable-services +./install --selection local/selection.json --apply-configs +sudo ./install --selection local/selection.json --deploy-bootloader ``` -Docker stays disabled until needed. The installer does not reboot or power off. +The root service command handles system services; the regular-user command +handles PipeWire units. User Flatpaks and user configs are never applied as root. -Flatpak apps are listed separately. Add Flathub if absent, then install only -the selected categories' apps explicitly, for example: +## Packages and repositories + +Existing GURU and steam-overlay checkouts are reused. Missing overlays needed +by the selected package sections are cloned into temporary storage before +Portage resolves the plan. Their identity is checked, and they are adopted +under `/var/db/repos` only after confirmation. Existing repository checkouts +are not automatically synced or replaced. + +Additional source builds prompt **compile or skip**, with compilation +recommended. Skipping a dependency also skips requested packages needing it. +A package report records requested, skipped and missing packages. There is a +final package confirmation before live Portage configuration changes. + +Read-only resolution and an export are available separately: ```sh -flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo -flatpak install --user flathub org.localsend.localsend_app -flatpak install --user flathub org.vinegarhq.Sober +./install --selection local/selection.json --check-packages +./install --selection local/selection.json --export local/stage ``` -The independently selectable `fonts` config includes the four used styles of -JetBrainsMono Nerd Font Mono from the official Nerd Fonts v3.5.1 release, -with its OFL licence. It works with KDE and portable apps alike. The entire -personal font collection is not included. Neovim uses its built-in theme as -a fallback when Catppuccin is not installed. Vesktop's selected Midnight -theme CSS is captured with a fixed upstream snapshot and its MIT licence. +The check may download a missing overlay into its temporary root; it does not +register that checkout or change the live Portage configuration. The export +contains proposed files and a plan, not a ready-to-merge repository checkout. + +Plasma Login Manager is enabled for selected KDE installs. The installer does +not restart the running greeter, reboot or power off. + +## Flatpaks and services + +LocalSend (`fiw-apps`) and Sober (`gaming`) have individual Flatpak checkboxes. +Both start unchecked. Selecting them ensures Flatpak is in the package plan; +the separate Flatpak action adds Flathub for the user if absent and installs +only missing selected apps. Existing user installations are kept. Failures +and skips are reported. + +Audio, NetworkManager, Bluetooth and power profiles have independent service +checkboxes, also initially unchecked. Their required packages are added when +needed. Enabling a service schedules it for the next boot or user login; the +workflow does not start, stop or restart services. Missing units are reported. +Docker remains a separate choice outside this service preset. + +## Configs and updates + +Every config is optional and independent of package installation groups. +The separate fonts config includes four JetBrainsMono Nerd Font Mono styles. +Vesktop's Midnight CSS is included with a fixed upstream snapshot. See the +[KDE styling reference](kde-style.md) and [shortcut list](shortcuts.md). + +Review Git changes before pulling updates, then restore your saved selection: + +```sh +./install --selection local/selection.json --apply-configs --update +``` + +Locally edited configs remain intact; proposed changes are written to `.new` +files. Replacements receive backups. Root Portage file conflicts ask before +replacement and receive backups too. Reports and user backups live under +`~/.local/state/Fiw-Gentoo-Dots`; system reports and backups live under +`/var/lib/Fiw-Gentoo-Dots`. ## Fiw tools -The `fiw-tools` section includes FiwNode's x86-64 binary release and the -released Apdatifier Gentoo widget, alongside OpenDeck and Music Presence. -Apdatifier installs QML/scripts without compilation; any dependencies needing -source builds still go through the normal compile/skip prompt. +The `fiw-tools` section includes FiwNode's binary release and the released +Apdatifier Gentoo widget, alongside OpenDeck and Music Presence. Apdatifier +installs QML/scripts without compilation; source-only dependencies still +require the normal compile/skip choice. -FiwNode uses its default configuration. The installer does not copy sound -libraries, microphone/output selections or personal app state. Older copies -in `~/.local/bin` can shadow the Portage-installed binaries in `/usr/bin`; -remove those manually when ready to switch. The daemon starts on demand. +FiwNode uses its default config. Sound libraries and audio device selections +stay local. Copies in `~/.local/bin` can shadow `/usr/bin`; manage old copies +manually when switching to the package. Its daemon starts on demand. -Apdatifier is available through Plasma's **Add Widgets** menu. Installing it -does not alter panels. An existing user-installed widget can override the -system copy; use `kpackagetool6` to manage it. Its preferences remain local. +Add Apdatifier through Plasma's **Add Widgets** menu. Installation does not +alter panels. An existing user-installed widget can override the system copy; +manage it with `kpackagetool6`. Its preferences stay local. -## Remaining work before declaring a fresh install verified +## Current scope -- Run both presets on a fresh Gentoo test installation. Root package execution - has not been exercised on the working desktop. -- Implement target-specific Limine/GRUB deployment after reviewing the target - ESP and boot layout. Current variants select packages and provide setup notes. -- Refresh the optional TideWM live ebuild branch/dependency recipe against the - upstream development version before building it on a new machine. -- Repository sync is an explicit prerequisite when GURU or steam-overlay is - absent; the draft aborts cleanly on resolution failure rather than guessing. +Automatic boot deployment supports amd64 UEFI, a mounted GPT ESP, plain ext4 +or Btrfs roots, and unsigned boot. See [boot setup](boot.md) for previews, +firmware choices and kernel update integration. Other layouts can keep their +existing loader. -Do not publish the private original fiw-gentoo repository. It is the working -archive and has different history and host-specific files. +The optional TideWM live recipe still needs a refresh against its upstream +development branch. End-to-end package and boot execution on a fresh Gentoo +installation remains unverified; development checks used temporary homes, +staged Portage configuration and mocked system commands. + +Do not publish the private original fiw-gentoo archive. It has different +history and host-specific files. diff --git a/lib/boot.py b/lib/boot.py new file mode 100644 index 0000000..b5ee5b8 --- /dev/null +++ b/lib/boot.py @@ -0,0 +1,431 @@ +#!/usr/bin/env python3 +"""Preview and deploy a separate amd64 UEFI Limine or GRUB installation.""" +import argparse +import hashlib +import json +import os +import platform +import re +import shlex +import shutil +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path + +STATE = Path('/etc/fiw-gentoo-dots/boot.json') +MANAGED = Path('/var/lib/Fiw-Gentoo-Dots') +BEGIN = '# >>> Fiw-Gentoo-Dots kernels' +END = '# <<< Fiw-Gentoo-Dots kernels' +EFI_DIRECTORY = 'EFI/Fiw-Gentoo' +GRUB_DIRECTORY = Path('/boot/fiw-gentoo') + + +def mount_info(path, exact=False): + result = subprocess.run(['findmnt', '--json', '--mountpoint' if exact else '--target', + str(path), '--output', 'TARGET,SOURCE,FSTYPE,UUID,OPTIONS'], + text=True, capture_output=True) + if result.returncode: + raise RuntimeError('Not a mounted filesystem: ' + str(path)) + data = json.loads(result.stdout)['filesystems'][0] + return data + + +def discover_esp(path=None): + candidates = [Path(path)] if path else [Path('/efi'), Path('/boot/efi'), Path('/boot')] + for candidate in candidates: + try: + info = mount_info(candidate, exact=True) + except RuntimeError: + continue + if info['fstype'] not in ('vfat', 'fat', 'fat32'): + continue + source = re.sub(r'\[.*\]$', '', info['source']) + device = Path(source).resolve() + partition = Path('/sys/class/block') / device.name / 'partition' + if not partition.is_file(): + continue + # Require the GPT ESP type, not an arbitrary mounted FAT data partition. + result = subprocess.run(['lsblk', '--noheadings', '--nodeps', '--output', 'PARTTYPE', str(device)], + text=True, capture_output=True) + esp_type = 'c12a7328f81f11d2ba4b00a0c93ec93b' # GPT ESP type, without separators + if result.returncode or result.stdout.strip().lower().replace('-', '') != esp_type: + continue + parent = (Path('/sys/class/block') / device.name).resolve().parent.name + identifier = subprocess.run(['lsblk', '--noheadings', '--nodeps', '--output', 'PARTUUID', str(device)], text=True, capture_output=True) + return {'partuuid': identifier.stdout.strip().lower(), 'path': str(candidate.resolve()), 'device': str(device), + 'disk': '/dev/' + parent, 'partition': int(partition.read_text())} + raise RuntimeError('Select a mounted GPT EFI System Partition with --esp. No ESP was found.') + + +def kernel_images(boot=Path('/boot'), exclude=None): + found = {} + for pattern, prefix in [('vmlinuz-*', 'vmlinuz-'), ('kernel-*', 'kernel-')]: + for path in boot.glob(pattern): + version = path.name[len(prefix):] + if path.is_file() and re.fullmatch(r'[A-Za-z0-9_.+-]+', version) and version != exclude: + initrds = [boot / ('initramfs-' + version + '.img'), boot / ('initrd-' + version), + boot / ('initrd.img-' + version)] + found.setdefault(version, {'version': version, 'kernel': str(path), + 'initrd': next((str(p) for p in initrds if p.is_file()), None)}) + # systemd BLS installations store a linux/initrd pair below their entry ID. + for path in boot.glob('*/*/linux'): + version = path.parent.name + if path.is_file() and re.fullmatch(r'[A-Za-z0-9_.+-]+', version) and version != exclude: + initrd = path.parent / 'initrd' + found.setdefault(version, {'version': version, 'kernel': str(path), + 'initrd': str(initrd) if initrd.is_file() else None}) + for path in Path('/usr/lib/modules').glob('*/vmlinuz'): + version = path.parent.name + if boot == Path('/boot') and path.is_file() and version != exclude and re.fullmatch(r'[A-Za-z0-9_.+-]+', version): + initrd = boot / ('initramfs-' + version + '.img') + found.setdefault(version, {'version': version, 'kernel': str(path), + 'initrd': str(initrd) if initrd.is_file() else None}) + versions = subprocess.run(['sort', '-V'], input='\n'.join(found), text=True, + capture_output=True, check=True).stdout.splitlines()[::-1] + versions.sort(key=lambda v: not any(word in v.lower() for word in ('fiw', 'cachy'))) + return [found[v] for v in versions] + + +def kernel_cmdline(root): + config = Path('/etc/kernel/cmdline') + raw = config.read_text().strip() if config.is_file() else Path('/proc/cmdline').read_text().strip() + tokens = shlex.split(raw) + tokens = [token for token in tokens if not token.startswith(('BOOT_IMAGE=', 'initrd=', 'root='))] + if not root.get('uuid'): + raise RuntimeError('Cannot identify the target root filesystem UUID.') + tokens.insert(0, 'root=UUID=' + root['uuid']) + if root['fstype'] == 'btrfs' and not any(t.startswith('rootflags=') for t in tokens): + flags = [flag for flag in root.get('options', '').split(',') if flag.startswith('subvol=')] + if flags: + tokens.append('rootflags=' + flags[0]) + text = ' '.join(tokens) + if any(char in text for char in ('\n', '\r', '\0')): + raise RuntimeError('Invalid kernel command line.') + return text + + +def secure_boot(): + files = list(Path('/sys/firmware/efi/efivars').glob('SecureBoot-*')) + return bool(files and len(files[0].read_bytes()) > 4 and files[0].read_bytes()[4] == 1) + + +def limine_loader(): + for path in [Path('/usr/share/limine/BOOTX64.EFI'), Path('/usr/share/limine/limine-uefi-cd.bin')]: + # A CD image is not an EFI executable; only actual PE EFI files qualify. + if path.is_file() and path.open('rb').read(2) == b'MZ': + return path + for path in Path('/usr/share/limine').rglob('BOOTX64.EFI'): + if path.is_file(): + return path + raise RuntimeError('Limine BOOTX64.EFI is missing; install sys-boot/limine with uefi-x86-64.') + + +def render_limine(images, cmdline): + lines = [BEGIN] + for image in images: + version = image['version'] + label = 'custom' if any(word in version.lower() for word in ('fiw', 'cachy')) else 'generic' + lines += [f'/Gentoo {version} ({label})', ' protocol: linux', + f' path: boot():/{EFI_DIRECTORY}/kernels/{version}/vmlinuz', + f' module_path: boot():/{EFI_DIRECTORY}/kernels/{version}/initramfs.img', + ' cmdline: ' + cmdline, ''] + return '\n'.join(lines + [END]) + '\n' + + +def managed_block(text): + if text.count(BEGIN) != text.count(END) or text.count(BEGIN) > 1: + raise RuntimeError('Malformed managed Limine block; preserving the config.') + if BEGIN not in text: + return None + start = text.index(BEGIN) + end = text.index(END, start) + len(END) + return text[start:end] + '\n' + + +def merge_limine(text, block): + old = managed_block(text) + if old is None: + return text.rstrip() + '\n\n' + block if text.strip() else 'timeout: 5\n\n' + block + return text.replace(old.rstrip('\n'), block.rstrip('\n'), 1) + + +def file_hash(path): + return hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else None + + +def block_hash(block): + return hashlib.sha256(block.encode()).hexdigest() if block else None + + +def install_config(old): + values = {'layout': 'compat', 'initrd_generator': 'dracut', 'uki_generator': 'none'} + lines, seen = [], set() + for line in old.splitlines(): + key = line.split('=', 1)[0].strip() + if key in values: + if key not in seen: + lines.append(key + '=' + values[key]) + seen.add(key) + else: + lines.append(line) + lines += [key + '=' + value for key, value in values.items() if key not in seen] + return '\n'.join(lines) + '\n' + + +def plan(mode, esp=None, exclude=None): + if mode == 'keep': + return {'mode': 'keep', 'changes': []} + if mode not in ('limine', 'grub'): + raise ValueError('Unknown bootloader') + if platform.machine() not in ('x86_64', 'amd64') or not Path('/sys/firmware/efi').is_dir(): + raise RuntimeError('Automatic boot deployment supports amd64 UEFI installations.') + root = mount_info('/') + if root['fstype'] not in ('ext4', 'btrfs') or root['source'].startswith('/dev/mapper/'): + raise RuntimeError('Automatic boot deployment currently supports plain ext4/Btrfs roots. Keep the existing loader for encrypted or other layouts.') + source = re.sub(r'\[.*\]$', '', root['source']) + device_type = subprocess.run(['lsblk', '--noheadings', '--nodeps', '--output', 'TYPE', source], text=True, capture_output=True) + if device_type.returncode or device_type.stdout.strip() not in ('part', 'disk'): + raise RuntimeError('Automatic boot deployment needs a plain disk/partition root; keep the existing loader for this storage layout.') + target = discover_esp(esp) + images = kernel_images(exclude=exclude) + if not images: + raise RuntimeError('No installed kernel/initramfs candidates found under /boot. Install a distribution kernel first.') + text = kernel_cmdline(root) + result = {'mode': mode, 'esp': target, 'root_filesystem': root['fstype'], + 'cmdline': text, 'images': images, 'secure_boot': secure_boot(), + 'firmware': 'preserve current entries/order unless explicitly selected', + 'kernel_refresh_hook': '/etc/kernel/install.d/96-fiw-dots-boot.install'} + if mode == 'limine': + result['loader'] = str(limine_loader()) + result['config'] = str(Path(target['path']) / EFI_DIRECTORY / 'limine.conf') + result['proposed_entries'] = render_limine(images, text) + else: + result['config'] = str(GRUB_DIRECTORY / 'grub/grub.cfg') + result['commands'] = [['grub-install', '--target=x86_64-efi', '--efi-directory=' + target['path'], + '--boot-directory=' + str(GRUB_DIRECTORY), '--bootloader-id=Fiw-Gentoo', '--no-nvram'], + ['grub-mkconfig', '-o', result['config']]] + prior = Path('/etc/kernel/install.conf').read_text() if Path('/etc/kernel/install.conf').is_file() else '' + result['installkernel_config'] = install_config(prior) + for image in images: + if not image['initrd']: + image['generate_initrd'] = '/boot/initramfs-' + image['version'] + '.img' + return result + + +def backup(path, root): + if path.exists() or path.is_symlink(): + target = root / str(path.absolute()).lstrip('/') + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, target, follow_symlinks=False) + + +def write(path, content, backups, mode=0o644): + backup(path, backups) + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_name('.' + path.name + '.fiw-dots-tmp') + tmp.write_text(content) + tmp.chmod(mode) + tmp.replace(path) + + +def refresh(saved, backups, exclude=None): + # Recheck the mount every time; never write into an unmounted ESP directory. + target = discover_esp(Path(saved['esp'])) + images = kernel_images(exclude=exclude) + if not images: + raise RuntimeError('No remaining kernels; preserving the boot menu.') + for image in images: + if not image['initrd']: + if not shutil.which('dracut'): + raise RuntimeError('Missing initramfs for ' + image['version'] + '; install sys-kernel/dracut.') + path = Path('/boot/initramfs-' + image['version'] + '.img') + subprocess.run(['dracut', '--force', str(path), image['version']], check=True) + image['initrd'] = str(path) + if saved['mode'] == 'grub': + config = GRUB_DIRECTORY / 'grub/grub.cfg' + old_hash = file_hash(config) + output = config + if old_hash and old_hash != saved.get('config_hash'): + output = config.with_name(config.name + '.new.' + datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S.%fZ')) + else: + backup(config, backups) + output.parent.mkdir(parents=True, exist_ok=True) + for image in images: + source = Path(image['kernel']) + if source.parent != Path('/boot'): + dest = Path('/boot') / ('kernel-' + image['version']) + if file_hash(dest) != file_hash(source): + backup(dest, backups) + shutil.copy2(source, dest) + source = Path(image['initrd']) + dest = Path('/boot') / ('initramfs-' + image['version'] + '.img') + if source != dest and not dest.exists(): + shutil.copy2(source, dest) + scratch = output.with_name('.' + output.name + '.tmp') + try: + subprocess.run(['grub-mkconfig', '-o', str(scratch)], check=True) + scratch.replace(output) + finally: + if scratch.exists(): + scratch.unlink() + if output != config: + print('Preserved edited GRUB config; review ' + str(output)) + else: + saved['config_hash'] = file_hash(config) + return saved + folder = Path(target['path']) / EFI_DIRECTORY + required = sum(Path(source).stat().st_size for image in images + for source, name in [(image['kernel'], 'vmlinuz'), (image['initrd'], 'initramfs.img')] + if file_hash(folder / 'kernels' / image['version'] / name) != file_hash(Path(source))) + if shutil.disk_usage(target['path']).free < required + 16 * 1024 * 1024: + raise RuntimeError('Not enough free space on the ESP for the selected kernels and initramfs files.') + for image in images: + directory = folder / 'kernels' / image['version'] + directory.mkdir(parents=True, exist_ok=True) + for source, name in [(image['kernel'], 'vmlinuz'), (image['initrd'], 'initramfs.img')]: + dest = directory / name + if file_hash(dest) != file_hash(Path(source)): + backup(dest, backups) + scratch = dest.with_name('.' + name + '.tmp') + shutil.copy2(source, scratch) + scratch.replace(dest) + config = folder / 'limine.conf' + old = config.read_text() if config.is_file() else '' + prior = managed_block(old) + block = render_limine(images, saved['cmdline']) + output = merge_limine(old, block) + if prior and block_hash(prior) != saved.get('block_hash'): + proposal = config.with_name(config.name + '.new.' + datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S.%fZ')) + write(proposal, output, backups) + print('Preserved edited Limine entries; review ' + str(proposal)) + elif output != old: + write(config, output, backups) + saved['block_hash'] = block_hash(block) + return saved + + +def confirm(prompt, choices, default): + if not sys.stdin.isatty(): + raise RuntimeError('Interactive confirmation required: ' + prompt) + while True: + value = input(prompt + ' [' + '/'.join(choices) + '] (default ' + default + '): ').strip().lower() or default + if value in choices: + return value + + +def firmware_commands(target, mode, loader_path, choice, listing): + if choice == 'keep': + return [] + label = 'Fiw-Gentoo-' + mode + matches = [] + for line in listing.splitlines(): + match = re.match(r'^Boot([0-9A-Fa-f]{4})\*?\s+' + re.escape(label) + r'\s', line) + if match and target.get('partuuid') and target['partuuid'] in line.lower() and loader_path.lower().replace('/', '\\') in line.lower(): + matches.append(match.group(1)) + if matches: + if choice == 'add': + return [] + order = re.search(r'^BootOrder:\s*(.*)$', listing, re.M) + sequence = [matches[0]] + [item for item in (order.group(1).split(',') if order else []) + if item.upper() != matches[0].upper()] + return [['efibootmgr', '--bootorder', ','.join(sequence)]] + return [['efibootmgr', '--create' if choice == 'default' else '--create-only', + '--disk', target['disk'], '--part', str(target['partition']), + '--label', label, '--loader', loader_path.replace('/', '\\')]] + + +def deploy(mode, esp=None, ask=confirm): + if mode == 'keep': + print('Keeping the current bootloader.') + return + if os.geteuid() != 0: + raise RuntimeError('Boot deployment needs root. Run sudo ./install --deploy-bootloader --selection .') + proposed = plan(mode, esp) + if proposed['secure_boot']: + raise RuntimeError('Secure Boot is active. This unsigned deployment requires a separately configured signing setup.') + for command in (['grub-install', 'grub-mkconfig'] if mode == 'grub' else []): + if not shutil.which(command): + raise RuntimeError('Missing command: ' + command) + if any(not image['initrd'] for image in proposed['images']) and not shutil.which('dracut'): + raise RuntimeError('Missing initramfs generator: dracut') + print(json.dumps(proposed, indent=2)) + firmware = ask('UEFI entry: preserve current order, add a menu entry, or make this loader default?', + ['keep', 'add', 'default'], 'keep') + if firmware != 'keep' and not shutil.which('efibootmgr'): + raise RuntimeError('Install sys-boot/efibootmgr before registering a firmware entry.') + loader_path = '/EFI/Fiw-Gentoo/' + ('BOOTX64.EFI' if mode == 'limine' else 'grubx64.efi') + listing = subprocess.run(['efibootmgr', '--verbose'], text=True, capture_output=True, check=True).stdout if firmware != 'keep' else '' + commands = firmware_commands(proposed['esp'], mode, loader_path, firmware, listing) + if commands: + print('Proposed firmware commands: ' + json.dumps(commands)) + if ask('Deploy the previewed bootloader and kernel refresh hook?', ['deploy', 'cancel'], 'cancel') != 'deploy': + return + stamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S.%fZ') + backups = MANAGED / 'backups' / ('boot-' + stamp) + folder = Path(proposed['esp']['path']) / EFI_DIRECTORY + if folder.exists(): + shutil.copytree(folder, backups / 'efi-directory', symlinks=True) + if GRUB_DIRECTORY.exists() and mode == 'grub': + shutil.copytree(GRUB_DIRECTORY, backups / 'grub-directory', symlinks=True) + prior = json.loads(STATE.read_text()) if STATE.is_file() else {} + saved = {'mode': mode, 'esp': proposed['esp']['path'], 'cmdline': proposed['cmdline']} + if prior.get('mode') == mode and prior.get('esp') == saved['esp']: + saved.update({key: prior[key] for key in ('config_hash', 'block_hash') if key in prior}) + folder.mkdir(parents=True, exist_ok=True) + if mode == 'limine': + loader = folder / 'BOOTX64.EFI' + backup(loader, backups) + shutil.copy2(proposed['loader'], loader) + loader_path = '/EFI/Fiw-Gentoo/BOOTX64.EFI' + else: + subprocess.run(proposed['commands'][0], check=True) + loader_path = '/EFI/Fiw-Gentoo/grubx64.efi' + saved = refresh(saved, backups) + # Native installkernel provides versioned /boot kernel/initrd files; our + # final hook refreshes only the chosen loader's own configuration. + write(Path('/etc/kernel/install.conf'), proposed['installkernel_config'], backups) + script = Path('/usr/local/libexec/fiw-dots-boot') + backup(script, backups) + script.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(Path(__file__), script) + script.chmod(0o755) + hook = '#!/bin/sh\ncase "$1" in\n add) exec /usr/local/libexec/fiw-dots-boot --refresh ;;\n remove) exec /usr/local/libexec/fiw-dots-boot --refresh --exclude-version "$2" ;;\nesac\n' + write(Path('/etc/kernel/install.d/96-fiw-dots-boot.install'), hook, backups, 0o755) + # Traditional Gentoo installkernel uses postinst.d rather than install.d. + legacy = '#!/bin/sh\nexec /usr/local/libexec/fiw-dots-boot --refresh\n' + write(Path('/etc/kernel/postinst.d/96-fiw-dots-boot'), legacy, backups, 0o755) + write(STATE, json.dumps(saved, indent=2) + '\n', backups, 0o600) + for command in commands: + subprocess.run(command, check=True) + report = MANAGED / ('boot-' + stamp + '.json') + report.parent.mkdir(parents=True, exist_ok=True) + report.write_text(json.dumps({'plan': proposed, 'firmware_choice': firmware, + 'backups': str(backups)}, indent=2) + '\n') + print('Boot deployment finished. Report: ' + str(report)) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--refresh', action='store_true') + parser.add_argument('--exclude-version') + args = parser.parse_args() + if not args.refresh: + parser.error('Use ./install --boot-plan or --deploy-bootloader for initial setup.') + if os.geteuid() != 0: + raise RuntimeError('Kernel refresh requires root.') + if not STATE.is_file(): + return + saved = json.loads(STATE.read_text()) + stamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S.%fZ') + backups = MANAGED / 'backups' / ('boot-refresh-' + stamp) + updated = refresh(saved, backups, args.exclude_version) + write(STATE, json.dumps(updated, indent=2) + '\n', backups, 0o600) + + +if __name__ == '__main__': + try: + main() + except (ValueError, OSError, RuntimeError, subprocess.CalledProcessError) as error: + print('Boot refresh: ' + str(error), file=sys.stderr) + sys.exit(1) diff --git a/lib/rice.py b/lib/rice.py index d278adf..9075202 100644 --- a/lib/rice.py +++ b/lib/rice.py @@ -14,7 +14,11 @@ from pathlib import Path REPO = Path(__file__).resolve().parents[1] sys.path.insert(0, str(REPO / 'tools')) +sys.path.insert(0, str(REPO / 'lib')) from capture import sections +from setup import (FLATPAKS, SERVICES, required_repositories, + existing_repository, repository_config, stage_repositories, + publish_repositories, install_flatpaks, enable_services) GROUPS = ['system', 'kde', 'fiw-apps', 'cli', 'dev', 'gaming', 'fiw-tools', 'tidewm'] EXTRAS = {'filelight': 'Filelight', 'btrfs': 'Btrfs tools', 'nvidia': 'NVIDIA drivers'} @@ -29,8 +33,14 @@ def load_selection(path=None, profile='stock'): selection = json.loads((Path(path) if path else REPO / 'presets' / (profile + '.json')).read_text()) if selection.get('profile') not in ('stock', 'fiw-ryzen'): raise ValueError('Unknown preset') - for field, allowed in [('groups', GROUPS), ('configs', CATALOG), ('extras', EXTRAS)]: - if any(x not in allowed for x in selection.get(field, [])): + for field in ('flatpaks', 'services'): + if selection.get(field) is None: + selection[field] = [] + for field, allowed in [('groups', GROUPS), ('configs', CATALOG), ('extras', EXTRAS), + ('flatpaks', FLATPAKS), ('services', SERVICES)]: + if not isinstance(selection.get(field, []), list): + raise ValueError('Selection must be a list: ' + field) + if any(not isinstance(x, str) or x not in allowed for x in selection.get(field, [])): raise ValueError('Unknown selection in ' + field) if selection.get('kernel') not in ('binary', 'custom'): raise ValueError('Unknown kernel choice') @@ -54,6 +64,17 @@ def package_map(selection): result[extra] = atoms(REPO / 'packages/optional' / (extra + '.list')) if selection['bootloader'] != 'keep': result['boot-' + selection['bootloader']] = atoms(REPO / 'packages/optional' / (selection['bootloader'] + '.list')) + support = [] + if selection.get('flatpaks'): + support.append('sys-apps/flatpak') + for key in selection.get('services', []): + support += SERVICES[key]['packages'] + if selection['bootloader'] != 'keep': + support += ['sys-kernel/installkernel', 'sys-kernel/dracut', 'sys-boot/efibootmgr'] + selected = {atom.split('::')[0] for packages in result.values() for atom in packages} + support = [atom for atom in dict.fromkeys(support) if atom not in selected] + if support: + result['setup'] = support return result @@ -153,11 +174,16 @@ def active_plasma(home): return result.returncode == 0 +def check_configs(selection, home): + entries = list(config_entries(selection)) + if any(CATALOG[name].get('kde') for name, _, _, _ in entries) and active_plasma(home.resolve()): + raise RuntimeError('Log out of Plasma and apply from a TTY to prevent KDE overwriting restored settings. Portable app configs can be applied separately.') + + def apply_configs(selection, home, update=False, conflict='ask'): home = home.resolve() entries = list(config_entries(selection)) - if any(CATALOG[name].get('kde') for name, _, _, _ in entries) and active_plasma(home): - raise RuntimeError('Log out of Plasma and apply from a TTY to prevent KDE overwriting restored settings. Portable app configs can be applied separately.') + check_configs(selection, home) state_dir = home / '.local/state/Fiw-Gentoo-Dots' index_path = state_dir / 'managed.json' index = json.loads(index_path.read_text()) if index_path.is_file() else {} @@ -255,8 +281,9 @@ def root_files(selection, repo_location=None): result['etc/portage/fiw-dots.conf'] += '\n# Persist the selected binary/source policy for later emerge operations.\n' result['etc/portage/fiw-dots.conf'] += 'EMERGE_DEFAULT_OPTS="${EMERGE_DEFAULT_OPTS} --getbinpkg ' + ' '.join('--usepkg-exclude=' + atom for atom in source_policy(selection)) + '"\n' result['etc/portage/repos.conf/fiw-dots.conf'] = '[fiw-dots]\nlocation = ' + str(repo_location or REPO / 'overlay') + '\npriority = 40\nauto-sync = no\n' - for name, uri in [('guru', 'https://github.com/gentoo-mirror/guru.git'), ('steam-overlay', 'https://github.com/anyc/steam-overlay.git')]: - result['etc/portage/repos.conf/fiw-dots-' + name + '.conf'] = '[' + name + ']\nlocation = /var/db/repos/' + name + '\nsync-type = git\nsync-uri = ' + uri + '\nsync-depth = 1\npriority = 10\n' + for name in required_repositories(selection): + if not existing_repository(name): + result['etc/portage/repos.conf/fiw-dots-' + name + '.conf'] = repository_config(name, '/var/db/repos/' + name) arch = 'x86-64-v3' if selection['profile'] == 'fiw-ryzen' else 'x86-64' result['etc/portage/binrepos.conf/fiw-dots.conf'] = '[gentoo]\npriority = 1\nsync-uri = https://distfiles.gentoo.org/releases/amd64/binpackages/23.0/' + arch + '/\nlocation = /var/cache/binhost/gentoo\nverify-signature = true\n' if selection['kernel'] == 'custom' and 'system' in selection['groups']: @@ -265,6 +292,14 @@ def root_files(selection, repo_location=None): result['etc/portage/package.accept_keywords/fiw-dots-kernel'] = 'sys-kernel/gentoo-kernel ~amd64\nsys-kernel/gentoo-kernel-bin ~amd64\nvirtual/dist-kernel ~amd64\n' result['etc/portage/env/sys-kernel/gentoo-kernel'] = (REPO / 'optional/kernel/guard').read_text() result['etc/portage/package.mask/fiw-dots-custom-kernel'] = '# Tested custom-kernel snapshot; update snapshot and patch explicitly.\n>sys-kernel/gentoo-kernel-7.2.8\n' + if selection['bootloader'] != 'keep': + result['etc/portage/package.use/fiw-dots-boot'] = ('sys-kernel/installkernel systemd dracut -grub -uki -ukify -ugrd -efistub -refind -systemd-boot\n') + if selection['bootloader'] == 'grub': + result['etc/portage/env/fiw-dots-grub.conf'] = 'GRUB_PLATFORMS="efi-64"\n' + result['etc/portage/package.env/fiw-dots-grub'] = 'sys-boot/grub fiw-dots-grub.conf\n' + else: + result['etc/portage/package.accept_keywords/fiw-dots-limine'] = 'sys-boot/limine ~amd64\n' + result['etc/portage/package.use/fiw-dots-limine'] = 'sys-boot/limine uefi-x86-64 -bios -bios-cd -bios-pxe -uefi-cd -uefi-ia32 -uefi-aarch64 -uefi-riscv64 -uefi-loongarch64\n' if 'tidewm' in selection['groups']: result['etc/portage/package.accept_keywords/fiw-dots-tidewm'] = 'gui-wm/tidewm::fiw-dots **\n' return result @@ -275,7 +310,11 @@ def preview(selection): lines = ["Fiw-Gentoo-Dots — " + selection['name'], ''] for group, values in packages.items(): lines += [group + ' (' + str(len(values)) + ')', ' ' + ', '.join(values)] - lines += ['', 'Compile deliberately: ' + ', '.join(source_policy(selection)), + lines += ['', 'Repositories: ' + (', '.join(required_repositories(selection)) or 'Gentoo + fiw-dots only'), + 'Missing selected overlays are staged before resolution and adopted after confirmation.', + 'Flatpaks (user): ' + (', '.join(selection.get('flatpaks', [])) or 'none'), + 'Optional services: ' + (', '.join(selection.get('services', [])) or 'none'), + 'Compile deliberately: ' + ', '.join(source_policy(selection)), 'Other packages: prefer binaries; ask compile/skip for additional source builds.', 'Kernel: ' + selection['kernel'] + (' + binary fallback' if selection['kernel'] == 'custom' else ''), 'Bootloader: ' + selection['bootloader'], '', 'Selected configs:'] @@ -286,7 +325,7 @@ def preview(selection): 'Existing config conflicts: ask; updates preserve edits as .new.'] if selection['bootloader'] != 'keep': - lines.append('Bootloader package selected; ESP/firmware deployment is a separate explicit setup step (docs/boot.md).') + lines.append('Boot deployment: target ESP and boot files are previewed before applying; firmware changes are separately selectable (docs/boot.md).') if selection.get('firefox_privacy'): lines.append('Optional manual setup: Firefox-Privacy (docs/firefox.md).') if selection.get('spotify_custom'): @@ -354,6 +393,7 @@ def install_packages(selection): profile.unlink() profile.symlink_to(Path('/etc/portage/make.profile').resolve()) export(selection, stage) + fetched = stage_repositories(selection, stage) make_conf = stage / 'etc/portage/make.conf' make_conf.write_text(make_conf.read_text() + '\nsource "' + str(stage / 'etc/portage/fiw-dots.conf') + '"\n') # Repository checkout is local and read-only for resolution. No live @@ -392,6 +432,7 @@ def install_packages(selection): return if ask('Apply the previewed Portage files and install the accepted packages?', ['apply', 'cancel'], 'cancel') != 'apply': return + publish_repositories(fetched) # Back up each existing managed destination before replacing it. stamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S.%fZ') backup_root = Path('/var/lib/Fiw-Gentoo-Dots/backups') / stamp @@ -456,7 +497,7 @@ def install_packages(selection): raise RuntimeError('Some packages did not install; consult the package report and emerge output.') if 'kde' in selection['groups'] and 'kde-plasma/plasma-login-manager' not in missing: subprocess.run(['systemctl', 'enable', '--force', 'plasmalogin.service'], check=True) - print('Portage installation finished. Plasma Login Manager is enabled for selected KDE installs; no running greeter was restarted. Flatpaks and boot deployment are explicit follow-up steps; see docs/setup.md.') + print('Portage installation finished. Plasma Login Manager is enabled for selected KDE installs; no running greeter was restarted. Use the selected Flatpak, service and boot actions or the full restore workflow to finish setup.') def check_packages(selection): @@ -470,6 +511,7 @@ def check_packages(selection): profile.unlink() profile.symlink_to(Path('/etc/portage/make.profile').resolve()) export(selection, stage) + fetched = stage_repositories(selection, stage) make_conf = stage / 'etc/portage/make.conf' make_conf.write_text(make_conf.read_text() + '\nsource "' + str(stage / 'etc/portage/fiw-dots.conf') + '"\n') result = resolve(stage, selection, requested) @@ -489,18 +531,28 @@ def main(): parser.add_argument('--catalog', action='store_true') parser.add_argument('--export', type=Path) parser.add_argument('--apply-configs', action='store_true') + parser.add_argument('--check-configs', action='store_true') parser.add_argument('--install-packages', action='store_true') parser.add_argument('--check-packages', action='store_true') + parser.add_argument('--install-flatpaks', action='store_true') + parser.add_argument('--enable-services', action='store_true') + parser.add_argument('--deploy-bootloader', action='store_true') + parser.add_argument('--boot-plan', action='store_true') + parser.add_argument('--esp', type=Path) parser.add_argument('--update', action='store_true') parser.add_argument('--conflict', choices=['ask', 'keep', 'apply'], default='ask') args = parser.parse_args() if args.catalog: print(json.dumps({'groups': GROUPS, 'configs': CATALOG, 'extras': EXTRAS, + 'flatpaks': FLATPAKS, 'services': SERVICES, 'stock': load_selection(profile='stock'), 'ryzen': load_selection(profile='fiw-ryzen')})) return selection = load_selection(args.selection, args.profile) if args.export: export(selection, args.export) + elif args.check_configs: + check_configs(selection, args.home) + print('Config restoration preflight passed.') elif args.apply_configs: if os.geteuid() == 0: raise RuntimeError('Apply user configs as the target user, not as root.') @@ -509,6 +561,16 @@ def main(): install_packages(selection) elif args.check_packages: check_packages(selection) + elif args.install_flatpaks: + install_flatpaks(selection, ask) + elif args.enable_services: + enable_services(selection, ask) + elif args.deploy_bootloader or args.boot_plan: + from boot import deploy, plan + if args.boot_plan: + print(json.dumps(plan(selection['bootloader'], args.esp), indent=2)) + else: + deploy(selection['bootloader'], args.esp, ask) else: print(preview(selection)) diff --git a/lib/setup.py b/lib/setup.py new file mode 100644 index 0000000..990db99 --- /dev/null +++ b/lib/setup.py @@ -0,0 +1,174 @@ +"""Optional repositories, Flatpaks and services for the restoration workflow.""" +import json +import os +import re +import shutil +import subprocess +from datetime import datetime, timezone +from pathlib import Path + +REPOSITORIES = { + 'guru': 'https://github.com/gentoo-mirror/guru.git', + 'steam-overlay': 'https://github.com/anyc/steam-overlay.git', +} +FLATPAKS = {} +LABELS = {'org.localsend.localsend_app': 'LocalSend', 'org.vinegarhq.Sober': 'Sober'} +for list_file in sorted((Path(__file__).resolve().parents[1] / 'flatpaks').glob('*.list')): + for line in list_file.read_text().splitlines(): + app = line.strip() + if not app or app.startswith('#'): + continue + if not re.fullmatch(r'[A-Za-z][A-Za-z0-9_.-]*', app) or app.count('.') < 2: + raise ValueError('Invalid Flatpak ID in ' + list_file.name) + FLATPAKS[app] = {'label': LABELS.get(app, app), 'group': list_file.stem} +SERVICES = { + 'audio': {'label': 'PipeWire audio (user)', 'scope': 'user', + 'units': ['pipewire.socket', 'pipewire-pulse.socket', 'wireplumber.service'], + 'packages': ['media-video/pipewire', 'media-video/wireplumber']}, + 'network': {'label': 'NetworkManager (system)', 'scope': 'system', + 'units': ['NetworkManager.service'], 'packages': ['net-misc/networkmanager']}, + 'bluetooth': {'label': 'Bluetooth (system)', 'scope': 'system', + 'units': ['bluetooth.service'], 'packages': ['net-wireless/bluez']}, + 'power': {'label': 'Power profiles (system)', 'scope': 'system', + 'units': ['power-profiles-daemon.service'], 'packages': ['sys-power/power-profiles-daemon']}, +} + + +def required_repositories(selection): + names = [] + if any(g in selection['groups'] for g in ('fiw-apps', 'gaming')): + names.append('guru') + if 'gaming' in selection['groups']: + names.append('steam-overlay') + return names + + +def existing_repository(name): + if not shutil.which('portageq'): + return None + result = subprocess.run(['portageq', 'get_repo_path', '/', name], text=True, + capture_output=True) + if result.returncode or not result.stdout.strip(): + return None + path = Path(result.stdout.strip()) + if (path / 'profiles/repo_name').is_file(): + return path + return None + + +def repository_config(name, location): + return (f'[{name}]\nlocation = {location}\nsync-type = git\n' + f'sync-uri = {REPOSITORIES[name]}\nsync-depth = 1\npriority = 10\n') + + +def stage_repositories(selection, stage): + """Fetch missing selected repositories into the temporary resolution root.""" + fetched = {} + for name in required_repositories(selection): + if existing_repository(name): + continue + if not shutil.which('git'): + raise RuntimeError('Repository preparation needs Git. Install dev-vcs/git first.') + path = stage / 'var/db/repos' / name + path.parent.mkdir(parents=True, exist_ok=True) + print('Preparing missing repository in temporary storage: ' + name, flush=True) + subprocess.run(['git', 'clone', '--depth=1', REPOSITORIES[name], str(path)], check=True) + if (path / 'profiles/repo_name').read_text().strip() != name: + raise RuntimeError('Unexpected repository identity for ' + name) + config = stage / 'etc/portage/repos.conf' / ('fiw-dots-' + name + '.conf') + config.parent.mkdir(parents=True, exist_ok=True) + config.write_text(repository_config(name, path)) + fetched[name] = path + return fetched + + +def publish_repositories(fetched): + """Adopt verified checkouts only after the installation confirmation.""" + for name, source in fetched.items(): + target = Path('/var/db/repos') / name + if target.exists(): + raise RuntimeError('Repository destination already exists; preserving it: ' + str(target)) + target.parent.mkdir(parents=True, exist_ok=True) + scratch = target.with_name('.fiw-dots-' + name + '-' + str(os.getpid())) + try: + shutil.copytree(source, scratch, symlinks=True) + scratch.rename(target) + finally: + if scratch.exists(): + shutil.rmtree(scratch) + + +def write_report(kind, report, system=False): + state = Path('/var/lib/Fiw-Gentoo-Dots') if system else Path.home() / '.local/state/Fiw-Gentoo-Dots' + state.mkdir(parents=True, exist_ok=True) + stamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S.%fZ') + path = state / (kind + '-' + stamp + '.json') + path.write_text(json.dumps(report, indent=2) + '\n') + print('Report: ' + str(path)) + return path + + +def install_flatpaks(selection, ask): + if os.geteuid() == 0: + raise RuntimeError('Install user Flatpaks as the target user, not root.') + apps = selection.get('flatpaks', []) + if not apps: + print('No Flatpaks selected.') + return + if not shutil.which('flatpak'): + raise RuntimeError('Flatpak is missing. Install sys-apps/flatpak or select it through the package workflow.') + missing = [app for app in apps if subprocess.run( + ['flatpak', 'info', '--user', app], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL).returncode] + report = {'selected': apps, 'already_installed': [app for app in apps if app not in missing], + 'installed': [], 'failed': [], 'skipped': []} + print('User Flatpaks from Flathub: ' + (', '.join(missing) or 'all selected apps already installed')) + if missing and ask('Install these selected Flatpaks for your user?', ['install', 'skip'], 'install') == 'skip': + report['skipped'] = missing + elif missing: + remote = subprocess.run(['flatpak', 'remote-add', '--user', '--if-not-exists', + 'flathub', 'https://flathub.org/repo/flathub.flatpakrepo']) + if remote.returncode: + report['failed'] = missing + else: + for app in missing: + result = subprocess.run(['flatpak', 'install', '--user', '--noninteractive', + '--assumeyes', 'flathub', app]) + present = subprocess.run(['flatpak', 'info', '--user', app], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + report['installed' if not result.returncode and not present.returncode else 'failed'].append(app) + write_report('flatpaks', report) + print('Missing Flatpaks: ' + (', '.join(report['failed'] + report['skipped']) or 'none')) + if report['failed']: + raise RuntimeError('Some Flatpaks did not install; see the report.') + + +def enable_services(selection, ask): + system = os.geteuid() == 0 + scope = 'system' if system else 'user' + units = list(dict.fromkeys(unit for key in selection.get('services', []) + if SERVICES[key]['scope'] == scope for unit in SERVICES[key]['units'])) + if not units: + print('No ' + scope + ' services selected.') + return + command = ['systemctl'] + ([] if system else ['--user']) + report = {'scope': scope, 'selected': units, 'enabled': [], 'missing': [], 'failed': [], 'skipped': []} + available = [] + for unit in units: + result = subprocess.run(command + ['show', '--property=LoadState', '--value', unit], + text=True, capture_output=True) + if result.returncode or result.stdout.strip() != 'loaded': + report['missing'].append(unit) + else: + available.append(unit) + print('Enable at next boot/login: ' + (', '.join(available) or 'none')) + if available and ask('Enable the selected ' + scope + ' services?', ['enable', 'skip'], 'enable') == 'skip': + report['skipped'] = available + else: + for unit in available: + result = subprocess.run(command + ['enable', unit]) + report['enabled' if not result.returncode else 'failed'].append(unit) + write_report('services', report, system) + print('Missing service units: ' + (', '.join(report['missing']) or 'none')) + if report['failed']: + raise RuntimeError('Some services could not be enabled; see the report.') diff --git a/presets/fiw-ryzen.json b/presets/fiw-ryzen.json index 81255a0..256b560 100644 --- a/presets/fiw-ryzen.json +++ b/presets/fiw-ryzen.json @@ -31,5 +31,7 @@ "btrfs" ], "firefox_privacy": false, - "spotify_custom": false + "spotify_custom": false, + "flatpaks": [], + "services": [] } diff --git a/presets/stock.json b/presets/stock.json index 9c93ed5..963a10f 100644 --- a/presets/stock.json +++ b/presets/stock.json @@ -26,5 +26,7 @@ "bootloader": "keep", "extras": [], "firefox_privacy": false, - "spotify_custom": false + "spotify_custom": false, + "flatpaks": [], + "services": [] } diff --git a/tests/test_setup.py b/tests/test_setup.py new file mode 100644 index 0000000..44d4979 --- /dev/null +++ b/tests/test_setup.py @@ -0,0 +1,142 @@ +import importlib.util +import json +import subprocess +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +LIB = Path(__file__).resolve().parents[1] / 'lib' + + +def module(name): + spec = importlib.util.spec_from_file_location(name, LIB / (name + '.py')) + value = importlib.util.module_from_spec(spec) + spec.loader.exec_module(value) + return value + + +setup = module('setup') +boot = module('boot') + + +class SetupTests(unittest.TestCase): + def test_only_selected_overlays_are_staged_and_existing_ones_are_preserved(self): + self.assertEqual(setup.required_repositories({'groups': ['kde']}), []) + self.assertEqual(setup.required_repositories({'groups': ['gaming']}), ['guru', 'steam-overlay']) + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + def clone(command, **kwargs): + destination = Path(command[-1]) + (destination / 'profiles').mkdir(parents=True) + (destination / 'profiles/repo_name').write_text(destination.name + '\n') + return subprocess.CompletedProcess(command, 0) + with patch.object(setup, 'existing_repository', side_effect=lambda name: Path('/existing') if name == 'guru' else None), patch.object(setup.shutil, 'which', return_value='/usr/bin/git'), patch.object(setup.subprocess, 'run', side_effect=clone) as run: + fetched = setup.stage_repositories({'groups': ['gaming']}, root) + self.assertEqual(set(fetched), {'steam-overlay'}) + self.assertEqual(run.call_count, 1) + config = root / 'etc/portage/repos.conf/fiw-dots-steam-overlay.conf' + self.assertIn(str(root / 'var/db/repos/steam-overlay'), config.read_text()) + self.assertFalse((root / 'etc/portage/repos.conf/fiw-dots-guru.conf').exists()) + + def test_flatpak_failure_is_reported_and_installs_are_user_scoped(self): + reports = [] + installed = set() + def run(command, **kwargs): + if command[:2] == ['flatpak', 'info']: + return subprocess.CompletedProcess(command, 0 if command[-1] in installed else 1) + if 'install' in command and command[-1] == 'org.localsend.localsend_app': + installed.add(command[-1]) + return subprocess.CompletedProcess(command, 0) + if 'install' in command: + return subprocess.CompletedProcess(command, 1) + return subprocess.CompletedProcess(command, 0) + with patch.object(setup.os, 'geteuid', return_value=1000), patch.object(setup.shutil, 'which', return_value='/usr/bin/flatpak'), patch.object(setup.subprocess, 'run', side_effect=run) as commands, patch.object(setup, 'write_report', side_effect=lambda kind, report: reports.append(report)): + with self.assertRaises(RuntimeError): + setup.install_flatpaks({'flatpaks': list(setup.FLATPAKS)}, lambda *args: 'install') + self.assertEqual(reports[0]['installed'], ['org.localsend.localsend_app']) + self.assertEqual(reports[0]['failed'], ['org.vinegarhq.Sober']) + for call in commands.call_args_list: + self.assertIn('--user', call.args[0]) + + def test_missing_services_are_reported_and_enabled_without_starting(self): + reports = [] + def run(command, **kwargs): + if 'show' in command: + state = 'not-found\n' if command[-1] == 'wireplumber.service' else 'loaded\n' + return subprocess.CompletedProcess(command, 0, state) + return subprocess.CompletedProcess(command, 0) + with patch.object(setup.os, 'geteuid', return_value=1000), patch.object(setup.subprocess, 'run', side_effect=run) as commands, patch.object(setup, 'write_report', side_effect=lambda kind, report, system: reports.append(report)): + setup.enable_services({'services': ['audio', 'network']}, lambda *args: 'enable') + self.assertEqual(reports[0]['missing'], ['wireplumber.service']) + self.assertEqual(reports[0]['enabled'], ['pipewire.socket', 'pipewire-pulse.socket']) + for call in commands.call_args_list: + self.assertNotIn('--now', call.args[0]) + self.assertIn('--user', call.args[0]) + + +class BootTests(unittest.TestCase): + def test_managed_limine_block_preserves_other_entries_and_rejects_bad_markers(self): + original = 'timeout: 8\n\n/Other OS\n protocol: efi\n path: boot():/other.efi\n' + block = boot.render_limine([{'version': 'test-gentoo'}], 'root=UUID=target-root ro') + merged = boot.merge_limine(original, block) + self.assertTrue(merged.startswith(original)) + self.assertEqual(boot.merge_limine(merged, block), merged) + with self.assertRaises(RuntimeError): + boot.merge_limine(original + boot.BEGIN, block) + + def test_edited_limine_entries_get_proposal_while_external_entries_remain(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + image = root / 'kernel';image.write_bytes(b'kernel') + initrd = root / 'initrd';initrd.write_bytes(b'initrd') + images = [{'version': 'test-gentoo', 'kernel': str(image), 'initrd': str(initrd)}] + saved = {'mode': 'limine', 'esp': str(root / 'esp'), 'cmdline': 'root=UUID=target-root ro'} + (root / 'esp').mkdir() + with patch.object(boot, 'discover_esp', return_value={'path': saved['esp']}), patch.object(boot, 'kernel_images', return_value=images): + saved = boot.refresh(saved, root / 'backups') + config = root / 'esp/EFI/Fiw-Gentoo/limine.conf' + config.write_text(config.read_text() + '\n/Other OS\n protocol: efi\n') + saved = boot.refresh(saved, root / 'backups') + self.assertIn('/Other OS', config.read_text()) + config.write_text(config.read_text().replace('target-root', 'user-edited-root')) + previous = config.read_text() + boot.refresh(saved, root / 'backups') + self.assertEqual(config.read_text(), previous) + proposals = list(config.parent.glob('limine.conf.new.*')) + self.assertEqual(len(proposals), 1) + self.assertIn('/Other OS', proposals[0].read_text()) + + def test_failed_grub_generation_does_not_replace_existing_menu(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + folder = root / 'boot';(folder / 'grub').mkdir(parents=True) + config = folder / 'grub/grub.cfg';config.write_text('working config\n') + images = [] + def run(command, **kwargs): + Path(command[-1]).write_text('partial failed config') + raise subprocess.CalledProcessError(1, command) + # /boot sources prevent normalization writes outside the staging dir. + images = [{'version': 'test', 'kernel': '/boot/kernel-test', 'initrd': '/boot/initramfs-test.img'}] + saved = {'mode': 'grub', 'esp': str(root), 'config_hash': boot.file_hash(config)} + with patch.object(boot, 'GRUB_DIRECTORY', folder), patch.object(boot, 'discover_esp', return_value={'path': str(root)}), patch.object(boot, 'kernel_images', return_value=images), patch.object(boot.subprocess, 'run', side_effect=run): + with self.assertRaises(subprocess.CalledProcessError): + boot.refresh(saved, root / 'backups') + self.assertEqual(config.read_text(), 'working config\n') + self.assertFalse(list(config.parent.glob('.*.tmp'))) + + def test_firmware_add_preserves_bootorder_and_repeat_default_does_not_duplicate(self): + target = {'disk': '/dev/testdisk', 'partition': 2, 'partuuid': 'target-partition'} + loader = '/EFI/Fiw-Gentoo/BOOTX64.EFI' + command = boot.firmware_commands(target, 'limine', loader, 'add', '')[0] + self.assertIn('--create-only', command) + self.assertNotIn('--create', command) + listing = 'BootOrder: 0001,0002\nBoot0002* Fiw-Gentoo-limine HD(2,GPT,target-partition)/File(\\EFI\\Fiw-Gentoo\\BOOTX64.EFI)\n' + commands = boot.firmware_commands(target, 'limine', loader, 'default', listing) + self.assertEqual(commands, [['efibootmgr', '--bootorder', '0002,0001']]) + self.assertEqual(boot.firmware_commands(target, 'limine', loader, 'add', listing), []) + self.assertEqual(boot.plan('keep'), {'mode': 'keep', 'changes': []}) + + +if __name__ == '__main__': + unittest.main()