Last active
September 13, 2026 03:58
-
-
Save psiborg/13105d9bfd23a6cf91995e22f80a8d4a to your computer and use it in GitHub Desktop.
sys-upd.sh — cross-distro system update helper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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