Skip to content

Commit 5ba8437

Browse files
authored
chore(librarian): add support for split repository (#14827)
This PR is needed to support the [librarian generate](https://github.com/googleapis/librarian/blob/main/doc/language-onboarding.md#generate) command in split repositories. The changes are mostly related to the difference in directory structure between a split repository and mono repository.
1 parent 6b4b00d commit 5ba8437

File tree

3 files changed

+142
-93
lines changed

3 files changed

+142
-93
lines changed

.generator/Dockerfile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@ RUN wget https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pando
6969
RUN tar -xvf pandoc-${PANDOC_VERSION}-linux-amd64.tar.gz -C pandoc-binary --strip-components=1
7070

7171
# Pin synthtool for a more hermetic build
72-
RUN git clone https://github.com/googleapis/synthtool.git synthtool
73-
RUN cd synthtool && git checkout 35313ccd8cdd2d12d2447ccdc497a7919aae1e3e
72+
# This needs to be a single command so that the git clone command is not cached
73+
RUN git clone https://github.com/googleapis/synthtool.git synthtool && \
74+
cd synthtool && \
75+
git checkout 6702a344265de050bceaff45d62358bb0023ba7d
7476

7577
# --- Final Stage ---
7678
# This stage creates the lightweight final image, copying only the

.generator/cli.py

Lines changed: 59 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
try:
3535
import synthtool
36-
from synthtool.languages import python_mono_repo
36+
from synthtool.languages import python, python_mono_repo
3737

3838
SYNTHTOOL_INSTALLED = True
3939
SYNTHTOOL_IMPORT_ERROR = None
@@ -316,20 +316,28 @@ def _get_library_id(request_data: Dict) -> str:
316316
return library_id
317317

318318

319-
def _run_post_processor(output: str, library_id: str):
319+
def _run_post_processor(output: str, library_id: str, is_mono_repo: bool):
320320
"""Runs the synthtool post-processor on the output directory.
321321
322322
Args:
323323
output(str): Path to the directory in the container where code
324324
should be generated.
325325
library_id(str): The library id to be used for post processing.
326-
326+
is_mono_repo(bool): True if the current repository is a mono-repo.
327327
"""
328328
os.chdir(output)
329-
path_to_library = f"packages/{library_id}"
329+
path_to_library = f"packages/{library_id}" if is_mono_repo else "."
330330
logger.info("Running Python post-processor...")
331331
if SYNTHTOOL_INSTALLED:
332-
python_mono_repo.owlbot_main(path_to_library)
332+
if is_mono_repo:
333+
python_mono_repo.owlbot_main(path_to_library)
334+
else:
335+
# Some repositories have customizations in `owlbot.py`. If this file exists,
336+
# run those customizations instead of `owlbot_main`
337+
if Path(f"{output}/owlbot.py").exists():
338+
subprocess.run(["python3.14", f"{output}/owlbot.py"])
339+
else:
340+
python.owlbot_main()
333341
else:
334342
raise SYNTHTOOL_IMPORT_ERROR # pragma: NO COVER
335343

@@ -342,7 +350,9 @@ def _run_post_processor(output: str, library_id: str):
342350
logger.info("Python post-processor ran successfully.")
343351

344352

345-
def _copy_files_needed_for_post_processing(output: str, input: str, library_id: str):
353+
def _copy_files_needed_for_post_processing(
354+
output: str, input: str, library_id: str, is_mono_repo: bool
355+
):
346356
"""Copy files to the output directory whcih are needed during the post processing
347357
step, such as .repo-metadata.json and script/client-post-processing, using
348358
the input directory as the source.
@@ -353,25 +363,23 @@ def _copy_files_needed_for_post_processing(output: str, input: str, library_id:
353363
input(str): The path to the directory in the container
354364
which contains additional generator input.
355365
library_id(str): The library id to be used for post processing.
366+
is_mono_repo(bool): True if the current repository is a mono-repo.
356367
"""
357368

358-
path_to_library = f"packages/{library_id}"
359-
repo_metadata_path = f"{input}/{path_to_library}/.repo-metadata.json"
369+
path_to_library = f"packages/{library_id}" if is_mono_repo else "."
370+
source_dir = f"{input}/{path_to_library}"
371+
372+
shutil.copytree(
373+
source_dir,
374+
output,
375+
dirs_exist_ok=True,
376+
ignore=shutil.ignore_patterns("client-post-processing"),
377+
)
360378

361379
# We need to create these directories so that we can copy files necessary for post-processing.
362380
os.makedirs(
363381
f"{output}/{path_to_library}/scripts/client-post-processing", exist_ok=True
364382
)
365-
# TODO(https://github.com/googleapis/librarian/issues/2334):
366-
# if `.repo-metadata.json` for a library exists in
367-
# `.librarian/generator-input`, then we override the generated `.repo-metadata.json`
368-
# with what we have in `generator-input`. Remove this logic once the
369-
# generated `.repo-metadata.json` file is completely backfilled.
370-
if os.path.exists(repo_metadata_path):
371-
shutil.copy(
372-
repo_metadata_path,
373-
f"{output}/{path_to_library}/.repo-metadata.json",
374-
)
375383

376384
# copy post-procesing files
377385
for post_processing_file in glob.glob(
@@ -385,7 +393,9 @@ def _copy_files_needed_for_post_processing(output: str, input: str, library_id:
385393
)
386394

387395

388-
def _clean_up_files_after_post_processing(output: str, library_id: str):
396+
def _clean_up_files_after_post_processing(
397+
output: str, library_id: str, is_mono_repo: bool
398+
):
389399
"""
390400
Clean up files which should not be included in the generated client.
391401
This function is idempotent and will not fail if files are already removed.
@@ -394,8 +404,9 @@ def _clean_up_files_after_post_processing(output: str, library_id: str):
394404
output(str): Path to the directory in the container where code
395405
should be generated.
396406
library_id(str): The library id to be used for post processing.
407+
is_mono_repo(bool): True if the current repository is a mono-repo.
397408
"""
398-
path_to_library = f"packages/{library_id}"
409+
path_to_library = f"packages/{library_id}" if is_mono_repo else "."
399410

400411
# Safely remove directories, ignoring errors if they don't exist.
401412
shutil.rmtree(f"{output}/{path_to_library}/.nox", ignore_errors=True)
@@ -486,7 +497,7 @@ def _create_repo_metadata_from_service_config(
486497

487498

488499
def _generate_repo_metadata_file(
489-
output: str, library_id: str, source: str, apis: List[Dict]
500+
output: str, library_id: str, source: str, apis: List[Dict], is_mono_repo: bool
490501
):
491502
"""Generates the .repo-metadata.json file from the primary API service config.
492503
@@ -495,8 +506,9 @@ def _generate_repo_metadata_file(
495506
library_id (str): The ID of the library.
496507
source (str): The path to the source directory.
497508
apis (List[Dict]): A list of APIs to generate.
509+
is_mono_repo(bool): True if the current repository is a mono-repo.
498510
"""
499-
path_to_library = f"packages/{library_id}"
511+
path_to_library = f"packages/{library_id}" if is_mono_repo else "."
500512
output_repo_metadata = f"{output}/{path_to_library}/.repo-metadata.json"
501513

502514
# TODO(https://github.com/googleapis/librarian/issues/2334)): If `.repo-metadata.json`
@@ -523,7 +535,7 @@ def _generate_repo_metadata_file(
523535
_write_json_file(output_repo_metadata, metadata_content)
524536

525537

526-
def _copy_readme_to_docs(output: str, library_id: str):
538+
def _copy_readme_to_docs(output: str, library_id: str, is_mono_repo: bool):
527539
"""Copies the README.rst file for a generated library to docs/README.rst.
528540
529541
This function is robust against various symlink configurations that could
@@ -536,7 +548,7 @@ def _copy_readme_to_docs(output: str, library_id: str):
536548
should be generated.
537549
library_id(str): The library id.
538550
"""
539-
path_to_library = f"packages/{library_id}"
551+
path_to_library = f"packages/{library_id}" if is_mono_repo else "."
540552
source_path = f"{output}/{path_to_library}/README.rst"
541553
docs_path = f"{output}/{path_to_library}/docs"
542554
destination_path = f"{docs_path}/README.rst"
@@ -593,6 +605,7 @@ def handle_generate(
593605
"""
594606

595607
try:
608+
is_mono_repo = _is_mono_repo(input)
596609
# Read a generate-request.json file
597610
request_data = _read_json_file(f"{librarian}/{GENERATE_REQUEST_FILE}")
598611
library_id = _get_library_id(request_data)
@@ -601,12 +614,16 @@ def handle_generate(
601614
for api in apis_to_generate:
602615
api_path = api.get("path")
603616
if api_path:
604-
_generate_api(api_path, library_id, source, output, version)
605-
_copy_files_needed_for_post_processing(output, input, library_id)
606-
_generate_repo_metadata_file(output, library_id, source, apis_to_generate)
607-
_run_post_processor(output, library_id)
608-
_copy_readme_to_docs(output, library_id)
609-
_clean_up_files_after_post_processing(output, library_id)
617+
_generate_api(
618+
api_path, library_id, source, output, version, is_mono_repo
619+
)
620+
_copy_files_needed_for_post_processing(output, input, library_id, is_mono_repo)
621+
_generate_repo_metadata_file(
622+
output, library_id, source, apis_to_generate, is_mono_repo
623+
)
624+
_run_post_processor(output, library_id, is_mono_repo)
625+
_copy_readme_to_docs(output, library_id, is_mono_repo)
626+
_clean_up_files_after_post_processing(output, library_id, is_mono_repo)
610627
except Exception as e:
611628
raise ValueError("Generation failed.") from e
612629
logger.info("'generate' command executed.")
@@ -821,7 +838,12 @@ def _stage_gapic_library(tmp_dir: str, staging_dir: str) -> None:
821838

822839

823840
def _generate_api(
824-
api_path: str, library_id: str, source: str, output: str, gapic_version: str
841+
api_path: str,
842+
library_id: str,
843+
source: str,
844+
output: str,
845+
gapic_version: str,
846+
is_mono_repo: bool,
825847
):
826848
"""
827849
Handles the generation and staging process for a single API path.
@@ -833,6 +855,7 @@ def _generate_api(
833855
output (str): Path to the output directory where code should be staged.
834856
gapic_version(str): The desired version number for the GAPIC client library
835857
in a format which follows PEP-440.
858+
is_mono_repo(bool): True if the current repository is a mono-repo.
836859
"""
837860
py_gapic_config = _read_bazel_build_py_rule(api_path, source)
838861
is_proto_only_library = len(py_gapic_config) == 0
@@ -854,9 +877,10 @@ def _generate_api(
854877
staging_child_directory = _get_staging_child_directory(
855878
api_path, is_proto_only_library
856879
)
857-
staging_dir = os.path.join(
858-
output, "owl-bot-staging", library_id, staging_child_directory
859-
)
880+
staging_dir = os.path.join(output, "owl-bot-staging")
881+
if is_mono_repo:
882+
staging_dir = os.path.join(staging_dir, library_id)
883+
staging_dir = os.path.join(staging_dir, staging_child_directory)
860884

861885
# 4. Stage the generated code
862886
if is_proto_only_library:
@@ -1464,10 +1488,7 @@ def handle_release_init(
14641488
f"{library_id} version: {previous_version}\n"
14651489
)
14661490

1467-
if is_mono_repo:
1468-
path_to_library = f"packages/{library_id}"
1469-
else:
1470-
path_to_library = "."
1491+
path_to_library = f"packages/{library_id}" if is_mono_repo else "."
14711492

14721493
_update_version_for_library(repo, output, path_to_library, version)
14731494
_update_changelog_for_library(

0 commit comments

Comments
 (0)