• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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    ),
27)
28
29def _split_config_aspect_impl(__, ctx):
30    return SplitConfigInfo(build_config = ctx.configuration)
31
32split_config_aspect = aspect(
33    implementation = _split_config_aspect_impl,
34)
35
36def process(ctx, filename):
37    """ Links native deps into a shared library
38
39    Args:
40      ctx: The context.
41      filename: String. The name of the artifact containing the name of the
42            linked shared library
43
44    Returns:
45        Tuple of (libs, libs_name) where libs is a depset of all native deps
46        and libs_name is a File containing the basename of the linked shared
47        library
48    """
49    actual_target_name = ctx.label.name.removesuffix(common.PACKAGED_RESOURCES_SUFFIX)
50    native_libs_basename = None
51    libs_name = None
52    libs = dict()
53    for key, deps in ctx.split_attr.deps.items():
54        cc_toolchain_dep = ctx.split_attr._cc_toolchain_split[key]
55        cc_toolchain = cc_toolchain_dep[cc_common.CcToolchainInfo]
56        build_config = cc_toolchain_dep[SplitConfigInfo].build_config
57        linker_input = cc_common.create_linker_input(
58            owner = ctx.label,
59            user_link_flags = ["-Wl,-soname=lib" + actual_target_name],
60        )
61        cc_info = cc_common.merge_cc_infos(
62            cc_infos = _concat(
63                [CcInfo(linking_context = cc_common.create_linking_context(
64                    linker_inputs = depset([linker_input]),
65                ))],
66                [dep[JavaInfo].cc_link_params_info for dep in deps if JavaInfo in dep],
67                [dep[AndroidCcLinkParamsInfo].link_params for dep in deps if AndroidCcLinkParamsInfo in dep],
68                [dep[CcInfo] for dep in deps if CcInfo in dep],
69            ),
70        )
71        libraries = []
72
73        native_deps_lib = _link_native_deps_if_present(ctx, cc_info, cc_toolchain, build_config, actual_target_name)
74        if native_deps_lib:
75            libraries.append(native_deps_lib)
76            native_libs_basename = native_deps_lib.basename
77
78        libraries.extend(_filter_unique_shared_libs(native_deps_lib, cc_info))
79
80        if libraries:
81            libs[key] = depset(libraries)
82
83    if libs and native_libs_basename:
84        libs_name = ctx.actions.declare_file("nativedeps_filename/" + actual_target_name + "/" + filename)
85        ctx.actions.write(output = libs_name, content = native_libs_basename)
86
87    transitive_native_libs = _get_transitive_native_libs(ctx)
88    return AndroidBinaryNativeLibsInfo(libs, libs_name, transitive_native_libs)
89
90# Collect all native shared libraries across split transitions. Some AARs
91# contain shared libraries across multiple architectures, e.g. x86 and
92# armeabi-v7a, and need to be packed into the APK.
93def _get_transitive_native_libs(ctx):
94    return depset(
95        transitive = [
96            dep[AndroidNativeLibsInfo].native_libs
97            for dep in ctx.attr.deps
98            if AndroidNativeLibsInfo in dep
99        ],
100    )
101
102def _all_inputs(cc_info):
103    return [
104        lib
105        for input in cc_info.linking_context.linker_inputs.to_list()
106        for lib in input.libraries
107    ]
108
109def _filter_unique_shared_libs(linked_lib, cc_info):
110    basenames = {}
111    artifacts = {}
112    if linked_lib:
113        basenames[linked_lib.basename] = linked_lib
114    for input in _all_inputs(cc_info):
115        if input.pic_static_library or input.static_library:
116            # This is not a shared library and will not be loaded by Android, so skip it.
117            continue
118
119        artifact = None
120        if input.interface_library:
121            if input.resolved_symlink_interface_library:
122                artifact = input.resolved_symlink_interface_library
123            else:
124                artifact = input.interface_library
125        elif input.resolved_symlink_dynamic_library:
126            artifact = input.resolved_symlink_dynamic_library
127        else:
128            artifact = input.dynamic_library
129
130        if not artifact:
131            fail("Should never happen: did not find artifact for link!")
132
133        if artifact in artifacts:
134            # We have already reached this library, e.g., through a different solib symlink.
135            continue
136        artifacts[artifact] = None
137        basename = artifact.basename
138        if basename in basenames:
139            old_artifact = basenames[basename]
140            fail(
141                "Each library in the transitive closure must have a " +
142                "unique basename to avoid name collisions when packaged into " +
143                "an apk, but two libraries have the basename '" + basename +
144                "': " + artifact + " and " + old_artifact + (
145                    " (the library compiled for this target)" if old_artifact == linked_lib else ""
146                ),
147            )
148        else:
149            basenames[basename] = artifact
150
151    return artifacts.keys()
152
153def _contains_code_to_link(input):
154    if not input.static_library and not input.pic_static_library:
155        # this is a shared library so we're going to have to copy it
156        return False
157    if input.objects:
158        object_files = input.objects
159    elif input.pic_objects:
160        object_files = input.pic_objects
161    elif _is_any_source_file(input.static_library, input.pic_static_library):
162        # this is an opaque library so we're going to have to link it
163        return True
164    else:
165        # if we reach here, this is a cc_library without sources generating an
166        # empty archive which does not need to be linked
167        # TODO(hvd): replace all such cc_library with exporting_cc_library
168        return False
169    for obj in object_files:
170        if not _is_shared_library(obj):
171            # this library was built with a non-shared-library object so we should link it
172            return True
173    return False
174
175def _is_any_source_file(*files):
176    for file in files:
177        if file and file.is_source:
178            return True
179    return False
180
181def _is_shared_library(lib_artifact):
182    if (lib_artifact.extension in ["so", "dll", "dylib"]):
183        return True
184
185    lib_name = lib_artifact.basename
186
187    # validate against the regex "^.+\\.((so)|(dylib))(\\.\\d\\w*)+$",
188    # must match VERSIONED_SHARED_LIBRARY.
189    for ext in (".so.", ".dylib."):
190        name, _, version = lib_name.rpartition(ext)
191        if name and version:
192            version_parts = version.split(".")
193            for part in version_parts:
194                if not part[0].isdigit():
195                    return False
196                for c in part[1:].elems():
197                    if not (c.isalnum() or c == "_"):
198                        return False
199            return True
200    return False
201
202def _get_build_info(ctx):
203    return cc_common.get_build_info(ctx)
204
205def _get_shared_native_deps_path(
206        linker_inputs,
207        link_opts,
208        linkstamps,
209        build_info_artifacts,
210        features,
211        is_test_target_partially_disabled_thin_lto):
212    fp = []
213    for artifact in linker_inputs:
214        fp.append(artifact.short_path)
215    fp.append(str(len(link_opts)))
216    for opt in link_opts:
217        fp.append(opt)
218    for artifact in linkstamps:
219        fp.append(artifact.short_path)
220    for artifact in build_info_artifacts:
221        fp.append(artifact.short_path)
222    for feature in features:
223        fp.append(feature)
224
225    fp.append("1" if is_test_target_partially_disabled_thin_lto else "0")
226
227    fingerprint = "%x" % hash("".join(fp))
228    return "_nativedeps/" + fingerprint
229
230def _get_static_mode_params_for_dynamic_library_libraries(libs):
231    linker_inputs = []
232    for lib in libs:
233        if lib.pic_static_library:
234            linker_inputs.append(lib.pic_static_library)
235        elif lib.static_library:
236            linker_inputs.append(lib.static_library)
237        elif lib.interface_library:
238            linker_inputs.append(lib.interface_library)
239        else:
240            linker_inputs.append(lib.dynamic_library)
241    return linker_inputs
242
243def _link_native_deps_if_present(ctx, cc_info, cc_toolchain, build_config, actual_target_name, is_test_rule_class = False):
244    needs_linking = False
245    for input in _all_inputs(cc_info):
246        needs_linking = needs_linking or _contains_code_to_link(input)
247
248    if not needs_linking:
249        return None
250
251    # This does not need to be shareable, but we use this API to specify the
252    # custom file root (matching the configuration)
253    output_lib = ctx.actions.declare_shareable_artifact(
254        ctx.label.package + "/nativedeps/" + actual_target_name + "/lib" + actual_target_name + ".so",
255        build_config.bin_dir,
256    )
257
258    link_opts = cc_info.linking_context.user_link_flags
259
260    linkstamps = []
261    for input in cc_info.linking_context.linker_inputs.to_list():
262        linkstamps.extend(input.linkstamps)
263    linkstamps_dict = {linkstamp: None for linkstamp in linkstamps}
264
265    build_info_artifacts = _get_build_info(ctx) if linkstamps_dict else []
266    requested_features = ["static_linking_mode", "native_deps_link"]
267    requested_features.extend(ctx.features)
268    if not "legacy_whole_archive" in ctx.disabled_features:
269        requested_features.append("legacy_whole_archive")
270    requested_features = sorted(requested_features)
271    feature_config = cc_common.configure_features(
272        ctx = ctx,
273        cc_toolchain = cc_toolchain,
274        requested_features = requested_features,
275        unsupported_features = ctx.disabled_features,
276    )
277    partially_disabled_thin_lto = (
278        cc_common.is_enabled(
279            feature_name = "thin_lto_linkstatic_tests_use_shared_nonlto_backends",
280            feature_configuration = feature_config,
281        ) and not cc_common.is_enabled(
282            feature_name = "thin_lto_all_linkstatic_use_shared_nonlto_backends",
283            feature_configuration = feature_config,
284        )
285    )
286    test_only_target = ctx.attr.testonly or is_test_rule_class
287    share_native_deps = ctx.fragments.cpp.share_native_deps()
288
289    linker_inputs = _get_static_mode_params_for_dynamic_library_libraries(cc_info.linking_context.libraries_to_link)
290
291    if share_native_deps:
292        shared_path = _get_shared_native_deps_path(
293            linker_inputs,
294            link_opts,
295            [linkstamp.file() for linkstamp in linkstamps_dict],
296            build_info_artifacts,
297            requested_features,
298            test_only_target and partially_disabled_thin_lto,
299        )
300        linked_lib = ctx.actions.declare_shareable_artifact(shared_path + ".so", build_config.bin_dir)
301    else:
302        linked_lib = output_lib
303
304    cc_common.link(
305        name = ctx.label.name,
306        actions = ctx.actions,
307        linking_contexts = [cc_info.linking_context],
308        output_type = "dynamic_library",
309        never_link = True,
310        native_deps = True,
311        feature_configuration = feature_config,
312        cc_toolchain = cc_toolchain,
313        test_only_target = test_only_target,
314        stamp = ctx.attr.stamp,
315        grep_includes = ctx.file._grep_includes,
316        main_output = linked_lib,
317        use_shareable_artifact_factory = True,
318        build_config = build_config,
319    )
320
321    if (share_native_deps):
322        ctx.actions.symlink(
323            output = output_lib,
324            target_file = linked_lib,
325        )
326        return output_lib
327    else:
328        return linked_lib
329
330def _concat(*list_of_lists):
331    res = []
332    for list in list_of_lists:
333        res.extend(list)
334    return res
335