# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under both the MIT license found in the
# LICENSE-MIT file in the root directory of this source tree and the Apache
# License, Version 2.0 found in the LICENSE-APACHE file in the root directory
# of this source tree.
load("@prelude//:validation_deps.bzl", "get_validation_deps_outputs")
load("@prelude//android:android_binary.bzl", "get_binary_info")
load("@prelude//android:android_providers.bzl", "AndroidApkInfo", "AndroidApkUnderTestInfo", "AndroidBinaryNativeLibsInfo", "AndroidBinaryResourcesInfo", "DexFilesInfo", "ExopackageInfo")
load("@prelude//android:android_toolchain.bzl", "AndroidToolchainInfo")
load("@prelude//java:class_to_srcs.bzl", "merge_class_to_source_map_from_jar")
load("@prelude//java:java_providers.bzl", "KeystoreInfo")
load("@prelude//java:java_toolchain.bzl", "JavaToolchainInfo")
load("@prelude//java/utils:java_more_utils.bzl", "get_path_separator_for_exec_os")
load("@prelude//java/utils:java_utils.bzl", "get_class_to_source_map_info")
load("@prelude//utils:argfile.bzl", "argfile")
load("@prelude//utils:utils.bzl", "flatten")
def android_apk_impl(ctx: AnalysisContext) -> list[Provider]:
android_binary_info = get_binary_info(ctx, use_proto_format = False)
java_packaging_deps = android_binary_info.java_packaging_deps
sub_targets = android_binary_info.sub_targets
dex_files_info = android_binary_info.dex_files_info
native_library_info = android_binary_info.native_library_info
resources_info = android_binary_info.resources_info
validation_info = android_binary_info.validation_info
keystore = ctx.attrs.keystore[KeystoreInfo]
output_apk = build_apk(
label = ctx.label,
actions = ctx.actions,
android_toolchain = ctx.attrs._android_toolchain[AndroidToolchainInfo],
keystore = keystore,
dex_files_info = dex_files_info,
native_library_info = native_library_info,
resources_info = resources_info,
compress_resources_dot_arsc = ctx.attrs.resource_compression == "enabled" or ctx.attrs.resource_compression == "enabled_with_strings_as_assets",
validation_deps_outputs = get_validation_deps_outputs(ctx),
packaging_options = ctx.attrs.packaging_options,
)
if dex_files_info.secondary_dex_exopackage_info or native_library_info.exopackage_info or resources_info.exopackage_info:
exopackage_info = ExopackageInfo(
secondary_dex_info = dex_files_info.secondary_dex_exopackage_info,
native_library_info = native_library_info.exopackage_info,
resources_info = resources_info.exopackage_info,
)
default_output = ctx.actions.write(
"{}_exopackage_apk_warning".format(ctx.label.name),
"exopackage apks should not be used externally, try buck install or building with exopackage disabled\n",
)
sub_targets["exo_apk"] = [DefaultInfo(default_output = output_apk)] # Used by tests
else:
exopackage_info = None
default_output = output_apk
class_to_srcs, _, class_to_srcs_subtargets = get_class_to_source_map_info(
ctx,
outputs = None,
deps = android_binary_info.deps_by_platform[android_binary_info.primary_platform],
)
transitive_class_to_src_map = merge_class_to_source_map_from_jar(
actions = ctx.actions,
name = ctx.label.name + ".transitive_class_to_src.json",
java_toolchain = ctx.attrs._java_toolchain[JavaToolchainInfo],
relative_to = None,
deps = [class_to_srcs],
)
sub_targets["transitive_class_to_src_map"] = [DefaultInfo(default_output = transitive_class_to_src_map)]
# We can only be sure that an APK has native libs if it has any shared libraries. Prebuilt native libraries dirs can exist but be empty.
definitely_has_native_libs = bool(native_library_info.shared_libraries)
install_info = get_install_info(ctx, output_apk = output_apk, manifest = resources_info.manifest, exopackage_info = exopackage_info, definitely_has_native_libs = definitely_has_native_libs)
return [
AndroidApkInfo(
apk = output_apk,
manifest = resources_info.manifest,
materialized_artifacts = android_binary_info.materialized_artifacts,
unstripped_shared_libraries = native_library_info.unstripped_shared_libraries,
),
AndroidApkUnderTestInfo(
java_packaging_deps = set([dep.label.raw_target() for dep in java_packaging_deps]),
keystore = keystore,
manifest_entries = ctx.attrs.manifest_entries,
min_sdk_version = ctx.attrs.min_sdk_version,
prebuilt_native_library_dirs = set([native_lib.raw_target for native_lib in native_library_info.prebuilt_native_library_dirs]),
platform_configurations = set([str(x.label.configured_target().config()) for x in flatten(android_binary_info.deps_by_platform.values())]),
platforms = android_binary_info.deps_by_platform.keys(),
primary_platform = android_binary_info.primary_platform,
resource_infos = set([info.raw_target for info in resources_info.unfiltered_resource_infos]),
r_dot_java_packages = set([info.specified_r_dot_java_package for info in resources_info.unfiltered_resource_infos if info.specified_r_dot_java_package]),
shared_libraries = set(native_library_info.shared_libraries),
),
DefaultInfo(default_output = default_output, other_outputs = install_info.files.values() + android_binary_info.materialized_artifacts, sub_targets = sub_targets | class_to_srcs_subtargets),
install_info,
TemplatePlaceholderInfo(
keyed_variables = {
"classpath": cmd_args([dep.jar for dep in java_packaging_deps if dep.jar], delimiter = get_path_separator_for_exec_os(ctx)),
"classpath_including_targets_with_no_output": cmd_args([dep.output_for_classpath_macro for dep in java_packaging_deps], delimiter = get_path_separator_for_exec_os(ctx)),
},
),
class_to_srcs,
] + validation_info
def build_apk(
label: Label,
actions: AnalysisActions,
keystore: KeystoreInfo,
android_toolchain: AndroidToolchainInfo,
dex_files_info: DexFilesInfo,
native_library_info: AndroidBinaryNativeLibsInfo,
resources_info: AndroidBinaryResourcesInfo,
compress_resources_dot_arsc: bool = False,
validation_deps_outputs: [list[Artifact], None] = None,
packaging_options: dict | None = None) -> Artifact:
output_apk = actions.declare_output("{}.apk".format(label.name))
apk_builder_args = cmd_args(
android_toolchain.apk_builder[RunInfo],
"--output-apk",
output_apk.as_output(),
"--resource-apk",
resources_info.primary_resources_apk,
"--dex-file",
dex_files_info.primary_dex,
"--keystore-path",
keystore.store,
"--keystore-properties-path",
keystore.properties,
"--zipalign_tool",
android_toolchain.zipalign[RunInfo],
"--package-meta-inf-version-files" if android_toolchain.package_meta_inf_version_files else [],
"--compress-resources-dot-arsc" if compress_resources_dot_arsc else [],
# The outputs of validation_deps need to be added as hidden arguments
# to an action for the validation_deps targets to be built and enforced.
hidden = validation_deps_outputs or [],
)
asset_directories = (
native_library_info.root_module_native_lib_assets +
native_library_info.non_root_module_native_lib_assets +
dex_files_info.root_module_bootstrap_dex_dirs +
dex_files_info.root_module_secondary_dex_dirs +
dex_files_info.non_root_module_secondary_dex_dirs +
resources_info.module_manifests
)
asset_directories_file = argfile(actions = actions, name = "asset_directories.txt", args = asset_directories)
native_library_directories = argfile(actions = actions, name = "native_library_directories", args = native_library_info.native_libs_for_primary_apk)
all_zip_files = [resources_info.packaged_string_assets] if resources_info.packaged_string_assets else []
zip_files = argfile(actions = actions, name = "zip_files", args = all_zip_files)
jar_files_that_may_contain_resources = argfile(actions = actions, name = "jar_files_that_may_contain_resources", args = resources_info.jar_files_that_may_contain_resources)
apk_builder_args.add([
"--asset-directories-list",
asset_directories_file,
"--native-libraries-directories-list",
native_library_directories,
"--zip-files-list",
zip_files,
"--jar-files-that-may-contain-resources-list",
jar_files_that_may_contain_resources,
])
if packaging_options:
for key, value in packaging_options.items():
if key != "excluded_resources":
fail("Only 'excluded_resources' is supported in packaging_options right now!")
else:
apk_builder_args.add("--excluded-resources", actions.write("excluded_resources.txt", value))
actions.run(apk_builder_args, category = "apk_build")
return output_apk
def get_install_info(
ctx: AnalysisContext,
output_apk: Artifact,
manifest: Artifact,
exopackage_info: [ExopackageInfo, None],
definitely_has_native_libs: bool = True,
apex_mode: bool = False) -> InstallInfo:
files = {
ctx.attrs.name: output_apk,
"manifest": manifest,
"options": generate_install_config(ctx, apex_mode),
}
if exopackage_info:
secondary_dex_exopackage_info = exopackage_info.secondary_dex_info
native_library_exopackage_info = exopackage_info.native_library_info
resources_info = exopackage_info.resources_info
else:
secondary_dex_exopackage_info = None
native_library_exopackage_info = None
resources_info = None
if secondary_dex_exopackage_info:
files["secondary_dex_exopackage_info_directory"] = secondary_dex_exopackage_info.directory
files["secondary_dex_exopackage_info_metadata"] = secondary_dex_exopackage_info.metadata
if native_library_exopackage_info:
files["native_library_exopackage_info_directory"] = native_library_exopackage_info.directory
files["native_library_exopackage_info_metadata"] = native_library_exopackage_info.metadata
if resources_info:
if resources_info.assets:
files["resources_exopackage_assets"] = resources_info.assets
files["resources_exopackage_assets_hash"] = resources_info.assets_hash
files["resources_exopackage_res"] = resources_info.res
files["resources_exopackage_res_hash"] = resources_info.res_hash
if secondary_dex_exopackage_info or native_library_exopackage_info or resources_info:
files["exopackage_agent_apk"] = ctx.attrs._android_toolchain[AndroidToolchainInfo].exopackage_agent_apk
if definitely_has_native_libs and hasattr(ctx.attrs, "cpu_filters"):
files["cpu_filters"] = ctx.actions.write("cpu_filters.txt", ctx.attrs.cpu_filters)
return InstallInfo(
installer = ctx.attrs._android_toolchain[AndroidToolchainInfo].installer,
files = files,
)
def generate_install_config(ctx: AnalysisContext, apex_mode: bool) -> Artifact:
data = get_install_config(apex_mode)
return ctx.actions.write_json("install_android_options.json", data)
def get_install_config(apex_mode: bool) -> dict[str, typing.Any]:
# TODO: read from toolchains
install_config = {
"adb_restart_on_failure": read_root_config("adb", "adb_restart_on_failure", "true"),
"agent_port_base": read_root_config("adb", "agent_port_base", "2828"),
"apex_mode": apex_mode,
"is_zstd_compression_enabled": read_root_config("adb", "is_zstd_compression_enabled", "false"),
"max_retries": read_root_config("adb", "retries", "5"),
"multi_install_mode": read_root_config("adb", "multi_install_mode", "false"),
"retry_delay_millis": read_root_config("adb", "retry_delay_millis", "500"),
"skip_install_metadata": read_root_config("adb", "skip_install_metadata", "false"),
"staged_install_mode": read_root_config("adb", "staged_install_mode", None),
}
adb_executable = read_root_config("android", "adb", None)
if adb_executable:
install_config["adb_executable"] = adb_executable
return install_config