blob: 4b41fd7892e09e9e48c6106579862e0c7eaabd96 [file] [log] [blame]
"""This is an experimental implementation of cc_shared_library.
We may change the implementation at any moment or even delete this file. Do not
rely on this. It requires bazel >1.2 and passing the flag
--experimental_cc_shared_library
"""
load("//cc:find_cc_toolchain.bzl", "find_cc_toolchain")
# TODO(#5200): Add export_define to library_to_link and cc_library
# TODO(plf): Even though at the begining the idea was to completely forbid
# anyone to link a library statically without permission from the library owner,
# there have been complaints about this. linked_statically_by is hard to use
# and too restrictive. static_deps will completely replace linked_statically_by.
# Library authors will still have control of who exports them by using exported_by.
GraphNodeInfo = provider(
fields = {
"children": "Other GraphNodeInfo from dependencies of this target",
"label": "Label of the target visited",
# TODO(plf): Completely remove linked_statically_by.
"linked_statically_by": "The value of this attribute if the target has it",
},
)
CcSharedLibraryInfo = provider(
fields = {
"dynamic_deps": "All shared libraries depended on transitively",
"exports": "cc_libraries that are linked statically and exported",
"linker_input": "the resulting linker input artifact for the shared library",
"preloaded_deps": "cc_libraries needed by this cc_shared_library that should" +
" be linked the binary. If this is set, this cc_shared_library has to " +
" be a direct dependency of the cc_binary",
"static_libs": "All libraries linked statically into this library",
},
)
def _separate_static_and_dynamic_link_libraries(
direct_children,
can_be_linked_dynamically,
preloaded_deps_direct_labels):
node = None
all_children = list(direct_children)
link_statically_labels = {}
link_dynamically_labels = {}
# Horrible I know. Perhaps Starlark team gives me a way to prune a tree.
for i in range(1, 2147483647):
if len(all_children) == 0:
break
node = all_children.pop(0)
node_label = str(node.label)
if node_label in can_be_linked_dynamically:
link_dynamically_labels[node_label] = None
elif node_label not in preloaded_deps_direct_labels:
link_statically_labels[node_label] = node.linked_statically_by
all_children.extend(node.children)
return (link_statically_labels, link_dynamically_labels)
def _create_linker_context(ctx, static_linker_inputs, dynamic_linker_inputs):
linker_inputs = []
# Statically linked symbols should take precedence over dynamically linked.
linker_inputs.extend(static_linker_inputs)
linker_inputs.extend(dynamic_linker_inputs)
return cc_common.create_linking_context(
linker_inputs = depset(linker_inputs, order = "topological"),
)
def _merge_cc_shared_library_infos(ctx):
dynamic_deps = []
transitive_dynamic_deps = []
for dep in ctx.attr.dynamic_deps:
if dep[CcSharedLibraryInfo].preloaded_deps != None:
fail("{} can only be a direct dependency of a " +
" cc_binary because it has " +
"preloaded_deps".format(str(dep.label)))
dynamic_dep_entry = (
dep[CcSharedLibraryInfo].exports,
dep[CcSharedLibraryInfo].linker_input,
dep[CcSharedLibraryInfo].static_libs,
)
dynamic_deps.append(dynamic_dep_entry)
transitive_dynamic_deps.append(dep[CcSharedLibraryInfo].dynamic_deps)
return depset(direct = dynamic_deps, transitive = transitive_dynamic_deps)
def _build_exports_map_from_only_dynamic_deps(merged_shared_library_infos):
exports_map = {}
for entry in merged_shared_library_infos.to_list():
exports = entry[0]
linker_input = entry[1]
for export in exports:
if export in exports_map:
fail("Two shared libraries in dependencies export the same symbols. Both " +
exports_map[export].libraries[0].dynamic_library.short_path +
" and " + linker_input.dynamic_library.short_path +
" export " + export)
exports_map[export] = linker_input
return exports_map
def _build_static_libs_map(merged_shared_library_infos):
static_libs_map = {}
for entry in merged_shared_library_infos.to_list():
static_libs = entry[2]
linker_input = entry[1]
for static_lib in static_libs:
if static_lib in static_libs_map:
fail("Two shared libraries in dependencies link the same " +
" library statically. Both " + static_libs_map[static_lib] +
" and " + str(linker_input.owner) +
" link statically" + static_lib)
static_libs_map[static_lib] = str(linker_input.owner)
return static_libs_map
def _wrap_static_library_with_alwayslink(ctx, feature_configuration, cc_toolchain, linker_input):
new_libraries_to_link = []
for old_library_to_link in linker_input.libraries:
# TODO(#5200): This will lose the object files from a library to link.
# Not too bad for the prototype but as soon as the library_to_link
# constructor has object parameters this should be changed.
new_library_to_link = cc_common.create_library_to_link(
actions = ctx.actions,
feature_configuration = feature_configuration,
cc_toolchain = cc_toolchain,
static_library = old_library_to_link.static_library,
pic_static_library = old_library_to_link.pic_static_library,
alwayslink = True,
)
new_libraries_to_link.append(new_library_to_link)
return cc_common.create_linker_input(
owner = linker_input.owner,
libraries = depset(direct = new_libraries_to_link),
user_link_flags = depset(direct = linker_input.user_link_flags),
additional_inputs = depset(direct = linker_input.additional_inputs),
)
def _check_if_target_under_path(path, target, target_specified):
if not _same_package_or_above(path, target):
return False
if target_specified:
return path.name == target.name
return True
def _is_target_specified(path):
if path.startswith("//") or path.startswith("@"):
if path.find(":") != -1:
return True
else:
return False
else:
return True
def _filter_inputs(
ctx,
feature_configuration,
cc_toolchain,
transitive_exports,
preloaded_deps_direct_labels,
static_libs_map):
static_linker_inputs = []
dynamic_linker_inputs = []
graph_structure_aspect_nodes = []
linker_inputs = []
direct_exports = {}
for export in ctx.attr.exports:
direct_exports[str(export.label)] = True
linker_inputs.extend(export[CcInfo].linking_context.linker_inputs.to_list())
graph_structure_aspect_nodes.append(export[GraphNodeInfo])
can_be_linked_dynamically = {}
for linker_input in linker_inputs:
owner = str(linker_input.owner)
if owner in transitive_exports:
can_be_linked_dynamically[owner] = True
(link_statically_labels, link_dynamically_labels) = _separate_static_and_dynamic_link_libraries(
graph_structure_aspect_nodes,
can_be_linked_dynamically,
preloaded_deps_direct_labels,
)
already_linked_dynamically = {}
owners_seen = {}
for linker_input in linker_inputs:
owner = str(linker_input.owner)
if owner in owners_seen:
continue
owners_seen[owner] = True
if owner in link_dynamically_labels:
dynamic_linker_input = transitive_exports[owner]
if str(dynamic_linker_input.owner) not in already_linked_dynamically:
already_linked_dynamically[str(dynamic_linker_input.owner)] = True
dynamic_linker_inputs.append(dynamic_linker_input)
elif owner in link_statically_labels:
if owner in static_libs_map:
fail(owner + " is already linked statically in " +
static_libs_map[owner] + " but not exported")
if owner in direct_exports:
static_linker_inputs.append(_wrap_static_library_with_alwayslink(
ctx,
feature_configuration,
cc_toolchain,
linker_input,
))
else:
can_be_linked_statically = False
# TODO(plf): This loop will go away when linked_statically_by is
# replaced completely by static_deps.
for linked_statically_by in link_statically_labels[owner]:
if linked_statically_by == str(ctx.label):
can_be_linked_statically = True
break
if not can_be_linked_statically:
for static_dep_path in ctx.attr.static_deps:
target_specified = _is_target_specified(static_dep_path)
static_dep_path_label = ctx.label.relative(static_dep_path)
owner_label = linker_input.owner
if _check_if_target_under_path(linker_input.owner, static_dep_path_label, target_specified):
can_be_linked_statically = True
break
if can_be_linked_statically:
static_linker_inputs.append(linker_input)
else:
fail("We can't link " +
str(owner) + " either statically or dynamically")
# Every direct dynamic_dep of this rule will be linked dynamically even if we
# didn't reach a cc_library exported by one of these dynamic_deps. In other
# words, the shared library might link more shared libraries than we need
# in the cc_library graph.
for dep in ctx.attr.dynamic_deps:
has_a_used_export = False
for export in dep[CcSharedLibraryInfo].exports:
if export in link_dynamically_labels:
has_a_used_export = True
break
if not has_a_used_export:
dynamic_linker_inputs.append(dep[CcSharedLibraryInfo].linker_input)
return (static_linker_inputs, dynamic_linker_inputs)
def _same_package_or_above(label_a, label_b):
if label_a.workspace_name != label_b.workspace_name:
return False
package_a_tokenized = label_a.package.split("/")
package_b_tokenized = label_b.package.split("/")
if len(package_b_tokenized) < len(package_a_tokenized):
return False
for i in range(len(package_a_tokenized)):
if package_a_tokenized[i] != package_b_tokenized[i]:
return False
return True
def _cc_shared_library_impl(ctx):
cc_toolchain = find_cc_toolchain(ctx)
feature_configuration = cc_common.configure_features(
ctx = ctx,
cc_toolchain = cc_toolchain,
requested_features = ctx.features,
unsupported_features = ctx.disabled_features,
)
merged_cc_shared_library_info = _merge_cc_shared_library_infos(ctx)
exports_map = _build_exports_map_from_only_dynamic_deps(merged_cc_shared_library_info)
for export in ctx.attr.exports:
if str(export.label) in exports_map:
fail("Trying to export a library already exported by a different shared library: " +
str(export.label))
if not _same_package_or_above(ctx.label, export[GraphNodeInfo].label):
fail(str(export.label) + " cannot be exported from " + str(ctx.label) +
" because " + str(export.label) + " is not in the same package " +
" or a sub-package")
preloaded_deps_direct_labels = {}
preloaded_dep_merged_cc_info = None
if len(ctx.attr.preloaded_deps) != 0:
preloaded_deps_cc_infos = []
for preloaded_dep in ctx.attr.preloaded_deps:
preloaded_deps_direct_labels[str(preloaded_dep.label)] = True
preloaded_deps_cc_infos.append(preloaded_dep[CcInfo])
preloaded_dep_merged_cc_info = cc_common.merge_cc_infos(cc_infos = preloaded_deps_cc_infos)
static_libs_map = _build_static_libs_map(merged_cc_shared_library_info)
(static_linker_inputs, dynamic_linker_inputs) = _filter_inputs(
ctx,
feature_configuration,
cc_toolchain,
exports_map,
preloaded_deps_direct_labels,
static_libs_map,
)
linking_context = _create_linker_context(ctx, static_linker_inputs, dynamic_linker_inputs)
# TODO(plf): Decide whether ctx.attr.user_link_flags should come before or after options
# added by the rule logic.
user_link_flags = []
additional_inputs = []
if ctx.file.visibility_file != None:
user_link_flags.append(
"-Wl,--version-script=" + ctx.file.visibility_file.path,
)
additional_inputs = [ctx.file.visibility_file]
user_link_flags.extend(ctx.attr.user_link_flags)
linking_outputs = cc_common.link(
actions = ctx.actions,
feature_configuration = feature_configuration,
cc_toolchain = cc_toolchain,
linking_contexts = [linking_context],
user_link_flags = user_link_flags,
additional_inputs = additional_inputs,
name = ctx.label.name,
output_type = "dynamic_library",
)
runfiles = ctx.runfiles(
files = [linking_outputs.library_to_link.resolved_symlink_dynamic_library],
)
for dep in ctx.attr.dynamic_deps:
runfiles = runfiles.merge(dep[DefaultInfo].data_runfiles)
exports = []
for export in ctx.attr.exports:
exports.append(str(export.label))
static_libs = []
for static_linker_input in static_linker_inputs:
static_libs.append(str(static_linker_input.owner))
return [
DefaultInfo(
files = depset([linking_outputs.library_to_link.resolved_symlink_dynamic_library]),
runfiles = runfiles,
),
CcSharedLibraryInfo(
dynamic_deps = merged_cc_shared_library_info,
exports = exports,
static_libs = static_libs,
linker_input = cc_common.create_linker_input(
owner = ctx.label,
libraries = depset([linking_outputs.library_to_link]),
),
preloaded_deps = preloaded_dep_merged_cc_info,
),
]
def _graph_structure_aspect_impl(target, ctx):
children = []
if hasattr(ctx.rule.attr, "deps"):
for dep in ctx.rule.attr.deps:
if GraphNodeInfo in dep:
children.append(dep[GraphNodeInfo])
linked_statically_by = []
if hasattr(ctx.rule.attr, "linked_statically_by"):
linked_statically_by = [str(label) for label in ctx.rule.attr.linked_statically_by]
return [GraphNodeInfo(
label = ctx.label,
linked_statically_by = linked_statically_by,
children = children,
)]
graph_structure_aspect = aspect(
attr_aspects = ["*"],
implementation = _graph_structure_aspect_impl,
)
cc_shared_library = rule(
implementation = _cc_shared_library_impl,
attrs = {
"dynamic_deps": attr.label_list(providers = [CcSharedLibraryInfo]),
"exports": attr.label_list(providers = [CcInfo], aspects = [graph_structure_aspect]),
"preloaded_deps": attr.label_list(providers = [CcInfo]),
# TODO(plf): Replaces linked_statically_by attribute. Instead of
# linked_statically_by attribute in each cc_library we will have the
# attribute exported_by which will restrict exportability.
"static_deps": attr.string_list(),
"user_link_flags": attr.string_list(),
"visibility_file": attr.label(allow_single_file = True),
"_cc_toolchain": attr.label(default = "@bazel_tools//tools/cpp:current_cc_toolchain"),
},
toolchains = ["@rules_cc//cc:toolchain_type"], # copybara-use-repo-external-label
fragments = ["cpp"],
)