blob: 1ffe0204cee05db768acbc1c508f9a0119597d9c [file] [log] [blame]
Fathi Boudra422bf772019-12-02 11:10:16 +02001#!/usr/bin/env python3
2#
Harrison Mutai8c3afa32022-02-04 09:33:24 +00003# Copyright (c) 2019-2022, 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
Fathi Boudra422bf772019-12-02 11:10:16 +020018
Fathi Boudra422bf772019-12-02 11:10:16 +020019# File extensions to check
Zelalem219df412020-05-17 19:21:20 -050020VALID_FILE_EXTENSIONS = (".c", ".S", ".h")
Fathi Boudra422bf772019-12-02 11:10:16 +020021
22
23# Paths inside the tree to ignore. Hidden folders and files are always ignored.
24# They mustn't end in '/'.
Zelalem219df412020-05-17 19:21:20 -050025IGNORED_FOLDERS = (
26 "include/lib/stdlib",
27 "include/lib/libc",
28 "include/lib/libfdt",
29 "lib/libfdt",
30 "lib/libc",
31 "lib/stdlib",
Fathi Boudra422bf772019-12-02 11:10:16 +020032)
33
Zelalem219df412020-05-17 19:21:20 -050034# List of ignored files in folders that aren't ignored
35IGNORED_FILES = ()
Fathi Boudra422bf772019-12-02 11:10:16 +020036
Zelalem219df412020-05-17 19:21:20 -050037INCLUDE_RE = re.compile(r"^\s*#\s*include\s\s*(?P<path>[\"<].+[\">])")
38INCLUDE_RE_DIFF = re.compile(r"^\+?\s*#\s*include\s\s*(?P<path>[\"<].+[\">])")
Fathi Boudra422bf772019-12-02 11:10:16 +020039
40
Zelalem219df412020-05-17 19:21:20 -050041def include_paths(lines, diff_mode=False):
42 """List all include paths in a file. Ignore starting `+` in diff mode."""
43 pattern = INCLUDE_RE_DIFF if diff_mode else INCLUDE_RE
44 matches = (pattern.match(line) for line in lines)
45 return [m.group("path") for m in matches if m]
Fathi Boudra422bf772019-12-02 11:10:16 +020046
47
Zelalem219df412020-05-17 19:21:20 -050048def file_include_list(path):
49 """Return a list of all include paths in a file or None on failure."""
Fathi Boudra422bf772019-12-02 11:10:16 +020050 try:
Zelalem219df412020-05-17 19:21:20 -050051 with codecs.open(path, encoding="utf-8") as f:
52 return include_paths(f)
53 except Exception:
54 logging.exception(path + ":error while parsing.")
Fathi Boudra422bf772019-12-02 11:10:16 +020055 return None
56
Fathi Boudra422bf772019-12-02 11:10:16 +020057
Zelalemf75daf22020-08-04 16:29:43 -050058@functools.lru_cache()
59def dir_include_paths(directory):
60 """Generate a set that contains all includes from a directory"""
61 dir_includes = set()
62 for (root, _dirs, files) in os.walk(directory):
63 for fname in files:
64 if fname.endswith(".h"):
65 names = os.path.join(root, fname).split(os.sep)
66 for i in range(len(names)):
67 suffix_path = "/".join(names[i:])
68 dir_includes.add(suffix_path)
69 return dir_includes
70
71
Fathi Boudra422bf772019-12-02 11:10:16 +020072def inc_order_is_correct(inc_list, path, commit_hash=""):
Zelalem219df412020-05-17 19:21:20 -050073 """Returns true if the provided list is in order. If not, output error
74 messages to stdout."""
Fathi Boudra422bf772019-12-02 11:10:16 +020075
76 # If there are less than 2 includes there's no need to check.
77 if len(inc_list) < 2:
78 return True
79
80 if commit_hash != "":
Zelalem219df412020-05-17 19:21:20 -050081 commit_hash = commit_hash + ":"
Fathi Boudra422bf772019-12-02 11:10:16 +020082
Zelalem219df412020-05-17 19:21:20 -050083 # First, check if all includes are in the appropriate group.
Harrison Mutai8c3afa32022-02-04 09:33:24 +000084 inc_group = "System", "Project", "Platform"
Zelalem219df412020-05-17 19:21:20 -050085 incs = collections.defaultdict(list)
86 error_msgs = []
Zelalemf75daf22020-08-04 16:29:43 -050087 plat_incs = dir_include_paths("plat") | dir_include_paths("include/plat")
Yann Gautier72d2dbc2021-09-14 10:48:24 +020088 plat_common_incs = dir_include_paths("include/plat/common")
89 plat_incs.difference_update(plat_common_incs)
Zelalemf75daf22020-08-04 16:29:43 -050090 libc_incs = dir_include_paths("include/lib/libc")
Harrison Mutai8c3afa32022-02-04 09:33:24 +000091 indices = []
Fathi Boudra422bf772019-12-02 11:10:16 +020092
93 for inc in inc_list:
Zelalemf75daf22020-08-04 16:29:43 -050094 inc_path = inc[1:-1]
95 if inc_path in libc_incs:
Harrison Mutai8c3afa32022-02-04 09:33:24 +000096 inc_group_index = 0
Zelalemf75daf22020-08-04 16:29:43 -050097 elif inc_path in plat_incs:
Harrison Mutai8c3afa32022-02-04 09:33:24 +000098 inc_group_index = 2
Zelalem219df412020-05-17 19:21:20 -050099 else:
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000100 inc_group_index = 1
101
102 incs[inc_group_index].append(inc_path)
103 indices.append((inc_group_index, inc))
104
105 index_sorted_paths = sorted(indices, key=lambda x: x[0])
106
107 if indices != index_sorted_paths:
108 error_msgs.append("Group ordering error, order should be:")
109 for index_orig, index_new in zip(indices, index_sorted_paths):
110 # Right angle brackets are a special entity in html, convert the
111 # name to an html friendly format.
112 path_ = index_new[1]
113 if "<" in path_:
114 path_ = f"&lt{path_[1:-1]}&gt"
115
116 if index_orig[0] != index_new[0]:
117 error_msgs.append(
118 f"\t** #include {path_:<30} --> " \
119 f"{inc_group[index_new[0]].lower()} header, moved to group "\
120 f"{index_new[0]+1}."
121 )
122 else:
123 error_msgs.append(f"\t#include {path_}")
Fathi Boudra422bf772019-12-02 11:10:16 +0200124
Zelalem219df412020-05-17 19:21:20 -0500125 # Then, check alphabetic order (system, project and user separately).
126 if not error_msgs:
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000127 for i, inc_list in incs.items():
Zelalem219df412020-05-17 19:21:20 -0500128 if sorted(inc_list) != inc_list:
Zelalemf75daf22020-08-04 16:29:43 -0500129 error_msgs.append(
130 "{} includes not in order. Include order should be {}".format(
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000131 inc_group[i], ", ".join(sorted(inc_list))
Zelalemf75daf22020-08-04 16:29:43 -0500132 )
133 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200134
135 # Output error messages.
Zelalem219df412020-05-17 19:21:20 -0500136 if error_msgs:
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000137 print(f"\n{commit_hash}:{path}:")
138 print(*error_msgs, sep="\n")
Zelalem219df412020-05-17 19:21:20 -0500139 return False
140 else:
141 return True
Fathi Boudra422bf772019-12-02 11:10:16 +0200142
143
144def file_is_correct(path):
Zelalem219df412020-05-17 19:21:20 -0500145 """Checks whether the order of includes in the file specified in the path
146 is correct or not."""
147 inc_list = file_include_list(path)
148 return inc_list is not None and inc_order_is_correct(inc_list, path)
Fathi Boudra422bf772019-12-02 11:10:16 +0200149
150
151def directory_tree_is_correct():
Zelalem219df412020-05-17 19:21:20 -0500152 """Checks all tracked files in the current git repository, except the ones
Fathi Boudra422bf772019-12-02 11:10:16 +0200153 explicitly ignored by this script.
Zelalem219df412020-05-17 19:21:20 -0500154 Returns True if all files are correct."""
155 (rc, stdout, stderr) = utils.shell_command(["git", "ls-files"])
Fathi Boudra422bf772019-12-02 11:10:16 +0200156 if rc != 0:
157 return False
Fathi Boudra422bf772019-12-02 11:10:16 +0200158 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500159 for f in stdout.splitlines():
160 if not utils.file_is_ignored(
161 f, VALID_FILE_EXTENSIONS, IGNORED_FILES, IGNORED_FOLDERS
162 ):
163 all_files_correct &= file_is_correct(f)
Fathi Boudra422bf772019-12-02 11:10:16 +0200164 return all_files_correct
165
166
Zelalem219df412020-05-17 19:21:20 -0500167def group_lines(patchlines, starting_with):
168 """Generator of (name, lines) almost the same as itertools.groupby
169
170 This function's control flow is non-trivial. In particular, the clearing
171 of the lines variable, marked with [1], is intentional and must come
172 after the yield. That's because we must yield the (name, lines) tuple
173 after we have found the name of the next section but before we assign the
174 name and start collecting lines. Further, [2] is required to yeild the
175 last block as there will not be a block start delimeter at the end of
176 the stream.
177 """
178 lines = []
179 name = None
180 for line in patchlines:
181 if line.startswith(starting_with):
182 if name:
183 yield name, lines
184 name = line[len(starting_with) :]
185 lines = [] # [1]
186 else:
187 lines.append(line)
188 yield name, lines # [2]
189
190
191def group_files(commitlines):
192 """Generator of (commit hash, lines) almost the same as itertools.groupby"""
193 return group_lines(commitlines, "+++ b/")
194
195
196def group_commits(commitlines):
197 """Generator of (file name, lines) almost the same as itertools.groupby"""
198 return group_lines(commitlines, "commit ")
199
200
Fathi Boudra422bf772019-12-02 11:10:16 +0200201def patch_is_correct(base_commit, end_commit):
Zelalem219df412020-05-17 19:21:20 -0500202 """Get the output of a git diff and analyse each modified file."""
Fathi Boudra422bf772019-12-02 11:10:16 +0200203
204 # Get patches of the affected commits with one line of context.
Zelalem219df412020-05-17 19:21:20 -0500205 gitlog = subprocess.run(
206 [
207 "git",
208 "log",
209 "--unified=1",
210 "--pretty=commit %h",
211 base_commit + ".." + end_commit,
212 ],
213 stdout=subprocess.PIPE,
214 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200215
Zelalem219df412020-05-17 19:21:20 -0500216 if gitlog.returncode != 0:
Fathi Boudra422bf772019-12-02 11:10:16 +0200217 return False
218
Zelalem219df412020-05-17 19:21:20 -0500219 gitlines = gitlog.stdout.decode("utf-8").splitlines()
Fathi Boudra422bf772019-12-02 11:10:16 +0200220 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500221 for commit, comlines in group_commits(gitlines):
222 for path, lines in group_files(comlines):
223 all_files_correct &= inc_order_is_correct(
224 include_paths(lines, diff_mode=True), path, commit
225 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200226 return all_files_correct
227
228
Fathi Boudra422bf772019-12-02 11:10:16 +0200229def parse_cmd_line(argv, prog_name):
230 parser = argparse.ArgumentParser(
231 prog=prog_name,
232 formatter_class=argparse.RawTextHelpFormatter,
233 description="Check alphabetical order of #includes",
234 epilog="""
235For each source file in the tree, checks that #include's C preprocessor
236directives are ordered alphabetically (as mandated by the Trusted
237Firmware coding style). System header includes must come before user
238header includes.
Zelalem219df412020-05-17 19:21:20 -0500239""",
240 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200241
Zelalem219df412020-05-17 19:21:20 -0500242 parser.add_argument(
243 "--tree",
244 "-t",
245 help="Path to the source tree to check (default: %(default)s)",
246 default=os.curdir,
247 )
248 parser.add_argument(
249 "--patch",
250 "-p",
251 help="""
Fathi Boudra422bf772019-12-02 11:10:16 +0200252Patch mode.
253Instead of checking all files in the source tree, the script will consider
254only files that are modified by the latest patch(es).""",
Zelalem219df412020-05-17 19:21:20 -0500255 action="store_true",
256 )
257 parser.add_argument(
258 "--from-ref",
259 help="Base commit in patch mode (default: %(default)s)",
260 default="master",
261 )
262 parser.add_argument(
263 "--to-ref",
264 help="Final commit in patch mode (default: %(default)s)",
265 default="HEAD",
266 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200267 args = parser.parse_args(argv)
268 return args
269
270
271if __name__ == "__main__":
272 args = parse_cmd_line(sys.argv[1:], sys.argv[0])
273
274 os.chdir(args.tree)
275
276 if args.patch:
Zelalem219df412020-05-17 19:21:20 -0500277 print(
278 "Checking files modified between patches "
279 + args.from_ref
280 + " and "
281 + args.to_ref
282 + "..."
283 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200284 if not patch_is_correct(args.from_ref, args.to_ref):
285 sys.exit(1)
286 else:
287 print("Checking all files in directory '%s'..." % os.path.abspath(args.tree))
288 if not directory_tree_is_correct():
289 sys.exit(1)
290
291 # All source code files are correct.
292 sys.exit(0)