diff --git a/bin/base-wrapper b/bin/base-wrapper index 5c7c9df5..af6ec0c7 100755 --- a/bin/base-wrapper +++ b/bin/base-wrapper @@ -45,6 +45,8 @@ Usage: Options: --project Run using ~/.base.d//.venv. + --preserve-project-context + Keep the inherited BASE_PROJECT while selecting a venv. -h, --help Show this help text. Purpose: @@ -61,6 +63,7 @@ EOF main() { local base_cli_path base_cli_source base_home project package python_bin old_pythonpath base_pythonpath + local preserve_project_context=0 base_home="$(base_wrapper_base_home)" || base_wrapper_die "Unable to resolve Base home." project="${BASE_PROJECT:-base}" @@ -78,6 +81,9 @@ main() { [[ -n "${1:-}" ]] || base_wrapper_die "Option '--project' requires an argument." project="$1" ;; + --preserve-project-context) + preserve_project_context=1 + ;; --) shift break @@ -111,7 +117,9 @@ main() { esac BASE_HOME="$base_home" - BASE_PROJECT="$project" + if [[ "$preserve_project_context" != 1 || -z "${BASE_PROJECT:-}" ]]; then + BASE_PROJECT="$project" + fi # This wrapper always launches Base's control-plane Python packages. The # selected project only chooses the virtual environment; it must not move # Base-owned runtime artifacts into a project's namespace. diff --git a/cli/bash/commands/basectl/tests/command_protocol_fixtures.bash b/cli/bash/commands/basectl/tests/command_protocol_fixtures.bash index bd836e78..175f15d2 100644 --- a/cli/bash/commands/basectl/tests/command_protocol_fixtures.bash +++ b/cli/bash/commands/basectl/tests/command_protocol_fixtures.bash @@ -3,7 +3,8 @@ # Fake Python executables are Bash scripts loaded through BASH_ENV. Normalize # interpreter isolation and the owned entrypoint into the module arguments # those stubs model. Real Python integration tests exercise the actual launcher. -if [[ "${0##*/}" == python* && "${1:-}" == -I ]]; then +if [[ "${BASE_TEST_PRESERVE_PYTHON_ENTRYPOINT:-}" != 1 && + "${0##*/}" == python* && "${1:-}" == -I ]]; then shift if [[ "${1:-}" == */base_cli_adapters/module_entrypoint.py ]]; then shift diff --git a/cli/bash/commands/basectl/tests/runtime-dispatch.bats b/cli/bash/commands/basectl/tests/runtime-dispatch.bats index 53f16999..c8c8f37f 100644 --- a/cli/bash/commands/basectl/tests/runtime-dispatch.bats +++ b/cli/bash/commands/basectl/tests/runtime-dispatch.bats @@ -843,6 +843,29 @@ EOF [[ "$output" == *"ARGS=-m base_setup --dry-run demo"* ]] } +@test "base-wrapper can select the Base venv without replacing active project context" { + local python_bin="$TEST_HOME/.base.d/base/.venv/bin/python" + + mkdir -p "$(dirname "$python_bin")" + cat > "$python_bin" <<'EOF' +#!/usr/bin/env bash +printf 'BASE_PROJECT=%s\n' "$BASE_PROJECT" +printf 'ARGS=%s\n' "$*" +EOF + chmod +x "$python_bin" + + run env \ + HOME="$TEST_HOME" \ + BASE_PROJECT=demo \ + BASE_TEST_PRESERVE_PYTHON_ENTRYPOINT=1 \ + "$BASE_REPO_ROOT/bin/base-wrapper" --project base --preserve-project-context \ + base_projects activation-sources demo --format command-protocol + + [ "$status" -eq 0 ] + [[ "$output" == *"BASE_PROJECT=demo"* ]] + [[ "$output" == *"ARGS=-I $BASE_REPO_ROOT/cli/python/base_cli_adapters/module_entrypoint.py base_projects activation-sources demo --format command-protocol"* ]] +} + @test "basectl propagates supported wrapper diagnostics to explicit Base scripts" { local script_path="$TEST_TMPDIR/wrapper-diagnostics-script" diff --git a/cli/bash/commands/basectl/tests/runtime-shell.bats b/cli/bash/commands/basectl/tests/runtime-shell.bats index e5097f07..31a3c374 100644 --- a/cli/bash/commands/basectl/tests/runtime-shell.bats +++ b/cli/bash/commands/basectl/tests/runtime-shell.bats @@ -94,7 +94,9 @@ EOF #!/usr/bin/env bash # Bash 3 reads BASH_ENV before assigning script arguments. source "${BASH_ENV:?}" -if [[ "${1:-}" == "-m" && "${2:-}" == "base_projects" && "${3:-}" == "activation-sources" && "${4:-}" == "demo" ]]; then +if [[ "${1:-}" == "-I" && + "${2:-}" == "$BASE_HOME/cli/python/base_cli_adapters/module_entrypoint.py" && + "${3:-}" == "base_projects" && "${4:-}" == "activation-sources" && "${5:-}" == "demo" ]]; then base_test_protocol_begin activation-source 0 base_test_protocol_end exit 0 @@ -103,6 +105,8 @@ printf 'unexpected base_projects args: %s\n' "$*" >&2 exit 1 EOF chmod +x "$venv_dir/bin/python" + mkdir -p "$TEST_HOME/.base.d/base/.venv/bin" + cp "$venv_dir/bin/python" "$TEST_HOME/.base.d/base/.venv/bin/python" run env \ HOME="$TEST_HOME" \ @@ -111,6 +115,7 @@ EOF BASE_PROJECT_ROOT="$project_root" \ BASE_PROJECT_MANIFEST="$project_root/base_manifest.yaml" \ BASE_PROJECT_VENV_DIR="$venv_dir" \ + BASE_TEST_PRESERVE_PYTHON_ENTRYPOINT=1 \ PATH="/usr/bin:/bin:/usr/sbin:/sbin" \ "$BASH" --rcfile "$BASE_REPO_ROOT/lib/bash/runtime/bashrc" -i -c '\ declare -p BASE_PROJECT; \ @@ -139,7 +144,9 @@ EOF #!/usr/bin/env bash # Bash 3 reads BASH_ENV before assigning script arguments. source "${BASH_ENV:?}" -if [[ "${1:-}" == "-m" && "${2:-}" == "base_projects" && "${3:-}" == "activation-sources" && "${4:-}" == "demo" ]]; then +if [[ "${1:-}" == "-I" && + "${2:-}" == "$BASE_HOME/cli/python/base_cli_adapters/module_entrypoint.py" && + "${3:-}" == "base_projects" && "${4:-}" == "activation-sources" && "${5:-}" == "demo" ]]; then base_test_protocol_begin activation-source 1 base_test_protocol_activation_source_record 0 "${BASE_TEST_ACTIVATION_SOURCE:?}" base_test_protocol_end @@ -156,6 +163,8 @@ project_activation_function() { } EOF chmod +x "$venv_dir/bin/python" + mkdir -p "$TEST_HOME/.base.d/base/.venv/bin" + cp "$venv_dir/bin/python" "$TEST_HOME/.base.d/base/.venv/bin/python" run env \ HOME="$TEST_HOME" \ @@ -165,6 +174,7 @@ EOF BASE_PROJECT_MANIFEST="$project_root/base_manifest.yaml" \ BASE_PROJECT_VENV_DIR="$venv_dir" \ BASE_TEST_ACTIVATION_SOURCE="$activation_script" \ + BASE_TEST_PRESERVE_PYTHON_ENTRYPOINT=1 \ PATH="/usr/bin:/bin:/usr/sbin:/sbin" \ "$BASH" --rcfile "$BASE_REPO_ROOT/lib/bash/runtime/bashrc" -i -c '\ printf "PROJECT_ACTIVATION_PROJECT=%s\n" "${PROJECT_ACTIVATION_PROJECT:-}"; \ @@ -197,7 +207,9 @@ EOF #!/usr/bin/env bash # Bash 3 reads BASH_ENV before assigning script arguments. source "${BASH_ENV:?}" -if [[ "${1:-}" == "-m" && "${2:-}" == "base_projects" && "${3:-}" == "activation-sources" && "${4:-}" == "demo" ]]; then +if [[ "${1:-}" == "-I" && + "${2:-}" == "$BASE_HOME/cli/python/base_cli_adapters/module_entrypoint.py" && + "${3:-}" == "base_projects" && "${4:-}" == "activation-sources" && "${5:-}" == "demo" ]]; then printf 'startup-run-root=%s\n' "${BASE_CLI_RUN_ROOT:-unset}" >&2 printf 'startup-primary-log=%s\n' "${BASE_BASH_LIBS_PRIMARY_LOG:-unset}" >&2 printf 'startup-history-scope=%s\n' "${BASE_CLI_HISTORY_SCOPE:-unset}" >&2 @@ -205,12 +217,20 @@ if [[ "${1:-}" == "-m" && "${2:-}" == "base_projects" && "${3:-}" == "activation base_test_protocol_end exit 0 fi -if [[ "${1:-}" == "-m" && "${2:-}" == "base_projects" && "${3:-}" == "list" ]]; then +if [[ ("${1:-}" == "-m" && "${2:-}" == "base_projects" && "${3:-}" == "list") || + ("${1:-}" == "-I" && "${2:-}" == "$BASE_HOME/cli/python/base_cli_adapters/module_entrypoint.py" && + "${3:-}" == "base_projects" && "${4:-}" == "list") ]]; then printf 'demo\t%s/demo\n' "${BASE_TEST_WORKSPACE:?}" exit 0 fi -if [[ "${1:-}" == "-m" && "${2:-}" == "base_history.record" ]]; then - printf '%s\n' "$*" >> "${BASE_TEST_RUNTIME_HISTORY_STATE:?}" +if [[ ("${1:-}" == "-m" && "${2:-}" == "base_history.record") || + ("${1:-}" == "-I" && "${2:-}" == "$BASE_HOME/cli/python/base_cli_adapters/module_entrypoint.py" && + "${3:-}" == "base_history.record") ]]; then + if [[ "${1:-}" == "-I" ]]; then + printf '%s\n' "-m ${*:3}" >> "${BASE_TEST_RUNTIME_HISTORY_STATE:?}" + else + printf '%s\n' "$*" >> "${BASE_TEST_RUNTIME_HISTORY_STATE:?}" + fi exit 0 fi printf 'unexpected base_projects args: %s\n' "$*" >&2 @@ -245,6 +265,7 @@ EOF BASE_CLI_PROJECT_NAME=demo \ BASE_CLI_PROJECT_ROOT="$project_root" \ BASE_CLI_PROJECT_MANIFEST="$project_root/base_manifest.yaml" \ + BASE_TEST_PRESERVE_PYTHON_ENTRYPOINT=1 \ PATH="/usr/bin:/bin:/usr/sbin:/sbin" \ "$BASH" --rcfile "$BASE_REPO_ROOT/lib/bash/runtime/bashrc" -i -c '\ for variable in \ @@ -338,7 +359,9 @@ EOF #!/usr/bin/env bash # Bash 3 reads BASH_ENV before assigning script arguments. source "${BASH_ENV:?}" -if [[ "${1:-}" == "-m" && "${2:-}" == "base_projects" && "${3:-}" == "activation-sources" && "${4:-}" == "demo" ]]; then +if [[ "${1:-}" == "-I" && + "${2:-}" == "$BASE_HOME/cli/python/base_cli_adapters/module_entrypoint.py" && + "${3:-}" == "base_projects" && "${4:-}" == "activation-sources" && "${5:-}" == "demo" ]]; then printf 'ERROR: %s: activate.source[1] script %q does not exist.\n' "$BASE_PROJECT_MANIFEST" ".base/missing.sh" >&2 exit 1 fi @@ -346,6 +369,8 @@ printf 'unexpected base_projects args: %s\n' "$*" >&2 exit 1 EOF chmod +x "$venv_dir/bin/python" + mkdir -p "$TEST_HOME/.base.d/base/.venv/bin" + cp "$venv_dir/bin/python" "$TEST_HOME/.base.d/base/.venv/bin/python" run env \ HOME="$TEST_HOME" \ @@ -354,6 +379,7 @@ EOF BASE_PROJECT_ROOT="$project_root" \ BASE_PROJECT_MANIFEST="$project_root/base_manifest.yaml" \ BASE_PROJECT_VENV_DIR="$venv_dir" \ + BASE_TEST_PRESERVE_PYTHON_ENTRYPOINT=1 \ BASE_CLI_RUNTIME_OWNER=base \ BASE_CLI_RUN_ID=failed-activation-run-id \ BASE_CLI_RUN_ROOT="$activation_run_root" \ @@ -376,6 +402,51 @@ EOF [[ "$output" == *"body-history-scope=unset"* ]] } +@test "Base runtime shell resolves activation sources with the Base virtual environment" { + local project_root="$TEST_TMPDIR/demo" + local project_venv_dir="$TEST_TMPDIR/demo-venv" + local base_venv_dir="$TEST_HOME/.base.d/base/.venv" + + mkdir -p "$project_root" "$project_venv_dir/bin" "$base_venv_dir/bin" + touch "$project_root/base_manifest.yaml" + cat > "$project_venv_dir/bin/activate" <<'EOF' +VIRTUAL_ENV="$BASE_PROJECT_VENV_DIR" +export VIRTUAL_ENV +EOF + cat > "$project_venv_dir/bin/python" <<'EOF' +#!/usr/bin/env bash +printf 'project virtual environment Python was used unexpectedly\n' >&2 +exit 1 +EOF + cat > "$base_venv_dir/bin/python" <<'EOF' +#!/usr/bin/env bash +source "${BASH_ENV:?}" +if [[ "${3:-}" == "base_projects" && "${4:-}" == "activation-sources" && "${5:-}" == "demo" ]]; then + base_test_protocol_begin activation-source 0 + base_test_protocol_end + exit 0 +fi +printf 'unexpected Base Python args: %s\n' "$*" >&2 +exit 1 +EOF + chmod +x "$project_venv_dir/bin/python" "$base_venv_dir/bin/python" + + run env \ + HOME="$TEST_HOME" \ + BASE_HOME="$BASE_REPO_ROOT" \ + BASE_PROJECT=demo \ + BASE_PROJECT_ROOT="$project_root" \ + BASE_PROJECT_MANIFEST="$project_root/base_manifest.yaml" \ + BASE_PROJECT_VENV_DIR="$project_venv_dir" \ + PATH="/usr/bin:/bin:/usr/sbin:/sbin" \ + "$BASH" --rcfile "$BASE_REPO_ROOT/lib/bash/runtime/bashrc" -i -c '\ + printf "VIRTUAL_ENV=%s\n" "$VIRTUAL_ENV"' + + [ "$status" -eq 0 ] + [[ "$output" == *"VIRTUAL_ENV=$project_venv_dir"* ]] + [[ "$output" != *"project virtual environment Python was used unexpectedly"* ]] +} + @test "Base runtime shell loads base_init before user bashrc and owns final prompt" { cat > "$TEST_HOME/.bashrc" <<'EOF' alias user_bashrc_alias='printf user-bashrc' diff --git a/lib/bash/runtime/bashrc b/lib/bash/runtime/bashrc index bb62ad75..790a05d5 100644 --- a/lib/bash/runtime/bashrc +++ b/lib/bash/runtime/bashrc @@ -115,7 +115,7 @@ _base_runtime_source_project_activation_scripts() { return 1 } - scripts_output="$("$wrapper" base_projects activation-sources "$BASE_PROJECT" --format command-protocol)" || { + scripts_output="$("$wrapper" --project base --preserve-project-context base_projects activation-sources "$BASE_PROJECT" --format command-protocol)" || { _base_runtime_error "Unable to resolve project activation scripts for '$BASE_PROJECT'." return 1 }