| # 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 = getattr(ctx.attr, "stamp", 0), | |
| 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 |