code_style.py (13013B)
1 #!/usr/bin/env python3 2 """Check or fix the code style by running Uncrustify. 3 4 This script must be run from the root of a Git work tree containing Mbed TLS. 5 """ 6 # Copyright The Mbed TLS Contributors 7 # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later 8 import argparse 9 import os 10 import re 11 import subprocess 12 import sys 13 from typing import FrozenSet, List, Optional 14 from mbedtls_framework import build_tree 15 16 UNCRUSTIFY_SUPPORTED_VERSION = "0.75.1" 17 CONFIG_FILE = ".uncrustify.cfg" 18 UNCRUSTIFY_EXE = "uncrustify" 19 UNCRUSTIFY_ARGS = ["-c", CONFIG_FILE] 20 CHECK_GENERATED_FILES = "tests/scripts/check-generated-files.sh" 21 22 def print_err(*args): 23 print("Error: ", *args, file=sys.stderr) 24 25 # Print the file names that will be skipped and the help message 26 def print_skip(files_to_skip): 27 print() 28 print(*files_to_skip, sep=", SKIP\n", end=", SKIP\n") 29 print("Warning: The listed files will be skipped because\n" 30 "they are not known to git.") 31 print() 32 33 # Match FILENAME(s) in "check SCRIPT (FILENAME...)" 34 CHECK_CALL_RE = re.compile(r"\n\s*check\s+[^\s#$&*?;|]+([^\n#$&*?;|]+)", 35 re.ASCII) 36 def list_generated_files() -> FrozenSet[str]: 37 """Return the names of generated files. 38 39 We don't reformat generated files, since the result might be different 40 from the output of the generator. Ideally the result of the generator 41 would conform to the code style, but this would be difficult, especially 42 with respect to the placement of line breaks in long logical lines. 43 """ 44 if build_tree.is_mbedtls_3_6(): 45 # Parse check-generated-files.sh to get an up-to-date list of 46 # generated files. Read the file rather than calling it so that 47 # this script only depends on Git, Python and uncrustify, and not other 48 # tools such as sh or grep which might not be available on Windows. 49 # This introduces a limitation: check-generated-files.sh must have 50 # the expected format and must list the files explicitly, not through 51 # wildcards or command substitution. 52 content = open(CHECK_GENERATED_FILES, encoding="utf-8").read() 53 checks = re.findall(CHECK_CALL_RE, content) 54 return frozenset(word for s in checks for word in s.split()) 55 else: 56 output = subprocess.check_output(["framework/scripts/make_generated_files.py", 57 "--list"], universal_newlines=True) 58 # psa_test_wrappers.[hc], generated by generate_psa_wrappers.py, are 59 # currently committed and unknown to make_generated_files.py. Add them 60 # here to the list of generated file as we do not want to check their 61 # coding style. 62 if build_tree.looks_like_tf_psa_crypto_root("."): 63 output += "tests/include/test/psa_test_wrappers.h\n" 64 output += "tests/src/psa_test_wrappers.c" 65 66 return frozenset(line for line in output.splitlines()) 67 68 # Check for comment string indicating an auto-generated file 69 AUTOGEN_RE = re.compile(r"Warning[ :-]+This file is (now )?auto[ -]?generated", 70 re.ASCII | re.IGNORECASE) 71 def is_file_autogenerated(filename): 72 content = open(filename, encoding="utf-8").read() 73 return AUTOGEN_RE.search(content) is not None 74 75 def get_src_files(since: Optional[str]) -> List[str]: 76 """ 77 Use git to get a list of the source files. 78 79 The optional argument since is a commit, indicating to only list files 80 that have changed since that commit. Without this argument, list all 81 files known to git. 82 83 Only C files are included, and certain files (generated, or third party) 84 are excluded. 85 """ 86 file_patterns = ["*.[hc]", 87 "tests/suites/*.function", 88 "scripts/data_files/*.fmt"] 89 output = subprocess.check_output(["git", "ls-files"] + file_patterns, 90 universal_newlines=True) 91 src_files = output.split() 92 93 # When this script is called from a git hook, some environment variables 94 # are set by default which force all git commands to use the main repository 95 # (i.e. prevent us from performing commands on the framework repo). 96 # Create an environment without these variables for running commands on the 97 # framework repo. 98 framework_env = os.environ.copy() 99 # Get a list of environment vars that git sets 100 git_env_vars = subprocess.check_output(["git", "rev-parse", "--local-env-vars"], 101 universal_newlines=True) 102 # Remove the vars from the environment 103 for var in git_env_vars.split(): 104 framework_env.pop(var, None) 105 106 output = subprocess.check_output(["git", "-C", "framework", "ls-files"] 107 + file_patterns, 108 universal_newlines=True, 109 env=framework_env) 110 framework_src_files = output.split() 111 112 if since: 113 # get all files changed in commits since the starting point in ... 114 # ... the main repository 115 cmd = ["git", "log", since + "..HEAD", "--ignore-submodules", 116 "--name-only", "--pretty=", "--"] + src_files 117 output = subprocess.check_output(cmd, universal_newlines=True) 118 committed_changed_files = output.split() 119 120 # ... the framework submodule 121 framework_since = get_submodule_hash(since, "framework") 122 cmd = ["git", "-C", "framework", "log", framework_since + "..HEAD", 123 "--name-only", "--pretty=", "--"] + framework_src_files 124 output = subprocess.check_output(cmd, universal_newlines=True, 125 env=framework_env) 126 committed_changed_files += ["framework/" + s for s in output.split()] 127 128 # and also get all files with uncommitted changes in ... 129 # ... the main repository 130 cmd = ["git", "diff", "--name-only", "--"] + src_files 131 output = subprocess.check_output(cmd, universal_newlines=True) 132 uncommitted_changed_files = output.split() 133 # ... the framework submodule 134 cmd = ["git", "-C", "framework", "diff", "--name-only", "--"] + \ 135 framework_src_files 136 output = subprocess.check_output(cmd, universal_newlines=True, 137 env=framework_env) 138 uncommitted_changed_files += ["framework/" + s for s in output.split()] 139 140 src_files = committed_changed_files + uncommitted_changed_files 141 else: 142 src_files += ["framework/" + s for s in framework_src_files] 143 144 generated_files = list_generated_files() 145 # Don't correct style for third-party files (and, for simplicity, 146 # companion files in the same subtree), or for automatically 147 # generated files (we're correcting the templates instead). 148 if build_tree.is_mbedtls_3_6(): 149 src_files = [filename for filename in src_files 150 if not (filename.startswith("3rdparty/") or 151 filename in generated_files or 152 is_file_autogenerated(filename))] 153 else: 154 src_files = [filename for filename in src_files 155 if not (filename.startswith("drivers/everest/") or 156 filename.startswith("drivers/p256-m/") or 157 filename in generated_files or 158 is_file_autogenerated(filename))] 159 return src_files 160 161 def get_submodule_hash(commit: str, submodule: str) -> str: 162 """Get the commit hash of a submodule at a given commit in the Git repository.""" 163 cmd = ["git", "ls-tree", commit, submodule] 164 output = subprocess.check_output(cmd, universal_newlines=True) 165 return output.split()[2] 166 167 def get_uncrustify_version() -> str: 168 """ 169 Get the version string from Uncrustify 170 """ 171 result = subprocess.run([UNCRUSTIFY_EXE, "--version"], 172 stdout=subprocess.PIPE, stderr=subprocess.PIPE, 173 check=False) 174 if result.returncode != 0: 175 print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8")) 176 return "" 177 else: 178 return str(result.stdout, "utf-8") 179 180 def check_style_is_correct(src_file_list: List[str]) -> bool: 181 """ 182 Check the code style and output a diff for each file whose style is 183 incorrect. 184 """ 185 style_correct = True 186 for src_file in src_file_list: 187 uncrustify_cmd = [UNCRUSTIFY_EXE] + UNCRUSTIFY_ARGS + [src_file] 188 result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE, 189 stderr=subprocess.PIPE, check=False) 190 if result.returncode != 0: 191 print_err("Uncrustify returned " + str(result.returncode) + 192 " correcting file " + src_file) 193 return False 194 195 # Uncrustify makes changes to the code and places the result in a new 196 # file with the extension ".uncrustify". To get the changes (if any) 197 # simply diff the 2 files. 198 diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"] 199 cp = subprocess.run(diff_cmd, check=False) 200 201 if cp.returncode == 1: 202 print(src_file + " changed - code style is incorrect.") 203 style_correct = False 204 elif cp.returncode != 0: 205 raise subprocess.CalledProcessError(cp.returncode, cp.args, 206 cp.stdout, cp.stderr) 207 208 # Tidy up artifact 209 os.remove(src_file + ".uncrustify") 210 211 return style_correct 212 213 def fix_style_single_pass(src_file_list: List[str]) -> bool: 214 """ 215 Run Uncrustify once over the source files. 216 """ 217 code_change_args = UNCRUSTIFY_ARGS + ["--no-backup"] 218 for src_file in src_file_list: 219 uncrustify_cmd = [UNCRUSTIFY_EXE] + code_change_args + [src_file] 220 result = subprocess.run(uncrustify_cmd, check=False) 221 if result.returncode != 0: 222 print_err("Uncrustify with file returned: " + 223 str(result.returncode) + " correcting file " + 224 src_file) 225 return False 226 return True 227 228 def fix_style(src_file_list: List[str]) -> int: 229 """ 230 Fix the code style. This takes 2 passes of Uncrustify. 231 """ 232 if not fix_style_single_pass(src_file_list): 233 return 1 234 if not fix_style_single_pass(src_file_list): 235 return 1 236 237 # Guard against future changes that cause the codebase to require 238 # more passes. 239 if not check_style_is_correct(src_file_list): 240 print_err("Code style still incorrect after second run of Uncrustify.") 241 return 1 242 else: 243 return 0 244 245 def main() -> int: 246 """ 247 Main with command line arguments. 248 """ 249 uncrustify_version = get_uncrustify_version().strip() 250 if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version: 251 print("Warning: Using unsupported Uncrustify version '" + 252 uncrustify_version + "'") 253 print("Note: The only supported version is " + 254 UNCRUSTIFY_SUPPORTED_VERSION) 255 256 parser = argparse.ArgumentParser() 257 parser.add_argument('-f', '--fix', action='store_true', 258 help=('modify source files to fix the code style ' 259 '(default: print diff, do not modify files)')) 260 parser.add_argument('-s', '--since', metavar='COMMIT', const='development', nargs='?', 261 help=('only check files modified since the specified commit' 262 ' (e.g. --since=HEAD~3 or --since=development). If no' 263 ' commit is specified, default to development.')) 264 # --subset is almost useless: it only matters if there are no files 265 # ('code_style.py' without arguments checks all files known to Git, 266 # 'code_style.py --subset' does nothing). In particular, 267 # 'code_style.py --fix --subset ...' is intended as a stable ("porcelain") 268 # way to restyle a possibly empty set of files. 269 parser.add_argument('--subset', action='store_true', 270 help='only check the specified files (default with non-option arguments)') 271 parser.add_argument('operands', nargs='*', metavar='FILE', 272 help='files to check (files MUST be known to git, if none: check all)') 273 274 args = parser.parse_args() 275 276 covered = frozenset(get_src_files(args.since)) 277 # We only check files that are known to git 278 if args.subset or args.operands: 279 src_files = [f for f in args.operands if f in covered] 280 skip_src_files = [f for f in args.operands if f not in covered] 281 if skip_src_files: 282 print_skip(skip_src_files) 283 else: 284 src_files = list(covered) 285 286 if args.fix: 287 # Fix mode 288 return fix_style(src_files) 289 else: 290 # Check mode 291 if check_style_is_correct(src_files): 292 print("Checked {} files, style ok.".format(len(src_files))) 293 return 0 294 else: 295 return 1 296 297 if __name__ == '__main__': 298 sys.exit(main())