From 52998d4aa190385f52eaefcf02733f634ebfdc06 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah Date: Wed, 8 Jul 2026 16:35:21 -0700 Subject: [PATCH] Detect mixed-architecture Base runtime chains --- CHANGELOG.md | 3 + .../commands/basectl/subcommands/check.sh | 1 + .../commands/basectl/subcommands/doctor.sh | 6 + .../commands/basectl/subcommands/setup.sh | 1 + .../basectl/subcommands/setup_common.sh | 176 ++++++++++++++++++ cli/bash/commands/basectl/tests/setup.bats | 79 ++++++++ 6 files changed, 266 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea411964..aca8ea4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,9 @@ and Base versions are tracked in the repo-root `VERSION` file. ### Fixed +- Reported `basectl setup`, `check`, and `doctor` runtime chain diagnostics up + front and rejected x86_64 Base virtual environments under Apple Silicon + Homebrew before profile setup reaches later `brew install` steps. - Hardened Ubuntu GitHub Actions package installs against hosted-runner third-party apt source failures before Base test jobs install their tools. diff --git a/cli/bash/commands/basectl/subcommands/check.sh b/cli/bash/commands/basectl/subcommands/check.sh index 5626fefe..d38b6fb0 100644 --- a/cli/bash/commands/basectl/subcommands/check.sh +++ b/cli/bash/commands/basectl/subcommands/check.sh @@ -135,6 +135,7 @@ base_check_subcommand_main() { if [[ "$output_format" == json ]]; then BASE_SETUP_XCODE_HOMEBREW_DIAGNOSTICS=true setup_run_check_json "$remote_network" else + setup_print_runtime_chain_summary BASE_SETUP_XCODE_HOMEBREW_DIAGNOSTICS=true setup_run_check fi } diff --git a/cli/bash/commands/basectl/subcommands/doctor.sh b/cli/bash/commands/basectl/subcommands/doctor.sh index 8a0ba12f..d1086ace 100644 --- a/cli/bash/commands/basectl/subcommands/doctor.sh +++ b/cli/bash/commands/basectl/subcommands/doctor.sh @@ -104,6 +104,9 @@ base_doctor_run_ci_runtime_text() { printf 'Base CI doctor\n\n' fi + setup_print_runtime_chain_summary + printf '\n' + base_doctor_print_collected_check_results if setup_profiles_enabled; then setup_run_base_dev_layer doctor @@ -274,6 +277,9 @@ base_doctor_subcommand_main() { printf 'Base doctor\n\n' fi + setup_print_runtime_chain_summary + printf '\n' + BASE_SETUP_XCODE_HOMEBREW_DIAGNOSTICS=true setup_collect_base_check_results warn || true errors="$(base_doctor_count_check_errors)" base_doctor_print_collected_check_results diff --git a/cli/bash/commands/basectl/subcommands/setup.sh b/cli/bash/commands/basectl/subcommands/setup.sh index 858720de..acecd113 100644 --- a/cli/bash/commands/basectl/subcommands/setup.sh +++ b/cli/bash/commands/basectl/subcommands/setup.sh @@ -90,6 +90,7 @@ base_setup_run_text() { if setup_notifications_enabled; then trap 'setup_notify_completion "$?"' EXIT fi + setup_print_runtime_chain_summary setup_run_install exit_code=$? if setup_notifications_enabled; then diff --git a/cli/bash/commands/basectl/subcommands/setup_common.sh b/cli/bash/commands/basectl/subcommands/setup_common.sh index 7e459824..cbe0741c 100644 --- a/cli/bash/commands/basectl/subcommands/setup_common.sh +++ b/cli/bash/commands/basectl/subcommands/setup_common.sh @@ -321,6 +321,47 @@ setup_virtualenv_home_has_python() { return 1 } +setup_python_machine() { + local machine python_bin="$1" + + [[ -x "$python_bin" ]] || return 1 + machine="$("$python_bin" -c 'import platform; print(platform.machine() or "unknown")' 2>/dev/null || true)" + [[ -n "$machine" ]] || return 1 + printf '%s\n' "$machine" +} + +setup_virtualenv_homebrew_architecture_compatible() { + local executable_path home_path homebrew_prefix pyvenv_cfg python_bin python_machine venv_dir="$1" + + [[ "$(setup_current_platform)" == macos ]] || return 0 + + homebrew_prefix="$(setup_homebrew_prefix 2>/dev/null || true)" + [[ "$homebrew_prefix" == "/opt/homebrew" ]] || return 0 + + pyvenv_cfg="$venv_dir/pyvenv.cfg" + python_bin="$venv_dir/bin/python" + python_machine="$(setup_python_machine "$python_bin" || true)" + executable_path="$(setup_pyvenv_cfg_value executable "$pyvenv_cfg" || true)" + home_path="$(setup_pyvenv_cfg_value home "$pyvenv_cfg" || true)" + + if [[ "$python_machine" == "x86_64" ]]; then + _BASE_SETUP_VENV_HEALTH_MESSAGE="Base virtual environment Python is x86_64 but Homebrew prefix is '$homebrew_prefix'." + return 1 + fi + + if [[ "$executable_path" == /usr/local/* ]]; then + _BASE_SETUP_VENV_HEALTH_MESSAGE="Base virtual environment Python executable '$executable_path' is under /usr/local but Homebrew prefix is '$homebrew_prefix'." + return 1 + fi + + if [[ "$home_path" == /usr/local/* ]]; then + _BASE_SETUP_VENV_HEALTH_MESSAGE="Base virtual environment Python home '$home_path' is under /usr/local but Homebrew prefix is '$homebrew_prefix'." + return 1 + fi + + return 0 +} + setup_virtualenv_healthy_path() { local executable_path home_path pyvenv_cfg python_bin venv_dir="$1" @@ -355,6 +396,8 @@ setup_virtualenv_healthy_path() { return 1 fi + setup_virtualenv_homebrew_architecture_compatible "$venv_dir" || return 1 + _BASE_SETUP_VENV_HEALTH_MESSAGE="Virtual environment is healthy at '$venv_dir'." return 0 } @@ -630,6 +673,137 @@ setup_find_brew_bin() { return 1 } +setup_homebrew_prefix() { + local brew_bin prefix + + if [[ -n "${BASE_SETUP_TEST_HOMEBREW_PREFIX+x}" ]]; then + setup_reject_test_hook_if_disallowed BASE_SETUP_TEST_HOMEBREW_PREFIX + [[ -n "$BASE_SETUP_TEST_HOMEBREW_PREFIX" ]] || return 1 + printf '%s\n' "$BASE_SETUP_TEST_HOMEBREW_PREFIX" + return 0 + fi + + if [[ -n "${HOMEBREW_PREFIX:-}" ]]; then + printf '%s\n' "$HOMEBREW_PREFIX" + return 0 + fi + + brew_bin="$(setup_find_brew_bin)" || return 1 + case "$brew_bin" in + /opt/homebrew/bin/brew) + printf '%s\n' "/opt/homebrew" + return 0 + ;; + /usr/local/bin/brew) + printf '%s\n' "/usr/local" + return 0 + ;; + esac + + prefix="$("$brew_bin" --prefix 2>/dev/null || true)" + [[ -n "$prefix" ]] || return 1 + printf '%s\n' "$prefix" +} + +setup_command_path() { + command -v "$1" 2>/dev/null || return 1 +} + +setup_current_machine() { + uname -m 2>/dev/null || printf 'unknown\n' +} + +setup_executable_architecture() { + local output path="$1" + + [[ -n "$path" ]] || return 1 + command -v file >/dev/null 2>&1 || return 1 + output="$(file -L "$path" 2>/dev/null || true)" + case "$output" in + *x86_64*arm64*|*arm64*x86_64*) + printf '%s\n' "universal" + ;; + *arm64*) + printf '%s\n' "arm64" + ;; + *x86_64*) + printf '%s\n' "x86_64" + ;; + *) + return 1 + ;; + esac +} + +setup_rosetta_translation_state() { + local translated + + [[ "$(setup_current_platform)" == macos ]] || { + printf '%s\n' "n/a" + return 0 + } + + translated="$(sysctl -in sysctl.proc_translated 2>/dev/null || true)" + case "$translated" in + 1) + printf '%s\n' "yes" + ;; + 0) + printf '%s\n' "no" + ;; + *) + printf '%s\n' "unknown" + ;; + esac +} + +setup_gh_version_line() { + local version + + version="$(gh --version 2>/dev/null | head -n 1)" || return 1 + [[ -n "$version" ]] || return 1 + printf '%s\n' "$version" +} + +setup_print_runtime_chain_summary() { + local brew_bin brew_prefix gh_arch gh_bin gh_version home_path platform pyvenv_cfg python_arch python_bin shell_machine shell_path translated venv_dir + + platform="$(setup_current_platform 2>/dev/null || printf 'unsupported\n')" + shell_path="${BASH:-}" + [[ -n "$shell_path" ]] || shell_path="$(setup_command_path bash 2>/dev/null || true)" + shell_machine="$(setup_current_machine)" + translated="$(setup_rosetta_translation_state)" + venv_dir="$(setup_venv_dir)" + python_bin="$venv_dir/bin/python" + + log_info "Runtime chain: BASE_OS=${BASE_OS:-unknown} BASE_PLATFORM=$platform BASE_HOST=${BASE_HOST:-unknown}" + log_info "Shell: path=${shell_path:-unknown} version=${BASH_VERSION:-unknown} machine=${shell_machine:-unknown} translated=${translated:-unknown}" + + if [[ -x "$python_bin" ]]; then + pyvenv_cfg="$venv_dir/pyvenv.cfg" + python_arch="$(setup_python_machine "$python_bin" 2>/dev/null || setup_executable_architecture "$python_bin" 2>/dev/null || true)" + home_path="$(setup_pyvenv_cfg_value home "$pyvenv_cfg" 2>/dev/null || true)" + log_info "Python: path=$python_bin machine=${python_arch:-unknown} home=${home_path:-unknown}" + else + log_info "Python: path=$python_bin status=missing" + fi + + if [[ "$platform" == macos ]]; then + if brew_bin="$(setup_find_brew_bin 2>/dev/null)"; then + brew_prefix="$(setup_homebrew_prefix 2>/dev/null || true)" + log_info "Homebrew: path=$brew_bin prefix=${brew_prefix:-unknown}" + else + log_info "Homebrew: status=missing" + fi + fi + + if gh_bin="$(setup_command_path gh 2>/dev/null)"; then + gh_version="$(setup_gh_version_line 2>/dev/null || true)" + gh_arch="$(setup_executable_architecture "$gh_bin" 2>/dev/null || true)" + log_info "GitHub CLI: path=$gh_bin version=${gh_version:-unknown} arch=${gh_arch:-unknown}" + fi +} + setup_refresh_brew_path() { local brew_bin @@ -1036,6 +1210,8 @@ setup_create_virtualenv() { venv_dir="$_BASE_SETUP_VENV_DIR_CACHE" if setup_virtualenv_exists && ! setup_base_recreate_venv_enabled; then + setup_virtualenv_healthy || + fatal_error "$_BASE_SETUP_VENV_HEALTH_MESSAGE $(setup_recovery_venv)" log_info "Virtual environment already exists at '$venv_dir'." return 0 fi diff --git a/cli/bash/commands/basectl/tests/setup.bats b/cli/bash/commands/basectl/tests/setup.bats index b9b7fa11..ae9c129a 100644 --- a/cli/bash/commands/basectl/tests/setup.bats +++ b/cli/bash/commands/basectl/tests/setup.bats @@ -351,6 +351,85 @@ EOF [ -f "$TEST_STATE_DIR/project-setup-ran" ] } +@test "basectl setup prints runtime chain summary" { + create_brew_stub + create_xcode_stubs + touch "$TEST_STATE_DIR/python-installed" + touch "$TEST_STATE_DIR/xcode-installed" + + run_base_command setup --dry-run + + [ "$status" -eq 0 ] + [[ "$output" == *"Runtime chain:"* ]] + [[ "$output" == *"BASE_OS=macos"* ]] + [[ "$output" == *"BASE_PLATFORM=macos"* ]] + [[ "$output" == *"Shell:"* ]] + [[ "$output" == *"Homebrew:"* ]] +} + +@test "basectl setup rejects x86_64 Base virtualenv under ARM Homebrew before dev profile setup" { + local python_executable + local python_home + local venv_dir="$TEST_HOME/.base.d/base/.venv" + + python_home="$TEST_TMPDIR/python-home" + python_executable="$python_home/python3.13" + create_brew_stub + create_xcode_stubs + mkdir -p "$python_home" + mkdir -p "$venv_dir/bin" + touch "$TEST_STATE_DIR/python-installed" + touch "$TEST_STATE_DIR/xcode-installed" + touch "$TEST_STATE_DIR/pyyaml-installed" + touch "$TEST_STATE_DIR/click-installed" + printf '#!/usr/bin/env bash\n' > "$venv_dir/bin/activate" + printf '#!/usr/bin/env bash\nexit 0\n' > "$python_executable" + chmod +x "$python_executable" + cat > "$venv_dir/pyvenv.cfg" < "$venv_dir/bin/python" <<'EOF' +#!/usr/bin/env bash +if [[ "${1:-}" == "--version" ]]; then + printf 'Python 3.13.test\n' + exit 0 +fi +if [[ "${1:-}" == "-c" ]]; then + printf 'x86_64\n' + exit 0 +fi +if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "show" ]]; then + exit 0 +fi +if [[ "${1:-}" == "-m" && "${2:-}" == "base_setup" ]]; then + if [[ "$*" == *"--action route"* ]]; then + printf 'base\t%s\t%s\t%s\tfalse\n' "$BASE_HOME" "$BASE_HOME/base_manifest.yaml" "$HOME/.base.d/base/.venv" + exit 0 + fi + touch "${BASE_SETUP_TEST_STATE_DIR:?}/project-setup-ran" + exit 0 +fi +if [[ "${1:-}" == "-m" && "${2:-}" == "base_dev" ]]; then + touch "${BASE_SETUP_TEST_STATE_DIR:?}/dev-setup-ran" + exit 0 +fi +printf 'unexpected stale venv python args: %s\n' "$*" >&2 +exit 1 +EOF + chmod +x "$venv_dir/bin/python" + + run_base_command \ + BASE_SETUP_TEST_PLATFORM=macos \ + BASE_SETUP_TEST_HOMEBREW_PREFIX=/opt/homebrew \ + setup --profile dev + + [ "$status" -eq 1 ] + [[ "$output" == *"Base virtual environment Python is x86_64 but Homebrew prefix is '/opt/homebrew'."* ]] + [[ "$output" == *"Run 'basectl setup --recreate-venv' to back up and recreate the Base virtual environment."* ]] + [ ! -f "$TEST_STATE_DIR/dev-setup-ran" ] +} + @test "basectl setup base ignores inherited project virtualenv" { local inherited_venv="$TEST_TMPDIR/inherited-base-venv" local venv_dir="$TEST_HOME/.base.d/base/.venv"