Skip to content

Commit c9ba49c

Browse files
committed
prototype migrate off of bazel
1 parent 6405a6e commit c9ba49c

File tree

3 files changed

+188
-214
lines changed

3 files changed

+188
-214
lines changed

.generator/Dockerfile

Lines changed: 32 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -68,44 +68,9 @@ RUN wget --no-check-certificate -O /tmp/get-pip.py 'https://bootstrap.pypa.io/ge
6868
done && \
6969
rm /tmp/get-pip.py
7070

71-
# Install Bazelisk
72-
RUN wget https://github.com/bazelbuild/bazelisk/releases/download/${BAZELISK_VERSION}/bazelisk-linux-amd64 -O /usr/local/bin/bazelisk && \
73-
chmod +x /usr/local/bin/bazelisk
74-
75-
# Set the working directory for build-related tasks.
76-
WORKDIR /app
77-
78-
# Create the group and user, but only if they don't already exist.
79-
ARG UID=1000
80-
ARG GID=1000
81-
82-
RUN if ! getent group $GID > /dev/null; then \
83-
groupadd -g $GID myuser; \
84-
fi && \
85-
if ! getent passwd $UID > /dev/null; then \
86-
useradd -u $UID -g $GID -ms /bin/bash myuser; \
87-
fi
88-
89-
# Set ownership of the app directory now, before we copy files into it.
90-
RUN mkdir -p /app && chown $UID:$GID /app
91-
92-
# We'll point both to the /bazel_cache directory which will be mounted as a volume.
93-
ENV BAZELISK_HOME="/bazel_cache/bazelisk"
94-
ENV BAZEL_HOME="/bazel_cache/bazel"
95-
96-
# Ensure the cache directories within the non-root user's context exist and are writable.
97-
# This is crucial as Bazel creates subdirectories under BAZEL_HOME.
98-
RUN mkdir -p ${BAZEL_HOME}/_bazel_ubuntu/cache/repos \
99-
${BAZEL_HOME}/_bazel_ubuntu/output_base \
100-
${BAZELISK_HOME} && \
101-
chown -R $UID:$GID ${BAZEL_HOME} ${BAZELISK_HOME}
102-
10371
RUN /usr/local/bin/python3.9 -m venv bazel_env
10472
RUN . bazel_env/bin/activate
10573

106-
RUN git clone https://github.com/googleapis/googleapis.git \
107-
&& cd googleapis \
108-
&& bazelisk --output_base=/bazel_cache/_bazel_ubuntu/output_base build --disk_cache=/bazel_cache/_bazel_ubuntu/cache/repos --incompatible_strict_action_env //google/cloud/language/v1:language-v1-py
10974

11075
# TODO(https://github.com/googleapis/librarian/issues/904): Install protoc for gencode.
11176

@@ -120,25 +85,14 @@ RUN apt-get update && \
12085
apt-get install -y --no-install-recommends \
12186
ca-certificates \
12287
git \
123-
libssl3 \
124-
zlib1g \
125-
libbz2-1.0 \
126-
libffi8 \
127-
libsqlite3-0 \
128-
libreadline8 \
129-
# For running bazelisk commands
130-
openjdk-17-jdk \
131-
# To avoid bazel error
132-
# "python interpreter `python3` not found in PATH"
133-
python3-dev \
134-
# To avoid bazel error
135-
# "Cannot find gcc or CC; either correct your path or set the CC environment variable"
136-
build-essential \
137-
# To avoid bazel error
138-
# unzip command not found
139-
unzip \
140-
&& apt-get clean && \
141-
rm -rf /var/lib/apt/lists/*
88+
pandoc \
89+
wget \
90+
unzip \
91+
zip \
92+
&& apt-get clean autoclean \
93+
&& apt-get autoremove -y \
94+
&& rm -rf /var/lib/apt/lists/* \
95+
&& rm -f /var/cache/apt/archives/*.deb
14296

14397
# Copy all Python interpreters, their pip executables, and their standard libraries from the builder.
14498
COPY --from=builder /usr/local/bin/python3.9 /usr/local/bin/
@@ -151,29 +105,40 @@ COPY --from=builder /usr/local/lib/python3.10 /usr/local/lib/python3.10
151105
COPY --from=builder /usr/local/bin/python3.13 /usr/local/bin/
152106
COPY --from=builder /usr/local/lib/python3.13 /usr/local/lib/python3.13
153107

154-
# Copy the bazelisk executable from the builder.
155-
COPY --from=builder /usr/local/bin/bazelisk /usr/local/bin/
156-
157-
# Copy bazel cache from the builder.
158-
COPY --from=builder /bazel_cache /bazel_cache
159-
RUN chmod -R 777 /bazel_cache
108+
# Download/install protoc
109+
RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v25.3/protoc-25.3-linux-x86_64.zip
110+
RUN unzip protoc-25.3-linux-x86_64.zip -d protoc
111+
RUN mv protoc/bin/* /usr/local/bin/
112+
RUN mv protoc/include/* /usr/local/include/
113+
RUN chmod +x /usr/local/bin/protoc
114+
ENV PATH="/usr/bin:${PATH}"
160115

161116
# Set the working directory in the container.
162117
WORKDIR /app
163118

164-
# Create a virtual env and set the Path to fix the missing nox error
165-
# when running the post processor changes.
166-
RUN /usr/local/bin/python3.9 -m venv bazel_env
167-
RUN . bazel_env/bin/activate
168119

169-
ENV PATH=/app/bazel_env/bin:$PATH
120+
# TODO: move to requirements.txt file
121+
RUN python3.9 -m pip install click black==23.7.0 isort==5.11.0
122+
RUN git clone --depth 1 https://github.com/googleapis/gapic-generator-python.git /tmp/gapic-generator-python && \
123+
python3.9 -m pip install /tmp/gapic-generator-python && \
124+
rm -rf /tmp/gapic-generator-python
170125

171126
RUN git clone --depth 1 https://github.com/googleapis/synthtool.git /tmp/synthtool && \
172-
bazel_env/bin/python3.9 -m pip install /tmp/synthtool nox && \
173-
rm -rf /tmp/synthtool
127+
python3.9 -m pip install /tmp/synthtool nox starlark-pyo3>=2025.1
174128

175129
# Copy the CLI script into the container.
176130
COPY .generator/cli.py .
177131
RUN chmod a+rx ./cli.py
178132

133+
COPY .generator/parse_googleapis_content.py .
134+
RUN chmod a+rx ./parse_googleapis_content.py
135+
136+
RUN mkdir -p /.cache/synthtool/synthtool
137+
RUN find /.cache -type d -exec chmod a+x {} \;
138+
139+
# Tell synthtool to pull templates from this docker image instead of from
140+
# the live repo.
141+
ENV SYNTHTOOL_TEMPLATES="/tmp/synthtool/synthtool/gcp/templates"
142+
143+
179144
ENTRYPOINT ["python3.9", "./cli.py"]

.generator/cli.py

Lines changed: 52 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818
import json
1919
import logging
2020
import os
21+
import parse_googleapis_content
2122
import re
2223
import shutil
2324
import subprocess
2425
import sys
2526
import yaml
2627
from datetime import datetime
28+
import tempfile
2729
from pathlib import Path
2830
from typing import Dict, List
2931

@@ -110,47 +112,7 @@ def _write_json_file(path: str, updated_content: Dict):
110112

111113
def handle_configure():
112114
# TODO(https://github.com/googleapis/librarian/issues/466): Implement configure command and update docstring.
113-
logger.info("'configure' command executed.")
114-
115-
116-
def _determine_bazel_rule(api_path: str, source: str) -> str:
117-
"""Finds a Bazel rule by parsing the BUILD.bazel file directly.
118-
119-
Args:
120-
api_path (str): The API path, e.g., 'google/cloud/language/v1'.
121-
source(str): The path to the root of the Bazel workspace.
122-
123-
Returns:
124-
str: The discovered Bazel rule, e.g., '//google/cloud/language/v1:language-v1-py'.
125-
126-
Raises:
127-
ValueError: If the file can't be processed or no matching rule is found.
128-
"""
129-
logger.info(f"Determining Bazel rule for api_path: '{api_path}' by parsing file.")
130-
try:
131-
build_file_path = os.path.join(source, api_path, "BUILD.bazel")
132-
133-
with open(build_file_path, "r") as f:
134-
content = f.read()
135-
136-
match = re.search(r'name\s*=\s*"([^"]+-py)"', content)
137-
138-
# This check is for a logical failure (no match), not a runtime exception.
139-
# It's good to keep it for clear error messaging.
140-
if not match: # pragma: NO COVER
141-
raise ValueError(
142-
f"No Bazel rule with a name ending in '-py' found in {build_file_path}"
143-
)
144-
145-
rule_name = match.group(1)
146-
bazel_rule = f"//{api_path}:{rule_name}"
147-
148-
logger.info(f"Found Bazel rule: {bazel_rule}")
149-
return bazel_rule
150-
except Exception as e:
151-
raise ValueError(
152-
f"Failed to determine Bazel rule for '{api_path}' by parsing."
153-
) from e
115+
logger.info("'configure' command executed.")
154116

155117

156118
def _get_library_id(request_data: Dict) -> str:
@@ -171,107 +133,6 @@ def _get_library_id(request_data: Dict) -> str:
171133
return library_id
172134

173135

174-
def _build_bazel_target(bazel_rule: str, source: str):
175-
"""Executes `bazelisk build` on a given Bazel rule.
176-
177-
Args:
178-
bazel_rule(str): The Bazel rule to build.
179-
source(str): The path to the root of the Bazel workspace.
180-
181-
Raises:
182-
ValueError: If the subprocess call fails.
183-
"""
184-
logger.info(f"Executing build for rule: {bazel_rule}")
185-
try:
186-
# We're using the prewarmed bazel cache from the docker image to speed up the bazelisk commands.
187-
# Previously built artifacts are stored in `/bazel_cache/_bazel_ubuntu/output_base` and will be
188-
# used to speed up the build. `disk_cache` is used as the 'remote cache' and is also prewarmed as part of
189-
# the docker image.
190-
# See https://bazel.build/remote/caching#disk-cache which explains using a file system as a 'remote cache'.
191-
command = [
192-
"bazelisk",
193-
"--output_base=/bazel_cache/_bazel_ubuntu/output_base",
194-
"build",
195-
"--disk_cache=/bazel_cache/_bazel_ubuntu/cache/repos",
196-
"--incompatible_strict_action_env",
197-
bazel_rule,
198-
]
199-
subprocess.run(
200-
command,
201-
cwd=source,
202-
text=True,
203-
check=True,
204-
)
205-
logger.info(f"Bazel build for {bazel_rule} rule completed successfully.")
206-
except Exception as e:
207-
raise ValueError(f"Bazel build for {bazel_rule} rule failed.") from e
208-
209-
210-
def _locate_and_extract_artifact(
211-
bazel_rule: str,
212-
library_id: str,
213-
source: str,
214-
output: str,
215-
api_path: str,
216-
):
217-
"""Finds and extracts the tarball artifact from a Bazel build.
218-
219-
Args:
220-
bazel_rule(str): The Bazel rule that was built.
221-
library_id(str): The ID of the library being generated.
222-
source(str): The path to the root of the Bazel workspace.
223-
output(str): The path to the location where generated output
224-
should be stored.
225-
api_path(str): The API path for the artifact
226-
227-
Raises:
228-
ValueError: If failed to locate or extract artifact.
229-
"""
230-
try:
231-
# 1. Find the bazel-bin output directory.
232-
logger.info("Locating Bazel output directory...")
233-
# Previously built artifacts are stored in `/bazel_cache/_bazel_ubuntu/output_base`.
234-
# See `--output_base` in `_build_bazel_target`
235-
info_command = [
236-
"bazelisk",
237-
"--output_base=/bazel_cache/_bazel_ubuntu/output_base",
238-
"info",
239-
"bazel-bin",
240-
]
241-
result = subprocess.run(
242-
info_command,
243-
cwd=source,
244-
text=True,
245-
check=True,
246-
capture_output=True,
247-
)
248-
bazel_bin_path = result.stdout.strip()
249-
250-
# 2. Construct the path to the generated tarball.
251-
rule_path, rule_name = bazel_rule.split(":")
252-
tarball_name = f"{rule_name}.tar.gz"
253-
tarball_path = os.path.join(bazel_bin_path, rule_path.strip("/"), tarball_name)
254-
logger.info(f"Found artifact at: {tarball_path}")
255-
256-
# 3. Create a staging directory.
257-
api_version = api_path.split("/")[-1]
258-
staging_dir = os.path.join(output, "owl-bot-staging", library_id, api_version)
259-
os.makedirs(staging_dir, exist_ok=True)
260-
logger.info(f"Preparing staging directory: {staging_dir}")
261-
262-
# 4. Extract the artifact.
263-
extract_command = ["tar", "-xvf", tarball_path, "--strip-components=1"]
264-
subprocess.run(
265-
extract_command, cwd=staging_dir, capture_output=True, text=True, check=True
266-
)
267-
logger.info(f"Artifact {tarball_path} extracted successfully.")
268-
269-
except Exception as e:
270-
raise ValueError(
271-
f"Failed to locate or extract artifact for {bazel_rule} rule"
272-
) from e
273-
274-
275136
def _run_post_processor(output: str, library_id: str):
276137
"""Runs the synthtool post-processor on the output directory.
277138
@@ -399,11 +260,55 @@ def handle_generate(
399260
for api in request_data.get("apis", []):
400261
api_path = api.get("path")
401262
if api_path:
402-
bazel_rule = _determine_bazel_rule(api_path, source)
403-
_build_bazel_target(bazel_rule, source)
404-
_locate_and_extract_artifact(
405-
bazel_rule, library_id, source, output, api_path
406-
)
263+
264+
generator_options=[]
265+
with open(f"{source}/{api_path}/BUILD.bazel", "r") as f:
266+
content = f.read()
267+
result = parse_googleapis_content.parse_content(content)
268+
py_gapic_entry = [key for key in result.keys() if key.endswith("_py_gapic")][0]
269+
270+
config_keys = [
271+
"grpc_service_config",
272+
"rest_numeric_enums",
273+
"service_yaml",
274+
"transport",
275+
]
276+
277+
for key in config_keys:
278+
config_value = result[py_gapic_entry].get(key, None)
279+
if config_value is not None:
280+
new_key = key.replace("_", "-")
281+
if key == "grpc_service_config":
282+
new_key = "retry-config"
283+
284+
# There is a bug in the Python generator that treats all values of
285+
# `rest-numeric-enums` as True, so just omit it if we want it to be False
286+
if new_key == 'rest-numeric-enums' and config_value == "False":
287+
continue
288+
elif new_key == "service-yaml" or new_key == "retry-config":
289+
generator_options.append(
290+
f"{new_key}={api_path}/{config_value},"
291+
)
292+
else:
293+
generator_options.append(f"{new_key}={config_value},")
294+
with tempfile.TemporaryDirectory() as tmp_dir:
295+
generator_command = (
296+
f"protoc {api_path}/*.proto --python_gapic_out={tmp_dir}"
297+
)
298+
if len(generator_options):
299+
generator_command += (
300+
f" --python_gapic_opt=metadata,"
301+
)
302+
for generator_option in generator_options:
303+
generator_command += generator_option
304+
subprocess.run([generator_command], cwd=source, shell=True)
305+
api_version = api_path.split("/")[-1]
306+
staging_dir = os.path.join(output, "owl-bot-staging", library_id, api_version)
307+
os.makedirs(staging_dir, exist_ok=True)
308+
logger.info(f"Preparing staging directory: {staging_dir}")
309+
subprocess.run(
310+
f"cp -r {tmp_dir}/. {staging_dir}", shell=True
311+
)
407312

408313
_copy_files_needed_for_post_processing(output, input, library_id)
409314
_run_post_processor(output, library_id)

0 commit comments

Comments
 (0)