1#!/usr/bin/env python3 2# coding=utf-8 3# Copyright (c) 2016 Google Inc. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16"""Checks for copyright notices in all the files that need them under the 17 18current directory. Optionally insert them. When inserting, replaces 19an MIT or Khronos free use license with Apache 2. 20""" 21 22import argparse 23import fileinput 24import fnmatch 25import inspect 26import os 27import re 28import sys 29 30# List of designated copyright owners. 31AUTHORS = ['The Khronos Group Inc.', 32 'LunarG Inc.', 33 'Google Inc.', 34 'Google LLC', 35 'Pierre Moreau', 36 'Samsung Inc', 37 'André Perez Maselco', 38 'Vasyl Teliman', 39 'Advanced Micro Devices, Inc.', 40 'Stefano Milizia', 41 'Alastair F. Donaldson', 42 'Mostafa Ashraf', 43 'Shiyu Liu', 44 'ZHOU He', 45 'Nintendo', 46 'Epic Games, Inc.', 47 'NVIDIA Corporation'] 48CURRENT_YEAR = 2023 49 50FIRST_YEAR = 2014 51FINAL_YEAR = CURRENT_YEAR + 5 52# A regular expression to match the valid years in the copyright information. 53YEAR_REGEX = '(' + '|'.join( 54 str(year) for year in range(FIRST_YEAR, FINAL_YEAR + 1)) + ')' 55 56# A regular expression to make a range of years in the form <year1>-<year2>. 57YEAR_RANGE_REGEX = '(' 58for year1 in range(FIRST_YEAR, FINAL_YEAR + 1): 59 for year2 in range(year1 + 1, FINAL_YEAR + 1): 60 YEAR_RANGE_REGEX += str(year1) + '-' + str(year2) + '|' 61YEAR_RANGE_REGEX = YEAR_RANGE_REGEX[:-1] + ')' 62 63# In the copyright info, the year can be a single year or a range. This is a 64# regex to make sure it matches one of them. 65YEAR_OR_RANGE_REGEX = '(' + YEAR_REGEX + '|' + YEAR_RANGE_REGEX + ')' 66 67# The final regular expression to match a valid copyright line. 68COPYRIGHT_RE = re.compile('Copyright \(c\) {} ({})'.format( 69 YEAR_OR_RANGE_REGEX, '|'.join(AUTHORS))) 70 71MIT_BEGIN_RE = re.compile('Permission is hereby granted, ' 72 'free of charge, to any person obtaining a') 73MIT_END_RE = re.compile('MATERIALS OR THE USE OR OTHER DEALINGS IN ' 74 'THE MATERIALS.') 75APACHE2_BEGIN_RE = re.compile('Licensed under the Apache License, ' 76 'Version 2.0 \(the "License"\);') 77APACHE2_END_RE = re.compile('limitations under the License.') 78 79LICENSED = """Licensed under the Apache License, Version 2.0 (the "License"); 80you may not use this file except in compliance with the License. 81You may obtain a copy of the License at 82 83 http://www.apache.org/licenses/LICENSE-2.0 84 85Unless required by applicable law or agreed to in writing, software 86distributed under the License is distributed on an "AS IS" BASIS, 87WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 88See the License for the specific language governing permissions and 89limitations under the License.""" 90LICENSED_LEN = 10 # Number of lines in LICENSED 91 92 93def find(top, filename_glob, skip_glob_dir_list, skip_glob_files_list): 94 """Returns files in the tree rooted at top matching filename_glob but not 95 in directories matching skip_glob_dir_list nor files matching 96 skip_glob_dir_list.""" 97 98 file_list = [] 99 for path, dirs, files in os.walk(top): 100 for glob in skip_glob_dir_list: 101 for match in fnmatch.filter(dirs, glob): 102 dirs.remove(match) 103 for filename in fnmatch.filter(files, filename_glob): 104 full_file = os.path.join(path, filename) 105 if full_file not in skip_glob_files_list: 106 file_list.append(full_file) 107 return file_list 108 109 110def filtered_descendants(glob): 111 """Returns glob-matching filenames under the current directory, but skips 112 some irrelevant paths.""" 113 return find('.', glob, ['third_party', 'external', 'CompilerIdCXX', 114 'build*', 'out*'], ['./utils/clang-format-diff.py']) 115 116 117def skip(line): 118 """Returns true if line is all whitespace or shebang.""" 119 stripped = line.lstrip() 120 return stripped == '' or stripped.startswith('#!') 121 122 123def comment(text, prefix): 124 """Returns commented-out text. 125 126 Each line of text will be prefixed by prefix and a space character. Any 127 trailing whitespace will be trimmed. 128 """ 129 accum = ['{} {}'.format(prefix, line).rstrip() for line in text.split('\n')] 130 return '\n'.join(accum) 131 132 133def insert_copyright(author, glob, comment_prefix): 134 """Finds all glob-matching files under the current directory and inserts the 135 copyright message, and license notice. An MIT license or Khronos free 136 use license (modified MIT) is replaced with an Apache 2 license. 137 138 The copyright message goes into the first non-whitespace, non-shebang line 139 in a file. The license notice follows it. Both are prefixed on each line 140 by comment_prefix and a space. 141 """ 142 143 copyright = comment('Copyright (c) {} {}'.format(CURRENT_YEAR, author), 144 comment_prefix) + '\n\n' 145 licensed = comment(LICENSED, comment_prefix) + '\n\n' 146 for file in filtered_descendants(glob): 147 # Parsing states are: 148 # 0 Initial: Have not seen a copyright declaration. 149 # 1 Seen a copyright line and no other interesting lines 150 # 2 In the middle of an MIT or Khronos free use license 151 # 9 Exited any of the above 152 state = 0 153 update_file = False 154 for line in fileinput.input(file, inplace=1): 155 emit = True 156 if state == 0: 157 if COPYRIGHT_RE.search(line): 158 state = 1 159 elif skip(line): 160 pass 161 else: 162 # Didn't see a copyright. Inject copyright and license. 163 sys.stdout.write(copyright) 164 sys.stdout.write(licensed) 165 # Assume there isn't a previous license notice. 166 state = 1 167 elif state == 1: 168 if MIT_BEGIN_RE.search(line): 169 state = 2 170 emit = False 171 elif APACHE2_BEGIN_RE.search(line): 172 # Assume an Apache license is preceded by a copyright 173 # notice. So just emit it like the rest of the file. 174 state = 9 175 elif state == 2: 176 # Replace the MIT license with Apache 2 177 emit = False 178 if MIT_END_RE.search(line): 179 state = 9 180 sys.stdout.write(licensed) 181 if emit: 182 sys.stdout.write(line) 183 184 185def alert_if_no_copyright(glob, comment_prefix): 186 """Prints names of all files missing either a copyright or Apache 2 license. 187 188 Finds all glob-matching files under the current directory and checks if they 189 contain the copyright message and license notice. Prints the names of all the 190 files that don't meet both criteria. 191 192 Returns the total number of file names printed. 193 """ 194 printed_count = 0 195 for file in filtered_descendants(glob): 196 has_copyright = False 197 has_apache2 = False 198 line_num = 0 199 apache_expected_end = 0 200 with open(file, encoding='utf-8') as contents: 201 for line in contents: 202 line_num += 1 203 if COPYRIGHT_RE.search(line): 204 has_copyright = True 205 if APACHE2_BEGIN_RE.search(line): 206 apache_expected_end = line_num + LICENSED_LEN 207 if (line_num is apache_expected_end) and APACHE2_END_RE.search(line): 208 has_apache2 = True 209 if not (has_copyright and has_apache2): 210 message = file 211 if not has_copyright: 212 message += ' has no copyright' 213 if not has_apache2: 214 message += ' has no Apache 2 license notice' 215 print(message) 216 printed_count += 1 217 return printed_count 218 219 220class ArgParser(argparse.ArgumentParser): 221 def __init__(self): 222 super(ArgParser, self).__init__( 223 description=inspect.getdoc(sys.modules[__name__])) 224 self.add_argument('--update', dest='author', action='store', 225 help='For files missing a copyright notice, insert ' 226 'one for the given author, and add a license ' 227 'notice. The author must be in the AUTHORS ' 228 'list in the script.') 229 230 231def main(): 232 glob_comment_pairs = [('*.h', '//'), ('*.hpp', '//'), ('*.sh', '#'), 233 ('*.py', '#'), ('*.cpp', '//'), 234 ('CMakeLists.txt', '#')] 235 argparser = ArgParser() 236 args = argparser.parse_args() 237 238 if args.author: 239 if args.author not in AUTHORS: 240 print('error: --update argument must be in the AUTHORS list in ' 241 'check_copyright.py: {}'.format(AUTHORS)) 242 sys.exit(1) 243 for pair in glob_comment_pairs: 244 insert_copyright(args.author, *pair) 245 sys.exit(0) 246 else: 247 count = sum([alert_if_no_copyright(*p) for p in glob_comment_pairs]) 248 sys.exit(count > 0) 249 250 251if __name__ == '__main__': 252 main() 253