Merge remote-tracking branch 'aosp/upstream-main' into HEAD am: 041e9eeb15 am: 78ce59df93
Original change: https://android-review.googlesource.com/c/platform/external/bazelbuild-rules_android/+/2722373
Change-Id: I86420496c0a0156277d264c9749f29b10ec84a64
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index 6df2bf1..93aab2f 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -9,33 +9,28 @@
- "//android/..."
- "//rules/..."
- "-//src/java/com/example/sampleapp/..."
- - "-//src/tools/ak/..."
- "//toolchains/..."
- "//tools/..."
+ - "-//tools/android/..." # TODO(#122): Un-exclude this once #122 is fixed.
test_targets:
- "//src/..."
- "//test/..."
- - "-//src/tools/..."
+ - "-//src/tools/enforce_min_sdk_floor/..."
- "-//src/java/com/example/sampleapp/..."
+ test_flags:
+ # Sandboxed SDK tools depend on libraries that require Java runtime 17 or higher.
+ - "--java_runtime_version=17"
tasks:
- ubuntu1604:
- <<: *common
- ubuntu1804:
+ ubuntu2004:
<<: *common
macos:
<<: *common
macos_arm64:
<<: *common
- ubuntu1604_bzlmod:
- name: Bzlmod ubuntu1604
- platform: ubuntu1604
- build_flags:
- - "--enable_bzlmod"
- <<: *common
- ubuntu1804_bzlmod:
- name: Bzlmod ubuntu1804
- platform: ubuntu1804
+ ubuntu2004_bzlmod:
+ name: Bzlmod ubuntu2004
+ platform: ubuntu2004
build_flags:
- "--enable_bzlmod"
<<: *common
@@ -50,4 +45,4 @@
platform: macos_arm64
build_flags:
- "--enable_bzlmod"
- <<: *common
\ No newline at end of file
+ <<: *common
diff --git a/MODULE.bazel b/MODULE.bazel
index 4bf2eb2..7a6c172 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -42,8 +42,13 @@
maven.install(
name = "rules_android_maven",
artifacts = [
- "com.android.tools.build:bundletool:1.6.1",
- "com.android.tools.build:gradle:8.0.1",
+ "com.android.tools.build:bundletool:1.15.2",
+ "com.android.tools.build:gradle:8.2.0-alpha15",
+ "com.google.guava:guava:32.1.2-jre",
+ "com.google.protobuf:protobuf-java-util:3.9.2",
+ "com.google.truth:truth:1.1.5",
+ "info.picocli:picocli:4.7.4",
+ "junit:junit:4.13.2",
],
repositories = [
"https://maven.google.com",
diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 0000000..209dd84
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,4 @@
+# Bazel Android Rules Roadmap
+
+See https://github.com/orgs/bazelbuild/projects/17 for the Starlark Android
+Rules roadmap.
\ No newline at end of file
diff --git a/WORKSPACE b/WORKSPACE
index fb3a90e..7aab8a6 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1,7 +1,7 @@
workspace(name = "build_bazel_rules_android")
-load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")
+load(":android_sdk_supplemental_repository.bzl", "android_sdk_supplemental_repository")
maybe(
android_sdk_repository,
@@ -13,6 +13,11 @@
name = "androidndk",
)
+# This can be removed once https://github.com/bazelbuild/bazel/commit/773b50f979b8f40e73cf547049bb8e1114fb670a
+# is released, or android_sdk_repository is properly Starlarkified and dexdump
+# added there.
+android_sdk_supplemental_repository(name = "androidsdk-supplemental")
+
load("prereqs.bzl", "rules_android_prereqs")
rules_android_prereqs()
diff --git a/WORKSPACE.bzlmod b/WORKSPACE.bzlmod
index 0aca21e..540b125 100644
--- a/WORKSPACE.bzlmod
+++ b/WORKSPACE.bzlmod
@@ -2,6 +2,7 @@
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")
+load(":android_sdk_supplemental_repository.bzl", "android_sdk_supplemental_repository")
maybe(
android_sdk_repository,
@@ -12,3 +13,8 @@
android_ndk_repository,
name = "androidndk",
)
+
+# This can be removed once https://github.com/bazelbuild/bazel/commit/773b50f979b8f40e73cf547049bb8e1114fb670a
+# is released, or android_sdk_repository is properly Starlarkified and dexdump
+# added there.
+android_sdk_supplemental_repository(name = "androidsdk-supplemental")
\ No newline at end of file
diff --git a/android/rules.bzl b/android/rules.bzl
index ecaef19..ed1cd2d 100644
--- a/android/rules.bzl
+++ b/android/rules.bzl
@@ -37,6 +37,14 @@
_android_ndk_repository = "android_ndk_repository",
)
load(
+ "//rules/android_sandboxed_sdk:android_sandboxed_sdk.bzl",
+ _android_sandboxed_sdk = "android_sandboxed_sdk",
+)
+load(
+ "//rules/android_sandboxed_sdk:android_sandboxed_sdk_bundle.bzl",
+ _android_sandboxed_sdk_bundle = "android_sandboxed_sdk_bundle",
+)
+load(
"//rules:android_sdk.bzl",
_android_sdk = "android_sdk",
)
@@ -57,6 +65,8 @@
android_binary = _android_binary
android_library = _android_library
android_ndk_repository = _android_ndk_repository
+android_sandboxed_sdk = _android_sandboxed_sdk
+android_sandboxed_sdk_bundle = _android_sandboxed_sdk_bundle
android_sdk = _android_sdk
android_sdk_repository = _android_sdk_repository
android_tools_defaults_jar = _android_tools_defaults_jar
diff --git a/android_sdk_supplemental_repository.bzl b/android_sdk_supplemental_repository.bzl
new file mode 100644
index 0000000..1ac031d
--- /dev/null
+++ b/android_sdk_supplemental_repository.bzl
@@ -0,0 +1,67 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""A repository rule for integrating the Android SDK."""
+
+def _parse_version(version):
+ # e.g.:
+ # "33.1.1" -> 330101
+ # "4.0.0" -> 40000
+ # "33.1.1" < "4.0.0" but 330101 > 40000
+ major, minor, micro = version.split(".")
+ return (int(major) * 10000 + int(minor) * 100 + int(micro), version)
+
+def _android_sdk_supplemental_repository_impl(ctx):
+ """A repository for additional SDK content.
+
+ Needed until android_sdk_repository is fully in Starlark.
+
+ Args:
+ ctx: An implementation context.
+
+ Returns:
+ A final dict of configuration attributes and values.
+ """
+ sdk_path = ctx.attr.path or ctx.os.environ.get("ANDROID_HOME", None)
+ if not sdk_path:
+ fail("Either the ANDROID_HOME environment variable or the " +
+ "path attribute of android_sdk_supplemental_repository " +
+ "must be set.")
+
+ build_tools_dirs = ctx.path(sdk_path + "/build-tools").readdir()
+ _, highest_build_tool_version = (
+ max([_parse_version(v.basename) for v in build_tools_dirs])
+ )
+ ctx.symlink(
+ sdk_path + "/build-tools/" + highest_build_tool_version,
+ "build-tools/" + highest_build_tool_version,
+ )
+ ctx.file(
+ "BUILD",
+ """
+filegroup(
+ name = "dexdump",
+ srcs = ["build-tools/%s/dexdump"],
+ visibility = ["//visibility:public"],
+)
+""" % highest_build_tool_version,
+ )
+
+android_sdk_supplemental_repository = repository_rule(
+ attrs = {
+ "path": attr.string(),
+ },
+ local = True,
+ implementation = _android_sdk_supplemental_repository_impl,
+)
diff --git a/defs.bzl b/defs.bzl
index 9f07ef3..8dfd421 100644
--- a/defs.bzl
+++ b/defs.bzl
@@ -21,6 +21,7 @@
load("@robolectric//bazel:robolectric.bzl", "robolectric_repositories")
load("@rules_java//java:repositories.bzl", "rules_java_dependencies", "rules_java_toolchains")
load("@rules_jvm_external//:defs.bzl", "maven_install")
+load("@rules_jvm_external//:specs.bzl", "maven")
load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_proto_toolchains")
load("@rules_python//python:repositories.bzl", "py_repositories")
@@ -33,8 +34,27 @@
maven_install(
name = "rules_android_maven",
artifacts = [
- "com.android.tools.build:bundletool:1.6.1",
- "com.android.tools.build:gradle:8.0.1",
+ "androidx.privacysandbox.tools:tools:1.0.0-alpha05",
+ maven.artifact(
+ group = "androidx.privacysandbox.tools",
+ artifact = "tools-apipackager",
+ version = "1.0.0-alpha05",
+ exclusions = [
+ # Alpha05 pulls in the lite version of protobuf library,
+ # which doesn't have the JSON utils we need and clashes with
+ # com.google.protobuf:protobuf-java-util.
+ # This was fixed in AOSP, so this can be removed once
+ # the packager releases a new version (>alpha05).
+ "com.google.protobuf:protobuf-javalite",
+ ],
+ ),
+ "com.android.tools.build:bundletool:1.15.2",
+ "com.android.tools.build:gradle:8.2.0-alpha15",
+ "com.google.guava:guava:32.1.2-jre",
+ "com.google.protobuf:protobuf-java-util:3.9.2",
+ "com.google.truth:truth:1.1.5",
+ "info.picocli:picocli:4.7.4",
+ "junit:junit:4.13.2",
],
repositories = [
"https://maven.google.com",
@@ -64,10 +84,10 @@
)
go_repository(
- name = "org_golang_x_sync",
- importpath = "golang.org/x/sync",
- sum = "h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=",
- version = "v0.0.0-20210220032951-036812b2e83c",
+ name = "org_golang_x_sync",
+ importpath = "golang.org/x/sync",
+ sum = "h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=",
+ version = "v0.0.0-20210220032951-036812b2e83c",
)
robolectric_repositories()
@@ -78,4 +98,4 @@
rules_proto_dependencies()
rules_proto_toolchains()
- py_repositories()
\ No newline at end of file
+ py_repositories()
diff --git a/kokoro/presubmit/kokoro_presubmit.sh b/kokoro/presubmit/kokoro_presubmit.sh
index d4f15d7..1c183f4 100644
--- a/kokoro/presubmit/kokoro_presubmit.sh
+++ b/kokoro/presubmit/kokoro_presubmit.sh
@@ -62,6 +62,11 @@
"--experimental_google_legacy_api"
"--experimental_enable_android_migration_apis"
"--build_tests_only"
+ # Java tests use language version at least 11, but they might depend on
+ # libraries that were built for Java 17.
+ "--java_language_version=11"
+ "--java_runtime_version=17"
+ "--test_output=errors"
)
# Go to rules_android workspace and run relevant tests.
@@ -73,7 +78,9 @@
"$bazel" test "${COMMON_ARGS[@]}" //src/common/golang/... \
//src/tools/ak/... \
+ //src/tools/javatests/... \
//src/tools/jdeps/... \
+ //src/tools/java/... \
//test/...
# Go to basic app workspace in the source tree
diff --git a/mobile_install/BUILD b/mobile_install/BUILD
new file mode 100644
index 0000000..3c89c82
--- /dev/null
+++ b/mobile_install/BUILD
@@ -0,0 +1,26 @@
+# Description:
+# Blaze mobile-install aspect package.
+
+load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+
+package(
+ default_applicable_licenses = ["//:license"],
+ default_visibility = ["//:__subpackages__"],
+)
+
+licenses(["notice"])
+
+exports_files(["mi.bzl"])
+
+filegroup(
+ name = "all_files",
+ srcs = glob(["**"]),
+)
+
+bzl_library(
+ name = "bzl",
+ srcs = glob(["**/*.bzl"]),
+ deps = [
+ "//rules:bzl",
+ ],
+)
diff --git a/mobile_install/adapters/aar_import.bzl b/mobile_install/adapters/aar_import.bzl
index f1e5db9..bbef556 100644
--- a/mobile_install/adapters/aar_import.bzl
+++ b/mobile_install/adapters/aar_import.bzl
@@ -76,7 +76,7 @@
dex_shards = dex(
ctx,
target[JavaInfo].runtime_output_jars,
- target[JavaInfo].transitive_deps,
+ target[JavaInfo].transitive_compile_time_jars,
),
deps = providers.collect(
MIAndroidDexInfo,
diff --git a/mobile_install/adapters/android_binary.bzl b/mobile_install/adapters/android_binary.bzl
index 98641e1..8ea2c04 100644
--- a/mobile_install/adapters/android_binary.bzl
+++ b/mobile_install/adapters/android_binary.bzl
@@ -82,7 +82,7 @@
) +
(
),
- target[JavaInfo].transitive_deps,
+ target[JavaInfo].transitive_compile_time_jars,
),
deps = providers.collect(MIAndroidDexInfo, ctx.rule.attr.deps),
),
diff --git a/mobile_install/adapters/android_library.bzl b/mobile_install/adapters/android_library.bzl
index be30a32..8681dba 100644
--- a/mobile_install/adapters/android_library.bzl
+++ b/mobile_install/adapters/android_library.bzl
@@ -83,7 +83,7 @@
ctx.label.name + "_resources.jar",
target[JavaInfo].runtime_output_jars,
),
- target[JavaInfo].transitive_deps,
+ target[JavaInfo].transitive_compile_time_jars,
),
deps = providers.collect(
MIAndroidDexInfo,
diff --git a/mobile_install/adapters/java_import.bzl b/mobile_install/adapters/java_import.bzl
index d4b9c92..9f6e964 100644
--- a/mobile_install/adapters/java_import.bzl
+++ b/mobile_install/adapters/java_import.bzl
@@ -45,7 +45,7 @@
dex_shards = dex(
ctx,
target[JavaInfo].runtime_output_jars,
- target[JavaInfo].transitive_deps,
+ target[JavaInfo].transitive_compile_time_jars,
create_file = utils.declare_file,
),
deps = providers.collect(
diff --git a/mobile_install/adapters/java_library.bzl b/mobile_install/adapters/java_library.bzl
index afeee64..7f8c902 100644
--- a/mobile_install/adapters/java_library.bzl
+++ b/mobile_install/adapters/java_library.bzl
@@ -44,7 +44,7 @@
dex_shards = dex(
ctx,
target[JavaInfo].runtime_output_jars,
- target[JavaInfo].transitive_deps,
+ target[JavaInfo].transitive_compile_time_jars,
),
deps = providers.collect(
MIAndroidDexInfo,
diff --git a/mobile_install/adapters/java_lite_grpc_library.bzl b/mobile_install/adapters/java_lite_grpc_library.bzl
index 5eff4a8..7880080 100644
--- a/mobile_install/adapters/java_lite_grpc_library.bzl
+++ b/mobile_install/adapters/java_lite_grpc_library.bzl
@@ -36,7 +36,7 @@
dex_shards = dex(
ctx,
target[JavaInfo].runtime_output_jars,
- target[JavaInfo].transitive_deps,
+ target[JavaInfo].transitive_compile_time_jars,
),
deps = providers.collect(
MIAndroidDexInfo,
diff --git a/mobile_install/adapters/proto_library.bzl b/mobile_install/adapters/proto_library.bzl
index 9e5d8da..fe402f0 100644
--- a/mobile_install/adapters/proto_library.bzl
+++ b/mobile_install/adapters/proto_library.bzl
@@ -38,7 +38,7 @@
dex_shards = dex(
ctx,
[j.class_jar for j in target[JavaInfo].outputs.jars],
- target[JavaInfo].transitive_deps,
+ target[JavaInfo].transitive_compile_time_jars,
),
deps = providers.collect(MIAndroidDexInfo, ctx.rule.attr.deps),
),
diff --git a/mobile_install/constants.bzl b/mobile_install/constants.bzl
new file mode 100644
index 0000000..ab8306d
--- /dev/null
+++ b/mobile_install/constants.bzl
@@ -0,0 +1,25 @@
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# 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.
+"""Constants."""
+
+constants = struct(
+ # Immutable empty collections.
+ EMPTY_LIST = [],
+ EMPTY_DICT = dict(),
+
+ # Skylark Types
+ TYPE_DEPSET = type(depset()),
+ TYPE_DICT = type(dict()),
+ TYPE_LIST = type([]),
+)
diff --git a/mobile_install/debug.bzl b/mobile_install/debug.bzl
new file mode 100644
index 0000000..9acc44e
--- /dev/null
+++ b/mobile_install/debug.bzl
@@ -0,0 +1,44 @@
+# Copyright 2019 The Bazel Authors. All rights reserved.
+#
+# 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.
+"""Module that enables debugging for mobile-install."""
+
+def _make_output_groups(infos):
+ output_groups = dict()
+ for info in infos:
+ if hasattr(info, "info"):
+ output_group = dict(
+ mi_java_info = info.info.runtime_output_jars,
+ )
+ elif hasattr(info, "transitive_java_resources"):
+ output_group = dict(
+ mi_java_resources_info = info.transitive_java_resources,
+ )
+ elif hasattr(info, "transitive_native_libs"):
+ output_group = dict(
+ mi_aar_native_libs_info = info.transitive_native_libs,
+ )
+ elif hasattr(info, "transitive_dex_shards"):
+ output_group = dict(
+ mi_android_dex_info = depset(
+ transitive = info.transitive_dex_shards,
+ ),
+ )
+ else:
+ fail("Unsupported provider %s" % info)
+ output_groups.update(output_group)
+ return output_groups
+
+debug = struct(
+ make_output_groups = _make_output_groups,
+)
diff --git a/mobile_install/dependency_map.bzl b/mobile_install/dependency_map.bzl
new file mode 100644
index 0000000..2769a90
--- /dev/null
+++ b/mobile_install/dependency_map.bzl
@@ -0,0 +1,63 @@
+# Copyright 2022 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""This file keeps track of the locations of binaries for Mobile-Install."""
+
+versioned_deps = struct(
+ mi_shell_app = struct(
+ head = "//tools/android:fail",
+ ),
+ android_kit = struct(
+ head = "//src/tools/ak",
+ ),
+ bootstraper = struct(
+ head = "//tools/android:fail",
+ ),
+ deploy = struct(
+ head = "//src/tools/mi/deployment:deploy_binary",
+ ),
+ deploy_info = struct(
+ head = "//src/tools/mi/deploy_info:deploy_info",
+ ),
+ forwarder = struct(
+ head = "//tools/android:fail",
+ ),
+ jar_tool = struct(
+ head = "@bazel_tools//tools/jdk:JavaBuilder_deploy.jar",
+ ),
+ make_sync = struct(
+ head = "//src/tools/mi/app_info:make_sync",
+ ),
+ merge_syncs = struct(
+ head = "//src/tools/mi/workspace:merge_syncs",
+ ),
+ pack_dexes = struct(
+ head = "//src/tools/mi/workspace:pack_dexes",
+ ),
+ pack_generic = struct(
+ head = "//src/tools/mi/workspace:pack_generic",
+ ),
+ res_v3_dummy_manifest = struct(
+ head = "//rules:res_v3_dummy_AndroidManifest.xml",
+ ),
+ res_v3_dummy_r_txt = struct(
+ head = "//rules:res_v3_dummy_R.txt",
+ ),
+ resource_extractor = struct(
+ head = "//src/tools/resource_extractor:main",
+ ),
+ sync_merger = struct(
+ head = "//src/tools/mi/app_info:sync_merger",
+ ),
+)
diff --git a/mobile_install/tools.bzl b/mobile_install/tools.bzl
new file mode 100644
index 0000000..5086779
--- /dev/null
+++ b/mobile_install/tools.bzl
@@ -0,0 +1,204 @@
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# 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.
+"""Tools needed by the mobile-install aspect defined as aspect attributes."""
+
+load(":dependency_map.bzl", "versioned_deps")
+
+TOOL_ATTRS = dict(
+ # Target Attrs
+ # This library should not be versioned. It needs to be built with the same
+ # config that is used to build the app. Android binds the application to a
+ # concrete achitecture during install time. If no libs are on the apk, it
+ # will select the most specific to the device is running. We want to use
+ # whatever the user builds as long as it is compatible. And since we push
+ # the native libs outside the apk to speed up transfer times, we need to
+ # use dummy libs.
+ _android_sdk = attr.label(
+ default = Label(
+ "@androidsdk//:sdk",
+ ),
+ allow_files = True,
+ cfg = "target",
+ ),
+ _flags = attr.label(
+ default = Label(
+ "//rules/flags",
+ ),
+ ),
+ _studio_deployer = attr.label(
+ default = "@androidsdk//:fail", # TODO(#119): Studio deployer jar to be released
+ allow_single_file = True,
+ cfg = "exec",
+ executable = True,
+ ),
+ _mi_shell_dummy_native_libs = attr.label(
+ default = Label(
+ "@androidsdk//:fail", # FIXME: Unused internally
+ ),
+ allow_single_file = True,
+ cfg = "target",
+ ),
+ _mi_shell_app = attr.label(
+ default = versioned_deps.mi_shell_app.head,
+ allow_files = True,
+ cfg = "target",
+ executable = True,
+ ),
+ _mi_java8_legacy_dex = attr.label(
+ default = Label("//tools/android:java8_legacy_dex"),
+ allow_single_file = True,
+ cfg = "target",
+ ),
+
+ # Host Attrs
+ _aapt2 = attr.label(
+ default = Label(
+ "@androidsdk//:aapt2_binary",
+ ),
+ allow_single_file = True,
+ cfg = "exec",
+ executable = True,
+ ),
+ _android_test_runner = attr.label(
+ default = Label(
+ "@bazel_tools//tools/jdk:TestRunner_deploy.jar",
+ ),
+ allow_single_file = True,
+ cfg = "exec",
+ executable = True,
+ ),
+ _apk_signer = attr.label(
+ default = Label("@androidsdk//:apksigner"),
+ allow_files = True,
+ cfg = "exec",
+ executable = True,
+ ),
+ _desugar_java8 = attr.label(
+ default = Label("//tools/android:desugar_java8"),
+ allow_files = True,
+ cfg = "exec",
+ executable = True,
+ ),
+ _d8 = attr.label(
+ default = Label("@bazel_tools//src/tools/android/java/com/google/devtools/build/android/r8:r8"),
+ allow_files = True,
+ cfg = "exec",
+ executable = True,
+ ),
+ _host_java_runtime = attr.label(
+ default = Label("//tools/jdk:current_host_java_runtime"),
+ cfg = "exec",
+ ),
+ _java_jdk = attr.label(
+ default = Label("//tools/jdk:current_java_runtime"),
+ allow_files = True,
+ cfg = "exec",
+ ),
+ _resource_busybox = attr.label(
+ default = Label("@bazel_tools//src/tools/android/java/com/google/devtools/build/android:ResourceProcessorBusyBox_deploy.jar"),
+ allow_files = True,
+ cfg = "exec",
+ executable = True,
+ ),
+ _zipalign = attr.label(
+ default = Label(
+ "@androidsdk//:zipalign_binary",
+ ),
+ allow_single_file = True,
+ cfg = "exec",
+ executable = True,
+ ),
+
+
+ # Versioned Host Attrs
+ _android_kit = attr.label(
+ default = versioned_deps.android_kit.head,
+ allow_files = True,
+ cfg = "exec",
+ executable = True,
+ ),
+ _deploy = attr.label(
+ default = versioned_deps.deploy.head,
+ allow_files = True,
+ cfg = "exec",
+ executable = True,
+ ),
+ _deploy_info = attr.label(
+ default = versioned_deps.deploy_info.head,
+ allow_files = True,
+ cfg = "exec",
+ executable = True,
+ ),
+ _jar_tool = attr.label(
+ default = versioned_deps.jar_tool.head,
+ allow_files = True,
+ cfg = "exec",
+ executable = True,
+ ),
+ _make_sync = attr.label(
+ default = versioned_deps.make_sync.head,
+ allow_files = True,
+ cfg = "exec",
+ executable = True,
+ ),
+ _merge_syncs = attr.label(
+ default = versioned_deps.merge_syncs.head,
+ allow_files = True,
+ cfg = "exec",
+ executable = True,
+ ),
+ _mi_android_java_toolchain = attr.label(
+ default = Label("//tools/jdk:toolchain_android_only"),
+ ),
+ _mi_java_toolchain = attr.label(
+ cfg = "exec",
+ default = Label("//tools/jdk:toolchain"),
+ ),
+ _mi_host_javabase = attr.label(
+ default = Label("//tools/jdk:current_host_java_runtime"),
+ ),
+ _pack_dexes = attr.label(
+ default = versioned_deps.pack_dexes.head,
+ allow_files = True,
+ cfg = "exec",
+ executable = True,
+ ),
+ _pack_generic = attr.label(
+ default = versioned_deps.pack_generic.head,
+ allow_files = True,
+ cfg = "exec",
+ executable = True,
+ ),
+ _res_v3_dummy_manifest = attr.label(
+ allow_single_file = True,
+ default = versioned_deps.res_v3_dummy_manifest.head,
+ ),
+ _res_v3_dummy_r_txt = attr.label(
+ allow_single_file = True,
+ default = versioned_deps.res_v3_dummy_r_txt.head,
+ ),
+ _resource_extractor = attr.label(
+ allow_single_file = True,
+ cfg = "exec",
+ default = versioned_deps.resource_extractor.head,
+ executable = True,
+ ),
+ _sync_merger = attr.label(
+ default = versioned_deps.sync_merger.head,
+ allow_files = True,
+ cfg = "exec",
+ executable = True,
+ ),
+
+)
diff --git a/mobile_install/transform.bzl b/mobile_install/transform.bzl
new file mode 100644
index 0000000..1371365
--- /dev/null
+++ b/mobile_install/transform.bzl
@@ -0,0 +1,157 @@
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# 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.
+"""Transform contains data transformation methods."""
+
+load(":constants.bzl", "constants")
+load(":utils.bzl", "utils")
+load("//rules/flags:flags.bzl", _flags = "flags")
+
+def _declare_file(ctx, filename, sibling = None):
+ return utils.isolated_declare_file(ctx, filename, sibling = sibling)
+
+def filter_jars(name, data):
+ """Filters out files that are not compiled Jars - includes header Jars.
+
+ Args:
+ name: Name of the file to filter, check uses endswith on the path.
+ data: The list of tuples where each entry contains the originating file path
+ and file to apply the filter.
+
+ Returns:
+ A list of tuples where each entry contains the originating Jar path and the
+ Jar file.
+ """
+ return [jar for jar in data if not jar.path.endswith(name)]
+
+def dex(
+ ctx,
+ data,
+ deps = constants.EMPTY_LIST,
+ num_shards = None,
+ create_file = _declare_file,
+ desugar = True):
+ """Dex a list of Jars.
+
+ Args:
+ ctx: The context.
+ data: The list of tuples where each entry contains the originating Jar
+ path and the Jar to Dex.
+ deps: The list of dependencies for the Jar being desugared.
+ num_shards: The number of shards to distribute the dexed files across,
+ this value overrides the default provided by ctx.attr._mi_dex_shards.
+ create_file: In rare occasions a custom method is required to
+ create a unique file, override the default here. The method must
+ implement the following interface:
+
+ def create_file(ctx, filename, sibling = None)
+ Args:
+ ctx: The context.
+ filename: string. The name of the file.
+ sibling: File. The location of the new file.
+
+ Returns:
+ A File.
+ desugar: A boolean that determines whether to apply desugaring.
+
+ Returns:
+ A list of tuples where each entry contains the originating Jar path and
+ the Dex shards.
+ """
+ if num_shards:
+ num_dex_shards = num_shards
+ elif _flags.get(ctx).use_custom_dex_shards:
+ num_dex_shards = _flags.get(ctx).num_dex_shards
+ else:
+ num_dex_shards = ctx.attr._mi_dex_shards
+
+ dex_files = []
+ for jar in data:
+ out_dex_shards = []
+ dirname = jar.basename + "_dex"
+ for i in range(num_shards or num_dex_shards):
+ out_dex_shards.append(create_file(
+ ctx,
+ dirname + "/" + str(i) + ".zip",
+ sibling = jar,
+ ))
+ utils.dex(ctx, jar, out_dex_shards, deps, desugar)
+ dex_files.append(out_dex_shards)
+ return dex_files
+
+def extract_jar_resources(ctx, data, create_file = _declare_file):
+ """Extracts the non-class files from the list of Jars.
+
+ Args:
+ ctx: The context
+ data: The list of tuples where each entry contains the originating Jar
+ path and the Jar with resources to extract.
+ create_file: In rare occasions a custom method is required to
+ create a unique file, override the default here. The method must
+ implement the following interface:
+
+ def create_file(ctx, filename, sibling = None)
+ Args:
+ ctx: The context.
+ filename: string. The name of the file.
+ sibling: File. The location of the new file.
+
+ Returns:
+ A File.
+
+ Returns:
+ A list of extracted resource zips.
+ """
+ resources_files = []
+ for jar in data:
+ out_resources_file = create_file(
+ ctx,
+ jar.basename + "_resources.zip",
+ sibling = jar,
+ )
+ utils.extract_jar_resources(ctx, jar, out_resources_file)
+ resources_files.append(out_resources_file)
+ return resources_files
+
+def merge_dex_shards(ctx, data, sibling):
+ """Merges all dex files in the transitive deps to a dex per shard.
+
+ Given a list of dex files (and resources.zips) this will create an
+ action per shard that runs dex_shard_merger on all dex files within that
+ shard.
+
+ Arguments:
+ ctx: The context.
+ data: A list of lists, where the inner list contains dex shards.
+ sibling: A file used to root the merged_dex shards.
+
+ Returns:
+ A list of merged dex shards.
+ """
+ merged_dex_shards = []
+ for idx, shard in enumerate(data):
+ # To ensure resource is added at the beginning, R.zip is named as 00.zip
+ # Thus data shards starts from 1 instead of 0 and ranges through 16
+ idx += 1
+
+ # Shards are sorted before deployment, to ensure all shards are correctly
+ # ordered 0 is padded to single digit shard counts
+ shard_name = "%s%s" % ("00"[len(str(idx)):], idx)
+ merged_dex_shard = utils.isolated_declare_file(
+ ctx,
+ "dex_shards/" + shard_name + ".zip",
+ sibling = sibling,
+ )
+ utils.merge_dex_shards(ctx, shard, merged_dex_shard)
+ merged_dex_shards.append(merged_dex_shard)
+ return merged_dex_shards
diff --git a/rules/BUILD b/rules/BUILD
index 3648349..f31dd0a 100644
--- a/rules/BUILD
+++ b/rules/BUILD
@@ -26,6 +26,7 @@
srcs = [
"aapt.bzl",
"acls.bzl",
+ "android_neverlink_aspect.bzl",
"attrs.bzl",
"bundletool.bzl",
"busybox.bzl",
@@ -51,3 +52,37 @@
"//rules/flags:bzl",
],
)
+
+bzl_library(
+ name = "android_binary_bzl",
+ srcs = [
+ "android_binary.bzl",
+ ],
+ visibility = ["//:__subpackages__"],
+ deps = [
+ "//rules/android_binary_internal:bzl",
+ ],
+)
+
+bzl_library(
+ name = "bzl",
+ srcs = [
+ "android_ndk_repository.bzl",
+ "android_sdk.bzl",
+ "android_sdk_repository.bzl",
+ "android_tools_defaults_jar.bzl",
+ "baseline_profiles.bzl",
+ "dex.bzl",
+ "dex_desugar_aspect.bzl",
+ "rules.bzl",
+ ],
+ visibility = ["//mobile_install:__pkg__"],
+ deps = [
+ ":android_binary_bzl",
+ ":common_bzl",
+ "//rules/aar_import:bzl",
+ "//rules/android_library:bzl",
+ "//rules/android_sandboxed_sdk:bzl",
+ "//rules/flags:bzl",
+ ],
+)
diff --git a/rules/aar_import/BUILD b/rules/aar_import/BUILD
index b57f593..fd6bded 100644
--- a/rules/aar_import/BUILD
+++ b/rules/aar_import/BUILD
@@ -14,6 +14,7 @@
bzl_library(
name = "bzl",
srcs = glob(["*.bzl"]),
+ visibility = ["//rules:__pkg__"],
deps = [
"//rules:common_bzl",
"//rules/flags:bzl",
diff --git a/rules/aar_import/attrs.bzl b/rules/aar_import/attrs.bzl
index 022231b..c8b2659 100644
--- a/rules/aar_import/attrs.bzl
+++ b/rules/aar_import/attrs.bzl
@@ -70,4 +70,5 @@
),
_attrs.DATA_CONTEXT,
_attrs.ANDROID_TOOLCHAIN_ATTRS,
+ _attrs.AUTOMATIC_EXEC_GROUPS_ENABLED,
)
diff --git a/rules/aar_import/impl.bzl b/rules/aar_import/impl.bzl
index d551085..0b149bb 100644
--- a/rules/aar_import/impl.bzl
+++ b/rules/aar_import/impl.bzl
@@ -34,6 +34,7 @@
)
load(
"//rules:utils.bzl",
+ "ANDROID_TOOLCHAIN_TYPE",
_get_android_toolchain = "get_android_toolchain",
_utils = "utils",
)
@@ -81,6 +82,7 @@
),
mnemonic = "AarFileExtractor",
progress_message = "Extracting %s from %s" % (filename, aar.basename),
+ toolchain = ANDROID_TOOLCHAIN_TYPE,
)
def _extract_resources(
@@ -100,6 +102,7 @@
outputs = [out_resources_dir, out_assets_dir],
mnemonic = "AarResourcesExtractor",
progress_message = "Extracting resources and assets from %s" % aar.basename,
+ toolchain = None,
)
def _extract_native_libs(
@@ -119,6 +122,7 @@
outputs = [output_zip],
mnemonic = "AarNativeLibsFilter",
progress_message = "Filtering AAR native libs by architecture",
+ toolchain = None,
)
def _process_resources(
@@ -192,6 +196,7 @@
outputs = [out_jars_tree_artifact, out_jars_params_file],
mnemonic = "AarEmbeddedJarsExtractor",
progress_message = "Extracting classes.jar and libs/*.jar from %s" % aar.basename,
+ toolchain = None,
)
def _merge_jars(
@@ -212,6 +217,7 @@
outputs = [out_jar],
mnemonic = "AarJarsMerger",
progress_message = "Merging AAR embedded jars",
+ toolchain = None,
)
def _extract_and_merge_jars(
@@ -383,6 +389,7 @@
outputs = [validation_output],
mnemonic = "ValidateAAR",
progress_message = "Validating aar_import %s" % str(ctx.label),
+ toolchain = None,
)
return validation_output
@@ -431,6 +438,7 @@
outputs = [out_proguard],
mnemonic = "AarEmbeddedProguardExtractor",
progress_message = "Extracting proguard spec from %s" % aar.basename,
+ toolchain = None,
)
transitive_proguard_specs = []
for p in _utils.collect_providers(ProguardSpecProvider, ctx.attr.deps, ctx.attr.exports):
diff --git a/rules/acls.bzl b/rules/acls.bzl
index 4fa01c4..9cd6f3f 100644
--- a/rules/acls.bzl
+++ b/rules/acls.bzl
@@ -42,6 +42,7 @@
load("//rules/acls:android_instrumentation_binary_starlark_resources.bzl", "ANDROID_INSTRUMENTATION_BINARY_STARLARK_RESOURCES_FALLBACK", "ANDROID_INSTRUMENTATION_BINARY_STARLARK_RESOURCES_ROLLOUT")
load("//rules/acls:android_binary_starlark_javac.bzl", "ANDROID_BINARY_STARLARK_JAVAC_FALLBACK", "ANDROID_BINARY_STARLARK_JAVAC_ROLLOUT")
load("//rules/acls:android_binary_starlark_split_transition.bzl", "ANDROID_BINARY_STARLARK_SPLIT_TRANSITION_FALLBACK", "ANDROID_BINARY_STARLARK_SPLIT_TRANSITION_ROLLOUT")
+load("//rules/acls:android_binary_with_sandboxed_sdks_allowlist.bzl", "ANDROID_BINARY_WITH_SANDBOXED_SDKS_ALLOWLIST")
load("//rules/acls:android_feature_splits_dogfood.bzl", "ANDROID_FEATURE_SPLITS_DOGFOOD")
load("//rules/acls:android_library_resources_without_srcs.bzl", "ANDROID_LIBRARY_RESOURCES_WITHOUT_SRCS", "ANDROID_LIBRARY_RESOURCES_WITHOUT_SRCS_GENERATOR_FUNCTIONS")
load("//rules/acls:android_library_starlark_resource_outputs.bzl", "ANDROID_LIBRARY_STARLARK_RESOURCE_OUTPUTS_FALLBACK", "ANDROID_LIBRARY_STARLARK_RESOURCE_OUTPUTS_ROLLOUT")
@@ -78,7 +79,10 @@
load("//rules/acls:shared_library_resource_linking.bzl", "SHARED_LIBRARY_RESOURCE_LINKING_ALLOWLIST")
load("//rules/acls:android_binary_starlark_dex_desugar_proguard.bzl", "ANDROID_BINARY_STARLARK_DEX_DESUGAR_PROGUARD_FALLBACK", "ANDROID_BINARY_STARLARK_DEX_DESUGAR_PROGUARD_ROLLOUT")
load("//rules/acls:android_binary_min_sdk_version_attribute.bzl", "ANDROID_BINARY_MIN_SDK_VERSION_ATTRIBUTE_ALLOWLIST")
+load("//rules/acls:android_binary_raw_access_to_resource_paths_allowlist.bzl", "ANDROID_BINARY_RAW_ACCESS_TO_RESOURCE_PATHS_ALLOWLIST")
+load("//rules/acls:android_binary_resource_name_obfuscation_opt_out_allowlist.bzl", "ANDROID_BINARY_RESOURCE_NAME_OBFUSCATION_OPT_OUT_ALLOWLIST")
load("//rules/acls:proguard_apply_mapping.bzl", "ALLOW_PROGUARD_APPLY_MAPPING")
+load("//rules/acls:r8.bzl", "USE_R8")
def _in_aar_import_deps_checker(fqn):
return not matches(fqn, AAR_IMPORT_DEPS_CHECKER_FALLBACK_DICT) and matches(fqn, AAR_IMPORT_DEPS_CHECKER_ROLLOUT_DICT)
@@ -110,6 +114,9 @@
def _in_android_binary_starlark_split_transition(fqn):
return not matches(fqn, ANDROID_BINARY_STARLARK_SPLIT_TRANSITION_FALLBACK_DICT) and matches(fqn, ANDROID_BINARY_STARLARK_SPLIT_TRANSITION_ROLLOUT_DICT)
+def _in_android_binary_with_sandboxed_sdks_allowlist(fqn):
+ return matches(fqn, ANDROID_BINARY_WITH_SANDBOXED_SDKS_ALLOWLIST_DICT)
+
def _in_android_feature_splits_dogfood(fqn):
return matches(fqn, ANDROID_FEATURE_SPLITS_DOGFOOD_DICT)
@@ -226,9 +233,18 @@
def _in_android_binary_min_sdk_version_attribute_allowlist(fqn):
return matches(fqn, ANDROID_BINARY_MIN_SDK_VERSION_ATTRIBUTE_DICT)
+def _in_android_binary_raw_access_to_resource_paths_allowlist(fqn):
+ return matches(fqn, ANDROID_BINARY_RAW_ACCESS_TO_RESOURCE_PATHS_ALLOWLIST_DICT)
+
+def _in_android_binary_resource_name_obfuscation_opt_out_allowlist(fqn):
+ return matches(fqn, ANDROID_BINARY_RESOURCE_NAME_OBFUSCATION_OPT_OUT_ALLOWLIST_DICT)
+
def _in_allow_proguard_apply_mapping(fqn):
return matches(fqn, ALLOW_PROGUARD_APPLY_MAPPING_DICT)
+def _use_r8(fqn):
+ return matches(fqn, USE_R8_DICT)
+
def make_dict(lst):
"""Do not use this method outside of acls directory."""
return {t: True for t in lst}
@@ -249,6 +265,7 @@
ANDROID_BINARY_STARLARK_JAVAC_FALLBACK_DICT = make_dict(ANDROID_BINARY_STARLARK_JAVAC_FALLBACK)
ANDROID_BINARY_STARLARK_SPLIT_TRANSITION_ROLLOUT_DICT = make_dict(ANDROID_BINARY_STARLARK_SPLIT_TRANSITION_ROLLOUT)
ANDROID_BINARY_STARLARK_SPLIT_TRANSITION_FALLBACK_DICT = make_dict(ANDROID_BINARY_STARLARK_SPLIT_TRANSITION_FALLBACK)
+ANDROID_BINARY_WITH_SANDBOXED_SDKS_ALLOWLIST_DICT = make_dict(ANDROID_BINARY_WITH_SANDBOXED_SDKS_ALLOWLIST)
ANDROID_FEATURE_SPLITS_DOGFOOD_DICT = make_dict(ANDROID_FEATURE_SPLITS_DOGFOOD)
ANDROID_LIBRARY_RESOURCES_WITHOUT_SRCS_DICT = make_dict(ANDROID_LIBRARY_RESOURCES_WITHOUT_SRCS)
ANDROID_LIBRARY_RESOURCES_WITHOUT_SRCS_GENERATOR_FUNCTIONS_DICT = make_dict(ANDROID_LIBRARY_RESOURCES_WITHOUT_SRCS_GENERATOR_FUNCTIONS)
@@ -307,7 +324,10 @@
ANDROID_BINARY_STARLARK_DEX_DESUGAR_PROGUARD_ROLLOUT_DICT = make_dict(ANDROID_BINARY_STARLARK_DEX_DESUGAR_PROGUARD_ROLLOUT)
ANDROID_BINARY_STARLARK_DEX_DESUGAR_PROGUARD_FALLBACK_DICT = make_dict(ANDROID_BINARY_STARLARK_DEX_DESUGAR_PROGUARD_FALLBACK)
ANDROID_BINARY_MIN_SDK_VERSION_ATTRIBUTE_DICT = make_dict(ANDROID_BINARY_MIN_SDK_VERSION_ATTRIBUTE_ALLOWLIST)
+ANDROID_BINARY_RAW_ACCESS_TO_RESOURCE_PATHS_ALLOWLIST_DICT = make_dict(ANDROID_BINARY_RAW_ACCESS_TO_RESOURCE_PATHS_ALLOWLIST)
+ANDROID_BINARY_RESOURCE_NAME_OBFUSCATION_OPT_OUT_ALLOWLIST_DICT = make_dict(ANDROID_BINARY_RESOURCE_NAME_OBFUSCATION_OPT_OUT_ALLOWLIST)
ALLOW_PROGUARD_APPLY_MAPPING_DICT = make_dict(ALLOW_PROGUARD_APPLY_MAPPING)
+USE_R8_DICT = make_dict(USE_R8)
def matches(fqn, dct):
# Labels with workspace names ("@workspace//pkg:target") are not supported.
@@ -362,6 +382,7 @@
in_android_instrumentation_binary_starlark_resources = _in_android_instrumentation_binary_starlark_resources,
in_android_binary_starlark_javac = _in_android_binary_starlark_javac,
in_android_binary_starlark_split_transition = _in_android_binary_starlark_split_transition,
+ in_android_binary_with_sandboxed_sdks_allowlist = _in_android_binary_with_sandboxed_sdks_allowlist,
in_android_feature_splits_dogfood = _in_android_feature_splits_dogfood,
in_android_library_starlark_resource_outputs_rollout = _in_android_library_starlark_resource_outputs_rollout,
in_android_library_resources_without_srcs = _in_android_library_resources_without_srcs,
@@ -396,7 +417,10 @@
in_shared_library_resource_linking_allowlist = _in_shared_library_resource_linking_allowlist,
in_android_binary_starlark_dex_desugar_proguard = _in_android_binary_starlark_dex_desugar_proguard,
in_android_binary_min_sdk_version_attribute_allowlist = _in_android_binary_min_sdk_version_attribute_allowlist,
+ in_android_binary_raw_access_to_resource_paths_allowlist = _in_android_binary_raw_access_to_resource_paths_allowlist,
+ in_android_binary_resource_name_obfuscation_opt_out_allowlist = _in_android_binary_resource_name_obfuscation_opt_out_allowlist,
in_allow_proguard_apply_mapping = _in_allow_proguard_apply_mapping,
+ use_r8 = _use_r8,
)
# Visible for testing
diff --git a/rules/acls/android_binary_raw_access_to_resource_paths_allowlist.bzl b/rules/acls/android_binary_raw_access_to_resource_paths_allowlist.bzl
new file mode 100644
index 0000000..3829883
--- /dev/null
+++ b/rules/acls/android_binary_raw_access_to_resource_paths_allowlist.bzl
@@ -0,0 +1,17 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""Allow list for android_binary targets with raw access to resource paths in the APK ."""
+ANDROID_BINARY_RAW_ACCESS_TO_RESOURCE_PATHS_ALLOWLIST = [
+]
diff --git a/rules/acls/android_binary_resource_name_obfuscation_opt_out_allowlist.bzl b/rules/acls/android_binary_resource_name_obfuscation_opt_out_allowlist.bzl
new file mode 100644
index 0000000..aa5fb0b
--- /dev/null
+++ b/rules/acls/android_binary_resource_name_obfuscation_opt_out_allowlist.bzl
@@ -0,0 +1,16 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""Allow list of android_binary targets that need to opt out of the AAPT2 resource name obfuscation optimization."""
+ANDROID_BINARY_RESOURCE_NAME_OBFUSCATION_OPT_OUT_ALLOWLIST = []
diff --git a/rules/acls/android_binary_starlark_dex_desugar_proguard.bzl b/rules/acls/android_binary_starlark_dex_desugar_proguard.bzl
index 1f7ec8b..ade3550 100644
--- a/rules/acls/android_binary_starlark_dex_desugar_proguard.bzl
+++ b/rules/acls/android_binary_starlark_dex_desugar_proguard.bzl
@@ -16,8 +16,9 @@
# keep sorted
ANDROID_BINARY_STARLARK_DEX_DESUGAR_PROGUARD_ROLLOUT = [
- "//test/rules/android_binary_internal:__subpackages__",
+ "//:__subpackages__",
]
# keep sorted
-ANDROID_BINARY_STARLARK_DEX_DESUGAR_PROGUARD_FALLBACK = []
+ANDROID_BINARY_STARLARK_DEX_DESUGAR_PROGUARD_FALLBACK = [
+]
diff --git a/rules/acls/android_binary_with_sandboxed_sdks_allowlist.bzl b/rules/acls/android_binary_with_sandboxed_sdks_allowlist.bzl
new file mode 100644
index 0000000..b926437
--- /dev/null
+++ b/rules/acls/android_binary_with_sandboxed_sdks_allowlist.bzl
@@ -0,0 +1,20 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""Allow list of android_binary_with_sandboxed_sdks rule."""
+
+# keep sorted
+ANDROID_BINARY_WITH_SANDBOXED_SDKS_ALLOWLIST = [
+ "//:__subpackages__",
+]
diff --git a/rules/acls/r8.bzl b/rules/acls/r8.bzl
new file mode 100644
index 0000000..bbc75ac
--- /dev/null
+++ b/rules/acls/r8.bzl
@@ -0,0 +1,20 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""Allowlist for R8"""
+
+# keep sorted
+USE_R8 = [
+ "//:__subpackages__",
+]
diff --git a/rules/android_application/BUILD b/rules/android_application/BUILD
index 47e783a..dadeefb 100644
--- a/rules/android_application/BUILD
+++ b/rules/android_application/BUILD
@@ -1,5 +1,6 @@
# The android_application rule.
+load("@rules_python//python:defs.bzl", "py_binary")
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
licenses(["notice"])
@@ -43,4 +44,3 @@
output_group = "python_zip_file",
visibility = ["//visibility:public"],
)
-
diff --git a/rules/android_binary.bzl b/rules/android_binary.bzl
index be9e7b0..b754e61 100644
--- a/rules/android_binary.bzl
+++ b/rules/android_binary.bzl
@@ -17,6 +17,7 @@
load(":common.bzl", "common")
load(":migration_tag_DONOTUSE.bzl", "add_migration_tag")
load("//rules/android_binary_internal:rule.bzl", "android_binary_internal_macro")
+load("//rules:acls.bzl", "acls")
def android_binary(**attrs):
"""Bazel android_binary rule.
@@ -37,9 +38,22 @@
attrs.pop("$enable_manifest_merging", None)
+ # dex_shards is deprecated and unused. This only existed for mobile-install classic which has
+ # been replaced by mobile-install v2
+ attrs.pop("dex_shards", None)
+
# resource_apks is not used by the native android_binary
attrs.pop("resource_apks", None)
+ fqn = "//%s:%s" % (native.package_name(), attrs["name"])
+ if acls.use_r8(fqn):
+ # Do not pass proguard specs to the native android_binary so that it does
+ # not try to use proguard and instead uses the dex files from the
+ # AndroidDexInfo provider from android_binary_internal.
+ # This also disables resource shrinking from native android_binary (reguardless of the
+ # shrink_resources attr).
+ attrs["proguard_specs"] = []
+
native.android_binary(
application_resources = android_binary_internal_name,
**add_migration_tag(attrs)
diff --git a/rules/android_binary_internal/attrs.bzl b/rules/android_binary_internal/attrs.bzl
index 2a3e614..0fadb06 100644
--- a/rules/android_binary_internal/attrs.bzl
+++ b/rules/android_binary_internal/attrs.bzl
@@ -103,6 +103,9 @@
incremental_dexing = _attrs.tristate.create(
default = _attrs.tristate.auto,
),
+ proguard_generate_mapping = attr.bool(default = False),
+ proguard_optimization_passes = attr.int(),
+ proguard_apply_mapping = attr.label(allow_single_file = True),
_java_toolchain = attr.label(
default = Label("//tools/jdk:toolchain_android_only"),
),
@@ -113,10 +116,31 @@
default = "@bazel_tools//tools/cpp:current_cc_toolchain",
aspects = [split_config_aspect],
),
+ _optimizing_dexer = attr.label(
+ cfg = "exec",
+ allow_single_file = True,
+ default = configuration_field(
+ fragment = "android",
+ name = "optimizing_dexer",
+ ),
+ ),
+ _desugared_java8_legacy_apis = attr.label(
+ default = Label("//tools/android:desugared_java8_legacy_apis"),
+ allow_single_file = True,
+ ),
+ _bytecode_optimizer = attr.label(
+ default = configuration_field(
+ fragment = "java",
+ name = "bytecode_optimizer",
+ ),
+ cfg = "exec",
+ executable = True,
+ ),
),
_attrs.COMPILATION,
_attrs.DATA_CONTEXT,
_attrs.ANDROID_TOOLCHAIN_ATTRS,
+ _attrs.AUTOMATIC_EXEC_GROUPS_ENABLED,
),
# TODO(b/167599192): don't override manifest attr to remove .xml file restriction.
manifest = attr.label(
diff --git a/rules/android_binary_internal/impl.bzl b/rules/android_binary_internal/impl.bzl
index ef77e2a..23de739 100644
--- a/rules/android_binary_internal/impl.bzl
+++ b/rules/android_binary_internal/impl.bzl
@@ -14,18 +14,27 @@
"""Implementation."""
+load(":r8.bzl", "process_r8", "process_resource_shrinking_r8")
load("//rules:acls.bzl", "acls")
load("//rules:baseline_profiles.bzl", _baseline_profiles = "baseline_profiles")
load("//rules:common.bzl", "common")
load("//rules:data_binding.bzl", "data_binding")
load("//rules:java.bzl", "java")
+load("//rules:proguard.bzl", "proguard", proguard_testing = "testing")
load(
"//rules:processing_pipeline.bzl",
"ProviderInfo",
"processing_pipeline",
)
load("//rules:resources.bzl", _resources = "resources")
-load("//rules:utils.bzl", "compilation_mode", "get_android_toolchain", "utils")
+load(
+ "//rules:utils.bzl",
+ "ANDROID_TOOLCHAIN_TYPE",
+ "compilation_mode",
+ "get_android_sdk",
+ "get_android_toolchain",
+ "utils",
+)
load(
"//rules:native_deps.bzl",
_process_native_deps = "process",
@@ -76,12 +85,15 @@
resource_apks = resource_apks,
instruments = ctx.attr.instruments,
aapt = get_android_toolchain(ctx).aapt2.files_to_run,
- android_jar = ctx.attr._android_sdk[AndroidSdkInfo].android_jar,
+ android_jar = get_android_sdk(ctx).android_jar,
legacy_merger = ctx.attr._android_manifest_merge_tool.files_to_run,
xsltproc = ctx.attr._xsltproc_tool.files_to_run,
instrument_xslt = ctx.file._add_g3itr_xslt,
busybox = get_android_toolchain(ctx).android_resources_busybox.files_to_run,
host_javabase = ctx.attr._host_javabase,
+ # The AndroidApplicationResourceInfo will be added to the list of providers in finalize()
+ # if R8-based resource shrinking is not performed.
+ add_application_resource_info_to_providers = False,
)
return ProviderInfo(
name = "packaged_resources_ctx",
@@ -124,6 +136,15 @@
),
)
+def _process_proto(_unused_ctx, **_unused_ctxs):
+ return ProviderInfo(
+ name = "proto_ctx",
+ value = struct(
+ providers = [],
+ class_jar = None,
+ ),
+ )
+
def _process_data_binding(ctx, java_package, packaged_resources_ctx, **_unused_ctxs):
if ctx.attr.enable_data_binding and not acls.in_databinding_allowed(str(ctx.label)):
fail("This target is not allowed to use databinding and enable_data_binding is True.")
@@ -194,7 +215,7 @@
),
)
-def _process_dex(ctx, stamp_ctx, packaged_resources_ctx, jvm_ctx, deploy_ctx, **_unused_ctxs):
+def _process_dex(ctx, stamp_ctx, packaged_resources_ctx, jvm_ctx, proto_ctx, deploy_ctx, **_unused_ctxs):
providers = []
classes_dex_zip = None
dex_info = None
@@ -205,10 +226,12 @@
if acls.in_android_binary_starlark_dex_desugar_proguard(str(ctx.label)):
java_info = java_common.merge([jvm_ctx.java_info, stamp_ctx.java_info]) if stamp_ctx.java_info else jvm_ctx.java_info
runtime_jars = java_info.runtime_output_jars + [packaged_resources_ctx.class_jar]
+ if proto_ctx.class_jar:
+ runtime_jars.append(proto_ctx.class_jar)
forbidden_dexopts = ctx.fragments.android.get_target_dexopts_that_prevent_incremental_dexing
java8_legacy_dex, java8_legacy_dex_map = _dex.get_java8_legacy_dex_and_map(
ctx,
- android_jar = ctx.attr._android_sdk[AndroidSdkInfo].android_jar,
+ android_jar = get_android_sdk(ctx).android_jar,
binary_jar = deploy_jar,
build_customized_files = is_binary_optimized,
)
@@ -235,6 +258,7 @@
desugar_dict = deploy_ctx.desugar_dict,
dexbuilder = get_android_toolchain(ctx).dexbuilder.files_to_run,
dexmerger = get_android_toolchain(ctx).dexmerger.files_to_run,
+ toolchain_type = ANDROID_TOOLCHAIN_TYPE,
)
if ctx.fragments.android.desugar_java8_libs and classes_dex_zip.extension == "zip":
@@ -264,7 +288,7 @@
),
)
-def _process_deploy_jar(ctx, stamp_ctx, packaged_resources_ctx, jvm_ctx, build_info_ctx, **_unused_ctxs):
+def _process_deploy_jar(ctx, stamp_ctx, packaged_resources_ctx, jvm_ctx, build_info_ctx, proto_ctx, **_unused_ctxs):
deploy_jar, desugar_dict = None, {}
if acls.in_android_binary_starlark_dex_desugar_proguard(str(ctx.label)):
@@ -274,6 +298,9 @@
incremental_dexopts = _dex.incremental_dexopts(ctx.attr.dexopts, ctx.fragments.android.get_dexopts_supported_in_incremental_dexing)
dex_archives = info.dex_archives_dict.get("".join(incremental_dexopts), depset()).to_list()
binary_runtime_jars = java_info.runtime_output_jars + [packaged_resources_ctx.class_jar]
+ if proto_ctx.class_jar:
+ binary_runtime_jars.append(proto_ctx.class_jar)
+
if ctx.fragments.android.desugar_java8:
desugared_jars = []
desugar_dict = {d.jar: d.desugared_jar for d in dex_archives}
@@ -288,6 +315,7 @@
bootclasspath = java_toolchain[java_common.JavaToolchainInfo].bootclasspath.to_list(),
min_sdk_version = ctx.attr.min_sdk_version,
desugar_exec = get_android_toolchain(ctx).desugar.files_to_run,
+ toolchain_type = ANDROID_TOOLCHAIN_TYPE,
)
desugared_jars.append(desugared_jar)
desugar_dict[jar] = desugared_jar
@@ -298,7 +326,7 @@
runtime_jars = depset(desugared_jars)
else:
- runtime_jars = depset(binary_runtime_jars, transitive = [java_info.transitive_runtime_jar])
+ runtime_jars = depset(binary_runtime_jars, transitive = [java_info.transitive_runtime_jars])
output = ctx.actions.declare_file(ctx.label.name + "_migrated_deploy.jar")
deploy_jar = java.create_deploy_jar(
@@ -310,6 +338,21 @@
deploy_manifest_lines = build_info_ctx.deploy_manifest_lines,
)
+ if _is_instrumentation(ctx):
+ filtered_deploy_jar = ctx.actions.declare_file(ctx.label.name + "_migrated_filtered.jar")
+ filter_jar = ctx.attr.instruments[AndroidPreDexJarInfo].pre_dex_jar
+ common.filter_zip_exclude(
+ ctx,
+ output = filtered_deploy_jar,
+ input = deploy_jar,
+ filter_zips = [filter_jar],
+ filter_types = [".class"],
+ # These files are generated by databinding in both the target and the instrumentation
+ # app with different contents. We want to keep the one from the target app.
+ filters = ["/BR\\.class$", "/databinding/[^/]+Binding\\.class$"],
+ )
+ deploy_jar = filtered_deploy_jar
+
return ProviderInfo(
name = "deploy_ctx",
value = struct(
@@ -338,12 +381,43 @@
return manifest_merger == "legacy"
-def finalize(ctx, providers, validation_outputs, **unused_ctxs):
+def finalize(
+ _unused_ctx,
+ providers,
+ validation_outputs,
+ packaged_resources_ctx,
+ resource_shrinking_r8_ctx,
+ **_unused_ctxs):
+ """Final step of the android_binary_internal processor pipeline.
+
+ Args:
+ _unused_ctx: The context.
+ providers: The list of providers for the android_binary_internal rule.
+ validation_outputs: Validation outputs for the rule.
+ packaged_resources_ctx: The packaged resources from the resource processing step.
+ resource_shrinking_r8_ctx: The context from the R8 resource shrinking step.
+ **_unused_ctxs: Other contexts.
+
+ Returns:
+ The list of providers the android_binary_internal rule should return.
+ """
providers.append(
OutputGroupInfo(
_validation = depset(validation_outputs),
),
)
+
+ # Add the AndroidApplicationResourceInfo provider from resource shrinking if it was performed.
+ # TODO(ahumesky): This can be cleaned up after the rules are fully migrated to Starlark.
+ # Packaging will be the final step in the pipeline, and that step can be responsible for picking
+ # between the two different contexts. Then this finalize can return back to its "simple" form.
+ if resource_shrinking_r8_ctx.android_application_resource_info_with_shrunk_resource_apk:
+ providers.append(
+ resource_shrinking_r8_ctx.android_application_resource_info_with_shrunk_resource_apk,
+ )
+ else:
+ providers.append(packaged_resources_ctx.android_application_resource)
+
return providers
def _is_test_binary(ctx):
@@ -355,7 +429,19 @@
Returns:
Boolean indicating whether the target is a test target.
"""
- return ctx.attr.testonly or ctx.attr.instruments or str(ctx.label).find("/javatests/") >= 0
+ return ctx.attr.testonly or _is_instrumentation(ctx) or str(ctx.label).find("/javatests/") >= 0
+
+def _is_instrumentation(ctx):
+ """Whether this android_binary target is an instrumentation binary.
+
+ Args:
+ ctx: The context.
+
+ Returns:
+ Boolean indicating whether the target is an instrumentation target.
+
+ """
+ return bool(ctx.attr.instruments)
def _process_baseline_profiles(ctx, dex_ctx, **_unused_ctxs):
providers = []
@@ -383,6 +469,92 @@
value = struct(providers = providers),
)
+def _process_optimize(ctx, deploy_ctx, packaged_resources_ctx, **_unused_ctxs):
+ if not acls.in_android_binary_starlark_dex_desugar_proguard(str(ctx.label)):
+ return ProviderInfo(
+ name = "optimize_ctx",
+ value = struct(),
+ )
+
+ # Validate attributes and lockdown lists
+ if ctx.file.proguard_apply_mapping and not acls.in_allow_proguard_apply_mapping(ctx.label):
+ fail("proguard_apply_mapping is not supported")
+ if ctx.file.proguard_apply_mapping and not ctx.files.proguard_specs:
+ fail("proguard_apply_mapping can only be used when proguard_specs is set")
+
+ proguard_specs = proguard.get_proguard_specs(
+ ctx,
+ packaged_resources_ctx.resource_proguard_config,
+ proguard_specs_for_manifest = [packaged_resources_ctx.resource_minsdk_proguard_config] if packaged_resources_ctx.resource_minsdk_proguard_config else [],
+ )
+ has_proguard_specs = bool(proguard_specs)
+ proguard_output = struct()
+
+ proguard_output_map = None
+ generate_proguard_map = (
+ ctx.attr.proguard_generate_mapping or
+ _resources.is_resource_shrinking_enabled(
+ ctx.attr.shrink_resources,
+ ctx.fragments.android.use_android_resource_shrinking,
+ )
+ )
+ desugar_java8_libs_generates_map = ctx.fragments.android.desugar_java8
+ optimizing_dexing = bool(ctx.attr._optimizing_dexer)
+
+ # TODO(b/261110876): potentially add codepaths below to support rex (postprocessingRewritesMap)
+ if generate_proguard_map:
+ # Determine the output of the Proguard map from shrinking the app. This depends on the
+ # additional steps which can process the map before the final Proguard map artifact is
+ # generated.
+ if not has_proguard_specs:
+ # When no shrinking happens a generating rule for the output map artifact is still needed.
+ proguard_output_map = proguard.get_proguard_output_map(ctx)
+ elif optimizing_dexing:
+ proguard_output_map = proguard.get_proguard_temp_artifact(ctx, "pre_dexing.map")
+ elif desugar_java8_libs_generates_map:
+ # Proguard map from shrinking will be merged with desugared library proguard map.
+ proguard_output_map = _dex.get_dx_artifact(ctx, "_proguard_output_for_desugared_library.map")
+ else:
+ # Proguard map from shrinking is the final output.
+ proguard_output_map = proguard.get_proguard_output_map(ctx)
+
+ proguard_output_jar = ctx.actions.declare_file(ctx.label.name + "_migrated_proguard.jar")
+ proguard_seeds = ctx.actions.declare_file(ctx.label.name + "_migrated_proguard.seeds")
+ proguard_usage = ctx.actions.declare_file(ctx.label.name + "_migrated_proguard.usage")
+
+ proguard_output = proguard.apply_proguard(
+ ctx,
+ input_jar = deploy_ctx.deploy_jar,
+ proguard_specs = proguard_specs,
+ proguard_optimization_passes = getattr(ctx.attr, "proguard_optimization_passes", None),
+ proguard_output_jar = proguard_output_jar,
+ proguard_mapping = ctx.file.proguard_apply_mapping,
+ proguard_output_map = proguard_output_map,
+ proguard_seeds = proguard_seeds,
+ proguard_usage = proguard_usage,
+ proguard_tool = get_android_sdk(ctx).proguard,
+ )
+
+ providers = []
+ if proguard_output:
+ providers.append(proguard_testing.ProguardOutputInfo(
+ input_jar = deploy_ctx.deploy_jar,
+ output_jar = proguard_output.output_jar,
+ mapping = proguard_output.mapping,
+ seeds = proguard_output.seeds,
+ usage = proguard_output.usage,
+ library_jar = proguard_output.library_jar,
+ config = proguard_output.config,
+ ))
+
+ return ProviderInfo(
+ name = "optimize_ctx",
+ value = struct(
+ proguard_output = proguard_output,
+ providers = providers,
+ ),
+ )
+
# Order dependent, as providers will not be available to downstream processors
# that may depend on the provider. Iteration order for a dictionary is based on
# insertion.
@@ -396,9 +568,13 @@
DataBindingProcessor = _process_data_binding,
JvmProcessor = _process_jvm,
BuildInfoProcessor = _process_build_info,
+ ProtoProcessor = _process_proto,
DeployJarProcessor = _process_deploy_jar,
+ OptimizeProcessor = _process_optimize,
DexProcessor = _process_dex,
BaselineProfilesProcessor = _process_baseline_profiles,
+ R8Processor = process_r8,
+ ResourecShrinkerR8Processor = process_resource_shrinking_r8,
)
_PROCESSING_PIPELINE = processing_pipeline.make_processing_pipeline(
diff --git a/rules/android_binary_internal/r8.bzl b/rules/android_binary_internal/r8.bzl
new file mode 100644
index 0000000..a4be61d
--- /dev/null
+++ b/rules/android_binary_internal/r8.bzl
@@ -0,0 +1,211 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""R8 processor steps for android_binary_internal."""
+
+load("//rules:acls.bzl", "acls")
+load("//rules:proguard.bzl", "proguard")
+load(
+ "//rules:utils.bzl",
+ "ANDROID_TOOLCHAIN_TYPE",
+ "get_android_sdk",
+ "get_android_toolchain",
+)
+load(
+ "//rules:processing_pipeline.bzl",
+ "ProviderInfo",
+)
+load("//rules:common.bzl", "common")
+load("//rules:java.bzl", "java")
+load("//rules:resources.bzl", _resources = "resources")
+
+def process_r8(ctx, jvm_ctx, packaged_resources_ctx, build_info_ctx, **_unused_ctxs):
+ """Runs R8 for desugaring, optimization, and dexing.
+
+ Args:
+ ctx: Rule contxt.
+ jvm_ctx: Context from the java processor.
+ packaged_resources_ctx: Context from resource processing.
+ build_info_ctx: Context from build info processor.
+ **_unused_ctxs: Unused context.
+
+ Returns:
+ The r8_ctx ProviderInfo.
+ """
+ local_proguard_specs = ctx.files.proguard_specs
+ if not acls.use_r8(str(ctx.label)) or not local_proguard_specs:
+ return ProviderInfo(
+ name = "r8_ctx",
+ value = struct(
+ providers = [],
+ ),
+ )
+
+ # The R8 processor step creates its own deploy jar instead of
+ # The deploy jar from the deploy_jar processor is not used because as of now, whether it
+ # actually produces a deploy jar is determinted by a separate set of ACLs, and also does
+ # desugaring differently than with R8.
+ deploy_jar = ctx.actions.declare_file(ctx.label.name + "_deploy.jar")
+ java.create_deploy_jar(
+ ctx,
+ output = deploy_jar,
+ runtime_jars = depset(
+ direct = jvm_ctx.java_info.runtime_output_jars + [packaged_resources_ctx.class_jar],
+ transitive = [jvm_ctx.java_info.transitive_runtime_jars],
+ ),
+ java_toolchain = common.get_java_toolchain(ctx),
+ build_target = ctx.label.name,
+ deploy_manifest_lines = build_info_ctx.deploy_manifest_lines,
+ )
+
+ dexes_zip = ctx.actions.declare_file(ctx.label.name + "_dexes.zip")
+
+ android_jar = get_android_sdk(ctx).android_jar
+ proguard_specs = proguard.get_proguard_specs(ctx, packaged_resources_ctx.resource_proguard_config)
+
+ args = ctx.actions.args()
+ args.add("--release")
+ args.add("--output", dexes_zip)
+ args.add_all(proguard_specs, before_each = "--pg-conf")
+ args.add("--lib", android_jar)
+ args.add(deploy_jar) # jar to optimize + desugar + dex
+
+ java.run(
+ ctx = ctx,
+ host_javabase = common.get_host_javabase(ctx),
+ executable = get_android_toolchain(ctx).r8.files_to_run,
+ arguments = [args],
+ inputs = [android_jar, deploy_jar] + proguard_specs,
+ outputs = [dexes_zip],
+ mnemonic = "AndroidR8",
+ progress_message = "R8 Optimizing, Desugaring, and Dexing %{label}",
+ )
+
+ android_dex_info = AndroidDexInfo(
+ deploy_jar = deploy_jar,
+ final_classes_dex_zip = dexes_zip,
+ # R8 preserves the Java resources (i.e. non-Java-class files) in its output zip, so no need
+ # to provide a Java resources zip.
+ java_resource_jar = None,
+ )
+
+ return ProviderInfo(
+ name = "r8_ctx",
+ value = struct(
+ final_classes_dex_zip = dexes_zip,
+ providers = [android_dex_info],
+ ),
+ )
+
+def process_resource_shrinking_r8(ctx, r8_ctx, packaged_resources_ctx, **_unused_ctxs):
+ """Runs resource shrinking.
+
+ Args:
+ ctx: Rule contxt.
+ r8_ctx: Context from the R8 processor.
+ packaged_resources_ctx: Context from resource processing.
+ **_unused_ctxs: Unused context.
+
+ Returns:
+ The r8_ctx ProviderInfo.
+ """
+ local_proguard_specs = ctx.files.proguard_specs
+ if (not acls.use_r8(str(ctx.label)) or
+ not local_proguard_specs or
+ not _resources.is_resource_shrinking_enabled(
+ ctx.attr.shrink_resources,
+ ctx.fragments.android.use_android_resource_shrinking,
+ )):
+ return ProviderInfo(
+ name = "resource_shrinking_r8_ctx",
+ value = struct(
+ android_application_resource_info_with_shrunk_resource_apk = None,
+ providers = [],
+ ),
+ )
+
+ android_toolchain = get_android_toolchain(ctx)
+
+ # 1. Convert the resource APK to proto format (resource shrinker operates on a proto apk)
+ proto_resource_apk = ctx.actions.declare_file(ctx.label.name + "_proto_resource_apk.ap_")
+ ctx.actions.run(
+ arguments = [ctx.actions.args()
+ .add("convert")
+ .add(packaged_resources_ctx.resources_apk) # input apk
+ .add("-o", proto_resource_apk) # output apk
+ .add("--output-format", "proto")],
+ executable = android_toolchain.aapt2.files_to_run,
+ inputs = [packaged_resources_ctx.resources_apk],
+ mnemonic = "Aapt2ConvertToProtoForResourceShrinkerR8",
+ outputs = [proto_resource_apk],
+ toolchain = ANDROID_TOOLCHAIN_TYPE,
+ )
+
+ # 2. Run the resource shrinker
+ proto_resource_apk_shrunk = ctx.actions.declare_file(
+ ctx.label.name + "_proto_resource_apk_shrunk.ap_",
+ )
+ java.run(
+ ctx = ctx,
+ host_javabase = common.get_host_javabase(ctx),
+ executable = android_toolchain.resource_shrinker.files_to_run,
+ arguments = [ctx.actions.args()
+ .add("--input", proto_resource_apk)
+ .add("--dex_input", r8_ctx.final_classes_dex_zip)
+ .add("--output", proto_resource_apk_shrunk)],
+ inputs = [proto_resource_apk, r8_ctx.final_classes_dex_zip],
+ outputs = [proto_resource_apk_shrunk],
+ mnemonic = "ResourceShrinkerForR8",
+ progress_message = "Shrinking resources %{label}",
+ )
+
+ # 3. Convert back to a binary APK
+ resource_apk_shrunk = ctx.actions.declare_file(ctx.label.name + "_resource_apk_shrunk.ap_")
+ ctx.actions.run(
+ arguments = [ctx.actions.args()
+ .add("convert")
+ .add(proto_resource_apk_shrunk) # input apk
+ .add("-o", resource_apk_shrunk) # output apk
+ .add("--output-format", "binary")],
+ executable = android_toolchain.aapt2.files_to_run,
+ inputs = [proto_resource_apk_shrunk],
+ mnemonic = "Aapt2ConvertBackToBinaryForResourceShrinkerR8",
+ outputs = [resource_apk_shrunk],
+ toolchain = ANDROID_TOOLCHAIN_TYPE,
+ )
+
+ aari = packaged_resources_ctx.android_application_resource
+
+ # Replace the resource apk in the AndroidApplicationResourceInfo provider from resource
+ # processing.
+ new_aari = AndroidApplicationResourceInfo(
+ resource_apk = resource_apk_shrunk,
+ resource_java_src_jar = aari.resource_java_src_jar,
+ resource_java_class_jar = aari.resource_java_class_jar,
+ manifest = aari.manifest,
+ resource_proguard_config = aari.resource_proguard_config,
+ main_dex_proguard_config = aari.main_dex_proguard_config,
+ r_txt = aari.r_txt,
+ resources_zip = aari.resources_zip,
+ databinding_info = aari.databinding_info,
+ should_compile_java_srcs = aari.should_compile_java_srcs,
+ )
+
+ return ProviderInfo(
+ name = "resource_shrinking_r8_ctx",
+ value = struct(
+ android_application_resource_info_with_shrunk_resource_apk = new_aari,
+ providers = [],
+ ),
+ )
diff --git a/rules/android_binary_internal/rule.bzl b/rules/android_binary_internal/rule.bzl
index 3ed451a..39f498c 100644
--- a/rules/android_binary_internal/rule.bzl
+++ b/rules/android_binary_internal/rule.bzl
@@ -21,7 +21,7 @@
_attrs = "attrs",
)
-_DEFAULT_ALLOWED_ATTRS = ["name", "visibility", "tags", "testonly", "transitive_configs", "$enable_manifest_merging", "features"]
+_DEFAULT_ALLOWED_ATTRS = ["name", "visibility", "tags", "testonly", "transitive_configs", "$enable_manifest_merging", "features", "exec_properties"]
_DEFAULT_PROVIDES = [AndroidApplicationResourceInfo, OutputGroupInfo]
@@ -47,6 +47,7 @@
provides = provides,
toolchains = [
"//toolchains/android:toolchain_type",
+ "//toolchains/android_sdk:toolchain_type",
"@bazel_tools//tools/jdk:toolchain_type",
] + additional_toolchains,
_skylark_testable = True,
diff --git a/rules/android_library/BUILD b/rules/android_library/BUILD
index 4eaeaf3..2aae34b 100644
--- a/rules/android_library/BUILD
+++ b/rules/android_library/BUILD
@@ -14,6 +14,7 @@
bzl_library(
name = "bzl",
srcs = glob(["*.bzl"]),
+ visibility = ["//rules:__pkg__"],
deps = [
"//rules:common_bzl",
"//rules/flags:bzl",
diff --git a/rules/android_library/attrs.bzl b/rules/android_library/attrs.bzl
index 8102aa1..651e68d 100644
--- a/rules/android_library/attrs.bzl
+++ b/rules/android_library/attrs.bzl
@@ -233,4 +233,5 @@
_attrs.COMPILATION,
_attrs.DATA_CONTEXT,
_attrs.ANDROID_TOOLCHAIN_ATTRS,
+ _attrs.AUTOMATIC_EXEC_GROUPS_ENABLED,
)
diff --git a/rules/android_neverlink_aspect.bzl b/rules/android_neverlink_aspect.bzl
new file mode 100644
index 0000000..1dab304
--- /dev/null
+++ b/rules/android_neverlink_aspect.bzl
@@ -0,0 +1,69 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""Aspect to collect neverlink libraries in the transitive closure.
+
+Used for determining the -libraryjars argument for Proguard. The compile-time classpath is
+unsufficient here as those are ijars.
+"""
+
+load(
+ "//rules:utils.bzl",
+ "utils",
+)
+
+StarlarkAndroidNeverlinkInfo = provider(
+ doc = "Contains all neverlink libraries in the transitive closure.",
+ fields = {
+ "transitive_neverlink_libraries": "Depset of transitive neverlink jars",
+ },
+)
+
+_ATTRS = ["deps", "exports", "runtime_deps", "binary_under_test", "$instrumentation_test_runner"]
+
+def _android_neverlink_aspect_impl(target, ctx):
+ # Only run on Android targets
+ if "android" not in getattr(ctx.rule.attr, "constraints", "") and not ctx.rule.kind.startswith("android_"):
+ return []
+
+ deps = []
+ for attr in _ATTRS:
+ if type(getattr(ctx.rule.attr, attr, None)) == "list":
+ deps.extend(getattr(ctx.rule.attr, attr))
+
+ direct_runtime_jars = depset(
+ target[JavaInfo].runtime_output_jars,
+ transitive = [target[AndroidLibraryResourceClassJarProvider].jars] if AndroidLibraryResourceClassJarProvider in target else [],
+ )
+
+ neverlink_libs = _collect_transitive_neverlink_libs(ctx, deps, direct_runtime_jars)
+
+ return [StarlarkAndroidNeverlinkInfo(transitive_neverlink_libraries = neverlink_libs)]
+
+def _collect_transitive_neverlink_libs(ctx, deps, runtime_jars):
+ neverlink_runtime_jars = []
+ for provider in utils.collect_providers(StarlarkAndroidNeverlinkInfo, deps):
+ neverlink_runtime_jars.append(provider.transitive_neverlink_libraries)
+
+ if getattr(ctx.rule.attr, "neverlink", False):
+ neverlink_runtime_jars.append(runtime_jars)
+ for java_info in utils.collect_providers(JavaInfo, deps):
+ neverlink_runtime_jars.append(java_info.transitive_runtime_jars)
+
+ return depset([], transitive = neverlink_runtime_jars)
+
+android_neverlink_aspect = aspect(
+ implementation = _android_neverlink_aspect_impl,
+ attr_aspects = _ATTRS,
+)
diff --git a/rules/android_sandboxed_sdk/BUILD b/rules/android_sandboxed_sdk/BUILD
new file mode 100644
index 0000000..f204a07
--- /dev/null
+++ b/rules/android_sandboxed_sdk/BUILD
@@ -0,0 +1,25 @@
+# Android Sandboxed SDK rules.
+
+load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+
+licenses(["notice"])
+
+exports_files([
+ "android_sandboxed_sdk.bzl",
+ "android_sandboxed_sdk_bundle.bzl",
+])
+
+filegroup(
+ name = "all_files",
+ srcs = glob(["**"]),
+)
+
+bzl_library(
+ name = "bzl",
+ srcs = glob(["*.bzl"]),
+ visibility = ["//rules:__pkg__"],
+ deps = [
+ "//rules:android_binary_bzl",
+ "//rules:common_bzl",
+ ],
+)
diff --git a/rules/android_sandboxed_sdk/android_binary_with_sandboxed_sdks_macro.bzl b/rules/android_sandboxed_sdk/android_binary_with_sandboxed_sdks_macro.bzl
new file mode 100644
index 0000000..3214f34
--- /dev/null
+++ b/rules/android_sandboxed_sdk/android_binary_with_sandboxed_sdks_macro.bzl
@@ -0,0 +1,220 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# 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 rule for defining an Android binary that depends on sandboxed SDKs."""
+
+load(":providers.bzl", "AndroidSandboxedSdkBundleInfo")
+load("//rules:acls.bzl", "acls")
+load("//rules:bundletool.bzl", _bundletool = "bundletool")
+load("//rules:common.bzl", _common = "common")
+load(
+ "//rules:utils.bzl",
+ _get_android_toolchain = "get_android_toolchain",
+)
+load("//rules:java.bzl", _java = "java")
+
+def _gen_sdk_dependencies_manifest_impl(ctx):
+ manifest = ctx.actions.declare_file(ctx.label.name + "_sdk_dep_manifest.xml")
+
+ module_configs = [
+ bundle[AndroidSandboxedSdkBundleInfo].sdk_info.sdk_module_config
+ for bundle in ctx.attr.sdk_bundles
+ ]
+
+ args = ctx.actions.args()
+ args.add("generate-sdk-dependencies-manifest")
+ args.add("--manifest-package", ctx.attr.package)
+ args.add("--sdk-module-configs", ",".join([config.path for config in module_configs]))
+ args.add("--debug-keystore", ctx.file.debug_key.path)
+ args.add("--debug-keystore-pass", "android")
+ args.add("--debug-keystore-alias", "androiddebugkey")
+ args.add("--output-manifest", manifest)
+ _java.run(
+ ctx = ctx,
+ host_javabase = _common.get_host_javabase(ctx),
+ executable = _get_android_toolchain(ctx).sandboxed_sdk_toolbox.files_to_run,
+ arguments = [args],
+ inputs = module_configs + [ctx.file.debug_key],
+ outputs = [manifest],
+ mnemonic = "GenSdkDepManifest",
+ progress_message = "Generate SDK dependencies manifest %s" % manifest.short_path,
+ )
+
+ return [
+ DefaultInfo(
+ files = depset([manifest]),
+ ),
+ ]
+
+_gen_sdk_dependencies_manifest = rule(
+ attrs = dict(
+ package = attr.string(),
+ sdk_bundles = attr.label_list(
+ providers = [
+ [AndroidSandboxedSdkBundleInfo],
+ ],
+ ),
+ debug_key = attr.label(
+ allow_single_file = True,
+ default = Label("//tools/android:debug_keystore"),
+ ),
+ _host_javabase = attr.label(
+ cfg = "exec",
+ default = Label("//tools/jdk:current_java_runtime"),
+ ),
+ ),
+ executable = False,
+ implementation = _gen_sdk_dependencies_manifest_impl,
+ toolchains = [
+ "//toolchains/android:toolchain_type",
+ ],
+)
+
+def _android_binary_with_sandboxed_sdks_impl(ctx):
+ sdk_apks = []
+ for idx, sdk_bundle_target in enumerate(ctx.attr.sdk_bundles):
+ apk_out = ctx.actions.declare_file("%s/sdk_dep_apks/%s.apk" % (
+ ctx.label.name,
+ idx,
+ ))
+ _bundletool.build_sdk_apks(
+ ctx,
+ out = apk_out,
+ aapt2 = _get_android_toolchain(ctx).aapt2.files_to_run,
+ sdk_bundle = sdk_bundle_target[AndroidSandboxedSdkBundleInfo].asb,
+ debug_key = ctx.file.debug_key,
+ bundletool = _get_android_toolchain(ctx).bundletool.files_to_run,
+ host_javabase = _common.get_host_javabase(ctx),
+ )
+ sdk_apks.append(apk_out)
+
+ app_apk = ctx.attr.internal_android_binary[ApkInfo].signed_apk
+ adb = _get_android_toolchain(ctx).adb.files_to_run.executable
+ substitutions = {
+ "%adb%": adb.short_path,
+ "%app_apk%": app_apk.short_path,
+ "%sdk_apks%": ",".join([apk.short_path for apk in sdk_apks]),
+ }
+
+ install_script = ctx.actions.declare_file("%s_install_script.sh" % ctx.label.name)
+ ctx.actions.expand_template(
+ template = ctx.file._install_script_template,
+ output = install_script,
+ substitutions = substitutions,
+ is_executable = True,
+ )
+
+ return [
+ DefaultInfo(
+ executable = install_script,
+ files = depset([app_apk] + sdk_apks),
+ runfiles = ctx.runfiles([
+ adb,
+ app_apk,
+ ] + sdk_apks),
+ ),
+ ]
+
+_android_binary_with_sandboxed_sdks = rule(
+ attrs = dict(
+ internal_android_binary = attr.label(
+ providers = [
+ [ApkInfo],
+ ],
+ ),
+ debug_key = attr.label(
+ allow_single_file = True,
+ default = Label("//tools/android:debug_keystore"),
+ ),
+ sdk_bundles = attr.label_list(
+ providers = [
+ [AndroidSandboxedSdkBundleInfo],
+ ],
+ ),
+ _install_script_template = attr.label(
+ allow_single_file = True,
+ default = ":install_script.sh_template",
+ ),
+ _host_javabase = attr.label(
+ cfg = "exec",
+ default = Label("//tools/jdk:current_java_runtime"),
+ ),
+ ),
+ executable = True,
+ implementation = _android_binary_with_sandboxed_sdks_impl,
+ toolchains = [
+ "//toolchains/android:toolchain_type",
+ ],
+)
+
+def android_binary_with_sandboxed_sdks_macro(
+ _android_binary,
+ _android_library,
+ **attrs):
+ """android_binary_with_sandboxed_sdks.
+
+ Args:
+ _android_binary: The android_binary rule to use.
+ _android_library: The android_library rule to use.
+ **attrs: android_binary attributes.
+ """
+
+ name = attrs.pop("name", None)
+ fully_qualified_name = "//%s:%s" % (native.package_name(), name)
+ if (not acls.in_android_binary_with_sandboxed_sdks_allowlist(fully_qualified_name)):
+ fail("%s is not allowed to use the android_binary_with_sandboxed_sdks macro." %
+ fully_qualified_name)
+
+ sdk_bundles = attrs.pop("sdk_bundles", None)
+ debug_keystore = getattr(attrs, "debug_keystore", None)
+
+ bin_package = _java.resolve_package_from_label(
+ Label(fully_qualified_name),
+ getattr(attrs, "custom_package", None),
+ )
+
+ # Generate a manifest that lists all the SDK dependencies with <uses-sdk-library> tags.
+ sdk_dependencies_manifest_name = "%s_sdk_dependencies_manifest" % name
+ _gen_sdk_dependencies_manifest(
+ name = sdk_dependencies_manifest_name,
+ package = "%s.internalsdkdependencies" % bin_package,
+ sdk_bundles = sdk_bundles,
+ )
+
+ # Use the manifest in a normal android_library. This will later be added as a dependency to the
+ # binary, so the manifest is merged with the app's.
+ sdk_dependencies_lib_name = "%s_sdk_dependencies_lib" % name
+ _android_library(
+ name = sdk_dependencies_lib_name,
+ exports_manifest = 1,
+ manifest = ":%s" % sdk_dependencies_manifest_name,
+ )
+ deps = attrs.pop("deps", [])
+ deps.append(":%s" % sdk_dependencies_lib_name)
+
+ # Generate the android_binary as normal, passing the extra flags.
+ bin_label = Label("%s_app_bin" % fully_qualified_name)
+ _android_binary(
+ name = bin_label.name,
+ deps = deps,
+ **attrs
+ )
+
+ # This final rule will call Bundletool to generate the SDK APKs and provide the install script.
+ _android_binary_with_sandboxed_sdks(
+ name = name,
+ sdk_bundles = sdk_bundles,
+ debug_key = debug_keystore,
+ internal_android_binary = bin_label,
+ )
diff --git a/rules/android_sandboxed_sdk/android_sandboxed_sdk.bzl b/rules/android_sandboxed_sdk/android_sandboxed_sdk.bzl
new file mode 100644
index 0000000..87f0311
--- /dev/null
+++ b/rules/android_sandboxed_sdk/android_sandboxed_sdk.bzl
@@ -0,0 +1,54 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""android_sandboxed_sdk rule.
+
+This file exists to inject the correct version of android_binary.
+"""
+
+load(":android_sandboxed_sdk_macro.bzl", _android_sandboxed_sdk_macro = "android_sandboxed_sdk_macro")
+load("//rules:android_binary.bzl", _android_binary = "android_binary")
+
+def android_sandboxed_sdk(
+ name,
+ sdk_modules_config,
+ deps,
+ min_sdk_version = 21,
+ custom_package = None):
+ """Rule to build an Android Sandboxed SDK.
+
+ A sandboxed SDK is a collection of libraries that can run independently in the Privacy Sandbox
+ or in a separate split APK of an app. See:
+ https://developer.android.com/design-for-safety/privacy-sandbox.
+
+ Args:
+ name: Unique name of this target.
+ sdk_modules_config: Module config for this SDK. For full definition see
+ https://github.com/google/bundletool/blob/master/src/main/proto/sdk_modules_config.proto
+ deps: Set of android libraries that make up this SDK.
+ min_sdk_version: Min SDK version for the SDK.
+ custom_package: Java package for which java sources will be generated. By default the package
+ is inferred from the directory where the BUILD file containing the rule is. You can specify
+ a different package but this is highly discouraged since it can introduce classpath
+ conflicts with other libraries that will only be detected at runtime.
+ """
+
+ _android_sandboxed_sdk_macro(
+ name = name,
+ sdk_modules_config = sdk_modules_config,
+ deps = deps,
+ min_sdk_version = min_sdk_version,
+ custom_package = custom_package,
+ android_binary = _android_binary,
+ )
diff --git a/rules/android_sandboxed_sdk/android_sandboxed_sdk_bundle.bzl b/rules/android_sandboxed_sdk/android_sandboxed_sdk_bundle.bzl
new file mode 100644
index 0000000..102459a
--- /dev/null
+++ b/rules/android_sandboxed_sdk/android_sandboxed_sdk_bundle.bzl
@@ -0,0 +1,103 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""Rule for creating an Android Sandboxed SDK Bundle (ASB)."""
+
+load(":providers.bzl", "AndroidSandboxedSdkBundleInfo", "AndroidSandboxedSdkInfo")
+load(
+ "//rules:aapt.bzl",
+ _aapt = "aapt",
+)
+load(
+ "//rules:bundletool.bzl",
+ _bundletool = "bundletool",
+)
+load(
+ "//rules:common.bzl",
+ _common = "common",
+)
+load(
+ "//rules:utils.bzl",
+ _get_android_toolchain = "get_android_toolchain",
+)
+
+_ATTRS = dict(
+ sdk = attr.label(
+ providers = [
+ [AndroidSandboxedSdkInfo],
+ ],
+ ),
+ _host_javabase = attr.label(
+ cfg = "exec",
+ default = Label("//tools/jdk:current_java_runtime"),
+ ),
+)
+
+def _impl(ctx):
+ host_javabase = _common.get_host_javabase(ctx)
+
+ # Convert internal APK to proto resources.
+ internal_proto_apk = ctx.actions.declare_file(ctx.label.name + "_internal_proto_apk")
+ _aapt.convert(
+ ctx,
+ out = internal_proto_apk,
+ input = ctx.attr.sdk[AndroidSandboxedSdkInfo].internal_apk_info.unsigned_apk,
+ to_proto = True,
+ aapt = _get_android_toolchain(ctx).aapt2.files_to_run,
+ )
+
+ # Invoke module builder to create a base.zip that bundletool accepts.
+ module_zip = ctx.actions.declare_file(ctx.label.name + "_module.zip")
+ _bundletool.build_sdk_module(
+ ctx,
+ out = module_zip,
+ internal_apk = internal_proto_apk,
+ bundletool_module_builder =
+ _get_android_toolchain(ctx).bundletool_module_builder.files_to_run,
+ host_javabase = host_javabase,
+ )
+
+ # Invoke bundletool and create the bundle.
+ _bundletool.build_sdk_bundle(
+ ctx,
+ out = ctx.outputs.asb,
+ module = module_zip,
+ sdk_modules_config = ctx.attr.sdk[AndroidSandboxedSdkInfo].sdk_module_config,
+ bundletool = _get_android_toolchain(ctx).bundletool.files_to_run,
+ host_javabase = host_javabase,
+ )
+
+ return [
+ AndroidSandboxedSdkBundleInfo(
+ asb = ctx.outputs.asb,
+ sdk_info = ctx.attr.sdk[AndroidSandboxedSdkInfo],
+ ),
+ ]
+
+android_sandboxed_sdk_bundle = rule(
+ attrs = _ATTRS,
+ executable = False,
+ implementation = _impl,
+ provides = [
+ AndroidSandboxedSdkBundleInfo,
+ ],
+ outputs = {
+ "asb": "%{name}.asb",
+ },
+ toolchains = [
+ "//toolchains/android:toolchain_type",
+ "//toolchains/android_sdk:toolchain_type",
+ ],
+ fragments = ["android"],
+)
diff --git a/rules/android_sandboxed_sdk/android_sandboxed_sdk_macro.bzl b/rules/android_sandboxed_sdk/android_sandboxed_sdk_macro.bzl
new file mode 100644
index 0000000..6d8deb1
--- /dev/null
+++ b/rules/android_sandboxed_sdk/android_sandboxed_sdk_macro.bzl
@@ -0,0 +1,131 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# 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 rule for defining an Android Sandboxed SDK."""
+
+load(":providers.bzl", "AndroidSandboxedSdkInfo")
+load(
+ "//rules:common.bzl",
+ _common = "common",
+)
+load(
+ "//rules:utils.bzl",
+ _get_android_toolchain = "get_android_toolchain",
+)
+load("//rules:java.bzl", _java = "java")
+
+_ATTRS = dict(
+ sdk_modules_config = attr.label(
+ allow_single_file = [".pb.json"],
+ ),
+ internal_android_binary = attr.label(),
+ sdk_deploy_jar = attr.label(
+ allow_single_file = [".jar"],
+ ),
+ _host_javabase = attr.label(
+ cfg = "exec",
+ default = Label("//tools/jdk:current_java_runtime"),
+ ),
+)
+
+def _impl(ctx):
+ sdk_api_descriptors = ctx.actions.declare_file(ctx.label.name + "_sdk_api_descriptors.jar")
+
+ args = ctx.actions.args()
+ args.add("extract-api-descriptors")
+ args.add("--sdk-deploy-jar", ctx.file.sdk_deploy_jar)
+ args.add("--output-sdk-api-descriptors", sdk_api_descriptors)
+ _java.run(
+ ctx = ctx,
+ host_javabase = _common.get_host_javabase(ctx),
+ executable = _get_android_toolchain(ctx).sandboxed_sdk_toolbox.files_to_run,
+ arguments = [args],
+ inputs = [ctx.file.sdk_deploy_jar],
+ outputs = [sdk_api_descriptors],
+ mnemonic = "ExtractApiDescriptors",
+ progress_message = "Extract SDK API descriptors %s" % sdk_api_descriptors.short_path,
+ )
+ return [
+ DefaultInfo(
+ files = depset([sdk_api_descriptors]),
+ ),
+ AndroidSandboxedSdkInfo(
+ internal_apk_info = ctx.attr.internal_android_binary[ApkInfo],
+ sdk_module_config = ctx.file.sdk_modules_config,
+ sdk_api_descriptors = sdk_api_descriptors,
+ ),
+ ]
+
+_android_sandboxed_sdk = rule(
+ attrs = _ATTRS,
+ executable = False,
+ implementation = _impl,
+ provides = [
+ AndroidSandboxedSdkInfo,
+ ],
+ toolchains = [
+ "//toolchains/android:toolchain_type",
+ ],
+)
+
+def android_sandboxed_sdk_macro(
+ name,
+ sdk_modules_config,
+ deps,
+ min_sdk_version = 21,
+ custom_package = None,
+ android_binary = None):
+ """Macro for an Android Sandboxed SDK.
+
+ Args:
+ name: Unique name of this target.
+ sdk_modules_config: Module config for this SDK.
+ deps: Set of android libraries that make up this SDK.
+ min_sdk_version: Min SDK version for the SDK.
+ custom_package: Java package for resources,
+ android_binary: android_binary rule used to create the intermediate SDK APK.
+ """
+ fully_qualified_name = "//%s:%s" % (native.package_name(), name)
+ package = _java.resolve_package_from_label(Label(fully_qualified_name), custom_package)
+
+ manifest_label = Label("%s_gen_manifest" % fully_qualified_name)
+ native.genrule(
+ name = manifest_label.name,
+ outs = [name + "/AndroidManifest.xml"],
+ cmd = """cat > $@ <<EOF
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="{package}">
+ <uses-sdk android:minSdkVersion="{min_sdk_version}"/>
+ <application />
+</manifest>
+EOF
+""".format(package = package, min_sdk_version = min_sdk_version),
+ )
+
+ bin_fqn = "%s_bin" % fully_qualified_name
+ bin_label = Label(bin_fqn)
+ android_binary(
+ name = bin_label.name,
+ manifest = str(manifest_label),
+ deps = deps,
+ )
+
+ sdk_deploy_jar = Label("%s_deploy.jar" % bin_fqn)
+ _android_sandboxed_sdk(
+ name = name,
+ sdk_modules_config = sdk_modules_config,
+ internal_android_binary = bin_label,
+ sdk_deploy_jar = sdk_deploy_jar,
+ )
diff --git a/rules/android_sandboxed_sdk/providers.bzl b/rules/android_sandboxed_sdk/providers.bzl
new file mode 100644
index 0000000..0771d99
--- /dev/null
+++ b/rules/android_sandboxed_sdk/providers.bzl
@@ -0,0 +1,37 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""Providers for Android Sandboxed SDK rules."""
+
+AndroidSandboxedSdkInfo = provider(
+ doc = "Provides information about a sandboxed Android SDK.",
+ fields = dict(
+ internal_apk_info = "ApkInfo for SDKs dexes and resources. Note: it cannot " +
+ "be installed on a device as is. It needs to be further processed by " +
+ "other sandboxed SDK rules.",
+ sdk_module_config = "The SDK Module config. For the full definition see " +
+ "https://github.com/google/bundletool/blob/master/src/main/proto/sdk_modules_config.proto",
+ sdk_api_descriptors = "Jar file with the SDK API Descriptors. This can later be used to " +
+ "generate sources for communicating with this SDK from the app " +
+ "process.",
+ ),
+)
+
+AndroidSandboxedSdkBundleInfo = provider(
+ doc = "Provides information about a sandboxed Android SDK Bundle (ASB).",
+ fields = dict(
+ sdk_info = "AndroidSandboxedSdkInfo with information about the SDK.",
+ asb = "Path to the final ASB, unsigned.",
+ ),
+)
diff --git a/rules/attrs.bzl b/rules/attrs.bzl
index b93c2e8..6a03d2d 100644
--- a/rules/attrs.bzl
+++ b/rules/attrs.bzl
@@ -337,6 +337,10 @@
ANDROID_TOOLS_DEFAULTS_JAR_ATTRS = _add(_ANDROID_SDK)
+_AUTOMATIC_EXEC_GROUPS_ENABLED = dict(
+ _use_auto_exec_groups = attr.bool(default = True),
+)
+
attrs = struct(
ANDROID_SDK = _ANDROID_SDK,
COMPILATION = _COMPILATION,
@@ -346,4 +350,5 @@
tristate = _tristate,
add = _add,
replace = _replace,
+ AUTOMATIC_EXEC_GROUPS_ENABLED = _AUTOMATIC_EXEC_GROUPS_ENABLED,
)
diff --git a/rules/baseline_profiles.bzl b/rules/baseline_profiles.bzl
index 7909d49..581631b 100644
--- a/rules/baseline_profiles.bzl
+++ b/rules/baseline_profiles.bzl
@@ -16,7 +16,7 @@
Defines baseline profiles processing.
"""
-load("//rules:utils.bzl", "get_android_toolchain")
+load("//rules:utils.bzl", "ANDROID_TOOLCHAIN_TYPE", "get_android_toolchain")
def _process(ctx, final_classes_dex, transitive_profiles):
""" Merges/compiles all the baseline profiles propagated from android_library and aar_import.
@@ -48,6 +48,7 @@
inputs = transitive_profiles,
outputs = [merged_profile],
use_default_shell_env = True,
+ toolchain = ANDROID_TOOLCHAIN_TYPE,
)
# Profgen
@@ -70,6 +71,7 @@
inputs = profgen_inputs,
outputs = [output_profile, output_profile_meta],
use_default_shell_env = True,
+ toolchain = ANDROID_TOOLCHAIN_TYPE,
)
# Zip ART profiles
@@ -86,6 +88,7 @@
inputs = [output_profile, output_profile_meta],
outputs = [output_profile_zip],
use_default_shell_env = True,
+ toolchain = ANDROID_TOOLCHAIN_TYPE,
)
return BaselineProfileProvider(
transitive_profiles,
diff --git a/rules/bundletool.bzl b/rules/bundletool.bzl
index cfc3e48..7988bac 100644
--- a/rules/bundletool.bzl
+++ b/rules/bundletool.bzl
@@ -27,77 +27,6 @@
"tvdpi": 213,
}
-def _proto_apk_to_module(
- ctx,
- out = None,
- proto_apk = None,
- zip = None,
- unzip = None):
- # TODO(timpeut): rewrite this as a standalone golang tool
- ctx.actions.run_shell(
- command = """
-set -e
-
-IN_DIR=$(mktemp -d)
-OUT_DIR=$(mktemp -d)
-CUR_PWD=$(pwd)
-UNZIP=%s
-ZIP=%s
-INPUT=%s
-OUTPUT=%s
-
-"${UNZIP}" -qq "${INPUT}" -d "${IN_DIR}"
-cd "${IN_DIR}"
-
-if [ -f resources.pb ]; then
- mv resources.pb "${OUT_DIR}/"
-fi
-
-if [ -f AndroidManifest.xml ]; then
- mkdir "${OUT_DIR}/manifest"
- mv AndroidManifest.xml "${OUT_DIR}/manifest/"
-fi
-
-NUM_DEX=`ls -1 *.dex 2>/dev/null | wc -l`
-if [ $NUM_DEX != 0 ]; then
- mkdir "${OUT_DIR}/dex"
- mv *.dex "${OUT_DIR}/dex/"
-fi
-
-if [ -d res ]; then
- mv res "${OUT_DIR}/res"
-fi
-
-if [ -d assets ]; then
- mv assets "${OUT_DIR}/"
-fi
-
-if [ -d lib ]; then
- mv lib "${OUT_DIR}/"
-fi
-
-UNKNOWN=`ls -1 * 2>/dev/null | wc -l`
-if [ $UNKNOWN != 0 ]; then
- mkdir "${OUT_DIR}/root"
- mv * "${OUT_DIR}/root/"
-fi
-
-cd "${OUT_DIR}"
-"${CUR_PWD}/${ZIP}" "${CUR_PWD}/${OUTPUT}" -Drq0 .
-""" % (
- unzip.executable.path,
- zip.executable.path,
- proto_apk.path,
- out.path,
- ),
- tools = [zip, unzip],
- arguments = [],
- inputs = [proto_apk],
- outputs = [out],
- mnemonic = "Rebundle",
- progress_message = "Rebundle to %s" % out.short_path,
- )
-
def _build(
ctx,
out = None,
@@ -131,66 +60,118 @@
progress_message = "Building bundle %s" % out.short_path,
)
-def _extract_config(
+def _build_device_json(
+ ctx,
+ out,
+ abis,
+ locales,
+ density,
+ sdk_version):
+ json_content = json.encode(struct(
+ supportedAbis = abis,
+ supportedLocales = locales,
+ screenDensity = _density_mapping[density],
+ sdkVersion = int(sdk_version),
+ ))
+ ctx.actions.write(out, json_content)
+
+def _build_sdk_apks(
ctx,
out = None,
- aab = None,
+ aapt2 = None,
+ sdk_bundle = None,
+ debug_key = None,
bundletool = None,
host_javabase = None):
- # Need to execute as a shell script as the tool outputs to stdout
- cmd = """
-set -e
-contents=`%s -jar %s dump config --bundle %s`
-echo "$contents" > %s
-""" % (
- host_javabase[java_common.JavaRuntimeInfo].java_executable_exec_path,
- bundletool.executable.path,
- aab.path,
- out.path,
+ apks_out = ctx.actions.declare_directory(ctx.label.name + "_sdk_apks")
+ args = ctx.actions.args()
+ args.add("build-sdk-apks")
+ args.add("--aapt2", aapt2.executable.path)
+ args.add("--sdk-bundle", sdk_bundle)
+ args.add("--ks", debug_key)
+ args.add("--ks-pass=pass:android")
+ args.add("--ks-key-alias=androiddebugkey")
+ args.add("--key-pass=pass:android")
+ args.add("--output-format=DIRECTORY")
+ args.add("--output", apks_out.path)
+ _java.run(
+ ctx = ctx,
+ host_javabase = host_javabase,
+ executable = bundletool,
+ arguments = [args],
+ inputs = [
+ sdk_bundle,
+ debug_key,
+ ],
+ tools = [aapt2],
+ outputs = [apks_out],
+ mnemonic = "BuildSdkApksDir",
+ progress_message = "Building SDK APKs directory %s" % apks_out.short_path,
)
+ # Now move standalone APK out of bundletool output dir.
ctx.actions.run_shell(
- inputs = [aab],
+ command = """
+set -e
+APKS_OUT_DIR=%s
+DEBUG_APK_PATH=%s
+
+mv "${APKS_OUT_DIR}/standalones/standalone.apk" "${DEBUG_APK_PATH}"
+""" % (
+ apks_out.path,
+ out.path,
+ ),
+ tools = [],
+ arguments = [],
+ inputs = [apks_out],
outputs = [out],
- tools = depset([bundletool.executable], transitive = [host_javabase[java_common.JavaRuntimeInfo].files]),
- mnemonic = "ExtractBundleConfig",
- progress_message = "Extract bundle config to %s" % out.short_path,
- command = cmd,
+ mnemonic = "ExtractDebugSdkApk",
+ progress_message = "Extract debug SDK APK to %s" % out.short_path,
)
-def _extract_manifest(
+def _build_sdk_bundle(
ctx,
out = None,
- aab = None,
module = None,
- xpath = None,
+ sdk_modules_config = None,
bundletool = None,
host_javabase = None):
- # Need to execute as a shell script as the tool outputs to stdout
- extra_flags = []
- if module:
- extra_flags.append("--module " + module)
- if xpath:
- extra_flags.append("--xpath " + xpath)
- cmd = """
-set -e
-contents=`%s -jar %s dump manifest --bundle %s %s`
-echo "$contents" > %s
-""" % (
- host_javabase[java_common.JavaRuntimeInfo].java_executable_exec_path,
- bundletool.executable.path,
- aab.path,
- " ".join(extra_flags),
- out.path,
+ args = ctx.actions.args()
+ args.add("build-sdk-bundle")
+
+ args.add("--sdk-modules-config", sdk_modules_config)
+ args.add("--modules", module)
+ args.add("--output", out)
+ _java.run(
+ ctx = ctx,
+ host_javabase = host_javabase,
+ executable = bundletool,
+ arguments = [args],
+ inputs = [
+ module,
+ sdk_modules_config,
+ ],
+ outputs = [out],
+ mnemonic = "BuildASB",
+ progress_message = "Building SDK bundle %s" % out.short_path,
)
- ctx.actions.run_shell(
- inputs = [aab],
+def _build_sdk_module(
+ ctx,
+ out = None,
+ internal_apk = None,
+ bundletool_module_builder = None,
+ host_javabase = None):
+ args = ctx.actions.args()
+ args.add("--internal_apk_path", internal_apk)
+ args.add("--output_module_path", out)
+ ctx.actions.run(
+ inputs = [internal_apk],
outputs = [out],
- tools = depset([bundletool.executable], transitive = [host_javabase[java_common.JavaRuntimeInfo].files]),
- mnemonic = "ExtractBundleManifest",
- progress_message = "Extract bundle manifest to %s" % out.short_path,
- command = cmd,
+ executable = bundletool_module_builder,
+ arguments = [args],
+ mnemonic = "BuildSdkModule",
+ progress_message = "Building ASB zip module %s" % out.short_path,
)
def _bundle_to_apks(
@@ -261,24 +242,145 @@
progress_message = "Converting bundle to .apks: %s" % out.short_path,
)
-def _build_device_json(
+def _extract_config(
ctx,
- out,
- abis,
- locales,
- density,
- sdk_version):
- json_content = json.encode(struct(
- supportedAbis = abis,
- supportedLocales = locales,
- screenDensity = _density_mapping[density],
- sdkVersion = int(sdk_version),
- ))
- ctx.actions.write(out, json_content)
+ out = None,
+ aab = None,
+ bundletool = None,
+ host_javabase = None):
+ # Need to execute as a shell script as the tool outputs to stdout
+ cmd = """
+set -e
+contents=`%s -jar %s dump config --bundle %s`
+echo "$contents" > %s
+""" % (
+ host_javabase[java_common.JavaRuntimeInfo].java_executable_exec_path,
+ bundletool.executable.path,
+ aab.path,
+ out.path,
+ )
+
+ ctx.actions.run_shell(
+ inputs = [aab],
+ outputs = [out],
+ tools = depset([bundletool.executable], transitive = [host_javabase[java_common.JavaRuntimeInfo].files]),
+ mnemonic = "ExtractBundleConfig",
+ progress_message = "Extract bundle config to %s" % out.short_path,
+ command = cmd,
+ )
+
+def _extract_manifest(
+ ctx,
+ out = None,
+ aab = None,
+ module = None,
+ xpath = None,
+ bundletool = None,
+ host_javabase = None):
+ # Need to execute as a shell script as the tool outputs to stdout
+ extra_flags = []
+ if module:
+ extra_flags.append("--module " + module)
+ if xpath:
+ extra_flags.append("--xpath " + xpath)
+ cmd = """
+set -e
+contents=`%s -jar %s dump manifest --bundle %s %s`
+echo "$contents" > %s
+""" % (
+ host_javabase[java_common.JavaRuntimeInfo].java_executable_exec_path,
+ bundletool.executable.path,
+ aab.path,
+ " ".join(extra_flags),
+ out.path,
+ )
+
+ ctx.actions.run_shell(
+ inputs = [aab],
+ outputs = [out],
+ tools = depset([bundletool.executable], transitive = [host_javabase[java_common.JavaRuntimeInfo].files]),
+ mnemonic = "ExtractBundleManifest",
+ progress_message = "Extract bundle manifest to %s" % out.short_path,
+ command = cmd,
+ )
+
+def _proto_apk_to_module(
+ ctx,
+ out = None,
+ proto_apk = None,
+ zip = None,
+ unzip = None):
+ # TODO(timpeut): migrate this to Bundletool module builder.
+ ctx.actions.run_shell(
+ command = """
+set -e
+
+IN_DIR=$(mktemp -d)
+OUT_DIR=$(mktemp -d)
+CUR_PWD=$(pwd)
+UNZIP=%s
+ZIP=%s
+INPUT=%s
+OUTPUT=%s
+
+"${UNZIP}" -qq "${INPUT}" -d "${IN_DIR}"
+cd "${IN_DIR}"
+
+if [ -f resources.pb ]; then
+ mv resources.pb "${OUT_DIR}/"
+fi
+
+if [ -f AndroidManifest.xml ]; then
+ mkdir "${OUT_DIR}/manifest"
+ mv AndroidManifest.xml "${OUT_DIR}/manifest/"
+fi
+
+NUM_DEX=`ls -1 *.dex 2>/dev/null | wc -l`
+if [ $NUM_DEX != 0 ]; then
+ mkdir "${OUT_DIR}/dex"
+ mv *.dex "${OUT_DIR}/dex/"
+fi
+
+if [ -d res ]; then
+ mv res "${OUT_DIR}/res"
+fi
+
+if [ -d assets ]; then
+ mv assets "${OUT_DIR}/"
+fi
+
+if [ -d lib ]; then
+ mv lib "${OUT_DIR}/"
+fi
+
+UNKNOWN=`ls -1 * 2>/dev/null | wc -l`
+if [ $UNKNOWN != 0 ]; then
+ mkdir "${OUT_DIR}/root"
+ mv * "${OUT_DIR}/root/"
+fi
+
+cd "${OUT_DIR}"
+"${CUR_PWD}/${ZIP}" "${CUR_PWD}/${OUTPUT}" -Drq0 .
+""" % (
+ unzip.executable.path,
+ zip.executable.path,
+ proto_apk.path,
+ out.path,
+ ),
+ tools = [zip, unzip],
+ arguments = [],
+ inputs = [proto_apk],
+ outputs = [out],
+ mnemonic = "Rebundle",
+ progress_message = "Rebundle to %s" % out.short_path,
+ )
bundletool = struct(
build = _build,
build_device_json = _build_device_json,
+ build_sdk_apks = _build_sdk_apks,
+ build_sdk_bundle = _build_sdk_bundle,
+ build_sdk_module = _build_sdk_module,
bundle_to_apks = _bundle_to_apks,
extract_config = _extract_config,
extract_manifest = _extract_manifest,
diff --git a/rules/common.bzl b/rules/common.bzl
index c54012c..8b6359d 100644
--- a/rules/common.bzl
+++ b/rules/common.bzl
@@ -14,7 +14,7 @@
"""Bazel common library for the Android rules."""
-load(":utils.bzl", "get_android_toolchain", _log = "log")
+load(":utils.bzl", "ANDROID_TOOLCHAIN_TYPE", "get_android_toolchain", _log = "log")
load("//rules/android_common:reexport_android_common.bzl", _native_android_common = "native_android_common")
# Suffix attached to the Starlark portion of android_binary target
@@ -59,6 +59,7 @@
outputs = [out_zip],
mnemonic = "FilterZipInclude",
progress_message = "Filtering %s" % in_zip.short_path,
+ toolchain = ANDROID_TOOLCHAIN_TYPE,
)
def _filter_zip_exclude(
@@ -111,6 +112,7 @@
outputs = [output],
mnemonic = "FilterZipExclude",
progress_message = "Filtering %s" % input.short_path,
+ toolchain = ANDROID_TOOLCHAIN_TYPE,
)
def _create_signer_properties(ctx, oldest_key):
diff --git a/rules/data_binding.bzl b/rules/data_binding.bzl
index e853058..f935436 100644
--- a/rules/data_binding.bzl
+++ b/rules/data_binding.bzl
@@ -14,7 +14,7 @@
"""Bazel Android Data Binding."""
-load(":utils.bzl", _utils = "utils")
+load(":utils.bzl", "ANDROID_TOOLCHAIN_TYPE", _utils = "utils")
# Data Binding context attributes.
_JAVA_ANNOTATION_PROCESSOR_ADDITIONAL_INPUTS = \
@@ -92,6 +92,7 @@
progress_message = (
"GenerateDataBindingBaseClasses %s" % class_info.short_path
),
+ toolchain = ANDROID_TOOLCHAIN_TYPE,
)
return srcjar, class_info
diff --git a/rules/desugar.bzl b/rules/desugar.bzl
index c498e00..9d4420f 100644
--- a/rules/desugar.bzl
+++ b/rules/desugar.bzl
@@ -22,7 +22,8 @@
bootclasspath = [],
min_sdk_version = 0,
library_desugaring = True,
- desugar_exec = None):
+ desugar_exec = None,
+ toolchain_type = None):
"""Desugars a JAR.
Args:
@@ -60,6 +61,7 @@
mnemonic = "Desugar",
progress_message = "Desugaring " + input.short_path + " for Android",
execution_requirements = {"supports-workers": "1"},
+ toolchain = toolchain_type,
)
desugar = struct(
diff --git a/rules/dex.bzl b/rules/dex.bzl
index 5ba3fe3..d9d9273 100644
--- a/rules/dex.bzl
+++ b/rules/dex.bzl
@@ -14,7 +14,7 @@
"""Bazel Dex Commands."""
-load(":utils.bzl", "get_android_toolchain", "utils")
+load(":utils.bzl", "ANDROID_TOOLCHAIN_TYPE", "get_android_toolchain", "utils")
load(":providers.bzl", "StarlarkAndroidDexInfo")
load("@bazel_skylib//lib:collections.bzl", "collections")
load("//rules:attrs.bzl", _attrs = "attrs")
@@ -31,7 +31,8 @@
java_info = None,
desugar_dict = {},
dexbuilder = None,
- dexmerger = None):
+ dexmerger = None,
+ toolchain_type = None):
classes_dex_zip = _get_dx_artifact(ctx, "classes.dex.zip")
info = _merge_infos(utils.collect_providers(StarlarkAndroidDexInfo, deps))
@@ -52,6 +53,7 @@
incremental_dexopts = incremental_dexopts,
min_sdk_version = min_sdk_version,
dex_exec = dexbuilder,
+ toolchain_type = toolchain_type,
)
dex_archives.append(dex_archive)
@@ -63,6 +65,7 @@
main_dex_list = main_dex_list,
dexopts = dexopts,
dexmerger = dexmerger,
+ toolchain_type = toolchain_type,
)
return classes_dex_zip
@@ -88,6 +91,7 @@
mnemonic = "AppendJava8LegacyDex",
use_default_shell_env = True,
progress_message = "Adding Java8 legacy library for %s" % ctx.label,
+ toolchain = ANDROID_TOOLCHAIN_TYPE,
)
def _to_dexed_classpath(dex_archives_dict = {}, classpath = [], runtime_jars = []):
@@ -109,7 +113,8 @@
output = None,
incremental_dexopts = [],
min_sdk_version = 0,
- dex_exec = None):
+ dex_exec = None,
+ toolchain_type = None):
"""Dexes a JAR.
Args:
@@ -143,6 +148,7 @@
mnemonic = "DexBuilder",
progress_message = "Dexing " + input.path + " with applicable dexopts " + str(incremental_dexopts),
execution_requirements = execution_requirements,
+ toolchain = toolchain_type,
)
def _get_dx_artifact(ctx, basename):
@@ -195,6 +201,7 @@
arguments = [args],
mnemonic = "BuildLegacyDex",
progress_message = "Building Java8 legacy library for %s" % ctx.label,
+ toolchain = ANDROID_TOOLCHAIN_TYPE,
)
return java8_legacy_dex, java8_legacy_dex_map
@@ -212,7 +219,8 @@
multidex_strategy = "minimal",
main_dex_list = None,
dexopts = [],
- dexmerger = None):
+ dexmerger = None,
+ toolchain_type = None):
args = ctx.actions.args()
args.add("--multidex", multidex_strategy)
args.add_all(inputs, before_each = "--input")
@@ -230,6 +238,7 @@
outputs = [output],
mnemonic = "DexMerger",
progress_message = "Assembling dex files into " + output.short_path,
+ toolchain = toolchain_type,
)
def _merger_dexopts(tokenized_dexopts, dexopts_supported_in_dex_merger):
diff --git a/rules/idl.bzl b/rules/idl.bzl
index c77b137..6ab76c9 100644
--- a/rules/idl.bzl
+++ b/rules/idl.bzl
@@ -16,7 +16,7 @@
load(":java.bzl", _java = "java")
load(":path.bzl", _path = "path")
-load(":utils.bzl", _log = "log")
+load(":utils.bzl", "ANDROID_TOOLCHAIN_TYPE", _log = "log")
_AIDL_TOOLCHAIN_MISSING_ERROR = (
"IDL sources provided without the Android IDL toolchain."
@@ -82,6 +82,7 @@
outputs = [out_idl_java_src],
mnemonic = "AndroidIDLGenerate",
progress_message = "Android IDL generation %s" % idl_src.path,
+ toolchain = ANDROID_TOOLCHAIN_TYPE,
)
def _get_idl_import_root_path(
diff --git a/rules/intellij.bzl b/rules/intellij.bzl
index c870b1b..e0c0f95 100644
--- a/rules/intellij.bzl
+++ b/rules/intellij.bzl
@@ -32,7 +32,10 @@
args.add("--manifest_proto", manifest_proto)
args.add("--output_class_jar", out_jar)
args.add("--output_source_jar", out_srcjar)
- args.add("--temp_dir", out_jar.dirname)
+
+ # tmp directory is removed by the idl tool before each invocation so create a unique dir.
+ # See src/main/java/com/google/devtools/build/lib/rules/android/AndroidIdlHelper.java
+ args.add("--temp_dir", "%s/%s_idl_tmp" % (out_jar.dirname, ctx.label.name))
args.add_all(idl_java_srcs)
_java.run(
diff --git a/rules/java.bzl b/rules/java.bzl
index 03ca1e9..57c343a 100644
--- a/rules/java.bzl
+++ b/rules/java.bzl
@@ -392,6 +392,7 @@
ctx.actions.run(
executable = java_toolchain[java_common.JavaToolchainInfo].single_jar,
+ toolchain = "@bazel_tools//tools/jdk:toolchain_type",
arguments = [args],
inputs = inputs,
outputs = [output],
@@ -433,6 +434,7 @@
java_runtime = host_javabase[java_common.JavaRuntimeInfo]
args["executable"] = java_runtime.java_executable_exec_path
+ args["toolchain"] = "@bazel_tools//tools/jdk:toolchain_type"
# inputs can be a list or a depset of File
inputs = args.get("inputs", default = [])
diff --git a/rules/native_deps.bzl b/rules/native_deps.bzl
index e5fe000..53d61ec 100644
--- a/rules/native_deps.bzl
+++ b/rules/native_deps.bzl
@@ -55,58 +55,13 @@
name = name + "-hwasan"
return name
-def process_java_infos(_ctx, deps):
- """Collects JavaInfos for process()
-
- Args:
- _ctx: Unused ctx (need this for uniformity)
- deps: List of deps
-
- Returns:
- List of JavaInfo.cc_link_params_info for all deps
- """
- return [dep[JavaInfo].cc_link_params_info for dep in deps if JavaInfo in dep]
-
-def process_android_cc_link_params_infos(_ctx, deps):
- """Collects AndroidCcLinkParamsInfo for process()
-
- Args:
- _ctx: Unused ctx (need this for uniformity)
- deps: List of deps
-
- Returns:
- List of AndroidCcLinkParamsInfo.link_params for all deps
- """
- return [dep[AndroidCcLinkParamsInfo].link_params for dep in deps if AndroidCcLinkParamsInfo in dep]
-
-def process_cc_infos(_ctx, deps):
- """Collects CcInfos for process()
-
- Args:
- _ctx: Unused ctx (need this for uniformity)
- deps: List of deps
-
- Returns:
- List of CcInfo's for all deps
- """
- return [dep[CcInfo] for dep in deps if CcInfo in dep]
-
-DEFAULT_NATIVE_DEP_SUBPROCESSORS = dict(
- NativeDepsProcessJavaInfos = process_java_infos,
- NativeDepsProcessAndroidCcLinkParamsInfos = process_android_cc_link_params_infos,
- NativeDepsProcessCcInfos = process_cc_infos,
-)
-
-def process(ctx, filename, subprocessors = DEFAULT_NATIVE_DEP_SUBPROCESSORS):
+def process(ctx, filename):
""" Links native deps into a shared library
Args:
ctx: The context.
filename: String. The name of the artifact containing the name of the
linked shared library
- subprocessors: Dict of function pointers, each element of which handles native
- dependency collection on a per-provider basis. Defaults to basic collection of JavaInfo,
- AndroidCcLinkParamsInfo, and CcInfo providers.
Returns:
Tuple of (libs, libs_name) where libs is a depset of all native deps
@@ -129,16 +84,14 @@
owner = ctx.label,
user_link_flags = ["-Wl,-soname=lib" + actual_target_name],
)
-
- processed_cc_infos = []
- for subproc in subprocessors.values():
- processed_cc_infos.extend(subproc(ctx, deps))
cc_info = cc_common.merge_cc_infos(
cc_infos = _concat(
[CcInfo(linking_context = cc_common.create_linking_context(
linker_inputs = depset([linker_input]),
))],
- processed_cc_infos,
+ [dep[JavaInfo].cc_link_params_info for dep in deps if JavaInfo in dep],
+ [dep[AndroidCcLinkParamsInfo].link_params for dep in deps if AndroidCcLinkParamsInfo in dep],
+ [dep[CcInfo] for dep in deps if CcInfo in dep],
),
)
libraries = []
diff --git a/rules/proguard.bzl b/rules/proguard.bzl
index 22daf8b..6273c5c 100644
--- a/rules/proguard.bzl
+++ b/rules/proguard.bzl
@@ -14,8 +14,10 @@
"""Bazel Android Proguard library for the Android rules."""
+load(":android_neverlink_aspect.bzl", "StarlarkAndroidNeverlinkInfo")
load(":common.bzl", "common")
-load(":utils.bzl", "utils")
+load(":java.bzl", "java")
+load(":utils.bzl", "ANDROID_TOOLCHAIN_TYPE", "get_android_sdk", "utils")
_ProguardSpecContextInfo = provider(
doc = "Contains data from processing Proguard specs.",
@@ -27,6 +29,21 @@
),
)
+_ProguardOutputInfo = provider(
+ doc = "Temporary provider to hold all proguard outputs. Will be replaced by a native " +
+ "provider. Useful for testing.",
+ fields = dict(
+ input_jar = "The input program jar, unoptimized",
+ output_jar = "The optimized output jar",
+ mapping = "Output proguard map",
+ proto_mapping = "Output proto mapping",
+ seeds = "Output seeds",
+ usage = "Output usage",
+ library_jar = "Merged library jar",
+ config = "Output config",
+ ),
+)
+
def _validate_proguard_spec(
ctx,
out_validated_proguard_spec,
@@ -45,6 +62,7 @@
progress_message = (
"Validating proguard configuration %s" % proguard_spec.short_path
),
+ toolchain = ANDROID_TOOLCHAIN_TYPE,
)
def _process_specs(
@@ -110,11 +128,11 @@
if len(local_proguard_specs) == 0:
return []
- proguard_specs = local_proguard_specs + specs_to_include
- for dep in proguard_deps:
- proguard_specs.extend(dep.specs.to_list())
-
- return sorted(proguard_specs)
+ proguard_specs = depset(
+ local_proguard_specs + specs_to_include,
+ transitive = [dep.specs for dep in proguard_deps],
+ )
+ return sorted(proguard_specs.to_list())
def _get_proguard_specs(
ctx,
@@ -165,19 +183,418 @@
progress_message = "Adding -assumevalues spec for minSdkVersion",
)
+def _optimization_action(
+ ctx,
+ output_jar,
+ program_jar,
+ library_jar,
+ proguard_specs,
+ proguard_mapping = None,
+ proguard_output_map = None,
+ proguard_seeds = None,
+ proguard_usage = None,
+ proguard_config_output = None,
+ runtype = None,
+ last_stage_output = None,
+ next_stage_output = None,
+ final = False,
+ mnemonic = None,
+ progress_message = None,
+ proguard_tool = None):
+ """Creates a Proguard optimization action.
+
+ This method is expected to be called one or more times to create Proguard optimization actions.
+ Most outputs will only be generated by the final optimization action, and should otherwise be
+ set to None. For the final action set `final = True` which will register the output_jar as an
+ output of the action.
+
+ TODO(b/286955442): Support baseline profiles.
+
+ Args:
+ ctx: The context.
+ output_jar: File. The final output jar.
+ program_jar: File. The jar to be optimized.
+ library_jar: File. The merged library jar. While the underlying tooling supports multiple
+ library jars, we merge these into a single jar before processing.
+ proguard_specs: Sequence of files. A list of proguard specs to use for the optimization.
+ proguard_mapping: File. Optional file to be used as a mapping for proguard. A mapping file
+ generated by proguard_generate_mapping to be re-used to apply the same map to a new build.
+ proguard_output_map: File. Optional file to be used to write the output map of obfuscated
+ class and member names.
+ proguard_seeds: File. Optional file used to write the "seeds", which is a list of all
+ classes and members which match a keep rule.
+ proguard_usage: File. Optional file used to write all classes and members that are removed
+ during shrinking (i.e. unused code).
+ proguard_config_output:File. Optional file used to write the entire configuration that has
+ been parsed, included files and replaced variables. Useful for debugging.
+ runtype: String. Optional string identifying this run. One of [INITIAL, OPTIMIZATION, FINAL]
+ last_stage_output: File. Optional input file to this optimization stage, which was output by
+ the previous optimization stage.
+ next_stage_output: File. Optional output file from this optimization stage, which will be
+ consunmed by the next optimization stage.
+ final: Boolean. Whether this is the final optimization stage, which will register output_jar
+ as an output of this action.
+ mnemonic: String. Action mnemonic.
+ progress_message: String. Action progress message.
+ proguard_tool: FilesToRunProvider. The proguard tool to execute.
+
+ Returns:
+ None
+ """
+
+ inputs = []
+ outputs = []
+ args = ctx.actions.args()
+
+ args.add("-forceprocessing")
+
+ args.add("-injars", program_jar)
+ inputs.append(program_jar)
+
+ args.add("-outjars", output_jar)
+ if final:
+ outputs.append(output_jar)
+
+ args.add("-libraryjars", library_jar)
+ inputs.append(library_jar)
+
+ if proguard_mapping:
+ args.add("-applymapping", proguard_mapping)
+ inputs.append(proguard_mapping)
+
+ args.add_all(proguard_specs, format_each = "@%s")
+ inputs.extend(proguard_specs)
+
+ if proguard_output_map:
+ args.add("-printmapping", proguard_output_map)
+ outputs.append(proguard_output_map)
+
+ if proguard_seeds:
+ args.add("-printseeds", proguard_seeds)
+ outputs.append(proguard_seeds)
+
+ if proguard_usage:
+ args.add("-printusage", proguard_usage)
+ outputs.append(proguard_usage)
+
+ if proguard_config_output:
+ args.add("-printconfiguration", proguard_config_output)
+ outputs.append(proguard_config_output)
+
+ if runtype:
+ args.add("-runtype " + runtype)
+
+ if last_stage_output:
+ args.add("-laststageoutput", last_stage_output)
+ inputs.append(last_stage_output)
+
+ if next_stage_output:
+ args.add("-nextstageoutput", next_stage_output)
+ outputs.append(next_stage_output)
+
+ ctx.actions.run(
+ outputs = outputs,
+ inputs = inputs,
+ executable = proguard_tool,
+ arguments = [args],
+ mnemonic = mnemonic,
+ progress_message = progress_message,
+ toolchain = None, # TODO(timpeut): correctly set this based off which optimizer is selected
+ )
+
def _get_proguard_temp_artifact_with_prefix(ctx, label, prefix, name):
native_label_name = label.name.removesuffix(common.PACKAGED_RESOURCES_SUFFIX)
return ctx.actions.declare_file("proguard/" + native_label_name + "/" + prefix + "_" + native_label_name + "_" + name)
+def _get_proguard_temp_artifact(ctx, name):
+ return _get_proguard_temp_artifact_with_prefix(ctx, ctx.label, "MIGRATED", name)
+
+def _get_proguard_output_map(ctx):
+ return ctx.actions.declare_file(ctx.label.name.removesuffix(common.PACKAGED_RESOURCES_SUFFIX) + "_proguard_MIGRATED_.map")
+
+def _apply_proguard(
+ ctx,
+ input_jar = None,
+ proguard_specs = [],
+ proguard_optimization_passes = None,
+ proguard_mapping = None,
+ proguard_output_jar = None,
+ proguard_output_map = None,
+ proguard_seeds = None,
+ proguard_usage = None,
+ proguard_tool = None):
+ """Top-level method to apply proguard to a jar.
+
+ Args:
+ ctx: The context
+ input_jar: File. The input jar to optimized.
+ proguard_specs: List of Files. The proguard specs to use for optimization.
+ proguard_optimization_passes: Integer. The number of proguard passes to apply.
+ proguard_mapping: File. The proguard mapping to apply.
+ proguard_output_jar: File. The output optimized jar.
+ proguard_output_map: File. The output proguard map.
+ proguard_seeds: File. The output proguard seeds.
+ proguard_usage: File. The output proguard usage.
+ proguard_tool: FilesToRun. The proguard executable.
+
+ Returns:
+ A struct of proguard outputs, corresponding to the fields in ProguardOutputInfo.
+ """
+ if not proguard_specs:
+ # Fail at execution time if these artifacts are requested, to avoid issue where outputs are
+ # declared without having any proguard specs. This can happen if specs is a select() that
+ # resolves to an empty list.
+ _fail_action(
+ ctx,
+ proguard_output_jar,
+ proguard_output_map,
+ proguard_seeds,
+ proguard_usage,
+ )
+ return None
+
+ library_jar_list = [get_android_sdk(ctx).android_jar]
+ if ctx.fragments.android.desugar_java8:
+ library_jar_list.append(ctx.file._desugared_java8_legacy_apis)
+ neverlink_infos = utils.collect_providers(StarlarkAndroidNeverlinkInfo, ctx.attr.deps)
+ library_jars = depset(library_jar_list, transitive = [info.transitive_neverlink_libraries for info in neverlink_infos])
+
+ return _create_optimization_actions(
+ ctx,
+ proguard_specs,
+ proguard_seeds,
+ proguard_usage,
+ proguard_mapping,
+ proguard_output_jar,
+ proguard_optimization_passes,
+ proguard_output_map,
+ input_jar,
+ library_jars,
+ proguard_tool,
+ )
+
+def _get_proguard_output(
+ ctx,
+ proguard_output_jar,
+ proguard_seeds,
+ proguard_usage,
+ proguard_output_map,
+ combined_library_jar):
+ """Helper method to get a struct of all proguard outputs."""
+ config_output = _get_proguard_temp_artifact(ctx, "_proguard.config")
+
+ return struct(
+ output_jar = proguard_output_jar,
+ mapping = proguard_output_map,
+ seeds = proguard_seeds,
+ usage = proguard_usage,
+ library_jar = combined_library_jar,
+ config = config_output,
+ )
+
+def _create_optimization_actions(
+ ctx,
+ proguard_specs = None,
+ proguard_seeds = None,
+ proguard_usage = None,
+ proguard_mapping = None,
+ proguard_output_jar = None,
+ num_passes = None,
+ proguard_output_map = None,
+ input_jar = None,
+ library_jars = depset(),
+ proguard_tool = None):
+ """Helper method to create all optimizaction actions based on the target configuration."""
+ if not proguard_specs:
+ fail("Missing proguard_specs in create_optimization_actions")
+
+ # Merge all library jars into a single jar
+ combined_library_jar = _get_proguard_temp_artifact(ctx, "_migrated_combined_library_jars.jar")
+ java.singlejar(
+ ctx,
+ library_jars,
+ combined_library_jar,
+ java_toolchain = common.get_java_toolchain(ctx),
+ )
+
+ # Filter library jar with program jar
+ filtered_library_jar = _get_proguard_temp_artifact(ctx, "_migrated_combined_library_jars_filtered.jar")
+ common.filter_zip_exclude(
+ ctx,
+ filtered_library_jar,
+ combined_library_jar,
+ filter_zips = [input_jar],
+ )
+
+ outputs = _get_proguard_output(
+ ctx,
+ proguard_output_jar,
+ proguard_seeds,
+ proguard_usage,
+ proguard_output_map,
+ combined_library_jar,
+ )
+
+ # TODO(timpeut): Validate that optimizer target selection is correct
+ mnemonic = ctx.fragments.java.bytecode_optimizer_mnemonic
+ optimizer_target = ctx.executable._bytecode_optimizer
+
+ # If num_passes is not specified run a single optimization action
+ if not num_passes:
+ _optimization_action(
+ ctx,
+ outputs.output_jar,
+ input_jar,
+ filtered_library_jar,
+ proguard_specs,
+ proguard_mapping = proguard_mapping,
+ proguard_output_map = outputs.mapping,
+ proguard_seeds = outputs.seeds,
+ proguard_usage = outputs.usage,
+ proguard_config_output = outputs.config,
+ final = True,
+ mnemonic = mnemonic,
+ progress_message = "Trimming binary with %s: %s" % (mnemonic, ctx.label),
+ proguard_tool = proguard_tool,
+ )
+ return outputs
+
+ # num_passes has been specified, create multiple proguard actions
+ split_bytecode_optimization_passes = ctx.fragments.java.split_bytecode_optimization_pass
+ bytecode_optimization_pass_actions = ctx.fragments.java.bytecode_optimization_pass_actions
+ last_stage_output = _get_proguard_temp_artifact(ctx, "_proguard_preoptimization.jar")
+ _optimization_action(
+ ctx,
+ outputs.output_jar,
+ input_jar,
+ filtered_library_jar,
+ proguard_specs,
+ proguard_mapping = proguard_mapping,
+ proguard_output_map = None,
+ proguard_seeds = outputs.seeds,
+ proguard_usage = None,
+ proguard_config_output = None,
+ final = False,
+ runtype = "INITIAL",
+ next_stage_output = last_stage_output,
+ mnemonic = mnemonic,
+ progress_message = "Trimming binary with %s: Verification/Shrinking Pass" % mnemonic,
+ proguard_tool = proguard_tool,
+ )
+ for i in range(1, num_passes + 1):
+ if split_bytecode_optimization_passes and bytecode_optimization_pass_actions < 2:
+ last_stage_output = _create_single_optimization_action(
+ ctx,
+ outputs.output_jar,
+ input_jar,
+ filtered_library_jar,
+ proguard_specs,
+ proguard_mapping,
+ i,
+ "_INITIAL",
+ mnemonic,
+ last_stage_output,
+ optimizer_target,
+ )
+ last_stage_output = _create_single_optimization_action(
+ ctx,
+ outputs.output_jar,
+ input_jar,
+ filtered_library_jar,
+ proguard_specs,
+ proguard_mapping,
+ i,
+ "_FINAL",
+ mnemonic,
+ last_stage_output,
+ optimizer_target,
+ )
+ else:
+ for j in range(1, bytecode_optimization_pass_actions + 1):
+ last_stage_output = _create_single_optimization_action(
+ ctx,
+ outputs.output_jar,
+ input_jar,
+ filtered_library_jar,
+ proguard_specs,
+ proguard_mapping,
+ i,
+ "_ACTION_%s_OF_%s_" % (j, bytecode_optimization_pass_actions),
+ mnemonic,
+ last_stage_output,
+ optimizer_target,
+ )
+
+ _optimization_action(
+ ctx,
+ outputs.output_jar,
+ input_jar,
+ filtered_library_jar,
+ proguard_specs,
+ proguard_mapping = proguard_mapping,
+ proguard_output_map = outputs.mapping,
+ proguard_seeds = None,
+ proguard_usage = outputs.usage,
+ proguard_config_output = outputs.config,
+ final = True,
+ runtype = "FINAL",
+ last_stage_output = last_stage_output,
+ mnemonic = mnemonic,
+ progress_message = "Trimming binary with %s: Obfuscation and Final Output Pass" % mnemonic,
+ proguard_tool = proguard_tool,
+ )
+ return outputs
+
+def _create_single_optimization_action(
+ ctx,
+ output_jar,
+ program_jar,
+ library_jar,
+ proguard_specs,
+ proguard_mapping,
+ optimization_pass_num,
+ runtype_suffix,
+ mnemonic,
+ last_stage_output,
+ proguard_tool):
+ next_stage_output = _get_proguard_temp_artifact(ctx, "_%s_optimization%s_%s.jar" % (mnemonic, runtype_suffix, optimization_pass_num))
+ _optimization_action(
+ ctx,
+ output_jar,
+ program_jar,
+ library_jar,
+ proguard_specs,
+ proguard_mapping = proguard_mapping,
+ mnemonic = mnemonic,
+ final = False,
+ runtype = "OPTIMIZATION" + runtype_suffix,
+ last_stage_output = last_stage_output,
+ next_stage_output = next_stage_output,
+ progress_message = "Trimming binary with %s: Optimization%s Pass %d" % (mnemonic, runtype_suffix, optimization_pass_num),
+ proguard_tool = proguard_tool,
+ )
+ return next_stage_output
+
+def _fail_action(ctx, *outputs):
+ ctx.actions.run_shell(
+ outputs = outputs,
+ command = "echo \"Unable to run proguard without `proguard_specs`\"; exit 1;",
+ )
+
proguard = struct(
+ apply_proguard = _apply_proguard,
process_specs = _process_specs,
generate_min_sdk_version_assumevalues = _generate_min_sdk_version_assumevalues,
+ get_proguard_output_map = _get_proguard_output_map,
get_proguard_specs = _get_proguard_specs,
+ get_proguard_temp_artifact = _get_proguard_temp_artifact,
get_proguard_temp_artifact_with_prefix = _get_proguard_temp_artifact_with_prefix,
)
testing = struct(
validate_proguard_spec = _validate_proguard_spec,
collect_transitive_proguard_specs = _collect_transitive_proguard_specs,
+ optimization_action = _optimization_action,
ProguardSpecContextInfo = _ProguardSpecContextInfo,
+ ProguardOutputInfo = _ProguardOutputInfo,
)
diff --git a/rules/resources.bzl b/rules/resources.bzl
index 23f4edd..aec519f 100644
--- a/rules/resources.bzl
+++ b/rules/resources.bzl
@@ -119,6 +119,7 @@
_PACKAGED_VALIDATION_RESULT = "validation_result"
_RESOURCE_MINSDK_PROGUARD_CONFIG = "resource_minsdk_proguard_config"
_RESOURCE_PROGUARD_CONFIG = "resource_proguard_config"
+_ANDROID_APPLICATION_RESOURCE = "android_application_resource"
_ResourcesPackageContextInfo = provider(
"Packaged resources context object",
@@ -132,6 +133,7 @@
_RESOURCE_MINSDK_PROGUARD_CONFIG: "Resource minSdkVersion proguard config",
_RESOURCE_PROGUARD_CONFIG: "Resource proguard config",
_PROVIDERS: "The list of all providers to propagate.",
+ _ANDROID_APPLICATION_RESOURCE: "The AndroidApplicationResourceInfo provider.",
},
)
@@ -210,6 +212,7 @@
outputs = [out_manifest],
mnemonic = "AddG3ITRStarlark",
progress_message = "Adding G3ITR to test manifest for %s" % ctx.label,
+ toolchain = None,
)
def _get_legacy_mergee_manifests(resources_infos):
@@ -273,6 +276,7 @@
""",
arguments = [manifest_args, args, manifest_params.path],
outputs = [manifest_params],
+ toolchain = None,
)
args = ctx.actions.args()
args.add(manifest_params, format = "--flagfile=%s")
@@ -284,6 +288,7 @@
outputs = [out_merged_manifest],
mnemonic = "StarlarkLegacyAndroidManifestMerger",
progress_message = "Merging Android Manifests",
+ toolchain = None,
)
def _make_databinding_outputs(
@@ -343,6 +348,7 @@
inputs = [compiled_resources],
tools = [zip_tool],
arguments = [compiled_resources.path, out_compiled_resources.path, zip_tool.executable.path],
+ toolchain = None,
command = """#!/bin/bash
set -e
@@ -461,7 +467,8 @@
xsltproc = None,
instrument_xslt = None,
busybox = None,
- host_javabase = None):
+ host_javabase = None,
+ add_application_resource_info_to_providers = True):
"""Package resources for top-level rules.
Args:
@@ -513,6 +520,7 @@
minsdk_proguard_config: Optional file. Proguard config for the minSdkVersion to include in the
returned resource context.
aapt: FilesToRunProvider. The aapt executable or FilesToRunProvider.
+ has_local_proguard_specs: If the target has proguard specs.
android_jar: File. The Android jar.
legacy_merger: FilesToRunProvider. The legacy manifest merger executable.
xsltproc: FilesToRunProvider. The xsltproc executable or
@@ -522,6 +530,8 @@
busybox: FilesToRunProvider. The ResourceBusyBox executable or
FilesToRunprovider
host_javabase: A Target. The host javabase.
+ add_application_resource_info_to_providers: boolean. Whether to add the
+ AndroidApplicationResourceInfo provider to the list of providers for this processor.
Returns:
A ResourcesPackageContextInfo containing packaged resource artifacts and
@@ -774,7 +784,7 @@
transitive_resource_apks = depset(),
))
- packaged_resources_ctx[_PROVIDERS].append(AndroidApplicationResourceInfo(
+ android_application_resource_info = AndroidApplicationResourceInfo(
resource_apk = resource_apk,
resource_java_src_jar = r_java,
resource_java_class_jar = class_jar,
@@ -785,7 +795,11 @@
resources_zip = resource_files_zip,
databinding_info = data_binding_layout_info,
should_compile_java_srcs = should_compile_java_srcs,
- ))
+ )
+ packaged_resources_ctx[_ANDROID_APPLICATION_RESOURCE] = android_application_resource_info
+ if add_application_resource_info_to_providers:
+ packaged_resources_ctx[_PROVIDERS].append(android_application_resource_info)
+
return _ResourcesPackageContextInfo(**packaged_resources_ctx)
def _liteparse(ctx, out_r_pb, resource_files, android_kit):
@@ -811,6 +825,7 @@
outputs = [out_r_pb],
mnemonic = "ResLiteParse",
progress_message = "Lite parse Android Resources %s" % ctx.label,
+ toolchain = None,
)
def _fastr(ctx, r_pbs, package, manifest, android_kit):
@@ -1070,6 +1085,7 @@
arguments = [args],
mnemonic = "BumpMinSdkFloor",
progress_message = "Bumping up AndroidManifest min SDK %s" % str(ctx.label),
+ toolchain = None,
)
manifest_ctx[_PROCESSED_MANIFEST] = out_manifest
@@ -1120,6 +1136,7 @@
arguments = [args],
mnemonic = "SetDefaultMinSdkFloor",
progress_message = "Setting AndroidManifest min SDK to default %s" % str(ctx.label),
+ toolchain = None,
)
manifest_ctx[_PROCESSED_MANIFEST] = out_manifest
@@ -1164,6 +1181,7 @@
arguments = [args],
mnemonic = "ValidateMinSdkFloor",
progress_message = "Validating AndroidManifest min SDK %s" % str(ctx.label),
+ toolchain = None,
)
manifest_validation_ctx[_VALIDATION_OUTPUTS].append(log)
@@ -1894,6 +1912,7 @@
set_default_min_sdk = _set_default_min_sdk,
# Exposed for android_binary
+ is_resource_shrinking_enabled = _is_resource_shrinking_enabled,
validate_min_sdk = _validate_min_sdk,
# Exposed for android_library, aar_import, and android_binary
diff --git a/rules/rules.bzl b/rules/rules.bzl
index ecaef19..ed1cd2d 100644
--- a/rules/rules.bzl
+++ b/rules/rules.bzl
@@ -37,6 +37,14 @@
_android_ndk_repository = "android_ndk_repository",
)
load(
+ "//rules/android_sandboxed_sdk:android_sandboxed_sdk.bzl",
+ _android_sandboxed_sdk = "android_sandboxed_sdk",
+)
+load(
+ "//rules/android_sandboxed_sdk:android_sandboxed_sdk_bundle.bzl",
+ _android_sandboxed_sdk_bundle = "android_sandboxed_sdk_bundle",
+)
+load(
"//rules:android_sdk.bzl",
_android_sdk = "android_sdk",
)
@@ -57,6 +65,8 @@
android_binary = _android_binary
android_library = _android_library
android_ndk_repository = _android_ndk_repository
+android_sandboxed_sdk = _android_sandboxed_sdk
+android_sandboxed_sdk_bundle = _android_sandboxed_sdk_bundle
android_sdk = _android_sdk
android_sdk_repository = _android_sdk_repository
android_tools_defaults_jar = _android_tools_defaults_jar
diff --git a/rules/utils.bzl b/rules/utils.bzl
index e9a28cc..e54a17f 100644
--- a/rules/utils.bzl
+++ b/rules/utils.bzl
@@ -16,6 +16,8 @@
load(":providers.bzl", "FailureInfo")
+ANDROID_TOOLCHAIN_TYPE = "//toolchains/android:toolchain_type"
+
_CUU = "\033[A"
_EL = "\033[K"
_DEFAULT = "\033[0m"
@@ -414,7 +416,7 @@
)
def get_android_toolchain(ctx):
- return ctx.toolchains["//toolchains/android:toolchain_type"]
+ return ctx.toolchains[ANDROID_TOOLCHAIN_TYPE]
def get_android_sdk(ctx):
if hasattr(ctx.fragments.android, "incompatible_use_toolchain_resolution") and ctx.fragments.android.incompatible_use_toolchain_resolution:
diff --git a/src/tools/bundletool_module_builder/BUILD b/src/tools/bundletool_module_builder/BUILD
new file mode 100644
index 0000000..142f89f
--- /dev/null
+++ b/src/tools/bundletool_module_builder/BUILD
@@ -0,0 +1,14 @@
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
+
+package(
+ default_applicable_licenses = ["//:license"],
+ default_visibility = ["//visibility:public"],
+)
+
+licenses(["notice"])
+
+go_binary(
+ name = "bundletool_module_builder",
+ srcs = ["bundletool_module_builder.go"],
+)
diff --git a/src/tools/bundletool_module_builder/bundletool_module_builder.go b/src/tools/bundletool_module_builder/bundletool_module_builder.go
new file mode 100644
index 0000000..ded6cad
--- /dev/null
+++ b/src/tools/bundletool_module_builder/bundletool_module_builder.go
@@ -0,0 +1,88 @@
+// Copyright 2023 The Bazel Authors. All rights reserved.
+//
+// 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.
+
+// Tool for building Bundletool modules for apps and SDKs.
+package main
+
+import (
+ "archive/zip"
+ "flag"
+ "log"
+ "os"
+ "strings"
+)
+
+var (
+ internalApkPathFlag = flag.String("internal_apk_path", "", "Path to an APK that contains the SDK classes and resources.")
+ outputModulePathFlag = flag.String("output_module_path", "", "Path to the resulting module, ready to be sent to Bundletool.")
+)
+
+func main() {
+ flag.Parse()
+ if *internalApkPathFlag == "" {
+ log.Fatal("Missing internal APK path")
+ }
+
+ if *internalApkPathFlag == "" {
+ log.Fatal("Missing ouput module path")
+ }
+ err := unzipApkAndCreateModule(*internalApkPathFlag, *outputModulePathFlag)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func unzipApkAndCreateModule(internalApkPath, outputModulePath string) error {
+ r, err := zip.OpenReader(internalApkPath)
+ if err != nil {
+ return err
+ }
+ defer r.Close()
+
+ w, err := os.Create(outputModulePath)
+ if err != nil {
+ return err
+ }
+ defer w.Close()
+ zipWriter := zip.NewWriter(w)
+ defer zipWriter.Close()
+
+ for _, f := range r.File {
+ f.Name = fileNameInOutput(f.Name)
+ if err := zipWriter.Copy(f); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func fileNameInOutput(oldName string) string {
+ switch {
+ // Passthrough files. They will just be copied into the output module.
+ case oldName == "resources.pb" ||
+ strings.HasPrefix(oldName, "res/") ||
+ strings.HasPrefix(oldName, "assets/") ||
+ strings.HasPrefix(oldName, "lib/"):
+ return oldName
+ // Manifest should be moved to manifest/ dir.
+ case oldName == "AndroidManifest.xml":
+ return "manifest/AndroidManifest.xml"
+ // Dex files need to be moved under dex/ dir.
+ case strings.HasSuffix(oldName, ".dex"):
+ return "dex/" + oldName
+ // All other files (probably JVM metadata files) should be moved to root/ dir.
+ default:
+ return "root/" + oldName
+ }
+}
diff --git a/src/tools/enforce_min_sdk_floor/BUILD b/src/tools/enforce_min_sdk_floor/BUILD
index 221fe8b..01b0578 100644
--- a/src/tools/enforce_min_sdk_floor/BUILD
+++ b/src/tools/enforce_min_sdk_floor/BUILD
@@ -1,5 +1,7 @@
# Description:
# Package for tool to enforce min SDK floor on AndroidManifests
+load("@rules_python//python:defs.bzl", "py_binary", "py_test")
+
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/BUILD b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/BUILD
new file mode 100644
index 0000000..17ec508
--- /dev/null
+++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/BUILD
@@ -0,0 +1,29 @@
+# Common tools for managing sandboxed SDKs.
+# Sandboxed SDKs are libraries that are released separately from Android apps and can run in the
+# Privacy Sandbox.
+
+package(
+ default_applicable_licenses = ["//:license"],
+ default_visibility = ["//:__subpackages__"],
+)
+
+licenses(["notice"])
+
+java_library(
+ name = "sandboxed_sdk_toolbox_lib",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors",
+ "//src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest",
+ "@rules_android_maven//:info_picocli_picocli",
+ ],
+)
+
+java_binary(
+ name = "sandboxed_sdk_toolbox",
+ main_class = "com.google.devtools.build.android.sandboxedsdktoolbox.SandboxedSdkToolbox",
+ visibility = ["//visibility:public"],
+ runtime_deps = [
+ ":sandboxed_sdk_toolbox_lib",
+ ],
+)
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/SandboxedSdkToolbox.java b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/SandboxedSdkToolbox.java
new file mode 100644
index 0000000..b555a61
--- /dev/null
+++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/SandboxedSdkToolbox.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 The Bazel Authors. All rights reserved.
+ *
+ * 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.
+ */
+package com.google.devtools.build.android.sandboxedsdktoolbox;
+
+import com.google.devtools.build.android.sandboxedsdktoolbox.apidescriptors.ExtractApiDescriptorsCommand;
+import com.google.devtools.build.android.sandboxedsdktoolbox.sdkdependenciesmanifest.GenerateSdkDependenciesManifestCommand;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+/** Entrypoint for the Sandboxed SDK Toolbox binary. */
+@Command(
+ name = "sandboxed-sdk-toolbox",
+ subcommands = {
+ ExtractApiDescriptorsCommand.class,
+ GenerateSdkDependenciesManifestCommand.class,
+ })
+public final class SandboxedSdkToolbox {
+
+ public static final CommandLine create() {
+ return new CommandLine(new SandboxedSdkToolbox());
+ }
+
+ public static final void main(String[] args) {
+ SandboxedSdkToolbox.create().execute(args);
+ }
+
+ private SandboxedSdkToolbox() {}
+}
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/BUILD b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/BUILD
new file mode 100644
index 0000000..7fe0422
--- /dev/null
+++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/BUILD
@@ -0,0 +1,17 @@
+# Command to extract API descriptors from a sandboxed SDK.
+
+package(
+ default_applicable_licenses = ["//:license"],
+ default_visibility = ["//:__subpackages__"],
+)
+
+licenses(["notice"])
+
+java_library(
+ name = "apidescriptors",
+ srcs = glob(["*.java"]),
+ deps = [
+ "@rules_android_maven//:androidx_privacysandbox_tools_tools_apipackager",
+ "@rules_android_maven//:info_picocli_picocli",
+ ],
+)
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/ExtractApiDescriptorsCommand.java b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/ExtractApiDescriptorsCommand.java
new file mode 100644
index 0000000..22882d1
--- /dev/null
+++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/ExtractApiDescriptorsCommand.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 The Bazel Authors. All rights reserved.
+ *
+ * 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.
+ */
+package com.google.devtools.build.android.sandboxedsdktoolbox.apidescriptors;
+
+import static java.nio.file.Files.createTempDirectory;
+
+import androidx.privacysandbox.tools.apipackager.PrivacySandboxApiPackager;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+
+/** Command that Extracts API descriptors from a sandboxed SDK's classpath. */
+@Command(
+ name = "extract-api-descriptors",
+ description = "Extracts API descriptors from a sandboxed SDK's classpath.")
+public final class ExtractApiDescriptorsCommand implements Runnable {
+
+ @Option(names = "--sdk-deploy-jar", required = true)
+ Path sdkDeployJarPath;
+
+ @Option(names = "--output-sdk-api-descriptors", required = true)
+ Path outputSdkApiDescriptorsPath;
+
+ private final PrivacySandboxApiPackager packager = new PrivacySandboxApiPackager();
+
+ @Override
+ public void run() {
+ try {
+ Path sdkClasspath = unzipSdkDeployJar();
+ packager.packageSdkDescriptors(sdkClasspath, outputSdkApiDescriptorsPath);
+ } catch (IOException e) {
+ throw new UncheckedIOException("Failed to package SDK API descriptors.", e);
+ }
+ }
+
+ private Path unzipSdkDeployJar() throws IOException {
+ Path sdkClasspath = createTempDirectory("tmp-sdk-classpath");
+ try (InputStream inputStream = Files.newInputStream(sdkDeployJarPath);
+ ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
+
+ ZipEntry entry = null;
+ while ((entry = zipInputStream.getNextEntry()) != null) {
+ Path entryPath = sdkClasspath.resolve(entry.getName()).normalize();
+ if (entry.isDirectory()) {
+ continue;
+ }
+
+ if (!entryPath.startsWith(sdkClasspath)) {
+ throw new IOException(
+ String.format("Invalid entry name in SDK classpath zip: %s", entry.getName()));
+ }
+
+ Files.createDirectories(entryPath.getParent());
+ Files.copy(zipInputStream, entryPath);
+ }
+ }
+ return sdkClasspath;
+ }
+
+ private ExtractApiDescriptorsCommand() {}
+}
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/config/BUILD b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/config/BUILD
new file mode 100644
index 0000000..cbc3f38
--- /dev/null
+++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/config/BUILD
@@ -0,0 +1,17 @@
+# Utilities for SDK module config proto message.
+
+package(
+ default_applicable_licenses = ["//:license"],
+ default_visibility = ["//src:__subpackages__"],
+)
+
+licenses(["notice"])
+
+java_library(
+ name = "config",
+ srcs = glob(["*.java"]),
+ deps = [
+ "@rules_android_maven//:com_android_tools_build_bundletool",
+ "@rules_android_maven//:com_google_protobuf_protobuf_java_util",
+ ],
+)
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/config/SdkModulesConfigUtils.java b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/config/SdkModulesConfigUtils.java
new file mode 100644
index 0000000..e023096
--- /dev/null
+++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/config/SdkModulesConfigUtils.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 The Bazel Authors. All rights reserved.
+ *
+ * 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.
+ */
+package com.google.devtools.build.android.sandboxedsdktoolbox.config;
+
+import com.android.bundle.SdkModulesConfigOuterClass.SdkModulesConfig;
+import com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder;
+import com.google.protobuf.util.JsonFormat;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/** Utilities for creating and extracting information from {@link SdkModulesConfig} messages. */
+public final class SdkModulesConfigUtils {
+
+ public static SdkModulesConfig readFromJsonFile(Path configPath) {
+ SdkModulesConfig.Builder builder = SdkModulesConfig.newBuilder();
+ try {
+ JsonFormat.parser().merge(Files.newBufferedReader(configPath), builder);
+ return builder.build();
+ } catch (IOException e) {
+ throw new UncheckedIOException("Failed to parse SDK Module Config.", e);
+ }
+ }
+
+ public static long getVersionMajor(SdkModulesConfig config) {
+ return RuntimeEnabledSdkVersionEncoder.encodeSdkMajorAndMinorVersion(
+ config.getSdkVersion().getMajor(), config.getSdkVersion().getMinor());
+ }
+
+ private SdkModulesConfigUtils() {}
+}
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/AndroidManifestWriter.java b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/AndroidManifestWriter.java
new file mode 100644
index 0000000..f840c5f
--- /dev/null
+++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/AndroidManifestWriter.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2023 The Bazel Authors. All rights reserved.
+ *
+ * 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.
+ */
+package com.google.devtools.build.android.sandboxedsdktoolbox.sdkdependenciesmanifest;
+
+import static com.google.devtools.build.android.sandboxedsdktoolbox.config.SdkModulesConfigUtils.getVersionMajor;
+
+import com.android.bundle.SdkModulesConfigOuterClass.SdkModulesConfig;
+import com.google.common.collect.ImmutableSet;
+import java.io.BufferedOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Path;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+/** Writes an Android manifest that lists SDK dependencies for an app. */
+final class AndroidManifestWriter {
+
+ private static final String ANDROID_NAME_ATTRIBUTE = "android:name";
+ private static final String ANDROID_VERSION_MAJOR_ATTRIBUTE = "android:versionMajor";
+ private static final String ANDROID_CERTIFICATE_DIGEST_ATTRIBUTE = "android:certDigest";
+ private static final String APPLICATION_ELEMENT_NAME = "application";
+ private static final String MANIFEST_ELEMENT_NAME = "manifest";
+ private static final String MANIFEST_NAMESPACE_URI = "http://schemas.android.com/apk/res/android";
+ private static final String MANIFEST_NAMESPACE_NAME = "xmlns:android";
+ private static final String MANIFEST_PACKAGE_ATTRIBUTE = "package";
+ private static final String SDK_DEPENDENCY_ELEMENT_NAME = "uses-sdk-library";
+
+ static void writeManifest(
+ String packageName,
+ String certificateDigest,
+ ImmutableSet<SdkModulesConfig> configs,
+ Path outputPath) {
+ Document root = newEmptyDocument();
+
+ Element manifestNode = root.createElement(MANIFEST_ELEMENT_NAME);
+ manifestNode.setAttribute(MANIFEST_NAMESPACE_NAME, MANIFEST_NAMESPACE_URI);
+ manifestNode.setAttribute(MANIFEST_PACKAGE_ATTRIBUTE, packageName);
+ root.appendChild(manifestNode);
+
+ Element applicationNode = root.createElement(APPLICATION_ELEMENT_NAME);
+ manifestNode.appendChild(applicationNode);
+
+ for (SdkModulesConfig config : configs) {
+ Element sdkDependencyElement = root.createElement(SDK_DEPENDENCY_ELEMENT_NAME);
+ sdkDependencyElement.setAttribute(ANDROID_NAME_ATTRIBUTE, config.getSdkPackageName());
+ sdkDependencyElement.setAttribute(
+ ANDROID_VERSION_MAJOR_ATTRIBUTE, Long.toString(getVersionMajor(config)));
+ sdkDependencyElement.setAttribute(ANDROID_CERTIFICATE_DIGEST_ATTRIBUTE, certificateDigest);
+ applicationNode.appendChild(sdkDependencyElement);
+ }
+
+ writeDocument(root, outputPath);
+ }
+
+ private static Document newEmptyDocument() {
+ try {
+ return DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
+ } catch (ParserConfigurationException e) {
+ throw new IllegalStateException("Failed to create new XML document.", e);
+ }
+ }
+
+ private static void writeDocument(Document document, Path outputPath) {
+ try (BufferedOutputStream outputStream =
+ new BufferedOutputStream(new FileOutputStream(outputPath.toFile()))) {
+ Transformer transformer = TransformerFactory.newInstance().newTransformer();
+ transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8");
+ transformer.setOutputProperty(OutputKeys.METHOD, "xml");
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.transform(new DOMSource(document), new StreamResult(outputStream));
+ } catch (TransformerException | IOException e) {
+ throw new IllegalStateException("Failed to write manifest.", e);
+ }
+ }
+
+ private AndroidManifestWriter() {}
+}
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/BUILD b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/BUILD
new file mode 100644
index 0000000..b2584b2
--- /dev/null
+++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/BUILD
@@ -0,0 +1,19 @@
+# Command for generating an SDK dependencies Android manifest.
+
+package(
+ default_applicable_licenses = ["//:license"],
+ default_visibility = ["//src:__subpackages__"],
+)
+
+licenses(["notice"])
+
+java_library(
+ name = "sdkdependenciesmanifest",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/config",
+ "@rules_android_maven//:com_android_tools_build_bundletool",
+ "@rules_android_maven//:com_google_guava_guava",
+ "@rules_android_maven//:info_picocli_picocli",
+ ],
+)
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/CertificateDigestGenerator.java b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/CertificateDigestGenerator.java
new file mode 100644
index 0000000..f8ad00b
--- /dev/null
+++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/CertificateDigestGenerator.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023 The Bazel Authors. All rights reserved.
+ *
+ * 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.
+ */
+package com.google.devtools.build.android.sandboxedsdktoolbox.sdkdependenciesmanifest;
+
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.hash.Hashing;
+import com.google.common.io.ByteSource;
+import com.google.common.primitives.Bytes;
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+
+/** Generates a SHA256 digest of a signing certificate. */
+final class CertificateDigestGenerator {
+
+ static final String generateCertificateDigest(
+ Path keystorePath, String keystorePassword, String keystoreAlias) {
+ X509Certificate certificate = readCertificate(keystorePath, keystorePassword, keystoreAlias);
+ return getCertificateDigest(certificate);
+ }
+
+ private static X509Certificate readCertificate(
+ Path keystorePath, String keystorePassword, String keystoreAlias) {
+ try (BufferedInputStream keystoreInputStream =
+ new BufferedInputStream(Files.newInputStream(keystorePath))) {
+ KeyStore keystore = KeyStore.getInstance("JKS");
+ keystore.load(keystoreInputStream, keystorePassword.toCharArray());
+ return (X509Certificate) keystore.getCertificate(keystoreAlias);
+ } catch (GeneralSecurityException | IOException e) {
+ throw new IllegalStateException("Failed to read certificate", e);
+ }
+ }
+
+ private static String getCertificateDigest(X509Certificate certificate) {
+ try {
+ return Bytes.asList(
+ ByteSource.wrap(certificate.getEncoded()).hash(Hashing.sha256()).asBytes())
+ .stream()
+ .map(b -> String.format("%02X", b))
+ .collect(joining(":"));
+ } catch (CertificateEncodingException | IOException e) {
+ throw new IllegalStateException("Failed to generate certificate digest", e);
+ }
+ }
+
+ private CertificateDigestGenerator() {}
+}
diff --git a/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/GenerateSdkDependenciesManifestCommand.java b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/GenerateSdkDependenciesManifestCommand.java
new file mode 100644
index 0000000..c390f27
--- /dev/null
+++ b/src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/GenerateSdkDependenciesManifestCommand.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2023 The Bazel Authors. All rights reserved.
+ *
+ * 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.
+ */
+package com.google.devtools.build.android.sandboxedsdktoolbox.sdkdependenciesmanifest;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.devtools.build.android.sandboxedsdktoolbox.sdkdependenciesmanifest.AndroidManifestWriter.writeManifest;
+import static com.google.devtools.build.android.sandboxedsdktoolbox.sdkdependenciesmanifest.CertificateDigestGenerator.generateCertificateDigest;
+import static java.util.Arrays.stream;
+
+import com.android.bundle.SdkModulesConfigOuterClass.SdkModulesConfig;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.android.sandboxedsdktoolbox.config.SdkModulesConfigUtils;
+import java.nio.file.Path;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+
+/** Command for generating SDK dependencies manifest. */
+@Command(
+ name = "generate-sdk-dependencies-manifest",
+ description =
+ "Generates an Android manifest with the <uses-sdk-library> tags from the given "
+ + "SDK bundles.")
+public final class GenerateSdkDependenciesManifestCommand implements Runnable {
+
+ @Option(names = "--manifest-package", required = true)
+ String manifestPackage;
+
+ @Option(names = "--sdk-module-configs", split = ",", required = true)
+ Path[] sdkModuleConfigPaths;
+
+ @Option(names = "--debug-keystore", required = true)
+ Path debugKeystorePath;
+
+ @Option(names = "--debug-keystore-pass", required = true)
+ String debugKeystorePassword;
+
+ @Option(names = "--debug-keystore-alias", required = true)
+ String debugKeystoreAlias;
+
+ @Option(names = "--output-manifest", required = true)
+ Path outputManifestPath;
+
+ @Override
+ public void run() {
+ ImmutableSet<SdkModulesConfig> configSet =
+ stream(sdkModuleConfigPaths)
+ .map(SdkModulesConfigUtils::readFromJsonFile)
+ .collect(toImmutableSet());
+
+ String certificateDigest =
+ generateCertificateDigest(debugKeystorePath, debugKeystorePassword, debugKeystoreAlias);
+
+ writeManifest(manifestPackage, certificateDigest, configSet, outputManifestPath);
+ }
+
+ private GenerateSdkDependenciesManifestCommand() {}
+}
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/BUILD b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/BUILD
new file mode 100644
index 0000000..678f495
--- /dev/null
+++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/BUILD
@@ -0,0 +1,22 @@
+# Tests for extract-api-descriptors command.
+
+package(
+ default_applicable_licenses = ["//:license"],
+ default_visibility = ["//src:__subpackages__"],
+)
+
+licenses(["notice"])
+
+java_test(
+ name = "ExtractApiDescriptorsCommandTest",
+ size = "small",
+ srcs = ["ExtractApiDescriptorsCommandTest.java"],
+ data = [
+ "//src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/testlibrary:libtestlibrary.jar",
+ ],
+ deps = [
+ "//src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils",
+ "@rules_android_maven//:com_google_truth_truth",
+ "@rules_android_maven//:junit_junit",
+ ],
+)
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/ExtractApiDescriptorsCommandTest.java b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/ExtractApiDescriptorsCommandTest.java
new file mode 100644
index 0000000..43d0937
--- /dev/null
+++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/ExtractApiDescriptorsCommandTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2023 The Bazel Authors. All rights reserved.
+ *
+ * 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.
+ */
+package com.google.devtools.build.android.sandboxedsdktoolbox.apidescriptors;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.android.sandboxedsdktoolbox.utils.Runner.runCommand;
+import static com.google.devtools.build.android.sandboxedsdktoolbox.utils.TestData.JAVATESTS_DIR;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.android.sandboxedsdktoolbox.utils.CommandResult;
+import java.nio.file.Path;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class ExtractApiDescriptorsCommandTest {
+ @Rule public final TemporaryFolder testFolder = new TemporaryFolder();
+
+ private static final Path TEST_LIBRARY_DEPLOY_JAR =
+ JAVATESTS_DIR.resolve(
+ Path.of(
+ "com/google/devtools/build/android/sandboxedsdktoolbox",
+ "apidescriptors/testlibrary/libtestlibrary.jar"));
+
+ @Test
+ public void extractApiDescriptors_keepsAnnotatedClassesInDescriptors() throws Exception {
+ Path outputFile = testFolder.getRoot().toPath().resolve("output.jar");
+
+ CommandResult result =
+ runCommand(
+ "extract-api-descriptors",
+ "--sdk-deploy-jar",
+ TEST_LIBRARY_DEPLOY_JAR.toString(),
+ "--output-sdk-api-descriptors",
+ outputFile.toString());
+ ImmutableList<String> outputJarEntryNames =
+ new ZipFile(outputFile.toFile()).stream().map(ZipEntry::getName).collect(toImmutableList());
+
+ assertThat(result.getStatusCode()).isEqualTo(0);
+ assertThat(result.getOutput()).isEmpty();
+ assertThat(outputJarEntryNames)
+ .containsExactly(
+ "com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/"
+ + "testlibrary/AnnotatedClass.class");
+ }
+}
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/testlibrary/AnnotatedClass.java b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/testlibrary/AnnotatedClass.java
new file mode 100644
index 0000000..7aeee4f
--- /dev/null
+++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/testlibrary/AnnotatedClass.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 The Bazel Authors. All rights reserved.
+ *
+ * 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.
+ */
+package com.google.devtools.build.android.sandboxedsdktoolbox.apidescriptors.testlibrary;
+
+import androidx.privacysandbox.tools.PrivacySandboxService;
+
+/** Class that should be part of the SDK API descriptors, since it's properly annotated. */
+@PrivacySandboxService
+public final class AnnotatedClass {}
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/testlibrary/BUILD b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/testlibrary/BUILD
new file mode 100644
index 0000000..e208cb5
--- /dev/null
+++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/testlibrary/BUILD
@@ -0,0 +1,18 @@
+# Library to test SDK API descritptor extraction.
+
+load("//rules:rules.bzl", "android_library")
+
+package(
+ default_applicable_licenses = ["//:license"],
+ default_visibility = ["//:__subpackages__"],
+)
+
+licenses(["notice"])
+
+android_library(
+ name = "testlibrary",
+ srcs = glob(["**/*.java"]),
+ deps = [
+ "@rules_android_maven//:androidx_privacysandbox_tools_tools",
+ ],
+)
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/testlibrary/ClassThatShouldBeIgnored.java b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/testlibrary/ClassThatShouldBeIgnored.java
new file mode 100644
index 0000000..eb0dbbc
--- /dev/null
+++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/apidescriptors/testlibrary/ClassThatShouldBeIgnored.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 The Bazel Authors. All rights reserved.
+ *
+ * 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.
+ */
+package com.google.devtools.build.android.sandboxedsdktoolbox.apidescriptors.testlibrary;
+
+/**
+ * Test class that should not be present in final SDK API descritors, since it doesn't have a
+ * Privacy Sandbox tool annotation.
+ */
+public final class ClassThatShouldBeIgnored {}
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/BUILD b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/BUILD
new file mode 100644
index 0000000..c2b38d1
--- /dev/null
+++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/BUILD
@@ -0,0 +1,26 @@
+# Tests for generate-sdk-dependencies-manifest command.
+
+package(
+ default_applicable_licenses = ["//:license"],
+ default_visibility = ["//:__subpackages__"],
+)
+
+licenses(["notice"])
+
+java_test(
+ name = "GenerateSdkDependenciesManifestCommandTest",
+ size = "small",
+ srcs = ["GenerateSdkDependenciesManifestCommandTest.java"],
+ data = [
+ "testdata/com.example.firstsdkconfig.json",
+ "testdata/com.example.secondsdkconfig.json",
+ "testdata/expected_manifest_multiple_sdks.xml",
+ "testdata/expected_manifest_single_sdk.xml",
+ "testdata/test_key",
+ ],
+ deps = [
+ "//src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils",
+ "@rules_android_maven//:junit_junit",
+ "@rules_android_maven//:com_google_truth_truth",
+ ],
+)
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/GenerateSdkDependenciesManifestCommandTest.java b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/GenerateSdkDependenciesManifestCommandTest.java
new file mode 100644
index 0000000..7dc04e7
--- /dev/null
+++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/GenerateSdkDependenciesManifestCommandTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2023 The Bazel Authors. All rights reserved.
+ *
+ * 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.
+ */
+package com.google.devtools.build.android.sandboxedsdktoolbox.sdkdependenciesmanifest;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.android.sandboxedsdktoolbox.utils.Runner.runCommand;
+import static com.google.devtools.build.android.sandboxedsdktoolbox.utils.TestData.JAVATESTS_DIR;
+import static com.google.devtools.build.android.sandboxedsdktoolbox.utils.TestData.readFromAbsolutePath;
+
+import com.google.devtools.build.android.sandboxedsdktoolbox.utils.CommandResult;
+import java.nio.file.Path;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class GenerateSdkDependenciesManifestCommandTest {
+
+ @Rule public final TemporaryFolder testFolder = new TemporaryFolder();
+
+ private static final Path TEST_DATA_DIR =
+ JAVATESTS_DIR.resolve(
+ Path.of(
+ "com/google/devtools/build/android/sandboxedsdktoolbox",
+ "sdkdependenciesmanifest/testdata"));
+ private static final Path FIRST_SDK_CONFIG_JSON_PATH =
+ TEST_DATA_DIR.resolve("com.example.firstsdkconfig.json");
+ private static final Path SECOND_SDK_CONFIG_JSON_PATH =
+ TEST_DATA_DIR.resolve("com.example.secondsdkconfig.json");
+ /*
+ The test key was generated with this command, its password is "android"
+ keytool -genkeypair \
+ -alias androiddebugkey \
+ -dname "CN=Android Debug, O=Android, C=US" \
+ -keystore test_key \
+ -sigalg SHA256withDSA \
+ -validity 10950
+ */
+ private static final Path TEST_KEY_PATH = TEST_DATA_DIR.resolve("test_key");
+
+ @Test
+ public void generateManifest_forSingleSdkModuleConfig_success() throws Exception {
+ String manifestPackage = "com.example.generatedmanifest";
+ Path outputFile = testFolder.newFile().toPath();
+
+ CommandResult result =
+ runCommand(
+ "generate-sdk-dependencies-manifest",
+ "--manifest-package",
+ manifestPackage,
+ "--sdk-module-configs",
+ FIRST_SDK_CONFIG_JSON_PATH.toString(),
+ "--debug-keystore",
+ TEST_KEY_PATH.toString(),
+ "--debug-keystore-pass",
+ "android",
+ "--debug-keystore-alias",
+ "androiddebugkey",
+ "--output-manifest",
+ outputFile.toString());
+
+ assertThat(result.getStatusCode()).isEqualTo(0);
+ assertThat(result.getOutput()).isEmpty();
+ assertThat(readFromAbsolutePath(outputFile))
+ .isEqualTo(readFromAbsolutePath(TEST_DATA_DIR.resolve("expected_manifest_single_sdk.xml")));
+ }
+
+ @Test
+ public void generateManifest_forMultipleSdkModuleConfigs_success() throws Exception {
+ String manifestPackage = "com.example.generatedmanifest";
+ String configPaths =
+ String.format("%s,%s", FIRST_SDK_CONFIG_JSON_PATH, SECOND_SDK_CONFIG_JSON_PATH);
+ Path outputFile = testFolder.newFile().toPath();
+
+ CommandResult result =
+ runCommand(
+ "generate-sdk-dependencies-manifest",
+ "--manifest-package",
+ manifestPackage,
+ "--sdk-module-configs",
+ configPaths,
+ "--debug-keystore",
+ TEST_KEY_PATH.toString(),
+ "--debug-keystore-pass",
+ "android",
+ "--debug-keystore-alias",
+ "androiddebugkey",
+ "--output-manifest",
+ outputFile.toString());
+
+ assertThat(result.getStatusCode()).isEqualTo(0);
+ assertThat(result.getOutput()).isEmpty();
+ assertThat(readFromAbsolutePath(outputFile))
+ .isEqualTo(
+ readFromAbsolutePath(TEST_DATA_DIR.resolve("expected_manifest_multiple_sdks.xml")));
+ }
+}
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/testdata/com.example.firstsdkconfig.json b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/testdata/com.example.firstsdkconfig.json
new file mode 100644
index 0000000..ac7561e
--- /dev/null
+++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/testdata/com.example.firstsdkconfig.json
@@ -0,0 +1,9 @@
+{
+ "sdk_package_name": "com.example.firstsdkconfig",
+ "sdk_provider_class_name": "com.testsdk.lib.FakeSdkProvider",
+ "sdk_version": {
+ "major": 2,
+ "minor": 3,
+ "patch": 1
+ }
+}
\ No newline at end of file
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/testdata/com.example.secondsdkconfig.json b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/testdata/com.example.secondsdkconfig.json
new file mode 100644
index 0000000..7320a85
--- /dev/null
+++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/testdata/com.example.secondsdkconfig.json
@@ -0,0 +1,9 @@
+{
+ "sdk_package_name": "com.example.secondsdkconfig",
+ "sdk_provider_class_name": "com.testsdk.lib.FakeSdkProvider",
+ "sdk_version": {
+ "major": 42,
+ "minor": 1,
+ "patch": 0
+ }
+}
\ No newline at end of file
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/testdata/expected_manifest_multiple_sdks.xml b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/testdata/expected_manifest_multiple_sdks.xml
new file mode 100644
index 0000000..1360bf1
--- /dev/null
+++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/testdata/expected_manifest_multiple_sdks.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" standalone="no"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.generatedmanifest">
+ <application>
+ <uses-sdk-library android:certDigest="91:8E:A3:7D:7D:D0:E0:A0:14:9F:21:28:83:95:8A:F0:80:E6:F9:7B:4D:5A:39:01:76:02:E8:2D:7D:FF:A9:10" android:name="com.example.firstsdkconfig" android:versionMajor="20003"/>
+ <uses-sdk-library android:certDigest="91:8E:A3:7D:7D:D0:E0:A0:14:9F:21:28:83:95:8A:F0:80:E6:F9:7B:4D:5A:39:01:76:02:E8:2D:7D:FF:A9:10" android:name="com.example.secondsdkconfig" android:versionMajor="420001"/>
+ </application>
+</manifest>
\ No newline at end of file
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/testdata/expected_manifest_single_sdk.xml b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/testdata/expected_manifest_single_sdk.xml
new file mode 100644
index 0000000..4bfc234
--- /dev/null
+++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/testdata/expected_manifest_single_sdk.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8" standalone="no"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.generatedmanifest">
+ <application>
+ <uses-sdk-library android:certDigest="91:8E:A3:7D:7D:D0:E0:A0:14:9F:21:28:83:95:8A:F0:80:E6:F9:7B:4D:5A:39:01:76:02:E8:2D:7D:FF:A9:10" android:name="com.example.firstsdkconfig" android:versionMajor="20003"/>
+ </application>
+</manifest>
\ No newline at end of file
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/testdata/test_key b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/testdata/test_key
new file mode 100644
index 0000000..e0061a5
--- /dev/null
+++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/sdkdependenciesmanifest/testdata/test_key
Binary files differ
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils/BUILD b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils/BUILD
new file mode 100644
index 0000000..4bf26b6
--- /dev/null
+++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils/BUILD
@@ -0,0 +1,16 @@
+# Common test utilities for SandboxedSdkToolbox.
+package(
+ default_applicable_licenses = ["//:license"],
+ default_visibility = ["//:__subpackages__"],
+)
+
+licenses(["notice"])
+
+java_library(
+ name = "utils",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox:sandboxed_sdk_toolbox_lib",
+ "@rules_android_maven//:info_picocli_picocli",
+ ],
+)
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils/CommandResult.java b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils/CommandResult.java
new file mode 100644
index 0000000..cafd1b2
--- /dev/null
+++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils/CommandResult.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 The Bazel Authors. All rights reserved.
+ *
+ * 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.
+ */
+package com.google.devtools.build.android.sandboxedsdktoolbox.utils;
+
+/** The result from executing a SandboxedSdkToolbox command. */
+public final class CommandResult {
+ private final int statusCode;
+ private final String output;
+
+ CommandResult(int statusCode, String output) {
+ this.statusCode = statusCode;
+ this.output = output;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+
+ public String getOutput() {
+ return output;
+ }
+}
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils/Runner.java b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils/Runner.java
new file mode 100644
index 0000000..bfd3ec9
--- /dev/null
+++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils/Runner.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 The Bazel Authors. All rights reserved.
+ *
+ * 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.
+ */
+package com.google.devtools.build.android.sandboxedsdktoolbox.utils;
+
+import com.google.devtools.build.android.sandboxedsdktoolbox.SandboxedSdkToolbox;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import picocli.CommandLine;
+
+/** Utilities for running SandboxedSdkToolbox commands. */
+public final class Runner {
+ public static CommandResult runCommand(String... parameters) {
+ CommandLine command = SandboxedSdkToolbox.create();
+ StringWriter stringWriter = new StringWriter();
+
+ command.setOut(new PrintWriter(stringWriter));
+ int statusCode = command.execute(parameters);
+ String output = stringWriter.toString();
+
+ return new CommandResult(statusCode, output);
+ }
+
+ private Runner() {}
+}
diff --git a/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils/TestData.java b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils/TestData.java
new file mode 100644
index 0000000..fdf96c5
--- /dev/null
+++ b/src/tools/javatests/com/google/devtools/build/android/sandboxedsdktoolbox/utils/TestData.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 The Bazel Authors. All rights reserved.
+ *
+ * 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.
+ */
+package com.google.devtools.build.android.sandboxedsdktoolbox.utils;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/** Utilities for test data. */
+public final class TestData {
+
+ /** Path to the javatests directory in runfiles. */
+ public static final Path JAVATESTS_DIR =
+ Path.of(
+ System.getenv("TEST_SRCDIR"),
+ "/build_bazel_rules_android/src/tools/javatests/");
+
+ /** Reads the contents of a file, assuming its path is absolute. */
+ public static String readFromAbsolutePath(Path absolutePath) throws Exception {
+ return String.join("\n", Files.readAllLines(absolutePath, UTF_8));
+ }
+
+ private TestData() {}
+}
diff --git a/test/bashunit/BUILD b/test/bashunit/BUILD
new file mode 100644
index 0000000..3404758
--- /dev/null
+++ b/test/bashunit/BUILD
@@ -0,0 +1,43 @@
+load("@rules_python//python:py_test.bzl", "py_test")
+
+package(
+ default_applicable_licenses = ["//:license"],
+ default_visibility = [
+ "//test:__subpackages__",
+ ],
+)
+
+licenses(["notice"])
+
+exports_files(
+ ["unittest.bash"],
+)
+
+sh_library(
+ name = "bashunit",
+ testonly = True,
+ srcs = [
+ "unittest.bash",
+ "unittest_utils.sh",
+ ],
+)
+
+# Test bashunit with python to avoid recursion.
+py_test(
+ name = "bashunit_test",
+ size = "medium",
+ srcs = ["unittest_test.py"],
+ data = [
+ ":bashunit",
+ # This test relies on writing shell scripts that use bash runfiles
+ # to load the actual copy of unittest.bash being tested.
+ "@bazel_tools//tools/bash/runfiles",
+ ],
+ main = "unittest_test.py",
+ python_version = "PY3",
+ srcs_version = "PY3",
+ tags = [
+ "manual", # TODO(b/266084774): Re-enable this.
+ "no_windows", # test runs bash scripts in a subprocess
+ ],
+)
diff --git a/test/bashunit/unittest.bash b/test/bashunit/unittest.bash
new file mode 100644
index 0000000..c88ba2c
--- /dev/null
+++ b/test/bashunit/unittest.bash
@@ -0,0 +1,845 @@
+#!/bin/bash
+#
+# Copyright 2015 The Bazel Authors. All rights reserved.
+#
+# 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.
+#
+# Common utility file for Bazel shell tests
+#
+# unittest.bash: a unit test framework in Bash.
+#
+# A typical test suite looks like so:
+#
+# ------------------------------------------------------------------------
+# #!/bin/bash
+#
+# source path/to/unittest.bash || exit 1
+#
+# # Test that foo works.
+# function test_foo() {
+# foo >$TEST_log || fail "foo failed";
+# expect_log "blah" "Expected to see 'blah' in output of 'foo'."
+# }
+#
+# # Test that bar works.
+# function test_bar() {
+# bar 2>$TEST_log || fail "bar failed";
+# expect_not_log "ERROR" "Unexpected error from 'bar'."
+# ...
+# assert_equals $x $y
+# }
+#
+# run_suite "Test suite for blah"
+# ------------------------------------------------------------------------
+#
+# Each test function is considered to pass iff fail() is not called
+# while it is active. fail() may be called directly, or indirectly
+# via other assertions such as expect_log(). run_suite must be called
+# at the very end.
+#
+# A test suite may redefine functions "set_up" and/or "tear_down";
+# these functions are executed before and after each test function,
+# respectively. Similarly, "cleanup" and "timeout" may be redefined,
+# and these function are called upon exit (of any kind) or a timeout.
+#
+# The user can pass --test_filter to blaze test to select specific tests
+# to run with Bash globs. A union of tests matching any of the provided globs
+# will be run. Additionally the user may define TESTS=(test_foo test_bar ...) to
+# specify a subset of test functions to execute, for example, a working set
+# during debugging. By default, all functions called test_* will be executed.
+#
+# This file provides utilities for assertions over the output of a
+# command. The output of the command under test is directed to the
+# file $TEST_log, and then the expect_log* assertions can be used to
+# test for the presence of certain regular expressions in that file.
+#
+# The test framework is responsible for restoring the original working
+# directory before each test.
+#
+# The order in which test functions are run is not defined, so it is
+# important that tests clean up after themselves.
+#
+# Each test will be run in a new subshell.
+#
+# Functions named __* are not intended for use by clients.
+#
+# This framework implements the "test sharding protocol".
+#
+
+[[ -n "$BASH_VERSION" ]] ||
+ { echo "unittest.bash only works with bash!" >&2; exit 1; }
+
+export BAZEL_SHELL_TEST=1
+
+DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+
+# Load the environment support utilities.
+source "${DIR}/unittest_utils.sh" || { echo "unittest_utils.sh not found" >&2; exit 1; }
+
+#### Global variables:
+
+TEST_name="" # The name of the current test.
+
+TEST_log=$TEST_TMPDIR/log # The log file over which the
+ # expect_log* assertions work. Must
+ # be absolute to be robust against
+ # tests invoking 'cd'!
+
+TEST_passed="true" # The result of the current test;
+ # failed assertions cause this to
+ # become false.
+
+# These variables may be overridden by the test suite:
+
+TESTS=() # A subset or "working set" of test
+ # functions that should be run. By
+ # default, all tests called test_* are
+ # run.
+
+_TEST_FILTERS=() # List of globs to use to filter the tests.
+ # If non-empty, all tests matching at least one
+ # of the globs are run and test list provided in
+ # the arguments is ignored if present.
+
+__in_tear_down=0 # Indicates whether we are in `tear_down` phase
+ # of test. Used to avoid re-entering `tear_down`
+ # on failures within it.
+
+if (( $# > 0 )); then
+ (
+ IFS=':'
+ echo "WARNING: Passing test names in arguments (--test_arg) is deprecated, please use --test_filter='$*' instead." >&2
+ )
+
+ # Legacy behavior is to ignore missing regexp, but with errexit
+ # the following line fails without || true.
+ # TODO(dmarting): maybe we should revisit the way of selecting
+ # test with that framework (use Bazel's environment variable instead).
+ TESTS=($(for i in "$@"; do echo $i; done | grep ^test_ || true))
+ if (( ${#TESTS[@]} == 0 )); then
+ echo "WARNING: Arguments do not specify tests!" >&2
+ fi
+fi
+# TESTBRIDGE_TEST_ONLY contains the value of --test_filter, if any. We want to
+# preferentially use that instead of $@ to determine which tests to run.
+if [[ ${TESTBRIDGE_TEST_ONLY:-} != "" ]]; then
+ if (( ${#TESTS[@]} != 0 )); then
+ echo "WARNING: Both --test_arg and --test_filter specified, ignoring --test_arg" >&2
+ TESTS=()
+ fi
+ # Split TESTBRIDGE_TEST_ONLY on colon and store it in `_TEST_FILTERS` array.
+ IFS=':' read -r -a _TEST_FILTERS <<< "$TESTBRIDGE_TEST_ONLY"
+fi
+
+TEST_verbose="true" # Whether or not to be verbose. A
+ # command; "true" or "false" are
+ # acceptable. The default is: true.
+
+TEST_script="$0" # Full path to test script
+# Check if the script path is absolute, if not prefix the PWD.
+if [[ ! "$TEST_script" = /* ]]; then
+ TEST_script="${PWD}/$0"
+fi
+
+
+#### Internal functions
+
+function __show_log() {
+ echo "-- Test log: -----------------------------------------------------------"
+ [[ -e $TEST_log ]] && cat "$TEST_log" || echo "(Log file did not exist.)"
+ echo "------------------------------------------------------------------------"
+}
+
+# Usage: __pad <title> <pad-char>
+# Print $title padded to 80 columns with $pad_char.
+function __pad() {
+ local title=$1
+ local pad=$2
+ # Ignore the subshell error -- `head` closes the fd before reading to the
+ # end, therefore the subshell will get SIGPIPE while stuck in `write`.
+ {
+ echo -n "${pad}${pad} ${title} "
+ printf "%80s" " " | tr ' ' "$pad"
+ } | head -c 80 || true
+ echo
+}
+
+#### Exported functions
+
+# Usage: init_test ...
+# Deprecated. Has no effect.
+function init_test() {
+ :
+}
+
+
+# Usage: set_up
+# Called before every test function. May be redefined by the test suite.
+function set_up() {
+ :
+}
+
+# Usage: tear_down
+# Called after every test function. May be redefined by the test suite.
+function tear_down() {
+ :
+}
+
+# Usage: cleanup
+# Called upon eventual exit of the test suite. May be redefined by
+# the test suite.
+function cleanup() {
+ :
+}
+
+# Usage: timeout
+# Called upon early exit from a test due to timeout.
+function timeout() {
+ :
+}
+
+# Usage: testenv_set_up
+# Called prior to set_up. For use by testenv.sh.
+function testenv_set_up() {
+ :
+}
+
+# Usage: testenv_tear_down
+# Called after tear_down. For use by testenv.sh.
+function testenv_tear_down() {
+ :
+}
+
+# Usage: fail <message> [<message> ...]
+# Print failure message with context information, and mark the test as
+# a failure. The context includes a stacktrace including the longest sequence
+# of calls outside this module. (We exclude the top and bottom portions of
+# the stack because they just add noise.) Also prints the contents of
+# $TEST_log.
+function fail() {
+ __show_log >&2
+ echo "${TEST_name} FAILED: $*." >&2
+ # Keep the original error message if we fail in `tear_down` after a failure.
+ [[ "${TEST_passed}" == "true" ]] && echo "$@" >"$TEST_TMPDIR"/__fail
+ TEST_passed="false"
+ __show_stack
+ # Cleanup as we are leaving the subshell now
+ __run_tear_down_after_failure
+ exit 1
+}
+
+function __run_tear_down_after_failure() {
+ # Skip `tear_down` after a failure in `tear_down` to prevent infinite
+ # recursion.
+ (( __in_tear_down )) && return
+ __in_tear_down=1
+ echo -e "\nTear down:\n" >&2
+ tear_down
+ testenv_tear_down
+}
+
+# Usage: warn <message>
+# Print a test warning with context information.
+# The context includes a stacktrace including the longest sequence
+# of calls outside this module. (We exclude the top and bottom portions of
+# the stack because they just add noise.)
+function warn() {
+ __show_log >&2
+ echo "${TEST_name} WARNING: $1." >&2
+ __show_stack
+
+ if [[ -n "${TEST_WARNINGS_OUTPUT_FILE:-}" ]]; then
+ echo "${TEST_name} WARNING: $1." >> "$TEST_WARNINGS_OUTPUT_FILE"
+ fi
+}
+
+# Usage: show_stack
+# Prints the portion of the stack that does not belong to this module,
+# i.e. the user's code that called a failing assertion. Stack may not
+# be available if Bash is reading commands from stdin; an error is
+# printed in that case.
+__show_stack() {
+ local i=0
+ local trace_found=0
+
+ # Skip over active calls within this module:
+ while (( i < ${#FUNCNAME[@]} )) && [[ ${BASH_SOURCE[i]:-} == "${BASH_SOURCE[0]}" ]]; do
+ (( ++i ))
+ done
+
+ # Show all calls until the next one within this module (typically run_suite):
+ while (( i < ${#FUNCNAME[@]} )) && [[ ${BASH_SOURCE[i]:-} != "${BASH_SOURCE[0]}" ]]; do
+ # Read online docs for BASH_LINENO to understand the strange offset.
+ # Undefined can occur in the BASH_SOURCE stack apparently when one exits from a subshell
+ echo "${BASH_SOURCE[i]:-"Unknown"}:${BASH_LINENO[i - 1]:-"Unknown"}: in call to ${FUNCNAME[i]:-"Unknown"}" >&2
+ (( ++i ))
+ trace_found=1
+ done
+
+ (( trace_found )) || echo "[Stack trace not available]" >&2
+}
+
+# Usage: expect_log <regexp> [error-message]
+# Asserts that $TEST_log matches regexp. Prints the contents of
+# $TEST_log and the specified (optional) error message otherwise, and
+# returns non-zero.
+function expect_log() {
+ local pattern=$1
+ local message=${2:-Expected regexp "$pattern" not found}
+ grep -sq -- "$pattern" $TEST_log && return 0
+
+ fail "$message"
+ return 1
+}
+
+# Usage: expect_log_warn <regexp> [error-message]
+# Warns if $TEST_log does not match regexp. Prints the contents of
+# $TEST_log and the specified (optional) error message on mismatch.
+function expect_log_warn() {
+ local pattern=$1
+ local message=${2:-Expected regexp "$pattern" not found}
+ grep -sq -- "$pattern" $TEST_log && return 0
+
+ warn "$message"
+ return 1
+}
+
+# Usage: expect_log_once <regexp> [error-message]
+# Asserts that $TEST_log contains one line matching <regexp>.
+# Prints the contents of $TEST_log and the specified (optional)
+# error message otherwise, and returns non-zero.
+function expect_log_once() {
+ local pattern=$1
+ local message=${2:-Expected regexp "$pattern" not found exactly once}
+ expect_log_n "$pattern" 1 "$message"
+}
+
+# Usage: expect_log_n <regexp> <count> [error-message]
+# Asserts that $TEST_log contains <count> lines matching <regexp>.
+# Prints the contents of $TEST_log and the specified (optional)
+# error message otherwise, and returns non-zero.
+function expect_log_n() {
+ local pattern=$1
+ local expectednum=${2:-1}
+ local message=${3:-Expected regexp "$pattern" not found exactly $expectednum times}
+ local count=$(grep -sc -- "$pattern" $TEST_log)
+ (( count == expectednum )) && return 0
+ fail "$message"
+ return 1
+}
+
+# Usage: expect_not_log <regexp> [error-message]
+# Asserts that $TEST_log does not match regexp. Prints the contents
+# of $TEST_log and the specified (optional) error message otherwise, and
+# returns non-zero.
+function expect_not_log() {
+ local pattern=$1
+ local message=${2:-Unexpected regexp "$pattern" found}
+ grep -sq -- "$pattern" $TEST_log || return 0
+
+ fail "$message"
+ return 1
+}
+
+# Usage: expect_query_targets <arguments>
+# Checks that log file contains exactly the targets in the argument list.
+function expect_query_targets() {
+ for arg in "$@"; do
+ expect_log_once "^$arg$"
+ done
+
+# Checks that the number of lines started with '//' equals to the number of
+# arguments provided.
+ expect_log_n "^//[^ ]*$" $#
+}
+
+# Usage: expect_log_with_timeout <regexp> <timeout> [error-message]
+# Waits for the given regexp in the $TEST_log for up to timeout seconds.
+# Prints the contents of $TEST_log and the specified (optional)
+# error message otherwise, and returns non-zero.
+function expect_log_with_timeout() {
+ local pattern=$1
+ local timeout=$2
+ local message=${3:-Regexp "$pattern" not found in "$timeout" seconds}
+ local count=0
+ while (( count < timeout )); do
+ grep -sq -- "$pattern" "$TEST_log" && return 0
+ let count=count+1
+ sleep 1
+ done
+
+ grep -sq -- "$pattern" "$TEST_log" && return 0
+ fail "$message"
+ return 1
+}
+
+# Usage: expect_cmd_with_timeout <expected> <cmd> [timeout]
+# Repeats the command once a second for up to timeout seconds (10s by default),
+# until the output matches the expected value. Fails and returns 1 if
+# the command does not return the expected value in the end.
+function expect_cmd_with_timeout() {
+ local expected="$1"
+ local cmd="$2"
+ local timeout=${3:-10}
+ local count=0
+ while (( count < timeout )); do
+ local actual="$($cmd)"
+ [[ "$expected" == "$actual" ]] && return 0
+ (( ++count ))
+ sleep 1
+ done
+
+ [[ "$expected" == "$actual" ]] && return 0
+ fail "Expected '${expected}' within ${timeout}s, was '${actual}'"
+ return 1
+}
+
+# Usage: assert_one_of <expected_list>... <actual>
+# Asserts that actual is one of the items in expected_list
+#
+# Example:
+# local expected=( "foo", "bar", "baz" )
+# assert_one_of $expected $actual
+function assert_one_of() {
+ local args=("$@")
+ local last_arg_index=$((${#args[@]} - 1))
+ local actual=${args[last_arg_index]}
+ unset args[last_arg_index]
+ for expected_item in "${args[@]}"; do
+ [[ "$expected_item" == "$actual" ]] && return 0
+ done;
+
+ fail "Expected one of '${args[*]}', was '$actual'"
+ return 1
+}
+
+# Usage: assert_not_one_of <expected_list>... <actual>
+# Asserts that actual is not one of the items in expected_list
+#
+# Example:
+# local unexpected=( "foo", "bar", "baz" )
+# assert_not_one_of $unexpected $actual
+function assert_not_one_of() {
+ local args=("$@")
+ local last_arg_index=$((${#args[@]} - 1))
+ local actual=${args[last_arg_index]}
+ unset args[last_arg_index]
+ for expected_item in "${args[@]}"; do
+ if [[ "$expected_item" == "$actual" ]]; then
+ fail "'${args[*]}' contains '$actual'"
+ return 1
+ fi
+ done;
+
+ return 0
+}
+
+# Usage: assert_equals <expected> <actual>
+# Asserts [[ expected == actual ]].
+function assert_equals() {
+ local expected=$1 actual=$2
+ [[ "$expected" == "$actual" ]] && return 0
+
+ fail "Expected '$expected', was '$actual'"
+ return 1
+}
+
+# Usage: assert_not_equals <unexpected> <actual>
+# Asserts [[ unexpected != actual ]].
+function assert_not_equals() {
+ local unexpected=$1 actual=$2
+ [[ "$unexpected" != "$actual" ]] && return 0;
+
+ fail "Expected not '${unexpected}', was '${actual}'"
+ return 1
+}
+
+# Usage: assert_contains <regexp> <file> [error-message]
+# Asserts that file matches regexp. Prints the contents of
+# file and the specified (optional) error message otherwise, and
+# returns non-zero.
+function assert_contains() {
+ local pattern=$1
+ local file=$2
+ local message=${3:-Expected regexp "$pattern" not found in "$file"}
+ grep -sq -- "$pattern" "$file" && return 0
+
+ cat "$file" >&2
+ fail "$message"
+ return 1
+}
+
+# Usage: assert_not_contains <regexp> <file> [error-message]
+# Asserts that file does not match regexp. Prints the contents of
+# file and the specified (optional) error message otherwise, and
+# returns non-zero.
+function assert_not_contains() {
+ local pattern=$1
+ local file=$2
+ local message=${3:-Expected regexp "$pattern" found in "$file"}
+
+ if [[ -f "$file" ]]; then
+ grep -sq -- "$pattern" "$file" || return 0
+ else
+ fail "$file is not a file: $message"
+ return 1
+ fi
+
+ cat "$file" >&2
+ fail "$message"
+ return 1
+}
+
+function assert_contains_n() {
+ local pattern=$1
+ local expectednum=${2:-1}
+ local file=$3
+ local message=${4:-Expected regexp "$pattern" not found exactly $expectednum times}
+ local count
+ if [[ -f "$file" ]]; then
+ count=$(grep -sc -- "$pattern" "$file")
+ else
+ fail "$file is not a file: $message"
+ return 1
+ fi
+ (( count == expectednum )) && return 0
+
+ cat "$file" >&2
+ fail "$message"
+ return 1
+}
+
+# Updates the global variables TESTS if
+# sharding is enabled, i.e. ($TEST_TOTAL_SHARDS > 0).
+function __update_shards() {
+ [[ -z "${TEST_TOTAL_SHARDS-}" ]] && return 0
+
+ (( TEST_TOTAL_SHARDS > 0 )) ||
+ { echo "Invalid total shards ${TEST_TOTAL_SHARDS}" >&2; exit 1; }
+
+ (( TEST_SHARD_INDEX < 0 || TEST_SHARD_INDEX >= TEST_TOTAL_SHARDS )) &&
+ { echo "Invalid shard ${TEST_SHARD_INDEX}" >&2; exit 1; }
+
+ IFS=$'\n' read -rd $'\0' -a TESTS < <(
+ for test in "${TESTS[@]}"; do echo "$test"; done |
+ awk "NR % ${TEST_TOTAL_SHARDS} == ${TEST_SHARD_INDEX}" &&
+ echo -en '\0')
+
+ [[ -z "${TEST_SHARD_STATUS_FILE-}" ]] || touch "$TEST_SHARD_STATUS_FILE"
+}
+
+# Usage: __test_terminated <signal-number>
+# Handler that is called when the test terminated unexpectedly
+function __test_terminated() {
+ __show_log >&2
+ echo "$TEST_name FAILED: terminated by signal $1." >&2
+ TEST_passed="false"
+ __show_stack
+ timeout
+ exit 1
+}
+
+# Usage: __test_terminated_err
+# Handler that is called when the test terminated unexpectedly due to "errexit".
+function __test_terminated_err() {
+ # When a subshell exits due to signal ERR, its parent shell also exits,
+ # thus the signal handler is called recursively and we print out the
+ # error message and stack trace multiple times. We're only interested
+ # in the first one though, as it contains the most information, so ignore
+ # all following.
+ if [[ -f $TEST_TMPDIR/__err_handled ]]; then
+ exit 1
+ fi
+ __show_log >&2
+ if [[ ! -z "$TEST_name" ]]; then
+ echo -n "$TEST_name " >&2
+ fi
+ echo "FAILED: terminated because this command returned a non-zero status:" >&2
+ touch $TEST_TMPDIR/__err_handled
+ TEST_passed="false"
+ __show_stack
+ # If $TEST_name is still empty, the test suite failed before we even started
+ # to run tests, so we shouldn't call tear_down.
+ if [[ -n "$TEST_name" ]]; then
+ __run_tear_down_after_failure
+ fi
+ exit 1
+}
+
+# Usage: __trap_with_arg <handler> <signals ...>
+# Helper to install a trap handler for several signals preserving the signal
+# number, so that the signal number is available to the trap handler.
+function __trap_with_arg() {
+ func="$1" ; shift
+ for sig ; do
+ trap "$func $sig" "$sig"
+ done
+}
+
+# Usage: <node> <block>
+# Adds the block to the given node in the report file. Quotes in the in
+# arguments need to be escaped.
+function __log_to_test_report() {
+ local node="$1"
+ local block="$2"
+ if [[ ! -e "$XML_OUTPUT_FILE" ]]; then
+ local xml_header='<?xml version="1.0" encoding="UTF-8"?>'
+ echo "${xml_header}<testsuites></testsuites>" > "$XML_OUTPUT_FILE"
+ fi
+
+ # replace match on node with block and match
+ # replacement expression only needs escaping for quotes
+ perl -e "\
+\$input = @ARGV[0]; \
+\$/=undef; \
+open FILE, '+<$XML_OUTPUT_FILE'; \
+\$content = <FILE>; \
+if (\$content =~ /($node.*)\$/) { \
+ seek FILE, 0, 0; \
+ print FILE \$\` . \$input . \$1; \
+}; \
+close FILE" "$block"
+}
+
+# Usage: <total> <passed>
+# Adds the test summaries to the xml nodes.
+function __finish_test_report() {
+ local suite_name="$1"
+ local total="$2"
+ local passed="$3"
+ local failed=$((total - passed))
+
+ # Update the xml output with the suite name and total number of
+ # passed/failed tests.
+ cat "$XML_OUTPUT_FILE" | \
+ sed \
+ "s/<testsuites>/<testsuites tests=\"$total\" failures=\"0\" errors=\"$failed\">/" | \
+ sed \
+ "s/<testsuite>/<testsuite name=\"${suite_name}\" tests=\"$total\" failures=\"0\" errors=\"$failed\">/" \
+ > "${XML_OUTPUT_FILE}.bak"
+
+ rm -f "$XML_OUTPUT_FILE"
+ mv "${XML_OUTPUT_FILE}.bak" "$XML_OUTPUT_FILE"
+}
+
+# Multi-platform timestamp function
+UNAME=$(uname -s | tr 'A-Z' 'a-z')
+if [[ "$UNAME" == "linux" ]] || [[ "$UNAME" =~ msys_nt* ]]; then
+ function timestamp() {
+ echo $(($(date +%s%N)/1000000))
+ }
+else
+ function timestamp() {
+ # macOS and BSDs do not have %N, so Python is the best we can do.
+ # LC_ALL=C works around python 3.8 and 3.9 crash on macOS when the
+ # filesystem encoding is unspecified (e.g. when LANG=en_US).
+ local PYTHON=python
+ command -v python3 &> /dev/null && PYTHON=python3
+ LC_ALL=C "${PYTHON}" -c 'import time; print(int(round(time.time() * 1000)))'
+ }
+fi
+
+function get_run_time() {
+ local ts_start=$1
+ local ts_end=$2
+ run_time_ms=$((ts_end - ts_start))
+ echo $((run_time_ms / 1000)).${run_time_ms: -3}
+}
+
+# Usage: run_tests <suite-comment>
+# Must be called from the end of the user's test suite.
+# Calls exit with zero on success, non-zero otherwise.
+function run_suite() {
+ local message="$1"
+ # The name of the suite should be the script being run, which
+ # will be the filename with the ".sh" extension removed.
+ local suite_name="$(basename "$0")"
+
+ echo >&2
+ echo "$message" >&2
+ echo >&2
+
+ __log_to_test_report "<\/testsuites>" "<testsuite></testsuite>"
+
+ local total=0
+ local passed=0
+
+ atexit "cleanup"
+
+ # If the user didn't specify an explicit list of tests (e.g. a
+ # working set), use them all.
+ if (( ${#TESTS[@]} == 0 )); then
+ # Even if there aren't any tests, this needs to succeed.
+ local all_tests=()
+ IFS=$'\n' read -d $'\0' -ra all_tests < <(
+ declare -F | awk '{print $3}' | grep ^test_ || true; echo -en '\0')
+
+ if (( "${#_TEST_FILTERS[@]}" == 0 )); then
+ # Use ${array[@]+"${array[@]}"} idiom to avoid errors when running with
+ # Bash version <= 4.4 with `nounset` when `all_tests` is empty (
+ # https://github.com/bminor/bash/blob/a0c0a00fc419b7bc08202a79134fcd5bc0427071/CHANGES#L62-L63).
+ TESTS=("${all_tests[@]+${all_tests[@]}}")
+ else
+ for t in "${all_tests[@]+${all_tests[@]}}"; do
+ local matches=0
+ for f in "${_TEST_FILTERS[@]}"; do
+ # We purposely want to glob match.
+ # shellcheck disable=SC2053
+ [[ "$t" = $f ]] && matches=1 && break
+ done
+ if (( matches )); then
+ TESTS+=("$t")
+ fi
+ done
+ fi
+
+ elif [[ -n "${TEST_WARNINGS_OUTPUT_FILE:-}" ]]; then
+ if grep -q "TESTS=" "$TEST_script" ; then
+ echo "TESTS variable overridden in sh_test. Please remove before submitting" \
+ >> "$TEST_WARNINGS_OUTPUT_FILE"
+ fi
+ fi
+
+ # Reset TESTS in the common case where it contains a single empty string.
+ if [[ -z "${TESTS[*]-}" ]]; then
+ TESTS=()
+ fi
+ local original_tests_size=${#TESTS[@]}
+
+ __update_shards
+
+ if [[ "${#TESTS[@]}" -ne 0 ]]; then
+ for TEST_name in "${TESTS[@]}"; do
+ >"$TEST_log" # Reset the log.
+ TEST_passed="true"
+
+ (( ++total ))
+ if [[ "$TEST_verbose" == "true" ]]; then
+ date >&2
+ __pad "$TEST_name" '*' >&2
+ fi
+
+ local run_time="0.0"
+ rm -f "${TEST_TMPDIR}"/{__ts_start,__ts_end}
+
+ if [[ "$(type -t "$TEST_name")" == function ]]; then
+ # Save exit handlers eventually set.
+ local SAVED_ATEXIT="$ATEXIT";
+ ATEXIT=
+
+ # Run test in a subshell.
+ rm -f "${TEST_TMPDIR}"/__err_handled
+ __trap_with_arg __test_terminated INT KILL PIPE TERM ABRT FPE ILL QUIT SEGV
+
+ # Remember -o pipefail value and disable it for the subshell result
+ # collection.
+ if [[ "${SHELLOPTS}" =~ (^|:)pipefail(:|$) ]]; then
+ local __opt_switch=-o
+ else
+ local __opt_switch=+o
+ fi
+ set +o pipefail
+ (
+ set "${__opt_switch}" pipefail
+ # if errexit is enabled, make sure we run cleanup and collect the log.
+ if [[ "$-" = *e* ]]; then
+ set -E
+ trap __test_terminated_err ERR
+ fi
+ timestamp >"${TEST_TMPDIR}"/__ts_start
+ testenv_set_up
+ set_up
+ eval "$TEST_name"
+ __in_tear_down=1
+ tear_down
+ testenv_tear_down
+ timestamp >"${TEST_TMPDIR}"/__ts_end
+ test "$TEST_passed" == "true"
+ ) 2>&1 | tee "${TEST_TMPDIR}"/__log
+ # Note that tee will prevent the control flow continuing if the test
+ # spawned any processes which are still running and have not closed
+ # their stdout.
+
+ test_subshell_status=${PIPESTATUS[0]}
+ set "${__opt_switch}" pipefail
+ if (( test_subshell_status != 0 )); then
+ TEST_passed="false"
+ # Ensure that an end time is recorded in case the test subshell
+ # terminated prematurely.
+ [[ -f "$TEST_TMPDIR"/__ts_end ]] || timestamp >"$TEST_TMPDIR"/__ts_end
+ fi
+
+ # Calculate run time for the testcase.
+ local ts_start
+ ts_start=$(<"${TEST_TMPDIR}"/__ts_start)
+ local ts_end
+ ts_end=$(<"${TEST_TMPDIR}"/__ts_end)
+ run_time=$(get_run_time $ts_start $ts_end)
+
+ # Eventually restore exit handlers.
+ if [[ -n "$SAVED_ATEXIT" ]]; then
+ ATEXIT="$SAVED_ATEXIT"
+ trap "$ATEXIT" EXIT
+ fi
+ else # Bad test explicitly specified in $TESTS.
+ fail "Not a function: '$TEST_name'"
+ fi
+
+ local testcase_tag=""
+
+ local red='\033[0;31m'
+ local green='\033[0;32m'
+ local no_color='\033[0m'
+
+ if [[ "$TEST_verbose" == "true" ]]; then
+ echo >&2
+ fi
+
+ if [[ "$TEST_passed" == "true" ]]; then
+ if [[ "$TEST_verbose" == "true" ]]; then
+ echo -e "${green}PASSED${no_color}: ${TEST_name}" >&2
+ fi
+ (( ++passed ))
+ testcase_tag="<testcase name=\"${TEST_name}\" status=\"run\" time=\"${run_time}\" classname=\"\"></testcase>"
+ else
+ echo -e "${red}FAILED${no_color}: ${TEST_name}" >&2
+ # end marker in CDATA cannot be escaped, we need to split the CDATA sections
+ log=$(sed 's/]]>/]]>]]><![CDATA[/g' "${TEST_TMPDIR}"/__log)
+ fail_msg=$(cat "${TEST_TMPDIR}"/__fail 2> /dev/null || echo "No failure message")
+ # Replacing '&' with '&', '<' with '<', '>' with '>', and '"' with '"'
+ escaped_fail_msg=$(echo "$fail_msg" | sed 's/&/\&/g' | sed 's/</\</g' | sed 's/>/\>/g' | sed 's/"/\"/g')
+ testcase_tag="<testcase name=\"${TEST_name}\" status=\"run\" time=\"${run_time}\" classname=\"\"><error message=\"${escaped_fail_msg}\"><![CDATA[${log}]]></error></testcase>"
+ fi
+
+ if [[ "$TEST_verbose" == "true" ]]; then
+ echo >&2
+ fi
+ __log_to_test_report "<\/testsuite>" "$testcase_tag"
+ done
+ fi
+
+ __finish_test_report "$suite_name" $total $passed
+ __pad "${passed} / ${total} tests passed." '*' >&2
+ if (( original_tests_size == 0 )); then
+ __pad "No tests found." '*'
+ exit 1
+ elif (( total != passed )); then
+ __pad "There were errors." '*' >&2
+ exit 1
+ elif (( total == 0 )); then
+ __pad "No tests executed due to sharding. Check your test's shard_count." '*'
+ __pad "Succeeding anyway." '*'
+ fi
+
+ exit 0
+}
diff --git a/test/bashunit/unittest_test.py b/test/bashunit/unittest_test.py
new file mode 100644
index 0000000..2ecc17f
--- /dev/null
+++ b/test/bashunit/unittest_test.py
@@ -0,0 +1,738 @@
+# Copyright 2020 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""Tests for unittest.bash."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import os
+import re
+import shutil
+import stat
+import subprocess
+import tempfile
+import textwrap
+import unittest
+
+# The test setup for this external test is forwarded to the internal bash test.
+# This allows the internal test to use the same runfiles to load unittest.bash.
+_TEST_PREAMBLE = """
+#!/bin/bash
+# --- begin runfiles.bash initialization ---
+if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
+ source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash"
+else
+ echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash"
+ exit 1
+fi
+# --- end runfiles.bash initialization ---
+
+echo "Writing XML to ${XML_OUTPUT_FILE}"
+
+source "$(rlocation "build_bazel_rules_android/test/bashunit/unittest.bash")" \
+ || { echo "Could not source unittest.bash" >&2; exit 1; }
+"""
+
+ANSI_ESCAPE = re.compile(r"(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]")
+
+
+def remove_ansi(line):
+ """Remove ANSI-style escape sequences from the input."""
+ return ANSI_ESCAPE.sub("", line)
+
+
+class TestResult(object):
+ """Save test results for easy checking."""
+
+ def __init__(self, asserter, return_code, output, xmlfile):
+ self._asserter = asserter
+ self._return_code = return_code
+ self._output = remove_ansi(output)
+
+ # Read in the XML result file.
+ if os.path.isfile(xmlfile):
+ with open(xmlfile, "r") as f:
+ self._xml = f.read()
+ else:
+ # Unable to read the file, errors will be reported later.
+ self._xml = ""
+
+ # Methods to assert on the state of the results.
+
+ def assertLogMessage(self, message):
+ self.assertExactlyOneMatch(self._output, message)
+
+ def assertNotLogMessage(self, message):
+ self._asserter.assertNotRegex(self._output, message)
+
+ def assertXmlMessage(self, message):
+ self.assertExactlyOneMatch(self._xml, message)
+
+ def assertNotXmlMessage(self, message):
+ self._asserter.assertNotRegex(self._xml, message)
+
+ def assertSuccess(self, suite_name):
+ self._asserter.assertEqual(0, self._return_code,
+ f"Script failed unexpectedly:\n{self._output}")
+ self.assertLogMessage(suite_name)
+ self.assertXmlMessage("<testsuites [^/]*failures=\"0\"")
+ self.assertXmlMessage("<testsuites [^/]*errors=\"0\"")
+
+ def assertNotSuccess(self, suite_name, failures=0, errors=0):
+ self._asserter.assertNotEqual(0, self._return_code)
+ self.assertLogMessage(suite_name)
+ if failures:
+ self.assertXmlMessage(f'<testsuites [^/]*failures="{failures}"')
+ if errors:
+ self.assertXmlMessage(f'<testsuites [^/]*errors="{errors}"')
+
+ def assertTestPassed(self, test_name):
+ self.assertLogMessage(f"PASSED: {test_name}")
+
+ def assertTestFailed(self, test_name, message=""):
+ self.assertLogMessage(f"{test_name} FAILED: {message}")
+
+ def assertExactlyOneMatch(self, text, pattern):
+ self._asserter.assertRegex(text, pattern)
+ self._asserter.assertEqual(
+ len(re.findall(pattern, text)),
+ 1,
+ msg=f"Found more than 1 match of '{pattern}' in '{text}'")
+
+
+class UnittestTest(unittest.TestCase):
+
+ def setUp(self):
+ """Create a working directory under our temp dir."""
+ super(UnittestTest, self).setUp()
+ self.work_dir = tempfile.mkdtemp(dir=os.environ["TEST_TMPDIR"])
+
+ def tearDown(self):
+ """Clean up the working directory."""
+ super(UnittestTest, self).tearDown()
+ shutil.rmtree(self.work_dir)
+
+ def write_file(self, filename, contents=""):
+ """Write the contents to a file in the workdir."""
+
+ filepath = os.path.join(self.work_dir, filename)
+ with open(filepath, "w") as f:
+ f.write(_TEST_PREAMBLE.strip())
+ f.write(contents)
+ os.chmod(filepath, stat.S_IEXEC | stat.S_IWRITE | stat.S_IREAD)
+
+ def find_runfiles(self):
+ if "RUNFILES_DIR" in os.environ:
+ return os.environ["RUNFILES_DIR"]
+
+ # Fall back to being based on the srcdir.
+ if "TEST_SRCDIR" in os.environ:
+ return os.environ["TEST_SRCDIR"]
+
+ # Base on the current dir
+ return f"{os.getcwd()}/.."
+
+ def execute_test(self, filename, env=None, args=()):
+ """Executes the file and stores the results."""
+
+ filepath = os.path.join(self.work_dir, filename)
+ xmlfile = os.path.join(self.work_dir, "dummy-testlog.xml")
+ test_env = {
+ "TEST_TMPDIR": self.work_dir,
+ "RUNFILES_DIR": self.find_runfiles(),
+ "TEST_SRCDIR": os.environ["TEST_SRCDIR"],
+ "XML_OUTPUT_FILE": xmlfile,
+ }
+ # Add in env, forcing everything to be a string.
+ if env:
+ for k, v in env.items():
+ test_env[k] = str(v)
+ completed = subprocess.run(
+ [filepath, *args],
+ env=test_env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ )
+ return TestResult(self, completed.returncode,
+ completed.stdout.decode("utf-8"), xmlfile)
+
+ # Actual test cases.
+
+ def test_success(self):
+ self.write_file(
+ "thing.sh", """
+function test_success() {
+ echo foo >&${TEST_log} || fail "expected echo to succeed"
+ expect_log "foo"
+}
+
+run_suite "success tests"
+""")
+
+ result = self.execute_test("thing.sh")
+ result.assertSuccess("success tests")
+ result.assertTestPassed("test_success")
+
+ def test_timestamp(self):
+ self.write_file(
+ "thing.sh", """
+function test_timestamp() {
+ local ts=$(timestamp)
+ [[ $ts =~ ^[0-9]{13}$ ]] || fail "timestamp wan't valid: $ts"
+
+ local time_diff=$(get_run_time 100000 223456)
+ assert_equals $time_diff 123.456
+}
+
+run_suite "timestamp tests"
+""")
+
+ result = self.execute_test("thing.sh")
+ result.assertSuccess("timestamp tests")
+ result.assertTestPassed("test_timestamp")
+
+ def test_failure(self):
+ self.write_file(
+ "thing.sh", """
+function test_failure() {
+ fail "I'm a failure with <>&\\" escaped symbols"
+}
+
+run_suite "failure tests"
+""")
+
+ result = self.execute_test("thing.sh")
+ result.assertNotSuccess("failure tests", failures=0, errors=1)
+ result.assertTestFailed("test_failure")
+ result.assertXmlMessage(
+ "message=\"I'm a failure with <>&" escaped symbols\"")
+ result.assertXmlMessage("I'm a failure with <>&\" escaped symbols")
+
+ def test_set_bash_errexit_prints_stack_trace(self):
+ self.write_file(
+ "thing.sh", """
+set -euo pipefail
+
+function helper() {
+ echo before
+ false
+ echo after
+}
+
+function test_failure_in_helper() {
+ helper
+}
+
+run_suite "bash errexit tests"
+""")
+
+ result = self.execute_test("thing.sh")
+ result.assertNotSuccess("bash errexit tests")
+ result.assertTestFailed("test_failure_in_helper")
+ result.assertLogMessage(r"./thing.sh:\d*: in call to helper")
+ result.assertLogMessage(
+ r"./thing.sh:\d*: in call to test_failure_in_helper")
+
+ def test_set_bash_errexit_runs_tear_down(self):
+ self.write_file(
+ "thing.sh", """
+set -euo pipefail
+
+function tear_down() {
+ echo "Running tear_down"
+}
+
+function testenv_tear_down() {
+ echo "Running testenv_tear_down"
+}
+
+function test_failure_in_helper() {
+ wrong_command
+}
+
+run_suite "bash errexit tests"
+""")
+
+ result = self.execute_test("thing.sh")
+ result.assertNotSuccess("bash errexit tests")
+ result.assertTestFailed("test_failure_in_helper")
+ result.assertLogMessage("Running tear_down")
+ result.assertLogMessage("Running testenv_tear_down")
+
+ def test_set_bash_errexit_pipefail_propagates_failure_through_pipe(self):
+ self.write_file(
+ "thing.sh", """
+set -euo pipefail
+
+function test_pipefail() {
+ wrong_command | cat
+ echo after
+}
+
+run_suite "bash errexit tests"
+""")
+
+ result = self.execute_test("thing.sh")
+ result.assertNotSuccess("bash errexit tests")
+ result.assertTestFailed("test_pipefail")
+ result.assertLogMessage("wrong_command: command not found")
+ result.assertNotLogMessage("after")
+
+ def test_set_bash_errexit_no_pipefail_ignores_failure_before_pipe(self):
+ self.write_file(
+ "thing.sh", """
+set -eu
+set +o pipefail
+
+function test_nopipefail() {
+ wrong_command | cat
+ echo after
+}
+
+run_suite "bash errexit tests"
+""")
+
+ result = self.execute_test("thing.sh")
+ result.assertSuccess("bash errexit tests")
+ result.assertTestPassed("test_nopipefail")
+ result.assertLogMessage("wrong_command: command not found")
+ result.assertLogMessage("after")
+
+ def test_set_bash_errexit_pipefail_long_testname_succeeds(self):
+ test_name = "x" * 1000
+ self.write_file(
+ "thing.sh", """
+set -euo pipefail
+
+function test_%s() {
+ :
+}
+
+run_suite "bash errexit tests"
+""" % test_name)
+
+ result = self.execute_test("thing.sh")
+ result.assertSuccess("bash errexit tests")
+
+ def test_empty_test_fails(self):
+ self.write_file("thing.sh", """
+# No tests present.
+
+run_suite "empty test suite"
+""")
+
+ result = self.execute_test("thing.sh")
+ result.assertNotSuccess("empty test suite")
+ result.assertLogMessage("No tests found.")
+
+ def test_empty_test_succeeds_sharding(self):
+ self.write_file(
+ "thing.sh", """
+# Only one test.
+function test_thing() {
+ echo
+}
+
+run_suite "empty test suite"
+""")
+
+ # First shard.
+ result = self.execute_test(
+ "thing.sh", env={
+ "TEST_TOTAL_SHARDS": 2,
+ "TEST_SHARD_INDEX": 0,
+ })
+ result.assertSuccess("empty test suite")
+ result.assertLogMessage("No tests executed due to sharding")
+
+ # Second shard.
+ result = self.execute_test(
+ "thing.sh", env={
+ "TEST_TOTAL_SHARDS": 2,
+ "TEST_SHARD_INDEX": 1,
+ })
+ result.assertSuccess("empty test suite")
+ result.assertNotLogMessage("No tests")
+
+ def test_filter_runs_only_matching_test(self):
+ self.write_file(
+ "thing.sh",
+ textwrap.dedent("""
+ function test_abc() {
+ :
+ }
+
+ function test_def() {
+ echo "running def"
+ }
+
+ run_suite "tests to filter"
+ """))
+
+ result = self.execute_test(
+ "thing.sh", env={"TESTBRIDGE_TEST_ONLY": "test_a*"})
+
+ result.assertSuccess("tests to filter")
+ result.assertTestPassed("test_abc")
+ result.assertNotLogMessage("running def")
+
+ def test_filter_prefix_match_only_skips_test(self):
+ self.write_file(
+ "thing.sh",
+ textwrap.dedent("""
+ function test_abc() {
+ echo "running abc"
+ }
+
+ run_suite "tests to filter"
+ """))
+
+ result = self.execute_test(
+ "thing.sh", env={"TESTBRIDGE_TEST_ONLY": "test_a"})
+
+ result.assertNotSuccess("tests to filter")
+ result.assertLogMessage("No tests found.")
+
+ def test_filter_multiple_globs_runs_tests_matching_any(self):
+ self.write_file(
+ "thing.sh",
+ textwrap.dedent("""
+ function test_abc() {
+ echo "running abc"
+ }
+
+ function test_def() {
+ echo "running def"
+ }
+
+ run_suite "tests to filter"
+ """))
+
+ result = self.execute_test(
+ "thing.sh", env={"TESTBRIDGE_TEST_ONLY": "donotmatch:*a*"})
+
+ result.assertSuccess("tests to filter")
+ result.assertTestPassed("test_abc")
+ result.assertNotLogMessage("running def")
+
+ def test_filter_character_group_runs_only_matching_tests(self):
+ self.write_file(
+ "thing.sh",
+ textwrap.dedent("""
+ function test_aaa() {
+ :
+ }
+
+ function test_daa() {
+ :
+ }
+
+ function test_zaa() {
+ echo "running zaa"
+ }
+
+ run_suite "tests to filter"
+ """))
+
+ result = self.execute_test(
+ "thing.sh", env={"TESTBRIDGE_TEST_ONLY": "test_[a-f]aa"})
+
+ result.assertSuccess("tests to filter")
+ result.assertTestPassed("test_aaa")
+ result.assertTestPassed("test_daa")
+ result.assertNotLogMessage("running zaa")
+
+ def test_filter_sharded_runs_subset_of_filtered_tests(self):
+ for index in range(2):
+ with self.subTest(index=index):
+ self.__filter_sharded_runs_subset_of_filtered_tests(index)
+
+ def __filter_sharded_runs_subset_of_filtered_tests(self, index):
+ self.write_file(
+ "thing.sh",
+ textwrap.dedent("""
+ function test_a0() {
+ echo "running a0"
+ }
+
+ function test_a1() {
+ echo "running a1"
+ }
+
+ function test_bb() {
+ echo "running bb"
+ }
+
+ run_suite "tests to filter"
+ """))
+
+ result = self.execute_test(
+ "thing.sh",
+ env={
+ "TESTBRIDGE_TEST_ONLY": "test_a*",
+ "TEST_TOTAL_SHARDS": 2,
+ "TEST_SHARD_INDEX": index
+ })
+
+ result.assertSuccess("tests to filter")
+ # The sharding logic is shifted by 1, starts with 2nd shard.
+ result.assertTestPassed("test_a" + str(index ^ 1))
+ result.assertLogMessage("running a" + str(index ^ 1))
+ result.assertNotLogMessage("running a" + str(index))
+ result.assertNotLogMessage("running bb")
+
+ def test_arg_runs_only_matching_test_and_issues_warning(self):
+ self.write_file(
+ "thing.sh",
+ textwrap.dedent("""
+ function test_abc() {
+ :
+ }
+
+ function test_def() {
+ echo "running def"
+ }
+
+ run_suite "tests to filter"
+ """))
+
+ result = self.execute_test("thing.sh", args=["test_abc"])
+
+ result.assertSuccess("tests to filter")
+ result.assertTestPassed("test_abc")
+ result.assertNotLogMessage("running def")
+ result.assertLogMessage(
+ r"WARNING: Passing test names in arguments \(--test_arg\) is "
+ "deprecated, please use --test_filter='test_abc' instead.")
+
+ def test_arg_multiple_tests_issues_warning_with_test_filter_command(self):
+ self.write_file(
+ "thing.sh",
+ textwrap.dedent("""
+ function test_abc() {
+ :
+ }
+
+ function test_def() {
+ :
+ }
+
+ run_suite "tests to filter"
+ """))
+
+ result = self.execute_test("thing.sh", args=["test_abc", "test_def"])
+
+ result.assertSuccess("tests to filter")
+ result.assertTestPassed("test_abc")
+ result.assertTestPassed("test_def")
+ result.assertLogMessage(
+ r"WARNING: Passing test names in arguments \(--test_arg\) is "
+ "deprecated, please use --test_filter='test_abc:test_def' instead.")
+
+ def test_arg_and_filter_ignores_arg(self):
+ self.write_file(
+ "thing.sh",
+ textwrap.dedent("""
+ function test_abc() {
+ :
+ }
+
+ function test_def() {
+ echo "running def"
+ }
+
+ run_suite "tests to filter"
+ """))
+
+ result = self.execute_test(
+ "thing.sh", args=["test_def"], env={"TESTBRIDGE_TEST_ONLY": "test_a*"})
+
+ result.assertSuccess("tests to filter")
+ result.assertTestPassed("test_abc")
+ result.assertNotLogMessage("running def")
+ result.assertLogMessage(
+ "WARNING: Both --test_arg and --test_filter specified, ignoring --test_arg"
+ )
+
+ def test_custom_ifs_variable_finds_and_runs_test(self):
+ for sharded in (False, True):
+ for ifs in (r"\t", "t"):
+ with self.subTest(ifs=ifs, sharded=sharded):
+ self.__custom_ifs_variable_finds_and_runs_test(ifs, sharded)
+
+ def __custom_ifs_variable_finds_and_runs_test(self, ifs, sharded):
+ self.write_file(
+ "thing.sh",
+ textwrap.dedent(r"""
+ set -euo pipefail
+ IFS=$'%s'
+ function test_foo() {
+ :
+ }
+
+ run_suite "custom IFS test"
+ """ % ifs))
+
+ result = self.execute_test(
+ "thing.sh",
+ env={} if not sharded else {
+ "TEST_TOTAL_SHARDS": 2,
+ "TEST_SHARD_INDEX": 1
+ })
+
+ result.assertSuccess("custom IFS test")
+ result.assertTestPassed("test_foo")
+
+ def test_fail_in_teardown_reports_failure(self):
+ self.write_file(
+ "thing.sh",
+ textwrap.dedent(r"""
+ function tear_down() {
+ echo "tear_down log" >"${TEST_log}"
+ fail "tear_down failure"
+ }
+
+ function test_foo() {
+ :
+ }
+
+ run_suite "Failure in tear_down test"
+ """))
+
+ result = self.execute_test("thing.sh")
+
+ result.assertNotSuccess("Failure in tear_down test", errors=1)
+ result.assertTestFailed("test_foo", "tear_down failure")
+ result.assertXmlMessage('message="tear_down failure"')
+ result.assertLogMessage("tear_down log")
+
+ def test_fail_in_teardown_after_test_failure_reports_both_failures(self):
+ self.write_file(
+ "thing.sh",
+ textwrap.dedent(r"""
+ function tear_down() {
+ echo "tear_down log" >"${TEST_log}"
+ fail "tear_down failure"
+ }
+
+ function test_foo() {
+ echo "test_foo log" >"${TEST_log}"
+ fail "Test failure"
+ }
+
+ run_suite "Failure in tear_down test"
+ """))
+
+ result = self.execute_test("thing.sh")
+
+ result.assertNotSuccess("Failure in tear_down test", errors=1)
+ result.assertTestFailed("test_foo", "Test failure")
+ result.assertTestFailed("test_foo", "tear_down failure")
+ result.assertXmlMessage('message="Test failure"')
+ result.assertNotXmlMessage('message="tear_down failure"')
+ result.assertXmlMessage("test_foo log")
+ result.assertXmlMessage("tear_down log")
+ result.assertLogMessage("Test failure")
+ result.assertLogMessage("tear_down failure")
+ result.assertLogMessage("test_foo log")
+ result.assertLogMessage("tear_down log")
+
+ def test_errexit_in_teardown_reports_failure(self):
+ self.write_file(
+ "thing.sh",
+ textwrap.dedent(r"""
+ set -euo pipefail
+
+ function tear_down() {
+ invalid_command
+ }
+
+ function test_foo() {
+ :
+ }
+
+ run_suite "errexit in tear_down test"
+ """))
+
+ result = self.execute_test("thing.sh")
+
+ result.assertNotSuccess("errexit in tear_down test")
+ result.assertLogMessage("invalid_command: command not found")
+ result.assertXmlMessage('message="No failure message"')
+ result.assertXmlMessage("invalid_command: command not found")
+
+ def test_fail_in_tear_down_after_errexit_reports_both_failures(self):
+ self.write_file(
+ "thing.sh",
+ textwrap.dedent(r"""
+ set -euo pipefail
+
+ function tear_down() {
+ echo "tear_down log" >"${TEST_log}"
+ fail "tear_down failure"
+ }
+
+ function test_foo() {
+ invalid_command
+ }
+
+ run_suite "fail after failure"
+ """))
+
+ result = self.execute_test("thing.sh")
+
+ result.assertNotSuccess("fail after failure")
+ result.assertTestFailed(
+ "test_foo",
+ "terminated because this command returned a non-zero status")
+ result.assertTestFailed("test_foo", "tear_down failure")
+ result.assertLogMessage("invalid_command: command not found")
+ result.assertLogMessage("tear_down log")
+ result.assertXmlMessage('message="No failure message"')
+ result.assertXmlMessage("invalid_command: command not found")
+
+ def test_errexit_in_tear_down_after_errexit_reports_both_failures(self):
+ self.write_file(
+ "thing.sh",
+ textwrap.dedent(r"""
+ set -euo pipefail
+
+ function tear_down() {
+ invalid_command_tear_down
+ }
+
+ function test_foo() {
+ invalid_command_test
+ }
+
+ run_suite "fail after failure"
+ """))
+
+ result = self.execute_test("thing.sh")
+
+ result.assertNotSuccess("fail after failure")
+ result.assertTestFailed(
+ "test_foo",
+ "terminated because this command returned a non-zero status")
+ result.assertLogMessage("invalid_command_test: command not found")
+ result.assertLogMessage("invalid_command_tear_down: command not found")
+ result.assertXmlMessage('message="No failure message"')
+ result.assertXmlMessage("invalid_command_test: command not found")
+ result.assertXmlMessage("invalid_command_tear_down: command not found")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/bashunit/unittest_utils.sh b/test/bashunit/unittest_utils.sh
new file mode 100644
index 0000000..be3409e
--- /dev/null
+++ b/test/bashunit/unittest_utils.sh
@@ -0,0 +1,181 @@
+# Copyright 2020 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+# Support for unittest.bash
+
+#### Set up the test environment.
+
+set -euo pipefail
+
+cat_jvm_log () {
+ if [[ "$log_content" =~ \
+ "(error code:".*", error message: '".*"', log file: '"(.*)"')" ]]; then
+ echo >&2
+ echo "Content of ${BASH_REMATCH[1]}:" >&2
+ cat "${BASH_REMATCH[1]}" >&2
+ fi
+}
+
+# Print message in "$1" then exit with status "$2"
+die () {
+ # second argument is optional, defaulting to 1
+ local status_code=${2:-1}
+ # Stop capturing stdout/stderr, and dump captured output
+ if [[ "$CAPTURED_STD_ERR" -ne 0 || "$CAPTURED_STD_OUT" -ne 0 ]]; then
+ restore_outputs
+ if [[ "$CAPTURED_STD_OUT" -ne 0 ]]; then
+ cat "${TEST_TMPDIR}/captured.out"
+ CAPTURED_STD_OUT=0
+ fi
+ if [[ "$CAPTURED_STD_ERR" -ne 0 ]]; then
+ cat "${TEST_TMPDIR}/captured.err" 1>&2
+ cat_jvm_log "$(cat "${TEST_TMPDIR}/captured.err")"
+ CAPTURED_STD_ERR=0
+ fi
+ fi
+
+ if [[ -n "${1-}" ]] ; then
+ echo "$1" 1>&2
+ fi
+ if [[ -n "${BASH-}" ]]; then
+ local caller_n=0
+ while [[ $caller_n -lt 4 ]] && \
+ caller_out=$(caller $caller_n 2>/dev/null); do
+ test $caller_n -eq 0 && echo "CALLER stack (max 4):"
+ echo " $caller_out"
+ let caller_n=caller_n+1
+ done 1>&2
+ fi
+ if [[ -n "${status_code}" && "${status_code}" -ne 0 ]]; then
+ exit "$status_code"
+ else
+ exit 1
+ fi
+}
+
+# Print message in "$1" then record that a non-fatal error occurred in
+# ERROR_COUNT
+ERROR_COUNT="${ERROR_COUNT:-0}"
+error () {
+ if [[ -n "$1" ]] ; then
+ echo "$1" 1>&2
+ fi
+ ERROR_COUNT=$(($ERROR_COUNT + 1))
+}
+
+# Die if "$1" != "$2", print $3 as death reason
+check_eq () {
+ [[ "$1" = "$2" ]] || die "Check failed: '$1' == '$2' ${3:+ ($3)}"
+}
+
+# Die if "$1" == "$2", print $3 as death reason
+check_ne () {
+ [[ "$1" != "$2" ]] || die "Check failed: '$1' != '$2' ${3:+ ($3)}"
+}
+
+# The structure of the following if statements is such that if '[[' fails
+# (e.g., a non-number was passed in) then the check will fail.
+
+# Die if "$1" > "$2", print $3 as death reason
+check_le () {
+ [[ "$1" -gt "$2" ]] || die "Check failed: '$1' <= '$2' ${3:+ ($3)}"
+}
+
+# Die if "$1" >= "$2", print $3 as death reason
+check_lt () {
+ [[ "$1" -lt "$2" ]] || die "Check failed: '$1' < '$2' ${3:+ ($3)}"
+}
+
+# Die if "$1" < "$2", print $3 as death reason
+check_ge () {
+ [[ "$1" -ge "$2" ]] || die "Check failed: '$1' >= '$2' ${3:+ ($3)}"
+}
+
+# Die if "$1" <= "$2", print $3 as death reason
+check_gt () {
+ [[ "$1" -gt "$2" ]] || die "Check failed: '$1' > '$2' ${3:+ ($3)}"
+}
+
+# Die if $2 !~ $1; print $3 as death reason
+check_match ()
+{
+ expr match "$2" "$1" >/dev/null || \
+ die "Check failed: '$2' does not match regex '$1' ${3:+ ($3)}"
+}
+
+# Run command "$1" at exit. Like "trap" but multiple atexits don't
+# overwrite each other. Will break if someone does call trap
+# directly. So, don't do that.
+ATEXIT="${ATEXIT-}"
+atexit () {
+ if [[ -z "$ATEXIT" ]]; then
+ ATEXIT="$1"
+ else
+ ATEXIT="$1 ; $ATEXIT"
+ fi
+ trap "$ATEXIT" EXIT
+}
+
+## TEST_TMPDIR
+if [[ -z "${TEST_TMPDIR:-}" ]]; then
+ export TEST_TMPDIR="$(mktemp -d ${TMPDIR:-/tmp}/bazel-test.XXXXXXXX)"
+fi
+if [[ ! -e "${TEST_TMPDIR}" ]]; then
+ mkdir -p -m 0700 "${TEST_TMPDIR}"
+ # Clean TEST_TMPDIR on exit
+ atexit "rm -fr ${TEST_TMPDIR}"
+fi
+
+# Functions to compare the actual output of a test to the expected
+# (golden) output.
+#
+# Usage:
+# capture_test_stdout
+# ... do something ...
+# diff_test_stdout "$TEST_SRCDIR/path/to/golden.out"
+
+# Redirect a file descriptor to a file.
+CAPTURED_STD_OUT="${CAPTURED_STD_OUT:-0}"
+CAPTURED_STD_ERR="${CAPTURED_STD_ERR:-0}"
+
+capture_test_stdout () {
+ exec 3>&1 # Save stdout as fd 3
+ exec 4>"${TEST_TMPDIR}/captured.out"
+ exec 1>&4
+ CAPTURED_STD_OUT=1
+}
+
+capture_test_stderr () {
+ exec 6>&2 # Save stderr as fd 6
+ exec 7>"${TEST_TMPDIR}/captured.err"
+ exec 2>&7
+ CAPTURED_STD_ERR=1
+}
+
+# Force XML_OUTPUT_FILE to an existing path
+if [[ -z "${XML_OUTPUT_FILE:-}" ]]; then
+ XML_OUTPUT_FILE=${TEST_TMPDIR}/output.xml
+fi
+
+# Functions to provide easy access to external repository outputs in the sibling
+# repository layout.
+#
+# Usage:
+# bin_dir <repository name>
+# genfiles_dir <repository name>
+# testlogs_dir <repository name>
+
+testlogs_dir() {
+ echo $(bazel info bazel-testlogs | sed "s|bazel-out|bazel-out/$1|")
+}
diff --git a/test/rules/android_binary_internal/r8_integration/BUILD b/test/rules/android_binary_internal/r8_integration/BUILD
new file mode 100644
index 0000000..fcefaeb
--- /dev/null
+++ b/test/rules/android_binary_internal/r8_integration/BUILD
@@ -0,0 +1,12 @@
+load("@rules_python//python:py_test.bzl", "py_test")
+
+py_test(
+ name = "r8_integration_test",
+ srcs = ["r8_integration_test.py"],
+ data = [
+ "//test/rules/android_binary_internal/r8_integration/java/com/basicapp:basic_app_R8_no_shrink",
+ "//test/rules/android_binary_internal/r8_integration/java/com/basicapp:basic_app_R8_shrink",
+ "//test/rules/android_binary_internal/r8_integration/java/com/basicapp:basic_app_no_R8",
+ "@androidsdk-supplemental//:dexdump",
+ ],
+)
diff --git a/test/rules/android_binary_internal/r8_integration/java/com/basicapp/AndroidManifest.xml b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/AndroidManifest.xml
new file mode 100644
index 0000000..8d9cc4a
--- /dev/null
+++ b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.basicapp" >
+
+ <uses-sdk
+ android:minSdkVersion="27"
+ android:targetSdkVersion="30" />
+
+ <application
+ android:allowBackup="true"
+ android:label="@string/app_name"
+ android:taskAffinity="" >
+ <activity
+ android:name="com.basicapp.BasicActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/test/rules/android_binary_internal/r8_integration/java/com/basicapp/AndroidManifest_lib.xml b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/AndroidManifest_lib.xml
new file mode 100644
index 0000000..f661d59
--- /dev/null
+++ b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/AndroidManifest_lib.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.basicapp" >
+
+ <uses-sdk
+ android:minSdkVersion="27"
+ android:targetSdkVersion="30" />
+
+</manifest>
diff --git a/test/rules/android_binary_internal/r8_integration/java/com/basicapp/BUILD b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/BUILD
new file mode 100644
index 0000000..4727887
--- /dev/null
+++ b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/BUILD
@@ -0,0 +1,56 @@
+load("//rules:rules.bzl", "android_binary", "android_library")
+
+[
+ android_binary(
+ name = name,
+ srcs = ["BasicActivity.java"],
+ manifest = "AndroidManifest.xml",
+ min_sdk_version = 27,
+ proguard_specs = specs,
+ resource_files = glob(["res/**"]),
+ shrink_resources = shrink,
+ visibility = ["//test/rules/android_binary_internal/r8_integration:__pkg__"],
+ deps = [
+ ":basic_lib",
+ ":lib_with_specs",
+ ],
+ # Work around --java_runtime_version=17 and --java_language_version=11
+ # set in the presubmit tests.
+ javacopts = ["-target", "8", "-source", "8"],
+ )
+ for name, specs, shrink in [
+ (
+ "basic_app_R8_shrink",
+ ["proguard.cfg"],
+ True,
+ ),
+ (
+ "basic_app_R8_no_shrink",
+ ["proguard.cfg"],
+ False,
+ ),
+ ("basic_app_no_R8", [], False),
+ ]
+]
+
+android_library(
+ name = "basic_lib",
+ srcs = ["UnusedActivity.java"],
+ manifest = "AndroidManifest_lib.xml",
+ resource_files = glob(["res_lib/**"]),
+)
+
+android_library(
+ name = "lib_with_specs",
+ srcs = ["LibWithSpecsActivity.java"],
+ manifest = "AndroidManifest_lib.xml",
+ proguard_specs = ["lib_proguard.cfg"],
+ deps = [":lib2_with_specs"],
+)
+
+android_library(
+ name = "lib2_with_specs",
+ srcs = ["Lib2WithSpecsActivity.java"],
+ manifest = "AndroidManifest_lib.xml",
+ proguard_specs = ["lib2_proguard.cfg"],
+)
diff --git a/test/rules/android_binary_internal/r8_integration/java/com/basicapp/BasicActivity.java b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/BasicActivity.java
new file mode 100644
index 0000000..4aee8d1
--- /dev/null
+++ b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/BasicActivity.java
@@ -0,0 +1,49 @@
+// Copyright 2023 The Bazel Authors. All rights reserved.
+//
+// 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.
+
+package com.basicapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+/** The main activity of the Basic Sample App. */
+public class BasicActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.basic_activity);
+
+ final Button buttons[] = {
+ findViewById(R.id.button_id_fizz), findViewById(R.id.button_id_buzz),
+ };
+
+ for (Button b : buttons) {
+ b.setOnClickListener(
+ new View.OnClickListener() {
+ public void onClick(View v) {
+ TextView tv = findViewById(R.id.text_hello);
+ if (v.getId() == R.id.button_id_fizz) {
+ tv.setText("fizz");
+ } else if (v.getId() == R.id.button_id_buzz) {
+ tv.setText("buzz");
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/test/rules/android_binary_internal/r8_integration/java/com/basicapp/Lib2WithSpecsActivity.java b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/Lib2WithSpecsActivity.java
new file mode 100644
index 0000000..57d613f
--- /dev/null
+++ b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/Lib2WithSpecsActivity.java
@@ -0,0 +1,29 @@
+// Copyright 2023 The Bazel Authors. All rights reserved.
+//
+// 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.
+
+package com.basicapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+
+/** The main activity of the Basic Sample App. */
+public class Lib2WithSpecsActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.i("TAG", "onCreate");
+ }
+}
diff --git a/test/rules/android_binary_internal/r8_integration/java/com/basicapp/LibWithSpecsActivity.java b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/LibWithSpecsActivity.java
new file mode 100644
index 0000000..b40be64
--- /dev/null
+++ b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/LibWithSpecsActivity.java
@@ -0,0 +1,29 @@
+// Copyright 2023 The Bazel Authors. All rights reserved.
+//
+// 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.
+
+package com.basicapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+
+/** The main activity of the Basic Sample App. */
+public class LibWithSpecsActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.i("TAG", "onCreate");
+ }
+}
diff --git a/test/rules/android_binary_internal/r8_integration/java/com/basicapp/UnusedActivity.java b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/UnusedActivity.java
new file mode 100644
index 0000000..e188f87
--- /dev/null
+++ b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/UnusedActivity.java
@@ -0,0 +1,30 @@
+// Copyright 2023 The Bazel Authors. All rights reserved.
+//
+// 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.
+
+package com.basicapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+
+/** The main activity of the Basic Sample App. */
+public class UnusedActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.unused_activity);
+ Log.i("TAG", "onCreate");
+ }
+}
diff --git a/test/rules/android_binary_internal/r8_integration/java/com/basicapp/lib2_proguard.cfg b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/lib2_proguard.cfg
new file mode 100644
index 0000000..3fddc01
--- /dev/null
+++ b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/lib2_proguard.cfg
@@ -0,0 +1 @@
+-keep class com.basicapp.Lib2WithSpecsActivity
\ No newline at end of file
diff --git a/test/rules/android_binary_internal/r8_integration/java/com/basicapp/lib_proguard.cfg b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/lib_proguard.cfg
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/lib_proguard.cfg
diff --git a/test/rules/android_binary_internal/r8_integration/java/com/basicapp/proguard.cfg b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/proguard.cfg
new file mode 100644
index 0000000..e0ef7ce
--- /dev/null
+++ b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/proguard.cfg
@@ -0,0 +1,2 @@
+# proguard specs
+-dontobfuscate
diff --git a/test/rules/android_binary_internal/r8_integration/java/com/basicapp/res/layout/basic_activity.xml b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/res/layout/basic_activity.xml
new file mode 100644
index 0000000..f84199c
--- /dev/null
+++ b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/res/layout/basic_activity.xml
@@ -0,0 +1,23 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/text_hello"
+ android:text="@string/hello_world"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <Button
+ android:id="@+id/button_id_fizz"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:text="fizz" />
+ <Button
+ android:id="@+id/button_id_buzz"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:text="buzz" />
+
+</LinearLayout>
diff --git a/test/rules/android_binary_internal/r8_integration/java/com/basicapp/res/values/strings.xml b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/res/values/strings.xml
new file mode 100644
index 0000000..565c987
--- /dev/null
+++ b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/res/values/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name" translatable="false">basicapp</string>
+ <string name="hello_world" translatable="false">Hello world!</string>
+ <string name="action_settings" translatable="false">Settings</string>
+
+</resources>
diff --git a/test/rules/android_binary_internal/r8_integration/java/com/basicapp/res_lib/layout/unused_activity.xml b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/res_lib/layout/unused_activity.xml
new file mode 100644
index 0000000..61fb73e
--- /dev/null
+++ b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/res_lib/layout/unused_activity.xml
@@ -0,0 +1,12 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/text_hello"
+ android:text="@string/hello_world"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+</LinearLayout>
diff --git a/test/rules/android_binary_internal/r8_integration/java/com/basicapp/res_lib/values/strings.xml b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/res_lib/values/strings.xml
new file mode 100644
index 0000000..79934a2
--- /dev/null
+++ b/test/rules/android_binary_internal/r8_integration/java/com/basicapp/res_lib/values/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name" translatable="false">basicapp</string>
+ <string name="hello_world" translatable="false">Hello world! unused</string>
+ <string name="action_settings" translatable="false">Settings</string>
+
+</resources>
diff --git a/test/rules/android_binary_internal/r8_integration/r8_integration_test.py b/test/rules/android_binary_internal/r8_integration/r8_integration_test.py
new file mode 100755
index 0000000..b9ba7e2
--- /dev/null
+++ b/test/rules/android_binary_internal/r8_integration/r8_integration_test.py
@@ -0,0 +1,91 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+import os
+import subprocess
+import unittest
+import zipfile
+
+
+class R8IntegrationTest(unittest.TestCase):
+ """Tests Bazel's R8 integration."""
+
+ def _r8_integration_check(
+ self, apk, expect_unused_activity_resource, expect_unused_activity_class
+ ):
+ tmp = os.environ["TEST_TMPDIR"]
+ apk_directory = (
+ "test/rules/android_binary_internal/r8_integration/java/com/basicapp"
+ )
+ apk_tmp = os.path.join(tmp, apk)
+ classes_dex = os.path.join(apk_tmp, "classes.dex")
+ with zipfile.ZipFile(os.path.join(apk_directory, apk)) as zf:
+ apk_files = zf.namelist()
+ zf.extract("classes.dex", apk_tmp)
+
+ self.assertEqual(
+ expect_unused_activity_resource,
+ "res/layout/unused_activity.xml" in apk_files,
+ )
+
+ build_tools_dir = "external/androidsdk-supplemental/build-tools"
+ build_tools_version = os.listdir(build_tools_dir)[0]
+ dexdump = os.path.join(build_tools_dir, build_tools_version, "dexdump")
+
+ dexdump_proc = subprocess.run(
+ [dexdump, classes_dex],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ check=True,
+ )
+ dexdump_stdout = str(dexdump_proc.stdout)
+ self.assertEqual(
+ expect_unused_activity_class,
+ "Class descriptor : 'Lcom/basicapp/UnusedActivity;'" in dexdump_stdout,
+ )
+
+ # In all cases, the Lib2WithSpecsActivity class should be in the app,
+ # since lib2_proguard.cfg (an indirect dependency) specifies to keep it.
+ self.assertIn(
+ "Class descriptor : 'Lcom/basicapp/Lib2WithSpecsActivity;'",
+ dexdump_stdout,
+ )
+
+ def test_r8_integration(self):
+ # No R8, so unused resources and unused classes should be in the app
+ self._r8_integration_check(
+ "basic_app_no_R8.apk",
+ expect_unused_activity_resource=True,
+ expect_unused_activity_class=True,
+ )
+
+ # Run R8, don't shrink, so unused class should not be in the app but unused
+ # resource should remain.
+ self._r8_integration_check(
+ "basic_app_R8_no_shrink.apk",
+ expect_unused_activity_resource=True,
+ expect_unused_activity_class=False,
+ )
+
+ # Run R8 and shrinkings, so unused classes and resources should not be in
+ # the app.
+ self._r8_integration_check(
+ "basic_app_R8_shrink.apk",
+ expect_unused_activity_resource=False,
+ expect_unused_activity_class=False,
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/utils/asserts.bzl b/test/utils/asserts.bzl
index a1e5a1d..7a81b61 100644
--- a/test/utils/asserts.bzl
+++ b/test/utils/asserts.bzl
@@ -27,6 +27,7 @@
expected_starlark_android_resources_info = attr.label(),
expected_output_group_info = attr.string_list_dict(),
expected_native_libs_info = attr.label(),
+ expected_generated_extension_registry_provider = attr.string_list_dict(),
)
def _expected_resources_node_info_impl(ctx):
@@ -536,6 +537,20 @@
"OutputGroupInfo." + key,
)
+def _assert_generated_extension_registry_provider(expected, actual):
+ if expected and not actual:
+ fail("GeneratedExtensionRegistryProvider was expected but not found!")
+ for key in expected:
+ actual_attr = getattr(actual, key, None)
+ if actual_attr == None: # both empty depset and list will fail.
+ fail("%s is not defined in OutputGroupInfo: %s" % (key, actual))
+
+ _assert_files(
+ expected[key],
+ [actual_attr] if type(actual_attr) != "depset" else actual_attr.to_list(),
+ "GeneratedExtensionRegistryProvider." + key,
+ )
+
def _is_suffix_sublist(full, suffixes):
"""Returns whether suffixes is a sublist of suffixes of full."""
for (fi, _) in enumerate(full):
@@ -598,6 +613,7 @@
starlark_android_resources_info = _assert_starlark_android_resources_info,
output_group_info = _assert_output_group_info,
native_libs_info = _assert_native_libs_info,
+ generated_extension_registry_provider = _assert_generated_extension_registry_provider,
),
files = _assert_files,
r_class = struct(
diff --git a/toolchains/android/toolchain.bzl b/toolchains/android/toolchain.bzl
index f403e75..452fe6f 100644
--- a/toolchains/android/toolchain.bzl
+++ b/toolchains/android/toolchain.bzl
@@ -96,6 +96,12 @@
default = "//tools/android:bundletool_deploy.jar",
executable = True,
),
+ bundletool_module_builder = attr.label(
+ allow_single_file = True,
+ cfg = "exec",
+ default = "//src/tools/bundletool_module_builder",
+ executable = True,
+ ),
centralize_r_class_tool = attr.label(
allow_files = True,
cfg = "exec",
@@ -145,7 +151,7 @@
idlclass = attr.label(
allow_files = True,
cfg = "exec",
- default = "@bazel_tools//tools/android:IdlClass", # _deploy.jar?
+ default = "@bazel_tools//src/tools/android/java/com/google/devtools/build/android/idlclass:IdlClass_deploy.jar",
executable = True,
),
import_deps_checker = attr.label(
@@ -170,10 +176,10 @@
executable = True,
),
merge_baseline_profiles_tool = attr.label(
- default = "@androidsdk//:fail",
- cfg = "exec",
- executable = True
- ),
+ default = "@androidsdk//:fail",
+ cfg = "exec",
+ executable = True,
+ ),
object_method_rewriter = attr.label(
allow_files = True,
cfg = "exec",
@@ -186,7 +192,7 @@
executable = True,
),
profgen = attr.label(
- default = "@androidsdk//:fail",
+ default = "@androidsdk//:fail",
cfg = "exec",
executable = True,
),
@@ -196,6 +202,18 @@
allow_files = True,
executable = True,
),
+ r8 = attr.label(
+ cfg = "exec",
+ default = "//tools/android:r8_deploy.jar",
+ executable = True,
+ allow_files = True,
+ ),
+ resource_shrinker = attr.label(
+ cfg = "exec",
+ default = "//tools/android:resource_shrinker_deploy.jar",
+ executable = True,
+ allow_files = True,
+ ),
res_v3_dummy_manifest = attr.label(
allow_files = True,
default = "//rules:res_v3_dummy_AndroidManifest.xml",
@@ -208,6 +226,12 @@
allow_files = True,
default = "//rules:robolectric_properties_template.txt",
),
+ sandboxed_sdk_toolbox = attr.label(
+ allow_single_file = True,
+ cfg = "exec",
+ default = "//src/tools/java/com/google/devtools/build/android/sandboxedsdktoolbox:sandboxed_sdk_toolbox_deploy.jar",
+ executable = True,
+ ),
testsupport = attr.label(
default = "@androidsdk//:fail",
),
diff --git a/tools/android/BUILD b/tools/android/BUILD
index 7a85b59..922e7e3 100644
--- a/tools/android/BUILD
+++ b/tools/android/BUILD
@@ -40,3 +40,35 @@
"@bazel_tools//src/tools/android/java/com/google/devtools/build/android:all_android_tools",
],
)
+
+alias(
+ name = "java8_legacy_dex",
+ actual = "@bazel_tools//tools/android:java8_legacy_dex",
+ visibility = ["//visibility:public"],
+)
+
+alias(
+ name = "desugar_java8",
+ actual = "@bazel_tools//tools/android:desugar_java8",
+ visibility = ["//visibility:public"],
+)
+
+alias(
+ name = "desugared_java8_legacy_apis",
+ actual = "@bazel_tools//tools/android:desugared_java8_legacy_apis",
+ visibility = ["//visibility:public"],
+)
+
+java_binary(
+ name = "r8",
+ main_class = "com.android.tools.r8.R8",
+ visibility = ["//visibility:public"],
+ runtime_deps = ["@android_gmaven_r8//jar"],
+)
+
+java_binary(
+ name = "resource_shrinker",
+ main_class = "com.android.build.shrinker.ResourceShrinkerCli",
+ visibility = ["//visibility:public"],
+ runtime_deps = ["@rules_android_maven//:com_android_tools_build_gradle"],
+)
diff --git a/tools/jdk/BUILD b/tools/jdk/BUILD
index 76b1921..f3259df 100644
--- a/tools/jdk/BUILD
+++ b/tools/jdk/BUILD
@@ -8,7 +8,19 @@
)
alias(
+ name = "toolchain",
+ actual = "@bazel_tools//tools/jdk:toolchain",
+ visibility = ["//visibility:public"],
+)
+
+alias(
name = "current_java_runtime",
actual = "@bazel_tools//tools/jdk:current_java_runtime",
visibility = ["//visibility:public"],
)
+
+alias(
+ name = "current_host_java_runtime",
+ actual = "@bazel_tools//tools/jdk:current_host_java_runtime",
+ visibility = ["//visibility:public"],
+)