Add support for generating a compile_commands.json file for clang-tidy
Change-Id: I3db1a69030f6ba6617ccaf3a1ebc319111145fe1
diff --git a/tools/actions/BUILD b/tools/actions/BUILD
new file mode 100644
index 0000000..2362307
--- /dev/null
+++ b/tools/actions/BUILD
@@ -0,0 +1,30 @@
+py_binary(
+ name = 'generate_compile_command',
+ srcs = [
+ 'generate_compile_command.py',
+ ],
+ deps = [
+ '//third_party/bazel:extra_actions_proto_py',
+ ],
+)
+
+action_listener(
+ name = 'generate_compile_commands_listener',
+ visibility = ['//visibility:public'],
+ mnemonics = [
+ 'CppCompile',
+ ],
+ extra_actions = [':generate_compile_commands_action'],
+)
+
+extra_action(
+ name = 'generate_compile_commands_action',
+ tools = [
+ ':generate_compile_command',
+ ],
+ out_templates = [
+ '$(ACTION_ID)_compile_command',
+ ],
+ cmd = '$(location :generate_compile_command) $(EXTRA_ACTION_FILE)' +
+ ' $(output $(ACTION_ID)_compile_command)',
+)
diff --git a/tools/actions/generate_compile_command.py b/tools/actions/generate_compile_command.py
new file mode 100755
index 0000000..b41f2fc
--- /dev/null
+++ b/tools/actions/generate_compile_command.py
@@ -0,0 +1,27 @@
+# This is the implementation of a Bazel extra_action which genenerates
+# _compile_command files for generate_compile_commands.py to consume.
+
+import sys
+
+import third_party.bazel.protos.extra_actions_base_pb2 as extra_actions_base_pb2
+
+def _get_cpp_command(cpp_compile_info):
+ compiler = cpp_compile_info.tool
+ options = ' '.join(cpp_compile_info.compiler_option)
+ source = cpp_compile_info.source_file
+ output = cpp_compile_info.output_file
+ return '%s %s -c %s -o %s' % (compiler, options, source, output), source
+
+def main(argv):
+ action = extra_actions_base_pb2.ExtraActionInfo()
+ with open(argv[1], 'rb') as f:
+ action.MergeFromString(f.read())
+ command, source_file = _get_cpp_command(
+ action.Extensions[extra_actions_base_pb2.CppCompileInfo.cpp_compile_info])
+ with open(argv[2], 'w') as f:
+ f.write(command)
+ f.write('\0')
+ f.write(source_file)
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv))
diff --git a/tools/actions/generate_compile_commands_json.py b/tools/actions/generate_compile_commands_json.py
new file mode 100755
index 0000000..d867698
--- /dev/null
+++ b/tools/actions/generate_compile_commands_json.py
@@ -0,0 +1,68 @@
+#!/usr/bin/python3
+
+# This reads the _compile_command files :generate_compile_commands_action
+# generates a outputs a compile_commands.json file at the top of the source
+# tree for things like clang-tidy to read.
+
+# Overall usage directions: run bazel with
+# --experimental_action_listener=//tools/actions:generate_compile_commands_listener
+# for all the files you want to use clang-tidy with and then run this script.
+# Afer that, `clang-tidy build_tests/gflags.cc` should work.
+
+import sys
+import pathlib
+import os.path
+import subprocess
+
+'''
+Args:
+ path: The pathlib.Path to _compile_command file.
+ command_directory: The directory commands are run from.
+
+Returns a string to stick in compile_commands.json.
+'''
+def _get_command(path, command_directory):
+ with path.open('r') as f:
+ contents = f.read().split('\0')
+ if len(contents) != 2:
+ # Old/incomplete file or something; silently ignore it.
+ return None
+ return '''{
+ "directory": "%s",
+ "command": "%s",
+ "file": "%s",
+ },''' % (command_directory, contents[0].replace('"', '\\"'), contents[1])
+
+'''
+Args:
+ path: A directory pathlib.Path to look for _compile_command files under.
+ command_directory: The directory commands are run from.
+
+Yields strings to stick in compile_commands.json.
+'''
+def _get_compile_commands(path, command_directory):
+ for f in path.iterdir():
+ if f.is_dir():
+ yield from _get_compile_commands(f, command_directory)
+ elif f.name.endswith('_compile_command'):
+ command = _get_command(f, command_directory)
+ if command:
+ yield command
+
+def main(argv):
+ source_path = os.path.join(os.path.dirname(__file__), '../..')
+ action_outs = os.path.join(source_path,
+ 'bazel-bin/../extra_actions',
+ 'tools/actions/generate_compile_commands_action')
+ command_directory = subprocess.check_output(
+ ('bazel', 'info', 'execution_root'),
+ cwd=source_path).decode('utf-8').rstrip()
+ commands = _get_compile_commands(pathlib.Path(action_outs), command_directory)
+ with open(os.path.join(source_path, 'compile_commands.json'), 'w') as f:
+ f.write('[')
+ for command in commands:
+ f.write(command)
+ f.write(']')
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv))