blob: 92ea0a2ee59e1ce6112250a2d42777d269819c9a [file] [log] [blame]
Tom Roederb3020462018-12-18 14:49:07 -08001#!/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
Masahiro Yamadaecca4fe2020-08-22 23:56:15 +090015import subprocess
Tom Roederb3020462018-12-18 14:49:07 -080016
17_DEFAULT_OUTPUT = 'compile_commands.json'
18_DEFAULT_LOG_LEVEL = 'WARNING'
19
20_FILENAME_PATTERN = r'^\..*\.cmd$'
21_LINE_PATTERN = r'^cmd_[^ ]*\.o := (.* )([^ ]*\.c)$'
22_VALID_LOG_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
23
24# A kernel build generally has over 2000 entries in its compile_commands.json
Masahiro Yamadacb369552019-07-27 12:01:10 +090025# database. If this code finds 300 or fewer, then warn the user that they might
Tom Roederb3020462018-12-18 14:49:07 -080026# not have all the .cmd files, and they might need to compile the kernel.
Masahiro Yamadacb369552019-07-27 12:01:10 +090027_LOW_COUNT_THRESHOLD = 300
Tom Roederb3020462018-12-18 14:49:07 -080028
29
30def parse_arguments():
31 """Sets up and parses command-line arguments.
32
33 Returns:
34 log_level: A logging level to filter log output.
Masahiro Yamada0a7d3762020-08-22 23:56:12 +090035 directory: The work directory where the objects were built.
Masahiro Yamadaecca4fe2020-08-22 23:56:15 +090036 ar: Command used for parsing .a archives.
Tom Roederb3020462018-12-18 14:49:07 -080037 output: Where to write the compile-commands JSON file.
Masahiro Yamadaecca4fe2020-08-22 23:56:15 +090038 paths: The list of files/directories to handle to find .cmd files.
Tom Roederb3020462018-12-18 14:49:07 -080039 """
40 usage = 'Creates a compile_commands.json database from kernel .cmd files'
41 parser = argparse.ArgumentParser(description=usage)
42
Masahiro Yamada0a7d3762020-08-22 23:56:12 +090043 directory_help = ('specify the output directory used for the kernel build '
Tom Roederb3020462018-12-18 14:49:07 -080044 '(defaults to the working directory)')
Masahiro Yamada6fca36f2020-08-22 23:56:13 +090045 parser.add_argument('-d', '--directory', type=str, default='.',
46 help=directory_help)
Tom Roederb3020462018-12-18 14:49:07 -080047
Masahiro Yamada6fca36f2020-08-22 23:56:13 +090048 output_help = ('path to the output command database (defaults to ' +
49 _DEFAULT_OUTPUT + ')')
50 parser.add_argument('-o', '--output', type=str, default=_DEFAULT_OUTPUT,
51 help=output_help)
Tom Roederb3020462018-12-18 14:49:07 -080052
Masahiro Yamadaea6cedc2020-08-22 23:56:10 +090053 log_level_help = ('the level of log messages to produce (defaults to ' +
Tom Roederb3020462018-12-18 14:49:07 -080054 _DEFAULT_LOG_LEVEL + ')')
Masahiro Yamadaea6cedc2020-08-22 23:56:10 +090055 parser.add_argument('--log_level', choices=_VALID_LOG_LEVELS,
56 default=_DEFAULT_LOG_LEVEL, help=log_level_help)
Tom Roederb3020462018-12-18 14:49:07 -080057
Masahiro Yamadaecca4fe2020-08-22 23:56:15 +090058 ar_help = 'command used for parsing .a archives'
59 parser.add_argument('-a', '--ar', type=str, default='llvm-ar', help=ar_help)
60
61 paths_help = ('directories to search or files to parse '
62 '(files should be *.o, *.a, or modules.order). '
63 'If nothing is specified, the current directory is searched')
64 parser.add_argument('paths', type=str, nargs='*', help=paths_help)
65
Tom Roederb3020462018-12-18 14:49:07 -080066 args = parser.parse_args()
67
Masahiro Yamada6fca36f2020-08-22 23:56:13 +090068 return (args.log_level,
69 os.path.abspath(args.directory),
Masahiro Yamadafc2cb222020-08-22 23:56:14 +090070 args.output,
Masahiro Yamadaecca4fe2020-08-22 23:56:15 +090071 args.ar,
72 args.paths if len(args.paths) > 0 else [args.directory])
Masahiro Yamadafc2cb222020-08-22 23:56:14 +090073
74
75def cmdfiles_in_dir(directory):
76 """Generate the iterator of .cmd files found under the directory.
77
78 Walk under the given directory, and yield every .cmd file found.
79
80 Args:
81 directory: The directory to search for .cmd files.
82
83 Yields:
84 The path to a .cmd file.
85 """
86
87 filename_matcher = re.compile(_FILENAME_PATTERN)
88
89 for dirpath, _, filenames in os.walk(directory):
90 for filename in filenames:
91 if filename_matcher.match(filename):
92 yield os.path.join(dirpath, filename)
Tom Roederb3020462018-12-18 14:49:07 -080093
94
Masahiro Yamadaecca4fe2020-08-22 23:56:15 +090095def to_cmdfile(path):
96 """Return the path of .cmd file used for the given build artifact
97
98 Args:
99 Path: file path
100
101 Returns:
102 The path to .cmd file
103 """
104 dir, base = os.path.split(path)
105 return os.path.join(dir, '.' + base + '.cmd')
106
107
108def cmdfiles_for_o(obj):
109 """Generate the iterator of .cmd files associated with the object
110
111 Yield the .cmd file used to build the given object
112
113 Args:
114 obj: The object path
115
116 Yields:
117 The path to .cmd file
118 """
119 yield to_cmdfile(obj)
120
121
122def cmdfiles_for_a(archive, ar):
123 """Generate the iterator of .cmd files associated with the archive.
124
125 Parse the given archive, and yield every .cmd file used to build it.
126
127 Args:
128 archive: The archive to parse
129
130 Yields:
131 The path to every .cmd file found
132 """
133 for obj in subprocess.check_output([ar, '-t', archive]).decode().split():
134 yield to_cmdfile(obj)
135
136
137def cmdfiles_for_modorder(modorder):
138 """Generate the iterator of .cmd files associated with the modules.order.
139
140 Parse the given modules.order, and yield every .cmd file used to build the
141 contained modules.
142
143 Args:
144 modorder: The modules.order file to parse
145
146 Yields:
147 The path to every .cmd file found
148 """
149 with open(modorder) as f:
150 for line in f:
151 ko = line.rstrip()
152 base, ext = os.path.splitext(ko)
153 if ext != '.ko':
154 sys.exit('{}: module path must end with .ko'.format(ko))
155 mod = base + '.mod'
156 # The first line of *.mod lists the objects that compose the module.
157 with open(mod) as m:
158 for obj in m.readline().split():
159 yield to_cmdfile(obj)
160
161
Masahiro Yamada6ca4c6d2020-08-22 23:56:11 +0900162def process_line(root_directory, command_prefix, file_path):
Tom Roederb3020462018-12-18 14:49:07 -0800163 """Extracts information from a .cmd line and creates an entry from it.
164
165 Args:
166 root_directory: The directory that was searched for .cmd files. Usually
167 used directly in the "directory" entry in compile_commands.json.
Tom Roederb3020462018-12-18 14:49:07 -0800168 command_prefix: The extracted command line, up to the last element.
Masahiro Yamada6ca4c6d2020-08-22 23:56:11 +0900169 file_path: The .c file from the end of the extracted command.
170 Usually relative to root_directory, but sometimes absolute.
Tom Roederb3020462018-12-18 14:49:07 -0800171
172 Returns:
173 An entry to append to compile_commands.
174
175 Raises:
Masahiro Yamada6ca4c6d2020-08-22 23:56:11 +0900176 ValueError: Could not find the extracted file based on file_path and
Tom Roederb3020462018-12-18 14:49:07 -0800177 root_directory or file_directory.
178 """
179 # The .cmd files are intended to be included directly by Make, so they
180 # escape the pound sign '#', either as '\#' or '$(pound)' (depending on the
181 # kernel version). The compile_commands.json file is not interepreted
182 # by Make, so this code replaces the escaped version with '#'.
183 prefix = command_prefix.replace('\#', '#').replace('$(pound)', '#')
184
Masahiro Yamada6ca4c6d2020-08-22 23:56:11 +0900185 # Use os.path.abspath() to normalize the path resolving '.' and '..' .
186 abs_path = os.path.abspath(os.path.join(root_directory, file_path))
187 if not os.path.exists(abs_path):
188 raise ValueError('File %s not found' % abs_path)
Tom Roederb3020462018-12-18 14:49:07 -0800189 return {
Masahiro Yamada6ca4c6d2020-08-22 23:56:11 +0900190 'directory': root_directory,
191 'file': abs_path,
192 'command': prefix + file_path,
Tom Roederb3020462018-12-18 14:49:07 -0800193 }
194
195
196def main():
197 """Walks through the directory and finds and parses .cmd files."""
Masahiro Yamadaecca4fe2020-08-22 23:56:15 +0900198 log_level, directory, output, ar, paths = parse_arguments()
Tom Roederb3020462018-12-18 14:49:07 -0800199
200 level = getattr(logging, log_level)
201 logging.basicConfig(format='%(levelname)s: %(message)s', level=level)
202
Tom Roederb3020462018-12-18 14:49:07 -0800203 line_matcher = re.compile(_LINE_PATTERN)
204
205 compile_commands = []
Tom Roederb3020462018-12-18 14:49:07 -0800206
Masahiro Yamadafc2cb222020-08-22 23:56:14 +0900207 for path in paths:
Masahiro Yamadaecca4fe2020-08-22 23:56:15 +0900208 # If 'path' is a directory, handle all .cmd files under it.
209 # Otherwise, handle .cmd files associated with the file.
210 # Most of built-in objects are linked via archives (built-in.a or lib.a)
211 # but some objects are linked to vmlinux directly.
212 # Modules are listed in modules.order.
213 if os.path.isdir(path):
214 cmdfiles = cmdfiles_in_dir(path)
215 elif path.endswith('.o'):
216 cmdfiles = cmdfiles_for_o(path)
217 elif path.endswith('.a'):
218 cmdfiles = cmdfiles_for_a(path, ar)
219 elif path.endswith('modules.order'):
220 cmdfiles = cmdfiles_for_modorder(path)
221 else:
222 sys.exit('{}: unknown file type'.format(path))
Masahiro Yamadafc2cb222020-08-22 23:56:14 +0900223
224 for cmdfile in cmdfiles:
225 with open(cmdfile, 'rt') as f:
Masahiro Yamada8a685db2020-08-22 23:56:09 +0900226 result = line_matcher.match(f.readline())
227 if result:
Tom Roederb3020462018-12-18 14:49:07 -0800228 try:
Masahiro Yamadafc2cb222020-08-22 23:56:14 +0900229 entry = process_line(directory, result.group(1),
230 result.group(2))
Tom Roederb3020462018-12-18 14:49:07 -0800231 compile_commands.append(entry)
232 except ValueError as err:
233 logging.info('Could not add line from %s: %s',
Masahiro Yamadafc2cb222020-08-22 23:56:14 +0900234 cmdfile, err)
Tom Roederb3020462018-12-18 14:49:07 -0800235
236 with open(output, 'wt') as f:
237 json.dump(compile_commands, f, indent=2, sort_keys=True)
238
239 count = len(compile_commands)
240 if count < _LOW_COUNT_THRESHOLD:
241 logging.warning(
242 'Found %s entries. Have you compiled the kernel?', count)
243
244
245if __name__ == '__main__':
246 main()