| # Rules for distributable C++ libraries | |
| load("@rules_cc//cc:action_names.bzl", cc_action_names = "ACTION_NAMES") | |
| load("@rules_cc//cc:find_cc_toolchain.bzl", "find_cc_toolchain") | |
| load("@rules_cc//cc/common:cc_common.bzl", "cc_common") | |
| load("@rules_cc//cc/common:cc_info.bzl", "CcInfo") | |
| ################################################################################ | |
| # Archive/linking support | |
| ################################################################################ | |
| def _collect_linker_input_objects(dep_label, cc_info, objs, pic_objs): | |
| """Accumulate .o and .pic.o files into `objs` and `pic_objs`.""" | |
| link_ctx = cc_info.linking_context | |
| if link_ctx == None: | |
| return | |
| linker_inputs = link_ctx.linker_inputs.to_list() | |
| for link_input in linker_inputs: | |
| if link_input.owner != dep_label: | |
| # This is a transitive dep: skip it. | |
| continue | |
| for lib in link_input.libraries: | |
| objs.extend(lib.objects or []) | |
| pic_objs.extend(lib.pic_objects or []) | |
| # Creates an action to build the `output_file` static library (archive) | |
| # using `object_files`. | |
| def _create_archive_action( | |
| ctx, | |
| feature_configuration, | |
| cc_toolchain_info, | |
| output_file, | |
| object_files): | |
| # Based on Bazel's src/main/starlark/builtins_bzl/common/cc/cc_import.bzl: | |
| # Build the command line and add args for all of the input files: | |
| archiver_variables = cc_common.create_link_variables( | |
| feature_configuration = feature_configuration, | |
| cc_toolchain = cc_toolchain_info, | |
| output_file = output_file.path, | |
| is_using_linker = False, | |
| ) | |
| command_line = cc_common.get_memory_inefficient_command_line( | |
| feature_configuration = feature_configuration, | |
| action_name = cc_action_names.cpp_link_static_library, | |
| variables = archiver_variables, | |
| ) | |
| args = ctx.actions.args() | |
| args.add_all(command_line) | |
| args.add_all(object_files) | |
| args.use_param_file("@%s", use_always = True) | |
| archiver_path = cc_common.get_tool_for_action( | |
| feature_configuration = feature_configuration, | |
| action_name = cc_action_names.cpp_link_static_library, | |
| ) | |
| env = cc_common.get_environment_variables( | |
| feature_configuration = feature_configuration, | |
| action_name = cc_action_names.cpp_link_static_library, | |
| variables = archiver_variables, | |
| ) | |
| ctx.actions.run( | |
| executable = archiver_path, | |
| arguments = [args], | |
| env = env, | |
| inputs = depset( | |
| direct = object_files, | |
| transitive = [ | |
| cc_toolchain_info.all_files, | |
| ], | |
| ), | |
| use_default_shell_env = False, | |
| outputs = [output_file], | |
| mnemonic = "CppArchiveDist", | |
| ) | |
| def _create_dso_link_action( | |
| ctx, | |
| feature_configuration, | |
| cc_toolchain_info, | |
| object_files, | |
| pic_object_files): | |
| compilation_outputs = cc_common.create_compilation_outputs( | |
| objects = depset(object_files), | |
| pic_objects = depset(pic_object_files), | |
| ) | |
| link_output = cc_common.link( | |
| actions = ctx.actions, | |
| feature_configuration = feature_configuration, | |
| cc_toolchain = cc_toolchain_info, | |
| compilation_outputs = compilation_outputs, | |
| name = ctx.label.name, | |
| output_type = "dynamic_library", | |
| user_link_flags = ctx.attr.linkopts, | |
| ) | |
| library_to_link = link_output.library_to_link | |
| outputs = [] | |
| # Note: library_to_link.dynamic_library and interface_library are often | |
| # symlinks in the solib directory. For DefaultInfo, prefer reporting | |
| # the resolved artifact paths. | |
| if library_to_link.resolved_symlink_dynamic_library != None: | |
| outputs.append(library_to_link.resolved_symlink_dynamic_library) | |
| elif library_to_link.dynamic_library != None: | |
| outputs.append(library_to_link.dynamic_library) | |
| if library_to_link.resolved_symlink_interface_library != None: | |
| outputs.append(library_to_link.resolved_symlink_interface_library) | |
| elif library_to_link.interface_library != None: | |
| outputs.append(library_to_link.interface_library) | |
| return outputs | |
| ################################################################################ | |
| # Source file/header support | |
| ################################################################################ | |
| CcFileList = provider( | |
| doc = "List of files to be built into a library.", | |
| fields = { | |
| # As a rule of thumb, `hdrs` and `textual_hdrs` are the files that | |
| # would be installed along with a prebuilt library. | |
| "hdrs": "public header files, including those used by generated code", | |
| "textual_hdrs": "files which are included but are not self-contained", | |
| # The `internal_hdrs` are header files which appear in `srcs`. | |
| # These are only used when compiling the library. | |
| "internal_hdrs": "internal header files (only used to build .cc files)", | |
| "srcs": "source files", | |
| }, | |
| ) | |
| def _flatten_target_files(targets): | |
| return depset(transitive = [ | |
| target.files | |
| for target in targets | |
| # Filter out targets from external workspaces. We also filter out | |
| # utf8_range since that has a separate CMake build for now. | |
| if (target.label.workspace_name == "" or | |
| target.label.workspace_name == "com_google_protobuf") and | |
| not target.label.package.startswith("third_party/utf8_range") | |
| ]) | |
| def _get_transitive_sources(targets, attr, deps): | |
| return depset(targets, transitive = [getattr(dep[CcFileList], attr) for dep in deps if CcFileList in dep]) | |
| def _cc_file_list_aspect_impl(target, ctx): | |
| # Extract sources from a `cc_library` (or similar): | |
| if CcInfo not in target: | |
| return [] | |
| # We're going to reach directly into the attrs on the traversed rule. | |
| rule_attr = ctx.rule.attr | |
| # CcInfo is a proxy for what we expect this rule to look like. | |
| # However, some deps may expose `CcInfo` without having `srcs`, | |
| # `hdrs`, etc., so we use `getattr` to handle that gracefully. | |
| internal_hdrs = [] | |
| srcs = [] | |
| # Filter `srcs` so it only contains source files. Headers will go | |
| # into `internal_headers`. | |
| for src in _flatten_target_files(getattr(rule_attr, "srcs", [])).to_list(): | |
| if src.extension.lower() in ["c", "cc", "cpp", "cxx"]: | |
| srcs.append(src) | |
| else: | |
| internal_hdrs.append(src) | |
| return [CcFileList( | |
| hdrs = _get_transitive_sources( | |
| _flatten_target_files(getattr(rule_attr, "hdrs", [])).to_list(), | |
| "hdrs", | |
| rule_attr.deps, | |
| ), | |
| textual_hdrs = _get_transitive_sources( | |
| _flatten_target_files(getattr(rule_attr, "textual_hdrs", [])).to_list(), | |
| "textual_hdrs", | |
| rule_attr.deps, | |
| ), | |
| internal_hdrs = _get_transitive_sources( | |
| internal_hdrs, | |
| "internal_hdrs", | |
| rule_attr.deps, | |
| ), | |
| srcs = _get_transitive_sources(srcs, "srcs", rule_attr.deps), | |
| )] | |
| cc_file_list_aspect = aspect( | |
| doc = """ | |
| Aspect to provide the list of sources and headers from a rule. | |
| Output is CcFileList. Example: | |
| cc_library( | |
| name = "foo", | |
| srcs = [ | |
| "foo.cc", | |
| "foo_internal.h", | |
| ], | |
| hdrs = ["foo.h"], | |
| textual_hdrs = ["foo_inl.inc"], | |
| ) | |
| # produces: | |
| # CcFileList( | |
| # hdrs = depset([File("foo.h")]), | |
| # textual_hdrs = depset([File("foo_inl.inc")]), | |
| # internal_hdrs = depset([File("foo_internal.h")]), | |
| # srcs = depset([File("foo.cc")]), | |
| # ) | |
| """, | |
| required_providers = [CcInfo], | |
| implementation = _cc_file_list_aspect_impl, | |
| attr_aspects = ["deps"], | |
| ) | |
| ################################################################################ | |
| # Rule impl | |
| ################################################################################ | |
| def _collect_inputs(deps): | |
| """Collects files from a list of deps. | |
| This rule collects source files and linker inputs transitively for C++ | |
| deps. | |
| The return value is a struct with object files (linker inputs), | |
| partitioned by PIC and non-pic, and the rules' source and header files: | |
| struct( | |
| objects = ..., # non-PIC object files | |
| pic_objects = ..., # PIC objects | |
| cc_file_list = ..., # a CcFileList | |
| ) | |
| Args: | |
| deps: Iterable of immediate deps, which will be treated as roots to | |
| recurse transitively. | |
| Returns: | |
| A struct with linker inputs, source files, and header files. | |
| """ | |
| objs = [] | |
| pic_objs = [] | |
| # The returned CcFileList will contain depsets of the deps' file lists. | |
| # These lists hold `depset()`s from each of `deps`. | |
| srcs = [] | |
| hdrs = [] | |
| internal_hdrs = [] | |
| textual_hdrs = [] | |
| for dep in deps: | |
| if CcInfo in dep: | |
| _collect_linker_input_objects( | |
| dep.label, | |
| dep[CcInfo], | |
| objs, | |
| pic_objs, | |
| ) | |
| if CcFileList in dep: | |
| cfl = dep[CcFileList] | |
| srcs.append(cfl.srcs) | |
| hdrs.append(cfl.hdrs) | |
| internal_hdrs.append(cfl.internal_hdrs) | |
| textual_hdrs.append(cfl.textual_hdrs) | |
| return struct( | |
| objects = objs, | |
| pic_objects = pic_objs, | |
| cc_file_list = CcFileList( | |
| srcs = depset(transitive = srcs), | |
| hdrs = depset(transitive = hdrs), | |
| internal_hdrs = depset(transitive = internal_hdrs), | |
| textual_hdrs = depset(transitive = textual_hdrs), | |
| ), | |
| ) | |
| # Given structs a and b returned from _collect_inputs(), returns a copy of a | |
| # but with all files from b subtracted out. | |
| def _subtract_files(a, b): | |
| result_args = {} | |
| top_level_fields = ["objects", "pic_objects"] | |
| for field in top_level_fields: | |
| to_remove = {e: None for e in getattr(b, field)} | |
| result_args[field] = [e for e in getattr(a, field) if not e in to_remove] | |
| cc_file_list_args = {} | |
| file_list_fields = ["srcs", "hdrs", "internal_hdrs", "textual_hdrs"] | |
| for field in file_list_fields: | |
| # only a subset of file.cc is used from protoc, to get all its symbols for tests we need to | |
| # also build & link it to tests. | |
| to_remove = {e: None for e in getattr(b.cc_file_list, field).to_list() if "src/google/protobuf/testing/file.cc" not in e.path} | |
| cc_file_list_args[field] = depset( | |
| [e for e in getattr(a.cc_file_list, field).to_list() if not e in to_remove], | |
| ) | |
| result_args["cc_file_list"] = CcFileList(**cc_file_list_args) | |
| return struct(**result_args) | |
| # Implementation for cc_dist_library rule. | |
| def _cc_dist_library_impl(ctx): | |
| cc_toolchain_info = find_cc_toolchain(ctx) | |
| feature_configuration = cc_common.configure_features( | |
| ctx = ctx, | |
| cc_toolchain = cc_toolchain_info, | |
| ) | |
| inputs = _subtract_files(_collect_inputs(ctx.attr.deps), _collect_inputs(ctx.attr.dist_deps)) | |
| # For static libraries, build separately with and without pic. | |
| stemname = "lib" + ctx.label.name | |
| outputs = [] | |
| if len(inputs.objects) > 0: | |
| archive_out = ctx.actions.declare_file(stemname + ".a") | |
| _create_archive_action( | |
| ctx, | |
| feature_configuration, | |
| cc_toolchain_info, | |
| archive_out, | |
| inputs.objects, | |
| ) | |
| outputs.append(archive_out) | |
| if len(inputs.pic_objects) > 0: | |
| pic_archive_out = ctx.actions.declare_file(stemname + ".pic.a") | |
| _create_archive_action( | |
| ctx, | |
| feature_configuration, | |
| cc_toolchain_info, | |
| pic_archive_out, | |
| inputs.pic_objects, | |
| ) | |
| outputs.append(pic_archive_out) | |
| # For dynamic libraries, use the `cc_common.link` command to ensure | |
| # everything gets built correctly according to toolchain definitions. | |
| outputs.extend(_create_dso_link_action( | |
| ctx, | |
| feature_configuration, | |
| cc_toolchain_info, | |
| inputs.objects, | |
| inputs.pic_objects, | |
| )) | |
| # We could expose the libraries for use from cc rules: | |
| # | |
| # linking_context = cc_common.create_linking_context( | |
| # linker_inputs = depset([ | |
| # cc_common.create_linker_input( | |
| # owner = ctx.label, | |
| # libraries = depset([library_to_link]), | |
| # ), | |
| # ]), | |
| # ) | |
| # cc_info = CcInfo(linking_context = linking_context) # and return this | |
| # | |
| # However, if the goal is to force a protobuf dependency to use the | |
| # DSO, then `cc_import` is a better-supported way to do so. | |
| # | |
| # If we wanted to expose CcInfo from this rule (and make it usable as a | |
| # C++ dependency), then we would probably want to include the static | |
| # archive and headers as well. exposing headers would probably require | |
| # an additional aspect to extract CcInfos with just the deps' headers. | |
| return [ | |
| DefaultInfo(files = depset(outputs)), | |
| inputs.cc_file_list, | |
| ] | |
| cc_dist_library = rule( | |
| implementation = _cc_dist_library_impl, | |
| doc = """ | |
| Create libraries suitable for distribution. | |
| This rule creates static and dynamic libraries from the libraries listed in | |
| 'deps'. The resulting libraries are suitable for distributing all of 'deps' | |
| in a single logical library, for example, in an installable binary package. | |
| The result includes all transitive dependencies, excluding those reachable | |
| from 'dist_deps' or defined in a separate repository (e.g. Abseil). | |
| The outputs of this rule are a dynamic library and a static library. (If | |
| the build produces both PIC and non-PIC object files, then there is also a | |
| second static library.) The example below illustrates additional details. | |
| This rule is different from Bazel's experimental `shared_cc_library` in two | |
| ways. First, this rule produces a static archive library in addition to the | |
| dynamic shared library. Second, this rule is not directly usable as a C++ | |
| dependency (although the outputs could be used, e.g., by `cc_import`). | |
| Example: | |
| cc_library(name = "a", srcs = ["a.cc"], hdrs = ["a.h"]) | |
| cc_library(name = "b", srcs = ["b.cc"], hdrs = ["b.h"], deps = [":a"]) | |
| cc_library(name = "c", srcs = ["c.cc"], hdrs = ["c.h"], deps = [":b"]) | |
| # Creates libdist.so and (typically) libdist.pic.a: | |
| # (This may also produce libdist.a if the build produces non-PIC objects.) | |
| cc_dist_library( | |
| name = "dist", | |
| linkopts = ["-la"], # libdist.so dynamically links against liba.so. | |
| deps = [":b", ":c"], # Output contains a.o, b.o, and c.o. | |
| ) | |
| """, | |
| attrs = { | |
| "deps": attr.label_list( | |
| doc = ("The list of libraries to be included in the outputs, " + | |
| "along with their transitive dependencies."), | |
| aspects = [cc_file_list_aspect], | |
| ), | |
| "dist_deps": attr.label_list( | |
| doc = ("The list of cc_dist_library dependencies that " + | |
| "should be excluded."), | |
| aspects = [cc_file_list_aspect], | |
| ), | |
| "linkopts": attr.string_list( | |
| doc = ("Add these flags to the C++ linker command when creating " + | |
| "the dynamic library."), | |
| ), | |
| # C++ toolchain before https://github.com/bazelbuild/bazel/issues/7260: | |
| "_cc_toolchain": attr.label( | |
| default = Label("@rules_cc//cc:current_cc_toolchain"), | |
| ), | |
| }, | |
| toolchains = [ | |
| # C++ toolchain after https://github.com/bazelbuild/bazel/issues/7260: | |
| "@bazel_tools//tools/cpp:toolchain_type", | |
| ], | |
| fragments = ["cpp"], | |
| ) |