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)