blob: b225060d1e3faff77e838494eaced7084cf3d72d [file] [log] [blame]
Fathi Boudra422bf772019-12-02 11:10:16 +02001#!/usr/bin/env python3
2#
Igor Podgainõif08bdb72025-01-23 14:43:38 +01003# Copyright (c) 2019-2025, 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.
Igor Podgainõif08bdb72025-01-23 14:43:38 +010093 inc_groups = collections.OrderedDict()
Zelalem219df412020-05-17 19:21:20 -050094 incs = collections.defaultdict(list)
95 error_msgs = []
Igor Podgainõif08bdb72025-01-23 14:43:38 +010096
97 # System (libc) includes
98 inc_groups[0] = dir_include_paths("include/lib/libc")
99 # Platform includes
100 inc_groups[3] = dir_include_paths("plat") | dir_include_paths("include/plat")
101 inc_groups[3].difference_update(dir_include_paths("include/plat/common") |
102 dir_include_paths("include/plat/arm"))
103 # Project includes
104 inc_groups[2] = dir_include_paths("include") | dir_include_paths("drivers")
Govindraj Raja59b872e2023-03-07 10:44:13 +0000105
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000106 indices = []
Fathi Boudra422bf772019-12-02 11:10:16 +0200107
108 for inc in inc_list:
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100109 inc_path = inc[1:-1].replace("..", Path(path).parents[1].as_posix())
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100110
Igor Podgainõif08bdb72025-01-23 14:43:38 +0100111 inc_group_index = 1 # Third-party includes
112 for index, group in inc_groups.items():
113 if inc_path in group:
114 inc_group_index = index
115 break
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000116
117 incs[inc_group_index].append(inc_path)
118 indices.append((inc_group_index, inc))
119
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100120 index_sorted_paths = sorted(indices, key=lambda x: (x[0], x[1][1:-1]))
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000121 if indices != index_sorted_paths:
Igor Podgainõif08bdb72025-01-23 14:43:38 +0100122 error_msgs.append("Include ordering warning, order should probably be:")
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100123 last_group = index_sorted_paths[0][0]
124 for inc in index_sorted_paths:
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000125 # Right angle brackets are a special entity in html, convert the
126 # name to an html friendly format.
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100127 path_ = inc[1] if "<" not in inc[1] else f"&lt{inc[1][1:-1]}&gt"
Harrison Mutai8c3afa32022-02-04 09:33:24 +0000128
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100129 if last_group != inc[0]:
130 error_msgs.append("")
131 last_group = inc[0]
Fathi Boudra422bf772019-12-02 11:10:16 +0200132
Harrison Mutaiec878ce2022-05-13 15:18:38 +0100133 error_msgs.append(f"\t#include {path_}")
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.
Paul Sokolovskyfefed492024-06-13 17:07:01 +0300205 gitlog = subprocess_run(
Zelalem219df412020-05-17 19:21:20 -0500206 [
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 )
Paul Sokolovskyfefed492024-06-13 17:07:01 +0300267 parser.add_argument(
268 "--debug",
269 help="Enable debug logging",
270 action="store_true",
271 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200272 args = parser.parse_args(argv)
273 return args
274
275
276if __name__ == "__main__":
277 args = parse_cmd_line(sys.argv[1:], sys.argv[0])
278
Paul Sokolovskyfefed492024-06-13 17:07:01 +0300279 if args.debug:
280 logging.basicConfig(level=logging.DEBUG)
281
Fathi Boudra422bf772019-12-02 11:10:16 +0200282 os.chdir(args.tree)
283
284 if args.patch:
Zelalem219df412020-05-17 19:21:20 -0500285 print(
286 "Checking files modified between patches "
287 + args.from_ref
288 + " and "
289 + args.to_ref
290 + "..."
291 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200292 if not patch_is_correct(args.from_ref, args.to_ref):
293 sys.exit(1)
294 else:
295 print("Checking all files in directory '%s'..." % os.path.abspath(args.tree))
296 if not directory_tree_is_correct():
297 sys.exit(1)
298
299 # All source code files are correct.
300 sys.exit(0)