#!/bin/bash # Function to check if a package is installed is_installed() { pacman -Qi "$1" &> /dev/null } # Function to check if a package is installed is_group_installed() { pacman -Qg "$1" &> /dev/null } # Packages and Flatpaks you don't want, one name per line (# starts a comment). # install_packages and install-flatpaks.sh leave them out, so update doesn't # put back what you've removed. SKIP_PACKAGES="$HOME/.config/coredesktop/skip-packages" # Print each argument that isn't in SKIP_PACKAGES, one per line without_skipped() { local -A skip=() local name if [ -f "$SKIP_PACKAGES" ]; then while read -r name _; do [[ -n "$name" && "$name" != \#* ]] && skip["$name"]=1 done < "$SKIP_PACKAGES" fi for name in "$@"; do [ -n "${skip[$name]}" ] || echo "$name" done } # Function to install packages if not already installed install_packages() { local packages=() local to_install=() local as_deps=() mapfile -t packages < <(without_skipped "$@") # pacman -Qdq with no names would list every dependency on the system [ ${#packages[@]} -ne 0 ] || return 0 for pkg in "${packages[@]}"; do if ! is_installed "$pkg" && ! is_group_installed "$pkg"; then to_install+=("$pkg") fi done # A package first installed as another one's dependency becomes an orphan # when that one is removed, and updates.d/95-cleanup.sh removes orphans mapfile -t as_deps < <(pacman -Qdq "${packages[@]}" 2> /dev/null) if [ ${#as_deps[@]} -ne 0 ]; then sudo pacman -D --asexplicit "${as_deps[@]}" > /dev/null fi if [ ${#to_install[@]} -ne 0 ]; then echo "Installing: ${to_install[*]}" yay -S --noconfirm --answerdiff=None --answeredit=None --answerclean=None "${to_install[@]}" fi } FEDERATED_REPO_URL="https://gitea.federated.computer/api/packages/Federated_Computer/arch" FEDERATED_REPO_KEY="6AB7C080D87C74BEE3925A026329376994F18B4E" # Add Federated Computer's pacman repo, which Gitea serves and signs, and trust # its key. Does nothing if both are already set up. ensure_federated_repo() { if ! sudo pacman-key --list-keys "$FEDERATED_REPO_KEY" &> /dev/null; then curl -fsSL "$FEDERATED_REPO_URL/repository.key" | sudo pacman-key --add - || return 1 sudo pacman-key --lsign-key "$FEDERATED_REPO_KEY" > /dev/null || return 1 fi if ! grep -q '^\[federated\]' /etc/pacman.conf; then printf '\n[federated]\nSigLevel = Required DatabaseRequired\nServer = %s/$repo/$arch\n' \ "$FEDERATED_REPO_URL" | sudo tee -a /etc/pacman.conf > /dev/null || return 1 fi } # Function to install pipx applications if not already installed install_pipx_apps() { local installed app status=0 installed="$(pipx list --short 2> /dev/null | cut -d' ' -f1)" for app in "$@"; do if ! grep -qx "$app" <<< "$installed"; then echo "Installing $app with pipx..." pipx install "$app" || status=1 fi done return $status } # Reinstall gext if missing or broken. Its pipx venv goes stale when Arch # upgrades Python, and migrations run before updates.d/60-pipx.sh rebuilds it. ensure_gext() { ~/.local/bin/gext --version &> /dev/null && return 0 echo "gext missing or broken, reinstalling..." pipx uninstall gnome-extensions-cli &> /dev/null || true pipx install --python "$(which python3)" gnome-extensions-cli --system-site-packages } # Function to remove pacman/AUR packages if installed remove_packages() { local packages=("$@") local to_remove=() # Match the exact name: pacman -Q also finds a package that provides it, such # as herdr-bin for herdr, which pacman -R won't remove by that name # An AUR package built with makepkg's debug option comes with pkg-debug, # which nothing depends on, so -Rns leaves it behind, and its files conflict # with the replacement's (voxtype-debug with voxtype-bin-debug) for pkg in "${packages[@]}"; do if [ "$(pacman -Qq "$pkg" 2> /dev/null)" = "$pkg" ]; then to_remove+=("$pkg") fi if [ "$(pacman -Qq "$pkg-debug" 2> /dev/null)" = "$pkg-debug" ]; then to_remove+=("$pkg-debug") fi done if [ ${#to_remove[@]} -ne 0 ]; then echo "Removing: ${to_remove[*]}" sudo pacman -Rns --noconfirm "${to_remove[@]}" fi } # Function to remove flatpak applications if installed remove_flatpaks() { local flatpaks=("$@") local to_remove=() for pak in "${flatpaks[@]}"; do if flatpak info --system "$pak" &> /dev/null; then to_remove+=("$pak") fi done if [ ${#to_remove[@]} -ne 0 ]; then echo "Removing flatpaks: ${to_remove[*]}" sudo flatpak uninstall --system --noninteractive "${to_remove[@]}" fi } # Function to enable systemd services if not already enabled # With --now, also start the services enabled here, after making systemd read # the unit files that packages just installed enable_services() { local now=() if [ "$1" = "--now" ]; then now=(--now) shift sudo systemctl daemon-reload fi for service in "$@"; do # audit records the services you turn off; leave those off if grep -qsx "system $service" "$HOME/.config/coredesktop/services-off"; then echo "$service is turned off (see audit)." elif ! systemctl is-enabled "$service" &> /dev/null; then echo "Enabling $service..." sudo systemctl enable "${now[@]}" "$service" else echo "$service is already enabled." fi done } # Run each script in migrations/ that hasn't run on this machine yet, oldest # first. A migration is recorded as done only when it succeeds, and the first # failure stops the run so a later migration never runs ahead of an earlier one. run_migrations() { local dir="${COREDESKTOP:-$HOME/.local/share/coredesktop}/migrations" local state="$HOME/.local/state/coredesktop/migrations" local file name mkdir -p "$state" for file in "$dir"/*.sh; do [ -f "$file" ] || continue name="$(basename "$file")" [ -f "$state/$name" ] && continue echo "Running migration $name..." if ! bash "$file"; then echo "Migration $name failed; it will run again on the next update." return 1 fi touch "$state/$name" done } # Output of the last update's pacman -Su, written by updates.d/10-pacman.sh PACMAN_UPDATE_LOG="$HOME/.local/state/coredesktop/pacman-update.log" # True if the last update rebuilt the initramfs and mkinitcpio reported a # failure. Rebooting then can leave the machine unbootable. initramfs_build_failed() { [ -f "$PACMAN_UPDATE_LOG" ] || return 1 grep -q "Updating linux initcpios" "$PACMAN_UPDATE_LOG" || return 1 grep -q "==> ERROR" "$PACMAN_UPDATE_LOG" && return 0 # Every image built must report success. Older mkinitcpio says "==> Image # generation successful", newer says "==> Initcpio image generation successful". local built succeeded built="$(grep -c "==> Building image from preset" "$PACMAN_UPDATE_LOG")" succeeded="$(grep -cE "==> (Initcpio i|I)mage generation successful" "$PACMAN_UPDATE_LOG")" (( succeeded == 0 || succeeded < built )) } # pacman downloads from the first mirror in this list (system/mirrors.sh keeps # it fresh). Overridable for the tests. MIRRORLIST="${MIRRORLIST:-/etc/pacman.d/mirrorlist}" # A mirror further behind Arch than this is stale; most sync every few hours. # Used by version and updates.d/10-pacman.sh. # shellcheck disable=SC2034 MIRROR_STALE_HOURS=24 # The first mirror in MIRRORLIST, without its /$repo/os/$arch first_mirror() { sed -nE 's/^[[:space:]]*Server[[:space:]]*=[[:space:]]*//p' "$MIRRORLIST" 2> /dev/null \ | head -n 1 | sed -E 's#/\$repo.*##' } # Print how many hours ago the first mirror last synced with Arch, from the # timestamp mirrors publish in lastsync. Fails if it couldn't tell. mirror_age_hours() { local mirror lastsync mirror="$(first_mirror)" [ -n "$mirror" ] || return 1 lastsync="$(curl -sf --max-time 10 "$mirror/lastsync" 2> /dev/null)" || return 1 [[ "$lastsync" =~ ^[0-9]+$ ]] || return 1 echo $(( ($(date +%s) - lastsync) / 3600 )) } # Install a script atomically (copy to a temp file, then rename over the target) # so a copy of it that is running right now keeps reading the old file install_script() { local tmp tmp="$(mktemp "$2.XXXXXX")" && cp "$1" "$tmp" && chmod 755 "$tmp" && mv -f "$tmp" "$2" }