blob: ff124fbb27300ae47d12643dec0364ae8f77d609 [file] [log] [blame]
Fathi Boudra422bf772019-12-02 11:10:16 +02001#!/usr/bin/env python3
2#
Zachary Leafb6d86302024-10-29 10:29:15 +00003# Copyright (c) 2019-2024, Arm Limited. All rights reserved.
Fathi Boudra422bf772019-12-02 11:10:16 +02004#
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:
Zelalem219df412020-05-17 19:21:20 -050011 /*
12 *
13 //
14 #
Fathi Boudra422bf772019-12-02 11:10:16 +020015"""
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
Zelalem219df412020-05-17 19:21:20 -050029VALID_FILE_EXTENSIONS = ('.c', '.conf', '.dts', '.dtsi', '.editorconfig',
30 '.h', '.i', '.ld', 'Makefile', '.mk', '.msvc',
Zachary Leafb6d86302024-10-29 10:29:15 +000031 '.py', '.S', '.scat', '.sh', '.rs')
Fathi Boudra422bf772019-12-02 11:10:16 +020032
33# Paths inside the tree to ignore. Hidden folders and files are always ignored.
34# They mustn't end in '/'.
35IGNORED_FOLDERS = (
Manish Pandeyd07fe0b2024-12-06 11:33:33 +000036 'include/lib/hob',
Fathi Boudra422bf772019-12-02 11:10:16 +020037 'include/lib/libfdt',
38 'lib/compiler-rt',
Manish Pandeyd07fe0b2024-12-06 11:33:33 +000039 'lib/hob',
Fathi Boudra422bf772019-12-02 11:10:16 +020040 'lib/libfdt',
41 'lib/zlib'
42)
43
44# List of ignored files in folders that aren't ignored
45IGNORED_FILES = (
46 'include/tools_share/uuid.h'
47)
48
49# Supported comment styles (Python regex)
Zelalem219df412020-05-17 19:21:20 -050050COMMENT_PATTERN = '(\*|/\*|\#|//)'
Fathi Boudra422bf772019-12-02 11:10:16 +020051
Zelalem219df412020-05-17 19:21:20 -050052# Any combination of spaces and/or tabs
53SPACING = '[ \t]*'
Fathi Boudra422bf772019-12-02 11:10:16 +020054
Zelalem219df412020-05-17 19:21:20 -050055# Line must start with a comment and optional spacing
56LINE_START = '^' + SPACING + COMMENT_PATTERN + SPACING
57
58# Line end with optional spacing
59EOL = SPACING + '$'
60
61# Year or period as YYYY or YYYY-YYYY
62TIME_PERIOD = '[0-9]{4}(-[0-9]{4})?'
63
64# Any string with valid license ID, don't allow adding postfix
Chris Kay345873c2021-04-27 13:24:51 +010065LICENSE_ID = '.*(BSD-3-Clause|BSD-2-Clause-FreeBSD|MIT)([ ,.\);].*)?'
Zelalem219df412020-05-17 19:21:20 -050066
67# File must contain both lines to pass the check
68COPYRIGHT_LINE = LINE_START + 'Copyright' + '.*' + TIME_PERIOD + '.*' + EOL
Tomás González1150fb82025-06-04 10:05:21 +010069RUST_COPYRIGHT_LINE = LINE_START + 'Copyright The Rusted Firmware-A Contributors.' + EOL
70
Zelalem219df412020-05-17 19:21:20 -050071LICENSE_ID_LINE = LINE_START + 'SPDX-License-Identifier:' + LICENSE_ID + EOL
72
73# Compiled license patterns
74COPYRIGHT_PATTERN = re.compile(COPYRIGHT_LINE, re.MULTILINE)
Tomás González1150fb82025-06-04 10:05:21 +010075RUST_COPYRIGHT_PATTERN = re.compile(RUST_COPYRIGHT_LINE, re.MULTILINE)
Zelalem219df412020-05-17 19:21:20 -050076LICENSE_ID_PATTERN = re.compile(LICENSE_ID_LINE, re.MULTILINE)
77
78CURRENT_YEAR = str(datetime.datetime.now().year)
Fathi Boudra422bf772019-12-02 11:10:16 +020079
80COPYRIGHT_OK = 0
81COPYRIGHT_ERROR = 1
Fathi Boudra422bf772019-12-02 11:10:16 +020082
Tomás González3923b552025-06-04 14:04:00 +010083def check_copyright(path, args, copyright_pattern, check_year, encoding='utf-8'):
Fathi Boudra422bf772019-12-02 11:10:16 +020084 '''Checks a file for a correct copyright header.'''
85
Zelalem219df412020-05-17 19:21:20 -050086 result = COPYRIGHT_OK
87
88 with open(path, encoding=encoding) as file_:
Fathi Boudra422bf772019-12-02 11:10:16 +020089 file_content = file_.read()
90
Tomás González3923b552025-06-04 14:04:00 +010091 copyright_line = copyright_pattern.search(file_content)
Tomás González1150fb82025-06-04 10:05:21 +010092
Zelalem219df412020-05-17 19:21:20 -050093 if not copyright_line:
94 print("ERROR: Missing copyright in " + file_.name)
95 result = COPYRIGHT_ERROR
Tomás González3923b552025-06-04 14:04:00 +010096 elif check_year and CURRENT_YEAR not in copyright_line.group():
Zelalem219df412020-05-17 19:21:20 -050097 print("WARNING: Copyright is out of date in " + file_.name + ": '" +
98 copyright_line.group() + "'")
Fathi Boudra422bf772019-12-02 11:10:16 +020099
Zelalem219df412020-05-17 19:21:20 -0500100 if not LICENSE_ID_PATTERN.search(file_content):
101 print("ERROR: License ID error in " + file_.name)
102 result = COPYRIGHT_ERROR
Fathi Boudra422bf772019-12-02 11:10:16 +0200103
Zelalem219df412020-05-17 19:21:20 -0500104 return result
Fathi Boudra422bf772019-12-02 11:10:16 +0200105
106def main(args):
107 print("Checking the copyrights in the code...")
108
Zelalem219df412020-05-17 19:21:20 -0500109 if args.verbose:
Tomás González1150fb82025-06-04 10:05:21 +0100110 if args.rusted:
111 print ("Copyright regexp: " + RUST_COPYRIGHT_LINE)
112 else:
113 print ("Copyright regexp: " + COPYRIGHT_LINE)
Zelalem219df412020-05-17 19:21:20 -0500114 print ("License regexp: " + LICENSE_ID_LINE)
Fathi Boudra422bf772019-12-02 11:10:16 +0200115
116 if args.patch:
Harrison Mutai7a93cd22022-09-29 12:31:31 +0100117 print("Checking files added between patches " + args.from_ref
Fathi Boudra422bf772019-12-02 11:10:16 +0200118 + " and " + args.to_ref + "...")
119
120 (rc, stdout, stderr) = utils.shell_command(['git', 'diff',
Harrison Mutai7a93cd22022-09-29 12:31:31 +0100121 '--diff-filter=ACRT', '--name-only', args.from_ref, args.to_ref ])
Fathi Boudra422bf772019-12-02 11:10:16 +0200122 if rc:
Zelalem219df412020-05-17 19:21:20 -0500123 return COPYRIGHT_ERROR
Fathi Boudra422bf772019-12-02 11:10:16 +0200124
125 files = stdout.splitlines()
126
127 else:
128 print("Checking all files tracked by git...")
129
130 (rc, stdout, stderr) = utils.shell_command([ 'git', 'ls-files' ])
131 if rc:
Zelalem219df412020-05-17 19:21:20 -0500132 return COPYRIGHT_ERROR
Fathi Boudra422bf772019-12-02 11:10:16 +0200133
134 files = stdout.splitlines()
135
136 count_ok = 0
137 count_warning = 0
138 count_error = 0
139
140 for f in files:
141
142 if utils.file_is_ignored(f, VALID_FILE_EXTENSIONS, IGNORED_FILES, IGNORED_FOLDERS):
143 if args.verbose:
144 print("Ignoring file " + f)
145 continue
146
147 if args.verbose:
148 print("Checking file " + f)
149
Tomás González3923b552025-06-04 14:04:00 +0100150 copyright_pattern = RUST_COPYRIGHT_PATTERN if args.rusted else COPYRIGHT_PATTERN
151 check_year = not args.rusted
152 rc = check_copyright(f, args, copyright_pattern, check_year)
Fathi Boudra422bf772019-12-02 11:10:16 +0200153
154 if rc == COPYRIGHT_OK:
155 count_ok += 1
Fathi Boudra422bf772019-12-02 11:10:16 +0200156 elif rc == COPYRIGHT_ERROR:
157 count_error += 1
Fathi Boudra422bf772019-12-02 11:10:16 +0200158
159 print("\nSummary:")
Zelalem219df412020-05-17 19:21:20 -0500160 print("\t{} files analyzed".format(count_ok + count_error))
Fathi Boudra422bf772019-12-02 11:10:16 +0200161
Zelalem219df412020-05-17 19:21:20 -0500162 if count_error == 0:
Fathi Boudra422bf772019-12-02 11:10:16 +0200163 print("\tNo errors found")
Zelalem219df412020-05-17 19:21:20 -0500164 return COPYRIGHT_OK
165 else:
Fathi Boudra422bf772019-12-02 11:10:16 +0200166 print("\t{} errors found".format(count_error))
Zelalem219df412020-05-17 19:21:20 -0500167 return COPYRIGHT_ERROR
Fathi Boudra422bf772019-12-02 11:10:16 +0200168
169def parse_cmd_line(argv, prog_name):
170 parser = argparse.ArgumentParser(
171 prog=prog_name,
172 formatter_class=argparse.RawTextHelpFormatter,
173 description="Check copyright of all files of codebase",
174 epilog="""
175For each source file in the tree, checks that the copyright header
176has the correct format.
177""")
178
179 parser.add_argument("--tree", "-t",
180 help="Path to the source tree to check (default: %(default)s)",
181 default=os.curdir)
182
Tomás González1150fb82025-06-04 10:05:21 +0100183 parser.add_argument("--rusted", "-r",
184 help="Check for Rusted Firmware CopyRight style (default: %(default)s)",
185 action='store_true', default=False)
186
Fathi Boudra422bf772019-12-02 11:10:16 +0200187 parser.add_argument("--verbose", "-v",
188 help="Increase verbosity to the source tree to check (default: %(default)s)",
189 action='store_true', default=False)
190
191 parser.add_argument("--patch", "-p",
192 help="""
193Patch mode.
194Instead of checking all files in the source tree, the script will consider
195only files that are modified by the latest patch(es).""",
196 action="store_true")
Zelalem219df412020-05-17 19:21:20 -0500197
Tomás Gonzálezd59d7232025-06-24 12:16:08 +0100198 (rc, stdout, stderr) = utils.shell_command(['git', 'merge-base', 'HEAD', 'refs/remotes/origin/tfa-next'])
Zelalem219df412020-05-17 19:21:20 -0500199 if rc:
200 print("Git merge-base command failed. Cannot determine base commit.")
201 sys.exit(rc)
202 merge_bases = stdout.splitlines()
203
204 # This should not happen, but it's better to be safe.
205 if len(merge_bases) > 1:
206 print("WARNING: Multiple merge bases found. Using the first one as base commit.")
207
Fathi Boudra422bf772019-12-02 11:10:16 +0200208 parser.add_argument("--from-ref",
209 help="Base commit in patch mode (default: %(default)s)",
Zelalem219df412020-05-17 19:21:20 -0500210 default=merge_bases[0])
Fathi Boudra422bf772019-12-02 11:10:16 +0200211 parser.add_argument("--to-ref",
212 help="Final commit in patch mode (default: %(default)s)",
213 default="HEAD")
214
215 args = parser.parse_args(argv)
216 return args
217
218
219if __name__ == "__main__":
220 args = parse_cmd_line(sys.argv[1:], sys.argv[0])
221
222 os.chdir(args.tree)
223
224 rc = main(args)
225
226 sys.exit(rc)