| # Copyright 2021 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. |
| |
| """android_application rule.""" |
| |
| load(":android_feature_module_rule.bzl", "get_feature_module_paths") |
| load(":attrs.bzl", "ANDROID_APPLICATION_ATTRS") |
| load( |
| "@rules_android//rules:aapt.bzl", |
| _aapt = "aapt", |
| ) |
| load( |
| "@rules_android//rules:bundletool.bzl", |
| _bundletool = "bundletool", |
| ) |
| load( |
| "@rules_android//rules:busybox.bzl", |
| _busybox = "busybox", |
| ) |
| load( |
| "@rules_android//rules:common.bzl", |
| _common = "common", |
| ) |
| load( |
| "@rules_android//rules:java.bzl", |
| _java = "java", |
| ) |
| load( |
| "@rules_android//rules:providers.bzl", |
| "AndroidBundleInfo", |
| "AndroidFeatureModuleInfo", |
| "StarlarkAndroidResourcesInfo", |
| ) |
| load( |
| "@rules_android//rules:utils.bzl", |
| "get_android_toolchain", |
| _log = "log", |
| ) |
| |
| UNSUPPORTED_ATTRS = [ |
| "srcs", |
| ] |
| |
| def _verify_attrs(attrs, fqn): |
| for attr in UNSUPPORTED_ATTRS: |
| if hasattr(attrs, attr): |
| _log.error("Unsupported attr: %s in android_application" % attr) |
| |
| if not attrs.get("manifest_values", default = {}).get("applicationId"): |
| _log.error("%s missing required applicationId in manifest_values" % fqn) |
| |
| for attr in ["deps"]: |
| if attr not in attrs: |
| _log.error("%s missing require attribute `%s`" % (fqn, attr)) |
| |
| def _process_feature_module( |
| ctx, |
| out = None, |
| base_apk = None, |
| feature_target = None, |
| java_package = None, |
| application_id = None): |
| manifest = _create_feature_manifest( |
| ctx, |
| base_apk, |
| java_package, |
| feature_target, |
| ctx.attr._android_sdk[AndroidSdkInfo].aapt2, |
| ctx.executable._feature_manifest_script, |
| ctx.executable._priority_feature_manifest_script, |
| get_android_toolchain(ctx).android_resources_busybox, |
| _common.get_host_javabase(ctx), |
| ) |
| res = feature_target[AndroidFeatureModuleInfo].library[StarlarkAndroidResourcesInfo] |
| binary = feature_target[AndroidFeatureModuleInfo].binary[ApkInfo].unsigned_apk |
| has_native_libs = bool(feature_target[AndroidFeatureModuleInfo].binary[AndroidIdeInfo].native_libs) |
| |
| # Create res .proto-apk_, output depending on whether this split has native libs. |
| if has_native_libs: |
| res_apk = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "/res.proto-ap_") |
| else: |
| res_apk = out |
| _busybox.package( |
| ctx, |
| out_r_src_jar = ctx.actions.declare_file("R.srcjar", sibling = manifest), |
| out_r_txt = ctx.actions.declare_file("R.txt", sibling = manifest), |
| out_symbols = ctx.actions.declare_file("merged.bin", sibling = manifest), |
| out_manifest = ctx.actions.declare_file("AndroidManifest_processed.xml", sibling = manifest), |
| out_proguard_cfg = ctx.actions.declare_file("proguard.cfg", sibling = manifest), |
| out_main_dex_proguard_cfg = ctx.actions.declare_file( |
| "main_dex_proguard.cfg", |
| sibling = manifest, |
| ), |
| out_resource_files_zip = ctx.actions.declare_file("resource_files.zip", sibling = manifest), |
| out_file = res_apk, |
| manifest = manifest, |
| java_package = java_package, |
| direct_resources_nodes = res.direct_resources_nodes, |
| transitive_resources_nodes = res.transitive_resources_nodes, |
| transitive_manifests = [res.transitive_manifests], |
| transitive_assets = [res.transitive_assets], |
| transitive_compiled_assets = [res.transitive_compiled_assets], |
| transitive_resource_files = [res.transitive_resource_files], |
| transitive_compiled_resources = [res.transitive_compiled_resources], |
| transitive_r_txts = [res.transitive_r_txts], |
| additional_apks_to_link_against = [base_apk], |
| proto_format = True, # required for aab. |
| android_jar = ctx.attr._android_sdk[AndroidSdkInfo].android_jar, |
| aapt = get_android_toolchain(ctx).aapt2.files_to_run, |
| busybox = get_android_toolchain(ctx).android_resources_busybox.files_to_run, |
| host_javabase = _common.get_host_javabase(ctx), |
| should_throw_on_conflict = True, |
| application_id = application_id, |
| ) |
| |
| if not has_native_libs: |
| return |
| |
| # Extract libs/ from split binary |
| native_libs = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "/native_libs.zip") |
| _common.filter_zip(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(ctx, res_apk, filtered_res, ["AndroidManifest.xml", "assets/*"]) |
| |
| # Merge into output |
| _java.singlejar( |
| ctx, |
| inputs = [filtered_res, native_libs], |
| output = out, |
| exclude_build_data = True, |
| java_toolchain = _common.get_java_toolchain(ctx), |
| ) |
| |
| def _create_feature_manifest( |
| ctx, |
| base_apk, |
| java_package, |
| feature_target, |
| aapt2, |
| feature_manifest_script, |
| priority_feature_manifest_script, |
| android_resources_busybox, |
| host_javabase): |
| info = feature_target[AndroidFeatureModuleInfo] |
| manifest = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "/AndroidManifest.xml") |
| |
| # Rule has not specified a manifest. Populate the default manifest template. |
| if not info.manifest: |
| args = ctx.actions.args() |
| args.add(manifest.path) |
| args.add(base_apk.path) |
| args.add(java_package) |
| args.add(info.feature_name) |
| args.add(info.title_id) |
| args.add(info.fused) |
| args.add(aapt2.executable) |
| |
| ctx.actions.run( |
| executable = feature_manifest_script, |
| inputs = [base_apk], |
| outputs = [manifest], |
| arguments = [args], |
| tools = [ |
| aapt2, |
| ], |
| mnemonic = "GenFeatureManifest", |
| progress_message = "Generating AndroidManifest.xml for " + feature_target.label.name, |
| ) |
| return manifest |
| |
| # Rule has a manifest (already validated by android_feature_module). |
| # Generate a priority manifest and then merge the user supplied manifest. |
| priority_manifest = ctx.actions.declare_file( |
| ctx.label.name + "/" + feature_target.label.name + "/Prioriy_AndroidManifest.xml", |
| ) |
| args = ctx.actions.args() |
| args.add(priority_manifest.path) |
| args.add(base_apk.path) |
| args.add(java_package) |
| args.add(info.feature_name) |
| args.add(aapt2.executable) |
| ctx.actions.run( |
| executable = priority_feature_manifest_script, |
| inputs = [base_apk], |
| outputs = [priority_manifest], |
| arguments = [args], |
| tools = [ |
| aapt2, |
| ], |
| mnemonic = "GenPriorityFeatureManifest", |
| progress_message = "Generating Priority AndroidManifest.xml for " + feature_target.label.name, |
| ) |
| |
| _busybox.merge_manifests( |
| ctx, |
| out_file = manifest, |
| manifest = priority_manifest, |
| mergee_manifests = depset([info.manifest]), |
| java_package = java_package, |
| busybox = android_resources_busybox.files_to_run, |
| host_javabase = host_javabase, |
| manifest_values = {"MODULE_TITLE": "@string/" + info.title_id}, |
| ) |
| |
| return manifest |
| |
| def _impl(ctx): |
| # Convert base apk to .proto_ap_ |
| base_apk = ctx.attr.base_module[ApkInfo].unsigned_apk |
| base_proto_apk = ctx.actions.declare_file(ctx.label.name + "/modules/base.proto-ap_") |
| _aapt.convert( |
| ctx, |
| out = base_proto_apk, |
| input = base_apk, |
| to_proto = True, |
| aapt = get_android_toolchain(ctx).aapt2.files_to_run, |
| ) |
| proto_apks = [base_proto_apk] |
| |
| # Convert each feature to .proto-ap_ |
| for feature in ctx.attr.feature_modules: |
| feature_proto_apk = ctx.actions.declare_file( |
| "%s.proto-ap_" % feature.label.name, |
| sibling = base_proto_apk, |
| ) |
| _process_feature_module( |
| ctx, |
| out = feature_proto_apk, |
| base_apk = base_apk, |
| feature_target = feature, |
| java_package = _java.resolve_package_from_label(ctx.label, ctx.attr.custom_package), |
| application_id = ctx.attr.application_id, |
| ) |
| proto_apks.append(feature_proto_apk) |
| |
| # Convert each each .proto-ap_ to module zip |
| modules = [] |
| for proto_apk in proto_apks: |
| module = ctx.actions.declare_file( |
| proto_apk.basename + ".zip", |
| sibling = proto_apk, |
| ) |
| modules.append(module) |
| _bundletool.proto_apk_to_module( |
| ctx, |
| out = module, |
| proto_apk = proto_apk, |
| unzip = get_android_toolchain(ctx).unzip_tool.files_to_run, |
| zip = get_android_toolchain(ctx).zip_tool.files_to_run, |
| ) |
| |
| metadata = dict() |
| if ProguardMappingInfo in ctx.attr.base_module: |
| metadata["com.android.tools.build.obfuscation/proguard.map"] = ctx.attr.base_module[ProguardMappingInfo].proguard_mapping |
| |
| if ctx.file.rotation_config: |
| metadata["com.google.play.apps.signing/RotationConfig.textproto"] = ctx.file.rotation_config |
| |
| if ctx.file.app_integrity_config: |
| metadata["com.google.play.apps.integrity/AppIntegrityConfig.pb"] = ctx.file.app_integrity_config |
| |
| # Create .aab |
| _bundletool.build( |
| ctx, |
| out = ctx.outputs.unsigned_aab, |
| modules = modules, |
| config = ctx.file.bundle_config_file, |
| metadata = metadata, |
| bundletool = get_android_toolchain(ctx).bundletool.files_to_run, |
| host_javabase = _common.get_host_javabase(ctx), |
| ) |
| |
| # Create `blaze run` script |
| subs = { |
| "%bundletool_path%": get_android_toolchain(ctx).bundletool.files_to_run.executable.short_path, |
| "%aab%": ctx.outputs.unsigned_aab.short_path, |
| "%key%": ctx.attr.base_module[ApkInfo].signing_keys[0].short_path, |
| } |
| ctx.actions.expand_template( |
| template = ctx.file._bundle_deploy, |
| output = ctx.outputs.deploy_script, |
| substitutions = subs, |
| is_executable = True, |
| ) |
| |
| return [ |
| ctx.attr.base_module[ApkInfo], |
| ctx.attr.base_module[AndroidPreDexJarInfo], |
| AndroidBundleInfo(unsigned_aab = ctx.outputs.unsigned_aab), |
| DefaultInfo( |
| executable = ctx.outputs.deploy_script, |
| runfiles = ctx.runfiles([ |
| ctx.outputs.unsigned_aab, |
| ctx.attr.base_module[ApkInfo].signing_keys[0], |
| get_android_toolchain(ctx).bundletool.files_to_run.executable, |
| ]), |
| ), |
| ] |
| |
| android_application = rule( |
| attrs = ANDROID_APPLICATION_ATTRS, |
| fragments = [ |
| "android", |
| "java", |
| ], |
| executable = True, |
| implementation = _impl, |
| outputs = { |
| "deploy_script": "%{name}.sh", |
| "unsigned_aab": "%{name}_unsigned.aab", |
| }, |
| toolchains = ["@rules_android//toolchains/android:toolchain_type"], |
| _skylark_testable = True, |
| ) |
| |
| def android_application_macro(_android_binary, **attrs): |
| """android_application_macro. |
| |
| Args: |
| _android_binary: The android_binary rule to use. |
| **attrs: android_application attributes. |
| """ |
| |
| fqn = "//%s:%s" % (native.package_name(), attrs["name"]) |
| |
| # Must pop these because android_binary does not have these attributes. |
| app_integrity_config = attrs.pop("app_integrity_config", default = None) |
| rotation_config = attrs.pop("rotation_config", default = None) |
| |
| # Simply fall back to android_binary if no feature splits or bundle_config |
| if not attrs.get("feature_modules", None) and not (attrs.get("bundle_config", None) or attrs.get("bundle_config_file", None)): |
| _android_binary(**attrs) |
| return |
| |
| _verify_attrs(attrs, fqn) |
| |
| # Create an android_binary base split, plus an android_application to produce the aab |
| name = attrs.pop("name") |
| base_split_name = "%s_base" % name |
| |
| # default to [] if feature_modules = None is passed |
| feature_modules = attrs.pop("feature_modules", default = []) or [] |
| bundle_config = attrs.pop("bundle_config", default = None) |
| bundle_config_file = attrs.pop("bundle_config_file", default = None) |
| |
| # bundle_config is deprecated in favor of bundle_config_file |
| # In the future bundle_config will accept a build rule rather than a raw file. |
| bundle_config_file = bundle_config_file or bundle_config |
| |
| for feature_module in feature_modules: |
| if not feature_module.startswith("//") or ":" not in feature_module: |
| _log.error("feature_modules expects fully qualified paths, i.e. //some/path:target") |
| module_targets = get_feature_module_paths(feature_module) |
| attrs["deps"].append(str(module_targets.title_lib)) |
| |
| _android_binary( |
| name = base_split_name, |
| **attrs |
| ) |
| |
| android_application( |
| name = name, |
| base_module = ":%s" % base_split_name, |
| bundle_config_file = bundle_config_file, |
| app_integrity_config = app_integrity_config, |
| rotation_config = rotation_config, |
| custom_package = attrs.get("custom_package", None), |
| testonly = attrs.get("testonly"), |
| transitive_configs = attrs.get("transitive_configs", []), |
| feature_modules = feature_modules, |
| application_id = attrs["manifest_values"]["applicationId"], |
| ) |