diff --git a/.generator/Dockerfile b/.generator/Dockerfile index 2785e9d5c2ef..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,6 +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.26.0 # Create a symbolic link for `python3` to point to our specific version. ENV PATH /usr/local/bin/python3.11:$PATH @@ -50,7 +53,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 fe87ce1a6b09..20d3daab3ee9 100644 --- a/.generator/cli.py +++ b/.generator/cli.py @@ -17,11 +17,13 @@ import logging import os import sys +import subprocess logger = logging.getLogger() LIBRARIAN_DIR = "librarian" GENERATE_REQUEST_FILE = "generate-request.json" +SOURCE_DIR = "source" def _read_json_file(path): @@ -47,6 +49,39 @@ 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, + cwd=f"{SOURCE_DIR}/googleapis", + capture_output=True, + text=True, + check=True, + ) + bazel_rule = result.stdout.strip() + if not bazel_rule: + raise ValueError(f"Bazelisk 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. @@ -61,12 +96,18 @@ def handle_generate(): # Read a generate-request.json file try: request_data = _read_json_file(f"{LIBRARIAN_DIR}/{GENERATE_REQUEST_FILE}") - 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: + bazel_rule = _determine_bazel_rule(api_path) - logger.info(json.dumps(request_data, indent=2)) + logger.info(json.dumps(request_data, indent=2)) + except Exception as e: + 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 cbf73bdfaea3..096ce8e67f87 100644 --- a/.generator/test_cli.py +++ b/.generator/test_cli.py @@ -16,11 +16,13 @@ import pytest import json import logging +import subprocess -from unittest.mock import mock_open +from unittest.mock import mock_open, MagicMock from cli import ( _read_json_file, + _determine_bazel_rule, handle_generate, handle_build, handle_configure, @@ -60,16 +62,51 @@ 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_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_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. """ caplog.set_level(logging.INFO) + mock_determine_rule = mocker.patch( + "cli._determine_bazel_rule", return_value="mock-rule" + ) + handle_generate() - assert "google-cloud-language" in caplog.text - assert "'generate' command executed." in caplog.text + mock_determine_rule.assert_called_once_with("google/cloud/language/v1") def test_handle_generate_fail(caplog):