diff --git a/examples/bundle/.bazelrc b/examples/bundle/.bazelrc
new file mode 100644
index 000000000..a28203e1e
--- /dev/null
+++ b/examples/bundle/.bazelrc
@@ -0,0 +1,10 @@
+# Flags needed while the Android rules are being migrated to Starlark.
+common --experimental_google_legacy_api
+common --experimental_enable_android_migration_apis
+common --android_sdk=@androidsdk//:sdk
+common:core_library_desugaring --desugar_java8_libs
+
+# Flags to enable mobile-install v3
+mobile-install --mode=skylark --mobile_install_aspect=@rules_android//mobile_install:mi.bzl --mobile_install_supported_rules=android_binary
+# Required to invoke the Studio deployer jar
+mobile-install --tool_java_runtime_version=17
diff --git a/examples/bundle/.gitignore b/examples/bundle/.gitignore
new file mode 100644
index 000000000..63f1fef0e
--- /dev/null
+++ b/examples/bundle/.gitignore
@@ -0,0 +1 @@
+*.lock
diff --git a/examples/bundle/BUILD b/examples/bundle/BUILD
new file mode 100644
index 000000000..a09fce916
--- /dev/null
+++ b/examples/bundle/BUILD
@@ -0,0 +1 @@
+# Empty build file to satisfy gazelle for rules_go.
\ No newline at end of file
diff --git a/examples/bundle/MODULE.bazel b/examples/bundle/MODULE.bazel
new file mode 100644
index 000000000..0d85b63a4
--- /dev/null
+++ b/examples/bundle/MODULE.bazel
@@ -0,0 +1,55 @@
+module(
+ name = "bundle",
+)
+
+bazel_dep(name = "rules_java", version = "7.4.0")
+bazel_dep(name = "bazel_skylib", version = "1.3.0")
+
+bazel_dep(
+ name = "rules_android",
+ version = "0.5.1",
+)
+
+local_path_override(
+ module_name = "rules_android",
+ path = "../../",
+)
+
+remote_android_extensions = use_extension(
+ "@rules_android//bzlmod_extensions:android_extensions.bzl",
+ "remote_android_tools_extensions")
+use_repo(remote_android_extensions, "android_tools")
+
+register_toolchains(
+ "@rules_android//toolchains/android:android_default_toolchain",
+ "@rules_android//toolchains/android_sdk:android_sdk_tools",
+)
+
+android_sdk_repository_extension = use_extension("@rules_android//rules/android_sdk_repository:rule.bzl", "android_sdk_repository_extension")
+use_repo(android_sdk_repository_extension, "androidsdk")
+
+register_toolchains("@androidsdk//:sdk-toolchain", "@androidsdk//:all")
+
+bazel_dep(name = "rules_jvm_external", version = "5.3")
+
+# Load the maven extension from rules_jvm_external
+maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
+
+maven.install(
+ name = "maven",
+ aar_import_bzl_label = "@rules_android//rules:rules.bzl",
+ artifacts = [
+ "com.google.guava:guava:32.1.2-android",
+ "com.google.android.play:core:1.10.3",
+ ],
+ repositories = [
+ "https://maven.google.com",
+ "https://repo1.maven.org/maven2",
+ ],
+ use_starlark_android_rules = True,
+)
+use_repo(maven, "maven")
+
+
+
+
diff --git a/examples/bundle/README.md b/examples/bundle/README.md
new file mode 100644
index 000000000..0d923e32d
--- /dev/null
+++ b/examples/bundle/README.md
@@ -0,0 +1,12 @@
+To build, ensure that the `ANDROID_HOME` environment variable is set to the path
+to an Android SDK, and run:
+
+```
+bazel build app:assets
+```
+
+This will build application bundle containing a dynamic feature containing assets (named assets.txt). Verify with :
+
+```
+jar -tf bazel-bin/app/assets_unsigned.aab | grep assets.txt
+```
diff --git a/examples/bundle/WORKSPACE b/examples/bundle/WORKSPACE
new file mode 100644
index 000000000..a23815374
--- /dev/null
+++ b/examples/bundle/WORKSPACE
@@ -0,0 +1,58 @@
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")
+
+maybe(
+ http_archive,
+ name = "rules_jvm_external",
+ strip_prefix = "rules_jvm_external-fa73b1a8e4846cee88240d0019b8f80d39feb1c3",
+ sha256 = "7e13e48b50f9505e8a99cc5a16c557cbe826e9b68d733050cd1e318d69f94bb5",
+ url = "https://github.com/bazelbuild/rules_jvm_external/archive/fa73b1a8e4846cee88240d0019b8f80d39feb1c3.zip",
+)
+
+maybe(
+ http_archive,
+ name = "bazel_skylib",
+ urls = [
+ "https://github.com/bazelbuild/bazel-skylib/releases/download/1.0.3/bazel-skylib-1.0.3.tar.gz",
+ "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.0.3/bazel-skylib-1.0.3.tar.gz",
+ ],
+ sha256 = "1c531376ac7e5a180e0237938a2536de0c54d93f5c278634818e0efc952dd56c",
+)
+load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace")
+bazel_skylib_workspace()
+
+local_repository(
+ name = "rules_android",
+ path = "../..", # rules_android's WORKSPACE relative to this inner workspace
+)
+
+load("@rules_android//:prereqs.bzl", "rules_android_prereqs")
+rules_android_prereqs()
+load("@rules_android//:defs.bzl", "rules_android_workspace")
+rules_android_workspace()
+
+load("@rules_android//rules:rules.bzl", "android_sdk_repository")
+android_sdk_repository(
+ name = "androidsdk",
+)
+
+register_toolchains(
+ "@rules_android//toolchains/android:android_default_toolchain",
+ "@rules_android//toolchains/android_sdk:android_sdk_tools",
+)
+
+load("@rules_jvm_external//:defs.bzl", "maven_install")
+
+maven_install(
+ name = "maven",
+ aar_import_bzl_label = "@rules_android//rules:rules.bzl",
+ artifacts = [
+ "com.google.guava:guava:32.1.2-android",
+ "com.arthenica:ffmpeg-kit-https:4.4.LTS",
+ ],
+ repositories = [
+ "https://maven.google.com",
+ "https://repo1.maven.org/maven2",
+ ],
+ use_starlark_android_rules = True,
+)
diff --git a/examples/bundle/WORKSPACE.bzlmod b/examples/bundle/WORKSPACE.bzlmod
new file mode 100644
index 000000000..df9ed0ec9
--- /dev/null
+++ b/examples/bundle/WORKSPACE.bzlmod
@@ -0,0 +1 @@
+workspace(name = "bundle")
diff --git a/examples/bundle/app/AndroidManifest.xml b/examples/bundle/app/AndroidManifest.xml
new file mode 100644
index 000000000..d56513140
--- /dev/null
+++ b/examples/bundle/app/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/bundle/app/BUILD b/examples/bundle/app/BUILD
new file mode 100644
index 000000000..dab595436
--- /dev/null
+++ b/examples/bundle/app/BUILD
@@ -0,0 +1,24 @@
+load("@rules_android//android:rules.bzl", "android_application", "android_library")
+
+android_application(
+ name = "assets",
+ manifest_values = {
+ "applicationId" : "com.examples.bundle.app",
+ "versionCode": "0",
+ },
+ feature_modules = ["//features/assets:feature_module"],
+ manifest = "AndroidManifest.xml",
+ proguard_specs = ["proguard.cfg"],
+ deps = [":lib"],
+)
+
+android_library(
+ name = "lib",
+ srcs = ["BasicActivity.java", "BundleApplication.java"],
+ manifest = "AndroidManifest.xml",
+ resource_files = glob(["res/**"]),
+ deps = [
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_android_play_core",
+ ]
+)
diff --git a/examples/bundle/app/BasicActivity.java b/examples/bundle/app/BasicActivity.java
new file mode 100644
index 000000000..2d4274cf8
--- /dev/null
+++ b/examples/bundle/app/BasicActivity.java
@@ -0,0 +1,144 @@
+// Copyright 2022 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.
+
+package com.examples.bundle.app;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.google.android.play.core.splitinstall.SplitInstallManager;
+import com.google.android.play.core.splitinstall.SplitInstallManagerFactory;
+import com.google.android.play.core.splitinstall.SplitInstallRequest;
+import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener;
+import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus;
+
+/**
+ * The main activity of the Basic Sample App.
+ */
+public class BasicActivity extends Activity {
+
+ private static final String FEATURE_MODULE_NAME = "asset_feature";
+ private static final String FEATURE_ACTIVITY_CLASS =
+ "com.example.bundle.features.assets.FeatureActivity";
+
+ private SplitInstallManager splitInstallManager;
+ private TextView statusTextView;
+
+ private final SplitInstallStateUpdatedListener listener = state -> {
+ switch (state.status()) {
+ case SplitInstallSessionStatus.DOWNLOADING:
+ statusTextView.setText("Downloading feature module...");
+ break;
+ case SplitInstallSessionStatus.INSTALLING:
+ statusTextView.setText("Installing feature module...");
+ break;
+ case SplitInstallSessionStatus.INSTALLED:
+ statusTextView.setText("Feature module installed!");
+ launchFeatureActivity();
+ break;
+ case SplitInstallSessionStatus.FAILED:
+ statusTextView.setText("Installation failed: " + state.errorCode());
+ break;
+ case SplitInstallSessionStatus.CANCELED:
+ statusTextView.setText("Installation canceled");
+ break;
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.basic_activity);
+
+ splitInstallManager = SplitInstallManagerFactory.create(this);
+ statusTextView = findViewById(R.id.text_hello);
+
+ final Button buttons[] = {
+ findViewById(R.id.button_id_fizz), findViewById(R.id.button_id_buzz),
+ };
+
+ for (Button b : buttons) {
+ b.setOnClickListener(
+ new View.OnClickListener() {
+ public void onClick(View v) {
+ TextView tv = findViewById(R.id.text_hello);
+ if (v.getId() == R.id.button_id_fizz) {
+ tv.setText("fizz");
+ } else if (v.getId() == R.id.button_id_buzz) {
+ tv.setText("buzz ");
+ }
+ }
+ });
+ }
+
+ Button loadFeatureButton = findViewById(R.id.button_load_feature);
+ loadFeatureButton.setOnClickListener(v -> loadFeatureModule());
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ splitInstallManager.registerListener(listener);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ splitInstallManager.unregisterListener(listener);
+ }
+
+ private void loadFeatureModule() {
+ if (splitInstallManager.getInstalledModules().contains(FEATURE_MODULE_NAME)) {
+ statusTextView.setText("Feature already installed!");
+ launchFeatureActivity();
+ return;
+ }
+
+ statusTextView.setText("Requesting feature module...");
+
+ SplitInstallRequest request = SplitInstallRequest.newBuilder()
+ .addModule(FEATURE_MODULE_NAME)
+ .build();
+
+ splitInstallManager.startInstall(request)
+ .addOnSuccessListener(sessionId -> {
+ statusTextView.setText("Installation started (session " + sessionId + ")");
+ })
+ .addOnFailureListener(e -> {
+ statusTextView.setText("Failed to start install: " + e.getMessage());
+ });
+ }
+
+ private void launchFeatureActivity() {
+ try {
+ Intent intent = new Intent();
+ intent.setClassName(getPackageName(), FEATURE_ACTIVITY_CLASS);
+ startActivity(intent);
+ } catch (Exception e) {
+ statusTextView.setText("Failed to launch: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.menu, menu);
+ return true;
+ }
+}
diff --git a/examples/bundle/app/BundleApplication.java b/examples/bundle/app/BundleApplication.java
new file mode 100644
index 000000000..bdc0e0728
--- /dev/null
+++ b/examples/bundle/app/BundleApplication.java
@@ -0,0 +1,31 @@
+// Copyright 2022 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.
+
+package com.examples.bundle.app;
+
+import android.app.Application;
+import android.content.Context;
+
+import com.google.android.play.core.splitcompat.SplitCompat;
+
+/**
+ * Application class that enables SplitCompat for dynamic feature modules.
+ */
+public class BundleApplication extends Application {
+ @Override
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(base);
+ SplitCompat.install(this);
+ }
+}
diff --git a/examples/bundle/app/proguard.cfg b/examples/bundle/app/proguard.cfg
new file mode 100644
index 000000000..f5ee7318a
--- /dev/null
+++ b/examples/bundle/app/proguard.cfg
@@ -0,0 +1,10 @@
+# Keep application classes
+-keep class com.examples.bundle.app.** { *; }
+-keep class com.example.bundle.features.** { *; }
+
+# Keep Android components
+-keep public class * extends android.app.Activity
+-keep public class * extends android.app.Application
+
+# Don't warn about missing classes
+-dontwarn **
diff --git a/examples/bundle/app/res/drawable-hdpi/ic_launcher.png b/examples/bundle/app/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 000000000..6ab2adde2
Binary files /dev/null and b/examples/bundle/app/res/drawable-hdpi/ic_launcher.png differ
diff --git a/examples/bundle/app/res/drawable-mdpi/ic_launcher.png b/examples/bundle/app/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 000000000..c0a73c33d
Binary files /dev/null and b/examples/bundle/app/res/drawable-mdpi/ic_launcher.png differ
diff --git a/examples/bundle/app/res/drawable-xhdpi/ic_launcher.png b/examples/bundle/app/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..014b0f106
Binary files /dev/null and b/examples/bundle/app/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/examples/bundle/app/res/drawable-xxhdpi/ic_launcher.png b/examples/bundle/app/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..20703a15c
Binary files /dev/null and b/examples/bundle/app/res/drawable-xxhdpi/ic_launcher.png differ
diff --git a/examples/bundle/app/res/layout/basic_activity.xml b/examples/bundle/app/res/layout/basic_activity.xml
new file mode 100644
index 000000000..2e08425a5
--- /dev/null
+++ b/examples/bundle/app/res/layout/basic_activity.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
diff --git a/examples/bundle/app/res/menu/menu.xml b/examples/bundle/app/res/menu/menu.xml
new file mode 100644
index 000000000..478f557fa
--- /dev/null
+++ b/examples/bundle/app/res/menu/menu.xml
@@ -0,0 +1,8 @@
+
diff --git a/examples/bundle/app/res/values/dimens.xml b/examples/bundle/app/res/values/dimens.xml
new file mode 100644
index 000000000..47c822467
--- /dev/null
+++ b/examples/bundle/app/res/values/dimens.xml
@@ -0,0 +1,5 @@
+
+
+ 16dp
+ 16dp
+
diff --git a/examples/bundle/app/res/values/strings.xml b/examples/bundle/app/res/values/strings.xml
new file mode 100644
index 000000000..451d3739f
--- /dev/null
+++ b/examples/bundle/app/res/values/strings.xml
@@ -0,0 +1,8 @@
+
+
+
+ basicbundle
+ Hello world!
+ Settings
+
+
diff --git a/examples/bundle/features/assets/BUILD b/examples/bundle/features/assets/BUILD
new file mode 100644
index 000000000..13f3f6d15
--- /dev/null
+++ b/examples/bundle/features/assets/BUILD
@@ -0,0 +1,21 @@
+load("@rules_android//android:rules.bzl", "android_library")
+load("@rules_android//rules:rules.bzl", "android_feature_module")
+
+android_library(
+ name = "lib",
+ srcs = ["src/FeatureActivity.java"],
+ manifest = "src/AndroidManifest.xml",
+ assets = ["src/assets.txt"],
+ custom_package = "com.example.bundle.features.assets",
+)
+
+android_feature_module(
+ name = "feature_module",
+ custom_package = "com.example.bundle.features.assets",
+ manifest = "src/AndroidManifest.xml",
+ title = "asset_feature",
+ library = ":lib",
+ feature_name = "asset_feature",
+ has_code = True,
+ visibility = ["//visibility:public"],
+)
\ No newline at end of file
diff --git a/examples/bundle/features/assets/src/AndroidManifest.xml b/examples/bundle/features/assets/src/AndroidManifest.xml
new file mode 100644
index 000000000..3f6ca6e77
--- /dev/null
+++ b/examples/bundle/features/assets/src/AndroidManifest.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/examples/bundle/features/assets/src/FeatureActivity.java b/examples/bundle/features/assets/src/FeatureActivity.java
new file mode 100644
index 000000000..8a22c28db
--- /dev/null
+++ b/examples/bundle/features/assets/src/FeatureActivity.java
@@ -0,0 +1,22 @@
+package com.example.bundle.features.assets;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.TextView;
+
+/** Activity provided by the dynamic feature module. */
+public class FeatureActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ TextView tv = new TextView(this);
+ tv.setText("Feature Module Loaded: " + getFeatureName());
+ tv.setTextSize(24);
+ tv.setPadding(32, 32, 32, 32);
+ setContentView(tv);
+ }
+
+ public static String getFeatureName() {
+ return "Asset Feature with Code";
+ }
+}
diff --git a/examples/bundle/features/assets/src/assets.txt b/examples/bundle/features/assets/src/assets.txt
new file mode 100644
index 000000000..08c4a31e7
--- /dev/null
+++ b/examples/bundle/features/assets/src/assets.txt
@@ -0,0 +1 @@
+This text originates from a dynamically loaded feature.
\ No newline at end of file
diff --git a/providers/providers.bzl b/providers/providers.bzl
index 0cececd35..2aff92b42 100644
--- a/providers/providers.bzl
+++ b/providers/providers.bzl
@@ -105,8 +105,11 @@ AndroidFeatureModuleInfo = provider(
binary = "String, target of the underlying split android_binary target",
feature_name = "String, the name of the feature module. If unspecified, the target name will be used.",
fused = "Boolean, whether the split is \"fused\" for the system image and for pre-L devices.",
+ has_code = "Boolean, whether the feature module contains code",
library = "String, target of the underlying split android_library target",
manifest = "Optional AndroidManifest.xml file to use for this feature.",
+ pre_dexed_jar = "File, contains pre-dexed jar file",
+ proguards_specs = "Files, contain proguard rules to be used by R8",
min_sdk_version = "String, the min SDK version for this feature.",
title_id = "String, resource identifier for the split title.",
title_lib = "String, target of the split title android_library.",
diff --git a/rules/BUILD b/rules/BUILD
index 5f35bea31..2bd6ad37a 100644
--- a/rules/BUILD
+++ b/rules/BUILD
@@ -3,6 +3,7 @@ load("@bazel_skylib//rules:common_settings.bzl", "int_setting")
load("@rules_java//java:defs.bzl", "java_library")
exports_files([
+ "bundle_keystore_properties.tmpl",
"data_binding_annotation_template.txt",
"res_v3_dummy_AndroidManifest.xml",
"res_v3_dummy_R.txt",
diff --git a/rules/android_application/android_application_rule.bzl b/rules/android_application/android_application_rule.bzl
index f96132caa..4c9fdca6a 100644
--- a/rules/android_application/android_application_rule.bzl
+++ b/rules/android_application/android_application_rule.bzl
@@ -60,6 +60,7 @@ load(
_log = "log",
)
load("//rules:visibility.bzl", "PROJECT_VISIBILITY")
+load("//rules/android_binary:r8.bzl", "process_feature_splits_r8")
load("@rules_java//java/common:java_common.bzl", "java_common")
load(":android_feature_module_rule.bzl", "get_feature_module_paths")
load(":attrs.bzl", "ANDROID_APPLICATION_ATTRS")
@@ -85,7 +86,8 @@ def _process_feature_module(
base_apk = None,
feature_target = None,
java_package = None,
- application_id = None):
+ application_id = None,
+ feature_dex = None):
manifest = _create_feature_manifest(
ctx,
base_apk,
@@ -98,24 +100,25 @@ def _process_feature_module(
_common.get_host_javabase(ctx),
)
- # Remove all dexes from the feature module apk. jvm / resources are not
- # supported in feature modules. The android_feature_module rule has
- # already validated that there are no transitive sources / resources, but
- # we may get dexes via e.g. the legacy dex or the record globals.
- binary = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "_filtered.apk")
- _common.filter_zip_exclude(
- ctx,
- output = binary,
- input = feature_target[AndroidFeatureModuleInfo].binary[ApkInfo].unsigned_apk,
- filter_types = [".dex"],
- )
+ has_code = bool(feature_target[AndroidFeatureModuleInfo].has_code)
+ if has_code:
+ binary = feature_target[AndroidFeatureModuleInfo].binary[ApkInfo].unsigned_apk
+ else:
+ # Remove the dex files.
+ binary = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "_filtered.apk")
+ _common.filter_zip_exclude(
+ ctx,
+ output = binary,
+ input = feature_target[AndroidFeatureModuleInfo].binary[ApkInfo].unsigned_apk,
+ filter_types = [".dex"],
+ )
res = feature_target[AndroidFeatureModuleInfo].library[StarlarkAndroidResourcesInfo]
has_native_libs = bool(feature_target[AndroidFeatureModuleInfo].binary[AndroidIdeInfo].native_libs)
is_asset_pack = bool(feature_target[AndroidFeatureModuleInfo].is_asset_pack)
# Create res .proto-apk_, output depending on whether further manipulations
# are required after busybox. This prevents action conflicts.
- if has_native_libs or is_asset_pack:
+ if has_native_libs or is_asset_pack or has_code:
res_apk = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "/res.proto-ap_")
else:
res_apk = out
@@ -155,25 +158,41 @@ def _process_feature_module(
application_id = application_id,
)
- if not is_asset_pack and not has_native_libs:
+ if not is_asset_pack and not has_native_libs and not has_code:
return
if is_asset_pack:
# Return AndroidManifest.xml and assets from res-ap_
_common.filter_zip_include(ctx, res_apk, out, ["AndroidManifest.xml", "assets/*"])
else:
- # Extract libs/ from split binary
- native_libs = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "/native_libs.zip")
- _common.filter_zip_include(ctx, binary, native_libs, ["lib/*"])
-
# Extract AndroidManifest.xml and assets from res-ap_
filtered_res = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "/filtered_res.zip")
_common.filter_zip_include(ctx, res_apk, filtered_res, ["AndroidManifest.xml", "assets/*"])
+ inputs_to_merge = [filtered_res]
+
+ # Extract libs/ from split binary if present
+ if has_native_libs:
+ native_libs = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "/native_libs.zip")
+ _common.filter_zip_include(ctx, binary, native_libs, ["lib/*"])
+ inputs_to_merge.append(native_libs)
+
+ # Add dex files if has_code is enabled
+ if has_code:
+ if feature_dex:
+ # Use R8 feature splits dex output
+ inputs_to_merge.append(feature_dex)
+ else:
+ # Extract dex files from split binary
+ dex_files = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "/dex_files.zip")
+ _common.filter_zip_include(ctx, binary, dex_files, ["*.dex", "classes*.dex"])
+ inputs_to_merge.append(dex_files)
+
# Merge into output
+ # Note: singlejar adds META-INF/MANIFEST.MF, but bundletool_module_builder filters it out
_java.singlejar(
ctx,
- inputs = [filtered_res, native_libs],
+ inputs = inputs_to_merge,
output = out,
java_toolchain = _common.get_java_toolchain(ctx),
)
@@ -201,6 +220,7 @@ def _create_feature_manifest(
args.add(info.title_id)
args.add(info.fused)
args.add(aapt2.executable)
+ args.add(info.has_code)
ctx.actions.run(
executable = feature_manifest_script,
@@ -219,6 +239,7 @@ def _create_feature_manifest(
# Rule has a manifest (already validated by android_feature_module).
# Generate a priority manifest and then merge the user supplied manifest.
is_asset_pack = feature_target[AndroidFeatureModuleInfo].is_asset_pack
+ has_code = feature_target[AndroidFeatureModuleInfo].has_code
priority_manifest = ctx.actions.declare_file(
ctx.label.name + "/" + feature_target.label.name + "/Priority_AndroidManifest.xml",
)
@@ -230,6 +251,7 @@ def _create_feature_manifest(
args.add(aapt2.executable)
args.add(info.manifest)
args.add(is_asset_pack)
+ args.add(has_code)
ctx.actions.run(
executable = priority_feature_manifest_script,
@@ -302,8 +324,28 @@ def _validate_manifest_values(manifest_values):
def _impl(ctx):
_validate_manifest_values(ctx.attr.manifest_values)
- # Convert base apk to .proto_ap_
base_apk = ctx.attr.base_module[ApkInfo].unsigned_apk
+
+ # Collect feature deploy jars and proguard specs for R8 processing (together with base module's deploy jar and proguard specs)
+ feature_deploy_jars = {}
+ all_proguard_specs = list(ctx.files.proguard_specs)
+ for feature in ctx.attr.feature_modules:
+ info = feature[AndroidFeatureModuleInfo]
+ if info.has_code:
+ feature_deploy_jars[info.feature_name] = info.pre_dexed_jar
+ if info.proguards_specs:
+ all_proguard_specs.extend(info.proguards_specs)
+
+ r8_output = None
+ if feature_deploy_jars and ctx.files.proguard_specs:
+ r8_output = process_feature_splits_r8(
+ ctx = ctx,
+ base_deploy_jar = ctx.attr.base_module[AndroidPreDexJarInfo].pre_dex_jar,
+ feature_deploy_jars = feature_deploy_jars,
+ proguard_specs = all_proguard_specs,
+ )
+
+ # Convert base apk to proto format
base_proto_apk = ctx.actions.declare_file(ctx.label.name + "/modules/base.proto-ap_")
_aapt.convert(
ctx,
@@ -314,6 +356,28 @@ def _impl(ctx):
)
modules = []
+
+ # Replace base dex files with optimized dex files from process_feature_splits_r8.
+ if r8_output:
+ base_no_dex = ctx.actions.declare_file(ctx.label.name + "/modules/base_no_dex.zip")
+ _common.filter_zip_exclude(
+ ctx,
+ output = base_no_dex,
+ input = base_proto_apk,
+ filters = [".*\\.dex"],
+ )
+
+ base_proto_apk_with_optimized_dex = ctx.actions.declare_file(ctx.label.name + "/modules/base_optimized.proto-ap_")
+ _java.singlejar(
+ ctx,
+ inputs = [base_no_dex, r8_output.base_dex_zip],
+ output = base_proto_apk_with_optimized_dex,
+ java_toolchain = _common.get_java_toolchain(ctx),
+ )
+ base_proto_apk_for_module = base_proto_apk_with_optimized_dex
+ else:
+ base_proto_apk_for_module = base_proto_apk
+
base_module = ctx.actions.declare_file(
base_proto_apk.basename + ".zip",
sibling = base_proto_apk,
@@ -322,15 +386,20 @@ def _impl(ctx):
_bundletool.proto_apk_to_module(
ctx,
out = base_module,
- proto_apk = base_proto_apk,
- # RuntimeEnabledSdkConfig should only be added to the base module.
+ proto_apk = base_proto_apk_for_module,
runtime_enabled_sdk_config = _generate_runtime_enabled_sdk_config(ctx, base_proto_apk),
bundletool_module_builder =
get_android_toolchain(ctx).bundletool_module_builder.files_to_run,
)
- # Convert each feature to module zip.
+ # Process each feature module
for feature in ctx.attr.feature_modules:
+ info = feature[AndroidFeatureModuleInfo]
+
+ feature_dex = None
+ if r8_output and info.feature_name in r8_output.feature_dex_zips:
+ feature_dex = r8_output.feature_dex_zips[info.feature_name]
+
proto_apk = ctx.actions.declare_file(
"%s.proto-ap_" % feature.label.name,
sibling = base_proto_apk,
@@ -342,6 +411,7 @@ def _impl(ctx):
feature_target = feature,
java_package = _java.resolve_package_from_label(ctx.label, ctx.attr.custom_package),
application_id = ctx.attr.manifest_values.get("applicationId"),
+ feature_dex = feature_dex,
)
module = ctx.actions.declare_file(
proto_apk.basename + ".zip",
@@ -357,7 +427,9 @@ def _impl(ctx):
)
metadata = dict()
- if ProguardMappingInfo in ctx.attr.base_module:
+ if r8_output:
+ metadata["com.android.tools.build.obfuscation/proguard.map"] = r8_output.proguard_mapping
+ elif ProguardMappingInfo in ctx.attr.base_module:
metadata["com.android.tools.build.obfuscation/proguard.map"] = ctx.attr.base_module[ProguardMappingInfo].proguard_mapping
if ctx.file.device_group_config:
@@ -506,9 +578,18 @@ def android_application_macro(_android_binary, **attrs):
module_targets = get_feature_module_paths(feature_module)
deps = deps + [str(module_targets.title_lib)]
+ tags = attrs.pop("tags", [])
+ proguard_specs = attrs.pop("proguard_specs", [])
+
+ # When feature modules exist, don't pass proguard_specs to base binary.
+ # Unified R8 at android_application level will handle optimization.
+ base_proguard_specs = [] if feature_modules else proguard_specs
+
_android_binary(
name = base_split_name,
deps = deps,
+ tags = tags,
+ proguard_specs = base_proguard_specs,
**attrs
)
@@ -526,6 +607,7 @@ def android_application_macro(_android_binary, **attrs):
sdk_archives = sdk_archives,
sdk_bundles = sdk_bundles,
manifest_values = attrs.get("manifest_values"),
+ proguard_specs = proguard_specs,
visibility = attrs.get("visibility", None),
- tags = attrs.get("tags", []),
+ tags = tags,
)
diff --git a/rules/android_application/android_feature_module_rule.bzl b/rules/android_application/android_feature_module_rule.bzl
index 820716b3c..e76f2b4fb 100644
--- a/rules/android_application/android_feature_module_rule.bzl
+++ b/rules/android_application/android_feature_module_rule.bzl
@@ -17,6 +17,7 @@ load(
"//providers:providers.bzl",
"AndroidFeatureModuleInfo",
"AndroidIdeInfo",
+ "AndroidPreDexJarInfo",
"ApkInfo",
)
load("//rules:acls.bzl", "acls")
@@ -29,14 +30,26 @@ load(
)
load("//rules:visibility.bzl", "PROJECT_VISIBILITY")
load(":attrs.bzl", "ANDROID_FEATURE_MODULE_ATTRS")
+load("@rules_java//java/common:proguard_spec_info.bzl", "ProguardSpecInfo")
visibility(PROJECT_VISIBILITY)
def _impl(ctx):
+ # Get the binary from the appropriate attribute based on has_code
+ binary = ctx.attr.binary_with_code if ctx.attr.has_code else ctx.attr.binary
+ pre_dexed_jar = binary[AndroidPreDexJarInfo].pre_dex_jar
+
+ # Collect proguard_specs from library dependencies of the feature module
+ proguard_specs = []
+ for library in ctx.attr.library:
+ if ProguardSpecInfo in library:
+ proguard_specs.extend(library[ProguardSpecInfo].specs.to_list())
+
validation = ctx.actions.declare_file(ctx.label.name + "_validation")
- if ctx.attr.binary[AndroidIdeInfo].native_libs and ctx.attr.is_asset_pack:
+ if binary[AndroidIdeInfo].native_libs and ctx.attr.is_asset_pack:
fail("Feature module %s is marked as an asset pack but contains native libraries" % ctx.label.name)
- inputs = [ctx.attr.binary[ApkInfo].unsigned_apk]
+
+ inputs = [binary[ApkInfo].unsigned_apk]
args = ctx.actions.args()
args.add(validation.path)
if ctx.file.manifest:
@@ -44,9 +57,9 @@ def _impl(ctx):
inputs.append(ctx.file.manifest)
else:
args.add("")
- args.add(ctx.attr.binary[ApkInfo].unsigned_apk.path)
+ args.add(binary[ApkInfo].unsigned_apk.path)
args.add(utils.dedupe_split_attr(ctx.split_attr.library).label)
- args.add(get_android_toolchain(ctx).xmllint_tool.files_to_run.executable)
+ args.add(get_android_toolchain(ctx).android_kit.files_to_run.executable)
args.add(get_android_toolchain(ctx).unzip_tool.files_to_run.executable)
args.add(ctx.attr.is_asset_pack)
@@ -56,7 +69,7 @@ def _impl(ctx):
outputs = [validation],
arguments = [args],
tools = [
- get_android_toolchain(ctx).xmllint_tool.files_to_run.executable,
+ get_android_toolchain(ctx).android_kit.files_to_run.executable,
get_android_toolchain(ctx).unzip_tool.files_to_run.executable,
],
mnemonic = "ValidateFeatureModule",
@@ -66,13 +79,16 @@ def _impl(ctx):
return [
AndroidFeatureModuleInfo(
- binary = ctx.attr.binary,
+ binary = binary,
+ has_code = ctx.attr.has_code,
library = utils.dedupe_split_attr(ctx.split_attr.library),
title_id = ctx.attr.title_id,
title_lib = ctx.attr.title_lib,
feature_name = ctx.attr.feature_name,
fused = ctx.attr.fused,
manifest = ctx.file.manifest,
+ pre_dexed_jar = pre_dexed_jar,
+ proguards_specs = proguard_specs,
is_asset_pack = ctx.attr.is_asset_pack,
),
OutputGroupInfo(_validation = depset([validation])),
@@ -95,10 +111,10 @@ def get_feature_module_paths(fqn):
# Given a fqn to an android_feature_module, returns the absolute paths to
# all implicitly generated targets
return struct(
- binary = Label("%s_bin" % fqn),
- manifest_lib = Label("%s_AndroidManifest" % fqn),
- title_strings_xml = Label("%s_title_strings_xml" % fqn),
- title_lib = Label("%s_title_lib" % fqn),
+ binary = native.package_relative_label("%s_bin" % fqn),
+ manifest_lib = native.package_relative_label("%s_AndroidManifest" % fqn),
+ title_strings_xml = native.package_relative_label("%s_title_strings_xml" % fqn),
+ title_lib = native.package_relative_label("%s_title_lib" % fqn),
)
def android_feature_module_macro(_android_binary, _android_library, **attrs):
@@ -185,6 +201,7 @@ EOF
"manifest": str(targets.manifest_lib),
"deps": [attrs.library],
"multidex": "native",
+ "proguard_specs": [], # No optimization here; unified R8 at android_application handles it.
"tags": tags,
"transitive_configs": transitive_configs,
"visibility": visibility,
@@ -194,14 +211,18 @@ EOF
}
_android_binary(**binary_attrs)
+ has_code = getattr(attrs, "has_code", False)
android_feature_module(
name = attrs.name,
library = attrs.library,
- binary = str(targets.binary),
+ # Use binary_with_code when has_code=True to skip validation aspect
+ binary = None if has_code else str(targets.binary),
+ binary_with_code = str(targets.binary) if has_code else None,
title_id = title_id,
title_lib = str(targets.title_lib),
feature_name = getattr(attrs, "feature_name", attrs.name),
fused = getattr(attrs, "fused", True),
+ has_code = has_code,
manifest = getattr(attrs, "manifest", None),
tags = tags,
transitive_configs = transitive_configs,
diff --git a/rules/android_application/attrs.bzl b/rules/android_application/attrs.bzl
index 88fb2ff4c..71eac2bc0 100644
--- a/rules/android_application/attrs.bzl
+++ b/rules/android_application/attrs.bzl
@@ -32,6 +32,10 @@ ANDROID_APPLICATION_ATTRS = _attrs.add(
dict(
manifest_values = attr.string_dict(),
base_module = attr.label(allow_files = False),
+ proguard_specs = attr.label_list(
+ allow_files = True,
+ doc = "Proguard specification files for R8 processing with feature modules.",
+ ),
bundle_config_file = attr.label(
allow_single_file = [".pb.json"],
doc = ("Path to config.pb.json file, see " +
@@ -81,8 +85,7 @@ ANDROID_APPLICATION_ATTRS = _attrs.add(
default = Label("//tools/jdk:toolchain_android_only"),
),
_merge_manifests = attr.label(
- default = ":merge_feature_manifests.par",
- allow_single_file = True,
+ default = ":merge_feature_manifests",
cfg = "exec",
executable = True,
),
@@ -105,8 +108,19 @@ ANDROID_APPLICATION_ATTRS = _attrs.add(
)
ANDROID_FEATURE_MODULE_ATTRS = dict(
+ # binary is used when has_code=False (with validation aspect)
binary = attr.label(aspects = [android_feature_module_validation_aspect]),
+ # binary_with_code is used when has_code=True (without validation aspect)
+ binary_with_code = attr.label(),
feature_name = attr.string(),
+ fused = attr.bool(
+ default = True,
+ doc = "Whether the split is fused for the system image and for pre-L devices.",
+ ),
+ has_code = attr.bool(
+ default = False,
+ doc = "Whether the feature module contains code (Kotlin/Java). Maps to android:hasCode in manifest.",
+ ),
library = attr.label(
allow_rules = ["android_library"],
cfg = android_split_transition,
diff --git a/rules/android_application/feature_module_validation.sh b/rules/android_application/feature_module_validation.sh
old mode 100644
new mode 100755
index 4c5c67e90..d85687580
--- a/rules/android_application/feature_module_validation.sh
+++ b/rules/android_application/feature_module_validation.sh
@@ -17,47 +17,16 @@ out="${1}"
manifest="${2}"
apk="${3}"
lib_label="${4}"
-xmllint="${5}"
+android_kit="${5}"
unzip="${6}"
is_asset_pack="${7}"
if [[ -n "$manifest" ]]; then
- node_count=$("$xmllint" --xpath "count(//manifest/*)" "$manifest")
- module_count=$("$xmllint" --xpath "count(//manifest/*[local-name()='module'])" "$manifest")
- application_count=$("$xmllint" --xpath "count(//manifest/*[local-name()='application'])" "$manifest")
- application_attr_count=$("$xmllint" --xpath "count(//manifest/application/@*)" "$manifest")
- application_content_count=$("$xmllint" --xpath "count(//manifest/application/*)" "$manifest")
- module_title=$("$xmllint" --xpath "string(//manifest/*[local-name()='module'][1]/@*[local-name()='title'])" "$manifest")
- valid=0
-
- # Valid manifest, containing a dist:module and an empty
- if [[ "$node_count" == "2" &&
- "$module_count" == "1" &&
- "$application_count" == "1" &&
- "$application_attr_count" == "0" &&
- "$application_content_count" == "0" ]]; then
- valid=1
- fi
-
- # Valid manifest, containing a dist:module
- if [[ "$node_count" == "1" && "$module_count" == "1" ]]; then
- valid=1
- fi
-
- if [[ "$valid" == "0" ]]; then
- echo ""
- echo "$manifest should only contain a single element (and optional empty ), nothing else"
- echo "Manifest contents: "
- cat "$manifest"
- exit 1
- fi
-
- if [[ "$is_asset_pack" = false && "$module_title" != "\${MODULE_TITLE}" ]]; then
- echo ""
- echo "$manifest dist:title should be \${MODULE_TITLE} placeholder"
- echo ""
- exit 1
- fi
+ "$android_kit" validatefeaturemanifest \
+ -manifest "$manifest" \
+ -output "$out" \
+ -is_asset_pack="$is_asset_pack"
+ exit $?
+else
+ touch "$out"
fi
-
-touch "$out"
diff --git a/rules/android_application/gen_android_feature_manifest.sh b/rules/android_application/gen_android_feature_manifest.sh
old mode 100644
new mode 100755
index 5b5552e0e..a4c2da562
--- a/rules/android_application/gen_android_feature_manifest.sh
+++ b/rules/android_application/gen_android_feature_manifest.sh
@@ -20,6 +20,7 @@ split="${4}"
title_id="${5}"
fused="${6}"
aapt="${7}"
+has_code="${8}"
aapt_cmd="$aapt dump xmltree $base_apk --file AndroidManifest.xml"
version_code=$(${aapt_cmd} | grep "http://schemas.android.com/apk/res/android:versionCode" | cut -d "=" -f2 | head -n 1 )
@@ -29,6 +30,13 @@ then
exit 1
fi
+# Determine hasCode value based on has_code parameter
+if [[ "$has_code" == "true" ]]; then
+ has_code_value="true"
+else
+ has_code_value="false"
+fi
+
cat >$out_manifest <
$out_manifest <
-
+
EOF
diff --git a/rules/android_application/gen_priority_android_feature_manifest.sh b/rules/android_application/gen_priority_android_feature_manifest.sh
old mode 100644
new mode 100755
index 066bfc783..780452a17
--- a/rules/android_application/gen_priority_android_feature_manifest.sh
+++ b/rules/android_application/gen_priority_android_feature_manifest.sh
@@ -20,6 +20,7 @@ split="${4}"
aapt="${5}"
in_manifest="${6}" # Developer-provided manifest for the feature module
is_asset_pack="${7}"
+has_code="${8}"
aapt_cmd="$aapt dump xmltree $base_apk --file AndroidManifest.xml"
version_code=$(${aapt_cmd} | grep "http://schemas.android.com/apk/res/android:versionCode" | cut -d "=" -f2 | head -n 1)
@@ -36,6 +37,13 @@ then
exit 1
fi
+# Determine hasCode value based on has_code parameter
+if [[ "$has_code" == "true" ]]; then
+ has_code_value="true"
+else
+ has_code_value="false"
+fi
+
if [ "$is_asset_pack" = true ]
then
# Note: tabs are required instead of spaces
@@ -58,7 +66,7 @@ else
android:versionCode="$version_code"
android:isFeatureSplit="true">
-
+
EOF
diff --git a/rules/android_binary/r8.bzl b/rules/android_binary/r8.bzl
index 973bc4d19..abf530c5f 100644
--- a/rules/android_binary/r8.bzl
+++ b/rules/android_binary/r8.bzl
@@ -13,7 +13,7 @@
# limitations under the License.
"""R8 processor steps for android_binary."""
-load("//providers:providers.bzl", "AndroidDexInfo", "AndroidPreDexJarInfo")
+load("//providers:providers.bzl", "AndroidDexInfo", "AndroidPreDexJarInfo", "ProguardMappingInfo")
load("//rules:acls.bzl", "acls")
load("//rules:android_neverlink_aspect.bzl", "StarlarkAndroidNeverlinkInfo")
load("//rules:common.bzl", "common")
@@ -124,10 +124,72 @@ def process_r8(ctx, validation_ctx, jvm_ctx, packaged_resources_ctx, build_info_
providers = [
android_dex_info,
AndroidPreDexJarInfo(pre_dex_jar = deploy_jar),
+ ProguardMappingInfo(proguard_mapping = proguard_mappings_output_file),
],
),
)
+# TODO: Unify with process_r8 or extract the common parts.
+def process_feature_splits_r8(ctx, base_deploy_jar, feature_deploy_jars, proguard_specs):
+ """Runs R8 with feature splits for consistent obfuscation across base and features.
+
+ Args:
+ ctx: Rule context.
+ base_deploy_jar: File. The deploy jar for the base module.
+ feature_deploy_jars: Dict[str, File]. Map of feature name to deploy jar.
+ proguard_specs: List of files with proguard specs.
+
+ Returns:
+ struct with base_dex_zip, feature_dex_zips, proguard_mapping
+ """
+ base_dex_zip = ctx.actions.declare_file(ctx.label.name + "_r8_base.zip")
+ proguard_mapping = ctx.actions.declare_file(ctx.label.name + "_r8.map")
+
+ feature_dex_zips = {}
+ for feature_name in feature_deploy_jars.keys():
+ feature_dex_zips[feature_name] = ctx.actions.declare_file(ctx.label.name + "_r8_feature_" + feature_name + ".zip")
+
+ android_jar = get_android_sdk(ctx).android_jar
+ min_sdk_version = getattr(ctx.attr, "min_sdk_version", None)
+
+ args = ctx.actions.args()
+ args.add("--release")
+ if min_sdk_version:
+ args.add("--min-api", min_sdk_version)
+ args.add("--output", base_dex_zip)
+ args.add_all(proguard_specs, before_each = "--pg-conf")
+ args.add("--lib", android_jar)
+ args.add("--pg-map-output", proguard_mapping)
+
+ for feature_name, feature_jar in feature_deploy_jars.items():
+ # R8 --feature flag syntax: --feature
+ args.add("--feature")
+ args.add(feature_jar)
+ args.add(feature_dex_zips[feature_name])
+
+ args.add(base_deploy_jar)
+
+ inputs = [android_jar, base_deploy_jar] + proguard_specs + list(feature_deploy_jars.values())
+ outputs = [base_dex_zip, proguard_mapping] + list(feature_dex_zips.values())
+
+ java.run(
+ ctx = ctx,
+ host_javabase = common.get_host_javabase(ctx),
+ executable = get_android_toolchain(ctx).r8.files_to_run,
+ arguments = [args],
+ inputs = depset(inputs),
+ outputs = outputs,
+ mnemonic = "AndroidR8FeatureSplits",
+ jvm_flags = ["-Xmx8G"],
+ progress_message = "R8 Optimizing, Desugaring, and Dexing %{label}",
+ )
+
+ return struct(
+ base_dex_zip = base_dex_zip,
+ feature_dex_zips = feature_dex_zips,
+ proguard_mapping = proguard_mapping,
+ )
+
def process_resource_shrinking_r8(ctx, r8_ctx, packaged_resources_ctx, **_unused_ctxs):
"""Runs resource shrinking.
diff --git a/rules/bundle_keystore_properties.tmpl b/rules/bundle_keystore_properties.tmpl
new file mode 100644
index 000000000..28f5f5efc
--- /dev/null
+++ b/rules/bundle_keystore_properties.tmpl
@@ -0,0 +1,4 @@
+keyAlias=android
+keyPassword=android
+storeFile=%oldest_key%
+storePassword=android
diff --git a/rules/rules.bzl b/rules/rules.bzl
index af06e80e2..0541501c2 100644
--- a/rules/rules.bzl
+++ b/rules/rules.bzl
@@ -41,6 +41,10 @@ load(
"//rules/android_application:android_application.bzl",
_android_application = "android_application",
)
+load(
+ "//rules/android_application:android_feature_module.bzl",
+ _android_feature_module = "android_feature_module",
+)
load(
"//rules/android_binary:rule.bzl",
_android_binary = "android_binary_macro",
@@ -76,6 +80,7 @@ RULES_ANDROID_VERSION = "0.1.0"
aar_import = _aar_import
android_application = _android_application
android_binary = _android_binary
+android_feature_module = _android_feature_module
android_library = _android_library
android_local_test = _android_local_test
android_sandboxed_sdk = _android_sandboxed_sdk
diff --git a/src/tools/ak/BUILD b/src/tools/ak/BUILD
index e1752a7af..3497ecd7a 100644
--- a/src/tools/ak/BUILD
+++ b/src/tools/ak/BUILD
@@ -64,5 +64,6 @@ go_library(
"//src/tools/ak/patch",
"//src/tools/ak/repack",
"//src/tools/ak/rjar",
+ "//src/tools/ak/validatefeaturemanifest",
],
)
diff --git a/src/tools/ak/akcommands.go b/src/tools/ak/akcommands.go
index b436493ca..ae70eb54e 100644
--- a/src/tools/ak/akcommands.go
+++ b/src/tools/ak/akcommands.go
@@ -25,6 +25,7 @@ import (
"src/tools/ak/link/link"
"src/tools/ak/liteparse/liteparse"
"src/tools/ak/manifest/manifest"
+ "src/tools/ak/validatefeaturemanifest/validatefeaturemanifest"
"src/tools/ak/minsdkfloor/minsdkfloor"
"src/tools/ak/nativelib/nativelib"
"src/tools/ak/patch/patch"
@@ -44,6 +45,7 @@ var (
"liteparse": liteparse.Cmd,
"generatemanifest": generatemanifest.Cmd,
"manifest": manifest.Cmd,
+ "validatefeaturemanifest": validatefeaturemanifest.Cmd,
"nativelib": nativelib.Cmd,
"patch": patch.Cmd,
"repack": repack.Cmd,
diff --git a/src/tools/ak/validatefeaturemanifest/BUILD b/src/tools/ak/validatefeaturemanifest/BUILD
new file mode 100644
index 000000000..78fd45d0d
--- /dev/null
+++ b/src/tools/ak/validatefeaturemanifest/BUILD
@@ -0,0 +1,16 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+licenses(["notice"])
+
+go_library(
+ name = "validatefeaturemanifest",
+ srcs = [
+ "validatefeaturemanifest.go",
+ ],
+ importpath = "src/tools/ak/validatefeaturemanifest/validatefeaturemanifest",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//src/common/golang:xml2",
+ "//src/tools/ak:types",
+ ],
+)
diff --git a/src/tools/ak/validatefeaturemanifest/validatefeaturemanifest.go b/src/tools/ak/validatefeaturemanifest/validatefeaturemanifest.go
new file mode 100644
index 000000000..d41e9477d
--- /dev/null
+++ b/src/tools/ak/validatefeaturemanifest/validatefeaturemanifest.go
@@ -0,0 +1,159 @@
+// 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.
+
+// Package validatefeaturemanifest is a tool to validate dynamic feature's manifest file.
+package validatefeaturemanifest
+
+import (
+ "encoding/xml"
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "strings"
+ "sync"
+
+ "src/tools/ak/types"
+)
+
+var (
+ Cmd = types.Command{
+ Init: Init,
+ Run: Run,
+ Desc: desc,
+ Flags: []string{
+ "manifest",
+ "output",
+ "is_asset_pack",
+ },
+ }
+
+ initOnce sync.Once
+
+ manifestFlag string
+ outputFlag string
+ isAssetPackFlag bool
+)
+
+// Manifest represents the root element
+type Manifest struct {
+ XMLName xml.Name `xml:"manifest"`
+ Children []Element `xml:",any"`
+}
+
+// Element represents any child element of manifest
+type Element struct {
+ XMLName xml.Name
+ Attrs []xml.Attr `xml:",any,attr"`
+ Content []byte `xml:",innerxml"`
+}
+
+func Init() {
+ initOnce.Do(func() {
+ flag.StringVar(&manifestFlag, "manifest", "", "AndroidManifest.xml for the dynamic feature")
+ flag.StringVar(&outputFlag, "output", "", "Output file to touch on success")
+ flag.BoolVar(&isAssetPackFlag, "is_asset_pack", false, "Whether this is an asset pack module")
+ })
+}
+
+func desc() string {
+ return "Validate feature module manifest"
+}
+
+func Run() {
+ if manifestFlag == "" {
+ touchOutput()
+ return
+ }
+
+ file, err := os.Open(manifestFlag)
+ if err != nil {
+ log.Fatalf("Error opening manifest: %v", err)
+ }
+ defer file.Close()
+
+ var manifest Manifest
+ decoder := xml.NewDecoder(file)
+ if err := decoder.Decode(&manifest); err != nil {
+ log.Fatalf("Error parsing manifest XML: %v", err)
+ }
+
+ nodeCount := len(manifest.Children)
+ moduleCount := 0
+ applicationCount := 0
+ applicationAttrCount := 0
+ applicationHasContent := false
+ moduleTitle := ""
+
+ for _, child := range manifest.Children {
+ localName := child.XMLName.Local
+
+ if localName == "module" {
+ moduleCount++
+ for _, attr := range child.Attrs {
+ if attr.Name.Local == "title" {
+ moduleTitle = attr.Value
+ break
+ }
+ }
+ }
+
+ if localName == "application" {
+ applicationCount++
+ applicationAttrCount = len(child.Attrs)
+ applicationHasContent = len(strings.TrimSpace(string(child.Content))) > 0
+ }
+ }
+
+ valid := false
+
+ if nodeCount == 2 &&
+ moduleCount == 1 &&
+ applicationCount == 1 &&
+ applicationAttrCount == 0 &&
+ !applicationHasContent {
+ valid = true
+ }
+
+ if nodeCount == 1 && moduleCount == 1 {
+ valid = true
+ }
+
+ if !valid {
+ fmt.Println()
+ fmt.Printf("%s should only contain a single element (and optional empty ), nothing else\n", manifestFlag)
+ fmt.Println("Manifest contents:")
+ content, _ := os.ReadFile(manifestFlag)
+ fmt.Println(string(content))
+ os.Exit(1)
+ }
+
+ if !isAssetPackFlag && moduleTitle != "${MODULE_TITLE}" {
+ fmt.Println()
+ fmt.Printf("%s dist:title should be ${MODULE_TITLE} placeholder, got: %s\n", manifestFlag, moduleTitle)
+ fmt.Println()
+ os.Exit(1)
+ }
+
+ // Validation passed
+ touchOutput()
+}
+
+func touchOutput() {
+ if outputFlag != "" {
+ if err := os.WriteFile(outputFlag, []byte{}, 0644); err != nil {
+ log.Fatalf("Error creating output file: %v", err)
+ }
+ }
+}
diff --git a/src/tools/bundletool_module_builder/bundletool_module_builder.go b/src/tools/bundletool_module_builder/bundletool_module_builder.go
index e495a296d..01b2dff38 100644
--- a/src/tools/bundletool_module_builder/bundletool_module_builder.go
+++ b/src/tools/bundletool_module_builder/bundletool_module_builder.go
@@ -65,7 +65,12 @@ func unzipApkAndCreateModule(internalApkPath, outputModulePath, runtimeEnabledSD
defer zipWriter.Close()
for _, f := range r.File {
- f.Name = fileNameInOutput(f.Name)
+ newName := fileNameInOutput(f.Name)
+ // Skip entries with empty output names (e.g., META-INF)
+ if newName == "" {
+ continue
+ }
+ f.Name = newName
if err := zipWriter.Copy(f); err != nil {
return err
}
@@ -104,6 +109,9 @@ func fileNameInOutput(oldName string) string {
// Dex files need to be moved under dex/ dir.
case strings.HasSuffix(oldName, ".dex"):
return "dex/" + oldName
+ // Skip META-INF entries (they conflict between modules in the AAB)
+ case strings.HasPrefix(oldName, "META-INF/"):
+ return ""
// All other files (probably JVM metadata files) should be moved to root/ dir.
default:
return "root/" + oldName
diff --git a/toolchains/android/BUILD b/toolchains/android/BUILD
index 941fbe17f..ae20aece2 100644
--- a/toolchains/android/BUILD
+++ b/toolchains/android/BUILD
@@ -56,3 +56,4 @@ sh_binary(
srcs = [":unzip.sh"],
visibility = ["//visibility:public"],
)
+
diff --git a/toolchains/android/toolchain.bzl b/toolchains/android/toolchain.bzl
index 98dd0f32a..606254f2e 100644
--- a/toolchains/android/toolchain.bzl
+++ b/toolchains/android/toolchain.bzl
@@ -310,10 +310,10 @@ _ATTRS = dict(
executable = True,
),
deploy_info_writer = attr.label(
- allow_single_file = True,
- cfg = "exec",
- default = Label("//src/tools/deploy_info"),
- executable = True,
+ allow_single_file = True,
+ cfg = "exec",
+ default = Label("//src/tools/deploy_info"),
+ executable = True,
),
translation_merger = attr.label(
cfg = "exec",