From 81e905d70e9ed932a6693a1e7d509e982224d3df Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 22 Jul 2025 06:04:13 +0000 Subject: [PATCH 1/5] add bazelisk command --- .generator/cli.py | 27 ++++++++++++++++++++++++++- .generator/test_cli.py | 18 ++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/.generator/cli.py b/.generator/cli.py index fe87ce1a6b09..543f9b72c5ac 100644 --- a/.generator/cli.py +++ b/.generator/cli.py @@ -17,6 +17,7 @@ import logging import os import sys +import subprocess logger = logging.getLogger() @@ -66,7 +67,31 @@ def handle_generate(): f"failed to read {LIBRARIAN_DIR}/{GENERATE_REQUEST_FILE}" ) from e - logger.info(json.dumps(request_data, indent=2)) + library_id = request_data.get("id") + if not library_id: + raise ValueError("Request file is missing required 'id' field.") + + for api in request_data.get("apis", []): + api_path = api.get("path") + if api_path: + try: + query = f'filter("-py$", kind("rule", //{api_path}/...:*))' + command = ["bazelisk", "query", query] + result = subprocess.run( + command, capture_output=True, text=True, check=True + ) + + bazel_rule = result.stdout.strip() + if not bazel_rule: + raise ValueError( + f"Bazel query `{query}` returned an empty bazel rule." + ) + + logger.info(f"Found Bazel rule: {bazel_rule}") + except Exception as e: + raise ValueError(f"Bazelisk query `{query}` failed") from e + + logger.info(json.dumps(request_data, indent=2)) # TODO(https://github.com/googleapis/librarian/issues/448): Implement generate command and update docstring. logger.info("'generate' command executed.") diff --git a/.generator/test_cli.py b/.generator/test_cli.py index cbf73bdfaea3..ab99ba70e3b1 100644 --- a/.generator/test_cli.py +++ b/.generator/test_cli.py @@ -17,7 +17,7 @@ import json import logging -from unittest.mock import mock_open +from unittest.mock import mock_open, MagicMock from cli import ( _read_json_file, @@ -60,16 +60,30 @@ def test_handle_configure_success(caplog, mock_generate_request_file): assert "'configure' command executed." in caplog.text -def test_handle_generate_success(caplog, mock_generate_request_file): +def test_handle_generate_success(caplog, mock_generate_request_file, mocker): """ Tests the successful execution path of handle_generate. """ caplog.set_level(logging.INFO) + mock_query_result = MagicMock( + stdout="//google/cloud/language/v1:google-cloud-language-v1-py\n", returncode=0 + ) + + mock_subprocess = mocker.patch( + "cli.subprocess.run", side_effect=[mock_query_result] + ) + handle_generate() + # captured = capsys.readouterr() assert "google-cloud-language" in caplog.text + assert ( + "Found Bazel rule: //google/cloud/language/v1:google-cloud-language-v1-py" + in caplog.text + ) assert "'generate' command executed." in caplog.text + assert mock_subprocess.call_count == 1 def test_handle_generate_fail(caplog): From db1a8800a8963d022b73abb3861ed360630f08e2 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 22 Jul 2025 18:58:33 +0000 Subject: [PATCH 2/5] address PR feedback --- .generator/Dockerfile | 5 +++- .generator/cli.py | 68 ++++++++++++++++++++++++------------------ .generator/test_cli.py | 34 ++++++++++++--------- 3 files changed, 63 insertions(+), 44 deletions(-) diff --git a/.generator/Dockerfile b/.generator/Dockerfile index 2785e9d5c2ef..e6c959059b75 100644 --- a/.generator/Dockerfile +++ b/.generator/Dockerfile @@ -35,6 +35,7 @@ RUN apt-get update && \ # Set up environment variables for tool versions to make updates easier. ENV PYTHON_VERSION=3.11.5 +ENV BAZELISK_VERSION=v1.19.0 # Create a symbolic link for `python3` to point to our specific version. ENV PATH /usr/local/bin/python3.11:$PATH @@ -50,7 +51,9 @@ RUN wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VER # TODO(https://github.com/googleapis/librarian/issues/904): Install protoc for gencode. -# TODO(https://github.com/googleapis/librarian/issues/903): Install Bazelisk for building a client library. +# Install Bazelisk +RUN wget https://github.com/bazelbuild/bazelisk/releases/download/${BAZELISK_VERSION}/bazelisk-linux-amd64 -O /usr/local/bin/bazelisk && \ + chmod +x /usr/local/bin/bazelisk # TODO(https://github.com/googleapis/librarian/issues/902): Create a dedicate non-root user and # switch to the non-root user to run subsequent commands. diff --git a/.generator/cli.py b/.generator/cli.py index 543f9b72c5ac..a85a112ed779 100644 --- a/.generator/cli.py +++ b/.generator/cli.py @@ -48,6 +48,34 @@ def handle_configure(): logger.info("'configure' command executed.") +def _determine_bazel_rule(api_path): + """Executes a `bazelisk query` to find a Bazel rule. + + Args: + api_path (str): The API path to query for. + + Returns: + str: The discovered Bazel rule. + + Raises: + Exception: If the subprocess call fails or returns an empty result. + """ + logger.info(f"Determining Bazel rule for api_path: '{api_path}'") + try: + query = f'filter("-py$", kind("rule", //{api_path}/...:*))' + command = ["bazelisk", "query", query] + result = subprocess.run(command, capture_output=True, text=True, check=True) + + bazel_rule = result.stdout.strip() + if not bazel_rule: + raise ValueError(f"Bazel query `{query}` returned an empty bazel rule.") + + logger.info(f"Found Bazel rule: {bazel_rule}") + return bazel_rule + except Exception as e: + raise ValueError(f"Bazelisk query `{query}` failed") from e + + def handle_generate(): """The main coordinator for the code generation process. @@ -62,36 +90,18 @@ def handle_generate(): # Read a generate-request.json file try: request_data = _read_json_file(f"{LIBRARIAN_DIR}/{GENERATE_REQUEST_FILE}") + library_id = request_data.get("id") + if not library_id: + raise ValueError("Request file is missing required 'id' field.") + + for api in request_data.get("apis", []): + api_path = api.get("path") + if api_path: + bazel_rule = _determine_bazel_rule(api_path) + + logger.info(json.dumps(request_data, indent=2)) except Exception as e: - raise ValueError( - f"failed to read {LIBRARIAN_DIR}/{GENERATE_REQUEST_FILE}" - ) from e - - library_id = request_data.get("id") - if not library_id: - raise ValueError("Request file is missing required 'id' field.") - - for api in request_data.get("apis", []): - api_path = api.get("path") - if api_path: - try: - query = f'filter("-py$", kind("rule", //{api_path}/...:*))' - command = ["bazelisk", "query", query] - result = subprocess.run( - command, capture_output=True, text=True, check=True - ) - - bazel_rule = result.stdout.strip() - if not bazel_rule: - raise ValueError( - f"Bazel query `{query}` returned an empty bazel rule." - ) - - logger.info(f"Found Bazel rule: {bazel_rule}") - except Exception as e: - raise ValueError(f"Bazelisk query `{query}` failed") from e - - logger.info(json.dumps(request_data, indent=2)) + raise ValueError("Generation failed.") from e # TODO(https://github.com/googleapis/librarian/issues/448): Implement generate command and update docstring. logger.info("'generate' command executed.") diff --git a/.generator/test_cli.py b/.generator/test_cli.py index ab99ba70e3b1..ca3df332b123 100644 --- a/.generator/test_cli.py +++ b/.generator/test_cli.py @@ -21,6 +21,7 @@ from cli import ( _read_json_file, + _determine_bazel_rule, handle_generate, handle_build, handle_configure, @@ -60,30 +61,35 @@ def test_handle_configure_success(caplog, mock_generate_request_file): assert "'configure' command executed." in caplog.text +def test_determine_bazel_rule_success(mocker, caplog): + """ + Tests the happy path of _determine_bazel_rule. + """ + caplog.set_level(logging.INFO) + mock_result = MagicMock( + stdout="//google/cloud/language/v1:google-cloud-language-v1-py\n" + ) + mocker.patch("cli.subprocess.run", return_value=mock_result) + + rule = _determine_bazel_rule("google/cloud/language/v1") + + assert rule == "//google/cloud/language/v1:google-cloud-language-v1-py" + assert "Found Bazel rule" in caplog.text + + def test_handle_generate_success(caplog, mock_generate_request_file, mocker): """ Tests the successful execution path of handle_generate. """ caplog.set_level(logging.INFO) - mock_query_result = MagicMock( - stdout="//google/cloud/language/v1:google-cloud-language-v1-py\n", returncode=0 - ) - - mock_subprocess = mocker.patch( - "cli.subprocess.run", side_effect=[mock_query_result] + mock_determine_rule = mocker.patch( + "cli._determine_bazel_rule", return_value="mock-rule" ) handle_generate() - # captured = capsys.readouterr() - assert "google-cloud-language" in caplog.text - assert ( - "Found Bazel rule: //google/cloud/language/v1:google-cloud-language-v1-py" - in caplog.text - ) - assert "'generate' command executed." in caplog.text - assert mock_subprocess.call_count == 1 + mock_determine_rule.assert_called_once_with("google/cloud/language/v1") def test_handle_generate_fail(caplog): From 4977201c195f6b4a9ef58850c5a6840b3d720436 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 22 Jul 2025 19:41:39 +0000 Subject: [PATCH 3/5] address PR feedback --- .generator/test_cli.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.generator/test_cli.py b/.generator/test_cli.py index ca3df332b123..096ce8e67f87 100644 --- a/.generator/test_cli.py +++ b/.generator/test_cli.py @@ -16,6 +16,7 @@ import pytest import json import logging +import subprocess from unittest.mock import mock_open, MagicMock @@ -77,6 +78,22 @@ def test_determine_bazel_rule_success(mocker, caplog): assert "Found Bazel rule" in caplog.text +def test_determine_bazel_rule_command_fails(mocker, caplog): + """ + Tests that an exception is raised if the subprocess command fails. + """ + caplog.set_level(logging.INFO) + mocker.patch( + "cli.subprocess.run", + side_effect=subprocess.CalledProcessError(1, "cmd", stderr="Bazel error"), + ) + + with pytest.raises(ValueError): + _determine_bazel_rule("google/cloud/language/v1") + + assert "Found Bazel rule" not in caplog.text + + def test_handle_generate_success(caplog, mock_generate_request_file, mocker): """ Tests the successful execution path of handle_generate. From ab1562234d3822181bf982b9dd3d0b63b7014f39 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 22 Jul 2025 20:58:44 +0000 Subject: [PATCH 4/5] add required deps --- .generator/Dockerfile | 4 +++- .generator/cli.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.generator/Dockerfile b/.generator/Dockerfile index e6c959059b75..ee96e705b0aa 100644 --- a/.generator/Dockerfile +++ b/.generator/Dockerfile @@ -22,6 +22,8 @@ RUN apt-get update && \ # For downloading secure files wget \ ca-certificates \ + # For running bazelisk commands + openjdk-17-jdk \ # --- Critical libraries for a complete Python build --- libssl-dev \ zlib1g-dev \ @@ -35,7 +37,7 @@ RUN apt-get update && \ # Set up environment variables for tool versions to make updates easier. ENV PYTHON_VERSION=3.11.5 -ENV BAZELISK_VERSION=v1.19.0 +ENV BAZELISK_VERSION=v1.26.0 # Create a symbolic link for `python3` to point to our specific version. ENV PATH /usr/local/bin/python3.11:$PATH diff --git a/.generator/cli.py b/.generator/cli.py index a85a112ed779..3f190d7cfdb9 100644 --- a/.generator/cli.py +++ b/.generator/cli.py @@ -23,6 +23,7 @@ LIBRARIAN_DIR = "librarian" GENERATE_REQUEST_FILE = "generate-request.json" +SOURCE_DIR = "source" def _read_json_file(path): @@ -64,8 +65,13 @@ def _determine_bazel_rule(api_path): try: query = f'filter("-py$", kind("rule", //{api_path}/...:*))' command = ["bazelisk", "query", query] - result = subprocess.run(command, capture_output=True, text=True, check=True) - + result = subprocess.run( + command, + cwd=f"{SOURCE_DIR}/googleapis", + capture_output=True, + text=True, + check=True, + ) bazel_rule = result.stdout.strip() if not bazel_rule: raise ValueError(f"Bazel query `{query}` returned an empty bazel rule.") From a90b90cc5929bdc79ab92238d0e47aaf322700e9 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 22 Jul 2025 18:22:41 -0400 Subject: [PATCH 5/5] Update .generator/cli.py Co-authored-by: Anthonios Partheniou --- .generator/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.generator/cli.py b/.generator/cli.py index 3f190d7cfdb9..20d3daab3ee9 100644 --- a/.generator/cli.py +++ b/.generator/cli.py @@ -74,7 +74,7 @@ def _determine_bazel_rule(api_path): ) bazel_rule = result.stdout.strip() if not bazel_rule: - raise ValueError(f"Bazel query `{query}` returned an empty bazel rule.") + raise ValueError(f"Bazelisk query `{query}` returned an empty bazel rule.") logger.info(f"Found Bazel rule: {bazel_rule}") return bazel_rule