diff --git a/docs/bootstrap.md b/docs/bootstrap.md index 337fbafe..e86a5a62 100644 --- a/docs/bootstrap.md +++ b/docs/bootstrap.md @@ -66,6 +66,106 @@ exec "$SHELL" -l integration remains an explicit `basectl update-profile` step so the user can see what was installed before Base changes future interactive shells. +## Inherited Or Migrated macOS Accounts + +An account restored from Time Machine, migrated from another Mac, or shared +with a previous owner can retain a stale shell profile and Homebrew state. On +Apple Silicon, the most common failure is an Intel Homebrew under +`/usr/local` being selected by a Rosetta-translated shell even though native +Homebrew is installed under `/opt/homebrew`. Homebrew's Ruby traceback may +appear before Base is mentioned, but the underlying problem is usually the +process architecture, prefix selection, Xcode license, or prefix ownership. + +Run this read-only diagnostic as the target user before retrying an install. It +inspects each known Homebrew path explicitly before selecting the compatible +prefix for the remainder of the current shell: + +```bash +machine="$(uname -m)" +translated="$(sysctl -in sysctl.proc_translated 2>/dev/null || printf '0')" +printf 'machine=%s\n' "$machine" +printf 'translated=%s\n' "$translated" +printf 'shell=%s\n' "${SHELL:-unknown}" +printf 'path=%s\n' "$PATH" + +for candidate in /opt/homebrew/bin/brew /usr/local/bin/brew; do + if [ -x "$candidate" ]; then + printf 'brew=%s\n' "$candidate" + file "$candidate" + "$candidate" --prefix 2>&1 || true + fi +done + +if [ "$machine" = "arm64" ] && [ "$translated" = "0" ]; then + export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH" +elif [ "$machine" = "x86_64" ] && [ "$translated" = "0" ]; then + export PATH="/usr/local/bin:/opt/homebrew/bin:$PATH" +fi + +if command -v brew >/dev/null 2>&1; then + printf 'selected-brew=%s\n' "$(command -v brew)" + brew --config 2>&1 || true +else + printf 'selected-brew=unresolved; stop before ownership checks\n' +fi +``` + +On Apple Silicon, a native terminal should report `machine=arm64` and +`translated=0`; the diagnostic puts `/opt/homebrew/bin` first for that shell. +If the process is Rosetta-translated (`machine=x86_64` and `translated=1`), +open a native terminal before continuing. On an Intel Mac, +`/usr/local/bin/brew` is the expected prefix. Do not select a prefix only +because it appears first on `PATH`; check the architecture and the explicit +candidate `--prefix` results together. + +Check the developer-tool boundary separately: + +```bash +/usr/bin/xcrun --find clang +/usr/bin/xcode-select --print-path +``` + +If either command reports that the Command Line Tools or Xcode license is +missing, complete Apple's interactive installation or license-acceptance flow +as the target user and rerun the read-only checks. Do not hide that prompt in a +non-interactive bootstrap pipeline. + +After selecting an architecture-compatible `brew`, inspect prefix ownership +and the lock directory before `brew install`: + +```bash +if command -v brew >/dev/null 2>&1; then + brew_prefix="$(brew --prefix 2>/dev/null || true)" + if [ -n "$brew_prefix" ]; then + ls -ld "$brew_prefix" "$brew_prefix/var" "$brew_prefix/var/homebrew" \ + "$brew_prefix/var/homebrew/locks" 2>/dev/null || true + test -w "$brew_prefix" && printf 'prefix-writable=yes\n' || printf 'prefix-writable=no\n' + test -w "$brew_prefix/var/homebrew/locks" && \ + printf 'locks-writable=yes\n' || printf 'locks-writable=no\n' + else + printf 'brew-prefix=unresolved; stop before ownership checks\n' + fi +else + printf 'brew=unresolved; stop before ownership checks\n' +fi +``` + +The prefix and its lock directory must be owned and writable for the account +that is running Base. If another macOS user owns them, stop and ask the owner +or your device administrator to repair the Homebrew installation. Base must +not automatically run `sudo chown`, recursively change ownership, delete lock +directories, or reset Homebrew state. A source-checkout install is the safer +alternative when the shared Homebrew prefix cannot be repaired or is not +owned by the target account; use the [source checkout install recipe](#source-checkout-install-recipe) +instead. + +Once the checks agree, follow the [canonical Homebrew install recipe](#homebrew-install-recipe). +The diagnostic has already placed the architecture-compatible prefix first on +`PATH` for the current native shell. If the architecture, prefix, ownership, +or Xcode checks do not agree, use the source checkout path or stop with the +collected read-only output; do not continue to the Homebrew install merely to +obtain a longer downstream traceback. + If `basectl` reports that the current Bash is too old, repair just that first: ```bash diff --git a/tests/test_bootstrap_docs.py b/tests/test_bootstrap_docs.py index 7bb12231..95bc31a8 100644 --- a/tests/test_bootstrap_docs.py +++ b/tests/test_bootstrap_docs.py @@ -50,6 +50,19 @@ def test_bootstrap_docs_explain_mutable_homebrew_default_rationale() -> None: assert "BASE_HOMEBREW_INSTALLER_SHA256" in normalized +def test_migrated_macos_recovery_uses_canonical_recipe_after_safe_selection() -> None: + text = BOOTSTRAP_DOC.read_text(encoding="utf-8") + recovery = section(text, "## Inherited Or Migrated macOS Accounts", "## Install Mode") + + assert "[canonical Homebrew install recipe](#homebrew-install-recipe)" in recovery + assert "brew trust basefoundry/base" not in recovery + assert "brew install basefoundry/base/base" not in recovery + assert "basectl setup --dry-run" not in recovery + path_export = 'export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"' + assert recovery.index(path_export) < recovery.index('brew_prefix="$(brew --prefix') + assert recovery.index(path_export) < recovery.index("brew --config") + + def test_readme_trust_conscious_proof_reviews_manifest_trust_before_demo() -> None: text = README.read_text(encoding="utf-8") proof = section(text, "### Trust-Conscious Proof, No Dotfile Changes", "## How Base Fits")