diff --git a/README.md b/README.md index faf8537..e494008 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,34 @@ Installs the bare necessities to turn a newly minted Arch Linux server into some ``` wget -qO- https://gitea.young.computer/david/MiniServer/raw/branch/main/install.sh | bash -``` \ No newline at end of file +``` + +## Keep your server up to date + +``` +update +``` + +This pulls the latest MiniServer into `~/.local/share/MiniServer`, then runs each step in `updates.d/` in order: the Arch keyring and system packages (plus any packages newly added to `packages.conf`), AUR packages, pm2, sibling projects, cache cleanup, and a reboot check. If a step fails, the others still run and the failed ones are listed at the end. + +## Refresh + +``` +refresh +``` + +Re-runs `install.sh` to pick up anything new in the installer (services, config). Run `update` afterwards. + +## Sibling projects + +A sibling project is another git repository that builds on MiniServer. To install one: + +``` +sibling +``` + +It asks for the repository's git URL, clones it into `~/.local/share/siblings/`, and runs the project's `install.sh` (if it has one). After that, every `update` pulls each sibling and runs its `update.sh` (if it has one). + +Use `sibling list` to see what's installed and `sibling remove {name}` to uninstall one. + +To make your own repository work as a sibling project, see [skills/sibling-project-format.md](skills/sibling-project-format.md). diff --git a/install.sh b/install.sh index c7b0324..3acabba 100755 --- a/install.sh +++ b/install.sh @@ -2,6 +2,10 @@ # # Purpose : MiniServer installer +# Wrapped in { ...; exit; } so bash reads the whole script up front; the git +# pull below can replace this file while it runs. +{ + # Print the logo print_logo() { cat << "EOF" @@ -61,15 +65,26 @@ else fi echo "Getting the latest version of MiniServer..." -rm -rf ~/.local/share/MiniServer -git clone https://gitea.young.computer/david/MiniServer.git "$HOME/.local/share/MiniServer" > /dev/null - -mkdir -p "$HOME/.local/bin" -cp "$HOME/.local/share/MiniServer/miniserverrefresh" "$HOME/.local/bin/miniserverrefresh" -cp "$HOME/.local/share/MiniServer/miniserverupdate" "$HOME/.local/bin/miniserverupdate" +if [ -d "$HOME/.local/share/MiniServer/.git" ]; then + git -C "$HOME/.local/share/MiniServer" pull +else + git clone https://gitea.young.computer/david/MiniServer.git "$HOME/.local/share/MiniServer" > /dev/null +fi cd ~/.local/share/MiniServer +# Source utility functions (provides install_packages, install_script and other helpers) +source utils.sh + +# Install the refresh, update and sibling management scripts to the user's bin +mkdir -p "$HOME/.local/bin" +install_script "$HOME/.local/share/MiniServer/refresh" "$HOME/.local/bin/refresh" +install_script "$HOME/.local/share/MiniServer/update" "$HOME/.local/bin/update" +install_script "$HOME/.local/share/MiniServer/sibling" "$HOME/.local/bin/sibling" + +# Replaced by refresh and update +rm -f "$HOME/.local/bin/miniserverrefresh" "$HOME/.local/bin/miniserverupdate" + echo "Updating system..." sudo pacman -Syu --noconfirm @@ -94,9 +109,6 @@ if ! yay --version &> /dev/null; then install_yay fi -# Source utility functions -source utils.sh - # Source package list if [ ! -f "packages.conf" ]; then echo "Error: packages.conf not found!" @@ -170,4 +182,6 @@ if ! ls /usr/lib/modules/$(uname -r) > /dev/null 2>&1; then esac else echo "Kernel is up to date, no reboot needed. You may want to consider a reboot if other significant software was updated or installed." -fi \ No newline at end of file +fi +exit +} diff --git a/miniserverupdate b/miniserverupdate deleted file mode 100755 index 5dcecf0..0000000 --- a/miniserverupdate +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash - -# Print the logo -print_logo() { - cat << "EOF" - - ███╗ ███╗██╗███╗ ██╗██╗ - ████╗ ████║██║████╗ ██║██║ - ██╔████╔██║██║██╔██╗ ██║██║ - ██║╚██╔╝██║██║██║╚██╗██║██║ - ██║ ╚═╝ ██║██║██║ ╚████║██║ - ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ - - ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗ - ██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗ - ███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝ - ╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗ - ███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║ - ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ - - - -EOF -} - -# Clear screen and show logo -clear -print_logo - -echo "Updating Arch..." -sudo pacman -Syu - -echo "Updating AUR..." - -if ! yay -Sua 2>/dev/null; then - echo "yay -Sua returned an error. Let's rebuild YAY." - sudo pacman -R yay - cd /tmp - git clone https://aur.archlinux.org/yay.git - cd yay - makepkg -si - cd .. - rm -rf yay - cd - yay -Sua --noconfirm -else - yay -Sua --noconfirm -fi - -# Updating the repo and installing... -echo "Updating MiniServer..." - -REPO_DIR="$HOME/.local/share/MiniServer" - -echo "Pulling latest MiniServer repo..." -git -C "$REPO_DIR" pull - -echo "Running install.sh..." -bash "$REPO_DIR/install.sh" - -# Check whether the running kernel matches the installed modules — if not, a -# newer kernel was installed during this run and a reboot is required -if ! ls /usr/lib/modules/$(uname -r) > /dev/null 2>&1; then - echo "A newer kernel has been installed, a reboot is recommended." - read -rp "Reboot now? [y/N] " response - case "$response" in - [yY][eE][sS]|[yY]) - echo "Rebooting..." - sudo reboot - ;; - *) - echo "Reboot skipped. Please remember to reboot when convenient." - exit 0 - ;; - esac -else - echo "Kernel is up to date, no reboot needed. You may want to consider a reboot if other significant software was updated or installed." -fi diff --git a/miniserverrefresh b/refresh similarity index 63% rename from miniserverrefresh rename to refresh index 3a3503a..d84efed 100755 --- a/miniserverrefresh +++ b/refresh @@ -1,5 +1,9 @@ #!/bin/bash +# Wrapped in { ...; exit; } so bash reads the whole script up front; install.sh +# replaces this file while it runs. +{ + # Print the logo print_logo() { cat << "EOF" @@ -27,26 +31,12 @@ EOF clear print_logo -echo "Let's get the lastest changes to MiniServer..." +echo "Let's get the latest changes to MiniServer..." -wget -qO- https://gitea.young.computer/david/MiniServer/raw/branch/main/install.sh | bash - -# Check whether the running kernel matches the installed modules — if not, a -# newer kernel was installed during this run and a reboot is required - -if ! ls /usr/lib/modules/$(uname -r) > /dev/null 2>&1; then - echo "A newer kernel has been installed, a reboot is recommended." - read -rp "Reboot now? [y/N] " response - case "$response" in - [yY][eE][sS]|[yY]) - echo "Rebooting..." - sudo reboot - ;; - *) - echo "Reboot skipped. Please remember to reboot when convenient." - exit 0 - ;; - esac +if [ -f "$HOME/.local/share/MiniServer/install.sh" ]; then + bash "$HOME/.local/share/MiniServer/install.sh" else - echo "Kernel is up to date, no reboot needed. You may want to consider a reboot if other significant software was updated or installed." + wget -qO- https://gitea.young.computer/david/MiniServer/raw/branch/main/install.sh | bash fi +exit +} diff --git a/sibling b/sibling new file mode 100755 index 0000000..8b9c1b9 --- /dev/null +++ b/sibling @@ -0,0 +1,122 @@ +#!/bin/bash +# +# Purpose : Install and manage MiniServer sibling projects +# +# A sibling is a git repository cloned into ~/.local/share/siblings/. +# If it has an install.sh, that runs once when the sibling is added. +# Every `update` pulls each sibling and runs its update.sh, if it has one +# (see updates.d/71-siblings.sh). + +SIBLINGS="$HOME/.local/share/siblings" + +usage() { + cat << EOF +Usage: sibling [command] + + add [url] Install a sibling project from a git repository (default) + list Show installed sibling projects + remove Uninstall a sibling project +EOF +} + +# Derive a directory name from a git URL, e.g. +# https://host/user/my-project.git -> my-project +sibling_name() { + local name="${1%/}" + name="${name##*/}" + name="${name##*:}" + echo "${name%.git}" +} + +add_sibling() { + local url="$1" name dir + + if [ -z "$url" ]; then + read -rp "Git repository URL for the sibling project: " url + fi + if [ -z "$url" ]; then + echo "No URL given." + return 1 + fi + + name="$(sibling_name "$url")" + if [[ -z "$name" || "$name" == .* ]]; then + echo "Could not work out a project name from: $url" + return 1 + fi + dir="$SIBLINGS/$name" + + mkdir -p "$SIBLINGS" + if [ -d "$dir/.git" ]; then + local existing + existing="$(git -C "$dir" remote get-url origin 2>/dev/null)" + if [ "$existing" != "$url" ]; then + echo "A different sibling named '$name' is already installed from: $existing" + return 1 + fi + echo "$name is already installed; pulling the latest version..." + git -C "$dir" pull --ff-only || return 1 + else + echo "Cloning $name..." + git clone "$url" "$dir" || return 1 + fi + + if [ -f "$dir/install.sh" ]; then + echo "Running $name's install.sh..." + if ! (cd "$dir" && bash install.sh); then + echo "$name's install.sh failed. Fix the problem and run 'sibling add $url' again." + return 1 + fi + fi + + # Confirm the install + if [ -d "$dir/.git" ] && git -C "$dir" rev-parse --verify --quiet HEAD > /dev/null; then + echo "Installed $name at $dir ($(git -C "$dir" log -1 --format='%h %s'))." + echo "'update' will keep it up to date." + else + echo "$name does not look installed correctly at $dir." + return 1 + fi +} + +list_siblings() { + local dir found=0 + for dir in "$SIBLINGS"/*/; do + [ -d "$dir/.git" ] || continue + found=1 + printf '%-24s %s (%s)\n' "$(basename "$dir")" \ + "$(git -C "$dir" remote get-url origin 2>/dev/null)" \ + "$(git -C "$dir" log -1 --format='%h, %cr' 2>/dev/null)" + done + [ $found -eq 1 ] || echo "No sibling projects installed." +} + +remove_sibling() { + local name="$1" + if [[ -z "$name" || "$name" == */* || "$name" == .* ]]; then + usage + return 1 + fi + if [ ! -d "$SIBLINGS/$name" ]; then + echo "No sibling named '$name' is installed." + return 1 + fi + read -rp "Remove $SIBLINGS/$name? [y/N] " response + case "$response" in + [yY][eE][sS]|[yY]) + rm -rf "${SIBLINGS:?}/$name" + echo "Removed $name. Anything its install.sh set up outside that folder is still there." + ;; + *) + echo "Nothing removed." + ;; + esac +} + +case "$1" in + ""|add) add_sibling "$2" ;; + list|ls) list_siblings ;; + remove|rm) remove_sibling "$2" ;; + -h|--help|help) usage ;; + *) usage; exit 1 ;; +esac diff --git a/skills/sibling-project-format.md b/skills/sibling-project-format.md new file mode 100644 index 0000000..6464e94 --- /dev/null +++ b/skills/sibling-project-format.md @@ -0,0 +1,217 @@ +# Skill: Formatting a Repository as a MiniServer Sibling Project + +## Overview + +A sibling project is a git repository that builds on MiniServer. Users install +one with MiniServer's `sibling` command, and from then on MiniServer's +`update` command keeps it up to date. + +Follow this document when you are working in a sibling repository and need it +to install and update correctly under MiniServer. A repository needs no +special files to be installable. It becomes useful by adding two optional +scripts at its top level: + +| File | When it runs | +|--------------|--------------------------------------------------------------| +| `install.sh` | Once, when the user runs `sibling add`, and again if they re-run it | +| `update.sh` | On every `update`, right after the repo has been pulled | + +Don't also copy a hook into MiniServer's `updates.d/` for the same project, or +it updates twice. + +## What MiniServer Does + +### `sibling add ` (or `sibling`, which asks for the URL) + +1. Works out the project name from the last part of the URL, dropping `.git`: + `git@gitea.young.computer:david/my-tools.git` → `my-tools`. +2. Clones the repo into `~/.local/share/siblings/`, or, if that folder + already holds a clone of the same URL, runs `git pull --ff-only` there. +3. If `install.sh` exists, runs `bash install.sh` with the repo folder as the + working directory. Its input and output are the user's terminal. +4. Reports success only if every step above succeeded. + +### `update` + +MiniServer runs its own update hooks in order (`10-pacman.sh` … `30-pm2.sh`). +Next, `updates.d/71-siblings.sh` takes each sibling in alphabetical order and: + +1. Runs `git pull --ff-only --quiet` in its folder. +2. If the pull succeeded and `update.sh` exists, runs `bash update.sh` with the + repo folder as the working directory. + +A failed pull or a non-zero exit from `update.sh` is printed and listed in +`update`'s "Done, with errors in: …" summary. Other siblings still update. +MiniServer's end-of-run hooks (cleanup, reboot check) run after all +siblings. + +### `sibling remove ` + +This only deletes `~/.local/share/siblings/`. MiniServer has no uninstall +step for siblings, so anything `install.sh` set up elsewhere stays. + +## Rules for the Repository + +### Layout + +- Put `install.sh` and `update.sh` at the top level of the repo. Other names or + locations are ignored. +- Both scripts are run with `bash`, so they don't need to be executable, but a + `#!/bin/bash` line keeps them runnable by hand. +- Locate files relative to the script, not to a hard-coded path. The user may + also clone the repo somewhere else and run `install.sh` by hand. + ```bash + SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + ``` + +### Environment + +- The scripts run as separate `bash` processes, so they inherit **no** + variables or functions from MiniServer. In particular `$MINISERVER` is + **not** set. Use the path directly: + ```bash + MINISERVER="$HOME/.local/share/MiniServer" + ``` +- MiniServer's helpers are available by sourcing `utils.sh`: + + | Function | What it does | + |--------------------------|-----------------------------------------------------| + | `install_packages ...` | Installs pacman/AUR packages via `yay`, skipping installed ones | + | `remove_packages ...` | Removes installed pacman/AUR packages | + | `enable_services ...` | Enables systemd services not yet enabled | + | `install_script src dst` | Copies a file atomically and makes it executable | + | `is_installed pkg` | True if a pacman package is installed | + +- `sudo` prompts for a password as normal. MiniServer does not grant + passwordless sudo to siblings. + +### `install.sh` + +- **Exit non-zero on failure.** `set -e` near the top is the simplest way. + `sibling` then tells the user it failed and how to retry. +- **Make it safe to re-run.** Re-running `sibling add` is how users repair an + install, so skip work that is already done instead of failing on it. +- It may prompt the user, since it runs in their terminal. +- Install commands into `~/.local/bin` (already on `PATH` under MiniServer) + with `install_script`. + +### `update.sh` + +- **Make it idempotent.** It runs on every `update`, usually with nothing to do. +- **Don't prompt.** Users expect `update` to run through without questions, + apart from `sudo` passwords. +- **Exit non-zero only for real failures**, so that `update`'s error summary + stays meaningful. +- **Print what it's doing.** MiniServer prints only the project's name before + it runs. +- The repo has already been pulled, so `update.sh` always runs its newest + version. Use it to apply changes made since the last update: new packages, + updated commands in `~/.local/bin`, config migrations. +- It's fine to omit `update.sh` if pulling the repo is enough, for example when + `~/.local/bin` holds symlinks into the repo. + +### Keep pulls working + +`update` pulls with `--ff-only` and never merges or resets. A pull fails if: + +- **History on the default branch is rewritten** (force-pushed). Never do this. +- **Tracked files are edited in place** in the clone and a new commit touches + them. Scripts must not write into tracked files. Write generated files, + caches and per-machine state outside the repo (e.g. `~/.config//`), or + into paths listed in `.gitignore`. + +### Authentication + +`sibling` clones with plain `git`. A private repo works only if the user can +already clone it (SSH key, credential helper, or a password typed at the +prompt). Every later `update` needs the same access. + +## Templates + +Share the install logic between the two scripts so that packages or commands +added later reach existing machines through `update`, not just new installs. + +`install.sh`: + +```bash +#!/bin/bash +# +# Purpose : Install (run by MiniServer's `sibling add`) + +set -e + +SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MINISERVER="$HOME/.local/share/MiniServer" +source "$MINISERVER/utils.sh" + +# One-time setup: prompts, accounts, initial config... +mkdir -p "$HOME/.config/" + +# Everything that update.sh also needs to keep current +bash "$SELF/update.sh" + +echo " installed." +``` + +`update.sh`: + +```bash +#!/bin/bash +# +# Purpose : Keep current (run by MiniServer's `update`) + +set -e + +SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MINISERVER="$HOME/.local/share/MiniServer" +source "$MINISERVER/utils.sh" + +PACKAGES=( + jq + ripgrep +) +install_packages "${PACKAGES[@]}" + +mkdir -p "$HOME/.local/bin" +install_script "$SELF/bin/" "$HOME/.local/bin/" +``` + +## Testing + +Test in a throwaway home folder so nothing touches the real one. Point +`sibling` at a local clone; it accepts any URL or path that `git clone` does. + +```bash +export HOME="$(mktemp -d)" +mkdir -p "$HOME/.local/share" +ln -s /path/to/real/MiniServer "$HOME/.local/share/MiniServer" + +# Install +bash "$HOME/.local/share/MiniServer/sibling" add /path/to/sibling-repo + +# Commit a change in /path/to/sibling-repo, then run the update hook the way +# `update` does +( source "$HOME/.local/share/MiniServer/updates.d/71-siblings.sh"; is_installed && run_update ) + +# Re-run the install to check it is safe to repeat +bash "$HOME/.local/share/MiniServer/sibling" add /path/to/sibling-repo +``` + +Scripts that call `sudo`, `yay` or `systemctl` still change the real system +even with a temporary `HOME`. Test those on a machine or VM you don't mind +changing. + +## Checklist + +- [ ] The repo can be cloned by URL by the people who will install it +- [ ] `install.sh` (optional) is at the top level, exits non-zero on failure, + and is safe to re-run +- [ ] `update.sh` (optional) is at the top level, idempotent, doesn't prompt, + and prints what it's doing +- [ ] Scripts find their own files relative to the script and set + `MINISERVER="$HOME/.local/share/MiniServer"` themselves +- [ ] Nothing writes to tracked files in the clone, and the default branch is + never force-pushed +- [ ] The project doesn't also copy a hook into MiniServer's `updates.d/` +- [ ] The README tells users to install with + `sibling add ` diff --git a/update b/update new file mode 100755 index 0000000..f07452d --- /dev/null +++ b/update @@ -0,0 +1,92 @@ +#!/bin/bash + +# The whole script is wrapped in { ...; exit; } so bash parses all of it before +# running anything. That way, replacing this file mid-run (e.g. by git pull and +# the copy below) can't make the running copy read a mix of old and new text. +{ + +# Install a script atomically (copy to a temp file, then rename over the target) +install_script() { + local tmp + tmp="$(mktemp "$2.XXXXXX")" && cp "$1" "$tmp" && chmod 755 "$tmp" && mv -f "$tmp" "$2" +} + +# Print the logo +print_logo() { + cat << "EOF" + + ███╗ ███╗██╗███╗ ██╗██╗ + ████╗ ████║██║████╗ ██║██║ + ██╔████╔██║██║██╔██╗ ██║██║ + ██║╚██╔╝██║██║██║╚██╗██║██║ + ██║ ╚═╝ ██║██║██║ ╚████║██║ + ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ + + ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗ + ██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗ + ███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝ + ╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗ + ███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║ + ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ + + + +EOF +} + +# Clear screen and show logo +clear +print_logo + +MINISERVER="$HOME/.local/share/MiniServer" + +echo "Updating MiniServer..." +if [[ -z "$UPDATE_REEXECED" ]] && ! git -C "$MINISERVER" pull; then + echo "WARNING: could not pull the latest MiniServer; running the update hooks already on disk." + export UPDATE_PULL_FAILED=1 +fi +mkdir -p "$HOME/.local/bin" "$MINISERVER/updates.d" +install_script "$MINISERVER/update" ~/.local/bin/update +install_script "$MINISERVER/refresh" ~/.local/bin/refresh +install_script "$MINISERVER/sibling" ~/.local/bin/sibling + +if [[ -z "$UPDATE_REEXECED" ]]; then + export UPDATE_REEXECED=1 + exec ~/.local/bin/update +fi + +# Ask for the password once, then keep sudo's cached credentials fresh until +# update exits, so long-running hooks don't stop for a password again. +sudo -v || exit 1 +while kill -0 $$ 2>/dev/null; do sudo -n -v; sleep 60; done 2>/dev/null & +SUDO_KEEPALIVE_PID=$! +trap 'kill "$SUDO_KEEPALIVE_PID" 2>/dev/null' EXIT + +FAILED=() +[[ -n "$UPDATE_PULL_FAILED" ]] && FAILED+=("git pull") + +# Each hook runs in its own subshell so a cd, set -e, exit, or stray variable +# in one hook can't leak into or abort the hooks after it. +for plugin in "$MINISERVER/updates.d/"*.sh; do + [ -f "$plugin" ] || continue + ( + unset -f is_installed run_update + source "$plugin" + if declare -f run_update > /dev/null; then + if ! declare -f is_installed > /dev/null || is_installed; then + run_update + fi + fi + ) + # Checked separately: under `( ... ) ||`, bash would ignore set -e inside the hook + [ $? -eq 0 ] || FAILED+=("$(basename "$plugin")") +done + +if (( ${#FAILED[@]} > 0 )); then + echo "Done, with errors in: ${FAILED[*]}" + exit 1 +fi + +echo "Done." +exit 0 +} diff --git a/updates.d/10-pacman.sh b/updates.d/10-pacman.sh new file mode 100755 index 0000000..4355eef --- /dev/null +++ b/updates.d/10-pacman.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +MINISERVER="$HOME/.local/share/MiniServer" + +source "$MINISERVER/utils.sh" +source "$MINISERVER/packages.conf" + +STATUS=0 + +# A machine that hasn't updated in a while can have a keyring too old to verify +# the new packages. Update it first; the -Su below completes this -Sy. +echo "Updating Arch keyring..." +sudo pacman -Sy --needed --noconfirm archlinux-keyring || STATUS=1 + +# script(1) keeps pacman's progress bars while logging its output for the +# initramfs check +echo "Updating Arch..." +mkdir -p "$(dirname "$PACMAN_UPDATE_LOG")" +: > "$PACMAN_UPDATE_LOG" +sudo script -qefc "pacman -Su --noconfirm" "$PACMAN_UPDATE_LOG" || STATUS=1 + +if initramfs_build_failed; then + echo -e "\e[31mRebuilding the initramfs failed. Don't reboot until it's fixed - see the output above or $PACMAN_UPDATE_LOG.\e[0m" + STATUS=1 +fi + +echo "Checking for new packages..." +install_packages "${SERVERS[@]}" +install_packages "${UTILITIES[@]}" + +exit $STATUS diff --git a/updates.d/20-aur.sh b/updates.d/20-aur.sh new file mode 100755 index 0000000..fa1fc4c --- /dev/null +++ b/updates.d/20-aur.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +echo "Updating AUR..." + +YAY_FLAGS="--noconfirm --answerdiff=None --answeredit=None --answerclean=None" + +# Only rebuild yay when yay itself is broken (e.g. a pacman upgrade bumped +# libalpm). A failing AUR package is not fixed by a rebuild, so let that error +# show and fail the hook instead. +if ! yay --version &>/dev/null; then + echo "yay is broken (likely a libalpm bump). Rebuilding yay..." + sudo pacman -R --noconfirm yay + cd /tmp + rm -rf yay + git clone https://aur.archlinux.org/yay.git + cd yay + makepkg -si --noconfirm + cd .. + rm -rf yay + cd +fi + +yay -Sua $YAY_FLAGS diff --git a/updates.d/30-pm2.sh b/updates.d/30-pm2.sh new file mode 100755 index 0000000..3bf4254 --- /dev/null +++ b/updates.d/30-pm2.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# pm2 is installed by install.sh with npm into ~/.local +is_installed() { + [ -x "$HOME/.local/bin/pm2" ] +} + +run_update() { + echo "Updating pm2..." + npm update -g pm2 --prefix "$HOME/.local" +} diff --git a/updates.d/71-siblings.sh b/updates.d/71-siblings.sh new file mode 100755 index 0000000..4e02bd9 --- /dev/null +++ b/updates.d/71-siblings.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Sibling projects installed with the `sibling` command +SIBLINGS="$HOME/.local/share/siblings" + +is_installed() { + compgen -G "$SIBLINGS/*/.git" > /dev/null +} + +run_update() { + echo "Updating sibling projects..." + local dir name failed=0 + for dir in "$SIBLINGS"/*/; do + [ -d "$dir/.git" ] || continue + name="$(basename "$dir")" + echo " $name" + if ! git -C "$dir" pull --ff-only --quiet; then + echo " Could not pull $name." + failed=1 + continue + fi + if [ -f "$dir/update.sh" ]; then + (cd "$dir" && bash update.sh) || { echo " $name's update.sh failed."; failed=1; } + fi + done + return $failed +} diff --git a/updates.d/95-cleanup.sh b/updates.d/95-cleanup.sh new file mode 100755 index 0000000..09bdc4c --- /dev/null +++ b/updates.d/95-cleanup.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +if command -v paccache &>/dev/null; then + echo "Trimming pacman package cache..." + sudo paccache -rk2 + sudo paccache -ruk0 +fi + +# Report packages that were installed as dependencies and are no longer needed. +# Not removed automatically: some may be tools you use directly. +ORPHANS="$(pacman -Qtdq 2>/dev/null)" +if [ -n "$ORPHANS" ]; then + echo "Packages no longer needed by anything:" + echo "$ORPHANS" | sed 's/^/ /' + echo "To remove them: sudo pacman -Rns \$(pacman -Qtdq)" +fi diff --git a/updates.d/99-reboot-check.sh b/updates.d/99-reboot-check.sh new file mode 100755 index 0000000..2832a6d --- /dev/null +++ b/updates.d/99-reboot-check.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +source "$HOME/.local/share/MiniServer/utils.sh" + +if initramfs_build_failed; then + echo -e "\e[31mDon't reboot yet: the initramfs failed to rebuild (see $PACMAN_UPDATE_LOG).\e[0m" +# If the running kernel's modules are gone, a newer kernel was installed +elif [ ! -d "/usr/lib/modules/$(uname -r)" ]; then + echo "A newer kernel has been installed. Please reboot when convenient." +fi diff --git a/utils.sh b/utils.sh index 62c5d07..a364ce5 100755 --- a/utils.sh +++ b/utils.sh @@ -23,6 +23,53 @@ install_packages() { if [ ${#to_install[@]} -ne 0 ]; then echo "Installing: ${to_install[*]}" - yay -S --noconfirm "${to_install[@]}" + yay -S --noconfirm --answerdiff=None --answeredit=None --answerclean=None "${to_install[@]}" fi -} \ No newline at end of file +} + +# Function to remove pacman/AUR packages if installed +remove_packages() { + local packages=("$@") + local to_remove=() + + for pkg in "${packages[@]}"; do + if is_installed "$pkg"; then + to_remove+=("$pkg") + fi + done + + if [ ${#to_remove[@]} -ne 0 ]; then + echo "Removing: ${to_remove[*]}" + sudo pacman -Rns --noconfirm "${to_remove[@]}" + fi +} + +# Function to enable systemd services if not already enabled +enable_services() { + for service in "$@"; do + if ! systemctl is-enabled "$service" &> /dev/null; then + echo "Enabling $service..." + sudo systemctl enable "$service" + else + echo "$service is already enabled." + fi + done +} + +# Output of the last update's pacman -Su, written by updates.d/10-pacman.sh +PACMAN_UPDATE_LOG="$HOME/.local/state/miniserver/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" || ! grep -q "Image generation successful" "$PACMAN_UPDATE_LOG" +} + +# 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" +}