blob: 822b85c184766d2a27f325068fb75abf82e50cb6 [file] [log] [blame]
Damian Michalak-Szmaciński61578592022-11-29 15:36:50 +01001#!/usr/bin/env -S python3 -B
2
3# Copyright (c) 2021 Project CHIP Authors
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import contextlib
18import glob
19import json
20import logging
21import os
22import re
23import shutil
24import subprocess
25import sys
26from pathlib import Path
27
28import click
29import coloredlogs
30
31# Supported log levels, mapping string values required for argument
32# parsing into logging constants
33__LOG_LEVELS__ = {
34 'debug': logging.DEBUG,
35 'info': logging.INFO,
36 'warn': logging.WARN,
37 'fatal': logging.FATAL,
38}
39
40root_dir = os.path.dirname(os.path.realpath(__file__))
41proj_root_dir = os.path.join(Path(root_dir).parent.parent)
42
43
44def find_program(names):
45 for name in names:
46 found = shutil.which(name)
47 if found is not None:
48 return found
49
50
51@click.command()
52@click.option(
53 '--log-level',
54 default='INFO',
55 type=click.Choice(__LOG_LEVELS__.keys(), case_sensitive=False),
56 help='Determines the verbosity of script output.')
57@click.option(
58 '--no-log-timestamps',
59 default=False,
60 is_flag=True,
61 help='Skip timestamps in log output')
62@click.option(
63 '--compile-commands-glob',
64 show_default=True,
65 default=os.path.join(proj_root_dir, "out", "debug", "compile_commands*.json"),
66 help='Set global pattern for compile_commands.json files'
67)
68@click.option(
69 '--scanning-destination',
70 show_default=True,
71 default=os.path.join(proj_root_dir, "src", "platform"),
72 help='Set scanning destination file(s) or directory /ies in project'
73)
74@click.option(
75 '--mapping-file-dir',
76 help='Set mapping file directory /ies manually. File should have name iwyu.imp'
77)
78@click.option(
79 '--iwyu-args',
80 show_default=True,
81 default="-Xiwyu --no_fwd_decls",
82 help='Set custom arg(s) for include what you use'
83)
84@click.option(
85 '--clang-args',
86 default="",
87 help='Set custom arg(s) for clang'
88)
89def main(compile_commands_glob, scanning_destination, mapping_file_dir,
90 iwyu_args, clang_args, log_level, no_log_timestamps):
91 # Ensures somewhat pretty logging of what is going on
92 log_fmt = '%(asctime)s %(levelname)-7s %(message)s'
93 if no_log_timestamps:
94 log_fmt = '%(levelname)-7s %(message)s'
95 coloredlogs.install(level=__LOG_LEVELS__[log_level], fmt=log_fmt)
96
97 # checking if a program IWYU exists
98 iwyu = find_program(('iwyu_tool', 'iwyu_tool.py'))
99
100 if iwyu is None:
101 logging.error("Can't find IWYU")
102 sys.exit(1)
103
104 # For iterating how many files had problems with includes
105 warning_in_files = 0
106
107 platform = ""
108 compile_commands_glob = glob.glob(compile_commands_glob)
109
110 if not compile_commands_glob:
111 logging.error("Can't find compile_commands.json file(s)")
112 sys.exit(1)
113
114 for compile_commands in compile_commands_glob:
115
116 compile_commands_path = os.path.dirname(compile_commands)
117 compile_commands_file = os.path.join(compile_commands_path, "compile_commands.json")
118 logging.debug("Copy compile command file %s to %s", compile_commands, compile_commands_file)
119
120 with contextlib.suppress(shutil.SameFileError):
121 shutil.copyfile(compile_commands, compile_commands_file)
122
123 # Prase json file for find target name
124 with open(compile_commands, 'r') as json_data:
125 json_data = json.load(json_data)
126
127 for key in json_data:
128 find_re = re.search(r'^.*/src/platform*?\/(.*)/.*$', key['file'])
129 if find_re is not None:
130 platform = find_re.group(1)
131 break
132 if not platform:
133 logging.error("Can't find platform")
134 sys.exit(1)
135
136 if not mapping_file_dir:
137 mapping_file_dir = os.path.join(root_dir, "platforms", platform)
138
139 # Platform specific clang arguments, as some platform
140 # may needed some hacks for clang compiler for work properly.
141 platform_clang_args = []
142
143 if platform == "Tizen":
144 platform_clang_args = [
145 "--target=arm-linux-gnueabi",
146 "-I$TIZEN_SDK_TOOLCHAIN/arm-tizen-linux-gnueabi/include/c++/9.2.0",
147 "-I$TIZEN_SDK_TOOLCHAIN/arm-tizen-linux-gnueabi/include/c++/9.2.0/arm-tizen-linux-gnueabi",
148 "-I$TIZEN_SDK_TOOLCHAIN/lib/gcc/arm-tizen-linux-gnueabi/9.2.0/include",
149 ]
150
151 # TODO: Add another platform for easy scanning
152 # Actually works scanning for platform: tizen, darwin, linux other not tested yet.
153
154 command_arr = [
155 iwyu,
156 "-p", compile_commands_path, scanning_destination,
157 "--", iwyu_args,
158 "-Xiwyu", "--mapping_file=" + mapping_file_dir + "/iwyu.imp",
159 ] + platform_clang_args + [clang_args]
160
161 logging.info("Used compile commands: %s", compile_commands)
162 logging.info("Scanning includes for platform: %s", platform)
163 logging.info("Scanning destination: %s", scanning_destination)
164
165 logging.debug("Command: %s", " ".join(command_arr))
166 status = subprocess.Popen(" ".join(command_arr),
167 shell=True,
168 text=True,
169 stdout=subprocess.PIPE,
170 stderr=subprocess.STDOUT)
171
172 logging.info("============== IWYU output start ================")
173
174 logger = logging.info
Arkadiusz Bokowya71f8142024-02-15 23:34:23 +0100175 for line in status.stdout:
176 line = line.rstrip()
Damian Michalak-Szmaciński61578592022-11-29 15:36:50 +0100177
178 if re.match(r"^warning:.*$", line):
179 logger = logging.warning
180 elif re.match(r"^.*([A-Za-z0-9]+(/[A-Za-z0-9]+)+)\.cpp should [a-zA-Z]+ these lines:$", line):
181 logger = logging.warning
182 elif re.match(r"^.*([A-Za-z0-9]+(/[A-Za-z0-9]+)+)\.[a-zA-Z]+ has correct #includes/fwd-decls\)$", line):
183 logger = logging.info
184 elif re.match(r"^The full include-list for .*$", line):
185 logger = logging.warning
186 warning_in_files += 1
187
188 logger("%s", line)
189
190 logging.info("============== IWYU output end ================")
191
192 if warning_in_files:
193 logging.error("Number of files with include issues: %d", warning_in_files)
194 sys.exit(2)
195 else:
196 logging.info("Every include looks good!")
197
198
199if __name__ == '__main__':
Arkadiusz Bokowy5529d742022-12-02 22:23:01 +0100200 main(auto_envvar_prefix='CHIP')