Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions cc/toolchains/cc_coverage_config.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Copyright 2025 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Rules to configure cc coverage collection."""

load("//cc/toolchains/impl:collect.bzl", "collect_data")
load(":cc_toolchain_info.bzl", "CoverageConfigInfo")

visibility("public")

def _cc_coverage_config_impl(ctx):
exe_info = ctx.attr.src[DefaultInfo]
if exe_info.files_to_run != None and exe_info.files_to_run.executable != None:
exe = exe_info.files_to_run.executable
elif len(exe_info.files.to_list()) == 1:
exe = exe_info.files.to_list()[0]
else:
fail("Expected cc_coverage_config's src attribute to be either an executable or a single file")

runfiles = collect_data(ctx, ctx.attr.data + [ctx.attr.src])
config = CoverageConfigInfo(
label = ctx.label,
type = ctx.attr.type,
exe = exe,
runfiles = runfiles,
)

link = ctx.actions.declare_file(ctx.label.name)
ctx.actions.symlink(
output = link,
target_file = exe,
is_executable = True,
)
return [
config,
# This isn't required, but now we can do "bazel run <config>", which can
# be very helpful when debugging toolchains.
DefaultInfo(
files = depset([link]),
runfiles = runfiles,
executable = link,
),
]

cc_coverage_config = rule(
implementation = _cc_coverage_config_impl,
attrs = {
"type": attr.string(
mandatory = True,
values = [
"gcov",
"llvm-cov",
],
doc = """
The type of coverage this config is for (e.g., gcov).
"""
),
"src": attr.label(
mandatory = True,
allow_files = True,
cfg = "exec",
doc = """
The tool to collect coverage with.
"""
),
"data": attr.label_list(
mandatory = False,
allow_files = True,
doc = """
Additional files that are required for this coverage config to run.
""",
),
},
doc = """
Defines the configuration to collect CC coverage.

Example:
```
load("//cc/toolchains:cc_coverage_config.bzl", "cc_coverage_config")

cc_coverage_config(
name = "gcov",
type = "gcov",
src = "bin/gcov",
)
```
""",
provides = [
CoverageConfigInfo,
],
)
14 changes: 13 additions & 1 deletion cc/toolchains/cc_toolchain_info.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,21 @@ ToolchainConfigInfo = provider(
"enabled_features": "(Sequence[FeatureInfo]) The features That are enabled by default for this toolchain",
"tool_map": "(ToolConfigInfo) A provider mapping toolchain action types to tools.",
"args": "(Sequence[ArgsInfo]) A list of arguments to be unconditionally applied to the toolchain.",
"artifact_name_patterns": "Sequence[ArtifactNamePatternInfo] A artifact name patterns for this toolchain",
"artifact_name_patterns": "Sequence[ArtifactNamePatternInfo] The artifact name patterns for this toolchain",
"coverage_config": "(CoverageConfigInfo) The coverage configuration for this toolchain.",
"make_variables": "Sequence[MakeVariableInfo] Make variable substitutions for this toolchain",
"files": "(dict[ActionTypeInfo, depset[File]]) Files required for the toolchain, keyed by the action type.",
"allowlist_include_directories": "(depset[DirectoryInfo]) Built-in include directories implied by this toolchain's args and tools that should be allowlisted in Bazel's include checker",
},
)

CoverageConfigInfo = provider(
doc = "A type of coverage (eg. gcov)",
# @unsorted-dict-items
fields = {
"label": "(Label) The label defining this provider. Place in error messages to simplify debugging",
"type": "(CoverageTypeInfo) A provider defining the type of coverage config",
"exe": "(File) The file corresponding to the coverage tool",
"runfiles": "(runfiles) The files required to run the coverage tool",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's tool path only, would I be correct in assuming that runfiles aren't accessible, and args don't do anything?

You might find it significantly simplifies things to just make the API as simple as:

cc_toolchain_config(
    name = "coverage_tools",
    ...,
    gcov = "@gcov_prebuilt//:gcov",
    llvm = "@llvm_prebuilt//:llvm_cov",
)

Defining coverage types probably doesn't do too much for you - we defined action types because they were a useful link between args and tools, but since you don't have args, it doesn't seem too useful.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, just double checking that coverage tools do indeed run on the target platform, not the exec platform? Fairly certain that should be the case, but just making sure because my above proposal won't work well if they run on the exec platform.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, just double checking that coverage tools do indeed run on the target platform, not the exec platform? Fairly certain that should be the case, but just making sure because my above proposal won't work well if they run on the exec platform.

The platform on which tests run on is unfortunately a bit tricky. One can make a case for the target platform (e.g., using x86_64 machine to build an arm64 binary, and then the the test only runs on the target platform). One can also make a case for the exec platform (e.g., I build an Android unit test, which has target Android, but the test runs in a simulator so the "test platform" is the exec platform).

FWIW, _collect_cc_coverage uses the exec platform [1]. I think for consistency, we should stick with that.

[1] https://cs.opensource.google/bazel/bazel/+/master:src/main/starlark/builtins_bzl/common/cc/semantics.bzl;l=79;drc=ea29e8177757286733d32b91304ab9f53974a138

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, if that's the case, then we should do the following instead:

coverage_tools(
    name = "coverage_tools",
    gcov = "@gcov_prebuilt//:gcov",
    llvm = "@llvm_prebuilt//:llvm_cov",
)

cc_toolchain_config(
    name = "coverage_tools",
    # The coverage attribute has cfg = "exec". This allows coverage_tools to select({}) on the exec platform.
    coverage_tools = ":coverage_tools",
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's tool path only, would I be correct in assuming that runfiles aren't accessible, and args don't do anything?

You might find it significantly simplifies things to just make the API as simple as:

cc_toolchain_config(
    name = "coverage_tools",
    ...,
    gcov = "@gcov_prebuilt//:gcov",
    llvm = "@llvm_prebuilt//:llvm_cov",
)

Defining coverage types probably doesn't do too much for you - we defined action types because they were a useful link between args and tools, but since you don't have args, it doesn't seem too useful.

I'm not aware of a way to pass args to gcov via the toolchain today. However, runfiles are accessible (in our private repo, we call runfiles.files and pass that to cc_toolchain.all_files. I prototyped with this PR internally, and the files from cc_coverage_config are available).

bazel coverage --test_env=VERBOSE_COVERAGE=1 //my/cc/target has the following in its test.log:

...
+ touch /mnt/engflow/worker/work/0/exec/bazel-out/k8-fastbuild/testlogs/<redacted>/coverage.dat
+ cd /mnt/engflow/worker/work/0/exec
+ [[ -n external/bazel_tools/tools/test/collect_cc_coverage.sh ]]
+ eval external/bazel_tools/tools/test/collect_cc_coverage.sh
++ external/bazel_tools/tools/test/collect_cc_coverage.sh
+ main
+ init_gcov
+ GCOV=/mnt/engflow/worker/work/0/exec/bazel-out/k8-fastbuild/testlogs/<redacted>/_coverage/gcov
+ '[' '!' -f platforms/debian11_x64/cc/gcov.sh ']'
++ cd platforms/debian11_x64/cc
++ pwd
...

So theoretically, this could work. However, I can see it becoming useful in the future (and hopefully providing a path to clean up the madness of magic coverage collector shell scripts there is).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, if that's the case, then we should do the following instead:

coverage_tools(
    name = "coverage_tools",
    gcov = "@gcov_prebuilt//:gcov",
    llvm = "@llvm_prebuilt//:llvm_cov",
)

cc_toolchain_config(
    name = "coverage_tools",
    # The coverage attribute has cfg = "exec". This allows coverage_tools to select({}) on the exec platform.
    coverage_tools = ":coverage_tools",
)

I don't think it makes sense to be able to specify multiple coverage collectors?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be fine change cc_coverage_config.type to attr.string(values = ["gcov", "llvm-cov", "..."]), although I think it might become useful in the future to have a target for that (e.g., to pass _collect_cc_coverage, which generally depends only on the type of the tool, not the exact tool).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think attr.string is fine. If a tool wants to use the coverage type later, they can also use an attr.string, and they can get the current tool's type from the cc_toolchain_config rule directly.

},
)
3 changes: 3 additions & 0 deletions cc/toolchains/impl/toolchain_config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ load(
"ActionTypeSetInfo",
"ArgsListInfo",
"ArtifactNamePatternInfo",
"CoverageConfigInfo",
"FeatureSetInfo",
"MakeVariableInfo",
"ToolConfigInfo",
Expand Down Expand Up @@ -61,6 +62,7 @@ def _cc_toolchain_config_impl(ctx):
tool_map = ctx.attr.tool_map,
args = ctx.attr.args,
artifact_name_patterns = ctx.attr.artifact_name_patterns,
coverage_config = ctx.attr.coverage_config,
make_variables = ctx.attr.make_variables,
)

Expand Down Expand Up @@ -110,6 +112,7 @@ cc_toolchain_config = rule(
"known_features": attr.label_list(providers = [FeatureSetInfo]),
"enabled_features": attr.label_list(providers = [FeatureSetInfo]),
"artifact_name_patterns": attr.label_list(providers = [ArtifactNamePatternInfo]),
"coverage_config": attr.label_list(providers = [CoverageConfigInfo]),
"make_variables": attr.label_list(providers = [MakeVariableInfo]),
"_builtin_features": attr.label(default = "//cc/toolchains/features:all_builtin_features"),
},
Expand Down
5 changes: 3 additions & 2 deletions cc/toolchains/impl/toolchain_config_info.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.
"""Helper functions to create and validate a ToolchainConfigInfo."""

load("//cc/toolchains:cc_toolchain_info.bzl", "ArtifactNamePatternInfo", "MakeVariableInfo", "ToolConfigInfo", "ToolchainConfigInfo")
load("//cc/toolchains:cc_toolchain_info.bzl", "ArtifactNamePatternInfo", "CoverageConfigInfo", "MakeVariableInfo", "ToolConfigInfo", "ToolchainConfigInfo")
load(":args_utils.bzl", "get_action_type")
load(":collect.bzl", "collect_args_lists", "collect_features")

Expand Down Expand Up @@ -162,7 +162,7 @@ def _collect_make_variables(targets, fail):

return make_variables.values()

def toolchain_config_info(label, known_features = [], enabled_features = [], args = [], artifact_name_patterns = [], make_variables = [], tool_map = None, fail = fail):
def toolchain_config_info(label, known_features = [], enabled_features = [], args = [], artifact_name_patterns = [], coverage_config = None, make_variables = [], tool_map = None, fail = fail):
"""Generates and validates a ToolchainConfigInfo from lists of labels.

Args:
Expand Down Expand Up @@ -214,6 +214,7 @@ def toolchain_config_info(label, known_features = [], enabled_features = [], arg
files = files,
allowlist_include_directories = allowlist_include_directories,
artifact_name_patterns = _collect_artifact_name_patterns(artifact_name_patterns, fail),
coverage_config = coverage_config[CoverageConfigInfo] if coverage_config else None,
make_variables = _collect_make_variables(make_variables, fail),
)
_validate_toolchain(toolchain_config, fail = fail)
Expand Down
2 changes: 2 additions & 0 deletions cc/toolchains/toolchain.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def cc_toolchain(
tool_map = None,
args = [],
artifact_name_patterns = [],
coverage_config = None,
make_variables = [],
known_features = [],
enabled_features = [],
Expand Down Expand Up @@ -171,6 +172,7 @@ def cc_toolchain(
tool_map = tool_map,
args = args,
artifact_name_patterns = artifact_name_patterns,
coverage_config = coverage_config,
make_variables = make_variables,
known_features = known_features,
enabled_features = enabled_features,
Expand Down