diff --git a/benchmarking/nighthawk-ingress/run-dev.sh b/benchmarking/nighthawk-ingress/run-dev.sh index 36e3d564b..3f73b4104 100755 --- a/benchmarking/nighthawk-ingress/run-dev.sh +++ b/benchmarking/nighthawk-ingress/run-dev.sh @@ -59,6 +59,8 @@ done REPO_ROOT="$(git rev-parse --show-toplevel)" cd "${REPO_ROOT}" +source hack/util/venv.sh + # --- preflight (each failure prints its fix) --------------------------------- [[ -f .ate-dev-env.sh ]] || { echo "ERROR: .ate-dev-env.sh not found at repo root (see GKE Quickstart in the repo README)" >&2 @@ -101,8 +103,9 @@ echo " atespace: ${ATESPACE}" echo " dest: ${DEST}" # venv: importing orchestrator.py (defaults/rendering/patch) needs PyYAML. +# One package, no requirements.txt, so check it by import. +ensure_venv "${VENV}" if ! "${VENV}/bin/python" -c 'import yaml' 2>/dev/null; then - python3 -m venv "${VENV}" "${VENV}/bin/pip" install --quiet pyyaml || "${VENV}/bin/pip" install --quiet --index-url https://pypi.org/simple/ pyyaml fi diff --git a/hack/update/python-codegen.sh b/hack/update/python-codegen.sh index 146b356be..2f18bb6b1 100755 --- a/hack/update/python-codegen.sh +++ b/hack/update/python-codegen.sh @@ -34,10 +34,9 @@ OUT_DIR="benchmarking/locust/common" # exporters and google-cloud-storage for nothing. CODEGEN_DIR="benchmarking/locust/codegen" VENV_DIR="${CODEGEN_DIR}/venv" -if [ ! -d "$VENV_DIR" ]; then - echo "Creating virtual environment in $VENV_DIR..." - python3 -m venv "$VENV_DIR" -fi +source hack/util/venv.sh +ensure_venv "${VENV_DIR}" +venv_sync_requirements "${VENV_DIR}" "${CODEGEN_DIR}/requirements.txt" # generate_proto compiles a single .proto file into ${OUT_DIR}, prepends the # project's license header, and rewrites the generated grpc file's intra-package @@ -81,18 +80,10 @@ generate_proto() { } # Run inside a subshell so `activate` doesn't leak VIRTUAL_ENV/PATH back to -# the caller; the subshell inherits errexit/nounset/pipefail. pip skips -# already-installed packages, so the install is cheap on re-runs. +# the caller; the subshell inherits errexit/nounset/pipefail. ( echo "Activating virtual environment..." source "$VENV_DIR/bin/activate" - echo "Installing dependencies from ${CODEGEN_DIR}/requirements.txt..." - pip install --upgrade pip - # The requirement is pinned, so an existing venv already holding that exact - # version is left alone and a venv holding any other version is corrected. - # (An unpinned requirement would instead be satisfied by whatever is already - # installed, which is how the generated files came to drift.) - pip install -r "${CODEGEN_DIR}/requirements.txt" generate_proto "pkg/proto/ateapipb" "ateapi" generate_proto "internal/proto/glutton" "glutton" diff --git a/hack/util/venv.sh b/hack/util/venv.sh new file mode 100644 index 000000000..4d1efa5c3 --- /dev/null +++ b/hack/util/venv.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Shared helpers for the scripts that need a Python virtual environment. + +# venv_is_usable reports whether the venv at $1 can be activated and run. A +# directory alone does not mean usable: an interrupted create leaves no +# bin/activate, and an interpreter upgrade leaves bin/python3 dangling. +venv_is_usable() { + local venv="$1" + [[ -f "${venv}/bin/activate" ]] && [[ -x "${venv}/bin/python3" ]] && + "${venv}/bin/python3" -c '' >/dev/null 2>&1 +} + +# ensure_venv creates the venv at $1 unless a usable one is already there. +ensure_venv() { + local venv="$1" + + if venv_is_usable "${venv}"; then + return 0 + fi + if [[ -e "${venv}" ]]; then + echo "Recreating unusable virtual environment in ${venv}..." + else + echo "Creating virtual environment in ${venv}..." + fi + # --clear is required: plain `python3 -m venv` and --upgrade both leave a + # dangling bin/python3 in place. + python3 -m venv --clear "${venv}" + # Only on create: this one asks the index every run. + "${venv}/bin/pip" install --quiet --upgrade pip +} + +# venv_sync_requirements installs $2 (a requirements.txt) into the venv at $1. +# Cheap to repeat: pip satisfies an unchanged file from installed metadata +# without asking the index. +venv_sync_requirements() { + local venv="$1" + local requirements="$2" + "${venv}/bin/pip" install --quiet -r "${requirements}" +} diff --git a/hack/verify/python-licenses.sh b/hack/verify/python-licenses.sh index d81745763..2fb1b6323 100755 --- a/hack/verify/python-licenses.sh +++ b/hack/verify/python-licenses.sh @@ -27,6 +27,8 @@ set -o errexit -o nounset -o pipefail ROOT="$(git rev-parse --show-toplevel)" cd "${ROOT}" +source hack/util/venv.sh + # Python projects with a requirements.txt to check. Each entry is a directory # that contains requirements.txt; its venv lives at /venv. Discovered # dynamically from tracked/untracked (but not ignored) requirements.txt files, @@ -62,28 +64,17 @@ check_project() { # Run inside a subshell so `activate` doesn't leak VIRTUAL_ENV/PATH back # to the caller; the subshell inherits errexit/nounset/pipefail. ( - if [[ ! -d "${venv}" ]]; then - echo " Creating venv at ${venv}..." - python3 -m venv "${venv}" || { - echo "ERROR: failed to create venv at ${venv}" >&2 - exit 1 - } - echo " Activating ${venv}..." - source "${venv}/bin/activate" - echo " Upgrading pip in ${venv}..." - pip install --quiet --upgrade pip || { - echo "ERROR: failed to upgrade pip in ${venv}" >&2 - exit 1 - } - echo " Installing ${dir}/requirements.txt into ${venv}..." - pip install --quiet -r "${dir}/requirements.txt" || { - echo "ERROR: failed to install ${dir}/requirements.txt into ${venv}" >&2 - exit 1 - } - else - echo " Activating ${venv}..." - source "${venv}/bin/activate" - fi + ensure_venv "${venv}" || { + echo "ERROR: failed to create venv at ${venv}" >&2 + exit 1 + } + echo " Installing ${dir}/requirements.txt into ${venv}..." + venv_sync_requirements "${venv}" "${dir}/requirements.txt" || { + echo "ERROR: failed to install ${dir}/requirements.txt into ${venv}" >&2 + exit 1 + } + echo " Activating ${venv}..." + source "${venv}/bin/activate" if ! command -v pip-licenses >/dev/null 2>&1; then echo " Installing pip-licenses into ${venv}..." pip install --quiet pip-licenses || {