blob: 27a0a5f9af0d3355e00cb8422c16579a61ac2f2a [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright (C) 2022 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Module that searches the tree for files named api_packages.json and returns
the Fully Qualified Bazel label of API domain contributions to API surfaces.
"""
import json
import os
from finder import FileFinder
# GLOBALS
# Filename to search for
API_PACKAGES_FILENAME = "api_packages.json"
# Default name of the api contribution Bazel target. Can be overridden by module
# authors in api_packages.json.
DEFAULT_API_TARGET = "contributions"
# Directories inside inner_tree that will be searched for api_packages.json
# This pruning improves the speed of the API export process
INNER_TREE_SEARCH_DIRS = [
("build", "orchestrator"
), # TODO: Remove once build/orchestrator stops contributing to system.
("frameworks", "base"),
("packages", "modules")
]
# TODO: Fix line lengths and re-enable the pylint check..
# pylint: disable=line-too-long
class BazelLabel:
"""Class to represent a Fully qualified API contribution Bazel target
https://docs.bazel.build/versions/main/skylark/lib/Label.html"""
def __init__(self, package: str, target: str):
self.package = package.rstrip(":")
self.target = target.lstrip(":")
def to_string(self):
return self.package + ":" + self.target
class ApiPackageDecodeException(Exception):
def __init__(self, filepath: str, msg: str):
self.filepath = filepath
msg = f"Found malformed api_packages.json file at {filepath}: " + msg
super().__init__(msg)
class ContributionData:
"""Class to represent metadata of API contributions in api_packages.json."""
def __init__(self, api_domain, api_bazel_label, is_apex=False):
self.api_domain = api_domain
self.api_contribution_bazel_label = api_bazel_label
self.is_apex = is_apex
def __repr__(self):
props = [f"api_domain={self.api_domain}"]
props.append(f"api_contribution_bazel_label={self.api_contribution_bazel_label}")
props.append(f"is_apex={self.is_apex}")
props_joined = ", ".join(props)
return f"ContributionData({props_joined})"
def read(filepath: str) -> ContributionData:
"""Deserialize the contents of the json file at <filepath>
Arguments:
filepath
Returns:
ContributionData object
"""
def _deserialize(filepath, json_contents) -> ContributionData:
domain = json_contents.get("api_domain")
package = json_contents.get("api_package")
target = json_contents.get("api_target", "") or DEFAULT_API_TARGET
is_apex = json_contents.get("is_apex", False)
if not domain:
raise ApiPackageDecodeException(
filepath,
"api_domain is a required field in api_packages.json")
if not package:
raise ApiPackageDecodeException(
filepath,
"api_package is a required field in api_packages.json")
return ContributionData(domain,
BazelLabel(package=package, target=target),
is_apex=is_apex)
with open(filepath, encoding='iso-8859-1') as f:
try:
return json.load(f,
object_hook=lambda json_contents: _deserialize(
filepath, json_contents))
except json.decoder.JSONDecodeError as ex:
raise ApiPackageDecodeException(filepath, "") from ex
class ApiPackageFinder:
"""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
Example api_packages.json
```
{
"api_domain": "system",
"api_package": "//build/orchestrator/apis",
"api_target": "system",
"is_apex": false
}
```
The search is restricted to $INNER_TREE_SEARCH_DIRS
"""
def __init__(self, inner_tree_root: str, search_depth=6):
self.inner_tree_root = inner_tree_root
self.search_depth = search_depth
self.finder = FileFinder(
filename=API_PACKAGES_FILENAME,
ignore_paths=[],
)
self._cache = None
def _create_cache(self) -> None:
self._cache = []
search_paths = [
os.path.join(self.inner_tree_root, *search_dir)
for search_dir in INNER_TREE_SEARCH_DIRS
]
for search_path in search_paths:
for packages_file in self.finder.find(
path=search_path, search_depth=self.search_depth):
api_contributions = read(packages_file)
self._cache.append(api_contributions)
def _find_api_label(self, api_domain_filter) -> list[BazelLabel]:
# Compare to None and not []. The latter value is possible if a tree has
# no API contributoins.
if self._cache is None:
self._create_cache()
return [
c.api_contribution_bazel_label for c in self._cache
if api_domain_filter(c)
]
def find_api_label_string(self, api_domain: str) -> str:
""" Return the fully qualified bazel label of the contribution target
Parameters:
api_domain: Name of the API domain, e.g. system
Raises:
ApiPackageDecodeException: If a malformed api_packages.json is found during search
Returns:
Bazel label, e.g. //frameworks/base:contribution
None if a contribution could not be found
"""
labels = self._find_api_label(lambda x: x.api_domain == api_domain)
assert len(
labels
) < 2, f"Duplicate contributions found for API domain: {api_domain}"
return labels[0].to_string() if labels else None
def find_api_package(self, api_domain: str) -> str:
""" Return the Bazel package of the contribution target
Parameters:
api_domain: Name of the API domain, e.g. system
Raises:
ApiPackageDecodeException: If a malformed api_packages.json is found during search
Returns:
Bazel label, e.g. //frameworks/base
None if a contribution could not be found
"""
labels = self._find_api_label(lambda x: x.api_domain == api_domain)
assert len(
labels
) < 2, f"Duplicate contributions found for API domain: {api_domain}"
return labels[0].package if label else None
def find_api_label_string_using_filter(self,
api_domain_filter: callable) -> str:
""" Return the Bazel label of the contributing targets
that match a search filter.
Parameters:
api_domain_filter: A callback function. The first arg to the function
is ContributionData
Raises:
ApiPackageDecodeException: If a malformed api_packages.json is found during search
Returns:
List of Bazel labels, e.g. [//frameworks/base:contribution, //packages/myapex:contribution]
None if a contribution could not be found
"""
labels = self._find_api_label(api_domain_filter)
return [label.to_string() for label in labels]