blob: aaf84f141b4980e0eccbf6a40ef980505634368b [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")
90 libc_incs = dir_include_paths("include/lib/libc")
Fathi Boudra422bf772019-12-02 11:10:16 +020091
92 for inc in inc_list:
Zelalemf75daf22020-08-04 16:29:43 -050093 inc_path = inc[1:-1]
94 if inc_path in libc_incs:
Zelalem219df412020-05-17 19:21:20 -050095 if inc_group != "System":
Zelalemf75daf22020-08-04 16:29:43 -050096 error_msgs.append(inc_path + " should be in system group, at the top")
97 elif inc_path in plat_incs:
Zelalem219df412020-05-17 19:21:20 -050098 inc_group = "Platform"
99 elif inc_group in ("Project", "System"):
100 inc_group = "Project"
101 else:
102 error_msgs.append(
Zelalemf75daf22020-08-04 16:29:43 -0500103 inc_path + " should be in project group, after system group"
Zelalem219df412020-05-17 19:21:20 -0500104 )
Zelalemf75daf22020-08-04 16:29:43 -0500105 incs[inc_group].append(inc_path)
Fathi Boudra422bf772019-12-02 11:10:16 +0200106
Zelalem219df412020-05-17 19:21:20 -0500107 # Then, check alphabetic order (system, project and user separately).
108 if not error_msgs:
109 for name, inc_list in incs.items():
110 if sorted(inc_list) != inc_list:
Zelalemf75daf22020-08-04 16:29:43 -0500111 error_msgs.append(
112 "{} includes not in order. Include order should be {}".format(
113 name, ", ".join(sorted(inc_list))
114 )
115 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200116
117 # Output error messages.
Zelalem219df412020-05-17 19:21:20 -0500118 if error_msgs:
119 print(yaml.dump({commit_hash + path: error_msgs}))
120 return False
121 else:
122 return True
Fathi Boudra422bf772019-12-02 11:10:16 +0200123
124
125def file_is_correct(path):
Zelalem219df412020-05-17 19:21:20 -0500126 """Checks whether the order of includes in the file specified in the path
127 is correct or not."""
128 inc_list = file_include_list(path)
129 return inc_list is not None and inc_order_is_correct(inc_list, path)
Fathi Boudra422bf772019-12-02 11:10:16 +0200130
131
132def directory_tree_is_correct():
Zelalem219df412020-05-17 19:21:20 -0500133 """Checks all tracked files in the current git repository, except the ones
Fathi Boudra422bf772019-12-02 11:10:16 +0200134 explicitly ignored by this script.
Zelalem219df412020-05-17 19:21:20 -0500135 Returns True if all files are correct."""
136 (rc, stdout, stderr) = utils.shell_command(["git", "ls-files"])
Fathi Boudra422bf772019-12-02 11:10:16 +0200137 if rc != 0:
138 return False
Fathi Boudra422bf772019-12-02 11:10:16 +0200139 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500140 for f in stdout.splitlines():
141 if not utils.file_is_ignored(
142 f, VALID_FILE_EXTENSIONS, IGNORED_FILES, IGNORED_FOLDERS
143 ):
144 all_files_correct &= file_is_correct(f)
Fathi Boudra422bf772019-12-02 11:10:16 +0200145 return all_files_correct
146
147
Zelalem219df412020-05-17 19:21:20 -0500148def group_lines(patchlines, starting_with):
149 """Generator of (name, lines) almost the same as itertools.groupby
150
151 This function's control flow is non-trivial. In particular, the clearing
152 of the lines variable, marked with [1], is intentional and must come
153 after the yield. That's because we must yield the (name, lines) tuple
154 after we have found the name of the next section but before we assign the
155 name and start collecting lines. Further, [2] is required to yeild the
156 last block as there will not be a block start delimeter at the end of
157 the stream.
158 """
159 lines = []
160 name = None
161 for line in patchlines:
162 if line.startswith(starting_with):
163 if name:
164 yield name, lines
165 name = line[len(starting_with) :]
166 lines = [] # [1]
167 else:
168 lines.append(line)
169 yield name, lines # [2]
170
171
172def group_files(commitlines):
173 """Generator of (commit hash, lines) almost the same as itertools.groupby"""
174 return group_lines(commitlines, "+++ b/")
175
176
177def group_commits(commitlines):
178 """Generator of (file name, lines) almost the same as itertools.groupby"""
179 return group_lines(commitlines, "commit ")
180
181
Fathi Boudra422bf772019-12-02 11:10:16 +0200182def patch_is_correct(base_commit, end_commit):
Zelalem219df412020-05-17 19:21:20 -0500183 """Get the output of a git diff and analyse each modified file."""
Fathi Boudra422bf772019-12-02 11:10:16 +0200184
185 # Get patches of the affected commits with one line of context.
Zelalem219df412020-05-17 19:21:20 -0500186 gitlog = subprocess.run(
187 [
188 "git",
189 "log",
190 "--unified=1",
191 "--pretty=commit %h",
192 base_commit + ".." + end_commit,
193 ],
194 stdout=subprocess.PIPE,
195 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200196
Zelalem219df412020-05-17 19:21:20 -0500197 if gitlog.returncode != 0:
Fathi Boudra422bf772019-12-02 11:10:16 +0200198 return False
199
Zelalem219df412020-05-17 19:21:20 -0500200 gitlines = gitlog.stdout.decode("utf-8").splitlines()
Fathi Boudra422bf772019-12-02 11:10:16 +0200201 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500202 for commit, comlines in group_commits(gitlines):
203 for path, lines in group_files(comlines):
204 all_files_correct &= inc_order_is_correct(
205 include_paths(lines, diff_mode=True), path, commit
206 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200207 return all_files_correct
208
209
Fathi Boudra422bf772019-12-02 11:10:16 +0200210def parse_cmd_line(argv, prog_name):
211 parser = argparse.ArgumentParser(
212 prog=prog_name,
213 formatter_class=argparse.RawTextHelpFormatter,
214 description="Check alphabetical order of #includes",
215 epilog="""
216For each source file in the tree, checks that #include's C preprocessor
217directives are ordered alphabetically (as mandated by the Trusted
218Firmware coding style). System header includes must come before user
219header includes.
Zelalem219df412020-05-17 19:21:20 -0500220""",
221 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200222
Zelalem219df412020-05-17 19:21:20 -0500223 parser.add_argument(
224 "--tree",
225 "-t",
226 help="Path to the source tree to check (default: %(default)s)",
227 default=os.curdir,
228 )
229 parser.add_argument(
230 "--patch",
231 "-p",
232 help="""
Fathi Boudra422bf772019-12-02 11:10:16 +0200233Patch mode.
234Instead of checking all files in the source tree, the script will consider
235only files that are modified by the latest patch(es).""",
Zelalem219df412020-05-17 19:21:20 -0500236 action="store_true",
237 )
238 parser.add_argument(
239 "--from-ref",
240 help="Base commit in patch mode (default: %(default)s)",
241 default="master",
242 )
243 parser.add_argument(
244 "--to-ref",
245 help="Final commit in patch mode (default: %(default)s)",
246 default="HEAD",
247 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200248 args = parser.parse_args(argv)
249 return args
250
251
252if __name__ == "__main__":
253 args = parse_cmd_line(sys.argv[1:], sys.argv[0])
254
255 os.chdir(args.tree)
256
257 if args.patch:
Zelalem219df412020-05-17 19:21:20 -0500258 print(
259 "Checking files modified between patches "
260 + args.from_ref
261 + " and "
262 + args.to_ref
263 + "..."
264 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200265 if not patch_is_correct(args.from_ref, args.to_ref):
266 sys.exit(1)
267 else:
268 print("Checking all files in directory '%s'..." % os.path.abspath(args.tree))
269 if not directory_tree_is_correct():
270 sys.exit(1)
271
272 # All source code files are correct.
273 sys.exit(0)