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