Skip to content

Commit 2ff9b26

Browse files
committed
Run R8 on all modules (base and features) simultaneously
1 parent 35dd2cb commit 2ff9b26

File tree

10 files changed

+200
-45
lines changed

10 files changed

+200
-45
lines changed

examples/bundle/app/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ android_application(
88
},
99
feature_modules = ["//features/assets:feature_module"],
1010
manifest = "AndroidManifest.xml",
11+
proguard_specs = ["proguard.cfg"],
1112
deps = [":lib"],
1213
)
1314

examples/bundle/app/proguard.cfg

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Keep application classes
2+
-keep class com.examples.bundle.app.** { *; }
3+
-keep class com.example.bundle.features.** { *; }
4+
5+
# Keep Android components
6+
-keep public class * extends android.app.Activity
7+
-keep public class * extends android.app.Application
8+
9+
# Don't warn about missing classes
10+
-dontwarn **

examples/bundle/features/assets/BUILD

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ android_feature_module(
1616
title = "asset_feature",
1717
library = ":lib",
1818
feature_name = "asset_feature",
19-
has_dex = True,
19+
has_code = True,
2020
visibility = ["//visibility:public"],
2121
)

providers/providers.bzl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,11 @@ AndroidFeatureModuleInfo = provider(
105105
binary = "String, target of the underlying split android_binary target",
106106
feature_name = "String, the name of the feature module. If unspecified, the target name will be used.",
107107
fused = "Boolean, whether the split is \"fused\" for the system image and for pre-L devices.",
108-
has_dex = "Boolean, whether the split android_library contains dex code",
108+
has_code = "Boolean, whether the feature module contains code",
109109
library = "String, target of the underlying split android_library target",
110110
manifest = "Optional AndroidManifest.xml file to use for this feature.",
111+
pre_dexed_jar = "File, contains pre-dexed jar file",
112+
proguards_specs = "Files, contain proguard rules to be used by R8",
111113
min_sdk_version = "String, the min SDK version for this feature.",
112114
title_id = "String, resource identifier for the split title.",
113115
title_lib = "String, target of the split title android_library.",

rules/android_application/android_application_rule.bzl

Lines changed: 87 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ load(
6060
_log = "log",
6161
)
6262
load("//rules:visibility.bzl", "PROJECT_VISIBILITY")
63+
load("//rules/android_binary:r8.bzl", "process_feature_splits_r8")
6364
load("@rules_java//java/common:java_common.bzl", "java_common")
6465
load(":android_feature_module_rule.bzl", "get_feature_module_paths")
6566
load(":attrs.bzl", "ANDROID_APPLICATION_ATTRS")
@@ -85,7 +86,8 @@ def _process_feature_module(
8586
base_apk = None,
8687
feature_target = None,
8788
java_package = None,
88-
application_id = None):
89+
application_id = None,
90+
feature_dex = None):
8991
manifest = _create_feature_manifest(
9092
ctx,
9193
base_apk,
@@ -98,14 +100,11 @@ def _process_feature_module(
98100
_common.get_host_javabase(ctx),
99101
)
100102

101-
has_dex = bool(feature_target[AndroidFeatureModuleInfo].has_dex)
102-
if has_dex:
103+
has_code = bool(feature_target[AndroidFeatureModuleInfo].has_code)
104+
if has_code:
103105
binary = feature_target[AndroidFeatureModuleInfo].binary[ApkInfo].unsigned_apk
104106
else:
105-
# Remove all dexes from the feature module apk. jvm / resources are not
106-
# supported in feature modules. The android_feature_module rule has
107-
# already validated that there are no transitive sources / resources, but
108-
# we may get dexes via e.g. the legacy dex or the record globals.
107+
# Remove the dex files.
109108
binary = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "_filtered.apk")
110109
_common.filter_zip_exclude(
111110
ctx,
@@ -119,7 +118,7 @@ def _process_feature_module(
119118

120119
# Create res .proto-apk_, output depending on whether further manipulations
121120
# are required after busybox. This prevents action conflicts.
122-
if has_native_libs or is_asset_pack or has_dex:
121+
if has_native_libs or is_asset_pack or has_code:
123122
res_apk = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "/res.proto-ap_")
124123
else:
125124
res_apk = out
@@ -159,7 +158,7 @@ def _process_feature_module(
159158
application_id = application_id,
160159
)
161160

162-
if not is_asset_pack and not has_native_libs and not has_dex:
161+
if not is_asset_pack and not has_native_libs and not has_code:
163162
return
164163

165164
if is_asset_pack:
@@ -178,11 +177,16 @@ def _process_feature_module(
178177
_common.filter_zip_include(ctx, binary, native_libs, ["lib/*"])
179178
inputs_to_merge.append(native_libs)
180179

181-
# Extract dex files from split binary if has_dex is enabled
182-
if has_dex:
183-
dex_files = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "/dex_files.zip")
184-
_common.filter_zip_include(ctx, binary, dex_files, ["*.dex", "classes*.dex"])
185-
inputs_to_merge.append(dex_files)
180+
# Add dex files if has_code is enabled
181+
if has_code:
182+
if feature_dex:
183+
# Use R8 feature splits dex output
184+
inputs_to_merge.append(feature_dex)
185+
else:
186+
# Extract dex files from split binary
187+
dex_files = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "/dex_files.zip")
188+
_common.filter_zip_include(ctx, binary, dex_files, ["*.dex", "classes*.dex"])
189+
inputs_to_merge.append(dex_files)
186190

187191
# Merge into output
188192
# Note: singlejar adds META-INF/MANIFEST.MF, but bundletool_module_builder filters it out
@@ -216,7 +220,7 @@ def _create_feature_manifest(
216220
args.add(info.title_id)
217221
args.add(info.fused)
218222
args.add(aapt2.executable)
219-
args.add(info.has_dex)
223+
args.add(info.has_code)
220224

221225
ctx.actions.run(
222226
executable = feature_manifest_script,
@@ -235,7 +239,7 @@ def _create_feature_manifest(
235239
# Rule has a manifest (already validated by android_feature_module).
236240
# Generate a priority manifest and then merge the user supplied manifest.
237241
is_asset_pack = feature_target[AndroidFeatureModuleInfo].is_asset_pack
238-
has_dex = feature_target[AndroidFeatureModuleInfo].has_dex
242+
has_code = feature_target[AndroidFeatureModuleInfo].has_code
239243
priority_manifest = ctx.actions.declare_file(
240244
ctx.label.name + "/" + feature_target.label.name + "/Priority_AndroidManifest.xml",
241245
)
@@ -247,7 +251,7 @@ def _create_feature_manifest(
247251
args.add(aapt2.executable)
248252
args.add(info.manifest)
249253
args.add(is_asset_pack)
250-
args.add(has_dex)
254+
args.add(has_code)
251255

252256
ctx.actions.run(
253257
executable = priority_feature_manifest_script,
@@ -320,8 +324,28 @@ def _validate_manifest_values(manifest_values):
320324
def _impl(ctx):
321325
_validate_manifest_values(ctx.attr.manifest_values)
322326

323-
# Convert base apk to .proto_ap_
324327
base_apk = ctx.attr.base_module[ApkInfo].unsigned_apk
328+
329+
# Collect feature deploy jars and proguard specs for R8 processing (together with base module's deploy jar and proguard specs)
330+
feature_deploy_jars = {}
331+
all_proguard_specs = list(ctx.files.proguard_specs)
332+
for feature in ctx.attr.feature_modules:
333+
info = feature[AndroidFeatureModuleInfo]
334+
if info.has_code:
335+
feature_deploy_jars[info.feature_name] = info.pre_dexed_jar
336+
if info.proguards_specs:
337+
all_proguard_specs.extend(info.proguards_specs)
338+
339+
r8_output = None
340+
if feature_deploy_jars and ctx.files.proguard_specs:
341+
r8_output = process_feature_splits_r8(
342+
ctx = ctx,
343+
base_deploy_jar = ctx.attr.base_module[AndroidPreDexJarInfo].pre_dex_jar,
344+
feature_deploy_jars = feature_deploy_jars,
345+
proguard_specs = all_proguard_specs,
346+
)
347+
348+
# Convert base apk to proto format
325349
base_proto_apk = ctx.actions.declare_file(ctx.label.name + "/modules/base.proto-ap_")
326350
_aapt.convert(
327351
ctx,
@@ -332,6 +356,28 @@ def _impl(ctx):
332356
)
333357

334358
modules = []
359+
360+
# Replace base dex files with optimized dex files from process_feature_splits_r8.
361+
if r8_output:
362+
base_no_dex = ctx.actions.declare_file(ctx.label.name + "/modules/base_no_dex.zip")
363+
_common.filter_zip_exclude(
364+
ctx,
365+
output = base_no_dex,
366+
input = base_proto_apk,
367+
filters = [".*\\.dex"],
368+
)
369+
370+
base_proto_apk_with_optimized_dex = ctx.actions.declare_file(ctx.label.name + "/modules/base_optimized.proto-ap_")
371+
_java.singlejar(
372+
ctx,
373+
inputs = [base_no_dex, r8_output.base_dex_zip],
374+
output = base_proto_apk_with_optimized_dex,
375+
java_toolchain = _common.get_java_toolchain(ctx),
376+
)
377+
base_proto_apk_for_module = base_proto_apk_with_optimized_dex
378+
else:
379+
base_proto_apk_for_module = base_proto_apk
380+
335381
base_module = ctx.actions.declare_file(
336382
base_proto_apk.basename + ".zip",
337383
sibling = base_proto_apk,
@@ -340,15 +386,20 @@ def _impl(ctx):
340386
_bundletool.proto_apk_to_module(
341387
ctx,
342388
out = base_module,
343-
proto_apk = base_proto_apk,
344-
# RuntimeEnabledSdkConfig should only be added to the base module.
389+
proto_apk = base_proto_apk_for_module,
345390
runtime_enabled_sdk_config = _generate_runtime_enabled_sdk_config(ctx, base_proto_apk),
346391
bundletool_module_builder =
347392
get_android_toolchain(ctx).bundletool_module_builder.files_to_run,
348393
)
349394

350-
# Convert each feature to module zip.
395+
# Process each feature module
351396
for feature in ctx.attr.feature_modules:
397+
info = feature[AndroidFeatureModuleInfo]
398+
399+
feature_dex = None
400+
if r8_output and info.feature_name in r8_output.feature_dex_zips:
401+
feature_dex = r8_output.feature_dex_zips[info.feature_name]
402+
352403
proto_apk = ctx.actions.declare_file(
353404
"%s.proto-ap_" % feature.label.name,
354405
sibling = base_proto_apk,
@@ -360,6 +411,7 @@ def _impl(ctx):
360411
feature_target = feature,
361412
java_package = _java.resolve_package_from_label(ctx.label, ctx.attr.custom_package),
362413
application_id = ctx.attr.manifest_values.get("applicationId"),
414+
feature_dex = feature_dex,
363415
)
364416
module = ctx.actions.declare_file(
365417
proto_apk.basename + ".zip",
@@ -375,7 +427,9 @@ def _impl(ctx):
375427
)
376428

377429
metadata = dict()
378-
if ProguardMappingInfo in ctx.attr.base_module:
430+
if r8_output:
431+
metadata["com.android.tools.build.obfuscation/proguard.map"] = r8_output.proguard_mapping
432+
elif ProguardMappingInfo in ctx.attr.base_module:
379433
metadata["com.android.tools.build.obfuscation/proguard.map"] = ctx.attr.base_module[ProguardMappingInfo].proguard_mapping
380434

381435
if ctx.file.device_group_config:
@@ -524,9 +578,18 @@ def android_application_macro(_android_binary, **attrs):
524578
module_targets = get_feature_module_paths(feature_module)
525579
deps = deps + [str(module_targets.title_lib)]
526580

581+
tags = attrs.pop("tags", [])
582+
proguard_specs = attrs.pop("proguard_specs", [])
583+
584+
# When feature modules exist, don't pass proguard_specs to base binary.
585+
# Unified R8 at android_application level will handle optimization.
586+
base_proguard_specs = [] if feature_modules else proguard_specs
587+
527588
_android_binary(
528589
name = base_split_name,
529590
deps = deps,
591+
tags = tags,
592+
proguard_specs = base_proguard_specs,
530593
**attrs
531594
)
532595

@@ -544,6 +607,7 @@ def android_application_macro(_android_binary, **attrs):
544607
sdk_archives = sdk_archives,
545608
sdk_bundles = sdk_bundles,
546609
manifest_values = attrs.get("manifest_values"),
610+
proguard_specs = proguard_specs,
547611
visibility = attrs.get("visibility", None),
548-
tags = attrs.get("tags", []),
612+
tags = tags,
549613
)

rules/android_application/android_feature_module_rule.bzl

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ load(
1717
"//providers:providers.bzl",
1818
"AndroidFeatureModuleInfo",
1919
"AndroidIdeInfo",
20+
"AndroidPreDexJarInfo",
2021
"ApkInfo",
2122
)
2223
load("//rules:acls.bzl", "acls")
@@ -29,12 +30,20 @@ load(
2930
)
3031
load("//rules:visibility.bzl", "PROJECT_VISIBILITY")
3132
load(":attrs.bzl", "ANDROID_FEATURE_MODULE_ATTRS")
33+
load("@rules_java//java/common:proguard_spec_info.bzl", "ProguardSpecInfo")
3234

3335
visibility(PROJECT_VISIBILITY)
3436

3537
def _impl(ctx):
36-
# Get the binary from the appropriate attribute based on has_dex
37-
binary = ctx.attr.binary_with_dex if ctx.attr.has_dex else ctx.attr.binary
38+
# Get the binary from the appropriate attribute based on has_code
39+
binary = ctx.attr.binary_with_code if ctx.attr.has_code else ctx.attr.binary
40+
pre_dexed_jar = binary[AndroidPreDexJarInfo].pre_dex_jar
41+
42+
# Collect proguard_specs from library dependencies of the feature module
43+
proguard_specs = []
44+
for library in ctx.attr.library:
45+
if ProguardSpecInfo in library:
46+
proguard_specs.extend(library[ProguardSpecInfo].specs.to_list())
3847

3948
validation = ctx.actions.declare_file(ctx.label.name + "_validation")
4049
if binary[AndroidIdeInfo].native_libs and ctx.attr.is_asset_pack:
@@ -71,13 +80,15 @@ def _impl(ctx):
7180
return [
7281
AndroidFeatureModuleInfo(
7382
binary = binary,
74-
has_dex = ctx.attr.has_dex,
83+
has_code = ctx.attr.has_code,
7584
library = utils.dedupe_split_attr(ctx.split_attr.library),
7685
title_id = ctx.attr.title_id,
7786
title_lib = ctx.attr.title_lib,
7887
feature_name = ctx.attr.feature_name,
7988
fused = ctx.attr.fused,
8089
manifest = ctx.file.manifest,
90+
pre_dexed_jar = pre_dexed_jar,
91+
proguards_specs = proguard_specs,
8192
is_asset_pack = ctx.attr.is_asset_pack,
8293
),
8394
OutputGroupInfo(_validation = depset([validation])),
@@ -190,6 +201,7 @@ EOF
190201
"manifest": str(targets.manifest_lib),
191202
"deps": [attrs.library],
192203
"multidex": "native",
204+
"proguard_specs": [], # No optimization here; unified R8 at android_application handles it.
193205
"tags": tags,
194206
"transitive_configs": transitive_configs,
195207
"visibility": visibility,
@@ -199,18 +211,18 @@ EOF
199211
}
200212
_android_binary(**binary_attrs)
201213

202-
has_dex = getattr(attrs, "has_dex", False)
214+
has_code = getattr(attrs, "has_code", False)
203215
android_feature_module(
204216
name = attrs.name,
205217
library = attrs.library,
206-
# Use binary_with_dex when has_dex=True to skip validation aspect
207-
binary = None if has_dex else str(targets.binary),
208-
binary_with_dex = str(targets.binary) if has_dex else None,
218+
# Use binary_with_code when has_code=True to skip validation aspect
219+
binary = None if has_code else str(targets.binary),
220+
binary_with_code = str(targets.binary) if has_code else None,
209221
title_id = title_id,
210222
title_lib = str(targets.title_lib),
211223
feature_name = getattr(attrs, "feature_name", attrs.name),
212224
fused = getattr(attrs, "fused", True),
213-
has_dex = has_dex,
225+
has_code = has_code,
214226
manifest = getattr(attrs, "manifest", None),
215227
tags = tags,
216228
transitive_configs = transitive_configs,

rules/android_application/attrs.bzl

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ ANDROID_APPLICATION_ATTRS = _attrs.add(
3232
dict(
3333
manifest_values = attr.string_dict(),
3434
base_module = attr.label(allow_files = False),
35+
proguard_specs = attr.label_list(
36+
allow_files = True,
37+
doc = "Proguard specification files for R8 processing with feature modules.",
38+
),
3539
bundle_config_file = attr.label(
3640
allow_single_file = [".pb.json"],
3741
doc = ("Path to config.pb.json file, see " +
@@ -104,18 +108,18 @@ ANDROID_APPLICATION_ATTRS = _attrs.add(
104108
)
105109

106110
ANDROID_FEATURE_MODULE_ATTRS = dict(
107-
# binary is used when has_dex=False (with validation aspect)
111+
# binary is used when has_code=False (with validation aspect)
108112
binary = attr.label(aspects = [android_feature_module_validation_aspect]),
109-
# binary_with_dex is used when has_dex=True (without validation aspect)
110-
binary_with_dex = attr.label(),
113+
# binary_with_code is used when has_code=True (without validation aspect)
114+
binary_with_code = attr.label(),
111115
feature_name = attr.string(),
112116
fused = attr.bool(
113117
default = True,
114118
doc = "Whether the split is fused for the system image and for pre-L devices.",
115119
),
116-
has_dex = attr.bool(
120+
has_code = attr.bool(
117121
default = False,
118-
doc = "Allows the library dependency to contain dex files (Kotlin/Java code).",
122+
doc = "Whether the feature module contains code (Kotlin/Java). Maps to android:hasCode in manifest.",
119123
),
120124
library = attr.label(
121125
allow_rules = ["android_library"],

0 commit comments

Comments
 (0)