Vendor dependencies

Let's see how I like this workflow.
This commit is contained in:
John Doty 2022-12-19 08:27:18 -08:00
parent 34d1830413
commit 9c435dc440
7500 changed files with 1665121 additions and 99 deletions

View file

@ -0,0 +1,109 @@
# 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//android:android_providers.bzl", "Aapt2LinkInfo")
BASE_PACKAGE_ID = 0x7f
def get_aapt2_link(
ctx: "context",
android_toolchain: "AndroidToolchainInfo",
aapt2_compile_rules: ["artifact"],
android_manifest: "artifact",
includes_vector_drawables: bool.type = False,
no_auto_version: bool.type = False,
no_version_transitions: bool.type = False,
no_auto_add_overlay: bool.type = False,
use_proto_format: bool.type = False,
no_resource_removal: bool.type = False,
should_keep_raw_values: bool.type = False,
package_id_offset: int.type = 0,
resource_stable_ids: ["artifact", None] = None,
preferred_density: [str.type, None] = None,
min_sdk: [int.type, None] = None,
filter_locales: bool.type = False,
locales: [str.type] = [],
compiled_resource_apks: ["artifact"] = [],
additional_aapt2_params: [str.type] = [],
extra_filtered_resources: [str.type] = []) -> Aapt2LinkInfo.type:
aapt2_command = cmd_args(android_toolchain.aapt2)
aapt2_command.add("link")
# aapt2 only supports @ for -R or input files, not for all args, so we pass in all "normal"
# args here.
resources_apk = ctx.actions.declare_output("resource-apk.ap_")
aapt2_command.add(["-o", resources_apk.as_output()])
proguard_config = ctx.actions.declare_output("proguard_config.pro")
aapt2_command.add(["--proguard", proguard_config.as_output()])
# We don't need the R.java output, but aapt2 won't output R.txt unless we also request R.java.
r_dot_java = ctx.actions.declare_output("initial-rdotjava")
aapt2_command.add(["--java", r_dot_java.as_output()])
r_dot_txt = ctx.actions.declare_output("R.txt")
aapt2_command.add(["--output-text-symbols", r_dot_txt.as_output()])
aapt2_command.add(["--manifest", android_manifest])
aapt2_command.add(["-I", android_toolchain.android_jar])
if includes_vector_drawables:
aapt2_command.add("--no-version-vectors")
if no_auto_version:
aapt2_command.add("--no-auto-version")
if no_version_transitions:
aapt2_command.add("--no-version-transitions")
if not no_auto_add_overlay:
aapt2_command.add("--auto-add-overlay")
if use_proto_format:
aapt2_command.add("--proto-format")
if no_resource_removal:
aapt2_command.add("--no-resource-removal")
if should_keep_raw_values:
aapt2_command.add("--keep-raw-values")
if package_id_offset != 0:
aapt2_command.add(["--package-id", "0x{}".format(BASE_PACKAGE_ID + package_id_offset)])
if resource_stable_ids != None:
aapt2_command.add(["--stable-ids", resource_stable_ids])
if preferred_density != None:
aapt2_command.add(["--preferred-density", preferred_density])
if min_sdk != None:
aapt2_command.add(["--min-sdk-version", min_sdk])
if filter_locales and len(locales) > 0:
aapt2_command.add("-c")
# "NONE" means "en", update the list of locales
aapt2_command.add(cmd_args([locale if locale != "NONE" else "en" for locale in locales], delimiter = ","))
for compiled_resource_apk in compiled_resource_apks:
aapt2_command.add(["-I", compiled_resource_apk])
aapt2_compile_rules_args_file = ctx.actions.write("aapt2_compile_rules_args_file", cmd_args(aapt2_compile_rules, delimiter = " "))
aapt2_command.add("-R")
aapt2_command.add(cmd_args(aapt2_compile_rules_args_file, format = "@{}"))
aapt2_command.hidden(aapt2_compile_rules)
aapt2_command.add(additional_aapt2_params)
ctx.actions.run(aapt2_command, category = "aapt2_link")
# The normal resource filtering apparatus is super slow, because it extracts the whole apk,
# strips files out of it, then repackages it.
#
# This is a faster filtering step that just uses zip -d to remove entries from the archive.
# It's also superbly dangerous.
if len(extra_filtered_resources) > 0:
filter_resources_cmd = cmd_args()
filter_resources_cmd.add(["zip", "-d"])
filter_resources_cmd.add(resources_apk)
filter_resources_cmd.add(extra_filtered_resources)
ctx.actions.run(filter_resources_cmd, category = "aapt2_filter_resources")
return Aapt2LinkInfo(
primary_resources_apk = resources_apk,
proguard_config_file = proguard_config,
r_dot_txt = r_dot_txt,
)

View file

@ -0,0 +1,208 @@
# 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//:attributes.bzl", "AaptMode", "DuplicateResourceBehaviour", "TargetCpuType")
load("@prelude//java:dex_toolchain.bzl", "DexToolchainInfo")
load("@prelude//java:java.bzl", "dex_min_sdk_version", "select_java_test_toolchain")
load("@prelude//java:java_toolchain.bzl", "JavaPlatformInfo", "JavaTestToolchainInfo", "JavaToolchainInfo")
load("@prelude//kotlin:kotlin_toolchain.bzl", "KotlinToolchainInfo")
load("@prelude//genrule.bzl", "genrule_attributes")
load(":android_apk.bzl", "android_apk_impl")
load(":android_build_config.bzl", "android_build_config_impl")
load(":android_instrumentation_apk.bzl", "android_instrumentation_apk_impl")
load(":android_instrumentation_test.bzl", "android_instrumentation_test_impl")
load(":android_library.bzl", "android_library_impl")
load(":android_manifest.bzl", "android_manifest_impl")
load(":android_prebuilt_aar.bzl", "android_prebuilt_aar_impl")
load(":android_resource.bzl", "android_resource_impl")
load(":android_toolchain.bzl", "AndroidPlatformInfo", "AndroidToolchainInfo")
load(":apk_genrule.bzl", "apk_genrule_impl")
load(":configuration.bzl", "cpu_split_transition", "cpu_split_transition_instrumentation_test_apk", "cpu_transition", "do_not_build_only_native_code_transition", "is_building_android_binary_attr")
load(":gen_aidl.bzl", "gen_aidl_impl")
load(":prebuilt_native_library.bzl", "prebuilt_native_library_impl")
load(":robolectric_test.bzl", "robolectric_test_impl")
load(":voltron.bzl", "android_app_modularity_impl")
def android_toolchain():
return attrs.toolchain_dep(
# FIXME: prelude// should be standalone (not refer to fbcode//)
default = "fbcode//buck2/platform/toolchain:android",
providers = [
AndroidPlatformInfo,
AndroidToolchainInfo,
],
)
def _dex_toolchain():
return attrs.toolchain_dep(
# FIXME: prelude// should be standalone (not refer to fbcode//)
default = "fbcode//buck2/platform/toolchain:dex_for_android",
providers = [
DexToolchainInfo,
],
)
def java_toolchain_for_android():
return attrs.toolchain_dep(
# FIXME: prelude// should be standalone (not refer to fbcode//)
default = "fbcode//buck2/platform/toolchain:java_for_android",
providers = [
JavaPlatformInfo,
JavaToolchainInfo,
],
)
def _kotlin_toolchain():
return attrs.toolchain_dep(
# FIXME: prelude// should be standalone (not refer to fbcode//)
default = "fbcode//buck2/platform/toolchain:kotlin",
providers = [
KotlinToolchainInfo,
],
)
def is_build_only_native_code():
return select(
{
"DEFAULT": False,
"fbsource//xplat/buck2/platform/android:build_only_native_code": True,
},
)
implemented_rules = {
"android_app_modularity": android_app_modularity_impl,
"android_binary": android_apk_impl,
"android_build_config": android_build_config_impl,
"android_instrumentation_apk": android_instrumentation_apk_impl,
"android_instrumentation_test": android_instrumentation_test_impl,
"android_library": android_library_impl,
"android_manifest": android_manifest_impl,
"android_prebuilt_aar": android_prebuilt_aar_impl,
"android_resource": android_resource_impl,
"apk_genrule": apk_genrule_impl,
"gen_aidl": gen_aidl_impl,
"prebuilt_native_library": prebuilt_native_library_impl,
"robolectric_test": robolectric_test_impl,
}
# Can't load `read_bool` here because it will cause circular load.
DISABLE_SPLIT_TRANSITIONS = read_config("buck2", "android_td_disable_transitions_hack") in ("True", "true")
def _transition_dep_wrapper(split_transition_dep, transition_dep):
if DISABLE_SPLIT_TRANSITIONS:
return transition_dep
return split_transition_dep
extra_attributes = {
"android_aar": {
"resources_root": attrs.option(attrs.string(), default = None),
},
"android_app_modularity": {
"_android_toolchain": android_toolchain(),
},
"android_binary": {
"aapt_mode": attrs.enum(AaptMode, default = "aapt1"), # Match default in V1
"application_module_configs": attrs.dict(key = attrs.string(), value = attrs.list(attrs.transition_dep(cfg = cpu_transition)), sorted = False, default = {}),
"build_config_values_file": attrs.option(attrs.one_of(attrs.transition_dep(cfg = cpu_transition), attrs.source()), default = None),
"deps": attrs.list(_transition_dep_wrapper(split_transition_dep = attrs.split_transition_dep(cfg = cpu_split_transition), transition_dep = attrs.transition_dep(cfg = cpu_transition)), default = []),
"dex_tool": attrs.string(default = "d8"), # Match default in V1
"duplicate_resource_behavior": attrs.enum(DuplicateResourceBehaviour, default = "allow_by_default"), # Match default in V1
"manifest": attrs.option(attrs.one_of(attrs.transition_dep(cfg = cpu_transition), attrs.source()), default = None),
"manifest_skeleton": attrs.option(attrs.one_of(attrs.transition_dep(cfg = cpu_transition), attrs.source()), default = None),
"min_sdk_version": attrs.option(attrs.int(), default = None),
"module_manifest_skeleton": attrs.option(attrs.one_of(attrs.transition_dep(cfg = cpu_transition), attrs.source()), default = None),
"_android_installer": attrs.default_only(attrs.label(
# FIXME: prelude// should be standalone (not refer to buck//)
default = "buck//src/com/facebook/buck/installer/android:android_installer",
)),
"_android_toolchain": android_toolchain(),
"_dex_toolchain": _dex_toolchain(),
"_is_building_android_binary": attrs.default_only(attrs.bool(default = True)),
"_java_toolchain": java_toolchain_for_android(),
},
"android_build_config": {
"_android_toolchain": android_toolchain(),
"_build_only_native_code": attrs.default_only(attrs.bool(default = is_build_only_native_code())),
"_is_building_android_binary": is_building_android_binary_attr(),
"_java_toolchain": java_toolchain_for_android(),
},
"android_instrumentation_apk": {
"aapt_mode": attrs.enum(AaptMode, default = "aapt1"), # Match default in V1
"apk": attrs.transition_dep(cfg = do_not_build_only_native_code_transition),
"cpu_filters": attrs.list(attrs.enum(TargetCpuType), default = []),
"deps": attrs.list(_transition_dep_wrapper(split_transition_dep = attrs.split_transition_dep(cfg = cpu_split_transition_instrumentation_test_apk), transition_dep = attrs.transition_dep(cfg = cpu_transition)), default = []),
"dex_tool": attrs.string(default = "d8"), # Match default in V1
"manifest": attrs.option(attrs.one_of(attrs.transition_dep(cfg = cpu_transition), attrs.source()), default = None),
"manifest_skeleton": attrs.option(attrs.one_of(attrs.transition_dep(cfg = cpu_transition), attrs.source()), default = None),
"min_sdk_version": attrs.option(attrs.int(), default = None),
"_android_toolchain": android_toolchain(),
"_dex_toolchain": _dex_toolchain(),
"_is_building_android_binary": attrs.default_only(attrs.bool(default = True)),
"_java_toolchain": java_toolchain_for_android(),
},
"android_instrumentation_test": {
"_android_toolchain": android_toolchain(),
"_java_toolchain": java_toolchain_for_android(),
},
"android_library": {
"resources_root": attrs.option(attrs.string(), default = None),
"_android_toolchain": android_toolchain(),
"_build_only_native_code": attrs.default_only(attrs.bool(default = is_build_only_native_code())),
"_dex_min_sdk_version": attrs.default_only(attrs.option(attrs.int(), default = dex_min_sdk_version())),
"_dex_toolchain": _dex_toolchain(),
"_is_building_android_binary": is_building_android_binary_attr(),
"_java_toolchain": java_toolchain_for_android(),
"_kotlin_toolchain": _kotlin_toolchain(),
},
"android_manifest": {
"_android_toolchain": android_toolchain(),
},
"android_prebuilt_aar": {
# Prebuilt jars are quick to build, and often contain third-party code, which in turn is
# often a source of annotations and constants. To ease migration to ABI generation from
# source without deps, we have them present during ABI gen by default.
"required_for_source_only_abi": attrs.bool(default = True),
"_android_toolchain": android_toolchain(),
"_build_only_native_code": attrs.default_only(attrs.bool(default = is_build_only_native_code())),
"_dex_min_sdk_version": attrs.default_only(attrs.option(attrs.int(), default = dex_min_sdk_version())),
"_dex_toolchain": _dex_toolchain(),
"_java_toolchain": java_toolchain_for_android(),
},
"android_resource": {
"assets": attrs.option(attrs.one_of(attrs.source(allow_directory = True), attrs.dict(key = attrs.string(), value = attrs.source(), sorted = True)), default = None),
"project_assets": attrs.option(attrs.source(allow_directory = True), default = None),
"project_res": attrs.option(attrs.source(allow_directory = True), default = None),
"res": attrs.option(attrs.one_of(attrs.source(allow_directory = True), attrs.dict(key = attrs.string(), value = attrs.source(), sorted = True)), default = None),
"_android_toolchain": android_toolchain(),
"_build_only_native_code": attrs.default_only(attrs.bool(default = is_build_only_native_code())),
},
"apk_genrule": genrule_attributes() | {
"type": attrs.string(default = "apk"),
},
"gen_aidl": {
"import_paths": attrs.list(attrs.arg(), default = []),
"_android_toolchain": android_toolchain(),
"_java_toolchain": java_toolchain_for_android(),
},
"prebuilt_native_library": {
"native_libs": attrs.source(allow_directory = True),
},
"robolectric_test": {
"resources_root": attrs.option(attrs.string(), default = None),
"robolectric_runtime_dependencies": attrs.list(attrs.source(), default = []),
"_android_toolchain": android_toolchain(),
"_is_building_android_binary": attrs.default_only(attrs.bool(default = False)),
"_java_test_toolchain": attrs.default_only(attrs.exec_dep(
default = select_java_test_toolchain(),
providers = [
JavaTestToolchainInfo,
],
)),
"_java_toolchain": java_toolchain_for_android(),
"_kotlin_toolchain": _kotlin_toolchain(),
},
}

View file

@ -0,0 +1,334 @@
# 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//android:android_binary_native_library_rules.bzl", "get_android_binary_native_library_info")
load("@prelude//android:android_binary_resources_rules.bzl", "get_android_binary_resources_info")
load("@prelude//android:android_build_config.bzl", "generate_android_build_config", "get_build_config_fields")
load("@prelude//android:android_providers.bzl", "AndroidApkInfo", "AndroidApkUnderTestInfo", "BuildConfigField", "CPU_FILTER_TO_ABI_DIRECTORY", "ExopackageInfo", "merge_android_packageable_info")
load("@prelude//android:android_toolchain.bzl", "AndroidToolchainInfo")
load("@prelude//android:configuration.bzl", "get_deps_by_platform")
load("@prelude//android:dex_rules.bzl", "get_multi_dex", "get_single_primary_dex", "get_split_dex_merge_config", "merge_to_single_dex", "merge_to_split_dex")
load("@prelude//android:exopackage.bzl", "get_exopackage_flags")
load("@prelude//android:preprocess_java_classes.bzl", "get_preprocessed_java_classes")
load("@prelude//android:proguard.bzl", "get_proguard_output")
load("@prelude//android:voltron.bzl", "get_target_to_module_mapping")
load("@prelude//java:java_providers.bzl", "KeystoreInfo", "create_java_packaging_dep", "get_all_java_packaging_deps", "get_all_java_packaging_deps_from_packaging_infos")
load("@prelude//utils:utils.bzl", "expect")
def android_apk_impl(ctx: "context") -> ["provider"]:
sub_targets = {}
_verify_params(ctx)
cpu_filters = ctx.attrs.cpu_filters or CPU_FILTER_TO_ABI_DIRECTORY.keys()
deps_by_platform = get_deps_by_platform(ctx)
primary_platform = cpu_filters[0]
deps = deps_by_platform[primary_platform]
no_dx_target_labels = [no_dx_target.label.raw_target() for no_dx_target in ctx.attrs.no_dx]
java_packaging_deps = [packaging_dep for packaging_dep in get_all_java_packaging_deps(ctx, deps) if packaging_dep.dex and packaging_dep.dex.dex.owner.raw_target() not in no_dx_target_labels]
android_packageable_info = merge_android_packageable_info(ctx.label, ctx.actions, deps)
build_config_infos = list(android_packageable_info.build_config_infos.traverse()) if android_packageable_info.build_config_infos else []
build_config_libs = _get_build_config_java_libraries(ctx, build_config_infos)
java_packaging_deps += get_all_java_packaging_deps_from_packaging_infos(ctx, build_config_libs)
has_proguard_config = ctx.attrs.proguard_config != None or ctx.attrs.android_sdk_proguard_config == "default" or ctx.attrs.android_sdk_proguard_config == "optimized"
should_pre_dex = not ctx.attrs.disable_pre_dex and not has_proguard_config and not ctx.attrs.preprocess_java_classes_bash
referenced_resources_lists = [java_packaging_dep.dex.referenced_resources for java_packaging_dep in java_packaging_deps] if ctx.attrs.trim_resource_ids and should_pre_dex else []
resources_info = get_android_binary_resources_info(
ctx,
deps,
android_packageable_info,
java_packaging_deps,
use_proto_format = False,
referenced_resources_lists = referenced_resources_lists,
manifest_entries = ctx.attrs.manifest_entries,
)
android_toolchain = ctx.attrs._android_toolchain[AndroidToolchainInfo]
java_packaging_deps += [
create_java_packaging_dep(
ctx,
r_dot_java.library_output.full_library,
dex_weight_factor = android_toolchain.r_dot_java_weight_factor,
)
for r_dot_java in resources_info.r_dot_javas
]
target_to_module_mapping_file = get_target_to_module_mapping(ctx, deps)
if should_pre_dex:
pre_dexed_libs = [java_packaging_dep.dex for java_packaging_dep in java_packaging_deps]
if ctx.attrs.use_split_dex:
dex_files_info = merge_to_split_dex(
ctx,
android_toolchain,
pre_dexed_libs,
get_split_dex_merge_config(ctx, android_toolchain),
target_to_module_mapping_file,
)
else:
dex_files_info = merge_to_single_dex(ctx, android_toolchain, pre_dexed_libs)
else:
jars_to_owners = {packaging_dep.jar: packaging_dep.jar.owner.raw_target() for packaging_dep in java_packaging_deps}
if ctx.attrs.preprocess_java_classes_bash:
jars_to_owners = get_preprocessed_java_classes(ctx, jars_to_owners)
if has_proguard_config:
proguard_output = get_proguard_output(ctx, jars_to_owners, java_packaging_deps, resources_info.proguard_config_file)
jars_to_owners = proguard_output.jars_to_owners
sub_targets["proguard_text_output"] = [
DefaultInfo(
default_outputs = [ctx.actions.symlinked_dir(
"proguard_text_output",
{artifact.basename: artifact for artifact in proguard_output.proguard_artifacts},
)],
),
]
else:
proguard_output = None
if ctx.attrs.use_split_dex:
dex_files_info = get_multi_dex(
ctx,
ctx.attrs._android_toolchain[AndroidToolchainInfo],
jars_to_owners,
ctx.attrs.primary_dex_patterns,
proguard_output.proguard_configuration_output_file if proguard_output else None,
proguard_output.proguard_mapping_output_file if proguard_output else None,
is_optimized = has_proguard_config,
apk_module_graph_file = target_to_module_mapping_file,
)
else:
dex_files_info = get_single_primary_dex(
ctx,
ctx.attrs._android_toolchain[AndroidToolchainInfo],
jars_to_owners.keys(),
is_optimized = has_proguard_config,
)
native_library_info = get_android_binary_native_library_info(ctx, android_packageable_info, deps_by_platform, apk_module_graph_file = target_to_module_mapping_file)
unstripped_native_libs = native_library_info.unstripped_libs
sub_targets["unstripped_native_libraries"] = [
DefaultInfo(
default_outputs = [ctx.actions.write("unstripped_native_libraries", unstripped_native_libs)],
other_outputs = unstripped_native_libs,
),
]
if resources_info.string_source_map:
sub_targets["generate_string_resources"] = [DefaultInfo(default_outputs = [resources_info.string_source_map])]
if dex_files_info.primary_dex_class_names:
sub_targets["primary_dex_class_names"] = [DefaultInfo(default_outputs = [dex_files_info.primary_dex_class_names])]
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",
)
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,
)
return [
AndroidApkInfo(apk = output_apk, manifest = resources_info.manifest),
AndroidApkUnderTestInfo(
java_packaging_deps = java_packaging_deps,
keystore = keystore,
manifest_entries = ctx.attrs.manifest_entries,
prebuilt_native_library_dirs = native_library_info.apk_under_test_prebuilt_native_library_dirs,
platforms = deps_by_platform.keys(),
primary_platform = primary_platform,
resource_infos = resources_info.unfiltered_resource_infos,
shared_libraries = native_library_info.apk_under_test_shared_libraries,
),
DefaultInfo(default_outputs = [output_apk], other_outputs = _get_exopackage_outputs(exopackage_info), sub_targets = sub_targets),
_get_install_info(ctx, output_apk = output_apk, manifest = resources_info.manifest, exopackage_info = exopackage_info),
]
def build_apk(
label: "label",
actions: "actions",
keystore: KeystoreInfo.type,
android_toolchain: AndroidToolchainInfo.type,
dex_files_info: "DexFilesInfo",
native_library_info: "AndroidBinaryNativeLibsInfo",
resources_info: "AndroidBinaryResourcesInfo",
compress_resources_dot_arsc: bool.type = False) -> "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],
])
if compress_resources_dot_arsc:
apk_builder_args.add("--compress-resources-dot-arsc")
asset_directories = native_library_info.native_lib_assets + dex_files_info.secondary_dex_dirs
asset_directories_file = actions.write("asset_directories.txt", asset_directories)
apk_builder_args.hidden(asset_directories)
native_library_directories = actions.write("native_library_directories", native_library_info.native_libs_for_primary_apk)
apk_builder_args.hidden(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 = actions.write("zip_files", all_zip_files)
apk_builder_args.hidden(all_zip_files)
jar_files_that_may_contain_resources = actions.write("jar_files_that_may_contain_resources", resources_info.jar_files_that_may_contain_resources)
apk_builder_args.hidden(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,
])
actions.run(apk_builder_args, category = "apk_build")
return output_apk
def _get_install_info(ctx: "context", output_apk: "artifact", manifest: "artifact", exopackage_info: ExopackageInfo.type) -> InstallInfo.type:
files = {
ctx.attrs.name: output_apk,
"manifest": manifest,
"options": generate_install_config(ctx),
}
secondary_dex_exopackage_info = exopackage_info.secondary_dex_info
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
native_library_exopackage_info = exopackage_info.native_library_info
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
resources_info = exopackage_info.resources_info
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
files["resources_exopackage_third_party_jar_resources"] = resources_info.third_party_jar_resources
files["resources_exopackage_third_party_jar_resources_hash"] = resources_info.third_party_jar_resources_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
return InstallInfo(
installer = ctx.attrs._android_installer,
files = files,
)
def _get_build_config_java_libraries(ctx: "context", build_config_infos: ["AndroidBuildConfigInfo"]) -> ["JavaPackagingInfo"]:
# BuildConfig deps should not be added for instrumented APKs because BuildConfig.class has
# already been added to the APK under test.
if ctx.attrs.package_type == "instrumented":
return []
build_config_constants = [
BuildConfigField(type = "boolean", name = "DEBUG", value = str(ctx.attrs.package_type != "release").lower()),
BuildConfigField(type = "boolean", name = "IS_EXOPACKAGE", value = str(len(ctx.attrs.exopackage_modes) > 0).lower()),
BuildConfigField(type = "int", name = "EXOPACKAGE_FLAGS", value = str(get_exopackage_flags(ctx.attrs.exopackage_modes))),
]
default_build_config_fields = get_build_config_fields(ctx.attrs.build_config_values)
java_libraries = []
java_packages_seen = []
for build_config_info in build_config_infos:
java_package = build_config_info.package
expect(java_package not in java_packages_seen, "Got the same java_package {} for different AndroidBuildConfigs".format(java_package))
java_packages_seen.append(java_package)
all_build_config_values = {}
for build_config_field in build_config_info.build_config_fields + default_build_config_fields + build_config_constants:
all_build_config_values[build_config_field.name] = build_config_field
java_libraries.append(generate_android_build_config(
ctx,
java_package,
java_package,
True, # use_constant_expressions
all_build_config_values.values(),
ctx.attrs.build_config_values_file[DefaultInfo].default_outputs[0] if type(ctx.attrs.build_config_values_file) == "dependency" else ctx.attrs.build_config_values_file,
)[1])
return java_libraries
def _get_exopackage_outputs(exopackage_info: ExopackageInfo.type) -> ["artifact"]:
outputs = []
secondary_dex_exopackage_info = exopackage_info.secondary_dex_info
if secondary_dex_exopackage_info:
outputs.append(secondary_dex_exopackage_info.metadata)
outputs.append(secondary_dex_exopackage_info.directory)
native_library_exopackage_info = exopackage_info.native_library_info
if native_library_exopackage_info:
outputs.append(native_library_exopackage_info.metadata)
outputs.append(native_library_exopackage_info.directory)
resources_info = exopackage_info.resources_info
if resources_info:
outputs.append(resources_info.res)
outputs.append(resources_info.res_hash)
outputs.append(resources_info.third_party_jar_resources)
outputs.append(resources_info.third_party_jar_resources_hash)
if resources_info.assets:
outputs.append(resources_info.assets)
outputs.append(resources_info.assets_hash)
return outputs
def _verify_params(ctx: "context"):
expect(ctx.attrs.aapt_mode == "aapt2", "aapt1 is deprecated!")
expect(ctx.attrs.dex_tool == "d8", "dx is deprecated!")
expect(ctx.attrs.allow_r_dot_java_in_secondary_dex == True)
def generate_install_config(ctx: "context") -> "artifact":
data = get_install_config()
return ctx.actions.write_json("install_android_options.json", data)
def get_install_config() -> {str.type: ""}:
# TODO: read from toolchains
return {
"adb_executable": read_config("android", "adb", "/opt/android_sdk/platform-tools/adb"),
"adb_restart_on_failure": read_config("adb", "adb_restart_on_failure", "false"),
"agent_port_base": read_config("adb", "agent_port_base", "2828"),
"always_use_java_agent": read_config("adb", "always_use_java_agent", "false"),
"is_zstd_compression_enabled": read_config("adb", "is_zstd_compression_enabled", "false"),
"multi_install_mode": read_config("adb", "multi_install_mode", "false"),
"skip_install_metadata": read_config("adb", "skip_install_metadata", "false"),
}

View file

@ -0,0 +1,404 @@
# 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//:paths.bzl", "paths")
load("@prelude//android:android_providers.bzl", "AndroidBinaryNativeLibsInfo", "CPU_FILTER_TO_ABI_DIRECTORY", "ExopackageNativeInfo")
load("@prelude//android:android_toolchain.bzl", "AndroidToolchainInfo")
load("@prelude//android:voltron.bzl", "ROOT_MODULE", "all_targets_in_root_module", "get_apk_module_graph_info", "is_root_module")
load("@prelude//linking:shared_libraries.bzl", "SharedLibraryInfo", "merge_shared_libraries", "traverse_shared_library_info")
load("@prelude//utils:utils.bzl", "expect")
# Native libraries on Android are built for a particular Application Binary Interface (ABI). We
# package native libraries for one (or more, for multi-arch builds) ABIs into an Android APK.
#
# Our native libraries come from two sources:
# 1. "Prebuilt native library dirs", which are directory artifacts whose sub-directories are ABIs,
# and those ABI subdirectories contain native libraries. These come from `android_prebuilt_aar`s
# and `prebuilt_native_library`s, for example.
# 2. "Native linkables". These are each a single shared library - `.so`s for one particular ABI.
#
# Native libraries can be packaged into Android APKs in two ways.
# 1. As native libraries. This means that they are passed to the APK builder as native libraries,
# and the APK builder will package `<ABI>/library.so` into the APK at `libs/<ABI>/library.so`.
# 2. As assets. These are passed to the APK build as assets, and are stored at
# `assets/lib/<ABI>/library.so` In the root module, we only package a native library as an
# asset if it is eligible to be an asset (e.g. `can_be_asset` on a `cxx_library`), and
# `package_asset_libraries` is set to True for the APK. We will additionally compress all the
# assets into a single `assets/lib/libs.xz` (or `assets/libs/libs.zstd` for `zstd` compression)
# if `compress_asset_libraries` is set to True for the APK. Regardless of whether we compress
# the assets or not, we create a metadata file at `assets/libs/metadata.txt` that has a single
# line entry for each packaged asset consisting of '<ABI/library_name> <file_size> <sha256>'.
#
# Any native library that is not part of the root module (i.e. it is part of some other Voltron
# module) is automatically packaged as an asset, and the assets for each module are compressed
# to a single `assets/<module_name>/libs.xz`. Similarly, the metadata for each module is stored
# at `assets/<module_name>/libs.txt`.
def get_android_binary_native_library_info(
ctx: "context",
android_packageable_info: "AndroidPackageableInfo",
deps_by_platform: {str.type: ["dependency"]},
apk_module_graph_file: ["artifact", None] = None,
prebuilt_native_library_dirs_to_exclude: ["PrebuiltNativeLibraryDir"] = [],
shared_libraries_to_exclude: ["SharedLibrary"] = []) -> AndroidBinaryNativeLibsInfo.type:
traversed_prebuilt_native_library_dirs = android_packageable_info.prebuilt_native_library_dirs.traverse() if android_packageable_info.prebuilt_native_library_dirs else []
all_prebuilt_native_library_dirs = [native_lib for native_lib in traversed_prebuilt_native_library_dirs if native_lib not in prebuilt_native_library_dirs_to_exclude]
unstripped_libs = []
all_shared_libraries = []
platform_to_native_linkables = {}
for platform, deps in deps_by_platform.items():
shared_library_info = merge_shared_libraries(
ctx.actions,
deps = filter(None, [x.get(SharedLibraryInfo) for x in deps]),
)
native_linkables = {so_name: shared_lib for so_name, shared_lib in traverse_shared_library_info(shared_library_info).items() if shared_lib not in shared_libraries_to_exclude}
all_shared_libraries.extend(native_linkables.values())
unstripped_libs += [shared_lib.lib.output for shared_lib in native_linkables.values()]
platform_to_native_linkables[platform] = native_linkables
if apk_module_graph_file == None:
native_libs_and_assets_info = _get_native_libs_and_assets(
ctx,
all_targets_in_root_module,
all_prebuilt_native_library_dirs,
platform_to_native_linkables,
)
native_libs_for_primary_apk, exopackage_info = _get_exopackage_info(
ctx,
native_libs_and_assets_info.native_libs_always_in_primary_apk,
native_libs_and_assets_info.native_libs,
native_libs_and_assets_info.native_libs_metadata,
)
native_lib_assets = filter(None, [
native_libs_and_assets_info.native_lib_assets_for_primary_apk,
native_libs_and_assets_info.stripped_native_linkable_assets_for_primary_apk,
native_libs_and_assets_info.metadata_assets,
native_libs_and_assets_info.compressed_lib_assets,
])
return AndroidBinaryNativeLibsInfo(
apk_under_test_prebuilt_native_library_dirs = all_prebuilt_native_library_dirs,
apk_under_test_shared_libraries = all_shared_libraries,
native_libs_for_primary_apk = native_libs_for_primary_apk,
exopackage_info = exopackage_info,
unstripped_libs = unstripped_libs,
native_lib_assets = native_lib_assets,
)
else:
native_libs = ctx.actions.declare_output("native_libs_symlink")
native_libs_metadata = ctx.actions.declare_output("native_libs_metadata_symlink")
native_libs_always_in_primary_apk = ctx.actions.declare_output("native_libs_always_in_primary_apk_symlink")
native_lib_assets_for_primary_apk = ctx.actions.declare_output("native_lib_assets_for_primary_apk_symlink")
stripped_native_linkable_assets_for_primary_apk = ctx.actions.declare_output("stripped_native_linkable_assets_for_primary_apk_symlink")
metadata_assets = ctx.actions.declare_output("metadata_assets_symlink")
compressed_lib_assets = ctx.actions.declare_output("compressed_lib_assets_symlink")
outputs = [
native_libs,
native_libs_metadata,
native_libs_always_in_primary_apk,
native_lib_assets_for_primary_apk,
stripped_native_linkable_assets_for_primary_apk,
metadata_assets,
compressed_lib_assets,
]
def get_native_libs_info_modular(ctx: "context", artifacts, outputs):
get_module_from_target = get_apk_module_graph_info(ctx, apk_module_graph_file, artifacts).target_to_module_mapping_function
dynamic_info = _get_native_libs_and_assets(
ctx,
get_module_from_target,
all_prebuilt_native_library_dirs,
platform_to_native_linkables,
)
# Since we are using a dynamic action, we need to declare the outputs in advance.
# Rather than passing the created outputs into `_get_native_libs_and_assets`, we
# just symlink to the outputs that function produces.
ctx.actions.symlink_file(outputs[native_libs], dynamic_info.native_libs)
ctx.actions.symlink_file(outputs[native_libs_metadata], dynamic_info.native_libs_metadata)
ctx.actions.symlink_file(outputs[native_libs_always_in_primary_apk], dynamic_info.native_libs_always_in_primary_apk)
ctx.actions.symlink_file(outputs[native_lib_assets_for_primary_apk], dynamic_info.native_lib_assets_for_primary_apk if dynamic_info.native_lib_assets_for_primary_apk else ctx.actions.symlinked_dir("empty_native_lib_assets", {}))
ctx.actions.symlink_file(outputs[stripped_native_linkable_assets_for_primary_apk], dynamic_info.stripped_native_linkable_assets_for_primary_apk if dynamic_info.stripped_native_linkable_assets_for_primary_apk else ctx.actions.symlinked_dir("empty_stripped_native_linkable_assets", {}))
ctx.actions.symlink_file(outputs[metadata_assets], dynamic_info.metadata_assets)
ctx.actions.symlink_file(outputs[compressed_lib_assets], dynamic_info.compressed_lib_assets)
ctx.actions.dynamic_output(dynamic = [apk_module_graph_file], inputs = [], outputs = outputs, f = get_native_libs_info_modular)
native_libs_for_primary_apk, exopackage_info = _get_exopackage_info(ctx, native_libs_always_in_primary_apk, native_libs, native_libs_metadata)
return AndroidBinaryNativeLibsInfo(
apk_under_test_prebuilt_native_library_dirs = all_prebuilt_native_library_dirs,
apk_under_test_shared_libraries = all_shared_libraries,
native_libs_for_primary_apk = native_libs_for_primary_apk,
exopackage_info = exopackage_info,
unstripped_libs = unstripped_libs,
native_lib_assets = [native_lib_assets_for_primary_apk, stripped_native_linkable_assets_for_primary_apk, metadata_assets, compressed_lib_assets],
)
# We could just return two artifacts of libs (one for the primary APK, one which can go
# either into the primary APK or be exopackaged), and one artifact of assets,
# but we'd need an extra action in order to combine them (we can't use `symlinked_dir` since
# the paths overlap) so it's easier to just be explicit about exactly what we produce.
_NativeLibsAndAssetsInfo = record(
native_libs = "artifact",
native_libs_metadata = "artifact",
native_libs_always_in_primary_apk = "artifact",
native_lib_assets_for_primary_apk = ["artifact", None],
stripped_native_linkable_assets_for_primary_apk = ["artifact", None],
metadata_assets = "artifact",
compressed_lib_assets = "artifact",
)
def _get_exopackage_info(
ctx: "context",
native_libs_always_in_primary_apk: "artifact",
native_libs: "artifact",
native_libs_metadata: "artifact") -> (["artifact"], [ExopackageNativeInfo.type, None]):
is_exopackage_enabled_for_native_libs = "native_library" in getattr(ctx.attrs, "exopackage_modes", [])
if is_exopackage_enabled_for_native_libs:
return [native_libs_always_in_primary_apk], ExopackageNativeInfo(directory = native_libs, metadata = native_libs_metadata)
else:
return [native_libs, native_libs_always_in_primary_apk], None
def _get_native_libs_and_assets(
ctx: "context",
get_module_from_target: "function",
all_prebuilt_native_library_dirs: ["PrebuiltNativeLibraryDir"],
platform_to_native_linkables: {str.type: {str.type: "SharedLibrary"}}) -> _NativeLibsAndAssetsInfo.type:
is_packaging_native_libs_as_assets_supported = getattr(ctx.attrs, "package_asset_libraries", False)
prebuilt_native_library_dirs = []
prebuilt_native_library_dirs_always_in_primary_apk = []
prebuilt_native_library_dir_assets_for_primary_apk = []
prebuilt_native_library_dir_module_assets_map = {}
for native_lib in all_prebuilt_native_library_dirs:
native_lib_target = str(native_lib.raw_target)
module = get_module_from_target(native_lib_target)
if not is_root_module(module):
# In buck1, we always package native libs as assets when they are not in the root module
expect(not native_lib.for_primary_apk, "{} which is marked as needing to be in the primary APK cannot be included in non-root-module {}".format(native_lib_target, module))
prebuilt_native_library_dir_module_assets_map.setdefault(module, []).append(native_lib)
elif native_lib.is_asset and is_packaging_native_libs_as_assets_supported:
expect(not native_lib.for_primary_apk, "{} which is marked as needing to be in the primary APK cannot be an asset".format(native_lib_target))
prebuilt_native_library_dir_assets_for_primary_apk.append(native_lib)
elif native_lib.for_primary_apk:
prebuilt_native_library_dirs_always_in_primary_apk.append(native_lib)
else:
prebuilt_native_library_dirs.append(native_lib)
native_libs = _filter_prebuilt_native_library_dir(
ctx,
prebuilt_native_library_dirs,
"native_libs",
)
native_libs_always_in_primary_apk = _filter_prebuilt_native_library_dir(
ctx,
prebuilt_native_library_dirs_always_in_primary_apk,
"native_libs_always_in_primary_apk",
)
native_lib_assets_for_primary_apk = _filter_prebuilt_native_library_dir(
ctx,
prebuilt_native_library_dir_assets_for_primary_apk,
"native_lib_assets_for_primary_apk",
package_as_assets = True,
module = ROOT_MODULE,
) if prebuilt_native_library_dir_assets_for_primary_apk else None
native_lib_module_assets_map = {}
for module, native_lib_dir in prebuilt_native_library_dir_module_assets_map.items():
native_lib_module_assets_map[module] = [_filter_prebuilt_native_library_dir(
ctx,
native_lib_dir,
"native_lib_assets_for_module_{}".format(module),
package_as_assets = True,
module = module,
)]
(
stripped_native_linkables,
stripped_native_linkables_always_in_primary_apk,
stripped_native_linkable_assets_for_primary_apk,
stripped_native_linkable_module_assets_map,
) = _get_native_linkables(ctx, platform_to_native_linkables, get_module_from_target, is_packaging_native_libs_as_assets_supported)
for module, native_linkable_assets in stripped_native_linkable_module_assets_map.items():
native_lib_module_assets_map.setdefault(module, []).append(native_linkable_assets)
metadata_srcs = {}
compressed_lib_srcs = {}
assets_for_primary_apk = filter(None, [native_lib_assets_for_primary_apk, stripped_native_linkable_assets_for_primary_apk])
if assets_for_primary_apk:
metadata_file, native_library_paths = _get_native_libs_as_assets_metadata(ctx, assets_for_primary_apk, ROOT_MODULE)
metadata_srcs[paths.join(_get_native_libs_as_assets_dir(ROOT_MODULE), "metadata.txt")] = metadata_file
if ctx.attrs.compress_asset_libraries:
compressed_lib_dir = _get_compressed_native_libs_as_assets(ctx, assets_for_primary_apk, native_library_paths, ROOT_MODULE)
compressed_lib_srcs[_get_native_libs_as_assets_dir(ROOT_MODULE)] = compressed_lib_dir
# Since we're storing these as compressed assets, we need to ignore the uncompressed libs.
native_lib_assets_for_primary_apk = None
stripped_native_linkable_assets_for_primary_apk = None
for module, native_lib_assets in native_lib_module_assets_map.items():
metadata_file, native_library_paths = _get_native_libs_as_assets_metadata(ctx, native_lib_assets, module)
metadata_srcs[paths.join(_get_native_libs_as_assets_dir(module), "libs.txt")] = metadata_file
compressed_lib_dir = _get_compressed_native_libs_as_assets(ctx, native_lib_assets, native_library_paths, module)
compressed_lib_srcs[_get_native_libs_as_assets_dir(module)] = compressed_lib_dir
combined_native_libs = ctx.actions.declare_output("combined_native_libs")
native_libs_metadata = ctx.actions.declare_output("native_libs_metadata.txt")
ctx.actions.run(cmd_args([
ctx.attrs._android_toolchain[AndroidToolchainInfo].combine_native_library_dirs[RunInfo],
"--output-dir",
combined_native_libs.as_output(),
"--library-dirs",
native_libs,
stripped_native_linkables,
"--metadata-file",
native_libs_metadata.as_output(),
]), category = "combine_native_libs")
combined_native_libs_always_in_primary_apk = ctx.actions.declare_output("combined_native_libs_always_in_primary_apk")
ctx.actions.run(cmd_args([
ctx.attrs._android_toolchain[AndroidToolchainInfo].combine_native_library_dirs[RunInfo],
"--output-dir",
combined_native_libs_always_in_primary_apk.as_output(),
"--library-dirs",
native_libs_always_in_primary_apk,
stripped_native_linkables_always_in_primary_apk,
]), category = "combine_native_libs_always_in_primary_apk")
return _NativeLibsAndAssetsInfo(
native_libs = combined_native_libs,
native_libs_metadata = native_libs_metadata,
native_libs_always_in_primary_apk = combined_native_libs_always_in_primary_apk,
native_lib_assets_for_primary_apk = native_lib_assets_for_primary_apk,
stripped_native_linkable_assets_for_primary_apk = stripped_native_linkable_assets_for_primary_apk,
metadata_assets = ctx.actions.symlinked_dir("metadata_assets", metadata_srcs),
compressed_lib_assets = ctx.actions.symlinked_dir("compressed_lib_assets", compressed_lib_srcs),
)
def _filter_prebuilt_native_library_dir(
ctx: "context",
native_libs: ["PrebuiltNativeLibraryDir"],
identifier: str.type,
package_as_assets: bool.type = False,
module: str.type = ROOT_MODULE) -> "artifact":
cpu_filters = ctx.attrs.cpu_filters or CPU_FILTER_TO_ABI_DIRECTORY.keys()
abis = [CPU_FILTER_TO_ABI_DIRECTORY[cpu] for cpu in cpu_filters]
filter_tool = ctx.attrs._android_toolchain[AndroidToolchainInfo].filter_prebuilt_native_library_dir[RunInfo]
native_libs_dirs = [native_lib.dir for native_lib in native_libs]
native_libs_dirs_file = ctx.actions.write("{}_list.txt".format(identifier), native_libs_dirs)
base_output_dir = ctx.actions.declare_output(identifier)
output_dir = base_output_dir.project(_get_native_libs_as_assets_dir(module)) if package_as_assets else base_output_dir
ctx.actions.run(
cmd_args([filter_tool, native_libs_dirs_file, output_dir.as_output(), "--abis"] + abis).hidden(native_libs_dirs),
category = "filter_prebuilt_native_library_dir",
identifier = identifier,
)
return base_output_dir
def _get_native_linkables(
ctx: "context",
platform_to_native_linkables: {str.type: {str.type: "SharedLibrary"}},
get_module_from_target: "function",
package_native_libs_as_assets_enabled: bool.type) -> ("artifact", "artifact", ["artifact", None], {str.type: "artifact"}):
stripped_native_linkables_srcs = {}
stripped_native_linkables_always_in_primary_apk_srcs = {}
stripped_native_linkable_assets_for_primary_apk_srcs = {}
stripped_native_linkable_module_assets_srcs = {}
cpu_filters = ctx.attrs.cpu_filters
for platform, native_linkables in platform_to_native_linkables.items():
if cpu_filters and platform not in cpu_filters:
fail("Platform `{}` is not in the CPU filters `{}`".format(platform, cpu_filters))
abi_directory = CPU_FILTER_TO_ABI_DIRECTORY[platform]
for so_name, native_linkable in native_linkables.items():
native_linkable_target = str(native_linkable.label.raw_target())
module = get_module_from_target(native_linkable_target)
if not is_root_module(module):
expect(not native_linkable.for_primary_apk, "{} which is marked as needing to be in the primary APK cannot be included in non-root-module {}".format(native_linkable_target, module))
so_name_path = paths.join(_get_native_libs_as_assets_dir(module), abi_directory, so_name)
stripped_native_linkable_module_assets_srcs.setdefault(module, {})[so_name_path] = native_linkable.stripped_lib
elif native_linkable.can_be_asset and package_native_libs_as_assets_enabled:
expect(not native_linkable.for_primary_apk, "{} which is marked as needing to be in the primary APK cannot be an asset".format(native_linkable_target))
so_name_path = paths.join(_get_native_libs_as_assets_dir(module), abi_directory, so_name)
stripped_native_linkable_assets_for_primary_apk_srcs[so_name_path] = native_linkable.stripped_lib
else:
so_name_path = paths.join(abi_directory, so_name)
if native_linkable.for_primary_apk:
stripped_native_linkables_always_in_primary_apk_srcs[so_name_path] = native_linkable.stripped_lib
else:
stripped_native_linkables_srcs[so_name_path] = native_linkable.stripped_lib
stripped_native_linkables = ctx.actions.symlinked_dir(
"stripped_native_linkables",
stripped_native_linkables_srcs,
)
stripped_native_linkables_always_in_primary_apk = ctx.actions.symlinked_dir(
"stripped_native_linkables_always_in_primary_apk",
stripped_native_linkables_always_in_primary_apk_srcs,
)
stripped_native_linkable_assets_for_primary_apk = ctx.actions.symlinked_dir(
"stripped_native_linkables_assets_for_primary_apk",
stripped_native_linkable_assets_for_primary_apk_srcs,
) if stripped_native_linkable_assets_for_primary_apk_srcs else None
stripped_native_linkable_module_assets_map = {}
for module, srcs in stripped_native_linkable_module_assets_srcs.items():
stripped_native_linkable_module_assets_map[module] = ctx.actions.symlinked_dir(
"stripped_native_linkable_assets_for_module_{}".format(module),
srcs,
)
return (
stripped_native_linkables,
stripped_native_linkables_always_in_primary_apk,
stripped_native_linkable_assets_for_primary_apk,
stripped_native_linkable_module_assets_map,
)
def _get_native_libs_as_assets_metadata(
ctx: "context",
native_lib_assets: ["artifact"],
module: str.type) -> ("artifact", "artifact"):
native_lib_assets_file = ctx.actions.write("{}/native_lib_assets".format(module), [cmd_args([native_lib_asset, _get_native_libs_as_assets_dir(module)], delimiter = "/") for native_lib_asset in native_lib_assets])
metadata_output = ctx.actions.declare_output("{}/native_libs_as_assets_metadata.txt".format(module))
native_library_paths = ctx.actions.declare_output("{}/native_libs_as_assets_paths.txt".format(module))
metadata_cmd = cmd_args([
ctx.attrs._android_toolchain[AndroidToolchainInfo].native_libs_as_assets_metadata[RunInfo],
"--native-library-dirs",
native_lib_assets_file,
"--metadata-output",
metadata_output.as_output(),
"--native-library-paths-output",
native_library_paths.as_output(),
]).hidden(native_lib_assets)
ctx.actions.run(metadata_cmd, category = "get_native_libs_as_assets_metadata", identifier = module)
return metadata_output, native_library_paths
def _get_compressed_native_libs_as_assets(
ctx: "context",
native_lib_assets: ["artifact"],
native_library_paths: "artifact",
module: str.type) -> "artifact":
output_dir = ctx.actions.declare_output("{}/compressed_native_libs_as_assets_dir".format(module))
compressed_libraries_cmd = cmd_args([
ctx.attrs._android_toolchain[AndroidToolchainInfo].compress_libraries[RunInfo],
"--libraries",
native_library_paths,
"--output-dir",
output_dir.as_output(),
"--compression-type",
ctx.attrs.asset_compression_algorithm or "xz",
"--xz-compression-level",
str(ctx.attrs.xz_compression_level),
]).hidden(native_lib_assets)
ctx.actions.run(compressed_libraries_cmd, category = "compress_native_libs_as_assets", identifier = module)
return output_dir
def _get_native_libs_as_assets_dir(module: str.type) -> str.type:
return "assets/{}".format("lib" if is_root_module(module) else module)

View file

@ -0,0 +1,473 @@
# 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//:attributes.bzl", "RType")
load("@prelude//:resources.bzl", "gather_resources")
load("@prelude//android:aapt2_link.bzl", "get_aapt2_link")
load("@prelude//android:android_manifest.bzl", "generate_android_manifest")
load("@prelude//android:android_providers.bzl", "AndroidBinaryResourcesInfo", "AndroidResourceInfo", "ExopackageResourcesInfo")
load("@prelude//android:android_resource.bzl", "aapt2_compile")
load("@prelude//android:android_toolchain.bzl", "AndroidToolchainInfo")
load("@prelude//android:r_dot_java.bzl", "generate_r_dot_javas")
load("@prelude//java:java_toolchain.bzl", "JavaToolchainInfo")
load("@prelude//utils:utils.bzl", "expect")
def get_android_binary_resources_info(
ctx: "context",
deps: ["dependency"],
android_packageable_info: "AndroidPackageableInfo",
java_packaging_deps: ["JavaPackagingDep"],
use_proto_format: bool.type,
referenced_resources_lists: ["artifact"],
manifest_entries: dict.type = {},
resource_infos_to_exclude: [AndroidResourceInfo.type] = []) -> "AndroidBinaryResourcesInfo":
android_toolchain = ctx.attrs._android_toolchain[AndroidToolchainInfo]
unfiltered_resource_infos = [resource_info for resource_info in list(android_packageable_info.resource_infos.traverse() if android_packageable_info.resource_infos else []) if resource_info not in resource_infos_to_exclude]
resource_infos, override_symbols, string_files_list, string_files_res_dirs = _maybe_filter_resources(
ctx,
unfiltered_resource_infos,
android_toolchain,
)
android_manifest = _get_manifest(ctx, android_packageable_info, manifest_entries)
aapt2_link_info = get_aapt2_link(
ctx,
ctx.attrs._android_toolchain[AndroidToolchainInfo],
[resource_info.aapt2_compile_output for resource_info in resource_infos if resource_info.aapt2_compile_output != None],
android_manifest,
includes_vector_drawables = getattr(ctx.attrs, "includes_vector_drawables", False),
no_auto_version = getattr(ctx.attrs, "no_auto_version_resources", False),
no_version_transitions = getattr(ctx.attrs, "no_version_transitions_resources", False),
no_auto_add_overlay = getattr(ctx.attrs, "no_auto_add_overlay_resources", False),
use_proto_format = use_proto_format,
no_resource_removal = True,
package_id_offset = 0,
should_keep_raw_values = getattr(ctx.attrs, "aapt2_keep_raw_values", False),
resource_stable_ids = getattr(ctx.attrs, "resource_stable_ids", None),
compiled_resource_apks = [],
additional_aapt2_params = getattr(ctx.attrs, "additional_aapt_params", []),
extra_filtered_resources = getattr(ctx.attrs, "extra_filtered_resources", []),
locales = getattr(ctx.attrs, "locales", []),
filter_locales = getattr(ctx.attrs, "aapt2_locale_filtering", False),
)
prebuilt_jars = [packaging_dep.jar for packaging_dep in java_packaging_deps if packaging_dep.is_prebuilt_jar]
cxx_resources = _get_cxx_resources(ctx, deps)
is_exopackaged_enabled_for_resources = "resources" in getattr(ctx.attrs, "exopackage_modes", [])
primary_resources_apk, exopackaged_assets, exopackaged_assets_hash = _merge_assets(
ctx,
is_exopackaged_enabled_for_resources,
aapt2_link_info.primary_resources_apk,
resource_infos,
cxx_resources,
)
if is_exopackaged_enabled_for_resources:
r_dot_txt = ctx.actions.declare_output("after_exo/R.txt")
primary_resources_apk = ctx.actions.declare_output("after_exo/primary_resources_apk.apk")
exo_resources = ctx.actions.declare_output("exo_resources.apk")
exo_resources_hash = ctx.actions.declare_output("exo_resources.apk.hash")
ctx.actions.run(cmd_args([
android_toolchain.exo_resources_rewriter[RunInfo],
"--original-r-dot-txt",
aapt2_link_info.r_dot_txt,
"--new-r-dot-txt",
r_dot_txt.as_output(),
"--original-primary-apk-resources",
aapt2_link_info.primary_resources_apk,
"--new-primary-apk-resources",
primary_resources_apk.as_output(),
"--exo-resources",
exo_resources.as_output(),
"--exo-resources-hash",
exo_resources_hash.as_output(),
"--zipalign-tool",
android_toolchain.zipalign[RunInfo],
]), category = "write_exo_resources")
third_party_jars = ctx.actions.write("third_party_jars", prebuilt_jars)
third_party_jar_resources = ctx.actions.declare_output("third_party_jars.resources")
third_party_jar_resources_hash = ctx.actions.declare_output("third_party_jars.resources.hash")
ctx.actions.run(cmd_args([
android_toolchain.merge_third_party_jar_resources[RunInfo],
"--output",
third_party_jar_resources.as_output(),
"--output-hash",
third_party_jar_resources_hash.as_output(),
"--third-party-jars",
third_party_jars,
]).hidden(prebuilt_jars), category = "merge_third_party_jar_resources")
exopackage_info = ExopackageResourcesInfo(
assets = exopackaged_assets,
assets_hash = exopackaged_assets_hash,
res = exo_resources,
res_hash = exo_resources_hash,
third_party_jar_resources = third_party_jar_resources,
third_party_jar_resources_hash = third_party_jar_resources_hash,
)
jar_files_that_may_contain_resources = []
else:
exopackage_info = None
jar_files_that_may_contain_resources = prebuilt_jars
r_dot_txt = aapt2_link_info.r_dot_txt
override_symbols_paths = [override_symbols] if override_symbols else []
resources = [resource for resource in resource_infos if resource.res != None]
r_dot_javas = [] if len(resources) == 0 else generate_r_dot_javas(
ctx,
ctx.attrs._android_toolchain[AndroidToolchainInfo].merge_android_resources[RunInfo],
ctx.attrs._java_toolchain[JavaToolchainInfo],
resources,
get_effective_banned_duplicate_resource_types(
getattr(ctx.attrs, "duplicate_resource_behavior", "allow_by_default"),
getattr(ctx.attrs, "allowed_duplicate_resource_types", []),
getattr(ctx.attrs, "banned_duplicate_resource_types", []),
),
[r_dot_txt],
override_symbols_paths,
getattr(ctx.attrs, "duplicate_resource_whitelist", None),
getattr(ctx.attrs, "resource_union_package", None),
referenced_resources_lists,
)
string_source_map = _maybe_generate_string_source_map(
ctx.actions,
getattr(ctx.attrs, "build_string_source_map", False),
resources,
android_toolchain,
)
packaged_string_assets = _maybe_package_strings_as_assets(
ctx,
string_files_list,
string_files_res_dirs,
r_dot_txt,
android_toolchain,
)
return AndroidBinaryResourcesInfo(
exopackage_info = exopackage_info,
manifest = android_manifest,
packaged_string_assets = packaged_string_assets,
primary_resources_apk = primary_resources_apk,
proguard_config_file = aapt2_link_info.proguard_config_file,
r_dot_javas = r_dot_javas,
string_source_map = string_source_map,
jar_files_that_may_contain_resources = jar_files_that_may_contain_resources,
unfiltered_resource_infos = unfiltered_resource_infos,
)
def _maybe_filter_resources(
ctx: "context",
resources: [AndroidResourceInfo.type],
android_toolchain: AndroidToolchainInfo.type) -> ([AndroidResourceInfo.type], ["artifact", None], ["artifact", None], ["artifact"]):
resources_filter_strings = getattr(ctx.attrs, "resource_filter", [])
resources_filter = _get_resources_filter(resources_filter_strings)
resource_compression_mode = getattr(ctx.attrs, "resource_compression", "disabled")
is_store_strings_as_assets = _is_store_strings_as_assets(resource_compression_mode)
locales = getattr(ctx.attrs, "locales", None)
use_aapt2_locale_filtering = getattr(ctx.attrs, "aapt2_locale_filtering", False)
needs_resource_filtering_for_locales = locales != None and len(locales) > 0 and not use_aapt2_locale_filtering
post_filter_resources_cmd = getattr(ctx.attrs, "post_filter_resources_cmd", None)
needs_resource_filtering = (
resources_filter != None or
is_store_strings_as_assets or
needs_resource_filtering_for_locales or
post_filter_resources_cmd != None
)
if not needs_resource_filtering:
return resources, None, None, []
res_info_to_out_res_dir = {}
res_infos_with_no_res = []
skip_crunch_pngs = getattr(ctx.attrs, "skip_crunch_pngs", None) or False
for i, resource in enumerate(resources):
if resource.res == None:
res_infos_with_no_res.append(resource)
else:
filtered_res = ctx.actions.declare_output("filtered_res_{}".format(i))
res_info_to_out_res_dir[resource] = filtered_res
filter_resources_cmd = cmd_args(android_toolchain.filter_resources[RunInfo])
in_res_dir_to_out_res_dir_dict = {
in_res.res: out_res
for in_res, out_res in res_info_to_out_res_dir.items()
}
in_res_dir_to_out_res_dir_map = ctx.actions.write_json("in_res_dir_to_out_res_dir_map", {"res_dir_map": in_res_dir_to_out_res_dir_dict})
in_res_dirs = [in_res.res for in_res in res_info_to_out_res_dir.keys()]
filter_resources_cmd.hidden(in_res_dirs)
filter_resources_cmd.hidden([out_res.as_output() for out_res in res_info_to_out_res_dir.values()])
filter_resources_cmd.add([
"--in-res-dir-to-out-res-dir-map",
in_res_dir_to_out_res_dir_map,
])
if resources_filter:
filter_resources_cmd.add([
"--target-densities",
",".join(resources_filter.densities),
])
all_strings_files_list = None
all_strings_files_res_dirs = []
if is_store_strings_as_assets:
all_strings_files_list = ctx.actions.declare_output("all_strings_files")
all_strings_files_res_dirs = in_res_dirs
filter_resources_cmd.add([
"--enable-string-as-assets-filtering",
"--string-files-list-output",
all_strings_files_list.as_output(),
])
packaged_locales = getattr(ctx.attrs, "packaged_locales", [])
if packaged_locales:
filter_resources_cmd.add([
"--packaged-locales",
",".join(packaged_locales),
])
not_filtered_string_dirs = [resource.res for resource in resources if not resource.allow_strings_as_assets_resource_filtering]
if not_filtered_string_dirs:
filter_resources_cmd.add([
"--not-filtered-string-dirs",
ctx.actions.write("not_filtered_string_dirs", not_filtered_string_dirs),
])
if needs_resource_filtering_for_locales:
filter_resources_cmd.add([
"--locales",
",".join(locales),
])
override_symbols_artifact = None
if post_filter_resources_cmd != None:
override_symbols_artifact = ctx.actions.declare_output("post_filter_resources_cmd/R.json")
filter_resources_cmd.add([
"--post-filter-resources-cmd",
post_filter_resources_cmd,
"--post-filter-resources-cmd-override-symbols-output",
override_symbols_artifact.as_output(),
])
ctx.actions.run(filter_resources_cmd, category = "filter_resources")
filtered_resource_infos = []
for i, resource in enumerate(resources):
if resource.res == None:
continue
filtered_res = res_info_to_out_res_dir[resource]
filtered_aapt2_compile_output = aapt2_compile(
ctx,
filtered_res,
android_toolchain,
skip_crunch_pngs = skip_crunch_pngs,
identifier = "filtered_res_{}".format(i),
)
filtered_resource = AndroidResourceInfo(
aapt2_compile_output = filtered_aapt2_compile_output,
assets = resource.assets,
manifest_file = resource.manifest_file,
r_dot_java_package = resource.r_dot_java_package,
res = filtered_res,
text_symbols = resource.text_symbols,
)
filtered_resource_infos.append(filtered_resource)
return (
res_infos_with_no_res + filtered_resource_infos,
override_symbols_artifact,
all_strings_files_list,
all_strings_files_res_dirs,
)
ResourcesFilter = record(
densities = [str.type],
downscale = bool.type,
)
def _get_resources_filter(resources_filter_strings: [str.type]) -> [ResourcesFilter.type, None]:
if not resources_filter_strings:
return None
densities = [resources_filter_string for resources_filter_string in resources_filter_strings if resources_filter_string != "downscale"]
if not densities:
return None
downscale = len(densities) < len(resources_filter_strings)
return ResourcesFilter(densities = densities, downscale = downscale)
def _maybe_generate_string_source_map(
actions: "actions",
should_build_source_string_map: bool.type,
resource_infos: [AndroidResourceInfo.type],
android_toolchain: AndroidToolchainInfo.type) -> ["artifact", None]:
if not should_build_source_string_map or len(resource_infos) == 0:
return None
res_dirs = [resource_info.res for resource_info in resource_infos]
output = actions.declare_output("string_source_map")
res_dirs_file = actions.write("resource_dirs_for_string_source_map", res_dirs)
generate_string_source_map_cmd = cmd_args([
android_toolchain.copy_string_resources[RunInfo],
"--res-dirs",
res_dirs_file,
"--output",
output.as_output(),
]).hidden(res_dirs)
actions.run(generate_string_source_map_cmd, category = "generate_string_source_map")
return output
def _maybe_package_strings_as_assets(
ctx: "context",
string_files_list: ["artifact", None],
string_files_res_dirs: ["artifact"],
r_dot_txt: "artifact",
android_toolchain: AndroidToolchainInfo.type) -> ["artifact", None]:
resource_compression_mode = getattr(ctx.attrs, "resource_compression", "disabled")
is_store_strings_as_assets = _is_store_strings_as_assets(resource_compression_mode)
expect(is_store_strings_as_assets == (string_files_list != None))
if not is_store_strings_as_assets:
return None
string_assets_dir = ctx.actions.declare_output("package_strings_as_assets/string_assets")
string_assets_zip = ctx.actions.declare_output("package_strings_as_assets/string_assets_zip.zip")
all_locales_string_assets_zip = ctx.actions.declare_output("package_strings_as_assets/all_locales_string_assets_zip.zip")
locales = getattr(ctx.attrs, "locales", [])
package_strings_as_assets_cmd = cmd_args([
android_toolchain.package_strings_as_assets[RunInfo],
"--string-files-list",
string_files_list,
"--r-dot-txt",
r_dot_txt,
"--string-assets-dir",
string_assets_dir.as_output(),
"--string-assets-zip",
string_assets_zip.as_output(),
"--all-locales-string-assets-zip",
all_locales_string_assets_zip.as_output(),
]).hidden(string_files_res_dirs)
if locales:
package_strings_as_assets_cmd.add("--locales", ",".join(locales))
ctx.actions.run(package_strings_as_assets_cmd, category = "package_strings_as_assets")
return string_assets_zip
def _get_manifest(
ctx: "context",
android_packageable_info: "AndroidPackageableInfo",
manifest_entries: dict.type) -> "artifact":
robolectric_manifest = getattr(ctx.attrs, "robolectric_manifest", None)
if robolectric_manifest:
return robolectric_manifest
if ctx.attrs.manifest:
expect(ctx.attrs.manifest_skeleton == None, "Only one of manifest and manifest_skeleton should be declared")
if type(ctx.attrs.manifest) == "dependency":
android_manifest = ctx.attrs.manifest[DefaultInfo].default_outputs[0]
else:
android_manifest = ctx.attrs.manifest
else:
expect(ctx.attrs.manifest_skeleton != None, "Must declare one of manifest and manifest_skeleton")
if type(ctx.attrs.manifest_skeleton) == "dependency":
manifest_skeleton = ctx.attrs.manifest_skeleton[DefaultInfo].default_outputs[0]
else:
manifest_skeleton = ctx.attrs.manifest_skeleton
android_manifest, _ = generate_android_manifest(
ctx,
ctx.attrs._android_toolchain[AndroidToolchainInfo].generate_manifest[RunInfo],
manifest_skeleton,
"dex", # ROOT_APKMODULE_NAME,
android_packageable_info.manifests,
manifest_entries.get("placeholders", {}),
)
return android_manifest
# Returns the "primary resources APK" (i.e. the resource that are packaged into the primary APK),
# and optionally an "exopackaged assets APK" and the hash for that APK.
def _merge_assets(
ctx: "context",
is_exopackaged_enabled_for_resources: bool.type,
base_apk: "artifact",
resource_infos: ["AndroidResourceInfo"],
cxx_resources: ["artifact", None]) -> ("artifact", ["artifact", None], ["artifact", None]):
assets_dirs = [resource_info.assets for resource_info in resource_infos if resource_info.assets]
if cxx_resources != None:
assets_dirs.extend([cxx_resources])
if len(assets_dirs) == 0:
return base_apk, None, None
merge_assets_cmd = cmd_args(ctx.attrs._android_toolchain[AndroidToolchainInfo].merge_assets[RunInfo])
merged_assets_output = ctx.actions.declare_output("merged_assets.ap_")
merge_assets_cmd.add(["--output-apk", merged_assets_output.as_output()])
if is_exopackaged_enabled_for_resources:
merged_assets_output_hash = ctx.actions.declare_output("merged_assets.ap_.hash")
merge_assets_cmd.add(["--output-apk-hash", merged_assets_output_hash.as_output()])
else:
merge_assets_cmd.add(["--base-apk", base_apk])
merged_assets_output_hash = None
assets_dirs_file = ctx.actions.write("assets_dirs", assets_dirs)
merge_assets_cmd.add(["--assets-dirs", assets_dirs_file])
merge_assets_cmd.hidden(assets_dirs)
ctx.actions.run(merge_assets_cmd, category = "merge_assets")
if is_exopackaged_enabled_for_resources:
return base_apk, merged_assets_output, merged_assets_output_hash
else:
return merged_assets_output, None, None
def get_effective_banned_duplicate_resource_types(
duplicate_resource_behavior: str.type,
allowed_duplicate_resource_types: [str.type],
banned_duplicate_resource_types: [str.type]) -> [str.type]:
if duplicate_resource_behavior == "allow_by_default":
expect(
len(allowed_duplicate_resource_types) == 0,
"Cannot set allowed_duplicate_resource_types if duplicate_resource_behaviour is allow_by_default",
)
return banned_duplicate_resource_types
elif duplicate_resource_behavior == "ban_by_default":
expect(
len(banned_duplicate_resource_types) == 0,
"Cannot set banned_duplicate_resource_types if duplicate_resource_behaviour is ban_by_default",
)
return [rtype for rtype in RType if rtype not in allowed_duplicate_resource_types]
else:
fail("Unrecognized duplicate_resource_behavior: {}".format(duplicate_resource_behavior))
def _get_cxx_resources(ctx: "context", deps: ["dependency"]) -> ["artifact", None]:
cxx_resources = gather_resources(
label = ctx.label,
resources = {},
deps = deps,
)
symlink_tree_dict = {}
resource_maps = cxx_resources.values()
for resource_map in resource_maps:
for name, (resource, _other) in resource_map.items():
symlink_tree_dict["cxx-resources/{}".format(name)] = resource
return ctx.actions.symlinked_dir("cxx_resources_dir", symlink_tree_dict) if symlink_tree_dict else None
def _is_store_strings_as_assets(resource_compression: str.type) -> bool.type:
return resource_compression == "enabled_strings_only" or resource_compression == "enabled_with_strings_as_assets"

View file

@ -0,0 +1,113 @@
# 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//android:android_toolchain.bzl", "AndroidToolchainInfo")
load("@prelude//java:java_library.bzl", "compile_to_jar")
load("@prelude//java:java_providers.bzl", "JavaLibraryInfo", "JavaPackagingDepTSet", "JavaPackagingInfo", "create_java_packaging_dep", "derive_compiling_deps")
load(":android_providers.bzl", "AndroidBuildConfigInfo", "BuildConfigField", "merge_android_packageable_info")
def android_build_config_impl(ctx: "context") -> ["provider"]:
providers = []
default_build_config_fields = get_build_config_fields(ctx.attrs.values)
android_build_config_info = AndroidBuildConfigInfo(package = ctx.attrs.package, build_config_fields = default_build_config_fields)
providers.append(android_build_config_info)
providers.append(merge_android_packageable_info(ctx.label, ctx.actions, deps = [], build_config_info = android_build_config_info))
build_config_dot_java_library, java_packaging_info = generate_android_build_config(
ctx,
ctx.attrs.name,
ctx.attrs.package,
False,
default_build_config_fields,
ctx.attrs.values_file,
)
providers.append(java_packaging_info)
providers.append(build_config_dot_java_library)
providers.append(DefaultInfo(default_outputs = [build_config_dot_java_library.library_output.full_library]))
return providers
def generate_android_build_config(
ctx: "context",
source: str.type,
java_package: str.type,
use_constant_expressions: bool.type,
default_values: ["BuildConfigField"],
values_file: ["artifact", None]) -> ("JavaLibraryInfo", "JavaPackagingInfo"):
build_config_dot_java = _generate_build_config_dot_java(ctx, source, java_package, use_constant_expressions, default_values, values_file)
compiled_build_config_dot_java = _compile_and_package_build_config_dot_java(ctx, java_package, build_config_dot_java)
library_output = compiled_build_config_dot_java.classpath_entry
packaging_deps_kwargs = {"value": create_java_packaging_dep(ctx, library_output.full_library)}
packaging_deps = ctx.actions.tset(JavaPackagingDepTSet, **packaging_deps_kwargs)
return (JavaLibraryInfo(
compiling_deps = derive_compiling_deps(ctx.actions, library_output, []),
library_output = library_output,
output_for_classpath_macro = library_output.full_library,
), JavaPackagingInfo(
packaging_deps = packaging_deps,
))
def _generate_build_config_dot_java(
ctx: "context",
source: str.type,
java_package: str.type,
use_constant_expressions: bool.type,
default_values: ["BuildConfigField"],
values_file: ["artifact", None]) -> "artifact":
generate_build_config_cmd = cmd_args(ctx.attrs._android_toolchain[AndroidToolchainInfo].generate_build_config[RunInfo])
generate_build_config_cmd.add([
"--source",
source,
"--java-package",
java_package,
"--use-constant-expressions",
str(use_constant_expressions),
])
default_values_file = ctx.actions.write(
_get_output_name(java_package, "default_values"),
["{} {} = {}".format(x.type, x.name, x.value) for x in default_values],
)
generate_build_config_cmd.add(["--default-values-file", default_values_file])
if values_file:
generate_build_config_cmd.add(["--values-file", values_file])
build_config_dot_java = ctx.actions.declare_output(_get_output_name(java_package, "BuildConfig.java"))
generate_build_config_cmd.add(["--output", build_config_dot_java.as_output()])
ctx.actions.run(
generate_build_config_cmd,
category = "android_generate_build_config",
identifier = java_package,
)
return build_config_dot_java
def _compile_and_package_build_config_dot_java(
ctx: "context",
java_package: str.type,
build_config_dot_java: "artifact") -> "JavaCompileOutputs":
return compile_to_jar(
ctx,
actions_prefix = "build_config_{}".format(java_package.replace(".", "_")),
srcs = [build_config_dot_java],
)
def get_build_config_fields(lines: [str.type]) -> ["BuildConfigField"]:
return [_get_build_config_field(line) for line in lines]
def _get_build_config_field(line: str.type) -> "BuildConfigField":
type_and_name, value = [x.strip() for x in line.split("=")]
field_type, name = type_and_name.split()
return BuildConfigField(type = field_type, name = name, value = value)
def _get_output_name(java_package: str.type, output_filename: str.type) -> str.type:
return "android_build_config/{}/{}".format(java_package.replace(".", "_"), output_filename)

View file

@ -0,0 +1,89 @@
# 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//android:android_apk.bzl", "build_apk")
load("@prelude//android:android_binary_native_library_rules.bzl", "get_android_binary_native_library_info")
load("@prelude//android:android_binary_resources_rules.bzl", "get_android_binary_resources_info")
load("@prelude//android:android_providers.bzl", "AndroidApkInfo", "AndroidApkUnderTestInfo", "AndroidInstrumentationApkInfo", "merge_android_packageable_info")
load("@prelude//android:android_toolchain.bzl", "AndroidToolchainInfo")
load("@prelude//android:configuration.bzl", "get_deps_by_platform")
load("@prelude//android:dex_rules.bzl", "merge_to_single_dex")
load("@prelude//java:java_providers.bzl", "create_java_packaging_dep", "get_all_java_packaging_deps")
load("@prelude//utils:utils.bzl", "expect")
def android_instrumentation_apk_impl(ctx: "context"):
# To begin with, let's just implement something that has a single DEX file and a manifest.
_verify_params(ctx)
apk_under_test_info = ctx.attrs.apk[AndroidApkUnderTestInfo]
# android_instrumentation_apk should just use the same platforms and primary_platform as the APK-under-test
unfiltered_deps_by_platform = get_deps_by_platform(ctx)
for platform in apk_under_test_info.platforms:
expect(
platform in unfiltered_deps_by_platform,
"Android instrumentation APK must have any platforms that are in the APK-under-test!",
)
deps_by_platform = {platform: deps for platform, deps in unfiltered_deps_by_platform.items() if platform in apk_under_test_info.platforms}
primary_platform = apk_under_test_info.primary_platform
deps = deps_by_platform[primary_platform]
java_packaging_deps = [packaging_dep for packaging_dep in get_all_java_packaging_deps(ctx, deps) if packaging_dep.dex and packaging_dep not in apk_under_test_info.java_packaging_deps]
android_packageable_info = merge_android_packageable_info(ctx.label, ctx.actions, deps)
resources_info = get_android_binary_resources_info(
ctx,
deps,
android_packageable_info,
java_packaging_deps = java_packaging_deps,
use_proto_format = False,
referenced_resources_lists = [],
manifest_entries = apk_under_test_info.manifest_entries,
resource_infos_to_exclude = apk_under_test_info.resource_infos,
)
android_toolchain = ctx.attrs._android_toolchain[AndroidToolchainInfo]
java_packaging_deps += [
create_java_packaging_dep(
ctx,
r_dot_java.library_output.full_library,
dex_weight_factor = android_toolchain.r_dot_java_weight_factor,
)
for r_dot_java in resources_info.r_dot_javas
]
# For instrumentation test APKs we always pre-dex, and we also always merge to a single dex.
pre_dexed_libs = [java_packaging_dep.dex for java_packaging_dep in java_packaging_deps]
dex_files_info = merge_to_single_dex(ctx, android_toolchain, pre_dexed_libs)
native_library_info = get_android_binary_native_library_info(
ctx,
android_packageable_info,
deps_by_platform,
prebuilt_native_library_dirs_to_exclude = apk_under_test_info.prebuilt_native_library_dirs,
shared_libraries_to_exclude = apk_under_test_info.shared_libraries,
)
output_apk = build_apk(
label = ctx.label,
actions = ctx.actions,
android_toolchain = ctx.attrs._android_toolchain[AndroidToolchainInfo],
keystore = apk_under_test_info.keystore,
dex_files_info = dex_files_info,
native_library_info = native_library_info,
resources_info = resources_info,
)
return [
AndroidApkInfo(apk = output_apk, manifest = resources_info.manifest),
AndroidInstrumentationApkInfo(apk_under_test = ctx.attrs.apk[AndroidApkInfo].apk),
DefaultInfo(default_outputs = [output_apk]),
]
def _verify_params(ctx: "context"):
expect(ctx.attrs.aapt_mode == "aapt2", "aapt1 is deprecated!")
expect(ctx.attrs.dex_tool == "d8", "dx is deprecated!")

View file

@ -0,0 +1,97 @@
# 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//android:android_providers.bzl", "AndroidApkInfo", "AndroidInstrumentationApkInfo")
load("@prelude//android:android_toolchain.bzl", "AndroidToolchainInfo")
load("@prelude//java:java_toolchain.bzl", "JavaToolchainInfo")
load("@prelude//java/utils:java_utils.bzl", "get_path_separator")
load("@prelude//utils:utils.bzl", "expect")
load("@prelude//test/inject_test_run_info.bzl", "inject_test_run_info")
def android_instrumentation_test_impl(ctx: "context"):
android_toolchain = ctx.attrs._android_toolchain[AndroidToolchainInfo]
cmd = [ctx.attrs._java_toolchain[JavaToolchainInfo].java_for_tests]
classpath = android_toolchain.instrumentation_test_runner_classpath
classpath_args = cmd_args()
classpath_args.add("-classpath")
classpath_args.add(cmd_args(classpath, delimiter = get_path_separator()))
classpath_args_file = ctx.actions.write("classpath_args_file", classpath_args)
cmd.append(cmd_args(classpath_args_file, format = "@{}").hidden(classpath_args))
cmd.append(android_toolchain.instrumentation_test_runner_main_class)
apk_info = ctx.attrs.apk.get(AndroidApkInfo)
expect(apk_info != None, "Provided APK must have AndroidApkInfo!")
instrumentation_apk_info = ctx.attrs.apk.get(AndroidInstrumentationApkInfo)
if instrumentation_apk_info != None:
cmd.extend(["--apk-under-test-path", instrumentation_apk_info.apk_under_test])
target_package_file = ctx.actions.declare_output("target_package_file")
package_file = ctx.actions.declare_output("package_file")
test_runner_file = ctx.actions.declare_output("test_runner_file")
manifest_utils_cmd = cmd_args(ctx.attrs._android_toolchain[AndroidToolchainInfo].manifest_utils[RunInfo])
manifest_utils_cmd.add([
"--manifest-path",
apk_info.manifest,
"--package-output",
package_file.as_output(),
"--target-package-output",
target_package_file.as_output(),
"--instrumentation-test-runner-output",
test_runner_file.as_output(),
])
ctx.actions.run(manifest_utils_cmd, category = "get_manifest_info")
cmd.extend(
[
"--test-package-name",
cmd_args(package_file, format = "@{}"),
"--target-package-name",
cmd_args(target_package_file, format = "@{}"),
"--test-runner",
cmd_args(test_runner_file, format = "@{}"),
],
)
cmd.extend(
[
"--adb-executable-path",
"required_but_unused",
"--instrumentation-apk-path",
apk_info.apk,
],
)
test_info = ExternalRunnerTestInfo(
type = "android_instrumentation",
command = cmd,
env = ctx.attrs.env,
# TODO(T122022107) support static listing
labels = ctx.attrs.labels + ["tpx::dynamic_listing_instrumentation_test"],
contacts = ctx.attrs.contacts,
run_from_project_root = True,
use_project_relative_paths = True,
executor_overrides = {
"android-emulator": CommandExecutorConfig(
local_enabled = False,
remote_enabled = True,
remote_execution_properties = {
"platform": "android-emulator",
"subplatform": "android-30",
},
remote_execution_use_case = "instrumentation-tests",
),
"static-listing": CommandExecutorConfig(local_enabled = True, remote_enabled = False),
},
)
return inject_test_run_info(ctx, test_info) + [
DefaultInfo(),
]

View file

@ -0,0 +1,92 @@
# 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//android:android_providers.bzl",
"AndroidLibraryIntellijInfo",
"AndroidResourceInfo",
"merge_android_packageable_info",
"merge_exported_android_resource_info",
)
load("@prelude//android:android_toolchain.bzl", "AndroidToolchainInfo")
load("@prelude//android:r_dot_java.bzl", "get_dummy_r_dot_java")
load("@prelude//java:java_library.bzl", "build_java_library")
load("@prelude//java:java_providers.bzl", "create_native_providers", "to_list")
load("@prelude//java:java_toolchain.bzl", "JavaToolchainInfo")
load("@prelude//kotlin:kotlin_library.bzl", "build_kotlin_library")
def android_library_impl(ctx: "context") -> ["provider"]:
packaging_deps = ctx.attrs.deps + (ctx.attrs.deps_query or []) + ctx.attrs.exported_deps + ctx.attrs.runtime_deps
if ctx.attrs._build_only_native_code:
shared_library_info, cxx_resource_info = create_native_providers(ctx.actions, ctx.label, packaging_deps)
return [
shared_library_info,
cxx_resource_info,
# Add an unused default output in case this target is used as an attr.source() anywhere.
DefaultInfo(default_outputs = [ctx.actions.write("unused.jar", [])]),
]
java_providers, android_library_intellij_info = build_android_library(ctx)
android_providers = [android_library_intellij_info] if android_library_intellij_info else []
return to_list(java_providers) + [
merge_android_packageable_info(
ctx.label,
ctx.actions,
packaging_deps,
manifest = ctx.attrs.manifest,
),
merge_exported_android_resource_info(ctx.attrs.exported_deps),
] + android_providers
def build_android_library(
ctx: "context") -> ("JavaProviders", [AndroidLibraryIntellijInfo.type, None]):
java_toolchain = ctx.attrs._java_toolchain[JavaToolchainInfo]
bootclasspath_entries = [] + ctx.attrs._android_toolchain[AndroidToolchainInfo].android_bootclasspath
additional_classpath_entries = []
android_library_intellij_info = None
dummy_r_dot_java = _get_dummy_r_dot_java(ctx, java_toolchain)
if dummy_r_dot_java:
additional_classpath_entries.append(dummy_r_dot_java)
android_library_intellij_info = AndroidLibraryIntellijInfo(
dummy_r_dot_java = dummy_r_dot_java,
)
if ctx.attrs.language != None and ctx.attrs.language.lower() == "kotlin":
return build_kotlin_library(
ctx,
additional_classpath_entries = additional_classpath_entries,
bootclasspath_entries = bootclasspath_entries,
), android_library_intellij_info
else:
return build_java_library(
ctx,
ctx.attrs.srcs,
additional_classpath_entries = additional_classpath_entries,
bootclasspath_entries = bootclasspath_entries,
), android_library_intellij_info
def _get_dummy_r_dot_java(
ctx: "context",
java_toolchain: "JavaToolchainInfo") -> ["artifact", None]:
android_resources = [resource for resource in filter(None, [
x.get(AndroidResourceInfo)
for x in ctx.attrs.deps + (ctx.attrs.deps_query or []) + ctx.attrs.provided_deps + (getattr(ctx.attrs, "provided_deps_query", []) or [])
]) if resource.res != None]
if len(android_resources) == 0:
return None
dummy_r_dot_java_library_info = get_dummy_r_dot_java(
ctx,
ctx.attrs._android_toolchain[AndroidToolchainInfo].merge_android_resources[RunInfo],
java_toolchain,
dedupe(android_resources),
ctx.attrs.resource_union_package,
)
return dummy_r_dot_java_library_info.library_output.abi

View file

@ -0,0 +1,74 @@
# 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//android:android_providers.bzl", "AndroidManifestInfo", "merge_android_packageable_info")
load("@prelude//android:android_toolchain.bzl", "AndroidToolchainInfo")
ROOT_APKMODULE_NAME = "dex"
def android_manifest_impl(ctx: "context") -> ["provider"]:
output, merge_report = generate_android_manifest(
ctx,
ctx.attrs._android_toolchain[AndroidToolchainInfo].generate_manifest[RunInfo],
ctx.attrs.skeleton,
ROOT_APKMODULE_NAME,
_get_manifests_from_deps(ctx),
{},
)
return [
AndroidManifestInfo(manifest = output, merge_report = merge_report),
DefaultInfo(default_outputs = [output], other_outputs = [merge_report]),
]
def generate_android_manifest(
ctx: "context",
generate_manifest: RunInfo.type,
manifest_skeleton: "artifact",
module_name: str.type,
manifests_tset: ["ManifestTSet", None],
placeholder_entries: "dict") -> ("artifact", "artifact"):
generate_manifest_cmd = cmd_args(generate_manifest)
generate_manifest_cmd.add([
"--skeleton-manifest",
manifest_skeleton,
"--module-name",
module_name,
])
manifests = manifests_tset.project_as_args("artifacts", ordering = "bfs") if manifests_tset else []
library_manifest_paths_file = ctx.actions.write("library_manifest_paths_file", manifests)
generate_manifest_cmd.add(["--library-manifests-list", library_manifest_paths_file])
generate_manifest_cmd.hidden(manifests)
placeholder_entries_args = cmd_args()
for key, val in placeholder_entries.items():
placeholder_entries_args.add(cmd_args(key, val, delimiter = " "))
placeholder_entries_file = ctx.actions.write("placeholder_entries_file", placeholder_entries_args)
generate_manifest_cmd.add(["--placeholder-entries-list", placeholder_entries_file])
output = ctx.actions.declare_output("AndroidManifest.xml")
merge_report = ctx.actions.declare_output("merge-report.txt")
generate_manifest_cmd.add([
"--output",
output.as_output(),
"--merge-report",
merge_report.as_output(),
])
ctx.actions.run(generate_manifest_cmd, category = "generate_manifest")
return (output, merge_report)
def _get_manifests_from_deps(ctx: "context") -> ["ManifestTSet", None]:
if len(ctx.attrs.deps) == 0:
return None
android_packageable_info = merge_android_packageable_info(ctx.label, ctx.actions, ctx.attrs.deps)
return android_packageable_info.manifests

View file

@ -0,0 +1,108 @@
# 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//android:android_providers.bzl", "AndroidResourceInfo", "PrebuiltNativeLibraryDir", "merge_android_packageable_info")
load("@prelude//android:android_resource.bzl", "aapt2_compile", "extract_package_from_manifest")
load("@prelude//android:android_toolchain.bzl", "AndroidToolchainInfo")
load(
"@prelude//java:java_providers.bzl",
"JavaClasspathEntry",
"create_abi",
"create_java_library_providers",
)
load("@prelude//java:java_toolchain.bzl", "JavaToolchainInfo")
def android_prebuilt_aar_impl(ctx: "context") -> ["provider"]:
manifest = ctx.actions.declare_output("AndroidManifest.xml")
all_classes_jar = ctx.actions.declare_output("classes.jar")
r_dot_txt = ctx.actions.declare_output("R.txt")
res = ctx.actions.declare_output("res")
assets = ctx.actions.declare_output("assets")
jni = ctx.actions.declare_output("jni")
annotation_jars_dir = ctx.actions.declare_output("annotation_jars")
android_toolchain = ctx.attrs._android_toolchain[AndroidToolchainInfo]
unpack_aar_tool = android_toolchain.unpack_aar[RunInfo]
java_toolchain = ctx.attrs._java_toolchain[JavaToolchainInfo]
jar_tool = java_toolchain.jar
unpack_aar_cmd = [
unpack_aar_tool,
"--aar",
ctx.attrs.aar,
"--manifest-path",
manifest.as_output(),
"--all-classes-jar-path",
all_classes_jar.as_output(),
"--r-dot-txt-path",
r_dot_txt.as_output(),
"--res-path",
res.as_output(),
"--assets-path",
assets.as_output(),
"--jni-path",
jni.as_output(),
"--annotation-jars-dir",
annotation_jars_dir.as_output(),
"--jar-tool",
jar_tool,
]
ctx.actions.run(unpack_aar_cmd, category = "android_unpack_aar")
resource_info = AndroidResourceInfo(
aapt2_compile_output = aapt2_compile(ctx, res, android_toolchain),
allow_strings_as_assets_resource_filtering = True,
assets = assets,
manifest_file = manifest,
r_dot_java_package = extract_package_from_manifest(ctx, manifest),
res = res,
text_symbols = r_dot_txt,
)
abi = None if java_toolchain.is_bootstrap_toolchain else create_abi(ctx.actions, java_toolchain.class_abi_generator, all_classes_jar)
library_output_classpath_entry = JavaClasspathEntry(
full_library = all_classes_jar,
abi = abi or all_classes_jar,
required_for_source_only_abi = ctx.attrs.required_for_source_only_abi,
)
java_library_info, java_packaging_info, shared_library_info, cxx_resource_info, template_placeholder_info, java_library_intellij_info = create_java_library_providers(
ctx = ctx,
library_output = library_output_classpath_entry,
exported_deps = ctx.attrs.deps,
needs_desugar = True,
is_prebuilt_jar = True,
annotation_jars_dir = annotation_jars_dir,
)
native_library = PrebuiltNativeLibraryDir(
raw_target = ctx.label.raw_target(),
dir = jni,
for_primary_apk = ctx.attrs.use_system_library_loader,
is_asset = False,
)
return [
java_library_info,
java_packaging_info,
shared_library_info,
cxx_resource_info,
template_placeholder_info,
java_library_intellij_info,
merge_android_packageable_info(ctx.label, ctx.actions, ctx.attrs.deps, manifest = manifest, prebuilt_native_library_dir = native_library, resource_info = resource_info),
resource_info,
DefaultInfo(default_outputs = [all_classes_jar], other_outputs = [
manifest,
r_dot_txt,
res,
assets,
jni,
annotation_jars_dir,
]),
]

View file

@ -0,0 +1,283 @@
# 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.
Aapt2LinkInfo = record(
# "APK" containing resources to be used by the Android binary
primary_resources_apk = "artifact",
# proguard config needed to retain used resources
proguard_config_file = "artifact",
# R.txt containing all the linked resources
r_dot_txt = "artifact",
)
AndroidBinaryNativeLibsInfo = record(
apk_under_test_prebuilt_native_library_dirs = ["PrebuiltNativeLibraryDir"],
apk_under_test_shared_libraries = ["SharedLibrary"],
exopackage_info = ["ExopackageNativeInfo", None],
native_lib_assets = ["artifact"],
native_libs_for_primary_apk = ["artifact"],
unstripped_libs = ["artifact"],
)
AndroidBinaryResourcesInfo = record(
# Optional information about resources that should be exopackaged
exopackage_info = ["ExopackageResourcesInfo", None],
# manifest to be used by the APK
manifest = "artifact",
# zip containing any strings packaged as assets
packaged_string_assets = ["artifact", None],
# "APK" containing resources to be used by the Android binary
primary_resources_apk = "artifact",
# proguard config needed to retain used resources
proguard_config_file = "artifact",
# R.java jars containing all the linked resources
r_dot_javas = ["JavaLibraryInfo"],
# directory containing filtered string resources files
string_source_map = ["artifact", None],
# list of jars that could contain resources that should be packaged into the APK
jar_files_that_may_contain_resources = ["artifact"],
# The resource infos that are used in this APK
unfiltered_resource_infos = ["AndroidResourceInfo"],
)
# Information about an `android_build_config`
BuildConfigField = record(
type = str.type,
name = str.type,
value = str.type,
)
AndroidBuildConfigInfo = provider(
fields = [
"package", # str.type
"build_config_fields", # ["BuildConfigField"]
],
)
# Information about an `android_manifest`
AndroidManifestInfo = provider(
fields = [
"manifest", # artifact
"merge_report", # artifact
],
)
AndroidApkInfo = provider(
fields = [
"apk",
"manifest",
],
)
AndroidApkUnderTestInfo = provider(
fields = [
"java_packaging_deps", # ["JavaPackagingDep"]
"keystore", # "KeystoreInfo"
"manifest_entries", # dict.type
"prebuilt_native_library_dirs", # ["PrebuiltNativeLibraryDir"]
"platforms", # [str.type]
"primary_platform", # str.type
"resource_infos", # ["ResourceInfos"]
"shared_libraries", # ["SharedLibrary"]
],
)
AndroidInstrumentationApkInfo = provider(
fields = [
"apk_under_test", # "artifact"
],
)
CPU_FILTER_TO_ABI_DIRECTORY = {
"arm64": "arm64-v8a",
"armv7": "armeabi-v7a",
"x86": "x86",
"x86_64": "x86_64",
}
PrebuiltNativeLibraryDir = record(
raw_target = "target_label",
dir = "artifact", # contains subdirectories for different ABIs.
for_primary_apk = bool.type,
is_asset = bool.type,
)
def _artifacts(value: "artifact"):
return value
AndroidBuildConfigInfoTSet = transitive_set()
AndroidDepsTSet = transitive_set()
ManifestTSet = transitive_set(args_projections = {"artifacts": _artifacts})
PrebuiltNativeLibraryDirTSet = transitive_set()
ResourceInfoTSet = transitive_set()
DepsInfo = record(
name = "target_label",
deps = ["target_label"],
)
AndroidPackageableInfo = provider(
fields = [
"target_label", # "target_label"
"build_config_infos", # ["AndroidBuildConfigInfoTSet", None]
"deps", # ["AndroidDepsTSet", None]
"manifests", # ["ManifestTSet", None]
"prebuilt_native_library_dirs", # ["PrebuiltNativeLibraryDirTSet", None]
"resource_infos", # ["AndroidResourceInfoTSet", None]
],
)
# Information about an `android_resource`
AndroidResourceInfo = provider(
fields = [
# output of running `aapt2_compile` on the resources, if resources are present
"aapt2_compile_output", # ["artifact", None]
# if False, then the "res" are not affected by the strings-as-assets resource filter
"allow_strings_as_assets_resource_filtering", # bool.type
# assets defined by this rule. May be empty
"assets", # ["artifact", None]
# manifest file used by the resources, if resources are present
"manifest_file", # ["artifact", None]
# package used for R.java, if resources are present
"r_dot_java_package", # ["artifact", None]
# resources defined by this rule. May be empty
"res", # ["artifact", None]
# symbols defined by the resources, if resources are present
"text_symbols", # ["artifact", None]
],
)
# `AndroidResourceInfos` that are exposed via `exported_deps`
ExportedAndroidResourceInfo = provider(
fields = [
"resource_infos", # ["AndroidResourceInfo"]
],
)
ExopackageDexInfo = record(
metadata = "artifact",
directory = "artifact",
)
ExopackageNativeInfo = record(
metadata = "artifact",
directory = "artifact",
)
ExopackageResourcesInfo = record(
assets = ["artifact", None],
assets_hash = ["artifact", None],
res = "artifact",
res_hash = "artifact",
third_party_jar_resources = "artifact",
third_party_jar_resources_hash = "artifact",
)
DexFilesInfo = record(
primary_dex = "artifact",
primary_dex_class_names = ["artifact", None],
secondary_dex_dirs = ["artifact"],
secondary_dex_exopackage_info = [ExopackageDexInfo.type, None],
proguard_text_files_path = ["artifact", None],
)
ExopackageInfo = record(
secondary_dex_info = [ExopackageDexInfo.type, None],
native_library_info = [ExopackageNativeInfo.type, None],
resources_info = [ExopackageResourcesInfo.type, None],
)
AndroidLibraryIntellijInfo = provider(
"Information about android library that is required for Intellij project generation",
fields = [
"dummy_r_dot_java", # ["artifact", None]
],
)
def merge_android_packageable_info(
label: "label",
actions: "actions",
deps: ["dependency"],
build_config_info: ["AndroidBuildConfigInfo", None] = None,
manifest: ["artifact", None] = None,
prebuilt_native_library_dir: [PrebuiltNativeLibraryDir.type, None] = None,
resource_info: ["AndroidResourceInfo", None] = None) -> "AndroidPackageableInfo":
android_packageable_deps = filter(None, [x.get(AndroidPackageableInfo) for x in deps])
build_config_infos = _get_transitive_set(
actions,
filter(None, [dep.build_config_infos for dep in android_packageable_deps]),
build_config_info,
AndroidBuildConfigInfoTSet,
)
deps = _get_transitive_set(
actions,
filter(None, [dep.deps for dep in android_packageable_deps]),
DepsInfo(
name = label.raw_target(),
deps = [dep.target_label for dep in android_packageable_deps],
),
AndroidDepsTSet,
)
manifests = _get_transitive_set(
actions,
filter(None, [dep.manifests for dep in android_packageable_deps]),
manifest,
ManifestTSet,
)
prebuilt_native_library_dirs = _get_transitive_set(
actions,
filter(None, [dep.prebuilt_native_library_dirs for dep in android_packageable_deps]),
prebuilt_native_library_dir,
PrebuiltNativeLibraryDirTSet,
)
resource_infos = _get_transitive_set(
actions,
filter(None, [dep.resource_infos for dep in android_packageable_deps]),
resource_info,
ResourceInfoTSet,
)
return AndroidPackageableInfo(
target_label = label.raw_target(),
build_config_infos = build_config_infos,
deps = deps,
manifests = manifests,
prebuilt_native_library_dirs = prebuilt_native_library_dirs,
resource_infos = resource_infos,
)
def _get_transitive_set(
actions: "actions",
children: ["transitive_set"],
node: "_a",
transitive_set_definition: "transitive_set_definition") -> ["transitive_set", None]:
kwargs = {}
if children:
kwargs["children"] = children
if node:
kwargs["value"] = node
return actions.tset(transitive_set_definition, **kwargs) if kwargs else None
def merge_exported_android_resource_info(
exported_deps: ["dependency"]) -> "ExportedAndroidResourceInfo":
exported_android_resource_infos = []
for exported_dep in exported_deps:
exported_resource_info = exported_dep.get(ExportedAndroidResourceInfo)
if exported_resource_info:
exported_android_resource_infos += exported_resource_info.resource_infos
android_resource = exported_dep.get(AndroidResourceInfo)
if android_resource:
exported_android_resource_infos.append(android_resource)
return ExportedAndroidResourceInfo(resource_infos = dedupe(exported_android_resource_infos))

View file

@ -0,0 +1,143 @@
# 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//java:java_providers.bzl", "get_java_packaging_info")
load("@prelude//utils:utils.bzl", "expect")
load(":android_providers.bzl", "AndroidResourceInfo", "ExportedAndroidResourceInfo", "merge_android_packageable_info")
load(":android_toolchain.bzl", "AndroidToolchainInfo")
JAVA_PACKAGE_FILENAME = "java_package.txt"
def _convert_to_artifact_dir(ctx: "context", attr: ["dependency", "dict", "artifact", None], attr_name: str.type) -> ["artifact", None]:
if type(attr) == "dependency":
expect(len(attr[DefaultInfo].default_outputs) == 1, "Expect one default output from build dep of attr {}!".format(attr_name))
return attr[DefaultInfo].default_outputs[0]
elif type(attr) == "dict" and len(attr) > 0:
return ctx.actions.symlinked_dir("{}_dir".format(attr_name), attr)
else:
return attr
def android_resource_impl(ctx: "context") -> ["provider"]:
if ctx.attrs._build_only_native_code:
return [DefaultInfo()]
# TODO(T100007184) filter res/assets by ignored filenames
sub_targets = {}
providers = []
default_outputs = []
res = _convert_to_artifact_dir(ctx, ctx.attrs.res, "res")
assets = _convert_to_artifact_dir(ctx, ctx.attrs.assets, "assets")
if res:
aapt2_compile_output = aapt2_compile(ctx, res, ctx.attrs._android_toolchain[AndroidToolchainInfo])
sub_targets["aapt2_compile"] = [DefaultInfo(default_outputs = [aapt2_compile_output])]
r_dot_txt_output = get_text_symbols(ctx, res, ctx.attrs.deps)
default_outputs.append(r_dot_txt_output)
r_dot_java_package = _get_package(ctx, ctx.attrs.package, ctx.attrs.manifest)
resource_info = AndroidResourceInfo(
aapt2_compile_output = aapt2_compile_output,
allow_strings_as_assets_resource_filtering = not ctx.attrs.has_whitelisted_strings,
assets = assets,
manifest_file = ctx.attrs.manifest,
r_dot_java_package = r_dot_java_package,
res = res,
text_symbols = r_dot_txt_output,
)
else:
resource_info = AndroidResourceInfo(
aapt2_compile_output = None,
allow_strings_as_assets_resource_filtering = not ctx.attrs.has_whitelisted_strings,
assets = assets,
manifest_file = ctx.attrs.manifest,
r_dot_java_package = None,
res = None,
text_symbols = None,
)
providers.append(resource_info)
providers.append(merge_android_packageable_info(ctx.label, ctx.actions, ctx.attrs.deps, manifest = ctx.attrs.manifest, resource_info = resource_info))
providers.append(get_java_packaging_info(ctx, ctx.attrs.deps))
providers.append(DefaultInfo(default_outputs = default_outputs, sub_targets = sub_targets))
return providers
def aapt2_compile(
ctx: "context",
resources_dir: "artifact",
android_toolchain: "AndroidToolchainInfo",
skip_crunch_pngs: bool.type = False,
identifier: [str.type, None] = None) -> "artifact":
aapt2_command = cmd_args(android_toolchain.aapt2)
aapt2_command.add("compile")
aapt2_command.add("--legacy")
if skip_crunch_pngs:
aapt2_command.add("--no-crunch")
aapt2_command.add(["--dir", resources_dir])
aapt2_output = ctx.actions.declare_output("{}_resources.flata".format(identifier) if identifier else "resources.flata")
aapt2_command.add("-o", aapt2_output.as_output())
ctx.actions.run(aapt2_command, category = "aapt2_compile", identifier = identifier)
return aapt2_output
def _get_package(ctx: "context", package: [str.type, None], manifest: ["artifact", None]) -> "artifact":
if package:
return ctx.actions.write(JAVA_PACKAGE_FILENAME, package)
else:
expect(manifest != None, "if package is not declared then a manifest must be")
return extract_package_from_manifest(ctx, manifest)
def extract_package_from_manifest(ctx: "context", manifest: "artifact") -> "artifact":
r_dot_java_package = ctx.actions.declare_output(JAVA_PACKAGE_FILENAME)
extract_package_cmd = cmd_args(ctx.attrs._android_toolchain[AndroidToolchainInfo].manifest_utils[RunInfo])
extract_package_cmd.add(["--manifest-path", manifest])
extract_package_cmd.add(["--package-output", r_dot_java_package.as_output()])
ctx.actions.run(extract_package_cmd, category = "android_extract_package")
return r_dot_java_package
def get_text_symbols(
ctx: "context",
res: "artifact",
deps: ["dependency"],
identifier: [str.type, None] = None):
mini_aapt_cmd = cmd_args(ctx.attrs._android_toolchain[AndroidToolchainInfo].mini_aapt[RunInfo])
mini_aapt_cmd.add(["--resource-paths", res])
dep_symbol_paths = cmd_args()
dep_symbols = _get_dep_symbols(deps)
dep_symbol_paths.add(dep_symbols)
dep_symbol_paths_file, _ = ctx.actions.write("{}_dep_symbol_paths_file".format(identifier) if identifier else "dep_symbol_paths_file", dep_symbol_paths, allow_args = True)
mini_aapt_cmd.add(["--dep-symbol-paths", dep_symbol_paths_file])
mini_aapt_cmd.hidden(dep_symbols)
text_symbols = ctx.actions.declare_output("{}_R.txt".format(identifier) if identifier else "R.txt")
mini_aapt_cmd.add(["--output-path", text_symbols.as_output()])
ctx.actions.run(mini_aapt_cmd, category = "mini_aapt", identifier = identifier)
return text_symbols
def _get_dep_symbols(deps: ["dependency"]) -> ["artifact"]:
dep_symbols = []
for dep in deps:
android_resource_info = dep.get(AndroidResourceInfo)
exported_android_resource_info = dep.get(ExportedAndroidResourceInfo)
expect(android_resource_info != None or exported_android_resource_info != None, "Dependencies of `android_resource` rules should be `android_resource`s or `android_library`s")
if android_resource_info and android_resource_info.text_symbols:
dep_symbols.append(android_resource_info.text_symbols)
if exported_android_resource_info:
dep_symbols += [resource_info.text_symbols for resource_info in exported_android_resource_info.resource_infos if resource_info.text_symbols]
return dedupe(dep_symbols)

View file

@ -0,0 +1,50 @@
# 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.
AndroidPlatformInfo = provider(fields = [
"name",
])
AndroidToolchainInfo = provider(fields = [
"aapt2",
"adb",
"aidl",
"android_jar",
"android_bootclasspath",
"apk_builder",
"apk_module_graph",
"combine_native_library_dirs",
"compress_libraries",
"d8_command",
"exo_resources_rewriter",
"exopackage_agent_apk",
"filter_dex_class_names",
"filter_prebuilt_native_library_dir",
"multi_dex_command",
"copy_string_resources",
"filter_resources",
"framework_aidl_file",
"generate_build_config",
"generate_manifest",
"instrumentation_test_runner_classpath",
"instrumentation_test_runner_main_class",
"manifest_utils",
"merge_android_resources",
"merge_assets",
"merge_third_party_jar_resources",
"mini_aapt",
"native_libs_as_assets_metadata",
"optimized_proguard_config",
"package_strings_as_assets",
"proguard_config",
"proguard_jar",
"proguard_max_heap_size",
"r_dot_java_weight_factor",
"secondary_dex_weight_limit",
"unpack_aar",
"zipalign",
])

View file

@ -0,0 +1,35 @@
# 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//:genrule.bzl", "process_genrule")
load("@prelude//android:android_providers.bzl", "AndroidApkInfo", "AndroidApkUnderTestInfo")
load("@prelude//utils:utils.bzl", "expect")
def apk_genrule_impl(ctx: "context") -> ["provider"]:
# TODO(T104150125) The underlying APK should not have exopackage enabled
input_android_apk_info = ctx.attrs.apk[AndroidApkInfo]
expect(input_android_apk_info != None, "'apk' attribute must be an Android APK!")
input_android_apk_under_test_info = ctx.attrs.apk[AndroidApkUnderTestInfo]
env_vars = {
"APK": cmd_args(input_android_apk_info.apk),
}
# Like buck1, we ignore the 'out' attribute and construct the output path ourselves.
output_apk_name = "{}.apk".format(ctx.label.name)
genrule_providers = process_genrule(ctx, output_apk_name, None, env_vars)
expect(len(genrule_providers) == 1 and type(genrule_providers[0]) == DefaultInfo.type, "Expecting just a single DefaultInfo, but got {}".format(genrule_providers))
output_apk = genrule_providers[0].default_outputs[0]
return genrule_providers + [
AndroidApkInfo(
apk = output_apk,
manifest = input_android_apk_info.manifest,
),
] + filter(None, [input_android_apk_under_test_info])

View file

@ -0,0 +1,190 @@
# 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//android:android_providers.bzl", "CPU_FILTER_TO_ABI_DIRECTORY")
load("@prelude//android:min_sdk_version.bzl", "get_min_sdk_version_constraint_value_name", "get_min_sdk_version_range")
# FIXME: prelude// should be standalone (not refer to ovr_config//)
_REFS = {
"arm64": "ovr_config//cpu/constraints:arm64",
"armv7": "ovr_config//cpu/constraints:arm32",
"build_only_native_code": "fbsource//xplat/buck2/platform/android:build_only_native_code",
"building_android_binary": "prelude//os:building_android_binary",
"cpu": "ovr_config//cpu/constraints:cpu",
"do_not_build_only_native_code": "fbsource//xplat/buck2/platform/android:do_not_build_only_native_code",
"maybe_build_only_native_code": "fbsource//xplat/buck2/platform/android:maybe_build_only_native_code",
"maybe_building_android_binary": "prelude//os:maybe_building_android_binary",
"min_sdk_version": "fbsource//xplat/buck2/platform/android:min_sdk_version",
"x86": "ovr_config//cpu/constraints:x86_32",
"x86_64": "ovr_config//cpu/constraints:x86_64",
}
for min_sdk in get_min_sdk_version_range():
constraint_value_name = get_min_sdk_version_constraint_value_name(min_sdk)
_REFS[constraint_value_name] = "fbsource//xplat/buck2/platform/android:{}".format(constraint_value_name)
def _cpu_split_transition_instrumentation_test_apk_impl(
platform: PlatformInfo.type,
refs: struct.type,
attrs: struct.type) -> {str.type: PlatformInfo.type}:
cpu_filters = attrs.cpu_filters or CPU_FILTER_TO_ABI_DIRECTORY.keys()
return _cpu_split_transition(platform, refs, cpu_filters, attrs.min_sdk_version, build_only_native_code_on_secondary_platforms = False)
def _cpu_split_transition_impl(
platform: PlatformInfo.type,
refs: struct.type,
attrs: struct.type) -> {str.type: PlatformInfo.type}:
cpu_filters = attrs.cpu_filters or CPU_FILTER_TO_ABI_DIRECTORY.keys()
do_not_build_only_native_code = refs.do_not_build_only_native_code[ConstraintValueInfo].label in [constraint.label for constraint in platform.configuration.constraints.values()]
return _cpu_split_transition(platform, refs, cpu_filters, attrs.min_sdk_version, build_only_native_code_on_secondary_platforms = not do_not_build_only_native_code)
def _cpu_split_transition(
platform: PlatformInfo.type,
refs: struct.type,
cpu_filters: [str.type],
min_sdk_version: [int.type, None],
build_only_native_code_on_secondary_platforms: bool.type) -> {str.type: PlatformInfo.type}:
cpu = refs.cpu
x86 = refs.x86[ConstraintValueInfo]
x86_64 = refs.x86_64[ConstraintValueInfo]
armv7 = refs.armv7[ConstraintValueInfo]
arm64 = refs.arm64[ConstraintValueInfo]
cpu_name_to_cpu_constraint = {}
for cpu_filter in cpu_filters:
if cpu_filter == "x86":
cpu_name_to_cpu_constraint["x86"] = x86
elif cpu_filter == "armv7":
cpu_name_to_cpu_constraint["armv7"] = armv7
elif cpu_filter == "x86_64":
cpu_name_to_cpu_constraint["x86_64"] = x86_64
elif cpu_filter == "arm64":
cpu_name_to_cpu_constraint["arm64"] = arm64
else:
fail("Unexpected cpu_filter: {}".format(cpu_filter))
base_constraints = {
constraint_setting_label: constraint_setting_value
for (constraint_setting_label, constraint_setting_value) in platform.configuration.constraints.items()
if constraint_setting_label != cpu[ConstraintSettingInfo].label and constraint_setting_label != refs.maybe_build_only_native_code[ConstraintSettingInfo].label
}
base_constraints[refs.maybe_building_android_binary[ConstraintSettingInfo].label] = refs.building_android_binary[ConstraintValueInfo]
if min_sdk_version:
base_constraints[refs.min_sdk_version[ConstraintSettingInfo].label] = _get_min_sdk_constraint_value(min_sdk_version, refs)
new_configs = {}
for platform_name, cpu_constraint in cpu_name_to_cpu_constraint.items():
updated_constraints = dict(base_constraints)
updated_constraints[refs.cpu[ConstraintSettingInfo].label] = cpu_constraint
if len(new_configs) > 0 and build_only_native_code_on_secondary_platforms:
updated_constraints[refs.maybe_build_only_native_code[ConstraintSettingInfo].label] = refs.build_only_native_code[ConstraintValueInfo]
new_configs[platform_name] = PlatformInfo(
label = platform_name,
configuration = ConfigurationInfo(
constraints = updated_constraints,
values = platform.configuration.values,
),
)
return new_configs
def _cpu_transition_impl(
platform: PlatformInfo.type,
refs: struct.type,
attrs: struct.type) -> PlatformInfo.type:
return _cpu_split_transition_impl(platform, refs, attrs).values()[0]
cpu_split_transition = transition(
impl = _cpu_split_transition_impl,
refs = _REFS,
attrs = [
"cpu_filters",
"min_sdk_version",
],
split = True,
)
cpu_split_transition_instrumentation_test_apk = transition(
impl = _cpu_split_transition_instrumentation_test_apk_impl,
refs = _REFS,
attrs = [
"cpu_filters",
"min_sdk_version",
],
split = True,
)
# If our deps have been split-transitioned by CPU then we are already analyzing the dependency
# graph using the resulting configurations. If there are any other attributes on the same target
# that also need to analyze the dependency graph, then we want to use one of the configurations
# from the split transition so that we don't end up analyzing the graph again using a different
# configuration. This rule just picks the first configuration from the split-transition.
#
# This is used for the `manifest` attribute of `android_binary`.
cpu_transition = transition(
impl = _cpu_transition_impl,
refs = _REFS,
attrs = [
"cpu_filters",
"min_sdk_version",
],
)
def _do_not_build_only_native_code_transition(
platform: PlatformInfo.type,
refs: struct.type) -> PlatformInfo.type:
constraints = dict(platform.configuration.constraints.items())
constraints[refs.maybe_build_only_native_code[ConstraintSettingInfo].label] = refs.do_not_build_only_native_code[ConstraintValueInfo]
return PlatformInfo(
label = platform.label,
configuration = ConfigurationInfo(
constraints = constraints,
values = platform.configuration.values,
),
)
do_not_build_only_native_code_transition = transition(
impl = _do_not_build_only_native_code_transition,
refs = {
"do_not_build_only_native_code": "fbsource//xplat/buck2/platform/android:do_not_build_only_native_code",
"maybe_build_only_native_code": "fbsource//xplat/buck2/platform/android:maybe_build_only_native_code",
},
)
def get_deps_by_platform(ctx: "context") -> {str.type: ["dependency"]}:
deps_by_platform = {}
for dep_dict in ctx.attrs.deps:
for platform, dep in dep_dict.items():
deps = deps_by_platform.get(platform, [])
deps.append(dep)
deps_by_platform[platform] = deps
return deps_by_platform
def _get_min_sdk_constraint_value(min_sdk_version: int.type, refs: struct.type) -> ConstraintValueInfo.type:
constraint_name = get_min_sdk_version_constraint_value_name(min_sdk_version)
constraint = getattr(refs, constraint_name, None)
if not constraint:
fail("Unsupported min_sdk_version {}, please report!".format(min_sdk_version))
return constraint[ConstraintValueInfo]
def _is_building_android_binary() -> "selector":
return select(
{
"DEFAULT": False,
"prelude//os:building_android_binary": True,
},
)
def is_building_android_binary_attr() -> "attribute":
return attrs.default_only(attrs.bool(default = _is_building_android_binary()))

View file

@ -0,0 +1,677 @@
# 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//android:android_providers.bzl", "DexFilesInfo", "ExopackageDexInfo")
load("@prelude//android:voltron.bzl", "ROOT_MODULE", "get_apk_module_graph_info", "get_root_module_only_apk_module_graph_info", "is_root_module")
load("@prelude//java:dex.bzl", "get_dex_produced_from_java_library")
load("@prelude//java:dex_toolchain.bzl", "DexToolchainInfo")
load("@prelude//java:java_library.bzl", "compile_to_jar")
load("@prelude//utils:utils.bzl", "expect", "flatten")
load("@prelude//paths.bzl", "paths")
# Android builds use a tool called `d8` to compile Java bytecode is DEX (Dalvik EXecutable)
# bytecode that runs on Android devices. Our Android builds have two distinct ways of
# doing that:
# 1) With pre-dexing enabled (this is the most common case for debug builds). That means that
# d8 runs on every individual .jar file (to produce a .jar.dex file), and then at the APK
# level we run d8 again to combine all the individual .jar.dex files.
# 2) With pre-dexing disabled (this is the case if it is explicitly disabled, we are running
# proguard, or we preprocess the java classes at the APK level). This means that we run
# d8 at the APK level on all the .jar files.
#
# The .dex files that we package into the APK consist of a single classes.dex "primary DEX"
# file, and N secondary DEX files. The classes that are put into the primary DEX are those
# that are required at startup, and are specified via `primary_dex_patterns` (classes which
# match one of those patterns are put into the primary DEX).
#
# The primary DEX is always stored in the root directory of the APK as `classes.dex`.
#
# We have 4 different ways of storing our secondary DEX files, which are specified via the
# `dex_compression` attribute:
# 1) `raw` compression. This means that we create `classes2.dex`, `classes3.dex`, ...,
# `classesN.dex` and store each of them in the root directory of the APK.
# 2) `jar` compression. For each secondary DEX file, we put a `classes.dex` entry into a
# JAR file, and store it as an asset at `assets/secondary-program-dex-jars/secondary-I.dex.jar`
# 3) `xz` compression. This is the same as `jar` compression, except that we run `xz` on the
# JAR file to produce `assets/secondary-program-dex-jars/secondary-I.dex.jar.xz`.
# 4) `xzs` compression. We do the same as `jar` compression, then concatenate all the jars
# together and do `xz` compression on the result to produce a single
# `assets/secondary-program-dex-jars/secondary.dex.jar.xzs`.
#
# For all compression types, we also package a `assets/secondary-program-dex-jars/metadata.txt`,
# which has an entry for each secondary DEX file:
# <secondary DEX file name> <sha1 hash of secondary DEX> <canary class name>
#
# A "canary class" is a Java class that we add to every secondary DEX. It is a known class that
# can be used for DEX verification when loading the DEX on a device.
#
# For compression types other than raw, we also include a metadata file per secondary DEX, which
# consists of a single line of the form:
# jar:<size of secondary dex jar (in bytes)> dex:<size of uncompressed dex file (in bytes)>
#
# If an APK has Voltron modules, then we produce a separate group of secondary DEX files for each
# module, and we put them into `assets/<module_name>` instead of `assets/secondary-program-dex-jars`.
# We produce a `metadata.txt` file for each Voltron module.
_DEX_MERGE_OPTIONS = ["--no-desugar", "--no-optimize"]
SplitDexMergeConfig = record(
dex_compression = str.type,
primary_dex_patterns = [str.type],
secondary_dex_weight_limit_bytes = int.type,
)
def _get_dex_compression(ctx: "context") -> str.type:
is_exopackage_enabled_for_secondary_dexes = "secondary_dex" in ctx.attrs.exopackage_modes
default_dex_compression = "jar" if is_exopackage_enabled_for_secondary_dexes else "raw"
dex_compression = ctx.attrs.dex_compression or default_dex_compression
expect(
dex_compression in ["raw", "jar", "xz", "xzs"],
"Only 'raw', 'jar', 'xz' and 'xzs' dex compression are supported at this time!",
)
return dex_compression
def get_split_dex_merge_config(
ctx: "context",
android_toolchain: "AndroidToolchainInfo") -> "SplitDexMergeConfig":
return SplitDexMergeConfig(
dex_compression = _get_dex_compression(ctx),
primary_dex_patterns = ctx.attrs.primary_dex_patterns,
secondary_dex_weight_limit_bytes = (
ctx.attrs.secondary_dex_weight_limit or
android_toolchain.secondary_dex_weight_limit
),
)
def get_single_primary_dex(
ctx: "context",
android_toolchain: "AndroidToolchainInfo",
java_library_jars: ["artifact"],
is_optimized: bool.type) -> "DexFilesInfo":
expect(
not _is_exopackage_enabled_for_secondary_dex(ctx),
"It doesn't make sense to enable secondary dex exopackage for single dex builds!",
)
d8_cmd = cmd_args(android_toolchain.d8_command[RunInfo])
output_dex_file = ctx.actions.declare_output("classes.dex")
d8_cmd.add(["--output-dex-file", output_dex_file.as_output()])
jar_to_dex_file = ctx.actions.write("jar_to_dex_file.txt", java_library_jars)
d8_cmd.add(["--files-to-dex-list", jar_to_dex_file])
d8_cmd.hidden(java_library_jars)
d8_cmd.add(["--android-jar", android_toolchain.android_jar])
if not is_optimized:
d8_cmd.add("--no-optimize")
ctx.actions.run(d8_cmd, category = "d8", identifier = "{}:{}".format(ctx.label.package, ctx.label.name))
return DexFilesInfo(
primary_dex = output_dex_file,
secondary_dex_dirs = [],
secondary_dex_exopackage_info = None,
proguard_text_files_path = None,
primary_dex_class_names = None,
)
def get_multi_dex(
ctx: "context",
android_toolchain: "AndroidToolchainInfo",
java_library_jars_to_owners: {"artifact": "target_label"},
primary_dex_patterns: [str.type],
proguard_configuration_output_file: ["artifact", None],
proguard_mapping_output_file: ["artifact", None],
is_optimized: bool.type,
apk_module_graph_file: ["artifact", None] = None) -> "DexFilesInfo":
expect(
not _is_exopackage_enabled_for_secondary_dex(ctx),
"secondary dex exopackage can only be enabled on pre-dexed builds!",
)
primary_dex_file = ctx.actions.declare_output("classes.dex")
primary_dex_class_names = ctx.actions.declare_output("primary_dex_class_names.txt")
root_module_secondary_dex_output_dir = ctx.actions.declare_output("root_module_secondary_dex_output_dir")
secondary_dex_dir = ctx.actions.declare_output("secondary_dex_output_dir")
# dynamic actions are not valid with no input, but it's easier to use the same code regardless,
# so just create an empty input.
inputs = [apk_module_graph_file] if apk_module_graph_file else [ctx.actions.write("empty_artifact_for_multi_dex_dynamic_action", [])]
outputs = [primary_dex_file, primary_dex_class_names, root_module_secondary_dex_output_dir, secondary_dex_dir]
def do_multi_dex(ctx: "context", artifacts, outputs):
apk_module_graph_info = get_apk_module_graph_info(ctx, apk_module_graph_file, artifacts) if apk_module_graph_file else get_root_module_only_apk_module_graph_info()
target_to_module_mapping_function = apk_module_graph_info.target_to_module_mapping_function
module_to_jars = {}
for java_library_jar, owner in java_library_jars_to_owners.items():
module = target_to_module_mapping_function(str(owner))
module_to_jars.setdefault(module, []).append(java_library_jar)
secondary_dex_dir_srcs = {}
for module, jars in module_to_jars.items():
multi_dex_cmd = cmd_args(android_toolchain.multi_dex_command[RunInfo])
if is_root_module(module):
multi_dex_cmd.add("--primary-dex", outputs[primary_dex_file].as_output())
multi_dex_cmd.add("--primary-dex-patterns-path", ctx.actions.write("primary_dex_patterns", primary_dex_patterns))
multi_dex_cmd.add("--primary-dex-class-names", outputs[primary_dex_class_names].as_output())
multi_dex_cmd.add("--secondary-dex-output-dir", outputs[root_module_secondary_dex_output_dir].as_output())
else:
secondary_dex_dir_for_module = ctx.actions.declare_output("secondary_dex_output_dir_for_module_{}".format(module))
secondary_dex_subdir = secondary_dex_dir_for_module.project(_get_secondary_dex_subdir(module))
secondary_dex_dir_srcs[_get_secondary_dex_subdir(module)] = secondary_dex_subdir
multi_dex_cmd.add("--secondary-dex-output-dir", secondary_dex_dir_for_module.as_output())
multi_dex_cmd.add("--module-deps", ctx.actions.write("module_deps_for_{}".format(module), apk_module_graph_info.module_to_module_deps_function(module)))
multi_dex_cmd.add("--module", module)
multi_dex_cmd.add("--canary-class-name", apk_module_graph_info.module_to_canary_class_name_function(module))
jar_to_dex_file = ctx.actions.write("jars_to_dex_file_for_module_{}.txt".format(module), jars)
multi_dex_cmd.add("--files-to-dex-list", jar_to_dex_file)
multi_dex_cmd.hidden(jars)
multi_dex_cmd.add("--android-jar", android_toolchain.android_jar)
if not is_optimized:
multi_dex_cmd.add("--no-optimize")
if proguard_configuration_output_file:
multi_dex_cmd.add("--proguard-configuration-file", proguard_configuration_output_file)
multi_dex_cmd.add("--proguard-mapping-file", proguard_mapping_output_file)
multi_dex_cmd.add("--compression", _get_dex_compression(ctx))
multi_dex_cmd.add("--xz-compression-level", str(ctx.attrs.xz_compression_level))
if ctx.attrs.minimize_primary_dex_size:
multi_dex_cmd.add("--minimize-primary-dex")
ctx.actions.run(multi_dex_cmd, category = "multi_dex", identifier = "{}:{}_module_{}".format(ctx.label.package, ctx.label.name, module))
ctx.actions.symlinked_dir(outputs[secondary_dex_dir], secondary_dex_dir_srcs)
ctx.actions.dynamic_output(dynamic = inputs, inputs = [], outputs = outputs, f = do_multi_dex)
return DexFilesInfo(
primary_dex = primary_dex_file,
secondary_dex_dirs = [root_module_secondary_dex_output_dir, secondary_dex_dir],
secondary_dex_exopackage_info = None,
proguard_text_files_path = None,
primary_dex_class_names = primary_dex_class_names,
)
def merge_to_single_dex(
ctx: "context",
android_toolchain: "AndroidToolchainInfo",
pre_dexed_libs: ["DexLibraryInfo"]) -> "DexFilesInfo":
expect(
not _is_exopackage_enabled_for_secondary_dex(ctx),
"It doesn't make sense to enable secondary dex exopackage for single dex builds!",
)
output_dex_file = ctx.actions.declare_output("classes.dex")
pre_dexed_artifacts_to_dex_file = ctx.actions.declare_output("pre_dexed_artifacts_to_dex_file.txt")
pre_dexed_artifacts = [pre_dexed_lib.dex for pre_dexed_lib in pre_dexed_libs if pre_dexed_lib.dex != None]
_merge_dexes(ctx, android_toolchain, output_dex_file, pre_dexed_artifacts, pre_dexed_artifacts_to_dex_file)
return DexFilesInfo(
primary_dex = output_dex_file,
secondary_dex_dirs = [],
secondary_dex_exopackage_info = None,
proguard_text_files_path = None,
primary_dex_class_names = None,
)
DexInputWithSpecifiedClasses = record(
lib = "DexLibraryInfo",
dex_class_names = [str.type],
)
DexInputWithClassNamesFile = record(
lib = "DexLibraryInfo",
filtered_class_names_file = "artifact",
)
# When using jar compression, the secondary dex directory consists of N secondary dex jars, each
# of which has a corresponding .meta file (the secondary_dex_metadata_file) containing a single
# line of the form:
# jar:<size of secondary dex jar (in bytes)> dex:<size of uncompressed dex file (in bytes)>
#
# It also contains a metadata.txt file, which consists on N lines, one for each secondary dex
# jar. Those lines consist of:
# <secondary dex file name> <sha1 hash of secondary dex> <canary class>
#
# We write the line that needs to be added to metadata.txt for this secondary dex jar to
# secondary_dex_metadata_line, and we use the secondary_dex_canary_class_name for the
# <canary class>.
#
# When we have finished building all of the secondary dexes, we read each of the
# secondary_dex_metadata_line artifacts and write them to a single metadata.txt file.
# We do that for raw compression too, since it also has a metadata.txt file.
SecondaryDexMetadataConfig = record(
secondary_dex_compression = str.type,
secondary_dex_metadata_path = [str.type, None],
secondary_dex_metadata_file = ["artifact", None],
secondary_dex_metadata_line = "artifact",
secondary_dex_canary_class_name = str.type,
)
def _get_secondary_dex_jar_metadata_config(
actions: "actions",
secondary_dex_path: str.type,
module: str.type,
module_to_canary_class_name_function: "function",
index: int.type) -> SecondaryDexMetadataConfig.type:
secondary_dex_metadata_path = secondary_dex_path + ".meta"
return SecondaryDexMetadataConfig(
secondary_dex_compression = "jar",
secondary_dex_metadata_path = secondary_dex_metadata_path,
secondary_dex_metadata_file = actions.declare_output(secondary_dex_metadata_path),
secondary_dex_metadata_line = actions.declare_output("metadata_line_artifacts/{}/{}".format(module, index + 1)),
secondary_dex_canary_class_name = _get_fully_qualified_canary_class_name(module, module_to_canary_class_name_function, index + 1),
)
def _get_secondary_dex_raw_metadata_config(
actions: "actions",
module: str.type,
module_to_canary_class_name_function: "function",
index: int.type) -> SecondaryDexMetadataConfig.type:
return SecondaryDexMetadataConfig(
secondary_dex_compression = "raw",
secondary_dex_metadata_path = None,
secondary_dex_metadata_file = None,
secondary_dex_metadata_line = actions.declare_output("metadata_line_artifacts/{}/{}".format(module, index + 1)),
secondary_dex_canary_class_name = _get_fully_qualified_canary_class_name(module, module_to_canary_class_name_function, index + 1),
)
def _get_filter_dex_batch_size() -> int.type:
return 100
def _filter_pre_dexed_libs(
actions: "actions",
android_toolchain: "AndroidToolchainInfo",
primary_dex_patterns_file: "artifact",
pre_dexed_libs: ["DexLibraryInfo"],
batch_number: int.type) -> [DexInputWithClassNamesFile.type]:
pre_dexed_lib_with_class_names_files = []
for pre_dexed_lib in pre_dexed_libs:
class_names = pre_dexed_lib.class_names
id = "{}_{}_{}".format(class_names.owner.package, class_names.owner.name, class_names.short_path)
filtered_class_names_file = actions.declare_output("primary_dex_class_names_for_{}".format(id))
pre_dexed_lib_with_class_names_files.append(
DexInputWithClassNamesFile(lib = pre_dexed_lib, filtered_class_names_file = filtered_class_names_file),
)
filter_dex_cmd = cmd_args([
android_toolchain.filter_dex_class_names[RunInfo],
"--primary-dex-patterns",
primary_dex_patterns_file,
"--class-names",
[x.lib.class_names for x in pre_dexed_lib_with_class_names_files],
"--output",
[x.filtered_class_names_file.as_output() for x in pre_dexed_lib_with_class_names_files],
])
actions.run(filter_dex_cmd, category = "filter_dex", identifier = "batch_{}".format(batch_number))
return pre_dexed_lib_with_class_names_files
_SortedPreDexedInputs = record(
module = str.type,
primary_dex_inputs = [DexInputWithSpecifiedClasses.type],
secondary_dex_inputs = [[DexInputWithSpecifiedClasses.type]],
)
def merge_to_split_dex(
ctx: "context",
android_toolchain: "AndroidToolchainInfo",
pre_dexed_libs: ["DexLibraryInfo"],
split_dex_merge_config: "SplitDexMergeConfig",
apk_module_graph_file: ["artifact", None] = None) -> "DexFilesInfo":
is_exopackage_enabled_for_secondary_dex = _is_exopackage_enabled_for_secondary_dex(ctx)
if is_exopackage_enabled_for_secondary_dex:
expect(
split_dex_merge_config.dex_compression == "jar",
"Exopackage can only be enabled for secondary dexes when the dex compression is 'jar', but the dex compression is '{}'".format(split_dex_merge_config.dex_compression),
)
primary_dex_patterns_file = ctx.actions.write("primary_dex_patterns_file", split_dex_merge_config.primary_dex_patterns)
pre_dexed_lib_with_class_names_files = []
batch_size = _get_filter_dex_batch_size()
for (batch_number, start_index) in enumerate(range(0, len(pre_dexed_libs), batch_size)):
end_index = min(start_index + batch_size, len(pre_dexed_libs))
pre_dexed_lib_with_class_names_files.extend(
_filter_pre_dexed_libs(
ctx.actions,
android_toolchain,
primary_dex_patterns_file,
pre_dexed_libs[start_index:end_index],
batch_number,
),
)
input_artifacts = flatten([[
input.lib.dex,
input.lib.weight_estimate,
input.filtered_class_names_file,
] for input in pre_dexed_lib_with_class_names_files]) + ([apk_module_graph_file] if apk_module_graph_file else [])
primary_dex_artifact_list = ctx.actions.declare_output("pre_dexed_artifacts_for_primary_dex.txt")
primary_dex_output = ctx.actions.declare_output("classes.dex")
primary_dex_class_names_list = ctx.actions.declare_output("primary_dex_class_names_list.txt")
root_module_secondary_dexes_dir = ctx.actions.declare_output("root_module_secondary_dexes_dir")
root_module_secondary_dexes_subdir = root_module_secondary_dexes_dir.project(_get_secondary_dex_subdir(ROOT_MODULE))
root_module_secondary_dexes_metadata = root_module_secondary_dexes_dir.project(paths.join(_get_secondary_dex_subdir(ROOT_MODULE), "metadata.txt"))
non_root_module_secondary_dexes_dir = ctx.actions.declare_output("non_root_module_secondary_dexes_dir")
outputs = [primary_dex_output, primary_dex_artifact_list, primary_dex_class_names_list, root_module_secondary_dexes_dir, non_root_module_secondary_dexes_dir]
def merge_pre_dexed_libs(ctx: "context", artifacts, outputs):
apk_module_graph_info = get_apk_module_graph_info(ctx, apk_module_graph_file, artifacts) if apk_module_graph_file else get_root_module_only_apk_module_graph_info()
module_to_canary_class_name_function = apk_module_graph_info.module_to_canary_class_name_function
sorted_pre_dexed_inputs = _sort_pre_dexed_files(
ctx,
artifacts,
pre_dexed_lib_with_class_names_files,
split_dex_merge_config,
get_module_from_target = apk_module_graph_info.target_to_module_mapping_function,
module_to_canary_class_name_function = module_to_canary_class_name_function,
)
root_module_secondary_dexes_for_symlinking = {}
non_root_module_secondary_dexes_for_symlinking = {}
metadata_line_artifacts_by_module = {}
metadata_dot_txt_files_by_module = {}
for sorted_pre_dexed_input in sorted_pre_dexed_inputs:
module = sorted_pre_dexed_input.module
secondary_dexes_for_symlinking = root_module_secondary_dexes_for_symlinking if is_root_module(module) else non_root_module_secondary_dexes_for_symlinking
primary_dex_inputs = sorted_pre_dexed_input.primary_dex_inputs
pre_dexed_artifacts = [primary_dex_input.lib.dex for primary_dex_input in primary_dex_inputs if primary_dex_input.lib.dex]
if pre_dexed_artifacts:
expect(is_root_module(module), "module {} should not have a primary dex!".format(module))
ctx.actions.write(
outputs[primary_dex_class_names_list].as_output(),
flatten([primary_dex_input.dex_class_names for primary_dex_input in primary_dex_inputs]),
)
_merge_dexes(
ctx,
android_toolchain,
outputs[primary_dex_output],
pre_dexed_artifacts,
outputs[primary_dex_artifact_list],
class_names_to_include = primary_dex_class_names_list,
)
else:
expect(
not is_root_module(module),
"No primary dex classes were specified! Please add primary_dex_patterns to ensure that at least one class exists in the primary dex.",
)
secondary_dex_inputs = sorted_pre_dexed_input.secondary_dex_inputs
raw_secondary_dexes_for_compressing = {}
for i in range(len(secondary_dex_inputs)):
if split_dex_merge_config.dex_compression == "jar" or split_dex_merge_config.dex_compression == "raw":
if split_dex_merge_config.dex_compression == "jar":
secondary_dex_path = _get_jar_secondary_dex_path(i, module)
secondary_dex_metadata_config = _get_secondary_dex_jar_metadata_config(ctx.actions, secondary_dex_path, module, module_to_canary_class_name_function, i)
secondary_dexes_for_symlinking[secondary_dex_metadata_config.secondary_dex_metadata_path] = secondary_dex_metadata_config.secondary_dex_metadata_file
else:
secondary_dex_path = _get_raw_secondary_dex_path(i, module)
secondary_dex_metadata_config = _get_secondary_dex_raw_metadata_config(ctx.actions, module, module_to_canary_class_name_function, i)
secondary_dex_output = ctx.actions.declare_output(secondary_dex_path)
secondary_dexes_for_symlinking[secondary_dex_path] = secondary_dex_output
metadata_line_artifacts_by_module.setdefault(module, []).append(secondary_dex_metadata_config.secondary_dex_metadata_line)
else:
secondary_dex_name = _get_raw_secondary_dex_name(i, module)
secondary_dex_output = ctx.actions.declare_output("{}/{}".format(module, secondary_dex_name))
raw_secondary_dexes_for_compressing[secondary_dex_name] = secondary_dex_output
secondary_dex_metadata_config = None
secondary_dex_artifact_list = ctx.actions.declare_output("pre_dexed_artifacts_for_secondary_dex_{}_for_module_{}.txt".format(i + 2, module))
secondary_dex_class_list = ctx.actions.write(
"class_list_for_secondary_dex_{}_for_module_{}.txt".format(i + 2, module),
flatten([secondary_dex_input.dex_class_names for secondary_dex_input in secondary_dex_inputs[i]]),
)
pre_dexed_artifacts = [secondary_dex_input.lib.dex for secondary_dex_input in secondary_dex_inputs[i] if secondary_dex_input.lib.dex]
_merge_dexes(
ctx,
android_toolchain,
secondary_dex_output,
pre_dexed_artifacts,
secondary_dex_artifact_list,
class_names_to_include = secondary_dex_class_list,
secondary_dex_metadata_config = secondary_dex_metadata_config,
)
if split_dex_merge_config.dex_compression == "jar" or split_dex_merge_config.dex_compression == "raw":
metadata_dot_txt_path = "{}/metadata.txt".format(_get_secondary_dex_subdir(module))
metadata_dot_txt_file = ctx.actions.declare_output(metadata_dot_txt_path)
secondary_dexes_for_symlinking[metadata_dot_txt_path] = metadata_dot_txt_file
metadata_dot_txt_files_by_module[module] = metadata_dot_txt_file
else:
raw_secondary_dexes_dir = ctx.actions.symlinked_dir("raw_secondary_dexes_dir_for_module_{}".format(module), raw_secondary_dexes_for_compressing)
secondary_dex_dir_for_module = ctx.actions.declare_output("secondary_dexes_dir_for_{}".format(module))
secondary_dex_subdir = secondary_dex_dir_for_module.project(_get_secondary_dex_subdir(module))
multi_dex_cmd = cmd_args(android_toolchain.multi_dex_command[RunInfo])
multi_dex_cmd.add("--secondary-dex-output-dir", secondary_dex_dir_for_module.as_output())
multi_dex_cmd.add("--raw-secondary-dexes-dir", raw_secondary_dexes_dir)
multi_dex_cmd.add("--compression", _get_dex_compression(ctx))
multi_dex_cmd.add("--xz-compression-level", str(ctx.attrs.xz_compression_level))
multi_dex_cmd.add("--module", module)
multi_dex_cmd.add("--canary-class-name", module_to_canary_class_name_function(module))
if not is_root_module(module):
multi_dex_cmd.add("--module-deps", ctx.actions.write("module_deps_for_{}".format(module), apk_module_graph_info.module_to_module_deps_function(module)))
ctx.actions.run(multi_dex_cmd, category = "multi_dex_from_raw_dexes", identifier = "{}:{}_module_{}".format(ctx.label.package, ctx.label.name, module))
secondary_dexes_for_symlinking[_get_secondary_dex_subdir(module)] = secondary_dex_subdir
if metadata_dot_txt_files_by_module:
def write_metadata_dot_txts(ctx: "context", artifacts, outputs):
for voltron_module, metadata_dot_txt in metadata_dot_txt_files_by_module.items():
metadata_line_artifacts = metadata_line_artifacts_by_module[voltron_module]
expect(metadata_line_artifacts != None, "Should have metadata lines!")
metadata_lines = [".id {}".format(voltron_module)]
metadata_lines.extend([".requires {}".format(module_dep) for module_dep in apk_module_graph_info.module_to_module_deps_function(voltron_module)])
if split_dex_merge_config.dex_compression == "raw" and is_root_module(voltron_module):
metadata_lines.append(".root_relative")
for metadata_line_artifact in metadata_line_artifacts:
metadata_lines.append(artifacts[metadata_line_artifact].read_string().strip())
ctx.actions.write(outputs[metadata_dot_txt], metadata_lines)
ctx.actions.dynamic_output(dynamic = flatten(metadata_line_artifacts_by_module.values()), inputs = [], outputs = metadata_dot_txt_files_by_module.values(), f = write_metadata_dot_txts)
ctx.actions.symlinked_dir(
outputs[root_module_secondary_dexes_dir],
root_module_secondary_dexes_for_symlinking,
)
ctx.actions.symlinked_dir(
outputs[non_root_module_secondary_dexes_dir],
non_root_module_secondary_dexes_for_symlinking,
)
ctx.actions.dynamic_output(dynamic = input_artifacts, inputs = [], outputs = outputs, f = merge_pre_dexed_libs)
if is_exopackage_enabled_for_secondary_dex:
secondary_dex_dirs = [non_root_module_secondary_dexes_dir]
secondary_dex_exopackage_info = ExopackageDexInfo(
metadata = root_module_secondary_dexes_metadata,
directory = root_module_secondary_dexes_subdir,
)
else:
secondary_dex_dirs = [root_module_secondary_dexes_dir, non_root_module_secondary_dexes_dir]
secondary_dex_exopackage_info = None
return DexFilesInfo(
primary_dex = primary_dex_output,
secondary_dex_dirs = secondary_dex_dirs,
secondary_dex_exopackage_info = secondary_dex_exopackage_info,
proguard_text_files_path = None,
primary_dex_class_names = primary_dex_class_names_list,
)
def _merge_dexes(
ctx: "context",
android_toolchain: "AndroidToolchainInfo",
output_dex_file: "artifact",
pre_dexed_artifacts: ["artifact"],
pre_dexed_artifacts_file: "artifact",
class_names_to_include: ["artifact", None] = None,
secondary_output_dex_file: ["artifact", None] = None,
secondary_dex_metadata_config: [SecondaryDexMetadataConfig.type, None] = None):
d8_cmd = cmd_args(android_toolchain.d8_command[RunInfo])
d8_cmd.add(["--output-dex-file", output_dex_file.as_output()])
pre_dexed_artifacts_to_dex_file = ctx.actions.write(pre_dexed_artifacts_file.as_output(), pre_dexed_artifacts)
d8_cmd.add(["--files-to-dex-list", pre_dexed_artifacts_to_dex_file])
d8_cmd.hidden(pre_dexed_artifacts)
d8_cmd.add(["--android-jar", android_toolchain.android_jar])
d8_cmd.add(_DEX_MERGE_OPTIONS)
if class_names_to_include:
d8_cmd.add(["--primary-dex-class-names-path", class_names_to_include])
if secondary_output_dex_file:
d8_cmd.add(["--secondary-output-dex-file", secondary_output_dex_file.as_output()])
if secondary_dex_metadata_config:
d8_cmd.add(["--secondary-dex-compression", secondary_dex_metadata_config.secondary_dex_compression])
if secondary_dex_metadata_config.secondary_dex_metadata_file:
d8_cmd.add(["--secondary-dex-metadata-file", secondary_dex_metadata_config.secondary_dex_metadata_file.as_output()])
d8_cmd.add(["--secondary-dex-metadata-line", secondary_dex_metadata_config.secondary_dex_metadata_line.as_output()])
d8_cmd.add(["--secondary-dex-canary-class-name", secondary_dex_metadata_config.secondary_dex_canary_class_name])
ctx.actions.run(
d8_cmd,
category = "d8",
identifier = "{}:{} {}".format(ctx.label.package, ctx.label.name, output_dex_file.short_path),
)
def _sort_pre_dexed_files(
ctx: "context",
artifacts,
pre_dexed_lib_with_class_names_files: ["DexInputWithClassNamesFile"],
split_dex_merge_config: "SplitDexMergeConfig",
get_module_from_target: "function",
module_to_canary_class_name_function: "function") -> [_SortedPreDexedInputs.type]:
sorted_pre_dexed_inputs_map = {}
current_secondary_dex_size_map = {}
current_secondary_dex_inputs_map = {}
for pre_dexed_lib_with_class_names_file in pre_dexed_lib_with_class_names_files:
pre_dexed_lib = pre_dexed_lib_with_class_names_file.lib
module = get_module_from_target(str(pre_dexed_lib.dex.owner.raw_target()))
primary_dex_data, secondary_dex_data = artifacts[pre_dexed_lib_with_class_names_file.filtered_class_names_file].read_string().split(";")
primary_dex_class_names = primary_dex_data.split(",") if primary_dex_data else []
secondary_dex_class_names = secondary_dex_data.split(",") if secondary_dex_data else []
module_pre_dexed_inputs = sorted_pre_dexed_inputs_map.setdefault(module, _SortedPreDexedInputs(
module = module,
primary_dex_inputs = [],
secondary_dex_inputs = [],
))
primary_dex_inputs = module_pre_dexed_inputs.primary_dex_inputs
secondary_dex_inputs = module_pre_dexed_inputs.secondary_dex_inputs
if len(primary_dex_class_names) > 0:
expect(
is_root_module(module),
"Non-root modules should not have anything that belongs in the primary dex, " +
"but {} is assigned to module {} and has the following class names in the primary dex: {}\n".format(
pre_dexed_lib.dex.owner,
module,
"\n".join(primary_dex_class_names),
),
)
primary_dex_inputs.append(
DexInputWithSpecifiedClasses(lib = pre_dexed_lib, dex_class_names = primary_dex_class_names),
)
if len(secondary_dex_class_names) > 0:
weight_estimate = int(artifacts[pre_dexed_lib.weight_estimate].read_string().strip())
current_secondary_dex_size = current_secondary_dex_size_map.get(module, 0)
if current_secondary_dex_size + weight_estimate > split_dex_merge_config.secondary_dex_weight_limit_bytes:
current_secondary_dex_size = 0
current_secondary_dex_inputs_map[module] = []
current_secondary_dex_inputs = current_secondary_dex_inputs_map.setdefault(module, [])
if len(current_secondary_dex_inputs) == 0:
canary_class_dex_input = _create_canary_class(
ctx,
len(secondary_dex_inputs) + 1,
module,
module_to_canary_class_name_function,
ctx.attrs._dex_toolchain[DexToolchainInfo],
)
current_secondary_dex_inputs.append(canary_class_dex_input)
secondary_dex_inputs.append(current_secondary_dex_inputs)
current_secondary_dex_size_map[module] = current_secondary_dex_size + weight_estimate
current_secondary_dex_inputs.append(
DexInputWithSpecifiedClasses(lib = pre_dexed_lib, dex_class_names = secondary_dex_class_names),
)
return sorted_pre_dexed_inputs_map.values()
def _get_raw_secondary_dex_name(index: int.type, module: str.type) -> str.type:
# Root module begins at 2 (primary classes.dex is 1)
# Non-root module begins at 1 (classes.dex)
if is_root_module(module):
return "classes{}.dex".format(index + 2)
elif index == 0:
return "classes.dex".format(module)
else:
return "classes{}.dex".format(module, index + 1)
def _get_raw_secondary_dex_path(index: int.type, module: str.type):
if is_root_module(module):
return _get_raw_secondary_dex_name(index, module)
else:
return "assets/{}/{}".format(module, _get_raw_secondary_dex_name(index, module))
def _get_jar_secondary_dex_path(index: int.type, module: str.type):
return "{}/{}-{}.dex.jar".format(
_get_secondary_dex_subdir(module),
"secondary" if is_root_module(module) else module,
index + 1,
)
def _get_secondary_dex_subdir(module: str.type):
return "assets/{}".format("secondary-program-dex-jars" if is_root_module(module) else module)
# We create "canary" classes and add them to each secondary dex jar to ensure each jar has a class
# that can be safely loaded on any system. This class is used during secondary dex verification.
_CANARY_FULLY_QUALIFIED_CLASS_NAME_TEMPLATE = "{}.dex{}.Canary"
_CANARY_FILE_NAME_TEMPLATE = "canary_classes/{}/dex{}/Canary.java"
_CANARY_CLASS_PACKAGE_TEMPLATE = "package {}.dex{};\n"
_CANARY_CLASS_INTERFACE_DEFINITION = "public interface Canary {}"
def _create_canary_class(
ctx: "context",
index: int.type,
module: str.type,
module_to_canary_class_name_function: "function",
dex_toolchain: DexToolchainInfo.type) -> DexInputWithSpecifiedClasses.type:
prefix = module_to_canary_class_name_function(module)
canary_class_java_file = ctx.actions.write(_CANARY_FILE_NAME_TEMPLATE.format(prefix, index), [_CANARY_CLASS_PACKAGE_TEMPLATE.format(prefix, index), _CANARY_CLASS_INTERFACE_DEFINITION])
canary_class_jar = ctx.actions.declare_output("canary_classes/{}/canary_jar_{}.jar".format(prefix, index))
compile_to_jar(ctx, [canary_class_java_file], output = canary_class_jar, actions_prefix = "{}_canary_class{}".format(prefix, index))
dex_library_info = get_dex_produced_from_java_library(ctx, dex_toolchain = dex_toolchain, jar_to_dex = canary_class_jar)
return DexInputWithSpecifiedClasses(
lib = dex_library_info,
dex_class_names = [_get_fully_qualified_canary_class_name(module, module_to_canary_class_name_function, index).replace(".", "/") + ".class"],
)
def _get_fully_qualified_canary_class_name(module: str.type, module_to_canary_class_name_function: "function", index: int.type) -> str.type:
prefix = module_to_canary_class_name_function(module)
return _CANARY_FULLY_QUALIFIED_CLASS_NAME_TEMPLATE.format(prefix, index)
def _is_exopackage_enabled_for_secondary_dex(ctx: "context") -> bool.type:
return "secondary_dex" in getattr(ctx.attrs, "exopackage_modes", [])

View file

@ -0,0 +1,27 @@
# 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.
SECONDARY_DEX = 1
NATIVE_LIBRARY = 2
RESOURCES = 4
MODULES = 8
ARCH64 = 16
def get_exopackage_flags(exopackage_modes: [str.type]) -> int.type:
flags = 0
for (name, flag) in [
("secondary_dex", SECONDARY_DEX),
("native_library", NATIVE_LIBRARY),
("resources", RESOURCES),
("modules", MODULES),
("arch64", ARCH64),
]:
if name in exopackage_modes:
flags += flag
return flags

View file

@ -0,0 +1,57 @@
# 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//java:java_toolchain.bzl", "JavaToolchainInfo")
load(":android_toolchain.bzl", "AndroidToolchainInfo")
_AidlSourceInfo = provider(fields = [
"srcs",
])
def gen_aidl_impl(ctx: "context") -> ["provider"]:
android_toolchain = ctx.attrs._android_toolchain[AndroidToolchainInfo]
aidl_cmd = cmd_args(android_toolchain.aidl)
aidl_cmd.add("-p", android_toolchain.framework_aidl_file)
aidl_cmd.add("-I", ctx.attrs.import_path)
for path in ctx.attrs.import_paths:
aidl_cmd.add("-I", path)
# We need the `aidl_srcs` files - otherwise the search on the `import_path` won't find anything.
aidl_cmd.hidden(ctx.attrs.aidl_srcs)
# Allow gen_aidl rules to depend on other gen_aidl rules, and make the source files from the
# deps accessible in this context. This is an alternative to adding dependent files in
# aidl_srcs.
dep_srcs = []
for dep in ctx.attrs.deps:
source_info = dep.get(_AidlSourceInfo)
if source_info != None:
dep_srcs += source_info.srcs
else:
warning("`{}` dependency `{}` is not a `gen_aidl` rule and will be ignored".format(ctx.label, dep.label))
aidl_cmd.hidden(dep_srcs)
aidl_out = ctx.actions.declare_output("aidl_output")
aidl_cmd.add("-o", aidl_out.as_output())
aidl_cmd.add(ctx.attrs.aidl)
ctx.actions.run(aidl_cmd, category = "aidl")
# Put the generated Java files into a zip file to be used as srcs to other rules.
java_toolchain = ctx.attrs._java_toolchain[JavaToolchainInfo]
jar_cmd = cmd_args(java_toolchain.jar)
jar_cmd.add("-cfM")
out = ctx.actions.declare_output("{}_aidl_java_output.src.zip".format(ctx.attrs.name))
jar_cmd.add(out.as_output())
jar_cmd.add(aidl_out)
ctx.actions.run(jar_cmd, category = "aidl_jar")
return [
DefaultInfo(default_outputs = [out]),
_AidlSourceInfo(srcs = [ctx.attrs.aidl] + ctx.attrs.aidl_srcs + dep_srcs),
]

View file

@ -0,0 +1,15 @@
# 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.
_MIN_SDK_VERSION = 19
_MAX_SDK_VERSION = 33
def get_min_sdk_version_constraint_value_name(min_sdk: int.type) -> str.type:
return "min_sdk_version_{}".format(min_sdk)
def get_min_sdk_version_range() -> range.type:
return range(_MIN_SDK_VERSION, _MAX_SDK_VERSION)

View file

@ -0,0 +1,38 @@
# 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//android:android_providers.bzl",
"PrebuiltNativeLibraryDir",
"merge_android_packageable_info",
)
def prebuilt_native_library_impl(ctx: "context") -> ["provider"]:
if ctx.attrs.is_asset and ctx.attrs.has_wrap_script:
fail("Cannot use `is_asset` and `has_wrap_script` in the same rule")
prebuilt_native_library_dir = PrebuiltNativeLibraryDir(
raw_target = ctx.label.raw_target(),
dir = ctx.attrs.native_libs,
for_primary_apk = ctx.attrs.has_wrap_script,
is_asset = ctx.attrs.is_asset,
)
android_packageable_info = merge_android_packageable_info(
ctx.label,
ctx.actions,
ctx.attrs.deps,
prebuilt_native_library_dir = prebuilt_native_library_dir,
)
return [
# Buck1 copies the input directory and returns it as the output path. We don't
# copy; we could just return the input directory itself as the output path, but
# we're avoiding that (due to potential confusion from the output path being an
# input directory) until we have an actual need for prebuilt_native_library
# having an output path.
DefaultInfo(),
android_packageable_info,
]

View file

@ -0,0 +1,55 @@
# 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//android:android_toolchain.bzl", "AndroidToolchainInfo")
load("@prelude//java/utils:java_utils.bzl", "get_path_separator")
load("@prelude//utils:utils.bzl", "expect")
def get_preprocessed_java_classes(ctx: "context", input_jars = {"artifact": "target_label"}) -> {"artifact": "target_label"}:
sh_script, _ = ctx.actions.write(
"preprocessed_java_classes/script.sh",
cmd_args(ctx.attrs.preprocess_java_classes_bash),
is_executable = True,
allow_args = True,
)
preprocess_cmd = cmd_args(["/bin/bash", sh_script])
preprocess_cmd.hidden(cmd_args(ctx.attrs.preprocess_java_classes_bash))
for dep in ctx.attrs.preprocess_java_classes_deps:
preprocess_cmd.hidden(dep[DefaultInfo].default_outputs + dep[DefaultInfo].other_outputs)
input_srcs = {}
output_jars = {}
for i, (input_jar, target_label) in enumerate(input_jars.items()):
expect(input_jar.extension == ".jar", "Expected {} to have extension .jar!".format(input_jar))
jar_name = "{}_{}".format(i, input_jar.basename)
input_srcs[jar_name] = input_jar
output_jar = ctx.actions.declare_output(
"preprocessed_java_classes/output_dir/{}".format(jar_name),
)
output_jars[output_jar] = target_label
preprocess_cmd.hidden(output_jar.as_output())
if not output_jars:
return {}
input_dir = ctx.actions.symlinked_dir("preprocessed_java_classes/input_dir", input_srcs)
output_dir = cmd_args(output_jars.keys()[0].as_output()).parent()
env = {
"ANDROID_BOOTCLASSPATH": cmd_args(
ctx.attrs._android_toolchain[AndroidToolchainInfo].android_bootclasspath,
delimiter = get_path_separator(),
),
"IN_JARS_DIR": cmd_args(input_dir),
"OUT_JARS_DIR": output_dir,
}
ctx.actions.run(preprocess_cmd, env = env, category = "preprocess_java_classes")
return output_jars

View file

@ -0,0 +1,168 @@
# 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//android:android_toolchain.bzl", "AndroidToolchainInfo")
load("@prelude//java:java_toolchain.bzl", "JavaToolchainInfo")
load("@prelude//java/utils:java_utils.bzl", "get_path_separator")
load("@prelude//utils:utils.bzl", "expect")
_UNSCRUBBED_JARS_DIR = "unscrubbed"
ProguardOutput = record(
jars_to_owners = {"artifact": "target_label"},
proguard_configuration_output_file = ["artifact", None],
proguard_mapping_output_file = "artifact",
proguard_artifacts = ["artifact"],
)
def _get_proguard_command_line_args(
ctx: "context",
inputs_to_unscrubbed_outputs: {"artifact": "artifact"},
proguard_configs: ["artifact"],
mapping: "artifact",
configuration: ["artifact", None],
seeds: ["artifact", None],
usage: ["artifact", None],
android_toolchain: "AndroidToolchainInfo") -> "cmd_args":
cmd = cmd_args()
cmd.add("-basedirectory", "<user.dir>")
android_sdk_proguard_config = ctx.attrs.android_sdk_proguard_config or "none"
if android_sdk_proguard_config == "optimized":
cmd.add("-include", android_toolchain.optimized_proguard_config)
cmd.add("-optimizationpasses", str(ctx.attrs.optimization_passes))
elif android_sdk_proguard_config == "default":
cmd.add("-include", android_toolchain.proguard_config)
else:
expect(android_sdk_proguard_config == "none")
for proguard_config in dedupe(proguard_configs):
cmd.add("-include")
cmd.add(cmd_args("\"", proguard_config, "\"", delimiter = ""))
for jar_input, jar_output in inputs_to_unscrubbed_outputs.items():
cmd.add("-injar", jar_input, "-outjar", jar_output if jar_output == jar_input else jar_output.as_output())
cmd.add("-library")
cmd.add(cmd_args(android_toolchain.android_bootclasspath, delimiter = get_path_separator()))
cmd.add("-printmapping", mapping.as_output())
if configuration:
cmd.add("-printconfiguration", configuration.as_output())
if seeds:
cmd.add("-printseeds", seeds.as_output())
if usage:
cmd.add("-printusage", usage.as_output())
return cmd
def run_proguard(
ctx: "context",
android_toolchain: "AndroidToolchainInfo",
java_toolchain: "JavaToolchainInfo",
command_line_args_file: "artifact",
command_line_args: "cmd_args",
mapping_file: "artifact"):
run_proguard_cmd = cmd_args()
run_proguard_cmd.add(
java_toolchain.java[RunInfo],
"-XX:-MaxFDLimit",
ctx.attrs.proguard_jvm_args,
"-Xmx{}".format(android_toolchain.proguard_max_heap_size),
"-jar",
android_toolchain.proguard_jar,
)
run_proguard_cmd.add(cmd_args(command_line_args_file, format = "@{}"))
run_proguard_cmd.hidden(command_line_args)
# Some proguard configs can propagate the "-dontobfuscate" flag which disables
# obfuscation and prevents the mapping.txt file from being generated.
sh_cmd = cmd_args([
"sh",
"-c",
"touch $1 && $2",
"--",
mapping_file.as_output(),
cmd_args(run_proguard_cmd, delimiter = " "),
])
ctx.actions.run(sh_cmd, category = "run_proguard")
# Note that ctx.attrs.skip_proguard means that we should create the proguard command line (since
# e.g. Redex might want to consume it) but we don't actually run the proguard command.
def get_proguard_output(
ctx: "context",
input_jars: {"artifact": "target_label"},
java_packaging_deps: ["JavaPackagingDep"],
aapt_generated_proguard_config: ["artifact", None]) -> ProguardOutput.type:
proguard_configs = [packaging_dep.proguard_config for packaging_dep in java_packaging_deps if packaging_dep.proguard_config]
if ctx.attrs.proguard_config:
proguard_configs.append(ctx.attrs.proguard_config)
if not ctx.attrs.ignore_aapt_proguard_config and aapt_generated_proguard_config:
proguard_configs.append(aapt_generated_proguard_config)
if ctx.attrs.skip_proguard:
inputs_to_unscrubbed_outputs = {input_jar: input_jar for input_jar in input_jars.keys()}
mapping = ctx.actions.write("proguard/mapping.txt", [])
configuration = None
seeds = None
usage = None
else:
inputs_to_unscrubbed_outputs = {input_jar: ctx.actions.declare_output(
"proguard_output_jars/{}/{}_{}_obfuscated.jar".format(_UNSCRUBBED_JARS_DIR, input_jar.short_path, i),
) for i, input_jar in enumerate(input_jars.keys())}
mapping = ctx.actions.declare_output("proguard/mapping.txt")
configuration = ctx.actions.declare_output("proguard/configuration.txt")
seeds = ctx.actions.declare_output("proguard/seeds.txt")
usage = ctx.actions.declare_output("proguard/usage.txt")
command_line_args = _get_proguard_command_line_args(
ctx,
inputs_to_unscrubbed_outputs,
proguard_configs,
mapping,
configuration,
seeds,
usage,
ctx.attrs._android_toolchain[AndroidToolchainInfo],
)
command_line_args_file = ctx.actions.write("proguard/command-line.txt", command_line_args)
if ctx.attrs.skip_proguard:
return ProguardOutput(
jars_to_owners = input_jars,
proguard_configuration_output_file = None,
proguard_mapping_output_file = mapping,
proguard_artifacts = [command_line_args_file, mapping],
)
else:
unscrubbed_output_jars = {unscrubbed_output: input_jars[input_jar] for input_jar, unscrubbed_output in inputs_to_unscrubbed_outputs.items()}
run_proguard(
ctx,
ctx.attrs._android_toolchain[AndroidToolchainInfo],
ctx.attrs._java_toolchain[JavaToolchainInfo],
command_line_args_file,
command_line_args,
mapping,
)
output_jars = {}
for i, (unscrubbed_jar, target_label) in enumerate(unscrubbed_output_jars.items()):
output = ctx.actions.declare_output(unscrubbed_jar.short_path.replace("{}/".format(_UNSCRUBBED_JARS_DIR), ""))
ctx.actions.run(
cmd_args([ctx.attrs._java_toolchain[JavaToolchainInfo].zip_scrubber, unscrubbed_jar, output.as_output()]),
category = "scrub_jar",
identifier = str(i),
)
output_jars[output] = target_label
return ProguardOutput(
jars_to_owners = output_jars,
proguard_configuration_output_file = configuration,
proguard_mapping_output_file = mapping,
proguard_artifacts = [command_line_args_file, mapping, configuration, seeds, usage],
)

View file

@ -0,0 +1,229 @@
# 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//java:java_library.bzl", "compile_to_jar")
load("@prelude//java:java_providers.bzl", "JavaClasspathEntry", "JavaLibraryInfo", "derive_compiling_deps")
RDotJavaSourceCode = record(
r_dot_java_source_code_dir = "artifact",
r_dot_java_source_code_dir_listing = "artifact",
strings_source_code_dir = ["artifact", None],
strings_source_code_dir_listing = ["artifact", None],
ids_source_code_dir = ["artifact", None],
ids_source_code_dir_listing = ["artifact", None],
)
def get_dummy_r_dot_java(
ctx: "context",
merge_android_resources_tool: RunInfo.type,
java_toolchain: "JavaToolchainInfo",
android_resources: ["AndroidResourceInfo"],
union_package: [str.type, None]) -> "JavaLibraryInfo":
r_dot_java_source_code = _generate_r_dot_java_source_code(ctx, merge_android_resources_tool, android_resources, "dummy_r_dot_java", union_package = union_package)
library_output = _generate_and_compile_r_dot_java(
ctx,
r_dot_java_source_code.r_dot_java_source_code_dir,
r_dot_java_source_code.r_dot_java_source_code_dir_listing,
java_toolchain,
"dummy_r_dot_java",
)
return JavaLibraryInfo(
compiling_deps = derive_compiling_deps(ctx.actions, library_output, []),
library_output = library_output,
output_for_classpath_macro = library_output.full_library,
)
def generate_r_dot_javas(
ctx: "context",
merge_android_resources_tool: RunInfo.type,
java_toolchain: "JavaToolchainInfo",
android_resources: ["AndroidResourceInfo"],
banned_duplicate_resource_types: [str.type],
uber_r_dot_txt_files: ["artifact"],
override_symbols_paths: ["artifact"],
duplicate_resources_allowlist: ["artifact", None],
union_package: [str.type, None],
referenced_resources_lists: ["artifact"]) -> ["JavaLibraryInfo"]:
r_dot_java_source_code = _generate_r_dot_java_source_code(
ctx,
merge_android_resources_tool,
android_resources,
"r_dot_java",
generate_strings_and_ids_separately = True,
force_final_resources_ids = True,
banned_duplicate_resource_types = banned_duplicate_resource_types,
uber_r_dot_txt_files = uber_r_dot_txt_files,
override_symbols_paths = override_symbols_paths,
duplicate_resources_allowlist = duplicate_resources_allowlist,
union_package = union_package,
referenced_resources_lists = referenced_resources_lists,
)
main_library_output = _generate_and_compile_r_dot_java(
ctx,
r_dot_java_source_code.r_dot_java_source_code_dir,
r_dot_java_source_code.r_dot_java_source_code_dir_listing,
java_toolchain,
"main_r_dot_java",
)
strings_library_output = _generate_and_compile_r_dot_java(
ctx,
r_dot_java_source_code.strings_source_code_dir,
r_dot_java_source_code.strings_source_code_dir_listing,
java_toolchain,
"strings_r_dot_java",
remove_classes = [".R$"],
)
ids_library_output = _generate_and_compile_r_dot_java(
ctx,
r_dot_java_source_code.ids_source_code_dir,
r_dot_java_source_code.ids_source_code_dir_listing,
java_toolchain,
"ids_r_dot_java",
remove_classes = [".R$"],
)
return [JavaLibraryInfo(
compiling_deps = derive_compiling_deps(ctx.actions, library_output, []),
library_output = library_output,
output_for_classpath_macro = library_output.full_library,
) for library_output in [main_library_output, strings_library_output, ids_library_output]]
def _generate_r_dot_java_source_code(
ctx: "context",
merge_android_resources_tool: RunInfo.type,
android_resources: ["AndroidResourceInfo"],
identifier: str.type,
force_final_resources_ids = False,
generate_strings_and_ids_separately = False,
banned_duplicate_resource_types: [str.type] = [],
uber_r_dot_txt_files: ["artifact"] = [],
override_symbols_paths: ["artifact"] = [],
duplicate_resources_allowlist: ["artifact", None] = None,
union_package: [str.type, None] = None,
referenced_resources_lists: ["artifact"] = []) -> RDotJavaSourceCode.type:
merge_resources_cmd = cmd_args(merge_android_resources_tool)
r_dot_txt_info = cmd_args()
for android_resource in android_resources:
r_dot_txt_info.add(cmd_args([android_resource.text_symbols, android_resource.r_dot_java_package, "_"], delimiter = " ")) # pass target name
r_dot_txt_info_file = ctx.actions.write("r_dot_txt_info_file_for_{}.txt".format(identifier), r_dot_txt_info)
merge_resources_cmd.add(["--symbol-file-info", r_dot_txt_info_file])
merge_resources_cmd.hidden([android_resource.r_dot_java_package for android_resource in android_resources])
merge_resources_cmd.hidden([android_resource.text_symbols for android_resource in android_resources])
output_dir = ctx.actions.declare_output("{}_source_code".format(identifier))
merge_resources_cmd.add(["--output-dir", output_dir.as_output()])
output_dir_listing = ctx.actions.declare_output("{}_source_code_listing".format(identifier))
merge_resources_cmd.add(["--output-dir-listing", output_dir_listing.as_output()])
if generate_strings_and_ids_separately:
strings_output_dir = ctx.actions.declare_output("strings_source_code")
merge_resources_cmd.add(["--strings-output-dir", strings_output_dir.as_output()])
strings_output_dir_listing = ctx.actions.declare_output("strings_source_code_listing")
merge_resources_cmd.add(["--strings-output-dir-listing", strings_output_dir_listing.as_output()])
ids_output_dir = ctx.actions.declare_output("ids_source_code")
merge_resources_cmd.add(["--ids-output-dir", ids_output_dir.as_output()])
ids_output_dir_listing = ctx.actions.declare_output("ids_source_code_listing")
merge_resources_cmd.add(["--ids-output-dir-listing", ids_output_dir_listing.as_output()])
else:
strings_output_dir = None
strings_output_dir_listing = None
ids_output_dir = None
ids_output_dir_listing = None
if force_final_resources_ids:
merge_resources_cmd.add("--force-final-resource-ids")
if len(banned_duplicate_resource_types) > 0:
banned_duplicate_resource_types_file = ctx.actions.write("banned_duplicate_resource_types_file", banned_duplicate_resource_types)
merge_resources_cmd.add(["--banned-duplicate-resource-types", banned_duplicate_resource_types_file])
if len(uber_r_dot_txt_files) > 0:
uber_r_dot_txt_files_list = ctx.actions.write("uber_r_dot_txt_files_list", uber_r_dot_txt_files)
merge_resources_cmd.add(["--uber-r-dot-txt", uber_r_dot_txt_files_list])
merge_resources_cmd.hidden(uber_r_dot_txt_files)
if len(override_symbols_paths) > 0:
override_symbols_paths_list = ctx.actions.write("override_symbols_paths_list", override_symbols_paths)
merge_resources_cmd.add(["--override-symbols", override_symbols_paths_list])
merge_resources_cmd.hidden(override_symbols_paths)
if duplicate_resources_allowlist != None:
merge_resources_cmd.add(["--duplicate-resource-allowlist-path", duplicate_resources_allowlist])
if union_package != None:
merge_resources_cmd.add(["--union-package", union_package])
if referenced_resources_lists:
referenced_resources_file = ctx.actions.write("referenced_resources_lists", referenced_resources_lists)
merge_resources_cmd.add(["--referenced-resources-lists", referenced_resources_file])
merge_resources_cmd.hidden(referenced_resources_lists)
ctx.actions.run(merge_resources_cmd, category = "r_dot_java_merge_resources", identifier = identifier)
return RDotJavaSourceCode(
r_dot_java_source_code_dir = output_dir,
r_dot_java_source_code_dir_listing = output_dir_listing,
strings_source_code_dir = strings_output_dir,
strings_source_code_dir_listing = strings_output_dir_listing,
ids_source_code_dir = ids_output_dir,
ids_source_code_dir_listing = ids_output_dir_listing,
)
def _generate_and_compile_r_dot_java(
ctx: "context",
r_dot_java_source_code_dir: "artifact",
r_dot_java_src_listing: "artifact",
java_toolchain: "JavaToolchainInfo",
identifier: str.type,
remove_classes: [str.type] = []) -> JavaClasspathEntry.type:
r_dot_java_out = ctx.actions.declare_output("{}.jar".format(identifier))
# @lint-ignore-every BUILDIFIERLINT
def compile_r_dot_java_srcs(ctx, artifacts, outputs):
src_listing_string = artifacts[r_dot_java_src_listing].read_string()
src_listing = src_listing_string.split("\n")[:-1] if src_listing_string else []
r_dot_java_srcs = []
copied_root = ctx.actions.declare_output("copied_{}".format(identifier))
for path in src_listing:
r_dot_java_srcs.append(copied_root.project(path))
cmd = cmd_args([
java_toolchain.src_dir_helper[RunInfo],
"copy",
"--src-dir",
r_dot_java_source_code_dir,
"--dest-dir",
copied_root.as_output(),
] + src_listing)
ctx.actions.run(
cmd,
category = "copy_r_dot_java_sources",
identifier = identifier,
)
compile_to_jar(
ctx,
output = outputs[r_dot_java_out],
actions_prefix = identifier,
javac_tool = None,
srcs = r_dot_java_srcs,
remove_classes = remove_classes,
)
# Extracting an abi is unnecessary as there's not really anything to strip.
outputs = JavaClasspathEntry(
full_library = r_dot_java_out,
abi = r_dot_java_out,
required_for_source_only_abi = False,
)
todo_inputs = []
ctx.actions.dynamic_output(dynamic = [r_dot_java_src_listing], inputs = todo_inputs, outputs = [r_dot_java_out], f = compile_r_dot_java_srcs)
return outputs

View file

@ -0,0 +1,91 @@
# 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//android:android_binary_resources_rules.bzl", "get_android_binary_resources_info")
load("@prelude//android:android_library.bzl", "build_android_library")
load("@prelude//android:android_providers.bzl", "merge_android_packageable_info")
load("@prelude//android:android_toolchain.bzl", "AndroidToolchainInfo")
load("@prelude//java:java_test.bzl", "build_junit_test")
load("@prelude//java:java_toolchain.bzl", "JavaToolchainInfo")
load("@prelude//utils:utils.bzl", "expect")
load("@prelude//test/inject_test_run_info.bzl", "inject_test_run_info")
def robolectric_test_impl(ctx: "context") -> ["provider"]:
_verify_attributes(ctx)
extra_cmds = []
# Force robolectric to only use local dependency resolution.
extra_cmds.append("-Drobolectric.offline=true")
if ctx.attrs.robolectric_runtime_dependency:
runtime_dependencies_dir = ctx.attrs.robolectric_runtime_dependency
else:
runtime_dependencies_dir = ctx.actions.symlinked_dir("runtime_dependencies", {
runtime_dep.basename: runtime_dep
for runtime_dep in ctx.attrs.robolectric_runtime_dependencies
})
extra_cmds.append(cmd_args(runtime_dependencies_dir, format = "-Drobolectric.dependency.dir={}"))
all_packaging_deps = ctx.attrs.deps + (ctx.attrs.deps_query or []) + ctx.attrs.exported_deps + ctx.attrs.runtime_deps
android_packageable_info = merge_android_packageable_info(ctx.label, ctx.actions, all_packaging_deps)
resources_info = get_android_binary_resources_info(
ctx,
all_packaging_deps,
android_packageable_info,
java_packaging_deps = [], # Only used for third-party jar resources, which we don't care about here.
use_proto_format = False,
referenced_resources_lists = [],
)
test_config_properties_file = ctx.actions.write(
"test_config.properties",
[
cmd_args(["android_resource_apk", resources_info.primary_resources_apk], delimiter = "="),
cmd_args(["android_merged_manifest", resources_info.manifest], delimiter = "="),
],
)
# Robolectric looks for a file named /com/android/tools/test_config.properties on the classpath
test_config_symlinked_dir = ctx.actions.symlinked_dir("test_config_symlinked_dir", {"com/android/tools/test_config.properties": test_config_properties_file})
test_config_properties_jar = ctx.actions.declare_output("test_config_properties.jar")
jar_cmd = cmd_args([
ctx.attrs._java_toolchain[JavaToolchainInfo].jar,
"-cfM", # -c: create new archive, -f: specify the file name, -M: do not create a manifest
test_config_properties_jar.as_output(),
"-C",
test_config_symlinked_dir,
".",
])
ctx.actions.run(jar_cmd, category = "test_config_properties_jar_cmd")
extra_cmds.append(cmd_args().hidden(resources_info.primary_resources_apk, resources_info.manifest))
extra_classpath_entries = [test_config_properties_jar] + ctx.attrs._android_toolchain[AndroidToolchainInfo].android_bootclasspath
extra_classpath_entries.extend([r_dot_java.library_output.full_library for r_dot_java in resources_info.r_dot_javas if r_dot_java.library_output])
java_providers, _ = build_android_library(ctx)
external_runner_test_info = build_junit_test(
ctx,
java_providers.java_library_info,
java_providers.java_packaging_info,
extra_cmds = extra_cmds,
extra_classpath_entries = extra_classpath_entries,
)
return inject_test_run_info(ctx, external_runner_test_info) + [
java_providers.java_library_info,
java_providers.java_packaging_info,
java_providers.template_placeholder_info,
java_providers.default_info,
]
def _verify_attributes(ctx: "context"):
expect(
bool(ctx.attrs.robolectric_runtime_dependencies) != (ctx.attrs.robolectric_runtime_dependency != None),
"Exactly one of robolectric_runtime_dependencies and robolectric_runtime_dependency must be specified!",
)

View file

@ -0,0 +1,240 @@
# 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//android:android_providers.bzl", "AndroidPackageableInfo", "merge_android_packageable_info")
load("@prelude//android:android_toolchain.bzl", "AndroidToolchainInfo")
load("@prelude//java:java_providers.bzl", "get_all_java_packaging_deps")
load("@prelude//linking:shared_libraries.bzl", "SharedLibraryInfo", "merge_shared_libraries", "traverse_shared_library_info")
load("@prelude//utils:utils.bzl", "expect", "flatten")
# "Voltron" gives us the ability to split our Android APKs into different "modules". These
# modules can then be downloaded on demand rather than shipped with the "main" APK.
#
# The module corresponding to the "main" APK is called the "root" module.
#
# Voltron support comes in two main parts:
# (1) Constructing the Voltron module graph (assigning targets to each module). This is done
# by constructing a "target graph" and then delegating to buck1 to produce the module graph.
# (2) Using the Voltron module graph while building our APK.
#
# For (1), in order to calculate which targets belong to each module, we reconstruct a "target
# graph" from "deps" information that is propagated up through AndroidPackageableInfo.
# In buck1 we use the underlying "TargetGraph" object that is based on the raw target
# definitions. This results in some slightly different behavior for `provided_deps` - in
# buck2, we (correctly) ignore `provided_deps`, since they do not influence the packaging of
# the APK, whereas in `buck1`, we treat `provided_deps` the same as `deps`.
# In practice, this rarely affects the module assignments, but can mean that `buck2` will
# put a target inside a module whereas `buck1` will put it into the main APK (since `buck1`
# can find a path from an "always in main APK seed" to the target via some `provided_dep`,
# whereas `buck2` does not).
#
# For (2), we package up secondary DEX files and native libs into `assets/module_name` (see
# dex_rules.bzl and android_binary_native_rules.bzl for more information on how we do that).
# It is worth noting that we still put all of the non-root modules into the final APK. If
# the module should be downloaded on demand, then it is removed from the final APK in a
# subsequent post-processing step.
#
# There is also an `android_app_modularity` rule that just prints out details of the Voltron
# module graph and is used for any subsequent verification.
def android_app_modularity_impl(ctx: "context") -> ["provider"]:
all_deps = ctx.attrs.deps + flatten(ctx.attrs.application_module_configs.values())
android_packageable_info = merge_android_packageable_info(ctx.label, ctx.actions, all_deps)
shared_library_info = merge_shared_libraries(
ctx.actions,
deps = filter(None, [x.get(SharedLibraryInfo) for x in all_deps]),
)
traversed_shared_library_info = traverse_shared_library_info(shared_library_info)
cmd, output = _get_base_cmd_and_output(
ctx.actions,
ctx.label,
android_packageable_info,
traversed_shared_library_info,
ctx.attrs._android_toolchain[AndroidToolchainInfo],
ctx.attrs.application_module_configs,
ctx.attrs.application_module_dependencies,
ctx.attrs.application_module_blacklist,
)
if ctx.attrs.should_include_classes:
no_dx_target_labels = [no_dx_target.label.raw_target() for no_dx_target in ctx.attrs.no_dx]
java_packaging_deps = [packaging_dep for packaging_dep in get_all_java_packaging_deps(ctx, all_deps) if packaging_dep.dex and packaging_dep.dex.dex.owner.raw_target() not in no_dx_target_labels]
targets_to_jars_args = [cmd_args([str(packaging_dep.label.raw_target()), packaging_dep.jar], delimiter = " ") for packaging_dep in java_packaging_deps]
targets_to_jars = ctx.actions.write("targets_to_jars.txt", targets_to_jars_args)
cmd.add([
"--targets-to-jars",
targets_to_jars,
]).hidden(targets_to_jars_args)
if ctx.attrs.should_include_libraries:
targets_to_so_names_args = [cmd_args([str(shared_lib.label.raw_target()), so_name, str(shared_lib.can_be_asset)], delimiter = " ") for so_name, shared_lib in traversed_shared_library_info.items()]
targets_to_so_names = ctx.actions.write("targets_to_so_names.txt", targets_to_so_names_args)
cmd.add([
"--targets-to-so-names",
targets_to_so_names,
]).hidden(targets_to_so_names_args)
ctx.actions.run(cmd, category = "apk_module_graph")
return [DefaultInfo(default_outputs = [output])]
def get_target_to_module_mapping(ctx: "context", deps: ["dependency"]) -> ["artifact", None]:
if not ctx.attrs.application_module_configs:
return None
all_deps = deps + flatten(ctx.attrs.application_module_configs.values())
android_packageable_info = merge_android_packageable_info(ctx.label, ctx.actions, all_deps)
shared_library_info = merge_shared_libraries(
ctx.actions,
deps = filter(None, [x.get(SharedLibraryInfo) for x in all_deps]),
)
traversed_shared_library_info = traverse_shared_library_info(shared_library_info)
cmd, output = _get_base_cmd_and_output(
ctx.actions,
ctx.label,
android_packageable_info,
traversed_shared_library_info,
ctx.attrs._android_toolchain[AndroidToolchainInfo],
ctx.attrs.application_module_configs,
ctx.attrs.application_module_dependencies,
ctx.attrs.application_module_blacklist,
)
cmd.add("--output-module-info-and-target-to-module-only")
ctx.actions.run(cmd, category = "apk_module_graph")
return output
def _get_base_cmd_and_output(
actions: "actions",
label: "label",
android_packageable_info: "AndroidPackageableInfo",
traversed_shared_library_info: {str.type: "SharedLibrary"},
android_toolchain: "AndroidToolchainInfo",
application_module_configs: {str.type: ["dependency"]},
application_module_dependencies: [{str.type: [str.type]}, None],
application_module_blocklist: [[["dependency"]], None]) -> ("cmd_args", "artifact"):
deps_infos = list(android_packageable_info.deps.traverse()) if android_packageable_info.deps else []
deps_map = {deps_info.name: deps_info.deps for deps_info in deps_infos}
target_graph_file = actions.write_json("target_graph.json", deps_map)
application_module_configs_map = {
module_name: [seed.label.raw_target() for seed in seeds if seed.get(AndroidPackageableInfo)]
for module_name, seeds in application_module_configs.items()
}
application_module_configs_file = actions.write_json("application_module_configs.json", application_module_configs_map)
application_module_dependencies_file = actions.write_json("application_module_dependencies.json", application_module_dependencies or {})
output = actions.declare_output("apk_module_metadata.txt")
cmd = cmd_args([
android_toolchain.apk_module_graph[RunInfo],
"--root-target",
str(label.raw_target()),
"--target-graph",
target_graph_file,
"--seed-config-map",
application_module_configs_file,
"--app-module-dependencies-map",
application_module_dependencies_file,
"--output",
output.as_output(),
])
# Anything that is used by a wrap script needs to go into the primary APK, as do all
# of their deps.
used_by_wrap_script_libs = [str(shared_lib.label.raw_target()) for shared_lib in traversed_shared_library_info.values() if shared_lib.for_primary_apk]
prebuilt_native_library_dirs = list(android_packageable_info.prebuilt_native_library_dirs.traverse()) if android_packageable_info.prebuilt_native_library_dirs else []
prebuilt_native_library_targets_for_primary_apk = [str(native_lib_dir.raw_target) for native_lib_dir in prebuilt_native_library_dirs if native_lib_dir.for_primary_apk]
if application_module_blocklist or used_by_wrap_script_libs or prebuilt_native_library_targets_for_primary_apk:
all_blocklisted_deps = used_by_wrap_script_libs + prebuilt_native_library_targets_for_primary_apk
if application_module_blocklist:
all_blocklisted_deps.extend([str(blocklisted_dep.label.raw_target()) for blocklisted_dep in flatten(application_module_blocklist)])
application_module_blocklist_file = actions.write(
"application_module_blocklist.txt",
all_blocklisted_deps,
)
cmd.add([
"--always-in-main-apk-seeds",
application_module_blocklist_file,
])
return cmd, output
ROOT_MODULE = "dex"
def is_root_module(module: str.type) -> bool.type:
return module == ROOT_MODULE
def all_targets_in_root_module(_module: str.type) -> str.type:
return ROOT_MODULE
APKModuleGraphInfo = record(
target_to_module_mapping_function = "function",
module_to_canary_class_name_function = "function",
module_to_module_deps_function = "function",
)
def get_root_module_only_apk_module_graph_info() -> APKModuleGraphInfo.type:
def root_module_canary_class_name(module: str.type):
expect(is_root_module(module))
return "secondary"
def root_module_deps(module: str.type):
expect(is_root_module(module))
return []
return APKModuleGraphInfo(
target_to_module_mapping_function = all_targets_in_root_module,
module_to_canary_class_name_function = root_module_canary_class_name,
module_to_module_deps_function = root_module_deps,
)
def get_apk_module_graph_info(
ctx: "context",
apk_module_graph_file: "artifact",
artifacts) -> APKModuleGraphInfo.type:
apk_module_graph_lines = artifacts[apk_module_graph_file].read_string().split("\n")
module_count = int(apk_module_graph_lines[0])
module_infos = apk_module_graph_lines[1:module_count + 1]
target_to_module_lines = apk_module_graph_lines[module_count + 1:-1]
expect(apk_module_graph_lines[-1] == "", "Expect last line to be an empty string!")
module_to_canary_class_name_map = {}
module_to_module_deps_map = {}
for line in module_infos:
line_data = line.split(" ")
module_name = line_data[0]
canary_class_name = line_data[1]
module_deps = [module_dep for module_dep in line_data[2:] if module_dep]
module_to_canary_class_name_map[module_name] = canary_class_name
module_to_module_deps_map[module_name] = module_deps
target_to_module_mapping = {str(ctx.label.raw_target()): ROOT_MODULE}
for line in target_to_module_lines:
target, module = line.split(" ")
target_to_module_mapping[target] = module
def target_to_module_mapping_function(raw_target: str.type) -> str.type:
return target_to_module_mapping.get(raw_target)
def module_to_canary_class_name_function(voltron_module: str.type) -> str.type:
return module_to_canary_class_name_map.get(voltron_module)
def module_to_module_deps_function(voltron_module: str.type) -> list.type:
return module_to_module_deps_map.get(voltron_module)
return APKModuleGraphInfo(
target_to_module_mapping_function = target_to_module_mapping_function,
module_to_canary_class_name_function = module_to_canary_class_name_function,
module_to_module_deps_function = module_to_module_deps_function,
)