make machine-setup.sh and officer-setup.sh actually run on macOS

Both scripts had real macOS branches already — MACOS_SKIP, Xcode CLT, Docker Desktop
detection — but neither had ever run past its own library load on a real Mac, and
verifying that live is what this commit is. This machine is the Mac.

The root cause was the interpreter, not missing OS coverage: macOS's stock /bin/bash is
3.2.57 (Apple stopped shipping newer once bash went GPLv3) and this machine has no newer
one installed. Both scripts used bash 4+/4.3+ syntax with zero version guard, so a real
run died in seconds — declare -A (base.sh, associative array), local -n (packages.sh,
nameref), mapfile (5 sites), ${var,,} (both scripts' own step() — confirmed --only killed
the ENTIRE run), and machine-setup.sh's own pre-flight hard-refused brew outright ("this
script does not implement yet") even once the bash-4 crash was fixed. Each got rewritten
to its bash-3.2-safe equivalent rather than requiring a newer bash and re-executing under
it — nine call sites, a bounded fix, and one less thing that can go wrong before the
script can report what's wrong.

Past that gate, a live probe run (real account/path prompts, killed before Database/
Services could touch this machine's actual state) reproduced a second bug in the first
few seconds: resolve_user_home used getent (Linux/glibc, absent on macOS) with a
hardcoded /home/<user> fallback, so Layout tried `install -d /home/pastilhas` and failed
with "Operation not supported". Fixed with the portable `eval echo ~user` tilde-expansion
idiom, re-ran the same probe, and it correctly created /Users/pastilhas/officerdev/ and
started a real git clone.

Everything else follows the same shape — an unguarded Linux-only tool with a portable or
brew-based macOS equivalent:

  - grep -oP (4 sites: both --list flags, env.sh's tailnet lookup, build.sh's schema
    table count) — BSD grep has no -P at all. Portable ERE+sed where regex was enough;
    jq where the input was already JSON (tailnet lookup) — jq is already an unconditional
    dependency elsewhere in this script, so that's a correctness fix, not just a port.
  - ss (postgres.sh, proxy.sh — is anything listening on this port, and is it bound wide
    enough for NPM's container to reach) — both silently mis-reported on macOS, since the
    "command not found" was swallowed by 2>/dev/null and read as "nothing's listening".
    New shared `listening_addrs()` in officer-setup's base.sh, branching to lsof.
  - getent hosts (proxy.sh's DNS-wait loop) — same silent-failure shape, would have spun
    its full 30-minute timeout regardless of whether the record actually resolved. macOS
    branch uses dscacheutil.
  - stat -c (layout.sh's ownership check) — BSD stat has no -c, confirmed live ("illegal
    option -- c"), and the failure was VISIBLE and made every layout directory report as
    wrong-owner. Portable file_owner() tries -c then falls back to -f %Su.
  - apt-only Postgres client, NodeSource's Debian-only setup script, lazydocker/lazygit's
    installers (one literally named install_update_linux.sh, the other hardcoding
    "_Linux_" into the release asset URL) — each gets a brew branch, formula names
    verified against the live Homebrew catalog rather than assumed.
  - sudo -u "$USERNAME" to become the SAME account the process already is — real on
    Linux, where this whole script runs as root and dropping to the owner needs no
    password; on macOS the process never escalates in the first place, so re-invoking
    sudo for yourself asks for a real password on every call, or fails outright without
    sudo rights. machine-setup's dev.sh had ~10 inline call sites (git, bun, deno, claude,
    opencode, editor detection) rerouted through a new shared run_as_owner(); officer-
    setup's own as_owner() (already the shared entry point there) got the same branch,
    and six direct sudo call sites in build.sh/secrets.sh/services.sh that bypassed it
    were rerouted through it too.

Three things stay Linux-only rather than getting an unverified macOS implementation,
each skipped with a stated reason at the point it would otherwise run: pm2's boot-
persistence hook (needs a launchd plist generated and loaded, not a systemd unit
printed and eval'd — real, separate work), the Tailscale exit-node NIC/sysctl tuning
(no macOS equivalent concept, and advertising a laptop as an exit node is already the
edge case MACOS_SKIP treats it as), and ufw's bridge-to-host firewall rule (pf is a
different rule language entirely, and Docker Desktop's own VM-based networking may not
even have the problem this rule exists to solve — guessing at a firewall rule felt like
the wrong place to be wrong).

Verified: both --list flags print step names instead of a grep usage error. A bounded
officer-setup.sh run (killed before Database) resolved the real macOS home directory,
created the layout with correct ownership, and began a real git clone — cleaned up
after (rm -rf the test directory, the gitignored .setup-progress file, the stray
install-report). machine-setup.sh's pre-flight now reports "Packages: brew" and reaches
the account/path prompts instead of refusing the OS outright. bash -n clean on all 16
files under bash 3.2, the actual interpreter these scripts run under on a Mac.

Not run further live: Database, Secrets, Services and Proxy would mutate this machine's
real state (a live Postgres, a live set of pm2 processes this session runs through) and
were left for a deliberate, supervised run rather than folded into validation.
This commit is contained in:
2026-08-26 21:10:08 +01:00
parent 7e6cfc85af
commit 681df1c8bc
16 changed files with 337 additions and 70 deletions
+50 -2
View File
@@ -62,7 +62,7 @@ pg_client_installed() {
# entry, no add-apt-repository. Non-fatal — an install without psql is a working platform,
# just a more annoying one to operate.
install_pg_client() {
local major codename
local major
major="$(pg_client_major)"
[[ -n "$major" ]] || {
warn "could not read a major version out of PG_IMAGE=${PG_IMAGE} — skipping the client"
@@ -74,6 +74,49 @@ install_pg_client() {
return 0
fi
if [[ "$(uname -s)" == "Darwin" ]]; then
install_pg_client_macos "$major"
return $?
fi
install_pg_client_linux "$major"
}
# Homebrew's `postgresql@<major>` matches the apt path's own logic: the version has to agree
# with the server's, not just be "a psql". It is keg-only — Homebrew never links more than one
# Postgres onto PATH by default, because a dev machine plausibly has several majors installed
# for different projects — so `brew install` alone does not make `psql` resolve. Symlinking
# only the four client binaries this platform actually uses, rather than `brew link --force`ing
# the whole keg, is the same caveat Homebrew's own postgresql formula prints; force-linking
# would silently steal `psql` from whatever other major was linked before this ran.
install_pg_client_macos() {
local major="$1" prefix
command -v brew &>/dev/null || {
warn "Homebrew is not installed — cannot install postgresql@${major}. Run machine-setup.sh first."
return 1
}
prefix="$(brew --prefix)"
brew install "postgresql@${major}" &>/dev/null || {
warn "brew install postgresql@${major} failed"
return 1
}
local bin
for bin in psql pg_dump pg_dumpall pg_restore; do
[[ -x "${prefix}/opt/postgresql@${major}/bin/${bin}" ]] || continue
ln -sf "${prefix}/opt/postgresql@${major}/bin/${bin}" "${prefix}/bin/${bin}"
done
pg_client_installed || {
warn "psql is not version ${major} after installing — check: brew list postgresql@${major}"
return 1
}
ok "psql $(psql --version | grep -oE '[0-9]+\.[0-9]+' | head -1) installed via Homebrew"
}
install_pg_client_linux() {
local major="$1" codename
codename="$(. /etc/os-release && echo "${VERSION_CODENAME:-}")"
[[ -n "$codename" ]] || {
warn "could not work out this release's codename — cannot add the PostgreSQL repository"
@@ -121,7 +164,12 @@ pg_container_running() { docker ps --filter "name=^${PG_CONTAINER}$" --format '{
# Is something already answering on the port? A Postgres the user runs their own
# way is a perfectly good answer, and finding out by failing to bind is not.
pg_port_in_use() { ss -ltn 2>/dev/null | grep -qE "127\.0\.0\.1:${PG_PORT}\b|\*:${PG_PORT}\b|0\.0\.0\.0:${PG_PORT}\b"; }
#
# `ss` is Linux-only; `listening_addrs` (lib/base.sh) is the portable version, branching to
# `lsof` on macOS. Confirmed empirically to fail silently the old way: `ss` doesn't exist on
# macOS, its "command not found" was swallowed by `2>/dev/null`, and an empty grep result
# reported the port as free even when something WAS listening.
pg_port_in_use() { [[ -n "$(listening_addrs "$PG_PORT")" ]]; }
# Bound to loopback, deliberately, and the reason is worth keeping next to the
# line it explains.