Compare commits

..
1 Commits
Author SHA1 Message Date
Pathfinder 9c0351a542 scratch: verify direct-push access (will be deleted) 2026-10-27 11:53:17 +01:00
16 changed files with 117 additions and 1880 deletions
+1
View File
@@ -0,0 +1 @@
pathfinder token verification 2026-10-27T10:53:17Z
-37
View File
@@ -34,8 +34,6 @@ lib/common.sh colours, logging, dnf helpers, rpmfusion/terra, nerd f
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
@@ -77,41 +75,6 @@ Each module pulls in `lib/` by relative path, so it works both ways.
`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,
+17 -226
View File
@@ -71,156 +71,15 @@ pause_if_debug() {
#------------------------------------------
# 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: $*"
}
@@ -233,17 +92,6 @@ pkg_installed() {
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
@@ -255,7 +103,7 @@ remove_packages() {
done
if [ ${#packages_to_remove[@]} -gt 0 ]; then
ui_quiet_run "Removing: ${packages_to_remove[*]}" \
info "Removing packages: ${packages_to_remove[*]}"
sudo dnf remove -y "${packages_to_remove[@]}"
fi
}
@@ -270,7 +118,7 @@ install_group() {
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"
sudo dnf group install -y "$group"
}
# Try each candidate group/environment in turn; first one that exists wins.
@@ -279,7 +127,7 @@ install_group_any() {
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"
sudo dnf group install -y "$g"
return 0
fi
done
@@ -291,17 +139,9 @@ install_group_any() {
#------------------------------------------
# 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."
if pkg_installed terra-release; then
ok "Terra repository already installed."
return 0
fi
info "Adding Terra repository (Fyra Labs)..."
@@ -315,9 +155,8 @@ add_terra_repo() {
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."
if pkg_installed rpmfusion-free-release rpmfusion-nonfree-release; then
ok "RPM Fusion already installed."
return 0
fi
info "Enabling RPM Fusion (free + nonfree) for Fedora ${rel}..."
@@ -382,9 +221,6 @@ disable_selinux() {
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."
@@ -422,14 +258,9 @@ ensure_btrfs_compression() {
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`.
# Full snapshot setup via the SysGuides installer (snapper + grub-btrfs +
# DNF5 pre/post snapshot actions). The installer re-validates btrfs itself.
setup_snapper() {
local vendor="$SCRIPT_DIR/vendor/snapper"
if pkg_installed snapper && [[ -d /.snapshots ]]; then
ok "Snapper already configured (config for / exists)."
return 0
@@ -438,57 +269,17 @@ setup_snapper() {
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)..."
info "Setting up snapper + grub-btrfs (SysGuides installer)..."
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
git clone --depth 1 https://github.com/SysGuides/sysguides-snapper-fedora "$tmp/sysguides" \
|| { err "Could not clone the SysGuides installer."; rm -rf "$tmp"; return 1; }
( cd "$tmp/sysguides" && ./install.sh )
local rc=$?
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
if (( rc == 0 )); then
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"
fi
return $rc
}
-54
View File
@@ -82,64 +82,10 @@ write_x11_session() {
# 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
View File
@@ -1,118 +0,0 @@
#!/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
-170
View File
@@ -1,170 +0,0 @@
# 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"
}
-82
View File
@@ -1,82 +0,0 @@
#!/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'"
-354
View File
@@ -1,354 +0,0 @@
#!/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."
+92 -568
View File
@@ -10,16 +10,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
# shellcheck source=lib/session.sh
source "$SCRIPT_DIR/lib/session.sh"
# shellcheck source=lib/ui.sh
source "$SCRIPT_DIR/lib/ui.sh"
# shellcheck source=lib/ui-block.sh
source "$SCRIPT_DIR/lib/ui-block.sh"
# never leave a block pulse loop running if the script is interrupted
trap 'ui_block_close' EXIT INT TERM
# Full dnf output from quiet transactions lands here (shown on failure).
export SETUP_DNF_LOG="${SETUP_DNF_LOG:-/tmp/fedora-setup-dnf-$(id -u).log}"
: >> "$SETUP_DNF_LOG"
#------------------------------------------
# Defaults + argument parsing
@@ -31,8 +21,6 @@ DE=""
TWM=""
SELINUX_OPT=""
SNAPPER_OPT=""
DOTFILES_OPT=""
NVIDIA_OPT=""
usage() {
cat <<'EOF'
@@ -47,10 +35,7 @@ Options:
--de=NAME gnome | plasma | xfce | none
--twm=NAME chadwm | hyprland | mango | niri | none
--selinux=X keep | disabled (asked, default keep)
--snapper=X yes | no (automatic on btrfs; use 'no' to opt out)
--dotfiles=X yes | no (clone personal dotfiles, default no)
--nvidia=X auto | no (auto: install the matching NVIDIA driver
when an NVIDIA GPU is detected; default auto)
--snapper=X yes | no (asked when / is btrfs, default yes)
-y, --yes never prompt (requires --de and --twm)
-d, --debug pause after each section instead of aborting on error
-h, --help show this help
@@ -74,8 +59,6 @@ for arg in "$@"; do
--de=*) DE="${arg#*=}" ;;
--selinux=*) SELINUX_OPT="${arg#*=}" ;;
--snapper=*) SNAPPER_OPT="${arg#*=}" ;;
--dotfiles=*) DOTFILES_OPT="${arg#*=}" ;;
--nvidia=*) NVIDIA_OPT="${arg#*=}" ;;
--twm=*) TWM="${arg#*=}" ;;
-h|--help) usage; exit 0 ;;
*)
@@ -111,14 +94,6 @@ if [[ ${EUID} -eq 0 ]]; then
exit 1
fi
# Ask for the sudo password ONCE, up front, on a clean screen. Otherwise the
# first sudo call mid-run prints its prompt straight into the live progress
# line ("Installing (optional): gum[sudo] password for yuri:" — VM-proven).
# The timestamp refreshes on every subsequent sudo use, so one prompt covers
# the whole run.
info "Requesting sudo credentials (asked once for the whole run)..."
sudo -v
if [[ "$ASSUME_YES" == "true" && ( -z "$DE" || -z "$TWM" ) ]]; then
err "--yes requires explicit --de and --twm, so nothing is guessed on an"
err "unattended run. Example:"
@@ -173,12 +148,6 @@ if ! command -v curl >/dev/null 2>&1; then
install_packages curl
fi
# gum drives the styled menus/spinners when a TTY is present; the script's
# plain-text fallback means this is an enhancement, not a dependency.
if ! pkg_installed gum; then
install_optional gum
fi
# config-manager lives in dnf5-plugins; every repo step below depends on it.
if ! dnf config-manager --help >/dev/null 2>&1; then
info "dnf config-manager missing. Installing dnf5-plugins..."
@@ -196,9 +165,6 @@ add_rpmfusion_repo
# Terra up front: it carries mangowc (noctalia moved into Fedora proper in
# F44). Idempotent; also kept for the standalone-mango-module case.
add_terra_repo
# Fedora's third-party repo set (ships the google-chrome repo, disabled by
# default — the Browsers menu option enables it via config-manager setopt).
install_optional fedora-workstation-repositories
pause_if_debug
if ! command -v flatpak >/dev/null 2>&1; then
@@ -208,14 +174,58 @@ fi
info "Ensuring Flathub is available (system-wide)..."
sudo flatpak remote-add --if-not-exists --system flathub https://dl.flathub.org/repo/flathub.flatpakrepo
# Update the system (spinner + one-line summary; full output in $SETUP_DNF_LOG)
ui_quiet_run "Updating the system" sudo dnf update -y
info "Updating the system..."
sudo dnf update -y
pause_if_debug
#------------------------------------------
# 2 Menu helpers
#------------------------------------------
# $1 = prompt, $2 = default answer, rest = valid answers
ask_choice() {
local prompt="$1" default="$2"; shift 2
local answer
if [[ "$ASSUME_YES" == "true" ]]; then
echo "$default"
return 0
fi
while true; do
if ! read -t 60 -rp "$prompt (default: $default): " answer; then
printf "\n"
answer=""
fi
answer="${answer:-$default}"
answer="${answer,,}"
local v
for v in "$@"; do
if [[ "$answer" == "$v" ]]; then
echo "$answer"
return 0
fi
done
err "Invalid option: '$answer'. Choose from: $*"
done
}
# Ask a yes/no question; $2 is the default (yes|no). Honours --yes.
ask_yes_no() {
local prompt="$1" default="$2" answer
if [[ "$ASSUME_YES" == "true" ]]; then
echo "$default"
return 0
fi
while true; do
read -t 60 -rp "$prompt [y/n] (default: $default): " answer || printf "\n"
answer="${answer,,}"
[[ -z "$answer" ]] && answer="$default"
case "$answer" in
y|yes) echo "yes"; return 0 ;;
n|no) echo "no"; return 0 ;;
esac
err "Please answer y or n."
done
}
#------------------------------------------
# 3 System settings
#------------------------------------------
@@ -227,12 +237,13 @@ if [[ -z "$SELINUX_OPT" ]]; then
rule
printf "\n"
printf "\n"
choice=$(ui_confirm "Disable SELinux permanently? (not recommended)" "no")
choice=$(ask_yes_no "Disable SELinux permanently? (not recommended)" "no")
[[ "$choice" == "yes" ]] && SELINUX_DISABLED=true
printf "\n"
if findmnt -n -o FSTYPE / 2>/dev/null | grep -q btrfs; then
ok "Root filesystem is btrfs: snapshots + zstd compression will be set up."
BTRFS_SNAPPER=true
ok "Root filesystem is btrfs — snapshot support is available."
choice=$(ask_yes_no "Set up snapper + grub-btrfs snapshots and zstd compression?" "yes")
[[ "$choice" == "yes" ]] && BTRFS_SNAPPER=true
else
info "Root filesystem is not btrfs; skipping snapshot setup."
fi
@@ -245,49 +256,32 @@ case "${SELINUX_OPT:-}" in
esac
case "${SNAPPER_OPT:-}" in
yes) BTRFS_SNAPPER=true ;;
no) BTRFS_SNAPPER=false ;; # explicit flag beats the interactive default
""|keep) : ;;
no|"") : ;;
*) err "Unknown --snapper value: '$SNAPPER_OPT' (yes|no)"; exit 2 ;;
esac
DOTFILES=false
if [[ -z "$DOTFILES_OPT" ]]; then
printf "\n"
choice=$(ui_confirm "Clone personal dotfiles (fedora-dots) into \$HOME?" "no")
[[ "$choice" == "yes" ]] && DOTFILES=true
else
case "$DOTFILES_OPT" in
yes) DOTFILES=true ;;
no) : ;;
*) err "Unknown --dotfiles value: '$DOTFILES_OPT' (yes|no)"; exit 2 ;;
esac
fi
# GPU drivers are driven by DETECTED hardware, not by a prompt: an NVIDIA card
# in the machine means install the branch that fits it. 'yes' is accepted as an
# alias of 'auto' so muscle memory does not hit an error.
case "${NVIDIA_OPT:-}" in
auto|yes|"") NVIDIA_OPT="auto" ;;
no) NVIDIA_OPT="no" ;;
*) err "Unknown --nvidia value: '$NVIDIA_OPT' (auto|no)"; exit 2 ;;
esac
pause_if_debug
#------------------------------------------
# 4 Desktop environment selection
#------------------------------------------
if [[ -z "$DE" ]]; then
step "4 Desktop environment"
step "3 Desktop environment"
rule
color_gray "Bare Fedora install: picking a DE is what gives you a graphical login."
choice=$(ui_choose "Desktop environment" "Select desktop environment:" \
"None (TWM only, SDDM via TWM module)" \
"GNOME (gdm)" "Plasma (sddm)" "XFCE (sddm)" "None (TWM only, SDDM via TWM module)")
printf "\n"
color_gray " This system is treated as a bare Fedora install — picking a DE"
color_gray " here is what gives you a graphical login."
printf "\n"
echo " 1) GNOME (gdm)"
echo " 2) Plasma (sddm)"
echo " 3) XFCE (sddm)"
echo " 4) None (TWM only — SDDM will be installed by the TWM module)"
printf "\n"
choice=$(ask_choice "Select desktop environment [1/2/3/4]" "4" 1 2 3 4)
case "$choice" in
GNOME*) DE="gnome" ;;
Plasma*) DE="plasma" ;;
XFCE*) DE="xfce" ;;
*) DE="none" ;;
1) DE="gnome" ;;
2) DE="plasma" ;;
3) DE="xfce" ;;
4) DE="none" ;;
esac
fi
@@ -303,16 +297,20 @@ pause_if_debug
if [[ -z "$TWM" ]]; then
step "5 Tiling window manager"
rule
choice=$(ui_choose "Tiling window manager" "Select a tiling window manager:" \
"None" \
"Niri (Wayland, scrollable)" "Hyprland (Wayland, dynamic)" \
"MangoWC (Wayland, Terra repo)" "chadwm (X11, source build)" "None")
printf "\n"
echo " 1) Niri (Wayland, scrollable)"
echo " 2) Hyprland (Wayland, dynamic)"
echo " 3) MangoWC (Wayland, dwl-based, Terra repo)"
echo " 4) chadwm (X11, dwm fork, built from source)"
echo " 5) None"
printf "\n"
choice=$(ask_choice "Select tiling window manager [1/2/3/4/5]" "5" 1 2 3 4 5)
case "$choice" in
Niri*) TWM="niri" ;;
Hyprland*) TWM="hyprland" ;;
MangoWC*) TWM="mango" ;;
chadwm*) TWM="chadwm" ;;
*) TWM="none" ;;
1) TWM="niri" ;;
2) TWM="hyprland" ;;
3) TWM="mango" ;;
4) TWM="chadwm" ;;
5) TWM="none" ;;
esac
fi
@@ -363,11 +361,6 @@ if [[ "$BTRFS_SNAPPER" == "true" ]]; then
pause_if_debug
fi
if [[ "$DOTFILES" == "true" ]]; then
run_module "$SCRIPT_DIR/modules/user/dotfiles.sh"
pause_if_debug
fi
if [[ "$DE" != "none" ]]; then
run_module "$SCRIPT_DIR/modules/de/${DE}.sh"
pause_if_debug
@@ -378,490 +371,14 @@ if [[ "$TWM" != "none" ]]; then
pause_if_debug
fi
# Hardware drivers. The module re-detects the card and exits quietly when there
# is none, so running it unconditionally is safe; --nvidia=no is the opt-out
# (nouveau or a hand-built driver).
if [[ "$NVIDIA_OPT" != "no" ]]; then
run_module "$SCRIPT_DIR/modules/user/nvidia.sh"
pause_if_debug
fi
#------------------------------------------
# 6 Done
#------------------------------------------
#------------------------------------------
# 6 Always-installed packages
#------------------------------------------
# Add a category here and it is installed on every run, no questions asked.
# Keep each category one line so diffs stay readable.
# A line that is a dnf comps GROUP is written "@group" (e.g. "@multimedia");
# the loop below routes it through install_group, which reports a bad group
# name with the list of groups this system actually has.
ALWAYS_PACKAGES=(
# Terminals
"ghostty ghostty-bash-completion zsh zsh-syntax-highlighting"
# Utilities
"fastfetch bash-completion hwinfo lshw duf chrony man-db tree xdg-user-dirs rsync bat hddtemp powertop inxi acpi plocate nm-connection-editor"
"python3-pylint qt6-qtsvg qt6-qtvirtualkeyboard qt6-qtmultimedia catfish galculator NetworkManager network-manager-applet"
"pipewire pipewire-alsa pipewire-pulse wireplumber pavucontrol playerctl gparted"
"system-config-printer system-config-printer-applet system-config-printer-libs ripgrep meld btop htop"
"gcc gcc-c++ make python3-devel libX11-devel libXi-devel"
# File Managers
"thunar thunar-archive-plugin thunar-volman baobab gvfs-smb squashfs-tools cifs-utils papirus-icon-theme"
# Internet
"wget"
# Compression tools
"zip gzip 7zip unace unrar xarchiver"
# Office and Editors
"libreoffice"
# Multimedia
"mpv gstreamer1-plugin-openh264 mozilla-openh264"
# Multimedia codecs: dnf comps group 'multimedia' (the group Fedora's own
# docs recommend for playing movies/music). It is a GROUP, not a package,
# so it gets its own line — and the loop below sends it to install_group.
"@multimedia"
)
step "6 Always-installed packages"
step "7 Summary"
rule
always_failed=""
for category in "${ALWAYS_PACKAGES[@]}"; do
# A "@group" line is a dnf comps group. install_group checks the group
# exists first and prints the real group list when it does not, instead
# of dnf's unhelpful "No match for argument: @multimedia".
if [[ "$category" == @* ]]; then
if ! install_group "${category#@}"; then
color_red " Failed: group ${category}"
always_failed="yes"
pause_if_debug
fi
continue
fi
# shellcheck disable=SC2086
if ! install_packages $category; then
color_red " Failed: $category"
color_red " (a package in this line has no match — the rest of the install continues)"
always_failed="yes"
pause_if_debug
fi
done
if [[ -z "$always_failed" ]]; then
ok "Always-packages installed."
else
warn "Always-packages finished with failures (see red lines above)."
fi
pause_if_debug
#------------------------------------------
# 7 Advanced: optional tool selection (menu-driven)
#------------------------------------------
# Add categories/apps by editing these parallel arrays:
# ADV_CATEGORIES : display order
# ADV_<NAME> : "pkg1 pkg2 ..." for each category (dots/minus -> underscore)
ADV_CATEGORIES=(
"Virtualisation"
"Browsers"
"Editors"
"Graphics and Multimedia"
"Development Tools"
)
# shellcheck disable=SC2034 # arrays are read via nameref from ADV_CATEGORIES
ADV_Virtualisation=(
"qemu-kvm:QEMU/KVM (full virt stack):qemu-kvm libvirt virt-install virt-manager virt-viewer edk2-ovmf swtpm guestfs-tools libguestfs-tools"
"virtualbox:VirtualBox (Oracle):VirtualBox kernel-devel kernel-headers dkms akmod-VirtualBox"
)
# shellcheck disable=SC2034 # arrays are read via nameref from ADV_CATEGORIES
ADV_Browsers=(
"firefox:Firefox:firefox"
"chromium:Chromium:chromium"
"google-chrome:Google Chrome (adds repo):google-chrome-stable"
"brave:Brave (adds repo):brave-browser"
)
# shellcheck disable=SC2034 # arrays are read via nameref from ADV_CATEGORIES
ADV_Editors=(
"vscodium:VSCodium (flatpak):flatpak:com.vscodium.codium"
)
# shellcheck disable=SC2034
ADV_Graphics_and_Multimedia=(
"gimp:GIMP:gimp"
"inkscape:Inkscape:inkscape"
"obs:OBS Studio:obs-studio"
"lollypop:Lollypop (music):lollypop"
"spotify:Spotify (flatpak):flatpak:com.spotify.Client"
)
# shellcheck disable=SC2034
ADV_Development_Tools=(
"devtools:Development Tools (dnf group + build tools):@development-tools wget cmake ninja-build pkgconf-pkg-config"
)
run_advanced_menu() {
local done_any=false
while true; do
local cat
cat=$(ui_choose "Optional tools" "Select a category (last item exits):" \
"Done with optional tools" \
"${ADV_CATEGORIES[@]}" "Done with optional tools")
[[ "$cat" == "Done with optional tools" ]] && break
# apps for this category — nameref, because ${!var} on an array name
# would only give element 0 (that bug showed in testing: Browsers
# listed exactly one entry)
local var="ADV_${cat//[ .-]/_}"
local -n entries_ref="$var"
local -a entries
# shellcheck disable=SC2206
entries=("${entries_ref[@]}")
# Labels for the picker (gum multi-select or numbered fallback).
# Entry format: id:Label:packages...
local entry rest lbl
local -a labels=()
for entry in "${entries[@]}"; do
rest="${entry#*:}"
labels+=("${rest%%:*}")
done
# ui_choose_multi prints the chosen labels (one per line).
local sel picked
sel=$(ui_choose_multi "$cat" "Install which? (multi-select, none = back):" "${labels[@]}")
[[ -z "$sel" ]] && continue
# Resolve each picked label to its OWN package list, then install
# PER ITEM: one failing package used to kill the whole multi-select
# transaction (VM: brave's no-match took chromium down with it).
# Repo wiring inside install_category_pkgs runs per item; failures
# warn in red and the remaining picks still install.
# Read the picks into an ARRAY first, install in a for-loop.
# A `while read ... <<< "$sel"` herestring REPLACES stdin for its whole
# body — inside it stdin is no longer a TTY, ui_quiet_run saw "piped"
# and menu installs ran in plain mode (VM-proven: the advanced menu
# was the one place dnf's full output still scrolled).
local -a picked_items=()
while IFS= read -r picked; do
[[ -n "$picked" ]] && picked_items+=("$picked")
done <<< "$sel"
local picked lbl it
for picked in "${picked_items[@]}"; do
for entry in "${entries[@]}"; do
rest="${entry#*:}"
lbl="${rest%%:*}"
[[ "$lbl" == "$picked" ]] || continue
rest="${rest#*:}" # drop id and label -> pkgs
# word-split: quoting $rest whole makes dnf look for one
# package named "qemu-kvm libvirt ..." (seen on the VM)
# shellcheck disable=SC2206
local -a item_pkgs=(${rest})
step "Installing from $cat: ${item_pkgs[*]}"
if install_category_pkgs "$cat" "${item_pkgs[@]}"; then
done_any=true
else
color_red " Installation failed: $lbl"
color_red " (dnf reported no match / transaction failure — see output above)"
warn " Continuing with the remaining picks; the system is otherwise unchanged."
pause_if_debug
fi
done
done
done
# not `&& ok` — a false done_any would return 1 and kill the script under set -e
if [[ "$done_any" == "true" ]]; then
ok "Optional tools installed."
fi
}
# Per-category pre-repo wiring. Add third-party repos here when you add an
# app that needs one; packages alone stay generic.
install_category_pkgs() {
local cat="$1"; shift
# flatpak:<id> entries install from Flathub; the rest go to dnf. Split so
# a flatpak in a mixed pick cannot fail the dnf transaction (and vice
# versa — same per-item resilience as the rest of the menu).
local -a fp=() dp=()
local item
for item in "$@"; do
if [[ "$item" == flatpak:* ]]; then
fp+=("${item#flatpak:}")
else
dp+=("$item")
fi
done
if (( ${#dp[@]} > 0 )); then
# existing per-category wiring + dnf install (unchanged)
install_category_pkgs_dnf "$cat" "${dp[@]}" || return 1
fi
if (( ${#fp[@]} > 0 )); then
# quiet like dnf: live block line, full output in the log, red + tail
# on failure (ui_quiet_run matches flatpak's 'Installing...' lines)
ui_quiet_run "Installing flatpak: ${fp[*]}" \
flatpak install -y flathub "${fp[@]}" || return 1
fi
return 0
}
install_category_pkgs_dnf() {
local cat="$1"; shift
case "$cat" in
Virtualisation)
# qemu:///system without password prompts: libvirt group + enabled
# daemon. Only when the picked packages actually use libvirt.
local wants_libvirt=false item
for item in "$@"; do
[[ "$item" == *libvirt* || "$item" == *virt-manager* || "$item" == *virt-install* ]] && wants_libvirt=true
done
if $wants_libvirt; then
local real_user="${SUDO_USER:-$USER}"
info "Enabling libvirtd and adding $real_user to the libvirt group..."
sudo systemctl enable --now libvirtd 2>/dev/null || true
sudo usermod -aG libvirt "$real_user" || true
warn "libvirt group membership applies after re-login."
fi
;;
Browsers)
local needs_chrome=false needs_brave=false item
for item in "$@"; do
[[ "$item" == *google-chrome* ]] && needs_chrome=true
[[ "$item" == *brave* ]] && needs_brave=true
done
if $needs_chrome && ! pkg_installed google-chrome-stable; then
info "Adding Google Chrome repo..."
install_optional fedora-workstation-repositories
if ! sudo dnf config-manager setopt google-chrome.enabled=1 >/dev/null 2>&1; then
# repo enable failed: install straight from Google's rpm
ui_quiet_run "Installing Google Chrome (direct rpm)" \
sudo dnf install -y "https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm"
fi
fi
if $needs_brave && ! pkg_installed brave-browser; then
# dnf5 (Fedora 41+) has no 'config-manager add' subcommand —
# the VM showed 'Unknown argument "add"', so the repo never
# landed and brave-browser had no match. Correct form per
# Brave's own F41+ docs: addrepo. The .repo carries the gpgkey,
# so no separate rpm --import is needed.
if [[ -f /etc/yum.repos.d/brave-browser.repo ]]; then
ok "Brave repo already present."
else
ui_quiet_run "Adding Brave repo (dnf5 addrepo)" \
sudo dnf config-manager addrepo \
--from-repofile=https://brave-browser-rpm-release.s3.brave.com/brave-browser.repo
fi
fi
;;
esac
# Resilient install: report failure but return non-zero instead of
# aborting under set -e; the caller turns it into a red warning.
install_packages "$@" || return 1
return 0
}
# Interactive only: an unattended --yes run must not hang on this menu.
# Skip silently there; use --de/--twm/--dotfiles for everything unattended.
if [[ "$ASSUME_YES" != "true" ]]; then
run_advanced_menu
fi
pause_if_debug
#------------------------------------------
# 8 Post-install tailoring
#------------------------------------------
# Ported from the old starburst minimal-fedora script, adapted:
# - polkit agent handled elsewhere (polkit-kde user unit, VM-proven)
# - resilient: each part warns red and continues (one missing package
# must not abort a finished install)
step "8 Post-install tailoring"
rule
# --- 8.1 PeaZip: toolkit variant follows the session (Qt6 for Qt-ish
# environments, GTK3 for the rest) ---
info "Tailoring toolkits for DE=$DE / TWM=$TWM..."
# The UI flavors all ship /usr/bin/peazip and file-conflict with each
# other — installing gtk3 over an existing qt5 dies mid-transaction
# (laptop-proven). Behaviors:
# no flavor -> install the environment-appropriate one
# right flavor -> nothing to do
# OTHER flavor -> dnf SWAP to the wanted one (atomic: on failure the
# existing flavor stays, which is the safe outcome)
#
# Flavor choice: PeaZip's GTK3 build is ALPHA (Lazarus GTK3 widgetset) —
# upstream ships it "for test purpose only", and it renders broken on
# non-GTK Wayland compositors (YuKu-proven on niri: broken UI until a
# manual swap to qt6). GTK3 therefore only for genuine GTK stacks
# (GNOME, XFCE); Qt6 everywhere else — including niri/MangoWC/Hyprland
# and chadwm (Qt6 handles both Wayland and X11 cleanly).
if [[ "$DE" == "gnome" || "$DE" == "xfce" ]]; then
peazip_wanted="peazip-gtk3"
else
peazip_wanted="peazip-qt6"
fi
peazip_existing=""
local_v=""
for local_v in peazip-qt5 peazip-qt6 peazip-gtk3 peazip-gtk2; do
pkg_installed "$local_v" && peazip_existing="$local_v"
done
if [[ -z "$peazip_existing" ]]; then
# shellcheck disable=SC2086
if ! install_packages peazip-common "$peazip_wanted"; then
color_red " Failed: peazip-common $peazip_wanted"
color_red " (continuing — PeaZip is a convenience, not a dependency)"
fi
elif [[ "$peazip_existing" == "$peazip_wanted" ]]; then
ok "PeaZip already present ($peazip_existing)."
else
info "PeaZip $peazip_existing does not match this environment — swapping to $peazip_wanted..."
if ui_quiet_run "Swapping PeaZip: $peazip_existing -> $peazip_wanted" \
sudo dnf swap -y "$peazip_existing" "$peazip_wanted"; then
ok "PeaZip swapped to $peazip_wanted."
else
warn "Swap failed — keeping the existing $peazip_existing (fully functional)."
fi
fi
pause_if_debug
# --- 8.2 Starship prompt + Tokyo Night preset ---
if ! command -v starship >/dev/null 2>&1; then
info "Installing Starship shell prompt..."
# starship's official installer; -y skips its confirmation
if ! curl -sS https://starship.rs/install.sh | sh -s -- -y; then
color_red " Failed: starship installer"
color_red " (continuing)"
fi
fi
# Preset only when the user has no config yet — never clobber existing
# configuration (which may arrive via the dotfiles checkout).
if command -v starship >/dev/null 2>&1 && [[ ! -f "$HOME/.config/starship.toml" ]]; then
info "Applying starship tokyo-night preset..."
mkdir -p "$HOME/.config"
starship preset tokyo-night -o "$HOME/.config/starship.toml" \
|| warn "Could not apply the preset (starship still installed)."
fi
# bashrc wiring — ONLY when dotfiles were not cloned: on machines that
# use fedora-dots, .bashrc is owned by the dots and must not be edited
# here. Also requires starship to actually be present (a missing binary
# would make every shell print an error from the eval line).
if [[ "$DOTFILES" != "true" ]] \
&& command -v starship >/dev/null 2>&1 \
&& ! grep -q "starship init" "$HOME/.bashrc" 2>/dev/null; then
info "Wiring starship into ~/.bashrc..."
cat >> "$HOME/.bashrc" <<'EOF'
# starship
export STARSHIP_CONFIG="$HOME/.config/starship.toml"
eval "$(starship init bash)"
EOF
fi
pause_if_debug
# --- 8.3 RobotoMono Nerd Font (the modules install JetBrainsMono) ---
install_nerd_font RobotoMono
pause_if_debug
# --- 8.4 Services ---
info "Enabling services..."
sudo systemctl enable --now fstrim.timer
sudo systemctl enable --now chronyd || true
info "Ensuring NetworkManager manages all devices..."
NM_CONF="/etc/NetworkManager/conf.d/10-globally-managed-devices.conf"
if [[ ! -f "$NM_CONF" ]]; then
sudo mkdir -p "$(dirname "$NM_CONF")"
printf '[ifupdown]\nmanaged=true\n\n[keyfile]\nunmanaged-devices=\n' \
| sudo tee "$NM_CONF" >/dev/null
sudo systemctl enable --now NetworkManager
sudo systemctl restart NetworkManager
warn "NetworkManager restarted; giving it 15s to settle..."
sleep 15
else
ok "NetworkManager already globally-managed (config present)."
fi
# pipewire user units: preset-enabled on install; enable explicitly for
# robustness (fails harmlessly in odd session contexts, hence || true)
systemctl --user enable pipewire pipewire-pulse wireplumber >/dev/null 2>&1 || true
pause_if_debug
# --- 8.5 SDDM theme (SilentSDDM) — only when SDDM is the greeter ---
# Requires SDDM >= 0.21 (Fedora 44 ships 0.24+). Skipped on GNOME (gdm) and
# any setup where sddm is not installed.
if pkg_installed sddm; then
info "Installing SDDM theme: SilentSDDM..."
GIT_DIR="$HOME/git"
REPO_DIR="$GIT_DIR/SilentSDDM"
REPO_URL="https://github.com/uiriansan/SilentSDDM"
mkdir -p "$GIT_DIR"
if [[ -d "$REPO_DIR/.git" ]]; then
ok "SilentSDDM checkout exists. Updating..."
git -C "$REPO_DIR" pull --rebase --quiet || warn "git pull failed; using existing checkout."
else
info "Cloning SilentSDDM..."
git clone -b main --depth 1 --quiet "$REPO_URL" "$REPO_DIR" \
|| { color_red " Failed: git clone SilentSDDM (continuing)"; }
fi
if [[ -f "$REPO_DIR/install.sh" ]]; then
# yes | install.sh: the installer prompts; SIGPIPE from 'yes' would
# read as a crash under pipefail, so suspend it for this one command.
# pipefail state is inherited by the ui_quiet_run subshell at fork.
# The theme's own dnf dependency flood goes to the log like every
# other transaction (laptop-proven it was the last raw wall left).
set +o pipefail
if ui_quiet_run "Installing SilentSDDM theme" \
bash -c 'cd "$1" && yes | ./install.sh' _ "$REPO_DIR"; then
ok "SilentSDDM theme installed."
else
color_red " Failed: SilentSDDM installer (continuing; SDDM keeps its default theme)"
fi
set -o pipefail
else
color_red " SilentSDDM install.sh not found; skipping theme."
fi
else
info "SDDM not installed (gdm/none setup) - skipping SDDM theme."
fi
pause_if_debug
step "9 Summary"
rule
# Post-install wiring that must see the FINAL package state. The libvirt
# group add earlier (menu/module time) can miss packages installed later in
# the same run — this is the authoritative pass (VM-proven: a fresh run left
# the user out of the group when virt-manager landed after the check).
ensure_libvirt_group() {
# shellcheck source=lib/common.sh
source "$SCRIPT_DIR/lib/common.sh" 2>/dev/null || true
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" || true
warn "Group membership applies after re-login — do it before using virt-manager."
fi
fi
}
ensure_libvirt_group
ok "DE: $DE"
ok "TWM: $TWM"
ok "SELinux: $([[ "$SELINUX_DISABLED" == "true" ]] && echo disabled || echo keeping)"
ok "Btrfs snapshots: $([[ "$BTRFS_SNAPPER" == "true" ]] && echo yes || echo skipped)"
ok "Dotfiles: $([[ "$DOTFILES" == "true" ]] && echo clone || echo no)"
NV_VER=$(modinfo -F version nvidia 2>/dev/null) || true
if [[ -n "${NV_VER:-}" ]]; then
ok "NVIDIA: module nvidia ${NV_VER} built (reboot if it was installed in this run)"
elif pkg_installed_any akmod-nvidia akmod-nvidia-580xx akmod-nvidia-470xx akmod-nvidia-390xx; then
ok "NVIDIA: driver packages installed; the kmod builds at the next boot"
else
ok "NVIDIA: none (no supported card, --nvidia=no, or no card present)"
fi
ok "SELinux: $([[ "$SELINUX_DISABLED" == "true" ]] && echo disabled || echo keeping)"; ok "Btrfs snapshots: $([[ "$BTRFS_SNAPPER" == "true" ]] && echo yes || echo skipped)"
DM=$(detect_display_manager)
ok "Display manager: ${DM:-none}"
@@ -877,11 +394,18 @@ if [[ "$ASSUME_YES" == "true" ]]; then
exit 0
fi
reboot_choice=$(ui_confirm "Reboot now?" "no")
if [[ "$reboot_choice" == "yes" ]]; then
if read -t 15 -rp "Reboot now? [y/N]: " reboot_choice; then
case "${reboot_choice,,}" in
y|yes)
color_red "Rebooting now."
sudo reboot
else
;;
*)
color_yellow "Skipping reboot."
exit 0
;;
esac
else
color_yellow "No input detected. Skipping reboot for safety."
exit 0
fi
-11
View File
@@ -1,11 +0,0 @@
# 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
View File
@@ -1,26 +0,0 @@
# 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
View File
@@ -1,35 +0,0 @@
#!/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
View File
@@ -1,49 +0,0 @@
#!/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
View File
@@ -1,51 +0,0 @@
#!/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
View File
@@ -1,45 +0,0 @@
#!/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
View File
@@ -1,47 +0,0 @@
#!/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