Four things the original got wrong, all of which only show up on a machine that is not this one: It detected swap with `swapon --show | grep -q '/'` — a test for a swap FILE. A machine using zram or a swap partition reports no swap at all, and the step would add a swapfile beside working swap. Reads SwapTotal from /proc/meminfo now, which covers every kind. It never looked at free disk. On a VPS with 4G free and 16G of RAM it would fallocate 8G, fail, and take the run down under `set -e`. The recommendation is now capped by what is actually there, keeping 5G back, and refuses rather than shrinking to something useless. fallocate was assumed to work. It produces a file that btrfs and zfs will not swap on, so dd is the fallback — slow, but it always works. swappiness was written by sed'ing /etc/sysctl.conf in place, tangling it with whatever else lives there. It is a drop-in at /etc/sysctl.d now, so what this script set is visible as its own file. Role-dependent, which is the first use of MACHINE_ROLE: swappiness 10 on a server, where swapping is the emergency valve and a page fault on a request path is latency somebody is waiting for; the kernel default of 60 on dev, where swapping out an application nobody has touched in an hour is exactly what you want. WSL is left alone entirely — WSL2 runs its own managed swap inside the VM, and a swapfile written here is wasted disk the kernel will not use. Sizes are reported rounded rather than floored. A 4 GiB swapfile is 4194300 kB, which floors to 3 and reads as though a gigabyte went missing. Free disk stays floored, deliberately: it decides how much to allocate, and rounding up invents space. Verified on this host — 4G RAM, 4G existing swap correctly detected and left alone — and with the disk check stubbed at 6G free (caps to 1G) and 3G free (refuses). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
221 lines
8.5 KiB
Bash
221 lines
8.5 KiB
Bash
#!/bin/bash
|
|
# =============================================================================
|
|
# machine-setup — system configuration
|
|
# =============================================================================
|
|
#
|
|
# Definitions only, like the other lib/ files. Locale, and the system-level
|
|
# settings that follow it.
|
|
|
|
[[ -n "${MACHINE_SETUP_SYSTEM_LOADED:-}" ]] && return 0
|
|
MACHINE_SETUP_SYSTEM_LOADED=1
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Locale
|
|
# -----------------------------------------------------------------------------
|
|
#
|
|
# Two separate facts, and the original only handled one of them:
|
|
#
|
|
# what a new login shell is told to use — LANG in /etc/default/locale
|
|
# whether that locale actually exists — whether it has been generated
|
|
#
|
|
# Setting LANG to a locale that was never generated is the state that produces
|
|
# "setlocale: LC_ALL: cannot change locale" on every ssh login and every perl
|
|
# invocation. Both are checked, so the step can say which one is missing.
|
|
|
|
# What a new login shell will be handed, or empty if nothing is configured.
|
|
locale_current() {
|
|
if [[ -r /etc/default/locale ]]; then
|
|
awk -F= '/^LANG=/ { gsub(/"/, "", $2); print $2 }' /etc/default/locale
|
|
elif [[ -r /etc/locale.conf ]]; then
|
|
awk -F= '/^LANG=/ { gsub(/"/, "", $2); print $2 }' /etc/locale.conf
|
|
fi
|
|
}
|
|
|
|
# Has this locale actually been built?
|
|
#
|
|
# `locale -a` prints en_US.utf8 where the configuration spells it en_US.UTF-8,
|
|
# so both sides are folded to lower case with the dashes removed before
|
|
# comparing. A literal match here would report a perfectly good locale missing.
|
|
locale_is_generated() {
|
|
local want="${1,,}"
|
|
want="${want//-/}"
|
|
locale -a 2>/dev/null | tr '[:upper:]' '[:lower:]' | tr -d '-' | grep -qx "$want"
|
|
}
|
|
|
|
locale_set() {
|
|
local want="$1"
|
|
local escaped="${want//./\\.}"
|
|
|
|
case "$PM" in
|
|
apt)
|
|
# locale-gen comes from the `locales` package, which minimal images and
|
|
# most cloud base images do not ship. Without this the step fails with
|
|
# "locale-gen: command not found" halfway through.
|
|
if ! pkg_is_installed locales; then
|
|
info " installing locales, which provides locale-gen"
|
|
pkg_install_now locales
|
|
fi
|
|
|
|
# Uncomment it if it is there commented out, add it if it is absent.
|
|
# Editing the file rather than passing the name to locale-gen is what makes
|
|
# it survive: a locale generated by argument alone is lost the next time
|
|
# anything regenerates from /etc/locale.gen.
|
|
if grep -qE "^#[[:space:]]*${escaped}[[:space:]]" /etc/locale.gen 2>/dev/null; then
|
|
sed -i "s/^#[[:space:]]*\(${escaped}[[:space:]]\)/\1/" /etc/locale.gen
|
|
elif ! grep -qE "^${escaped}[[:space:]]" /etc/locale.gen 2>/dev/null; then
|
|
# The charset is the part after the dot: en_US.UTF-8 -> UTF-8
|
|
echo "${want} ${want##*.}" >>/etc/locale.gen
|
|
fi
|
|
|
|
locale-gen
|
|
update-locale LANG="$want"
|
|
;;
|
|
pacman)
|
|
if grep -qE "^#[[:space:]]*${escaped}[[:space:]]" /etc/locale.gen 2>/dev/null; then
|
|
sed -i "s/^#[[:space:]]*\(${escaped}[[:space:]]\)/\1/" /etc/locale.gen
|
|
fi
|
|
locale-gen
|
|
echo "LANG=${want}" >/etc/locale.conf
|
|
;;
|
|
dnf)
|
|
# No locale.gen here — the locales come prebuilt in langpack packages.
|
|
pkg_install_now "glibc-langpack-${want%%_*}"
|
|
localectl set-locale "LANG=${want}"
|
|
;;
|
|
brew)
|
|
warn "macOS has no system locale to set — it is per-user, from the terminal's settings"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Swap
|
|
# -----------------------------------------------------------------------------
|
|
|
|
SWAPFILE=/swapfile
|
|
|
|
# Rounded to nearest, not floored: a 4 GiB swapfile is 4194300 kB, which floors
|
|
# to 3 and reads as though a gigabyte went missing. Same for RAM, where 3.7 GiB
|
|
# reporting as "3G" makes the sizing tiers look wrong.
|
|
kb_to_gb_rounded() { echo $((($1 + 524288) / 1048576)); }
|
|
|
|
# Total active swap in GiB, 0 if there is none.
|
|
#
|
|
# From /proc/meminfo rather than by grepping swapon's output for a slash, which
|
|
# is what the original did to spot a swap FILE — that test reports no swap at all
|
|
# on a machine using zram or a swap partition, and the step would then add a
|
|
# swapfile beside perfectly good swap.
|
|
swap_active_gb() { kb_to_gb_rounded "$(awk '/^SwapTotal:/ { print $2 }' /proc/meminfo)"; }
|
|
|
|
ram_gb() { kb_to_gb_rounded "$(awk '/^MemTotal:/ { print $2 }' /proc/meminfo)"; }
|
|
|
|
# Free space on the filesystem that would hold the swapfile, in GiB. Floored
|
|
# rather than rounded, deliberately: this one decides how much to allocate, and
|
|
# rounding up invents space that is not there.
|
|
disk_free_gb() { echo $(($(df -Pk "$(dirname "$SWAPFILE")" | awk 'NR == 2 { print $4 }') / 1024 / 1024)); }
|
|
|
|
# How much swap this machine should have.
|
|
#
|
|
# The tiers are the original's. What is new is that the answer is capped by what
|
|
# is actually on the disk — the original would try to fallocate 8G on a VPS with
|
|
# 4G free, fail, and take the run down with it.
|
|
swap_recommended_gb() {
|
|
local ram size
|
|
ram="$(ram_gb)"
|
|
if ((ram <= 2)); then
|
|
size=2
|
|
elif ((ram <= 8)); then
|
|
size=4
|
|
else
|
|
size=8
|
|
fi
|
|
|
|
# Leave a few gigabytes behind. A swapfile that fills the disk is a worse
|
|
# problem than no swapfile.
|
|
local room=$(($(disk_free_gb) - 5))
|
|
((room < size)) && size="$room"
|
|
((size < 1)) && size=0
|
|
echo "$size"
|
|
}
|
|
|
|
# How eagerly the kernel swaps, by role.
|
|
#
|
|
# 10 on a server: swapping is the emergency valve, not a routine, and the cost of
|
|
# a page fault on a request path is latency somebody is waiting for. A desktop is
|
|
# the opposite case — swapping out an application nobody has touched in an hour
|
|
# is exactly what you want — so dev keeps the kernel default of 60.
|
|
swappiness_for_role() { if is_server; then echo 10; else echo 60; fi; }
|
|
|
|
swap_create() {
|
|
local gb="$1"
|
|
|
|
# fallocate is instant but produces a file some filesystems refuse to swap on
|
|
# (btrfs without the right attributes, zfs at all). dd is slow and always
|
|
# works, so it is the fallback rather than the default.
|
|
if ! fallocate -l "${gb}G" "$SWAPFILE" 2>/dev/null; then
|
|
info " fallocate is not usable here — writing the file with dd, which is slower"
|
|
dd if=/dev/zero of="$SWAPFILE" bs=1M count=$((gb * 1024)) status=none
|
|
fi
|
|
|
|
chmod 600 "$SWAPFILE"
|
|
mkswap "$SWAPFILE" >/dev/null
|
|
swapon "$SWAPFILE"
|
|
|
|
grep -qs "^${SWAPFILE}[[:space:]]" /etc/fstab || echo "${SWAPFILE} none swap sw 0 0" >>/etc/fstab
|
|
}
|
|
|
|
# Written as a drop-in rather than by rewriting /etc/sysctl.conf in place. The
|
|
# original sed'd that file, which means the setting is tangled up with whatever
|
|
# else lives there and is invisible to anyone looking for what this script did.
|
|
swappiness_set() {
|
|
echo "vm.swappiness=$1" >/etc/sysctl.d/99-machine-setup-swappiness.conf
|
|
sysctl -q -w "vm.swappiness=$1"
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Timezone
|
|
# -----------------------------------------------------------------------------
|
|
|
|
# The shortlist offered at the prompt. Any zone name can be typed instead, so
|
|
# this is a convenience rather than a limit.
|
|
TZ_OPTIONS=(UTC Europe/Lisbon Europe/London Europe/Berlin Europe/Stockholm US/Eastern US/Pacific Asia/Tokyo)
|
|
|
|
# What the machine is set to now.
|
|
#
|
|
# Three sources because they disagree about availability rather than about the
|
|
# answer: timedatectl is absent without systemd (containers, WSL), /etc/timezone
|
|
# is Debian-specific, and the /etc/localtime symlink is the one thing that is
|
|
# always true when any of them are.
|
|
timezone_current() {
|
|
if command -v timedatectl &>/dev/null && timedatectl show -p Timezone --value 2>/dev/null | grep -q .; then
|
|
timedatectl show -p Timezone --value 2>/dev/null
|
|
elif [[ -r /etc/timezone ]]; then
|
|
tr -d '[:space:]' </etc/timezone
|
|
elif [[ -L /etc/localtime ]]; then
|
|
readlink -f /etc/localtime | sed 's|.*/zoneinfo/||'
|
|
fi
|
|
}
|
|
|
|
# Checked against the zoneinfo database before it is used. `timedatectl
|
|
# set-timezone` on a name that does not exist fails, and under `set -e` that
|
|
# takes the whole run down over a typo.
|
|
timezone_is_valid() { [[ -f "/usr/share/zoneinfo/$1" ]]; }
|
|
|
|
timezone_set() {
|
|
local tz="$1"
|
|
case "$PM" in
|
|
brew) systemsetup -settimezone "$tz" >/dev/null ;;
|
|
*)
|
|
# timedatectl where there is a systemd to talk to; the files directly
|
|
# otherwise, which is the same thing it would have written.
|
|
if command -v timedatectl &>/dev/null && [[ "$IS_WSL" != true ]]; then
|
|
timedatectl set-timezone "$tz"
|
|
else
|
|
ln -sf "/usr/share/zoneinfo/${tz}" /etc/localtime
|
|
echo "$tz" >/etc/timezone
|
|
fi
|
|
;;
|
|
esac
|
|
}
|