|
1 | 1 | #!/bin/bash |
| 2 | +# Copyright 2026 Google LLC |
| 3 | +# |
| 4 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +# you may not use this file except in compliance with the License. |
| 6 | +# You may obtain a copy of the License at |
| 7 | +# |
| 8 | +# https://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +# |
| 10 | +# Unless required by applicable law or agreed to in writing, software |
| 11 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +# See the License for the specific language governing permissions and |
| 14 | +# limitations under the License. |
| 15 | + |
2 | 16 | set -e |
3 | 17 |
|
| 18 | +# Avoid virtualenv/pip trying to download/upgrade tools from PyPI on host |
| 19 | +export VIRTUALENV_NO_DOWNLOAD=1 |
| 20 | +export PIP_DISABLE_PIP_VERSION_CHECK=1 |
| 21 | + |
| 22 | +# Pass these environment variables to the cibuildwheel Docker container |
| 23 | +export CIBW_ENVIRONMENT="VIRTUALENV_NO_DOWNLOAD=1 PIP_DISABLE_PIP_VERSION_CHECK=1" |
| 24 | +export CIBW_DEPENDENCY_VERSIONS="latest" |
| 25 | +export CIBW_CONTAINER_ENGINE_EXTRA_ARGS="--network=host" |
| 26 | + |
4 | 27 | # If running locally (not on Kokoro), authenticate with gcloud. |
5 | 28 | if [ -z "${KOKORO_BUILD_ID}" ]; then |
6 | 29 | if ! gcloud auth application-default print-access-token --quiet > /dev/null; then |
7 | 30 | gcloud auth application-default login |
8 | 31 | fi |
9 | 32 | fi |
10 | 33 |
|
11 | | -pip install -U keyring keyrings.google-artifactregistry-auth twine cibuildwheel |
| 34 | +# We use --no-cache-dir to force pip to download packages fresh and bypass the local |
| 35 | +# cache. In a sandboxed build environment, writing to the default cache directory |
| 36 | +# (~/.cache/pip) can encounter permission/sandbox restrictions or lead to stale |
| 37 | +# dependency resolution. Disabling the cache ensures a reliable, reproducible install. |
| 38 | +pip install --no-cache-dir -U keyring keyrings.google-artifactregistry-auth twine cibuildwheel |
| 39 | + |
| 40 | +# ============================================================================== |
| 41 | +# FUTURE-PROOF RUNTIME PATCHING OF CIBUILDWHEEL |
| 42 | +# ============================================================================== |
| 43 | +# To run cibuildwheel on Google's sandboxed RBE/Kokoro infrastructure, we must: |
| 44 | +# 1. Bypass RBE's stdout proxy buffering deadlock (requires 32KB padding). |
| 45 | +# 2. Bypass RBE's stdin EOF deadlock during copy-in (requires 'docker cp' |
| 46 | +# since we use disable_host_mount: True in pyproject.toml). |
| 47 | +# |
| 48 | +# Since cibuildwheel is installed fresh from PyPI on every build (ensuring we get |
| 49 | +# the latest security and feature updates), we apply these patches at runtime. |
| 50 | +# |
| 51 | +# Why this patching strategy is future-proof and safe: |
| 52 | +# - Strict Validation: The Python patcher strictly validates that all target |
| 53 | +# code blocks exist before applying replacements. If cibuildwheel's internal |
| 54 | +# code changes in a future release, the patcher will FAIL LOUDLY and exit the |
| 55 | +# build immediately (sys.exit(1)) rather than silently running a broken, |
| 56 | +# hanging build. |
| 57 | +# - Stable Boundaries: The copy_into patch uses a robust regular expression |
| 58 | +# anchored to class method boundaries (def copy_into -> def copy_out). These |
| 59 | +# are stable, long-standing internal APIs of cibuildwheel's OCIContainer. |
| 60 | +# - Core Protocol Stability: The buffering patches target the core protocol |
| 61 | +# used to communicate with the container's persistent bash shell. This |
| 62 | +# protocol is fundamental to cibuildwheel and highly unlikely to change. |
| 63 | +# ============================================================================== |
| 64 | +OCI_PATH=$(python3 -c "import cibuildwheel.oci_container; print(cibuildwheel.oci_container.__file__)") |
| 65 | +echo "Patching cibuildwheel at $OCI_PATH..." |
| 66 | + |
| 67 | +cat << 'EOF' > patch_oci.py |
| 68 | +import sys |
| 69 | +import re |
| 70 | +
|
| 71 | +path = sys.argv[1] |
| 72 | +with open(path, 'r') as f: |
| 73 | + content = f.read() |
| 74 | +
|
| 75 | +# 1. Force a 32KB flush at the end of every command execution |
| 76 | +target_write = 'printf "%04d%s\\n" $? {end_of_message}' |
| 77 | +replacement_write = 'printf "%04d%s\\n%32768s\\n" $? {end_of_message} " "' |
| 78 | +if target_write in content: |
| 79 | + content = content.replace(target_write, replacement_write) |
| 80 | + print("Patched write loop.") |
| 81 | +else: |
| 82 | + print("ERROR: Could not find write loop target in oci_container.py! The cibuildwheel version might have changed.") |
| 83 | + sys.exit(1) |
| 84 | +
|
| 85 | +# 2. Read and discard the 32KB padding to keep the stream clean |
| 86 | +target_read = """ # add the last line to output, without the footer |
| 87 | + output_io.write(line[0:footer_offset]) |
| 88 | + output_io.flush() |
| 89 | + break""" |
| 90 | +
|
| 91 | +replacement_read = """ # add the last line to output, without the footer |
| 92 | + output_io.write(line[0:footer_offset]) |
| 93 | + output_io.flush() |
| 94 | + # Read and discard the 32KB padding line to clear the stream! |
| 95 | + self.bash_stdout.readline() |
| 96 | + break""" |
| 97 | +
|
| 98 | +if target_read in content: |
| 99 | + content = content.replace(target_read, replacement_read) |
| 100 | + print("Patched read loop.") |
| 101 | +else: |
| 102 | + print("ERROR: Could not find read loop target in oci_container.py! The cibuildwheel version might have changed.") |
| 103 | + sys.exit(1) |
| 104 | +
|
| 105 | +# 3. Patch the entire copy_into method using a unique regex to use native 'docker cp'. |
| 106 | +# This bypasses the RBE stdin EOF deadlock when copying the project into the container. |
| 107 | +pattern = re.compile(r' def copy_into\(self,.*?\).*?:.*? def copy_out', re.DOTALL) |
| 108 | +
|
| 109 | +replacement_copy = """ def copy_into(self, from_path: Path, to_path: PurePath) -> None: |
| 110 | + if from_path.is_dir(): |
| 111 | + self.call(["mkdir", "-p", to_path]) |
| 112 | + subprocess.run( |
| 113 | + f"tar -c {self.host_tar_format} -f - . | {self.engine.name} exec -i {self.name} tar --no-same-owner -xC {shell_quote(to_path)} -f -", |
| 114 | + shell=True, |
| 115 | + check=True, |
| 116 | + cwd=from_path, |
| 117 | + ) |
| 118 | + else: |
| 119 | + self.call(["mkdir", "-p", to_path.parent]) |
| 120 | + # Use native docker cp to copy the file, avoiding stdin EOF deadlocks in RBE |
| 121 | + subprocess.run( |
| 122 | + [ |
| 123 | + self.engine.name, |
| 124 | + "cp", |
| 125 | + str(from_path), |
| 126 | + f"{self.name}:{to_path}", |
| 127 | + ], |
| 128 | + check=True, |
| 129 | + ) |
| 130 | +
|
| 131 | + def copy_out""" |
| 132 | +
|
| 133 | +if pattern.search(content): |
| 134 | + content = pattern.sub(replacement_copy, content) |
| 135 | + print("Patched copy_into method using unique regex.") |
| 136 | +else: |
| 137 | + print("ERROR: Could not find copy_into method boundary in oci_container.py! The cibuildwheel version might have changed.") |
| 138 | + sys.exit(1) |
| 139 | +
|
| 140 | +with open(path, 'w') as f: |
| 141 | + f.write(content) |
| 142 | +
|
| 143 | +print("Successfully patched oci_container.py!") |
| 144 | +EOF |
| 145 | + |
| 146 | +python3 patch_oci.py "$OCI_PATH" |
| 147 | +rm patch_oci.py |
| 148 | + |
| 149 | +# Verify that the patched file is syntactically valid Python |
| 150 | +echo "Verifying patched oci_container.py syntax..." |
| 151 | +python3 -m py_compile "$OCI_PATH" || { echo "ERROR: Patched oci_container.py is corrupted!"; exit 1; } |
| 152 | + |
| 153 | +REPO_DIR="" |
| 154 | +TMP_DIR="" |
| 155 | +cleanup() { |
| 156 | + echo "Cleaning up temporary directories..." |
| 157 | + [ -n "${REPO_DIR}" ] && rm -rf "${REPO_DIR}" |
| 158 | + [ -n "${TMP_DIR}" ] && rm -rf "${TMP_DIR}" |
| 159 | +} |
| 160 | +trap cleanup EXIT |
12 | 161 |
|
13 | 162 | REPO_DIR=$(mktemp -d) |
14 | 163 | echo "Created temporary directory: ${REPO_DIR}" |
15 | 164 |
|
16 | | -# Ensure the temporary directory is removed on script exit |
17 | | -trap 'echo "Cleaning up temporary directory: ${REPO_DIR}"; rm -rf "${REPO_DIR}"' EXIT |
18 | | - |
19 | 165 | if [ "${DRY_RUN}" = "true" ]; then |
20 | 166 | echo "[DRY RUN] Using local Kokoro clone instead of cloning main." |
21 | 167 | SRC_DIR="$(cd "$(dirname "$0")/../.." && pwd)" |
|
40 | 186 | VERSION=${VERSION#v} |
41 | 187 | echo "Building release for version: ${VERSION}" |
42 | 188 |
|
43 | | -TMP_DIR=$(mktemp -d) |
| 189 | +# Create the build directory inside the workspace volume (SRC_DIR) |
| 190 | +# instead of the ephemeral /tmp, so that the sibling container can |
| 191 | +# access it natively via volume propagation |
| 192 | +TMP_DIR="${SRC_DIR}/build_area" |
| 193 | +mkdir -p "${TMP_DIR}" |
44 | 194 | echo "Build directory: ${TMP_DIR}" |
45 | | - |
46 | | -# Add trap cleanup for TMP_DIR as well |
47 | | -trap 'echo "Cleaning up temporary directories: ${REPO_DIR} ${TMP_DIR}"; rm -rf "${REPO_DIR}" "${TMP_DIR}"' EXIT |
| 195 | +export TMPDIR="${TMP_DIR}/tmp" |
| 196 | +mkdir -p "${TMPDIR}" |
48 | 197 |
|
49 | 198 | pushd "${TMP_DIR}" |
50 | 199 |
|
51 | 200 | cp -r "${SRC_DIR}"/{*,.*} . 2>/dev/null || true |
52 | 201 | cp -r "${SRC_DIR}"/release/* . 2>/dev/null || true |
53 | 202 | rm -rf cel_expr_python/*_test.py |
54 | 203 |
|
| 204 | +echo "Downloading bazelisk on host..." |
| 205 | +curl -LO https://github.com/bazelbuild/bazelisk/releases/download/v1.19.0/bazelisk-linux-amd64 |
| 206 | +chmod +x bazelisk-linux-amd64 |
| 207 | + |
55 | 208 | # Check if pyproject.toml exists before running sed |
56 | 209 | if [ -f pyproject.toml ]; then |
57 | 210 | sed -i "" "s/\$VERSION/${VERSION}/g" pyproject.toml || sed -i "s/\$VERSION/${VERSION}/g" pyproject.toml |
58 | 211 | fi |
59 | 212 |
|
60 | | -echo "Running cibuildwheel: ${CIBWHEEL_BIN}" |
| 213 | +echo "Running cibuildwheel..." |
61 | 214 | # Default CIBWHEEL_BIN if not set |
62 | 215 | if [ -z "${CIBWHEEL_BIN}" ]; then |
63 | 216 | CIBWHEEL_BIN="python3 -m cibuildwheel" |
|
0 commit comments