Skip to content

Instantly share code, notes, and snippets.

@psiborg
Last active September 13, 2026 03:58
Show Gist options
  • Select an option

  • Save psiborg/13105d9bfd23a6cf91995e22f80a8d4a to your computer and use it in GitHub Desktop.

Select an option

Save psiborg/13105d9bfd23a6cf91995e22f80a8d4a to your computer and use it in GitHub Desktop.
sys-upd.sh — cross-distro system update helper
#!/usr/bin/env bash
#
# sys-upd.sh — cross-distro system update helper
# Supports: Debian/Ubuntu, Arch, Fedora/RHEL, openSUSE families.
#
# Usage: sys-upd.sh [-f|--firmware] [-t|--toolchains] [-a|--all] [-h|--help]
#
set -uo pipefail
WITH_FIRMWARE=0
WITH_TOOLCHAINS=0
# Ollama models matching any of these globs are never pulled — they are built
# locally with 'ollama create' and do not exist in the registry.
# Override per-run, e.g.: OLLAMA_SKIP="ai-* test-*" ./sys-upd.sh
read -r -a OLLAMA_SKIP_PATTERNS <<< "${OLLAMA_SKIP:-ai-*}"
usage() {
cat <<'EOF'
Usage: sys-upd.sh [OPTIONS]
By default updates system packages, Flatpaks, yt-dlp and Ollama.
Options:
-f, --firmware Also check for firmware updates via fwupd
-t, --toolchains Also update language toolchains (pipx, rustup, npm)
-a, --all Equivalent to --firmware --toolchains
-h, --help Show this help and exit
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
-f|--firmware) WITH_FIRMWARE=1 ; shift ;;
-t|--toolchains) WITH_TOOLCHAINS=1 ; shift ;;
-a|--all) WITH_FIRMWARE=1 ; WITH_TOOLCHAINS=1 ; shift ;;
-h|--help) usage ; exit 0 ;;
*) echo "Unknown option: $1" >&2 ; usage >&2 ; exit 1 ;;
esac
done
# --- helpers ----------------------------------------------------------------
SUDO=""
if [[ ${EUID} -ne 0 ]]; then
if command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
else
echo "Root privileges are required and sudo was not found." >&2
exit 1
fi
fi
have() { command -v "$1" >/dev/null 2>&1; }
section() { printf '\n=== %s ===\n' "$1"; }
confirm() {
local answer
read -rp "$1 (y/n) " answer
[[ ${answer,,} == y || ${answer,,} == yes ]]
}
# --- distro detection -------------------------------------------------------
detect_family() {
local id="" id_like=""
if [[ -r /etc/os-release ]]; then
# shellcheck source=/dev/null
. /etc/os-release
id="${ID:-}"
id_like="${ID_LIKE:-}"
fi
# ID_LIKE is a space-separated list, so pad and glob-match on whole words.
case " ${id} ${id_like} " in
*" debian "*|*" ubuntu "*) echo debian ; return ;;
*" arch "*|*" archlinux "*) echo arch ; return ;;
*" fedora "*|*" rhel "*|*" centos "*) echo fedora ; return ;;
*" suse "*|*" opensuse "*) echo suse ; return ;;
esac
# Fallback: identify by whichever package manager is installed.
if have apt-get ; then echo debian
elif have pacman ; then echo arch
elif have dnf ; then echo fedora
elif have zypper ; then echo suse
else echo unknown
fi
}
# --- per-family update routines ---------------------------------------------
update_debian() {
${SUDO} apt-get update
${SUDO} apt-get upgrade -y
${SUDO} apt-get autoremove -y
}
update_arch() {
# AUR helpers wrap pacman and must NOT be run under sudo.
if have paru ; then
paru -Syu --noconfirm
elif have yay ; then
yay -Syu --noconfirm
else
${SUDO} pacman -Syu --noconfirm
fi
# pacman has no autoremove; drop orphaned dependencies manually.
local orphans
orphans=$(pacman -Qtdq 2>/dev/null || true)
if [[ -n ${orphans} ]]; then
# shellcheck disable=SC2086
${SUDO} pacman -Rns --noconfirm ${orphans}
fi
}
update_fedora() {
local mgr="dnf"
have dnf || mgr="yum"
${SUDO} "${mgr}" upgrade --refresh -y
${SUDO} "${mgr}" autoremove -y
}
update_suse() {
${SUDO} zypper --non-interactive refresh
# Tumbleweed is a rolling release and wants a distribution upgrade.
if [[ ${ID:-} == "opensuse-tumbleweed" ]]; then
${SUDO} zypper --non-interactive dup
else
${SUDO} zypper --non-interactive update
fi
}
# --- cross-distro extras ----------------------------------------------------
update_flatpak() {
have flatpak || return 0
section "Flatpak"
flatpak update -y
flatpak uninstall --unused -y
}
update_firmware() {
(( WITH_FIRMWARE )) || return 0
if ! have fwupdmgr ; then
echo "fwupdmgr not installed — skipping firmware check." >&2
return 0
fi
section "Firmware"
# No --force: fwupd caches LVFS metadata for 24h and refresh returns
# non-zero when it is already current, which is not an error here.
fwupdmgr refresh || true
if ! fwupdmgr get-updates >/dev/null 2>&1; then
echo "No firmware updates available."
return 0
fi
fwupdmgr get-updates
if confirm "Apply these firmware updates?"; then
# Most devices stage the update and flash it during the next boot.
fwupdmgr update -y || echo "Firmware update reported errors." >&2
REBOOT_HINT="firmware"
else
echo "Skipping firmware updates."
fi
}
update_yt_dlp() {
have yt-dlp || return 0
section "yt-dlp"
if ! yt-dlp -U; then
echo "yt-dlp self-update failed — it is probably managed by your"
echo "package manager or pipx, in which case it is already current."
fi
}
ollama_installed_version() {
# Prints e.g. "ollama version is 0.12.4", possibly preceded by a warning
# line when the local server is not running.
ollama --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1
}
ollama_latest_version() {
have curl || return 1
local json tag
json=$(curl -fsSL --max-time 10 \
https://api.github.com/repos/ollama/ollama/releases/latest 2>/dev/null) || return 1
if have jq ; then
tag=$(printf '%s' "${json}" | jq -r '.tag_name // empty')
else
tag=$(printf '%s' "${json}" \
| grep -m1 '"tag_name"' \
| sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
fi
[[ -n ${tag} ]] || return 1
printf '%s' "${tag#v}"
}
ollama_wait_ready() {
# The installer restarts the systemd unit, so the API is briefly down.
local tries="${1:-20}" i
for (( i = 0; i < tries; i++ )); do
ollama list >/dev/null 2>&1 && return 0
sleep 1
done
return 1
}
update_ollama() {
have ollama || return 0
section "Ollama"
local installed latest updated=0
installed=$(ollama_installed_version)
latest=$(ollama_latest_version) || latest=""
printf ' installed: %s\n' "${installed:-unknown}"
printf ' latest: %s\n' "${latest:-unavailable (could not reach GitHub)}"
if [[ -n ${installed} && -n ${latest} && ${installed} == "${latest}" ]]; then
echo "Runtime is already current — skipping."
elif confirm "Update the Ollama runtime?"; then
echo "Updating Ollama..."
if curl -fsSL https://ollama.com/install.sh | sh; then
updated=1
echo "Now running $(ollama_installed_version)."
else
echo "Ollama update failed." >&2
fi
else
echo "Skipping runtime update."
fi
if (( updated )); then
printf 'Waiting for the Ollama service to come back up... '
if ollama_wait_ready 20; then
echo "ready."
else
echo "timed out."
echo "Could not reach the API — skipping model updates." >&2
echo "Check 'systemctl status ollama', then re-run to update models." >&2
return 0
fi
elif ! ollama list >/dev/null 2>&1; then
echo "Ollama API is not responding — skipping model updates." >&2
return 0
fi
local models=()
mapfile -t models < <(ollama list 2>/dev/null | awk 'NR>1 && NF {print $1}')
if (( ${#models[@]} == 0 )); then
echo "No local models found — nothing to update."
return 0
fi
# Partition into registry models (pullable) and locally-built ones.
local pullable=() skipped=() m pat match
for m in "${models[@]}"; do
match=0
for pat in "${OLLAMA_SKIP_PATTERNS[@]}"; do
# shellcheck disable=SC2053
[[ ${m} == ${pat} ]] && { match=1 ; break ; }
done
if (( match )); then
skipped+=("${m}")
else
pullable+=("${m}")
fi
done
if (( ${#skipped[@]} )); then
echo "Ignoring ${#skipped[@]} locally-built model(s): ${skipped[*]}"
fi
if (( ${#pullable[@]} == 0 )); then
echo "No registry models to update."
return 0
fi
echo "${#pullable[@]} registry model(s) found. Re-pulling can transfer several GB."
if confirm "Update local models?"; then
for m in "${pullable[@]}"; do
echo "--- pulling ${m}"
ollama pull "${m}" </dev/null || echo "Failed to pull ${m}" >&2
done
else
echo "Skipping model updates."
fi
}
update_toolchains() {
section "Language toolchains"
if have pipx ; then
pipx upgrade-all || true
fi
if have rustup ; then
rustup update || true
# cargo-install-update is a separate crate: cargo install cargo-update
if cargo install-update --version >/dev/null 2>&1; then
cargo install-update -a || true
fi
fi
if have npm ; then
npm update -g || echo "npm global update failed (may need sudo)." >&2
fi
}
# --- reboot check -----------------------------------------------------------
REBOOT_HINT=""
check_reboot() {
local family="$1" needed=0 reason=""
# Generic signal: the running kernel's module tree no longer exists,
# which means the kernel package was replaced during this run.
if [[ ! -d /usr/lib/modules/$(uname -r) && ! -d /lib/modules/$(uname -r) ]]; then
needed=1 ; reason="kernel updated"
fi
case "${family}" in
debian)
if [[ -f /var/run/reboot-required ]]; then
needed=1 ; reason="/var/run/reboot-required is present"
fi
;;
fedora)
if have needs-restarting ; then
needs-restarting -r >/dev/null 2>&1 || { needed=1 ; reason="dnf needs-restarting"; }
elif have dnf ; then
${SUDO} dnf needs-restarting -r >/dev/null 2>&1 || { needed=1 ; reason="dnf needs-restarting"; }
fi
;;
suse)
if have zypper ; then
${SUDO} zypper ps -s >/dev/null 2>&1 || true
fi
;;
esac
if [[ -n ${REBOOT_HINT} ]]; then
needed=1 ; reason="${reason:+${reason}, }pending ${REBOOT_HINT}"
fi
section "Reboot check"
if (( needed )); then
echo "A reboot is recommended (${reason})."
else
echo "No reboot appears to be required."
fi
}
# --- Arch config file review ------------------------------------------------
check_pacnew() {
have pacdiff || return 0
local pacnew
pacnew=$(find /etc -name '*.pacnew' -o -name '*.pacsave' 2>/dev/null | head -n 5)
[[ -n ${pacnew} ]] || return 0
section "Pending config merges"
echo "${pacnew}"
echo "Run 'sudo -E pacdiff' to review these."
}
# --- main -------------------------------------------------------------------
main() {
local family
family=$(detect_family)
section "System packages (${family})"
case "${family}" in
debian) update_debian ;;
arch) update_arch ;;
fedora) update_fedora ;;
suse) update_suse ;;
*)
echo "Unsupported distribution — no known package manager found." >&2
exit 1
;;
esac
[[ ${family} == arch ]] && check_pacnew
update_flatpak
update_firmware
update_yt_dlp
update_ollama
(( WITH_TOOLCHAINS )) && update_toolchains
check_reboot "${family}"
printf '\nDone.\n'
}
main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment