| # Copyright 2022 The Bazel Authors. All rights reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """ |
| Defines the native libs processing and an aspect to collect build configuration |
| of split deps |
| """ |
| |
| load("//rules:common.bzl", "common") |
| |
| SplitConfigInfo = provider( |
| doc = "Provides information about configuration for a split config dep", |
| fields = dict( |
| build_config = "The build configuration of the dep.", |
| android_config = "Select fields from the android configuration of the dep.", |
| target_platform = "The target platform label of the dep.", |
| ), |
| ) |
| |
| def _split_config_aspect_impl(__, ctx): |
| android_cfg = ctx.fragments.android |
| return SplitConfigInfo( |
| build_config = ctx.configuration, |
| android_config = struct( |
| incompatible_use_toolchain_resolution = android_cfg.incompatible_use_toolchain_resolution, |
| android_cpu = android_cfg.android_cpu, |
| hwasan = android_cfg.hwasan, |
| ), |
| target_platform = ctx.fragments.platform.platform, |
| ) |
| |
| split_config_aspect = aspect( |
| implementation = _split_config_aspect_impl, |
| fragments = ["android"], |
| ) |
| |
| def _get_libs_dir_name(android_config, target_platform): |
| if android_config.incompatible_use_toolchain_resolution: |
| name = target_platform.name |
| else: |
| # Legacy builds use the CPU as the name. |
| name = android_config.android_cpu |
| if android_config.hwasan: |
| name = name + "-hwasan" |
| return name |
| |
| def process(ctx, filename, merged_native_libs = {}): |
| """ Links native deps into a shared library |
| |
| Args: |
| ctx: The context. |
| filename: String. The name of the artifact containing the name of the |
| linked shared library |
| merged_native_libs: A dict that maps cpu to merged native libraries. This maps to empty |
| lists if native library merging is not enabled. |
| |
| Returns: |
| Tuple of (libs, libs_name) where libs is a depset of all native deps |
| and libs_name is a File containing the basename of the linked shared |
| library |
| """ |
| actual_target_name = ctx.label.name.removesuffix(common.PACKAGED_RESOURCES_SUFFIX) |
| native_libs_basename = None |
| libs_name = None |
| libs = dict() |
| for key, deps in ctx.split_attr.deps.items(): |
| cc_toolchain_dep = ctx.split_attr._cc_toolchain_split[key] |
| cc_toolchain = cc_toolchain_dep[cc_common.CcToolchainInfo] |
| build_config = cc_toolchain_dep[SplitConfigInfo].build_config |
| libs_dir_name = _get_libs_dir_name( |
| cc_toolchain_dep[SplitConfigInfo].android_config, |
| cc_toolchain_dep[SplitConfigInfo].target_platform, |
| ) |
| linker_input = cc_common.create_linker_input( |
| owner = ctx.label, |
| user_link_flags = ["-Wl,-soname=lib" + actual_target_name], |
| ) |
| cc_info = cc_common.merge_cc_infos( |
| cc_infos = _concat( |
| [CcInfo(linking_context = cc_common.create_linking_context( |
| linker_inputs = depset([linker_input]), |
| ))], |
| [dep[JavaInfo].cc_link_params_info for dep in deps if JavaInfo in dep], |
| [dep[AndroidCcLinkParamsInfo].link_params for dep in deps if AndroidCcLinkParamsInfo in dep], |
| [dep[CcInfo] for dep in deps if CcInfo in dep], |
| ), |
| ) |
| libraries = [] |
| if merged_native_libs: |
| libraries.extend(merged_native_libs[key]) |
| |
| native_deps_lib = _link_native_deps_if_present(ctx, cc_info, cc_toolchain, build_config, actual_target_name) |
| if native_deps_lib: |
| libraries.append(native_deps_lib) |
| native_libs_basename = native_deps_lib.basename |
| |
| libraries.extend(_filter_unique_shared_libs(libraries, cc_info)) |
| |
| if libraries: |
| libs[libs_dir_name] = depset(libraries) |
| |
| if libs and native_libs_basename: |
| libs_name = ctx.actions.declare_file("nativedeps_filename/" + actual_target_name + "/" + filename) |
| ctx.actions.write(output = libs_name, content = native_libs_basename) |
| |
| transitive_native_libs = _get_transitive_native_libs(ctx) |
| return AndroidBinaryNativeLibsInfo(libs, libs_name, transitive_native_libs) |
| |
| # Collect all native shared libraries across split transitions. Some AARs |
| # contain shared libraries across multiple architectures, e.g. x86 and |
| # armeabi-v7a, and need to be packed into the APK. |
| def _get_transitive_native_libs(ctx): |
| return depset( |
| transitive = [ |
| dep[AndroidNativeLibsInfo].native_libs |
| for deps in ctx.split_attr.deps.values() |
| for dep in deps |
| if AndroidNativeLibsInfo in dep |
| ], |
| ) |
| |
| def _all_inputs(cc_info): |
| return [ |
| lib |
| for input in cc_info.linking_context.linker_inputs.to_list() |
| for lib in input.libraries |
| ] |
| |
| def _filter_unique_shared_libs(linked_libs, cc_info): |
| basenames = {} |
| artifacts = {} |
| if linked_libs: |
| basenames = { |
| linked_lib.basename: linked_lib |
| for linked_lib in linked_libs |
| } |
| artifacts = { |
| linked_lib: None |
| for linked_lib in linked_libs |
| } |
| for input in _all_inputs(cc_info): |
| if input.pic_static_library or input.static_library: |
| # This is not a shared library and will not be loaded by Android, so skip it. |
| continue |
| |
| artifact = None |
| if input.interface_library: |
| if input.resolved_symlink_interface_library: |
| artifact = input.resolved_symlink_interface_library |
| else: |
| artifact = input.interface_library |
| elif input.resolved_symlink_dynamic_library: |
| artifact = input.resolved_symlink_dynamic_library |
| else: |
| artifact = input.dynamic_library |
| |
| if not artifact: |
| fail("Should never happen: did not find artifact for link!") |
| |
| if artifact in artifacts: |
| # We have already reached this library, e.g., through a different solib symlink. |
| continue |
| artifacts[artifact] = None |
| basename = artifact.basename |
| if basename in basenames: |
| old_artifact = basenames[basename] |
| fail( |
| "Each library in the transitive closure must have a " + |
| "unique basename to avoid name collisions when packaged into " + |
| "an apk, but two libraries have the basename '" + basename + |
| "': " + str(artifact) + " and " + str(old_artifact) + ( |
| " (the library already seen by this target)" if old_artifact in linked_libs else "" |
| ), |
| ) |
| else: |
| basenames[basename] = artifact |
| |
| return artifacts.keys() |
| |
| def _contains_code_to_link(input): |
| if not input.static_library and not input.pic_static_library: |
| # this is a shared library so we're going to have to copy it |
| return False |
| if input.objects: |
| object_files = input.objects |
| elif input.pic_objects: |
| object_files = input.pic_objects |
| elif _is_any_source_file(input.static_library, input.pic_static_library): |
| # this is an opaque library so we're going to have to link it |
| return True |
| else: |
| # if we reach here, this is a cc_library without sources generating an |
| # empty archive which does not need to be linked |
| # TODO(hvd): replace all such cc_library with exporting_cc_library |
| return False |
| for obj in object_files: |
| if not _is_shared_library(obj): |
| # this library was built with a non-shared-library object so we should link it |
| return True |
| return False |
| |
| def _is_any_source_file(*files): |
| for file in files: |
| if file and file.is_source: |
| return True |
| return False |
| |
| def _is_shared_library(lib_artifact): |
| if (lib_artifact.extension in ["so", "dll", "dylib"]): |
| return True |
| |
| lib_name = lib_artifact.basename |
| |
| # validate against the regex "^.+\\.((so)|(dylib))(\\.\\d\\w*)+$", |
| # must match VERSIONED_SHARED_LIBRARY. |
| for ext in (".so.", ".dylib."): |
| name, _, version = lib_name.rpartition(ext) |
| if name and version: |
| version_parts = version.split(".") |
| for part in version_parts: |
| if not part[0].isdigit(): |
| return False |
| for c in part[1:].elems(): |
| if not (c.isalnum() or c == "_"): |
| return False |
| return True |
| return False |
| |
| def _is_stamping_enabled(ctx): |
| if ctx.configuration.is_tool_configuration(): |
| return 0 |
| return getattr(ctx.attr, "stamp", 0) |
| |
| def _get_build_info(ctx, cc_toolchain): |
| if _is_stamping_enabled(ctx): |
| return cc_toolchain.build_info_files().non_redacted_build_info_files.to_list() |
| else: |
| return cc_toolchain.build_info_files().redacted_build_info_files.to_list() |
| |
| def _get_shared_native_deps_path( |
| linker_inputs, |
| link_opts, |
| linkstamps, |
| build_info_artifacts, |
| features, |
| is_test_target_partially_disabled_thin_lto): |
| fp = [] |
| for artifact in linker_inputs: |
| fp.append(artifact.short_path) |
| fp.append(str(len(link_opts))) |
| for opt in link_opts: |
| fp.append(opt) |
| for artifact in linkstamps: |
| fp.append(artifact.short_path) |
| for artifact in build_info_artifacts: |
| fp.append(artifact.short_path) |
| for feature in features: |
| fp.append(feature) |
| |
| fp.append("1" if is_test_target_partially_disabled_thin_lto else "0") |
| |
| fingerprint = "%x" % hash("".join(fp)) |
| return "_nativedeps/" + fingerprint |
| |
| def _get_static_mode_params_for_dynamic_library_libraries(libs): |
| linker_inputs = [] |
| for lib in libs: |
| if lib.pic_static_library: |
| linker_inputs.append(lib.pic_static_library) |
| elif lib.static_library: |
| linker_inputs.append(lib.static_library) |
| elif lib.interface_library: |
| linker_inputs.append(lib.interface_library) |
| else: |
| linker_inputs.append(lib.dynamic_library) |
| return linker_inputs |
| |
| def _link_native_deps_if_present(ctx, cc_info, cc_toolchain, build_config, actual_target_name, is_test_rule_class = False): |
| needs_linking = False |
| all_inputs = _all_inputs(cc_info) |
| for input in all_inputs: |
| needs_linking = needs_linking or _contains_code_to_link(input) |
| |
| if not needs_linking: |
| return None |
| |
| # This does not need to be shareable, but we use this API to specify the |
| # custom file root (matching the configuration) |
| output_lib = ctx.actions.declare_shareable_artifact( |
| ctx.label.package + "/nativedeps/" + actual_target_name + "/lib" + actual_target_name + ".so", |
| build_config.bin_dir, |
| ) |
| |
| linker_inputs = cc_info.linking_context.linker_inputs.to_list() |
| |
| link_opts = [] |
| for linker_input in linker_inputs: |
| for flag in linker_input.user_link_flags: |
| link_opts.append(flag) |
| |
| linkstamps = [] |
| for linker_input in linker_inputs: |
| linkstamps.extend(linker_input.linkstamps) |
| linkstamps_dict = {linkstamp: None for linkstamp in linkstamps} |
| |
| build_info_artifacts = _get_build_info(ctx, cc_toolchain) if linkstamps_dict else [] |
| requested_features = ["static_linking_mode", "native_deps_link"] |
| requested_features.extend(ctx.features) |
| if not "legacy_whole_archive" in ctx.disabled_features: |
| requested_features.append("legacy_whole_archive") |
| requested_features = sorted(requested_features) |
| feature_config = cc_common.configure_features( |
| ctx = ctx, |
| cc_toolchain = cc_toolchain, |
| requested_features = requested_features, |
| unsupported_features = ctx.disabled_features, |
| ) |
| partially_disabled_thin_lto = ( |
| cc_common.is_enabled( |
| feature_name = "thin_lto_linkstatic_tests_use_shared_nonlto_backends", |
| feature_configuration = feature_config, |
| ) and not cc_common.is_enabled( |
| feature_name = "thin_lto_all_linkstatic_use_shared_nonlto_backends", |
| feature_configuration = feature_config, |
| ) |
| ) |
| test_only_target = ctx.attr.testonly or is_test_rule_class |
| share_native_deps = ctx.fragments.cpp.share_native_deps() |
| |
| linker_inputs = _get_static_mode_params_for_dynamic_library_libraries(all_inputs) |
| |
| if share_native_deps: |
| shared_path = _get_shared_native_deps_path( |
| linker_inputs, |
| link_opts, |
| [linkstamp.file() for linkstamp in linkstamps_dict], |
| build_info_artifacts, |
| requested_features, |
| test_only_target and partially_disabled_thin_lto, |
| ) |
| linked_lib = ctx.actions.declare_shareable_artifact(shared_path + ".so", build_config.bin_dir) |
| else: |
| linked_lib = output_lib |
| |
| cc_common.link( |
| name = ctx.label.name, |
| actions = ctx.actions, |
| linking_contexts = [cc_info.linking_context], |
| output_type = "dynamic_library", |
| never_link = True, |
| native_deps = True, |
| feature_configuration = feature_config, |
| cc_toolchain = cc_toolchain, |
| test_only_target = test_only_target, |
| stamp = getattr(ctx.attr, "stamp", 0), |
| main_output = linked_lib, |
| use_shareable_artifact_factory = True, |
| build_config = build_config, |
| ) |
| |
| if (share_native_deps): |
| ctx.actions.symlink( |
| output = output_lib, |
| target_file = linked_lib, |
| ) |
| return output_lib |
| else: |
| return linked_lib |
| |
| def _concat(*list_of_lists): |
| res = [] |
| for list in list_of_lists: |
| res.extend(list) |
| return res |