# 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