Skip to content

Commit c5490ce

Browse files
author
Chad Norvell
committed
Support custom output configurations
This change adds two main features: 1. You can configure compile commands for certain targets to go into separate files For embedded systems development in particular, `clangd` doesn't work well with `compile_commands.json` generated for multiple targets. For example, if you have a build target that runs on your host machine in one configuration and another that runs on device in another configuration, `compile_commands.json` will contain multiple conflicting compile commands for the same source files. `clangd` will just use the first one it finds, which may not be the one you want to use for code intelligence. It's convenient to have separate compile commands files for each target, and switch the file `clangd` uses depending on how you want to navigate your code. By providing the `target_file_names` argument, you can associate targets with names. Separate compile commands files will be generated for each of the specified targets, and will be placed in subdirectories with the specified names. Compile commands for any targets that aren't specified in `target_file_names` will go into the main compile commands file, just like before. 2. You can specify a different output path If you are generating multiple compile commands files, its preferable not to output them into the workspace root. So you can specify a separate output path, relative to the workspace root. This patch doesn't change any existing behavior; if you don't add either of the new arguments to your invocation of `refresh_compile_commands`, everything will work exactly as it did before.
1 parent a14ad3a commit c5490ce

File tree

2 files changed

+78
-13
lines changed

2 files changed

+78
-13
lines changed

refresh.template.py

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,20 +1405,60 @@ def main():
14051405
# End: template filled by Bazel
14061406
]
14071407

1408-
compile_command_entries = []
1408+
target_file_names = {
1409+
# Begin: template filled by Bazel
1410+
{target_file_names}
1411+
# End: template filled by Bazel
1412+
}
1413+
1414+
# Associates compilation database file names with lists of compile commands.
1415+
# __all__ is a special case: It's the "catch all" for any compile commands that aren't
1416+
# assigned to a specific file. If no targets are assigned to specific files (i.e., if
1417+
# target_file_names is empty), all compile commands will go into __all__ and end up
1418+
# in one file.
1419+
compile_command_sets = {
1420+
'__all__': []
1421+
}
1422+
14091423
for (target, flags) in target_flag_pairs:
1410-
compile_command_entries.extend(_get_commands(target, flags))
1424+
# If the target has a specific file name assigned, put the compile commands in their
1425+
# own set, to be written to their own file.
1426+
if target in target_file_names:
1427+
target_name = target_file_names[target]
1428+
compile_command_sets[target_name] = list(_get_commands(target, flags))
1429+
# Otherwise, put them into the main file.
1430+
else:
1431+
compile_command_sets['__all__'].extend(_get_commands(target, flags))
14111432

1412-
if not compile_command_entries:
1433+
if len(compile_command_sets) <= 1 and len(compile_command_sets['__all__']) == 0:
14131434
log_error(""">>> Not (over)writing compile_commands.json, since no commands were extracted and an empty file is of no use.
14141435
There should be actionable warnings, above, that led to this.""")
14151436
sys.exit(1)
14161437

1438+
1439+
if not (root_dir := pathlib.Path({out_dir})).exists():
1440+
root_dir.mkdir(parents=True)
1441+
14171442
# Chain output into compile_commands.json
1418-
with open('compile_commands.json', 'w') as output_file:
1419-
json.dump(
1420-
compile_command_entries,
1421-
output_file,
1422-
indent=2, # Yay, human readability!
1423-
check_circular=False # For speed.
1424-
)
1443+
for target_name in compile_command_sets:
1444+
# If the target doesn't have a specified file name, put it into the "catch all"
1445+
# compilation database.
1446+
if target_name == '__all__':
1447+
file_path = root_dir / "compile_commands.json"
1448+
# Otherwise, write the database to the specific target file.
1449+
else:
1450+
target_dir = root_dir / target_name
1451+
target_dir.mkdir(exist_ok=True)
1452+
file_path = target_dir / "compile_commands.json"
1453+
1454+
# This condition is only relevant to __all__. If each specified target has a specified
1455+
# file name, there won't be any compile commands in __all__, and we shouldn't write
1456+
# a file with an empty array.
1457+
if (len(compile_command_sets[target_name]) > 0):
1458+
with open(file_path, 'w') as output_file:
1459+
json.dump(
1460+
compile_command_sets[target_name],
1461+
output_file,
1462+
indent=2, # Yay, human readability!
1463+
check_circular=False # For speed.
1464+
)

refresh_compile_commands.bzl

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ refresh_compile_commands(
3636
# ^ excluding headers will speed up compile_commands.json generation *considerably* because we won't need to preprocess your code to figure out which headers you use.
3737
# However, if you use clangd and are looking for speed, we strongly recommend you follow the instructions below instead, since clangd is going to regularly infer the wrong commands for headers and give you lots of annoyingly unnecessary red squigglies.
3838
39+
# Need to create separate files for specific targets? Give those targets a name and their compile commands file will be written into a subdirectory with that name.
40+
# target_file_names = {
41+
# "//:host_build": "host",
42+
# "//:target_build": "target",
43+
# }
44+
45+
# Need to write compile commands to some directory other than the workspace root? Provide a path relative to the workspace root.
46+
# out_dir = ".compile_commands"
47+
3948
# Need things to run faster? [Either for compile_commands.json generation or clangd indexing.]
4049
# First: You might be able to refresh compile_commands.json slightly less often, making the current runtime okay.
4150
# If you're adding files, clangd should make pretty decent guesses at completions, using commands from nearby files. And if you're deleting files, there's not a problem. So you may not need to rerun refresh.py on every change to BUILD files. Instead, maybe refresh becomes something you run every so often when you can spare the time, making the current runtime okay.
@@ -62,6 +71,8 @@ load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
6271
def refresh_compile_commands(
6372
name,
6473
targets = None,
74+
target_file_names = None,
75+
out_dir = None,
6576
exclude_headers = None,
6677
exclude_external_sources = False,
6778
**kwargs): # For the other common attributes. Tags, compatible_with, etc. https://docs.bazel.build/versions/main/be/common-definitions.html#common-attributes.
@@ -78,9 +89,15 @@ def refresh_compile_commands(
7889
elif type(targets) != "dict": # Assume they've supplied a single string/label and wrap it
7990
targets = {targets: ""}
8091

81-
# Make any package-relative labels absolute
8292
targets = {
83-
target if target.startswith("/") or target.startswith("@") else "{}//{}:{}".format(native.repository_name(), native.package_name(), target.removeprefix(":")): flags for target, flags in targets.items()
93+
_make_label_absolute(target): flags for target, flags in targets.items()
94+
}
95+
96+
if not target_file_names:
97+
target_file_names = {}
98+
99+
target_file_names = {
100+
_make_label_absolute(target): file_names for target, file_names in target_file_names.items()
84101
}
85102

86103
# Create a wrapper script that prints a helpful error message if the python version is too old, generated from check_python_version.template.py
@@ -89,7 +106,7 @@ def refresh_compile_commands(
89106

90107
# Generate the core, runnable python script from refresh.template.py
91108
script_name = name + ".py"
92-
_expand_template(name = script_name, labels_to_flags = targets, exclude_headers = exclude_headers, exclude_external_sources = exclude_external_sources, **kwargs)
109+
_expand_template(name = script_name, labels_to_flags = targets, labels_to_file_names = target_file_names, out_dir = out_dir, exclude_headers = exclude_headers, exclude_external_sources = exclude_external_sources, **kwargs)
93110

94111
# Combine them so the wrapper calls the main script
95112
native.py_binary(
@@ -101,6 +118,10 @@ def refresh_compile_commands(
101118
**kwargs
102119
)
103120

121+
def _make_label_absolute(label):
122+
# Make any package-relative labels absolute
123+
return label if label.startswith("/") or label.startswith("@") else "{}//{}:{}".format(native.repository_name(), native.package_name(), label.removeprefix(":"))
124+
104125
def _expand_template_impl(ctx):
105126
"""Inject targets of interest--and other settings--into refresh.template.py, and set it up to be run."""
106127
script = ctx.actions.declare_file(ctx.attr.name)
@@ -111,7 +132,9 @@ def _expand_template_impl(ctx):
111132
substitutions = {
112133
# Note, don't delete whitespace. Correctly doing multiline indenting.
113134
" {target_flag_pairs}": "\n".join([" {},".format(pair) for pair in ctx.attr.labels_to_flags.items()]),
135+
" {target_file_names}": "\n".join([" '{}': '{}',".format(target, file_name) for (target, file_name) in ctx.attr.labels_to_file_names.items()]),
114136
" {windows_default_include_paths}": "\n".join([" %r," % path for path in find_cpp_toolchain(ctx).built_in_include_directories]), # find_cpp_toolchain is from https://docs.bazel.build/versions/main/integrating-with-rules-cc.html
137+
"{out_dir}": repr(ctx.attr.out_dir),
115138
"{exclude_headers}": repr(ctx.attr.exclude_headers),
116139
"{exclude_external_sources}": repr(ctx.attr.exclude_external_sources),
117140
"{print_args_executable}": repr(ctx.executable._print_args_executable.path),
@@ -122,6 +145,8 @@ def _expand_template_impl(ctx):
122145
_expand_template = rule(
123146
attrs = {
124147
"labels_to_flags": attr.string_dict(mandatory = True), # string keys instead of label_keyed because Bazel doesn't support parsing wildcard target patterns (..., *, :all) in BUILD attributes.
148+
"labels_to_file_names": attr.string_dict(),
149+
"out_dir": attr.string(default = "."),
125150
"exclude_external_sources": attr.bool(default = False),
126151
"exclude_headers": attr.string(values = ["all", "external", ""]), # "" needed only for compatibility with Bazel < 3.6.0
127152
"_script_template": attr.label(allow_single_file = True, default = "refresh.template.py"),

0 commit comments

Comments
 (0)