Compare commits
68
Commits
4f59afbac1
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
482bd797d0 | ||
|
|
f08abda392 | ||
|
|
6733cde613 | ||
|
|
64fff4f200 | ||
|
|
17d09d985e | ||
|
|
fec520be9d | ||
|
|
065a8cd0ae | ||
|
|
e6698cdb44 | ||
|
|
88bd9d6a20 | ||
|
|
d1031277bf | ||
|
|
ee10829452 | ||
|
|
1586e10df1 | ||
|
|
4e0dad3483 | ||
|
|
b79735d82e | ||
|
|
8fb78651c9 | ||
|
|
c9a127a07e | ||
|
|
86358466e1 | ||
|
|
35c1939f28 | ||
|
|
a8796467df | ||
|
|
3590c1b477 | ||
|
|
d4f04ac486 | ||
|
|
a5e4facf42 | ||
|
|
835b6b0040 | ||
|
|
cb60610942 | ||
|
|
ea50f64ef2 | ||
|
|
82b35e4bdb | ||
|
|
85ba9ddb51 | ||
|
|
0630acf020 | ||
|
|
4a53155c7e | ||
|
|
71780291a3 | ||
|
|
3d21a1c427 | ||
|
|
35906d4df0 | ||
|
|
9c0b716758 | ||
|
|
49ea36e089 | ||
|
|
8595e3d721 | ||
|
|
1869743d4a | ||
|
|
3e23ce1210 | ||
|
|
4cf680b854 | ||
|
|
0050f3a4a8 | ||
|
|
a54f14018e | ||
|
|
9dd1ecada8 | ||
|
|
c6e41cbf1e | ||
|
|
2b58c4f788 | ||
|
|
f069026a7c | ||
|
|
df56379556 | ||
|
|
847e76614a | ||
|
|
c552b30112 | ||
|
|
e99c60a073 | ||
|
|
0814c5bde7 | ||
|
|
6a9941771a | ||
|
|
dbf75d1239 | ||
|
|
eacceee296 | ||
|
|
8ca045de4a | ||
|
|
fceeb57254 | ||
|
|
af4504fdbe | ||
|
|
5b7ed5ede9 | ||
|
|
525537c929 | ||
|
|
3776dcb5a4 | ||
|
|
47991727ff | ||
|
|
80d9dccb61 | ||
|
|
aadead9f38 | ||
|
|
632a0267dd | ||
|
|
0939b228b6 | ||
|
|
1cd8d26d01 | ||
|
|
c443e4edf3 | ||
|
|
047eeebcc2 | ||
|
|
234bf8214e | ||
|
|
b9c86feba5 |
@@ -1,3 +1,194 @@
|
|||||||
# Fedora Unified Config
|
# Fedora Unified Config
|
||||||
|
|
||||||
The goal for this repo is to create a unified configuration for Fedora 44 and newer.
|
Unified configuration for **bare/minimal Fedora 44 and newer**. Pick a desktop
|
||||||
|
environment and optionally a tiling window manager; the script wires up repos,
|
||||||
|
packages, the greeter and the session entry.
|
||||||
|
|
||||||
|
This repo is the Fedora-only successor to `starburst`. Everything multi-distro
|
||||||
|
(Arch, Debian, Ubuntu branches, OS guessing, per-distro package helpers) is gone
|
||||||
|
on purpose.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.ku-it.org/yuri/fedora.git
|
||||||
|
cd fedora
|
||||||
|
./setup.sh # interactive: choose DE, then TWM
|
||||||
|
./setup.sh --debug # same, but pause after each section
|
||||||
|
```
|
||||||
|
|
||||||
|
Non-interactive:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./setup.sh --yes --de=none --twm=niri
|
||||||
|
./setup.sh --yes --de=gnome --twm=none
|
||||||
|
```
|
||||||
|
|
||||||
|
Run as your **normal user**, not root. `sudo` is used where needed.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
setup.sh entrypoint: args, preconditions, base setup, menus, dispatch
|
||||||
|
lib/common.sh colours, logging, dnf helpers, rpmfusion/terra, nerd fonts
|
||||||
|
lib/session.sh display-manager detection/switching, session entries
|
||||||
|
modules/de/{gnome,plasma,xfce}.sh
|
||||||
|
modules/twm/{niri,hyprland,mango,chadwm}.sh
|
||||||
|
modules/user/dotfiles.sh bare-repo dotfiles checkout
|
||||||
|
modules/user/nvidia.sh GPU-detected NVIDIA driver install
|
||||||
|
```
|
||||||
|
|
||||||
|
Modules are **separate processes**, not sourced into `setup.sh`. That means a
|
||||||
|
module's own `set -e` cannot leak into the caller, and every module can be run
|
||||||
|
on its own for testing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DE=none TWM=niri DEBUG_MODE=false bash modules/twm/niri.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Each module pulls in `lib/` by relative path, so it works both ways.
|
||||||
|
|
||||||
|
## Selection matrix
|
||||||
|
|
||||||
|
| DE | TWM | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| any | any | DE provides the session; TWM is added alongside it |
|
||||||
|
| `none` | any | TWM-only machine, SDDM installed as the greeter |
|
||||||
|
| any | `none` | plain desktop, no tiling WM |
|
||||||
|
| `none` | `none` | **refused** — that is not a graphical system |
|
||||||
|
|
||||||
|
### Desktop environments
|
||||||
|
|
||||||
|
| DE | dnf group (tried in order) | native greeter |
|
||||||
|
|---|---|---|
|
||||||
|
| `gnome` | `gnome-desktop`, `gnome-desktop-environment`, `workstation-product-environment` | gdm |
|
||||||
|
| `plasma` | `kde-desktop`, `kde-desktop-environment` | sddm |
|
||||||
|
| `xfce` | `xfce-desktop`, `xfce-desktop-environment` | sddm |
|
||||||
|
|
||||||
|
### Tiling window managers
|
||||||
|
|
||||||
|
| TWM | Source | Session type | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `niri` | Fedora repos | Wayland | noctalia is packaged since F44; ships its own session entry |
|
||||||
|
| `hyprland` | **COPR** (lionheartp, then hermitfeather, then solopasha) | Wayland | Retired from Fedora repos after F42; first COPR with a chroot for your release wins |
|
||||||
|
| `mango` | **Terra repo only** | Wayland | Session entry written if the package lacks one |
|
||||||
|
| `chadwm` | **built from source** | **X11** | dwm fork; not packaged anywhere for Fedora |
|
||||||
|
|
||||||
|
`chadwm` is the only X11 entry in the menu. The other three are Wayland
|
||||||
|
compositors — do not expect the same config story from it.
|
||||||
|
|
||||||
|
## GPU drivers (NVIDIA)
|
||||||
|
|
||||||
|
`modules/user/nvidia.sh` runs on **detected hardware**, no question asked: the
|
||||||
|
card in the machine decides what is installed. Opt out with `--nvidia=no`
|
||||||
|
(e.g. to stay on nouveau).
|
||||||
|
|
||||||
|
Branch selection comes from the chip family in the name `lspci` prints, because
|
||||||
|
the numeric device-id ranges overlap between families and a wrong guess installs
|
||||||
|
a module the card cannot use:
|
||||||
|
|
||||||
|
| Chip family in the name | Driver branch | Packages |
|
||||||
|
|---|---|---|
|
||||||
|
| `GB`/`AD`/`GA`/`TU` (Turing and newer) | current | `akmod-nvidia` + `xorg-x11-drv-nvidia-cuda` |
|
||||||
|
| `GV`/`GP`/`GM` (Volta, Pascal, Maxwell) | 580xx | `akmod-nvidia-580xx` + `xorg-x11-drv-nvidia-580xx{,-cuda}` |
|
||||||
|
| `GK` (Kepler) | 470xx | `akmod-nvidia-470xx` + `xorg-x11-drv-nvidia-470xx{,-cuda}` |
|
||||||
|
| `GF` (Fermi) | 390xx | `akmod-nvidia-390xx` + `xorg-x11-drv-nvidia-390xx{,-cuda}` |
|
||||||
|
| pre-Fermi (`G92`, `GT200`, ...) | none | refused: 340xx is EOL and needs a longterm kernel |
|
||||||
|
|
||||||
|
If the name cannot be parsed, the module asks for the branch instead of guessing.
|
||||||
|
|
||||||
|
**Secure Boot.** With enforcement on, the akmods key is generated and imported,
|
||||||
|
and the module then *stops before installing the driver* — installing an
|
||||||
|
unsigned module while Secure Boot enforces is the documented way to get a black
|
||||||
|
screen, because the driver package blacklists nouveau as it lands. Enrol the key
|
||||||
|
in MokManager at the next boot, then run the setup again. With Secure Boot off
|
||||||
|
or in **Setup mode**, no signing happens at all: shim skips MokManager when
|
||||||
|
enforcement is off, so an import request would just sit in the efivars.
|
||||||
|
|
||||||
|
After the transaction the module waits (bounded, 5 min) for the akmod to build
|
||||||
|
so you cannot reboot into a driverless boot, runs `dnf mark user` on the akmod
|
||||||
|
(so `dnf autoremove` cannot take it), and offers VA-API decode + vulkan as an
|
||||||
|
opt-in extra. Nothing here sets `nvidia-drm.modeset=1`: RPM Fusion's kernel
|
||||||
|
package already enables KMS, and the flag now conflicts with Fedora's simpledrm
|
||||||
|
early-boot patch.
|
||||||
|
|
||||||
|
## Display manager policy
|
||||||
|
|
||||||
|
* A **DE module** leaves the DE's own greeter alone (GNOME→gdm, Plasma→sddm,
|
||||||
|
XFCE→sddm). It never fights a desktop over its own login screen.
|
||||||
|
* A **TWM module** enforces **SDDM**, because it is the only greeter that lists
|
||||||
|
Wayland compositors *and* X11 sessions in one menu.
|
||||||
|
* Either way `graphical.target` becomes the default.
|
||||||
|
|
||||||
|
|
||||||
|
## System settings
|
||||||
|
|
||||||
|
Between base setup and the DE menu, the script asks two questions:
|
||||||
|
|
||||||
|
1. **Disable SELinux permanently?** — default **no**. Writes `SELINUX=disabled`
|
||||||
|
to `/etc/selinux/config` (backup kept); a reboot applies it. Recommended
|
||||||
|
answer is no.
|
||||||
|
2. **Snapper + grub-btrfs + zstd compression** — only asked when `/` is btrfs,
|
||||||
|
default **yes**. Runs, in order:
|
||||||
|
- `btrfs subvolume list /` and the fstab btrfs entries are shown
|
||||||
|
- every fstab btrfs entry with a `subvol=` but no `compress=` gets
|
||||||
|
`,compress=zstd:1` appended (fstab backed up first; re-runs cannot stack
|
||||||
|
duplicate options)
|
||||||
|
- the [SysGuides installer](https://github.com/SysGuides/sysguides-snapper-fedora)
|
||||||
|
sets up snapper (root + home), grub-btrfs, and DNF5 pre/post snapshot hooks,
|
||||||
|
so every `dnf` transaction is snapshotted and `snapper undochange` works.
|
||||||
|
|
||||||
|
Unattended equivalents: `--selinux=keep|disabled` and `--snapper=yes|no`.
|
||||||
|
|
||||||
|
## Design rules
|
||||||
|
|
||||||
|
1. **Fail loudly, early.** `set -e` is on unless `--debug`. Unsupported distro,
|
||||||
|
release below 44, `DE=none` + `TWM=none`, and a missing module all abort with
|
||||||
|
an explanation instead of half-installing.
|
||||||
|
2. **Be idempotent.** Re-running must be safe. Repos are checked with `rpm -q`
|
||||||
|
before being added, the Nerd Font is skipped if already present, and session
|
||||||
|
entries are not overwritten.
|
||||||
|
3. **Prefer official repos, then Terra, then a source build.** That order is why
|
||||||
|
only `mango` and `chadwm` need special handling.
|
||||||
|
4. **No silent optional failures.** `install_optional` warns and continues for
|
||||||
|
nice-to-haves; everything load-bearing uses `install_packages` and aborts.
|
||||||
|
5. **Guard anything environment-dependent.** `clear` needs `|| true` (no `TERM`
|
||||||
|
under cron/pipes), `os-release` fields are defaulted, and
|
||||||
|
`/usr/share/xsessions` is created before writing into it.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
The script was verified on a non-Fedora host with a stateful stub harness
|
||||||
|
(fake `sudo`/`dnf`/`rpm`/`systemctl`/`git`/`make`/`curl`, a fake `os-release`,
|
||||||
|
and sandboxed `/usr` + `$HOME`). Covered: every DE×(TWM|none) combination, the
|
||||||
|
refusal paths, `--help`/bad args, interactive menus, `--debug` abort, and
|
||||||
|
re-running for idempotency.
|
||||||
|
|
||||||
|
`OS_RELEASE_FILE` is overridable purely so the preconditions can be tested:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OS_RELEASE_FILE=/tmp/fake-os-release ./setup.sh --yes --de=xfce --twm=chadwm
|
||||||
|
```
|
||||||
|
|
||||||
|
## Known limitations
|
||||||
|
|
||||||
|
* `mangowc` comes from **Terra** (added in the base section) and `hyprland`
|
||||||
|
from a **COPR** — third-party sources whose layout is not guaranteed across
|
||||||
|
Fedora releases; `mango` aborts with build-from-source instructions if the
|
||||||
|
package is missing. `noctalia` has been in the Fedora repos since F44, so
|
||||||
|
the niri module needs no third-party repo.
|
||||||
|
* Package *names* are not verified by installation in this repo's test harness —
|
||||||
|
only control flow is. First real run on a fresh F44 box is the actual proof.
|
||||||
|
* `gnome-tweaks`/`gnome-extensions-app`, `wofi`, `waybar`, `hyprlock` and friends
|
||||||
|
are installed with `install_optional`, so a rename will warn rather than fail.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- [ ] Profile files (`--profile=laptop`) instead of individual flags
|
||||||
|
- [ ] Optional dotfiles layer (noctalia/hyprland/niri configs)
|
||||||
|
- [ ] `--dry-run` that prints the dnf transaction without applying it
|
||||||
|
- [ ] Pre-flight report: what is already installed, what would change
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See `LICENSE`.
|
||||||
|
|||||||
+494
@@ -0,0 +1,494 @@
|
|||||||
|
# shellcheck shell=bash
|
||||||
|
#------------------------------------------
|
||||||
|
# lib/common.sh — shared helpers
|
||||||
|
#
|
||||||
|
# Sourced by setup.sh and by every module. Modules source it directly, so a
|
||||||
|
# module can be run standalone for testing:
|
||||||
|
# DE=none TWM=niri bash modules/twm/niri.sh
|
||||||
|
#------------------------------------------
|
||||||
|
|
||||||
|
# Guard against double-sourcing (setup.sh and a module can both pull it in)
|
||||||
|
if [[ -n "${_SB_COMMON_LOADED:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
_SB_COMMON_LOADED=1
|
||||||
|
|
||||||
|
#------------------------------------------
|
||||||
|
# Colors (printf-safe, no tput)
|
||||||
|
#------------------------------------------
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
PURPLE='\033[0;35m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
GRAY='\033[0;37m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
RESET='\033[0m'
|
||||||
|
|
||||||
|
# Disable colors if output is not a terminal
|
||||||
|
# (PURPLE/BLUE/BOLD defined for future use; shellcheck has no way to know that)
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
if [ ! -t 1 ]; then
|
||||||
|
RED='' GREEN='' YELLOW='' CYAN='' PURPLE='' BLUE='' GRAY='' BOLD='' RESET=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
color_yellow() { printf "%b%s%b\n" "$YELLOW" "$*" "$RESET"; }
|
||||||
|
color_cyan() { printf "%b%s%b\n" "$CYAN" "$*" "$RESET"; }
|
||||||
|
color_red() { printf "%b%s%b\n" "$RED" "$*" "$RESET"; }
|
||||||
|
color_green() { printf "%b%s%b\n" "$GREEN" "$*" "$RESET"; }
|
||||||
|
color_gray() { printf "%b%s%b\n" "$GRAY" "$*" "$RESET"; }
|
||||||
|
|
||||||
|
#------------------------------------------
|
||||||
|
# Log levels
|
||||||
|
#------------------------------------------
|
||||||
|
info() { color_cyan " $*"; }
|
||||||
|
ok() { color_green " $*"; }
|
||||||
|
warn() { color_yellow " $*"; }
|
||||||
|
err() { color_red " $*" >&2; }
|
||||||
|
step() { printf "\n"; color_yellow "$*"; }
|
||||||
|
rule() { printf "%s\n" "----------------------------------------------------------------"; }
|
||||||
|
|
||||||
|
#------------------------------------------
|
||||||
|
# Debug pausing
|
||||||
|
#------------------------------------------
|
||||||
|
is_debug() { [[ "${DEBUG_MODE:-false}" == "true" ]]; }
|
||||||
|
|
||||||
|
pause_if_debug() {
|
||||||
|
if is_debug; then
|
||||||
|
printf "\n"
|
||||||
|
color_yellow "----------------------------------------------------------------"
|
||||||
|
read -rp "Section complete. Press [Enter] to continue, or [q] to abort: " debug_choice
|
||||||
|
if [[ "${debug_choice,,}" == "q" ]]; then
|
||||||
|
color_red "Script aborted by user."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
color_yellow "----------------------------------------------------------------"
|
||||||
|
printf "\n"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#------------------------------------------
|
||||||
|
# Package helpers (dnf5)
|
||||||
|
#------------------------------------------
|
||||||
|
# True when gum is installed AND we are interactive (stdin tty + /dev/tty
|
||||||
|
# writable). stdout must NOT be checked: it is a pipe inside $(...).
|
||||||
|
# Unattended runs never get the TUI (see ui_quiet_run).
|
||||||
|
have_gum() {
|
||||||
|
command -v gum >/dev/null 2>&1 || return 1
|
||||||
|
[[ "${ASSUME_YES:-false}" == "true" ]] && return 1
|
||||||
|
[ -t 0 ] || return 1
|
||||||
|
[ -w /dev/tty ] || return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Quiet dnf transaction: spinner while it runs, one ok-line after, full output
|
||||||
|
# appended to $SETUP_DNF_LOG and printed (tail) only on failure. Unattended or
|
||||||
|
# no-gum runs execute plainly with normal output (logs matter there).
|
||||||
|
ui_quiet_run() {
|
||||||
|
local msg="$1"
|
||||||
|
shift
|
||||||
|
local log="${SETUP_DNF_LOG:-/tmp/fedora-setup-dnf.log}"
|
||||||
|
local before n
|
||||||
|
|
||||||
|
# Live-quiet mode needs only an interactive terminal (stdin tty + writable
|
||||||
|
# /dev/tty), NOT gum — the progress line is pure bash. Unattended runs and
|
||||||
|
# pipes keep plain full output (logs/cron contract).
|
||||||
|
if [[ "${ASSUME_YES:-false}" == "true" ]] || [ ! -t 0 ] || [ ! -w /dev/tty ]; then
|
||||||
|
"$@"
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
before=$(wc -l < "$log" 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
# ---- status BLOCK (TTY-safe: a background-coloured SPACE, never a glyph) --
|
||||||
|
# busy: blue block that breathes (bg-colour ramp) + current dnf action
|
||||||
|
# ok : solid green block + package count
|
||||||
|
# fail: solid RED block (spec change from the X, per YuKu)
|
||||||
|
# Colour-depth aware: 256 -> ramp, 16 -> blue/bright-blue, none -> ASCII.
|
||||||
|
local depth=0 nc
|
||||||
|
nc=$(tput colors 2>/dev/null || echo 0)
|
||||||
|
case "$nc" in ''|*[!0-9]*) nc=0 ;; esac
|
||||||
|
if (( nc >= 256 )); then depth=256
|
||||||
|
elif (( nc >= 8 )); then depth=16
|
||||||
|
fi
|
||||||
|
# The bare Linux VT is 16-color VGA: 48;5;N codes are ignored there, and
|
||||||
|
# its blue/bright-blue backgrounds render nearly identically (VM-proven:
|
||||||
|
# "solid blue, no breathing" on the console). Cap to the 16-color path.
|
||||||
|
case "${TERM:-}" in linux*|console*) depth=16 ;; esac
|
||||||
|
|
||||||
|
# Per-depth frame interval: the console colour-pulse runs slower (a fast
|
||||||
|
# blue/grey flip reads as flicker — YuKu asked for half-to-third speed);
|
||||||
|
# the 256-colour breathe ramp keeps its 50ms.
|
||||||
|
local tick=0.05
|
||||||
|
(( depth == 16 )) && tick=0.15
|
||||||
|
|
||||||
|
local -a bg=()
|
||||||
|
case "$depth" in
|
||||||
|
# Deep-amplitude breathe: dark navy (17) to BRIGHT blue (45). Adjacent
|
||||||
|
# shades must differ enough to survive flicker fusion — a subtle ramp
|
||||||
|
# at 20fps reads as a solid block (VM-proven).
|
||||||
|
256) bg=( "48;5;17" "48;5;19" "48;5;21" "48;5;27" "48;5;33" "48;5;39" "48;5;45" "48;5;39" "48;5;33" "48;5;27" "48;5;21" "48;5;19" ) ;;
|
||||||
|
# Colour pulse with CONSTANT layout: blue <-> dark grey (bright black).
|
||||||
|
# Never remove the block on a frame — a missing block shifted the
|
||||||
|
# whole line by one char on the console (VM-proven).
|
||||||
|
16) bg=( "44" "44" "100" "100" ) ;;
|
||||||
|
esac
|
||||||
|
local -a ch=( "|" "/" "-" "\\" )
|
||||||
|
|
||||||
|
"$@" >>"$log" 2>&1 &
|
||||||
|
local pid=$!
|
||||||
|
local f=0 cur
|
||||||
|
while kill -0 "$pid" 2>/dev/null; do
|
||||||
|
# Match dnf ('Installing: pkg') AND flatpak ('Installing... id', a
|
||||||
|
# Unicode ellipsis) progress lines; sanitize to printable ASCII so a
|
||||||
|
# non-UTF8 console never sees garbage bytes in the live line.
|
||||||
|
cur=$(sed -n "$((before+1)),\$p" "$log" 2>/dev/null \
|
||||||
|
| grep -aE '^Installing|^Upgrading|^Downgrading|^Removing|^Verifying|^Preparing|^Cleanup' \
|
||||||
|
| tail -n 1 \
|
||||||
|
| tr -cd '\11\12\40-\176' || true)
|
||||||
|
# Compose ONCE, then clamp to the terminal width: a live line longer
|
||||||
|
# than the screen wraps on every redraw and floods the screen with
|
||||||
|
# duplicate partial lines (VM-proven on the 80-col TTY).
|
||||||
|
local cols line
|
||||||
|
cols=$(tput cols 2>/dev/null)
|
||||||
|
[[ "$cols" =~ ^[0-9]+$ ]] || cols=80
|
||||||
|
if [[ -n "$cur" ]]; then
|
||||||
|
line="$msg: $cur"
|
||||||
|
else
|
||||||
|
line="$msg"
|
||||||
|
fi
|
||||||
|
if (( ${#line} > cols - 2 )); then
|
||||||
|
line="${line:0:cols-4}.."
|
||||||
|
fi
|
||||||
|
if (( depth >= 16 )); then
|
||||||
|
local code="${bg[$(( f % ${#bg[@]} ))]}"
|
||||||
|
printf '\r\033[%sm \033[0m %s' "$code" "$line" >&2
|
||||||
|
else
|
||||||
|
printf '\r %s %s' "${ch[$(( f % ${#ch[@]} ))]}" "$line" >&2
|
||||||
|
fi
|
||||||
|
f=$(( f + 1 ))
|
||||||
|
sleep "$tick"
|
||||||
|
done
|
||||||
|
|
||||||
|
local rc=0
|
||||||
|
wait "$pid" || rc=$?
|
||||||
|
# ANSI erase-line: wipes the live line without leaving trailing spaces
|
||||||
|
# (a 100-space wipe left a visible gap when the summary was shorter)
|
||||||
|
printf '\r\033[K' >&2
|
||||||
|
|
||||||
|
if (( rc == 0 )); then
|
||||||
|
n=$(sed -n "$((before+1)),\$p" "$log" 2>/dev/null | grep -ac '^Installing' || true)
|
||||||
|
local count_txt=""
|
||||||
|
if (( n > 0 )); then
|
||||||
|
local plural="s"
|
||||||
|
(( n == 1 )) && plural=""
|
||||||
|
count_txt=" (${n} package${plural})"
|
||||||
|
fi
|
||||||
|
# n == 0 means nothing new was installed (idempotent re-run /
|
||||||
|
# deps already present): print the message without a count.
|
||||||
|
if (( depth == 256 )); then
|
||||||
|
printf '\033[48;5;34m \033[0m %s%s\n' "$msg" "$count_txt" >&2
|
||||||
|
elif (( depth == 16 )); then
|
||||||
|
printf '\033[42m \033[0m %s%s\n' "$msg" "$count_txt" >&2
|
||||||
|
else
|
||||||
|
ok "$msg${count_txt}"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
if (( depth == 256 )); then
|
||||||
|
printf '\033[48;5;196m \033[0m %s\n' "$msg" >&2
|
||||||
|
elif (( depth == 16 )); then
|
||||||
|
printf '\033[41m \033[0m %s\n' "$msg" >&2
|
||||||
|
else
|
||||||
|
color_red " Failed: $msg"
|
||||||
|
fi
|
||||||
|
color_red " (dnf reported a transaction failure)"
|
||||||
|
color_red " Last lines from $log:"
|
||||||
|
sed -n "$((before+1)),\$p" "$log" 2>/dev/null | tail -n 20 | sed 's/^/ /' >&2
|
||||||
|
return "$rc"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_packages() {
|
||||||
|
# -y : no prompts
|
||||||
|
# weak deps on : keep default behaviour, stated explicitly so it is greppable
|
||||||
|
ui_quiet_run "Installing: $*" \
|
||||||
|
sudo dnf install -y --setopt=install_weak_deps=True "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Same, but a missing/renamed package warns instead of aborting the whole run.
|
||||||
|
# Use for anything that is a nice-to-have rather than load-bearing.
|
||||||
|
install_optional() {
|
||||||
|
ui_quiet_run "Installing (optional): $*" \
|
||||||
|
sudo dnf install -y --setopt=install_weak_deps=True "$@" \
|
||||||
|
|| warn "Optional packages not installed: $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg_installed() {
|
||||||
|
local p
|
||||||
|
for p in "$@"; do
|
||||||
|
rpm -q "$p" &>/dev/null || return 1
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# True if ANY of the listed packages is installed (pkg_installed is AND).
|
||||||
|
pkg_installed_any() {
|
||||||
|
local p
|
||||||
|
for p in "$@"; do
|
||||||
|
if rpm -q "$p" &>/dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_packages() {
|
||||||
|
local packages_to_remove=()
|
||||||
|
for pkg in "$@"; do
|
||||||
|
if pkg_installed "$pkg"; then
|
||||||
|
packages_to_remove+=("$pkg")
|
||||||
|
else
|
||||||
|
color_gray " Package '$pkg' not installed. Skipping removal."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#packages_to_remove[@]} -gt 0 ]; then
|
||||||
|
ui_quiet_run "Removing: ${packages_to_remove[*]}" \
|
||||||
|
sudo dnf remove -y "${packages_to_remove[@]}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# dnf comps group. Fail loudly with the real group list rather than letting
|
||||||
|
# dnf's generic "no match" scroll past.
|
||||||
|
install_group() {
|
||||||
|
local group="$1"
|
||||||
|
if ! dnf group info "$group" >/dev/null 2>&1; then
|
||||||
|
err "No such dnf group: @$group"
|
||||||
|
err "Available groups on this system:"
|
||||||
|
dnf group list --hidden 2>/dev/null | sed 's/^/ /' >&2 || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
ui_quiet_run "Installing group: @$group" sudo dnf group install -y "$group"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try each candidate group/environment in turn; first one that exists wins.
|
||||||
|
install_group_any() {
|
||||||
|
local g
|
||||||
|
for g in "$@"; do
|
||||||
|
if dnf group info "$g" >/dev/null 2>&1; then
|
||||||
|
info "Using dnf group: @$g"
|
||||||
|
ui_quiet_run "Installing group: @$g" sudo dnf group install -y "$g"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
err "None of these dnf groups exist: $*"
|
||||||
|
dnf group list --hidden 2>/dev/null | sed 's/^/ /' >&2 || true
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#------------------------------------------
|
||||||
|
# Third-party repos
|
||||||
|
#------------------------------------------
|
||||||
|
# True when a repo id is CONFIGURED, whatever installed it. This is the
|
||||||
|
# reliable source of truth: a machine can have the repo without the
|
||||||
|
# -release RPM (hand-added .repo file) — and bootstrapping over an existing
|
||||||
|
# repo id via --repofrompath collides and fails (laptop-proven).
|
||||||
|
repo_enabled() {
|
||||||
|
dnf repolist --all 2>/dev/null | awk '{print $1}' | grep -qx "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
add_terra_repo() {
|
||||||
|
if pkg_installed terra-release || repo_enabled terra; then
|
||||||
|
ok "Terra repository already present."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
info "Adding Terra repository (Fyra Labs)..."
|
||||||
|
# Terra's own instructions use --nogpgcheck for the bootstrap package;
|
||||||
|
# once terra-release is in place the repo itself is signature-checked.
|
||||||
|
sudo dnf install -y --nogpgcheck \
|
||||||
|
--repofrompath 'terra,https://repos.fyralabs.com/terra$releasever' \
|
||||||
|
terra-release
|
||||||
|
}
|
||||||
|
|
||||||
|
add_rpmfusion_repo() {
|
||||||
|
local rel
|
||||||
|
rel=$(rpm -E %fedora)
|
||||||
|
if pkg_installed rpmfusion-free-release rpmfusion-nonfree-release \
|
||||||
|
|| { repo_enabled rpmfusion-free && repo_enabled rpmfusion-nonfree; }; then
|
||||||
|
ok "RPM Fusion already present."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
info "Enabling RPM Fusion (free + nonfree) for Fedora ${rel}..."
|
||||||
|
sudo dnf install -y \
|
||||||
|
"https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-${rel}.noarch.rpm" \
|
||||||
|
"https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-${rel}.noarch.rpm"
|
||||||
|
}
|
||||||
|
|
||||||
|
#------------------------------------------
|
||||||
|
# Fonts
|
||||||
|
#------------------------------------------
|
||||||
|
# Nerd Fonts are not in the Fedora repos, so pull the release zip from GitHub
|
||||||
|
# into the user's font dir. Idempotent: skipped if the family is already there.
|
||||||
|
install_nerd_font() {
|
||||||
|
local family="${1:-JetBrainsMono}"
|
||||||
|
local dest="${HOME}/.local/share/fonts/NerdFonts-${family}"
|
||||||
|
local url="https://github.com/ryanoasis/nerd-fonts/releases/latest/download/${family}.zip"
|
||||||
|
|
||||||
|
if [[ -d "$dest" ]] && compgen -G "$dest/*.ttf" >/dev/null; then
|
||||||
|
ok "Nerd Font ${family} already installed."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Installing Nerd Font ${family}..."
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
|
||||||
|
if ! curl -fsSL -o "$tmp/${family}.zip" "$url"; then
|
||||||
|
warn "Could not download ${family} Nerd Font. Skipping."
|
||||||
|
rm -rf "$tmp"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v unzip >/dev/null 2>&1; then
|
||||||
|
install_packages unzip
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$dest"
|
||||||
|
unzip -qo "$tmp/${family}.zip" -d "$dest"
|
||||||
|
rm -rf "$tmp"
|
||||||
|
|
||||||
|
fc-cache -f "$dest" >/dev/null 2>&1 || true
|
||||||
|
ok "Nerd Font ${family} installed to $dest"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#------------------------------------------
|
||||||
|
# System: SELinux, Btrfs, snapper
|
||||||
|
#------------------------------------------
|
||||||
|
# Permanently disable SELinux. Destructive-ish: warn loudly, and remind the
|
||||||
|
# user a reboot is needed for the change to take effect.
|
||||||
|
disable_selinux() {
|
||||||
|
# FSTAB/SELINUX file overridable for testing on non-Fedora hosts
|
||||||
|
local cfg="${SELINUX_CONFIG:-/etc/selinux/config}"
|
||||||
|
if [[ -f "$cfg" ]] && grep -q '^SELINUX=disabled' "$cfg"; then
|
||||||
|
ok "SELinux already disabled in $cfg."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
warn "Disabling SELinux permanently. This removes a significant layer of"
|
||||||
|
warn "system protection. Only do this if you specifically need it."
|
||||||
|
if [[ -f "$cfg" ]]; then
|
||||||
|
sudo cp "$cfg" "${cfg}.bkp.$(date +%F)"
|
||||||
|
sudo sed -i 's/^SELINUX=.*/SELINUX=disabled/' "$cfg"
|
||||||
|
else
|
||||||
|
# belt-and-suspenders: on real Fedora /etc/selinux always exists, but
|
||||||
|
# do not die if it somehow does not
|
||||||
|
sudo mkdir -p "$(dirname "$cfg")"
|
||||||
|
printf 'SELINUX=disabled\nSELINUXTYPE=targeted\n' | sudo tee "$cfg" >/dev/null
|
||||||
|
fi
|
||||||
|
ok "SELINUX=disabled written to $cfg (backup saved). Reboot to apply."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show the btrfs layout, then make sure every btrfs fstab entry compresses
|
||||||
|
# with zstd. The sed variant from the SysGuides guide appends ,compress=zstd:1
|
||||||
|
# after the subvol= option; we guard it so re-runs cannot stack duplicates.
|
||||||
|
ensure_btrfs_compression() {
|
||||||
|
local fstab="${FSTAB_FILE:-/etc/fstab}"
|
||||||
|
info "Current btrfs subvolumes on /:"
|
||||||
|
sudo btrfs subvolume list / 2>/dev/null | sed 's/^/ /' || warn "btrfs subvolume list failed (not btrfs?)"
|
||||||
|
|
||||||
|
info "fstab btrfs entries:"
|
||||||
|
grep ' btrfs ' "$fstab" | sed 's/^/ /' || true
|
||||||
|
|
||||||
|
local fixed=0 line
|
||||||
|
# Only touch btrfs lines that have a subvol= option and no compress= yet.
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if grep -q ' btrfs ' <<<"$line" && grep -q 'subvol=' <<<"$line" && ! grep -q 'compress=' <<<"$line"; then
|
||||||
|
fixed=$((fixed+1))
|
||||||
|
fi
|
||||||
|
done < "$fstab"
|
||||||
|
|
||||||
|
if (( fixed == 0 )); then
|
||||||
|
ok "All btrfs fstab entries already carry a compress= option (or none exist)."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Adding compress=zstd:1 to ${fixed} btrfs fstab entrie(s)..."
|
||||||
|
# Backup once per run, then apply the SysGuides sed to every qualifying line.
|
||||||
|
sudo cp "$fstab" "$fstab.bkp.$(date +%F-%H%M)"
|
||||||
|
sudo sed -i '/ btrfs / s/subvol=[^ ,]*/&,compress=zstd:1/' "$fstab"
|
||||||
|
ok "fstab updated. Remount or reboot for compression to take effect:"
|
||||||
|
info " sudo mount -o remount / && sudo mount -o remount /home"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Snapshot setup using the vendored SysGuides scripts (vendor/snapper/).
|
||||||
|
# Original: https://github.com/SysGuides/sysguides-snapper-fedora (MIT).
|
||||||
|
# Vendored so we do not depend on that repo at runtime; upstream changes can
|
||||||
|
# be pulled deliberately, not accidentally.
|
||||||
|
# Exec bits are not relied on: scripts are invoked via `bash`.
|
||||||
|
setup_snapper() {
|
||||||
|
local vendor="$SCRIPT_DIR/vendor/snapper"
|
||||||
|
|
||||||
|
if pkg_installed snapper && [[ -d /.snapshots ]]; then
|
||||||
|
ok "Snapper already configured (config for / exists)."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if ! findmnt -n -o FSTYPE / 2>/dev/null | grep -q btrfs; then
|
||||||
|
warn "Root filesystem is not btrfs; skipping snapshot setup."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$vendor/scripts/snapper-pre.sh" ]]; then
|
||||||
|
err "Vendored snapper scripts missing at $vendor — repo checkout incomplete?"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Installing snapper + grub-btrfs packages..."
|
||||||
|
install_packages snapper libdnf5-plugin-actions btrfs-assistant inotify-tools make git
|
||||||
|
|
||||||
|
info "Creating snapper configs (root + home)..."
|
||||||
|
[[ -d /.snapshots ]] || sudo snapper -c root create-config /
|
||||||
|
[[ -d /home/.snapshots ]] || sudo snapper -c home create-config /home
|
||||||
|
sudo restorecon -RF /.snapshots /home/.snapshots 2>/dev/null || true
|
||||||
|
|
||||||
|
local real_user="${SUDO_USER:-$USER}"
|
||||||
|
sudo snapper -c root set-config ALLOW_USERS="$real_user" SYNC_ACL=yes
|
||||||
|
sudo snapper -c home set-config ALLOW_USERS="$real_user" SYNC_ACL=yes
|
||||||
|
# no home timeline: snapshots of your own files on every dnf run are noise
|
||||||
|
sudo snapper -c home set-config TIMELINE_CREATE=no
|
||||||
|
|
||||||
|
info "Excluding .snapshots from updatedb..."
|
||||||
|
if grep -q '^PRUNENAMES' /etc/updatedb.conf 2>/dev/null; then
|
||||||
|
grep -q '\.snapshots' /etc/updatedb.conf || \
|
||||||
|
sudo sed -i 's|^PRUNENAMES *= *"|PRUNENAMES = ".snapshots |' /etc/updatedb.conf
|
||||||
|
else
|
||||||
|
printf 'PRUNENAMES = ".snapshots"\n' | sudo tee -a /etc/updatedb.conf >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Installing grub-btrfs (from source, Antynea/grub-btrfs)..."
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
git clone --quiet --depth 1 https://github.com/Antynea/grub-btrfs "$tmp/grub-btrfs"
|
||||||
|
sed -i \
|
||||||
|
-e 's|^#GRUB_BTRFS_SNAPSHOT_KERNEL_PARAMETERS=.*|GRUB_BTRFS_SNAPSHOT_KERNEL_PARAMETERS="rd.live.overlay.overlayfs=1"|' \
|
||||||
|
-e 's|^#GRUB_BTRFS_GRUB_DIRNAME=.*|GRUB_BTRFS_GRUB_DIRNAME="/boot/grub2"|' \
|
||||||
|
-e 's|^#GRUB_BTRFS_MKCONFIG=.*|GRUB_BTRFS_MKCONFIG=/usr/bin/grub2-mkconfig|' \
|
||||||
|
-e 's|^#GRUB_BTRFS_SCRIPT_CHECK=.*|GRUB_BTRFS_SCRIPT_CHECK=grub2-script-check|' \
|
||||||
|
"$tmp/grub-btrfs/config"
|
||||||
|
sudo make -C "$tmp/grub-btrfs" install
|
||||||
|
sudo systemctl enable --now grub-btrfsd.service
|
||||||
|
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
|
||||||
|
rm -rf "$tmp"
|
||||||
|
|
||||||
|
info "Installing DNF5 snapper integration (vendored scripts)..."
|
||||||
|
sudo install -m 755 "$vendor"/scripts/* /usr/local/bin/
|
||||||
|
sudo restorecon -v /usr/local/bin/snapper-*.sh 2>/dev/null || true
|
||||||
|
sudo mkdir -p /etc/dnf/libdnf5-plugins/actions.d/
|
||||||
|
sudo install -m 644 "$vendor/config/snapper.actions" /etc/dnf/libdnf5-plugins/actions.d/
|
||||||
|
|
||||||
|
info "Enabling snapper timers..."
|
||||||
|
sudo systemctl enable --now snapper-timeline.timer snapper-cleanup.timer
|
||||||
|
|
||||||
|
ok "Snapper + grub-btrfs installed. Every dnf transaction now gets a pre/post snapshot."
|
||||||
|
info "Try it: snapper ls | snapper status 1..2 | sudo snapper undochange 1..2"
|
||||||
|
}
|
||||||
+145
@@ -0,0 +1,145 @@
|
|||||||
|
# shellcheck shell=bash
|
||||||
|
#------------------------------------------
|
||||||
|
# lib/session.sh — display manager and graphical session wiring
|
||||||
|
#
|
||||||
|
# DM policy:
|
||||||
|
# * A DE module leaves its native DM alone (GNOME→gdm, Plasma→sddm, XFCE→sddm).
|
||||||
|
# * A TWM module enforces SDDM, because SDDM is the only DM that presents both
|
||||||
|
# Wayland compositors and X11 sessions in one menu. One greeter, no surprises.
|
||||||
|
#------------------------------------------
|
||||||
|
|
||||||
|
if [[ -n "${_SB_SESSION_LOADED:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
_SB_SESSION_LOADED=1
|
||||||
|
|
||||||
|
# Name of the active display manager ("sddm", "gdm", "lightdm"), or "" if none.
|
||||||
|
detect_display_manager() {
|
||||||
|
local link=/etc/systemd/system/display-manager.service
|
||||||
|
if [[ -L "$link" ]]; then
|
||||||
|
basename "$(readlink "$link")" | sed 's/\.service$//'
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
enable_graphical_target() {
|
||||||
|
sudo systemctl set-default graphical.target
|
||||||
|
}
|
||||||
|
|
||||||
|
# Make $1 the display manager, disabling whatever is active now.
|
||||||
|
switch_display_manager() {
|
||||||
|
local target="$1"
|
||||||
|
local current
|
||||||
|
current=$(detect_display_manager)
|
||||||
|
|
||||||
|
if [[ "$current" == "$target" ]]; then
|
||||||
|
ok "Display manager already $target."
|
||||||
|
else
|
||||||
|
if [[ -n "$current" ]]; then
|
||||||
|
info "Disabling current display manager: $current"
|
||||||
|
# disable may fail if the unit vanished; that is not fatal
|
||||||
|
sudo systemctl disable "$current" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
info "Enabling display manager: $target"
|
||||||
|
sudo systemctl enable "$target"
|
||||||
|
fi
|
||||||
|
|
||||||
|
enable_graphical_target
|
||||||
|
}
|
||||||
|
|
||||||
|
# Register a Wayland session so the DM can list it.
|
||||||
|
# $1 = identifier, $2 = display name, $3 = command
|
||||||
|
write_wayland_session() {
|
||||||
|
local id="$1" name="$2" exec_cmd="$3"
|
||||||
|
local file="/usr/share/wayland-sessions/${id}.desktop"
|
||||||
|
if [[ -f "$file" ]]; then
|
||||||
|
ok "Wayland session already registered: $id"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
info "Registering Wayland session: $id"
|
||||||
|
sudo mkdir -p "$(dirname "$file")"
|
||||||
|
printf '[Desktop Entry]\nName=%s\nComment=%s\nExec=%s\nType=Application\nDesktopNames=%s\n' \
|
||||||
|
"$name" "$name session" "$exec_cmd" "$id" | sudo tee "$file" >/dev/null
|
||||||
|
sudo chmod 644 "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Register an X11 session so the DM can list it.
|
||||||
|
write_x11_session() {
|
||||||
|
local id="$1" name="$2" exec_cmd="$3"
|
||||||
|
local file="/usr/share/xsessions/${id}.desktop"
|
||||||
|
if [[ -f "$file" ]]; then
|
||||||
|
ok "X11 session already registered: $id"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
info "Registering X11 session: $id"
|
||||||
|
# A bare Fedora has no X session yet, so the directory may not exist.
|
||||||
|
sudo mkdir -p "$(dirname "$file")"
|
||||||
|
printf '[Desktop Entry]\nName=%s\nComment=%s\nExec=%s\nType=Application\n' \
|
||||||
|
"$name" "$name session" "$exec_cmd" | sudo tee "$file" >/dev/null
|
||||||
|
sudo chmod 644 "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Common login-time plumbing every Wayland compositor wants on a bare install.
|
||||||
|
install_session_prereqs() {
|
||||||
|
# polkit-kde ships the agent (polkit-gnome is orphaned, EPEL-only).
|
||||||
|
# BUT Fedora's autostart .desktop carries X-systemd-skip=true (and a
|
||||||
|
# OnlyShowIn on some versions): niri starts apps via systemd's
|
||||||
|
# xdg-desktop-autostart.target, which honors those flags -> agent never
|
||||||
|
# starts in a niri session (VM-proven twice). Fighting .desktop semantics
|
||||||
|
# is fragile, so we ship our own user unit bound to
|
||||||
|
# graphical-session.target, which niri/Hyprland/MangoWC all bring up.
|
||||||
|
install_packages \
|
||||||
|
xdg-desktop-portal \
|
||||||
|
polkit \
|
||||||
|
polkit-kde \
|
||||||
|
pipewire \
|
||||||
|
pipewire-pulseaudio \
|
||||||
|
wireplumber
|
||||||
|
|
||||||
|
local unit_dir="/etc/systemd/user"
|
||||||
|
if [[ ! -f "$unit_dir/polkit-agent.service" ]]; then
|
||||||
|
info "Installing polkit-agent user unit (compositor-safe)..."
|
||||||
|
sudo mkdir -p "$unit_dir"
|
||||||
|
sudo tee "$unit_dir/polkit-agent.service" >/dev/null <<'UNIT'
|
||||||
|
[Unit]
|
||||||
|
Description=PolicyKit Authentication Agent (for bare compositors)
|
||||||
|
PartOf=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
# polkit-kde's binary on Fedora (kf6 path). Restart in case the agent dies
|
||||||
|
# before the session settles.
|
||||||
|
ExecStart=/usr/libexec/kf6/polkit-kde-authentication-agent-1
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=2
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=graphical-session.target
|
||||||
|
UNIT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Enable per-user: preset-all would also do, but be explicit. This is a
|
||||||
|
# global unit dir; each user links it at first login via the preset.
|
||||||
|
if [[ ! -f /etc/systemd/user/graphical-session.target.wants/polkit-agent.service ]]; then
|
||||||
|
sudo mkdir -p /etc/systemd/user/graphical-session.target.wants
|
||||||
|
sudo ln -sf ../polkit-agent.service \
|
||||||
|
/etc/systemd/user/graphical-session.target.wants/polkit-agent.service
|
||||||
|
fi
|
||||||
|
|
||||||
|
# libvirt group: members manage qemu:///system without a polkit prompt.
|
||||||
|
# Without this, virt-manager dies on "access denied by policy" even with
|
||||||
|
# a working agent (VM-proven). Idempotent; applies after re-login.
|
||||||
|
#
|
||||||
|
# Gate on the DAEMON, not the 'libvirt' metapkg name: virt-manager only
|
||||||
|
# requires libvirt-glib client-side, so on a fresh VM libvirt can be
|
||||||
|
# functionally present via libvirt-daemon* without rpm -q libvirt passing
|
||||||
|
# (VM-proven: user stuck in yuri+wheel with virt-manager installed).
|
||||||
|
if pkg_installed_any libvirt libvirt-daemon libvirt-daemon-driver-qemu virt-manager \
|
||||||
|
|| [[ -x /usr/sbin/libvirtd ]]; then
|
||||||
|
local real_user="${SUDO_USER:-$USER}"
|
||||||
|
if ! id -nG "$real_user" 2>/dev/null | grep -qw libvirt; then
|
||||||
|
info "Adding $real_user to the libvirt group (re-login to apply)..."
|
||||||
|
sudo usermod -aG libvirt "$real_user"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ui-block.sh - TTY-safe status "block" indicator (no special fonts)
|
||||||
|
#
|
||||||
|
# busy : blue block that "breathes" (background colour pulses)
|
||||||
|
# ok : solid green block
|
||||||
|
# fail : solid red block (changed from red X per YuKu's spec)
|
||||||
|
#
|
||||||
|
# Why this works on a bare TTY: the "block" is a BACKGROUND-COLOURED SPACE,
|
||||||
|
# not a U+2588 glyph. No fonts, no UTF-8, no locale dependency.
|
||||||
|
#
|
||||||
|
# Draws on /dev/tty, never stdout: ui_choose/ui_confirm capture stdout with $(),
|
||||||
|
# so a status line on stdout would corrupt every captured answer.
|
||||||
|
|
||||||
|
_UB_FD=""
|
||||||
|
_UB_PID=""
|
||||||
|
_UB_MSG=""
|
||||||
|
|
||||||
|
_ub_tty() {
|
||||||
|
if [[ -z "$_UB_FD" ]]; then
|
||||||
|
# probe first: a bare `exec {fd}>/dev/tty` failure still prints its error
|
||||||
|
{ : >/dev/tty; } 2>/dev/null || { _UB_FD=""; return 1; }
|
||||||
|
exec {_UB_FD}>/dev/tty 2>/dev/null || { _UB_FD=""; return 1; }
|
||||||
|
fi
|
||||||
|
[[ -n "$_UB_FD" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
_ub_depth() {
|
||||||
|
local n
|
||||||
|
n=$(tput colors 2>/dev/null || echo 0)
|
||||||
|
case "$n" in ''|*[!0-9]*) n=0 ;; esac
|
||||||
|
if [[ "$n" -ge 256 ]]; then echo 256
|
||||||
|
elif [[ "$n" -ge 8 ]]; then echo 16
|
||||||
|
else echo 0; fi
|
||||||
|
# bare Linux VT: cap regardless of what tput reports
|
||||||
|
case "${TERM:-}" in linux*|console*) echo 16 ;; esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# paint one background-coloured block (a SPACE) + message
|
||||||
|
_ub_paint() {
|
||||||
|
printf '\r\033[%sm \033[0m %s' "$1" "$2" >&"$_UB_FD"
|
||||||
|
}
|
||||||
|
|
||||||
|
_ub_busy_loop() {
|
||||||
|
local depth pal=() i tick=0.05
|
||||||
|
depth=$(_ub_depth)
|
||||||
|
[[ "$depth" -eq 16 ]] && tick=0.15 # console pulse: slow, not flicker
|
||||||
|
case "$depth" in
|
||||||
|
256) pal=(17 19 21 27 33 39 45 39 33 27 21 19) ;; # deep breathe: dark navy to bright blue
|
||||||
|
16) pal=(44 44 100 100) ;; # blue <-> dark grey, constant layout
|
||||||
|
*) pal=("|" "/" "-" "\\") ;; # last-resort spinner
|
||||||
|
esac
|
||||||
|
while :; do
|
||||||
|
for i in "${pal[@]}"; do
|
||||||
|
if [[ "$depth" -eq 256 ]]; then
|
||||||
|
_ub_paint "48;5;$i" "$_UB_MSG" # 256-colour cube background
|
||||||
|
elif [[ "$depth" -eq 16 ]]; then
|
||||||
|
_ub_paint "$i" "$_UB_MSG" # blue <-> dark grey pulse
|
||||||
|
else
|
||||||
|
printf '\r[%s] %s' "$i" "$_UB_MSG" >&"$_UB_FD"
|
||||||
|
fi
|
||||||
|
sleep "$tick" || return 0
|
||||||
|
done
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
_ub_stop() {
|
||||||
|
if [[ -n "$_UB_PID" ]]; then
|
||||||
|
kill "$_UB_PID" 2>/dev/null
|
||||||
|
wait "$_UB_PID" 2>/dev/null
|
||||||
|
_UB_PID=""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ui_block_start() {
|
||||||
|
_UB_MSG="${1:-Working}"
|
||||||
|
_ub_tty || return 0 # no tty: silent no-op
|
||||||
|
_ub_stop
|
||||||
|
_ub_busy_loop &
|
||||||
|
_UB_PID=$!
|
||||||
|
}
|
||||||
|
|
||||||
|
ui_block_ok() {
|
||||||
|
local msg="${1:-Done}" d
|
||||||
|
_ub_tty || return 0
|
||||||
|
_ub_stop
|
||||||
|
d=$(_ub_depth)
|
||||||
|
case "$d" in
|
||||||
|
256) _ub_paint "48;5;34" "$msg" ;;
|
||||||
|
16) _ub_paint "42" "$msg" ;;
|
||||||
|
*) printf '\r[OK] %s' "$msg" >&"$_UB_FD" ;;
|
||||||
|
esac
|
||||||
|
printf '\033[0m\n' >&"$_UB_FD"
|
||||||
|
}
|
||||||
|
|
||||||
|
ui_block_fail() {
|
||||||
|
local msg="${1:-Failed}" d
|
||||||
|
_ub_tty || return 0
|
||||||
|
_ub_stop
|
||||||
|
d=$(_ub_depth)
|
||||||
|
case "$d" in
|
||||||
|
256) printf '\r\033[48;5;196m \033[0m %s' "$msg" >&"$_UB_FD" ;;
|
||||||
|
16) printf '\r\033[41m \033[0m %s' "$msg" >&"$_UB_FD" ;;
|
||||||
|
*) printf '\r[!] %s' "$msg" >&"$_UB_FD" ;;
|
||||||
|
esac
|
||||||
|
printf '\n' >&"$_UB_FD"
|
||||||
|
}
|
||||||
|
|
||||||
|
ui_block_close() {
|
||||||
|
_ub_stop
|
||||||
|
if [[ -n "$_UB_FD" ]]; then
|
||||||
|
exec {_UB_FD}>&- 2>/dev/null
|
||||||
|
_UB_FD=""
|
||||||
|
fi
|
||||||
|
return 0 # an EXIT trap's status becomes the script's exit code — never fail here
|
||||||
|
}
|
||||||
|
|
||||||
|
# ALWAYS install a cleanup trap in the sourcing script:
|
||||||
|
# trap 'ui_block_close' EXIT INT TERM
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
# shellcheck shell=bash
|
||||||
|
#------------------------------------------
|
||||||
|
# lib/ui.sh — gum-based TUI with plain-text fallback
|
||||||
|
#
|
||||||
|
# Every ui_* helper has three behaviors:
|
||||||
|
# 1. ASSUME_YES (unattended) -> returns the default, no UI at all
|
||||||
|
# 2. gum available + a TTY -> styled TUI (menus, confirms, spinners)
|
||||||
|
# 3. otherwise -> the classic numbered prompts
|
||||||
|
#
|
||||||
|
# The wrappers print the ANSWER on stdout (like the old ask_* did), so call
|
||||||
|
# sites never change based on which mode is active.
|
||||||
|
#------------------------------------------
|
||||||
|
|
||||||
|
if [[ -n "${_SB_UI_LOADED:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
_SB_UI_LOADED=1
|
||||||
|
|
||||||
|
# have_gum lives in lib/common.sh (install_packages needs it too).
|
||||||
|
|
||||||
|
# Single choice from a list.
|
||||||
|
# ui_choose "<header>" "<prompt>" "<default>" item1 item2 ...
|
||||||
|
# prints the chosen item verbatim; prints the default when unattended.
|
||||||
|
ui_choose() {
|
||||||
|
local header="$1" prompt="$2" default="$3"
|
||||||
|
shift 3
|
||||||
|
local items=("$@")
|
||||||
|
|
||||||
|
if [[ "$ASSUME_YES" == "true" ]]; then
|
||||||
|
echo "$default"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if have_gum; then
|
||||||
|
# gum choose returns 130 on Esc/Ctrl-C; treat as default rather than
|
||||||
|
# killing the whole install.
|
||||||
|
local sel
|
||||||
|
sel=$(gum choose --header "$header"$'\n'"$prompt" "${items[@]}") || sel="$default"
|
||||||
|
echo "${sel:-$default}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# classic fallback: numbered list. ALL display goes to stderr — stdout is
|
||||||
|
# captured by the caller ($(ui_choose ...)) and must carry ONLY the answer
|
||||||
|
# (a stray printf newline once made choice='\nno' and broke every case match).
|
||||||
|
printf "\n" >&2
|
||||||
|
color_cyan "$header" >&2
|
||||||
|
local i=1 it
|
||||||
|
for it in "${items[@]}"; do
|
||||||
|
echo " $i) $it" >&2
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
local n=${#items[@]} answer
|
||||||
|
while true; do
|
||||||
|
if ! read -t 90 -rp "$prompt [1-$n] (default: shown above): " answer; then
|
||||||
|
printf "\n" >&2
|
||||||
|
answer=""
|
||||||
|
fi
|
||||||
|
# empty/timeout -> the default item (its position in the list)
|
||||||
|
if [[ -z "$answer" ]]; then
|
||||||
|
local k=0
|
||||||
|
for it in "${items[@]}"; do
|
||||||
|
k=$((k+1))
|
||||||
|
[[ "$it" == "$default" ]] && { echo "$it"; return 0; }
|
||||||
|
done
|
||||||
|
echo "${items[-1]}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "${answer,,}" == "x" ]]; then
|
||||||
|
echo "$default"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "$answer" =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= n )); then
|
||||||
|
echo "${items[$((answer-1))]}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
err "Invalid choice: $answer"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Yes/no. ui_confirm "<prompt>" "<yes|no default>" -> prints yes/no
|
||||||
|
ui_confirm() {
|
||||||
|
local prompt="$1" default="${2:-no}"
|
||||||
|
|
||||||
|
if [[ "$ASSUME_YES" == "true" ]]; then
|
||||||
|
echo "$default"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if have_gum; then
|
||||||
|
if gum confirm "$prompt"; then
|
||||||
|
echo "yes"
|
||||||
|
else
|
||||||
|
echo "no"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "\n" >&2
|
||||||
|
read -t 60 -rp "$prompt [y/n] (default: $default): " answer || printf "\n" >&2
|
||||||
|
answer="${answer,,}"
|
||||||
|
[[ -z "$answer" ]] && answer="$default"
|
||||||
|
case "$answer" in
|
||||||
|
y|yes) echo "yes" ;;
|
||||||
|
*) echo "no" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Multi-select from a list (one per line on stdout; empty = none).
|
||||||
|
# ui_choose_multi "<header>" "<prompt>" item1 item2 ...
|
||||||
|
ui_choose_multi() {
|
||||||
|
local header="$1" prompt="$2"
|
||||||
|
shift 2
|
||||||
|
local items=("$@")
|
||||||
|
|
||||||
|
if [[ "$ASSUME_YES" == "true" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if have_gum; then
|
||||||
|
gum choose --no-limit --header "$header"$'\n'"$prompt" "${items[@]}" || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "\n" >&2
|
||||||
|
color_cyan "$header" >&2
|
||||||
|
local i=1 it
|
||||||
|
for it in "${items[@]}"; do
|
||||||
|
echo " $i) $it" >&2
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
echo " x) None / back" >&2
|
||||||
|
local sel picks=() pick
|
||||||
|
read -t 90 -rp "Pick one or more (comma-separate, default: none): " sel || printf "\n"
|
||||||
|
[[ "${sel,,}" == "x" || -z "$sel" ]] && return 0
|
||||||
|
IFS=',' read -ra picks <<< "$sel"
|
||||||
|
for pick in "${picks[@]}"; do
|
||||||
|
pick="${pick// /}"
|
||||||
|
[[ "$pick" =~ ^[0-9]+$ ]] || continue
|
||||||
|
(( pick >= 1 && pick <= ${#items[@]} )) && echo "${items[$((pick-1))]}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Spinner around a long command. Falls back to running it plainly.
|
||||||
|
# ui_run "Updating system..." sudo dnf update -y
|
||||||
|
ui_run() {
|
||||||
|
local msg="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
if have_gum; then
|
||||||
|
if gum spin --title "$msg" --show-output -- "$@"; then
|
||||||
|
ok "$msg"
|
||||||
|
else
|
||||||
|
local rc=$?
|
||||||
|
color_red " Failed: $msg"
|
||||||
|
return "$rc"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "$msg"
|
||||||
|
"$@"
|
||||||
|
local rc=$?
|
||||||
|
if (( rc == 0 )); then
|
||||||
|
ok "$msg"
|
||||||
|
else
|
||||||
|
color_red " Failed: $msg"
|
||||||
|
fi
|
||||||
|
return "$rc"
|
||||||
|
}
|
||||||
Executable
+29
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=../../lib/common.sh
|
||||||
|
source "$HERE/../../lib/common.sh"
|
||||||
|
# shellcheck source=../../lib/session.sh
|
||||||
|
source "$HERE/../../lib/session.sh"
|
||||||
|
|
||||||
|
if is_debug; then set +e; else set -e; fi
|
||||||
|
|
||||||
|
step "Desktop environment: GNOME"
|
||||||
|
rule
|
||||||
|
|
||||||
|
if pkg_installed gnome-shell; then
|
||||||
|
ok "GNOME already installed."
|
||||||
|
else
|
||||||
|
info "Installing the GNOME desktop..."
|
||||||
|
# Group names have shifted across releases; take whichever exists.
|
||||||
|
install_group_any gnome-desktop gnome-desktop-environment workstation-product-environment
|
||||||
|
install_optional gnome-tweaks gnome-extensions-app
|
||||||
|
fi
|
||||||
|
|
||||||
|
# GNOME ships and enables gdm itself. We deliberately leave it alone: a DE module
|
||||||
|
# never fights the DE over its own greeter. A TWM module switched on top will
|
||||||
|
# move the system to SDDM instead.
|
||||||
|
info "Current display manager: $(detect_display_manager)"
|
||||||
|
|
||||||
|
ok "GNOME setup complete."
|
||||||
Executable
+26
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=../../lib/common.sh
|
||||||
|
source "$HERE/../../lib/common.sh"
|
||||||
|
# shellcheck source=../../lib/session.sh
|
||||||
|
source "$HERE/../../lib/session.sh"
|
||||||
|
|
||||||
|
if is_debug; then set +e; else set -e; fi
|
||||||
|
|
||||||
|
step "Desktop environment: KDE Plasma"
|
||||||
|
rule
|
||||||
|
|
||||||
|
if pkg_installed plasma-workspace; then
|
||||||
|
ok "Plasma already installed."
|
||||||
|
else
|
||||||
|
info "Installing the KDE Plasma desktop..."
|
||||||
|
install_group_any kde-desktop kde-desktop-environment
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Plasma's native greeter is SDDM; make sure it is actually the active one.
|
||||||
|
install_packages sddm
|
||||||
|
switch_display_manager sddm
|
||||||
|
|
||||||
|
ok "Plasma setup complete."
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=../../lib/common.sh
|
||||||
|
source "$HERE/../../lib/common.sh"
|
||||||
|
# shellcheck source=../../lib/session.sh
|
||||||
|
source "$HERE/../../lib/session.sh"
|
||||||
|
|
||||||
|
if is_debug; then set +e; else set -e; fi
|
||||||
|
|
||||||
|
step "Desktop environment: XFCE"
|
||||||
|
rule
|
||||||
|
|
||||||
|
if pkg_installed xfce4-session; then
|
||||||
|
ok "XFCE already installed."
|
||||||
|
else
|
||||||
|
info "Installing the XFCE desktop..."
|
||||||
|
install_group_any xfce-desktop xfce-desktop-environment
|
||||||
|
install_optional "@xfce-extra-plugins"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# XFCE is X11. It needs an X server present even when it is only the session
|
||||||
|
# provider for chadwm.
|
||||||
|
install_packages xorg-x11-server-Xorg xorg-x11-xinit
|
||||||
|
|
||||||
|
# XFCE runs fine under SDDM too — unify on SDDM so every session has one greeter
|
||||||
|
if ! pkg_installed sddm; then
|
||||||
|
install_packages sddm
|
||||||
|
fi
|
||||||
|
switch_display_manager sddm
|
||||||
|
|
||||||
|
install_nerd_font JetBrainsMono
|
||||||
|
install_packages alacritty dconf
|
||||||
|
|
||||||
|
# Point XFCE's terminal preference at alacritty, seeding the config directly so
|
||||||
|
# it applies on first login rather than needing a session restart.
|
||||||
|
XFCONF_DIR="$HOME/.config/xfce4/xfconf/xfce-perchannel-xml"
|
||||||
|
mkdir -p "$XFCONF_DIR"
|
||||||
|
info "Seeding XFCE session defaults..."
|
||||||
|
cat > "$XFCONF_DIR/xfce4-session.xml" <<'EOF'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<channel name="xfce4-session" version="1.0">
|
||||||
|
<property name="general" type="empty">
|
||||||
|
<property name="TerminalEmulator" type="string" value="alacritty"/>
|
||||||
|
</property>
|
||||||
|
</channel>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
ok "XFCE setup complete."
|
||||||
Executable
+64
@@ -0,0 +1,64 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=../../lib/common.sh
|
||||||
|
source "$HERE/../../lib/common.sh"
|
||||||
|
# shellcheck source=../../lib/session.sh
|
||||||
|
source "$HERE/../../lib/session.sh"
|
||||||
|
|
||||||
|
if is_debug; then set +e; else set -e; fi
|
||||||
|
|
||||||
|
step "Tiling window manager: chadwm"
|
||||||
|
rule
|
||||||
|
|
||||||
|
# chadwm is an X11 dwm fork and is not packaged for Fedora anywhere. It is
|
||||||
|
# built from source. Note this is the only non-Wayland option in the menu.
|
||||||
|
info "Installing chadwm build dependencies..."
|
||||||
|
install_packages \
|
||||||
|
gcc make git \
|
||||||
|
libX11-devel libXft-devel libXinerama-devel imlib2-devel \
|
||||||
|
dash xsetroot feh picom rofi alacritty \
|
||||||
|
xorg-x11-server-Xorg xorg-x11-xinit
|
||||||
|
|
||||||
|
CHAD_DIR="$HOME/.config/chadwm"
|
||||||
|
|
||||||
|
if [[ ! -d "$CHAD_DIR/.git" ]]; then
|
||||||
|
info "Cloning chadwm into $CHAD_DIR..."
|
||||||
|
git clone --depth 1 https://github.com/siduck/chadwm "$CHAD_DIR"
|
||||||
|
else
|
||||||
|
ok "chadwm source already present at $CHAD_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x /usr/local/bin/chadwm ]]; then
|
||||||
|
info "Building and installing chadwm..."
|
||||||
|
# make install needs root to write /usr/local/bin
|
||||||
|
sudo make -C "$CHAD_DIR/chadwm" install
|
||||||
|
else
|
||||||
|
ok "chadwm binary already installed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Session wrapper: dwm forks do not do autostart themselves, so set up the
|
||||||
|
# wallpaper/compositor here instead of leaving the user with an empty desktop.
|
||||||
|
info "Installing session wrapper /usr/local/bin/chadwm-session..."
|
||||||
|
sudo tee /usr/local/bin/chadwm-session >/dev/null <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# chadwm session: compositor + wallpaper + status, then hand over to chadwm.
|
||||||
|
pgrep -x picom >/dev/null || picom --daemon &
|
||||||
|
if [ -d "$HOME/Pictures" ]; then
|
||||||
|
feh --bg-fill "$(find "$HOME/Pictures" -maxdepth 1 -type f \
|
||||||
|
\( -iname '*.jpg' -o -iname '*.png' \) | head -n1)" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
xsetroot -cursor_name left_ptr 2>/dev/null || true
|
||||||
|
exec chadwm
|
||||||
|
EOF
|
||||||
|
sudo chmod 755 /usr/local/bin/chadwm-session
|
||||||
|
|
||||||
|
write_x11_session chadwm "chadwm" "/usr/local/bin/chadwm-session"
|
||||||
|
|
||||||
|
install_nerd_font JetBrainsMono
|
||||||
|
|
||||||
|
install_packages sddm
|
||||||
|
switch_display_manager sddm
|
||||||
|
|
||||||
|
ok "chadwm setup complete. Log out and pick chadwm in SDDM."
|
||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=../../lib/common.sh
|
||||||
|
source "$HERE/../../lib/common.sh"
|
||||||
|
# shellcheck source=../../lib/session.sh
|
||||||
|
source "$HERE/../../lib/session.sh"
|
||||||
|
|
||||||
|
if is_debug; then set +e; else set -e; fi
|
||||||
|
|
||||||
|
step "Tiling window manager: Hyprland"
|
||||||
|
rule
|
||||||
|
|
||||||
|
# hyprland was retired from the Fedora repos after F42 (dependency churn), so it
|
||||||
|
# comes from a COPR. Which COPR has a fedora-44 chroot changes over time —
|
||||||
|
# solopasha (the usual recommendation) was rawhide-only when this was written.
|
||||||
|
# Try candidates in order; first one that enables wins. If none works, abort
|
||||||
|
# with an actionable message instead of a cryptic "chroot not found".
|
||||||
|
if ! pkg_installed hyprland; then
|
||||||
|
enabled=""
|
||||||
|
for copr in lionheartp/Hyprland hermitfeather/hyprland solopasha/hyprland; do
|
||||||
|
info "Enabling copr: $copr..."
|
||||||
|
if sudo dnf copr enable -y "$copr" >/dev/null 2>&1; then
|
||||||
|
enabled="$copr"
|
||||||
|
ok "Copr enabled: $copr"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
warn "Copr $copr has no chroot for this Fedora release; trying next..."
|
||||||
|
done
|
||||||
|
if [[ -z "$enabled" ]]; then
|
||||||
|
err "None of the Hyprland COPRs have a chroot for Fedora $(rpm -E %fedora)."
|
||||||
|
err "Check https://copr.fedorainfracloud.org/coprs/ for an up-to-date one, or:"
|
||||||
|
err " sudo dnf copr enable -y <owner>/<project> fedora-rawhide-x86_64 # last resort"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Installing Hyprland and its ecosystem..."
|
||||||
|
# COPR package sets differ; treat everything beyond the core compositor as
|
||||||
|
# optional so a name drift warns instead of killing the transaction.
|
||||||
|
install_packages hyprland xdg-desktop-portal-hyprland alacritty
|
||||||
|
install_optional hyprpaper hypridle hyprlock waybar mako wofi xwayland-satellite
|
||||||
|
|
||||||
|
install_session_prereqs
|
||||||
|
install_nerd_font JetBrainsMono
|
||||||
|
|
||||||
|
# The Fedora hyprland package ships hyprland.desktop in wayland-sessions.
|
||||||
|
install_packages sddm
|
||||||
|
switch_display_manager sddm
|
||||||
|
|
||||||
|
ok "Hyprland setup complete. Log out and pick Hyprland in SDDM."
|
||||||
Executable
+39
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=../../lib/common.sh
|
||||||
|
source "$HERE/../../lib/common.sh"
|
||||||
|
# shellcheck source=../../lib/session.sh
|
||||||
|
source "$HERE/../../lib/session.sh"
|
||||||
|
|
||||||
|
if is_debug; then set +e; else set -e; fi
|
||||||
|
|
||||||
|
step "Tiling window manager: MangoWC"
|
||||||
|
rule
|
||||||
|
|
||||||
|
# MangoWC is not in the Fedora repos; it is packaged in Terra.
|
||||||
|
add_terra_repo
|
||||||
|
|
||||||
|
info "Installing MangoWC..."
|
||||||
|
if ! sudo dnf install -y mangowc; then
|
||||||
|
err "mangowc is not available from Terra on this release."
|
||||||
|
err "Alternatives: copr dennemann/MangoWC, or build from source:"
|
||||||
|
err " https://github.com/GravityShark/mangowc"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
install_packages fuzzel alacritty
|
||||||
|
install_optional xdg-desktop-portal-wlr
|
||||||
|
install_optional xwayland-satellite
|
||||||
|
|
||||||
|
install_session_prereqs
|
||||||
|
install_nerd_font JetBrainsMono
|
||||||
|
|
||||||
|
# MangoWC's session entry is not guaranteed by the package; write one if absent.
|
||||||
|
write_wayland_session mangowc "MangoWC" "mangowc"
|
||||||
|
|
||||||
|
install_packages sddm
|
||||||
|
switch_display_manager sddm
|
||||||
|
|
||||||
|
ok "MangoWC setup complete. Log out and pick MangoWC in SDDM."
|
||||||
Executable
+31
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=../../lib/common.sh
|
||||||
|
source "$HERE/../../lib/common.sh"
|
||||||
|
# shellcheck source=../../lib/session.sh
|
||||||
|
source "$HERE/../../lib/session.sh"
|
||||||
|
|
||||||
|
if is_debug; then set +e; else set -e; fi
|
||||||
|
|
||||||
|
step "Tiling window manager: Niri"
|
||||||
|
rule
|
||||||
|
|
||||||
|
# Noctalia is in the Fedora repos since F44 (noctalia 5.3.0 in updates) —
|
||||||
|
# no third-party repo needed for this module. (Terra stays in the base
|
||||||
|
# section; mangowc still requires it.)
|
||||||
|
info "Installing Niri + Noctalia..."
|
||||||
|
install_packages niri noctalia fuzzel alacritty
|
||||||
|
# niri is Wayland-only; this lets X11 apps run inside it
|
||||||
|
install_optional xwayland-satellite
|
||||||
|
install_optional xdg-desktop-portal-wlr
|
||||||
|
|
||||||
|
install_session_prereqs
|
||||||
|
install_nerd_font JetBrainsMono
|
||||||
|
|
||||||
|
# niri ships /usr/share/wayland-sessions/niri.desktop itself, so no entry needed.
|
||||||
|
install_packages sddm
|
||||||
|
switch_display_manager sddm
|
||||||
|
|
||||||
|
ok "Niri setup complete. Log out and pick Niri in SDDM."
|
||||||
Executable
+82
@@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=../../lib/common.sh
|
||||||
|
source "$HERE/../../lib/common.sh"
|
||||||
|
|
||||||
|
if is_debug; then set +e; else set -e; fi
|
||||||
|
|
||||||
|
DOTFILES_REPO="${DOTFILES_REPO:-https://git.ku-it.org/yuri/fedora-dots.git}"
|
||||||
|
CFG_DIR="$HOME/.cfg.git"
|
||||||
|
|
||||||
|
step "Personal dotfiles: fedora-dots"
|
||||||
|
rule
|
||||||
|
|
||||||
|
if [[ ! -d "$HOME/.config" ]]; then
|
||||||
|
mkdir -p "$HOME/.config"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Install the cfg command, ALWAYS (even if the repo exists already) ---
|
||||||
|
# The alias lives in ~/.bash_aliases (user-owned, trackable in fedora-dots),
|
||||||
|
# and ~/.bashrc sources that file. Without this, `cfg` exists only on the
|
||||||
|
# machine where it was typed by hand (VM-proven: "cfg pull: command not found").
|
||||||
|
ALIASES_FILE="$HOME/.bash_aliases"
|
||||||
|
BASHRC="$HOME/.bashrc"
|
||||||
|
|
||||||
|
if ! grep -q "alias cfg=" "$ALIASES_FILE" 2>/dev/null; then
|
||||||
|
info "Installing cfg alias into ~/.bash_aliases..."
|
||||||
|
cat >> "$ALIASES_FILE" <<'EOF'
|
||||||
|
# cfg: manage the bare dotfiles repo (~/.cfg.git, work tree $HOME)
|
||||||
|
alias cfg='git --git-dir=$HOME/.cfg.git --work-tree=$HOME'
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q "bash_aliases" "$BASHRC" 2>/dev/null; then
|
||||||
|
info "Making ~/.bashrc load ~/.bash_aliases..."
|
||||||
|
touch "$BASHRC"
|
||||||
|
cat >> "$BASHRC" <<'EOF'
|
||||||
|
|
||||||
|
# user aliases
|
||||||
|
if [ -f ~/.bash_aliases ]; then . ~/.bash_aliases; fi
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$CFG_DIR" ]]; then
|
||||||
|
ok "Dotfiles already set up ($CFG_DIR exists)."
|
||||||
|
ok "cfg alias ensured. Open a new shell or run: source ~/.bashrc"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A bare repo is the storage; $HOME is the work tree. Checkout happens through
|
||||||
|
# the alias form, never a plain `git checkout` inside $HOME.
|
||||||
|
info "Cloning dotfiles (bare) into $CFG_DIR..."
|
||||||
|
# --bare is what makes ~/.cfg.git the storage; the plain clone YuKu first ran
|
||||||
|
# created a working copy instead and broke the alias (proven the hard way).
|
||||||
|
git clone --quiet --bare "$DOTFILES_REPO" "$CFG_DIR"
|
||||||
|
|
||||||
|
cfg() {
|
||||||
|
git --git-dir="$CFG_DIR" --work-tree="$HOME" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Empty repo (no commits yet): nothing to check out, but leave everything
|
||||||
|
# configured so the first `cfg add` on this machine just works.
|
||||||
|
if ! git --git-dir="$CFG_DIR" rev-parse --verify -q HEAD >/dev/null; then
|
||||||
|
warn "Dotfiles repo has no commits yet — checkout skipped."
|
||||||
|
warn "Add folders on your main machine first; here you can start fresh."
|
||||||
|
cfg config status.showUntrackedFiles no
|
||||||
|
ok "cfg alias ready: git --git-dir=$CFG_DIR --work-tree=$HOME"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Checking dotfiles out into \$HOME..."
|
||||||
|
# -f on purpose: a fresh machine HAS stock files (e.g. .bashrc); the repo's
|
||||||
|
# versions win on first setup. Untracked files are never touched.
|
||||||
|
cfg checkout -f
|
||||||
|
cfg config status.showUntrackedFiles no
|
||||||
|
|
||||||
|
ok "Dotfiles checked out. Tracked paths:"
|
||||||
|
git --git-dir="$CFG_DIR" ls-tree -r --name-only HEAD | cut -d/ -f1-2 | sort -u | sed 's/^/ /'
|
||||||
|
|
||||||
|
info "Suggested shell alias for managing them:"
|
||||||
|
info " alias cfg='git --git-dir=$CFG_DIR --work-tree=\$HOME'"
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# modules/user/nvidia.sh — install the NVIDIA driver branch that matches the
|
||||||
|
# GPU that is actually in the machine.
|
||||||
|
#
|
||||||
|
# Runs on its own:
|
||||||
|
# bash modules/user/nvidia.sh
|
||||||
|
# LSPCI_BIN=/tmp/fake-lspci bash modules/user/nvidia.sh
|
||||||
|
#
|
||||||
|
# Behaviour:
|
||||||
|
# * no NVIDIA GPU -> prints one line and exits 0
|
||||||
|
# * GPU found, SB off/setup -> installs, no signing, no MOK enrolment
|
||||||
|
# * GPU found, SB enabled -> generates + imports the akmods key, then STOPS
|
||||||
|
# and asks for a reboot. The driver is installed
|
||||||
|
# on the next run, once the key is enrolled.
|
||||||
|
# (Installing before enrolment is the documented
|
||||||
|
# way to get a black screen: RPM Fusion blacklists
|
||||||
|
# nouveau the moment the driver package lands.)
|
||||||
|
# * already installed -> skips the transaction, keeps `dnf mark user`
|
||||||
|
#
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=../../lib/common.sh
|
||||||
|
source "$HERE/../../lib/common.sh"
|
||||||
|
# shellcheck source=../../lib/ui.sh
|
||||||
|
source "$HERE/../../lib/ui.sh"
|
||||||
|
|
||||||
|
if is_debug; then set +e; else set -e; fi
|
||||||
|
|
||||||
|
# Overridable so the detection logic can be tested on a host with no NVIDIA
|
||||||
|
# hardware (same pattern as OS_RELEASE_FILE / FSTAB_FILE in lib/common.sh).
|
||||||
|
LSPCI_BIN="${LSPCI_BIN:-lspci}"
|
||||||
|
MOKUTIL_BIN="${MOKUTIL_BIN:-mokutil}"
|
||||||
|
EFIVARS_DIR="${EFIVARS_DIR:-/sys/firmware/efi/efivars}"
|
||||||
|
AKMODS_CERT="${AKMODS_CERT:-/etc/pki/akmods/certs/public_key.der}"
|
||||||
|
AKMOD_COMPILE_TIMEOUT="${AKMOD_COMPILE_TIMEOUT:-300}"
|
||||||
|
|
||||||
|
step "NVIDIA driver"
|
||||||
|
rule
|
||||||
|
|
||||||
|
#--------------------------------------------------
|
||||||
|
# Detection
|
||||||
|
#--------------------------------------------------
|
||||||
|
# Prints: "<branch>|<model>|<pciid>" or nothing at all when there is no card.
|
||||||
|
#
|
||||||
|
# Branch is derived from the chip family in the name lspci prints (hwdata always
|
||||||
|
# has it): GB/AD/GA/TU -> current, GV/GP/GM -> 580xx, GK -> 470xx, GF -> 390xx.
|
||||||
|
# The numeric device id is NOT used for the decision: the id ranges overlap
|
||||||
|
# between families, and a wrong guess installs a module the card cannot use.
|
||||||
|
detect_nvidia_gpu() {
|
||||||
|
local line model arch id
|
||||||
|
|
||||||
|
line=$("$LSPCI_BIN" -nn 2>/dev/null \
|
||||||
|
| grep -iE '(VGA compatible|3D|Display) controller' \
|
||||||
|
| grep -iE 'nvidia|\[10de:' \
|
||||||
|
| head -n1) || true
|
||||||
|
[[ -z "$line" ]] && return 1
|
||||||
|
|
||||||
|
id=$(printf '%s' "$line" \
|
||||||
|
| grep -oE '\[10de:[0-9a-fA-F]{4}\]' | head -n1 | tr -d '[]') || true
|
||||||
|
|
||||||
|
# "01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA102
|
||||||
|
# [GeForce RTX 3080 Ti] [10de:2208] (rev a1)" -> "GA102 [GeForce RTX 3080 Ti]"
|
||||||
|
model=$(printf '%s' "$line" \
|
||||||
|
| sed -n 's/.*NVIDIA Corporation \(.*\)\[10de:[0-9a-fA-F]\{4\}\].*/\1/p' \
|
||||||
|
| sed -e 's/[[:space:]]*$//') || true
|
||||||
|
[[ -z "$model" ]] && model="unknown NVIDIA device"
|
||||||
|
|
||||||
|
arch=$(printf '%s' "$model" | grep -oE '(GB|AD|GA|TU|GV|GP|GM|GK|GF)[0-9]{3}[A-Z]?' \
|
||||||
|
| head -n1 | grep -oE '^(GB|AD|GA|TU|GV|GP|GM|GK|GF)') || true
|
||||||
|
|
||||||
|
local branch="unknown"
|
||||||
|
case "$arch" in
|
||||||
|
GB|AD|GA|TU) branch="current" ;; # Turing and newer: open kernel module
|
||||||
|
GV|GP|GM) branch="580xx" ;; # Volta / Pascal / Maxwell (last branch)
|
||||||
|
GK) branch="470xx" ;; # Kepler, Xorg only
|
||||||
|
GF) branch="390xx" ;; # Fermi, Xorg only, EOL upstream
|
||||||
|
"")
|
||||||
|
# Pre-Fermi naming (G80/G92/GT200/GT215...) has no architecture
|
||||||
|
# prefix; those cards need the EOL 340xx branch plus a longterm
|
||||||
|
# kernel, which this script deliberately does not automate.
|
||||||
|
if printf '%s' "$model" | grep -qE '(^|[^A-Za-z])(G[0-9]{2,3}|GT[0-9]{3}|NV[0-9]{2})'; then
|
||||||
|
branch="340xx"
|
||||||
|
else
|
||||||
|
branch="unknown"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
printf '%s|%s|%s\n' "$branch" "$model" "${id:-10de:unknown}"
|
||||||
|
}
|
||||||
|
|
||||||
|
gpu_info=""
|
||||||
|
if ! command -v "$LSPCI_BIN" >/dev/null 2>&1; then
|
||||||
|
info "pciutils missing (no lspci). Installing..."
|
||||||
|
install_optional pciutils
|
||||||
|
fi
|
||||||
|
if command -v "$LSPCI_BIN" >/dev/null 2>&1; then
|
||||||
|
gpu_info=$(detect_nvidia_gpu) || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$gpu_info" ]]; then
|
||||||
|
info "No NVIDIA GPU detected. Nothing to do."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
BRANCH="${gpu_info%%|*}"
|
||||||
|
rest="${gpu_info#*|}"
|
||||||
|
MODEL="${rest%%|*}"
|
||||||
|
PCIID="${rest#*|}"
|
||||||
|
|
||||||
|
ok "GPU: $MODEL ($PCIID)"
|
||||||
|
|
||||||
|
# Multiple cards of different generations are not something one dnf transaction
|
||||||
|
# can serve correctly: the packages are mutually exclusive.
|
||||||
|
nv_count=$("$LSPCI_BIN" -nn 2>/dev/null \
|
||||||
|
| grep -icE '(VGA compatible|3D|Display) controller.*(nvidia|\[10de:)') || true
|
||||||
|
if [[ "${nv_count:-0}" -gt 1 ]]; then
|
||||||
|
warn "Found $nv_count NVIDIA display devices. Only the first one determines"
|
||||||
|
warn "the driver branch; mixed generations on one machine need manual work."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A 3D controller (rather than a VGA/Display controller) is the Optimus layout.
|
||||||
|
if printf '%s' "$("$LSPCI_BIN" -nn 2>/dev/null | grep -i '10de:' | head -n1)" \
|
||||||
|
| grep -q '3D controller'; then
|
||||||
|
info "Hybrid (Optimus) layout: Fedora handles the iGPU/dGPU split automatically."
|
||||||
|
fi
|
||||||
|
|
||||||
|
#--------------------------------------------------
|
||||||
|
# Branch -> packages
|
||||||
|
#--------------------------------------------------
|
||||||
|
if [[ "$BRANCH" == "unknown" ]]; then
|
||||||
|
warn "Could not identify the GPU generation from '$MODEL'."
|
||||||
|
choice=$(ui_choose "NVIDIA driver branch" "Which driver branch does this card need?" \
|
||||||
|
"current (Turing / Ampere / Ada / Blackwell)" \
|
||||||
|
"current (Turing / Ampere / Ada / Blackwell)" \
|
||||||
|
"580xx (Maxwell / Pascal / Volta)" \
|
||||||
|
"470xx (Kepler)" \
|
||||||
|
"390xx (Fermi)")
|
||||||
|
case "$choice" in
|
||||||
|
580xx*) BRANCH="580xx" ;;
|
||||||
|
470xx*) BRANCH="470xx" ;;
|
||||||
|
390xx*) BRANCH="390xx" ;;
|
||||||
|
*) BRANCH="current" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$BRANCH" in
|
||||||
|
current)
|
||||||
|
PKGS=(akmod-nvidia xorg-x11-drv-nvidia-cuda)
|
||||||
|
MARK="akmod-nvidia"
|
||||||
|
BRANCH_LABEL="current (Turing/Ampere/Ada/Blackwell, open kernel module)"
|
||||||
|
;;
|
||||||
|
580xx)
|
||||||
|
PKGS=(akmod-nvidia-580xx xorg-x11-drv-nvidia-580xx xorg-x11-drv-nvidia-580xx-cuda)
|
||||||
|
MARK="akmod-nvidia-580xx"
|
||||||
|
BRANCH_LABEL="580xx (Maxwell/Pascal/Volta, last branch for those cards)"
|
||||||
|
;;
|
||||||
|
470xx)
|
||||||
|
PKGS=(akmod-nvidia-470xx xorg-x11-drv-nvidia-470xx xorg-x11-drv-nvidia-470xx-cuda)
|
||||||
|
MARK="akmod-nvidia-470xx"
|
||||||
|
BRANCH_LABEL="470xx (Kepler, Xorg only)"
|
||||||
|
;;
|
||||||
|
390xx)
|
||||||
|
PKGS=(akmod-nvidia-390xx xorg-x11-drv-nvidia-390xx xorg-x11-drv-nvidia-390xx-cuda)
|
||||||
|
MARK="akmod-nvidia-390xx"
|
||||||
|
BRANCH_LABEL="390xx (Fermi, EOL upstream, Xorg only)"
|
||||||
|
;;
|
||||||
|
340xx)
|
||||||
|
warn "This card predates Fermi. Driver 340xx is end-of-life and needs a"
|
||||||
|
warn "longterm kernel from a COPR, which this script does not automate."
|
||||||
|
warn "See https://rpmfusion.org/Howto/NVIDIA and install it by hand."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
info "Driver branch: $BRANCH_LABEL"
|
||||||
|
if [[ "$BRANCH" != "current" ]]; then
|
||||||
|
warn "This branch has no Wayland support. Plan on an X11 session."
|
||||||
|
fi
|
||||||
|
|
||||||
|
#--------------------------------------------------
|
||||||
|
# Secure Boot gate
|
||||||
|
#--------------------------------------------------
|
||||||
|
# enabled -> sign and enrol, then stop for a reboot
|
||||||
|
# disabled / setup mode -> no signing at all (an import request would never be
|
||||||
|
# processed: shim skips MokManager when enforcement is
|
||||||
|
# off, so the request just sits in the efivars)
|
||||||
|
sb_state() {
|
||||||
|
local out sb_var pk_var
|
||||||
|
|
||||||
|
if command -v "$MOKUTIL_BIN" >/dev/null 2>&1; then
|
||||||
|
out=$("$MOKUTIL_BIN" --sb-state 2>/dev/null) || true
|
||||||
|
case "$out" in
|
||||||
|
*"SecureBoot enabled"*) echo "enabled"; return 0 ;;
|
||||||
|
*"Setup mode"*) echo "setup"; return 0 ;;
|
||||||
|
*"SecureBoot disabled"*) echo "disabled"; return 0 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback: read the efivars directly (byte 5 of the variable data).
|
||||||
|
if [[ -d "$EFIVARS_DIR" ]]; then
|
||||||
|
sb_var=$(ls "$EFIVARS_DIR"/SecureBoot-* 2>/dev/null | head -n1) || true
|
||||||
|
pk_var=$(ls "$EFIVARS_DIR"/PK-* 2>/dev/null | head -n1) || true
|
||||||
|
if [[ -n "$sb_var" ]]; then
|
||||||
|
if [[ "$(od -An -t u1 -j 4 -N 1 "$sb_var" 2>/dev/null | tr -d '[:space:]')" == "1" ]]; then
|
||||||
|
echo "enabled"; return 0
|
||||||
|
fi
|
||||||
|
# No Platform Key enrolled = Setup mode: nothing can be enforced.
|
||||||
|
[[ -z "$pk_var" ]] && { echo "setup"; return 0; }
|
||||||
|
echo "disabled"; return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
SB_STATE=$(sb_state)
|
||||||
|
case "$SB_STATE" in
|
||||||
|
enabled)
|
||||||
|
info "Secure Boot is enabled: the module has to be signed and the key enrolled."
|
||||||
|
install_packages akmods
|
||||||
|
|
||||||
|
if [[ ! -f "$AKMODS_CERT" ]]; then
|
||||||
|
info "Generating the akmods signing key..."
|
||||||
|
sudo kmodgenca -a
|
||||||
|
else
|
||||||
|
ok "akmods signing key present."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if sudo "$MOKUTIL_BIN" --test-key "$AKMODS_CERT" 2>&1 | grep -qi 'not enrolled'; then
|
||||||
|
warn "The akmods key is not enrolled yet. Importing it now."
|
||||||
|
sudo "$MOKUTIL_BIN" --import "$AKMODS_CERT"
|
||||||
|
printf "\n"
|
||||||
|
color_yellow "------------------------------------------------------------"
|
||||||
|
color_yellow " ACTION REQUIRED: enrolment is pending"
|
||||||
|
color_yellow "------------------------------------------------------------"
|
||||||
|
info "The driver is deliberately NOT installed yet: with Secure Boot"
|
||||||
|
info "enforcing, an unsigned module leaves you with a black screen."
|
||||||
|
printf "\n"
|
||||||
|
info "1. Reboot now."
|
||||||
|
info "2. At the boot screen press a key during the countdown."
|
||||||
|
info "3. Enroll MOK -> View key 0 (check the fingerprint) -> Continue"
|
||||||
|
info " -> Yes -> type the password you just set. The keyboard there"
|
||||||
|
info " is QWERTY, so use a short, symbol-free password."
|
||||||
|
info "4. Boot Fedora and run this setup again: it will install the"
|
||||||
|
info " driver now that the key is trusted."
|
||||||
|
printf "\n"
|
||||||
|
warn "If you miss the countdown the request is dropped silently and"
|
||||||
|
warn "you have to import again."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
ok "akmods key already enrolled."
|
||||||
|
;;
|
||||||
|
setup)
|
||||||
|
ok "Secure Boot is off (platform is in Setup mode): no signing needed."
|
||||||
|
warn "Setup mode is a temporary state: a firmware update or 'Restore"
|
||||||
|
warn "Factory Keys' turns enforcement on, and this unsigned module then"
|
||||||
|
warn "stops loading at the next boot. Enrol the key before you need it:"
|
||||||
|
warn " sudo dnf install akmods && sudo kmodgenca -a"
|
||||||
|
warn " sudo mokutil --import /etc/pki/akmods/certs/public_key.der"
|
||||||
|
;;
|
||||||
|
disabled)
|
||||||
|
ok "Secure Boot is disabled: no signing needed."
|
||||||
|
warn "Keep it that way deliberately: a firmware update or 'Restore Factory"
|
||||||
|
warn "Keys' can enable enforcement, and the unsigned module then fails to"
|
||||||
|
warn "load at the next boot."
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
warn "Could not determine the Secure Boot state. Assuming it is off."
|
||||||
|
warn "If Secure Boot actually is on, the driver will not load: check"
|
||||||
|
warn "'mokutil --sb-state' and enrol the akmods key first."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
#--------------------------------------------------
|
||||||
|
# Install
|
||||||
|
#--------------------------------------------------
|
||||||
|
if pkg_installed_any "${PKGS[@]}"; then
|
||||||
|
ok "NVIDIA driver packages already installed."
|
||||||
|
else
|
||||||
|
info "Installing: ${PKGS[*]}"
|
||||||
|
if ! install_packages "${PKGS[@]}"; then
|
||||||
|
err "Driver installation failed for branch $BRANCH."
|
||||||
|
err "Check that RPM Fusion nonfree is enabled: dnf repolist | grep rpmfusion"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# An akmod is removable as an "unused dependency" unless it is marked user
|
||||||
|
# installed; `dnf autoremove` has taken the driver (and its compiler) away on
|
||||||
|
# people before.
|
||||||
|
if ! sudo dnf mark user "$MARK" >/dev/null 2>&1; then
|
||||||
|
warn "Could not mark $MARK as user-installed (dnf autoremove may remove it)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
#--------------------------------------------------
|
||||||
|
# Wait for the module build
|
||||||
|
#--------------------------------------------------
|
||||||
|
# dnf returns before the kmod exists. Rebooting in that window gives a boot with
|
||||||
|
# no driver at all, because nouveau is already blacklisted by the driver package.
|
||||||
|
NEWEST_KERNEL=$(rpm -q --qf '%{VERSION}-%{RELEASE}.%{ARCH}\n' kernel-core 2>/dev/null \
|
||||||
|
| sort -V | tail -n1) || true
|
||||||
|
|
||||||
|
info "Waiting for the akmod build (up to ${AKMOD_COMPILE_TIMEOUT}s)..."
|
||||||
|
built=false
|
||||||
|
deadline=$(( SECONDS + AKMOD_COMPILE_TIMEOUT ))
|
||||||
|
while (( SECONDS < deadline )); do
|
||||||
|
if [[ -n "$NEWEST_KERNEL" ]] && modinfo -k "$NEWEST_KERNEL" -F version nvidia >/dev/null 2>&1; then
|
||||||
|
built=true; break
|
||||||
|
fi
|
||||||
|
if [[ -z "$NEWEST_KERNEL" ]] && modinfo -F version nvidia >/dev/null 2>&1; then
|
||||||
|
built=true; break
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$built" == "true" ]]; then
|
||||||
|
if [[ -n "$NEWEST_KERNEL" ]]; then
|
||||||
|
VER=$(modinfo -k "$NEWEST_KERNEL" -F version nvidia 2>/dev/null) || true
|
||||||
|
else
|
||||||
|
VER=$(modinfo -F version nvidia 2>/dev/null) || true
|
||||||
|
fi
|
||||||
|
ok "Kernel module built: nvidia ${VER:-unknown}"
|
||||||
|
if [[ -n "$NEWEST_KERNEL" ]] && ! [[ "$NEWEST_KERNEL" == "$(uname -r)" ]]; then
|
||||||
|
warn "Built for $NEWEST_KERNEL but you are running $(uname -r): reboot."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "The module is not built yet after ${AKMOD_COMPILE_TIMEOUT}s."
|
||||||
|
warn "Do NOT reboot until it is: the next boot would have no driver."
|
||||||
|
warn "Check the build log, then retry:"
|
||||||
|
warn " ls -l /var/cache/akmods/nvidia/"
|
||||||
|
warn " sudo akmods --force"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#--------------------------------------------------
|
||||||
|
# Optional extras
|
||||||
|
#--------------------------------------------------
|
||||||
|
printf "\n"
|
||||||
|
extras=$(ui_confirm "Install the optional graphics extras (VA-API decode, vulkan)?" "no")
|
||||||
|
if [[ "$extras" == "yes" ]]; then
|
||||||
|
install_optional libva-nvidia-driver libva-utils vulkan
|
||||||
|
fi
|
||||||
|
|
||||||
|
#--------------------------------------------------
|
||||||
|
# What to check after the reboot
|
||||||
|
#--------------------------------------------------
|
||||||
|
printf "\n"
|
||||||
|
info "After the reboot, verify with:"
|
||||||
|
info " nvidia-smi"
|
||||||
|
info " cat /sys/module/nvidia_drm/parameters/modeset # expect Y"
|
||||||
|
info " lsmod | grep -Ei 'nouveau|nova_core' # expect nothing"
|
||||||
|
warn "A reboot is required before the driver is active."
|
||||||
File diff suppressed because it is too large
Load Diff
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
# Vendored: SysGuides snapper scripts for Fedora
|
||||||
|
|
||||||
|
Source: https://github.com/SysGuides/sysguides-snapper-fedora (MIT, Madhu Desai / SysGuides)
|
||||||
|
|
||||||
|
Vendored (2026-10-27, upstream commit 0a28eb4) so setup does not depend on an
|
||||||
|
external repo at runtime. `lib/common.sh:setup_snapper` installs:
|
||||||
|
|
||||||
|
- scripts/snapper-*.sh -> /usr/local/bin/ (DNF5 pre/post snapshot actions)
|
||||||
|
- config/snapper.actions -> /etc/dnf/libdnf5-plugins/actions.d/
|
||||||
|
|
||||||
|
To update: re-fetch from upstream, diff, commit here.
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
# snapper.actions
|
||||||
|
# ----------------
|
||||||
|
# Snapper integration with libdnf5 (DNF5 transactions)
|
||||||
|
#
|
||||||
|
# This file defines hooks that:
|
||||||
|
# - Create PRE snapshots before transactions
|
||||||
|
# - Capture GUI package details
|
||||||
|
# - Create POST snapshots after transactions
|
||||||
|
# - Apply WAL checkpoint fix for consistency
|
||||||
|
#
|
||||||
|
# Project: sysguides-snapper-fedora
|
||||||
|
# Author: Madhu Desai (SysGuides)
|
||||||
|
# Website: https://sysguides.com
|
||||||
|
# GitHub: https://github.com/SysGuides/sysguides-snapper-fedora
|
||||||
|
|
||||||
|
# Create PRE snapshot
|
||||||
|
pre_transaction::::/usr/local/bin/snapper-pre.sh ${pid}
|
||||||
|
|
||||||
|
# Capture GUI package info (incoming packages)
|
||||||
|
pre_transaction:*:in::/usr/local/bin/snapper-gui-pkg.sh ${pid} ${pkg.action} ${pkg.name}
|
||||||
|
|
||||||
|
# Capture GUI package info (outgoing packages)
|
||||||
|
pre_transaction:*:out::/usr/local/bin/snapper-gui-pkg.sh ${pid} ${pkg.action} ${pkg.name}
|
||||||
|
|
||||||
|
# Create POST snapshot (with WAL fix)
|
||||||
|
post_transaction::::/usr/local/bin/snapper-post.sh ${pid}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# snapper-desc.sh
|
||||||
|
# ----------------
|
||||||
|
# Determines a human-readable description for the transaction.
|
||||||
|
#
|
||||||
|
# This script inspects the process that triggered the DNF5 transaction.
|
||||||
|
#
|
||||||
|
# Behavior:
|
||||||
|
# - If the transaction originates from GUI tools (PackageKit / dnf5daemon),
|
||||||
|
# it returns "GUI"
|
||||||
|
# - Otherwise, it returns the actual command (e.g., "dnf install vim")
|
||||||
|
#
|
||||||
|
# This description is later used by Snapper for snapshot naming.
|
||||||
|
#
|
||||||
|
# Project: sysguides-snapper-fedora
|
||||||
|
# Author: Madhu Desai (SysGuides)
|
||||||
|
# Website: https://sysguides.com
|
||||||
|
# GitHub: https://github.com/SysGuides/sysguides-snapper-fedora
|
||||||
|
|
||||||
|
PID="$1"
|
||||||
|
|
||||||
|
# Get the full command of the originating process
|
||||||
|
cmd=$(ps -o command --no-headers -p "$PID" 2>/dev/null || echo "Unknown Task")
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
*/dnf5daemon* | */packagekitd*)
|
||||||
|
# GUI-based transaction
|
||||||
|
echo "GUI"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# CLI or unknown source
|
||||||
|
echo "$cmd"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# snapper-gui-pkg.sh
|
||||||
|
# ------------------
|
||||||
|
# Captures the first package involved in a GUI-based transaction.
|
||||||
|
#
|
||||||
|
# This script:
|
||||||
|
# - Runs during DNF5 transaction events (pre_transaction hooks)
|
||||||
|
# - Detects if the transaction originated from a GUI
|
||||||
|
# - Records the first package name and action (install/remove)
|
||||||
|
#
|
||||||
|
# This allows snapshots to have meaningful descriptions like:
|
||||||
|
# "GUI install firefox"
|
||||||
|
#
|
||||||
|
# Only the first package is recorded to keep descriptions concise.
|
||||||
|
#
|
||||||
|
# Project: sysguides-snapper-fedora
|
||||||
|
# Author: Madhu Desai (SysGuides)
|
||||||
|
# Website: https://sysguides.com
|
||||||
|
# GitHub: https://github.com/SysGuides/sysguides-snapper-fedora
|
||||||
|
|
||||||
|
PID="$1"
|
||||||
|
ACTION="$2"
|
||||||
|
NAME="$3"
|
||||||
|
|
||||||
|
STATE_DIR="/run/snapper-actions"
|
||||||
|
DESC_FILE="$STATE_DIR/snapper_desc_${PID}"
|
||||||
|
PKG_FILE="$STATE_DIR/snapper_gui_${PID}"
|
||||||
|
|
||||||
|
# Read previously stored description (set in PRE phase)
|
||||||
|
desc=$(cat "$DESC_FILE" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Only proceed if this is a GUI transaction
|
||||||
|
[[ "$desc" != "GUI" ]] && exit 0
|
||||||
|
|
||||||
|
# Only capture the first package to avoid overwriting
|
||||||
|
[[ -f "$PKG_FILE" ]] && exit 0
|
||||||
|
|
||||||
|
# Determine action type and store description
|
||||||
|
case "$ACTION" in
|
||||||
|
I|U|D|R)
|
||||||
|
# Install / upgrade / downgrade / reinstall
|
||||||
|
echo "GUI install ${NAME}" > "$PKG_FILE"
|
||||||
|
;;
|
||||||
|
E|O)
|
||||||
|
# Erase / obsolete
|
||||||
|
echo "GUI remove ${NAME}" > "$PKG_FILE"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# snapper-post.sh
|
||||||
|
# ----------------
|
||||||
|
# Creates a POST snapshot after a DNF5 transaction completes.
|
||||||
|
#
|
||||||
|
# This script:
|
||||||
|
# - Retrieves stored PRE snapshot and description
|
||||||
|
# - Improves description using GUI package info (if available)
|
||||||
|
# - Applies SQLite WAL checkpoint fix for libdnf5
|
||||||
|
# - Creates the POST snapshot linked to the PRE snapshot
|
||||||
|
# - Cleans up temporary state files
|
||||||
|
#
|
||||||
|
# The WAL checkpoint is necessary on Fedora 44+ to reduce
|
||||||
|
# inconsistencies between filesystem state and RPM database.
|
||||||
|
#
|
||||||
|
# Project: sysguides-snapper-fedora
|
||||||
|
# Author: Madhu Desai (SysGuides)
|
||||||
|
# Website: https://sysguides.com
|
||||||
|
# GitHub: https://github.com/SysGuides/sysguides-snapper-fedora
|
||||||
|
|
||||||
|
PID="$1"
|
||||||
|
STATE_DIR="/run/snapper-actions"
|
||||||
|
|
||||||
|
DESC_FILE="$STATE_DIR/snapper_desc_${PID}"
|
||||||
|
PRE_FILE="$STATE_DIR/snapper_pre_${PID}"
|
||||||
|
GUI_FILE="$STATE_DIR/snapper_gui_${PID}"
|
||||||
|
|
||||||
|
# Load stored state (if available)
|
||||||
|
desc=$(cat "$DESC_FILE" 2>/dev/null || echo "")
|
||||||
|
pre=$(cat "$PRE_FILE" 2>/dev/null || echo "")
|
||||||
|
gui_pkg=$(cat "$GUI_FILE" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# If no PRE snapshot exists, nothing to do
|
||||||
|
[[ -z "$pre" ]] && exit 0
|
||||||
|
|
||||||
|
# If GUI package info exists, improve snapshot description
|
||||||
|
if [[ -n "$gui_pkg" ]]; then
|
||||||
|
desc="$gui_pkg"
|
||||||
|
snapper -c root modify -d "$desc" "$pre" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Apply WAL checkpoint (best-effort; do not fail if it doesn't succeed)
|
||||||
|
/usr/local/bin/snapper-wal-checkpoint.sh || true
|
||||||
|
|
||||||
|
# Create POST snapshot linked to PRE snapshot
|
||||||
|
snapper -c root create -c number -t post \
|
||||||
|
--pre-number "$pre" -d "$desc"
|
||||||
|
|
||||||
|
# Clean up temporary state files
|
||||||
|
rm -f "$DESC_FILE" "$PRE_FILE" "$GUI_FILE"
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# snapper-pre.sh
|
||||||
|
# ----------------
|
||||||
|
# Creates a PRE snapshot before a DNF5 transaction begins.
|
||||||
|
#
|
||||||
|
# This script:
|
||||||
|
# - Generates a description using snapper-desc.sh
|
||||||
|
# - Stores the description and snapshot number in /run for later use
|
||||||
|
#
|
||||||
|
# The stored data is used by the POST script to:
|
||||||
|
# - Pair snapshots correctly
|
||||||
|
# - Improve snapshot descriptions (especially for GUI installs)
|
||||||
|
#
|
||||||
|
# Project: sysguides-snapper-fedora
|
||||||
|
# Author: Madhu Desai (SysGuides)
|
||||||
|
# Website: https://sysguides.com
|
||||||
|
# GitHub: https://github.com/SysGuides/sysguides-snapper-fedora
|
||||||
|
|
||||||
|
PID="$1"
|
||||||
|
STATE_DIR="/run/snapper-actions"
|
||||||
|
|
||||||
|
# Ensure runtime directory exists with secure permissions
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
chmod 700 "$STATE_DIR"
|
||||||
|
|
||||||
|
# On a fresh setup, DNF5 may fail with "filesystem error: cannot copy: packages.toml"
|
||||||
|
# if this directory is missing, leaving an orphaned PRE snapshot with no POST.
|
||||||
|
# This block ensures the directory exists before the transaction proceeds.
|
||||||
|
if [[ ! -d /usr/lib/sysimage/libdnf5 ]]; then
|
||||||
|
mkdir -p /usr/lib/sysimage/libdnf5
|
||||||
|
restorecon -q /usr/lib/sysimage/libdnf5 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get transaction description (GUI or CLI command)
|
||||||
|
desc=$(/usr/local/bin/snapper-desc.sh "$PID")
|
||||||
|
|
||||||
|
# Store description for use in POST phase
|
||||||
|
echo "$desc" > "$STATE_DIR/snapper_desc_${PID}"
|
||||||
|
|
||||||
|
# Create PRE snapshot and store snapshot number
|
||||||
|
pre=$(snapper -c root create -c number -t pre -p -d "$desc") || exit 1
|
||||||
|
|
||||||
|
echo "$pre" > "$STATE_DIR/snapper_pre_${PID}"
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# snapper-wal-checkpoint.sh
|
||||||
|
# --------------------------
|
||||||
|
# Forces an SQLite WAL checkpoint for the RPM database used by libdnf5.
|
||||||
|
#
|
||||||
|
# Fedora 44 introduced DNF5 (libdnf5), which uses SQLite with WAL mode.
|
||||||
|
# This can lead to inconsistencies during snapper undochange, where:
|
||||||
|
# - Files are reverted correctly
|
||||||
|
# - But the RPM database still reports packages as installed
|
||||||
|
#
|
||||||
|
# This script attempts to flush WAL data to the main database file
|
||||||
|
# before the POST snapshot is created.
|
||||||
|
#
|
||||||
|
# The operation is best-effort:
|
||||||
|
# - Retries multiple times if the database is busy
|
||||||
|
# - Fails silently if unable to complete
|
||||||
|
#
|
||||||
|
# Project: sysguides-snapper-fedora
|
||||||
|
# Author: Madhu Desai (SysGuides)
|
||||||
|
# Website: https://sysguides.com
|
||||||
|
# GitHub: https://github.com/SysGuides/sysguides-snapper-fedora
|
||||||
|
|
||||||
|
python3 - <<'EOF'
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
DB = "/usr/lib/sysimage/rpm/rpmdb.sqlite"
|
||||||
|
|
||||||
|
# Try up to 10 times to checkpoint WAL (single process, no respawn overhead)
|
||||||
|
for i in range(10):
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB, timeout=3)
|
||||||
|
conn.execute("PRAGMA busy_timeout=3000")
|
||||||
|
result = conn.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone()
|
||||||
|
conn.close()
|
||||||
|
# result[1] = number of remaining frames in WAL (0 = fully flushed)
|
||||||
|
if result and result[1] == 0:
|
||||||
|
sys.exit(0)
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Best-effort fallback
|
||||||
|
sys.exit(1)
|
||||||
|
EOF
|
||||||
Reference in New Issue
Block a user