Damian Michalak-Szmaciński | 6157859 | 2022-11-29 15:36:50 +0100 | [diff] [blame] | 1 | #!/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 | |
| 17 | import contextlib |
| 18 | import glob |
| 19 | import json |
| 20 | import logging |
| 21 | import os |
| 22 | import re |
| 23 | import shutil |
| 24 | import subprocess |
| 25 | import sys |
| 26 | from pathlib import Path |
| 27 | |
| 28 | import click |
| 29 | import 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 | |
| 40 | root_dir = os.path.dirname(os.path.realpath(__file__)) |
| 41 | proj_root_dir = os.path.join(Path(root_dir).parent.parent) |
| 42 | |
| 43 | |
| 44 | def 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 | ) |
| 89 | def 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 Bokowy | a71f814 | 2024-02-15 23:34:23 +0100 | [diff] [blame] | 175 | for line in status.stdout: |
| 176 | line = line.rstrip() |
Damian Michalak-Szmaciński | 6157859 | 2022-11-29 15:36:50 +0100 | [diff] [blame] | 177 | |
| 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 | |
| 199 | if __name__ == '__main__': |
Arkadiusz Bokowy | 5529d74 | 2022-12-02 22:23:01 +0100 | [diff] [blame] | 200 | main(auto_envvar_prefix='CHIP') |