blob: f0c537f47fe227e6ff2ca20518fdd89686662cad [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ález1150fb82025-06-04 10:05:21 +010083def check_copyright(path, args, rusted=False, 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ález1150fb82025-06-04 10:05:21 +010091 if rusted:
92 copyright_line = RUST_COPYRIGHT_PATTERN.search(file_content)
93 else:
94 copyright_line = COPYRIGHT_PATTERN.search(file_content)
95
Zelalem219df412020-05-17 19:21:20 -050096 if not copyright_line:
97 print("ERROR: Missing copyright in " + file_.name)
98 result = COPYRIGHT_ERROR
Tomás González1150fb82025-06-04 10:05:21 +010099 elif not rusted and CURRENT_YEAR not in copyright_line.group():
Zelalem219df412020-05-17 19:21:20 -0500100 print("WARNING: Copyright is out of date in " + file_.name + ": '" +
101 copyright_line.group() + "'")
Fathi Boudra422bf772019-12-02 11:10:16 +0200102
Zelalem219df412020-05-17 19:21:20 -0500103 if not LICENSE_ID_PATTERN.search(file_content):
104 print("ERROR: License ID error in " + file_.name)
105 result = COPYRIGHT_ERROR
Fathi Boudra422bf772019-12-02 11:10:16 +0200106
Zelalem219df412020-05-17 19:21:20 -0500107 return result
Fathi Boudra422bf772019-12-02 11:10:16 +0200108
109def main(args):
110 print("Checking the copyrights in the code...")
111
Zelalem219df412020-05-17 19:21:20 -0500112 if args.verbose:
Tomás González1150fb82025-06-04 10:05:21 +0100113 if args.rusted:
114 print ("Copyright regexp: " + RUST_COPYRIGHT_LINE)
115 else:
116 print ("Copyright regexp: " + COPYRIGHT_LINE)
Zelalem219df412020-05-17 19:21:20 -0500117 print ("License regexp: " + LICENSE_ID_LINE)
Fathi Boudra422bf772019-12-02 11:10:16 +0200118
119 if args.patch:
Harrison Mutai7a93cd22022-09-29 12:31:31 +0100120 print("Checking files added between patches " + args.from_ref
Fathi Boudra422bf772019-12-02 11:10:16 +0200121 + " and " + args.to_ref + "...")
122
123 (rc, stdout, stderr) = utils.shell_command(['git', 'diff',
Harrison Mutai7a93cd22022-09-29 12:31:31 +0100124 '--diff-filter=ACRT', '--name-only', args.from_ref, args.to_ref ])
Fathi Boudra422bf772019-12-02 11:10:16 +0200125 if rc:
Zelalem219df412020-05-17 19:21:20 -0500126 return COPYRIGHT_ERROR
Fathi Boudra422bf772019-12-02 11:10:16 +0200127
128 files = stdout.splitlines()
129
130 else:
131 print("Checking all files tracked by git...")
132
133 (rc, stdout, stderr) = utils.shell_command([ 'git', 'ls-files' ])
134 if rc:
Zelalem219df412020-05-17 19:21:20 -0500135 return COPYRIGHT_ERROR
Fathi Boudra422bf772019-12-02 11:10:16 +0200136
137 files = stdout.splitlines()
138
139 count_ok = 0
140 count_warning = 0
141 count_error = 0
142
143 for f in files:
144
145 if utils.file_is_ignored(f, VALID_FILE_EXTENSIONS, IGNORED_FILES, IGNORED_FOLDERS):
146 if args.verbose:
147 print("Ignoring file " + f)
148 continue
149
150 if args.verbose:
151 print("Checking file " + f)
152
Tomás González1150fb82025-06-04 10:05:21 +0100153 rc = check_copyright(f, args, rusted=args.rusted)
Fathi Boudra422bf772019-12-02 11:10:16 +0200154
155 if rc == COPYRIGHT_OK:
156 count_ok += 1
Fathi Boudra422bf772019-12-02 11:10:16 +0200157 elif rc == COPYRIGHT_ERROR:
158 count_error += 1
Fathi Boudra422bf772019-12-02 11:10:16 +0200159
160 print("\nSummary:")
Zelalem219df412020-05-17 19:21:20 -0500161 print("\t{} files analyzed".format(count_ok + count_error))
Fathi Boudra422bf772019-12-02 11:10:16 +0200162
Zelalem219df412020-05-17 19:21:20 -0500163 if count_error == 0:
Fathi Boudra422bf772019-12-02 11:10:16 +0200164 print("\tNo errors found")
Zelalem219df412020-05-17 19:21:20 -0500165 return COPYRIGHT_OK
166 else:
Fathi Boudra422bf772019-12-02 11:10:16 +0200167 print("\t{} errors found".format(count_error))
Zelalem219df412020-05-17 19:21:20 -0500168 return COPYRIGHT_ERROR
Fathi Boudra422bf772019-12-02 11:10:16 +0200169
170def parse_cmd_line(argv, prog_name):
171 parser = argparse.ArgumentParser(
172 prog=prog_name,
173 formatter_class=argparse.RawTextHelpFormatter,
174 description="Check copyright of all files of codebase",
175 epilog="""
176For each source file in the tree, checks that the copyright header
177has the correct format.
178""")
179
180 parser.add_argument("--tree", "-t",
181 help="Path to the source tree to check (default: %(default)s)",
182 default=os.curdir)
183
Tomás González1150fb82025-06-04 10:05:21 +0100184 parser.add_argument("--rusted", "-r",
185 help="Check for Rusted Firmware CopyRight style (default: %(default)s)",
186 action='store_true', default=False)
187
Fathi Boudra422bf772019-12-02 11:10:16 +0200188 parser.add_argument("--verbose", "-v",
189 help="Increase verbosity to the source tree to check (default: %(default)s)",
190 action='store_true', default=False)
191
192 parser.add_argument("--patch", "-p",
193 help="""
194Patch mode.
195Instead of checking all files in the source tree, the script will consider
196only files that are modified by the latest patch(es).""",
197 action="store_true")
Zelalem219df412020-05-17 19:21:20 -0500198
Leonardo Sandoval9b3163e2020-10-13 12:55:24 -0500199 (rc, stdout, stderr) = utils.shell_command(['git', 'merge-base', 'HEAD', 'refs/remotes/origin/master'])
Zelalem219df412020-05-17 19:21:20 -0500200 if rc:
201 print("Git merge-base command failed. Cannot determine base commit.")
202 sys.exit(rc)
203 merge_bases = stdout.splitlines()
204
205 # This should not happen, but it's better to be safe.
206 if len(merge_bases) > 1:
207 print("WARNING: Multiple merge bases found. Using the first one as base commit.")
208
Fathi Boudra422bf772019-12-02 11:10:16 +0200209 parser.add_argument("--from-ref",
210 help="Base commit in patch mode (default: %(default)s)",
Zelalem219df412020-05-17 19:21:20 -0500211 default=merge_bases[0])
Fathi Boudra422bf772019-12-02 11:10:16 +0200212 parser.add_argument("--to-ref",
213 help="Final commit in patch mode (default: %(default)s)",
214 default="HEAD")
215
216 args = parser.parse_args(argv)
217 return args
218
219
220if __name__ == "__main__":
221 args = parse_cmd_line(sys.argv[1:], sys.argv[0])
222
223 os.chdir(args.tree)
224
225 rc = main(args)
226
227 sys.exit(rc)