Files
platform/scripts/setup/machine-setup/lib/files.sh
T
pastilhasandClaude Opus 5 e120dfa36e full read: fix the set -e footguns a full run would have hit
Read the whole thing — 2392 lines of entry point and 2700 of libraries — looking
for what shellcheck cannot see. shellcheck itself is clean at error level; its
warnings are cross-file false positives and one deliberate tilde in a display
string. Everything below is a real defect.

── The Git section aborted on any machine where git was not already configured ──

`git config --global --get <key>` exits NON-ZERO when the key is simply unset,
and `VAR="$(git_get …)"` propagates that under `set -e`. So on a fresh machine —
the case this script exists for — the section died at its first assignment,
before printing anything, and took the remaining nine sections with it.

It passed every earlier test because those harnesses sourced the section under a
`bash -c` with no `set -e`. Verified now against a genuinely fresh account with
the real script: the section completes and writes a correct .gitconfig.

── An optional step failing aborted the whole run ──

Twelve functions ended on a command that can fail — `systemctl enable --now
earlyoom`, `systemctl restart systemd-logind`, `chsh`, `sysctl -w`, `chown -R`,
the oh-my-zsh installer, and others. Called as plain commands under `set -e`, any
one of them failing ends the script, so a masked unit or a container without
systemd would abort a 28-section run over an optional improvement.

They now return 0 explicitly and the callers verify the outcome instead — which
also fixed a lie: the sleep section printed "sleep disabled, logind reloaded"
whether or not the restart had worked. It now checks the targets and the logind
values and reports honestly.

── chown user:user assumed the primary group is named after the user ──

True on Debian and Ubuntu, which create a group per user. Not true for an account
from LDAP, or made with `useradd -g users`, or on an image with a shared group —
there `install -g <user>` fails with "invalid group" and the step aborts. Proved
it against an account whose primary group is `oddgroup`: the old form fails, the
new one gets ownership right. Eight call sites now ask `id -gn`.

── Also hardened ──

agent_path and current_editor gained `|| true` for the same reason git_get needed
it: "nothing is set" is an answer, not a failure.

Verified afterwards: shellcheck clean at error level, every section runs
standalone without aborting, and the two apparent failures in that sweep are
correct behaviour — Timezone and Git refusing an empty answer from /dev/null.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 21:34:20 +00:00

128 lines
4.5 KiB
Bash

#!/bin/bash
# =============================================================================
# machine-setup — writing files into somebody's home
# =============================================================================
#
# Definitions only, like the other lib/ files.
#
# ── The rule ──
#
# A setup script may create a config file. It may not silently replace one the
# user wrote. The original did the second: `cp .tmux.conf $USER_HOME/` on every
# run, over whatever was there, and five separate `cat >>` into .zshrc with no
# guard — so a second pass duplicated the starship init, the nvim PATH, bun, deno
# and the aliases.
#
# Both of those are the same mistake in different shapes: writing without looking
# first. The two helpers here are the two safe shapes.
[[ -n "${MACHINE_SETUP_FILES_LOADED:-}" ]] && return 0
MACHINE_SETUP_FILES_LOADED=1
# Put a config file in place, asking before it replaces one the user has.
#
# Three outcomes, and the caller can tell them apart by the return code:
#
# 0 installed — there was nothing there, or the user chose to replace
# 1 identical — already exactly this, nothing done
# 2 kept — the user chose to keep theirs
#
# When the file exists and differs, this ASKS rather than deciding. Silently
# keeping theirs is safe but unhelpful — they never learn that a newer version
# exists — and silently replacing it is how a setup script eats somebody's
#configuration. So: keep, replace, or show the difference first, and a replaced file is
# always kept beside the new one.
#
# NOTE for callers: 1 and 2 are outcomes, not failures — but they are still
# non-zero, so calling this as a plain command under `set -e` ends the script
# before the result can be read. Always capture it:
#
# install_config "$src" "$dest" "$user" && rc=0 || rc=$?
install_config() {
local src="$1" dest="$2" owner="$3" answer
if [[ ! -f "$dest" ]]; then
install -D -m 0644 -o "$owner" -g "$(user_group "$owner")" "$src" "$dest"
return 0
fi
cmp -s "$src" "$dest" && return 1
echo ""
warn "${dest} already exists here, and differs from the one this script ships."
# Never replace a file the user has without being told to. An unattended run
# answers "keep", because the alternative is destroying configuration nobody
# was present to defend.
if [[ "${ASSUME_YES:-}" == "1" ]] || [[ ! -t 0 ]]; then
echo " keeping yours (nothing was asked, so nothing is replaced)"
return 2
fi
while true; do
echo " [1] keep yours — nothing changes"
echo " [2] use ours — yours is kept as ${dest}.before-machine-setup"
echo " [3] show me the difference first"
echo ""
if ! read -rp " Which one? (1/2/3) [1]: " answer; then
echo ""
echo " keeping yours"
return 2
fi
case "${answer:-1}" in
1)
echo " keeping yours"
return 2
;;
2)
cp -a "$dest" "${dest}.before-machine-setup"
install -D -m 0644 -o "$owner" -g "$(user_group "$owner")" "$src" "$dest"
ok "replaced — yours is at ${dest}.before-machine-setup"
return 0
;;
3)
echo ""
# yours on the left, ours on the right: - is what you would lose,
# + is what you would gain.
diff -u --label "yours: ${dest}" --label "ours: ${src}" "$dest" "$src" | page
echo ""
;;
*) warn "Pick 1, 2 or 3." ;;
esac
done
}
# Append a block to a file exactly once.
#
# The block is wrapped in markers naming what it is, so a second run recognises
# its own work instead of adding it again — and so a human reading the file can
# see which lines came from here and delete them as a unit.
#
# append_once ~/.zshrc bun <<'EOF'
# export PATH="$HOME/.bun/bin:$PATH"
# EOF
#
# Returns 0 if it wrote, 1 if the block was already there.
#
# One limitation, and it bites the author rather than the user: RENAMING a marker
# orphans the block that used the old name. append_once only recognises the name
# it is given, so the previous block stays in the file doing whatever it did.
# Changing a block's CONTENT has the same shape — the marker is found, so the new
# content is never written. Both need the old block removed by hand.
append_once() {
local file="$1" name="$2"
local begin="# >>> machine-setup: ${name} >>>"
local end="# <<< machine-setup: ${name} <<<"
if [[ -f "$file" ]] && grep -qF "$begin" "$file"; then
return 1
fi
{
echo ""
echo "$begin"
cat
echo "$end"
} >>"$file"
}