Skip to content

Commit af26b7f

Browse files
authored
chore: add support for cli arguments (googleapis#14195)
This PR adds support for CLI arguments `librarian`, `input`, `output` and `source`
1 parent 9abaf2f commit af26b7f

File tree

2 files changed

+109
-37
lines changed

2 files changed

+109
-37
lines changed

.generator/cli.py

Lines changed: 97 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535

3636
LIBRARIAN_DIR = "librarian"
3737
GENERATE_REQUEST_FILE = "generate-request.json"
38+
INPUT_DIR = "input"
39+
BUILD_REQUEST_FILE = "build-request.json"
3840
SOURCE_DIR = "source"
3941
OUTPUT_DIR = "output"
4042
REPO_DIR = "repo"
@@ -44,7 +46,7 @@ def _read_json_file(path: str) -> Dict:
4446
"""Helper function that reads a json file path and returns the loaded json content.
4547
4648
Args:
47-
path (str): The file path to read.
49+
path(str): The file path to read.
4850
4951
Returns:
5052
dict: The parsed JSON content.
@@ -63,11 +65,12 @@ def handle_configure():
6365
logger.info("'configure' command executed.")
6466

6567

66-
def _determine_bazel_rule(api_path: str) -> str:
68+
def _determine_bazel_rule(api_path: str, source: str) -> str:
6769
"""Executes a `bazelisk query` to find a Bazel rule.
6870
6971
Args:
70-
api_path (str): The API path to query for.
72+
api_path(str): The API path to query for.
73+
source(str): The path to the root of the Bazel workspace.
7174
7275
Returns:
7376
str: The discovered Bazel rule.
@@ -81,7 +84,7 @@ def _determine_bazel_rule(api_path: str) -> str:
8184
command = ["bazelisk", "query", query]
8285
result = subprocess.run(
8386
command,
84-
cwd=f"{SOURCE_DIR}/googleapis",
87+
cwd=source,
8588
capture_output=True,
8689
text=True,
8790
check=True,
@@ -114,11 +117,12 @@ def _get_library_id(request_data: Dict) -> str:
114117
return library_id
115118

116119

117-
def _build_bazel_target(bazel_rule: str):
120+
def _build_bazel_target(bazel_rule: str, source: str):
118121
"""Executes `bazelisk build` on a given Bazel rule.
119122
120123
Args:
121-
bazel_rule (str): The Bazel rule to build.
124+
bazel_rule(str): The Bazel rule to build.
125+
source(str): The path to the root of the Bazel workspace.
122126
123127
Raises:
124128
ValueError: If the subprocess call fails.
@@ -128,7 +132,7 @@ def _build_bazel_target(bazel_rule: str):
128132
command = ["bazelisk", "build", bazel_rule]
129133
subprocess.run(
130134
command,
131-
cwd=f"{SOURCE_DIR}/googleapis",
135+
cwd=source,
132136
text=True,
133137
check=True,
134138
)
@@ -137,12 +141,22 @@ def _build_bazel_target(bazel_rule: str):
137141
raise ValueError(f"Bazel build for {bazel_rule} rule failed.") from e
138142

139143

140-
def _locate_and_extract_artifact(bazel_rule: str, library_id: str):
144+
def _locate_and_extract_artifact(
145+
bazel_rule: str,
146+
library_id: str,
147+
source: str,
148+
output: str,
149+
api_path: str,
150+
):
141151
"""Finds and extracts the tarball artifact from a Bazel build.
142152
143153
Args:
144-
bazel_rule (str): The Bazel rule that was built.
145-
library_id (str): The ID of the library being generated.
154+
bazel_rule(str): The Bazel rule that was built.
155+
library_id(str): The ID of the library being generated.
156+
source(str): The path to the root of the Bazel workspace.
157+
output(str): The path to the location where generated output
158+
should be stored.
159+
api_path(str): The API path for the artifact
146160
147161
Raises:
148162
ValueError: If failed to locate or extract artifact.
@@ -153,7 +167,7 @@ def _locate_and_extract_artifact(bazel_rule: str, library_id: str):
153167
info_command = ["bazelisk", "info", "bazel-bin"]
154168
result = subprocess.run(
155169
info_command,
156-
cwd=f"{SOURCE_DIR}/googleapis",
170+
cwd=source,
157171
text=True,
158172
check=True,
159173
capture_output=True,
@@ -167,7 +181,8 @@ def _locate_and_extract_artifact(bazel_rule: str, library_id: str):
167181
logger.info(f"Found artifact at: {tarball_path}")
168182

169183
# 3. Create a staging directory.
170-
staging_dir = os.path.join(OUTPUT_DIR, "owl-bot-staging", library_id)
184+
api_version = api_path.split("/")[-1]
185+
staging_dir = os.path.join(output, "owl-bot-staging", library_id, api_version)
171186
os.makedirs(staging_dir, exist_ok=True)
172187
logger.info(f"Preparing staging directory: {staging_dir}")
173188

@@ -185,8 +200,7 @@ def _locate_and_extract_artifact(bazel_rule: str, library_id: str):
185200

186201

187202
def _run_post_processor():
188-
"""Runs the synthtool post-processor on the output directory.
189-
"""
203+
"""Runs the synthtool post-processor on the output directory."""
190204
logger.info("Running Python post-processor...")
191205
if SYNTHTOOL_INSTALLED:
192206
command = ["python3", "-m", "synthtool.languages.python_mono_repo"]
@@ -196,29 +210,48 @@ def _run_post_processor():
196210
logger.info("Python post-processor ran successfully.")
197211

198212

199-
def handle_generate():
213+
def handle_generate(
214+
librarian: str = LIBRARIAN_DIR,
215+
source: str = SOURCE_DIR,
216+
output: str = OUTPUT_DIR,
217+
input: str = INPUT_DIR,
218+
):
200219
"""The main coordinator for the code generation process.
201220
202221
This function orchestrates the generation of a client library by reading a
203222
`librarian/generate-request.json` file, determining the necessary Bazel rule for each API, and
204223
(in future steps) executing the build.
205224
225+
See https://github.com/googleapis/librarian/blob/main/doc/container-contract.md#generate-container-command
226+
227+
Args:
228+
librarian(str): Path to the directory in the container which contains
229+
the librarian configuration.
230+
source(str): Path to the directory in the container which contains
231+
API protos.
232+
output(str): Path to the directory in the container where code
233+
should be generated.
234+
input(str): The path path to the directory in the container
235+
which contains additional generator input.
236+
206237
Raises:
207238
ValueError: If the `generate-request.json` file is not found or read.
208239
"""
209240

210241
try:
211242
# Read a generate-request.json file
212-
request_data = _read_json_file(f"{LIBRARIAN_DIR}/{GENERATE_REQUEST_FILE}")
243+
request_data = _read_json_file(f"{librarian}/{GENERATE_REQUEST_FILE}")
213244
library_id = _get_library_id(request_data)
214245

215246
for api in request_data.get("apis", []):
216247
api_path = api.get("path")
217248
if api_path:
218-
bazel_rule = _determine_bazel_rule(api_path)
219-
_build_bazel_target(bazel_rule)
220-
_locate_and_extract_artifact(bazel_rule, library_id)
221-
_run_post_processor()
249+
bazel_rule = _determine_bazel_rule(api_path, source)
250+
_build_bazel_target(bazel_rule, source)
251+
_locate_and_extract_artifact(
252+
bazel_rule, library_id, source, output, api_path
253+
)
254+
_run_post_processor(output, f"packages/{library_id}")
222255

223256
except Exception as e:
224257
raise ValueError("Generation failed.") from e
@@ -227,16 +260,17 @@ def handle_generate():
227260
logger.info("'generate' command executed.")
228261

229262

230-
def _run_nox_sessions(sessions: List[str]):
263+
def _run_nox_sessions(sessions: List[str], librarian: str):
231264
"""Calls nox for all specified sessions.
232265
233266
Args:
234-
path(List[str]): The list of nox sessions to run.
267+
sessions(List[str]): The list of nox sessions to run.
268+
librarian(str): The path to the librarian build configuration directory
235269
"""
236-
# Read a generate-request.json file
270+
# Read a build-request.json file
237271
current_session = None
238272
try:
239-
request_data = _read_json_file(f"{LIBRARIAN_DIR}/{GENERATE_REQUEST_FILE}")
273+
request_data = _read_json_file(f"{librarian}/{BUILD_REQUEST_FILE}")
240274
library_id = _get_library_id(request_data)
241275
for nox_session in sessions:
242276
_run_individual_session(nox_session, library_id)
@@ -263,7 +297,7 @@ def _run_individual_session(nox_session: str, library_id: str):
263297
logger.info(result)
264298

265299

266-
def handle_build():
300+
def handle_build(librarian: str = LIBRARIAN_DIR):
267301
"""The main coordinator for validating client library generation."""
268302
sessions = [
269303
"unit-3.9",
@@ -278,7 +312,7 @@ def handle_build():
278312
"mypy",
279313
"check_lower_bounds",
280314
]
281-
_run_nox_sessions(sessions)
315+
_run_nox_sessions(sessions, librarian)
282316

283317
logger.info("'build' command executed.")
284318

@@ -303,10 +337,46 @@ def handle_build():
303337
]:
304338
parser_cmd = subparsers.add_parser(command_name, help=help_text)
305339
parser_cmd.set_defaults(func=handler_map[command_name])
306-
340+
parser_cmd.add_argument(
341+
"--librarian",
342+
type=str,
343+
help="Path to the directory in the container which contains the librarian configuration",
344+
default=LIBRARIAN_DIR,
345+
)
346+
parser_cmd.add_argument(
347+
"--input",
348+
type=str,
349+
help="Path to the directory in the container which contains additional generator input",
350+
default=INPUT_DIR,
351+
)
352+
parser_cmd.add_argument(
353+
"--output",
354+
type=str,
355+
help="Path to the directory in the container where code should be generated",
356+
default=OUTPUT_DIR,
357+
)
358+
parser_cmd.add_argument(
359+
"--source",
360+
type=str,
361+
help="Path to the directory in the container which contains API protos",
362+
default=SOURCE_DIR,
363+
)
307364
if len(sys.argv) == 1:
308365
parser.print_help(sys.stderr)
309366
sys.exit(1)
310367

311368
args = parser.parse_args()
312369
args.func()
370+
371+
# Pass specific arguments to the handler functions for generate/build
372+
if args.command == "generate":
373+
args.func(
374+
librarian=args.librarian,
375+
source=args.source,
376+
output=args.output,
377+
input=args.input,
378+
)
379+
elif args.command == "build":
380+
args.func(librarian=args.librarian)
381+
else:
382+
args.func()

.generator/test_cli.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ def test_determine_bazel_rule_success(mocker, caplog):
115115
)
116116
mocker.patch("cli.subprocess.run", return_value=mock_result)
117117

118-
rule = _determine_bazel_rule("google/cloud/language/v1")
118+
rule = _determine_bazel_rule("google/cloud/language/v1", "source")
119119

120120
assert rule == "//google/cloud/language/v1:google-cloud-language-v1-py"
121121
assert "Found Bazel rule" in caplog.text
@@ -127,7 +127,7 @@ def test_build_bazel_target_success(mocker, caplog):
127127
"""
128128
caplog.set_level(logging.INFO)
129129
mocker.patch("cli.subprocess.run", return_value=MagicMock(returncode=0))
130-
_build_bazel_target("mock/bazel:rule")
130+
_build_bazel_target("mock/bazel:rule", "source")
131131
assert "Bazel build for mock/bazel:rule rule completed successfully" in caplog.text
132132

133133

@@ -141,7 +141,7 @@ def test_build_bazel_target_fails(mocker, caplog):
141141
side_effect=subprocess.CalledProcessError(1, "cmd", stderr="Build failed"),
142142
)
143143
with pytest.raises(ValueError):
144-
_build_bazel_target("mock/bazel:rule")
144+
_build_bazel_target("mock/bazel:rule", "source")
145145

146146

147147
def test_determine_bazel_rule_command_fails(mocker, caplog):
@@ -155,7 +155,7 @@ def test_determine_bazel_rule_command_fails(mocker, caplog):
155155
)
156156

157157
with pytest.raises(ValueError):
158-
_determine_bazel_rule("google/cloud/language/v1")
158+
_determine_bazel_rule("google/cloud/language/v1", "source")
159159

160160
assert "Found Bazel rule" not in caplog.text
161161

@@ -172,8 +172,10 @@ def test_locate_and_extract_artifact_success(mocker, caplog):
172172
_locate_and_extract_artifact(
173173
"//google/cloud/language/v1:rule-py",
174174
"google-cloud-language",
175+
"source",
176+
"output",
177+
"google/cloud/language/v1"
175178
)
176-
177179
assert (
178180
"Found artifact at: /path/to/bazel-bin/google/cloud/language/v1/rule-py.tar.gz"
179181
in caplog.text
@@ -248,7 +250,7 @@ def test_handle_generate_success(caplog, mock_generate_request_file, mocker):
248250

249251
handle_generate()
250252

251-
mock_determine_rule.assert_called_once_with("google/cloud/language/v1")
253+
mock_determine_rule.assert_called_once_with("google/cloud/language/v1", "source")
252254

253255

254256
def test_handle_generate_fail(caplog):
@@ -301,7 +303,7 @@ def test_run_nox_sessions_success(mocker, mock_generate_request_data_for_nox):
301303
mock_run_individual_session = mocker.patch("cli._run_individual_session")
302304

303305
sessions_to_run = ["unit-3.9", "lint"]
304-
_run_nox_sessions(sessions_to_run)
306+
_run_nox_sessions(sessions_to_run, "librarian")
305307

306308
assert mock_run_individual_session.call_count == len(sessions_to_run)
307309
mock_run_individual_session.assert_has_calls(
@@ -317,7 +319,7 @@ def test_run_nox_sessions_read_file_failure(mocker):
317319
mocker.patch("cli._read_json_file", side_effect=FileNotFoundError("file not found"))
318320

319321
with pytest.raises(ValueError, match="Failed to run the nox session"):
320-
_run_nox_sessions(["unit-3.9"])
322+
_run_nox_sessions(["unit-3.9"], "librarian")
321323

322324

323325
def test_run_nox_sessions_get_library_id_failure(mocker):
@@ -329,7 +331,7 @@ def test_run_nox_sessions_get_library_id_failure(mocker):
329331
)
330332

331333
with pytest.raises(ValueError, match="Failed to run the nox session"):
332-
_run_nox_sessions(["unit-3.9"])
334+
_run_nox_sessions(["unit-3.9"], "librarian")
333335

334336

335337
def test_run_nox_sessions_individual_session_failure(
@@ -345,7 +347,7 @@ def test_run_nox_sessions_individual_session_failure(
345347

346348
sessions_to_run = ["unit-3.9", "lint"]
347349
with pytest.raises(ValueError, match="Failed to run the nox session"):
348-
_run_nox_sessions(sessions_to_run)
350+
_run_nox_sessions(sessions_to_run, "librarian")
349351

350352
# Check that _run_individual_session was called at least once
351353
assert mock_run_individual_session.call_count > 0

0 commit comments

Comments
 (0)