aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Roeder <tmroeder@google.com>2018-12-18 17:49:07 -0500
committerMasahiro Yamada <yamada.masahiro@socionext.com>2018-12-19 09:41:36 -0500
commitb30204640192234d18f9168f19f9cd693485b86d (patch)
tree6bdf4e832f439c814af9369dbeaaac0a32d380ae
parent61a0902a06d602377cb6526deacfe4f0a7eade73 (diff)
scripts: add a tool to produce a compile_commands.json file
The LLVM/Clang project provides many tools for analyzing C source code. Many of these tools are based on LibTooling (https://clang.llvm.org/docs/LibTooling.html), which depends on a database of compiler flags. The standard container for this database is compile_commands.json, which consists of a list of JSON objects, each with "directory", "file", and "command" fields. Some build systems, like cmake or bazel, produce this compilation information directly. Naturally, Makefiles don't. However, the kernel makefiles already create .<target>.o.cmd files that contain all the information needed to build a compile_commands.json file. So, this commit adds scripts/gen_compile_commands.py, which recursively searches through a directory for .<target>.o.cmd files and extracts appropriate compile commands from them. It writes a compile_commands.json file that LibTooling-based tools can use. By default, gen_compile_commands.py starts its search in its working directory and (over)writes compile_commands.json in the working directory. However, it also supports --output and --directory flags for out-of-tree use. Note that while gen_compile_commands.py enables the use of clang-based tools, it does not require the kernel to be compiled with clang. E.g., the following sequence of commands produces a compile_commands.json file that works correctly with LibTooling. make defconfig make scripts/gen_compile_commands.py Also note that this script is written to work correctly in both Python 2 and Python 3, so it does not specify the Python version in its first line. For an example of the utility of this script: after running gen_compile_commands.json on the latest kernel version, I was able to use Vim + the YouCompleteMe pluging + clangd to automatically jump to definitions and declarations. Obviously, cscope and ctags provide some of this functionality; the advantage of supporting LibTooling is that it opens the door to many other clang-based tools that understand the code directly and do not rely on regular expressions and heuristics. Tested: Built several recent kernel versions and ran the script against them, testing tools like clangd (for editor/LSP support) and clang-check (for static analysis). Also extracted some test .cmd files from a kernel build and wrote a test script to check that the script behaved correctly with all permutations of the --output and --directory flags. Signed-off-by: Tom Roeder <tmroeder@google.com> Signed-off-by: Masahiro Yamada <yamada.masahiro@socionext.com>
-rwxr-xr-xscripts/gen_compile_commands.py151
1 files changed, 151 insertions, 0 deletions
diff --git a/scripts/gen_compile_commands.py b/scripts/gen_compile_commands.py
new file mode 100755
index 000000000000..7915823b92a5
--- /dev/null
+++ b/scripts/gen_compile_commands.py
@@ -0,0 +1,151 @@
1#!/usr/bin/env python
2# SPDX-License-Identifier: GPL-2.0
3#
4# Copyright (C) Google LLC, 2018
5#
6# Author: Tom Roeder <tmroeder@google.com>
7#
8"""A tool for generating compile_commands.json in the Linux kernel."""
9
10import argparse
11import json
12import logging
13import os
14import re
15
16_DEFAULT_OUTPUT = 'compile_commands.json'
17_DEFAULT_LOG_LEVEL = 'WARNING'
18
19_FILENAME_PATTERN = r'^\..*\.cmd$'
20_LINE_PATTERN = r'^cmd_[^ ]*\.o := (.* )([^ ]*\.c)$'
21_VALID_LOG_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
22
23# A kernel build generally has over 2000 entries in its compile_commands.json
24# database. If this code finds 500 or fewer, then warn the user that they might
25# not have all the .cmd files, and they might need to compile the kernel.
26_LOW_COUNT_THRESHOLD = 500
27
28
29def parse_arguments():
30 """Sets up and parses command-line arguments.
31
32 Returns:
33 log_level: A logging level to filter log output.
34 directory: The directory to search for .cmd files.
35 output: Where to write the compile-commands JSON file.
36 """
37 usage = 'Creates a compile_commands.json database from kernel .cmd files'
38 parser = argparse.ArgumentParser(description=usage)
39
40 directory_help = ('Path to the kernel source directory to search '
41 '(defaults to the working directory)')
42 parser.add_argument('-d', '--directory', type=str, help=directory_help)
43
44 output_help = ('The location to write compile_commands.json (defaults to '
45 'compile_commands.json in the search directory)')
46 parser.add_argument('-o', '--output', type=str, help=output_help)
47
48 log_level_help = ('The level of log messages to produce (one of ' +
49 ', '.join(_VALID_LOG_LEVELS) + '; defaults to ' +
50 _DEFAULT_LOG_LEVEL + ')')
51 parser.add_argument(
52 '--log_level', type=str, default=_DEFAULT_LOG_LEVEL,
53 help=log_level_help)
54
55 args = parser.parse_args()
56
57 log_level = args.log_level
58 if log_level not in _VALID_LOG_LEVELS:
59 raise ValueError('%s is not a valid log level' % log_level)
60
61 directory = args.directory or os.getcwd()
62 output = args.output or os.path.join(directory, _DEFAULT_OUTPUT)
63 directory = os.path.abspath(directory)
64
65 return log_level, directory, output
66
67
68def process_line(root_directory, file_directory, command_prefix, relative_path):
69 """Extracts information from a .cmd line and creates an entry from it.
70
71 Args:
72 root_directory: The directory that was searched for .cmd files. Usually
73 used directly in the "directory" entry in compile_commands.json.
74 file_directory: The path to the directory the .cmd file was found in.
75 command_prefix: The extracted command line, up to the last element.
76 relative_path: The .c file from the end of the extracted command.
77 Usually relative to root_directory, but sometimes relative to
78 file_directory and sometimes neither.
79
80 Returns:
81 An entry to append to compile_commands.
82
83 Raises:
84 ValueError: Could not find the extracted file based on relative_path and
85 root_directory or file_directory.
86 """
87 # The .cmd files are intended to be included directly by Make, so they
88 # escape the pound sign '#', either as '\#' or '$(pound)' (depending on the
89 # kernel version). The compile_commands.json file is not interepreted
90 # by Make, so this code replaces the escaped version with '#'.
91 prefix = command_prefix.replace('\#', '#').replace('$(pound)', '#')
92
93 cur_dir = root_directory
94 expected_path = os.path.join(cur_dir, relative_path)
95 if not os.path.exists(expected_path):
96 # Try using file_directory instead. Some of the tools have a different
97 # style of .cmd file than the kernel.
98 cur_dir = file_directory
99 expected_path = os.path.join(cur_dir, relative_path)
100 if not os.path.exists(expected_path):
101 raise ValueError('File %s not in %s or %s' %
102 (relative_path, root_directory, file_directory))
103 return {
104 'directory': cur_dir,
105 'file': relative_path,
106 'command': prefix + relative_path,
107 }
108
109
110def main():
111 """Walks through the directory and finds and parses .cmd files."""
112 log_level, directory, output = parse_arguments()
113
114 level = getattr(logging, log_level)
115 logging.basicConfig(format='%(levelname)s: %(message)s', level=level)
116
117 filename_matcher = re.compile(_FILENAME_PATTERN)
118 line_matcher = re.compile(_LINE_PATTERN)
119
120 compile_commands = []
121 for dirpath, _, filenames in os.walk(directory):
122 for filename in filenames:
123 if not filename_matcher.match(filename):
124 continue
125 filepath = os.path.join(dirpath, filename)
126
127 with open(filepath, 'rt') as f:
128 for line in f:
129 result = line_matcher.match(line)
130 if not result:
131 continue
132
133 try:
134 entry = process_line(directory, dirpath,
135 result.group(1), result.group(2))
136 compile_commands.append(entry)
137 except ValueError as err:
138 logging.info('Could not add line from %s: %s',
139 filepath, err)
140
141 with open(output, 'wt') as f:
142 json.dump(compile_commands, f, indent=2, sort_keys=True)
143
144 count = len(compile_commands)
145 if count < _LOW_COUNT_THRESHOLD:
146 logging.warning(
147 'Found %s entries. Have you compiled the kernel?', count)
148
149
150if __name__ == '__main__':
151 main()