71a0630ae1887c62a74555209bdea3c1aa8476b4
[linux-2.6-microblaze.git] / scripts / gen_compile_commands.py
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
10 import argparse
11 import json
12 import logging
13 import os
14 import 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 300 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 = 300
27
28
29 def 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 work directory where the objects were built.
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 = ('specify the output directory used for the kernel build '
41                       '(defaults to the working directory)')
42     parser.add_argument('-d', '--directory', type=str, default='.',
43                         help=directory_help)
44
45     output_help = ('path to the output command database (defaults to ' +
46                    _DEFAULT_OUTPUT + ')')
47     parser.add_argument('-o', '--output', type=str, default=_DEFAULT_OUTPUT,
48                         help=output_help)
49
50     log_level_help = ('the level of log messages to produce (defaults to ' +
51                       _DEFAULT_LOG_LEVEL + ')')
52     parser.add_argument('--log_level', choices=_VALID_LOG_LEVELS,
53                         default=_DEFAULT_LOG_LEVEL, help=log_level_help)
54
55     args = parser.parse_args()
56
57     return (args.log_level,
58             os.path.abspath(args.directory),
59             args.output)
60
61
62 def process_line(root_directory, command_prefix, file_path):
63     """Extracts information from a .cmd line and creates an entry from it.
64
65     Args:
66         root_directory: The directory that was searched for .cmd files. Usually
67             used directly in the "directory" entry in compile_commands.json.
68         command_prefix: The extracted command line, up to the last element.
69         file_path: The .c file from the end of the extracted command.
70             Usually relative to root_directory, but sometimes absolute.
71
72     Returns:
73         An entry to append to compile_commands.
74
75     Raises:
76         ValueError: Could not find the extracted file based on file_path and
77             root_directory or file_directory.
78     """
79     # The .cmd files are intended to be included directly by Make, so they
80     # escape the pound sign '#', either as '\#' or '$(pound)' (depending on the
81     # kernel version). The compile_commands.json file is not interepreted
82     # by Make, so this code replaces the escaped version with '#'.
83     prefix = command_prefix.replace('\#', '#').replace('$(pound)', '#')
84
85     # Use os.path.abspath() to normalize the path resolving '.' and '..' .
86     abs_path = os.path.abspath(os.path.join(root_directory, file_path))
87     if not os.path.exists(abs_path):
88         raise ValueError('File %s not found' % abs_path)
89     return {
90         'directory': root_directory,
91         'file': abs_path,
92         'command': prefix + file_path,
93     }
94
95
96 def main():
97     """Walks through the directory and finds and parses .cmd files."""
98     log_level, directory, output = parse_arguments()
99
100     level = getattr(logging, log_level)
101     logging.basicConfig(format='%(levelname)s: %(message)s', level=level)
102
103     filename_matcher = re.compile(_FILENAME_PATTERN)
104     line_matcher = re.compile(_LINE_PATTERN)
105
106     compile_commands = []
107     for dirpath, _, filenames in os.walk(directory):
108         for filename in filenames:
109             if not filename_matcher.match(filename):
110                 continue
111             filepath = os.path.join(dirpath, filename)
112
113             with open(filepath, 'rt') as f:
114                 result = line_matcher.match(f.readline())
115                 if result:
116                     try:
117                         entry = process_line(directory,
118                                              result.group(1), result.group(2))
119                         compile_commands.append(entry)
120                     except ValueError as err:
121                         logging.info('Could not add line from %s: %s',
122                                      filepath, err)
123
124     with open(output, 'wt') as f:
125         json.dump(compile_commands, f, indent=2, sort_keys=True)
126
127     count = len(compile_commands)
128     if count < _LOW_COUNT_THRESHOLD:
129         logging.warning(
130             'Found %s entries. Have you compiled the kernel?', count)
131
132
133 if __name__ == '__main__':
134     main()