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(),
+        ],
+    )