xref: /aosp_15_r20/external/bazelbuild-rules_android/rules/native_deps.bzl (revision 9e965d6fece27a77de5377433c2f7e6999b8cc0b)
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