blob: 5252c9bedfd31c5bdeea8c4a9721f4c9646007eb [file] [log] [blame]
David L. Jones46710ca2022-04-20 16:26:44 -07001# 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
David L. Jones586b72c2022-05-24 12:31:56 -07006################################################################################
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
David L. Jones46710ca2022-04-20 16:26:44 -070026# Creates an action to build the `output_file` static library (archive)
27# using `object_files`.
28def _create_archive_action(
29 ctx,
30 feature_configuration,
David L. Jones586b72c2022-05-24 12:31:56 -070031 cc_toolchain_info,
David L. Jones46710ca2022-04-20 16:26:44 -070032 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,
David L. Jones586b72c2022-05-24 12:31:56 -070039 cc_toolchain = cc_toolchain_info,
David L. Jones46710ca2022-04-20 16:26:44 -070040 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 = [
David L. Jones586b72c2022-05-24 12:31:56 -070071 cc_toolchain_info.all_files,
David L. Jones46710ca2022-04-20 16:26:44 -070072 ],
73 ),
David L. Jones4e5b9202022-05-16 16:11:09 -070074 use_default_shell_env = False,
David L. Jones46710ca2022-04-20 16:26:44 -070075 outputs = [output_file],
76 mnemonic = "CppArchiveDist",
77 )
78
David L. Jones586b72c2022-05-24 12:31:56 -070079def _create_dso_link_action(
80 ctx,
81 feature_configuration,
82 cc_toolchain_info,
83 object_files,
84 pic_object_files):
David L. Jones46710ca2022-04-20 16:26:44 -070085 compilation_outputs = cc_common.create_compilation_outputs(
David L. Jones586b72c2022-05-24 12:31:56 -070086 objects = depset(object_files),
87 pic_objects = depset(pic_object_files),
David L. Jones46710ca2022-04-20 16:26:44 -070088 )
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
David L. Jones586b72c2022-05-24 12:31:56 -0700100 outputs = []
101
David L. Jones46710ca2022-04-20 16:26:44 -0700102 # 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
David L. Jones586b72c2022-05-24 12:31:56 -0700115 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):
Adam Cozzetteb1465192022-10-25 14:40:38 +0000137 return depset(transitive = [
138 target.files
139 for target in targets
Adam Cozzette5ca84002023-11-08 15:36:33 -0800140 # 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")
Adam Cozzetteb1465192022-10-25 14:40:38 +0000145 ])
David L. Jones586b72c2022-05-24 12:31:56 -0700146
Adam Cozzetteb1465192022-10-25 14:40:38 +0000147def _get_transitive_sources(targets, attr, deps):
148 return depset(targets, transitive = [getattr(dep[CcFileList], attr) for dep in deps if CcFileList in dep])
David L. Jones586b72c2022-05-24 12:31:56 -0700149
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(
Adam Cozzetteb1465192022-10-25 14:40:38 +0000174 hdrs = _get_transitive_sources(
Adam Cozzette77aa9132023-10-21 09:37:33 -0700175 _flatten_target_files(getattr(rule_attr, "hdrs", [])).to_list(),
Adam Cozzetteb1465192022-10-25 14:40:38 +0000176 "hdrs",
177 rule_attr.deps,
178 ),
179 textual_hdrs = _get_transitive_sources(
Adam Cozzette77aa9132023-10-21 09:37:33 -0700180 _flatten_target_files(getattr(rule_attr, "textual_hdrs", [])).to_list(),
David L. Jones586b72c2022-05-24 12:31:56 -0700181 "textual_hdrs",
Adam Cozzetteb1465192022-10-25 14:40:38 +0000182 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),
David L. Jones586b72c2022-05-24 12:31:56 -0700190 )]
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""",
Adam Cozzetteb1465192022-10-25 14:40:38 +0000215 required_providers = [CcInfo],
David L. Jones586b72c2022-05-24 12:31:56 -0700216 implementation = _cc_file_list_aspect_impl,
Adam Cozzetteb1465192022-10-25 14:40:38 +0000217 attr_aspects = ["deps"],
David L. Jones586b72c2022-05-24 12:31:56 -0700218)
219
220################################################################################
221# Rule impl
222################################################################################
223
224def _collect_inputs(deps):
Adam Cozzetteb1465192022-10-25 14:40:38 +0000225 """Collects files from a list of deps.
David L. Jones586b72c2022-05-24 12:31:56 -0700226
Adam Cozzetteb1465192022-10-25 14:40:38 +0000227 This rule collects source files and linker inputs transitively for C++
228 deps.
David L. Jones586b72c2022-05-24 12:31:56 -0700229
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:
Adam Cozzetteb1465192022-10-25 14:40:38 +0000240 deps: Iterable of immediate deps, which will be treated as roots to
241 recurse transitively.
David L. Jones586b72c2022-05-24 12:31:56 -0700242 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
Adam Cozzetteb1465192022-10-25 14:40:38 +0000283# 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 to_remove = {e: None for e in getattr(b.cc_file_list, field).to_list()}
297 cc_file_list_args[field] = depset(
298 [e for e in getattr(a.cc_file_list, field).to_list() if not e in to_remove],
299 )
300 result_args["cc_file_list"] = CcFileList(**cc_file_list_args)
301
302 return struct(**result_args)
303
David L. Jones586b72c2022-05-24 12:31:56 -0700304# Implementation for cc_dist_library rule.
305def _cc_dist_library_impl(ctx):
306 cc_toolchain_info = find_cc_toolchain(ctx)
307
308 feature_configuration = cc_common.configure_features(
309 ctx = ctx,
310 cc_toolchain = cc_toolchain_info,
311 )
312
Adam Cozzetteb1465192022-10-25 14:40:38 +0000313 inputs = _subtract_files(_collect_inputs(ctx.attr.deps), _collect_inputs(ctx.attr.dist_deps))
David L. Jones586b72c2022-05-24 12:31:56 -0700314
315 # For static libraries, build separately with and without pic.
316
317 stemname = "lib" + ctx.label.name
318 outputs = []
319
320 if len(inputs.objects) > 0:
321 archive_out = ctx.actions.declare_file(stemname + ".a")
322 _create_archive_action(
323 ctx,
324 feature_configuration,
325 cc_toolchain_info,
326 archive_out,
327 inputs.objects,
328 )
329 outputs.append(archive_out)
330
331 if len(inputs.pic_objects) > 0:
332 pic_archive_out = ctx.actions.declare_file(stemname + ".pic.a")
333 _create_archive_action(
334 ctx,
335 feature_configuration,
336 cc_toolchain_info,
337 pic_archive_out,
338 inputs.pic_objects,
339 )
340 outputs.append(pic_archive_out)
341
342 # For dynamic libraries, use the `cc_common.link` command to ensure
343 # everything gets built correctly according to toolchain definitions.
344 outputs.extend(_create_dso_link_action(
345 ctx,
346 feature_configuration,
347 cc_toolchain_info,
348 inputs.objects,
349 inputs.pic_objects,
350 ))
351
David L. Jones46710ca2022-04-20 16:26:44 -0700352 # We could expose the libraries for use from cc rules:
353 #
354 # linking_context = cc_common.create_linking_context(
355 # linker_inputs = depset([
356 # cc_common.create_linker_input(
357 # owner = ctx.label,
358 # libraries = depset([library_to_link]),
359 # ),
360 # ]),
361 # )
362 # cc_info = CcInfo(linking_context = linking_context) # and return this
363 #
364 # However, if the goal is to force a protobuf dependency to use the
365 # DSO, then `cc_import` is a better-supported way to do so.
366 #
367 # If we wanted to expose CcInfo from this rule (and make it usable as a
368 # C++ dependency), then we would probably want to include the static
369 # archive and headers as well. exposing headers would probably require
370 # an additional aspect to extract CcInfos with just the deps' headers.
371
372 return [
373 DefaultInfo(files = depset(outputs)),
David L. Jones586b72c2022-05-24 12:31:56 -0700374 inputs.cc_file_list,
David L. Jones46710ca2022-04-20 16:26:44 -0700375 ]
376
377cc_dist_library = rule(
378 implementation = _cc_dist_library_impl,
379 doc = """
380Create libraries suitable for distribution.
381
382This rule creates static and dynamic libraries from the libraries listed in
383'deps'. The resulting libraries are suitable for distributing all of 'deps'
384in a single logical library, for example, in an installable binary package.
Adam Cozzetteb1465192022-10-25 14:40:38 +0000385The result includes all transitive dependencies, excluding those reachable
386from 'dist_deps' or defined in a separate repository (e.g. Abseil).
David L. Jones46710ca2022-04-20 16:26:44 -0700387
388The outputs of this rule are a dynamic library and a static library. (If
389the build produces both PIC and non-PIC object files, then there is also a
390second static library.) The example below illustrates additional details.
391
Adam Cozzetteb1465192022-10-25 14:40:38 +0000392This rule is different from Bazel's experimental `shared_cc_library` in two
393ways. First, this rule produces a static archive library in addition to the
394dynamic shared library. Second, this rule is not directly usable as a C++
395dependency (although the outputs could be used, e.g., by `cc_import`).
David L. Jones46710ca2022-04-20 16:26:44 -0700396
397Example:
398
399 cc_library(name = "a", srcs = ["a.cc"], hdrs = ["a.h"])
400 cc_library(name = "b", srcs = ["b.cc"], hdrs = ["b.h"], deps = [":a"])
401 cc_library(name = "c", srcs = ["c.cc"], hdrs = ["c.h"], deps = [":b"])
402
403 # Creates libdist.so and (typically) libdist.pic.a:
404 # (This may also produce libdist.a if the build produces non-PIC objects.)
405 cc_dist_library(
406 name = "dist",
407 linkopts = ["-la"], # libdist.so dynamically links against liba.so.
Adam Cozzetteb1465192022-10-25 14:40:38 +0000408 deps = [":b", ":c"], # Output contains a.o, b.o, and c.o.
David L. Jones46710ca2022-04-20 16:26:44 -0700409 )
410""",
411 attrs = {
412 "deps": attr.label_list(
Adam Cozzetteb1465192022-10-25 14:40:38 +0000413 doc = ("The list of libraries to be included in the outputs, " +
414 "along with their transitive dependencies."),
415 aspects = [cc_file_list_aspect],
416 ),
417 "dist_deps": attr.label_list(
418 doc = ("The list of cc_dist_library dependencies that " +
419 "should be excluded."),
David L. Jones586b72c2022-05-24 12:31:56 -0700420 aspects = [cc_file_list_aspect],
David L. Jones46710ca2022-04-20 16:26:44 -0700421 ),
422 "linkopts": attr.string_list(
423 doc = ("Add these flags to the C++ linker command when creating " +
424 "the dynamic library."),
425 ),
426 # C++ toolchain before https://github.com/bazelbuild/bazel/issues/7260:
427 "_cc_toolchain": attr.label(
428 default = Label("@rules_cc//cc:current_cc_toolchain"),
429 ),
430 },
431 toolchains = [
432 # C++ toolchain after https://github.com/bazelbuild/bazel/issues/7260:
433 "@bazel_tools//tools/cpp:toolchain_type",
434 ],
435 fragments = ["cpp"],
436)