From 39e7a46c251906adab3279dea5a21cda7b125274 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah Date: Sun, 12 Jul 2026 11:18:54 -0700 Subject: [PATCH] Extract setup profiles helper --- .../basectl/subcommands/setup_common.sh | 124 +--------------- .../basectl/subcommands/setup_profiles.sh | 137 ++++++++++++++++++ .../commands/basectl/tests/setup-common.bats | 26 ++++ docs/setup-common-ownership.md | 53 ++++--- tests/test_setup_common_ownership_docs.py | 12 ++ 5 files changed, 209 insertions(+), 143 deletions(-) create mode 100644 cli/bash/commands/basectl/subcommands/setup_profiles.sh diff --git a/cli/bash/commands/basectl/subcommands/setup_common.sh b/cli/bash/commands/basectl/subcommands/setup_common.sh index 394087f8..ac839ff2 100644 --- a/cli/bash/commands/basectl/subcommands/setup_common.sh +++ b/cli/bash/commands/basectl/subcommands/setup_common.sh @@ -23,6 +23,7 @@ source "$BASE_HOME/cli/bash/commands/basectl/subcommands/setup_check_results.sh" source "$BASE_HOME/cli/bash/commands/basectl/subcommands/setup_linux_debian.sh" source "$BASE_HOME/cli/bash/commands/basectl/subcommands/setup_macos_homebrew.sh" source "$BASE_HOME/cli/bash/commands/basectl/subcommands/setup_venv.sh" +source "$BASE_HOME/cli/bash/commands/basectl/subcommands/setup_profiles.sh" _BASE_SETUP_VENV_DIR_CACHE="" _BASE_SETUP_PYTHONPATH_CACHE="" @@ -62,14 +63,6 @@ setup_enable_debug_logging() { export LOG_DEBUG=1 } -setup_supported_profiles() { - printf '%s\n' "dev sre ai linux-lab" -} - -setup_supported_profiles_display() { - printf '%s\n' "dev, sre, ai, linux-lab" -} - setup_epoch_seconds() { printf '%(%s)T\n' -1 } @@ -78,94 +71,6 @@ setup_backup_timestamp() { printf '%(%Y%m%dT%H%M%S)T\n' -1 } -setup_profile_supported() { - local profile="$1" - - case "$profile" in - dev|sre|ai|linux-lab) - return 0 - ;; - *) - return 1 - ;; - esac -} - -setup_normalize_profile_name() { - local profile="$1" - - profile="${profile//[[:space:]]/}" - printf '%s' "${profile,,}" -} - -setup_profile_enabled() { - local enabled_profile - local profile="$1" - - for enabled_profile in ${BASE_SETUP_PROFILES:-}; do - [[ "$enabled_profile" == "$profile" ]] && return 0 - done - return 1 -} - -setup_enable_profile() { - local profile="$1" - - setup_profile_supported "$profile" || return 1 - if setup_profile_enabled "$profile"; then - return 0 - fi - BASE_SETUP_PROFILES="${BASE_SETUP_PROFILES:+$BASE_SETUP_PROFILES }$profile" - export BASE_SETUP_PROFILES -} - -setup_enable_profile_argument() { - local compact profile profile_arg="$1" - local profiles=() - - BASE_SETUP_PROFILE_ERROR="" - compact="${profile_arg//[[:space:]]/}" - if [[ -z "$compact" || "$compact" == ,* || "$compact" == *, || "$compact" == *,,* ]]; then - BASE_SETUP_PROFILE_ERROR="Profile list must not contain empty entries." - return 1 - fi - - str_split profiles "$compact" "," - for profile in "${profiles[@]}"; do - profile="$(setup_normalize_profile_name "$profile")" - if ! setup_profile_supported "$profile"; then - # shellcheck disable=SC2034 # Consumed by setup.sh after this helper returns. - BASE_SETUP_PROFILE_ERROR="Unsupported profile '$profile'. Expected one of: $(setup_supported_profiles_display)." - return 1 - fi - setup_enable_profile "$profile" - done -} - -setup_profiles_enabled() { - [[ -n "${BASE_SETUP_PROFILES:-}" ]] -} - -setup_profile_json_key() { - local suffix="$1" - - printf 'profile_%s\n' "$suffix" -} - -setup_profiles_csv() { - local first=true profile - - for profile in ${BASE_SETUP_PROFILES:-}; do - if [[ "$first" == true ]]; then - printf '%s' "$profile" - first=false - else - printf ',%s' "$profile" - fi - done - printf '\n' -} - setup_is_dry_run() { [[ "${DRY_RUN-}" == true ]] } @@ -1251,33 +1156,6 @@ setup_run_project_artifact_doctor_json() { setup_run_project_artifact_layer doctor json } -setup_run_base_dev_layer() { - local args=("$@") - local platform - local profile_args=() - local venv_dir - - if setup_is_dry_run && - { ! setup_base_python_package_installed "$(setup_pyyaml_package)" || - ! setup_base_python_package_installed "$(setup_click_package)"; }; then - log_info "[DRY-RUN] Would run Python prerequisite profile layer after Base Python bootstrap dependencies are installed." - return 0 - fi - - setup_ensure_cached_paths - venv_dir="$_BASE_SETUP_VENV_DIR_CACHE" - if ! setup_base_venv_python_bin "$venv_dir" >/dev/null 2>&1; then - log_warn "Python prerequisite profile layer cannot run because Base virtual environment Python was not found at '$venv_dir/bin/python'." - log_warn "$(setup_recovery_venv)" - return 1 - fi - - profile_args=(--profile "$(setup_profiles_csv)") - platform="$(setup_current_platform)" || return 1 - - env BASE_PLATFORM="$platform" "$BASE_HOME/bin/base-wrapper" --project base base_dev "${args[@]}" "${profile_args[@]}" -} - setup_wait_for_base_check_probes() { local failed=0 local pid diff --git a/cli/bash/commands/basectl/subcommands/setup_profiles.sh b/cli/bash/commands/basectl/subcommands/setup_profiles.sh new file mode 100644 index 00000000..6c55c87d --- /dev/null +++ b/cli/bash/commands/basectl/subcommands/setup_profiles.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash + +# +# setup_profiles.sh +# Base setup/check profile parsing and dispatch helpers for setup_common.sh. +# +# This file is sourced by setup_common.sh. It intentionally preserves the +# existing setup_* function names so setup/check/doctor call sites remain +# behavior-compatible while profile ownership moves out of the shared file. +# + +[[ -n "${_base_setup_profiles_sourced:-}" ]] && return 0 +_base_setup_profiles_sourced=1 +readonly _base_setup_profiles_sourced + +setup_supported_profiles() { + printf '%s\n' "dev sre ai linux-lab" +} + +setup_supported_profiles_display() { + printf '%s\n' "dev, sre, ai, linux-lab" +} + +setup_profile_supported() { + local profile="$1" + + case "$profile" in + dev|sre|ai|linux-lab) + return 0 + ;; + *) + return 1 + ;; + esac +} + +setup_normalize_profile_name() { + local profile="$1" + + profile="${profile//[[:space:]]/}" + printf '%s' "${profile,,}" +} + +setup_profile_enabled() { + local enabled_profile + local profile="$1" + + for enabled_profile in ${BASE_SETUP_PROFILES:-}; do + [[ "$enabled_profile" == "$profile" ]] && return 0 + done + return 1 +} + +setup_enable_profile() { + local profile="$1" + + setup_profile_supported "$profile" || return 1 + if setup_profile_enabled "$profile"; then + return 0 + fi + BASE_SETUP_PROFILES="${BASE_SETUP_PROFILES:+$BASE_SETUP_PROFILES }$profile" + export BASE_SETUP_PROFILES +} + +setup_enable_profile_argument() { + local compact profile profile_arg="$1" + local profiles=() + + BASE_SETUP_PROFILE_ERROR="" + compact="${profile_arg//[[:space:]]/}" + if [[ -z "$compact" || "$compact" == ,* || "$compact" == *, || "$compact" == *,,* ]]; then + BASE_SETUP_PROFILE_ERROR="Profile list must not contain empty entries." + return 1 + fi + + str_split profiles "$compact" "," + for profile in "${profiles[@]}"; do + profile="$(setup_normalize_profile_name "$profile")" + if ! setup_profile_supported "$profile"; then + # shellcheck disable=SC2034 # Consumed by setup.sh after this helper returns. + BASE_SETUP_PROFILE_ERROR="Unsupported profile '$profile'. Expected one of: $(setup_supported_profiles_display)." + return 1 + fi + setup_enable_profile "$profile" + done +} + +setup_profiles_enabled() { + [[ -n "${BASE_SETUP_PROFILES:-}" ]] +} + +setup_profile_json_key() { + local suffix="$1" + + printf 'profile_%s\n' "$suffix" +} + +setup_profiles_csv() { + local first=true profile + + for profile in ${BASE_SETUP_PROFILES:-}; do + if [[ "$first" == true ]]; then + printf '%s' "$profile" + first=false + else + printf ',%s' "$profile" + fi + done + printf '\n' +} + +setup_run_base_dev_layer() { + local args=("$@") + local platform + local profile_args=() + local venv_dir + + if setup_is_dry_run && + { ! setup_base_python_package_installed "$(setup_pyyaml_package)" || + ! setup_base_python_package_installed "$(setup_click_package)"; }; then + log_info "[DRY-RUN] Would run Python prerequisite profile layer after Base Python bootstrap dependencies are installed." + return 0 + fi + + setup_ensure_cached_paths + venv_dir="$_BASE_SETUP_VENV_DIR_CACHE" + if ! setup_base_venv_python_bin "$venv_dir" >/dev/null 2>&1; then + log_warn "Python prerequisite profile layer cannot run because Base virtual environment Python was not found at '$venv_dir/bin/python'." + log_warn "$(setup_recovery_venv)" + return 1 + fi + + profile_args=(--profile "$(setup_profiles_csv)") + platform="$(setup_current_platform)" || return 1 + + env BASE_PLATFORM="$platform" "$BASE_HOME/bin/base-wrapper" --project base base_dev "${args[@]}" "${profile_args[@]}" +} diff --git a/cli/bash/commands/basectl/tests/setup-common.bats b/cli/bash/commands/basectl/tests/setup-common.bats index 3bcbe7a6..e1eb2098 100644 --- a/cli/bash/commands/basectl/tests/setup-common.bats +++ b/cli/bash/commands/basectl/tests/setup-common.bats @@ -145,6 +145,32 @@ run_setup_common_script() { [[ "$output" == *"guard=1"* ]] } +@test "setup_common sources profiles helper idempotently" { + run_setup_common_script ' + source "$BASE_HOME/cli/bash/commands/basectl/subcommands/setup_profiles.sh" + source "$BASE_HOME/cli/bash/commands/basectl/subcommands/setup_profiles.sh" + for helper in \ + setup_enable_profile_argument \ + setup_profiles_csv \ + setup_profile_json_key \ + setup_run_base_dev_layer; do + declare -F "$helper" >/dev/null || { + printf "missing helper: %s\n" "$helper" >&2 + exit 23 + } + done + setup_enable_profile_argument "DEV, sre" + printf "profiles=%s\n" "$(setup_profiles_csv)" + printf "json_key=%s\n" "$(setup_profile_json_key checks)" + printf "guard=%s\n" "${_base_setup_profiles_sourced:-}" + ' + + [ "$status" -eq 0 ] + [[ "$output" == *"profiles=dev,sre"* ]] + [[ "$output" == *"json_key=profile_checks"* ]] + [[ "$output" == *"guard=1"* ]] +} + @test "setup_common reports WSL2 host context without changing platform support" { run_setup_common_script ' BASE_TEST_MODE=true diff --git a/docs/setup-common-ownership.md b/docs/setup-common-ownership.md index 49984a60..cf2290aa 100644 --- a/docs/setup-common-ownership.md +++ b/docs/setup-common-ownership.md @@ -1,10 +1,10 @@ # `setup_common.sh` Ownership Reduction Status: active #1570 decomposition map. The first platform helpers, -`setup_linux_debian.sh` and `setup_macos_homebrew.sh`, plus the Base runtime -helper `setup_venv.sh`, have been extracted from the shared 2,906-line -baseline; `setup_common.sh` is now the shared orchestrator plus remaining -unextracted domains. +`setup_linux_debian.sh` and `setup_macos_homebrew.sh`, the Base runtime helper +`setup_venv.sh`, and the profile helper `setup_profiles.sh` have been +extracted from the shared 2,906-line baseline; `setup_common.sh` is now the +shared orchestrator plus remaining unextracted domains. `cli/bash/commands/basectl/subcommands/setup_common.sh` is intentionally shared by `basectl setup`, `basectl check`, `basectl doctor`, and @@ -43,21 +43,21 @@ entry-point functions are the stable anchors for future edits. | File / span | Current responsibility | Entry-point anchors | Target owner | | --- | --- | --- | --- | -| `setup_common.sh` 1-274 | Source guard, helper sourcing, shared cached paths, run state, profile flags, dry-run/debug/yes/CI toggles, Ubuntu/Debian consent prompts, notification toggles, and CI mode detection. | `setup_refresh_cached_paths()`, `setup_enable_profile_argument()`, `setup_require_linux_debian_system_consent()` | Keep in shared shell orchestration until profile parsing can move independently. | -| `setup_common.sh` 278-363 | Platform/host-env helpers, platform support messages, test-hook gates, and shared non-runtime recovery text. | `setup_current_platform()`, `setup_current_host_env()`, `setup_reject_test_hook_if_disallowed()` | Keep platform policy shared while OS-specific implementation remains in platform helpers. | -| `setup_common.sh` 367-405 | macOS completion notification behavior. | `setup_notify_completion()` | Candidate `setup_notifications.sh`; low risk but low value unless it still reduces ownership after platform helpers settle. | -| `setup_common.sh` 409-505 | Shared command-path probes, executable architecture, Rosetta state, GitHub CLI version display, and runtime-chain summary rendering. | `setup_command_path()`, `setup_rosetta_translation_state()`, `setup_print_runtime_chain_summary()` | Keep shared because the summary combines platform helper data with cross-platform runtime state. | -| `setup_common.sh` 509-700 | Base check finding metadata, base-bash-libs status, PYTHONPATH, and the diagnostics JSON bridge. | `setup_base_check_finding_id()`, `setup_diagnostics_python_bin()`, `setup_run_diagnostics_json()` | Finding metadata should eventually move to Python; the diagnostics bridge remains shared shell until check JSON assembly moves. | -| `setup_common.sh` 702-850 | Project manifest resolution, project route dispatch, check-result recording, user config seeding, and legacy project-venv fallback helpers. | `setup_resolve_project_manifest()`, `setup_resolve_project_route()`, `setup_record_project_check_result()` | Continue moving structured route policy to Python; keep shell dispatch thin. | -| `setup_common.sh` 856-945 | Doctor visual status and project virtualenv JSON snippets for pre-venv failure handling. | `setup_print_doctor_finding()`, `setup_print_project_venv_check_json()`, `setup_print_project_venv_doctor_json()` | Move structured JSON assembly to Python before considering command-local doctor formatting. | -| `setup_common.sh` 958-1246 | Project pre-venv, bootstrap, artifact setup/check/doctor, uv-manager, wrapper, and remote-network dispatch. | `setup_run_project_pre_venv_layer()`, `setup_run_project_bootstrap_layer()`, `setup_run_project_artifact_layer()` | Keep as shell dispatch; reduce by moving project policy and payload shape to Python. | -| `setup_common.sh` 1254-1277 | `base_dev` prerequisite profile dispatch. | `setup_run_base_dev_layer()` | Candidate `setup_profiles.sh` once profile parsing, profile JSON keys, and profile dispatch can move together. | -| `setup_common.sh` 1281-1309 | Shared probe waiting plus platform/base check dispatch. | `setup_wait_for_base_check_probes()`, `setup_collect_platform_base_check_results()`, `setup_collect_base_check_results()` | Keep dispatch shared until check JSON assembly and probe orchestration have clearer Python boundaries. | -| `setup_common.sh` 1319-1459 | Base check text rendering, project check result status handling, top-level check orchestration, and check JSON argument assembly. | `setup_run_check()`, `setup_run_check_json()`, `setup_print_check_text_results()` | Move check JSON assembly to Python; keep human text rendering and exit orchestration in shell. | -| `setup_common.sh` 1462-1486 | Platform install dispatch and top-level setup dispatch. | `setup_run_platform_install()`, `setup_run_install()` | Keep shared dispatch in `setup_common.sh`; install bodies belong in domain helpers. | +| `setup_common.sh` 1-179 | Source guard, helper sourcing, shared cached paths, run state, dry-run/debug/yes/CI toggles, Ubuntu/Debian consent prompts, notification toggles, and CI mode detection. | `setup_refresh_cached_paths()`, `setup_clear_run_state()`, `setup_require_linux_debian_system_consent()` | Keep in shared shell orchestration. | +| `setup_common.sh` 183-268 | Platform/host-env helpers, platform support messages, test-hook gates, and shared non-runtime recovery text. | `setup_current_platform()`, `setup_current_host_env()`, `setup_reject_test_hook_if_disallowed()` | Keep platform policy shared while OS-specific implementation remains in platform helpers. | +| `setup_common.sh` 272-310 | macOS completion notification behavior. | `setup_notify_completion()` | Candidate `setup_notifications.sh`; low risk but low value unless it still reduces ownership after platform helpers settle. | +| `setup_common.sh` 314-410 | Shared command-path probes, executable architecture, Rosetta state, GitHub CLI version display, and runtime-chain summary rendering. | `setup_command_path()`, `setup_rosetta_translation_state()`, `setup_print_runtime_chain_summary()` | Keep shared because the summary combines platform helper data with cross-platform runtime state. | +| `setup_common.sh` 414-605 | Base check finding metadata, base-bash-libs status, PYTHONPATH, and the diagnostics JSON bridge. | `setup_base_check_finding_id()`, `setup_diagnostics_python_bin()`, `setup_run_diagnostics_json()` | Finding metadata should eventually move to Python; the diagnostics bridge remains shared shell until check JSON assembly moves. | +| `setup_common.sh` 607-755 | Project manifest resolution, project route dispatch, check-result recording, user config seeding, and legacy project-venv fallback helpers. | `setup_resolve_project_manifest()`, `setup_resolve_project_route()`, `setup_record_project_check_result()` | Continue moving structured route policy to Python; keep shell dispatch thin. | +| `setup_common.sh` 761-850 | Doctor visual status and project virtualenv JSON snippets for pre-venv failure handling. | `setup_print_doctor_finding()`, `setup_print_project_venv_check_json()`, `setup_print_project_venv_doctor_json()` | Move structured JSON assembly to Python before considering command-local doctor formatting. | +| `setup_common.sh` 863-1151 | Project pre-venv, bootstrap, artifact setup/check/doctor, uv-manager, wrapper, and remote-network dispatch. | `setup_run_project_pre_venv_layer()`, `setup_run_project_bootstrap_layer()`, `setup_run_project_artifact_layer()` | Keep as shell dispatch; reduce by moving project policy and payload shape to Python. | +| `setup_common.sh` 1159-1187 | Shared probe waiting plus platform/base check dispatch. | `setup_wait_for_base_check_probes()`, `setup_collect_platform_base_check_results()`, `setup_collect_base_check_results()` | Keep dispatch shared until check JSON assembly and probe orchestration have clearer Python boundaries. | +| `setup_common.sh` 1197-1337 | Base check text rendering, project check result status handling, top-level check orchestration, and check JSON argument assembly. | `setup_run_check()`, `setup_run_check_json()`, `setup_print_check_text_results()` | Move check JSON assembly to Python; keep human text rendering and exit orchestration in shell. | +| `setup_common.sh` 1340-1364 | Platform install dispatch and top-level setup dispatch. | `setup_run_platform_install()`, `setup_run_install()` | Keep shared dispatch in `setup_common.sh`; install bodies belong in domain helpers. | | `setup_linux_debian.sh` 1-422 | Ubuntu/Debian recovery text, Python finder, runtime tool probes, check collector, apt prerequisites, GitHub CLI apt-repo setup, and Linux install body. | `setup_find_linux_python_bin()`, `setup_collect_linux_debian_base_check_results()`, `setup_run_linux_debian_install()` | Extracted OS/platform helper; keep future Ubuntu/Debian policy here unless it is structured data better owned by Python. | | `setup_macos_homebrew.sh` 1-601 | macOS/Homebrew recovery text, Homebrew discovery and installer policy, Xcode command-line tools, macOS Python finder, macOS host probes, and macOS install body. | `setup_find_brew_bin()`, `setup_install_homebrew()`, `setup_collect_macos_base_check_results()`, `setup_run_macos_install()` | Extracted OS/platform helper; keep future macOS/Homebrew policy here unless it is structured data better owned by Python. | | `setup_venv.sh` 1-444 | Base runtime virtualenv health, pyvenv inspection, recreate behavior, platform Python dispatch, Base bootstrap package checks/install, venv check probes, CI-runtime checks, and CI-runtime install body. | `setup_virtualenv_healthy_path()`, `setup_create_virtualenv()`, `setup_collect_ci_runtime_check_results()`, `setup_run_ci_runtime_install()` | Extracted Base runtime helper; keep future runtime bootstrap policy here unless structured check output moves to Python. | +| `setup_profiles.sh` 1-137 | Setup/check profile parsing, profile state, profile JSON key naming, profile CSV rendering, and `base_dev` prerequisite profile dispatch. | `setup_enable_profile_argument()`, `setup_profiles_csv()`, `setup_run_base_dev_layer()` | Extracted profile helper; keep future profile parsing and dispatch policy here unless profile state moves to Python. | ## Decomposition Strategy @@ -153,9 +153,22 @@ doing it after platform helpers are stable gives the venv helper a clean API. Move smaller orchestration surfaces only after the platform and venv layers have settled. -Candidate helpers: +Implemented profile dispatch fourth as `setup_profiles.sh`. + +The profile helper owns: + +- supported profile names and display text: + `setup_supported_profiles()` and `setup_supported_profiles_display()`; +- profile normalization and enablement: + `setup_normalize_profile_name()`, `setup_enable_profile_argument()`, and + `setup_profiles_enabled()`; +- profile payload keys and CSV rendering: + `setup_profile_json_key()` and `setup_profiles_csv()`; +- `base_dev` prerequisite profile dispatch: + `setup_run_base_dev_layer()`. + +Remaining candidate: -- `setup_profiles.sh` for profile parsing and `setup_run_base_dev_layer()`; - `setup_notifications.sh` for `setup_notify_completion()`. These are cleanup slices, not product enablers. They are useful only when they @@ -207,8 +220,8 @@ Each sourced helper PR should follow this protocol: ## Recommended PR Sequence -1. Complete the Linux/Debian, macOS/Homebrew, and Base runtime helper +1. Complete the Linux/Debian, macOS/Homebrew, Base runtime, and profile helper extractions and keep #1570 open for the remaining staged breakup. -2. Move profile and notification helpers if they still reduce ownership. +2. Move notification helpers if they still reduce ownership. 3. Move structured check/doctor JSON assembly into Python-owned code, not into another shell helper. diff --git a/tests/test_setup_common_ownership_docs.py b/tests/test_setup_common_ownership_docs.py index 7d3d7c46..1690bedb 100644 --- a/tests/test_setup_common_ownership_docs.py +++ b/tests/test_setup_common_ownership_docs.py @@ -16,6 +16,9 @@ SETUP_VENV_SCRIPT = ( REPO_ROOT / "cli" / "bash" / "commands" / "basectl" / "subcommands" / "setup_venv.sh" ) +SETUP_PROFILES_SCRIPT = ( + REPO_ROOT / "cli" / "bash" / "commands" / "basectl" / "subcommands" / "setup_profiles.sh" +) DOCS_README = REPO_ROOT / "docs" / "README.md" @@ -35,6 +38,7 @@ def setup_shell_sources() -> str: SETUP_LINUX_DEBIAN_SCRIPT, SETUP_MACOS_HOMEBREW_SCRIPT, SETUP_VENV_SCRIPT, + SETUP_PROFILES_SCRIPT, ) ) @@ -108,3 +112,11 @@ def test_setup_common_sources_venv_helper() -> None: assert 'source "$BASE_HOME/cli/bash/commands/basectl/subcommands/setup_venv.sh"' in common_source assert "_base_setup_venv_sourced" in venv_source + + +def test_setup_common_sources_profiles_helper() -> None: + common_source = setup_common_script() + profiles_source = SETUP_PROFILES_SCRIPT.read_text(encoding="utf-8") + + assert 'source "$BASE_HOME/cli/bash/commands/basectl/subcommands/setup_profiles.sh"' in common_source + assert "_base_setup_profiles_sourced" in profiles_source