Support "*" wildcard in API export
The current implementation of ApiPackageFinder matches the top-level
contribution target using the name of the api_domain in
api_packages.json. Since mcombo files accept "*" as a wildcard to match
all apexes, add a new function to the ApiPackageFinder API that accepts
a generic filter.
api_packages.json now accepts an additional key `is_apex` to identify
whether an api_domain is an apex
Test: python -m test_find_api_packages
Change-Id: I0570948e7975a0e949ad5665952ff7a073d48990
diff --git a/inner_build/find_api_packages.py b/inner_build/find_api_packages.py
index 3db8e82..27a0a5f 100644
--- a/inner_build/find_api_packages.py
+++ b/inner_build/find_api_packages.py
@@ -17,8 +17,6 @@
the Fully Qualified Bazel label of API domain contributions to API surfaces.
"""
-from collections import namedtuple
-
import json
import os
@@ -33,9 +31,8 @@
# 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.
+ ("build", "orchestrator"
+ ), # TODO: Remove once build/orchestrator stops contributing to system.
("frameworks", "base"),
("packages", "modules")
]
@@ -56,15 +53,27 @@
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)
-ContributionData = namedtuple("ContributionData",
- ("api_domain", "api_contribution_bazel_label"))
+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>
@@ -78,6 +87,7 @@
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,
@@ -87,7 +97,8 @@
filepath,
"api_package is a required field in api_packages.json")
return ContributionData(domain,
- BazelLabel(package=package, target=target))
+ BazelLabel(package=package, target=target),
+ is_apex=is_apex)
with open(filepath, encoding='iso-8859-1') as f:
try:
@@ -106,7 +117,8 @@
{
"api_domain": "system",
"api_package": "//build/orchestrator/apis",
- "api_target": "system"
+ "api_target": "system",
+ "is_apex": false
}
```
@@ -120,12 +132,10 @@
filename=API_PACKAGES_FILENAME,
ignore_paths=[],
)
- self._cache = {}
+ self._cache = None
- def _find_api_label(self, api_domain: str) -> BazelLabel:
- if api_domain in self._cache:
- return self._cache.get(api_domain)
-
+ 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
@@ -133,17 +143,18 @@
for search_path in search_paths:
for packages_file in self.finder.find(
path=search_path, search_depth=self.search_depth):
- # Read values and add them to cache
- results = read(packages_file)
- self._cache[
- results.api_domain] = results.api_contribution_bazel_label
- # If an entry is found, stop searching
- if api_domain in self._cache:
- return self._cache.get(api_domain)
+ api_contributions = read(packages_file)
+ self._cache.append(api_contributions)
- # Do not raise exception if a contribution target could not be found for an api domain
- # e.g. vendor might not have any api contributions
- return None
+ 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
@@ -158,10 +169,13 @@
Bazel label, e.g. //frameworks/base:contribution
None if a contribution could not be found
"""
- label = self._find_api_label(api_domain)
- return label.to_string() if label else None
+ 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_name: str) -> str:
+ def find_api_package(self, api_domain: str) -> str:
""" Return the Bazel package of the contribution target
Parameters:
@@ -174,5 +188,27 @@
Bazel label, e.g. //frameworks/base
None if a contribution could not be found
"""
- label = self._find_api_label(api_domain_name)
- return label.package if label else None
+ 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]
diff --git a/inner_build/inner_build_soong.py b/inner_build/inner_build_soong.py
index 427f674..6d97ca5 100755
--- a/inner_build/inner_build_soong.py
+++ b/inner_build/inner_build_soong.py
@@ -108,6 +108,14 @@
otherpath) < os.path.getmtime(self.fullpath())
+# ApiPackageFinder filters for special chars in .mcombo files
+_MCOMBO_WILDCARD_FILTERS = {
+ "*": lambda x: x.is_apex,
+ # TODO: Support more wildcards if necessary (e.g. vendor apex, google
+ # variants etc.)
+}
+
+
class ApiExporterBazel(object):
"""Generate API surface metadata files into a well-known directory
@@ -142,9 +150,12 @@
finder = ApiPackageFinder(inner_tree_root=self.inner_tree)
contribution_targets = []
for api_domain in self.api_domains:
- label = finder.find_api_label_string(api_domain)
- if label is not None:
- contribution_targets.append(label)
+ default_name_filter = lambda x: x.api_domain == api_domain
+ api_domain_filter = _MCOMBO_WILDCARD_FILTERS.get(
+ api_domain, default_name_filter)
+ labels = finder.find_api_label_string_using_filter(
+ api_domain_filter)
+ contribution_targets.extend(labels)
return contribution_targets
def _build_api_domain_contribution_targets(
diff --git a/inner_build/test_find_api_packages.py b/inner_build/test_find_api_packages.py
index 2070166..bd9dace 100644
--- a/inner_build/test_find_api_packages.py
+++ b/inner_build/test_find_api_packages.py
@@ -24,6 +24,7 @@
class TestBazelLabel(unittest.TestCase):
+
def test_label_to_string(self):
label = BazelLabel(package="//build/bazel", target="mytarget")
self.assertEqual("//build/bazel:mytarget", label.to_string())
@@ -38,6 +39,7 @@
class TestApiPackageReadUtils(unittest.TestCase):
+
def test_read_empty_file(self):
test_data = ""
with patch("builtins.open", mock_open(read_data=test_data)):
@@ -88,6 +90,13 @@
class TestApiPackageFinder(unittest.TestCase):
+
+ def _mock_fs(self, mock_data):
+ # Create a mock fs for finder.find
+ # The mock fs contains files in packages/modules.
+ return lambda path, search_depth: list(mock_data.keys(
+ )) if "packages/modules" in path else []
+
@patch.object(FileFinder, "find")
def test_exception_if_api_package_file_is_missing(self, find_mock):
find_mock.return_value = [] # no files found
@@ -97,10 +106,15 @@
@patch("find_api_packages.read")
@patch.object(FileFinder, "find")
- def test_exception_if_api_domain_not_found(self, find_mock, read_mock):
+ def test_no_exception_if_api_domain_not_found(self, find_mock, read_mock):
# api_packages.json files exist in the tree, but none of them contain
# the api_domain we are interested in.
- find_mock.return_value = ["somefile.json"]
+
+ # Return a mock file from packages/modules.
+ def _mock_fs(path, search_depth):
+ return ["some_file.json"] if "packages/modules" in path else []
+
+ find_mock.side_effect = _mock_fs
read_mock.return_value = ContributionData(
"com.android.foo",
BazelLabel("//packages/modules/foo", "contributions"))
@@ -113,21 +127,49 @@
@patch("find_api_packages.read")
@patch.object(FileFinder, "find")
- def test_first_find_wins(self, find_mock, read_mock):
- find_mock.return_value = ["first.json", "second.json"]
+ def test_exception_duplicate_entries(self, find_mock, read_mock):
first_contribution_data = ContributionData(
"com.android.foo",
BazelLabel("//packages/modules/foo", "contributions"))
second_contribution_data = ContributionData(
"com.android.foo",
BazelLabel("//packages/modules/foo_other", "contributions"))
- read_mock.side_effect = [
- first_contribution_data, second_contribution_data
- ]
+ mock_data = {
+ "first.json": first_contribution_data,
+ "second.json": second_contribution_data,
+ }
+
+ find_mock.side_effect = self._mock_fs(mock_data)
+ read_mock.side_effect = lambda x: mock_data.get(x)
api_package_finder = ApiPackageFinder("mock_inner_tree")
- self.assertEqual(
- "//packages/modules/foo:contributions",
- api_package_finder.find_api_label_string("com.android.foo"))
+ with self.assertRaises(AssertionError):
+ api_package_finder.find_api_label_string("com.android.foo")
+
+ @patch("find_api_packages.read")
+ @patch.object(FileFinder, "find")
+ def test_user_provided_filter(self, find_mock, read_mock):
+ foo_contribution_data = ContributionData(
+ "com.android.foo",
+ BazelLabel("//packages/modules/foo", "contributions"))
+ bar_contribution_data = ContributionData(
+ "com.android.bar",
+ BazelLabel("//packages/modules/bar", "contributions"))
+ mock_data = {
+ "foo.json": foo_contribution_data,
+ "bar.json": bar_contribution_data,
+ }
+
+ find_mock.side_effect = self._mock_fs(mock_data)
+ read_mock.side_effect = lambda x: mock_data.get(x)
+ api_package_finder = ApiPackageFinder("mock_inner_tree")
+
+ all_contributions = api_package_finder.find_api_label_string_using_filter(
+ lambda x: True)
+ self.assertEqual(2, len(all_contributions))
+ self.assertEqual("//packages/modules/foo:contributions",
+ all_contributions[0])
+ self.assertEqual("//packages/modules/bar:contributions",
+ all_contributions[1])
if __name__ == "__main__":