Start Kitty in Fish and fix boot and module restoration after QEMU checks
All checks were successful
Check and mirror to GitHub / checks (push) Successful in 20s
Check and mirror to GitHub / github-mirror (push) Successful in 7s

This commit is contained in:
Fi3w0 2026-10-04 19:34:31 +02:00
parent 52d4e90883
commit ba98cf48a7
15 changed files with 159 additions and 42 deletions

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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)

View file

@ -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 <file>.')
@ -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:

View file

@ -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"

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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():