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