# 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.", ), ) def _split_config_aspect_impl(__, ctx): return SplitConfigInfo(build_config = ctx.configuration) split_config_aspect = aspect( implementation = _split_config_aspect_impl, ) def process(ctx, filename): """ 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 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 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 = [] 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(native_deps_lib, cc_info)) if libraries: libs[key] = 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 dep in ctx.attr.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_lib, cc_info): basenames = {} artifacts = {} if linked_lib: basenames[linked_lib.basename] = linked_lib 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 + "': " + artifact + " and " + old_artifact + ( " (the library compiled for this target)" if old_artifact == linked_lib 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 _get_build_info(ctx): return cc_common.get_build_info(ctx) 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 for input in _all_inputs(cc_info): 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, ) link_opts = cc_info.linking_context.user_link_flags linkstamps = [] for input in cc_info.linking_context.linker_inputs.to_list(): linkstamps.extend(input.linkstamps) linkstamps_dict = {linkstamp: None for linkstamp in linkstamps} build_info_artifacts = _get_build_info(ctx) 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(cc_info.linking_context.libraries_to_link) 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 = ctx.attr.stamp, grep_includes = ctx.file._grep_includes, 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