diff --git a/.bazelrc b/.bazelrc index faa25c257..d0ec28748 100644 --- a/.bazelrc +++ b/.bazelrc @@ -27,4 +27,3 @@ common:windows --host_per_file_copt=external/.*@/w build:windows --define=protobuf_allow_msvc=true common --enable_platform_specific_config - diff --git a/BUILD b/BUILD index 5cd188c26..b17116fd4 100644 --- a/BUILD +++ b/BUILD @@ -6,6 +6,8 @@ package( default_visibility = ["//visibility:public"], ) +exports_files(["MODULE.bazel"]) + license( name = "license", package_name = "bazelbuild/rules_android", diff --git a/rules/BUILD b/rules/BUILD index b0d524b10..e924b6b79 100644 --- a/rules/BUILD +++ b/rules/BUILD @@ -109,6 +109,7 @@ bzl_library( visibility = [ "//mobile_install:__pkg__", "//stardoc:__pkg__", + "//test/rules/android_binary/r8_integration:__pkg__", "//test/rules/android_sdk_repository:__pkg__", ], deps = [ diff --git a/rules/android_binary/r8.bzl b/rules/android_binary/r8.bzl index 9178f3877..9c8c0dbec 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 = "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,21 +109,45 @@ 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"], progress_message = "R8 Optimizing, Desugaring, and Dexing %{label}", ) + # When R8 runs with --desugared-lib, it rewrites java.* API calls to j$.* + # backport references, but does NOT include the j$.* implementation classes + # in its output. Append the prebuilt desugared library DEX so the j$.* + # classes are available at runtime. + if ctx.fragments.android.desugar_java8_libs and desugared_lib_config: + final_classes_dex_zip = ctx.actions.declare_file(ctx.label.name + "_final_dexes.zip") + java8_legacy_dex = utils.only( + get_android_toolchain(ctx).java8_legacy_dex.files.to_list(), + ) + _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, + ) + else: + final_classes_dex_zip = dexes_zip + 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 +156,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, diff --git a/test/rules/android_binary/r8_integration/BUILD b/test/rules/android_binary/r8_integration/BUILD index d3f4325ff..5cc853f3c 100644 --- a/test/rules/android_binary/r8_integration/BUILD +++ b/test/rules/android_binary/r8_integration/BUILD @@ -1,5 +1,11 @@ +load("@bazel_binaries//:defs.bzl", "bazel_binaries") load("@bazel_skylib//rules:build_test.bzl", "build_test") +load( + "@rules_bazel_integration_test//bazel_integration_test:defs.bzl", + "script_test", +) load("@rules_python//python:py_test.bzl", "py_test") +load("@rules_shell//shell:sh_library.bzl", "sh_library") load(":test.bzl", "r8_neverlink_deps_test") py_test( @@ -24,3 +30,33 @@ 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"], ) + +sh_library( + name = "r8_desugaring_helper", + testonly = True, + srcs = ["r8_desugaring_helper.sh"], + data = [ + "//:MODULE.bazel", + "//rules:bzl", + ], + visibility = ["//visibility:private"], + deps = [ + "//test/bashunit", + "@rules_shell//shell/runfiles", + ], +) + +script_test( + name = "r8_desugaring_integration_test", + size = "enormous", + srcs = ["r8_desugaring_integration_test.sh"], + bazel_binaries = bazel_binaries, + bazel_version = bazel_binaries.versions.current, + tags = ["manual"], + timeout = "eternal", + deps = [ + ":r8_desugaring_helper", + "//test/bashunit", + "@rules_shell//shell/runfiles", + ], +) diff --git a/test/rules/android_binary/r8_integration/r8_desugaring_helper.sh b/test/rules/android_binary/r8_integration/r8_desugaring_helper.sh new file mode 100755 index 000000000..3ef491232 --- /dev/null +++ b/test/rules/android_binary/r8_integration/r8_desugaring_helper.sh @@ -0,0 +1,236 @@ +#!/bin/bash +# +# 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. + +# Helper functions for R8 desugaring integration tests. + +# --- begin runfiles.bash initialization v2 --- +# Copy-pasted from the Bazel Bash runfiles library v2. +set -uo pipefail; f=bazel_tools/tools/bash/runfiles/runfiles.bash +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \ + source "$0.runfiles/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e +# --- end runfiles.bash initialization v2 --- + +source "$(rlocation rules_android/test/bashunit/unittest.bash)" || \ + (echo >&2 "Failed to locate bashunit.sh" && exit 1) + +_WORKSPACE_INITIALIZED=false + +# Resolve the real filesystem path of the rules_android source tree. +function get_rules_android_path() { + local module_bazel="$(rlocation rules_android/MODULE.bazel)" + if [[ -z "${module_bazel}" || ! -f "${module_bazel}" ]]; then + fail "Failed to locate rules_android MODULE.bazel" + fi + local real_path + real_path="$(python3 -c "import os; print(os.path.realpath('${module_bazel}'))")" + dirname "${real_path}" +} + +# set_up is called before each test by bashunit. We only initialize the +# workspace once since the inner Bazel build is expensive. Subsequent tests +# reuse the same workspace and inner Bazel server. +function set_up() { + if [[ "${_WORKSPACE_INITIALIZED}" == "true" ]]; then + return + fi + _WORKSPACE_INITIALIZED=true + + # Clean out the workspace. + rm -rf * + + set_up_workspace + create_desugaring_app +} + +function set_up_workspace() { + local rules_dir="$(get_rules_android_path)" + + # Find the Android SDK path from environment. + local sdk_path="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}" + if [[ -z "${sdk_path}" ]]; then + fail "ANDROID_HOME or ANDROID_SDK_ROOT must be set" + fi + + cat > MODULE.bazel < .bazelrc < app/BUILD <<'EOF' +load("@rules_android//rules:rules.bzl", "android_binary") + +android_binary( + name = "desugaring_app", + srcs = [ + "DesugaringActivity.java", + "DurationUser.java", + ], + manifest = "AndroidManifest.xml", + proguard_specs = ["proguard.cfg"], + resource_files = glob(["res/**"]), +) +EOF + + cat > app/AndroidManifest.xml <<'EOF' + + + + + + + + + + + +EOF + + cat > app/DesugaringActivity.java <<'EOF' +package com.desugaring.test; + +import android.app.Activity; +import android.os.Bundle; +import java.time.Duration; + +public class DesugaringActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + long seconds = DurationUser.getSeconds(Duration.ofMinutes(5)); + setTitle("Seconds: " + seconds); + } +} +EOF + + cat > app/DurationUser.java <<'EOF' +package com.desugaring.test; + +import java.time.Duration; + +public class DurationUser { + public static long getSeconds(Duration duration) { + return duration.toSeconds(); + } +} +EOF + + cat > app/proguard.cfg <<'EOF' +-dontobfuscate +-keep class com.desugaring.test.DurationUser { *; } +-keep class com.desugaring.test.DesugaringActivity { *; } +EOF + + cat > app/res/layout/activity_main.xml <<'EOF' + + +EOF + + cat > app/res/values/strings.xml <<'EOF' + + + DesugarTest + +EOF +} + +# Build the desugaring app with extra Bazel flags. +# Usage: build_desugaring_app [--desugar_java8_libs | --nodesugar_java8_libs] +function build_desugaring_app() { + "${BIT_BAZEL_BINARY}" build "$@" -- //app:desugaring_app >& $TEST_log || \ + fail "Failed to build desugaring app" +} + +# Returns 0 if any dex in the APK contains a string matching the given pattern. +# Usage: apk_dex_contains +function apk_dex_contains() { + local pattern="$1" + local apk_path="bazel-bin/app/desugaring_app.apk" + if [[ ! -f "${apk_path}" ]]; then + echo "APK not found at ${apk_path}" >&2 + return 1 + fi + + local tmpdir=$(mktemp -d) + unzip -o "${apk_path}" '*.dex' -d "${tmpdir}" > /dev/null 2>&1 + + local found=false + for dex in "${tmpdir}"/classes*.dex; do + if [[ -f "${dex}" ]] && strings "${dex}" | grep -q "${pattern}"; then + found=true + break + fi + done + + rm -rf "${tmpdir}" + [[ "${found}" == "true" ]] +} diff --git a/test/rules/android_binary/r8_integration/r8_desugaring_integration_test.sh b/test/rules/android_binary/r8_integration/r8_desugaring_integration_test.sh new file mode 100755 index 000000000..0fe90e72f --- /dev/null +++ b/test/rules/android_binary/r8_integration/r8_desugaring_integration_test.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# +# 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. + +# Integration tests for R8 desugaring with and without --desugar_java8_libs. +# +# These tests verify that: +# 1. When --desugar_java8_libs is enabled, R8 rewrites java.* calls to j$.* +# backports AND the j$.* implementation classes are included in the APK. +# 2. When --nodesugar_java8_libs is set, no j$.* classes appear in the APK +# and java.* calls remain as-is. +# 3. DurationUser is retained in both cases (proguard keep rule). + +# --- begin runfiles.bash initialization v2 --- +# Copy-pasted from the Bazel Bash runfiles library v2. +set -uo pipefail; f=bazel_tools/tools/bash/runfiles/runfiles.bash +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \ + source "$0.runfiles/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e +# --- end runfiles.bash initialization v2 --- + +source "$(rlocation rules_android/test/bashunit/unittest.bash)" || \ + (echo >&2 "Failed to locate bashunit.sh" && exit 1) + +source "$(rlocation rules_android/test/rules/android_binary/r8_integration/r8_desugaring_helper.sh)" || \ + (echo >&2 "Failed to locate r8_desugaring_helper.sh" && exit 1) + +# Test: with --desugar_java8_libs enabled, R8 desugars java.time APIs and the +# backport implementation classes (j$.*) are included in the APK. +function test_desugaring_enabled() { + build_desugaring_app --desugar_java8_libs + + # DurationUser must be retained by proguard keep rules. + apk_dex_contains 'Lcom/desugaring/test/DurationUser;' || \ + fail "DurationUser class not found in DEX" + + # Duration.toSeconds() (API 31) must be desugared: the raw java.time + # invocation is rewritten to a j$.time backport, so the original + # method reference should not appear in the constant pool. + if apk_dex_contains 'java/time/Duration;.*toSeconds'; then + fail "Expected Duration.toSeconds() to be desugared but raw java/time reference found" + fi + + # The j$.* backport implementation classes must be present. Without them + # the app crashes at runtime with NoClassDefFoundError (e.g. j$/net/URLEncoder). + apk_dex_contains 'Lj\$/' || \ + fail "Expected j\$.* desugared library classes in the APK" +} + +# Test: with --nodesugar_java8_libs, R8 does not rewrite java.* references +# and no desugared library DEX is appended. +function test_desugaring_disabled() { + build_desugaring_app --nodesugar_java8_libs + + # DurationUser must still be retained regardless of the desugaring flag. + apk_dex_contains 'Lcom/desugaring/test/DurationUser;' || \ + fail "DurationUser class not found in DEX" + + # Without desugaring, java.time.Duration.toSeconds() remains as a direct + # call in the DEX. (It will crash on API < 31 devices, but the reference + # should be present.) + apk_dex_contains 'java/time/Duration' || \ + fail "Expected raw java/time/Duration reference when desugaring is disabled" + + # No j$.* classes should be present. + if apk_dex_contains 'Lj\$/'; then + fail "Unexpected j\$.* desugared library classes when desugaring is disabled" + fi +} + +run_suite "R8 desugaring integration tests"