• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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