#!/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:]' /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 }