1#!/usr/bin/env python3 2 3# Copyright 2015 gRPC authors. 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 17import argparse 18import datetime 19import os 20import re 21import sys 22import subprocess 23 24# find our home 25ROOT = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '../..')) 26os.chdir(ROOT) 27 28# parse command line 29argp = argparse.ArgumentParser(description='copyright checker') 30argp.add_argument('-o', 31 '--output', 32 default='details', 33 choices=['list', 'details']) 34argp.add_argument('-s', '--skips', default=0, action='store_const', const=1) 35argp.add_argument('-a', '--ancient', default=0, action='store_const', const=1) 36argp.add_argument('--precommit', default=False, action='store_true') 37args = argp.parse_args() 38 39# open the license text 40with open('NOTICE.txt') as f: 41 LICENSE_NOTICE = f.read().splitlines() 42 43# license format by file extension 44# key is the file extension, value is a format string 45# that given a line of license text, returns what should 46# be in the file 47LICENSE_PREFIX = { 48 '.bat': r'@rem\s*', 49 '.c': r'\s*(?://|\*)\s*', 50 '.cc': r'\s*(?://|\*)\s*', 51 '.h': r'\s*(?://|\*)\s*', 52 '.m': r'\s*\*\s*', 53 '.mm': r'\s*\*\s*', 54 '.php': r'\s*\*\s*', 55 '.js': r'\s*\*\s*', 56 '.py': r'#\s*', 57 '.pyx': r'#\s*', 58 '.pxd': r'#\s*', 59 '.pxi': r'#\s*', 60 '.rb': r'#\s*', 61 '.sh': r'#\s*', 62 '.proto': r'//\s*', 63 '.cs': r'//\s*', 64 '.mak': r'#\s*', 65 'Makefile': r'#\s*', 66 'Dockerfile': r'#\s*', 67 'BUILD': r'#\s*', 68} 69 70_EXEMPT = frozenset(( 71 # Generated protocol compiler output. 72 'examples/python/helloworld/helloworld_pb2.py', 73 'examples/python/helloworld/helloworld_pb2_grpc.py', 74 'examples/python/multiplex/helloworld_pb2.py', 75 'examples/python/multiplex/helloworld_pb2_grpc.py', 76 'examples/python/multiplex/route_guide_pb2.py', 77 'examples/python/multiplex/route_guide_pb2_grpc.py', 78 'examples/python/route_guide/route_guide_pb2.py', 79 'examples/python/route_guide/route_guide_pb2_grpc.py', 80 81 # Generated doxygen config file 82 'tools/doxygen/Doxyfile.php', 83 84 # An older file originally from outside gRPC. 85 'src/php/tests/bootstrap.php', 86 # census.proto copied from github 87 'tools/grpcz/census.proto', 88 # status.proto copied from googleapis 89 'src/proto/grpc/status/status.proto', 90 91 # Gradle wrappers used to build for Android 92 'examples/android/helloworld/gradlew.bat', 93 'src/android/test/interop/gradlew.bat', 94 95 # Designer-generated source 96 'examples/csharp/HelloworldXamarin/Droid/Resources/Resource.designer.cs', 97 'examples/csharp/HelloworldXamarin/iOS/ViewController.designer.cs', 98 99 # BoringSSL generated header. It has commit version information at the head 100 # of the file so we cannot check the license info. 101 'src/boringssl/boringssl_prefix_symbols.h', 102)) 103 104RE_YEAR = r'Copyright (?P<first_year>[0-9]+\-)?(?P<last_year>[0-9]+) ([Tt]he )?gRPC [Aa]uthors(\.|)' 105RE_LICENSE = dict( 106 (k, r'\n'.join(LICENSE_PREFIX[k] + 107 (RE_YEAR if re.search(RE_YEAR, line) else re.escape(line)) 108 for line in LICENSE_NOTICE)) 109 for k, v in LICENSE_PREFIX.items()) 110 111if args.precommit: 112 FILE_LIST_COMMAND = 'git status -z | grep -Poz \'(?<=^[MARC][MARCD ] )[^\s]+\'' 113else: 114 FILE_LIST_COMMAND = 'git ls-tree -r --name-only -r HEAD | ' \ 115 'grep -v ^third_party/ |' \ 116 'grep -v "\(ares_config.h\|ares_build.h\)"' 117 118 119def load(name): 120 with open(name) as f: 121 return f.read() 122 123 124def save(name, text): 125 with open(name, 'w') as f: 126 f.write(text) 127 128 129assert (re.search(RE_LICENSE['Makefile'], load('Makefile'))) 130 131 132def log(cond, why, filename): 133 if not cond: return 134 if args.output == 'details': 135 print('%s: %s' % (why, filename)) 136 else: 137 print(filename) 138 139 140# scan files, validate the text 141ok = True 142filename_list = [] 143try: 144 filename_list = subprocess.check_output(FILE_LIST_COMMAND, 145 shell=True).decode().splitlines() 146except subprocess.CalledProcessError: 147 sys.exit(0) 148 149for filename in filename_list: 150 if filename in _EXEMPT: 151 continue 152 # Skip check for upb generated code. 153 if (filename.endswith('.upb.h') or filename.endswith('.upb.c') or 154 filename.endswith('.upbdefs.h') or filename.endswith('.upbdefs.c')): 155 continue 156 ext = os.path.splitext(filename)[1] 157 base = os.path.basename(filename) 158 if ext in RE_LICENSE: 159 re_license = RE_LICENSE[ext] 160 elif base in RE_LICENSE: 161 re_license = RE_LICENSE[base] 162 else: 163 log(args.skips, 'skip', filename) 164 continue 165 try: 166 text = load(filename) 167 except: 168 continue 169 m = re.search(re_license, text) 170 if m: 171 pass 172 elif 'DO NOT EDIT' not in text: 173 log(1, 'copyright missing', filename) 174 ok = False 175 176sys.exit(0 if ok else 1) 177