|
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 | + |
4 | 26 | # If running locally (not on Kokoro), authenticate with gcloud. |
5 | 27 | if [ -z "${KOKORO_BUILD_ID}" ]; then |
6 | 28 | if ! gcloud auth application-default print-access-token --quiet > /dev/null; then |
7 | 29 | gcloud auth application-default login |
8 | 30 | fi |
9 | 31 | fi |
10 | 32 |
|
11 | | -pip install -U keyring keyrings.google-artifactregistry-auth twine cibuildwheel |
| 33 | +# We use --no-cache-dir to force pip to download packages fresh and bypass the local |
| 34 | +# cache. In Kokoro/RBE sandboxed environments, writing to the default cache directory |
| 35 | +# (~/.cache/pip) can encounter permission/sandbox restrictions or lead to stale |
| 36 | +# dependency resolution. Disabling the cache ensures a reliable, reproducible install. |
| 37 | +pip install --no-cache-dir -U keyring keyrings.google-artifactregistry-auth twine cibuildwheel |
| 38 | + |
| 39 | +# Patch cibuildwheel at runtime to bypass the RBE stdout buffering deadlock. |
| 40 | +# The RBE proxy buffers the persistent container bash stdout. By appending a 4KB |
| 41 | +# padding line to the end of every command output, we force the proxy to flush the |
| 42 | +# buffer immediately. We then read and discard this padding to keep the stream clean. |
| 43 | +OCI_PATH=$(python3 -c "import cibuildwheel.oci_container; print(cibuildwheel.oci_container.__file__)") |
| 44 | +echo "Patching cibuildwheel at $OCI_PATH..." |
| 45 | + |
| 46 | +cat << 'EOF' > patch_oci.py |
| 47 | +import sys |
| 48 | +import re |
| 49 | +
|
| 50 | +path = sys.argv[1] |
| 51 | +with open(path, 'r') as f: |
| 52 | + content = f.read() |
| 53 | +
|
| 54 | +# 1. Force a 32KB flush at the end of every command execution |
| 55 | +target_write = 'printf "%04d%s\\n" $? {end_of_message}' |
| 56 | +replacement_write = 'printf "%04d%s\\n%32768s\\n" $? {end_of_message} " "' |
| 57 | +if target_write in content: |
| 58 | + content = content.replace(target_write, replacement_write) |
| 59 | + print("Patched write loop.") |
| 60 | +
|
| 61 | +# 2. Read and discard the 32KB padding to keep the stream clean |
| 62 | +target_read = """ # add the last line to output, without the footer |
| 63 | + output_io.write(line[0:footer_offset]) |
| 64 | + output_io.flush() |
| 65 | + break""" |
| 66 | +
|
| 67 | +replacement_read = """ # add the last line to output, without the footer |
| 68 | + output_io.write(line[0:footer_offset]) |
| 69 | + output_io.flush() |
| 70 | + # Read and discard the 32KB padding line to clear the stream! |
| 71 | + self.bash_stdout.readline() |
| 72 | + break""" |
| 73 | +
|
| 74 | +if target_read in content: |
| 75 | + content = content.replace(target_read, replacement_read) |
| 76 | + print("Patched read loop.") |
| 77 | +
|
| 78 | +# 3. Patch the entire copy_into method using a unique regex to use native 'docker cp'. |
| 79 | +# This bypasses the RBE stdin EOF deadlock when copying the project into the container. |
| 80 | +pattern = re.compile(r' def copy_into\(self,.*?\).*?:.*? def copy_out', re.DOTALL) |
| 81 | +
|
| 82 | +replacement_copy = """ def copy_into(self, from_path: Path, to_path: PurePath) -> None: |
| 83 | + if from_path.is_dir(): |
| 84 | + self.call(["mkdir", "-p", to_path]) |
| 85 | + subprocess.run( |
| 86 | + 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 -", |
| 87 | + shell=True, |
| 88 | + check=True, |
| 89 | + cwd=from_path, |
| 90 | + ) |
| 91 | + else: |
| 92 | + self.call(["mkdir", "-p", to_path.parent]) |
| 93 | + # Use native docker cp to copy the file, avoiding stdin EOF deadlocks in RBE |
| 94 | + subprocess.run( |
| 95 | + [ |
| 96 | + self.engine.name, |
| 97 | + "cp", |
| 98 | + str(from_path), |
| 99 | + f"{self.name}:{to_path}", |
| 100 | + ], |
| 101 | + check=True, |
| 102 | + ) |
| 103 | +
|
| 104 | + def copy_out""" |
| 105 | +
|
| 106 | +if pattern.search(content): |
| 107 | + content = pattern.sub(replacement_copy, content) |
| 108 | + print("Patched copy_into method using unique regex.") |
| 109 | +else: |
| 110 | + print("Error: copy_into method pattern not found!") |
| 111 | + sys.exit(1) |
| 112 | +
|
| 113 | +with open(path, 'w') as f: |
| 114 | + f.write(content) |
| 115 | +
|
| 116 | +print("Successfully patched oci_container.py!") |
| 117 | +EOF |
| 118 | + |
| 119 | +python3 patch_oci.py "$OCI_PATH" |
| 120 | +rm patch_oci.py |
| 121 | + |
| 122 | +# Verify that the patched file is syntactically valid Python |
| 123 | +echo "Verifying patched oci_container.py syntax..." |
| 124 | +python3 -m py_compile "$OCI_PATH" || { echo "ERROR: Patched oci_container.py is corrupted!"; exit 1; } |
| 125 | + |
| 126 | +REPO_DIR="" |
| 127 | +TMP_DIR="" |
| 128 | +cleanup() { |
| 129 | + echo "Cleaning up temporary directories..." |
| 130 | + [ -n "${REPO_DIR}" ] && rm -rf "${REPO_DIR}" |
| 131 | + [ -n "${TMP_DIR}" ] && rm -rf "${TMP_DIR}" |
| 132 | +} |
| 133 | +trap cleanup EXIT |
12 | 134 |
|
13 | 135 | REPO_DIR=$(mktemp -d) |
14 | 136 | echo "Created temporary directory: ${REPO_DIR}" |
15 | 137 |
|
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 | 138 | if [ "${DRY_RUN}" = "true" ]; then |
20 | 139 | echo "[DRY RUN] Using local Kokoro clone instead of cloning main." |
21 | 140 | SRC_DIR="$(cd "$(dirname "$0")/../.." && pwd)" |
|
40 | 159 | VERSION=${VERSION#v} |
41 | 160 | echo "Building release for version: ${VERSION}" |
42 | 161 |
|
43 | | -TMP_DIR=$(mktemp -d) |
| 162 | +# Create the build directory inside the workspace volume (SRC_DIR) |
| 163 | +# instead of the ephemeral /tmp, so that the sibling container can |
| 164 | +# access it natively via volume propagation! |
| 165 | +TMP_DIR="${SRC_DIR}/build_area" |
| 166 | +mkdir -p "${TMP_DIR}" |
44 | 167 | echo "Build directory: ${TMP_DIR}" |
45 | 168 |
|
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 |
| 169 | +# Force cibuildwheel to create all its temporary directories (like test_cwd) |
| 170 | +# inside this volume as well, avoiding any host-mount mismatches! |
| 171 | +export TMPDIR="${TMP_DIR}/tmp" |
| 172 | +mkdir -p "${TMPDIR}" |
48 | 173 |
|
49 | 174 | pushd "${TMP_DIR}" |
50 | 175 |
|
51 | 176 | cp -r "${SRC_DIR}"/{*,.*} . 2>/dev/null || true |
52 | 177 | cp -r "${SRC_DIR}"/release/* . 2>/dev/null || true |
53 | 178 | rm -rf cel_expr_python/*_test.py |
54 | 179 |
|
| 180 | +echo "Downloading bazelisk on host..." |
| 181 | +curl -LO https://github.com/bazelbuild/bazelisk/releases/download/v1.19.0/bazelisk-linux-amd64 |
| 182 | +chmod +x bazelisk-linux-amd64 |
| 183 | + |
55 | 184 | # Check if pyproject.toml exists before running sed |
56 | 185 | if [ -f pyproject.toml ]; then |
57 | 186 | sed -i "" "s/\$VERSION/${VERSION}/g" pyproject.toml || sed -i "s/\$VERSION/${VERSION}/g" pyproject.toml |
58 | 187 | fi |
59 | 188 |
|
60 | | -echo "Running cibuildwheel: ${CIBWHEEL_BIN}" |
| 189 | +# We use disable_host_mount: True in pyproject.toml to completely avoid host mounts, |
| 190 | +# so we do not need to propagate volumes anymore. Just use host networking. |
| 191 | +export CIBW_CONTAINER_ENGINE_EXTRA_ARGS="--network=host" |
| 192 | + |
| 193 | +echo "Running cibuildwheel..." |
61 | 194 | # Default CIBWHEEL_BIN if not set |
62 | 195 | if [ -z "${CIBWHEEL_BIN}" ]; then |
63 | 196 | CIBWHEEL_BIN="python3 -m cibuildwheel" |
64 | 197 | fi |
| 198 | + |
| 199 | +# Run cibuildwheel synchronously. Output goes directly to the console in real-time. |
65 | 200 | ${CIBWHEEL_BIN} --platform linux --output-dir dist |
66 | 201 |
|
67 | 202 | if [ "${DRY_RUN}" = "true" ]; then |
|
0 commit comments