1# Copyright 2021 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#    http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""android_application rule."""
16
17load(":android_feature_module_rule.bzl", "get_feature_module_paths")
18load(":attrs.bzl", "ANDROID_APPLICATION_ATTRS")
19load(
20    "//rules:aapt.bzl",
21    _aapt = "aapt",
22)
23load(
24    "//rules:bundletool.bzl",
25    _bundletool = "bundletool",
26)
27load(
28    "//rules:busybox.bzl",
29    _busybox = "busybox",
30)
31load(
32    "//rules:common.bzl",
33    _common = "common",
34)
35load(
36    "//rules:java.bzl",
37    _java = "java",
38)
39load(
40    "//rules:providers.bzl",
41    "AndroidBundleInfo",
42    "AndroidFeatureModuleInfo",
43    "StarlarkAndroidResourcesInfo",
44)
45load(
46    "//rules:utils.bzl",
47    "get_android_toolchain",
48    _log = "log",
49)
50
51UNSUPPORTED_ATTRS = [
52    "srcs",
53]
54
55def _verify_attrs(attrs, fqn):
56    for attr in UNSUPPORTED_ATTRS:
57        if hasattr(attrs, attr):
58            _log.error("Unsupported attr: %s in android_application" % attr)
59
60    if not attrs.get("manifest_values", {}).get("applicationId"):
61        _log.error("%s missing required applicationId in manifest_values" % fqn)
62
63    for attr in ["deps"]:
64        if attr not in attrs:
65            _log.error("%s missing require attribute `%s`" % (fqn, attr))
66
67def _process_feature_module(
68        ctx,
69        out = None,
70        base_apk = None,
71        feature_target = None,
72        java_package = None,
73        application_id = None):
74    manifest = _create_feature_manifest(
75        ctx,
76        base_apk,
77        java_package,
78        feature_target,
79        ctx.attr._android_sdk[AndroidSdkInfo].aapt2,
80        ctx.executable._feature_manifest_script,
81        ctx.executable._priority_feature_manifest_script,
82        get_android_toolchain(ctx).android_resources_busybox,
83        _common.get_host_javabase(ctx),
84    )
85    res = feature_target[AndroidFeatureModuleInfo].library[StarlarkAndroidResourcesInfo]
86    binary = feature_target[AndroidFeatureModuleInfo].binary[ApkInfo].unsigned_apk
87    has_native_libs = bool(feature_target[AndroidFeatureModuleInfo].binary[AndroidIdeInfo].native_libs)
88
89    # Create res .proto-apk_, output depending on whether this split has native libs.
90    if has_native_libs:
91        res_apk = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "/res.proto-ap_")
92    else:
93        res_apk = out
94    _busybox.package(
95        ctx,
96        out_r_src_jar = ctx.actions.declare_file("R.srcjar", sibling = manifest),
97        out_r_txt = ctx.actions.declare_file("R.txt", sibling = manifest),
98        out_symbols = ctx.actions.declare_file("merged.bin", sibling = manifest),
99        out_manifest = ctx.actions.declare_file("AndroidManifest_processed.xml", sibling = manifest),
100        out_proguard_cfg = ctx.actions.declare_file("proguard.cfg", sibling = manifest),
101        out_main_dex_proguard_cfg = ctx.actions.declare_file(
102            "main_dex_proguard.cfg",
103            sibling = manifest,
104        ),
105        out_resource_files_zip = ctx.actions.declare_file("resource_files.zip", sibling = manifest),
106        out_file = res_apk,
107        manifest = manifest,
108        java_package = java_package,
109        direct_resources_nodes = res.direct_resources_nodes,
110        transitive_resources_nodes = res.transitive_resources_nodes,
111        transitive_manifests = [res.transitive_manifests],
112        transitive_assets = [res.transitive_assets],
113        transitive_compiled_assets = [res.transitive_compiled_assets],
114        transitive_resource_files = [res.transitive_resource_files],
115        transitive_compiled_resources = [res.transitive_compiled_resources],
116        transitive_r_txts = [res.transitive_r_txts],
117        additional_apks_to_link_against = [base_apk],
118        proto_format = True,  # required for aab.
119        android_jar = ctx.attr._android_sdk[AndroidSdkInfo].android_jar,
120        aapt = get_android_toolchain(ctx).aapt2.files_to_run,
121        busybox = get_android_toolchain(ctx).android_resources_busybox.files_to_run,
122        host_javabase = _common.get_host_javabase(ctx),
123        should_throw_on_conflict = True,
124        application_id = application_id,
125    )
126
127    if not has_native_libs:
128        return
129
130    # Extract libs/ from split binary
131    native_libs = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "/native_libs.zip")
132    _common.filter_zip_include(ctx, binary, native_libs, ["lib/*"])
133
134    # Extract AndroidManifest.xml and assets from res-ap_
135    filtered_res = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "/filtered_res.zip")
136    _common.filter_zip_include(ctx, res_apk, filtered_res, ["AndroidManifest.xml", "assets/*"])
137
138    # Merge into output
139    _java.singlejar(
140        ctx,
141        inputs = [filtered_res, native_libs],
142        output = out,
143        java_toolchain = _common.get_java_toolchain(ctx),
144    )
145
146def _create_feature_manifest(
147        ctx,
148        base_apk,
149        java_package,
150        feature_target,
151        aapt2,
152        feature_manifest_script,
153        priority_feature_manifest_script,
154        android_resources_busybox,
155        host_javabase):
156    info = feature_target[AndroidFeatureModuleInfo]
157    manifest = ctx.actions.declare_file(ctx.label.name + "/" + feature_target.label.name + "/AndroidManifest.xml")
158
159    # Rule has not specified a manifest. Populate the default manifest template.
160    if not info.manifest:
161        args = ctx.actions.args()
162        args.add(manifest.path)
163        args.add(base_apk.path)
164        args.add(java_package)
165        args.add(info.feature_name)
166        args.add(info.title_id)
167        args.add(info.fused)
168        args.add(aapt2.executable)
169
170        ctx.actions.run(
171            executable = feature_manifest_script,
172            inputs = [base_apk],
173            outputs = [manifest],
174            arguments = [args],
175            tools = [
176                aapt2,
177            ],
178            mnemonic = "GenFeatureManifest",
179            progress_message = "Generating AndroidManifest.xml for " + feature_target.label.name,
180            toolchain = None,
181        )
182        return manifest
183
184    # Rule has a manifest (already validated by android_feature_module).
185    # Generate a priority manifest and then merge the user supplied manifest.
186    priority_manifest = ctx.actions.declare_file(
187        ctx.label.name + "/" + feature_target.label.name + "/Prioriy_AndroidManifest.xml",
188    )
189    args = ctx.actions.args()
190    args.add(priority_manifest.path)
191    args.add(base_apk.path)
192    args.add(java_package)
193    args.add(info.feature_name)
194    args.add(aapt2.executable)
195    ctx.actions.run(
196        executable = priority_feature_manifest_script,
197        inputs = [base_apk],
198        outputs = [priority_manifest],
199        arguments = [args],
200        tools = [
201            aapt2,
202        ],
203        mnemonic = "GenPriorityFeatureManifest",
204        progress_message = "Generating Priority AndroidManifest.xml for " + feature_target.label.name,
205        toolchain = None,
206    )
207
208    args = ctx.actions.args()
209    args.add("--main_manifest", priority_manifest.path)
210    args.add("--feature_manifest", info.manifest.path)
211    args.add("--feature_title", "@string/" + info.title_id)
212    args.add("--out", manifest.path)
213    ctx.actions.run(
214        executable = ctx.attr._merge_manifests.files_to_run,
215        inputs = [priority_manifest, info.manifest],
216        outputs = [manifest],
217        arguments = [args],
218        toolchain = None,
219    )
220
221    return manifest
222
223def _impl(ctx):
224    # Convert base apk to .proto_ap_
225    base_apk = ctx.attr.base_module[ApkInfo].unsigned_apk
226    base_proto_apk = ctx.actions.declare_file(ctx.label.name + "/modules/base.proto-ap_")
227    _aapt.convert(
228        ctx,
229        out = base_proto_apk,
230        input = base_apk,
231        to_proto = True,
232        aapt = get_android_toolchain(ctx).aapt2.files_to_run,
233    )
234    proto_apks = [base_proto_apk]
235
236    # Convert each feature to .proto-ap_
237    for feature in ctx.attr.feature_modules:
238        feature_proto_apk = ctx.actions.declare_file(
239            "%s.proto-ap_" % feature.label.name,
240            sibling = base_proto_apk,
241        )
242        _process_feature_module(
243            ctx,
244            out = feature_proto_apk,
245            base_apk = base_apk,
246            feature_target = feature,
247            java_package = _java.resolve_package_from_label(ctx.label, ctx.attr.custom_package),
248            application_id = ctx.attr.application_id,
249        )
250        proto_apks.append(feature_proto_apk)
251
252    # Convert each each .proto-ap_ to module zip
253    modules = []
254    for proto_apk in proto_apks:
255        module = ctx.actions.declare_file(
256            proto_apk.basename + ".zip",
257            sibling = proto_apk,
258        )
259        modules.append(module)
260        _bundletool.proto_apk_to_module(
261            ctx,
262            out = module,
263            proto_apk = proto_apk,
264            bundletool_module_builder =
265                get_android_toolchain(ctx).bundletool_module_builder.files_to_run,
266        )
267
268    metadata = dict()
269    if ProguardMappingInfo in ctx.attr.base_module:
270        metadata["com.android.tools.build.obfuscation/proguard.map"] = ctx.attr.base_module[ProguardMappingInfo].proguard_mapping
271
272    if ctx.file.rotation_config:
273        metadata["com.google.play.apps.signing/RotationConfig.textproto"] = ctx.file.rotation_config
274
275    if ctx.file.app_integrity_config:
276        metadata["com.google.play.apps.integrity/AppIntegrityConfig.pb"] = ctx.file.app_integrity_config
277
278    # Create .aab
279    _bundletool.build(
280        ctx,
281        out = ctx.outputs.unsigned_aab,
282        modules = modules,
283        config = ctx.file.bundle_config_file,
284        metadata = metadata,
285        bundletool = get_android_toolchain(ctx).bundletool.files_to_run,
286        host_javabase = _common.get_host_javabase(ctx),
287    )
288
289    # Create `blaze run` script
290    base_apk_info = ctx.attr.base_module[ApkInfo]
291    deploy_script_files = [base_apk_info.signing_keys[-1]]
292    subs = {
293        "%bundletool_path%": get_android_toolchain(ctx).bundletool.files_to_run.executable.short_path,
294        "%aab%": ctx.outputs.unsigned_aab.short_path,
295        "%newest_key%": base_apk_info.signing_keys[-1].short_path,
296    }
297    if base_apk_info.signing_lineage:
298        signer_properties = _common.create_signer_properties(ctx, base_apk_info.signing_keys[0])
299        subs["%oldest_signer_properties%"] = signer_properties.short_path
300        subs["%lineage%"] = base_apk_info.signing_lineage.short_path
301        subs["%min_rotation_api%"] = base_apk_info.signing_min_v3_rotation_api_version
302        deploy_script_files.extend(
303            [signer_properties, base_apk_info.signing_lineage, base_apk_info.signing_keys[0]],
304        )
305    else:
306        subs["%oldest_signer_properties%"] = ""
307        subs["%lineage%"] = ""
308        subs["%min_rotation_api%"] = ""
309    ctx.actions.expand_template(
310        template = ctx.file._bundle_deploy,
311        output = ctx.outputs.deploy_script,
312        substitutions = subs,
313        is_executable = True,
314    )
315
316    return [
317        ctx.attr.base_module[ApkInfo],
318        ctx.attr.base_module[AndroidPreDexJarInfo],
319        AndroidBundleInfo(unsigned_aab = ctx.outputs.unsigned_aab),
320        DefaultInfo(
321            executable = ctx.outputs.deploy_script,
322            runfiles = ctx.runfiles([
323                ctx.outputs.unsigned_aab,
324                get_android_toolchain(ctx).bundletool.files_to_run.executable,
325            ] + deploy_script_files),
326        ),
327    ]
328
329android_application = rule(
330    attrs = ANDROID_APPLICATION_ATTRS,
331    cfg = android_common.android_platforms_transition,
332    fragments = [
333        "android",
334        "java",
335    ],
336    executable = True,
337    implementation = _impl,
338    outputs = {
339        "deploy_script": "%{name}.sh",
340        "unsigned_aab": "%{name}_unsigned.aab",
341    },
342    toolchains = [
343        "//toolchains/android:toolchain_type",
344        "@bazel_tools//tools/jdk:toolchain_type",
345    ],
346    _skylark_testable = True,
347)
348
349def android_application_macro(_android_binary, **attrs):
350    """android_application_macro.
351
352    Args:
353      _android_binary: The android_binary rule to use.
354      **attrs: android_application attributes.
355    """
356
357    fqn = "//%s:%s" % (native.package_name(), attrs["name"])
358
359    # Must pop these because android_binary does not have these attributes.
360    app_integrity_config = attrs.pop("app_integrity_config", None)
361    rotation_config = attrs.pop("rotation_config", None)
362
363    # Simply fall back to android_binary if no feature splits or bundle_config
364    if not attrs.get("feature_modules", None) and not (attrs.get("bundle_config", None) or attrs.get("bundle_config_file", None)):
365        _android_binary(**attrs)
366        return
367
368    _verify_attrs(attrs, fqn)
369
370    # Create an android_binary base split, plus an android_application to produce the aab
371    name = attrs.pop("name")
372    base_split_name = "%s_base" % name
373
374    # default to [] if feature_modules = None is passed
375    feature_modules = attrs.pop("feature_modules", []) or []
376    bundle_config = attrs.pop("bundle_config", None)
377    bundle_config_file = attrs.pop("bundle_config_file", None)
378
379    # bundle_config is deprecated in favor of bundle_config_file
380    # In the future bundle_config will accept a build rule rather than a raw file.
381    bundle_config_file = bundle_config_file or bundle_config
382
383    for feature_module in feature_modules:
384        if not feature_module.startswith("//") or ":" not in feature_module:
385            _log.error("feature_modules expects fully qualified paths, i.e. //some/path:target")
386        module_targets = get_feature_module_paths(feature_module)
387        attrs["deps"].append(str(module_targets.title_lib))
388
389    _android_binary(
390        name = base_split_name,
391        **attrs
392    )
393
394    android_application(
395        name = name,
396        base_module = ":%s" % base_split_name,
397        bundle_config_file = bundle_config_file,
398        app_integrity_config = app_integrity_config,
399        rotation_config = rotation_config,
400        custom_package = attrs.get("custom_package", None),
401        testonly = attrs.get("testonly"),
402        transitive_configs = attrs.get("transitive_configs", []),
403        feature_modules = feature_modules,
404        application_id = attrs["manifest_values"]["applicationId"],
405        visibility = attrs.get("visibility", None),
406    )
407