blob: 19963708bcf87712384eaedbba359e278cb78321 [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
Tom Roederb3020462018-12-18 14:49:07 -080024
25def parse_arguments():
26 """Sets up and parses command-line arguments.
27
28 Returns:
29 log_level: A logging level to filter log output.
Masahiro Yamada0a7d3762020-08-22 23:56:12 +090030 directory: The work directory where the objects were built.
Masahiro Yamadaecca4fe2020-08-22 23:56:15 +090031 ar: Command used for parsing .a archives.
Tom Roederb3020462018-12-18 14:49:07 -080032 output: Where to write the compile-commands JSON file.
Masahiro Yamadaecca4fe2020-08-22 23:56:15 +090033 paths: The list of files/directories to handle to find .cmd files.
Tom Roederb3020462018-12-18 14:49:07 -080034 """
35 usage = 'Creates a compile_commands.json database from kernel .cmd files'
36 parser = argparse.ArgumentParser(description=usage)
37
Masahiro Yamada0a7d3762020-08-22 23:56:12 +090038 directory_help = ('specify the output directory used for the kernel build '
Tom Roederb3020462018-12-18 14:49:07 -080039 '(defaults to the working directory)')
Masahiro Yamada6fca36f2020-08-22 23:56:13 +090040 parser.add_argument('-d', '--directory', type=str, default='.',
41 help=directory_help)
Tom Roederb3020462018-12-18 14:49:07 -080042
Masahiro Yamada6fca36f2020-08-22 23:56:13 +090043 output_help = ('path to the output command database (defaults to ' +
44 _DEFAULT_OUTPUT + ')')
45 parser.add_argument('-o', '--output', type=str, default=_DEFAULT_OUTPUT,
46 help=output_help)
Tom Roederb3020462018-12-18 14:49:07 -080047
Masahiro Yamadaea6cedc2020-08-22 23:56:10 +090048 log_level_help = ('the level of log messages to produce (defaults to ' +
Tom Roederb3020462018-12-18 14:49:07 -080049 _DEFAULT_LOG_LEVEL + ')')
Masahiro Yamadaea6cedc2020-08-22 23:56:10 +090050 parser.add_argument('--log_level', choices=_VALID_LOG_LEVELS,
51 default=_DEFAULT_LOG_LEVEL, help=log_level_help)
Tom Roederb3020462018-12-18 14:49:07 -080052
Masahiro Yamadaecca4fe2020-08-22 23:56:15 +090053 ar_help = 'command used for parsing .a archives'
54 parser.add_argument('-a', '--ar', type=str, default='llvm-ar', help=ar_help)
55
56 paths_help = ('directories to search or files to parse '
57 '(files should be *.o, *.a, or modules.order). '
58 'If nothing is specified, the current directory is searched')
59 parser.add_argument('paths', type=str, nargs='*', help=paths_help)
60
Tom Roederb3020462018-12-18 14:49:07 -080061 args = parser.parse_args()
62
Masahiro Yamada6fca36f2020-08-22 23:56:13 +090063 return (args.log_level,
64 os.path.abspath(args.directory),
Masahiro Yamadafc2cb222020-08-22 23:56:14 +090065 args.output,
Masahiro Yamadaecca4fe2020-08-22 23:56:15 +090066 args.ar,
67 args.paths if len(args.paths) > 0 else [args.directory])
Masahiro Yamadafc2cb222020-08-22 23:56:14 +090068
69
70def cmdfiles_in_dir(directory):
71 """Generate the iterator of .cmd files found under the directory.
72
73 Walk under the given directory, and yield every .cmd file found.
74
75 Args:
76 directory: The directory to search for .cmd files.
77
78 Yields:
79 The path to a .cmd file.
80 """
81
82 filename_matcher = re.compile(_FILENAME_PATTERN)
83
84 for dirpath, _, filenames in os.walk(directory):
85 for filename in filenames:
86 if filename_matcher.match(filename):
87 yield os.path.join(dirpath, filename)
Tom Roederb3020462018-12-18 14:49:07 -080088
89
Masahiro Yamadaecca4fe2020-08-22 23:56:15 +090090def to_cmdfile(path):
91 """Return the path of .cmd file used for the given build artifact
92
93 Args:
94 Path: file path
95
96 Returns:
97 The path to .cmd file
98 """
99 dir, base = os.path.split(path)
100 return os.path.join(dir, '.' + base + '.cmd')
101
102
103def cmdfiles_for_o(obj):
104 """Generate the iterator of .cmd files associated with the object
105
106 Yield the .cmd file used to build the given object
107
108 Args:
109 obj: The object path
110
111 Yields:
112 The path to .cmd file
113 """
114 yield to_cmdfile(obj)
115
116
117def cmdfiles_for_a(archive, ar):
118 """Generate the iterator of .cmd files associated with the archive.
119
120 Parse the given archive, and yield every .cmd file used to build it.
121
122 Args:
123 archive: The archive to parse
124
125 Yields:
126 The path to every .cmd file found
127 """
128 for obj in subprocess.check_output([ar, '-t', archive]).decode().split():
129 yield to_cmdfile(obj)
130
131
132def cmdfiles_for_modorder(modorder):
133 """Generate the iterator of .cmd files associated with the modules.order.
134
135 Parse the given modules.order, and yield every .cmd file used to build the
136 contained modules.
137
138 Args:
139 modorder: The modules.order file to parse
140
141 Yields:
142 The path to every .cmd file found
143 """
144 with open(modorder) as f:
145 for line in f:
146 ko = line.rstrip()
147 base, ext = os.path.splitext(ko)
148 if ext != '.ko':
149 sys.exit('{}: module path must end with .ko'.format(ko))
150 mod = base + '.mod'
151 # The first line of *.mod lists the objects that compose the module.
152 with open(mod) as m:
153 for obj in m.readline().split():
154 yield to_cmdfile(obj)
155
156
Masahiro Yamada6ca4c6d2020-08-22 23:56:11 +0900157def process_line(root_directory, command_prefix, file_path):
Tom Roederb3020462018-12-18 14:49:07 -0800158 """Extracts information from a .cmd line and creates an entry from it.
159
160 Args:
161 root_directory: The directory that was searched for .cmd files. Usually
162 used directly in the "directory" entry in compile_commands.json.
Tom Roederb3020462018-12-18 14:49:07 -0800163 command_prefix: The extracted command line, up to the last element.
Masahiro Yamada6ca4c6d2020-08-22 23:56:11 +0900164 file_path: The .c file from the end of the extracted command.
165 Usually relative to root_directory, but sometimes absolute.
Tom Roederb3020462018-12-18 14:49:07 -0800166
167 Returns:
168 An entry to append to compile_commands.
169
170 Raises:
Masahiro Yamada6ca4c6d2020-08-22 23:56:11 +0900171 ValueError: Could not find the extracted file based on file_path and
Tom Roederb3020462018-12-18 14:49:07 -0800172 root_directory or file_directory.
173 """
174 # The .cmd files are intended to be included directly by Make, so they
175 # escape the pound sign '#', either as '\#' or '$(pound)' (depending on the
176 # kernel version). The compile_commands.json file is not interepreted
177 # by Make, so this code replaces the escaped version with '#'.
178 prefix = command_prefix.replace('\#', '#').replace('$(pound)', '#')
179
Masahiro Yamada6ca4c6d2020-08-22 23:56:11 +0900180 # Use os.path.abspath() to normalize the path resolving '.' and '..' .
181 abs_path = os.path.abspath(os.path.join(root_directory, file_path))
182 if not os.path.exists(abs_path):
183 raise ValueError('File %s not found' % abs_path)
Tom Roederb3020462018-12-18 14:49:07 -0800184 return {
Masahiro Yamada6ca4c6d2020-08-22 23:56:11 +0900185 'directory': root_directory,
186 'file': abs_path,
187 'command': prefix + file_path,
Tom Roederb3020462018-12-18 14:49:07 -0800188 }
189
190
191def main():
192 """Walks through the directory and finds and parses .cmd files."""
Masahiro Yamadaecca4fe2020-08-22 23:56:15 +0900193 log_level, directory, output, ar, paths = parse_arguments()
Tom Roederb3020462018-12-18 14:49:07 -0800194
195 level = getattr(logging, log_level)
196 logging.basicConfig(format='%(levelname)s: %(message)s', level=level)
197
Tom Roederb3020462018-12-18 14:49:07 -0800198 line_matcher = re.compile(_LINE_PATTERN)
199
200 compile_commands = []
Tom Roederb3020462018-12-18 14:49:07 -0800201
Masahiro Yamadafc2cb222020-08-22 23:56:14 +0900202 for path in paths:
Masahiro Yamadaecca4fe2020-08-22 23:56:15 +0900203 # If 'path' is a directory, handle all .cmd files under it.
204 # Otherwise, handle .cmd files associated with the file.
205 # Most of built-in objects are linked via archives (built-in.a or lib.a)
206 # but some objects are linked to vmlinux directly.
207 # Modules are listed in modules.order.
208 if os.path.isdir(path):
209 cmdfiles = cmdfiles_in_dir(path)
210 elif path.endswith('.o'):
211 cmdfiles = cmdfiles_for_o(path)
212 elif path.endswith('.a'):
213 cmdfiles = cmdfiles_for_a(path, ar)
214 elif path.endswith('modules.order'):
215 cmdfiles = cmdfiles_for_modorder(path)
216 else:
217 sys.exit('{}: unknown file type'.format(path))
Masahiro Yamadafc2cb222020-08-22 23:56:14 +0900218
219 for cmdfile in cmdfiles:
220 with open(cmdfile, 'rt') as f:
Masahiro Yamada8a685db2020-08-22 23:56:09 +0900221 result = line_matcher.match(f.readline())
222 if result:
Tom Roederb3020462018-12-18 14:49:07 -0800223 try:
Masahiro Yamadafc2cb222020-08-22 23:56:14 +0900224 entry = process_line(directory, result.group(1),
225 result.group(2))
Tom Roederb3020462018-12-18 14:49:07 -0800226 compile_commands.append(entry)
227 except ValueError as err:
228 logging.info('Could not add line from %s: %s',
Masahiro Yamadafc2cb222020-08-22 23:56:14 +0900229 cmdfile, err)
Tom Roederb3020462018-12-18 14:49:07 -0800230
231 with open(output, 'wt') as f:
232 json.dump(compile_commands, f, indent=2, sort_keys=True)
233
Tom Roederb3020462018-12-18 14:49:07 -0800234
235if __name__ == '__main__':
236 main()