1#!/usr/bin/env python3 2# 3# Copyright (C) 2022 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16"""Module that searches the tree for files named api_packages.json and returns 17the Fully Qualified Bazel label of API domain contributions to API surfaces. 18""" 19 20import json 21import os 22 23from finder import FileFinder 24 25# GLOBALS 26# Filename to search for 27API_PACKAGES_FILENAME = "api_packages.json" 28# Default name of the api contribution Bazel target. Can be overridden by module 29# authors in api_packages.json. 30DEFAULT_API_TARGET = "contributions" 31# Directories inside inner_tree that will be searched for api_packages.json 32# This pruning improves the speed of the API export process 33INNER_TREE_SEARCH_DIRS = [ 34 ("build", "orchestrator" 35 ), # TODO: Remove once build/orchestrator stops contributing to system. 36 ("frameworks", "base"), 37 ("packages", "modules") 38] 39 40 41# TODO: Fix line lengths and re-enable the pylint check.. 42# pylint: disable=line-too-long 43class BazelLabel: 44 """Class to represent a Fully qualified API contribution Bazel target 45 https://docs.bazel.build/versions/main/skylark/lib/Label.html""" 46 47 def __init__(self, package: str, target: str): 48 self.package = package.rstrip(":") 49 self.target = target.lstrip(":") 50 51 def to_string(self): 52 return self.package + ":" + self.target 53 54 55class ApiPackageDecodeException(Exception): 56 57 def __init__(self, filepath: str, msg: str): 58 self.filepath = filepath 59 msg = f"Found malformed api_packages.json file at {filepath}: " + msg 60 super().__init__(msg) 61 62 63class ContributionData: 64 """Class to represent metadata of API contributions in api_packages.json.""" 65 66 def __init__(self, api_domain, api_bazel_label, is_apex=False): 67 self.api_domain = api_domain 68 self.api_contribution_bazel_label = api_bazel_label 69 self.is_apex = is_apex 70 71 def __repr__(self): 72 props = [f"api_domain={self.api_domain}"] 73 props.append(f"api_contribution_bazel_label={self.api_contribution_bazel_label}") 74 props.append(f"is_apex={self.is_apex}") 75 props_joined = ", ".join(props) 76 return f"ContributionData({props_joined})" 77 78def read(filepath: str) -> ContributionData: 79 """Deserialize the contents of the json file at <filepath> 80 Arguments: 81 filepath 82 Returns: 83 ContributionData object 84 """ 85 86 def _deserialize(filepath, json_contents) -> ContributionData: 87 domain = json_contents.get("api_domain") 88 package = json_contents.get("api_package") 89 target = json_contents.get("api_target", "") or DEFAULT_API_TARGET 90 is_apex = json_contents.get("is_apex", False) 91 if not domain: 92 raise ApiPackageDecodeException( 93 filepath, 94 "api_domain is a required field in api_packages.json") 95 if not package: 96 raise ApiPackageDecodeException( 97 filepath, 98 "api_package is a required field in api_packages.json") 99 return ContributionData(domain, 100 BazelLabel(package=package, target=target), 101 is_apex=is_apex) 102 103 with open(filepath, encoding='iso-8859-1') as f: 104 try: 105 return json.load(f, 106 object_hook=lambda json_contents: _deserialize( 107 filepath, json_contents)) 108 except json.decoder.JSONDecodeError as ex: 109 raise ApiPackageDecodeException(filepath, "") from ex 110 111 112class ApiPackageFinder: 113 """A class that searches the tree for files named api_packages.json and returns the fully qualified Bazel label of the API contributions of API domains 114 115 Example api_packages.json 116 ``` 117 [ 118 { 119 "api_domain": "system", 120 "api_package": "//build/orchestrator/apis", 121 "api_target": "system", 122 "is_apex": false 123 } 124 ] 125 ``` 126 127 The search is restricted to $INNER_TREE_SEARCH_DIRS 128 """ 129 130 def __init__(self, inner_tree_root: str, search_depth=6): 131 self.inner_tree_root = inner_tree_root 132 self.search_depth = search_depth 133 self.finder = FileFinder( 134 filename=API_PACKAGES_FILENAME, 135 ignore_paths=[], 136 ) 137 self._cache = None 138 139 def _create_cache(self) -> None: 140 self._cache = [] 141 search_paths = [ 142 os.path.join(self.inner_tree_root, *search_dir) 143 for search_dir in INNER_TREE_SEARCH_DIRS 144 ] 145 for search_path in search_paths: 146 for packages_file in self.finder.find( 147 path=search_path, search_depth=self.search_depth): 148 api_contributions = read(packages_file) 149 self._cache.extend(api_contributions) 150 151 def _find_api_label(self, api_domain_filter) -> list[BazelLabel]: 152 # Compare to None and not []. The latter value is possible if a tree has 153 # no API contributoins. 154 if self._cache is None: 155 self._create_cache() 156 return [ 157 c.api_contribution_bazel_label for c in self._cache 158 if api_domain_filter(c) 159 ] 160 161 def find_api_label_string(self, api_domain: str) -> str: 162 """ Return the fully qualified bazel label of the contribution target 163 164 Parameters: 165 api_domain: Name of the API domain, e.g. system 166 167 Raises: 168 ApiPackageDecodeException: If a malformed api_packages.json is found during search 169 170 Returns: 171 Bazel label, e.g. //frameworks/base:contribution 172 None if a contribution could not be found 173 """ 174 labels = self._find_api_label(lambda x: x.api_domain == api_domain) 175 assert len( 176 labels 177 ) < 2, f"Duplicate contributions found for API domain: {api_domain}" 178 return labels[0].to_string() if labels else None 179 180 def find_api_package(self, api_domain: str) -> str: 181 """ Return the Bazel package of the contribution target 182 183 Parameters: 184 api_domain: Name of the API domain, e.g. system 185 186 Raises: 187 ApiPackageDecodeException: If a malformed api_packages.json is found during search 188 189 Returns: 190 Bazel label, e.g. //frameworks/base 191 None if a contribution could not be found 192 """ 193 labels = self._find_api_label(lambda x: x.api_domain == api_domain) 194 assert len( 195 labels 196 ) < 2, f"Duplicate contributions found for API domain: {api_domain}" 197 return labels[0].package if label else None 198 199 def find_api_label_string_using_filter(self, 200 api_domain_filter: callable) -> str: 201 """ Return the Bazel label of the contributing targets 202 that match a search filter. 203 204 Parameters: 205 api_domain_filter: A callback function. The first arg to the function 206 is ContributionData 207 208 Raises: 209 ApiPackageDecodeException: If a malformed api_packages.json is found during search 210 211 Returns: 212 List of Bazel labels, e.g. [//frameworks/base:contribution, //packages/myapex:contribution] 213 None if a contribution could not be found 214 """ 215 labels = self._find_api_label(api_domain_filter) 216 return [label.to_string() for label in labels] 217