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