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