1*55e87721SMatt Gilbride# Copyright 2018 Google LLC 2*55e87721SMatt Gilbride# 3*55e87721SMatt Gilbride# Licensed under the Apache License, Version 2.0 (the "License"); 4*55e87721SMatt Gilbride# you may not use this file except in compliance with the License. 5*55e87721SMatt Gilbride# You may obtain a copy of the License at 6*55e87721SMatt Gilbride# 7*55e87721SMatt Gilbride# https://www.apache.org/licenses/LICENSE-2.0 8*55e87721SMatt Gilbride# 9*55e87721SMatt Gilbride# Unless required by applicable law or agreed to in writing, software 10*55e87721SMatt Gilbride# distributed under the License is distributed on an "AS IS" BASIS, 11*55e87721SMatt Gilbride# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*55e87721SMatt Gilbride# See the License for the specific language governing permissions and 13*55e87721SMatt Gilbride# limitations under the License. 14*55e87721SMatt Gilbride 15*55e87721SMatt Gilbrideimport json 16*55e87721SMatt Gilbrideimport os 17*55e87721SMatt Gilbrideimport re 18*55e87721SMatt Gilbrideimport shutil 19*55e87721SMatt Gilbrideimport fnmatch 20*55e87721SMatt Gilbridefrom copy import deepcopy 21*55e87721SMatt Gilbridefrom pathlib import Path 22*55e87721SMatt Gilbridefrom typing import Dict, List, Optional 23*55e87721SMatt Gilbrideimport jinja2 24*55e87721SMatt Gilbridefrom datetime import date 25*55e87721SMatt Gilbride 26*55e87721SMatt Gilbridefrom synthtool import shell, _tracked_paths 27*55e87721SMatt Gilbridefrom synthtool.gcp import partials 28*55e87721SMatt Gilbridefrom synthtool.languages import node, node_mono_repo 29*55e87721SMatt Gilbridefrom synthtool.log import logger 30*55e87721SMatt Gilbridefrom synthtool.sources import git, templates 31*55e87721SMatt Gilbride 32*55e87721SMatt GilbridePathOrStr = templates.PathOrStr 33*55e87721SMatt GilbrideTEMPLATES_URL: str = git.make_repo_clone_url("googleapis/synthtool") 34*55e87721SMatt GilbrideDEFAULT_TEMPLATES_PATH = "synthtool/gcp/templates" 35*55e87721SMatt GilbrideLOCAL_TEMPLATES: Optional[str] = os.environ.get("SYNTHTOOL_TEMPLATES") 36*55e87721SMatt Gilbride 37*55e87721SMatt Gilbride 38*55e87721SMatt Gilbrideclass CommonTemplates: 39*55e87721SMatt Gilbride def __init__(self, template_path: Optional[Path] = None): 40*55e87721SMatt Gilbride if template_path: 41*55e87721SMatt Gilbride self._template_root = template_path 42*55e87721SMatt Gilbride elif LOCAL_TEMPLATES: 43*55e87721SMatt Gilbride logger.debug(f"Using local templates at {LOCAL_TEMPLATES}") 44*55e87721SMatt Gilbride self._template_root = Path(LOCAL_TEMPLATES) 45*55e87721SMatt Gilbride else: 46*55e87721SMatt Gilbride templates_git = git.clone(TEMPLATES_URL) 47*55e87721SMatt Gilbride self._template_root = templates_git / DEFAULT_TEMPLATES_PATH 48*55e87721SMatt Gilbride 49*55e87721SMatt Gilbride self._templates = templates.Templates(self._template_root) 50*55e87721SMatt Gilbride self.excludes = [] # type: List[str] 51*55e87721SMatt Gilbride 52*55e87721SMatt Gilbride def _generic_library(self, directory: str, relative_dir=None, **kwargs) -> Path: 53*55e87721SMatt Gilbride # load common repo meta information (metadata that's not language specific). 54*55e87721SMatt Gilbride if "metadata" in kwargs: 55*55e87721SMatt Gilbride self._load_generic_metadata(kwargs["metadata"], relative_dir=relative_dir) 56*55e87721SMatt Gilbride # if no samples were found, don't attempt to render a 57*55e87721SMatt Gilbride # samples/README.md. 58*55e87721SMatt Gilbride if "samples" not in kwargs["metadata"] or not kwargs["metadata"]["samples"]: 59*55e87721SMatt Gilbride self.excludes.append("samples/README.md") 60*55e87721SMatt Gilbride 61*55e87721SMatt Gilbride t = templates.TemplateGroup(self._template_root / directory, self.excludes) 62*55e87721SMatt Gilbride 63*55e87721SMatt Gilbride if "repository" in kwargs["metadata"] and "repo" in kwargs["metadata"]: 64*55e87721SMatt Gilbride kwargs["metadata"]["repo"]["default_branch"] = _get_default_branch_name( 65*55e87721SMatt Gilbride kwargs["metadata"]["repository"] 66*55e87721SMatt Gilbride ) 67*55e87721SMatt Gilbride 68*55e87721SMatt Gilbride # TODO: migrate to python.py once old sample gen is deprecated 69*55e87721SMatt Gilbride if directory == "python_samples": 70*55e87721SMatt Gilbride t.env.globals["get_help"] = lambda filename: shell.run( 71*55e87721SMatt Gilbride ["python", filename, "--help"] 72*55e87721SMatt Gilbride ).stdout 73*55e87721SMatt Gilbride 74*55e87721SMatt Gilbride result = t.render(**kwargs) 75*55e87721SMatt Gilbride _tracked_paths.add(result) 76*55e87721SMatt Gilbride 77*55e87721SMatt Gilbride return result 78*55e87721SMatt Gilbride 79*55e87721SMatt Gilbride def py_samples(self, **kwargs) -> List[Path]: 80*55e87721SMatt Gilbride """ 81*55e87721SMatt Gilbride Handles generation of README.md templates for Python samples 82*55e87721SMatt Gilbride - Determines whether generation is being done in a client library or in a samples 83*55e87721SMatt Gilbride folder automatically 84*55e87721SMatt Gilbride - Otherwise accepts manually set sample_project_dir through kwargs metadata 85*55e87721SMatt Gilbride - Delegates generation of additional sample documents alternate/overridden folders 86*55e87721SMatt Gilbride through py_samples_override() 87*55e87721SMatt Gilbride """ 88*55e87721SMatt Gilbride # kwargs["metadata"] is required to load values from .repo-metadata.json 89*55e87721SMatt Gilbride if "metadata" not in kwargs: 90*55e87721SMatt Gilbride kwargs["metadata"] = {} 91*55e87721SMatt Gilbride 92*55e87721SMatt Gilbride # load common repo meta information (metadata that's not language specific). 93*55e87721SMatt Gilbride self._load_generic_metadata(kwargs["metadata"]) 94*55e87721SMatt Gilbride 95*55e87721SMatt Gilbride # temporary exclusion prior to old templates being migrated out 96*55e87721SMatt Gilbride self.excludes.extend( 97*55e87721SMatt Gilbride [ 98*55e87721SMatt Gilbride "README.rst", 99*55e87721SMatt Gilbride "auth_api_key.tmpl.rst", 100*55e87721SMatt Gilbride "auth.tmpl.rst", 101*55e87721SMatt Gilbride "install_deps.tmpl.rst", 102*55e87721SMatt Gilbride "install_portaudio.tmpl.rst", 103*55e87721SMatt Gilbride "noxfile.py.j2", 104*55e87721SMatt Gilbride ] 105*55e87721SMatt Gilbride ) 106*55e87721SMatt Gilbride 107*55e87721SMatt Gilbride # ensure samples will generate 108*55e87721SMatt Gilbride kwargs["metadata"]["samples"] = True 109*55e87721SMatt Gilbride 110*55e87721SMatt Gilbride # determine if in client lib and set custom root sample dir if specified, else None 111*55e87721SMatt Gilbride in_client_library = Path("samples").exists() 112*55e87721SMatt Gilbride sample_project_dir = kwargs["metadata"]["repo"].get("sample_project_dir") 113*55e87721SMatt Gilbride 114*55e87721SMatt Gilbride if sample_project_dir is None: # Not found in metadata 115*55e87721SMatt Gilbride if in_client_library: 116*55e87721SMatt Gilbride sample_project_dir = "samples" 117*55e87721SMatt Gilbride else: 118*55e87721SMatt Gilbride sample_project_dir = "." 119*55e87721SMatt Gilbride elif not Path(sample_project_dir).exists(): 120*55e87721SMatt Gilbride raise Exception(f"'{sample_project_dir}' does not exist") 121*55e87721SMatt Gilbride 122*55e87721SMatt Gilbride override_paths_to_samples: Dict[ 123*55e87721SMatt Gilbride str, List[str] 124*55e87721SMatt Gilbride ] = {} # Dict of format { override_path : sample(s) } 125*55e87721SMatt Gilbride samples_dict = deepcopy(kwargs["metadata"]["repo"].get("samples")) 126*55e87721SMatt Gilbride default_samples_dict = [] # Dict which will generate in sample_project_dir 127*55e87721SMatt Gilbride 128*55e87721SMatt Gilbride # Iterate through samples to store override_paths_to_samples for all existing 129*55e87721SMatt Gilbride # override paths 130*55e87721SMatt Gilbride for sample_idx, sample in enumerate(samples_dict): 131*55e87721SMatt Gilbride override_path = samples_dict[sample_idx].get("override_path") 132*55e87721SMatt Gilbride 133*55e87721SMatt Gilbride if override_path is not None: 134*55e87721SMatt Gilbride # add absolute path to metadata so `python foo.py --help` succeeds 135*55e87721SMatt Gilbride if sample.get("file") is not None: 136*55e87721SMatt Gilbride path = os.path.join( 137*55e87721SMatt Gilbride sample_project_dir, override_path, sample.get("file") 138*55e87721SMatt Gilbride ) 139*55e87721SMatt Gilbride sample["abs_path"] = Path(path).resolve() 140*55e87721SMatt Gilbride 141*55e87721SMatt Gilbride cur_override_sample = override_paths_to_samples.get(override_path) 142*55e87721SMatt Gilbride # Base case: No samples are yet planned to gen in this override dir 143*55e87721SMatt Gilbride if cur_override_sample is None: 144*55e87721SMatt Gilbride override_paths_to_samples[override_path] = [sample] 145*55e87721SMatt Gilbride # Else: Sample docs will be generated in README merged with other 146*55e87721SMatt Gilbride # sample doc(s) already planned to generate in this dir 147*55e87721SMatt Gilbride else: 148*55e87721SMatt Gilbride cur_override_sample.append(sample) 149*55e87721SMatt Gilbride override_paths_to_samples[override_path] = cur_override_sample 150*55e87721SMatt Gilbride # If override path none, will be generated in the default 151*55e87721SMatt Gilbride # folder: sample_project_dir 152*55e87721SMatt Gilbride else: 153*55e87721SMatt Gilbride if sample.get("file") is not None: 154*55e87721SMatt Gilbride path = os.path.join(sample_project_dir, sample.get("file")) 155*55e87721SMatt Gilbride sample["abs_path"] = Path(path).resolve() 156*55e87721SMatt Gilbride default_samples_dict.append(sample) 157*55e87721SMatt Gilbride 158*55e87721SMatt Gilbride # List of paths to tempdirs which will be copied into sample folders 159*55e87721SMatt Gilbride result = [] 160*55e87721SMatt Gilbride 161*55e87721SMatt Gilbride # deep copy is req. here to avoid kwargs being affected 162*55e87721SMatt Gilbride overridden_samples_kwargs = deepcopy(kwargs) 163*55e87721SMatt Gilbride for override_path in override_paths_to_samples: 164*55e87721SMatt Gilbride # Generate override sample docs 165*55e87721SMatt Gilbride result.append( 166*55e87721SMatt Gilbride self.py_samples_override( 167*55e87721SMatt Gilbride root=sample_project_dir, 168*55e87721SMatt Gilbride override_path=override_path, 169*55e87721SMatt Gilbride override_samples=override_paths_to_samples[override_path], 170*55e87721SMatt Gilbride **overridden_samples_kwargs, 171*55e87721SMatt Gilbride ) 172*55e87721SMatt Gilbride ) 173*55e87721SMatt Gilbride kwargs["metadata"]["repo"]["samples"] = default_samples_dict 174*55e87721SMatt Gilbride 175*55e87721SMatt Gilbride logger.debug( 176*55e87721SMatt Gilbride f"Generating templates for samples directory '{sample_project_dir}'" 177*55e87721SMatt Gilbride ) 178*55e87721SMatt Gilbride kwargs["subdir"] = sample_project_dir 179*55e87721SMatt Gilbride # Generate default sample docs 180*55e87721SMatt Gilbride result.append(self._generic_library("python_samples", **kwargs)) 181*55e87721SMatt Gilbride 182*55e87721SMatt Gilbride for path in result: 183*55e87721SMatt Gilbride # .add() records the root of the paths and needs to be applied to each 184*55e87721SMatt Gilbride _tracked_paths.add(path) 185*55e87721SMatt Gilbride 186*55e87721SMatt Gilbride return result 187*55e87721SMatt Gilbride 188*55e87721SMatt Gilbride def py_samples_override( 189*55e87721SMatt Gilbride self, root, override_path, override_samples, **overridden_samples_kwargs 190*55e87721SMatt Gilbride ) -> Path: 191*55e87721SMatt Gilbride """ 192*55e87721SMatt Gilbride Handles additional generation of READMEs where "override_path"s 193*55e87721SMatt Gilbride are set in one or more samples' metadata 194*55e87721SMatt Gilbride """ 195*55e87721SMatt Gilbride overridden_samples_kwargs["metadata"]["repo"][ 196*55e87721SMatt Gilbride "sample_project_dir" 197*55e87721SMatt Gilbride ] = override_path 198*55e87721SMatt Gilbride # Set samples metadata to ONLY samples intended to generate 199*55e87721SMatt Gilbride # under this directory (override_path) 200*55e87721SMatt Gilbride overridden_samples_kwargs["metadata"]["repo"]["samples"] = override_samples 201*55e87721SMatt Gilbride if root != ".": 202*55e87721SMatt Gilbride override_path = Path(root) / override_path 203*55e87721SMatt Gilbride 204*55e87721SMatt Gilbride logger.debug(f"Generating templates for override path '{override_path}'") 205*55e87721SMatt Gilbride 206*55e87721SMatt Gilbride overridden_samples_kwargs["subdir"] = override_path 207*55e87721SMatt Gilbride return self._generic_library("python_samples", **overridden_samples_kwargs) 208*55e87721SMatt Gilbride 209*55e87721SMatt Gilbride def python_notebooks(self, **kwargs) -> Path: 210*55e87721SMatt Gilbride # kwargs["metadata"] is required to load values from .repo-metadata.json 211*55e87721SMatt Gilbride if "metadata" not in kwargs: 212*55e87721SMatt Gilbride kwargs["metadata"] = {} 213*55e87721SMatt Gilbride return self._generic_library("python_notebooks", **kwargs) 214*55e87721SMatt Gilbride 215*55e87721SMatt Gilbride def py_mono_repo_library(self, relative_dir, **kwargs) -> Path: 216*55e87721SMatt Gilbride # kwargs["metadata"] is required to load values from .repo-metadata.json 217*55e87721SMatt Gilbride if "metadata" not in kwargs: 218*55e87721SMatt Gilbride kwargs["metadata"] = {} 219*55e87721SMatt Gilbride 220*55e87721SMatt Gilbride # load common repo meta information (metadata that's not language specific). 221*55e87721SMatt Gilbride self._load_generic_metadata(kwargs["metadata"], relative_dir) 222*55e87721SMatt Gilbride 223*55e87721SMatt Gilbride # initialize default_version if it doesn't exist in kwargs["metadata"]['repo'] 224*55e87721SMatt Gilbride if "default_version" not in kwargs["metadata"]["repo"]: 225*55e87721SMatt Gilbride kwargs["metadata"]["repo"]["default_version"] = "" 226*55e87721SMatt Gilbride 227*55e87721SMatt Gilbride # Don't add `docs/index.rst` if `versions` is not provided or `default_version` is empty 228*55e87721SMatt Gilbride if ( 229*55e87721SMatt Gilbride "versions" not in kwargs 230*55e87721SMatt Gilbride or not kwargs["metadata"]["repo"]["default_version"] 231*55e87721SMatt Gilbride or kwargs["metadata"]["repo"]["default_version"] == "apiVersion" 232*55e87721SMatt Gilbride ): 233*55e87721SMatt Gilbride self.excludes += ["docs/index.rst"] 234*55e87721SMatt Gilbride 235*55e87721SMatt Gilbride # If the directory `google/cloud` exists, add kwargs to signal that the client library is for a Cloud API 236*55e87721SMatt Gilbride if Path("google/cloud").exists(): 237*55e87721SMatt Gilbride kwargs["is_google_cloud_api"] = True 238*55e87721SMatt Gilbride 239*55e87721SMatt Gilbride return self._generic_library("python_mono_repo_library", relative_dir, **kwargs) 240*55e87721SMatt Gilbride 241*55e87721SMatt Gilbride def py_library(self, **kwargs) -> Path: 242*55e87721SMatt Gilbride # kwargs["metadata"] is required to load values from .repo-metadata.json 243*55e87721SMatt Gilbride if "metadata" not in kwargs: 244*55e87721SMatt Gilbride kwargs["metadata"] = {} 245*55e87721SMatt Gilbride 246*55e87721SMatt Gilbride # load common repo meta information (metadata that's not language specific). 247*55e87721SMatt Gilbride self._load_generic_metadata(kwargs["metadata"]) 248*55e87721SMatt Gilbride 249*55e87721SMatt Gilbride # initialize default_version if it doesn't exist in kwargs["metadata"]['repo'] 250*55e87721SMatt Gilbride if "default_version" not in kwargs["metadata"]["repo"]: 251*55e87721SMatt Gilbride kwargs["metadata"]["repo"]["default_version"] = "" 252*55e87721SMatt Gilbride 253*55e87721SMatt Gilbride # rename variable to accommodate existing owlbot.py files 254*55e87721SMatt Gilbride if "system_test_dependencies" in kwargs: 255*55e87721SMatt Gilbride kwargs["system_test_local_dependencies"] = kwargs[ 256*55e87721SMatt Gilbride "system_test_dependencies" 257*55e87721SMatt Gilbride ] 258*55e87721SMatt Gilbride logger.warning( 259*55e87721SMatt Gilbride "Template argument 'system_test_dependencies' is deprecated." 260*55e87721SMatt Gilbride "Use 'system_test_local_dependencies' or 'system_test_external_dependencies'" 261*55e87721SMatt Gilbride "instead." 262*55e87721SMatt Gilbride ) 263*55e87721SMatt Gilbride 264*55e87721SMatt Gilbride # Set default Python versions for noxfile.py 265*55e87721SMatt Gilbride if "default_python_version" not in kwargs: 266*55e87721SMatt Gilbride kwargs["default_python_version"] = "3.8" 267*55e87721SMatt Gilbride if "unit_test_python_versions" not in kwargs: 268*55e87721SMatt Gilbride kwargs["unit_test_python_versions"] = ["3.7", "3.8", "3.9", "3.10"] 269*55e87721SMatt Gilbride 270*55e87721SMatt Gilbride if "system_test_python_versions" not in kwargs: 271*55e87721SMatt Gilbride kwargs["system_test_python_versions"] = ["3.8"] 272*55e87721SMatt Gilbride 273*55e87721SMatt Gilbride # If cov_level is not given, set it to None. 274*55e87721SMatt Gilbride if "cov_level" not in kwargs: 275*55e87721SMatt Gilbride kwargs["cov_level"] = None 276*55e87721SMatt Gilbride 277*55e87721SMatt Gilbride # Don't add samples templates if there are no samples 278*55e87721SMatt Gilbride if "samples" not in kwargs: 279*55e87721SMatt Gilbride self.excludes += ["samples/AUTHORING_GUIDE.md", "samples/CONTRIBUTING.md"] 280*55e87721SMatt Gilbride 281*55e87721SMatt Gilbride # Don't add `docs/index.rst` if `versions` is not provided or `default_version` is empty 282*55e87721SMatt Gilbride if ( 283*55e87721SMatt Gilbride "versions" not in kwargs 284*55e87721SMatt Gilbride or not kwargs["metadata"]["repo"]["default_version"] 285*55e87721SMatt Gilbride ): 286*55e87721SMatt Gilbride self.excludes += ["docs/index.rst"] 287*55e87721SMatt Gilbride 288*55e87721SMatt Gilbride # Add kwargs to signal that UPGRADING.md should be included in docs/index.rst if it exists 289*55e87721SMatt Gilbride if Path("docs/UPGRADING.md").exists() or Path("docs/UPGRADING.rst").exists(): 290*55e87721SMatt Gilbride kwargs["include_uprading_doc"] = True 291*55e87721SMatt Gilbride 292*55e87721SMatt Gilbride # If the directory `google/cloud` exists, add kwargs to signal that the client library is for a Cloud API 293*55e87721SMatt Gilbride if Path("google/cloud").exists(): 294*55e87721SMatt Gilbride kwargs["is_google_cloud_api"] = True 295*55e87721SMatt Gilbride 296*55e87721SMatt Gilbride # If Dockerfile exists in .kokoro/docker/samples, add kwargs to 297*55e87721SMatt Gilbride # signal that a custom docker image should be used when testing samples. 298*55e87721SMatt Gilbride kwargs["custom_samples_dockerfile"] = Path( 299*55e87721SMatt Gilbride ".kokoro/docker/samples/Dockerfile" 300*55e87721SMatt Gilbride ).exists() 301*55e87721SMatt Gilbride 302*55e87721SMatt Gilbride ret = self._generic_library("python_library", **kwargs) 303*55e87721SMatt Gilbride 304*55e87721SMatt Gilbride # If split_system_tests is set to True, we disable the system 305*55e87721SMatt Gilbride # test in the main presubmit build and create individual build 306*55e87721SMatt Gilbride # configs for each python versions. 307*55e87721SMatt Gilbride if kwargs.get("split_system_tests", False): 308*55e87721SMatt Gilbride template_root = self._template_root / "py_library_split_systests" 309*55e87721SMatt Gilbride # copy the main presubmit config 310*55e87721SMatt Gilbride shutil.copy2( 311*55e87721SMatt Gilbride template_root / ".kokoro/presubmit/presubmit.cfg", 312*55e87721SMatt Gilbride ret / ".kokoro/presubmit/presubmit.cfg", 313*55e87721SMatt Gilbride ) 314*55e87721SMatt Gilbride env = jinja2.Environment(loader=jinja2.FileSystemLoader(str(template_root))) 315*55e87721SMatt Gilbride tmpl = env.get_template(".kokoro/presubmit/system.cfg") 316*55e87721SMatt Gilbride for v in kwargs["system_test_python_versions"]: 317*55e87721SMatt Gilbride nox_session = f"system-{v}" 318*55e87721SMatt Gilbride dest = ret / f".kokoro/presubmit/system-{v}.cfg" 319*55e87721SMatt Gilbride content = tmpl.render(nox_session=nox_session) 320*55e87721SMatt Gilbride with open(dest, "w") as f: 321*55e87721SMatt Gilbride f.write(content) 322*55e87721SMatt Gilbride return ret 323*55e87721SMatt Gilbride 324*55e87721SMatt Gilbride def java_library(self, **kwargs) -> Path: 325*55e87721SMatt Gilbride # kwargs["metadata"] is required to load values from .repo-metadata.json 326*55e87721SMatt Gilbride if "metadata" not in kwargs: 327*55e87721SMatt Gilbride kwargs["metadata"] = {} 328*55e87721SMatt Gilbride return self._generic_library("java_library", **kwargs) 329*55e87721SMatt Gilbride 330*55e87721SMatt Gilbride def node_library(self, **kwargs) -> Path: 331*55e87721SMatt Gilbride # TODO: once we've migrated all Node.js repos to either having 332*55e87721SMatt Gilbride # .repo-metadata.json, or excluding README.md, we can remove this. 333*55e87721SMatt Gilbride if not os.path.exists("./.repo-metadata.json"): 334*55e87721SMatt Gilbride self.excludes.append("README.md") 335*55e87721SMatt Gilbride if "samples/README.md" not in self.excludes: 336*55e87721SMatt Gilbride self.excludes.append("samples/README.md") 337*55e87721SMatt Gilbride 338*55e87721SMatt Gilbride kwargs["metadata"] = node.template_metadata() 339*55e87721SMatt Gilbride kwargs["publish_token"] = node.get_publish_token(kwargs["metadata"]["name"]) 340*55e87721SMatt Gilbride 341*55e87721SMatt Gilbride ignore_src_index = [ 342*55e87721SMatt Gilbride "yes" for f in self.excludes if fnmatch.fnmatch("src/index.ts", f) 343*55e87721SMatt Gilbride ] 344*55e87721SMatt Gilbride # generate root-level `src/index.ts` to export multiple versions and its default clients 345*55e87721SMatt Gilbride if ( 346*55e87721SMatt Gilbride "versions" in kwargs 347*55e87721SMatt Gilbride and "default_version" in kwargs 348*55e87721SMatt Gilbride and not ignore_src_index 349*55e87721SMatt Gilbride ): 350*55e87721SMatt Gilbride node.generate_index_ts( 351*55e87721SMatt Gilbride versions=kwargs["versions"], default_version=kwargs["default_version"] 352*55e87721SMatt Gilbride ) 353*55e87721SMatt Gilbride 354*55e87721SMatt Gilbride return self._generic_library("node_library", **kwargs) 355*55e87721SMatt Gilbride 356*55e87721SMatt Gilbride def node_mono_repo_library(self, relative_dir, **kwargs) -> Path: 357*55e87721SMatt Gilbride # TODO: once we've migrated all Node.js repos to either having 358*55e87721SMatt Gilbride # .repo-metadata.json, or excluding README.md, we can remove this. 359*55e87721SMatt Gilbride if not os.path.exists(Path(relative_dir, ".repo-metadata.json").resolve()): 360*55e87721SMatt Gilbride self.excludes.append("README.md") 361*55e87721SMatt Gilbride if "samples/README.md" not in self.excludes: 362*55e87721SMatt Gilbride self.excludes.append("samples/README.md") 363*55e87721SMatt Gilbride 364*55e87721SMatt Gilbride kwargs["metadata"] = node_mono_repo.template_metadata(relative_dir) 365*55e87721SMatt Gilbride 366*55e87721SMatt Gilbride ignore_src_index = [ 367*55e87721SMatt Gilbride "yes" for f in self.excludes if fnmatch.fnmatch("src/index.ts", f) 368*55e87721SMatt Gilbride ] 369*55e87721SMatt Gilbride # generate root-level `src/index.ts` to export multiple versions and its default clients 370*55e87721SMatt Gilbride if ( 371*55e87721SMatt Gilbride "versions" in kwargs 372*55e87721SMatt Gilbride and "default_version" in kwargs 373*55e87721SMatt Gilbride and not ignore_src_index 374*55e87721SMatt Gilbride ): 375*55e87721SMatt Gilbride node_mono_repo.generate_index_ts( 376*55e87721SMatt Gilbride versions=kwargs["versions"], 377*55e87721SMatt Gilbride default_version=kwargs["default_version"], 378*55e87721SMatt Gilbride relative_dir=relative_dir, 379*55e87721SMatt Gilbride year=str(date.today().year), 380*55e87721SMatt Gilbride ) 381*55e87721SMatt Gilbride 382*55e87721SMatt Gilbride return self._generic_library( 383*55e87721SMatt Gilbride "node_mono_repo_library", relative_dir=relative_dir, **kwargs 384*55e87721SMatt Gilbride ) 385*55e87721SMatt Gilbride 386*55e87721SMatt Gilbride def php_library(self, **kwargs) -> Path: 387*55e87721SMatt Gilbride return self._generic_library("php_library", **kwargs) 388*55e87721SMatt Gilbride 389*55e87721SMatt Gilbride def ruby_library(self, **kwargs) -> Path: 390*55e87721SMatt Gilbride # kwargs["metadata"] is required to load values from .repo-metadata.json 391*55e87721SMatt Gilbride if "metadata" not in kwargs: 392*55e87721SMatt Gilbride kwargs["metadata"] = {} 393*55e87721SMatt Gilbride return self._generic_library("ruby_library", **kwargs) 394*55e87721SMatt Gilbride 395*55e87721SMatt Gilbride def render(self, template_name: str, **kwargs) -> Path: 396*55e87721SMatt Gilbride template = self._templates.render(template_name, **kwargs) 397*55e87721SMatt Gilbride _tracked_paths.add(template) 398*55e87721SMatt Gilbride return template 399*55e87721SMatt Gilbride 400*55e87721SMatt Gilbride def _load_generic_metadata(self, metadata: Dict, relative_dir=None): 401*55e87721SMatt Gilbride """ 402*55e87721SMatt Gilbride loads additional meta information from .repo-metadata.json. 403*55e87721SMatt Gilbride """ 404*55e87721SMatt Gilbride metadata["partials"] = partials.load_partials() 405*55e87721SMatt Gilbride 406*55e87721SMatt Gilbride # Loads repo metadata information from the default location if it 407*55e87721SMatt Gilbride # hasn't already been set. Some callers may have already loaded repo 408*55e87721SMatt Gilbride # metadata, so we don't need to do it again or overwrite it. Also, only 409*55e87721SMatt Gilbride # set the "repo" key. 410*55e87721SMatt Gilbride if "repo" not in metadata: 411*55e87721SMatt Gilbride metadata["repo"] = _load_repo_metadata(relative_dir=relative_dir) 412*55e87721SMatt Gilbride 413*55e87721SMatt Gilbride 414*55e87721SMatt Gilbridedef detect_versions( 415*55e87721SMatt Gilbride path: str = "./src", 416*55e87721SMatt Gilbride default_version: Optional[str] = None, 417*55e87721SMatt Gilbride default_first: Optional[bool] = None, 418*55e87721SMatt Gilbride) -> List[str]: 419*55e87721SMatt Gilbride """ 420*55e87721SMatt Gilbride Detects the versions a library has, based on distinct folders 421*55e87721SMatt Gilbride within path. This is based on the fact that our GAPIC libraries are 422*55e87721SMatt Gilbride structured as follows: 423*55e87721SMatt Gilbride 424*55e87721SMatt Gilbride src/v1 425*55e87721SMatt Gilbride src/v1beta 426*55e87721SMatt Gilbride src/v1alpha 427*55e87721SMatt Gilbride 428*55e87721SMatt Gilbride With folder names mapping directly to versions. 429*55e87721SMatt Gilbride 430*55e87721SMatt Gilbride Returns: a list of the sorted subdirectories; for the example above: 431*55e87721SMatt Gilbride ['v1', 'v1alpha', 'v1beta'] 432*55e87721SMatt Gilbride If the `default_version` argument is not provided, the `default_version` 433*55e87721SMatt Gilbride will be read from `.repo-metadata.json`, if it exists. 434*55e87721SMatt Gilbride If `default_version` is available, the `default_version` is moved to 435*55e87721SMatt Gilbride at the front or the end of the sorted list depending on the value of `default_first`. 436*55e87721SMatt Gilbride The `default_version` will be first in the list when `default_first` is `True`. 437*55e87721SMatt Gilbride """ 438*55e87721SMatt Gilbride 439*55e87721SMatt Gilbride versions = [] 440*55e87721SMatt Gilbride 441*55e87721SMatt Gilbride if not default_version: 442*55e87721SMatt Gilbride try: 443*55e87721SMatt Gilbride # Get the `default_version` from ``.repo-metadata.json`. 444*55e87721SMatt Gilbride default_version = json.load(open(".repo-metadata.json", "rt")).get( 445*55e87721SMatt Gilbride "default_version" 446*55e87721SMatt Gilbride ) 447*55e87721SMatt Gilbride except FileNotFoundError: 448*55e87721SMatt Gilbride pass 449*55e87721SMatt Gilbride 450*55e87721SMatt Gilbride # Detect versions up to a depth of 4 in directory hierarchy 451*55e87721SMatt Gilbride for level in ("*v[1-9]*", "*/*v[1-9]*", "*/*/*v[1-9]*", "*/*/*/*v[1-9]*"): 452*55e87721SMatt Gilbride # Sort the sub directories alphabetically. 453*55e87721SMatt Gilbride sub_dirs = sorted([p.name for p in Path(path).glob(level) if p.is_dir()]) 454*55e87721SMatt Gilbride # Don't proceed to the next level if we've detected versions in this depth level 455*55e87721SMatt Gilbride if sub_dirs: 456*55e87721SMatt Gilbride break 457*55e87721SMatt Gilbride 458*55e87721SMatt Gilbride if sub_dirs: 459*55e87721SMatt Gilbride # if `default_version` is not specified, return the sorted directories. 460*55e87721SMatt Gilbride if not default_version: 461*55e87721SMatt Gilbride versions = sub_dirs 462*55e87721SMatt Gilbride else: 463*55e87721SMatt Gilbride # The subdirectory with the same suffix as the default_version 464*55e87721SMatt Gilbride # will be the default client. 465*55e87721SMatt Gilbride default_client = next( 466*55e87721SMatt Gilbride iter([d for d in sub_dirs if d.endswith(default_version)]), None 467*55e87721SMatt Gilbride ) 468*55e87721SMatt Gilbride 469*55e87721SMatt Gilbride # start with all the versions except for the default client 470*55e87721SMatt Gilbride versions = [d for d in sub_dirs if not d.endswith(default_version)] 471*55e87721SMatt Gilbride 472*55e87721SMatt Gilbride if default_client: 473*55e87721SMatt Gilbride # If `default_first` is true, the default_client will be first 474*55e87721SMatt Gilbride # in the list. 475*55e87721SMatt Gilbride if default_first: 476*55e87721SMatt Gilbride versions = [default_client] + versions 477*55e87721SMatt Gilbride else: 478*55e87721SMatt Gilbride versions += [default_client] 479*55e87721SMatt Gilbride return versions 480*55e87721SMatt Gilbride 481*55e87721SMatt Gilbride 482*55e87721SMatt Gilbridedef decamelize(value: str): 483*55e87721SMatt Gilbride """Parser to convert fooBar.js to Foo Bar.""" 484*55e87721SMatt Gilbride if not value: 485*55e87721SMatt Gilbride return "" 486*55e87721SMatt Gilbride str_decamelize = re.sub("^.", value[0].upper(), value) # apple -> Apple. 487*55e87721SMatt Gilbride str_decamelize = re.sub( 488*55e87721SMatt Gilbride "([A-Z]+)([A-Z])([a-z0-9])", r"\1 \2\3", str_decamelize 489*55e87721SMatt Gilbride ) # ACLBatman -> ACL Batman. 490*55e87721SMatt Gilbride return re.sub("([a-z0-9])([A-Z])", r"\1 \2", str_decamelize) # FooBar -> Foo Bar. 491*55e87721SMatt Gilbride 492*55e87721SMatt Gilbride 493*55e87721SMatt Gilbridedef _load_repo_metadata( 494*55e87721SMatt Gilbride relative_dir=None, metadata_file: str = "./.repo-metadata.json" 495*55e87721SMatt Gilbride) -> Dict: 496*55e87721SMatt Gilbride """Parse a metadata JSON file into a Dict. 497*55e87721SMatt Gilbride 498*55e87721SMatt Gilbride Currently, the defined fields are: 499*55e87721SMatt Gilbride * `name` - The service's API name 500*55e87721SMatt Gilbride * `name_pretty` - The service's API title. This will be used for generating titles on READMEs 501*55e87721SMatt Gilbride * `product_documentation` - The product documentation on cloud.google.com 502*55e87721SMatt Gilbride * `client_documentation` - The client library reference documentation 503*55e87721SMatt Gilbride * `issue_tracker` - The public issue tracker for the product 504*55e87721SMatt Gilbride * `release_level` - The release level of the client library. One of: alpha, beta, 505*55e87721SMatt Gilbride ga, deprecated, preview, stable 506*55e87721SMatt Gilbride * `language` - The repo language. One of dotnet, go, java, nodejs, php, python, ruby 507*55e87721SMatt Gilbride * `repo` - The GitHub repo in the format {owner}/{repo} 508*55e87721SMatt Gilbride * `distribution_name` - The language-idiomatic package/distribution name 509*55e87721SMatt Gilbride * `api_id` - The API ID associated with the service. Fully qualified identifier use to 510*55e87721SMatt Gilbride enable a service in the cloud platform (e.g. monitoring.googleapis.com) 511*55e87721SMatt Gilbride * `requires_billing` - Whether or not the API requires billing to be configured on the 512*55e87721SMatt Gilbride customer's acocunt 513*55e87721SMatt Gilbride 514*55e87721SMatt Gilbride Args: 515*55e87721SMatt Gilbride metadata_file (str, optional): Path to the metadata json file 516*55e87721SMatt Gilbride 517*55e87721SMatt Gilbride Returns: 518*55e87721SMatt Gilbride A dictionary of metadata. This may not necessarily include all the defined fields above. 519*55e87721SMatt Gilbride """ 520*55e87721SMatt Gilbride if relative_dir is not None: 521*55e87721SMatt Gilbride if os.path.exists(Path(relative_dir, metadata_file).resolve()): 522*55e87721SMatt Gilbride with open(Path(relative_dir, metadata_file).resolve()) as f: 523*55e87721SMatt Gilbride return json.load(f) 524*55e87721SMatt Gilbride elif os.path.exists(metadata_file): 525*55e87721SMatt Gilbride with open(metadata_file) as f: 526*55e87721SMatt Gilbride return json.load(f) 527*55e87721SMatt Gilbride return {} 528*55e87721SMatt Gilbride 529*55e87721SMatt Gilbride 530*55e87721SMatt Gilbridedef _get_default_branch_name(repository_name: str) -> str: 531*55e87721SMatt Gilbride """Read the default branch name from the environment. 532*55e87721SMatt Gilbride 533*55e87721SMatt Gilbride First checks environment variable DEFAULT_BRANCH_PATH. If found, it 534*55e87721SMatt Gilbride reads the contents of the file at DEFAULT_BRANCH_PATH and returns it. 535*55e87721SMatt Gilbride 536*55e87721SMatt Gilbride Then checks environment varabile DEFAULT_BRANCH, and returns it if found. 537*55e87721SMatt Gilbride """ 538*55e87721SMatt Gilbride default_branch_path = os.getenv("DEFAULT_BRANCH_PATH") 539*55e87721SMatt Gilbride if default_branch_path: 540*55e87721SMatt Gilbride return Path(default_branch_path).read_text().strip() 541*55e87721SMatt Gilbride 542*55e87721SMatt Gilbride # This default should be switched to "main" once we've migrated 543*55e87721SMatt Gilbride # the majority of our repositories: 544*55e87721SMatt Gilbride return os.getenv("DEFAULT_BRANCH", "master") 545