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