From 0905f858a9563074e001ac37b005c5b50bb6029b Mon Sep 17 00:00:00 2001 From: David Young Date: Wed, 21 Oct 2026 08:33:19 -0600 Subject: [PATCH] numerous updates --- README.md | 55 +++++++++++++++++++++- install.sh | 28 ++++++++++- migrations/1792592700.sh | 4 ++ migrations/1792592710.sh | 4 ++ migrations/1792592720.sh | 4 ++ migrations/1792592730.sh | 4 ++ migrations/1792592740.sh | 4 ++ migrations/1792592750.sh | 4 ++ packages.conf | 4 ++ shell/aliases | 31 ++++++++++++ shell/bashrc | 37 +++++++++++++++ shell/functions | 78 +++++++++++++++++++++++++++++++ shell/setup.sh | 13 ++++++ shell/starship.toml | 32 +++++++++++++ skills/sibling-project-updates.md | 3 +- system/defaults.sh | 65 ++++++++++++++++++++++++++ system/firewall.sh | 31 ++++++++++++ system/nvidia.sh | 67 ++++++++++++++++++++++++++ system/snapper.sh | 54 +++++++++++++++++++++ system/vulkan.sh | 25 ++++++++++ update | 10 ++++ updates.d/09-snapshot.sh | 15 ++++++ updates.d/10-pacman.sh | 20 +++++++- updates.d/12-migrations.sh | 15 ++++++ updates.d/95-cleanup.sh | 9 ++++ updates.d/99-reboot-check.sh | 6 ++- utils.sh | 34 ++++++++++++++ 27 files changed, 651 insertions(+), 5 deletions(-) create mode 100644 migrations/1792592710.sh create mode 100644 migrations/1792592720.sh create mode 100644 migrations/1792592730.sh create mode 100644 migrations/1792592740.sh create mode 100644 migrations/1792592750.sh create mode 100644 shell/aliases create mode 100644 shell/bashrc create mode 100644 shell/functions create mode 100644 shell/setup.sh create mode 100644 shell/starship.toml create mode 100644 system/defaults.sh create mode 100644 system/firewall.sh create mode 100644 system/nvidia.sh create mode 100644 system/snapper.sh create mode 100644 system/vulkan.sh create mode 100644 updates.d/09-snapshot.sh create mode 100644 updates.d/12-migrations.sh diff --git a/README.md b/README.md index f39c870..68c94d2 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,48 @@ The printing service (cups) is turned on. This may need more work to "just work" mDNS is installed and enabled so the your can find and SSH to your machine on your LAN using {machinename}.local. +### Firewall + +The ufw firewall is on. It blocks incoming connections except SSH (22), mDNS (5353/udp), LocalSend (53317) and anything arriving over Tailscale. ufw-docker is installed too, because ports published by Docker containers otherwise get around ufw. To let a container's port through, use `sudo ufw-docker allow {container} {port}`. To open a port for something else, e.g. a dev server you want to reach from another machine, use `sudo ufw allow {port}/tcp`. `sudo ufw status` shows the current rules. + +### Shell + +Your ~/.bashrc sources ~/.local/share/coredesktop/shell/bashrc, which sets up the starship prompt, zoxide (`z {part of a path}` jumps to a directory you've used before), fzf key bindings (Ctrl-R searches history, Ctrl-T inserts a file path), eza for `ls`/`ll`/`la`/`lt`, and the aliases and functions in [shell/](shell/). If you already have ~/.config/starship.toml, your prompt config is used. Anything you add to ~/.bashrc after the CoreDesktop line overrides it. + +### Snapshots + +If / is btrfs (the archinstall default), Snapper takes a snapshot of / before every `update` and keeps the last 5. /home is a separate subvolume and isn't included, so rolling back never touches your files. `sudo snapper -c root list` shows the snapshots. + +These snapshots can't be picked from the boot menu. To roll back after a bad update, boot the Arch ISO and replace the @ subvolume with a snapshot: + +``` +mount -o subvolid=5 /dev/{root partition} /mnt +mv /mnt/@ /mnt/@.broken +btrfs subvolume snapshot /mnt/@.snapshots/{number}/snapshot /mnt/@ +reboot +``` + +If the disk is encrypted, open it with `cryptsetup open` first and mount the opened device. Once the system works again, free the space the broken root used: + +``` +sudo mount -o subvolid=5 /dev/{root partition} /mnt +sudo btrfs subvolume delete /mnt/@.broken +sudo umount /mnt +``` + +### GPU drivers + +The installer adds the Vulkan driver for Intel and AMD GPUs. If there's an NVIDIA GPU and archinstall's proprietary driver wasn't chosen, it installs NVIDIA's driver: the open kernel modules for GTX 16xx / RTX 20xx and newer, or the 580xx legacy driver for older cards back to the GTX 900 series. Reboot afterwards. + +### System defaults + +- SSH connections notice a dropped link within a minute instead of hanging (override per host in ~/.ssh/config). +- Logins allow 10 wrong passwords before locking the account for 2 minutes (Arch's default is 3 tries, then 10 minutes). +- Shutdown waits at most 10 seconds for a service that won't stop, instead of 90. +- If memory runs out, systemd-oomd closes the app causing it instead of the whole desktop freezing. +- Boot no longer waits for the network. +- Commands run over SSH (`ssh {machine} {command}`) can find tools in ~/.local/bin and mise. + ## Keep your System Up-to-date From time-to-time, you'll want to update your system. Hopefully you'll do this more than once-a-month. @@ -103,7 +145,16 @@ To update the system type into the terminal: update ``` -The script does three things. It runs 'flatpak update' to update your Flatpak apps, it runs 'gem update' to update your Rails environment, and it runs 'sudo pacman -Syu' to update your Arch and Gnome installation. +The script pulls the latest CoreDesktop, then runs each step in ~/.local/share/coredesktop/updates.d/ in order. Among other things it: + +- stops if there's less than 10 GiB free on / (run `UPDATE_FORCE=1 update` to skip the check) +- snapshots / first, if Snapper is set up (see Snapshots above) +- updates the Arch keyring, then Arch and GNOME, then AUR packages, Flatpaks, Ruby, GNOME extensions and firmware +- warns you not to reboot if the initramfs failed to rebuild +- runs any one-time migrations, the way existing machines pick up new CoreDesktop setup +- lists packages that are no longer needed, and the command to remove them + +If a step fails, the other steps still run, and the failed ones are listed at the end. It is a good idea to reboot after an update especially if you see you have a new Linux kernel installed (and that happens frequently enough for Arch). @@ -141,6 +192,8 @@ If you want to use mDNS networking on your LAN (the ability to log in to other l This whole "project" is licensed under the MIT License. Want to help make CoreDesktop better? Awesome! We welcome any and all help. Just contribute a PR and we're off to a better CoreDesktop. +Changes to install.sh only reach new installs. If existing machines need the change too, also add a migration: a script named `migrations/$(date +%s).sh` that `update` runs once on each machine. It may also run on fresh installs, so make it safe to repeat. + ## Bonus Bonus: here's a [nice song to listen to while you're installing](https://music.youtube.com/watch?v=y-5o2f9wzmw&si=_z9kcKHRwj-KTs7w). diff --git a/install.sh b/install.sh index 3b13667..9b4e87c 100755 --- a/install.sh +++ b/install.sh @@ -70,7 +70,8 @@ echo "Starting full system setup..." # Bring the system fully up to date before installing anything new echo "Updating system..." -sudo pacman -Syu --noconfirm +sudo pacman -Sy --needed --noconfirm archlinux-keyring +sudo pacman -Su --noconfirm cd ~/ @@ -168,6 +169,26 @@ if ! systemctl is-active tailscaled &> /dev/null; then sudo systemctl start tailscaled fi +# SSH keepalives, login lockout, shutdown timeout, OOM protection, boot, SSH PATH +echo "Applying system defaults..." +bash ~/.local/share/coredesktop/system/defaults.sh + +# Block incoming connections except SSH, mDNS, LocalSend and Tailscale +echo "Configuring the firewall..." +bash ~/.local/share/coredesktop/system/firewall.sh + +# Snapshot / before each update (btrfs only) +echo "Configuring Snapper..." +bash ~/.local/share/coredesktop/system/snapper.sh + +echo "Installing GPU drivers..." +bash ~/.local/share/coredesktop/system/vulkan.sh +bash ~/.local/share/coredesktop/system/nvidia.sh + +# Prompt, zoxide, fzf key bindings, aliases and functions +echo "Configuring the shell..." +bash ~/.local/share/coredesktop/shell/setup.sh + # Apply GNOME dconf settings only if the settings file has changed since last run bash ~/.local/share/coredesktop/gnome/apply-gnome-settings.sh @@ -177,6 +198,11 @@ if systemctl is-enabled systemd-resolved.service &> /dev/null; then bash ~/.local/share/coredesktop/fixmdns.sh fi +# Run one-time migrations not yet recorded on this machine. On a fresh install +# they repeat setup done above, which is safe because each one is idempotent. +echo "Running migrations..." +run_migrations + # 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 diff --git a/migrations/1792592700.sh b/migrations/1792592700.sh index ec289eb..26040f8 100755 --- a/migrations/1792592700.sh +++ b/migrations/1792592700.sh @@ -1,5 +1,7 @@ #!/bin/bash +# Rename the coreupdate and corerefresh commands to update and refresh + COREDESKTOP="$HOME/.local/share/coredesktop" source "$COREDESKTOP/utils.sh" @@ -15,3 +17,5 @@ if [ -f ~/.local/bin/corerefresh ]; then install_script "$COREDESKTOP/refresh" ~/.local/bin/refresh sudo rm -f ~/.local/bin/corerefresh fi + +exit 0 diff --git a/migrations/1792592710.sh b/migrations/1792592710.sh new file mode 100644 index 0000000..269ae47 --- /dev/null +++ b/migrations/1792592710.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Hook CoreDesktop's shell setup into ~/.bashrc +bash "$HOME/.local/share/coredesktop/shell/setup.sh" diff --git a/migrations/1792592720.sh b/migrations/1792592720.sh new file mode 100644 index 0000000..269228e --- /dev/null +++ b/migrations/1792592720.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# SSH keepalives, login lockout, shutdown timeout, OOM protection, boot and SSH PATH +bash "$HOME/.local/share/coredesktop/system/defaults.sh" diff --git a/migrations/1792592730.sh b/migrations/1792592730.sh new file mode 100644 index 0000000..708bacd --- /dev/null +++ b/migrations/1792592730.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Turn on the ufw firewall +bash "$HOME/.local/share/coredesktop/system/firewall.sh" diff --git a/migrations/1792592740.sh b/migrations/1792592740.sh new file mode 100644 index 0000000..e7a73d9 --- /dev/null +++ b/migrations/1792592740.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Set up Snapper for pre-update snapshots on btrfs +bash "$HOME/.local/share/coredesktop/system/snapper.sh" diff --git a/migrations/1792592750.sh b/migrations/1792592750.sh new file mode 100644 index 0000000..a72b4d6 --- /dev/null +++ b/migrations/1792592750.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Install Vulkan drivers for Intel and AMD GPUs +bash "$HOME/.local/share/coredesktop/system/vulkan.sh" diff --git a/packages.conf b/packages.conf index 094fb79..71cb326 100644 --- a/packages.conf +++ b/packages.conf @@ -1,6 +1,7 @@ # System utilities SYSTEM_UTILS=( age + bash-completion bluez bluez-utils borg @@ -48,6 +49,8 @@ SYSTEM_UTILS=( tailscale traceroute tree + ufw + ufw-docker unzip usbutils vi @@ -128,6 +131,7 @@ MEDIA=( FONTS=( noto-fonts noto-fonts-emoji + ttf-nerd-fonts-symbols ) # Services to enable diff --git a/shell/aliases b/shell/aliases new file mode 100644 index 0000000..044e281 --- /dev/null +++ b/shell/aliases @@ -0,0 +1,31 @@ +# CoreDesktop aliases, sourced by shell/bashrc + +# Files +if command -v eza &> /dev/null; then + alias ls='eza --group-directories-first --icons=auto' + alias ll='eza -lh --group-directories-first --icons=auto' + alias la='eza -lha --group-directories-first --icons=auto' + alias lt='eza --tree --level=2 --long --icons=auto --git' +fi + +# Fuzzy-find a file with a preview, or open the one you pick in your editor +alias ff="fzf --preview 'bat --style=numbers --color=always {}'" +alias eff='${EDITOR:-nvim} "$(ff)"' + +alias decompress='tar -xzf' + +# Directories +alias ..='cd ..' +alias ...='cd ../..' +alias ....='cd ../../..' + +# Tools +alias g='git' +alias lg='lazygit' +alias d='docker' +alias ld='lazydocker' + +# Git +alias gcm='git commit -m' +alias gcam='git commit -a -m' +alias gcad='git commit -a --amend' diff --git a/shell/bashrc b/shell/bashrc new file mode 100644 index 0000000..6098b6a --- /dev/null +++ b/shell/bashrc @@ -0,0 +1,37 @@ +# CoreDesktop shell setup, sourced from ~/.bashrc (added by shell/setup.sh). +# Anything in ~/.bashrc after the line that sources this file overrides it. + +# Only for interactive shells +[[ $- != *i* ]] && return + +COREDESKTOP_SHELL="$HOME/.local/share/coredesktop/shell" + +# History: keep a lot of it, skip duplicates, and don't let shells overwrite +# each other's history on exit +shopt -s histappend +HISTCONTROL=ignoreboth +HISTSIZE=32768 +HISTFILESIZE="$HISTSIZE" + +if [[ ! -v BASH_COMPLETION_VERSINFO && -f /usr/share/bash-completion/bash_completion ]]; then + source /usr/share/bash-completion/bash_completion +fi + +source "$COREDESKTOP_SHELL/aliases" +source "$COREDESKTOP_SHELL/functions" + +# Prompt. Uses CoreDesktop's starship config unless you have your own. +if [[ $TERM != "dumb" ]] && command -v starship &> /dev/null; then + [[ -f "$HOME/.config/starship.toml" ]] || export STARSHIP_CONFIG="$COREDESKTOP_SHELL/starship.toml" + eval "$(starship init bash)" +fi + +# z jumps to a directory you've visited before +if command -v zoxide &> /dev/null; then + eval "$(zoxide init bash)" +fi + +# Ctrl-R fuzzy history search, Ctrl-T to insert a file path, Alt-C to cd +if [[ -f /usr/share/fzf/key-bindings.bash ]]; then + source /usr/share/fzf/key-bindings.bash +fi diff --git a/shell/functions b/shell/functions new file mode 100644 index 0000000..867bd7b --- /dev/null +++ b/shell/functions @@ -0,0 +1,78 @@ +# CoreDesktop shell functions, sourced by shell/bashrc + +# n opens Neovim in the current directory, or on the files given +n() { if [ "$#" -eq 0 ]; then command nvim . ; else command nvim "$@"; fi; } + +# open a file or URL with its default app, detached from the terminal +open() ( + xdg-open "$@" > /dev/null 2>&1 & +) + +# compress makes .tar.gz (decompress is an alias for tar -xzf) +compress() { tar -czf "${1%/}.tar.gz" "${1%/}"; } + +# SSH port forwarding +# fip ... forward each localhost port to the same port on host +# dip ... stop forwarding those ports +# lip list active forwards +fip() { + (( $# < 2 )) && echo "Usage: fip [port2] ..." && return 1 + local host="$1" + shift + for port in "$@"; do + ssh -f -N -L "${port}:localhost:${port}" "$host" && echo "Forwarding localhost:$port -> $host:$port" + done +} + +dip() { + (( $# == 0 )) && echo "Usage: dip [port2] ..." && return 1 + for port in "$@"; do + pkill -f "ssh.*-L ${port}:localhost:${port}" && echo "Stopped forwarding port $port" || echo "No forwarding on port $port" + done +} + +lip() { + pgrep -af "ssh.*-L [0-9]+:localhost:[0-9]+" || echo "No active forwards" +} + +# Git worktrees +# ga create ../-- on a new branch and cd into it +# gd from inside that worktree, remove it and its branch +ga() { + if [[ -z "$1" ]]; then + echo "Usage: ga [branch name]" + return 1 + fi + + local branch="$1" + local base="$(basename "$PWD")" + local wt_path="../${base}--${branch}" + + git worktree add -b "$branch" "$wt_path" || return 1 + command -v mise &> /dev/null && mise trust "$wt_path" + cd "$wt_path" +} + +gd() { + local cwd base branch root worktree answer + + cwd="$(pwd)" + worktree="$(basename "$cwd")" + + # split on first `--` + root="${worktree%%--*}" + branch="${worktree#*--}" + + # Protect against accidentally nuking a non-worktree directory + if [[ "$root" == "$worktree" ]]; then + echo "Not in a worktree made by ga (expected a -- directory)." + return 1 + fi + + read -rp "Remove worktree $worktree and branch $branch? [y/N] " answer + [[ $answer == [yY]* ]] || return 0 + + cd "../$root" + git worktree remove "$cwd" --force || return 1 + git branch -D "$branch" +} diff --git a/shell/setup.sh b/shell/setup.sh new file mode 100644 index 0000000..980091e --- /dev/null +++ b/shell/setup.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# +# Purpose : Hook CoreDesktop's shell setup (prompt, zoxide, fzf key bindings, +# aliases and functions in shell/) into ~/.bashrc + +LINE='[[ -f "$HOME/.local/share/coredesktop/shell/bashrc" ]] && source "$HOME/.local/share/coredesktop/shell/bashrc"' + +if grep -qF "$LINE" ~/.bashrc 2>/dev/null; then + echo "CoreDesktop shell setup already in .bashrc" +else + echo "$LINE" >> ~/.bashrc + echo "CoreDesktop shell setup added to .bashrc" +fi diff --git a/shell/starship.toml b/shell/starship.toml new file mode 100644 index 0000000..36f15d1 --- /dev/null +++ b/shell/starship.toml @@ -0,0 +1,32 @@ +add_newline = true +command_timeout = 200 +format = "[$directory$git_branch$git_status]($style)$character" + +[character] +error_symbol = "[✗](bold cyan)" +success_symbol = "[❯](bold cyan)" + +[directory] +truncation_length = 2 +truncation_symbol = "…/" +repo_root_style = "bold cyan" +repo_root_format = "[$repo_root]($repo_root_style)[$path]($style)[$read_only]($read_only_style) " + +[git_branch] +format = "[$branch]($style) " +style = "italic cyan" + +[git_status] +format = '[$all_status]($style)' +style = "cyan" +ahead = "⇡${count} " +diverged = "⇕⇡${ahead_count}⇣${behind_count} " +behind = "⇣${count} " +conflicted = " " +up_to_date = " " +untracked = "? " +modified = " " +stashed = "" +staged = "" +renamed = "" +deleted = "" diff --git a/skills/sibling-project-updates.md b/skills/sibling-project-updates.md index f4b30b9..38d1b19 100644 --- a/skills/sibling-project-updates.md +++ b/skills/sibling-project-updates.md @@ -60,8 +60,9 @@ Existing coredesktop hooks use a numeric prefix to control execution order: | File | Purpose | |---------------------------|-----------------------------------------| -| `00-rename-migration.sh` | Legacy command renames | +| `09-snapshot.sh` | Snapper snapshot of / (btrfs only) | | `10-pacman.sh` | Arch system update | +| `12-migrations.sh` | One-time migrations from `migrations/` | | `15-services.sh` | Enable new systemd services | | `20-aur.sh` | AUR packages | | `30-flatpak.sh` | Flatpak apps | diff --git a/system/defaults.sh b/system/defaults.sh new file mode 100644 index 0000000..062c437 --- /dev/null +++ b/system/defaults.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# +# Purpose : System defaults adapted from Omarchy: SSH keepalives, a gentler +# login lockout, faster shutdown, out-of-memory protection, faster +# boot, and a PATH for commands run over SSH + +set -e + +# SSH client: notice a dropped connection within a minute instead of hanging +# until TCP gives up. Settings in ~/.ssh/config still win per host. +echo "Configuring SSH keepalives..." +sudo install -d -m 755 /etc/ssh/ssh_config.d +sudo tee /etc/ssh/ssh_config.d/20-coredesktop-keepalive.conf > /dev/null << 'EOF' +# CoreDesktop: notice dropped connections quickly instead of hanging until TCP +# times out. Settings in ~/.ssh/config take precedence over these defaults. +Host * + ServerAliveInterval 15 + ServerAliveCountMax 3 + ConnectTimeout 10 +EOF + +# Login lockout: Arch locks an account for 10 minutes after 3 wrong passwords. +# Allow 10 tries and unlock after 2 minutes. +echo "Relaxing the failed-login lockout..." +sudo sed -i -E \ + -e 's/^#?[[:space:]]*deny[[:space:]]*=.*/deny = 10/' \ + -e 's/^#?[[:space:]]*unlock_time[[:space:]]*=.*/unlock_time = 120/' \ + /etc/security/faillock.conf + +# Shutdown: don't wait 90 seconds on a service or app that won't stop +echo "Shortening the shutdown timeout..." +sudo install -d -m 755 /etc/systemd/system.conf.d /etc/systemd/user.conf.d +printf '[Manager]\nDefaultTimeoutStopSec=10s\n' \ + | sudo tee /etc/systemd/system.conf.d/10-coredesktop-timeout.conf > /dev/null +printf '[Manager]\nDefaultTimeoutStopSec=10s\n' \ + | sudo tee /etc/systemd/user.conf.d/10-coredesktop-timeout.conf > /dev/null + +# Out of memory: let systemd-oomd kill the app causing memory pressure instead +# of the whole session freezing. GNOME launches apps into app.slice, so GNOME +# Shell itself is never a candidate. +echo "Enabling out-of-memory protection..." +sudo install -d -m 755 /etc/systemd/user/app.slice.d +sudo tee /etc/systemd/user/app.slice.d/10-coredesktop-oomd.conf > /dev/null << 'EOF' +[Slice] +ManagedOOMMemoryPressure=kill +ManagedOOMSwap=kill +EOF +sudo systemctl daemon-reload +sudo systemctl enable --now systemd-oomd.service + +# Boot: nothing on the desktop needs to wait for the network to come up +echo "Masking NetworkManager-wait-online..." +sudo systemctl mask NetworkManager-wait-online.service + +# `ssh host command` runs without a login shell, so ~/.bashrc never sets PATH +# and tools in ~/.local/bin or installed with mise aren't found. PAM sets the +# environment for every login, including those. @{HOME} expands per user. +if ! grep -qE '^PATH[[:space:]]' /etc/security/pam_env.conf; then + echo "Adding user tool paths for SSH commands..." + sudo tee -a /etc/security/pam_env.conf > /dev/null << 'EOF' + +# CoreDesktop: give SSH commands and other non-shell logins the user-level tool paths +PATH DEFAULT=/usr/local/sbin:/usr/local/bin:/usr/bin:@{HOME}/.local/share/mise/shims:@{HOME}/.local/bin +EOF +fi diff --git a/system/firewall.sh b/system/firewall.sh new file mode 100644 index 0000000..22f1570 --- /dev/null +++ b/system/firewall.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# +# Purpose : Turn on the ufw firewall. Incoming connections are blocked except +# for the services CoreDesktop runs, and ufw-docker stops ports +# published by Docker containers from getting around the firewall. + +set -e + +source "$HOME/.local/share/coredesktop/utils.sh" + +install_packages ufw ufw-docker + +sudo ufw default deny incoming +sudo ufw default allow outgoing + +sudo ufw allow 22/tcp comment 'SSH' +sudo ufw allow 5353/udp comment 'mDNS' +sudo ufw allow 53317 comment 'LocalSend' +sudo ufw allow in on tailscale0 comment 'Tailscale' + +sudo ufw --force enable +sudo systemctl enable ufw.service + +# ufw-docker only installs its rules while ufw is active, and they take effect +# once ufw restarts +if ! sudo grep -q 'BEGIN UFW AND DOCKER' /etc/ufw/after.rules; then + sudo ufw-docker install + sudo systemctl restart ufw.service +fi + +sudo ufw status verbose diff --git a/system/nvidia.sh b/system/nvidia.sh new file mode 100644 index 0000000..44edb77 --- /dev/null +++ b/system/nvidia.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# +# Purpose : Install NVIDIA's driver when the machine has an NVIDIA GPU and no +# NVIDIA driver yet (i.e. archinstall's proprietary option wasn't +# picked). Turing (GTX 16xx / RTX 20xx) and newer get the open kernel +# modules; Maxwell, Pascal and Volta get the 580xx legacy driver from +# the AUR. Older cards stay on nouveau. +# +# Run from install.sh only, never from an update: silently moving a +# working machine off nouveau is too risky to do mid-update. + +set -e + +source "$HOME/.local/share/coredesktop/utils.sh" + +# Read device IDs from sysfs: lspci needs pciutils and wakes suspended GPUs. +# Turing is the first generation with GSP firmware (open modules) and the first +# with device IDs at 0x1e00 or above; Maxwell starts at 0x1340. +GPU=none +for device in /sys/bus/pci/devices/*; do + [[ $(< "$device/vendor") == 0x10de && $(< "$device/class") == 0x03* ]] || continue + id=$(( $(< "$device/device") )) + if (( id >= 0x1e00 )); then + GPU=open + elif (( id >= 0x1340 )) && [ "$GPU" = none ]; then + GPU=legacy + fi +done + +if [ "$GPU" = none ]; then + echo "No NVIDIA GPU supported by the current drivers found." + exit 0 +fi + +if is_installed nvidia-utils || is_installed nvidia-580xx-utils; then + echo "NVIDIA driver already installed." + exit 0 +fi + +# DKMS builds the module for every installed kernel, so each needs its headers +PACKAGES=() +for kernel in linux linux-lts linux-zen linux-hardened; do + if is_installed "$kernel"; then + PACKAGES+=("$kernel-headers") + fi +done + +if [ "$GPU" = open ]; then + PACKAGES+=(nvidia-open-dkms nvidia-utils libva-nvidia-driver) +else + PACKAGES+=(nvidia-580xx-dkms nvidia-580xx-utils) +fi + +echo "Installing NVIDIA driver..." +install_packages "${PACKAGES[@]}" + +# Load the driver from the initramfs so the GPU is ready when GDM starts +sudo install -d -m 755 /etc/mkinitcpio.conf.d +echo 'MODULES+=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)' \ + | sudo tee /etc/mkinitcpio.conf.d/nvidia.conf > /dev/null +sudo mkinitcpio -P + +# GDM only offers Wayland on NVIDIA when these are enabled, since without them +# the GPU loses its memory contents across suspend +sudo systemctl enable nvidia-suspend.service nvidia-hibernate.service nvidia-resume.service + +echo "NVIDIA driver installed. Reboot to start using it." diff --git a/system/snapper.sh b/system/snapper.sh new file mode 100644 index 0000000..1c4135f --- /dev/null +++ b/system/snapper.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# +# Purpose : Configure Snapper so update can snapshot / before each upgrade +# (updates.d/09-snapshot.sh). Only applies when / is btrfs, which is +# archinstall's default. /home is a separate subvolume and is not +# included in the snapshots. + +if [ "$(findmnt -no FSTYPE /)" != "btrfs" ]; then + echo "/ is not btrfs; skipping Snapper setup." + exit 0 +fi + +set -e + +source "$HOME/.local/share/coredesktop/utils.sh" + +install_packages snapper + +if [ ! -f /etc/snapper/configs/root ]; then + echo "Creating Snapper config for /..." + + # create-config makes its own /.snapshots subvolume and fails if the path + # already exists. archinstall mounts a separate @.snapshots subvolume there, + # so follow the Arch wiki: unmount it, let snapper create its config, then + # swap snapper's nested subvolume back out for the @.snapshots mount. + if mountpoint -q /.snapshots; then + sudo umount /.snapshots + sudo rmdir /.snapshots + sudo snapper -c root create-config / + sudo btrfs subvolume delete /.snapshots + sudo mkdir /.snapshots + sudo mount /.snapshots + else + if [ -e /.snapshots ] && ! sudo rmdir /.snapshots 2>/dev/null; then + echo "/.snapshots exists and isn't an empty directory. Move it aside and run this again." + exit 1 + fi + sudo snapper -c root create-config / + fi + + # Keep only the last 5 pre-update snapshots, and no hourly timeline. Only set + # on a config we created, so a config you made yourself keeps its settings. + sudo snapper -c root set-config \ + TIMELINE_CREATE=no \ + NUMBER_CLEANUP=yes \ + NUMBER_MIN_AGE=0 \ + NUMBER_LIMIT=5 \ + NUMBER_LIMIT_IMPORTANT=5 + sudo systemctl disable --now snapper-timeline.timer 2> /dev/null || true +else + echo "Snapper config for / already exists." +fi + +sudo systemctl enable --now snapper-cleanup.timer diff --git a/system/vulkan.sh b/system/vulkan.sh new file mode 100644 index 0000000..1b0304c --- /dev/null +++ b/system/vulkan.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# +# Purpose : Install the Vulkan driver for each Intel or AMD GPU in the machine. +# NVIDIA's Vulkan driver comes with nvidia-utils (see nvidia.sh). + +set -e + +source "$HOME/.local/share/coredesktop/utils.sh" + +# Read vendor IDs from sysfs: lspci needs pciutils and wakes suspended GPUs +PACKAGES=() +for device in /sys/bus/pci/devices/*; do + [[ $(< "$device/class") == 0x03* ]] || continue + case "$(< "$device/vendor")" in + 0x8086) PACKAGES+=(vulkan-intel) ;; + 0x1002) PACKAGES+=(vulkan-radeon) ;; + esac +done + +if (( ${#PACKAGES[@]} > 0 )); then + mapfile -t PACKAGES < <(printf '%s\n' "${PACKAGES[@]}" | sort -u) + install_packages "${PACKAGES[@]}" +else + echo "No Intel or AMD GPU found." +fi diff --git a/update b/update index 831078d..c530b34 100755 --- a/update +++ b/update @@ -48,6 +48,16 @@ if [[ -z "$UPDATE_REEXECED" ]]; then exec ~/.local/bin/update fi +# An upgrade that runs out of disk space part way through can leave the system +# half-upgraded. Set UPDATE_FORCE=1 to skip this check. +MIN_FREE_GIB=10 +FREE_KIB="$(df -k --output=avail / | tail -n 1 | tr -d ' ')" +if [[ -z "$UPDATE_FORCE" && "$FREE_KIB" =~ ^[0-9]+$ ]] && (( FREE_KIB < MIN_FREE_GIB * 1024 * 1024 )); then + echo "Only $(( FREE_KIB / 1024 / 1024 )) GiB free on /; updating needs at least $MIN_FREE_GIB GiB." + echo "Free up space (sudo paccache -rk1 clears old cached packages), or run: UPDATE_FORCE=1 update" + exit 1 +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 diff --git a/updates.d/09-snapshot.sh b/updates.d/09-snapshot.sh new file mode 100644 index 0000000..de66c2d --- /dev/null +++ b/updates.d/09-snapshot.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Snapshot / before the system update so a bad upgrade can be rolled back. +# system/snapper.sh creates the "root" config on btrfs machines and keeps the +# last 5 snapshots. + +is_installed() { + command -v snapper &>/dev/null && [ -f /etc/snapper/configs/root ] +} + +run_update() { + echo "Snapshotting / before updating..." + sudo snapper -c root create -c number -d "Before CoreDesktop update" \ + && sudo snapper -c root cleanup number +} diff --git a/updates.d/10-pacman.sh b/updates.d/10-pacman.sh index bde676f..2a2a73f 100644 --- a/updates.d/10-pacman.sh +++ b/updates.d/10-pacman.sh @@ -6,12 +6,28 @@ source "$COREDESKTOP/utils.sh" source "$COREDESKTOP/packages.conf" source "$COREDESKTOP/remove-packages.conf" +STATUS=0 + echo "Removing unwanted packages..." remove_packages "${REMOVE_PACMAN[@]}" remove_flatpaks "${REMOVE_FLATPAKS[@]}" +# 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..." -sudo pacman -Syu --noconfirm +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 "${SYSTEM_UTILS[@]}" @@ -20,3 +36,5 @@ install_packages "${MAINTENANCE[@]}" install_packages "${DESKTOP[@]}" install_packages "${MEDIA[@]}" install_packages "${FONTS[@]}" + +exit $STATUS diff --git a/updates.d/12-migrations.sh b/updates.d/12-migrations.sh new file mode 100644 index 0000000..a878ae7 --- /dev/null +++ b/updates.d/12-migrations.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Run one-time changes from migrations/ that haven't run on this machine yet. +# This is how machines installed before a change to install.sh catch up, so a +# change that should reach existing machines needs a migration as well. To add +# one, create migrations/$(date +%s).sh; it runs with bash, once, on success. +# +# Runs after 10-pacman.sh so migrations install packages against fresh +# package databases. + +COREDESKTOP="$HOME/.local/share/coredesktop" + +source "$COREDESKTOP/utils.sh" + +run_migrations diff --git a/updates.d/95-cleanup.sh b/updates.d/95-cleanup.sh index fead088..e418cd7 100755 --- a/updates.d/95-cleanup.sh +++ b/updates.d/95-cleanup.sh @@ -8,3 +8,12 @@ if command -v paccache &>/dev/null; then 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 index 3068171..c4ce7b9 100755 --- a/updates.d/99-reboot-check.sh +++ b/updates.d/99-reboot-check.sh @@ -1,6 +1,10 @@ #!/bin/bash +source "$HOME/.local/share/coredesktop/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 -if [ ! -d "/usr/lib/modules/$(uname -r)" ]; then +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 58d4c7d..2d1e7cf 100755 --- a/utils.sh +++ b/utils.sh @@ -73,6 +73,40 @@ enable_services() { 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" || ! 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() {