blob: 5c2bb07bc5184269e9e014328d1e6cd8c3fc4c93 [file] [log] [blame]
Leonardo Sandoval314eed82020-08-05 13:32:04 -05001#!/usr/bin/env python3
2#
Xinyu Zhang235d5ae2021-02-07 10:42:38 +08003# Copyright (c) 2019-2021, Arm Limited. All rights reserved.
Leonardo Sandoval314eed82020-08-05 13:32:04 -05004#
5# SPDX-License-Identifier: BSD-3-Clause
6#
7
8"""
9Check if a given file includes the copyright boiler plate.
10This checker supports the following comment styles:
11 /*
12 *
13 //
14 #
15"""
16
17import argparse
18import datetime
19import collections
20import fnmatch
21import shlex
22import os
23import re
24import sys
25import utils
26from itertools import islice
27
28# File extensions to check
29VALID_FILE_EXTENSIONS = ('.c', '.conf', '.dts', '.dtsi', '.editorconfig',
30 '.h', '.i', '.ld', 'Makefile', '.mk', '.msvc',
31 '.py', '.S', '.scat', '.sh')
32
33# Paths inside the tree to ignore. Hidden folders and files are always ignored.
34# They mustn't end in '/'.
35IGNORED_FOLDERS = (
Xinyu Zhang235d5ae2021-02-07 10:42:38 +080036 'bl2/ext',
37 'docs',
Antonio de Angelis2e526ca2024-04-11 15:44:46 +010038 'interface/include/mbedtls',
Xinyu Zhang235d5ae2021-02-07 10:42:38 +080039 'lib',
Antonio de Angelis2e526ca2024-04-11 15:44:46 +010040 'platform/ext',
Xinyu Zhang235d5ae2021-02-07 10:42:38 +080041 'tools'
Leonardo Sandoval314eed82020-08-05 13:32:04 -050042)
43
44# List of ignored files in folders that aren't ignored
Antonio de Angelis2e526ca2024-04-11 15:44:46 +010045IGNORED_FILES = (
46 'interface/include/psa/build_info.h',
47 'interface/include/psa/crypto.h',
48 'interface/include/psa/crypto_adjust_auto_enabled.h',
Antonio de Angelis423eb5f2024-09-19 14:31:03 +010049 'interface/include/psa/crypto_adjust_config_dependencies.h',
Antonio de Angelis2e526ca2024-04-11 15:44:46 +010050 'interface/include/psa/crypto_adjust_config_key_pair_types.h',
51 'interface/include/psa/crypto_adjust_config_synonyms.h',
52 'interface/include/psa/crypto_builtin_composites.h',
53 'interface/include/psa/crypto_builtin_key_derivation.h',
54 'interface/include/psa/crypto_builtin_primitives.h',
55 'interface/include/psa/crypto_compat.h',
56 'interface/include/psa/crypto_driver_common.h',
57 'interface/include/psa/crypto_driver_contexts_composites.h',
58 'interface/include/psa/crypto_driver_contexts_key_derivation.h',
59 'interface/include/psa/crypto_driver_contexts_primitives.h',
60 'interface/include/psa/crypto_extra.h',
61 'interface/include/psa/crypto_legacy.h',
62 'interface/include/psa/crypto_platform.h',
63 'interface/include/psa/crypto_se_driver.h',
64 'interface/include/psa/crypto_sizes.h',
65 'interface/include/psa/crypto_struct.h',
66 'interface/include/psa/crypto_types.h',
Antonio de Angelis9985b422024-12-26 21:55:46 +010067 'interface/include/psa/crypto_values.h',
68 'interface/include/psa/crypto_values_lms.h'
Antonio de Angelis2e526ca2024-04-11 15:44:46 +010069)
Leonardo Sandoval314eed82020-08-05 13:32:04 -050070
71# Supported comment styles (Python regex)
72COMMENT_PATTERN = '(\*|/\*|\#|//)'
73
74# Any combination of spaces and/or tabs
75SPACING = '[ \t]*'
76
77# Line must start with a comment and optional spacing
78LINE_START = '^' + SPACING + COMMENT_PATTERN + SPACING
79
80# Line end with optional spacing
81EOL = SPACING + '$'
82
Antonio de Angelis2e526ca2024-04-11 15:44:46 +010083# Year or period as YYYY or YYYY-YYYY, or nothing as per the
84# Linux Foundation copyright notice recommendation
85TIME_PERIOD = '([0-9]{4}(-[0-9]{4})?)?'
Leonardo Sandoval314eed82020-08-05 13:32:04 -050086
87# Any string with valid license ID, don't allow adding postfix
88LICENSE_ID = '.*(BSD-3-Clause|BSD-2-Clause-FreeBSD)([ ,.\);].*)?'
89
90# File must contain both lines to pass the check
91COPYRIGHT_LINE = LINE_START + 'Copyright' + '.*' + TIME_PERIOD + '.*' + EOL
Antonio de Angelis9985b422024-12-26 21:55:46 +010092COPYRIGHT_LINE_ALT = LINE_START + 'SPDX-FileCopyrightText: Copyright The TrustedFirmware-M Contributors' + EOL
Leonardo Sandoval314eed82020-08-05 13:32:04 -050093LICENSE_ID_LINE = LINE_START + 'SPDX-License-Identifier:' + LICENSE_ID + EOL
94
95# Compiled license patterns
96COPYRIGHT_PATTERN = re.compile(COPYRIGHT_LINE, re.MULTILINE)
Antonio de Angelis9985b422024-12-26 21:55:46 +010097COPYRIGHT_PATTERN_ALT = re.compile(COPYRIGHT_LINE_ALT, re.MULTILINE)
Leonardo Sandoval314eed82020-08-05 13:32:04 -050098LICENSE_ID_PATTERN = re.compile(LICENSE_ID_LINE, re.MULTILINE)
99
100CURRENT_YEAR = str(datetime.datetime.now().year)
101
102COPYRIGHT_OK = 0
103COPYRIGHT_ERROR = 1
104
105def check_copyright(path, args, encoding='utf-8'):
106 '''Checks a file for a correct copyright header.'''
107
108 result = COPYRIGHT_OK
109
110 with open(path, encoding=encoding) as file_:
111 file_content = file_.read()
112
113 copyright_line = COPYRIGHT_PATTERN.search(file_content)
Antonio de Angelis9985b422024-12-26 21:55:46 +0100114 coypright_line_alt = COPYRIGHT_PATTERN_ALT.search(file_content)
115 if not copyright_line and not copyright_line_alt:
Leonardo Sandoval314eed82020-08-05 13:32:04 -0500116 print("ERROR: Missing copyright in " + file_.name)
117 result = COPYRIGHT_ERROR
Leonardo Sandoval314eed82020-08-05 13:32:04 -0500118
119 if not LICENSE_ID_PATTERN.search(file_content):
120 print("ERROR: License ID error in " + file_.name)
121 result = COPYRIGHT_ERROR
122
123 return result
124
125def main(args):
126 print("Checking the copyrights in the code...")
127
128 if args.verbose:
129 print ("Copyright regexp: " + COPYRIGHT_LINE)
130 print ("License regexp: " + LICENSE_ID_LINE)
131
132 if args.patch:
133 print("Checking files modified between patches " + args.from_ref
134 + " and " + args.to_ref + "...")
135
136 (rc, stdout, stderr) = utils.shell_command(['git', 'diff',
137 '--diff-filter=ACMRT', '--name-only', args.from_ref, args.to_ref ])
138 if rc:
139 return COPYRIGHT_ERROR
140
141 files = stdout.splitlines()
142
143 else:
144 print("Checking all files tracked by git...")
145
146 (rc, stdout, stderr) = utils.shell_command([ 'git', 'ls-files' ])
147 if rc:
148 return COPYRIGHT_ERROR
149
150 files = stdout.splitlines()
151
152 count_ok = 0
153 count_warning = 0
154 count_error = 0
155
156 for f in files:
157
158 if utils.file_is_ignored(f, VALID_FILE_EXTENSIONS, IGNORED_FILES, IGNORED_FOLDERS):
159 if args.verbose:
160 print("Ignoring file " + f)
161 continue
162
163 if args.verbose:
164 print("Checking file " + f)
165
166 rc = check_copyright(f, args)
167
168 if rc == COPYRIGHT_OK:
169 count_ok += 1
170 elif rc == COPYRIGHT_ERROR:
171 count_error += 1
172
173 print("\nSummary:")
174 print("\t{} files analyzed".format(count_ok + count_error))
175
176 if count_error == 0:
177 print("\tNo errors found")
178 return COPYRIGHT_OK
179 else:
180 print("\t{} errors found".format(count_error))
181 return COPYRIGHT_ERROR
182
183def parse_cmd_line(argv, prog_name):
184 parser = argparse.ArgumentParser(
185 prog=prog_name,
186 formatter_class=argparse.RawTextHelpFormatter,
187 description="Check copyright of all files of codebase",
188 epilog="""
189For each source file in the tree, checks that the copyright header
190has the correct format.
191""")
192
193 parser.add_argument("--tree", "-t",
194 help="Path to the source tree to check (default: %(default)s)",
195 default=os.curdir)
196
197 parser.add_argument("--verbose", "-v",
198 help="Increase verbosity to the source tree to check (default: %(default)s)",
199 action='store_true', default=False)
200
201 parser.add_argument("--patch", "-p",
202 help="""
203Patch mode.
204Instead of checking all files in the source tree, the script will consider
205only files that are modified by the latest patch(es).""",
206 action="store_true")
207
Leonardo Sandoval900de582020-09-07 18:34:57 -0500208 (rc, stdout, stderr) = utils.shell_command(['git', 'merge-base', 'HEAD', 'origin/master'])
Leonardo Sandoval314eed82020-08-05 13:32:04 -0500209 if rc:
210 print("Git merge-base command failed. Cannot determine base commit.")
211 sys.exit(rc)
212 merge_bases = stdout.splitlines()
213
214 # This should not happen, but it's better to be safe.
215 if len(merge_bases) > 1:
216 print("WARNING: Multiple merge bases found. Using the first one as base commit.")
217
218 parser.add_argument("--from-ref",
219 help="Base commit in patch mode (default: %(default)s)",
220 default=merge_bases[0])
221 parser.add_argument("--to-ref",
222 help="Final commit in patch mode (default: %(default)s)",
223 default="HEAD")
224
225 args = parser.parse_args(argv)
226 return args
227
228
229if __name__ == "__main__":
230 args = parse_cmd_line(sys.argv[1:], sys.argv[0])
231
232 os.chdir(args.tree)
233
234 rc = main(args)
235
236 sys.exit(rc)