1#!/usr/bin/env python3 2# SPDX-License-Identifier: BSD-3-Clause 3# SPDX-FileCopyrightText: Copyright TF-RMM Contributors. 4# 5 6from argparse import ArgumentParser 7import locale 8import traceback 9import sys 10import re 11import os 12from os import access, R_OK 13from os.path import isfile 14 15SPDX_LICENSE_TAG = 'SPDX-License-Identifier:' 16SPDX_COPYRIGHT_TAG = 'SPDX-FileCopyrightText:' 17SPDX_DEFAULT_LICENSE = 'BSD-3-Clause' 18OTHER_PROJECTS_FILES = [] 19PREFERRED_SPDX_LICENSE_LINE_NUMBER = 2 20RST_PREFERRED_SPDX_LICENSE_LINE_NUMBER = 1 21 22COPYRIGHT_KEYWORD = 'Copyright' 23LICENSE_TAG_PATTERN = r'^(#|\*|//|..|/\*| \*)[ \t*#/]* ' + SPDX_LICENSE_TAG + r'[\w\W]*' 24COPYRIGHT_TAG_PATTERN=r'^(#|\*|//|..|/\*| \*)[ \t*#/]* ' + SPDX_COPYRIGHT_TAG + r'[\w\W]*' 25 26THIRD_PARTY_FILE_TABLE = '.. list-table:: \*\*List of files with different license\*\*' 27RST_TABLE_COL1_PATTERN = r'^( |\t)*\* - [\w\W]*' 28RST_TABLE_COL2_PATTERN = r'^( |\t)*- [\w\W]*' 29 30# check if 'file' is a regular file and it is readable 31def file_readable(file): 32 if not isfile(file): 33 print(file + ": WARNING: File not found") 34 return 0 35 36 if not access(file, R_OK): 37 print(file + ": WARNING: File not readable") 38 return 0 39 40 return 1 41 42# exit program with rc 43def print_error_and_exit(total_errors): 44 if total_errors: 45 print("total: " + str(total_errors) + " errors") 46 sys.exit(1) 47 else: 48 sys.exit(0) 49 50# Get other project file and its license name 51def get_other_projects_files(rst_file): 52 if not file_readable(rst_file): 53 return "" 54 55 empty_line = 0 56 col_num = 1 57 add_row = 0 58 59 with open(rst_file, encoding='utf-8') as fh: 60 for line in fh: 61 if re.search(r'^' + THIRD_PARTY_FILE_TABLE + r'$', line): 62 # Parse the rst table 63 for line in fh: 64 line = line.rstrip() 65 # second empty line denotes end of table 66 if empty_line > 1: 67 break 68 69 # ignore first empty line 70 if not line: 71 empty_line += 1 72 continue 73 74 if col_num == 1 and re.search(RST_TABLE_COL1_PATTERN, line): 75 col1 = line.split('-',1)[1].strip() 76 col_num = 2 77 elif col_num == 2 and re.search(RST_TABLE_COL2_PATTERN, 78 line): 79 col2 = line.split('-',1)[1].strip() 80 col_num = 1 81 add_row = 1 82 else: 83 print(rst_file + ": WARNING: Invalid list-table " + 84 "format in line \"" + line + "\"") 85 break 86 87 if add_row: 88 OTHER_PROJECTS_FILES.append(col1 + "%" + col2) 89 add_row = 0 90 91 # after parsing the table break 92 break 93 94 return 95 96def license_in_other_project(file, license): 97 search_str = file + "%" + license 98 if search_str in OTHER_PROJECTS_FILES: 99 return 1 100 else: 101 return 0 102 103# Check "SPDX-License-Identifier" tag is at required line number 104# Check if "SPDX-License-Identifier" has a valid license 105def verify_spdx_license_tag(file, line, line_number): 106 errors = 0 107 108 if re.search(r'\.rst$', file): 109 preferred_line_no = RST_PREFERRED_SPDX_LICENSE_LINE_NUMBER 110 else: 111 preferred_line_no = PREFERRED_SPDX_LICENSE_LINE_NUMBER 112 113 if line_number != preferred_line_no: 114 print(file + ": ERROR: \"" + SPDX_LICENSE_TAG + "\" is at line:" + 115 str(line_number) + " preferred line number is " + 116 str(preferred_line_no)) 117 errors += 1 118 119 license_string = line.split(SPDX_LICENSE_TAG)[1].strip() 120 if license_string: 121 license = license_string.split()[0] 122 if (license != SPDX_DEFAULT_LICENSE and 123 not license_in_other_project(file, license)): 124 print(file + ": ERROR: Invalid license \"" + license + 125 "\" at line: " + str(line_number)) 126 errors += 1 127 else: 128 print(file + ": ERROR: License name not found at line: " + 129 str(line_number)) 130 errors += 1 131 132 return errors 133 134# Check if "SPDX-FileCopyrightText:" starts with COPYRIGHT_KEYWORD 135def verify_spdx_copyright_tag(file, line, line_number): 136 errors = 0 137 138 cpr_string = line.split(SPDX_COPYRIGHT_TAG)[1].strip() 139 if not cpr_string or COPYRIGHT_KEYWORD != cpr_string.split()[0]: 140 print(file + ": ERROR: Copyright text doesn't starts with \"" + 141 COPYRIGHT_KEYWORD + " \" keyword at line: " + str(line_number)) 142 errors += 1 143 144 return errors 145 146# 147# Check for tags: "SPDX-License-Identifier", "SPDX-FileCopyrightText" 148# 149# Check if "SPDX-FileCopyrightText" is present. 150# This tag must appear be after "SPDX-License-Identifier" tag 151# 152def verify_spdx_headers(file): 153 print("Checking file: " + file) 154 if not file_readable(file): 155 return 0 156 157 lic_tag_found = 0 158 cpr_tag_found = 0 159 errors = 0 160 161 # read first 25 lines 162 with open(file, encoding='utf-8') as fh: 163 for l in range(1, 25): 164 line = fh.readline() 165 if not line: # EOF 166 break 167 line = line.rstrip() 168 if not line: # empty line 169 continue 170 171 if re.search(LICENSE_TAG_PATTERN, line): 172 if lic_tag_found >= 1: 173 print(file + ": ERROR: Duplicate \"" + SPDX_LICENSE_TAG + 174 "\" tag at line: " + str(l)) 175 errors += 1 176 else: 177 errors += verify_spdx_license_tag(file, line, l) 178 lic_tag_found += 1 179 continue 180 181 if re.search(COPYRIGHT_TAG_PATTERN, line): 182 if not lic_tag_found: 183 print(file + ": ERROR: \"" + SPDX_COPYRIGHT_TAG + 184 "\" at line: " + str(l) + " must come after \"" 185 + SPDX_LICENSE_TAG + "\"") 186 errors += 1 187 errors += verify_spdx_copyright_tag(file, line, l) 188 cpr_tag_found += 1 189 continue 190 191 if not lic_tag_found: 192 print(file + ": ERROR: Missing \"" + SPDX_LICENSE_TAG + "\" tag") 193 errors += 1 194 195 if not cpr_tag_found: 196 print(file + ": ERROR: Missing \"" + SPDX_COPYRIGHT_TAG + "\" tag") 197 errors += 1 198 199 if errors: 200 print(file + ": " + str(errors) + " errors found") 201 202 return errors 203 204# main 205if __name__ == '__main__': 206 ap = ArgumentParser(description='Check SPDX headers') 207 ap.add_argument('files', nargs='*', help='Check files.') 208 ap.add_argument('-r', '--readme-rst', type=str, 209 help='path to readme.rst file', required=True) 210 args = ap.parse_args() 211 212 total_errors = 0 213 readme_file = args.readme_rst 214 215 # Parse readme file and get the list of files that have non-default license 216 get_other_projects_files(readme_file) 217 218 for file in args.files: 219 total_errors += verify_spdx_headers(file) 220 221 print_error_and_exit(total_errors) 222