From da1bf6ea846cf1714237452655eefea9b9c00043 Mon Sep 17 00:00:00 2001 From: Ben Lee Date: Wed, 11 Mar 2026 17:41:30 -0700 Subject: [PATCH 1/3] Add esugar_java8_libs tests --- .../rules/android_binary/r8_integration/BUILD | 10 +++ .../java/com/desugaring/AndroidManifest.xml | 19 +++++ .../r8_integration/java/com/desugaring/BUILD | 16 ++++ .../com/desugaring/DesugaringActivity.java | 17 ++++ .../java/com/desugaring/DurationUser.java | 18 +++++ .../java/com/desugaring/proguard.cfg | 2 + .../desugaring/res/layout/activity_main.xml | 5 ++ .../com/desugaring/res/values/strings.xml | 4 + .../r8_integration/r8_desugaring_test.py | 80 +++++++++++++++++++ 9 files changed, 171 insertions(+) create mode 100644 test/rules/android_binary/r8_integration/java/com/desugaring/AndroidManifest.xml create mode 100644 test/rules/android_binary/r8_integration/java/com/desugaring/BUILD create mode 100644 test/rules/android_binary/r8_integration/java/com/desugaring/DesugaringActivity.java create mode 100644 test/rules/android_binary/r8_integration/java/com/desugaring/DurationUser.java create mode 100644 test/rules/android_binary/r8_integration/java/com/desugaring/proguard.cfg create mode 100644 test/rules/android_binary/r8_integration/java/com/desugaring/res/layout/activity_main.xml create mode 100644 test/rules/android_binary/r8_integration/java/com/desugaring/res/values/strings.xml create mode 100644 test/rules/android_binary/r8_integration/r8_desugaring_test.py diff --git a/test/rules/android_binary/r8_integration/BUILD b/test/rules/android_binary/r8_integration/BUILD index d3f4325ff..dc2ca1947 100644 --- a/test/rules/android_binary/r8_integration/BUILD +++ b/test/rules/android_binary/r8_integration/BUILD @@ -24,3 +24,13 @@ build_test( name = "android_binary_with_neverlink_deps_build_test", targets = ["//test/rules/android_binary/r8_integration/java/com/neverlink:android_binary_with_neverlink_deps"], ) + +py_test( + name = "r8_desugaring_test", + srcs = ["r8_desugaring_test.py"], + args = ["$(location @androidsdk//:dexdump)"], + data = [ + "//test/rules/android_binary/r8_integration/java/com/desugaring:desugaring_app_r8", + "@androidsdk//:dexdump", + ], +) diff --git a/test/rules/android_binary/r8_integration/java/com/desugaring/AndroidManifest.xml b/test/rules/android_binary/r8_integration/java/com/desugaring/AndroidManifest.xml new file mode 100644 index 000000000..67e5eae7c --- /dev/null +++ b/test/rules/android_binary/r8_integration/java/com/desugaring/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/test/rules/android_binary/r8_integration/java/com/desugaring/BUILD b/test/rules/android_binary/r8_integration/java/com/desugaring/BUILD new file mode 100644 index 000000000..334093979 --- /dev/null +++ b/test/rules/android_binary/r8_integration/java/com/desugaring/BUILD @@ -0,0 +1,16 @@ +load("//rules:rules.bzl", "android_binary", "android_library") + +android_library( + name = "duration_lib", + srcs = ["DurationUser.java"], +) + +android_binary( + name = "desugaring_app_r8", + srcs = ["DesugaringActivity.java"], + manifest = "AndroidManifest.xml", + proguard_specs = ["proguard.cfg"], + resource_files = glob(["res/**"]), + visibility = ["//test/rules/android_binary/r8_integration:__pkg__"], + deps = [":duration_lib"], +) diff --git a/test/rules/android_binary/r8_integration/java/com/desugaring/DesugaringActivity.java b/test/rules/android_binary/r8_integration/java/com/desugaring/DesugaringActivity.java new file mode 100644 index 000000000..d9c42a681 --- /dev/null +++ b/test/rules/android_binary/r8_integration/java/com/desugaring/DesugaringActivity.java @@ -0,0 +1,17 @@ +package com.desugaring; + +import android.app.Activity; +import android.os.Bundle; +import java.time.Duration; + +/** Activity that exercises Duration.toSeconds() to test core library desugaring. */ +public class DesugaringActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Force the compiler to retain the call to DurationUser.getSeconds + long seconds = DurationUser.getSeconds(Duration.ofMinutes(5)); + setTitle("Seconds: " + seconds); + } +} diff --git a/test/rules/android_binary/r8_integration/java/com/desugaring/DurationUser.java b/test/rules/android_binary/r8_integration/java/com/desugaring/DurationUser.java new file mode 100644 index 000000000..2c63bec78 --- /dev/null +++ b/test/rules/android_binary/r8_integration/java/com/desugaring/DurationUser.java @@ -0,0 +1,18 @@ +package com.desugaring; + +import java.time.Duration; + +/** + * A class that uses Duration.toSeconds(), which was added in API 31. + * This simulates a third-party library (like Google Nav SDK) that calls + * methods not available on all supported API levels. + * + * Without core library desugaring, this causes NoSuchMethodError on + * API 26-30 devices. + */ +public class DurationUser { + public static long getSeconds(Duration duration) { + // Duration.toSeconds() requires API 31+ + return duration.toSeconds(); + } +} diff --git a/test/rules/android_binary/r8_integration/java/com/desugaring/proguard.cfg b/test/rules/android_binary/r8_integration/java/com/desugaring/proguard.cfg new file mode 100644 index 000000000..7ef29b7a9 --- /dev/null +++ b/test/rules/android_binary/r8_integration/java/com/desugaring/proguard.cfg @@ -0,0 +1,2 @@ +-dontobfuscate +-keep class com.desugaring.DurationUser { *; } diff --git a/test/rules/android_binary/r8_integration/java/com/desugaring/res/layout/activity_main.xml b/test/rules/android_binary/r8_integration/java/com/desugaring/res/layout/activity_main.xml new file mode 100644 index 000000000..bce65f74a --- /dev/null +++ b/test/rules/android_binary/r8_integration/java/com/desugaring/res/layout/activity_main.xml @@ -0,0 +1,5 @@ + + + diff --git a/test/rules/android_binary/r8_integration/java/com/desugaring/res/values/strings.xml b/test/rules/android_binary/r8_integration/java/com/desugaring/res/values/strings.xml new file mode 100644 index 000000000..a5efdad92 --- /dev/null +++ b/test/rules/android_binary/r8_integration/java/com/desugaring/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Desugaring Test + diff --git a/test/rules/android_binary/r8_integration/r8_desugaring_test.py b/test/rules/android_binary/r8_integration/r8_desugaring_test.py new file mode 100644 index 000000000..a990b2c6c --- /dev/null +++ b/test/rules/android_binary/r8_integration/r8_desugaring_test.py @@ -0,0 +1,80 @@ +# Copyright 2024 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 that R8 properly applies core library desugaring. + +Verifies that methods added after API 26 (like Duration.toSeconds() from +API 31) are retargeted to their backported implementations when R8 processes +an android_binary with core library desugaring enabled. +""" + +import os +import subprocess +import sys +import unittest +import zipfile + + +class R8DesugaringTest(unittest.TestCase): + """Tests R8 core library desugaring integration.""" + + def _get_dexdump_output(self, apk_name): + tmp = os.environ["TEST_TMPDIR"] + apk_directory = "test/rules/android_binary/r8_integration/java/com/desugaring" + apk_path = os.path.join(apk_directory, apk_name) + apk_tmp = os.path.join(tmp, apk_name) + + all_output = [] + with zipfile.ZipFile(apk_path) as zf: + for name in zf.namelist(): + if name.endswith(".dex"): + zf.extract(name, apk_tmp) + dex_path = os.path.join(apk_tmp, name) + proc = subprocess.run( + [dexdump, "-d", dex_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + ) + all_output.append(proc.stdout.decode(errors="replace")) + + return "\n".join(all_output) + + def test_duration_to_seconds_is_desugared(self): + """Duration.toSeconds() (API 31) must not appear as a raw call in the DEX.""" + output = self._get_dexdump_output("desugaring_app_r8.apk") + + self.assertNotIn( + "Ljava/time/Duration;.toSeconds:()J", + output, + "Duration.toSeconds() was NOT desugared. This method requires API 31 " + "and will cause NoSuchMethodError on API 28-30 devices. " + "R8 must be passed --desugared-lib to retarget this call.", + ) + + def test_desugared_duration_class_present(self): + """The desugared library runtime must be included in the APK.""" + output = self._get_dexdump_output("desugaring_app_r8.apk") + + # The DurationUser class should still be in the DEX (kept by proguard rules) + self.assertIn( + "Class descriptor : 'Lcom/desugaring/DurationUser;'", + output, + "DurationUser class not found in DEX output", + ) + + +if __name__ == "__main__": + dexdump = sys.argv.pop() + unittest.main(argv=None) From 30355ea41f17984159622c8083b8329102790bb8 Mon Sep 17 00:00:00 2001 From: Ben Lee Date: Wed, 11 Mar 2026 17:48:43 -0700 Subject: [PATCH 2/3] Fix R8 desugar_java8_libs support --- rules/android_binary/r8.bzl | 41 ++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/rules/android_binary/r8.bzl b/rules/android_binary/r8.bzl index 9178f3877..d0ab986b3 100644 --- a/rules/android_binary/r8.bzl +++ b/rules/android_binary/r8.bzl @@ -13,10 +13,12 @@ # limitations under the License. """R8 processor steps for android_binary.""" +load("@rules_java//java/common:java_common.bzl", "java_common") load("//providers:providers.bzl", "AndroidDexInfo", "AndroidPreDexJarInfo") load("//rules:acls.bzl", "acls") load("//rules:android_neverlink_aspect.bzl", "StarlarkAndroidNeverlinkInfo") load("//rules:common.bzl", "common") +load("//rules:dex.bzl", "dex") load("//rules:java.bzl", "java") load("//rules:min_sdk_version.bzl", "min_sdk_version") load( @@ -97,6 +99,9 @@ def process_r8(ctx, validation_ctx, jvm_ctx, packaged_resources_ctx, build_info_ neverlink_infos = utils.collect_providers(StarlarkAndroidNeverlinkInfo, ctx.attr.deps) neverlink_jars = depset(transitive = [info.transitive_neverlink_libraries for info in neverlink_infos]) + desugared_lib_config = ctx.file._desugared_lib_config + desugar_java8_libs = ctx.fragments.android.desugar_java8_libs + args = ctx.actions.args() args.add("--release") args.add("--min-api", effective_min_sdk) @@ -107,21 +112,51 @@ def process_r8(ctx, validation_ctx, jvm_ctx, packaged_resources_ctx, build_info_ args.add(deploy_jar) # jar to optimize + desugar + dex args.add("--pg-map-output", proguard_mappings_output_file) + r8_inputs = [android_jar, deploy_jar] + proguard_specs + if desugar_java8_libs and desugared_lib_config: + args.add("--desugared-lib", desugared_lib_config) + r8_inputs.append(desugared_lib_config) + java.run( ctx = ctx, host_javabase = common.get_host_javabase(ctx), executable = get_android_toolchain(ctx).r8.files_to_run, arguments = [args], - inputs = depset([android_jar, deploy_jar] + proguard_specs, transitive = [neverlink_jars]), + inputs = depset(r8_inputs, transitive = [neverlink_jars]), outputs = [dexes_zip, proguard_mappings_output_file], mnemonic = "AndroidR8", jvm_flags = ["-Xmx8G"], progress_message = "R8 Optimizing, Desugaring, and Dexing %{label}", ) + # Build and append the core library desugaring runtime (java8 legacy dex). + # This contains backported implementations of java.time, java.util.stream, etc. + final_classes_dex_zip = dexes_zip + if desugar_java8_libs: + java8_legacy_dex, java8_legacy_dex_map = dex.get_java8_legacy_dex_and_map( + ctx, + bootclasspath_jar = utils.only(common.get_java_toolchain(ctx)[java_common.JavaToolchainInfo].bootclasspath.to_list()), + binary_jar = deploy_jar, + # Use the prebuilt legacy dex rather than building a customized one. + # The deploy jar still references standard java.time.* APIs (not j$.* backports), + # so tracereferences won't find references to the desugared library. + # R8 handles the retargeting internally via --desugared-lib. + build_customized_files = False, + min_sdk_version = min_sdk_version.clamp(effective_min_sdk), + ) + + final_classes_dex_zip = ctx.actions.declare_file(ctx.label.name + "_final_classes_dex.zip") + dex.append_desugar_dexes( + ctx, + output = final_classes_dex_zip, + input = dexes_zip, + dexes = [java8_legacy_dex], + dex_zips_merger = get_android_toolchain(ctx).dex_zips_merger.files_to_run, + ) + android_dex_info = AndroidDexInfo( deploy_jar = deploy_jar, - final_classes_dex_zip = dexes_zip, + final_classes_dex_zip = final_classes_dex_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, @@ -130,7 +165,7 @@ def process_r8(ctx, validation_ctx, jvm_ctx, packaged_resources_ctx, build_info_ return ProviderInfo( name = "r8_ctx", value = struct( - final_classes_dex_zip = dexes_zip, + final_classes_dex_zip = final_classes_dex_zip, dex_info = android_dex_info, providers = [ android_dex_info, From 8ff7b3276d19ffa65a640af1e51801eb6f9fb9d8 Mon Sep 17 00:00:00 2001 From: Ben Lee Date: Wed, 11 Mar 2026 17:49:13 -0700 Subject: [PATCH 3/3] Revert "Fix R8 desugar_java8_libs support" This reverts commit 30355ea41f17984159622c8083b8329102790bb8. --- rules/android_binary/r8.bzl | 41 +++---------------------------------- 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/rules/android_binary/r8.bzl b/rules/android_binary/r8.bzl index d0ab986b3..9178f3877 100644 --- a/rules/android_binary/r8.bzl +++ b/rules/android_binary/r8.bzl @@ -13,12 +13,10 @@ # limitations under the License. """R8 processor steps for android_binary.""" -load("@rules_java//java/common:java_common.bzl", "java_common") load("//providers:providers.bzl", "AndroidDexInfo", "AndroidPreDexJarInfo") load("//rules:acls.bzl", "acls") load("//rules:android_neverlink_aspect.bzl", "StarlarkAndroidNeverlinkInfo") load("//rules:common.bzl", "common") -load("//rules:dex.bzl", "dex") load("//rules:java.bzl", "java") load("//rules:min_sdk_version.bzl", "min_sdk_version") load( @@ -99,9 +97,6 @@ def process_r8(ctx, validation_ctx, jvm_ctx, packaged_resources_ctx, build_info_ neverlink_infos = utils.collect_providers(StarlarkAndroidNeverlinkInfo, ctx.attr.deps) neverlink_jars = depset(transitive = [info.transitive_neverlink_libraries for info in neverlink_infos]) - desugared_lib_config = ctx.file._desugared_lib_config - desugar_java8_libs = ctx.fragments.android.desugar_java8_libs - args = ctx.actions.args() args.add("--release") args.add("--min-api", effective_min_sdk) @@ -112,51 +107,21 @@ def process_r8(ctx, validation_ctx, jvm_ctx, packaged_resources_ctx, build_info_ args.add(deploy_jar) # jar to optimize + desugar + dex args.add("--pg-map-output", proguard_mappings_output_file) - r8_inputs = [android_jar, deploy_jar] + proguard_specs - if desugar_java8_libs and desugared_lib_config: - args.add("--desugared-lib", desugared_lib_config) - r8_inputs.append(desugared_lib_config) - java.run( ctx = ctx, host_javabase = common.get_host_javabase(ctx), executable = get_android_toolchain(ctx).r8.files_to_run, arguments = [args], - inputs = depset(r8_inputs, transitive = [neverlink_jars]), + inputs = depset([android_jar, deploy_jar] + proguard_specs, transitive = [neverlink_jars]), outputs = [dexes_zip, proguard_mappings_output_file], mnemonic = "AndroidR8", jvm_flags = ["-Xmx8G"], progress_message = "R8 Optimizing, Desugaring, and Dexing %{label}", ) - # Build and append the core library desugaring runtime (java8 legacy dex). - # This contains backported implementations of java.time, java.util.stream, etc. - final_classes_dex_zip = dexes_zip - if desugar_java8_libs: - java8_legacy_dex, java8_legacy_dex_map = dex.get_java8_legacy_dex_and_map( - ctx, - bootclasspath_jar = utils.only(common.get_java_toolchain(ctx)[java_common.JavaToolchainInfo].bootclasspath.to_list()), - binary_jar = deploy_jar, - # Use the prebuilt legacy dex rather than building a customized one. - # The deploy jar still references standard java.time.* APIs (not j$.* backports), - # so tracereferences won't find references to the desugared library. - # R8 handles the retargeting internally via --desugared-lib. - build_customized_files = False, - min_sdk_version = min_sdk_version.clamp(effective_min_sdk), - ) - - final_classes_dex_zip = ctx.actions.declare_file(ctx.label.name + "_final_classes_dex.zip") - dex.append_desugar_dexes( - ctx, - output = final_classes_dex_zip, - input = dexes_zip, - dexes = [java8_legacy_dex], - dex_zips_merger = get_android_toolchain(ctx).dex_zips_merger.files_to_run, - ) - android_dex_info = AndroidDexInfo( deploy_jar = deploy_jar, - final_classes_dex_zip = final_classes_dex_zip, + 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, @@ -165,7 +130,7 @@ def process_r8(ctx, validation_ctx, jvm_ctx, packaged_resources_ctx, build_info_ return ProviderInfo( name = "r8_ctx", value = struct( - final_classes_dex_zip = final_classes_dex_zip, + final_classes_dex_zip = dexes_zip, dex_info = android_dex_info, providers = [ android_dex_info,