Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions docs/bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions tests/test_bootstrap_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading