#!/bin/bash # Post install script for Fedora # Author: Jacob Schantli # Version: 1.1 # Make a pipeline fail if any command in it fails. Without this, # `curl ... | bash` reports success even when the download fails. set -o pipefail # Steps that failed during this run (shown in the summary at the end) FAILED_STEPS=() # ----------------------------------------------------------------------------- # Global Configuration # ----------------------------------------------------------------------------- DNF_PACKAGES=( "gnome-tweaks" "steam" "lutris" "btop" "fastfetch" "distrobox" "pipx" "bash-completion" "tar" "bat" "tree" "unzip" "git" "fzf" "mpv" "fuse" "fuse-libs" "nodejs" ) FLATPAK_PACKAGES=( "com.github.tchx84.Flatseal" "net.nokyan.Resources" "com.mattjakeman.ExtensionManager" "io.github.realmazharhussain.GdmSettings" "com.ranfdev.DistroShelf" "it.mijorus.gearlever" "io.github.fastrizwaan.WineZGUI" "com.vysp3r.ProtonPlus" "io.missioncenter.MissionCenter" "io.github.peazip.PeaZip" #"app.devsuite.Ptyxis" "re.sonny.Junction" #"com.github.rafostar.Clapper" "io.github.flattool.Ignition" "io.github.swordpuffin.rewaita" "page.tesk.Refine" #"io.gitlab.adhami3310.Converter" "com.usebottles.bottles" "com.bambulab.BambuStudio" "com.collaboraoffice.Office" "io.github.kolunmi.Bazaar" "com.fastmail.Fastmail" ) GNOME_EXTENSIONS=( "dhruva@narkagni" "app-grid-wizard@mirzadeh.pro" "appindicatorsupport@rgcjonas.gmail.com" "blur-my-shell@aunetx" "caffeine@patapon.info" "dynamic-music-pill@andbal" "gnome-fuzzy-app-search@gnome-shell-extensions.Czarlie.gitlab.com" #"IP-Finder@linxgem33.com" #"lan-ip-address@mrhuber.com" #"update-extension@purejava.org" "user-theme@gnome-shell-extensions.gcampax.github.com" "gnome-wallpaper-engine@gjs.com.zip" ) # ----------------------------------------------------------------------------- # Helper Functions # ----------------------------------------------------------------------------- # Colors for output GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[1;33m' NC='\033[0m' # No Color log_info() { echo -e "${GREEN}[INFO]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } command_exists() { command -v "$1" >/dev/null 2>&1 } # Check if the last command was successful check_status() { local status=$? if [ $status -eq 0 ]; then log_info "$1 successful." else log_error "$1 failed." FAILED_STEPS+=("$1") # If the second argument is "critical", exit the script if [ "$2" == "critical" ]; then log_error "Critical error encountered. Exiting." exit 1 fi fi } # Append text to a file if it doesn't already exist append_to_file_if_missing() { local file="$1" local text="$2" local description="$3" local use_sudo="$4" if grep -Fq "$text" "$file"; then log_warn "$description already exists in $file. Skipping." else if [ "$use_sudo" == "true" ]; then echo "$text" | sudo tee -a "$file" > /dev/null else echo "$text" >> "$file" fi log_info "Added $description to $file." fi } # ----------------------------------------------------------------------------- # Installation Functions # ----------------------------------------------------------------------------- optimize_dnf() { log_info "----------------------------------------------------" log_info "Modifying /etc/dnf/dnf.conf for faster downloads." log_info "----------------------------------------------------" DNF_CONF="/etc/dnf/dnf.conf" if [ -f "$DNF_CONF" ]; then append_to_file_if_missing "$DNF_CONF" "max_parallel_downloads=10" "max_parallel_downloads" "true" append_to_file_if_missing "$DNF_CONF" "defaultyes=True" "defaultyes" "true" append_to_file_if_missing "$DNF_CONF" "keepcache=True" "keepcache" "true" else log_error "$DNF_CONF not found!" fi sleep 1 } install_fonts() { log_info "----------------------------------------------------" log_info "Installing Microsoft Core Fonts and Meslo Nerd Font." log_info "----------------------------------------------------" log_info "Updating system and installing prerequisites..." sudo dnf upgrade --refresh -y check_status "System upgrade" "critical" sudo dnf install -y curl cabextract xorg-x11-font-utils fontconfig wget unzip check_status "Prerequisites installation" "critical" log_info "Installing Microsoft Core Fonts..." if rpm -q msttcore-fonts-installer > /dev/null 2>&1; then log_warn "Microsoft Core Fonts already installed." else sudo rpm -i https://downloads.sourceforge.net/project/mscorefonts2/rpms/msttcore-fonts-installer-2.6-1.noarch.rpm check_status "Microsoft Core Fonts installation" "non-critical" fi log_info "Installing Meslo Nerd Font..." FONT_NAME="MesloLGS Nerd Font Mono" # Redirect instead of `grep -q` so fc-list isn't killed by SIGPIPE (pipefail) if fc-list :family | grep -i "$FONT_NAME" > /dev/null; then log_info "Font '$FONT_NAME' is already installed." else log_info "Installing font '$FONT_NAME'..." FONT_URL="https://github.com/ryanoasis/nerd-fonts/releases/latest/download/Meslo.zip" FONT_DIR="$HOME/.local/share/fonts" TEMP_DIR=$(mktemp -d) wget -q -O "$TEMP_DIR/Meslo.zip" "$FONT_URL" \ && unzip -q "$TEMP_DIR/Meslo.zip" -d "$TEMP_DIR" \ && mkdir -p "$FONT_DIR/$FONT_NAME" \ && mv "$TEMP_DIR"/*.ttf "$FONT_DIR/$FONT_NAME/" \ && fc-cache -f "$FONT_DIR" check_status "Meslo Nerd Font installation" "non-critical" rm -rf "$TEMP_DIR" fi } install_repos() { log_info "----------------------------------------------------" log_info "Adding RPM Fusion repositories." log_info "----------------------------------------------------" sudo dnf install -y \ "https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm" \ "https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm" check_status "RPM Fusion repositories installation" "non-critical" } install_software() { log_info "----------------------------------------------------" log_info "Installing most used software." log_info "----------------------------------------------------" log_info "Installing DNF packages..." sudo dnf install -y "${DNF_PACKAGES[@]}" check_status "DNF packages installation" "non-critical" # Ensure flatpak is set up if ! command -v flatpak &> /dev/null; then log_info "Flatpak not found. Installing..." sudo dnf install -y flatpak fi log_info "Adding Flathub remote..." flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo check_status "Flathub remote add" "non-critical" log_info "Installing Flatpak packages..." # One transaction is much faster (single dependency resolution, shared # runtimes downloaded once). If it fails, retry one-by-one so a single # bad app ID doesn't block the rest. if flatpak install flathub -y "${FLATPAK_PACKAGES[@]}"; then log_info "Flatpak packages installation successful." else log_warn "Batch Flatpak install failed. Retrying apps individually..." for app in "${FLATPAK_PACKAGES[@]}"; do log_info "Installing $app..." flatpak install flathub -y "$app" check_status "Flatpak $app" "non-critical" done fi log_info "----------------------------------------------------" log_info "Installing Homebrew" log_info "----------------------------------------------------" # Install Homebrew (also detect it when it isn't on PATH yet) if command_exists brew || [ -x "/home/linuxbrew/.linuxbrew/bin/brew" ]; then log_info "Homebrew already installed." else log_info "Installing Homebrew..." # Download first so a failed download isn't reported as success. # NONINTERACTIVE=1 avoids prompts during brew install. BREW_INSTALLER=$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh) \ && NONINTERACTIVE=1 /bin/bash -c "$BREW_INSTALLER" check_status "Homebrew installation" "non-critical" # Add brew to path for the current session if [ -d "/home/linuxbrew/.linuxbrew/bin" ]; then eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" elif [ -d "$HOME/.linuxbrew/bin" ]; then eval "$($HOME/.linuxbrew/bin/brew shellenv)" fi fi } install_discord() { log_info "----------------------------------------------------" log_info "Installing Discord" log_info "----------------------------------------------------" if rpm -q discord > /dev/null 2>&1; then log_info "Discord already installed." else log_info "Downloading and installing Discord..." sudo dnf install -y "https://discord.com/api/download?platform=linux&format=rpm" check_status "Discord installation" "non-critical" fi } install_1password() { log_info "----------------------------------------------------" log_info "Installing 1Password" log_info "----------------------------------------------------" if rpm -q 1password > /dev/null 2>&1; then log_info "1Password already installed." else log_info "Downloading and installing 1Password..." sudo dnf install -y https://downloads.1password.com/linux/rpm/stable/x86_64/1password-latest.rpm check_status "1Password installation" "non-critical" fi } install_PIA() { log_info "----------------------------------------------------" log_info "Installing Private Internet Access (PIA)" log_info "----------------------------------------------------" if [ -d "/opt/piavpn" ]; then log_info "PIA is already installed." else log_info "Downloading and installing PIA..." PIA_URL=$(curl -s https://www.privateinternetaccess.com/download/linux-vpn | grep -oP 'https://[^\"]+pia-linux-[0-9\.\-]+\.run' | head -n 1) if [ -n "$PIA_URL" ]; then wget -qO /tmp/pia-linux.run "$PIA_URL" chmod +x /tmp/pia-linux.run # Run the installer as the current user, letting it prompt for sudo/pkexec if needed /tmp/pia-linux.run --quiet --accept rm /tmp/pia-linux.run check_status "PIA installation" "non-critical" else log_error "Failed to fetch PIA download URL." FAILED_STEPS+=("PIA installation") fi fi } install_cachyos() { log_info "----------------------------------------------------" log_info "Installing CachyOS kernel" log_info "----------------------------------------------------" # kernel-cachyos is built for x86-64-v3 CPUs. Ask before installing it on # an older CPU, where the new (default) boot entry may not start. if ! /lib64/ld-linux-x86-64.so.2 --help 2>/dev/null | grep 'x86-64-v3 (supported' > /dev/null; then log_warn "Your CPU does not appear to support x86-64-v3, which kernel-cachyos targets." read -p "Install the CachyOS kernel anyway? [y/N]: " reply if [[ ! "$reply" =~ ^[Yy]$ ]]; then log_info "Skipping CachyOS kernel." return fi fi sudo dnf copr enable -y bieszczaders/kernel-cachyos check_status "Enabling CachyOS Copr" "non-critical" sudo dnf install -y kernel-cachyos kernel-cachyos-devel-matched check_status "CachyOS Kernel installation" "non-critical" log_info "Installing CachyOS addons" sudo dnf copr enable -y bieszczaders/kernel-cachyos-addons check_status "Enabling CachyOS Addons Copr" "non-critical" sudo dnf swap -y zram-generator-defaults cachyos-settings sudo dracut -f sudo dnf -y install scx-scheds scx-tools scx-manager ananicy-cpp check_status "CachyOS addons installation" "non-critical" } install_p03() { log_info "----------------------------------------------------" log_info "Installing P03 kernel" log_info "----------------------------------------------------" # The default kernel-p03 build needs x86-64-v3 and won't boot on older # CPUs. Those get the x86-64-v2 build (kernel-p03-gcc) instead. if /lib64/ld-linux-x86-64.so.2 --help 2>/dev/null | grep 'x86-64-v3 (supported' > /dev/null; then P03_PACKAGE="kernel-p03" else log_warn "CPU lacks x86-64-v3 support. Installing the x86-64-v2 build (kernel-p03-gcc)." P03_PACKAGE="kernel-p03-gcc" fi sudo dnf copr enable -y catpieleaf/kernel-p03 check_status "Enabling P03 Copr" "non-critical" sudo dnf install -y "$P03_PACKAGE" check_status "P03 kernel installation" "non-critical" # P03 signs itself with a locally generated key. With Secure Boot on, that # key must be enrolled or the kernel won't boot. if mokutil --sb-state 2>/dev/null | grep -i 'SecureBoot enabled' > /dev/null; then log_info "Secure Boot is enabled. Enrolling the P03 signing key..." log_info "Pick a one-time password. On the next boot, choose 'Enroll MOK' and enter it." sudo mokutil --import /etc/kernel/certs/p03-kernel/mok.der check_status "P03 Secure Boot key enrollment" "non-critical" fi if rpm -q kernel-cachyos > /dev/null 2>&1; then log_warn "The CachyOS kernel is also installed. The newest kernel usually boots by default;" log_warn "pick the other one from the boot menu if needed." fi log_warn "P03 uses the PRJC scheduler, so sched_ext schedulers (scx-scheds) likely won't work on it." } install_linuxtoys() { log_info "----------------------------------------------------" log_info "Installing LinuxToys" log_info "----------------------------------------------------" curl -fsSL https://linux.toys/install.sh | bash check_status "LinuxToys installation" "non-critical" } install_shell_env() { log_info "----------------------------------------------------" log_info "Installing Shell Environment (Starship, FZF, Zoxide, MyBash)" log_info "----------------------------------------------------" # Install Starship if command_exists starship; then log_info "Starship already installed." else log_info "Installing Starship..." # -y skips the installer's confirmation prompt curl -sSL https://starship.rs/install.sh | sudo sh -s -- -y check_status "Starship installation" "non-critical" fi # Install FZF (Fallback if not installed via DNF) if ! command_exists fzf; then log_info "Installing FZF via git..." git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf ~/.fzf/install --all fi # Install Zoxide if command_exists zoxide; then log_info "Zoxide already installed." else log_info "Installing Zoxide..." curl -sSL https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | sh check_status "Zoxide installation" "non-critical" fi # Clone MyBash log_info "Cloning MyBash configuration..." GITPATH="$HOME/.local/share/mybash" if [ -d "$GITPATH" ]; then rm -rf "$GITPATH" fi mkdir -p "$HOME/.local/share" if git clone https://gitea.schantli.ca/jschantli/mybash.git "$GITPATH"; then log_info "MyBash cloned successfully." # Link Config log_info "Linking configuration files..." OLD_BASHRC="$HOME/.bashrc" # Check if .bashrc exists in the cloned repo if [ -f "$GITPATH/.bashrc" ]; then if [ -e "$OLD_BASHRC" ] && [ ! -e "$HOME/.bashrc.bak" ]; then log_info "Backing up .bashrc to .bashrc.bak..." mv "$OLD_BASHRC" "$HOME/.bashrc.bak" fi log_info "Linking new .bashrc..." ln -svf "$GITPATH/.bashrc" "$HOME/.bashrc" # Ensure Starship is initialized in the new .bashrc if ! grep -q "starship init bash" "$HOME/.bashrc"; then log_warn ".bashrc does not contain starship init. Appending it..." echo 'eval "$(starship init bash)"' >> "$HOME/.bashrc" fi else log_error "$GITPATH/.bashrc not found! Skipping .bashrc linking." fi # Check if starship.toml exists in the cloned repo if [ -f "$GITPATH/starship.toml" ]; then log_info "Linking starship.toml..." mkdir -p "$HOME/.config" ln -svf "$GITPATH/starship.toml" "$HOME/.config/starship.toml" else log_warn "$GITPATH/starship.toml not found! Skipping Starship config linking." fi # Check if config.jsonc exists in the cloned repo if [ -f "$GITPATH/config.jsonc" ]; then log_info "Linking config.jsonc..." mkdir -p "$HOME/.config/fastfetch/" ln -svf "$GITPATH/config.jsonc" "$HOME/.config/fastfetch/config.jsonc" else log_warn "$GITPATH/config.jsonc not found! Skipping fastfetch config linking." fi check_status "Linking config files" "non-critical" else log_error "Failed to clone MyBash repository. Skipping shell configuration." fi } install_topgrade() { log_info "----------------------------------------------------" log_info "Installing Topgrade" log_info "----------------------------------------------------" if command_exists topgrade; then log_info "Topgrade already installed." return fi sudo dnf copr enable -y lilay/topgrade check_status "Enabling Topgrade" "non-critical" sudo dnf install -y topgrade check_status "Topgrade installation" "non-critical" } install_gnome_extensions() { log_info "----------------------------------------------------" log_info "Installing GNOME extensions" log_info "----------------------------------------------------" # Ensure pipx is installed and path is set if ! command -v pipx &> /dev/null; then log_info "pipx not found. Installing..." sudo dnf install -y pipx pipx ensurepath fi # Install gnome-extensions-cli log_info "Installing gnome-extensions-cli..." pipx install gnome-extensions-cli --system-site-packages check_status "gnome-extensions-cli installation" "non-critical" # Define the tool path explicitly to avoid 'pipx run' overhead/warnings EXTENSION_TOOL="$HOME/.local/bin/gnome-extensions-cli" log_info "Installing GNOME extensions..." log_info "NOTE: A popup may appear asking you to confirm installation for each extension." log_info "If the extension is already installed, no popup will appear." for extension in "${GNOME_EXTENSIONS[@]}"; do log_info "Processing $extension..." # Install (using the direct binary path) "$EXTENSION_TOOL" install "$extension" check_status "GNOME Extension $extension" "non-critical" # Attempt to enable the extension (in case it's installed but disabled) log_info "Ensuring $extension is enabled..." gnome-extensions enable "$extension" # Prompt user to press Enter to continue read -p "Press Enter to continue (if a popup appeared, confirm it first)..." done } install_antigravity() { log_info "----------------------------------------------------" log_info "Installing Antigravity App, IDE, and CLI" log_info "----------------------------------------------------" log_info "Installing Antigravity CLI..." curl -fsSL https://antigravity.google/cli/install.sh | bash check_status "Antigravity CLI installation" "non-critical" log_info "Installing Antigravity App and IDE..." # Community installer (opensnap). It downloads Google's official tarballs # into /opt and adds launchers. It needs python3, and nautilus-python # enables its "Open in Antigravity IDE" entry in Files. sudo dnf install -y python3 nautilus-python INSTALLER_URL="https://opensnap.github.io/antigravity/install.sh" curl -fsSL "$INSTALLER_URL" | sudo -E env ANTIGRAVITY_LINUX_INSTALLER_URL="$INSTALLER_URL" bash -s -- --all check_status "Antigravity App and IDE installation" "non-critical" } # ----------------------------------------------------------------------------- # Main Menu # ----------------------------------------------------------------------------- show_menu() { clear echo "====================================================" echo " Fedora Post-Install Script - Main Menu" echo "====================================================" echo "1. Install Everything (Recommended)" echo "2. Install Everything (No CachyOS Kernel)" echo "3. Optimize DNF" echo "4. Install Fonts" echo "5. Add Repositories (RPM Fusion)" echo "6. Install Software (DNF & Flatpak)" echo "7. Install CachyOS Kernel" echo "8. Install LinuxToys" echo "9. Install Shell Environment (Starship, MyBash)" echo "10. Install GNOME Extensions" echo "11. Install Discord" echo "12. Install 1Password" echo "13. Install Private Internet Access (PIA)" echo "14. Install Topgrade" echo "15. Install Antigravity App, IDE, and CLI" echo "16. Install P03 Kernel (experimental, not in Install Everything)" echo "0. Exit" echo "====================================================" read -p "Enter your choice [0-16]: " choice } # Run every installation step. Pass "skip_kernel" to leave out CachyOS. run_all() { optimize_dnf install_fonts install_repos install_software install_discord install_1password install_PIA install_topgrade if [ "$1" != "skip_kernel" ]; then install_cachyos fi install_linuxtoys install_shell_env install_gnome_extensions install_antigravity } # Show which steps failed during this run, if any print_summary() { log_info "----------------------------------------------------" if [ ${#FAILED_STEPS[@]} -eq 0 ]; then log_info "Post-install script completed successfully!" else log_warn "Post-install script finished with ${#FAILED_STEPS[@]} failed step(s):" for step in "${FAILED_STEPS[@]}"; do log_warn " - $step" done fi log_info "Please restart your shell or run 'source ~/.bashrc' to see the changes." log_info "----------------------------------------------------" } # ----------------------------------------------------------------------------- # Main Execution # ----------------------------------------------------------------------------- log_info "Starting Post-install script for Fedora..." sleep 2 # Ensure sudo permissions are available upfront sudo -v while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null & while true; do show_menu case $choice in 1) run_all break ;; 2) run_all skip_kernel break ;; 3) optimize_dnf ;; 4) install_fonts ;; 5) install_repos ;; 6) install_software ;; 7) install_cachyos ;; 8) install_linuxtoys ;; 9) install_shell_env ;; 10) install_gnome_extensions ;; 11) install_discord ;; 12) install_1password ;; 13) install_PIA ;; 14) install_topgrade ;; 15) install_antigravity ;; 16) install_p03 ;; 0) log_info "Exiting..." print_summary exit 0 ;; *) log_error "Invalid option. Please try again." sleep 2 ;; esac if [ "$choice" != "1" ] && [ "$choice" != "2" ]; then read -p "Press Enter to return to the main menu..." fi done print_summary