blob: 53d355bd66bf1adeb1f9d968c1b0153077706f8e [file] [log] [blame]
Fathi Boudra422bf772019-12-02 11:10:16 +02001#!/usr/bin/env python3
2#
Zelalem219df412020-05-17 19:21:20 -05003# Copyright (c) 2019-2020, 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 yaml
18import logging
Fathi Boudra422bf772019-12-02 11:10:16 +020019
20
21# File extensions to check
Zelalem219df412020-05-17 19:21:20 -050022VALID_FILE_EXTENSIONS = (".c", ".S", ".h")
Fathi Boudra422bf772019-12-02 11:10:16 +020023
24
25# Paths inside the tree to ignore. Hidden folders and files are always ignored.
26# They mustn't end in '/'.
Zelalem219df412020-05-17 19:21:20 -050027IGNORED_FOLDERS = (
28 "include/lib/stdlib",
29 "include/lib/libc",
30 "include/lib/libfdt",
31 "lib/libfdt",
32 "lib/libc",
33 "lib/stdlib",
Fathi Boudra422bf772019-12-02 11:10:16 +020034)
35
Zelalem219df412020-05-17 19:21:20 -050036# List of ignored files in folders that aren't ignored
37IGNORED_FILES = ()
Fathi Boudra422bf772019-12-02 11:10:16 +020038
Zelalem219df412020-05-17 19:21:20 -050039INCLUDE_RE = re.compile(r"^\s*#\s*include\s\s*(?P<path>[\"<].+[\">])")
40INCLUDE_RE_DIFF = re.compile(r"^\+?\s*#\s*include\s\s*(?P<path>[\"<].+[\">])")
Fathi Boudra422bf772019-12-02 11:10:16 +020041
42
Zelalem219df412020-05-17 19:21:20 -050043def include_paths(lines, diff_mode=False):
44 """List all include paths in a file. Ignore starting `+` in diff mode."""
45 pattern = INCLUDE_RE_DIFF if diff_mode else INCLUDE_RE
46 matches = (pattern.match(line) for line in lines)
47 return [m.group("path") for m in matches if m]
Fathi Boudra422bf772019-12-02 11:10:16 +020048
49
Zelalem219df412020-05-17 19:21:20 -050050def file_include_list(path):
51 """Return a list of all include paths in a file or None on failure."""
Fathi Boudra422bf772019-12-02 11:10:16 +020052 try:
Zelalem219df412020-05-17 19:21:20 -050053 with codecs.open(path, encoding="utf-8") as f:
54 return include_paths(f)
55 except Exception:
56 logging.exception(path + ":error while parsing.")
Fathi Boudra422bf772019-12-02 11:10:16 +020057 return None
58
Fathi Boudra422bf772019-12-02 11:10:16 +020059
Zelalemf75daf22020-08-04 16:29:43 -050060@functools.lru_cache()
61def dir_include_paths(directory):
62 """Generate a set that contains all includes from a directory"""
63 dir_includes = set()
64 for (root, _dirs, files) in os.walk(directory):
65 for fname in files:
66 if fname.endswith(".h"):
67 names = os.path.join(root, fname).split(os.sep)
68 for i in range(len(names)):
69 suffix_path = "/".join(names[i:])
70 dir_includes.add(suffix_path)
71 return dir_includes
72
73
Fathi Boudra422bf772019-12-02 11:10:16 +020074def inc_order_is_correct(inc_list, path, commit_hash=""):
Zelalem219df412020-05-17 19:21:20 -050075 """Returns true if the provided list is in order. If not, output error
76 messages to stdout."""
Fathi Boudra422bf772019-12-02 11:10:16 +020077
78 # If there are less than 2 includes there's no need to check.
79 if len(inc_list) < 2:
80 return True
81
82 if commit_hash != "":
Zelalem219df412020-05-17 19:21:20 -050083 commit_hash = commit_hash + ":"
Fathi Boudra422bf772019-12-02 11:10:16 +020084
Zelalem219df412020-05-17 19:21:20 -050085 # First, check if all includes are in the appropriate group.
86 inc_group = "System"
87 incs = collections.defaultdict(list)
88 error_msgs = []
Zelalemf75daf22020-08-04 16:29:43 -050089 plat_incs = dir_include_paths("plat") | dir_include_paths("include/plat")
Yann Gautier72d2dbc2021-09-14 10:48:24 +020090 plat_common_incs = dir_include_paths("include/plat/common")
91 plat_incs.difference_update(plat_common_incs)
Zelalemf75daf22020-08-04 16:29:43 -050092 libc_incs = dir_include_paths("include/lib/libc")
Fathi Boudra422bf772019-12-02 11:10:16 +020093
94 for inc in inc_list:
Zelalemf75daf22020-08-04 16:29:43 -050095 inc_path = inc[1:-1]
96 if inc_path in libc_incs:
Zelalem219df412020-05-17 19:21:20 -050097 if inc_group != "System":
Zelalemf75daf22020-08-04 16:29:43 -050098 error_msgs.append(inc_path + " should be in system group, at the top")
99 elif inc_path in plat_incs:
Zelalem219df412020-05-17 19:21:20 -0500100 inc_group = "Platform"
101 elif inc_group in ("Project", "System"):
102 inc_group = "Project"
103 else:
104 error_msgs.append(
Zelalemf75daf22020-08-04 16:29:43 -0500105 inc_path + " should be in project group, after system group"
Zelalem219df412020-05-17 19:21:20 -0500106 )
Zelalemf75daf22020-08-04 16:29:43 -0500107 incs[inc_group].append(inc_path)
Fathi Boudra422bf772019-12-02 11:10:16 +0200108
Zelalem219df412020-05-17 19:21:20 -0500109 # Then, check alphabetic order (system, project and user separately).
110 if not error_msgs:
111 for name, inc_list in incs.items():
112 if sorted(inc_list) != inc_list:
Zelalemf75daf22020-08-04 16:29:43 -0500113 error_msgs.append(
114 "{} includes not in order. Include order should be {}".format(
115 name, ", ".join(sorted(inc_list))
116 )
117 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200118
119 # Output error messages.
Zelalem219df412020-05-17 19:21:20 -0500120 if error_msgs:
121 print(yaml.dump({commit_hash + path: error_msgs}))
122 return False
123 else:
124 return True
Fathi Boudra422bf772019-12-02 11:10:16 +0200125
126
127def file_is_correct(path):
Zelalem219df412020-05-17 19:21:20 -0500128 """Checks whether the order of includes in the file specified in the path
129 is correct or not."""
130 inc_list = file_include_list(path)
131 return inc_list is not None and inc_order_is_correct(inc_list, path)
Fathi Boudra422bf772019-12-02 11:10:16 +0200132
133
134def directory_tree_is_correct():
Zelalem219df412020-05-17 19:21:20 -0500135 """Checks all tracked files in the current git repository, except the ones
Fathi Boudra422bf772019-12-02 11:10:16 +0200136 explicitly ignored by this script.
Zelalem219df412020-05-17 19:21:20 -0500137 Returns True if all files are correct."""
138 (rc, stdout, stderr) = utils.shell_command(["git", "ls-files"])
Fathi Boudra422bf772019-12-02 11:10:16 +0200139 if rc != 0:
140 return False
Fathi Boudra422bf772019-12-02 11:10:16 +0200141 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500142 for f in stdout.splitlines():
143 if not utils.file_is_ignored(
144 f, VALID_FILE_EXTENSIONS, IGNORED_FILES, IGNORED_FOLDERS
145 ):
146 all_files_correct &= file_is_correct(f)
Fathi Boudra422bf772019-12-02 11:10:16 +0200147 return all_files_correct
148
149
Zelalem219df412020-05-17 19:21:20 -0500150def group_lines(patchlines, starting_with):
151 """Generator of (name, lines) almost the same as itertools.groupby
152
153 This function's control flow is non-trivial. In particular, the clearing
154 of the lines variable, marked with [1], is intentional and must come
155 after the yield. That's because we must yield the (name, lines) tuple
156 after we have found the name of the next section but before we assign the
157 name and start collecting lines. Further, [2] is required to yeild the
158 last block as there will not be a block start delimeter at the end of
159 the stream.
160 """
161 lines = []
162 name = None
163 for line in patchlines:
164 if line.startswith(starting_with):
165 if name:
166 yield name, lines
167 name = line[len(starting_with) :]
168 lines = [] # [1]
169 else:
170 lines.append(line)
171 yield name, lines # [2]
172
173
174def group_files(commitlines):
175 """Generator of (commit hash, lines) almost the same as itertools.groupby"""
176 return group_lines(commitlines, "+++ b/")
177
178
179def group_commits(commitlines):
180 """Generator of (file name, lines) almost the same as itertools.groupby"""
181 return group_lines(commitlines, "commit ")
182
183
Fathi Boudra422bf772019-12-02 11:10:16 +0200184def patch_is_correct(base_commit, end_commit):
Zelalem219df412020-05-17 19:21:20 -0500185 """Get the output of a git diff and analyse each modified file."""
Fathi Boudra422bf772019-12-02 11:10:16 +0200186
187 # Get patches of the affected commits with one line of context.
Zelalem219df412020-05-17 19:21:20 -0500188 gitlog = subprocess.run(
189 [
190 "git",
191 "log",
192 "--unified=1",
193 "--pretty=commit %h",
194 base_commit + ".." + end_commit,
195 ],
196 stdout=subprocess.PIPE,
197 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200198
Zelalem219df412020-05-17 19:21:20 -0500199 if gitlog.returncode != 0:
Fathi Boudra422bf772019-12-02 11:10:16 +0200200 return False
201
Zelalem219df412020-05-17 19:21:20 -0500202 gitlines = gitlog.stdout.decode("utf-8").splitlines()
Fathi Boudra422bf772019-12-02 11:10:16 +0200203 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500204 for commit, comlines in group_commits(gitlines):
205 for path, lines in group_files(comlines):
206 all_files_correct &= inc_order_is_correct(
207 include_paths(lines, diff_mode=True), path, commit
208 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200209 return all_files_correct
210
211
Fathi Boudra422bf772019-12-02 11:10:16 +0200212def parse_cmd_line(argv, prog_name):
213 parser = argparse.ArgumentParser(
214 prog=prog_name,
215 formatter_class=argparse.RawTextHelpFormatter,
216 description="Check alphabetical order of #includes",
217 epilog="""
218For each source file in the tree, checks that #include's C preprocessor
219directives are ordered alphabetically (as mandated by the Trusted
220Firmware coding style). System header includes must come before user
221header includes.
Zelalem219df412020-05-17 19:21:20 -0500222""",
223 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200224
Zelalem219df412020-05-17 19:21:20 -0500225 parser.add_argument(
226 "--tree",
227 "-t",
228 help="Path to the source tree to check (default: %(default)s)",
229 default=os.curdir,
230 )
231 parser.add_argument(
232 "--patch",
233 "-p",
234 help="""
Fathi Boudra422bf772019-12-02 11:10:16 +0200235Patch mode.
236Instead of checking all files in the source tree, the script will consider
237only files that are modified by the latest patch(es).""",
Zelalem219df412020-05-17 19:21:20 -0500238 action="store_true",
239 )
240 parser.add_argument(
241 "--from-ref",
242 help="Base commit in patch mode (default: %(default)s)",
243 default="master",
244 )
245 parser.add_argument(
246 "--to-ref",
247 help="Final commit in patch mode (default: %(default)s)",
248 default="HEAD",
249 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200250 args = parser.parse_args(argv)
251 return args
252
253
254if __name__ == "__main__":
255 args = parse_cmd_line(sys.argv[1:], sys.argv[0])
256
257 os.chdir(args.tree)
258
259 if args.patch:
Zelalem219df412020-05-17 19:21:20 -0500260 print(
261 "Checking files modified between patches "
262 + args.from_ref
263 + " and "
264 + args.to_ref
265 + "..."
266 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200267 if not patch_is_correct(args.from_ref, args.to_ref):
268 sys.exit(1)
269 else:
270 print("Checking all files in directory '%s'..." % os.path.abspath(args.tree))
271 if not directory_tree_is_correct():
272 sys.exit(1)
273
274 # All source code files are correct.
275 sys.exit(0)