1# Copyright 2022 The Bazel Authors. All rights reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15""" 16Defines the native libs processing and an aspect to collect build configuration 17of split deps 18""" 19 20load("//rules:common.bzl", "common") 21 22SplitConfigInfo = provider( 23 doc = "Provides information about configuration for a split config dep", 24 fields = dict( 25 build_config = "The build configuration of the dep.", 26 android_config = "Select fields from the android configuration of the dep.", 27 target_platform = "The target platform label of the dep.", 28 ), 29) 30 31def _split_config_aspect_impl(__, ctx): 32 android_cfg = ctx.fragments.android 33 return SplitConfigInfo( 34 build_config = ctx.configuration, 35 android_config = struct( 36 incompatible_use_toolchain_resolution = android_cfg.incompatible_use_toolchain_resolution, 37 android_cpu = android_cfg.android_cpu, 38 hwasan = android_cfg.hwasan, 39 ), 40 target_platform = ctx.fragments.platform.platform, 41 ) 42 43split_config_aspect = aspect( 44 implementation = _split_config_aspect_impl, 45 fragments = ["android"], 46) 47 48def _get_libs_dir_name(android_config, target_platform): 49 if android_config.incompatible_use_toolchain_resolution: 50 name = target_platform.name 51 else: 52 # Legacy builds use the CPU as the name. 53 name = android_config.android_cpu 54 if android_config.hwasan: 55 name = name + "-hwasan" 56 return name 57 58def process(ctx, filename, merged_native_libs = {}): 59 """ Links native deps into a shared library 60 61 Args: 62 ctx: The context. 63 filename: String. The name of the artifact containing the name of the 64 linked shared library 65 merged_native_libs: A dict that maps cpu to merged native libraries. This maps to empty 66 lists if native library merging is not enabled. 67 68 Returns: 69 Tuple of (libs, libs_name) where libs is a depset of all native deps 70 and libs_name is a File containing the basename of the linked shared 71 library 72 """ 73 actual_target_name = ctx.label.name.removesuffix(common.PACKAGED_RESOURCES_SUFFIX) 74 native_libs_basename = None 75 libs_name = None 76 libs = dict() 77 for key, deps in ctx.split_attr.deps.items(): 78 cc_toolchain_dep = ctx.split_attr._cc_toolchain_split[key] 79 cc_toolchain = cc_toolchain_dep[cc_common.CcToolchainInfo] 80 build_config = cc_toolchain_dep[SplitConfigInfo].build_config 81 libs_dir_name = _get_libs_dir_name( 82 cc_toolchain_dep[SplitConfigInfo].android_config, 83 cc_toolchain_dep[SplitConfigInfo].target_platform, 84 ) 85 linker_input = cc_common.create_linker_input( 86 owner = ctx.label, 87 user_link_flags = ["-Wl,-soname=lib" + actual_target_name], 88 ) 89 cc_info = cc_common.merge_cc_infos( 90 cc_infos = _concat( 91 [CcInfo(linking_context = cc_common.create_linking_context( 92 linker_inputs = depset([linker_input]), 93 ))], 94 [dep[JavaInfo].cc_link_params_info for dep in deps if JavaInfo in dep], 95 [dep[AndroidCcLinkParamsInfo].link_params for dep in deps if AndroidCcLinkParamsInfo in dep], 96 [dep[CcInfo] for dep in deps if CcInfo in dep], 97 ), 98 ) 99 libraries = [] 100 if merged_native_libs: 101 libraries.extend(merged_native_libs[key]) 102 103 native_deps_lib = _link_native_deps_if_present(ctx, cc_info, cc_toolchain, build_config, actual_target_name) 104 if native_deps_lib: 105 libraries.append(native_deps_lib) 106 native_libs_basename = native_deps_lib.basename 107 108 libraries.extend(_filter_unique_shared_libs(libraries, cc_info)) 109 110 if libraries: 111 libs[libs_dir_name] = depset(libraries) 112 113 if libs and native_libs_basename: 114 libs_name = ctx.actions.declare_file("nativedeps_filename/" + actual_target_name + "/" + filename) 115 ctx.actions.write(output = libs_name, content = native_libs_basename) 116 117 transitive_native_libs = _get_transitive_native_libs(ctx) 118 return AndroidBinaryNativeLibsInfo(libs, libs_name, transitive_native_libs) 119 120# Collect all native shared libraries across split transitions. Some AARs 121# contain shared libraries across multiple architectures, e.g. x86 and 122# armeabi-v7a, and need to be packed into the APK. 123def _get_transitive_native_libs(ctx): 124 return depset( 125 transitive = [ 126 dep[AndroidNativeLibsInfo].native_libs 127 for deps in ctx.split_attr.deps.values() 128 for dep in deps 129 if AndroidNativeLibsInfo in dep 130 ], 131 ) 132 133def _all_inputs(cc_info): 134 return [ 135 lib 136 for input in cc_info.linking_context.linker_inputs.to_list() 137 for lib in input.libraries 138 ] 139 140def _filter_unique_shared_libs(linked_libs, cc_info): 141 basenames = {} 142 artifacts = {} 143 if linked_libs: 144 basenames = { 145 linked_lib.basename: linked_lib 146 for linked_lib in linked_libs 147 } 148 artifacts = { 149 linked_lib: None 150 for linked_lib in linked_libs 151 } 152 for input in _all_inputs(cc_info): 153 if input.pic_static_library or input.static_library: 154 # This is not a shared library and will not be loaded by Android, so skip it. 155 continue 156 157 artifact = None 158 if input.interface_library: 159 if input.resolved_symlink_interface_library: 160 artifact = input.resolved_symlink_interface_library 161 else: 162 artifact = input.interface_library 163 elif input.resolved_symlink_dynamic_library: 164 artifact = input.resolved_symlink_dynamic_library 165 else: 166 artifact = input.dynamic_library 167 168 if not artifact: 169 fail("Should never happen: did not find artifact for link!") 170 171 if artifact in artifacts: 172 # We have already reached this library, e.g., through a different solib symlink. 173 continue 174 artifacts[artifact] = None 175 basename = artifact.basename 176 if basename in basenames: 177 old_artifact = basenames[basename] 178 fail( 179 "Each library in the transitive closure must have a " + 180 "unique basename to avoid name collisions when packaged into " + 181 "an apk, but two libraries have the basename '" + basename + 182 "': " + str(artifact) + " and " + str(old_artifact) + ( 183 " (the library already seen by this target)" if old_artifact in linked_libs else "" 184 ), 185 ) 186 else: 187 basenames[basename] = artifact 188 189 return artifacts.keys() 190 191def _contains_code_to_link(input): 192 if not input.static_library and not input.pic_static_library: 193 # this is a shared library so we're going to have to copy it 194 return False 195 if input.objects: 196 object_files = input.objects 197 elif input.pic_objects: 198 object_files = input.pic_objects 199 elif _is_any_source_file(input.static_library, input.pic_static_library): 200 # this is an opaque library so we're going to have to link it 201 return True 202 else: 203 # if we reach here, this is a cc_library without sources generating an 204 # empty archive which does not need to be linked 205 # TODO(hvd): replace all such cc_library with exporting_cc_library 206 return False 207 for obj in object_files: 208 if not _is_shared_library(obj): 209 # this library was built with a non-shared-library object so we should link it 210 return True 211 return False 212 213def _is_any_source_file(*files): 214 for file in files: 215 if file and file.is_source: 216 return True 217 return False 218 219def _is_shared_library(lib_artifact): 220 if (lib_artifact.extension in ["so", "dll", "dylib"]): 221 return True 222 223 lib_name = lib_artifact.basename 224 225 # validate against the regex "^.+\\.((so)|(dylib))(\\.\\d\\w*)+$", 226 # must match VERSIONED_SHARED_LIBRARY. 227 for ext in (".so.", ".dylib."): 228 name, _, version = lib_name.rpartition(ext) 229 if name and version: 230 version_parts = version.split(".") 231 for part in version_parts: 232 if not part[0].isdigit(): 233 return False 234 for c in part[1:].elems(): 235 if not (c.isalnum() or c == "_"): 236 return False 237 return True 238 return False 239 240def _is_stamping_enabled(ctx): 241 if ctx.configuration.is_tool_configuration(): 242 return 0 243 return getattr(ctx.attr, "stamp", 0) 244 245def _get_build_info(ctx, cc_toolchain): 246 if _is_stamping_enabled(ctx): 247 return cc_toolchain.build_info_files().non_redacted_build_info_files.to_list() 248 else: 249 return cc_toolchain.build_info_files().redacted_build_info_files.to_list() 250 251def _get_shared_native_deps_path( 252 linker_inputs, 253 link_opts, 254 linkstamps, 255 build_info_artifacts, 256 features, 257 is_test_target_partially_disabled_thin_lto): 258 fp = [] 259 for artifact in linker_inputs: 260 fp.append(artifact.short_path) 261 fp.append(str(len(link_opts))) 262 for opt in link_opts: 263 fp.append(opt) 264 for artifact in linkstamps: 265 fp.append(artifact.short_path) 266 for artifact in build_info_artifacts: 267 fp.append(artifact.short_path) 268 for feature in features: 269 fp.append(feature) 270 271 fp.append("1" if is_test_target_partially_disabled_thin_lto else "0") 272 273 fingerprint = "%x" % hash("".join(fp)) 274 return "_nativedeps/" + fingerprint 275 276def _get_static_mode_params_for_dynamic_library_libraries(libs): 277 linker_inputs = [] 278 for lib in libs: 279 if lib.pic_static_library: 280 linker_inputs.append(lib.pic_static_library) 281 elif lib.static_library: 282 linker_inputs.append(lib.static_library) 283 elif lib.interface_library: 284 linker_inputs.append(lib.interface_library) 285 else: 286 linker_inputs.append(lib.dynamic_library) 287 return linker_inputs 288 289def _link_native_deps_if_present(ctx, cc_info, cc_toolchain, build_config, actual_target_name, is_test_rule_class = False): 290 needs_linking = False 291 all_inputs = _all_inputs(cc_info) 292 for input in all_inputs: 293 needs_linking = needs_linking or _contains_code_to_link(input) 294 295 if not needs_linking: 296 return None 297 298 # This does not need to be shareable, but we use this API to specify the 299 # custom file root (matching the configuration) 300 output_lib = ctx.actions.declare_shareable_artifact( 301 ctx.label.package + "/nativedeps/" + actual_target_name + "/lib" + actual_target_name + ".so", 302 build_config.bin_dir, 303 ) 304 305 linker_inputs = cc_info.linking_context.linker_inputs.to_list() 306 307 link_opts = [] 308 for linker_input in linker_inputs: 309 for flag in linker_input.user_link_flags: 310 link_opts.append(flag) 311 312 linkstamps = [] 313 for linker_input in linker_inputs: 314 linkstamps.extend(linker_input.linkstamps) 315 linkstamps_dict = {linkstamp: None for linkstamp in linkstamps} 316 317 build_info_artifacts = _get_build_info(ctx, cc_toolchain) if linkstamps_dict else [] 318 requested_features = ["static_linking_mode", "native_deps_link"] 319 requested_features.extend(ctx.features) 320 if not "legacy_whole_archive" in ctx.disabled_features: 321 requested_features.append("legacy_whole_archive") 322 requested_features = sorted(requested_features) 323 feature_config = cc_common.configure_features( 324 ctx = ctx, 325 cc_toolchain = cc_toolchain, 326 requested_features = requested_features, 327 unsupported_features = ctx.disabled_features, 328 ) 329 partially_disabled_thin_lto = ( 330 cc_common.is_enabled( 331 feature_name = "thin_lto_linkstatic_tests_use_shared_nonlto_backends", 332 feature_configuration = feature_config, 333 ) and not cc_common.is_enabled( 334 feature_name = "thin_lto_all_linkstatic_use_shared_nonlto_backends", 335 feature_configuration = feature_config, 336 ) 337 ) 338 test_only_target = ctx.attr.testonly or is_test_rule_class 339 share_native_deps = ctx.fragments.cpp.share_native_deps() 340 341 linker_inputs = _get_static_mode_params_for_dynamic_library_libraries(all_inputs) 342 343 if share_native_deps: 344 shared_path = _get_shared_native_deps_path( 345 linker_inputs, 346 link_opts, 347 [linkstamp.file() for linkstamp in linkstamps_dict], 348 build_info_artifacts, 349 requested_features, 350 test_only_target and partially_disabled_thin_lto, 351 ) 352 linked_lib = ctx.actions.declare_shareable_artifact(shared_path + ".so", build_config.bin_dir) 353 else: 354 linked_lib = output_lib 355 356 cc_common.link( 357 name = ctx.label.name, 358 actions = ctx.actions, 359 linking_contexts = [cc_info.linking_context], 360 output_type = "dynamic_library", 361 never_link = True, 362 native_deps = True, 363 feature_configuration = feature_config, 364 cc_toolchain = cc_toolchain, 365 test_only_target = test_only_target, 366 stamp = getattr(ctx.attr, "stamp", 0), 367 main_output = linked_lib, 368 use_shareable_artifact_factory = True, 369 build_config = build_config, 370 ) 371 372 if (share_native_deps): 373 ctx.actions.symlink( 374 output = output_lib, 375 target_file = linked_lib, 376 ) 377 return output_lib 378 else: 379 return linked_lib 380 381def _concat(*list_of_lists): 382 res = [] 383 for list in list_of_lists: 384 res.extend(list) 385 return res 386