blob: 095350788dbcce61646596f8db6463f77788264f [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",
29 "include/lib/libfdt",
30 "lib/libfdt",
31 "lib/libc",
32 "lib/stdlib",
Fathi Boudra422bf772019-12-02 11:10:16 +020033)
34
Zelalem219df412020-05-17 19:21:20 -050035# List of ignored files in folders that aren't ignored
36IGNORED_FILES = ()
Fathi Boudra422bf772019-12-02 11:10:16 +020037
Zelalem219df412020-05-17 19:21:20 -050038INCLUDE_RE = re.compile(r"^\s*#\s*include\s\s*(?P<path>[\"<].+[\">])")
39INCLUDE_RE_DIFF = re.compile(r"^\+?\s*#\s*include\s\s*(?P<path>[\"<].+[\">])")
Fathi Boudra422bf772019-12-02 11:10:16 +020040
41
Zelalem219df412020-05-17 19:21:20 -050042def include_paths(lines, diff_mode=False):
43 """List all include paths in a file. Ignore starting `+` in diff mode."""
44 pattern = INCLUDE_RE_DIFF if diff_mode else INCLUDE_RE
45 matches = (pattern.match(line) for line in lines)
46 return [m.group("path") for m in matches if m]
Fathi Boudra422bf772019-12-02 11:10:16 +020047
48
Zelalem219df412020-05-17 19:21:20 -050049def file_include_list(path):
50 """Return a list of all include paths in a file or None on failure."""
Fathi Boudra422bf772019-12-02 11:10:16 +020051 try:
Zelalem219df412020-05-17 19:21:20 -050052 with codecs.open(path, encoding="utf-8") as f:
53 return include_paths(f)
54 except Exception:
55 logging.exception(path + ":error while parsing.")
Fathi Boudra422bf772019-12-02 11:10:16 +020056 return None
57
Fathi Boudra422bf772019-12-02 11:10:16 +020058
Zelalemf75daf22020-08-04 16:29:43 -050059@functools.lru_cache()
60def dir_include_paths(directory):
61 """Generate a set that contains all includes from a directory"""
62 dir_includes = set()
63 for (root, _dirs, files) in os.walk(directory):
64 for fname in files:
65 if fname.endswith(".h"):
66 names = os.path.join(root, fname).split(os.sep)
67 for i in range(len(names)):
68 suffix_path = "/".join(names[i:])
69 dir_includes.add(suffix_path)
70 return dir_includes
71
72
Fathi Boudra422bf772019-12-02 11:10:16 +020073def inc_order_is_correct(inc_list, path, commit_hash=""):
Zelalem219df412020-05-17 19:21:20 -050074 """Returns true if the provided list is in order. If not, output error
75 messages to stdout."""
Fathi Boudra422bf772019-12-02 11:10:16 +020076
77 # If there are less than 2 includes there's no need to check.
78 if len(inc_list) < 2:
79 return True
80
81 if commit_hash != "":
Zelalem219df412020-05-17 19:21:20 -050082 commit_hash = commit_hash + ":"
Fathi Boudra422bf772019-12-02 11:10:16 +020083
Zelalem219df412020-05-17 19:21:20 -050084 # First, check if all includes are in the appropriate group.
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 Mutaiec878ce2022-05-13 15:18:38 +010091 proj_incs = dir_include_paths("include/")
Harrison Mutai8c3afa32022-02-04 09:33:24 +000092 indices = []
Fathi Boudra422bf772019-12-02 11:10:16 +020093
94 for inc in inc_list:
Harrison Mutaiec878ce2022-05-13 15:18:38 +010095 inc_path = inc[1:-1].replace("..", Path(path).parents[1].as_posix())
96 inc_group_index = int(inc_path not in libc_incs)
97
98 if inc_group_index:
99 if inc_path in proj_incs:
100 inc_group_index = 1
101 elif inc_path in plat_incs:
102 inc_group_index = 2
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000103
104 incs[inc_group_index].append(inc_path)
105 indices.append((inc_group_index, inc))
106
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100107 index_sorted_paths = sorted(indices, key=lambda x: (x[0], x[1][1:-1]))
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000108 if indices != index_sorted_paths:
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100109 error_msgs.append("Include ordering error, order should be:")
110 last_group = index_sorted_paths[0][0]
111 for inc in index_sorted_paths:
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000112 # Right angle brackets are a special entity in html, convert the
113 # name to an html friendly format.
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100114 path_ = inc[1] if "<" not in inc[1] else f"&lt{inc[1][1:-1]}&gt"
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000115
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100116 if last_group != inc[0]:
117 error_msgs.append("")
118 last_group = inc[0]
Fathi Boudra422bf772019-12-02 11:10:16 +0200119
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100120 error_msgs.append(f"\t#include {path_}")
Fathi Boudra422bf772019-12-02 11:10:16 +0200121
122 # Output error messages.
Zelalem219df412020-05-17 19:21:20 -0500123 if error_msgs:
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000124 print(f"\n{commit_hash}:{path}:")
125 print(*error_msgs, sep="\n")
Zelalem219df412020-05-17 19:21:20 -0500126 return False
127 else:
128 return True
Fathi Boudra422bf772019-12-02 11:10:16 +0200129
130
131def file_is_correct(path):
Zelalem219df412020-05-17 19:21:20 -0500132 """Checks whether the order of includes in the file specified in the path
133 is correct or not."""
134 inc_list = file_include_list(path)
135 return inc_list is not None and inc_order_is_correct(inc_list, path)
Fathi Boudra422bf772019-12-02 11:10:16 +0200136
137
138def directory_tree_is_correct():
Zelalem219df412020-05-17 19:21:20 -0500139 """Checks all tracked files in the current git repository, except the ones
Fathi Boudra422bf772019-12-02 11:10:16 +0200140 explicitly ignored by this script.
Zelalem219df412020-05-17 19:21:20 -0500141 Returns True if all files are correct."""
142 (rc, stdout, stderr) = utils.shell_command(["git", "ls-files"])
Fathi Boudra422bf772019-12-02 11:10:16 +0200143 if rc != 0:
144 return False
Fathi Boudra422bf772019-12-02 11:10:16 +0200145 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500146 for f in stdout.splitlines():
147 if not utils.file_is_ignored(
148 f, VALID_FILE_EXTENSIONS, IGNORED_FILES, IGNORED_FOLDERS
149 ):
150 all_files_correct &= file_is_correct(f)
Fathi Boudra422bf772019-12-02 11:10:16 +0200151 return all_files_correct
152
153
Zelalem219df412020-05-17 19:21:20 -0500154def group_lines(patchlines, starting_with):
155 """Generator of (name, lines) almost the same as itertools.groupby
156
157 This function's control flow is non-trivial. In particular, the clearing
158 of the lines variable, marked with [1], is intentional and must come
159 after the yield. That's because we must yield the (name, lines) tuple
160 after we have found the name of the next section but before we assign the
161 name and start collecting lines. Further, [2] is required to yeild the
162 last block as there will not be a block start delimeter at the end of
163 the stream.
164 """
165 lines = []
166 name = None
167 for line in patchlines:
168 if line.startswith(starting_with):
169 if name:
170 yield name, lines
171 name = line[len(starting_with) :]
172 lines = [] # [1]
173 else:
174 lines.append(line)
175 yield name, lines # [2]
176
177
178def group_files(commitlines):
179 """Generator of (commit hash, lines) almost the same as itertools.groupby"""
180 return group_lines(commitlines, "+++ b/")
181
182
183def group_commits(commitlines):
184 """Generator of (file name, lines) almost the same as itertools.groupby"""
185 return group_lines(commitlines, "commit ")
186
187
Fathi Boudra422bf772019-12-02 11:10:16 +0200188def patch_is_correct(base_commit, end_commit):
Zelalem219df412020-05-17 19:21:20 -0500189 """Get the output of a git diff and analyse each modified file."""
Fathi Boudra422bf772019-12-02 11:10:16 +0200190
191 # Get patches of the affected commits with one line of context.
Zelalem219df412020-05-17 19:21:20 -0500192 gitlog = subprocess.run(
193 [
194 "git",
195 "log",
196 "--unified=1",
197 "--pretty=commit %h",
198 base_commit + ".." + end_commit,
199 ],
200 stdout=subprocess.PIPE,
201 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200202
Zelalem219df412020-05-17 19:21:20 -0500203 if gitlog.returncode != 0:
Fathi Boudra422bf772019-12-02 11:10:16 +0200204 return False
205
Zelalem219df412020-05-17 19:21:20 -0500206 gitlines = gitlog.stdout.decode("utf-8").splitlines()
Fathi Boudra422bf772019-12-02 11:10:16 +0200207 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500208 for commit, comlines in group_commits(gitlines):
209 for path, lines in group_files(comlines):
210 all_files_correct &= inc_order_is_correct(
211 include_paths(lines, diff_mode=True), path, commit
212 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200213 return all_files_correct
214
215
Fathi Boudra422bf772019-12-02 11:10:16 +0200216def parse_cmd_line(argv, prog_name):
217 parser = argparse.ArgumentParser(
218 prog=prog_name,
219 formatter_class=argparse.RawTextHelpFormatter,
220 description="Check alphabetical order of #includes",
221 epilog="""
222For each source file in the tree, checks that #include's C preprocessor
223directives are ordered alphabetically (as mandated by the Trusted
224Firmware coding style). System header includes must come before user
225header includes.
Zelalem219df412020-05-17 19:21:20 -0500226""",
227 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200228
Zelalem219df412020-05-17 19:21:20 -0500229 parser.add_argument(
230 "--tree",
231 "-t",
232 help="Path to the source tree to check (default: %(default)s)",
233 default=os.curdir,
234 )
235 parser.add_argument(
236 "--patch",
237 "-p",
238 help="""
Fathi Boudra422bf772019-12-02 11:10:16 +0200239Patch mode.
240Instead of checking all files in the source tree, the script will consider
241only files that are modified by the latest patch(es).""",
Zelalem219df412020-05-17 19:21:20 -0500242 action="store_true",
243 )
244 parser.add_argument(
245 "--from-ref",
246 help="Base commit in patch mode (default: %(default)s)",
247 default="master",
248 )
249 parser.add_argument(
250 "--to-ref",
251 help="Final commit in patch mode (default: %(default)s)",
252 default="HEAD",
253 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200254 args = parser.parse_args(argv)
255 return args
256
257
258if __name__ == "__main__":
259 args = parse_cmd_line(sys.argv[1:], sys.argv[0])
260
261 os.chdir(args.tree)
262
263 if args.patch:
Zelalem219df412020-05-17 19:21:20 -0500264 print(
265 "Checking files modified between patches "
266 + args.from_ref
267 + " and "
268 + args.to_ref
269 + "..."
270 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200271 if not patch_is_correct(args.from_ref, args.to_ref):
272 sys.exit(1)
273 else:
274 print("Checking all files in directory '%s'..." % os.path.abspath(args.tree))
275 if not directory_tree_is_correct():
276 sys.exit(1)
277
278 # All source code files are correct.
279 sys.exit(0)