blob: 1a9321200ace1ff0bf90b2c32f6ce43a2208dcfe [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
Paul Sokolovskye295b282024-06-13 17:07:01 +030042def subprocess_run(cmd, **kwargs):
43 logging.debug("Running command: %r %r", cmd, kwargs)
44 return subprocess.run(cmd, **kwargs)
45
46
Zelalem219df412020-05-17 19:21:20 -050047def include_paths(lines, diff_mode=False):
48 """List all include paths in a file. Ignore starting `+` in diff mode."""
49 pattern = INCLUDE_RE_DIFF if diff_mode else INCLUDE_RE
50 matches = (pattern.match(line) for line in lines)
51 return [m.group("path") for m in matches if m]
Fathi Boudra422bf772019-12-02 11:10:16 +020052
53
Zelalem219df412020-05-17 19:21:20 -050054def file_include_list(path):
55 """Return a list of all include paths in a file or None on failure."""
Fathi Boudra422bf772019-12-02 11:10:16 +020056 try:
Zelalem219df412020-05-17 19:21:20 -050057 with codecs.open(path, encoding="utf-8") as f:
58 return include_paths(f)
59 except Exception:
60 logging.exception(path + ":error while parsing.")
Fathi Boudra422bf772019-12-02 11:10:16 +020061 return None
62
Fathi Boudra422bf772019-12-02 11:10:16 +020063
Zelalemf75daf22020-08-04 16:29:43 -050064@functools.lru_cache()
65def dir_include_paths(directory):
66 """Generate a set that contains all includes from a directory"""
67 dir_includes = set()
68 for (root, _dirs, files) in os.walk(directory):
69 for fname in files:
70 if fname.endswith(".h"):
71 names = os.path.join(root, fname).split(os.sep)
72 for i in range(len(names)):
73 suffix_path = "/".join(names[i:])
74 dir_includes.add(suffix_path)
75 return dir_includes
76
77
Fathi Boudra422bf772019-12-02 11:10:16 +020078def inc_order_is_correct(inc_list, path, commit_hash=""):
Zelalem219df412020-05-17 19:21:20 -050079 """Returns true if the provided list is in order. If not, output error
80 messages to stdout."""
Fathi Boudra422bf772019-12-02 11:10:16 +020081
82 # If there are less than 2 includes there's no need to check.
83 if len(inc_list) < 2:
84 return True
85
86 if commit_hash != "":
Zelalem219df412020-05-17 19:21:20 -050087 commit_hash = commit_hash + ":"
Fathi Boudra422bf772019-12-02 11:10:16 +020088
Zelalem219df412020-05-17 19:21:20 -050089 # First, check if all includes are in the appropriate group.
Zelalem219df412020-05-17 19:21:20 -050090 incs = collections.defaultdict(list)
91 error_msgs = []
Zelalemf75daf22020-08-04 16:29:43 -050092 plat_incs = dir_include_paths("plat") | dir_include_paths("include/plat")
Yann Gautier72d2dbc2021-09-14 10:48:24 +020093 plat_common_incs = dir_include_paths("include/plat/common")
94 plat_incs.difference_update(plat_common_incs)
Zelalemf75daf22020-08-04 16:29:43 -050095 libc_incs = dir_include_paths("include/lib/libc")
Harrison Mutaiec878ce2022-05-13 15:18:38 +010096 proj_incs = dir_include_paths("include/")
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:
104 if inc_path in proj_incs:
105 inc_group_index = 1
106 elif inc_path in plat_incs:
107 inc_group_index = 2
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000108
109 incs[inc_group_index].append(inc_path)
110 indices.append((inc_group_index, inc))
111
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100112 index_sorted_paths = sorted(indices, key=lambda x: (x[0], x[1][1:-1]))
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000113 if indices != index_sorted_paths:
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100114 error_msgs.append("Include ordering error, order should be:")
115 last_group = index_sorted_paths[0][0]
116 for inc in index_sorted_paths:
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000117 # Right angle brackets are a special entity in html, convert the
118 # name to an html friendly format.
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100119 path_ = inc[1] if "<" not in inc[1] else f"&lt{inc[1][1:-1]}&gt"
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000120
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100121 if last_group != inc[0]:
122 error_msgs.append("")
123 last_group = inc[0]
Fathi Boudra422bf772019-12-02 11:10:16 +0200124
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100125 error_msgs.append(f"\t#include {path_}")
Fathi Boudra422bf772019-12-02 11:10:16 +0200126
127 # Output error messages.
Zelalem219df412020-05-17 19:21:20 -0500128 if error_msgs:
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000129 print(f"\n{commit_hash}:{path}:")
130 print(*error_msgs, sep="\n")
Zelalem219df412020-05-17 19:21:20 -0500131 return False
132 else:
133 return True
Fathi Boudra422bf772019-12-02 11:10:16 +0200134
135
136def file_is_correct(path):
Zelalem219df412020-05-17 19:21:20 -0500137 """Checks whether the order of includes in the file specified in the path
138 is correct or not."""
139 inc_list = file_include_list(path)
140 return inc_list is not None and inc_order_is_correct(inc_list, path)
Fathi Boudra422bf772019-12-02 11:10:16 +0200141
142
143def directory_tree_is_correct():
Zelalem219df412020-05-17 19:21:20 -0500144 """Checks all tracked files in the current git repository, except the ones
Fathi Boudra422bf772019-12-02 11:10:16 +0200145 explicitly ignored by this script.
Zelalem219df412020-05-17 19:21:20 -0500146 Returns True if all files are correct."""
147 (rc, stdout, stderr) = utils.shell_command(["git", "ls-files"])
Fathi Boudra422bf772019-12-02 11:10:16 +0200148 if rc != 0:
149 return False
Fathi Boudra422bf772019-12-02 11:10:16 +0200150 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500151 for f in stdout.splitlines():
152 if not utils.file_is_ignored(
153 f, VALID_FILE_EXTENSIONS, IGNORED_FILES, IGNORED_FOLDERS
154 ):
155 all_files_correct &= file_is_correct(f)
Fathi Boudra422bf772019-12-02 11:10:16 +0200156 return all_files_correct
157
158
Zelalem219df412020-05-17 19:21:20 -0500159def group_lines(patchlines, starting_with):
160 """Generator of (name, lines) almost the same as itertools.groupby
161
162 This function's control flow is non-trivial. In particular, the clearing
163 of the lines variable, marked with [1], is intentional and must come
164 after the yield. That's because we must yield the (name, lines) tuple
165 after we have found the name of the next section but before we assign the
166 name and start collecting lines. Further, [2] is required to yeild the
167 last block as there will not be a block start delimeter at the end of
168 the stream.
169 """
170 lines = []
171 name = None
172 for line in patchlines:
173 if line.startswith(starting_with):
174 if name:
175 yield name, lines
176 name = line[len(starting_with) :]
177 lines = [] # [1]
178 else:
179 lines.append(line)
180 yield name, lines # [2]
181
182
183def group_files(commitlines):
184 """Generator of (commit hash, lines) almost the same as itertools.groupby"""
185 return group_lines(commitlines, "+++ b/")
186
187
188def group_commits(commitlines):
189 """Generator of (file name, lines) almost the same as itertools.groupby"""
190 return group_lines(commitlines, "commit ")
191
192
Fathi Boudra422bf772019-12-02 11:10:16 +0200193def patch_is_correct(base_commit, end_commit):
Zelalem219df412020-05-17 19:21:20 -0500194 """Get the output of a git diff and analyse each modified file."""
Fathi Boudra422bf772019-12-02 11:10:16 +0200195
196 # Get patches of the affected commits with one line of context.
Paul Sokolovskye295b282024-06-13 17:07:01 +0300197 gitlog = subprocess_run(
Zelalem219df412020-05-17 19:21:20 -0500198 [
199 "git",
200 "log",
201 "--unified=1",
202 "--pretty=commit %h",
203 base_commit + ".." + end_commit,
204 ],
205 stdout=subprocess.PIPE,
206 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200207
Zelalem219df412020-05-17 19:21:20 -0500208 if gitlog.returncode != 0:
Fathi Boudra422bf772019-12-02 11:10:16 +0200209 return False
210
Zelalem219df412020-05-17 19:21:20 -0500211 gitlines = gitlog.stdout.decode("utf-8").splitlines()
Fathi Boudra422bf772019-12-02 11:10:16 +0200212 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500213 for commit, comlines in group_commits(gitlines):
214 for path, lines in group_files(comlines):
215 all_files_correct &= inc_order_is_correct(
216 include_paths(lines, diff_mode=True), path, commit
217 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200218 return all_files_correct
219
220
Fathi Boudra422bf772019-12-02 11:10:16 +0200221def parse_cmd_line(argv, prog_name):
222 parser = argparse.ArgumentParser(
223 prog=prog_name,
224 formatter_class=argparse.RawTextHelpFormatter,
225 description="Check alphabetical order of #includes",
226 epilog="""
227For each source file in the tree, checks that #include's C preprocessor
228directives are ordered alphabetically (as mandated by the Trusted
229Firmware coding style). System header includes must come before user
230header includes.
Zelalem219df412020-05-17 19:21:20 -0500231""",
232 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200233
Zelalem219df412020-05-17 19:21:20 -0500234 parser.add_argument(
235 "--tree",
236 "-t",
237 help="Path to the source tree to check (default: %(default)s)",
238 default=os.curdir,
239 )
240 parser.add_argument(
241 "--patch",
242 "-p",
243 help="""
Fathi Boudra422bf772019-12-02 11:10:16 +0200244Patch mode.
245Instead of checking all files in the source tree, the script will consider
246only files that are modified by the latest patch(es).""",
Zelalem219df412020-05-17 19:21:20 -0500247 action="store_true",
248 )
249 parser.add_argument(
250 "--from-ref",
251 help="Base commit in patch mode (default: %(default)s)",
laurenw-arm7cdf1b12023-07-26 11:29:18 -0500252 default="lts-v2.8",
Zelalem219df412020-05-17 19:21:20 -0500253 )
254 parser.add_argument(
255 "--to-ref",
256 help="Final commit in patch mode (default: %(default)s)",
257 default="HEAD",
258 )
Paul Sokolovskye295b282024-06-13 17:07:01 +0300259 parser.add_argument(
260 "--debug",
261 help="Enable debug logging",
262 action="store_true",
263 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200264 args = parser.parse_args(argv)
265 return args
266
267
268if __name__ == "__main__":
269 args = parse_cmd_line(sys.argv[1:], sys.argv[0])
270
Paul Sokolovskye295b282024-06-13 17:07:01 +0300271 if args.debug:
272 logging.basicConfig(level=logging.DEBUG)
273
Fathi Boudra422bf772019-12-02 11:10:16 +0200274 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)