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 subprocess 22import sys 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( 31 "-o", "--output", default="details", choices=["list", "details"] 32) 33argp.add_argument("-s", "--skips", default=0, action="store_const", const=1) 34argp.add_argument("-a", "--ancient", default=0, action="store_const", const=1) 35argp.add_argument("--precommit", action="store_true") 36argp.add_argument("--fix", 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_RE = { 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 ".bazel": r"#\s*", 66 ".bzl": r"#\s*", 67 "Makefile": r"#\s*", 68 "Dockerfile": r"#\s*", 69 "BUILD": r"#\s*", 70} 71 72# The key is the file extension, while the value is a tuple of fields 73# (header, prefix, footer). 74# For example, for javascript multi-line comments, the header will be '/*', the 75# prefix will be '*' and the footer will be '*/'. 76# If header and footer are irrelevant for a specific file extension, they are 77# set to None. 78LICENSE_PREFIX_TEXT = { 79 ".bat": (None, "@rem", None), 80 ".c": (None, "//", None), 81 ".cc": (None, "//", None), 82 ".h": (None, "//", None), 83 ".m": ("/**", " *", " */"), 84 ".mm": ("/**", " *", " */"), 85 ".php": ("/**", " *", " */"), 86 ".js": ("/**", " *", " */"), 87 ".py": (None, "#", None), 88 ".pyx": (None, "#", None), 89 ".pxd": (None, "#", None), 90 ".pxi": (None, "#", None), 91 ".rb": (None, "#", None), 92 ".sh": (None, "#", None), 93 ".proto": (None, "//", None), 94 ".cs": (None, "//", None), 95 ".mak": (None, "#", None), 96 ".bazel": (None, "#", None), 97 ".bzl": (None, "#", None), 98 "Makefile": (None, "#", None), 99 "Dockerfile": (None, "#", None), 100 "BUILD": (None, "#", None), 101} 102 103_EXEMPT = frozenset( 104 ( 105 # Generated protocol compiler output. 106 "examples/python/helloworld/helloworld_pb2.py", 107 "examples/python/helloworld/helloworld_pb2_grpc.py", 108 "examples/python/multiplex/helloworld_pb2.py", 109 "examples/python/multiplex/helloworld_pb2_grpc.py", 110 "examples/python/multiplex/route_guide_pb2.py", 111 "examples/python/multiplex/route_guide_pb2_grpc.py", 112 "examples/python/route_guide/route_guide_pb2.py", 113 "examples/python/route_guide/route_guide_pb2_grpc.py", 114 # Generated doxygen config file 115 "tools/doxygen/Doxyfile.php", 116 # An older file originally from outside gRPC. 117 "src/php/tests/bootstrap.php", 118 # census.proto copied from github 119 "tools/grpcz/census.proto", 120 # status.proto copied from googleapis 121 "src/proto/grpc/status/status.proto", 122 # Gradle wrappers used to build for Android 123 "examples/android/helloworld/gradlew.bat", 124 "src/android/test/interop/gradlew.bat", 125 # Designer-generated source 126 "examples/csharp/HelloworldXamarin/Droid/Resources/Resource.designer.cs", 127 "examples/csharp/HelloworldXamarin/iOS/ViewController.designer.cs", 128 # BoringSSL generated header. It has commit version information at the head 129 # of the file so we cannot check the license info. 130 "src/boringssl/boringssl_prefix_symbols.h", 131 ) 132) 133 134_ENFORCE_CPP_STYLE_COMMENT_PATH_PREFIX = tuple( 135 [ 136 "include/grpc++/", 137 "include/grpcpp/", 138 "src/core/", 139 "src/cpp/", 140 "test/core/", 141 "test/cpp/", 142 "fuzztest/", 143 ] 144) 145 146RE_YEAR = ( 147 r"Copyright (?P<first_year>[0-9]+\-)?(?P<last_year>[0-9]+) ([Tt]he )?gRPC" 148 r" [Aa]uthors(\.|)" 149) 150RE_LICENSE = dict( 151 ( 152 k, 153 r"\n".join( 154 LICENSE_PREFIX_RE[k] 155 + (RE_YEAR if re.search(RE_YEAR, line) else re.escape(line)) 156 for line in LICENSE_NOTICE 157 ), 158 ) 159 for k, v in list(LICENSE_PREFIX_RE.items()) 160) 161 162RE_C_STYLE_COMMENT_START = r"^/\*\s*\n" 163RE_C_STYLE_COMMENT_OPTIONAL_LINE = r"(?:\s*\*\s*\n)*" 164RE_C_STYLE_COMMENT_END = r"\s*\*/" 165RE_C_STYLE_COMMENT_LICENSE = ( 166 RE_C_STYLE_COMMENT_START 167 + RE_C_STYLE_COMMENT_OPTIONAL_LINE 168 + r"\n".join( 169 r"\s*(?:\*)\s*" 170 + (RE_YEAR if re.search(RE_YEAR, line) else re.escape(line)) 171 for line in LICENSE_NOTICE 172 ) 173 + r"\n" 174 + RE_C_STYLE_COMMENT_OPTIONAL_LINE 175 + RE_C_STYLE_COMMENT_END 176) 177RE_CPP_STYLE_COMMENT_LICENSE = r"\n".join( 178 r"\s*(?://)\s*" + (RE_YEAR if re.search(RE_YEAR, line) else re.escape(line)) 179 for line in LICENSE_NOTICE 180) 181 182YEAR = datetime.datetime.now().year 183 184LICENSE_YEAR = f"Copyright {YEAR} gRPC authors." 185 186 187def join_license_text(header, prefix, footer, notice): 188 text = (header + "\n") if header else "" 189 190 def add_prefix(prefix, line): 191 # Don't put whitespace between prefix and empty line to avoid having 192 # trailing whitespaces. 193 return prefix + ("" if len(line) == 0 else " ") + line 194 195 text += "\n".join( 196 add_prefix(prefix, (LICENSE_YEAR if re.search(RE_YEAR, line) else line)) 197 for line in LICENSE_NOTICE 198 ) 199 text += "\n" 200 if footer: 201 text += footer + "\n" 202 return text 203 204 205LICENSE_TEXT = dict( 206 ( 207 k, 208 join_license_text( 209 LICENSE_PREFIX_TEXT[k][0], 210 LICENSE_PREFIX_TEXT[k][1], 211 LICENSE_PREFIX_TEXT[k][2], 212 LICENSE_NOTICE, 213 ), 214 ) 215 for k, v in list(LICENSE_PREFIX_TEXT.items()) 216) 217 218if args.precommit: 219 FILE_LIST_COMMAND = ( 220 "git status -z | grep -Poz '(?<=^[MARC][MARCD ] )[^\s]+'" 221 ) 222else: 223 FILE_LIST_COMMAND = ( 224 "git ls-tree -r --name-only -r HEAD | " 225 "grep -v ^third_party/ |" 226 'grep -v "\(ares_config.h\|ares_build.h\)"' 227 ) 228 229 230def load(name): 231 with open(name) as f: 232 return f.read() 233 234 235def save(name, text): 236 with open(name, "w") as f: 237 f.write(text) 238 239 240assert re.search(RE_LICENSE["Makefile"], load("Makefile")) 241 242 243def log(cond, why, filename): 244 if not cond: 245 return 246 if args.output == "details": 247 print(("%s: %s" % (why, filename))) 248 else: 249 print(filename) 250 251 252def write_copyright(license_text, file_text, filename): 253 shebang = "" 254 lines = file_text.split("\n") 255 if lines and lines[0].startswith("#!"): 256 shebang = lines[0] + "\n" 257 file_text = file_text[len(shebang) :] 258 259 rewritten_text = shebang + license_text + "\n" + file_text 260 with open(filename, "w") as f: 261 f.write(rewritten_text) 262 263 264def replace_copyright(license_text, file_text, filename): 265 m = re.search(RE_C_STYLE_COMMENT_LICENSE, text) 266 if m: 267 rewritten_text = license_text + file_text[m.end() :] 268 with open(filename, "w") as f: 269 f.write(rewritten_text) 270 return True 271 return False 272 273 274# scan files, validate the text 275ok = True 276filename_list = [] 277try: 278 filename_list = ( 279 subprocess.check_output(FILE_LIST_COMMAND, shell=True) 280 .decode() 281 .splitlines() 282 ) 283except subprocess.CalledProcessError: 284 sys.exit(0) 285 286for filename in filename_list: 287 enforce_cpp_style_comment = False 288 if filename in _EXEMPT: 289 continue 290 # Skip check for upb generated code. 291 if ( 292 filename.endswith(".upb.h") 293 or filename.endswith(".upbdefs.h") 294 or filename.endswith(".upbdefs.c") 295 or filename.endswith(".upb_minitable.h") 296 or filename.endswith(".upb_minitable.c") 297 ): 298 continue 299 # Allow empty __init__.py files for code generated by xds_protos 300 if filename.startswith("tools/distrib/python/xds_protos") and ( 301 filename.endswith("__init__.py") 302 or filename.endswith("generated_file_import_test.py") 303 ): 304 continue 305 ext = os.path.splitext(filename)[1] 306 base = os.path.basename(filename) 307 if filename.startswith(_ENFORCE_CPP_STYLE_COMMENT_PATH_PREFIX) and ext in [ 308 ".cc", 309 ".h", 310 ]: 311 enforce_cpp_style_comment = True 312 re_license = RE_CPP_STYLE_COMMENT_LICENSE 313 license_text = LICENSE_TEXT[ext] 314 elif ext in RE_LICENSE: 315 re_license = RE_LICENSE[ext] 316 license_text = LICENSE_TEXT[ext] 317 elif base in RE_LICENSE: 318 re_license = RE_LICENSE[base] 319 license_text = LICENSE_TEXT[base] 320 else: 321 log(args.skips, "skip", filename) 322 continue 323 try: 324 text = load(filename) 325 except: 326 continue 327 m = re.search(re_license, text) 328 if m: 329 pass 330 elif enforce_cpp_style_comment: 331 log( 332 1, 333 "copyright missing or does not use cpp-style copyright header", 334 filename, 335 ) 336 if args.fix: 337 # Attempt fix: search for c-style copyright header and replace it 338 # with cpp-style copyright header. If that doesn't work 339 # (e.g. missing copyright header), write cpp-style copyright header. 340 if not replace_copyright(license_text, text, filename): 341 write_copyright(license_text, text, filename) 342 ok = False 343 elif "DO NOT EDIT" not in text: 344 if args.fix: 345 write_copyright(license_text, text, filename) 346 log(1, "copyright missing (fixed)", filename) 347 else: 348 log(1, "copyright missing", filename) 349 ok = False 350 351if not ok and not args.fix: 352 print( 353 "You may use following command to automatically fix copyright headers:" 354 ) 355 print(" tools/distrib/check_copyright.py --fix") 356 357sys.exit(0 if ok else 1) 358