888 lines
33 KiB
Bash
Executable File
888 lines
33 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# setup.sh — unified Fedora configuration
|
|
# Target: bare/minimal Fedora 44 and newer
|
|
#
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=lib/common.sh
|
|
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
|
|
# DEBUG_MODE must be initialised before anything reads it under set -u.
|
|
#------------------------------------------
|
|
DEBUG_MODE=false
|
|
ASSUME_YES=false
|
|
DE=""
|
|
TWM=""
|
|
SELINUX_OPT=""
|
|
SNAPPER_OPT=""
|
|
DOTFILES_OPT=""
|
|
NVIDIA_OPT=""
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
setup.sh — unified Fedora configuration (Fedora 44+)
|
|
|
|
Usage:
|
|
./setup.sh interactive: pick a DE and a TWM
|
|
./setup.sh --de=none --twm=niri non-interactive, explicit choices
|
|
./setup.sh --debug pause after each section
|
|
|
|
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)
|
|
-y, --yes never prompt (requires --de and --twm)
|
|
-d, --debug pause after each section instead of aborting on error
|
|
-h, --help show this help
|
|
|
|
Valid DE/TWM combinations:
|
|
any DE + any TWM DE provides the session, TWM is added alongside it
|
|
none + any TWM TWM-only machine, SDDM is installed as the greeter
|
|
any DE + none plain desktop, no tiling window manager
|
|
none + none refused: that is not a graphical system
|
|
|
|
Notes:
|
|
* chadwm is an X11 window manager; the other three are Wayland compositors.
|
|
* Run as your normal user, not root. sudo is used where needed.
|
|
EOF
|
|
}
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
-d|--debug) DEBUG_MODE=true ;;
|
|
-y|--yes) ASSUME_YES=true ;;
|
|
--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 ;;
|
|
*)
|
|
err "Unknown argument: $arg"
|
|
printf "\n"
|
|
usage
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
export DEBUG_MODE ASSUME_YES
|
|
|
|
if is_debug; then
|
|
set +e # walk through failures instead of dying on the first one
|
|
else
|
|
set -e
|
|
fi
|
|
|
|
#------------------------------------------
|
|
# Preconditions
|
|
#------------------------------------------
|
|
# clear can fail without TERM (cron, piped install, ssh without a tty) and would
|
|
# abort the whole run under set -e.
|
|
clear 2>/dev/null || true
|
|
|
|
printf "\n"
|
|
color_yellow "----------------------------------------------------------------"
|
|
color_yellow "Fedora unified configuration"
|
|
color_yellow "----------------------------------------------------------------"
|
|
|
|
if [[ ${EUID} -eq 0 ]]; then
|
|
err "Do not run this as root. Run it as your user; it calls sudo itself."
|
|
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:"
|
|
err " ./setup.sh --yes --de=none --twm=niri"
|
|
exit 2
|
|
fi
|
|
|
|
if is_debug; then
|
|
color_red "!!! DEBUG MODE ENABLED !!!"
|
|
color_red "The script will pause after each section."
|
|
fi
|
|
|
|
#------------------------------------------
|
|
# OS gate — Fedora only, 44+
|
|
#------------------------------------------
|
|
step "OS detection"
|
|
rule
|
|
|
|
# Overridable so the script can be tested on a non-Fedora host.
|
|
OS_RELEASE_FILE="${OS_RELEASE_FILE:-/etc/os-release}"
|
|
# shellcheck source=/dev/null
|
|
source "$OS_RELEASE_FILE"
|
|
|
|
OS_ID="${ID:-unknown}"; OS_ID="${OS_ID,,}"
|
|
OS_PRETTY="${PRETTY_NAME:-unknown}"
|
|
|
|
if [[ "$OS_ID" != "fedora" ]]; then
|
|
err "Unsupported distribution: $OS_PRETTY (ID=${OS_ID})"
|
|
err "This repo targets Fedora only. RHEL clones are refused on purpose:"
|
|
err "they ship no dnf5 config-manager and no RPM Fusion, so both the base"
|
|
err "setup and every module would fail half-way."
|
|
exit 1
|
|
fi
|
|
|
|
FEDORA_REL=$(rpm -E %fedora)
|
|
if [[ "$FEDORA_REL" =~ ^[0-9]+$ ]] && (( FEDORA_REL < 44 )); then
|
|
err "$OS_PRETTY reports release ${FEDORA_REL}, but this setup targets Fedora 44+."
|
|
exit 1
|
|
fi
|
|
|
|
ok "Detected: $OS_PRETTY (release ${FEDORA_REL})"
|
|
pause_if_debug
|
|
|
|
#------------------------------------------
|
|
# 1 Base settings
|
|
#------------------------------------------
|
|
step "1 Base settings"
|
|
rule
|
|
|
|
if ! command -v curl >/dev/null 2>&1; then
|
|
info "curl missing. Installing..."
|
|
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..."
|
|
install_packages dnf5-plugins
|
|
fi
|
|
|
|
info "Tuning DNF..."
|
|
sudo dnf config-manager setopt max_parallel_downloads=20
|
|
sudo dnf config-manager setopt defaultyes=True
|
|
|
|
info "Refreshing metadata..."
|
|
sudo dnf makecache --refresh
|
|
|
|
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
|
|
info "Flatpak missing. Installing..."
|
|
install_packages flatpak
|
|
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
|
|
pause_if_debug
|
|
|
|
#------------------------------------------
|
|
# 2 Menu helpers
|
|
#------------------------------------------
|
|
# $1 = prompt, $2 = default answer, rest = valid answers
|
|
#------------------------------------------
|
|
# 3 System settings
|
|
#------------------------------------------
|
|
SELINUX_DISABLED=false
|
|
BTRFS_SNAPPER=false
|
|
|
|
if [[ -z "$SELINUX_OPT" ]]; then
|
|
step "3 System settings"
|
|
rule
|
|
printf "\n"
|
|
printf "\n"
|
|
choice=$(ui_confirm "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
|
|
else
|
|
info "Root filesystem is not btrfs; skipping snapshot setup."
|
|
fi
|
|
fi
|
|
|
|
case "${SELINUX_OPT:-}" in
|
|
disabled) SELINUX_DISABLED=true ;;
|
|
keep|"") : ;;
|
|
*) err "Unknown --selinux value: '$SELINUX_OPT' (keep|disabled)"; exit 2 ;;
|
|
esac
|
|
case "${SNAPPER_OPT:-}" in
|
|
yes) BTRFS_SNAPPER=true ;;
|
|
no) BTRFS_SNAPPER=false ;; # explicit flag beats the interactive default
|
|
""|keep) : ;;
|
|
*) 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"
|
|
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)")
|
|
case "$choice" in
|
|
GNOME*) DE="gnome" ;;
|
|
Plasma*) DE="plasma" ;;
|
|
XFCE*) DE="xfce" ;;
|
|
*) DE="none" ;;
|
|
esac
|
|
fi
|
|
|
|
case "$DE" in
|
|
gnome|plasma|xfce|none) ;;
|
|
*) err "Unknown --de value: '$DE' (gnome|plasma|xfce|none)"; exit 2 ;;
|
|
esac
|
|
pause_if_debug
|
|
|
|
#------------------------------------------
|
|
# 5 Tiling window manager selection
|
|
#------------------------------------------
|
|
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")
|
|
case "$choice" in
|
|
Niri*) TWM="niri" ;;
|
|
Hyprland*) TWM="hyprland" ;;
|
|
MangoWC*) TWM="mango" ;;
|
|
chadwm*) TWM="chadwm" ;;
|
|
*) TWM="none" ;;
|
|
esac
|
|
fi
|
|
|
|
case "$TWM" in
|
|
niri|hyprland|mango|chadwm|none) ;;
|
|
*) err "Unknown --twm value: '$TWM' (niri|hyprland|mango|chadwm|none)"; exit 2 ;;
|
|
esac
|
|
|
|
ok "Selection: DE=${DE} TWM=${TWM}"
|
|
|
|
# A bare install with neither is not a graphical system. Refuse rather than
|
|
# leave the user with a machine that boots to a black screen.
|
|
if [[ "$DE" == "none" && "$TWM" == "none" ]]; then
|
|
err "DE=none with TWM=none would leave no graphical session at all."
|
|
err "Pick a desktop environment, a tiling window manager, or both."
|
|
exit 2
|
|
fi
|
|
pause_if_debug
|
|
|
|
#------------------------------------------
|
|
# 5 Dispatch
|
|
#------------------------------------------
|
|
export DE TWM
|
|
|
|
run_module() {
|
|
local path="$1"
|
|
if [[ ! -f "$path" ]]; then
|
|
err "Module not found: $path"
|
|
return 1
|
|
fi
|
|
info "Running $(basename "$path")"
|
|
printf "\n"
|
|
# Separate process: a module's own shell options and exit codes stay its own.
|
|
bash "$path"
|
|
}
|
|
|
|
step "5 Installation"
|
|
rule
|
|
|
|
if [[ "$SELINUX_DISABLED" == "true" ]]; then
|
|
disable_selinux
|
|
pause_if_debug
|
|
fi
|
|
|
|
if [[ "$BTRFS_SNAPPER" == "true" ]]; then
|
|
ensure_btrfs_compression
|
|
setup_snapper
|
|
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
|
|
fi
|
|
|
|
if [[ "$TWM" != "none" ]]; then
|
|
run_module "$SCRIPT_DIR/modules/twm/${TWM}.sh"
|
|
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"
|
|
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
|
|
DM=$(detect_display_manager)
|
|
ok "Display manager: ${DM:-none}"
|
|
|
|
if [[ -z "$DM" ]]; then
|
|
warn "No display manager is active. You will boot to a text console."
|
|
warn "Install one, or run a TWM module which sets up SDDM."
|
|
fi
|
|
|
|
printf "\n"
|
|
color_green "Fedora setup complete. A reboot is recommended."
|
|
if [[ "$ASSUME_YES" == "true" ]]; then
|
|
info "Unattended run: not rebooting."
|
|
exit 0
|
|
fi
|
|
|
|
reboot_choice=$(ui_confirm "Reboot now?" "no")
|
|
if [[ "$reboot_choice" == "yes" ]]; then
|
|
color_red "Rebooting now."
|
|
sudo reboot
|
|
else
|
|
color_yellow "Skipping reboot."
|
|
exit 0
|
|
fi
|