1# Rules for distributable C++ libraries 2 3load("@rules_cc//cc:action_names.bzl", cc_action_names = "ACTION_NAMES") 4load("@rules_cc//cc:find_cc_toolchain.bzl", "find_cc_toolchain") 5 6################################################################################ 7# Archive/linking support 8################################################################################ 9 10def _collect_linker_input_objects(dep_label, cc_info, objs, pic_objs): 11 """Accumulate .o and .pic.o files into `objs` and `pic_objs`.""" 12 link_ctx = cc_info.linking_context 13 if link_ctx == None: 14 return 15 16 linker_inputs = link_ctx.linker_inputs.to_list() 17 for link_input in linker_inputs: 18 if link_input.owner != dep_label: 19 # This is a transitive dep: skip it. 20 continue 21 22 for lib in link_input.libraries: 23 objs.extend(lib.objects or []) 24 pic_objs.extend(lib.pic_objects or []) 25 26# Creates an action to build the `output_file` static library (archive) 27# using `object_files`. 28def _create_archive_action( 29 ctx, 30 feature_configuration, 31 cc_toolchain_info, 32 output_file, 33 object_files): 34 # Based on Bazel's src/main/starlark/builtins_bzl/common/cc/cc_import.bzl: 35 36 # Build the command line and add args for all of the input files: 37 archiver_variables = cc_common.create_link_variables( 38 feature_configuration = feature_configuration, 39 cc_toolchain = cc_toolchain_info, 40 output_file = output_file.path, 41 is_using_linker = False, 42 ) 43 command_line = cc_common.get_memory_inefficient_command_line( 44 feature_configuration = feature_configuration, 45 action_name = cc_action_names.cpp_link_static_library, 46 variables = archiver_variables, 47 ) 48 args = ctx.actions.args() 49 args.add_all(command_line) 50 args.add_all(object_files) 51 args.use_param_file("@%s", use_always = True) 52 53 archiver_path = cc_common.get_tool_for_action( 54 feature_configuration = feature_configuration, 55 action_name = cc_action_names.cpp_link_static_library, 56 ) 57 58 env = cc_common.get_environment_variables( 59 feature_configuration = feature_configuration, 60 action_name = cc_action_names.cpp_link_static_library, 61 variables = archiver_variables, 62 ) 63 64 ctx.actions.run( 65 executable = archiver_path, 66 arguments = [args], 67 env = env, 68 inputs = depset( 69 direct = object_files, 70 transitive = [ 71 cc_toolchain_info.all_files, 72 ], 73 ), 74 use_default_shell_env = False, 75 outputs = [output_file], 76 mnemonic = "CppArchiveDist", 77 ) 78 79def _create_dso_link_action( 80 ctx, 81 feature_configuration, 82 cc_toolchain_info, 83 object_files, 84 pic_object_files): 85 compilation_outputs = cc_common.create_compilation_outputs( 86 objects = depset(object_files), 87 pic_objects = depset(pic_object_files), 88 ) 89 link_output = cc_common.link( 90 actions = ctx.actions, 91 feature_configuration = feature_configuration, 92 cc_toolchain = cc_toolchain_info, 93 compilation_outputs = compilation_outputs, 94 name = ctx.label.name, 95 output_type = "dynamic_library", 96 user_link_flags = ctx.attr.linkopts, 97 ) 98 library_to_link = link_output.library_to_link 99 100 outputs = [] 101 102 # Note: library_to_link.dynamic_library and interface_library are often 103 # symlinks in the solib directory. For DefaultInfo, prefer reporting 104 # the resolved artifact paths. 105 if library_to_link.resolved_symlink_dynamic_library != None: 106 outputs.append(library_to_link.resolved_symlink_dynamic_library) 107 elif library_to_link.dynamic_library != None: 108 outputs.append(library_to_link.dynamic_library) 109 110 if library_to_link.resolved_symlink_interface_library != None: 111 outputs.append(library_to_link.resolved_symlink_interface_library) 112 elif library_to_link.interface_library != None: 113 outputs.append(library_to_link.interface_library) 114 115 return outputs 116 117################################################################################ 118# Source file/header support 119################################################################################ 120 121CcFileList = provider( 122 doc = "List of files to be built into a library.", 123 fields = { 124 # As a rule of thumb, `hdrs` and `textual_hdrs` are the files that 125 # would be installed along with a prebuilt library. 126 "hdrs": "public header files, including those used by generated code", 127 "textual_hdrs": "files which are included but are not self-contained", 128 129 # The `internal_hdrs` are header files which appear in `srcs`. 130 # These are only used when compiling the library. 131 "internal_hdrs": "internal header files (only used to build .cc files)", 132 "srcs": "source files", 133 }, 134) 135 136def _flatten_target_files(targets): 137 return depset(transitive = [ 138 target.files 139 for target in targets 140 # Filter out targets from external workspaces. We also filter out 141 # utf8_range since that has a separate CMake build for now. 142 if (target.label.workspace_name == "" or 143 target.label.workspace_name == "com_google_protobuf") and 144 not target.label.package.startswith("third_party/utf8_range") 145 ]) 146 147def _get_transitive_sources(targets, attr, deps): 148 return depset(targets, transitive = [getattr(dep[CcFileList], attr) for dep in deps if CcFileList in dep]) 149 150def _cc_file_list_aspect_impl(target, ctx): 151 # Extract sources from a `cc_library` (or similar): 152 if CcInfo not in target: 153 return [] 154 155 # We're going to reach directly into the attrs on the traversed rule. 156 rule_attr = ctx.rule.attr 157 158 # CcInfo is a proxy for what we expect this rule to look like. 159 # However, some deps may expose `CcInfo` without having `srcs`, 160 # `hdrs`, etc., so we use `getattr` to handle that gracefully. 161 162 internal_hdrs = [] 163 srcs = [] 164 165 # Filter `srcs` so it only contains source files. Headers will go 166 # into `internal_headers`. 167 for src in _flatten_target_files(getattr(rule_attr, "srcs", [])).to_list(): 168 if src.extension.lower() in ["c", "cc", "cpp", "cxx"]: 169 srcs.append(src) 170 else: 171 internal_hdrs.append(src) 172 173 return [CcFileList( 174 hdrs = _get_transitive_sources( 175 _flatten_target_files(getattr(rule_attr, "hdrs", [])).to_list(), 176 "hdrs", 177 rule_attr.deps, 178 ), 179 textual_hdrs = _get_transitive_sources( 180 _flatten_target_files(getattr(rule_attr, "textual_hdrs", [])).to_list(), 181 "textual_hdrs", 182 rule_attr.deps, 183 ), 184 internal_hdrs = _get_transitive_sources( 185 internal_hdrs, 186 "internal_hdrs", 187 rule_attr.deps, 188 ), 189 srcs = _get_transitive_sources(srcs, "srcs", rule_attr.deps), 190 )] 191 192cc_file_list_aspect = aspect( 193 doc = """ 194Aspect to provide the list of sources and headers from a rule. 195 196Output is CcFileList. Example: 197 198 cc_library( 199 name = "foo", 200 srcs = [ 201 "foo.cc", 202 "foo_internal.h", 203 ], 204 hdrs = ["foo.h"], 205 textual_hdrs = ["foo_inl.inc"], 206 ) 207 # produces: 208 # CcFileList( 209 # hdrs = depset([File("foo.h")]), 210 # textual_hdrs = depset([File("foo_inl.inc")]), 211 # internal_hdrs = depset([File("foo_internal.h")]), 212 # srcs = depset([File("foo.cc")]), 213 # ) 214""", 215 required_providers = [CcInfo], 216 implementation = _cc_file_list_aspect_impl, 217 attr_aspects = ["deps"], 218) 219 220################################################################################ 221# Rule impl 222################################################################################ 223 224def _collect_inputs(deps): 225 """Collects files from a list of deps. 226 227 This rule collects source files and linker inputs transitively for C++ 228 deps. 229 230 The return value is a struct with object files (linker inputs), 231 partitioned by PIC and non-pic, and the rules' source and header files: 232 233 struct( 234 objects = ..., # non-PIC object files 235 pic_objects = ..., # PIC objects 236 cc_file_list = ..., # a CcFileList 237 ) 238 239 Args: 240 deps: Iterable of immediate deps, which will be treated as roots to 241 recurse transitively. 242 Returns: 243 A struct with linker inputs, source files, and header files. 244 """ 245 246 objs = [] 247 pic_objs = [] 248 249 # The returned CcFileList will contain depsets of the deps' file lists. 250 # These lists hold `depset()`s from each of `deps`. 251 srcs = [] 252 hdrs = [] 253 internal_hdrs = [] 254 textual_hdrs = [] 255 256 for dep in deps: 257 if CcInfo in dep: 258 _collect_linker_input_objects( 259 dep.label, 260 dep[CcInfo], 261 objs, 262 pic_objs, 263 ) 264 265 if CcFileList in dep: 266 cfl = dep[CcFileList] 267 srcs.append(cfl.srcs) 268 hdrs.append(cfl.hdrs) 269 internal_hdrs.append(cfl.internal_hdrs) 270 textual_hdrs.append(cfl.textual_hdrs) 271 272 return struct( 273 objects = objs, 274 pic_objects = pic_objs, 275 cc_file_list = CcFileList( 276 srcs = depset(transitive = srcs), 277 hdrs = depset(transitive = hdrs), 278 internal_hdrs = depset(transitive = internal_hdrs), 279 textual_hdrs = depset(transitive = textual_hdrs), 280 ), 281 ) 282 283# Given structs a and b returned from _collect_inputs(), returns a copy of a 284# but with all files from b subtracted out. 285def _subtract_files(a, b): 286 result_args = {} 287 288 top_level_fields = ["objects", "pic_objects"] 289 for field in top_level_fields: 290 to_remove = {e: None for e in getattr(b, field)} 291 result_args[field] = [e for e in getattr(a, field) if not e in to_remove] 292 293 cc_file_list_args = {} 294 file_list_fields = ["srcs", "hdrs", "internal_hdrs", "textual_hdrs"] 295 for field in file_list_fields: 296 # only a subset of file.cc is used from protoc, to get all its symbols for tests we need to 297 # also build & link it to tests. 298 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} 299 cc_file_list_args[field] = depset( 300 [e for e in getattr(a.cc_file_list, field).to_list() if not e in to_remove], 301 ) 302 result_args["cc_file_list"] = CcFileList(**cc_file_list_args) 303 304 return struct(**result_args) 305 306# Implementation for cc_dist_library rule. 307def _cc_dist_library_impl(ctx): 308 cc_toolchain_info = find_cc_toolchain(ctx) 309 310 feature_configuration = cc_common.configure_features( 311 ctx = ctx, 312 cc_toolchain = cc_toolchain_info, 313 ) 314 315 inputs = _subtract_files(_collect_inputs(ctx.attr.deps), _collect_inputs(ctx.attr.dist_deps)) 316 317 # For static libraries, build separately with and without pic. 318 319 stemname = "lib" + ctx.label.name 320 outputs = [] 321 322 if len(inputs.objects) > 0: 323 archive_out = ctx.actions.declare_file(stemname + ".a") 324 _create_archive_action( 325 ctx, 326 feature_configuration, 327 cc_toolchain_info, 328 archive_out, 329 inputs.objects, 330 ) 331 outputs.append(archive_out) 332 333 if len(inputs.pic_objects) > 0: 334 pic_archive_out = ctx.actions.declare_file(stemname + ".pic.a") 335 _create_archive_action( 336 ctx, 337 feature_configuration, 338 cc_toolchain_info, 339 pic_archive_out, 340 inputs.pic_objects, 341 ) 342 outputs.append(pic_archive_out) 343 344 # For dynamic libraries, use the `cc_common.link` command to ensure 345 # everything gets built correctly according to toolchain definitions. 346 outputs.extend(_create_dso_link_action( 347 ctx, 348 feature_configuration, 349 cc_toolchain_info, 350 inputs.objects, 351 inputs.pic_objects, 352 )) 353 354 # We could expose the libraries for use from cc rules: 355 # 356 # linking_context = cc_common.create_linking_context( 357 # linker_inputs = depset([ 358 # cc_common.create_linker_input( 359 # owner = ctx.label, 360 # libraries = depset([library_to_link]), 361 # ), 362 # ]), 363 # ) 364 # cc_info = CcInfo(linking_context = linking_context) # and return this 365 # 366 # However, if the goal is to force a protobuf dependency to use the 367 # DSO, then `cc_import` is a better-supported way to do so. 368 # 369 # If we wanted to expose CcInfo from this rule (and make it usable as a 370 # C++ dependency), then we would probably want to include the static 371 # archive and headers as well. exposing headers would probably require 372 # an additional aspect to extract CcInfos with just the deps' headers. 373 374 return [ 375 DefaultInfo(files = depset(outputs)), 376 inputs.cc_file_list, 377 ] 378 379cc_dist_library = rule( 380 implementation = _cc_dist_library_impl, 381 doc = """ 382Create libraries suitable for distribution. 383 384This rule creates static and dynamic libraries from the libraries listed in 385'deps'. The resulting libraries are suitable for distributing all of 'deps' 386in a single logical library, for example, in an installable binary package. 387The result includes all transitive dependencies, excluding those reachable 388from 'dist_deps' or defined in a separate repository (e.g. Abseil). 389 390The outputs of this rule are a dynamic library and a static library. (If 391the build produces both PIC and non-PIC object files, then there is also a 392second static library.) The example below illustrates additional details. 393 394This rule is different from Bazel's experimental `shared_cc_library` in two 395ways. First, this rule produces a static archive library in addition to the 396dynamic shared library. Second, this rule is not directly usable as a C++ 397dependency (although the outputs could be used, e.g., by `cc_import`). 398 399Example: 400 401 cc_library(name = "a", srcs = ["a.cc"], hdrs = ["a.h"]) 402 cc_library(name = "b", srcs = ["b.cc"], hdrs = ["b.h"], deps = [":a"]) 403 cc_library(name = "c", srcs = ["c.cc"], hdrs = ["c.h"], deps = [":b"]) 404 405 # Creates libdist.so and (typically) libdist.pic.a: 406 # (This may also produce libdist.a if the build produces non-PIC objects.) 407 cc_dist_library( 408 name = "dist", 409 linkopts = ["-la"], # libdist.so dynamically links against liba.so. 410 deps = [":b", ":c"], # Output contains a.o, b.o, and c.o. 411 ) 412""", 413 attrs = { 414 "deps": attr.label_list( 415 doc = ("The list of libraries to be included in the outputs, " + 416 "along with their transitive dependencies."), 417 aspects = [cc_file_list_aspect], 418 ), 419 "dist_deps": attr.label_list( 420 doc = ("The list of cc_dist_library dependencies that " + 421 "should be excluded."), 422 aspects = [cc_file_list_aspect], 423 ), 424 "linkopts": attr.string_list( 425 doc = ("Add these flags to the C++ linker command when creating " + 426 "the dynamic library."), 427 ), 428 # C++ toolchain before https://github.com/bazelbuild/bazel/issues/7260: 429 "_cc_toolchain": attr.label( 430 default = Label("@rules_cc//cc:current_cc_toolchain"), 431 ), 432 }, 433 toolchains = [ 434 # C++ toolchain after https://github.com/bazelbuild/bazel/issues/7260: 435 "@bazel_tools//tools/cpp:toolchain_type", 436 ], 437 fragments = ["cpp"], 438) 439