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