Skip to content

Commit 7ccec6d

Browse files
chore: transfer java-related synthtool code into the library_generation image (#3011)
This PR moves `java.py` and the minimum required dependencies from [synthtool](https://github.com/googleapis/synthtool/tree/master/synthtool). It contains the changes of @JoeWang1127's googleapis/synthtool@696c4bf ### Details of code transfer Some files have been simplified and others simply verbatim-copied. Here is a detailed list of changes (the paths are relative to `library_generation/owlbot`: * __`synthtool/__init__.py`__: No changes made. This is the module initialization configuration to allow statements such as `import synthtool as s` combined with `s.copy`, `s.replace`, etc. Without this, we would have to use `import synthtool.transforms as s`. * __`synthtool/_tracked_paths.py`__: No changes made. It allows to use relative paths internally when working with library files. * __`synthtool/gcp/common.py`__: Simplified to use java-only functions. This file originally had a set of functions to support postprocessing of languages written in multiple languages. The most important function is `common_templates`, which renders the templates for the library (e.g. workflow files, kokoro files). Note that `common_templates` was modified in order to require and only allow specifying the path to the templates via the `SYNTHTOOL_TEMPLATES` env var, as opposed to its original support of 3 separate ways, including cloning synthtool and reading the templates from there, because this is now an internal detail of how the `library_generation` image will work. * __`synthtool/gcp/samples.py`__: No changes made. This is a helper to obtain path and metadata about the generated samples of a library. It is then used [when rendering the README](https://github.com/googleapis/sdk-platform-java/blob/554329eae8d6970223dc64a920f7714246afe7a1/library_generation/owlbot/templates/java_library/README.md?plain=1#L141-L150). * __`synthtool/gcp/snippets.py`__: No changes made. Similar to `samples.py` in the way it's [used](https://github.com/googleapis/sdk-platform-java/blob/554329eae8d6970223dc64a920f7714246afe7a1/library_generation/owlbot/templates/java_library/README.md?plain=1#L68-L80) to render a library's README * __`synthool/languages/java.py`__: Small modifications around the fact that we dropped several files (e.g. use `gcp.common.CommonTemplates` instead of `gcp.CommonTemplates` to save us from an extra `gcp/__init__.py`). * __`synthtool/sources/templates.py`__: No changes made. Internally used by `common_templates`. Contains the underlying usage of jinja2 to render the templates. * __`synthtool/transforms.py`__: No changes made. Contains a few functions that are commonly used by `owlbot.py` files ([example](https://github.com/googleapis/java-bigtable/blob/45732201880a13eeced3d0332bd172aae0f73dbe/owlbot.py#L50)) ### Changes in Dockerfile We will not clone `synthtool` anymore. We will instead install it as a separate package whose source code is within sdk-platform-java.
1 parent 2fc938a commit 7ccec6d

File tree

13 files changed

+2093
-29
lines changed

13 files changed

+2093
-29
lines changed

.cloudbuild/library_generation/library_generation.Dockerfile

+1-10
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ FROM gcr.io/cloud-devrel-public-resources/python
1717

1818
SHELL [ "/bin/bash", "-c" ]
1919

20-
ARG SYNTHTOOL_COMMITTISH=696c4bff721f5541cd75fdc97d413f8f39d2a2c1
2120
ARG OWLBOT_CLI_COMMITTISH=ac84fa5c423a0069bbce3d2d869c9730c8fdf550
2221
ARG PROTOC_VERSION=25.3
2322
ENV HOME=/home
@@ -44,19 +43,11 @@ RUN ln -s $(which python3.11) /usr/local/bin/python
4443
RUN ln -s $(which python3.11) /usr/local/bin/python3
4544
RUN python -m pip install --upgrade pip
4645

47-
# install scripts as a python package
46+
# install main scripts as a python package
4847
WORKDIR /src
4948
RUN python -m pip install -r requirements.txt
5049
RUN python -m pip install .
5150

52-
# install synthtool
53-
WORKDIR /tools
54-
RUN git clone https://github.com/googleapis/synthtool
55-
WORKDIR /tools/synthtool
56-
RUN git checkout "${SYNTHTOOL_COMMITTISH}"
57-
RUN python3 -m pip install --no-deps -e .
58-
RUN python3 -m pip install -r requirements.in
59-
6051
# Install nvm with node and npm
6152
ENV NODE_VERSION 20.12.0
6253
WORKDIR /home

.github/snippet-bot.yml

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ ignoreFiles:
55
- test/**
66
- showcase/**
77
- library_generation/owlbot/templates/java_library/samples/install-without-bom/pom.xml
8+
- library_generation/owlbot/synthtool/gcp/snippets.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright 2024 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Synthtool synthesizes libraries from disparate sources."""
16+
17+
import sys
18+
19+
from synthtool.transforms import (
20+
move,
21+
replace,
22+
get_staging_dirs,
23+
remove_staging_dirs,
24+
)
25+
26+
copy = move
27+
28+
__all__ = [
29+
"copy",
30+
"move",
31+
"replace",
32+
"get_staging_dirs",
33+
"remove_staging_dirs",
34+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright 2024 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Tracked paths.
16+
17+
This is a bit of a hack (imported from original synthtool library).
18+
"""
19+
20+
import pathlib
21+
22+
23+
_tracked_paths = []
24+
25+
26+
def add(path):
27+
_tracked_paths.append(pathlib.Path(path))
28+
# Reverse sort the list, so that the deepest paths get matched first.
29+
_tracked_paths.sort(key=lambda s: -len(str(s)))
30+
31+
32+
def relativize(path):
33+
path = pathlib.Path(path)
34+
for tracked_path in _tracked_paths:
35+
try:
36+
return path.relative_to(tracked_path)
37+
except ValueError:
38+
pass
39+
raise ValueError(f"The root for {path} is not tracked.")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Copyright 2024 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
import os
17+
import re
18+
import sys
19+
import shutil
20+
import yaml
21+
from pathlib import Path
22+
from typing import Dict, List, Optional
23+
import jinja2
24+
import logging
25+
26+
from synthtool import _tracked_paths
27+
from synthtool.sources import templates
28+
29+
logger = logging.getLogger()
30+
logger.setLevel(logging.DEBUG)
31+
32+
33+
DEFAULT_TEMPLATES_PATH = "synthtool/gcp/templates"
34+
LOCAL_TEMPLATES: Optional[str] = os.environ.get("SYNTHTOOL_TEMPLATES")
35+
36+
# Originally brought from gcp/partials.py.
37+
# These are the default locations to look up
38+
_DEFAULT_PARTIAL_FILES = [
39+
".readme-partials.yml",
40+
".readme-partials.yaml",
41+
".integration-partials.yaml",
42+
]
43+
44+
45+
class CommonTemplates:
46+
def __init__(self, template_path: Optional[Path] = None):
47+
if LOCAL_TEMPLATES is None:
48+
logger.error("env var SYNTHTOOL_TEMPLATES must be set")
49+
sys.exit(1)
50+
logger.debug(f"Using local templates at {LOCAL_TEMPLATES}")
51+
self._template_root = Path(LOCAL_TEMPLATES)
52+
self._templates = templates.Templates(self._template_root)
53+
self.excludes = [] # type: List[str]
54+
55+
def _generic_library(self, directory: str, relative_dir=None, **kwargs) -> Path:
56+
# load common repo meta information (metadata that's not language specific).
57+
if "metadata" in kwargs:
58+
self._load_generic_metadata(kwargs["metadata"], relative_dir=relative_dir)
59+
# if no samples were found, don't attempt to render a
60+
# samples/README.md.
61+
if "samples" not in kwargs["metadata"] or not kwargs["metadata"]["samples"]:
62+
self.excludes.append("samples/README.md")
63+
64+
t = templates.TemplateGroup(self._template_root / directory, self.excludes)
65+
66+
result = t.render(**kwargs)
67+
_tracked_paths.add(result)
68+
69+
return result
70+
71+
def java_library(self, **kwargs) -> Path:
72+
# kwargs["metadata"] is required to load values from .repo-metadata.json
73+
if "metadata" not in kwargs:
74+
kwargs["metadata"] = {}
75+
return self._generic_library("java_library", **kwargs)
76+
77+
def render(self, template_name: str, **kwargs) -> Path:
78+
template = self._templates.render(template_name, **kwargs)
79+
_tracked_paths.add(template)
80+
return template
81+
82+
def _load_generic_metadata(self, metadata: Dict, relative_dir=None):
83+
"""
84+
loads additional meta information from .repo-metadata.json.
85+
"""
86+
metadata["partials"] = load_partials()
87+
88+
# Loads repo metadata information from the default location if it
89+
# hasn't already been set. Some callers may have already loaded repo
90+
# metadata, so we don't need to do it again or overwrite it. Also, only
91+
# set the "repo" key.
92+
if "repo" not in metadata:
93+
metadata["repo"] = _load_repo_metadata(relative_dir=relative_dir)
94+
95+
96+
def _load_repo_metadata(
97+
relative_dir=None, metadata_file: str = "./.repo-metadata.json"
98+
) -> Dict:
99+
"""Parse a metadata JSON file into a Dict.
100+
101+
Currently, the defined fields are:
102+
* `name` - The service's API name
103+
* `name_pretty` - The service's API title. This will be used for generating titles on READMEs
104+
* `product_documentation` - The product documentation on cloud.google.com
105+
* `client_documentation` - The client library reference documentation
106+
* `issue_tracker` - The public issue tracker for the product
107+
* `release_level` - The release level of the client library. One of: alpha, beta,
108+
ga, deprecated, preview, stable
109+
* `language` - The repo language. One of dotnet, go, java, nodejs, php, python, ruby
110+
* `repo` - The GitHub repo in the format {owner}/{repo}
111+
* `distribution_name` - The language-idiomatic package/distribution name
112+
* `api_id` - The API ID associated with the service. Fully qualified identifier use to
113+
enable a service in the cloud platform (e.g. monitoring.googleapis.com)
114+
* `requires_billing` - Whether or not the API requires billing to be configured on the
115+
customer's acocunt
116+
117+
Args:
118+
metadata_file (str, optional): Path to the metadata json file
119+
120+
Returns:
121+
A dictionary of metadata. This may not necessarily include all the defined fields above.
122+
"""
123+
if relative_dir is not None:
124+
if os.path.exists(Path(relative_dir, metadata_file).resolve()):
125+
with open(Path(relative_dir, metadata_file).resolve()) as f:
126+
return json.load(f)
127+
elif os.path.exists(metadata_file):
128+
with open(metadata_file) as f:
129+
return json.load(f)
130+
return {}
131+
132+
133+
def load_partials(files: List[str] = []) -> Dict:
134+
"""
135+
hand-crafted artisanal markdown can be provided in a .readme-partials.yml.
136+
The following fields are currently supported:
137+
138+
body: custom body to include in the usage section of the document.
139+
samples_body: an optional body to place below the table of contents
140+
in samples/README.md.
141+
introduction: a more thorough introduction than metadata["description"].
142+
title: provide markdown to use as a custom title.
143+
deprecation_warning: a warning to indicate that the library has been
144+
deprecated and a pointer to an alternate option
145+
"""
146+
result: Dict[str, Dict] = {}
147+
cwd_path = Path(os.getcwd())
148+
for file in files + _DEFAULT_PARTIAL_FILES:
149+
partials_file = cwd_path / file
150+
if os.path.exists(partials_file):
151+
with open(partials_file) as f:
152+
result.update(yaml.load(f, Loader=yaml.SafeLoader))
153+
return result
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Copyright 2020 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import glob
16+
import logging
17+
import re
18+
import os
19+
import yaml
20+
from typing import List, Dict
21+
22+
logger = logging.getLogger()
23+
logger.setLevel(logging.DEBUG)
24+
25+
26+
def _read_sample_metadata_comment(sample_file: str) -> Dict:
27+
"""Additional meta-information can be provided through embedded comments:
28+
29+
// sample-metadata:
30+
// title: ACL (Access Control)
31+
// description: Demonstrates setting access control rules.
32+
// usage: node iam.js --help
33+
"""
34+
sample_metadata = {} # type: Dict[str, str]
35+
with open(sample_file) as f:
36+
contents = f.read()
37+
match = re.search(
38+
r"(?P<metadata>// *sample-metadata:([^\n]+|\n//)+)", contents, re.DOTALL
39+
)
40+
if match:
41+
# the metadata yaml is stored in a comments, remove the
42+
# prefix so that we can parse the yaml contained.
43+
sample_metadata_string = re.sub(r"((#|//) ?)", "", match.group("metadata"))
44+
try:
45+
sample_metadata = yaml.load(
46+
sample_metadata_string, Loader=yaml.SafeLoader
47+
)["sample-metadata"]
48+
except yaml.scanner.ScannerError:
49+
# warn and continue on bad metadata
50+
logger.warning(f"bad metadata detected in {sample_file}")
51+
return sample_metadata
52+
53+
54+
def _sample_metadata(file: str) -> Dict[str, str]:
55+
metadata = {
56+
"title": _decamelize(os.path.splitext(os.path.basename(file))[0]),
57+
"file": file,
58+
}
59+
return {**metadata, **_read_sample_metadata_comment(file)}
60+
61+
62+
def all_samples(sample_globs: List[str]) -> List[Dict[str, str]]:
63+
"""Walks samples directory and builds up samples data-structure
64+
65+
Args:
66+
sample_globs: (List[str]): List of path globs to search for samples
67+
68+
Returns:
69+
A list of sample metadata in the format:
70+
{
71+
"title": "Requester Pays",
72+
"file": "samples/requesterPays.js"
73+
}
74+
The file path is the relative path from the repository root.
75+
"""
76+
files = []
77+
for sample_glob in sample_globs:
78+
for file in glob.glob(sample_glob, recursive=True):
79+
files.append(file)
80+
return [_sample_metadata(file) for file in sorted(files)]
81+
82+
83+
def _decamelize(value: str):
84+
"""Parser to convert fooBar.js to Foo Bar."""
85+
if not value:
86+
return ""
87+
str_decamelize = re.sub("^.", value[0].upper(), value) # apple -> Apple.
88+
str_decamelize = re.sub(
89+
"([A-Z]+)([A-Z])([a-z0-9])", r"\1 \2\3", str_decamelize
90+
) # ACLBatman -> ACL Batman.
91+
return re.sub("([a-z0-9])([A-Z])", r"\1 \2", str_decamelize) # FooBar -> Foo Bar.

0 commit comments

Comments
 (0)