blob: 4961c0523ed8a5c841ebc561665411514822f606 [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
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:
63 if fname.endswith(".h"):
64 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
79 if commit_hash != "":
Zelalem219df412020-05-17 19:21:20 -050080 commit_hash = commit_hash + ":"
Fathi Boudra422bf772019-12-02 11:10:16 +020081
Zelalem219df412020-05-17 19:21:20 -050082 # First, check if all includes are in the appropriate group.
Zelalem219df412020-05-17 19:21:20 -050083 incs = collections.defaultdict(list)
84 error_msgs = []
Zelalemf75daf22020-08-04 16:29:43 -050085 plat_incs = dir_include_paths("plat") | dir_include_paths("include/plat")
Yann Gautier72d2dbc2021-09-14 10:48:24 +020086 plat_common_incs = dir_include_paths("include/plat/common")
87 plat_incs.difference_update(plat_common_incs)
Zelalemf75daf22020-08-04 16:29:43 -050088 libc_incs = dir_include_paths("include/lib/libc")
Harrison Mutaiec878ce2022-05-13 15:18:38 +010089 proj_incs = dir_include_paths("include/")
Govindraj Raja59b872e2023-03-07 10:44:13 +000090 third_party_incs = []
91 third_party_incs.append(dir_include_paths("mbedtls"))
92 third_party_incs.append(dir_include_paths("include/lib/libfdt"))
93 third_party_incs.append(dir_include_paths("lib/compiler-rt"))
94 third_party_incs.append(dir_include_paths("lib/libfdt"))
95 third_party_incs.append(dir_include_paths("lib/zlib"))
96
Harrison Mutai8c3afa32022-02-04 09:33:24 +000097 indices = []
Fathi Boudra422bf772019-12-02 11:10:16 +020098
99 for inc in inc_list:
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100100 inc_path = inc[1:-1].replace("..", Path(path).parents[1].as_posix())
101 inc_group_index = int(inc_path not in libc_incs)
102
103 if inc_group_index:
Govindraj Raja59b872e2023-03-07 10:44:13 +0000104 if inc_path in third_party_incs:
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100105 inc_group_index = 1
Govindraj Raja59b872e2023-03-07 10:44:13 +0000106 elif inc_path in proj_incs:
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100107 inc_group_index = 2
Govindraj Raja59b872e2023-03-07 10:44:13 +0000108 elif inc_path in plat_incs:
109 inc_group_index = 3
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000110
111 incs[inc_group_index].append(inc_path)
112 indices.append((inc_group_index, inc))
113
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100114 index_sorted_paths = sorted(indices, key=lambda x: (x[0], x[1][1:-1]))
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000115 if indices != index_sorted_paths:
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100116 error_msgs.append("Include ordering error, order should be:")
117 last_group = index_sorted_paths[0][0]
118 for inc in index_sorted_paths:
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000119 # Right angle brackets are a special entity in html, convert the
120 # name to an html friendly format.
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100121 path_ = inc[1] if "<" not in inc[1] else f"&lt{inc[1][1:-1]}&gt"
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000122
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100123 if last_group != inc[0]:
124 error_msgs.append("")
125 last_group = inc[0]
Fathi Boudra422bf772019-12-02 11:10:16 +0200126
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100127 error_msgs.append(f"\t#include {path_}")
Fathi Boudra422bf772019-12-02 11:10:16 +0200128
129 # Output error messages.
Zelalem219df412020-05-17 19:21:20 -0500130 if error_msgs:
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000131 print(f"\n{commit_hash}:{path}:")
132 print(*error_msgs, sep="\n")
Zelalem219df412020-05-17 19:21:20 -0500133 return False
134 else:
135 return True
Fathi Boudra422bf772019-12-02 11:10:16 +0200136
137
138def file_is_correct(path):
Zelalem219df412020-05-17 19:21:20 -0500139 """Checks whether the order of includes in the file specified in the path
140 is correct or not."""
141 inc_list = file_include_list(path)
142 return inc_list is not None and inc_order_is_correct(inc_list, path)
Fathi Boudra422bf772019-12-02 11:10:16 +0200143
144
145def directory_tree_is_correct():
Zelalem219df412020-05-17 19:21:20 -0500146 """Checks all tracked files in the current git repository, except the ones
Fathi Boudra422bf772019-12-02 11:10:16 +0200147 explicitly ignored by this script.
Zelalem219df412020-05-17 19:21:20 -0500148 Returns True if all files are correct."""
149 (rc, stdout, stderr) = utils.shell_command(["git", "ls-files"])
Fathi Boudra422bf772019-12-02 11:10:16 +0200150 if rc != 0:
151 return False
Fathi Boudra422bf772019-12-02 11:10:16 +0200152 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500153 for f in stdout.splitlines():
154 if not utils.file_is_ignored(
155 f, VALID_FILE_EXTENSIONS, IGNORED_FILES, IGNORED_FOLDERS
156 ):
157 all_files_correct &= file_is_correct(f)
Fathi Boudra422bf772019-12-02 11:10:16 +0200158 return all_files_correct
159
160
Zelalem219df412020-05-17 19:21:20 -0500161def group_lines(patchlines, starting_with):
162 """Generator of (name, lines) almost the same as itertools.groupby
163
164 This function's control flow is non-trivial. In particular, the clearing
165 of the lines variable, marked with [1], is intentional and must come
166 after the yield. That's because we must yield the (name, lines) tuple
167 after we have found the name of the next section but before we assign the
168 name and start collecting lines. Further, [2] is required to yeild the
169 last block as there will not be a block start delimeter at the end of
170 the stream.
171 """
172 lines = []
173 name = None
174 for line in patchlines:
175 if line.startswith(starting_with):
176 if name:
177 yield name, lines
178 name = line[len(starting_with) :]
179 lines = [] # [1]
180 else:
181 lines.append(line)
182 yield name, lines # [2]
183
184
185def group_files(commitlines):
186 """Generator of (commit hash, lines) almost the same as itertools.groupby"""
187 return group_lines(commitlines, "+++ b/")
188
189
190def group_commits(commitlines):
191 """Generator of (file name, lines) almost the same as itertools.groupby"""
192 return group_lines(commitlines, "commit ")
193
194
Fathi Boudra422bf772019-12-02 11:10:16 +0200195def patch_is_correct(base_commit, end_commit):
Zelalem219df412020-05-17 19:21:20 -0500196 """Get the output of a git diff and analyse each modified file."""
Fathi Boudra422bf772019-12-02 11:10:16 +0200197
198 # Get patches of the affected commits with one line of context.
Zelalem219df412020-05-17 19:21:20 -0500199 gitlog = subprocess.run(
200 [
201 "git",
202 "log",
203 "--unified=1",
204 "--pretty=commit %h",
205 base_commit + ".." + end_commit,
206 ],
207 stdout=subprocess.PIPE,
208 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200209
Zelalem219df412020-05-17 19:21:20 -0500210 if gitlog.returncode != 0:
Fathi Boudra422bf772019-12-02 11:10:16 +0200211 return False
212
Zelalem219df412020-05-17 19:21:20 -0500213 gitlines = gitlog.stdout.decode("utf-8").splitlines()
Fathi Boudra422bf772019-12-02 11:10:16 +0200214 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500215 for commit, comlines in group_commits(gitlines):
216 for path, lines in group_files(comlines):
217 all_files_correct &= inc_order_is_correct(
218 include_paths(lines, diff_mode=True), path, commit
219 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200220 return all_files_correct
221
222
Fathi Boudra422bf772019-12-02 11:10:16 +0200223def parse_cmd_line(argv, prog_name):
224 parser = argparse.ArgumentParser(
225 prog=prog_name,
226 formatter_class=argparse.RawTextHelpFormatter,
227 description="Check alphabetical order of #includes",
228 epilog="""
229For each source file in the tree, checks that #include's C preprocessor
230directives are ordered alphabetically (as mandated by the Trusted
231Firmware coding style). System header includes must come before user
232header includes.
Zelalem219df412020-05-17 19:21:20 -0500233""",
234 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200235
Zelalem219df412020-05-17 19:21:20 -0500236 parser.add_argument(
237 "--tree",
238 "-t",
239 help="Path to the source tree to check (default: %(default)s)",
240 default=os.curdir,
241 )
242 parser.add_argument(
243 "--patch",
244 "-p",
245 help="""
Fathi Boudra422bf772019-12-02 11:10:16 +0200246Patch mode.
247Instead of checking all files in the source tree, the script will consider
248only files that are modified by the latest patch(es).""",
Zelalem219df412020-05-17 19:21:20 -0500249 action="store_true",
250 )
251 parser.add_argument(
252 "--from-ref",
253 help="Base commit in patch mode (default: %(default)s)",
254 default="master",
255 )
256 parser.add_argument(
257 "--to-ref",
258 help="Final commit in patch mode (default: %(default)s)",
259 default="HEAD",
260 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200261 args = parser.parse_args(argv)
262 return args
263
264
265if __name__ == "__main__":
266 args = parse_cmd_line(sys.argv[1:], sys.argv[0])
267
268 os.chdir(args.tree)
269
270 if args.patch:
Zelalem219df412020-05-17 19:21:20 -0500271 print(
272 "Checking files modified between patches "
273 + args.from_ref
274 + " and "
275 + args.to_ref
276 + "..."
277 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200278 if not patch_is_correct(args.from_ref, args.to_ref):
279 sys.exit(1)
280 else:
281 print("Checking all files in directory '%s'..." % os.path.abspath(args.tree))
282 if not directory_tree_is_correct():
283 sys.exit(1)
284
285 # All source code files are correct.
286 sys.exit(0)