blob: 2160165e84b60a5c830cf2088ed80faf93a7d320 [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
Paul Sokolovskyfefed492024-06-13 17:07:01 +030040def subprocess_run(cmd, **kwargs):
41 logging.debug("Running command: %r %r", cmd, kwargs)
42 return subprocess.run(cmd, **kwargs)
43
44
Zelalem219df412020-05-17 19:21:20 -050045def include_paths(lines, diff_mode=False):
46 """List all include paths in a file. Ignore starting `+` in diff mode."""
47 pattern = INCLUDE_RE_DIFF if diff_mode else INCLUDE_RE
48 matches = (pattern.match(line) for line in lines)
49 return [m.group("path") for m in matches if m]
Fathi Boudra422bf772019-12-02 11:10:16 +020050
51
Zelalem219df412020-05-17 19:21:20 -050052def file_include_list(path):
53 """Return a list of all include paths in a file or None on failure."""
Fathi Boudra422bf772019-12-02 11:10:16 +020054 try:
Zelalem219df412020-05-17 19:21:20 -050055 with codecs.open(path, encoding="utf-8") as f:
56 return include_paths(f)
57 except Exception:
58 logging.exception(path + ":error while parsing.")
Fathi Boudra422bf772019-12-02 11:10:16 +020059 return None
60
Fathi Boudra422bf772019-12-02 11:10:16 +020061
Zelalemf75daf22020-08-04 16:29:43 -050062@functools.lru_cache()
63def dir_include_paths(directory):
64 """Generate a set that contains all includes from a directory"""
65 dir_includes = set()
66 for (root, _dirs, files) in os.walk(directory):
67 for fname in files:
Stephan Gerhold8ffa3d52023-06-20 16:19:40 +020068 if fname.endswith(".h") or fname.endswith(".S"):
Zelalemf75daf22020-08-04 16:29:43 -050069 names = os.path.join(root, fname).split(os.sep)
70 for i in range(len(names)):
71 suffix_path = "/".join(names[i:])
72 dir_includes.add(suffix_path)
73 return dir_includes
74
75
Fathi Boudra422bf772019-12-02 11:10:16 +020076def inc_order_is_correct(inc_list, path, commit_hash=""):
Zelalem219df412020-05-17 19:21:20 -050077 """Returns true if the provided list is in order. If not, output error
78 messages to stdout."""
Fathi Boudra422bf772019-12-02 11:10:16 +020079
80 # If there are less than 2 includes there's no need to check.
81 if len(inc_list) < 2:
82 return True
83
Yann Gautiere5b08f22023-09-04 15:01:59 +020084 if utils.file_is_ignored(
85 path, VALID_FILE_EXTENSIONS, IGNORED_FILES, IGNORED_FOLDERS
86 ):
87 return True
88
Fathi Boudra422bf772019-12-02 11:10:16 +020089 if commit_hash != "":
Zelalem219df412020-05-17 19:21:20 -050090 commit_hash = commit_hash + ":"
Fathi Boudra422bf772019-12-02 11:10:16 +020091
Zelalem219df412020-05-17 19:21:20 -050092 # First, check if all includes are in the appropriate group.
Zelalem219df412020-05-17 19:21:20 -050093 incs = collections.defaultdict(list)
94 error_msgs = []
Zelalemf75daf22020-08-04 16:29:43 -050095 plat_incs = dir_include_paths("plat") | dir_include_paths("include/plat")
Yann Gautier72d2dbc2021-09-14 10:48:24 +020096 plat_common_incs = dir_include_paths("include/plat/common")
97 plat_incs.difference_update(plat_common_incs)
Zelalemf75daf22020-08-04 16:29:43 -050098 libc_incs = dir_include_paths("include/lib/libc")
Manish V Badarkhe4eae5432024-02-20 14:09:35 +000099 proj_incs = dir_include_paths("include/") | dir_include_paths("drivers/")
Govindraj Raja59b872e2023-03-07 10:44:13 +0000100 third_party_incs = []
101 third_party_incs.append(dir_include_paths("mbedtls"))
102 third_party_incs.append(dir_include_paths("include/lib/libfdt"))
103 third_party_incs.append(dir_include_paths("lib/compiler-rt"))
104 third_party_incs.append(dir_include_paths("lib/libfdt"))
105 third_party_incs.append(dir_include_paths("lib/zlib"))
106
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000107 indices = []
Fathi Boudra422bf772019-12-02 11:10:16 +0200108
109 for inc in inc_list:
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100110 inc_path = inc[1:-1].replace("..", Path(path).parents[1].as_posix())
111 inc_group_index = int(inc_path not in libc_incs)
112
113 if inc_group_index:
Govindraj Raja59b872e2023-03-07 10:44:13 +0000114 if inc_path in third_party_incs:
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100115 inc_group_index = 1
Govindraj Raja59b872e2023-03-07 10:44:13 +0000116 elif inc_path in proj_incs:
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100117 inc_group_index = 2
Govindraj Raja59b872e2023-03-07 10:44:13 +0000118 elif inc_path in plat_incs:
119 inc_group_index = 3
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000120
121 incs[inc_group_index].append(inc_path)
122 indices.append((inc_group_index, inc))
123
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100124 index_sorted_paths = sorted(indices, key=lambda x: (x[0], x[1][1:-1]))
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000125 if indices != index_sorted_paths:
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100126 error_msgs.append("Include ordering error, order should be:")
127 last_group = index_sorted_paths[0][0]
128 for inc in index_sorted_paths:
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000129 # Right angle brackets are a special entity in html, convert the
130 # name to an html friendly format.
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100131 path_ = inc[1] if "<" not in inc[1] else f"&lt{inc[1][1:-1]}&gt"
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000132
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100133 if last_group != inc[0]:
134 error_msgs.append("")
135 last_group = inc[0]
Fathi Boudra422bf772019-12-02 11:10:16 +0200136
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100137 error_msgs.append(f"\t#include {path_}")
Fathi Boudra422bf772019-12-02 11:10:16 +0200138
139 # Output error messages.
Zelalem219df412020-05-17 19:21:20 -0500140 if error_msgs:
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000141 print(f"\n{commit_hash}:{path}:")
142 print(*error_msgs, sep="\n")
Zelalem219df412020-05-17 19:21:20 -0500143 return False
144 else:
145 return True
Fathi Boudra422bf772019-12-02 11:10:16 +0200146
147
148def file_is_correct(path):
Zelalem219df412020-05-17 19:21:20 -0500149 """Checks whether the order of includes in the file specified in the path
150 is correct or not."""
151 inc_list = file_include_list(path)
152 return inc_list is not None and inc_order_is_correct(inc_list, path)
Fathi Boudra422bf772019-12-02 11:10:16 +0200153
154
155def directory_tree_is_correct():
Zelalem219df412020-05-17 19:21:20 -0500156 """Checks all tracked files in the current git repository, except the ones
Fathi Boudra422bf772019-12-02 11:10:16 +0200157 explicitly ignored by this script.
Zelalem219df412020-05-17 19:21:20 -0500158 Returns True if all files are correct."""
159 (rc, stdout, stderr) = utils.shell_command(["git", "ls-files"])
Fathi Boudra422bf772019-12-02 11:10:16 +0200160 if rc != 0:
161 return False
Fathi Boudra422bf772019-12-02 11:10:16 +0200162 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500163 for f in stdout.splitlines():
164 if not utils.file_is_ignored(
165 f, VALID_FILE_EXTENSIONS, IGNORED_FILES, IGNORED_FOLDERS
166 ):
167 all_files_correct &= file_is_correct(f)
Fathi Boudra422bf772019-12-02 11:10:16 +0200168 return all_files_correct
169
170
Zelalem219df412020-05-17 19:21:20 -0500171def group_lines(patchlines, starting_with):
172 """Generator of (name, lines) almost the same as itertools.groupby
173
174 This function's control flow is non-trivial. In particular, the clearing
175 of the lines variable, marked with [1], is intentional and must come
176 after the yield. That's because we must yield the (name, lines) tuple
177 after we have found the name of the next section but before we assign the
178 name and start collecting lines. Further, [2] is required to yeild the
179 last block as there will not be a block start delimeter at the end of
180 the stream.
181 """
182 lines = []
183 name = None
184 for line in patchlines:
185 if line.startswith(starting_with):
186 if name:
187 yield name, lines
188 name = line[len(starting_with) :]
189 lines = [] # [1]
190 else:
191 lines.append(line)
192 yield name, lines # [2]
193
194
195def group_files(commitlines):
196 """Generator of (commit hash, lines) almost the same as itertools.groupby"""
197 return group_lines(commitlines, "+++ b/")
198
199
200def group_commits(commitlines):
201 """Generator of (file name, lines) almost the same as itertools.groupby"""
202 return group_lines(commitlines, "commit ")
203
204
Fathi Boudra422bf772019-12-02 11:10:16 +0200205def patch_is_correct(base_commit, end_commit):
Zelalem219df412020-05-17 19:21:20 -0500206 """Get the output of a git diff and analyse each modified file."""
Fathi Boudra422bf772019-12-02 11:10:16 +0200207
208 # Get patches of the affected commits with one line of context.
Paul Sokolovskyfefed492024-06-13 17:07:01 +0300209 gitlog = subprocess_run(
Zelalem219df412020-05-17 19:21:20 -0500210 [
211 "git",
212 "log",
213 "--unified=1",
214 "--pretty=commit %h",
215 base_commit + ".." + end_commit,
216 ],
217 stdout=subprocess.PIPE,
218 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200219
Zelalem219df412020-05-17 19:21:20 -0500220 if gitlog.returncode != 0:
Fathi Boudra422bf772019-12-02 11:10:16 +0200221 return False
222
Zelalem219df412020-05-17 19:21:20 -0500223 gitlines = gitlog.stdout.decode("utf-8").splitlines()
Fathi Boudra422bf772019-12-02 11:10:16 +0200224 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500225 for commit, comlines in group_commits(gitlines):
226 for path, lines in group_files(comlines):
227 all_files_correct &= inc_order_is_correct(
228 include_paths(lines, diff_mode=True), path, commit
229 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200230 return all_files_correct
231
232
Fathi Boudra422bf772019-12-02 11:10:16 +0200233def parse_cmd_line(argv, prog_name):
234 parser = argparse.ArgumentParser(
235 prog=prog_name,
236 formatter_class=argparse.RawTextHelpFormatter,
237 description="Check alphabetical order of #includes",
238 epilog="""
239For each source file in the tree, checks that #include's C preprocessor
240directives are ordered alphabetically (as mandated by the Trusted
241Firmware coding style). System header includes must come before user
242header includes.
Zelalem219df412020-05-17 19:21:20 -0500243""",
244 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200245
Zelalem219df412020-05-17 19:21:20 -0500246 parser.add_argument(
247 "--tree",
248 "-t",
249 help="Path to the source tree to check (default: %(default)s)",
250 default=os.curdir,
251 )
252 parser.add_argument(
253 "--patch",
254 "-p",
255 help="""
Fathi Boudra422bf772019-12-02 11:10:16 +0200256Patch mode.
257Instead of checking all files in the source tree, the script will consider
258only files that are modified by the latest patch(es).""",
Zelalem219df412020-05-17 19:21:20 -0500259 action="store_true",
260 )
261 parser.add_argument(
262 "--from-ref",
263 help="Base commit in patch mode (default: %(default)s)",
264 default="master",
265 )
266 parser.add_argument(
267 "--to-ref",
268 help="Final commit in patch mode (default: %(default)s)",
269 default="HEAD",
270 )
Paul Sokolovskyfefed492024-06-13 17:07:01 +0300271 parser.add_argument(
272 "--debug",
273 help="Enable debug logging",
274 action="store_true",
275 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200276 args = parser.parse_args(argv)
277 return args
278
279
280if __name__ == "__main__":
281 args = parse_cmd_line(sys.argv[1:], sys.argv[0])
282
Paul Sokolovskyfefed492024-06-13 17:07:01 +0300283 if args.debug:
284 logging.basicConfig(level=logging.DEBUG)
285
Fathi Boudra422bf772019-12-02 11:10:16 +0200286 os.chdir(args.tree)
287
288 if args.patch:
Zelalem219df412020-05-17 19:21:20 -0500289 print(
290 "Checking files modified between patches "
291 + args.from_ref
292 + " and "
293 + args.to_ref
294 + "..."
295 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200296 if not patch_is_correct(args.from_ref, args.to_ref):
297 sys.exit(1)
298 else:
299 print("Checking all files in directory '%s'..." % os.path.abspath(args.tree))
300 if not directory_tree_is_correct():
301 sys.exit(1)
302
303 # All source code files are correct.
304 sys.exit(0)