blob: 2788a99779830858fc061f1d7183055fe4112ba2 [file] [log] [blame]
Fathi Boudra422bf772019-12-02 11:10:16 +02001#!/usr/bin/env python3
2#
Yann Gautiere5b08f22023-09-04 15:01:59 +02003# Copyright (c) 2019-2023, Arm Limited. All rights reserved.
Fathi Boudra422bf772019-12-02 11:10:16 +02004#
5# SPDX-License-Identifier: BSD-3-Clause
6#
7
8import argparse
9import codecs
Zelalem219df412020-05-17 19:21:20 -050010import collections
Zelalemf75daf22020-08-04 16:29:43 -050011import functools
Fathi Boudra422bf772019-12-02 11:10:16 +020012import os
13import re
Zelalem219df412020-05-17 19:21:20 -050014import subprocess
Fathi Boudra422bf772019-12-02 11:10:16 +020015import sys
16import utils
Zelalem219df412020-05-17 19:21:20 -050017import logging
Harrison Mutaiec878ce2022-05-13 15:18:38 +010018from pathlib import Path
Fathi Boudra422bf772019-12-02 11:10:16 +020019
Fathi Boudra422bf772019-12-02 11:10:16 +020020# File extensions to check
Zelalem219df412020-05-17 19:21:20 -050021VALID_FILE_EXTENSIONS = (".c", ".S", ".h")
Fathi Boudra422bf772019-12-02 11:10:16 +020022
23
24# Paths inside the tree to ignore. Hidden folders and files are always ignored.
25# They mustn't end in '/'.
Zelalem219df412020-05-17 19:21:20 -050026IGNORED_FOLDERS = (
27 "include/lib/stdlib",
28 "include/lib/libc",
Zelalem219df412020-05-17 19:21:20 -050029 "lib/libc",
30 "lib/stdlib",
Fathi Boudra422bf772019-12-02 11:10:16 +020031)
32
Zelalem219df412020-05-17 19:21:20 -050033# List of ignored files in folders that aren't ignored
34IGNORED_FILES = ()
Fathi Boudra422bf772019-12-02 11:10:16 +020035
Zelalem219df412020-05-17 19:21:20 -050036INCLUDE_RE = re.compile(r"^\s*#\s*include\s\s*(?P<path>[\"<].+[\">])")
37INCLUDE_RE_DIFF = re.compile(r"^\+?\s*#\s*include\s\s*(?P<path>[\"<].+[\">])")
Fathi Boudra422bf772019-12-02 11:10:16 +020038
39
Zelalem219df412020-05-17 19:21:20 -050040def include_paths(lines, diff_mode=False):
41 """List all include paths in a file. Ignore starting `+` in diff mode."""
42 pattern = INCLUDE_RE_DIFF if diff_mode else INCLUDE_RE
43 matches = (pattern.match(line) for line in lines)
44 return [m.group("path") for m in matches if m]
Fathi Boudra422bf772019-12-02 11:10:16 +020045
46
Zelalem219df412020-05-17 19:21:20 -050047def file_include_list(path):
48 """Return a list of all include paths in a file or None on failure."""
Fathi Boudra422bf772019-12-02 11:10:16 +020049 try:
Zelalem219df412020-05-17 19:21:20 -050050 with codecs.open(path, encoding="utf-8") as f:
51 return include_paths(f)
52 except Exception:
53 logging.exception(path + ":error while parsing.")
Fathi Boudra422bf772019-12-02 11:10:16 +020054 return None
55
Fathi Boudra422bf772019-12-02 11:10:16 +020056
Zelalemf75daf22020-08-04 16:29:43 -050057@functools.lru_cache()
58def dir_include_paths(directory):
59 """Generate a set that contains all includes from a directory"""
60 dir_includes = set()
61 for (root, _dirs, files) in os.walk(directory):
62 for fname in files:
Stephan Gerhold8ffa3d52023-06-20 16:19:40 +020063 if fname.endswith(".h") or fname.endswith(".S"):
Zelalemf75daf22020-08-04 16:29:43 -050064 names = os.path.join(root, fname).split(os.sep)
65 for i in range(len(names)):
66 suffix_path = "/".join(names[i:])
67 dir_includes.add(suffix_path)
68 return dir_includes
69
70
Fathi Boudra422bf772019-12-02 11:10:16 +020071def inc_order_is_correct(inc_list, path, commit_hash=""):
Zelalem219df412020-05-17 19:21:20 -050072 """Returns true if the provided list is in order. If not, output error
73 messages to stdout."""
Fathi Boudra422bf772019-12-02 11:10:16 +020074
75 # If there are less than 2 includes there's no need to check.
76 if len(inc_list) < 2:
77 return True
78
Yann Gautiere5b08f22023-09-04 15:01:59 +020079 if utils.file_is_ignored(
80 path, VALID_FILE_EXTENSIONS, IGNORED_FILES, IGNORED_FOLDERS
81 ):
82 return True
83
Fathi Boudra422bf772019-12-02 11:10:16 +020084 if commit_hash != "":
Zelalem219df412020-05-17 19:21:20 -050085 commit_hash = commit_hash + ":"
Fathi Boudra422bf772019-12-02 11:10:16 +020086
Zelalem219df412020-05-17 19:21:20 -050087 # First, check if all includes are in the appropriate group.
Zelalem219df412020-05-17 19:21:20 -050088 incs = collections.defaultdict(list)
89 error_msgs = []
Zelalemf75daf22020-08-04 16:29:43 -050090 plat_incs = dir_include_paths("plat") | dir_include_paths("include/plat")
Yann Gautier72d2dbc2021-09-14 10:48:24 +020091 plat_common_incs = dir_include_paths("include/plat/common")
92 plat_incs.difference_update(plat_common_incs)
Zelalemf75daf22020-08-04 16:29:43 -050093 libc_incs = dir_include_paths("include/lib/libc")
Harrison Mutaiec878ce2022-05-13 15:18:38 +010094 proj_incs = dir_include_paths("include/")
Govindraj Raja59b872e2023-03-07 10:44:13 +000095 third_party_incs = []
96 third_party_incs.append(dir_include_paths("mbedtls"))
97 third_party_incs.append(dir_include_paths("include/lib/libfdt"))
98 third_party_incs.append(dir_include_paths("lib/compiler-rt"))
99 third_party_incs.append(dir_include_paths("lib/libfdt"))
100 third_party_incs.append(dir_include_paths("lib/zlib"))
101
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000102 indices = []
Fathi Boudra422bf772019-12-02 11:10:16 +0200103
104 for inc in inc_list:
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100105 inc_path = inc[1:-1].replace("..", Path(path).parents[1].as_posix())
106 inc_group_index = int(inc_path not in libc_incs)
107
108 if inc_group_index:
Govindraj Raja59b872e2023-03-07 10:44:13 +0000109 if inc_path in third_party_incs:
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100110 inc_group_index = 1
Govindraj Raja59b872e2023-03-07 10:44:13 +0000111 elif inc_path in proj_incs:
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100112 inc_group_index = 2
Govindraj Raja59b872e2023-03-07 10:44:13 +0000113 elif inc_path in plat_incs:
114 inc_group_index = 3
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000115
116 incs[inc_group_index].append(inc_path)
117 indices.append((inc_group_index, inc))
118
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100119 index_sorted_paths = sorted(indices, key=lambda x: (x[0], x[1][1:-1]))
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000120 if indices != index_sorted_paths:
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100121 error_msgs.append("Include ordering error, order should be:")
122 last_group = index_sorted_paths[0][0]
123 for inc in index_sorted_paths:
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000124 # Right angle brackets are a special entity in html, convert the
125 # name to an html friendly format.
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100126 path_ = inc[1] if "<" not in inc[1] else f"&lt{inc[1][1:-1]}&gt"
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000127
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100128 if last_group != inc[0]:
129 error_msgs.append("")
130 last_group = inc[0]
Fathi Boudra422bf772019-12-02 11:10:16 +0200131
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100132 error_msgs.append(f"\t#include {path_}")
Fathi Boudra422bf772019-12-02 11:10:16 +0200133
134 # Output error messages.
Zelalem219df412020-05-17 19:21:20 -0500135 if error_msgs:
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000136 print(f"\n{commit_hash}:{path}:")
137 print(*error_msgs, sep="\n")
Zelalem219df412020-05-17 19:21:20 -0500138 return False
139 else:
140 return True
Fathi Boudra422bf772019-12-02 11:10:16 +0200141
142
143def file_is_correct(path):
Zelalem219df412020-05-17 19:21:20 -0500144 """Checks whether the order of includes in the file specified in the path
145 is correct or not."""
146 inc_list = file_include_list(path)
147 return inc_list is not None and inc_order_is_correct(inc_list, path)
Fathi Boudra422bf772019-12-02 11:10:16 +0200148
149
150def directory_tree_is_correct():
Zelalem219df412020-05-17 19:21:20 -0500151 """Checks all tracked files in the current git repository, except the ones
Fathi Boudra422bf772019-12-02 11:10:16 +0200152 explicitly ignored by this script.
Zelalem219df412020-05-17 19:21:20 -0500153 Returns True if all files are correct."""
154 (rc, stdout, stderr) = utils.shell_command(["git", "ls-files"])
Fathi Boudra422bf772019-12-02 11:10:16 +0200155 if rc != 0:
156 return False
Fathi Boudra422bf772019-12-02 11:10:16 +0200157 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500158 for f in stdout.splitlines():
159 if not utils.file_is_ignored(
160 f, VALID_FILE_EXTENSIONS, IGNORED_FILES, IGNORED_FOLDERS
161 ):
162 all_files_correct &= file_is_correct(f)
Fathi Boudra422bf772019-12-02 11:10:16 +0200163 return all_files_correct
164
165
Zelalem219df412020-05-17 19:21:20 -0500166def group_lines(patchlines, starting_with):
167 """Generator of (name, lines) almost the same as itertools.groupby
168
169 This function's control flow is non-trivial. In particular, the clearing
170 of the lines variable, marked with [1], is intentional and must come
171 after the yield. That's because we must yield the (name, lines) tuple
172 after we have found the name of the next section but before we assign the
173 name and start collecting lines. Further, [2] is required to yeild the
174 last block as there will not be a block start delimeter at the end of
175 the stream.
176 """
177 lines = []
178 name = None
179 for line in patchlines:
180 if line.startswith(starting_with):
181 if name:
182 yield name, lines
183 name = line[len(starting_with) :]
184 lines = [] # [1]
185 else:
186 lines.append(line)
187 yield name, lines # [2]
188
189
190def group_files(commitlines):
191 """Generator of (commit hash, lines) almost the same as itertools.groupby"""
192 return group_lines(commitlines, "+++ b/")
193
194
195def group_commits(commitlines):
196 """Generator of (file name, lines) almost the same as itertools.groupby"""
197 return group_lines(commitlines, "commit ")
198
199
Fathi Boudra422bf772019-12-02 11:10:16 +0200200def patch_is_correct(base_commit, end_commit):
Zelalem219df412020-05-17 19:21:20 -0500201 """Get the output of a git diff and analyse each modified file."""
Fathi Boudra422bf772019-12-02 11:10:16 +0200202
203 # Get patches of the affected commits with one line of context.
Zelalem219df412020-05-17 19:21:20 -0500204 gitlog = subprocess.run(
205 [
206 "git",
207 "log",
208 "--unified=1",
209 "--pretty=commit %h",
210 base_commit + ".." + end_commit,
211 ],
212 stdout=subprocess.PIPE,
213 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200214
Zelalem219df412020-05-17 19:21:20 -0500215 if gitlog.returncode != 0:
Fathi Boudra422bf772019-12-02 11:10:16 +0200216 return False
217
Zelalem219df412020-05-17 19:21:20 -0500218 gitlines = gitlog.stdout.decode("utf-8").splitlines()
Fathi Boudra422bf772019-12-02 11:10:16 +0200219 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500220 for commit, comlines in group_commits(gitlines):
221 for path, lines in group_files(comlines):
222 all_files_correct &= inc_order_is_correct(
223 include_paths(lines, diff_mode=True), path, commit
224 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200225 return all_files_correct
226
227
Fathi Boudra422bf772019-12-02 11:10:16 +0200228def parse_cmd_line(argv, prog_name):
229 parser = argparse.ArgumentParser(
230 prog=prog_name,
231 formatter_class=argparse.RawTextHelpFormatter,
232 description="Check alphabetical order of #includes",
233 epilog="""
234For each source file in the tree, checks that #include's C preprocessor
235directives are ordered alphabetically (as mandated by the Trusted
236Firmware coding style). System header includes must come before user
237header includes.
Zelalem219df412020-05-17 19:21:20 -0500238""",
239 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200240
Zelalem219df412020-05-17 19:21:20 -0500241 parser.add_argument(
242 "--tree",
243 "-t",
244 help="Path to the source tree to check (default: %(default)s)",
245 default=os.curdir,
246 )
247 parser.add_argument(
248 "--patch",
249 "-p",
250 help="""
Fathi Boudra422bf772019-12-02 11:10:16 +0200251Patch mode.
252Instead of checking all files in the source tree, the script will consider
253only files that are modified by the latest patch(es).""",
Zelalem219df412020-05-17 19:21:20 -0500254 action="store_true",
255 )
256 parser.add_argument(
257 "--from-ref",
258 help="Base commit in patch mode (default: %(default)s)",
259 default="master",
260 )
261 parser.add_argument(
262 "--to-ref",
263 help="Final commit in patch mode (default: %(default)s)",
264 default="HEAD",
265 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200266 args = parser.parse_args(argv)
267 return args
268
269
270if __name__ == "__main__":
271 args = parse_cmd_line(sys.argv[1:], sys.argv[0])
272
273 os.chdir(args.tree)
274
275 if args.patch:
Zelalem219df412020-05-17 19:21:20 -0500276 print(
277 "Checking files modified between patches "
278 + args.from_ref
279 + " and "
280 + args.to_ref
281 + "..."
282 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200283 if not patch_is_correct(args.from_ref, args.to_ref):
284 sys.exit(1)
285 else:
286 print("Checking all files in directory '%s'..." % os.path.abspath(args.tree))
287 if not directory_tree_is_correct():
288 sys.exit(1)
289
290 # All source code files are correct.
291 sys.exit(0)