Implement Bazel rule for cc_api_contribution and cc_api_headers
Create Bazel rules for cc_api_headers and cc_api_contribution. These
rules do not have any build actions, but provide "provider" objects
Some deatils
- cc_api_headers provides `CcApiHeaderInfo`. This includes metadata such
as include_dir_path, system (i.e. -I or -isystem), arch. Since we need
this metadata, something like a filegroup is not the best solution to
declare headers
- cc_api_contribution provides `CcApiContributionInfo`. This includes
path to the .map.txt and the headers metadata
Test: b test //build/bazel/rules/apis:cc_api_test_suite
Bug: 220938703
Change-Id: I609480eecc234ab583dcfb5294694f7527a6ccd3
diff --git a/rules/apis/BUILD b/rules/apis/BUILD
new file mode 100644
index 0000000..ac63295
--- /dev/null
+++ b/rules/apis/BUILD
@@ -0,0 +1,18 @@
+"""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.
+"""
+
+load(":cc_api_contribution_test.bzl", "cc_api_test_suite")
+
+cc_api_test_suite(name = "cc_api_test_suite")
diff --git a/rules/apis/README.md b/rules/apis/README.md
new file mode 100644
index 0000000..8c95adf
--- /dev/null
+++ b/rules/apis/README.md
@@ -0,0 +1,9 @@
+# Bazel rules for API export
+This package contains Bazel rules for declaring API contributions of API
+domains to API surfaces (go/android-build-api-domains)
+
+## WARNING:
+API export is expected to run in **Standalone Bazel mode**
+(go/multi-tree-api-export). As such, rules defined in this package should not
+have any dependencies on bp2build (most notably the generated `@soong_injection`
+workspace)
diff --git a/rules/apis/cc_api_contribution.bzl b/rules/apis/cc_api_contribution.bzl
new file mode 100644
index 0000000..93964f1
--- /dev/null
+++ b/rules/apis/cc_api_contribution.bzl
@@ -0,0 +1,125 @@
+"""
+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.
+"""
+
+"""Bazel rules for exporting API contributions of CC libraries"""
+
+load("@bazel_skylib//lib:paths.bzl", "paths")
+load("//build/bazel/rules/cc:cc_constants.bzl", "constants")
+
+"""A Bazel provider that encapsulates the headers presented to an API surface"""
+CcApiHeaderInfo = provider(
+ fields = {
+ "name": "Name identifying the header files",
+ "root": "Directory containing the header files, relative to workspace root. This will become the -I parameter in consuming API domains. This defaults to the current Bazel package",
+ "headers": "The header (.h) files presented by the library to an API surface",
+ "system": "bool, This will determine whether the include path will be -I or -isystem",
+ "arch": "Target arch of devices that use these header files to compile. The default is empty, which means that it is arch-agnostic",
+ },
+)
+
+def _cc_api_header_impl(ctx):
+ """Implementation for the cc_api_headers rule.
+ This rule does not have any build actions, but returns a `CcApiHeaderInfo` provider object"""
+ headers_filepath = [header.path for header in ctx.files.hdrs]
+ root = paths.dirname(ctx.build_file_path)
+ if ctx.attr.include_dir:
+ root = paths.join(root, ctx.attr.include_dir)
+ return [
+ CcApiHeaderInfo(
+ name = ctx.label.name,
+ root = root,
+ headers = headers_filepath,
+ system = ctx.attr.system,
+ arch = ctx.attr.arch,
+ ),
+ ]
+
+"""A bazel rule that encapsulates the header contributions of a CC library to an API surface
+This rule does not contain the API symbolfile (.map.txt). The API symbolfile is part of the cc_api_contribution rule
+This layering is necessary since the symbols present in a single .map.txt file can be defined in different include directories
+e.g.
+├── Android.bp
+├── BUILD
+├── include <-- cc_api_headers
+├── include_other <-- cc_api_headers
+├── libfoo.map.txt
+"""
+cc_api_headers = rule(
+ implementation = _cc_api_header_impl,
+ attrs = {
+ "include_dir": attr.string(
+ mandatory = False,
+ doc = "Directory containing the header files, relative to the Bazel package. This relative path will be joined with the Bazel package path to become the -I parameter in the consuming API domain",
+ ),
+ "hdrs": attr.label_list(
+ mandatory = True,
+ allow_files = constants.hdr_dot_exts,
+ doc = "List of .h files presented to the API surface. Glob patterns are allowed",
+ ),
+ "system": attr.bool(
+ default = False,
+ doc = "Boolean to indicate whether these are system headers",
+ ),
+ "arch": attr.string(
+ mandatory = False,
+ values = ["arm", "arm64", "x86", "x86_64"],
+ doc = "Arch of the target device. The default is empty, which means that the headers are arch-agnostic",
+ ),
+ },
+)
+
+"""A Bazel provider that encapsulates the contributions of a CC library to an API surface"""
+CcApiContributionInfo = provider(
+ fields = {
+ "name": "Name of the cc library",
+ "api": "Path of map.txt describing the stable APIs of the library. Path is relative to workspace root",
+ "headers": "metadata of the header files of the cc library",
+ },
+)
+
+def _cc_api_contribution_impl(ctx):
+ """Implemenation for the cc_api_contribution rule
+ This rule does not have any build actions, but returns a `CcApiContributionInfo` provider object"""
+ api_filepath = ctx.file.api.path
+ headers = [header[CcApiHeaderInfo] for header in ctx.attr.hdrs]
+ name = ctx.attr.library_name or ctx.label.name
+ return [
+ CcApiContributionInfo(
+ name = name,
+ api = api_filepath,
+ headers = headers,
+ ),
+ ]
+
+cc_api_contribution = rule(
+ implementation = _cc_api_contribution_impl,
+ attrs = {
+ "library_name": attr.string(
+ mandatory = False,
+ doc = "Name of the library. This can be different from `name` to prevent name collision with the implementation of the library in the same Bazel package. Defaults to label.name",
+ ),
+ "api": attr.label(
+ mandatory = True,
+ allow_single_file = [".map.txt"],
+ doc = ".map.txt file of the library",
+ ),
+ "hdrs": attr.label_list(
+ mandatory = False,
+ providers = [CcApiHeaderInfo],
+ doc = "Header contributions of the cc library. This should return a `CcApiHeaderInfo` provider",
+ ),
+ },
+)
diff --git a/rules/apis/cc_api_contribution_test.bzl b/rules/apis/cc_api_contribution_test.bzl
new file mode 100644
index 0000000..378d1ed
--- /dev/null
+++ b/rules/apis/cc_api_contribution_test.bzl
@@ -0,0 +1,165 @@
+"""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.
+"""
+
+load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
+load("@bazel_skylib//lib:paths.bzl", "paths")
+load(":cc_api_contribution.bzl", "CcApiContributionInfo", "CcApiHeaderInfo", "cc_api_contribution", "cc_api_headers")
+
+def _empty_include_dir_test_impl(ctx):
+ env = analysistest.begin(ctx)
+ target_under_test = analysistest.target_under_test(env)
+ asserts.equals(env, paths.dirname(ctx.build_file_path), target_under_test[CcApiHeaderInfo].root)
+ return analysistest.end(env)
+
+empty_include_dir_test = analysistest.make(_empty_include_dir_test_impl)
+
+def _empty_include_dir_test():
+ test_name = "empty_include_dir_test"
+ subject_name = test_name + "_subject"
+ cc_api_headers(
+ name = subject_name,
+ hdrs = ["hdr.h"],
+ tags = ["manual"],
+ )
+ empty_include_dir_test(
+ name = test_name,
+ target_under_test = subject_name,
+ )
+ return test_name
+
+def _nonempty_include_dir_test_impl(ctx):
+ env = analysistest.begin(ctx)
+ target_under_test = analysistest.target_under_test(env)
+ expected_root = paths.join(paths.dirname(ctx.build_file_path), ctx.attr.expected_include_dir)
+ asserts.equals(env, expected_root, target_under_test[CcApiHeaderInfo].root)
+ return analysistest.end(env)
+
+nonempty_include_dir_test = analysistest.make(
+ impl = _nonempty_include_dir_test_impl,
+ attrs = {
+ "expected_include_dir": attr.string(),
+ },
+)
+
+def _nonempty_include_dir_test():
+ test_name = "nonempty_include_dir_test"
+ subject_name = test_name + "_subject"
+ include_dir = "my/include"
+ cc_api_headers(
+ name = subject_name,
+ include_dir = include_dir,
+ hdrs = ["my/include/hdr.h"],
+ tags = ["manual"],
+ )
+ nonempty_include_dir_test(
+ name = test_name,
+ target_under_test = subject_name,
+ expected_include_dir = include_dir,
+ )
+ return test_name
+
+def _api_path_is_relative_to_workspace_root_test_impl(ctx):
+ env = analysistest.begin(ctx)
+ target_under_test = analysistest.target_under_test(env)
+ expected_path = paths.join(paths.dirname(ctx.build_file_path), ctx.attr.expected_symbolfile)
+ asserts.equals(env, expected_path, target_under_test[CcApiContributionInfo].api)
+ return analysistest.end(env)
+
+api_path_is_relative_to_workspace_root_test = analysistest.make(
+ impl = _api_path_is_relative_to_workspace_root_test_impl,
+ attrs = {
+ "expected_symbolfile": attr.string(),
+ },
+)
+
+def _api_path_is_relative_to_workspace_root_test():
+ test_name = "api_path_is_relative_workspace_root"
+ subject_name = test_name + "_subject"
+ symbolfile = "libfoo.map.txt"
+ cc_api_contribution(
+ name = subject_name,
+ api = symbolfile,
+ tags = ["manual"],
+ )
+ api_path_is_relative_to_workspace_root_test(
+ name = test_name,
+ target_under_test = subject_name,
+ expected_symbolfile = symbolfile,
+ )
+ return test_name
+
+def _empty_library_name_gets_label_name_impl(ctx):
+ env = analysistest.begin(ctx)
+ target_under_test = analysistest.target_under_test(env)
+ asserts.equals(env, target_under_test.label.name, target_under_test[CcApiContributionInfo].name)
+ return analysistest.end(env)
+
+empty_library_name_gets_label_name_test = analysistest.make(_empty_library_name_gets_label_name_impl)
+
+def _empty_library_name_gets_label_name_test():
+ test_name = "empty_library_name_gets_label_name"
+ subject_name = test_name + "_subject"
+ cc_api_contribution(
+ name = subject_name,
+ api = ":libfoo.map.txt",
+ tags = ["manual"],
+ )
+ empty_library_name_gets_label_name_test(
+ name = test_name,
+ target_under_test = subject_name,
+ )
+ return test_name
+
+def _nonempty_library_name_preferred_impl(ctx):
+ env = analysistest.begin(ctx)
+ target_under_test = analysistest.target_under_test(env)
+ asserts.equals(env, ctx.attr.expected_library_name, target_under_test[CcApiContributionInfo].name)
+ return analysistest.end(env)
+
+nonempty_library_name_preferred_test = analysistest.make(
+ impl = _nonempty_library_name_preferred_impl,
+ attrs = {
+ "expected_library_name": attr.string(),
+ },
+)
+
+def _nonempty_library_name_preferred_test():
+ test_name = "nonempty_library_name_preferred_test"
+ subject_name = test_name + "_subject"
+ library_name = "mylibrary"
+ cc_api_contribution(
+ name = subject_name,
+ library_name = library_name,
+ api = ":libfoo.map.txt",
+ tags = ["manual"],
+ )
+ nonempty_library_name_preferred_test(
+ name = test_name,
+ target_under_test = subject_name,
+ expected_library_name = library_name,
+ )
+ return test_name
+
+def cc_api_test_suite(name):
+ native.test_suite(
+ name = name,
+ tests = [
+ _empty_include_dir_test(),
+ _nonempty_include_dir_test(),
+ _api_path_is_relative_to_workspace_root_test(),
+ _empty_library_name_gets_label_name_test(),
+ _nonempty_library_name_preferred_test(),
+ ],
+ )