From 416b99d3b7ebbda7a70ba963e645f09ca2cde768 Mon Sep 17 00:00:00 2001 From: Ben Lee Date: Wed, 11 Mar 2026 18:13:30 -0700 Subject: [PATCH 1/6] fix-desugar-jdk8-libs-support --- rules/android_binary/r8.bzl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/rules/android_binary/r8.bzl b/rules/android_binary/r8.bzl index 9178f387..c34a51b5 100644 --- a/rules/android_binary/r8.bzl +++ b/rules/android_binary/r8.bzl @@ -17,6 +17,7 @@ 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( @@ -80,6 +81,7 @@ def process_r8(ctx, validation_ctx, jvm_ctx, packaged_resources_ctx, build_info_ android_jar = get_android_sdk(ctx).android_jar proguard_specs = proguard.get_proguard_specs(ctx, packaged_resources_ctx.resource_proguard_config) + desugared_lib_config = ctx.file._desugared_lib_config # Get min SDK version from attribute, manifest_values, or depot floor effective_min_sdk = min_sdk_version.DEPOT_FLOOR @@ -107,12 +109,17 @@ 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 ctx.fragments.android.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"], From a231876f24e718664089a6505b93abef44a1d1e4 Mon Sep 17 00:00:00 2001 From: Ben Lee Date: Thu, 12 Mar 2026 10:33:32 -0700 Subject: [PATCH 2/6] Update r8.bzl --- rules/android_binary/r8.bzl | 1 - 1 file changed, 1 deletion(-) diff --git a/rules/android_binary/r8.bzl b/rules/android_binary/r8.bzl index c34a51b5..53e5b1d3 100644 --- a/rules/android_binary/r8.bzl +++ b/rules/android_binary/r8.bzl @@ -17,7 +17,6 @@ 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( From 1e372f99509ce4ee5df08cdb65ba64fa139f5dfe Mon Sep 17 00:00:00 2001 From: Ben Lee Date: Wed, 18 Mar 2026 09:13:47 -0700 Subject: [PATCH 3/6] Add 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 d3f4325f..dc2ca194 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 00000000..67e5eae7 --- /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 00000000..33409397 --- /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 00000000..d9c42a68 --- /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 00000000..2c63bec7 --- /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 00000000..7ef29b7a --- /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 00000000..bce65f74 --- /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 00000000..a5efdad9 --- /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 00000000..a990b2c6 --- /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 74f853eee55247bfc0bad8f1b6a33d716419aa8f Mon Sep 17 00:00:00 2001 From: Ben Lee Date: Wed, 18 Mar 2026 09:21:12 -0700 Subject: [PATCH 4/6] Update .bazelrc with --desugar_java8_libs --- .bazelrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.bazelrc b/.bazelrc index faa25c25..8a3a2950 100644 --- a/.bazelrc +++ b/.bazelrc @@ -28,3 +28,4 @@ build:windows --define=protobuf_allow_msvc=true common --enable_platform_specific_config +common --desugar_java8_libs From 1af0b39dc3fc5b0cf4208e31d921f3f339a65cb1 Mon Sep 17 00:00:00 2001 From: Ben Lee Date: Wed, 18 Mar 2026 09:29:19 -0700 Subject: [PATCH 5/6] Test cleanup --- .../r8_integration/r8_desugaring_test.py | 81 ++++++++----------- 1 file changed, 33 insertions(+), 48 deletions(-) diff --git a/test/rules/android_binary/r8_integration/r8_desugaring_test.py b/test/rules/android_binary/r8_integration/r8_desugaring_test.py index a990b2c6..979fe8c3 100644 --- a/test/rules/android_binary/r8_integration/r8_desugaring_test.py +++ b/test/rules/android_binary/r8_integration/r8_desugaring_test.py @@ -12,13 +12,6 @@ # 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 @@ -27,54 +20,46 @@ class R8DesugaringTest(unittest.TestCase): - """Tests R8 core library desugaring integration.""" + """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) + 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")) + with zipfile.ZipFile(apk_path) as zf: + zf.extract("classes.dex", apk_tmp) - return "\n".join(all_output) + dexdump_proc = subprocess.run( + [dexdump, "-d", os.path.join(apk_tmp, "classes.dex")], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + ) + return str(dexdump_proc.stdout) - 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") + def test_duration_to_seconds_is_desugared(self): + 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.", - ) + # Duration.toSeconds() (API 31) must not appear as a raw call in the DEX. + # If present, R8 was not passed --desugared-lib and the call will cause + # NoSuchMethodError on API 28-30 devices. + self.assertNotIn( + "Ljava/time/Duration;.toSeconds:()J", + output, + ) - 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") + def test_desugared_duration_class_present(self): + 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", - ) + # The DurationUser class should still be in the DEX (kept by proguard rules) + self.assertIn( + "Class descriptor : 'Lcom/desugaring/DurationUser;'", + output, + ) if __name__ == "__main__": - dexdump = sys.argv.pop() - unittest.main(argv=None) + dexdump = sys.argv.pop() + unittest.main(argv=None) From e17c4c12d05ef59d0660c3314f1361b122484bcb Mon Sep 17 00:00:00 2001 From: Ben Lee Date: Thu, 19 Mar 2026 06:58:09 -0700 Subject: [PATCH 6/6] More fixes --- test/rules/android_binary/r8_integration/r8_desugaring_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/rules/android_binary/r8_integration/r8_desugaring_test.py b/test/rules/android_binary/r8_integration/r8_desugaring_test.py index 979fe8c3..49a06261 100644 --- a/test/rules/android_binary/r8_integration/r8_desugaring_test.py +++ b/test/rules/android_binary/r8_integration/r8_desugaring_test.py @@ -37,7 +37,7 @@ def _get_dexdump_output(self, apk_name): stderr=subprocess.PIPE, check=True, ) - return str(dexdump_proc.stdout) + return dexdump_proc.stdout.decode() def test_duration_to_seconds_is_desugared(self): output = self._get_dexdump_output("desugaring_app_r8.apk")