diff --git a/README.md b/README.md index b26e9bd..027ea93 100644 --- a/README.md +++ b/README.md @@ -170,13 +170,14 @@ The installer adds the Vulkan driver for Intel and AMD GPUs, and Intel's video d Besides installing software, CoreDesktop changes these settings. They're listed so you know what you're running, and so you can undo them (see "Uninstalling"). - **/etc/pacman.conf**: adds the `[federated]` repository and trusts its signing key (see Pacman above). +- **Mirrors**: reflector rewrites /etc/pacman.d/mirrorlist once a week with fast HTTPS mirrors that synced with Arch in the last 12 hours, using /etc/xdg/reflector/reflector.conf. To choose your own mirrors, delete that file's first (CoreDesktop) line and edit the rest; to stop CoreDesktop changing your mirrors at all, add `reflector` to ~/.config/coredesktop/skip-packages. - **SSH**: sshd is on, with key login only (/etc/ssh/sshd_config.d/10-coredesktop-keys-only.conf). The SSH client keepalives are in /etc/ssh/ssh_config.d/20-coredesktop-keepalive.conf. - **Firewall**: ufw is on, with the rules under Firewall above, and ufw-docker's rules in /etc/ufw/after.rules. - **Login lockout**: deny and unlock_time in /etc/security/faillock.conf. - **systemd**: a 10-second stop timeout (/etc/systemd/system.conf.d/ and /etc/systemd/user.conf.d/10-coredesktop-timeout.conf), systemd-oomd for apps (/etc/systemd/user/app.slice.d/10-coredesktop-oomd.conf), and NetworkManager-wait-online is masked. - **/etc/security/pam_env.conf**: a PATH line, so SSH commands find ~/.local/bin and mise. - **mDNS**: Avahi is on, nss-mdns is added to /etc/nsswitch.conf, and SSH is advertised in /etc/avahi/services/ssh.service. -- **Services turned on**: Avahi, Bluetooth, CUPS (printing), fwupd, GDM, NetworkManager, sshd, Docker, Tailscale, ufw and systemd-oomd. Your user is added to the docker group. +- **Services turned on**: Avahi, Bluetooth, CUPS (printing), fwupd, GDM, NetworkManager, reflector (weekly), sshd, Docker, Tailscale, ufw and systemd-oomd. Your user is added to the docker group. - **Snapper** (btrfs only): a config for / in /etc/snapper/configs/root. - **NVIDIA** (only if it installs the driver): /etc/mkinitcpio.conf.d/nvidia.conf and NVIDIA's suspend services. - **Your home folder**: lines in ~/.bashrc (CoreDesktop's shell setup, ~/.local/bin on PATH, the Bitwarden SSH agent), the commands in ~/.local/bin (update, refresh, sibling, version, configure, audit), GNOME extensions and their settings, Claude Code's ~/.claude/settings.json if you had none, a user timer that checks for updates, and a user service that pauses your Core's email if its password stops working (see Configure). @@ -196,6 +197,7 @@ The script pulls the latest CoreDesktop, then runs each step in ~/.local/share/c - 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) +- switches to up-to-date mirrors first if yours is more than a day behind Arch, as it would otherwise miss new packages without saying so - updates the Arch keyring, then Arch and GNOME, then AUR packages, Flatpaks, the languages you installed with mise, the pipx apps CoreDesktop installs, 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 @@ -232,7 +234,7 @@ Video driver Mesa 26.2.6 Last update 2026-10-22 13:43 ``` -The kernel and video driver (NVIDIA's if it's installed, otherwise Mesa, which drives Intel and AMD graphics) come from Arch, so they're shown separately. "version" tells you if Arch has a newer kernel or video driver, or if a newer version of CoreDesktop is available; "update" installs them. If a newer kernel is waiting for a reboot, it says that too. +The kernel and video driver (NVIDIA's if it's installed, otherwise Mesa, which drives Intel and AMD graphics) come from Arch, so they're shown separately. "version" tells you if Arch has a newer kernel or video driver, or if a newer version of CoreDesktop is available; "update" installs them. If a newer kernel is waiting for a reboot, it says that too. It can only see what your package mirror has, so if the mirror is more than a day behind Arch it warns you; "update" then switches to up-to-date mirrors. You don't have to check by hand: a few minutes after you log in, and every few hours after that, CoreDesktop checks for a new version and shows a notification if there is one. Click it to run "update" in a terminal, or dismiss it. Once dismissed, you won't be notified again until there's an even newer version. diff --git a/install.sh b/install.sh index 6f9d136..c54140c 100755 --- a/install.sh +++ b/install.sh @@ -219,6 +219,10 @@ bash ~/.local/share/coredesktop/system/sshd.sh echo "Configuring the firewall..." bash ~/.local/share/coredesktop/system/firewall.sh +# Keep pacman on mirrors that are up to date with Arch +echo "Configuring package mirrors..." +bash ~/.local/share/coredesktop/system/mirrors.sh + # Snapshot / before each update (btrfs only) echo "Configuring Snapper..." bash ~/.local/share/coredesktop/system/snapper.sh diff --git a/migrations/1793800328.sh b/migrations/1793800328.sh new file mode 100644 index 0000000..050261c --- /dev/null +++ b/migrations/1793800328.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# Machines installed before system/mirrors.sh can be stuck on a mirror that +# stopped syncing, which hides new packages such as kernels. Set up reflector +# to keep the mirrorlist fresh, and pick fresh mirrors now. +bash "$HOME/.local/share/coredesktop/system/mirrors.sh" diff --git a/packages.conf b/packages.conf index 3c54b02..591be65 100644 --- a/packages.conf +++ b/packages.conf @@ -112,6 +112,8 @@ MAINTENANCE=( gnome-extra gnome-keyring pacman-contrib + # Keeps the mirrorlist on up-to-date mirrors (system/mirrors.sh) + reflector ) # Desktop environment @@ -164,6 +166,7 @@ SERVICES=( fwupd-refresh.timer gdm.service NetworkManager.service + reflector.timer sshd.service ) diff --git a/system/mirrors.sh b/system/mirrors.sh new file mode 100644 index 0000000..4040a5f --- /dev/null +++ b/system/mirrors.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# +# Purpose : Keep pacman's mirrorlist on mirrors that are up to date with Arch. +# A mirror that stops syncing doesn't cause an error: update and +# version just stop seeing new packages, such as a new kernel. +# +# reflector.timer picks fresh mirrors once a week, and update picks new ones +# straight away when the first mirror falls a day behind (updates.d/10-pacman.sh). +# To choose mirrors yourself, edit /etc/xdg/reflector/reflector.conf: once it +# doesn't start with the CoreDesktop line below, this leaves it alone. To stop +# CoreDesktop changing the mirrorlist at all, add reflector to +# ~/.config/coredesktop/skip-packages. + +COREDESKTOP="$HOME/.local/share/coredesktop" + +source "$COREDESKTOP/utils.sh" + +CONF=/etc/xdg/reflector/reflector.conf +MARKER="# CoreDesktop: choose up-to-date mirrors (system/mirrors.sh)" + +install_packages reflector || exit 1 +# Skipped in skip-packages +is_installed reflector || exit 0 + +# Ours, or reflector's own that no one has edited, is ours to replace (pacman +# translates UNMODIFIED, hence LC_ALL=C) +if head -n 1 "$CONF" 2> /dev/null | grep -qxF "$MARKER" \ + || LC_ALL=C pacman -Qii reflector 2> /dev/null | grep -qE "^UNMODIFIED[[:space:]]+$CONF$"; then + # Mirrors that synced in the last 12 hours, fastest first. --latest rates only + # the 20 most recently synced, as rating every mirror takes minutes. + sudo tee "$CONF" > /dev/null << EOF +$MARKER +# Delete the line above to choose your own; CoreDesktop then leaves this alone. +--save /etc/pacman.d/mirrorlist +--protocol https +--age 12 +--completion-percent 100 +--latest 20 +--sort rate +EOF +else + echo "Keeping your own mirror choices in $CONF." +fi + +enable_services reflector.timer + +# A failed refresh leaves the mirrorlist as it was +echo "Choosing up-to-date mirrors..." +if ! sudo systemctl start reflector.service; then + echo "Couldn't choose new mirrors; keeping the ones in /etc/pacman.d/mirrorlist." +fi +exit 0 diff --git a/tests/utils.bats b/tests/utils.bats index acce5a3..e869523 100644 --- a/tests/utils.bats +++ b/tests/utils.bats @@ -258,3 +258,50 @@ pacman_log() { run initramfs_build_failed [ "$status" -eq 0 ] } + +# --- mirrors ---------------------------------------------------------------- + +setup_mirrors() { + export MIRRORLIST="$BATS_TEST_TMPDIR/mirrorlist" + cat > "$MIRRORLIST" << 'LIST' +## Worldwide +#Server = https://commented.example/archlinux/$repo/os/$arch +Server = https://first.example/archlinux/$repo/os/$arch +Server = https://second.example/$repo/os/$arch +LIST + # lastsync is $SYNCED, a Unix time + stub curl 'echo "$SYNCED"; [ -n "$SYNCED" ]' + source "$REPO/utils.sh" +} + +@test "first_mirror is the first Server line that isn't commented out" { + setup_mirrors + [ "$(first_mirror)" = "https://first.example/archlinux" ] +} + +@test "mirror_age_hours reads the first mirror's lastsync" { + setup_mirrors + export SYNCED="$(( $(date +%s) - 30 * 3600 ))" + run mirror_age_hours + [ "$status" -eq 0 ] + [ "$output" = 30 ] + called curl -sf --max-time 10 https://first.example/archlinux/lastsync +} + +@test "mirror_age_hours fails when the mirror can't be reached or has no lastsync" { + setup_mirrors + export SYNCED="" + run mirror_age_hours + [ "$status" -ne 0 ] + export SYNCED="Not Found" + run mirror_age_hours + [ "$status" -ne 0 ] +} + +@test "mirror_age_hours fails without a mirrorlist" { + setup_mirrors + rm "$MIRRORLIST" + run mirror_age_hours + [ "$status" -ne 0 ] + not_called curl +} diff --git a/tests/version.bats b/tests/version.bats index 79e0d19..8802dc7 100644 --- a/tests/version.bats +++ b/tests/version.bats @@ -22,6 +22,10 @@ setup() { stub pacman 'grep "^$2 " <<< "$INSTALLED" || exit 1' stub checkupdates 'printf "%s" "$PENDING"; [ -n "$PENDING" ] || exit 2' export INSTALLED="mesa 1:26.2.6-1" PENDING="" + # The mirror synced $SYNCED_HOURS ago (an hour by default) + export MIRRORLIST="$BATS_TEST_TMPDIR/mirrorlist" SYNCED_HOURS=1 + echo 'Server = https://mirror.example/archlinux/$repo/os/$arch' > "$MIRRORLIST" + stub curl 'echo "$(( $(date +%s) - SYNCED_HOURS * 3600 ))"' } @test "version shows the kernel and Mesa, with nothing newer to install" { @@ -31,6 +35,7 @@ setup() { [[ "$output" == *"Kernel 9.9.1-arch1-1 (newer kernel installed, reboot to use it)"$'\n'* ]] [[ "$output" == *"Video driver Mesa 26.2.6"$'\n'* ]] [[ "$output" != *"is available"* ]] + [[ "$output" != *"mirror"* ]] } @test "version suggests update for a newer kernel and video driver" { @@ -68,3 +73,19 @@ setup() { [[ "$output" == *"Kernel 9.9.1-arch1-1"* ]] [[ "$output" != *"is available"* ]] } + +@test "version warns when the package mirror is behind Arch" { + export SYNCED_HOURS=73 + run bash "$CD/version" + [ "$status" -eq 0 ] + [[ "$output" == *"Your package mirror (https://mirror.example/archlinux) last synced with Arch 3 days ago"* ]] + [[ "$output" == *"Run update"* ]] +} + +@test "version doesn't warn about a mirror it can't reach" { + rm "$STUB_BIN/curl" + stub curl 'exit 7' + run bash "$CD/version" + [ "$status" -eq 0 ] + [[ "$output" != *"mirror"* ]] +} diff --git a/updates.d/10-pacman.sh b/updates.d/10-pacman.sh index ef19b6d..81c5337 100644 --- a/updates.d/10-pacman.sh +++ b/updates.d/10-pacman.sh @@ -17,6 +17,13 @@ remove_flatpaks "${REMOVE_FLATPAKS[@]}" echo "Checking the Federated Computer package repo..." ensure_federated_repo || STATUS=1 +# A stale mirror hides new packages instead of failing, so pick fresh ones +# before syncing. mirrors.sh also sets up reflector on machines without it. +if age="$(mirror_age_hours)" && (( age >= MIRROR_STALE_HOURS )); then + echo "Your package mirror ($(first_mirror)) last synced with Arch $age hours ago." + bash "$COREDESKTOP/system/mirrors.sh" +fi + # 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..." diff --git a/utils.sh b/utils.sh index 2d18039..e630184 100755 --- a/utils.sh +++ b/utils.sh @@ -199,6 +199,29 @@ initramfs_build_failed() { (( 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 +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() { diff --git a/version b/version index 3260767..4391d63 100755 --- a/version +++ b/version @@ -124,3 +124,11 @@ echo "CoreDesktop $version" echo "Kernel $kernel" [ -n "$driver" ] && echo "Video driver $driver" echo "Last update $last_update" + +# Packages newer than the mirror's last sync aren't shown above, or installed +if age="$(mirror_age_hours)" && (( age >= MIRROR_STALE_HOURS )); then + if (( age >= 48 )); then ago="$(( age / 24 )) days"; else ago="$age hours"; fi + echo + echo -e "\e[33mYour package mirror ($(first_mirror)) last synced with Arch $ago ago, so" + echo -e "newer packages may be missing. Run update to switch to up-to-date mirrors.\e[0m" +fi