diff --git a/CHANGELOG.md b/CHANGELOG.md index 783342c..c9f501d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and Base versions are tracked in the repo-root `VERSION` file. ### Changed +- Suppressed pip self-upgrade notices during Base-managed pip installs so + setup output stays focused on Base actions and real install failures. - Made live workflow docs and installer examples main-ready, using default-branch URLs for raw GitHub install scripts and `main` for contributor branch examples. diff --git a/cli/bash/commands/basectl/subcommands/setup_common.sh b/cli/bash/commands/basectl/subcommands/setup_common.sh index 8943465..513a304 100644 --- a/cli/bash/commands/basectl/subcommands/setup_common.sh +++ b/cli/bash/commands/basectl/subcommands/setup_common.sh @@ -713,7 +713,7 @@ setup_install_base_python_package() { python_bin="$(setup_base_venv_python_bin "$venv_dir")" || fatal_error "Base virtual environment Python was not found at '$venv_dir/bin/python'. $(setup_recovery_venv)" log_info "Installing Python package '$package' in the Base virtual environment." - run "$python_bin" -m pip install "$package" + run "$python_bin" -m pip install --disable-pip-version-check "$package" } setup_install_pyyaml() { diff --git a/cli/bash/commands/basectl/tests/setup.bats b/cli/bash/commands/basectl/tests/setup.bats index f7ca66d..37e9c60 100644 --- a/cli/bash/commands/basectl/tests/setup.bats +++ b/cli/bash/commands/basectl/tests/setup.bats @@ -61,7 +61,7 @@ if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "show" && "${4:-}" == [[ -f "${BASE_SETUP_TEST_STATE_DIR:?}/pyyaml-installed" ]] exit $? fi -if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "install" && "${4:-}" == "$pyyaml_package" ]]; then +if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "install" && "${4:-}" == "--disable-pip-version-check" && "${5:-}" == "$pyyaml_package" ]]; then touch "${BASE_SETUP_TEST_STATE_DIR:?}/pyyaml-install-ran" touch "${BASE_SETUP_TEST_STATE_DIR:?}/pyyaml-installed" exit 0 @@ -70,7 +70,7 @@ if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "show" && "${4:-}" == [[ -f "${BASE_SETUP_TEST_STATE_DIR:?}/click-installed" ]] exit $? fi -if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "install" && "${4:-}" == "$click_package" ]]; then +if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "install" && "${4:-}" == "--disable-pip-version-check" && "${5:-}" == "$click_package" ]]; then touch "${BASE_SETUP_TEST_STATE_DIR:?}/click-install-ran" touch "${BASE_SETUP_TEST_STATE_DIR:?}/click-installed" exit 0 @@ -120,6 +120,41 @@ EOF [[ "$(cat "$TEST_STATE_DIR/project-setup-python")" != *"$inherited_venv/bin/python"* ]] } +@test "setup installs Base Python packages without pip self-version notices" { + local bash_libs_dir + local venv_dir="$TEST_HOME/.base.d/base/.venv" + + bash_libs_dir="$(base_bash_libs_fixture_dir)" + mkdir -p "$venv_dir/bin" + : > "$venv_dir/pyvenv.cfg" + cat > "$venv_dir/bin/python" <<'EOF' +#!/usr/bin/env bash +if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "show" ]]; then + exit 1 +fi +if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "install" ]]; then + printf '%s\n' "$@" > "${BASE_SETUP_TEST_STATE_DIR:?}/pip-install-args" + exit 0 +fi +printf 'unexpected venv python args: %s\n' "$*" >&2 +exit 1 +EOF + chmod +x "$venv_dir/bin/python" + + run env \ + HOME="$TEST_HOME" \ + PATH="$TEST_MOCKBIN:$TEST_BASH_BIN_DIR:/usr/bin:/bin:/usr/sbin:/sbin" \ + OSTYPE=darwin24 \ + BASE_HOME="$BASE_REPO_ROOT" \ + BASE_BASH_LIBS_DIR="$bash_libs_dir" \ + BASE_SETUP_VENV_DIR="$venv_dir" \ + BASE_SETUP_TEST_STATE_DIR="$TEST_STATE_DIR" \ + bash -c 'source "$BASE_HOME/base_init.sh"; source "$BASE_HOME/cli/bash/commands/basectl/subcommands/setup_common.sh"; setup_install_base_python_package requests' + + [ "$status" -eq 0 ] + grep -Fxq -- "--disable-pip-version-check" "$TEST_STATE_DIR/pip-install-args" +} + @test "basectl setup installs missing dependencies and creates the Base virtual environment" { local installer local venv_dir="$TEST_HOME/.base.d/base/.venv" diff --git a/cli/bash/commands/basectl/tests/setup_helpers.bash b/cli/bash/commands/basectl/tests/setup_helpers.bash index 8f8c098..7bec1fc 100644 --- a/cli/bash/commands/basectl/tests/setup_helpers.bash +++ b/cli/bash/commands/basectl/tests/setup_helpers.bash @@ -102,7 +102,7 @@ if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "show" && "${4:-}" == [[ -f "${BASE_SETUP_TEST_STATE_DIR:?}/pyyaml-installed" ]] exit $? fi -if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "install" && "${4:-}" == "$pyyaml_package" ]]; then +if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "install" && "${4:-}" == "--disable-pip-version-check" && "${5:-}" == "$pyyaml_package" ]]; then touch "${BASE_SETUP_TEST_STATE_DIR:?}/pyyaml-install-ran" touch "${BASE_SETUP_TEST_STATE_DIR:?}/pyyaml-installed" exit 0 @@ -111,7 +111,7 @@ if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "show" && "${4:-}" == [[ -f "${BASE_SETUP_TEST_STATE_DIR:?}/click-installed" ]] exit $? fi -if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "install" && "${4:-}" == "$click_package" ]]; then +if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "install" && "${4:-}" == "--disable-pip-version-check" && "${5:-}" == "$click_package" ]]; then touch "${BASE_SETUP_TEST_STATE_DIR:?}/click-install-ran" touch "${BASE_SETUP_TEST_STATE_DIR:?}/click-installed" exit 0 @@ -198,7 +198,7 @@ if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "show" && "${4:-}" == [[ -f "${BASE_SETUP_TEST_STATE_DIR:?}/pyyaml-installed" ]] exit $? fi -if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "install" && "${4:-}" == "$pyyaml_package" ]]; then +if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "install" && "${4:-}" == "--disable-pip-version-check" && "${5:-}" == "$pyyaml_package" ]]; then touch "${BASE_SETUP_TEST_STATE_DIR:?}/pyyaml-install-ran" touch "${BASE_SETUP_TEST_STATE_DIR:?}/pyyaml-installed" exit 0 @@ -207,7 +207,7 @@ if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "show" && "${4:-}" == [[ -f "${BASE_SETUP_TEST_STATE_DIR:?}/click-installed" ]] exit $? fi -if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "install" && "${4:-}" == "$click_package" ]]; then +if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "install" && "${4:-}" == "--disable-pip-version-check" && "${5:-}" == "$click_package" ]]; then touch "${BASE_SETUP_TEST_STATE_DIR:?}/click-install-ran" touch "${BASE_SETUP_TEST_STATE_DIR:?}/click-installed" exit 0 @@ -346,7 +346,7 @@ if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "show" && "${4:-}" == [[ -f "${BASE_SETUP_TEST_STATE_DIR:?}/pyyaml-installed" ]] exit $? fi -if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "install" && "${4:-}" == "$pyyaml_package" ]]; then +if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "install" && "${4:-}" == "--disable-pip-version-check" && "${5:-}" == "$pyyaml_package" ]]; then touch "${BASE_SETUP_TEST_STATE_DIR:?}/pyyaml-install-ran" touch "${BASE_SETUP_TEST_STATE_DIR:?}/pyyaml-installed" exit 0 @@ -355,7 +355,7 @@ if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "show" && "${4:-}" == [[ -f "${BASE_SETUP_TEST_STATE_DIR:?}/click-installed" ]] exit $? fi -if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "install" && "${4:-}" == "$click_package" ]]; then +if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "install" && "${4:-}" == "--disable-pip-version-check" && "${5:-}" == "$click_package" ]]; then touch "${BASE_SETUP_TEST_STATE_DIR:?}/click-install-ran" touch "${BASE_SETUP_TEST_STATE_DIR:?}/click-installed" exit 0 diff --git a/cli/python/base_setup/artifacts.py b/cli/python/base_setup/artifacts.py index 133b526..6e18d6f 100644 --- a/cli/python/base_setup/artifacts.py +++ b/cli/python/base_setup/artifacts.py @@ -3,6 +3,7 @@ import os import subprocess import venv +from collections.abc import Iterable from pathlib import Path import base_cli @@ -13,6 +14,8 @@ from .manifest import ArtifactRequest from .registry import ArtifactDefinition, get_artifact_definition +PIP_INSTALL_COMMAND_PREFIX = ("-m", "pip", "install", "--disable-pip-version-check") + def homebrew_no_auto_update_env() -> dict[str, str]: env = os.environ.copy() @@ -309,7 +312,7 @@ def reconcile_python_artifacts( if dry_run: if not python_bin.exists(): ctx.log.info("[DRY-RUN] Would create project virtual environment at '%s'.", venv_dir) - process.dry_run_command(ctx, [str(python_bin), "-m", "pip", "install", *requirements]) + process.dry_run_command(ctx, pip_install_command(python_bin, requirements)) return if not python_bin.exists(): @@ -318,7 +321,7 @@ def reconcile_python_artifacts( names = ", ".join(definition.name for definition, _version, _requirement in missing) ctx.log.info("Installing Python artifacts into project virtual environment: %s.", names) - command = [str(python_bin), "-m", "pip", "install", *requirements] + command = pip_install_command(python_bin, requirements) try: process.run_command(ctx, command) except ArtifactError as exc: @@ -342,13 +345,17 @@ def reconcile_python_artifacts_sequential( ) continue ctx.log.info("Installing Python artifact '%s' into project virtual environment.", definition.name) - process.run_command(ctx, [str(python_bin), "-m", "pip", "install", requirement]) + process.run_command(ctx, pip_install_command(python_bin, (requirement,))) def python_requirement(definition: ArtifactDefinition, version: str) -> str: return f"{definition.package}=={version}" if version != "latest" else definition.package +def pip_install_command(python_bin: Path, requirements: Iterable[str]) -> list[str]: + return [str(python_bin), *PIP_INSTALL_COMMAND_PREFIX, *requirements] + + def project_venv_dir(project: str) -> Path: override = os.environ.get("BASE_PROJECT_VENV_DIR") if override: diff --git a/cli/python/base_setup/tests/test_artifacts.py b/cli/python/base_setup/tests/test_artifacts.py index 9108079..0f0fa5e 100644 --- a/cli/python/base_setup/tests/test_artifacts.py +++ b/cli/python/base_setup/tests/test_artifacts.py @@ -181,7 +181,7 @@ def test_unknown_python_package_artifact_dry_run_uses_pip(self) -> None: status, _stdout, stderr = run_engine(["--dry-run", "--manifest", str(manifest_path)]) self.assertEqual(status, 0) - self.assertIn("pip install click==8.4.1 PyYAML==6.0.3 rich", stderr) + self.assertIn("pip install --disable-pip-version-check click==8.4.1 PyYAML==6.0.3 rich", stderr) @unittest.skipUnless(importlib.util.find_spec("click"), "Click is not installed") def test_dry_run_ignores_inherited_project_runtime_environment(self) -> None: @@ -217,7 +217,7 @@ def test_dry_run_ignores_inherited_project_runtime_environment(self) -> None: self.assertEqual(status, 0) self.assertNotIn(str(inherited_venv_dir), stderr) - self.assertIn("pip install click==8.4.1 PyYAML==6.0.3 rich", stderr) + self.assertIn("pip install --disable-pip-version-check click==8.4.1 PyYAML==6.0.3 rich", stderr) @unittest.skipUnless(importlib.util.find_spec("click"), "Click is not installed") def test_known_homebrew_artifact_dry_run_does_not_require_brew(self) -> None: @@ -392,7 +392,7 @@ def test_python_artifact_honors_project_venv_dir_override(self) -> None: info_messages, ) self.assertIn( - f"[DRY-RUN] Would run: {venv_dir}/bin/python -m pip install requests", + f"[DRY-RUN] Would run: {venv_dir}/bin/python -m pip install --disable-pip-version-check requests", info_messages, ) @@ -444,7 +444,15 @@ def test_reconcile_artifacts_batches_python_installs(self) -> None: run_command.assert_called_once_with( ctx, - [str(python_bin), "-m", "pip", "install", "click==8.4.1", "requests"], + [ + str(python_bin), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "click==8.4.1", + "requests", + ], ) def test_reconcile_artifacts_retries_python_installs_sequentially_after_batch_failure(self) -> None: @@ -480,9 +488,40 @@ def test_reconcile_artifacts_retries_python_installs_sequentially_after_batch_fa self.assertEqual( run_command.call_args_list, [ - mock.call(ctx, [str(python_bin), "-m", "pip", "install", "click==8.4.1", "requests"]), - mock.call(ctx, [str(python_bin), "-m", "pip", "install", "click==8.4.1"]), - mock.call(ctx, [str(python_bin), "-m", "pip", "install", "requests"]), + mock.call( + ctx, + [ + str(python_bin), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "click==8.4.1", + "requests", + ], + ), + mock.call( + ctx, + [ + str(python_bin), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "click==8.4.1", + ], + ), + mock.call( + ctx, + [ + str(python_bin), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "requests", + ], + ), ], ) ctx.log.warning.assert_called_once_with(