Two bugs, one story: ./install on a machine that had been half set up before produced no Hyprland at all, and said so in one line among twenty minutes of scrollback. Terra bootstraps itself with --repofrompath, which defines a throwaway repo id just long enough to install terra-release. Run it again on a machine that already has terra-release and dnf5 refuses the whole transaction -- the throwaway id collides with the real one. That step sits above everything, so set -e ended the stage before a single package was considered. It is skipped now when terra-release is already installed. The rest is the reason one failed repo cost the desktop. Hyprland was installed near the bottom of the stage, below a codec swap, two group updates and a GStreamer glob, any one of which can fail for reasons outside this repository. It now installs directly after the packages it needs and before anything optional, and everything fragile below it runs through a soft helper that logs and continues rather than ending the run. What was stepped over is listed at the end, because tolerating a failure only beats aborting on it if somebody is told. A missing Hyprland is still fatal, and now says so in words. Also removes the leftover disabled solopasha/hyprland COPR, which would mix with lionheartp's the moment anyone enabled it while debugging. Fixes the usage widget reading 1500%: the endpoint reports percentages, not 0..1 fractions. Clamped as well, and the widget answers a click now -- it set interactive:false, which disables the mouse area its own handler needed.
378 lines
17 KiB
Bash
Executable File
378 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
# --- Helper functions ---
|
|
log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
|
exists() { command -v "$1" >/dev/null 2>&1; }
|
|
|
|
# The package names in a list, without the comments that explain them.
|
|
#
|
|
# The lists are annotated -- which package exists for which settings page, why
|
|
# an exception was made -- and those annotations are for whoever reads the file
|
|
# next. dnf is not so forgiving: it does not ignore an argument it cannot
|
|
# match, it reports "No match for argument: #" and exits 1, and with `set -e`
|
|
# above that ends this stage on the first annotated list it reaches.
|
|
#
|
|
# It could not be seen from here. On a machine that already has everything, a
|
|
# re-run matches every real name and fails only on the comments; and every
|
|
# contract that reads these lists strips comments before comparing, so the
|
|
# tests were reading a file this script was not.
|
|
packages_in() {
|
|
sed 's/#.*//' "$1" | tr "\n" " "
|
|
}
|
|
|
|
# Runs something whose failure must not cost you the desktop.
|
|
#
|
|
# `set -e` above is right for the packages Panama cannot work without and wrong
|
|
# for everything else. A codec swap that finds nothing to swap, a group update
|
|
# renamed upstream, a third-party host that is down -- each of those used to end
|
|
# this stage wherever it happened to sit, and the desktop was installed near the
|
|
# bottom, so any one of them meant a machine with no Hyprland on it and a single
|
|
# line of dnf output to explain why.
|
|
#
|
|
# So the ordering rule for this file: anything that can fail for a reason
|
|
# outside this repository goes below the desktop, and goes through here.
|
|
soft() {
|
|
local what="$1"; shift
|
|
"$@" >/dev/null 2>&1 || { log "$what did not complete; continuing"; softly_failed+=("$what"); }
|
|
}
|
|
softly_failed=()
|
|
|
|
# --- Defined Paths ---
|
|
# The default, not an assignment: ./install and link-dotfiles honor an exported
|
|
# PANAMA_PATH, and clobbering it here made a clone anywhere else source the
|
|
# extras catalog from a path that does not exist.
|
|
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
|
|
|
# Reading the extras catalog, shared with `panama apps` so the two front doors
|
|
# cannot disagree about what a category contains.
|
|
# shellcheck source=../lib/extras-catalog
|
|
source "$PANAMA_PATH/setup/lib/extras-catalog"
|
|
|
|
echo -e "\n--- Installing Repositories ---"
|
|
log "Installing RPM Fusion Free and Nonfree Repositories"
|
|
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 > /dev/null
|
|
log "Enabling Fedora Cisco OpenH264 Repository"
|
|
sudo dnf config-manager setopt fedora-cisco-openh264.enabled=1
|
|
log "Installing RPM Fusion AppStream Metadata"
|
|
sudo dnf update @core -y > /dev/null
|
|
sudo dnf install -y rpmfusion-\*-appstream-data > /dev/null
|
|
# Terra bootstraps itself: --repofrompath defines a throwaway repo just long
|
|
# enough to install terra-release, which then writes the real /etc/yum.repos.d
|
|
# entry. Doing that a second time is not harmless -- dnf5 refuses the whole
|
|
# transaction with 'Id is present more than once in the configuration', because
|
|
# the throwaway id collides with the one terra-release already installed.
|
|
#
|
|
# That is what killed a re-run on a machine Terra had already reached: this sits
|
|
# in the repository section, above everything, so `set -e` ended the stage
|
|
# before a single package was considered. An installer whose second run does
|
|
# less than its first is worse than one that never ran.
|
|
if rpm -q terra-release >/dev/null 2>&1; then
|
|
log "Terra repository already installed"
|
|
else
|
|
log "Installing Terra Repository"
|
|
sudo dnf install -y --nogpgcheck --repofrompath 'terra,https://repos.fyralabs.com/terra$releasever' terra-release > /dev/null
|
|
fi
|
|
|
|
echo -e "\n--- Installing relevant packages ---"
|
|
log "Updating all packages. This may take a while"
|
|
sudo dnf update -y --refresh > /dev/null
|
|
|
|
# --- Install all initial packages ---
|
|
PACKAGES_FILE="$PANAMA_PATH/setup/packages/initial-packages"
|
|
if [[ -f "$PACKAGES_FILE" ]]; then
|
|
INITIAL_PACKAGES=$(packages_in "$PACKAGES_FILE")
|
|
log "Installing Initial Packages"
|
|
echo -e "Includes the following packages:"
|
|
echo -e "$(<"$PACKAGES_FILE")"
|
|
sudo dnf install -y $INITIAL_PACKAGES > /dev/null
|
|
log "Initial packages installed!"
|
|
else
|
|
log "Package list was not in specified path: $PACKAGES_FILE"
|
|
fi
|
|
|
|
# --- Install Desktop Packages ---
|
|
DESKTOP_FILE="$PANAMA_PATH/setup/packages/desktop-packages"
|
|
if [[ -f "$DESKTOP_FILE" ]]; then
|
|
DESKTOP_PACKAGES=$(packages_in "$DESKTOP_FILE")
|
|
log "Installing Desktop Packages"
|
|
echo -e "Includes the following packages:"
|
|
echo -e "$(<"$DESKTOP_FILE")"
|
|
sudo dnf install -y $DESKTOP_PACKAGES > /dev/null
|
|
log "Desktop packages installed!"
|
|
else
|
|
log "Package list was not in specified path: $DESKTOP_FILE"
|
|
fi
|
|
|
|
# --- Install the Hyprland desktop ---
|
|
#
|
|
# Directly after desktop-packages, which is what supplies the dnf plugin that
|
|
# `dnf copr` needs, and deliberately before anything optional. This is the one
|
|
# thing on the list that Panama is; a machine that gets only this far is a
|
|
# machine you can log into, and every step below it is a convenience.
|
|
#
|
|
# Most of these live in the lionheartp/Hyprland COPR rather than Fedora proper.
|
|
HYPR_FILE="$PANAMA_PATH/setup/packages/hyprland-packages"
|
|
if [[ -f "$HYPR_FILE" ]]; then
|
|
log "Enabling Hyprland COPR"
|
|
sudo dnf copr enable -y lionheartp/Hyprland > /dev/null
|
|
HYPR_PACKAGES=$(packages_in "$HYPR_FILE")
|
|
log "Installing Hyprland desktop packages"
|
|
echo -e "Includes the following packages:"
|
|
echo -e "$(<"$HYPR_FILE")"
|
|
sudo dnf install -y --setopt=install_weak_deps=False $HYPR_PACKAGES > /dev/null
|
|
log "Hyprland packages installed!"
|
|
else
|
|
log "Package list was not in specified path: $HYPR_FILE"
|
|
fi
|
|
|
|
# Said out loud, because the failure this guards against was silent. The stage
|
|
# used to die somewhere above this point and report one red line among twenty
|
|
# minutes of scrollback, and the machine looked installed until you tried to log
|
|
# into it.
|
|
if rpm -q hyprland >/dev/null 2>&1; then
|
|
log "Hyprland $(rpm -q --queryformat '%{VERSION}' hyprland) is installed."
|
|
else
|
|
log "Hyprland is NOT installed. Nothing below this point will give you a desktop."
|
|
exit 1
|
|
fi
|
|
|
|
# --- Codecs and multimedia ---------------------------------------------------
|
|
#
|
|
# Below the desktop and every one of them non-fatal, because none is a
|
|
# dependency of it and each can fail for reasons that have nothing to do with
|
|
# this repository -- a swap whose source package this spin never shipped, a
|
|
# group renamed upstream between Fedora releases.
|
|
#
|
|
# A trailing `&& sync` on the group update previously meant a failure was exempt
|
|
# from set -e as well (bash does not apply -e to the left of a && list), so it
|
|
# went unreported rather than being deliberately tolerated. It is deliberate now.
|
|
|
|
log "Updating core, multimedia, and sound-and-video groups"
|
|
soft "the multimedia group update" \
|
|
sudo dnf4 groupupdate -y 'core' 'multimedia' 'sound-and-video' \
|
|
--setop='install_weak_deps=False' \
|
|
--exclude='PackageKit-gstreamer-plugin' \
|
|
--allowerasing
|
|
sync
|
|
log "Swapping ffmpeg-free for ffmpeg"
|
|
soft "the ffmpeg swap" sudo dnf swap -y 'ffmpeg-free' 'ffmpeg' --allowerasing
|
|
log "Swapping mesa-va-drivers for mesa-va-drivers-freeworld"
|
|
soft "the mesa driver swap" sudo dnf swap -y mesa-va-drivers mesa-va-drivers-freeworld
|
|
log "Upgrading Multimedia group with optional packages"
|
|
soft "the optional Multimedia upgrade" sudo dnf4 group upgrade -y --with-optional Multimedia
|
|
log "Installing GStreamer plugins (bad, good, base)"
|
|
soft "the GStreamer plugins" \
|
|
sudo dnf install -y gstreamer1-plugins-{bad-\*,good-\*,base} \
|
|
--exclude=gstreamer1-plugins-bad-free-devel
|
|
|
|
# --- Install Development Packages needed for Neovim ---
|
|
DEV_FILE="$PANAMA_PATH/setup/packages/development-packages"
|
|
if [[ -f "$DEV_FILE" ]]; then
|
|
DEV_PACKAGES=$(packages_in "$DEV_FILE")
|
|
log "Installing Development Packages. Mostly for Neovim."
|
|
echo -e "Includes the following packages:"
|
|
echo -e "$(<"$DEV_FILE")"
|
|
soft "the development packages" sudo dnf install -y $DEV_PACKAGES
|
|
log "Development packages installed!"
|
|
else
|
|
log "Package list was not in specified path: $DEV_FILE"
|
|
fi
|
|
|
|
# --- Node and pnpm, through nvm ----------------------------------------------
|
|
#
|
|
# nvm is a shell function rather than a binary, so it has to be sourced before
|
|
# it can be used at all -- and its script reads variables that `set -u` above
|
|
# treats as fatal, so the strictness is lifted for exactly that source and put
|
|
# straight back.
|
|
#
|
|
# Deliberately not dnf's nodejs: config/bash/shell switches Node per project
|
|
# from .nvmrc, and a system Node earlier on PATH would win every switch, leaving
|
|
# `nvm use` looking like it did nothing.
|
|
#
|
|
# pnpm goes inside the nvm-managed Node rather than beside it as its own dnf
|
|
# package, so it travels with the version it belongs to instead of outliving it.
|
|
if [[ -s /etc/profile.d/nvm.sh ]]; then
|
|
log "Installing the latest Node LTS through nvm"
|
|
set +u
|
|
# shellcheck source=/dev/null
|
|
source /etc/profile.d/nvm.sh
|
|
if nvm install --lts >/dev/null 2>&1; then
|
|
nvm alias default 'lts/*' >/dev/null 2>&1 || true
|
|
npm install -g pnpm >/dev/null 2>&1 || log "pnpm did not install"
|
|
log "Node $(node --version 2>/dev/null) with pnpm $(pnpm --version 2>/dev/null)"
|
|
else
|
|
log "nvm could not install Node; skipping"
|
|
fi
|
|
set -u
|
|
else
|
|
log "nvm is not installed, so Node was not set up"
|
|
fi
|
|
|
|
# --- Applications no repository packages -------------------------------------
|
|
#
|
|
# Everything else Panama installs comes from dnf or Flathub. These four do not
|
|
# exist in either, so each is an explicit exception with a reason, and each is
|
|
# skipped when already present so a re-run costs nothing.
|
|
#
|
|
# Claude Desktop is the half-exception: a repository does carry it, just not one
|
|
# Fedora or Flathub knows about, so what is exceptional there is adding the
|
|
# repository rather than installing around one.
|
|
#
|
|
# None of them pins a version. sunhat pinned URLs -- upscayl 2.11.5, LACT 0.5.4,
|
|
# a fedora-40 RPM -- and every one of them was a 404 within a release cycle. An
|
|
# installer that resolves "latest" keeps working; one that names a version rots.
|
|
#
|
|
# A failure here is logged and stepped over rather than aborting: this stage has
|
|
# already installed the desktop by this point, and an unreachable third-party
|
|
# host should not cost you that.
|
|
|
|
# Bun: the JavaScript runtime and package manager. No RPM, no flatpak.
|
|
if [[ -x "$HOME/.bun/bin/bun" ]]; then
|
|
log "Bun already installed at \"$HOME/.bun/bin/bun\""
|
|
else
|
|
log "Installing Bun via curl..."
|
|
curl -fsSL https://bun.sh/install | bash > /dev/null 2>&1 || log "Bun install failed; skipping"
|
|
fi
|
|
|
|
# Claude Code: Anthropic's CLI. The official installer keeps itself updated
|
|
# afterwards, so this runs once and then never needs to again.
|
|
if command -v claude >/dev/null 2>&1; then
|
|
log "Claude Code already installed at \"$(command -v claude)\""
|
|
else
|
|
log "Installing Claude Code via the official installer..."
|
|
curl -fsSL https://claude.ai/install.sh | bash > /dev/null 2>&1 || log "Claude Code install failed; skipping"
|
|
fi
|
|
|
|
# Claude Desktop: Anthropic ships macOS and Windows only, so this is a community
|
|
# RPM built from the official release. Panama used to build it from source -- it
|
|
# was `panama app claude-desktop` -- because no repository carried it. Upstream
|
|
# publishes one now, which is strictly better: the result upgrades with every
|
|
# other package instead of needing a slow rebuild each time a version ships.
|
|
#
|
|
# The repository is added by upstream's own setup script rather than by writing
|
|
# the .repo file out here. A baseurl copied into this repository is a pin by
|
|
# another name, and that script is the part upstream keeps correct.
|
|
if rpm -q claude-desktop-extra >/dev/null 2>&1; then
|
|
log "Claude Desktop already installed"
|
|
else
|
|
if [[ ! -f /etc/yum.repos.d/claude-desktop.repo ]]; then
|
|
log "Adding the Claude Desktop repository..."
|
|
curl -fsSL https://patrickjaja.github.io/claude-desktop-extra/install-rpm.sh \
|
|
| sudo bash > /dev/null 2>&1 || log "Could not add the Claude Desktop repository"
|
|
fi
|
|
log "Installing Claude Desktop..."
|
|
sudo dnf install -y claude-desktop-extra > /dev/null \
|
|
|| log "Claude Desktop install failed; skipping"
|
|
fi
|
|
|
|
# RustDesk: remote desktop. The flatpak cannot register the root-owned system
|
|
# service that unattended access needs -- see panama-doctor's rustdesk check --
|
|
# so this takes the RPM. The download URL is resolved from the latest release
|
|
# rather than written down, so it does not go stale.
|
|
if rpm -q rustdesk >/dev/null 2>&1; then
|
|
log "RustDesk already installed"
|
|
else
|
|
log "Resolving the latest RustDesk release..."
|
|
# `|| true` because a failed curl -- unauthenticated GitHub API calls get
|
|
# rate-limited -- would otherwise trip set -e and kill the stage before the
|
|
# empty-result fallback below could do its job.
|
|
rustdesk_url="$(curl -fsSL https://api.github.com/repos/rustdesk/rustdesk/releases/latest 2>/dev/null \
|
|
| jq -r '.assets[].browser_download_url | select(test("x86_64\\.rpm$")) | select(test("suse") | not)' \
|
|
| head -1 || true)"
|
|
if [[ -n "$rustdesk_url" ]]; then
|
|
log "Installing RustDesk from $rustdesk_url"
|
|
# The RPM ships rustdesk.service already enabled, which is what provides
|
|
# unattended access; Panama deliberately does not start it a second time.
|
|
sudo dnf install -y "$rustdesk_url" > /dev/null || log "RustDesk install failed; skipping"
|
|
else
|
|
log "Could not resolve a RustDesk release; skipping"
|
|
fi
|
|
fi
|
|
|
|
# --- Install Flatpak Packages ---
|
|
FLATPAK_FILE="$PANAMA_PATH/setup/packages/flatpak-packages"
|
|
if [[ -f "$FLATPAK_FILE" ]]; then
|
|
FLATPAK_PACKAGES=$(packages_in "$FLATPAK_FILE")
|
|
log "Adding Flathub remote"
|
|
soft "adding the Flathub remote" \
|
|
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
|
log "Installing Flatpak Packages"
|
|
echo -e "Includes the following packages:"
|
|
echo -e "$(<"$FLATPAK_FILE")"
|
|
# One ID renamed on Flathub must not cost the rest of the run; the desktop
|
|
# is already installed by this point and none of these is part of it.
|
|
soft "some Flatpak packages" sudo flatpak install -y flathub $FLATPAK_PACKAGES
|
|
log "Flatpak packages installed!"
|
|
else
|
|
log "Package list was not in specified path: $FLATPAK_FILE"
|
|
fi
|
|
|
|
# --- Install the extras that were chosen ------------------------------------
|
|
#
|
|
# Everything above is what every Panama machine gets. This is what one machine
|
|
# asked for: the interview offers the categories in setup/packages/extras/ as a
|
|
# checklist and records the chosen names, so a work laptop does not acquire
|
|
# emulators and a desktop does not skip Steam.
|
|
#
|
|
# Absent means none. That is what makes this stage safe to re-run by hand while
|
|
# repairing one piece of a machine -- and it means a category is installed only
|
|
# by an explicit answer, never by a default that drifted.
|
|
#
|
|
# A category mixes both package managers, because the applications do: some are
|
|
# in Fedora or RPM Fusion and some publish only a flatpak. A bare line is a dnf
|
|
# package and a `flatpak:` line is a Flathub ID, so one file per category holds
|
|
# the whole answer rather than splitting each category across two.
|
|
#
|
|
# Reading the file is setup/lib/extras-catalog's job, not this function's, because
|
|
# `panama apps` offers the same catalog from the other side. Two parsers would
|
|
# eventually disagree about what a category contains, and the one that disagreed
|
|
# quietly would be this one -- it runs unattended.
|
|
#
|
|
# Neither install is fatal. A category is a set of applications somebody wanted,
|
|
# not a dependency of the desktop, and losing the rest of the run because one of
|
|
# them was renamed upstream would be the wrong trade.
|
|
install_extra_category() {
|
|
local file="$1" name
|
|
name="$(basename "$file")"
|
|
|
|
local dnf_packages flatpak_ids
|
|
# sed rather than grep -v: most categories are flatpak-only, and grep exits 1
|
|
# when it selects nothing, which set -e above turns into a dead stage.
|
|
dnf_packages=$(catalog_all_targets "$file" | sed '/^flatpak:/d' | tr "\n" " ")
|
|
flatpak_ids=$(catalog_all_targets "$file" | sed -n 's/^flatpak://p' | tr "\n" " ")
|
|
|
|
if [[ -n "${dnf_packages// /}" ]]; then
|
|
log "Installing $name: $dnf_packages"
|
|
sudo dnf install -y $dnf_packages > /dev/null || log "Some $name packages did not install"
|
|
fi
|
|
if [[ -n "${flatpak_ids// /}" ]]; then
|
|
log "Installing $name flatpaks: $flatpak_ids"
|
|
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo > /dev/null
|
|
sudo flatpak install -y flathub $flatpak_ids > /dev/null || log "Some $name flatpaks did not install"
|
|
fi
|
|
}
|
|
|
|
EXTRAS_DIR="$PANAMA_PATH/setup/packages/extras"
|
|
for extra in ${PANAMA_EXTRAS:-}; do
|
|
if [[ -f "$EXTRAS_DIR/$extra" ]]; then
|
|
install_extra_category "$EXTRAS_DIR/$extra"
|
|
else
|
|
log "No such extras category: $extra"
|
|
fi
|
|
done
|
|
|
|
# --- What was stepped over ---------------------------------------------------
|
|
#
|
|
# Tolerating a failure is only better than aborting on it if somebody is told.
|
|
# This stage now survives a codec swap that finds nothing to swap, and the whole
|
|
# point of surviving it is that the desktop gets installed anyway -- but a
|
|
# machine missing its video codecs should say so once, here, rather than be
|
|
# discovered a week later by a video that will not play.
|
|
if (( ${#softly_failed[@]} > 0 )); then
|
|
log "Installed, but these were stepped over:"
|
|
printf ' - %s\n' "${softly_failed[@]}"
|
|
log "None of them stops the desktop. Re-run this stage to try them again."
|
|
fi
|