diff --git a/cli/bash/commands/basectl/subcommands/setup_common.sh b/cli/bash/commands/basectl/subcommands/setup_common.sh index 10e2a7d..1c82223 100644 --- a/cli/bash/commands/basectl/subcommands/setup_common.sh +++ b/cli/bash/commands/basectl/subcommands/setup_common.sh @@ -437,8 +437,72 @@ setup_base_python_package_check_message() { fi } +setup_project_setup_pythonpath() { + local base_pythonpath old_pythonpath + + base_pythonpath="$BASE_HOME/lib/python:$BASE_HOME/cli/python" + old_pythonpath="${PYTHONPATH-}" + if [[ -n "$old_pythonpath" ]]; then + base_pythonpath="$base_pythonpath:$old_pythonpath" + fi + printf '%s\n' "$base_pythonpath" +} + +setup_resolve_project_name() { + local manifest_arg project python_bin venv_dir + + if [[ -n "${BASE_SETUP_PROJECT_NAME:-}" ]]; then + printf '%s\n' "$BASE_SETUP_PROJECT_NAME" + return 0 + fi + + venv_dir="$(setup_venv_dir)" + python_bin="$(setup_base_venv_python_bin "$venv_dir")" || fatal_error "Base virtual environment Python was not found at '$venv_dir/bin/python'." + + manifest_arg="${BASE_SETUP_MANIFEST:-}" + project="$( + env BASE_HOME="$BASE_HOME" PYTHONPATH="$(setup_project_setup_pythonpath)" "$python_bin" -c ' +from pathlib import Path +import sys + +from base_cli.paths import discover_manifest +from base_setup.manifest import read_manifest + +manifest_arg = sys.argv[1] +start_dir = Path(sys.argv[2]) +if manifest_arg: + manifest_path = Path(manifest_arg).expanduser().resolve() +else: + manifest_path = discover_manifest(start_dir) + +print(read_manifest(manifest_path).project_name if manifest_path else "base") +' "$manifest_arg" "$PWD" + )" || fatal_error "Unable to resolve Base project name from manifest." + + printf '%s\n' "$project" +} + +setup_validate_dev_project() { + local project + + if ! setup_dev_dependencies_enabled; then + return 0 + fi + + if setup_is_dry_run && ! setup_base_python_package_installed "$(setup_pyyaml_package)"; then + log_info "[DRY-RUN] Would validate that --dev is only used for the Base project after PyYAML is installed." + return 0 + fi + + project="$(setup_resolve_project_name)" + if [[ "$project" != base ]]; then + print_error "--dev is only supported for the Base project. Run without --dev for project '$project'." + return 1 + fi +} + setup_run_project_artifact_setup() { - local exit_code project wrapper + local exit_code project python_bin venv_dir local args=() if setup_is_dry_run && ! setup_base_python_package_installed "$(setup_pyyaml_package)"; then @@ -446,20 +510,22 @@ setup_run_project_artifact_setup() { return 0 fi - project="${BASE_SETUP_PROJECT_NAME:-base}" - wrapper="$BASE_HOME/bin/base-wrapper" - [[ -x "$wrapper" ]] || fatal_error "Base Python wrapper '$wrapper' is missing or is not executable." + venv_dir="$(setup_venv_dir)" + python_bin="$(setup_base_venv_python_bin "$venv_dir")" || fatal_error "Base virtual environment Python was not found at '$venv_dir/bin/python'." + project="$(setup_resolve_project_name)" if setup_is_dry_run; then args+=(--dry-run) fi if [[ -n "${BASE_SETUP_MANIFEST:-}" ]]; then args+=(--manifest "$BASE_SETUP_MANIFEST") + elif [[ "$project" == base ]]; then + args+=(--manifest "$BASE_HOME/base_manifest.yaml") fi args+=("$project") log_info "Running Python project setup layer." - "$wrapper" --project "$project" base_setup "${args[@]}" + env BASE_HOME="$BASE_HOME" BASE_PROJECT="$project" PYTHONPATH="$(setup_project_setup_pythonpath)" "$python_bin" -m base_setup "${args[@]}" exit_code=$? exit_if_error "$exit_code" "Python project setup layer failed." @@ -674,13 +740,14 @@ setup_run_install() { setup_install_homebrew setup_install_xcode_tools setup_install_python + setup_create_virtualenv + setup_install_pyyaml + setup_install_click + setup_validate_dev_project || return 1 if setup_dev_dependencies_enabled; then setup_install_bats setup_install_gh fi - setup_create_virtualenv - setup_install_pyyaml - setup_install_click setup_run_project_artifact_setup if setup_is_dry_run; then diff --git a/cli/bash/commands/basectl/tests/setup.bats b/cli/bash/commands/basectl/tests/setup.bats index f42ea0e..344d415 100644 --- a/cli/bash/commands/basectl/tests/setup.bats +++ b/cli/bash/commands/basectl/tests/setup.bats @@ -102,9 +102,16 @@ if [[ "${1:-}" == "-m" && "${2:-}" == "venv" && -n "${3:-}" ]]; then pyyaml_package="${BASE_SETUP_PYYAML_PACKAGE:-PyYAML}" click_package="${BASE_SETUP_CLICK_PACKAGE:-click}" if [[ "${1:-}" == "-m" && "${2:-}" == "base_setup" ]]; then + shift 2 + printf '%s\n' "$@" > "${BASE_SETUP_TEST_STATE_DIR:?}/project-setup-args" + printf '%s\n' "${BASE_PROJECT:-}" > "${BASE_SETUP_TEST_STATE_DIR:?}/project-setup-project" touch "${BASE_SETUP_TEST_STATE_DIR:?}/project-setup-ran" exit 0 fi +if [[ "${1:-}" == "-c" ]]; then + printf 'base\n' + exit 0 +fi if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "show" && "${4:-}" == "$pyyaml_package" ]]; then [[ -f "${BASE_SETUP_TEST_STATE_DIR:?}/pyyaml-installed" ]] exit $? @@ -130,9 +137,16 @@ VENVEOF exit 0 fi if [[ "${1:-}" == "-m" && "${2:-}" == "base_setup" ]]; then + shift 2 + printf '%s\n' "$@" > "${BASE_SETUP_TEST_STATE_DIR:?}/project-setup-args" + printf '%s\n' "${BASE_PROJECT:-}" > "${BASE_SETUP_TEST_STATE_DIR:?}/project-setup-project" touch "${BASE_SETUP_TEST_STATE_DIR:?}/project-setup-ran" exit 0 fi +if [[ "${1:-}" == "-c" ]]; then + printf 'base\n' + exit 0 +fi printf 'unexpected python3 args: %s\n' "$*" >&2 exit 1 PYEOF @@ -218,9 +232,16 @@ if [[ "${1:-}" == "-m" && "${2:-}" == "venv" && -n "${3:-}" ]]; then pyyaml_package="${BASE_SETUP_PYYAML_PACKAGE:-PyYAML}" click_package="${BASE_SETUP_CLICK_PACKAGE:-click}" if [[ "${1:-}" == "-m" && "${2:-}" == "base_setup" ]]; then + shift 2 + printf '%s\n' "$@" > "${BASE_SETUP_TEST_STATE_DIR:?}/project-setup-args" + printf '%s\n' "${BASE_PROJECT:-}" > "${BASE_SETUP_TEST_STATE_DIR:?}/project-setup-project" touch "${BASE_SETUP_TEST_STATE_DIR:?}/project-setup-ran" exit 0 fi +if [[ "${1:-}" == "-c" ]]; then + printf 'base\n' + exit 0 +fi if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "show" && "${4:-}" == "$pyyaml_package" ]]; then [[ -f "${BASE_SETUP_TEST_STATE_DIR:?}/pyyaml-installed" ]] exit $? @@ -246,9 +267,16 @@ VENVEOF exit 0 fi if [[ "${1:-}" == "-m" && "${2:-}" == "base_setup" ]]; then + shift 2 + printf '%s\n' "$@" > "${BASE_SETUP_TEST_STATE_DIR:?}/project-setup-args" + printf '%s\n' "${BASE_PROJECT:-}" > "${BASE_SETUP_TEST_STATE_DIR:?}/project-setup-project" touch "${BASE_SETUP_TEST_STATE_DIR:?}/project-setup-ran" exit 0 fi +if [[ "${1:-}" == "-c" ]]; then + printf 'base\n' + exit 0 +fi printf 'unexpected python3 args: %s\n' "$*" >&2 exit 1 PYEOF @@ -360,6 +388,15 @@ if [[ "${1:-}" == "-m" && "${2:-}" == "base_setup" ]]; then touch "$BASE_SETUP_TEST_STATE_DIR/project-setup-ran" exit "$(cat "$BASE_SETUP_TEST_STATE_DIR/project-setup-exit-code")" fi +if [[ "${1:-}" == "-c" ]]; then + manifest_path="${3:-}" + if [[ -n "$manifest_path" && -f "$manifest_path" ]]; then + awk '/^[[:space:]]*name:/ { print $2; exit }' "$manifest_path" + else + printf 'base\n' + fi + exit 0 +fi if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "show" && "${4:-}" == "$pyyaml_package" ]]; then [[ -f "${BASE_SETUP_TEST_STATE_DIR:?}/pyyaml-installed" ]] exit $? @@ -503,7 +540,7 @@ EOF [ -f "$venv_dir/pyvenv.cfg" ] } -@test "basectl setup forwards project setup arguments through base-wrapper" { +@test "basectl setup forwards explicit project setup arguments through the Base venv" { local base_venv_dir="$TEST_HOME/.base.d/base/.venv" local demo_venv_dir="$TEST_HOME/.base.d/demo/.venv" local manifest_path="$TEST_TMPDIR/demo_manifest.yaml" @@ -515,8 +552,7 @@ EOF touch "$TEST_STATE_DIR/python-installed" touch "$TEST_STATE_DIR/pyyaml-installed" touch "$TEST_STATE_DIR/click-installed" - create_base_venv_stub "$base_venv_dir" - create_project_setup_venv_stub "$demo_venv_dir" + create_project_setup_venv_stub "$base_venv_dir" printf 'project:\n name: demo\nartifacts: []\n' > "$manifest_path" run_base_command setup --dry-run --manifest "$manifest_path" demo @@ -526,6 +562,56 @@ EOF [ -f "$TEST_STATE_DIR/project-setup-ran" ] [ "$(cat "$TEST_STATE_DIR/project-setup-project")" = "demo" ] [ "$(cat "$TEST_STATE_DIR/project-setup-args")" = "$(printf '%s\n' --dry-run --manifest "$manifest_path" demo)" ] + [ ! -e "$demo_venv_dir/bin/python" ] +} + +@test "basectl setup infers omitted project argument from the manifest" { + local base_venv_dir="$TEST_HOME/.base.d/base/.venv" + local demo_venv_dir="$TEST_HOME/.base.d/demo/.venv" + local manifest_path="$TEST_TMPDIR/demo_manifest.yaml" + + create_brew_stub + create_xcode_stubs + touch "$TEST_STATE_DIR/xcode-installed" + mkdir -p "$TEST_TMPDIR/CommandLineTools" + touch "$TEST_STATE_DIR/python-installed" + touch "$TEST_STATE_DIR/pyyaml-installed" + touch "$TEST_STATE_DIR/click-installed" + create_project_setup_venv_stub "$base_venv_dir" + printf 'project:\n name: demo\nartifacts: []\n' > "$manifest_path" + + run_base_command setup --dry-run --manifest "$manifest_path" + + [ "$status" -eq 0 ] + [ -f "$TEST_STATE_DIR/project-setup-ran" ] + [ "$(cat "$TEST_STATE_DIR/project-setup-project")" = "demo" ] + [ "$(cat "$TEST_STATE_DIR/project-setup-args")" = "$(printf '%s\n' --dry-run --manifest "$manifest_path" demo)" ] + [ ! -e "$demo_venv_dir/bin/python" ] +} + +@test "basectl setup --dev fails for non-Base projects before installing developer dependencies" { + local base_venv_dir="$TEST_HOME/.base.d/base/.venv" + local manifest_path="$TEST_TMPDIR/demo_manifest.yaml" + + create_brew_stub + create_xcode_stubs + touch "$TEST_STATE_DIR/xcode-installed" + mkdir -p "$TEST_TMPDIR/CommandLineTools" + touch "$TEST_STATE_DIR/python-installed" + touch "$TEST_STATE_DIR/pyyaml-installed" + touch "$TEST_STATE_DIR/click-installed" + create_project_setup_venv_stub "$base_venv_dir" + printf 'project:\n name: demo\nartifacts: []\n' > "$manifest_path" + + run_base_command setup --dev --manifest "$manifest_path" + + [ "$status" -eq 1 ] + [[ "$output" == *"--dev is only supported for the Base project. Run without --dev for project 'demo'."* ]] + [[ "$output" != *"FATAL"* ]] + [[ "$output" != *"Encountered a fatal error"* ]] + [ ! -f "$TEST_STATE_DIR/bats-install-ran" ] + [ ! -f "$TEST_STATE_DIR/gh-install-ran" ] + [ ! -f "$TEST_STATE_DIR/project-setup-ran" ] } @test "basectl setup propagates Python project setup failure" { @@ -549,6 +635,7 @@ EOF @test "basectl setup --dev installs developer dependencies" { local installer + local expected_args create_xcode_stubs installer="$(create_homebrew_installer_stub)" @@ -563,6 +650,8 @@ EOF [[ "$output" == *"Installing GitHub CLI formula 'gh' via Homebrew."* ]] [ -f "$TEST_STATE_DIR/bats-install-ran" ] [ -f "$TEST_STATE_DIR/gh-install-ran" ] + expected_args="$(printf '%s\n' --manifest "$BASE_REPO_ROOT/base_manifest.yaml" base)" + [ "$(cat "$TEST_STATE_DIR/project-setup-args")" = "$expected_args" ] } @test "basectl setup backs up an existing non-venv path before creating the Base virtual environment" {