Add Gentoo Portage host configuration
This commit is contained in:
1 parent
d40cb10f21
commit
b19d64a282
57 files changed
+23234
-2
No files matched your search
+10
@@ -0,0 +1,10 @@
|
||||
# Local OpenPGP signing keys and trust state
|
||||
/gnupg/
|
||||
|
||||
# Protected-update candidates generated by dispatch-conf / etc-update
|
||||
**/._cfg*
|
||||
|
||||
# Portage backups
|
||||
**/*~
|
||||
**/*.bak*
|
||||
**/*.backup*
|
||||
@@ -1,3 +1,137 @@
|
||||
# gentoo-portage-config
|
||||
# Gentoo Portage: amd64, musl, LLVM
|
||||
|
||||
My config files for Gentoo's Portage.
|
||||
This repository is a versioned snapshot of the local `/etc/portage` policy on my Gentoo machine. It targets the `default/linux/amd64/23.0/musl/llvm` profile and `x86_64-pc-linux-musl`; it is not a generic Gentoo configuration. The profile selection itself remains a system symlink and is recorded here rather than copied into the repository.
|
||||
|
||||
The screenshot below shows the host and active Gentoo profile from the terminal. The captured kernel and package counts describe the system at screenshot time, not necessarily its current state.
|
||||
|
||||

|
||||
|
||||
## Explicit world set
|
||||
|
||||
This is the host's `/var/lib/portage/world` snapshot, captured on 2026-10-15. It records packages selected explicitly with Portage; dependencies installed to satisfy them aren't necessarily listed here. The plain source list is also tracked as [`world`](world).
|
||||
|
||||
```text
|
||||
app-admin/doas
|
||||
app-editors/nano
|
||||
app-editors/neovim
|
||||
app-eselect/eselect-repository
|
||||
app-misc/brightnessctl
|
||||
app-misc/fastfetch
|
||||
app-portage/gentoolkit
|
||||
app-shells/fish
|
||||
dev-cpp/cairomm
|
||||
dev-lang/execline
|
||||
dev-util/codex
|
||||
dev-util/flatpak-builder
|
||||
gui-apps/foot
|
||||
gui-apps/grim
|
||||
gui-apps/hyprpaper
|
||||
gui-apps/hyprsunset
|
||||
gui-apps/quickshell
|
||||
gui-apps/slurp
|
||||
gui-apps/tuigreet
|
||||
gui-apps/wl-clip-persist
|
||||
gui-apps/wl-clipboard
|
||||
gui-libs/display-manager-init
|
||||
gui-libs/greetd
|
||||
gui-libs/hyprcursor
|
||||
gui-libs/hyprland-guiutils
|
||||
gui-wm/cage
|
||||
gui-wm/hyprland
|
||||
media-fonts/fontawesome
|
||||
media-fonts/maple-mono
|
||||
media-fonts/nerdfonts
|
||||
media-gfx/imv
|
||||
media-libs/gst-plugins-good
|
||||
media-libs/libpulse
|
||||
media-sound/pavucontrol
|
||||
media-sound/playerctl
|
||||
media-video/pipewire
|
||||
net-misc/chrony
|
||||
net-vpn/wireguard-tools
|
||||
net-wireless/iwd
|
||||
sys-apps/flatpak
|
||||
sys-apps/pciutils
|
||||
sys-apps/s6
|
||||
sys-apps/s6-linux-init
|
||||
sys-apps/s6-rc
|
||||
sys-auth/seatd
|
||||
sys-boot/efibootmgr
|
||||
sys-boot/limine
|
||||
sys-cluster/kubectl
|
||||
sys-devel/mold
|
||||
sys-firmware/intel-microcode
|
||||
sys-fs/btrfs-progs
|
||||
sys-fs/fuse:0
|
||||
sys-kernel/cachyos-kernel
|
||||
sys-kernel/dracut
|
||||
sys-kernel/gentoo-kernel-bin
|
||||
sys-kernel/gentoo-sources
|
||||
sys-kernel/installkernel
|
||||
sys-kernel/linux-firmware
|
||||
sys-libs/gcompat
|
||||
sys-process/htop
|
||||
x11-themes/bibata-xcursors
|
||||
```
|
||||
|
||||
## Build contract
|
||||
|
||||
| Setting | Current value | Effect |
|
||||
| --- | --- | --- |
|
||||
| Profile | `default/linux/amd64/23.0/musl/llvm` | amd64, musl, LLVM profile defaults |
|
||||
| `CHOST` | `x86_64-pc-linux-musl` | Build target tuple for the musl userspace |
|
||||
| C/C++ flags | `-march=native -O3 -pipe` | Tune builds for the local CPU; resulting binaries are not portable to older or different CPUs |
|
||||
| Rust | `-C target-cpu=native` | Apply the same local-CPU assumption to Rust builds |
|
||||
| Linker | `-fuse-ld=mold` globally | Use mold for builds that honor `LDFLAGS` |
|
||||
| Build parallelism | `MAKEOPTS="-j4 -l5"`, emerge `--jobs=4` | Limit make jobs and concurrent emerge work |
|
||||
| Global USE | `wayland -X hyprland dbus wifi bluetooth system-pipewire pulseaudio` | Set the desktop and service feature baseline |
|
||||
| Licenses | `ACCEPT_LICENSE="*"` | Accept every license identifier; review this policy before reusing the config |
|
||||
|
||||
The selected profile owns the system toolchain defaults; `make.conf` does not globally force `CC` or `CXX`. Compiler and resource changes are attached to package atoms through `package.env`. Some of those snippets select Clang/LLVM tools and LLD with ThinLTO, while the global linker is mold, so check the resolved environment for the package you are building instead of assuming one linker applies everywhere.
|
||||
|
||||
## Repository map
|
||||
|
||||
The files at the repository root mirror `/etc/portage`:
|
||||
|
||||
| Path | Role |
|
||||
| --- | --- |
|
||||
| `make.conf` | Global flags, USE policy, license policy, jobs, binary-package features, and mirrors |
|
||||
| `repos.conf/` | Gentoo rsync configuration and Git overlays, including CachyOS kernels, GURU, Hyprland, LibreWolf, and local repositories |
|
||||
| `binrepos.conf/` | Signed Gentoo binary-package repository |
|
||||
| `package.use/` | Directory-form per-package USE overrides, split into desktop, kernel, audio, and other groups |
|
||||
| `package.accept_keywords/` | Narrow keyword exceptions for selected packages and overlays |
|
||||
| `package.unmask/` | Local version policy exceptions; no local package masks are configured |
|
||||
| `package.env/`, `env/` | Package-scoped environment files for compiler, linker, and resource settings |
|
||||
| `savedconfig/` | Saved CachyOS kernel and linux-firmware package configuration |
|
||||
| `world` | Captured explicit Portage world atoms from `/var/lib/portage/world` |
|
||||
| `bashrc` | Portage package hooks, including Btrfs snapshots and a Firefox-specific Rust flag |
|
||||
| `usr/local/libexec/` | Executable helpers called by the Portage hooks: Btrfs snapshot lifecycle and Limine menu maintenance |
|
||||
|
||||
## Btrfs and Limine hooks
|
||||
|
||||
`bashrc` calls `portage-btrfs-snapshot` after each package install and removal. The helper limits itself to operations against the live root, checks that `/` and `/.snapshots` are on the same Btrfs filesystem, requires at least 2 GiB free, creates read-only snapshots under `/.snapshots/portage`, and retains up to 32 of its own snapshots. When pruning, it removes the corresponding Limine recovery entry and writable clone before deleting the old read-only snapshot.
|
||||
|
||||
`portage-limine-menu` maintains `/boot/limine.conf`. After a `sys-kernel` install, the snapshot helper asks it to reconcile kernel entries and attach compatible snapshots. Other package installs create recovery entries when a compatible kernel/initramfs group exists; removals clean up the matching Limine entries and writable recovery clones. The manager validates the menu before writing it and keeps a one-time pre-change copy alongside the Limine config. Both scripts assume the host layout and Btrfs/Limine setup above; they are not portable drop-in hooks for another machine.
|
||||
|
||||
The scripts are stored at their corresponding paths under `usr/local/libexec/` in this repository. To install or update them on this host, from the repository root:
|
||||
|
||||
```sh
|
||||
doas install -D -m 0755 usr/local/libexec/portage-btrfs-snapshot /usr/local/libexec/portage-btrfs-snapshot
|
||||
doas install -D -m 0755 usr/local/libexec/portage-limine-menu /usr/local/libexec/portage-limine-menu
|
||||
```
|
||||
|
||||
## Snapshot boundaries
|
||||
|
||||
The non-backup CachyOS kernel configs and the `linux-firmware` saved config are included. Portage’s generated `._cfg*` merge candidates and timestamped backups are omitted; those are pending-update artifacts or historical copies, not active policy. `/etc/portage/gnupg` is also excluded because it can contain private signing keys and local trust state. The signed-binhost configuration is tracked, but its key material is not.
|
||||
|
||||
## Applying the snapshot
|
||||
|
||||
First compare the target host’s state and profile; don’t copy this over a different ABI or profile. The diff excludes repository metadata and files deliberately omitted from the snapshot:
|
||||
|
||||
```sh
|
||||
eselect profile show
|
||||
emerge --info
|
||||
diff -ru --exclude=.git --exclude=gnupg --exclude=make.profile --exclude=README.md --exclude=LICENSE --exclude=.gitignore --exclude=usr --exclude='._cfg*' --exclude='*~' --exclude='*.backup*' --exclude='*.bak*' /etc/portage .
|
||||
```
|
||||
|
||||
After reviewing the diff, merge only the intended files into `/etc/portage`. This checkout also contains Git metadata, documentation, and ignore rules, so copying the repository root wholesale is not appropriate. Before any package operation, preview the resolver plan with `emerge -pv` and inspect USE, slots, keywords, masks, and removals.
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
# BEGIN managed Portage Btrfs snapshots
|
||||
# Portage invokes these phase hooks after package post-install and post-remove.
|
||||
# This is one snapshot per changed package, not one per emerge command.
|
||||
post_pkg_postinst() {
|
||||
/usr/local/libexec/portage-btrfs-snapshot install "${CATEGORY:-unknown}" "${PF:-unknown}" "${ROOT:-/}" "${EROOT:-/}" || \
|
||||
printf '!!! Btrfs snapshot failed after installing %s/%s.\n' "${CATEGORY:-unknown}" "${PF:-unknown}" >&2
|
||||
}
|
||||
|
||||
post_pkg_postrm() {
|
||||
/usr/local/libexec/portage-btrfs-snapshot remove "${CATEGORY:-unknown}" "${PF:-unknown}" "${ROOT:-/}" "${EROOT:-/}" || \
|
||||
printf '!!! Btrfs snapshot failed after removing %s/%s.\n' "${CATEGORY:-unknown}" "${PF:-unknown}" >&2
|
||||
}
|
||||
# END managed Portage Btrfs snapshots
|
||||
|
||||
if [ "${PN}" = "firefox" ]; then
|
||||
export RUSTFLAGS="${RUSTFLAGS} -C target-feature=-crt-static"
|
||||
fi
|
||||
@@ -0,0 +1,9 @@
|
||||
# These settings were set by the catalyst build script that automatically
|
||||
# built this stage.
|
||||
# Please consider using a local mirror.
|
||||
|
||||
[gentoo]
|
||||
priority = 9999
|
||||
sync-uri = https://distfiles.gentoo.org/releases/amd64/binpackages/23.0/x86-64_musl_llvm
|
||||
location = /var/cache/binhost/gentoo
|
||||
verify-signature = true
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
# /etc/portage/env/codex-lowmem.conf
|
||||
# Alternative compiler configuration (Clang/LLVM) to mitigate low-memory compilation issues
|
||||
|
||||
# 1. Alternative compiler: Clang/LLVM toolchain
|
||||
CC="clang"
|
||||
CXX="clang++"
|
||||
CPP="clang-cpp"
|
||||
AR="llvm-ar"
|
||||
NM="llvm-nm"
|
||||
RANLIB="llvm-ranlib"
|
||||
|
||||
# 2. Parallel jobs reduction: limit make to 1 job to prevent RAM exhaustion
|
||||
MAKEOPTS="-j1"
|
||||
|
||||
# 3. Memory-efficient linker: use LLD and release unneeded memory pages
|
||||
LDFLAGS="${LDFLAGS} -fuse-ld=lld -Wl,--no-keep-memory"
|
||||
|
||||
# 4. Memory-efficient LTO: use ThinLTO instead of full monolithic LTO
|
||||
CFLAGS="${CFLAGS} -flto=thin"
|
||||
CXXFLAGS="${CXXFLAGS} -flto=thin"
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
# /etc/portage/env/compiler-clang.conf
|
||||
CC="clang"
|
||||
CXX="clang++"
|
||||
CPP="clang-cpp"
|
||||
AR="llvm-ar"
|
||||
NM="llvm-nm"
|
||||
RANLIB="llvm-ranlib"
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
# /etc/portage/env/codex-lowmem.conf
|
||||
# Alternative compiler configuration (Clang/LLVM) to mitigate low-memory compilation issues
|
||||
|
||||
# 1. Alternative compiler: Clang/LLVM toolchain
|
||||
CC="clang"
|
||||
CXX="clang++"
|
||||
CPP="clang-cpp"
|
||||
AR="llvm-ar"
|
||||
NM="llvm-nm"
|
||||
RANLIB="llvm-ranlib"
|
||||
|
||||
# 2. Parallel jobs reduction: limit make to 1 job to prevent RAM exhaustion
|
||||
MAKEOPTS="-j1"
|
||||
|
||||
# 3. Memory-efficient linker: use LLD and release unneeded memory pages
|
||||
LDFLAGS="${LDFLAGS} -fuse-ld=lld -Wl,--no-keep-memory"
|
||||
|
||||
# 4. Memory-efficient LTO: use ThinLTO instead of full monolithic LTO
|
||||
CFLAGS="${CFLAGS} -flto=thin"
|
||||
CXXFLAGS="${CXXFLAGS} -flto=thin"
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export RUSTFLAGS="${RUSTFLAGS} -C target-feature=-crt-static"
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
# /etc/portage/env/low-memory.conf
|
||||
MAKEOPTS="-j1"
|
||||
LDFLAGS="${LDFLAGS} -fuse-ld=lld -Wl,--no-keep-memory"
|
||||
CFLAGS="${CFLAGS} -flto=thin"
|
||||
CXXFLAGS="${CXXFLAGS} -flto=thin"
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
LDFLAGS="-Wl,-O1 -Wl,--as-needed"
|
||||
@@ -0,0 +1,34 @@
|
||||
# These settings were set by the catalyst build script that automatically
|
||||
# built this stage.
|
||||
# Please consult /usr/share/portage/config/make.conf.example for a more
|
||||
# detailed example.
|
||||
COMMON_FLAGS="-march=native -O3 -pipe"
|
||||
RUSTFLAGS=" -C target-cpu=native"
|
||||
LDFLAGS="${LDFLAGS} -fuse-ld=mold"
|
||||
CFLAGS="${COMMON_FLAGS}"
|
||||
CXXFLAGS="${COMMON_FLAGS}"
|
||||
FCFLAGS="${COMMON_FLAGS}"
|
||||
FFLAGS="${COMMON_FLAGS}"
|
||||
|
||||
USE="wayland -X hyprland dbus wifi bluetooth system-pipewire pulseaudio"
|
||||
|
||||
# WARNING: Changing your CHOST is not something that should be done lightly.
|
||||
# Please consult https://wiki.gentoo.org/wiki/Changing_the_CHOST_variable before changing.
|
||||
CHOST="x86_64-pc-linux-musl"
|
||||
|
||||
MAKEOPTS="-j4 -l5"
|
||||
|
||||
ACCEPT_LICENSE="*"
|
||||
|
||||
FEATURES="getbinpkg binpkg-request-signature"
|
||||
EMERGE_DEFAULT_OPTS="--jobs=4"
|
||||
|
||||
# NOTE: This stage was built with the bindist USE flag enabled
|
||||
|
||||
# This sets the language of build output to English.
|
||||
# Please keep this setting intact when reporting bugs.
|
||||
LC_MESSAGES=C.UTF-8
|
||||
|
||||
GENTOO_MIRRORS="https://ftp.agdsn.de/gentoo \
|
||||
https://mirror.alldaydev.com/gentoo"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
sys-process/bashtop ~amd64
|
||||
@@ -0,0 +1 @@
|
||||
*/*::cachy ~amd64
|
||||
@@ -0,0 +1,3 @@
|
||||
sys-libs/libucontext ~amd64
|
||||
sys-libs/gcompat ~amd64
|
||||
sys-libs/gcompat ** ~amd64
|
||||
@@ -0,0 +1 @@
|
||||
*/*::guru ~amd64
|
||||
@@ -0,0 +1 @@
|
||||
*/*::hypr ~amd64
|
||||
@@ -0,0 +1,3 @@
|
||||
*/*::librewolf ~amd64
|
||||
dev-libs/nss ~amd64
|
||||
www-client/librewolf -X
|
||||
@@ -0,0 +1 @@
|
||||
sys-boot/limine ~amd64
|
||||
@@ -0,0 +1 @@
|
||||
*/*::local ~amd64
|
||||
@@ -0,0 +1 @@
|
||||
www-client/luakit ~amd64
|
||||
@@ -0,0 +1,2 @@
|
||||
dev-libs/libdwarf ~amd64
|
||||
dev-cpp/cpptrace ~amd64
|
||||
@@ -0,0 +1 @@
|
||||
media-sound/sidra-bin ~amd64
|
||||
@@ -0,0 +1 @@
|
||||
dev-util/codex codex-lowmem.conf
|
||||
@@ -0,0 +1 @@
|
||||
www-client/firefox firefox.conf
|
||||
@@ -0,0 +1 @@
|
||||
sys-kernel/cachyos-kernel no-mold
|
||||
@@ -0,0 +1,2 @@
|
||||
sys-libs/gcompat
|
||||
sys-libs/libucontext
|
||||
@@ -0,0 +1 @@
|
||||
*/* CPU_FLAGS_X86: aes avx f16c mmx mmxext pclmul popcnt rdrand sse sse2 sse3 sse4_1 sse4_2 ssse3
|
||||
@@ -0,0 +1 @@
|
||||
*/* VIDEO_CARDS: -* intel i915
|
||||
@@ -0,0 +1,12 @@
|
||||
sys-boot/limine -bios -bios-cd -bios-pxe -uefi-aarch64 -uefi-cd -uefi-ia32 -uefi-loongarch64 -uefi-riscv64 uefi-x86-64
|
||||
# required by www-client/librewolf-156.0.1_p1::librewolf[system-libvpx]
|
||||
# required by librewolf (argument)
|
||||
>=media-libs/libvpx-1.16.0 postproc
|
||||
# required by x11-base/xwayland-24.1.13::gentoo
|
||||
# required by gui-wm/hyprland-0.56.2::hypr[X]
|
||||
# required by hyprland (argument)
|
||||
>=media-libs/libepoxy-1.5.10-r3 X
|
||||
# required by x11-base/xwayland-24.1.13::gentoo
|
||||
# required by gui-wm/hyprland-0.56.2::hypr[X]
|
||||
# required by hyprland (argument)
|
||||
>=media-libs/mesa-26.1.8 X
|
||||
@@ -0,0 +1 @@
|
||||
sys-boot/limine -bios -bios-cd -bios-pxe -uefi-aarch64 -uefi-cd -uefi-ia32 -uefi-loongarch64 -uefi-riscv64 uefi-x86-64
|
||||
@@ -0,0 +1,41 @@
|
||||
# required by dev-qt/qttools-6.11.2::gentoo[widgets]
|
||||
# required by dev-qt/qttranslations-6.11.2::gentoo
|
||||
>=dev-qt/qtbase-6.11.2-r2 vulkan opengl wayland
|
||||
# required by gui-apps/quickshell-0.3.1::guru[crash-handler]
|
||||
# required by quickshell (argument)
|
||||
#>=dev-cpp/cpptrace-1.0.4-r1 -unwind llvm
|
||||
# required by net-misc/networkmanager-1.56.1::gentoo[-iwd,wifi]
|
||||
# required by gui-apps/quickshell-0.3.1::guru[networkmanager]
|
||||
# required by quickshell (argument)
|
||||
>=net-wireless/wpa_supplicant-2.11-r4 dbus
|
||||
# required by media-libs/mesa-26.1.8::gentoo
|
||||
# required by gui-libs/aquamarine-0.15.1::hypr
|
||||
# required by gui-libs/hyprtoolkit-0.6.0::hypr
|
||||
# required by gui-libs/hyprland-guiutils-0.2.2::hypr
|
||||
# required by gui-wm/hyprland-0.56.2::hypr[guiutils]
|
||||
# required by hyprland (argument)
|
||||
>=media-libs/libglvnd-1.7.0 X
|
||||
# required by gui-apps/quickshell-0.3.1::guru
|
||||
# required by quickshell (argument)
|
||||
>=dev-qt/qtdeclarative-6.11.2-r1 opengl vulkan wayland
|
||||
# required by dev-qt/qtbase-6.11.2-r2::gentoo[gui]
|
||||
# required by dev-qt/qttools-6.11.2::gentoo[assistant]
|
||||
# required by dev-qt/qttranslations-6.11.2::gentoo
|
||||
>=x11-libs/libxkbcommon-1.13.2 X
|
||||
# required by dev-qt/qttranslations-6.11.2::gentoo
|
||||
# required by dev-qt/qtbase-6.11.2-r2::gentoo[nls]
|
||||
# required by dev-qt/qtsvg-6.11.2::gentoo
|
||||
# required by gui-apps/quickshell-0.3.1::guru
|
||||
# required by quickshell (argument)
|
||||
>=dev-qt/qttools-6.11.2 opengl
|
||||
# required by x11-libs/pango-1.57.1::gentoo
|
||||
# required by gui-libs/aquamarine-0.15.1::hypr
|
||||
# required by gui-libs/hyprtoolkit-0.6.0::hypr
|
||||
# required by gui-libs/hyprland-guiutils-0.2.2::hypr
|
||||
# required by gui-wm/hyprland-0.56.2::hypr[guiutils]
|
||||
# required by hyprland (argument)
|
||||
>=media-libs/freetype-2.14.3 harfbuzz
|
||||
|
||||
gui-apps/quickshell -i3 hyprland -crash-handler
|
||||
gui-wm/hyprland -guiutils X hyprpm
|
||||
net-misc/networkmanager bluetooth iwd
|
||||
@@ -0,0 +1 @@
|
||||
app-admin/doas persist
|
||||
@@ -0,0 +1,2 @@
|
||||
app-containers/docker btrfs
|
||||
app-containers/containerd btrfs
|
||||
Whitespace-only changes.
@@ -0,0 +1,3 @@
|
||||
# required by dev-util/flatpak-builder-1.4.8::gentoo
|
||||
# required by flatpak-builder (argument)
|
||||
>=dev-libs/appstream-1.0.6 compose
|
||||
@@ -0,0 +1 @@
|
||||
sys-libs/gcompat libucontext obstack
|
||||
@@ -0,0 +1 @@
|
||||
sys-kernel/installkernel dracut
|
||||
@@ -0,0 +1,7 @@
|
||||
sys-boot/limine -bios -bios-cd -bios-pxe -uefi-aarch64 -uefi-cd -uefi-ia32 -uefi-loongarch64 -uefi-riscv64 uefi-x86-64
|
||||
# required by sys-kernel/cachyos-kernel-7.2.8::cachy[autofdo]
|
||||
# required by virtual/dist-kernel-7.2.8_p0::cachy
|
||||
# required by sys-kernel/gentoo-kernel-bin-6.18.50::gentoo
|
||||
# required by @selected
|
||||
# required by @world (argument)
|
||||
>=dev-util/perf-6.19.12 libpfm
|
||||
@@ -0,0 +1 @@
|
||||
media-fonts/maple-mono nerd
|
||||
@@ -0,0 +1 @@
|
||||
sys-apps/s6-linux-init -sysv-utils
|
||||
@@ -0,0 +1 @@
|
||||
sys-auth/seatd builtin server
|
||||
@@ -0,0 +1,2 @@
|
||||
media-video/pipewire sound-server pipewire-alsa system-service
|
||||
media-video/wireplumber system-service
|
||||
@@ -0,0 +1 @@
|
||||
net-vpn/wireguard-tools wg-quick
|
||||
@@ -0,0 +1 @@
|
||||
sys-kernel/cachyos-kernel savedconfig
|
||||
@@ -0,0 +1,6 @@
|
||||
[cachy]
|
||||
location = /var/db/repos/cachy
|
||||
sync-type = git
|
||||
sync-uri = https://github.com/Szowisz/CachyOS-kernels.git
|
||||
auto-sync = yes
|
||||
force-name = cachy
|
||||
@@ -0,0 +1,12 @@
|
||||
# created by eselect-repo
|
||||
|
||||
|
||||
[librewolf]
|
||||
location = /var/db/repos/librewolf
|
||||
sync-type = git
|
||||
sync-uri = https://codeberg.org/librewolf/gentoo.git
|
||||
|
||||
[ruizhi-overlay]
|
||||
location = /var/db/repos/ruizhi-overlay
|
||||
sync-type = git
|
||||
sync-uri = https://github.com/ruizhi-lab/gentoo-overlay.git
|
||||
@@ -0,0 +1,21 @@
|
||||
[DEFAULT]
|
||||
main-repo = gentoo
|
||||
|
||||
[gentoo]
|
||||
location = /var/db/repos/gentoo
|
||||
sync-type = rsync
|
||||
sync-uri = rsync://rsync.gentoo.org/gentoo-portage/
|
||||
auto-sync = yes
|
||||
sync-rsync-verify-jobs = 1
|
||||
sync-rsync-verify-metamanifest = yes
|
||||
sync-rsync-verify-max-age = 3
|
||||
sync-openpgp-key-package = sec-keys/openpgp-keys-gentoo-release
|
||||
sync-openpgp-key-path = /usr/share/openpgp-keys/gentoo-release.asc
|
||||
sync-openpgp-keyserver = hkps://keys.gentoo.org
|
||||
sync-openpgp-key-refresh-retry-count = 40
|
||||
sync-openpgp-key-refresh-retry-overall-timeout = 1200
|
||||
sync-openpgp-key-refresh-retry-delay-exp-base = 2
|
||||
sync-openpgp-key-refresh-retry-delay-max = 60
|
||||
sync-openpgp-key-refresh-retry-delay-mult = 4
|
||||
sync-webrsync-verify-signature = yes
|
||||
sync-git-verify-commit-signature = true
|
||||
@@ -0,0 +1,5 @@
|
||||
[guru]
|
||||
location = /var/db/repos/guru
|
||||
sync-type = git
|
||||
sync-uri = https://github.com/gentoo-mirror/guru.git
|
||||
auto-sync = yes
|
||||
@@ -0,0 +1,6 @@
|
||||
[hypr]
|
||||
location = /var/db/repos/hypr
|
||||
sync-type = git
|
||||
sync-uri = https://codeberg.org/hyproverlay/hyproverlay.git
|
||||
auto-sync = yes
|
||||
force-name = hypr
|
||||
@@ -0,0 +1,5 @@
|
||||
[local]
|
||||
location = /var/db/repos/local
|
||||
masters = gentoo
|
||||
auto-sync = yes
|
||||
priority = 50
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Binary file not shown.
|
After Width: | Height: | Size: 220 KiB |
Executable
+108
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly snapshot_mount='/.snapshots'
|
||||
readonly snapshot_dir='/.snapshots/portage'
|
||||
readonly keep_count=32
|
||||
readonly minimum_free_bytes=2147483648
|
||||
|
||||
warn() {
|
||||
printf 'portage-btrfs-snapshot: %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
fail() {
|
||||
warn "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ ${EUID} -eq 0 ]] || fail 'must run as root from a Portage package phase.'
|
||||
[[ $# -eq 5 ]] || fail 'usage: portage-btrfs-snapshot {install|remove} CATEGORY PF ROOT EROOT'
|
||||
|
||||
operation=$1
|
||||
category=$2
|
||||
package_pf=$3
|
||||
target_root=$4
|
||||
effective_root=$5
|
||||
case "$operation" in
|
||||
install|remove) ;;
|
||||
*) fail 'unsupported package operation.' ;;
|
||||
esac
|
||||
|
||||
# Restrict this hook to merges for the live root. Do not snapshot a prefix or
|
||||
# alternate ROOT that happens to share the same Portage configuration.
|
||||
[[ "$target_root" == / && "$effective_root" == / ]] || exit 0
|
||||
|
||||
for tool in awk btrfs date df find findmnt head install mountpoint python3 sed sort tr; do
|
||||
command -v "$tool" >/dev/null 2>&1 || fail "required command not found: $tool"
|
||||
done
|
||||
|
||||
[[ $(findmnt -n -o FSTYPE --target /) == btrfs ]] || fail 'the active root is not Btrfs.'
|
||||
mountpoint -q -- "$snapshot_mount" || fail "$snapshot_mount is not a mounted filesystem."
|
||||
[[ $(findmnt -n -o FSTYPE --mountpoint "$snapshot_mount") == btrfs ]] || \
|
||||
fail "$snapshot_mount is not a Btrfs mountpoint."
|
||||
|
||||
root_uuid=$(btrfs filesystem show / 2>/dev/null | sed -n 's/.*uuid: \([^[:space:]]*\).*/\1/p' | head -n 1)
|
||||
snap_uuid=$(btrfs filesystem show "$snapshot_mount" 2>/dev/null | sed -n 's/.*uuid: \([^[:space:]]*\).*/\1/p' | head -n 1)
|
||||
[[ -n "$root_uuid" && "$root_uuid" == "$snap_uuid" ]] || \
|
||||
fail 'root and snapshot destination are not on the same Btrfs filesystem.'
|
||||
|
||||
root_id=$(btrfs inspect-internal rootid / 2>/dev/null) || fail 'cannot identify the root subvolume.'
|
||||
dest_id=$(btrfs inspect-internal rootid "$snapshot_mount" 2>/dev/null) || \
|
||||
fail 'cannot identify the snapshot destination subvolume.'
|
||||
[[ "$root_id" != "$dest_id" ]] || fail 'snapshot destination resolves to the live root subvolume.'
|
||||
|
||||
space_info=$(df -P -B1 "$snapshot_mount" | awk 'NR == 2 { print $2, $4 }') || \
|
||||
fail 'cannot determine free space on the Btrfs filesystem.'
|
||||
read -r _total_bytes available_bytes <<< "$space_info"
|
||||
[[ ${available_bytes:-} =~ ^[0-9]+$ ]] || fail 'cannot determine free space on the Btrfs filesystem.'
|
||||
(( available_bytes >= minimum_free_bytes )) || \
|
||||
fail 'less than 2 GiB is available on Btrfs; refusing to create another snapshot.'
|
||||
|
||||
install -d -o root -g root -m 0700 -- "$snapshot_dir"
|
||||
|
||||
safe_package=$(printf '%s_%s' "$category" "$package_pf" | LC_ALL=C tr -c 'A-Za-z0-9._+-' '_')
|
||||
timestamp=$(date -u +%Y%m%dT%H%M%S%N)
|
||||
snapshot_path="${snapshot_dir}/portage-${timestamp}-${operation}-${safe_package}"
|
||||
|
||||
btrfs subvolume snapshot -r / "$snapshot_path" >/dev/null || \
|
||||
fail "snapshot creation failed for ${category}/${package_pf} (${operation})."
|
||||
printf 'Created read-only Btrfs snapshot: %s\n' "$snapshot_path"
|
||||
|
||||
menu_manager='/usr/local/libexec/portage-limine-menu'
|
||||
if [[ -x "$menu_manager" ]]; then
|
||||
if [[ "$operation" == install && "$category" == sys-kernel ]]; then
|
||||
# Kernel package post-install runs after its image has been installed.
|
||||
# Sync kernel groups and attach all compatible snapshots, including
|
||||
# the snapshot just created above.
|
||||
"$menu_manager" sync-kernels || \
|
||||
warn "Limine kernel/snapshot menu reconciliation failed after ${category}/${package_pf}."
|
||||
else
|
||||
"$menu_manager" add-snapshot "$snapshot_path" || \
|
||||
warn "Limine recovery entry creation failed for $snapshot_path."
|
||||
fi
|
||||
else
|
||||
warn 'Limine menu manager is missing; the read-only snapshot was created without a boot entry.'
|
||||
fi
|
||||
|
||||
# Names begin with a sortable UTC timestamp. Only remove our own snapshots,
|
||||
# after confirming each candidate is an actual Btrfs subvolume.
|
||||
mapfile -t snapshots < <(find "$snapshot_dir" -mindepth 1 -maxdepth 1 -type d \
|
||||
-name 'portage-*' -printf '%f\n' | LC_ALL=C sort)
|
||||
excess=$((${#snapshots[@]} - keep_count))
|
||||
if (( excess > 0 )); then
|
||||
for ((i = 0; i < excess; i++)); do
|
||||
old_path="${snapshot_dir}/${snapshots[i]}"
|
||||
if btrfs subvolume show "$old_path" >/dev/null 2>&1; then
|
||||
if [[ -x "$menu_manager" ]]; then
|
||||
"$menu_manager" remove-snapshot "$old_path" || {
|
||||
warn "retention kept snapshot because its Limine entry/clone could not be removed: $old_path"
|
||||
continue
|
||||
}
|
||||
fi
|
||||
btrfs subvolume delete "$old_path" >/dev/null || \
|
||||
warn "could not prune old snapshot: $old_path"
|
||||
else
|
||||
warn "retention skipped a non-subvolume entry: $old_path"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
Executable
+582
@@ -0,0 +1,582 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Maintain Limine entries for Gentoo kernel installs and Portage snapshots."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
CONFIG = Path("/boot/limine.conf")
|
||||
BOOT = Path("/boot")
|
||||
MODULE_ROOTS = (Path("/usr/lib/modules"), Path("/lib/modules"))
|
||||
SNAPSHOT_ROOT = Path("/.snapshots/portage")
|
||||
CLONE_ROOT = Path("/.snapshots/bootable")
|
||||
MARKER_START = "# BEGIN PORTAGE SNAPSHOT "
|
||||
MARKER_END = "# END PORTAGE SNAPSHOT "
|
||||
ENTRY_RE = re.compile(r"(?m)^(/+)([^\r\n]*)\r?\n")
|
||||
|
||||
|
||||
class MenuError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Entry:
|
||||
depth: int
|
||||
title: str
|
||||
start: int
|
||||
end: int
|
||||
block: str
|
||||
|
||||
|
||||
def run(argv: list[str], *, capture: bool = True) -> str:
|
||||
result = subprocess.run(
|
||||
argv,
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE if capture else None,
|
||||
stderr=subprocess.PIPE if capture else None,
|
||||
)
|
||||
if result.returncode:
|
||||
details = (result.stderr or result.stdout or "").strip()
|
||||
raise MenuError(f"command failed ({result.returncode}): {' '.join(argv)}\n{details}")
|
||||
return result.stdout or ""
|
||||
|
||||
|
||||
def split_entries(text: str) -> tuple[str, list[Entry]]:
|
||||
matches = list(ENTRY_RE.finditer(text))
|
||||
if not matches:
|
||||
raise MenuError("Limine configuration has no menu entries")
|
||||
entries: list[Entry] = []
|
||||
for index, match in enumerate(matches):
|
||||
end = matches[index + 1].start() if index + 1 < len(matches) else len(text)
|
||||
entries.append(
|
||||
Entry(
|
||||
depth=len(match.group(1)),
|
||||
title=match.group(2),
|
||||
start=match.start(),
|
||||
end=end,
|
||||
block=text[match.start():end],
|
||||
)
|
||||
)
|
||||
return text[:matches[0].start()], entries
|
||||
|
||||
|
||||
def value(entry: Entry, key: str) -> str | None:
|
||||
match = re.search(rf"(?mi)^\s*{re.escape(key)}\s*:\s*(.*?)\s*$", entry.block)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def all_entries(text: str) -> list[Entry]:
|
||||
return split_entries(text)[1]
|
||||
|
||||
|
||||
def children_of(entries: list[Entry], parent: Entry) -> list[Entry]:
|
||||
try:
|
||||
index = entries.index(parent)
|
||||
except ValueError:
|
||||
return []
|
||||
children: list[Entry] = []
|
||||
for entry in entries[index + 1:]:
|
||||
if entry.depth <= parent.depth:
|
||||
break
|
||||
children.append(entry)
|
||||
return children
|
||||
|
||||
|
||||
def kernel_release(kernel_path: str | None) -> str | None:
|
||||
if not kernel_path:
|
||||
return None
|
||||
path = kernel_path.rsplit(":", 1)[-1]
|
||||
name = Path(path).name
|
||||
if not name.startswith("vmlinuz-") or name.endswith(".old"):
|
||||
return None
|
||||
return name.removeprefix("vmlinuz-")
|
||||
|
||||
|
||||
def root_subvol(cmdline: str | None) -> str | None:
|
||||
if not cmdline:
|
||||
return None
|
||||
match = re.search(r"(?:^|\s)rootflags=([^\s]+)", cmdline)
|
||||
if not match:
|
||||
return None
|
||||
for option in match.group(1).split(","):
|
||||
if option.startswith("subvol="):
|
||||
return option.removeprefix("subvol=")
|
||||
return None
|
||||
|
||||
|
||||
def read_config() -> str:
|
||||
try:
|
||||
return CONFIG.read_text(encoding="utf-8")
|
||||
except OSError as error:
|
||||
raise MenuError(f"cannot read {CONFIG}: {error}") from error
|
||||
|
||||
|
||||
def backup_config() -> None:
|
||||
backup = Path(str(CONFIG) + ".pre-portage-recovery-menu")
|
||||
if not backup.exists():
|
||||
shutil.copy2(CONFIG, backup)
|
||||
print(f"Saved original Limine config: {backup}")
|
||||
|
||||
|
||||
def write_config(text: str) -> None:
|
||||
if not text.endswith("\n"):
|
||||
text += "\n"
|
||||
validate_config(text)
|
||||
backup_config()
|
||||
fd, temporary = tempfile.mkstemp(prefix="limine.conf.portage.", dir=CONFIG.parent)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as stream:
|
||||
stream.write(text)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(temporary, CONFIG)
|
||||
except BaseException:
|
||||
try:
|
||||
os.unlink(temporary)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
|
||||
|
||||
def validate_config(text: str) -> None:
|
||||
prefix, entries = split_entries(text)
|
||||
if not re.search(r"(?mi)^\s*timeout\s*:\s*", prefix):
|
||||
raise MenuError("Limine config validation failed: global timeout is missing")
|
||||
roots = [entry for entry in entries if entry.depth == 1]
|
||||
if not roots:
|
||||
raise MenuError("Limine config validation failed: no top-level entries")
|
||||
current_path = None
|
||||
default = re.search(r"(?mi)^\s*default_entry\s*:\s*(.*?)\s*$", prefix)
|
||||
if default:
|
||||
current_path = default.group(1).split("#", 1)[0]
|
||||
if any(children_of(entries, root) for root in roots) and not current_path:
|
||||
raise MenuError("Limine config validation failed: nested kernel entries require an explicit default_entry path")
|
||||
if current_path:
|
||||
if current_path.isdecimal():
|
||||
raise MenuError("Limine config validation failed: use a leaf entry path for default_entry when kernel directories exist")
|
||||
parts = current_path.split("/")
|
||||
parent_title = parts[0]
|
||||
child_title = parts[1] if len(parts) > 1 else None
|
||||
parent = next((entry for entry in roots if entry.title == parent_title), None)
|
||||
if parent is None:
|
||||
raise MenuError(f"Limine config validation failed: default parent is missing: {parent_title}")
|
||||
if child_title and not any(
|
||||
entry.depth == 2 and entry.title == child_title
|
||||
for entry in children_of(entries, parent)
|
||||
):
|
||||
raise MenuError(f"Limine config validation failed: default subentry is missing: {current_path}")
|
||||
if child_title is None and children_of(entries, parent):
|
||||
raise MenuError(f"Limine config validation failed: default_entry points to a directory: {current_path}")
|
||||
for entry in entries:
|
||||
if entry.depth == 2 and entry.title.startswith("Portage "):
|
||||
for key in ("protocol", "kernel_path", "module_path", "cmdline"):
|
||||
if value(entry, key) is None:
|
||||
raise MenuError(f"Limine config validation failed: generated entry {entry.title!r} lacks {key}")
|
||||
|
||||
|
||||
def migrate() -> None:
|
||||
text = read_config()
|
||||
prefix, entries = split_entries(text)
|
||||
if any(entry.depth > 1 for entry in entries):
|
||||
validate_config(text)
|
||||
if kernel_groups(text):
|
||||
print("Limine kernel groups are already configured.")
|
||||
return
|
||||
raise MenuError("Limine config has nested entries but no managed kernel groups; refusing migration")
|
||||
|
||||
kernels: list[Entry] = []
|
||||
recovery: list[Entry] = []
|
||||
other: list[Entry] = []
|
||||
for entry in entries:
|
||||
if value(entry, "protocol") != "linux" or not value(entry, "kernel_path"):
|
||||
other.append(entry)
|
||||
continue
|
||||
root = root_subvol(value(entry, "cmdline"))
|
||||
if root == "@":
|
||||
kernels.append(entry)
|
||||
elif root and root.startswith("@snapshots/bootable/"):
|
||||
recovery.append(entry)
|
||||
else:
|
||||
other.append(entry)
|
||||
|
||||
dist = next((entry for entry in kernels if "Dist Kernel" in entry.title), None)
|
||||
if dist is None:
|
||||
raise MenuError("expected Gentoo Dist Kernel entry was not found; refusing to change the boot menu")
|
||||
if not any("CachyOS Kernel" in entry.title for entry in kernels):
|
||||
raise MenuError("expected CachyOS Kernel entry was not found; refusing to change the boot menu")
|
||||
|
||||
groups: dict[str, list[str]] = {}
|
||||
releases: dict[str, str] = {}
|
||||
for entry in kernels:
|
||||
title = entry.title
|
||||
release = kernel_release(value(entry, "kernel_path"))
|
||||
if not release:
|
||||
raise MenuError(f"cannot derive kernel release for {title}")
|
||||
if title in groups:
|
||||
raise MenuError(f"duplicate kernel group title: {title}")
|
||||
body = entry.block[entry.block.find("\n") + 1:]
|
||||
groups[title] = [f"/{title}\n", f"//Current boot\n{body}"]
|
||||
releases[release] = title
|
||||
|
||||
for entry in recovery:
|
||||
release = kernel_release(value(entry, "kernel_path"))
|
||||
title = releases.get(release or "")
|
||||
if title is None:
|
||||
other.append(entry)
|
||||
continue
|
||||
body = entry.block[entry.block.find("\n") + 1:]
|
||||
child_title = entry.title
|
||||
match = re.match(r"Gentoo Linux \((.*)\)", child_title)
|
||||
if match:
|
||||
child_title = match.group(1)
|
||||
groups[title].append(f"//{child_title}\n{body}")
|
||||
|
||||
result_prefix = prefix
|
||||
if re.search(r"(?mi)^\s*default_entry\s*:", result_prefix):
|
||||
result_prefix = re.sub(
|
||||
r"(?mi)^\s*default_entry\s*:\s*[^\r\n]*",
|
||||
"default_entry: Gentoo Linux (Dist Kernel)/Current boot",
|
||||
result_prefix,
|
||||
count=1,
|
||||
)
|
||||
else:
|
||||
result_prefix = result_prefix.rstrip() + "\ndefault_entry: Gentoo Linux (Dist Kernel)/Current boot\n\n"
|
||||
|
||||
pieces = [result_prefix.rstrip() + "\n\n"]
|
||||
written: set[str] = set()
|
||||
for entry in entries:
|
||||
if entry.title in groups and entry.title not in written:
|
||||
pieces.extend(groups[entry.title])
|
||||
pieces.append("\n")
|
||||
written.add(entry.title)
|
||||
elif entry in other:
|
||||
pieces.append(entry.block)
|
||||
for title, group in groups.items():
|
||||
if title not in written:
|
||||
pieces.extend(group)
|
||||
pieces.append("\n")
|
||||
write_config("".join(pieces))
|
||||
print("Migrated existing kernel entries into Limine kernel groups; Dist Kernel remains the default.")
|
||||
|
||||
|
||||
def kernel_groups(text: str) -> list[tuple[Entry, Entry, str]]:
|
||||
_, entries = split_entries(text)
|
||||
groups: list[tuple[Entry, Entry, str]] = []
|
||||
for parent in entries:
|
||||
if parent.depth != 1:
|
||||
continue
|
||||
for child in children_of(entries, parent):
|
||||
if child.depth != 2 or child.title != "Current boot":
|
||||
continue
|
||||
release = kernel_release(value(child, "kernel_path"))
|
||||
if release:
|
||||
groups.append((parent, child, release))
|
||||
return groups
|
||||
|
||||
|
||||
def option_block(title: str, kernel: str, module_paths: list[str], cmdline: str, depth: int = 2) -> str:
|
||||
slashes = "/" * depth
|
||||
content = (
|
||||
f"{slashes}{title}\n"
|
||||
" protocol: linux\n"
|
||||
f" kernel_path: boot():/{kernel}\n"
|
||||
)
|
||||
content += "".join(f" module_path: boot():/{module}\n" for module in module_paths)
|
||||
return content + f" cmdline: {cmdline}\n"
|
||||
|
||||
|
||||
def module_releases(root: Path) -> set[str]:
|
||||
releases: set[str] = set()
|
||||
for base in (root / "usr/lib/modules", root / "lib/modules"):
|
||||
try:
|
||||
releases.update(path.name for path in base.iterdir() if path.is_dir())
|
||||
except OSError:
|
||||
pass
|
||||
return releases
|
||||
|
||||
|
||||
def compatible_groups(root: Path, text: str) -> list[tuple[Entry, Entry, str]]:
|
||||
releases = module_releases(root)
|
||||
compatible: list[tuple[Entry, Entry, str]] = []
|
||||
for group, current, release in kernel_groups(text):
|
||||
kernel_path = value(current, "kernel_path")
|
||||
module_paths = re.findall(r"(?mi)^\s*module_path\s*:\s*(.*?)\s*$", current.block)
|
||||
if release not in releases or not kernel_path or not module_paths:
|
||||
continue
|
||||
if not boot_asset_exists(kernel_path) or not all(boot_asset_exists(path) for path in module_paths):
|
||||
continue
|
||||
compatible.append((group, current, release))
|
||||
return compatible
|
||||
|
||||
|
||||
def boot_asset_exists(limine_path: str) -> bool:
|
||||
if not limine_path.startswith("boot():/"):
|
||||
return False
|
||||
return (BOOT / limine_path.removeprefix("boot():/")).is_file()
|
||||
|
||||
|
||||
def legacy_clone_for(source: Path, group: Entry, text: str) -> tuple[Path, str] | None:
|
||||
"""Reuse a pre-manager recovery clone when it clearly matches this package."""
|
||||
lowered = source.name.lower()
|
||||
token = "fastfetch" if "install-app-misc_fastfetch" in lowered else None
|
||||
if token is None:
|
||||
return None
|
||||
_, entries = split_entries(text)
|
||||
fresh_group = next((entry for entry in entries if entry.depth == 1 and entry.title == group.title), None)
|
||||
if fresh_group is None:
|
||||
return None
|
||||
for child in children_of(entries, fresh_group):
|
||||
if child.depth != 2 or child.title == "Current boot" or child.title.startswith("Portage "):
|
||||
continue
|
||||
if token not in child.title.lower():
|
||||
continue
|
||||
subvol = root_subvol(value(child, "cmdline"))
|
||||
if not subvol or not subvol.startswith("@snapshots/bootable/"):
|
||||
continue
|
||||
clone = CLONE_ROOT / subvol.removeprefix("@snapshots/bootable/")
|
||||
if not clone.is_dir():
|
||||
continue
|
||||
try:
|
||||
if run(["btrfs", "property", "get", "-ts", str(clone), "ro"]).strip() == "ro=false":
|
||||
return clone, subvol
|
||||
except MenuError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def marker_block(snapshot_name: str, release: str, clone_subvol: str, group_title: str, child_title: str) -> str:
|
||||
_, entries = split_entries(read_config())
|
||||
group = next((entry for entry in entries if entry.depth == 1 and entry.title == group_title), None)
|
||||
if group is None:
|
||||
raise MenuError(f"kernel group disappeared during menu update: {group_title}")
|
||||
current = next((entry for entry in children_of(entries, group) if entry.depth == 2 and entry.title == "Current boot"), None)
|
||||
if current is None:
|
||||
raise MenuError(f"kernel group has no Current boot subentry: {group.title}")
|
||||
cmdline = value(current, "cmdline")
|
||||
if not cmdline or not re.search(r"(?:^|\s)rootflags=[^\s]+", cmdline):
|
||||
raise MenuError(f"kernel group has no rootflags in its command line: {group.title}")
|
||||
cmdline = re.sub(r"((?:^|\s)rootflags=)[^\s]+", rf"\g<1>subvol={clone_subvol}", cmdline, count=1)
|
||||
modules = re.findall(r"(?mi)^\s*module_path\s*:\s*(.*?)\s*$", current.block)
|
||||
module_names = [Path(module.split(":", 1)[-1]).name for module in modules]
|
||||
body = option_block(child_title, Path(value(current, "kernel_path").split(":", 1)[-1]).name,
|
||||
module_names, cmdline)
|
||||
marker_id = f"{snapshot_name} {release}"
|
||||
return f"{MARKER_START}{marker_id}\n{body}{MARKER_END}{marker_id}\n"
|
||||
|
||||
|
||||
def clone_name(snapshot_name: str) -> str:
|
||||
digest = hashlib.sha256(snapshot_name.encode("utf-8")).hexdigest()[:20]
|
||||
return f"portage-{digest}"
|
||||
|
||||
|
||||
def subvol_path(path: Path) -> str:
|
||||
listing = run(["btrfs", "subvolume", "list", "-a", "/"])
|
||||
wanted = path.name
|
||||
found: list[str] = []
|
||||
for line in listing.splitlines():
|
||||
if " path " not in line:
|
||||
continue
|
||||
subvol = line.split(" path ", 1)[1].strip()
|
||||
if subvol.startswith("<FS_TREE>/"):
|
||||
subvol = subvol[len("<FS_TREE>/"):]
|
||||
if subvol == wanted or subvol.endswith("/" + wanted):
|
||||
found.append(subvol)
|
||||
if len(found) != 1:
|
||||
raise MenuError(f"cannot uniquely resolve Btrfs top-level path for {path}")
|
||||
return found[0]
|
||||
|
||||
|
||||
def writable_clone(source: Path) -> tuple[Path, str]:
|
||||
if not source.is_dir():
|
||||
raise MenuError(f"snapshot source is missing: {source}")
|
||||
ro = run(["btrfs", "property", "get", "-ts", str(source), "ro"]).strip()
|
||||
if ro != "ro=true":
|
||||
raise MenuError(f"refusing to clone a non-read-only source snapshot: {source} ({ro})")
|
||||
CLONE_ROOT.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
target = CLONE_ROOT / clone_name(source.name)
|
||||
if not target.exists():
|
||||
available = shutil.disk_usage("/.snapshots").free
|
||||
if available < 2 * 1024**3:
|
||||
raise MenuError("less than 2 GiB is free; refusing to create a writable snapshot clone")
|
||||
run(["btrfs", "subvolume", "snapshot", str(source), str(target)], capture=False)
|
||||
print(f"Created writable recovery clone: {target}")
|
||||
clone_ro = run(["btrfs", "property", "get", "-ts", str(target), "ro"]).strip()
|
||||
if clone_ro != "ro=false":
|
||||
raise MenuError(f"recovery clone is not writable: {target} ({clone_ro})")
|
||||
return target, subvol_path(target)
|
||||
|
||||
|
||||
def insert_child(text: str, parent_title: str, child: str) -> str:
|
||||
prefix, entries = split_entries(text)
|
||||
parent_index = next((i for i, entry in enumerate(entries) if entry.depth == 1 and entry.title == parent_title), None)
|
||||
if parent_index is None:
|
||||
raise MenuError(f"kernel group disappeared during menu update: {parent_title}")
|
||||
insert_at = len(text)
|
||||
for entry in entries[parent_index + 1:]:
|
||||
if entry.depth == 1:
|
||||
insert_at = entry.start
|
||||
break
|
||||
before = text[:insert_at]
|
||||
if before and not before.endswith("\n\n"):
|
||||
before = before.rstrip("\n") + "\n\n"
|
||||
after = text[insert_at:]
|
||||
if after and not child.endswith("\n\n"):
|
||||
child += "\n"
|
||||
return before + child + after
|
||||
|
||||
|
||||
def add_snapshot(source: Path) -> bool:
|
||||
name = source.name
|
||||
text = read_config()
|
||||
compatible = compatible_groups(source, text)
|
||||
if not compatible:
|
||||
print(f"Snapshot has no matching kernel/initramfs menu group; keeping snapshot only: {source}", file=sys.stderr)
|
||||
return False
|
||||
adopted: tuple[Path, str] | None = None
|
||||
for group, _current, _release in compatible:
|
||||
adopted = legacy_clone_for(source, group, text)
|
||||
if adopted:
|
||||
break
|
||||
clone, subvol = adopted if adopted else writable_clone(source)
|
||||
changed = False
|
||||
for group, _current, _release in compatible:
|
||||
marker = f"{MARKER_START}{name} {_release}\n"
|
||||
if marker in text:
|
||||
continue
|
||||
if legacy_clone_for(source, group, text):
|
||||
print(f"Existing recovery entry under {group.title} already covers {source.name}")
|
||||
continue
|
||||
title = f"Portage {name.removeprefix('portage-')}"
|
||||
child = marker_block(name, _release, subvol, group.title, title)
|
||||
text = insert_child(text, group.title, child)
|
||||
changed = True
|
||||
if changed:
|
||||
write_config(text)
|
||||
print(f"Added Limine recovery option(s) for {source.name} using {clone.name}")
|
||||
return changed
|
||||
|
||||
|
||||
def remove_snapshot(source: Path) -> None:
|
||||
name = source.name
|
||||
text = read_config()
|
||||
pattern = re.compile(
|
||||
rf"(?ms)^# BEGIN PORTAGE SNAPSHOT {re.escape(name)} [^\n]+\n.*?^# END PORTAGE SNAPSHOT {re.escape(name)} [^\n]+\n?"
|
||||
)
|
||||
new_text, count = pattern.subn("", text)
|
||||
if count:
|
||||
write_config(new_text)
|
||||
print(f"Removed {count} Limine recovery option(s) for expired snapshot {name}")
|
||||
clone = CLONE_ROOT / clone_name(name)
|
||||
if clone.exists():
|
||||
show = run(["btrfs", "subvolume", "show", str(clone)])
|
||||
if "Subvolume ID:" not in show:
|
||||
raise MenuError(f"refusing to delete unverified clone path: {clone}")
|
||||
run(["btrfs", "subvolume", "delete", str(clone)], capture=False)
|
||||
print(f"Deleted expired writable recovery clone: {clone}")
|
||||
|
||||
|
||||
def ensure_kernel_groups() -> int:
|
||||
text = read_config()
|
||||
groups = kernel_groups(text)
|
||||
releases = module_releases(Path("/"))
|
||||
created = 0
|
||||
template = next((entry for _group, entry, _release in groups if value(entry, "cmdline")), None)
|
||||
if template is None:
|
||||
raise MenuError("cannot add kernel groups without an existing Current boot command line")
|
||||
template_cmdline = value(template, "cmdline") or ""
|
||||
for release in sorted(releases):
|
||||
kernel = BOOT / f"vmlinuz-{release}"
|
||||
initramfs = BOOT / f"initramfs-{release}.img"
|
||||
if not kernel.is_file():
|
||||
continue
|
||||
if not initramfs.is_file():
|
||||
print(f"Kernel {release} has no matching initramfs at {initramfs}; no Limine entry was created.", file=sys.stderr)
|
||||
continue
|
||||
text = read_config()
|
||||
groups = kernel_groups(text)
|
||||
if any(existing == release for _group, _entry, existing in groups):
|
||||
continue
|
||||
title = f"Gentoo Linux (Kernel {release})"
|
||||
if any(entry.depth == 1 and entry.title == title for entry in all_entries(text)):
|
||||
continue
|
||||
cmdline = template_cmdline
|
||||
if not re.search(r"(?:^|\s)rootflags=", cmdline):
|
||||
raise MenuError("existing kernel template has no rootflags; refusing to invent a root command line")
|
||||
body = option_block("Current boot", kernel.name, [initramfs.name], cmdline)
|
||||
text = text.rstrip() + f"\n\n/{title}\n{body}\n"
|
||||
write_config(text)
|
||||
created += 1
|
||||
print(f"Added Limine kernel group for {release}")
|
||||
|
||||
text = read_config()
|
||||
try:
|
||||
snapshots = sorted(
|
||||
path for path in SNAPSHOT_ROOT.iterdir()
|
||||
if path.is_dir() and path.name.startswith("portage-")
|
||||
)
|
||||
except OSError:
|
||||
snapshots = []
|
||||
for snapshot in snapshots:
|
||||
add_snapshot(snapshot)
|
||||
return created
|
||||
|
||||
|
||||
def reconcile() -> None:
|
||||
ensure_kernel_groups()
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
if os.geteuid() != 0:
|
||||
raise MenuError("must run as root from the Portage hook or installer")
|
||||
if len(argv) < 2 or argv[1] not in {"migrate", "reconcile", "sync-kernels", "add-snapshot", "remove-snapshot"}:
|
||||
raise MenuError("usage: portage-limine-menu {migrate|reconcile|sync-kernels|add-snapshot|remove-snapshot} [SNAPSHOT]")
|
||||
action = argv[1]
|
||||
os.makedirs("/run/lock", exist_ok=True)
|
||||
with open("/run/lock/portage-limine-menu.lock", "w", encoding="ascii") as lock:
|
||||
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
||||
if action == "migrate":
|
||||
migrate()
|
||||
elif action == "reconcile":
|
||||
migrate_if_needed()
|
||||
reconcile()
|
||||
elif action == "sync-kernels":
|
||||
ensure_kernel_groups()
|
||||
elif action in {"add-snapshot", "remove-snapshot"}:
|
||||
migrate_if_needed()
|
||||
source = Path(argv[2]) if len(argv) > 2 else None
|
||||
if source is None:
|
||||
raise MenuError(f"{action} requires a snapshot path")
|
||||
if action == "add-snapshot":
|
||||
add_snapshot(source)
|
||||
else:
|
||||
remove_snapshot(source)
|
||||
return 0
|
||||
|
||||
|
||||
def migrate_if_needed() -> None:
|
||||
text = read_config()
|
||||
_, entries = split_entries(text)
|
||||
if not any(entry.depth > 1 for entry in entries):
|
||||
migrate()
|
||||
return
|
||||
validate_config(text)
|
||||
if not kernel_groups(text):
|
||||
raise MenuError("Limine menu is nested but has no managed kernel groups")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main(sys.argv))
|
||||
except (MenuError, OSError, subprocess.SubprocessError) as error:
|
||||
print(f"portage-limine-menu: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,61 @@
|
||||
app-admin/doas
|
||||
app-editors/nano
|
||||
app-editors/neovim
|
||||
app-eselect/eselect-repository
|
||||
app-misc/brightnessctl
|
||||
app-misc/fastfetch
|
||||
app-portage/gentoolkit
|
||||
app-shells/fish
|
||||
dev-cpp/cairomm
|
||||
dev-lang/execline
|
||||
dev-util/codex
|
||||
dev-util/flatpak-builder
|
||||
gui-apps/foot
|
||||
gui-apps/grim
|
||||
gui-apps/hyprpaper
|
||||
gui-apps/hyprsunset
|
||||
gui-apps/quickshell
|
||||
gui-apps/slurp
|
||||
gui-apps/tuigreet
|
||||
gui-apps/wl-clip-persist
|
||||
gui-apps/wl-clipboard
|
||||
gui-libs/display-manager-init
|
||||
gui-libs/greetd
|
||||
gui-libs/hyprcursor
|
||||
gui-libs/hyprland-guiutils
|
||||
gui-wm/cage
|
||||
gui-wm/hyprland
|
||||
media-fonts/fontawesome
|
||||
media-fonts/maple-mono
|
||||
media-fonts/nerdfonts
|
||||
media-gfx/imv
|
||||
media-libs/gst-plugins-good
|
||||
media-libs/libpulse
|
||||
media-sound/pavucontrol
|
||||
media-sound/playerctl
|
||||
media-video/pipewire
|
||||
net-misc/chrony
|
||||
net-vpn/wireguard-tools
|
||||
net-wireless/iwd
|
||||
sys-apps/flatpak
|
||||
sys-apps/pciutils
|
||||
sys-apps/s6
|
||||
sys-apps/s6-linux-init
|
||||
sys-apps/s6-rc
|
||||
sys-auth/seatd
|
||||
sys-boot/efibootmgr
|
||||
sys-boot/limine
|
||||
sys-cluster/kubectl
|
||||
sys-devel/mold
|
||||
sys-firmware/intel-microcode
|
||||
sys-fs/btrfs-progs
|
||||
sys-fs/fuse:0
|
||||
sys-kernel/cachyos-kernel
|
||||
sys-kernel/dracut
|
||||
sys-kernel/gentoo-kernel-bin
|
||||
sys-kernel/gentoo-sources
|
||||
sys-kernel/installkernel
|
||||
sys-kernel/linux-firmware
|
||||
sys-libs/gcompat
|
||||
sys-process/htop
|
||||
x11-themes/bibata-xcursors
|
||||
Reference in new issue
Block a user