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:
@@ -58,7 +58,9 @@ proxy_ask() {
|
||||
# reads like a firewall fault. So the bind ADDRESS is what gets checked.
|
||||
proxy_require_listening() {
|
||||
local port="$1" listen
|
||||
listen="$(ss -ltnH "sport = :$port" 2>/dev/null | awk '{print $4}')"
|
||||
# `ss` is Linux-only; `listening_addrs` (lib/base.sh) branches to `lsof` on macOS. Same
|
||||
# silent-empty-result failure mode as postgres.sh's pg_port_in_use if this were left as-is.
|
||||
listen="$(listening_addrs "$port" | head -1)"
|
||||
[[ -n "$listen" ]] || {
|
||||
warn "nothing is listening on port ${port} — start Officer first"
|
||||
return 1
|
||||
@@ -170,7 +172,15 @@ proxy_wait_for_dns() {
|
||||
info "(Tailnet: the certificate can issue without this, but the name will not resolve until it exists.)"
|
||||
|
||||
while ((elapsed < timeout)); do
|
||||
got="$(getent hosts "$domain" 2>/dev/null | awk '{print $1}' | head -1)"
|
||||
# `getent` is glibc/Linux-only — absent on macOS (confirmed: "command not found" on a
|
||||
# real machine), silently swallowed by 2>/dev/null, which would have spun this through
|
||||
# its full 30-minute timeout regardless of whether the record actually resolved.
|
||||
# `dscacheutil` is macOS's own equivalent, consulting the same system resolver.
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
got="$(dscacheutil -q host -a name "$domain" 2>/dev/null | awk '/^ip_address:/ {print $2; exit}')"
|
||||
else
|
||||
got="$(getent hosts "$domain" 2>/dev/null | awk '{print $1}' | head -1)"
|
||||
fi
|
||||
if [[ "$got" == "$want" ]]; then
|
||||
ok "${domain} resolves to ${want}"
|
||||
return 0
|
||||
@@ -194,8 +204,18 @@ proxy_ensure_network() {
|
||||
|
||||
# NPM binds its admin UI to the tailnet IP. If docker starts before tailscaled that
|
||||
# address does not exist yet and the WHOLE container fails to start, not just that port.
|
||||
#
|
||||
# Docker Desktop on macOS has no systemd unit to order — it is a GUI app the OS launches, not
|
||||
# a service tailscaled could be declared a dependency of — and this account does not run as
|
||||
# root there either, so `mkdir -p /etc/systemd/...` would fail on permissions rather than do
|
||||
# something merely unnecessary. Same shape as machine-setup's MACOS_SKIP: skip with a reason
|
||||
# rather than let a Linux-only action fail confusingly.
|
||||
proxy_order_docker_after_tailscaled() {
|
||||
[[ "$CHALLENGE" == "dns" ]] || return 0
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
ok "Docker Desktop has no systemd unit to order — skipping"
|
||||
return 0
|
||||
fi
|
||||
local f=/etc/systemd/system/docker.service.d/10-after-tailscaled.conf
|
||||
[[ -f "$f" ]] && return 0
|
||||
mkdir -p "$(dirname "$f")"
|
||||
@@ -378,8 +398,20 @@ proxy_get_token() {
|
||||
# bypass ufw entirely. The symptom is a 504 that looks like a network fault. A container
|
||||
# upstream would need none of this, which is why container upstreams are preferable when
|
||||
# there is a choice.
|
||||
# `ufw` is Debian/Ubuntu — no macOS equivalent through this interface (pf/pfctl is a
|
||||
# different rule syntax entirely, and the Application Firewall machine-setup's own
|
||||
# MACOS_SKIP already documents for "Firewall" is per-APP, not per-subnet/per-port). Skipped
|
||||
# rather than guessed at: Docker Desktop for Mac also runs containers inside its own VM, so
|
||||
# bridge-to-host traffic does not cross the real host firewall the same way it does when
|
||||
# Docker runs directly on a Linux kernel — an unverified pf rule here could be solving a
|
||||
# problem macOS's Docker networking model does not actually have.
|
||||
proxy_allow_bridge_to_host() {
|
||||
local port="$1" subnet
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
ok "no ufw on macOS — Docker Desktop's own networking model handles bridge-to-host, skipping"
|
||||
BRIDGE_GATEWAY="$(docker network inspect "$PROXY_NET" -f '{{(index .IPAM.Config 0).Gateway}}')"
|
||||
return 0
|
||||
fi
|
||||
subnet="$(docker network inspect "$PROXY_NET" -f '{{(index .IPAM.Config 0).Subnet}}')"
|
||||
if ufw status 2>/dev/null | grep -q "${port}.*${subnet%%/*}"; then
|
||||
ok "ufw already allows the bridge to reach port ${port}"
|
||||
|
||||
Reference in New Issue
Block a user