Implement a (partial) cc_stub_library rule for cc_library.stubs in
Android.bp file.

This rule is responsible for emitting an object generated from a
.map.txt files for symbols associated to a shared library for a
particular API level. These libraries set the "stubs" property in their
Android.bp definitions. These libraries are used as build-time
dependencies of Mainline modules (not the platform, i.e. NDK!). For stub
libraries in the NDK, see ndk_library modules).

These dependencies are *not* included in the Mainline modules
themselves, because the non-stub versions are made available in the
device environment through other means (e.g. installed onto the system
partition or via other APEXes).

These libraries are ABI stable.

For this CL's scope, we introduce a new rule that calls ndkstubgen, a
python binary, on a library's .map.txt file, which is actually in the
format of a LD version script, to generate a .c file containing function
prototypes, and two additional metadata files. The .c file would be
eventually compiled into a stub .so, replacing the real .so for APEX
dependents.

The stub .so will be made available through a dedicated
cc_shared_library_proxy provider so dependents can easily determine if a
transitive dep has stubs for a requested API level.

Bug: 207812332
Test: CI
Change-Id: I47689c15c375b5dd9f5e70ad213d7d113a9442cc
diff --git a/common.bazelrc b/common.bazelrc
index a1d4347..fdae71a 100644
--- a/common.bazelrc
+++ b/common.bazelrc
@@ -83,6 +83,11 @@
 build --nouse_workers_with_dexbuilder
 build --fat_apk_cpu=k8
 
+# TODO(b/199038020): Use a python_toolchain when we have Starlark rules_python.
+# This also means all python scripts are using py3 runtime.
+build --python_top=//prebuilts/build-tools:python3
+build --noincompatible_use_python_toolchains
+
 # Developer instance for result storage. This only works if you have access
 # to the Bazel GCP project. Follow the GCP gcloud client's auth instructions to
 # use --google_default_credentials.
diff --git a/platforms/rule_utilities.bzl b/platforms/rule_utilities.bzl
new file mode 100644
index 0000000..97481b3
--- /dev/null
+++ b/platforms/rule_utilities.bzl
@@ -0,0 +1,47 @@
+# 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.
+
+"""Utilities for rule implementations to interact with platform definitions."""
+
+# Merge ARCH_CONSTRAINT_ATTRS with the rule attrs to use get_arch(ctx).
+ARCH_CONSTRAINT_ATTRS = {
+    "_x86_constraint": attr.label(default = Label("//build/bazel/platforms/arch:x86")),
+    "_x86_64_constraint": attr.label(default = Label("//build/bazel/platforms/arch:x86_64")),
+    "_arm_constraint": attr.label(default = Label("//build/bazel/platforms/arch:arm")),
+    "_arm64_constraint": attr.label(default = Label("//build/bazel/platforms/arch:arm64")),
+}
+
+# get_arch takes a rule context with ARCH_CONSTRAINT_ATTRS and returns the string representation
+# of the target platform by executing the target_platform_has_constraint boilerplate.
+def get_arch(ctx):
+    if not hasattr(ctx.attr, "_x86_constraint") or \
+      not hasattr(ctx.attr, "_x86_64_constraint") or \
+      not hasattr(ctx.attr, "_arm_constraint") or \
+      not hasattr(ctx.attr, "_arm64_constraint"):
+      fail("Could not get the target architecture of this rule due to missing constraint attrs.",
+           "Have you merged ARCH_CONSTRAINT_ATTRS into this rule's attributes?")
+
+    x86_constraint = ctx.attr._x86_constraint[platform_common.ConstraintValueInfo]
+    x86_64_constraint = ctx.attr._x86_64_constraint[platform_common.ConstraintValueInfo]
+    arm_constraint = ctx.attr._arm_constraint[platform_common.ConstraintValueInfo]
+    arm64_constraint = ctx.attr._arm64_constraint[platform_common.ConstraintValueInfo]
+
+    if ctx.target_platform_has_constraint(x86_constraint):
+        return "x86"
+    elif ctx.target_platform_has_constraint(x86_64_constraint):
+        return "x86_64"
+    elif ctx.target_platform_has_constraint(arm_constraint):
+        return "arm"
+    elif ctx.target_platform_has_constraint(arm64_constraint):
+        return "arm64"
diff --git a/rules/cc_library_shared.bzl b/rules/cc_library_shared.bzl
index 92cd783..2aa2a68 100644
--- a/rules/cc_library_shared.bzl
+++ b/rules/cc_library_shared.bzl
@@ -16,6 +16,7 @@
 
 load(":cc_library_common.bzl", "add_lists_defaulting_to_none", "disable_crt_link", "system_dynamic_deps_defaults")
 load(":cc_library_static.bzl", "cc_library_static")
+load(":cc_stub_library.bzl", "cc_stub_library")
 load(":generate_toc.bzl", "shared_library_toc", _CcTocInfo = "CcTocInfo")
 load(":stl.bzl", "shared_stl_deps")
 load(":stripped_cc_common.bzl", "stripped_shared_library")
@@ -67,6 +68,9 @@
         data = [],
 
         use_version_lib = False,
+
+        stubs_symbol_file = None,
+        stubs_versions = [],
         **kwargs):
     "Bazel macro to correspond with the cc_library_shared Soong module."
 
@@ -178,6 +182,29 @@
         target_compatible_with = target_compatible_with,
     )
 
+    # Emit the stub version of this library (e.g. for libraries that are
+    # provided by the NDK)
+    #
+    # TODO(b/207812332): This only calls ndkstubgen to generate the src now.
+    # Make this compile into an .so as well, and include the stub .so in a
+    # _cc_library_shared_proxy provider so dependents can easily determine if
+    # this target has stubs for a specific API version.
+    if stubs_symbol_file and len(stubs_versions) > 0:
+        # TODO(b/193663198): This unconditionally creates stubs for every version, but
+        # that's not always true depending on whether this module is available
+        # on the host, ramdisk, vendor ramdisk. We currently don't have
+        # information about the image variant yet, so we'll create stub targets
+        # for all shared libraries with the stubs property for now.
+        #
+        # See: https://cs.android.com/android/platform/superproject/+/master:build/soong/cc/library.go;l=2316-2377;drc=3d3b35c94ed2a3432b2e5e7e969a3a788a7a80b5
+        for version in stubs_versions:
+            stubs_library_name = "_".join([name, version, "stubs"])
+            cc_stub_library(
+                name = stubs_library_name,
+                symbol_file = stubs_symbol_file,
+                version = version,
+            )
+
     _cc_library_shared_proxy(
         name = name,
         shared = stripped_name,
diff --git a/rules/cc_stub_library.bzl b/rules/cc_stub_library.bzl
new file mode 100644
index 0000000..aaba972
--- /dev/null
+++ b/rules/cc_stub_library.bzl
@@ -0,0 +1,72 @@
+# 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:dicts.bzl", "dicts")
+load("//build/bazel/platforms:rule_utilities.bzl", "ARCH_CONSTRAINT_ATTRS", "get_arch")
+
+# This file contains the implementation for the cc_stub_library rule.
+#
+# TODO(b/207812332):
+# - compiling/linking generated stub.c file into a .so: https://cs.android.com/android/platform/superproject/+/master:build/soong/cc/library.go;l=1013;drc=d8a72d7dc91b2122b7b10b47b80cf2f7c65f9049
+# - ndk_api_coverage_parser: https://cs.android.com/android/platform/superproject/+/master:build/soong/cc/coverage.go;l=248-262;drc=master
+
+def _cc_stub_library_impl(ctx):
+    # The name of this target.
+    name = ctx.attr.name
+
+    # All declared outputs of ndkstubgen.
+    out_stub_c = ctx.actions.declare_file("/".join([name, "stub.c"]))
+    out_stub_map = ctx.actions.declare_file("/".join([name, "stub.map"]))
+    out_abi_symbol_list = ctx.actions.declare_file("/".join([name, "abi_symbol_list.txt"]))
+
+    outputs = [out_stub_c, out_stub_map, out_abi_symbol_list]
+
+    arch = get_arch(ctx)
+
+    ndkstubgen_args = ctx.actions.args()
+    ndkstubgen_args.add_all(["--arch", arch])
+    ndkstubgen_args.add_all(["--api", ctx.attr.version])
+    ndkstubgen_args.add_all(["--api-map", ctx.file._api_levels_file])
+    # TODO(b/207812332): This always parses and builds the stub library as a dependency of an APEX. Parameterize this
+    # for non-APEX use cases.
+    ndkstubgen_args.add_all(["--apex", ctx.file.symbol_file])
+    ndkstubgen_args.add_all(outputs)
+    ctx.actions.run(
+        executable = ctx.executable._ndkstubgen,
+        inputs = [
+            ctx.file.symbol_file,
+            ctx.file._api_levels_file,
+        ],
+        outputs = outputs,
+        arguments = [ndkstubgen_args],
+    )
+
+    files = depset(direct = outputs)
+    return [
+        DefaultInfo(files = files)
+    ]
+
+cc_stub_library = rule(
+    implementation = _cc_stub_library_impl,
+    attrs = dicts.add({
+        # Public attributes
+        "symbol_file": attr.label(mandatory = True, allow_single_file = [".map.txt"]),
+        "version": attr.string(mandatory = True, default = "current"),
+        # Private attributes
+        "_api_levels_file": attr.label(default = "@soong_injection//api_levels:api_levels.json", allow_single_file = True),
+        # TODO(b/199038020): Use //build/soong/cc/ndkstubgen when py_runtime is set up on CI for hermetic python usage.
+        # "_ndkstubgen": attr.label(default = "@make_injection//:host/linux-x86/bin/ndkstubgen", executable = True, cfg = "host", allow_single_file = True),
+        "_ndkstubgen": attr.label(default = "//build/soong/cc/ndkstubgen", executable = True, cfg = "host"),
+    }, ARCH_CONSTRAINT_ATTRS),
+)
diff --git a/rules/soong_injection.bzl b/rules/soong_injection.bzl
index 80950c2..acbb5d9 100644
--- a/rules/soong_injection.bzl
+++ b/rules/soong_injection.bzl
@@ -21,6 +21,7 @@
     rctx.symlink(soong_injection_dir + "/mixed_builds", "mixed_builds")
     rctx.symlink(soong_injection_dir + "/cc_toolchain", "cc_toolchain")
     rctx.symlink(soong_injection_dir + "/product_config", "product_config")
+    rctx.symlink(soong_injection_dir + "/api_levels", "api_levels")
     rctx.symlink(soong_injection_dir + "/metrics", "metrics")
 
 soong_injection_repository = repository_rule(