1#!/usr/bin/env python3 2# 3# Arm SCP/MCP Software 4# Copyright (c) 2015-2023, Arm Limited and Contributors. All rights reserved. 5# 6# SPDX-License-Identifier: BSD-3-Clause 7# 8 9""" 10Check if a given file includes the correct license header. 11This checker supports the following comment styles: 12 * Used by .c, .h and .s/.S (GCC) files 13 ; Used by .s (ARM toolchain) and .scat (scatter file) files 14 # Used by Makefile (including .mk), .py (Python) and dxy (Doxygen) files 15""" 16import os 17import fnmatch 18import re 19import sys 20import datetime 21import subprocess 22import glob 23from itertools import islice 24 25# 26# Directories to exclude 27# 28 29# Exclude all mod_test "mocks" directories 30UNIT_TEST_MOCKS = glob.glob('module/*/test/**/mocks', recursive=True) 31 32EXCLUDE_DIRECTORIES = [ 33 '.git', 34 'build', 35 'contrib/cmsis/git', 36 "contrib/run-clang-format/git", 37 "contrib/cmock/git", 38 'product/rcar/src/CMSIS-FreeRTOS', 39 'unit_test/unity_mocks', 40] + UNIT_TEST_MOCKS 41 42# 43# Supported file types 44# 45FILE_TYPES = [ 46 'Makefile', 47 '*.mk', 48 '*.c', 49 '*.h', 50 '*.s', 51 '*.S', 52 '*.py', 53 '*.scat', 54 '*CMakeLists.txt', 55 '*.cmake', 56 "*.rb", 57 "*.yaml", 58 "*.yml", 59] 60 61# 62# Supported comment styles (Python regex) 63# 64COMMENT_PATTERN = '^(( \\*)|(;)|(\\#))' 65COMPANY_PATTERN = '(Arm|Renesas|Linaro)' 66COMPANY_FULL_NAME_PATTERN = \ 67 '(Arm Limited and Contributors|Renesas Electronics Corporation|'\ 68 'Linaro Limited and Contributors)' 69 70# 71# git command using diff-filter to include Added (A), Copied (C), Modified (M), 72# Renamed (R), type changed (T), Unmerged (U), Unknown (X) files 73# Deleted files (D) are not included 74# 75GIT_CMD = \ 76 'git diff-tree --name-only --no-commit-id -r --diff-filter=ACMRTUX HEAD' 77 78# 79# License pattern to match 80# 81LICENSE_PATTERN = \ 82 '{0} {1} SCP/MCP Software$\n'\ 83 '({0} Copyright \\(c\\) (?P<years>[0-9]{{4}}(-[0-9]{{4}})?), {2}.'\ 84 ' All rights(( )|(\n{0} ))reserved.$\n)+'\ 85 '{0}$\n'\ 86 '{0} SPDX-License-Identifier: BSD-3-Clause$\n'\ 87 .format(COMMENT_PATTERN, COMPANY_PATTERN, COMPANY_FULL_NAME_PATTERN) 88 89# 90# The number of lines from the beginning of the file to search for the 91# copyright header. This limit avoids the tool searching the whole file when 92# the header always appears near the top. 93# 94# Note: The copyright notice does not usually start on the first line of the 95# file. The value should be enough to include the all of the lines in the 96# LICENSE_PATTERN, plus any extra lines that appears before the license. The 97# performance of the tool may degrade if this value is increased significantly. 98# 99HEAD_LINE_COUNT = 10 100 101 102class ErrorYear(Exception): 103 pass 104 105 106class ErrorCopyright(Exception): 107 pass 108 109 110class ErrorYearNotCurrent(Exception): 111 pass 112 113 114def is_valid_directory(filename): 115 for dir in EXCLUDE_DIRECTORIES: 116 if filename.startswith(dir): 117 return False 118 return True 119 120 121def is_valid_file_type(filename): 122 for file_type in FILE_TYPES: 123 if fnmatch.fnmatch(filename, file_type): 124 return True 125 return False 126 127 128def check_copyright(pattern, filename): 129 with open(filename, encoding="utf-8") as file: 130 # Read just the first HEAD_LINE_COUNT lines of a file 131 head_lines = islice(file, HEAD_LINE_COUNT) 132 head = '' 133 for line in head_lines: 134 head += line 135 136 match = pattern.search(head) 137 if not match: 138 raise ErrorCopyright 139 140 years = match.group('years').split('-') 141 if len(years) > 1: 142 if years[0] > years[1]: 143 raise ErrorYear 144 145 now = datetime.datetime.now() 146 final_year = len(years) - 1 147 if int(years[final_year]) != now.year: 148 raise ErrorYearNotCurrent 149 150 151def main(): 152 pattern = re.compile(LICENSE_PATTERN, re.MULTILINE) 153 error_year_count = 0 154 error_copyright_count = 0 155 error_incorrect_year_count = 0 156 157 print("Checking the copyrights in the code...") 158 159 cwd = os.getcwd() 160 print("Executing from {}".format(cwd)) 161 162 try: 163 result = subprocess.Popen( 164 GIT_CMD, 165 shell=True, 166 stdout=subprocess.PIPE, 167 stderr=subprocess.PIPE) 168 except subprocess.CalledProcessError as e: 169 print("ERROR " + e.returncode + ": Failed to get last changed files") 170 return 1 171 172 for line in result.stdout: 173 filename = line.decode("utf-8").strip('\n') 174 175 if is_valid_file_type(filename) and is_valid_directory(filename): 176 try: 177 check_copyright(pattern, filename) 178 except ErrorYear: 179 print("{}: Invalid year format.".format(filename)) 180 error_year_count += 1 181 182 except ErrorCopyright: 183 print("{}: Invalid copyright header.".format(filename)) 184 error_copyright_count += 1 185 186 except ErrorYearNotCurrent: 187 print("{}: Outdated copyright year range.". 188 format(filename)) 189 error_incorrect_year_count += 1 190 191 if error_year_count != 0 or error_copyright_count != 0 or \ 192 error_incorrect_year_count != 0: 193 print("\t{} files with invalid year(s) format." 194 .format(error_year_count)) 195 print("\t{} files with invalid copyright." 196 .format(error_copyright_count)) 197 print("\t{} files with incorrect year ranges." 198 .format(error_incorrect_year_count)) 199 200 return 1 201 else: 202 print("Check copyright - No errors found.") 203 return 0 204 205 206if __name__ == "__main__": 207 sys.exit(main()) 208