• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""This is an experimental implementation of cc_shared_library.
2
3We may change the implementation at any moment or even delete this file. Do not
4rely on this. It requires bazel >1.2  and passing the flag
5--experimental_cc_shared_library
6"""
7
8load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
9load("//cc:find_cc_toolchain.bzl", "find_cc_toolchain")
10
11# TODO(#5200): Add export_define to library_to_link and cc_library
12
13# Add this as a tag to any target that can be linked by more than one
14# cc_shared_library because it doesn't have static initializers or anything
15# else that may cause issues when being linked more than once. This should be
16# used sparingly after making sure it's safe to use.
17LINKABLE_MORE_THAN_ONCE = "LINKABLE_MORE_THAN_ONCE"
18
19CcSharedLibraryPermissionsInfo = provider(
20    "Permissions for a cc shared library.",
21    fields = {
22        "targets": "Matches targets that can be exported.",
23    },
24)
25GraphNodeInfo = provider(
26    "Nodes in the graph of shared libraries.",
27    fields = {
28        "children": "Other GraphNodeInfo from dependencies of this target",
29        "label": "Label of the target visited",
30        "linkable_more_than_once": "Linkable into more than a single cc_shared_library",
31    },
32)
33CcSharedLibraryInfo = provider(
34    "Information about a cc shared library.",
35    fields = {
36        "dynamic_deps": "All shared libraries depended on transitively",
37        "exports": "cc_libraries that are linked statically and exported",
38        "link_once_static_libs": "All libraries linked statically into this library that should " +
39                                 "only be linked once, e.g. because they have static " +
40                                 "initializers. If we try to link them more than once, " +
41                                 "we will throw an error",
42        "linker_input": "the resulting linker input artifact for the shared library",
43        "preloaded_deps": "cc_libraries needed by this cc_shared_library that should" +
44                          " be linked the binary. If this is set, this cc_shared_library has to " +
45                          " be a direct dependency of the cc_binary",
46    },
47)
48
49def _separate_static_and_dynamic_link_libraries(
50        direct_children,
51        can_be_linked_dynamically,
52        preloaded_deps_direct_labels):
53    node = None
54    all_children = list(direct_children)
55    link_statically_labels = {}
56    link_dynamically_labels = {}
57
58    seen_labels = {}
59
60    # Horrible I know. Perhaps Starlark team gives me a way to prune a tree.
61    for i in range(2147483647):
62        if i == len(all_children):
63            break
64
65        node = all_children[i]
66        node_label = str(node.label)
67
68        if node_label in seen_labels:
69            continue
70        seen_labels[node_label] = True
71
72        if node_label in can_be_linked_dynamically:
73            link_dynamically_labels[node_label] = True
74        elif node_label not in preloaded_deps_direct_labels:
75            link_statically_labels[node_label] = node.linkable_more_than_once
76            all_children.extend(node.children)
77
78    return (link_statically_labels, link_dynamically_labels)
79
80def _create_linker_context(ctx, linker_inputs):
81    return cc_common.create_linking_context(
82        linker_inputs = depset(linker_inputs, order = "topological"),
83    )
84
85def _merge_cc_shared_library_infos(ctx):
86    dynamic_deps = []
87    transitive_dynamic_deps = []
88    for dep in ctx.attr.dynamic_deps:
89        if dep[CcSharedLibraryInfo].preloaded_deps != None:
90            fail("{} can only be a direct dependency of a " +
91                 " cc_binary because it has " +
92                 "preloaded_deps".format(str(dep.label)))
93        dynamic_dep_entry = (
94            dep[CcSharedLibraryInfo].exports,
95            dep[CcSharedLibraryInfo].linker_input,
96            dep[CcSharedLibraryInfo].link_once_static_libs,
97        )
98        dynamic_deps.append(dynamic_dep_entry)
99        transitive_dynamic_deps.append(dep[CcSharedLibraryInfo].dynamic_deps)
100
101    return depset(direct = dynamic_deps, transitive = transitive_dynamic_deps)
102
103def _build_exports_map_from_only_dynamic_deps(merged_shared_library_infos):
104    exports_map = {}
105    for entry in merged_shared_library_infos.to_list():
106        exports = entry[0]
107        linker_input = entry[1]
108        for export in exports:
109            if export in exports_map:
110                fail("Two shared libraries in dependencies export the same symbols. Both " +
111                     exports_map[export].libraries[0].dynamic_library.short_path +
112                     " and " + linker_input.libraries[0].dynamic_library.short_path +
113                     " export " + export)
114            exports_map[export] = linker_input
115    return exports_map
116
117def _build_link_once_static_libs_map(merged_shared_library_infos):
118    link_once_static_libs_map = {}
119    for entry in merged_shared_library_infos.to_list():
120        link_once_static_libs = entry[2]
121        linker_input = entry[1]
122        for static_lib in link_once_static_libs:
123            if static_lib in link_once_static_libs_map:
124                fail("Two shared libraries in dependencies link the same " +
125                     " library statically. Both " + link_once_static_libs_map[static_lib] +
126                     " and " + str(linker_input.owner) +
127                     " link statically" + static_lib)
128            link_once_static_libs_map[static_lib] = str(linker_input.owner)
129    return link_once_static_libs_map
130
131def _wrap_static_library_with_alwayslink(ctx, feature_configuration, cc_toolchain, linker_input):
132    new_libraries_to_link = []
133    for old_library_to_link in linker_input.libraries:
134        # TODO(#5200): This will lose the object files from a library to link.
135        # Not too bad for the prototype but as soon as the library_to_link
136        # constructor has object parameters this should be changed.
137        new_library_to_link = cc_common.create_library_to_link(
138            actions = ctx.actions,
139            feature_configuration = feature_configuration,
140            cc_toolchain = cc_toolchain,
141            static_library = old_library_to_link.static_library,
142            pic_static_library = old_library_to_link.pic_static_library,
143            alwayslink = True,
144        )
145        new_libraries_to_link.append(new_library_to_link)
146
147    return cc_common.create_linker_input(
148        owner = linker_input.owner,
149        libraries = depset(direct = new_libraries_to_link),
150        user_link_flags = depset(direct = linker_input.user_link_flags),
151        additional_inputs = depset(direct = linker_input.additional_inputs),
152    )
153
154def _check_if_target_under_path(value, pattern):
155    if pattern.workspace_name != value.workspace_name:
156        return False
157    if pattern.name == "__pkg__":
158        return pattern.package == value.package
159    if pattern.name == "__subpackages__":
160        return _same_package_or_above(pattern, value)
161
162    return pattern.package == value.package and pattern.name == value.name
163
164def _check_if_target_can_be_exported(target, current_label, permissions):
165    if permissions == None:
166        return True
167
168    if (target.workspace_name != current_label.workspace_name or
169        _same_package_or_above(current_label, target)):
170        return True
171
172    matched_by_target = False
173    for permission in permissions:
174        for permission_target in permission[CcSharedLibraryPermissionsInfo].targets:
175            if _check_if_target_under_path(target, permission_target):
176                return True
177
178    return False
179
180def _check_if_target_should_be_exported_without_filter(target, current_label, permissions):
181    return _check_if_target_should_be_exported_with_filter(target, current_label, None, permissions)
182
183def _check_if_target_should_be_exported_with_filter(target, current_label, exports_filter, permissions):
184    should_be_exported = False
185    if exports_filter == None:
186        should_be_exported = True
187    else:
188        for export_filter in exports_filter:
189            export_filter_label = current_label.relative(export_filter)
190            if _check_if_target_under_path(target, export_filter_label):
191                should_be_exported = True
192                break
193
194    if should_be_exported:
195        if _check_if_target_can_be_exported(target, current_label, permissions):
196            return True
197        else:
198            matched_by_filter_text = ""
199            if exports_filter:
200                matched_by_filter_text = " (matched by filter) "
201            fail(str(target) + matched_by_filter_text +
202                 " cannot be exported from " + str(current_label) +
203                 " because it's not in the same package/subpackage and the library " +
204                 "doesn't have the necessary permissions. Use cc_shared_library_permissions.")
205
206    return False
207
208def _filter_inputs(
209        ctx,
210        feature_configuration,
211        cc_toolchain,
212        transitive_exports,
213        preloaded_deps_direct_labels,
214        link_once_static_libs_map):
215    linker_inputs = []
216    link_once_static_libs = []
217
218    graph_structure_aspect_nodes = []
219    dependency_linker_inputs = []
220    direct_exports = {}
221    for export in ctx.attr.roots:
222        direct_exports[str(export.label)] = True
223        dependency_linker_inputs.extend(export[CcInfo].linking_context.linker_inputs.to_list())
224        graph_structure_aspect_nodes.append(export[GraphNodeInfo])
225
226    can_be_linked_dynamically = {}
227    for linker_input in dependency_linker_inputs:
228        owner = str(linker_input.owner)
229        if owner in transitive_exports:
230            can_be_linked_dynamically[owner] = True
231
232    (link_statically_labels, link_dynamically_labels) = _separate_static_and_dynamic_link_libraries(
233        graph_structure_aspect_nodes,
234        can_be_linked_dynamically,
235        preloaded_deps_direct_labels,
236    )
237
238    exports = {}
239    owners_seen = {}
240    for linker_input in dependency_linker_inputs:
241        owner = str(linker_input.owner)
242        if owner in owners_seen:
243            continue
244        owners_seen[owner] = True
245        if owner in link_dynamically_labels:
246            dynamic_linker_input = transitive_exports[owner]
247            linker_inputs.append(dynamic_linker_input)
248        elif owner in link_statically_labels:
249            if owner in link_once_static_libs_map:
250                fail(owner + " is already linked statically in " +
251                     link_once_static_libs_map[owner] + " but not exported")
252
253            if owner in direct_exports:
254                wrapped_library = _wrap_static_library_with_alwayslink(
255                    ctx,
256                    feature_configuration,
257                    cc_toolchain,
258                    linker_input,
259                )
260
261                if not link_statically_labels[owner]:
262                    link_once_static_libs.append(owner)
263                linker_inputs.append(wrapped_library)
264            else:
265                can_be_linked_statically = False
266
267                for static_dep_path in ctx.attr.static_deps:
268                    static_dep_path_label = ctx.label.relative(static_dep_path)
269                    if _check_if_target_under_path(linker_input.owner, static_dep_path_label):
270                        can_be_linked_statically = True
271                        break
272
273                if _check_if_target_should_be_exported_with_filter(
274                    linker_input.owner,
275                    ctx.label,
276                    ctx.attr.exports_filter,
277                    _get_permissions(ctx),
278                ):
279                    exports[owner] = True
280                    can_be_linked_statically = True
281
282                if can_be_linked_statically:
283                    if not link_statically_labels[owner]:
284                        link_once_static_libs.append(owner)
285                    linker_inputs.append(linker_input)
286                else:
287                    fail("We can't link " +
288                         str(owner) + " either statically or dynamically")
289
290    # Divergence from rules_cc: Add all dynamic dependencies as linker inputs
291    # even if they do not contain transitive dependencies of the roots.
292    # TODO(cparsons): Push this as an option upstream..
293    for dynamic_dep_input in transitive_exports.values():
294        linker_inputs.append(dynamic_dep_input)
295
296    return (exports, linker_inputs, link_once_static_libs)
297
298def _same_package_or_above(label_a, label_b):
299    if label_a.workspace_name != label_b.workspace_name:
300        return False
301    package_a_tokenized = label_a.package.split("/")
302    package_b_tokenized = label_b.package.split("/")
303    if len(package_b_tokenized) < len(package_a_tokenized):
304        return False
305
306    if package_a_tokenized[0] != "":
307        for i in range(len(package_a_tokenized)):
308            if package_a_tokenized[i] != package_b_tokenized[i]:
309                return False
310
311    return True
312
313def _get_permissions(ctx):
314    if ctx.attr._enable_permissions_check[BuildSettingInfo].value:
315        return ctx.attr.permissions
316    return None
317
318def _process_version_script(ctx):
319    if ctx.attr.version_script == None:
320        return ([], [])
321
322    version_script = ctx.files.version_script[0]
323    version_script_arg = "-Wl,--version-script," + version_script.path
324    return ([version_script], [version_script_arg])
325
326def _cc_shared_library_impl(ctx):
327    cc_common.check_experimental_cc_shared_library()
328    cc_toolchain = find_cc_toolchain(ctx)
329    feature_configuration = cc_common.configure_features(
330        ctx = ctx,
331        cc_toolchain = cc_toolchain,
332        requested_features = ctx.features,
333        unsupported_features = ctx.disabled_features,
334    )
335
336    merged_cc_shared_library_info = _merge_cc_shared_library_infos(ctx)
337    exports_map = _build_exports_map_from_only_dynamic_deps(merged_cc_shared_library_info)
338    for export in ctx.attr.roots:
339        if str(export.label) in exports_map:
340            fail("Trying to export a library already exported by a different shared library: " +
341                 str(export.label))
342
343        _check_if_target_should_be_exported_without_filter(export.label, ctx.label, _get_permissions(ctx))
344
345    preloaded_deps_direct_labels = {}
346    preloaded_dep_merged_cc_info = None
347    if len(ctx.attr.preloaded_deps) != 0:
348        preloaded_deps_cc_infos = []
349        for preloaded_dep in ctx.attr.preloaded_deps:
350            preloaded_deps_direct_labels[str(preloaded_dep.label)] = True
351            preloaded_deps_cc_infos.append(preloaded_dep[CcInfo])
352
353        preloaded_dep_merged_cc_info = cc_common.merge_cc_infos(cc_infos = preloaded_deps_cc_infos)
354
355    link_once_static_libs_map = _build_link_once_static_libs_map(merged_cc_shared_library_info)
356
357    (exports, linker_inputs, link_once_static_libs) = _filter_inputs(
358        ctx,
359        feature_configuration,
360        cc_toolchain,
361        exports_map,
362        preloaded_deps_direct_labels,
363        link_once_static_libs_map,
364    )
365
366    linking_context = _create_linker_context(ctx, linker_inputs)
367
368    # Divergence from rules_cc: that version does not support version scripts
369    version_script, version_script_arg = _process_version_script(ctx)
370
371    user_link_flags = version_script_arg[:]
372    for user_link_flag in ctx.attr.user_link_flags:
373        user_link_flags.append(ctx.expand_location(user_link_flag, targets = ctx.attr.additional_linker_inputs))
374
375    linking_outputs = cc_common.link(
376        actions = ctx.actions,
377        feature_configuration = feature_configuration,
378        cc_toolchain = cc_toolchain,
379        linking_contexts = [linking_context],
380        user_link_flags = user_link_flags,
381        additional_inputs = ctx.files.additional_linker_inputs + version_script,
382        name = ctx.label.name,
383        output_type = "dynamic_library",
384    )
385
386    runfiles = ctx.runfiles(
387        files = [linking_outputs.library_to_link.resolved_symlink_dynamic_library],
388    )
389    for dep in ctx.attr.dynamic_deps:
390        runfiles = runfiles.merge(dep[DefaultInfo].data_runfiles)
391
392    for export in ctx.attr.roots:
393        exports[str(export.label)] = True
394
395    debug_files = []
396    if ctx.attr._experimental_debug[BuildSettingInfo].value:
397        exports_debug_file = ctx.actions.declare_file(ctx.label.name + "_exports.txt")
398        ctx.actions.write(content = "\n".join(exports.keys()), output = exports_debug_file)
399
400        link_once_static_libs_debug_file = ctx.actions.declare_file(ctx.label.name + "_link_once_static_libs.txt")
401        ctx.actions.write(content = "\n".join(link_once_static_libs), output = link_once_static_libs_debug_file)
402
403        debug_files.append(exports_debug_file)
404        debug_files.append(link_once_static_libs_debug_file)
405
406    if not ctx.attr._incompatible_link_once[BuildSettingInfo].value:
407        link_once_static_libs = []
408
409    return [
410        DefaultInfo(
411            files = depset([linking_outputs.library_to_link.resolved_symlink_dynamic_library] + debug_files),
412            runfiles = runfiles,
413        ),
414        CcSharedLibraryInfo(
415            dynamic_deps = merged_cc_shared_library_info,
416            exports = exports.keys(),
417            link_once_static_libs = link_once_static_libs,
418            linker_input = cc_common.create_linker_input(
419                owner = ctx.label,
420                libraries = depset([linking_outputs.library_to_link]),
421            ),
422            preloaded_deps = preloaded_dep_merged_cc_info,
423        ),
424    ]
425
426def _collect_graph_structure_info_from_children(ctx, attr):
427    children = []
428    deps = getattr(ctx.rule.attr, attr, [])
429    if type(deps) == "list":
430        for dep in deps:
431            if GraphNodeInfo in dep:
432                children.append(dep[GraphNodeInfo])
433    elif deps != None and GraphNodeInfo in deps:
434        # Single dep.
435        children.append(deps[GraphNodeInfo])
436    return children
437
438
439def _graph_structure_aspect_impl(target, ctx):
440    children = []
441
442    # This is a deviation from HEAD rules_cc because full_cc_library.bzl uses
443    # static/shared (among others) attrs to combine multiple targets into one,
444    # and the aspect needs to be able to traverse them to correctly populate
445    # linker_inputs in the cc_shared_library impl.
446    children += _collect_graph_structure_info_from_children(ctx, "deps")
447    children += _collect_graph_structure_info_from_children(ctx, "whole_archive_deps")
448    children += _collect_graph_structure_info_from_children(ctx, "dynamic_deps")
449    children += _collect_graph_structure_info_from_children(ctx, "implementation_deps")
450    children += _collect_graph_structure_info_from_children(ctx, "static")
451    children += _collect_graph_structure_info_from_children(ctx, "shared")
452
453    # TODO(bazel-team): Add flag to Bazel that can toggle the initialization of
454    # linkable_more_than_once.
455    linkable_more_than_once = False
456    if hasattr(ctx.rule.attr, "tags"):
457        for tag in ctx.rule.attr.tags:
458            if tag == LINKABLE_MORE_THAN_ONCE:
459                linkable_more_than_once = True
460
461    return [GraphNodeInfo(
462        label = ctx.label,
463        children = children,
464        linkable_more_than_once = linkable_more_than_once,
465    )]
466
467def _cc_shared_library_permissions_impl(ctx):
468    targets = []
469    for target_filter in ctx.attr.targets:
470        target_filter_label = ctx.label.relative(target_filter)
471        if not _check_if_target_under_path(target_filter_label, ctx.label.relative(":__subpackages__")):
472            fail("A cc_shared_library_permissions rule can only list " +
473                 "targets that are in the same package or a sub-package")
474        targets.append(target_filter_label)
475
476    return [CcSharedLibraryPermissionsInfo(
477        targets = targets,
478    )]
479
480graph_structure_aspect = aspect(
481    attr_aspects = ["*"],
482    implementation = _graph_structure_aspect_impl,
483)
484
485cc_shared_library_permissions = rule(
486    implementation = _cc_shared_library_permissions_impl,
487    attrs = {
488        "targets": attr.string_list(),
489    },
490)
491
492cc_shared_library = rule(
493    implementation = _cc_shared_library_impl,
494    attrs = {
495        "additional_linker_inputs": attr.label_list(allow_files = True),
496        "dynamic_deps": attr.label_list(providers = [CcSharedLibraryInfo]),
497        "exports_filter": attr.string_list(),
498        "permissions": attr.label_list(providers = [CcSharedLibraryPermissionsInfo]),
499        "preloaded_deps": attr.label_list(providers = [CcInfo]),
500        "roots": attr.label_list(providers = [CcInfo], aspects = [graph_structure_aspect]),
501        "static_deps": attr.string_list(),
502        "version_script": attr.label(allow_single_file = True),
503        "user_link_flags": attr.string_list(),
504        "_cc_toolchain": attr.label(default = "@bazel_tools//tools/cpp:current_cc_toolchain"),
505        "_enable_permissions_check": attr.label(default = "//examples:enable_permissions_check"),
506        "_experimental_debug": attr.label(default = "//examples:experimental_debug"),
507        "_incompatible_link_once": attr.label(default = "//examples:incompatible_link_once"),
508    },
509    toolchains = ["@bazel_tools//tools/cpp:toolchain_type"],  # copybara-use-repo-external-label
510    fragments = ["cpp"],
511    incompatible_use_toolchain_transition = True,
512)
513
514for_testing_dont_use_check_if_target_under_path = _check_if_target_under_path
515