Compare commits

...
33 Commits
Author SHA1 Message Date
yuri 482bd797d0 added papirus-icon-theme 2026-11-13 17:31:58 +01:00
yuri f08abda392 Merge pull request 'Always-packages: install the multimedia dnf group' (#17) from feat/always-multimedia-group into main 2026-11-13 16:27:26 +01:00
Pathfinder 6733cde613 Always-packages: install the multimedia dnf group
Adds '@multimedia' as an ALWAYS_PACKAGES entry and teaches the always-loop
to route '@group' lines through install_group, so a group is checked for
existence first and a bad name prints the system's real group list instead
of dnf's 'No match for argument: @multimedia'.

Group install failures warn in red and continue, like every other
always-package line.
2026-11-13 16:26:04 +01:00
yuri 64fff4f200 added packages for building Hermes Desktop 2026-11-13 16:19:07 +01:00
yuri 17d09d985e added extra mandatory packages during install 2026-11-13 16:12:46 +01:00
yuri fec520be9d Merge pull request 'NVIDIA driver module: detect the GPU, install the right branch' (#16) from feat/nvidia-driver-module into main 2026-11-13 12:39:29 +01:00
Pathfinder 065a8cd0ae Add GPU-detected NVIDIA driver module
modules/user/nvidia.sh detects the card via lspci and installs the branch
that matches it: current (Turing+), 580xx (Volta/Pascal/Maxwell), 470xx
(Kepler), 390xx (Fermi); pre-Fermi is refused with an explanation.

- no prompt for a detected GPU (--nvidia=no is the opt-out, wired in setup.sh)
- Secure Boot gate: with enforcement on, the akmods key is generated and
  imported and the module stops BEFORE installing the driver, so nobody gets
  the documented unsigned-module black screen; off/setup-mode skips signing
  entirely (a MOK import is never processed when enforcement is off)
- bounded wait for the kmod build, so a reboot cannot land mid-build
- dnf mark user on the akmod; VA-API/vulkan offered as an opt-in extra
- never sets nvidia-drm.modeset=1 (RPM Fusion enables KMS; the flag conflicts
  with Fedora's simpledrm patch)
- branch chosen from the chip family in the PCI name, not the device-id
  ranges, which overlap between families

Tested with a stub harness (38 assertions, 14 scenarios): all four branch
mappings, multi-GPU warning, Optimus, pre-Fermi refusal, SB on/off/setup,
unparseable name fallback, idempotent re-run, build timeout.
2026-11-13 12:18:23 +01:00
yuri e6698cdb44 Merge pull request 'PeaZip: Qt6 default (GTK3 build is upstream alpha)' (#15) from fix/peazip-qt6-default into main 2026-11-11 16:27:09 +01:00
Pathfinder 88bd9d6a20 PeaZip: Qt6 everywhere except GNOME/XFCE (GTK3 build is upstream alpha)
YuKu tested gtk3 on niri: broken UI until a manual swap to qt6 fixed
it. Upstream's own changelog: the GTK3 package is 'for test purpose
only' (Lazarus GTK3 widgetset is alpha) and Qt6 supersedes Qt5.

New mapping: gtk3 ONLY for genuine GTK stacks (gnome, xfce); qt6 for
everything else - plasma, hyprland, niri, mangowc, chadwm (Qt6 handles
Wayland and X11 cleanly).

Harness: full DE/TWM matrix passes (7/7); existing qt5 on niri now
swaps to qt6 (not gtk3); a machine already on qt6 (YuKu's laptop
after the manual swap) is a clean no-op.
2026-11-11 16:14:53 +01:00
yuri d1031277bf Merge pull request 'Repo guards (repolist) + PeaZip flavor guard' (#14) from fix/repo-and-peazip-guards into main 2026-11-11 15:48:11 +01:00
Pathfinder ee10829452 Cosmetic: ANSI line wipe; hide (0 packages) on idempotent re-runs
Two nits from YuKu's laptop paste: the 100-space line wipe left a
visible gap before short summaries (now ANSI EL: \r\033[K, no
residue), and '(0 packages)' printed when nothing new was installed
(idempotent re-run / theme deps all present) — the count parenthetical
is now omitted when zero.
2026-11-11 15:46:45 +01:00
Pathfinder 1586e10df1 SilentSDDM install: quiet like everything else
The theme's own install.sh runs dnf for its dependencies and its
output flooded the screen raw (laptop-proven: the last wall of text
left in the run). Now wrapped in ui_quiet_run: live block line with
the current dependency, summary count after, full output in the
setup log, red + tail on failure. Unattended/piped runs keep plain
output. pipefail still suspended around the yes| pipe (inherited at
subshell fork).
2026-11-11 15:42:49 +01:00
Pathfinder 4e0dad3483 PeaZip: swap to the wanted flavor when an incompatible one is detected
Per YuKu: instead of skipping when an older flavor is present, SWAP it
to the environment-appropriate one (dnf swap -y old new). Atomic —
if the swap fails, the existing flavor stays and the run continues
with a warning (the safe outcome). Right flavor already present ->
no-op; fresh machine -> plain install.

Verified: qt5+niri -> swap to gtk3 (your laptop scenario); qt6+hyprland
-> no-op; forced swap failure -> warn + keep qt5; fresh -> gtk3.
2026-11-11 15:38:57 +01:00
Pathfinder b79735d82e Repo guards check repolist, not just RPMs; PeaZip skips when any flavor exists
Laptop run (YuKu, existing machine) exposed two fresh-install blind
spots:

1. Terra add failed on a machine that already HAD the repo but not a
   queryable terra-release RPM — bootstrapping --repofrompath over an
   existing repo id collides. New repo_enabled() checks dnf repolist
   (the source of truth); add_terra_repo and add_rpmfusion_repo now
   skip when the repo is configured OR the release RPM present.

2. PeaZip tailoring tried gtk3 over an existing peazip-qt5: all UI
   flavors ship /usr/bin/peazip and file-conflict, transaction died.
   Any installed flavor (qt5/qt6/gtk3/gtk2) now wins and the section
   skips with the flavor named.

Harness: laptop sim (repo configured, peazip-qt5 present) -> zero
bootstrap calls, PeaZip skipped; fresh machine -> terra bootstraps +
gtk3 installs; re-run -> skip via repolist; peazip-qt6 + hyprland ->
skip; unattended/piped green.
2026-11-11 15:35:15 +01:00
yuri 8fb78651c9 Merge pull request 'Post-install tailoring (PeaZip variants, starship+preset, RobotoMono, services)' (#13) from feat/post-install-tailoring into main 2026-11-02 17:50:02 +01:00
Pathfinder c9a127a07e Advanced-menu installs go quiet: fix stdin theft + silent wiring
YuKu on the VM: menu installs were not quiet — the ONE place dnf's
full output still scrolled.

Root cause: the per-item install loop read picks with
'while read ... <<< "$sel"' — a herestring REPLACES stdin for its
whole body, so ui_quiet_run's tty gate saw 'piped' inside the loop
and every menu install ran in plain mode. Picks are now read into an
array first (herestring fills ONLY that); installs run in a for-loop
that never touches stdin.

Also silenced the remaining raw output in the menu path:
- Chrome repo wiring -> install_optional (quiet) + setopt silenced,
  direct-rpm fallback via ui_quiet_run
- Brave addrepo -> ui_quiet_run
- flatpak installs -> ui_quiet_run; ui_quiet_run now also matches
  flatpak's 'Installing...' progress (Unicode ellipsis) for the live
  line and count, sanitized to printable ASCII (tr -cd) so a
  non-UTF8 console never sees garbage bytes.

Pty-verified: live block lines + green summaries for chrome/gimp/
vscodium picks, flatpak progress only in the log, zero raw dnf or
flatpak text on screen.
2026-11-02 17:40:42 +01:00
Pathfinder 86358466e1 SilentSDDM theme, workstation-repos, Editors/Graphics/DevTools menus, flatpak support
Ported from the old fedora-full script:

- SilentSDDM SDDM theme in the tailoring section, gated on sddm
  being installed (skipped on gdm setups), idempotent via the
  ~/git/SilentSDDM checkout (pull --rebase when present), pipefail
  suspended around 'yes | ./install.sh' for the SIGPIPE trap.
- fedora-workstation-repositories added in base alongside the other
  repos (it ships the google-chrome repo the Browsers option enables).
- Advanced menu: Editors (VSCodium flatpak), Graphics and Multimedia
  (gimp/inkscape/obs/lollypop via dnf, spotify via flatpak), Development
  Tools (single option: @development-tools wget cmake ninja-build
  pkgconf-pkg-config). All names verified on F44.
- install_category_pkgs splits flatpak:<id> entries to Flathub so mixed
  picks cannot fail each other's transactions.

Harness: sddm->theme installs, gnome->skipped, vscodium+spotify via
flatpak, lollypop via dnf in the same multi-pick, devtools as one
transaction, all five categories listed, unattended+piped green.
2026-11-02 17:25:16 +01:00
Pathfinder 35c1939f28 Tailoring: wire starship into .bashrc only when dotfiles are absent
Per YuKu: after the starship install, append
  export STARSHIP_CONFIG=$HOME/.config/starship.toml
  eval "$(starship init bash)"
to ~/.bashrc — but ONLY when fedora-dots was NOT cloned (on dots
machines .bashrc belongs to the dots). Also gated on starship actually
being present (no dead eval in every shell) and idempotent (grep
guard). Harness: appended once with dots=no, never with dots=yes
(clean-home re-test; the first test was invalid because rm -rf home/*
left the previous .bashrc — the known dotfile-cleanup pitfall), and
not duplicated on re-run.
2026-11-02 16:58:34 +01:00
Pathfinder a8796467df Post-install tailoring section (ported from starburst minimal-fedora)
Section 8, after the advanced menu (summary renumbered to 9):

- PeaZip variant by environment: peazip-common + peazip-qt6 for
  plasma/hyprland, peazip-common + peazip-gtk3 otherwise (all verified
  on F44; peazip-qt6-10.9.0-1.fc44). Resilient: warns red, continues.
- Starship prompt via the official installer (command -v guarded), then
  the tokyo-night preset ONLY when no starship.toml exists yet —
  dotfiles checkouts always win over the preset.
- RobotoMono Nerd Font via the existing install_nerd_font helper
  (modules keep installing JetBrainsMono; both are small).
- Services: fstrim.timer, chronyd, NetworkManager globally-managed
  config (write + restart + 15s settle ONLY when the conf is absent —
  no needless NM restarts on re-runs, and no SSH self-drop when the
  config already exists), pipewire user units best-effort.

Polkit agent deliberately NOT ported: solved properly by the
polkit-kde user unit (VM-proven), the old polkit-kde-agent-1 line is
superseded.
2026-11-02 16:45:21 +01:00
yuri 3590c1b477 Merge pull request 'Always-packages: p7zip->7zip (retired in F44) + resilient loop' (#12) from fix/always-packages-p7zip-resilience into main 2026-11-02 16:31:09 +01:00
Pathfinder d4f04ac486 Always-packages: drop X11-only tools (numlockx, arandr)
Both are inert on the niri/Wayland target (numlockx needs X11; arandr
drives RandR). numlock at the greeter is SDDM's own setting.
2026-11-02 16:21:59 +01:00
Pathfinder a5e4facf42 Live progress line: clamp to terminal width
YuKu's promised screenshot revealed the real section-6 chaos: the live
line 'block + msg + current package' is longer than 80 columns, so
every redraw WRAPPED to a new line instead of overwriting — the screen
filled with duplicate partial progress lines.

The line is now composed once and clamped to $(tput cols) - 2 with an
ASCII '..' tail. 80-col pty run: zero wrapped live lines (the only
>82-char output left is plain-mode dnf, unchanged by design).
2026-11-02 16:19:52 +01:00
Pathfinder 835b6b0040 Always-packages: p7zip -> 7zip (retired in F44) + resilient loop
YuKu's expanded always-section died at section 6: p7zip was RETIRED
from Fedora 44 (replaced by the official '7zip' package) -> the
compression line's transaction failed -> set -e killed the whole
install after everything before it had already succeeded.

Two changes:
- p7zip -> 7zip in the Compression line (unace/unrar verified fine:
  they live in RPM Fusion, which base enables).
- The always-loop now warns in red per failing category line and
  continues — same resilience contract as the advanced menu. One
  missing package can no longer abort a 30-minute install at the
  finish line.

Harness: healthy run installs everything incl. 7zip/mpv/wget; a
deliberately broken compression line produces the red Failed block,
'finished with failures' summary, exit 0, and later categories
(mpv, wget...) still install.
2026-11-02 16:16:37 +01:00
yuri cb60610942 Update setup.sh
added more packages to 6 Always-installed packages
2026-11-02 13:54:48 +01:00
yuri ea50f64ef2 Merge pull request 'Brave via dnf5 addrepo + per-item installs in advanced menu' (#11) from fix/brave-addrepo-per-item into main
Reviewed-on: #11
2026-11-02 13:05:07 +01:00
Pathfinder 82b35e4bdb Browsers: dnf5 addrepo for Brave; per-item installs in the menu
Two bugs read straight out of YuKu's VM screenshot:

1. 'Unknown argument "add" for command "config-manager"' — dnf5 has
   no 'add' subcommand, so the Brave repo never landed and
   brave-browser had no match. Now uses 'config-manager addrepo
   --from-repofile=' per Brave's own Fedora 41+ docs; skips when
   /etc/yum.repos.d/brave-browser.repo exists. The .repo carries the
   gpgkey, so the rpm --import dance is gone.

2. 'Installation failed: chromium brave-browser' — multi-select put
   all picks in ONE dnf transaction, so brave's no-match killed
   chromium too. Picks now install PER ITEM: one failure red-warns
   with the item's own name and the remaining picks still install.
2026-10-27 22:58:01 +01:00
yuri 85ba9ddb51 Merge pull request 'Breathing-block status indicator (blue/green/red)' (#10) from feat/breathing-block into main 2026-10-27 22:50:24 +01:00
Pathfinder 0630acf020 Console pulse: one-third speed (150ms frames), 256-ramp untouched
YuKu: 'slow the flicker a bit (half or one-third speed)'. The 16-colour
blue<->grey pulse now ticks at 150ms (600ms full cycle, calm); the
256-colour terminal breathe keeps its 50ms deep ramp. Per-depth tick
variable; same in the vendored helper.

Rate proven by pty trace: 8 frames in a 1.2s transaction = 150ms
exactly. (Frame-counting via grep -c undercounts — \r overwrites
collapse frames onto one line; count matches, not lines.)
2026-10-27 22:48:50 +01:00
Pathfinder 4a53155c7e TTY pulse: constant layout, blue <-> dark grey (no more block-off)
The on/off pulse shifted the whole line by one character during the
dark frames (off-frame printed one space fewer than the block frame).
Per YuKu: keep the block ALWAYS present and alternate its colour
instead — blue (44) <-> dark grey (100, bright black). Layout is
constant, pulse is colour-based, nothing moves but the block itself.

Also: disable_selinux now mkdir -p's /etc/selinux before writing a
fresh config (harness caught a die on a missing parent dir).
2026-10-27 22:45:43 +01:00
Pathfinder 71780291a3 TTY: on/off block pulse on the Linux console (shade fusion defeated)
YuKu tests on the bare Linux VT (not a terminal emulator): my 16-colour
fallback alternated blue (44) / bright blue (104), which render nearly
identically on VGA consoles -> fused into 'solid blue'. If TERM
misreports 256, 48;5;N codes are ignored outright.

Fixes:
- TERM linux*/console* forces the 16-colour path regardless of tput.
- 16-colour mode now PULSES THE BLOCK ON/OFF (2 frames blue block,
  2 frames no block, ~2.5Hz) instead of shade alternation — visible
  on any console hardware, no colour fidelity required.
- 256-colour terminals keep the deep 17->45 breathe ramp.

Verified: TERM=linux pty shows block/no-block alternation with zero
48;5 codes; TERM=xterm-256color keeps the 8-shade ramp; unattended +
piped regressions green.
2026-10-27 22:41:48 +01:00
Pathfinder 3d21a1c427 Breathing: deep amplitude so the pulse survives flicker fusion
At 20fps the previous narrow ramp (shades 17-33) flicker-fused into an
apparent SOLID block on the VM. Amplitude now spans dark navy (17) to
bright blue (45) in 12 steps — every frame is visibly different, cycle
is 0.6s. Speed unchanged.
2026-10-27 22:38:02 +01:00
Pathfinder 35906d4df0 Breathing pulse: 3x faster (0.15s -> 0.05s per frame)
YuKu: 'the breathing is super slow'. Frame interval down from 150ms
to 50ms (~7fps -> ~20fps pulse). Same change in the vendored
lib/ui-block.sh helper for consistency.
2026-10-27 22:35:35 +01:00
Pathfinder 9c0b716758 Status blocks replace the spinner: blue breathing / green / red
ui_quiet_run now paints a TTY-safe status BLOCK: a background-coloured
SPACE (never a glyph, no font/locale dependency — the braille lesson).

  busy : blue block breathing through a 256-colour ramp (48;5;17..33)
         with the live dnf action after it
  ok   : solid green block + package count
  fail : solid RED block (spec: YuKu changed the red X to a red block)
         followed by the log tail

Colour-depth aware: 256-colour ramp -> 16-colour blue/bright-blue ->
plain ASCII spinner when the terminal reports no colour. LANG=C pty
run shows zero non-ASCII bytes.

Also vendors lib/ui-block.sh (the standalone helper from the Telegram
session prototype; fail changed to red block) and installs the
ui_block_close EXIT trap — with a return 0, because an EXIT trap's
status becomes the script's exit code (harness caught every-successful-
run exiting 1 through that path).
2026-10-27 22:32:58 +01:00
5 changed files with 957 additions and 65 deletions
+37
View File
@@ -34,6 +34,8 @@ 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
@@ -75,6 +77,41 @@ 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,
+103 -25
View File
@@ -101,43 +101,109 @@ ui_quiet_run() {
before=$(wc -l < "$log" 2>/dev/null || echo 0)
# Child writes to the log; we draw a LIVE line: spinner frame + the package
# dnf is currently working on. gum spin was tried here first but rendered
# as a frozen block on the real VM ("looks like it hangs") — this variant
# animates independently of gum and shows real progress, not just motion.
# ---- 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=$!
# ASCII frames ONLY: braille/em-dash are multibyte UTF-8 and rendered as
# garbage letters when the session locale is not UTF-8 (VM-proven: the
# spinner appeared as changing random chars, not as a spinner).
local frames=( '|' '/' '-' '\\' )
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 -E '^(Installing|Upgrading|Downgrading|Removing|Verifying|Preparing|Cleanup):' \
| tail -n 1 || true)
| 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
printf '\r %s %s: %s' "${frames[$f]}" "$msg" "$cur" >&2
line="$msg: $cur"
else
printf '\r %s %s' "${frames[$f]}" "$msg" >&2
line="$msg"
fi
f=$(( (f + 1) % ${#frames[@]} ))
sleep 0.15
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=$?
# wipe the progress line before printing the result
printf '\r%*s\r' 100 '' >&2
# 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 -c '^Installing:' || true)
local plural="s"
(( n == 1 )) && plural=""
ok "$msg (${n} package${plural})"
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
color_red " Failed: $msg"
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"
@@ -225,9 +291,17 @@ 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; then
ok "Terra repository already installed."
if pkg_installed terra-release || repo_enabled terra; then
ok "Terra repository already present."
return 0
fi
info "Adding Terra repository (Fyra Labs)..."
@@ -241,8 +315,9 @@ add_terra_repo() {
add_rpmfusion_repo() {
local rel
rel=$(rpm -E %fedora)
if pkg_installed rpmfusion-free-release rpmfusion-nonfree-release; then
ok "RPM Fusion already installed."
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}..."
@@ -307,6 +382,9 @@ 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."
+118
View File
@@ -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
+354
View File
@@ -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."
+345 -40
View File
@@ -12,6 +12,10 @@ source "$SCRIPT_DIR/lib/common.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}"
@@ -28,6 +32,7 @@ TWM=""
SELINUX_OPT=""
SNAPPER_OPT=""
DOTFILES_OPT=""
NVIDIA_OPT=""
usage() {
cat <<'EOF'
@@ -44,6 +49,8 @@ Options:
--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)
-y, --yes never prompt (requires --de and --twm)
-d, --debug pause after each section instead of aborting on error
-h, --help show this help
@@ -68,6 +75,7 @@ for arg in "$@"; do
--selinux=*) SELINUX_OPT="${arg#*=}" ;;
--snapper=*) SNAPPER_OPT="${arg#*=}" ;;
--dotfiles=*) DOTFILES_OPT="${arg#*=}" ;;
--nvidia=*) NVIDIA_OPT="${arg#*=}" ;;
--twm=*) TWM="${arg#*=}" ;;
-h|--help) usage; exit 0 ;;
*)
@@ -188,6 +196,9 @@ 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
@@ -251,6 +262,15 @@ else
*) 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
#------------------------------------------
@@ -358,6 +378,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
#------------------------------------------
@@ -367,22 +395,65 @@ fi
#------------------------------------------
# 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"
)
"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"
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
install_packages $category
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
ok "Always-packages installed."
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)
#------------------------------------------
@@ -392,6 +463,9 @@ pause_if_debug
ADV_CATEGORIES=(
"Virtualisation"
"Browsers"
"Editors"
"Graphics and Multimedia"
"Development Tools"
)
# shellcheck disable=SC2034 # arrays are read via nameref from ADV_CATEGORIES
ADV_Virtualisation=(
@@ -405,6 +479,22 @@ ADV_Browsers=(
"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
@@ -438,38 +528,43 @@ run_advanced_menu() {
sel=$(ui_choose_multi "$cat" "Install which? (multi-select, none = back):" "${labels[@]}")
[[ -z "$sel" ]] && continue
# Map chosen labels back to their package lists. Word-split each
# package list into SEPARATE args: quoting it whole makes dnf look for
# one package named "qemu-kvm libvirt ..." (seen on the VM).
local -a to_install=()
# 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
[[ -z "$picked" ]] && continue
[[ -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%%:*}"
if [[ "$lbl" == "$picked" ]]; then
rest="${entry#*:}"; rest="${rest#*:}" # drop id and label
# shellcheck disable=SC2206
to_install+=(${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 <<< "$sel"
if (( ${#to_install[@]} > 0 )); then
step "Installing from $cat: ${to_install[*]}"
# install_category_pkgs wires any needed third-party repo first.
# Failure must NOT kill the script: warn in red and drop back to
# the menu (set -e is bypassed for this one call).
if install_category_pkgs "$cat" "${to_install[@]}"; then
done_any=true
else
color_red " Installation failed: ${to_install[*]}"
color_red " (dnf reported no match / transaction failure — see output above)"
warn " Returning to the menu. The system is unchanged."
pause_if_debug
fi
unset to_install; local -a to_install=() # fresh slate for next pick
fi
done
done
# not `&& ok` — a false done_any would return 1 and kill the script under set -e
if [[ "$done_any" == "true" ]]; then
@@ -481,6 +576,35 @@ run_advanced_menu() {
# 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
@@ -505,16 +629,26 @@ install_category_pkgs() {
done
if $needs_chrome && ! pkg_installed google-chrome-stable; then
info "Adding Google Chrome repo..."
sudo dnf install -y fedora-workstation-repositories 2>/dev/null || true
sudo dnf config-manager setopt google-chrome.enabled=1 2>/dev/null || \
sudo dnf install -y "https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm"
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
info "Adding Brave repo..."
sudo dnf install -y dnf-plugins-core
sudo dnf config-manager add --from-repofile=https://brave-browser-rpm-release.s3.brave.com/brave-browser.repo || \
true
sudo rpm --import https://brave-browser-rpm-release.s3.brave.com/brave-core.asc 2>/dev/null || true
# 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
@@ -531,7 +665,169 @@ if [[ "$ASSUME_YES" != "true" ]]; then
fi
pause_if_debug
step "8 Summary"
#------------------------------------------
# 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
@@ -557,6 +853,15 @@ 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
DM=$(detect_display_manager)
ok "Display manager: ${DM:-none}"