From ba98cf48a7a9b387e89aaa116da17281f73a3220 Mon Sep 17 00:00:00 2001 From: Fi3w0 <98111375+Fi3w0@users.noreply.github.com> Date: Sun, 4 Oct 2026 19:34:31 +0200 Subject: [PATCH] Start Kitty in Fish and fix boot and module restoration after QEMU checks --- README.md | 6 +- configs/catalog.json | 5 +- .../files/.config/kitty/kitty-common.conf | 1 + docs/app-configs.md | 4 ++ docs/boot.md | 13 ++++- docs/reports.md | 1 + docs/setup.md | 25 ++++++--- docs/tidewm.md | 7 ++- lib/boot.py | 56 +++++++++++++++++-- lib/rice.py | 44 ++++++++++++++- optional/fiw-cachy-patch | 5 +- optional/spotify/spotify-patch | 4 +- portage/dev/package.env/heavy | 16 +++--- portage/dev/package.env/notmpfs | 4 +- tools/capture.py | 10 +++- 15 files changed, 159 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 84c1bf6..bf9075c 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,9 @@ My personal Gentoo restoration kit, mainly for my own devices: - Binaries preferred, with a small deliberate source-build list. - A Bubble Tea chooser with a final preview, conflict prompts and backups. -The Stock preset has been installed in a fresh Gentoo QEMU/KVM guest: packages, -config restoration and Plasma Login Manager login passed. Custom kernels, -automatic bootloader deployment and optional setups still need full runtime checks. +Stock and Fiw's Ryzen have been installed and checked in Gentoo QEMU/KVM. +The full TUI restore, KDE presets, optional apps, GRUB/Limine and ext4/Btrfs +booting passed. See [tested scope and hardware limits](docs/setup.md#current-scope). ## Screenshots diff --git a/configs/catalog.json b/configs/catalog.json index 9eff4ab..068beb8 100644 --- a/configs/catalog.json +++ b/configs/catalog.json @@ -61,9 +61,10 @@ "label": "Kitty lavender profiles", "group": "fiw-apps", "packages": [ - "x11-terms/kitty" + "x11-terms/kitty", + "app-shells/fish" ], - "note": "Select fonts separately for the bundled JetBrainsMono Nerd Font." + "note": "Both profiles start Fish. Select fonts separately for the bundled JetBrainsMono Nerd Font." }, "neovim": { "label": "Neovim transparent purple style", diff --git a/configs/kitty/files/.config/kitty/kitty-common.conf b/configs/kitty/files/.config/kitty/kitty-common.conf index 69f5d0a..9135e22 100644 --- a/configs/kitty/files/.config/kitty/kitty-common.conf +++ b/configs/kitty/files/.config/kitty/kitty-common.conf @@ -5,6 +5,7 @@ italic_font auto bold_italic_font auto font_size 11.0 +shell fish linux_display_server auto background_blur 32 dynamic_background_opacity yes diff --git a/docs/app-configs.md b/docs/app-configs.md index 4df0d53..2d05751 100644 --- a/docs/app-configs.md +++ b/docs/app-configs.md @@ -43,6 +43,10 @@ packages again, including requirements of apps whose installation was skipped. Fish, Kitty, Neovim and Fastfetch have no Plasma or KWin requirements. Fonts have their own checkbox; Kitty can use its fallback font if those are omitted. +Both Kitty profiles start Fish explicitly, so the Kitty preset requires Fish +even when the optional Fish preferences are unchecked. +For Fish in TTY and SSH logins too, run `chsh -s "$(command -v fish)"` as your +regular user after installing Fish, then log in again. Neovim's config includes a pinned Catppuccin runtime in its native package directory and loads it before the captured purple highlights. It needs no plugin manager or runtime download. A built-in fallback remains available if diff --git a/docs/boot.md b/docs/boot.md index 0a3efbd..1d9ad62 100644 --- a/docs/boot.md +++ b/docs/boot.md @@ -55,6 +55,11 @@ 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`. +Explicit deployment asks before replacing a conflicting GRUB menu or Limine +managed block, with a backup. Keeping it writes a proposal and leaves firmware +registration and order unchanged. Automatic kernel refreshes preserve edits +without prompting; review their proposals before using the new entries. + ## Kernel updates and backups Deployment installs a standalone refresh helper at @@ -72,9 +77,11 @@ 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. A fresh ext4 QEMU/KVM guest booted the binary kernel with a -separately provisioned UEFI GRUB loader. The automatic deployment and kernel -update hooks described here still need end-to-end boot checks. +before changes. Automatic GRUB and Limine deployment and booting were checked +in Gentoo QEMU/KVM with ext4 and a Btrfs `@` root. The custom Ryzen kernel and +binary fallback booted, and addition/removal hooks and preservation of edited +menus were checked. Physical firmware and unsupported storage layouts still +need their own setup. `optional/fiw-cachy-patch` refreshes the custom kernel patch; it does not select a bootloader. See the [helper list](scripts.md) before using it. diff --git a/docs/reports.md b/docs/reports.md index 6c1a903..24664f9 100644 --- a/docs/reports.md +++ b/docs/reports.md @@ -8,6 +8,7 @@ The report includes: - Completed, partial, failed, cancelled and not attempted steps. - Confirmed present Portage packages and skipped or missing packages. +- NVIDIA module checks/builds for a selected custom kernel and binary fallback. - Installed, existing, skipped or failed user Flatpaks. - Enabled, missing or skipped service units. - Applied configs, preserved conflicts and pending `.new` proposals. diff --git a/docs/setup.md b/docs/setup.md index 5e3e53a..578cc6a 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -69,6 +69,10 @@ Binary packages retain the binhost's USE settings; required dependency flags are still checked. The preview includes dependency updates and replacements, such as adding Steam's 32-bit libraries to a fresh multilib installation. +Custom kernel installs with NVIDIA also check its modules against the selected +custom kernel and binary fallback. Missing or outdated modules are compiled +for each, and failures appear in the package report. + Read-only resolution and an export are available separately: ```sh @@ -144,14 +148,19 @@ 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. -The optional [TideWM recipe](tidewm.md) follows the audited upstream master -branch and uses the normal compile-or-skip prompt. The Stock package installation completed in a fresh ext4 Gentoo QEMU/KVM guest -(539 package steps, no missing selected packages). Config restoration, conflict -handling, backups, the bundled chooser and Plasma Login Manager login were -checked there, along with portable terminal configs and optional service enabling. -The guest booted a binary kernel using a separately provisioned UEFI GRUB loader. -Automatic bootloader deployment, custom Ryzen kernel builds, Btrfs, TideWM, -optional Flatpak installation and physical GPU behaviour remain unverified. +Gentoo QEMU/KVM checks covered a fresh Stock installation, Fiw's Ryzen custom +kernel and binary fallback, all package sections, the complete TUI restore, +all 16 config presets, capture filtering, preserved edits, proposals and backups. +Plasma Login Manager, KDE/GTK styling, shortcuts, optional autostart, service +enabling and both user Flatpaks were checked. GRUB and Limine booted ext4 and +a Btrfs `@` root; kernel addition/removal hooks and menu conflicts were checked. +The Btrfs guest used a converted copy of the installed ext4 guest. + +App checks cover launches and local configuration, including MangoHud, +GameMode, FiwNode audio and Apdatifier. [TideWM](tidewm.md) compiled and ran +with virtual graphics. NVIDIA modules were built and checked for both selected +kernels. Physical NVIDIA rendering, Wi-Fi/Bluetooth devices, real audio hardware, +account sign-ins and gameplay still need checks on the target device. Do not publish the private original fiw-gentoo archive. It has different history and host-specific files. diff --git a/docs/tidewm.md b/docs/tidewm.md index d587191..ed9251e 100644 --- a/docs/tidewm.md +++ b/docs/tidewm.md @@ -19,8 +19,11 @@ is enabled by default, including the session-specific portal files. This is a source build. The main installer shows it as an additional compile-or-skip choice; it is not part of the deliberately small source list. A live recipe follows future upstream changes, while the current audit SHA -documents the version reviewed for this snapshot. Compilation of the -refreshed tree on a fresh device remains unverified. +documents the version reviewed for this snapshot. That commit compiled and +ran in the Gentoo QEMU/KVM guest, both nested inside Plasma and directly from +a TTY with virtual graphics. Kitty started Fish with the restored terminal +preferences in both sessions. Physical GPU and screencasting behaviour remain +device-specific checks. After installation, choose **TideWM** at login. It creates its own default configuration on first launch. No existing TideWM settings are captured or diff --git a/lib/boot.py b/lib/boot.py index 958437d..a8a3ef0 100644 --- a/lib/boot.py +++ b/lib/boot.py @@ -10,6 +10,7 @@ import shlex import shutil import subprocess import sys +import tempfile from datetime import datetime, timezone from pathlib import Path @@ -229,7 +230,35 @@ def write(path, content, backups, mode=0o644): tmp.replace(path) -def refresh(saved, backups, exclude=None): +def grub_config(output, images, exclude=None): + env = dict(os.environ, GRUB_TOP_LEVEL=images[0]['kernel']) + command = ['grub-mkconfig', '-o', str(output)] + if not exclude: + subprocess.run(command, env=env, check=True) + return + # Removal hooks can run before the kernel file disappears. Filter discovery + # through GRUB's library override, without moving installed boot artifacts. + library = Path('/usr/share/grub') + source = (library / 'grub-mkconfig_lib').read_text() + source, count = re.subn(r'\bgrub_file_is_not_garbage\s*\(\s*\)', + 'fiw_dots_file_is_not_garbage ()', source, count=1) + if count != 1: + raise RuntimeError('Cannot filter this GRUB library; preserving the boot menu.') + paths = ['/boot/kernel-' + exclude] + [prefix + exclude for prefix in + ('/boot/vmlinuz-', '/vmlinuz-', '/boot/vmlinux-', '/vmlinux-')] + source += '\ngrub_file_is_not_garbage () {\n case "$1" in\n ' + '|'.join( + shlex.quote(path) for path in paths) + ') return 1 ;;\n esac\n fiw_dots_file_is_not_garbage "$@"\n}\n' + with tempfile.TemporaryDirectory(prefix='fiw-dots-grub-') as directory: + root = Path(directory) + for path in library.iterdir(): + if path.name != 'grub-mkconfig_lib': + (root / path.name).symlink_to(path) + (root / 'grub-mkconfig_lib').write_text(source) + subprocess.run(command, env=dict(env, pkgdatadir=directory), check=True) + + +def refresh(saved, backups, exclude=None, ask=None): + saved.pop('pending_config', None) # Recheck the mount every time; never write into an unmounted ESP directory. target = discover_esp(Path(saved['esp'])) images = kernel_images(exclude=exclude) @@ -246,7 +275,10 @@ def refresh(saved, backups, exclude=None): config = GRUB_DIRECTORY / 'grub/grub.cfg' old_hash = file_hash(config) output = config - if old_hash and old_hash != saved.get('config_hash'): + edited = old_hash and old_hash != saved.get('config_hash') + replace = edited and ask and ask('Existing GRUB menu differs from the saved version. Replace it with the generated menu?', + ['replace', 'keep'], 'keep') == 'replace' + if edited and not replace: output = config.with_name(config.name + '.new.' + datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S.%fZ')) else: backup(config, backups) @@ -264,12 +296,13 @@ def refresh(saved, backups, exclude=None): shutil.copy2(source, dest) scratch = output.with_name('.' + output.name + '.tmp') try: - subprocess.run(['grub-mkconfig', '-o', str(scratch)], check=True) + grub_config(scratch, images, exclude) scratch.replace(output) finally: if scratch.exists(): scratch.unlink() if output != config: + saved['pending_config'] = str(output) print('Preserved edited GRUB config; review ' + str(output)) else: saved['config_hash'] = file_hash(config) @@ -295,9 +328,13 @@ def refresh(saved, backups, exclude=None): 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'): + edited = prior and block_hash(prior) != saved.get('block_hash') + replace = edited and ask and ask('Existing Limine entries differ from the saved version. Replace the managed entries?', + ['replace', 'keep'], 'keep') == 'replace' + if edited and not replace: proposal = config.with_name(config.name + '.new.' + datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S.%fZ')) write(proposal, output, backups) + saved['pending_config'] = str(proposal) print('Preserved edited Limine entries; review ' + str(proposal)) elif output != old: write(config, output, backups) @@ -381,7 +418,12 @@ def deploy(mode, esp=None, ask=confirm): else: subprocess.run(proposed['commands'][0], check=True) loader_path = '/EFI/Fiw-Gentoo/grubx64.efi' - saved = refresh(saved, backups) + saved = refresh(saved, backups, ask=ask) + pending = saved.pop('pending_config', None) + if pending and firmware != 'keep': + commands = [] + firmware = 'keep' + print('Kept the existing boot menu; firmware entries and order remain unchanged. Review ' + pending) # 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) @@ -403,7 +445,8 @@ def deploy(mode, esp=None, ask=confirm): report.write_text(json.dumps({'plan': proposed, 'firmware_choice': firmware, 'backups': str(backups)}, indent=2) + '\n') print('Boot deployment finished. Report: ' + str(report)) - return {'status': 'completed', 'mode': mode, 'firmware_choice': firmware, + return {'status': 'partial' if pending else 'completed', 'mode': mode, 'firmware_choice': firmware, + 'review': [pending] if pending else [], 'report': str(report), 'backups': str(backups)} @@ -422,6 +465,7 @@ def main(): 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) + updated.pop('pending_config', None) write(STATE, json.dumps(updated, indent=2) + '\n', backups, 0o600) diff --git a/lib/rice.py b/lib/rice.py index a3810ee..0bd4784 100644 --- a/lib/rice.py +++ b/lib/rice.py @@ -447,6 +447,37 @@ def resolve(stage, selection, packages, execute=False): stderr=None if execute else subprocess.STDOUT) +def nvidia_modules(): + """Check both selected distribution kernels, not just /usr/src/linux.""" + snapshot = json.loads((REPO / 'optional/kernel/snapshot.json').read_text()) + driver = subprocess.check_output(['portageq', 'best_version', '/', 'x11-drivers/nvidia-drivers'], text=True).strip() + expected = re.sub(r'-r\d+$', '', driver.split('nvidia-drivers-', 1)[1]) + result = {'checked': [], 'rebuilt': [], 'missing': []} + for atom in [snapshot['atom'], 'sys-kernel/gentoo-kernel-bin']: + cpv = subprocess.check_output(['portageq', 'best_version', '/', atom], text=True).strip() + contents = (Path('/var/db/pkg') / cpv / 'CONTENTS').read_text() if cpv else '' + versions = sorted(set(re.findall(r'/(?:usr/)?lib/modules/([^/ ]+)/', contents))) + if not versions: + result['missing'].append('NVIDIA kernel target: ' + atom) + for version in versions: + result['checked'].append(version) + def ready(): + return all(subprocess.run(['modinfo', '-k', version, '-F', 'version', module], + text=True, capture_output=True).stdout.strip() == expected + for module in ['nvidia', 'nvidia_drm', 'nvidia_modeset', 'nvidia_uvm']) + if ready(): + continue + build = Path('/lib/modules') / version / 'build' + print('Building NVIDIA modules for ' + version, flush=True) + command = ['emerge', '--oneshot', '--nodeps', '--usepkg=n', '--getbinpkg=n', + '--autounmask=n', '=' + driver] + if build.is_dir() and subprocess.run(command, env=dict(os.environ, KERNEL_DIR=str(build))).returncode == 0 and ready(): + result['rebuilt'].append(version) + else: + result['missing'].append('NVIDIA modules for ' + version) + return result + + def install_packages(selection): if os.geteuid() != 0: raise RuntimeError('Package installation requires root. Run sudo ./install --install-packages --selection .') @@ -504,6 +535,11 @@ def install_packages(selection): if not accepted: print('All requested packages were skipped; no live configuration applied.') return {'status': 'skipped', 'requested': requested, 'skipped': skipped, 'missing': requested} + build_modules = (selection['kernel'] == 'custom' and 'system' in selection['groups'] + and 'x11-drivers/nvidia-drivers' in accepted + and any(atom.startswith('=sys-kernel/gentoo-kernel-') for atom in accepted)) + if build_modules: + print('NVIDIA modules will be checked and compiled when needed for the selected custom kernel and binary fallback.') if ask('Apply the previewed Portage files and install the accepted packages?', ['apply', 'cancel'], 'cancel') != 'apply': return {'status': 'cancelled', 'requested': requested, 'skipped': skipped} publish_repositories(fetched) @@ -556,20 +592,24 @@ def install_packages(selection): if final.returncode or ({cp_from_cpv(x) for x in source_builds(final.stdout)} - approved): raise RuntimeError('The final package plan changed. Review it before retrying; backups are at ' + str(backup_root)) result = resolve(Path('/'), selection, ['@fiw-dots-selected'], execute=True) + modules = nvidia_modules() if result.returncode == 0 and build_modules else {} missing = [atom for atom in requested if subprocess.run( ['portageq', 'has_version', '/', atom], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0] report = {'requested': requested, 'present': [atom for atom in accepted if atom not in missing], 'skipped': skipped, 'emerge_exit_code': result.returncode, 'missing': missing, + 'modules': modules, 'failed': modules.get('missing', []), 'pending': []} state = Path('/var/lib/Fiw-Gentoo-Dots') state.mkdir(parents=True, exist_ok=True) (state / ('packages-' + stamp + '.json')).write_text(json.dumps(report, indent=2) + '\n') print('Package report: ' + str(state / ('packages-' + stamp + '.json'))) print('Missing packages: ' + (', '.join(missing) or 'none')) - if result.returncode: - error = RuntimeError('Some packages did not install; consult the package report and emerge output.') + if build_modules and result.returncode == 0: + print('Missing NVIDIA modules: ' + (', '.join(modules.get('missing', [])) or 'none')) + if result.returncode or modules.get('missing'): + error = RuntimeError('Some packages or kernel modules did not install; consult the package report and emerge output.') error.report = report raise error if 'kde' in selection['groups'] and 'kde-plasma/plasma-login-manager' not in missing: diff --git a/optional/fiw-cachy-patch b/optional/fiw-cachy-patch index 4fec488..a07887b 100755 --- a/optional/fiw-cachy-patch +++ b/optional/fiw-cachy-patch @@ -15,9 +15,10 @@ trap 'rm -rf "$work"' EXIT cd "$work" # Newest CachyOS release for this kernel version (cachyos-7.2.8-1, -2, ...) -tag=$(curl -fsSL "https://api.github.com/repos/CachyOS/linux/releases?per_page=50" | +releases=$(curl -fsSL "https://api.github.com/repos/CachyOS/linux/releases?per_page=50") +tag=$(printf '%s\n' "$releases" | grep -oE "\"tag_name\": *\"cachyos-${ver//./\\.}-[0-9]+\"" | - grep -oE "cachyos-[0-9.]+-[0-9]+" | sort -V | tail -1) + grep -oE "cachyos-[0-9.]+-[0-9]+" | sort -V | tail -1 || true) [[ -n $tag ]] || { echo "CachyOS has no release for $ver yet" >&2; exit 1; } echo "Using $tag" diff --git a/optional/spotify/spotify-patch b/optional/spotify/spotify-patch index c1d9871..46b855f 100755 --- a/optional/spotify/spotify-patch +++ b/optional/spotify/spotify-patch @@ -1,7 +1,7 @@ #!/usr/bin/env bash # SpotX-Bash + spicetify (Marketplace) on Gentoo's media-sound/spotify. # Run as your regular user after the first setup and after every Spotify update. -# Write access comes from an ACL (/etc/portage/env/media-sound/spotify). +# Write access must be configured explicitly before opting in. set -euo pipefail client=/opt/spotify/spotify-client @@ -9,7 +9,7 @@ spotx_url=https://raw.githubusercontent.com/SpotX-Official/SpotX-Bash/main/spotx spicetify=$HOME/.spicetify/spicetify [[ -w $client/Apps/xpui.spa && -w $client/spotify ]] || { - echo "No write access to $client. Run once as root: the optional Spotify ACL setup described in docs/spotify.md" >&2 + echo "No write access to $client. Review docs/spotify.md and configure client write access explicitly before opting in." >&2 exit 1 } diff --git a/portage/dev/package.env/heavy b/portage/dev/package.env/heavy index e247c95..9e1c998 100644 --- a/portage/dev/package.env/heavy +++ b/portage/dev/package.env/heavy @@ -1,8 +1,8 @@ -llvm-core/llvm heavy.conf notmpfs.conf -llvm-core/clang heavy.conf notmpfs.conf -dev-qt/qtwebengine heavy.conf notmpfs.conf -net-libs/nodejs heavy.conf -dev-lang/spidermonkey heavy.conf -media-libs/embree heavy.conf notmpfs.conf -media-gfx/blender heavy.conf notmpfs.conf -media-gfx/openvdb heavy.conf notmpfs.conf +llvm-core/llvm fiw-dots-heavy.conf fiw-dots-notmpfs.conf +llvm-core/clang fiw-dots-heavy.conf fiw-dots-notmpfs.conf +dev-qt/qtwebengine fiw-dots-heavy.conf fiw-dots-notmpfs.conf +net-libs/nodejs fiw-dots-heavy.conf +dev-lang/spidermonkey fiw-dots-heavy.conf +media-libs/embree fiw-dots-heavy.conf fiw-dots-notmpfs.conf +media-gfx/blender fiw-dots-heavy.conf fiw-dots-notmpfs.conf +media-gfx/openvdb fiw-dots-heavy.conf fiw-dots-notmpfs.conf diff --git a/portage/dev/package.env/notmpfs b/portage/dev/package.env/notmpfs index aba2156..ffcc3ab 100644 --- a/portage/dev/package.env/notmpfs +++ b/portage/dev/package.env/notmpfs @@ -1,2 +1,2 @@ -sys-kernel/gentoo-kernel notmpfs.conf -sys-devel/gcc notmpfs.conf +sys-kernel/gentoo-kernel fiw-dots-notmpfs.conf +sys-devel/gcc fiw-dots-notmpfs.conf diff --git a/tools/capture.py b/tools/capture.py index b9e7bc5..1695cf3 100644 --- a/tools/capture.py +++ b/tools/capture.py @@ -183,10 +183,16 @@ def capture_fish(): def capture_kitty(): + def portable_profile(text, name): + text = re.sub(r'(?m)^linux_display_server\s+wayland\s*$', + 'linux_display_server auto', text) + text = re.sub(r'(?m)^shell\s+.*\n?', '', text) + if name == 'kitty-common.conf': + text = text.rstrip() + '\n\nshell fish\n' + return text for name in ['kitty.conf', 'kitty-main.conf', 'kitty-bright.conf', 'kitty-common.conf']: copy('kitty', '.config/kitty/' + name, - lambda t: re.sub(r'(?m)^linux_display_server\s+wayland\s*$', - 'linux_display_server auto', t)) + lambda t: portable_profile(t, name)) def capture_neovim():