• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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