Support filtering stub libraries from the APEX based on dependency
transitiveness.

This CL results in both com.android.adbd and the minimal example APEX
correctly filtering out the various bionic libraries, which all have
versioned stubs.

Test: apex_diff_test
Fixes: 207812332
Change-Id: Id5e4d2cfae9237feb09010335716112d4628c689
diff --git a/rules/apex.bzl b/rules/apex.bzl
index 32c216e..8350b7c 100644
--- a/rules/apex.bzl
+++ b/rules/apex.bzl
@@ -153,7 +153,13 @@
     args.add_all(["--file_contexts", file_contexts.path])
     args.add_all(["--key", privkey.path])
     args.add_all(["--pubkey", pubkey.path])
-    args.add_all(["--min_sdk_version", ctx.attr.min_sdk_version])
+    min_sdk_version = ctx.attr.min_sdk_version
+    # TODO(b/215339575): This is a super rudimentary way to convert "current" to a numerical number.
+    # Generalize this to API level handling logic in a separate Starlark utility, preferably using
+    # API level maps dumped from api_levels.go
+    if min_sdk_version == "current":
+        min_sdk_version = "10000"
+    args.add_all(["--min_sdk_version", min_sdk_version])
     args.add_all(["--bazel_apexer_wrapper_manifest", bazel_apexer_wrapper_manifest])
     args.add_all(["--apexer_tool_path", apex_toolchain.apexer.dirname])
     args.add_all(["--apex_output_file", apex_output_file])
@@ -246,7 +252,7 @@
         "file_contexts": attr.label(allow_single_file = True, mandatory = True),
         "key": attr.label(providers = [ApexKeyInfo]),
         "certificate": attr.label(providers = [AndroidAppCertificateInfo]),
-        "min_sdk_version": attr.string(),
+        "min_sdk_version": attr.string(default = "current"),
         "updatable": attr.bool(default = True),
         "installable": attr.bool(default = True),
         "native_shared_libs_32": attr.label_list(
diff --git a/rules/apex/BUILD b/rules/apex/BUILD
index ece87ea..6120267 100644
--- a/rules/apex/BUILD
+++ b/rules/apex/BUILD
@@ -1,8 +1,23 @@
 load("//build/bazel/rules/apex:toolchain.bzl", "apex_toolchain")
-load("@bazel_skylib//rules:common_settings.bzl", "string_setting")
+load("@bazel_skylib//rules:common_settings.bzl", "string_setting", "string_list_setting")
 
-string_setting(name = "apex_name", build_setting_default = "")
-string_setting(name = "min_sdk_version", build_setting_default = "")
+string_setting(
+    name = "apex_name",
+    build_setting_default = "",
+    visibility = ["//visibility:public"],
+)
+
+string_setting(
+    name = "min_sdk_version",
+    build_setting_default = "",
+    visibility = ["//visibility:public"],
+)
+
+string_list_setting(
+    name = "apex_direct_deps",
+    build_setting_default = [],
+    visibility = ["//visibility:public"],
+)
 
 toolchain_type(name = "apex_toolchain_type")
 
diff --git a/rules/apex/cc.bzl b/rules/apex/cc.bzl
index ef1d3a5..bd83499 100644
--- a/rules/apex/cc.bzl
+++ b/rules/apex/cc.bzl
@@ -14,6 +14,9 @@
 limitations under the License.
 """
 
+load("//build/bazel/rules:cc_library_shared.bzl", "CcStubLibrariesInfo")
+load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
+
 ApexCcInfo = provider(
     "Info needed to use CC targets in APEXes",
     fields = {
@@ -21,14 +24,72 @@
     },
 )
 
+# Return True if this target provides stubs that is equal to, or below, the
+# APEX's min_sdk_level.
+#
+# These stable ABI libraries are intentionally omitted from APEXes as they are
+# provided from another APEX or the platform.  By omitting them from APEXes, we
+# ensure that there are no multiple copies of such libraries on a device.
+def has_cc_stubs(target, ctx):
+    if ctx.rule.kind != "_cc_library_shared_proxy":
+        # only _cc_library_shared_proxy contains merged CcStubLibrariesInfo providers
+        # (a provider aggregating CcStubInfo and CcSharedLibraryInfo)
+        return False
+
+    if len(target[CcStubLibrariesInfo].infos) == 0:
+        # Not all shared library targets have stubs
+        return False
+
+    # Minimum SDK version supported by the APEX that transitively depends on
+    # this target.
+    min_sdk_version = ctx.attr._min_sdk_version[BuildSettingInfo].value
+    apex_name = ctx.attr._apex_name[BuildSettingInfo].value
+
+    available_versions = []
+    # Check that the shared library has stubs built for (at least) the
+    # min_sdk_version of the APEX
+    for stub_info in target[CcStubLibrariesInfo].infos:
+        stub_version = stub_info["CcStubInfo"].version
+        available_versions.append(stub_version)
+        if stub_version <= min_sdk_version:
+            return True
+
+    fail("cannot find a stub lib version for min_sdk_level %s (%s apex)\navailable versions: %s (%s)"
+         % (min_sdk_version, apex_name, available_versions, target.label))
+
+# Check if this target is specified as a direct dependency of the APEX,
+# as opposed to a transitive dependency, as the transitivity impacts
+# the files that go into an APEX.
+def is_apex_direct_dep(target, ctx):
+    apex_direct_deps = ctx.attr._apex_direct_deps[BuildSettingInfo].value
+    return str(target.label) in apex_direct_deps
+
 def _apex_cc_aspect_impl(target, ctx):
+    # Whether this dep is a direct dep of an APEX or makes a difference in dependency
+    # traversal, and aggregation of libs that are required from the platform/other APEXes,
+    # and libs that this APEX will provide to others.
+    is_direct_dep = is_apex_direct_dep(target, ctx)
+
+    if has_cc_stubs(target, ctx):
+        if is_direct_dep:
+            # TODO(b/215500321): Mark these libraries as "stub-providing" exports
+            # of this APEX, which the system and other APEXes can depend on,
+            # and propagate this list.
+            pass
+        else:
+            # If this is not a direct dep, and stubs are available, don't propagate
+            # the libraries.
+            #
+            # TODO(b/215500321): In a bundled build, ensure that these libraries are
+            # available on the system either via the system partition, or another APEX
+            # and propagate this list.
+            return [ApexCcInfo(transitive_shared_libs = depset())]
+
     shared_object_files = []
 
     # Transitive deps containing shared libraries to be propagated the apex.
     transitive_deps = []
 
-    # TODO(b/207812332): Filter out the ones with stable APIs
-
     # Exclude the stripped and unstripped so files
     if ctx.rule.kind == "_cc_library_shared_proxy":
         for output_file in target[DefaultInfo].files.to_list():
@@ -57,6 +118,11 @@
 # This aspect is intended to be applied on a apex.native_shared_libs attribute
 apex_cc_aspect = aspect(
     implementation = _apex_cc_aspect_impl,
+    attrs = {
+        "_min_sdk_version": attr.label(default = "//build/bazel/rules/apex:min_sdk_version"),
+        "_apex_name": attr.label(default = "//build/bazel/rules/apex:apex_name"),
+        "_apex_direct_deps": attr.label(default = "//build/bazel/rules/apex:apex_direct_deps"),
+    },
     attr_aspects = ["dynamic_deps", "shared", "src"],
     # TODO: Have this aspect also propagate along attributes of native_shared_libs?
 )
diff --git a/rules/apex/transition.bzl b/rules/apex/transition.bzl
index 86d39cc..e6ffa37 100644
--- a/rules/apex/transition.bzl
+++ b/rules/apex/transition.bzl
@@ -25,14 +25,23 @@
 # will use a different configuration from building T indirectly as a dependency of A. The
 # latter will contain APEX specific configuration settings that its rule or an aspect can
 # use to create different actions or providers for APEXes specifically..
+#
+# The outgoing transitions are similar to ApexInfo propagation in Soong's
+# top-down ApexInfoMutator:
+# https://cs.android.com/android/platform/superproject/+/master:build/soong/apex/apex.go;l=948-962;drc=539d41b686758eeb86236c0e0dcf75478acb77f3
+
+load("@bazel_skylib//lib:dicts.bzl", "dicts")
+
+def _create_apex_configuration(attr, additional = {}):
+    return dicts.add({
+        "//build/bazel/rules/apex:apex_name": attr.name,  # Name of the APEX
+        "//build/bazel/rules/apex:min_sdk_version": attr.min_sdk_version,  # Min SDK version of the APEX
+    }, additional)
 
 def _impl(settings, attr):
     # Perform a transition to apply APEX specific build settings on the
     # destination target (i.e. an APEX dependency).
-    return {
-        "//build/bazel/rules/apex:apex_name": attr.name,  # Name of the APEX
-        "//build/bazel/rules/apex:min_sdk_version": attr.min_sdk_version,  # Min SDK version of the APEX
-    }
+    return _create_apex_configuration(attr)
 
 apex_transition = transition(
     implementation = _impl,
@@ -47,19 +56,19 @@
     # Perform a transition to apply APEX specific build settings on the
     # destination target (i.e. an APEX dependency).
 
+    direct_deps = [str(dep) for dep in attr.native_shared_libs_32]
+
     # TODO: We need to check if this is a x86 or arm arch then only set one platform
     # instead of this 1:2 split to avoid performance hit.
     return {
-        "x86": {
+        "x86": _create_apex_configuration(attr, {
             "//command_line_option:platforms": "//build/bazel/platforms:android_x86",
-            "//build/bazel/rules/apex:apex_name": attr.name,  # Name of the APEX
-            "//build/bazel/rules/apex:min_sdk_version": attr.min_sdk_version,  # Min SDK version of the APEX
-        },
-        "arm": {
+            "//build/bazel/rules/apex:apex_direct_deps": direct_deps,
+        }),
+        "arm": _create_apex_configuration(attr, {
             "//command_line_option:platforms": "//build/bazel/platforms:android_arm",
-            "//build/bazel/rules/apex:apex_name": attr.name,  # Name of the APEX
-            "//build/bazel/rules/apex:min_sdk_version": attr.min_sdk_version,  # Min SDK version of the APEX
-        },
+            "//build/bazel/rules/apex:apex_direct_deps": direct_deps,
+        }),
     }
 
 shared_lib_transition_32 = transition(
@@ -68,6 +77,7 @@
     outputs = [
         "//build/bazel/rules/apex:apex_name",
         "//build/bazel/rules/apex:min_sdk_version",
+        "//build/bazel/rules/apex:apex_direct_deps",
         "//command_line_option:platforms",
     ],
 )
@@ -76,19 +86,19 @@
     # Perform a transition to apply APEX specific build settings on the
     # destination target (i.e. an APEX dependency).
 
+    direct_deps = [str(dep) for dep in attr.native_shared_libs_64]
+
     # TODO: We need to check if this is a x86 or arm arch then only set one platform
     # instead of this 1:2 split to avoid performance hit.
     return {
-        "x86_64": {
+        "x86_64": _create_apex_configuration(attr, {
             "//command_line_option:platforms": "//build/bazel/platforms:android_x86_64",
-            "//build/bazel/rules/apex:apex_name": attr.name,  # Name of the APEX
-            "//build/bazel/rules/apex:min_sdk_version": attr.min_sdk_version,  # Min SDK version of the APEX
-        },
-        "arm64": {
+            "//build/bazel/rules/apex:apex_direct_deps": direct_deps,
+        }),
+        "arm64": _create_apex_configuration(attr, {
             "//command_line_option:platforms": "//build/bazel/platforms:android_arm64",
-            "//build/bazel/rules/apex:apex_name": attr.name,  # Name of the APEX
-            "//build/bazel/rules/apex:min_sdk_version": attr.min_sdk_version,  # Min SDK version of the APEX
-        },
+            "//build/bazel/rules/apex:apex_direct_deps": direct_deps,
+        }),
     }
 
 shared_lib_transition_64 = transition(
@@ -97,6 +107,7 @@
     outputs = [
         "//build/bazel/rules/apex:apex_name",
         "//build/bazel/rules/apex:min_sdk_version",
+        "//build/bazel/rules/apex:apex_direct_deps",
         "//command_line_option:platforms",
     ],
 )
diff --git a/tests/apex/BUILD b/tests/apex/BUILD
index 7342dc2..9580606 100644
--- a/tests/apex/BUILD
+++ b/tests/apex/BUILD
@@ -10,14 +10,10 @@
     name = "build.bazel.examples.apex.minimal",
     apex1 = "//build/bazel/examples/apex/minimal:build.bazel.examples.apex.minimal",
     apex2 = "@make_injection//:target/product/generic/system/product/apex/build.bazel.examples.apex.minimal.apex",
-    # Expected not to match exactly yet.
-    expected_diff = "expected_build.bazel.examples.apex.minimal",
 )
 
 apex_diff_test(
     name = "com.android.adbd",
     apex1 = "//packages/modules/adb/apex:com.android.adbd",
     apex2 = "@make_injection//:target/product/generic/system/apex/com.android.adbd.capex",
-    # Expected not to match exactly yet.
-    expected_diff = "expected_com.android.adbd",
 )
diff --git a/tests/apex/expected_build.bazel.examples.apex.minimal b/tests/apex/expected_build.bazel.examples.apex.minimal
deleted file mode 100644
index 0784195..0000000
--- a/tests/apex/expected_build.bazel.examples.apex.minimal
+++ /dev/null
@@ -1,6 +0,0 @@
-7d6
-< ./lib/ld-android.so
-9,11d7
-< ./lib/libc.so
-< ./lib/libdl.so
-< ./lib/libm.so
diff --git a/tests/apex/expected_com.android.adbd b/tests/apex/expected_com.android.adbd
deleted file mode 100644
index f942f36..0000000
--- a/tests/apex/expected_com.android.adbd
+++ /dev/null
@@ -1,8 +0,0 @@
-4d3
-< ./lib/ld-android.so
-11d9
-< ./lib/libc.so
-15,17d12
-< ./lib/libdl.so
-< ./lib/liblog.so
-< ./lib/libm.so