diff options
Diffstat (limited to 'rules/android_application/android_application_rule.bzl')
-rw-r--r-- | rules/android_application/android_application_rule.bzl | 385 |
1 files changed, 385 insertions, 0 deletions
diff --git a/rules/android_application/android_application_rule.bzl b/rules/android_application/android_application_rule.bzl new file mode 100644 index 0000000..ab26456 --- /dev/null +++ b/rules/android_application/android_application_rule.bzl @@ -0,0 +1,385 @@ +# 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"], + ) |