check_copyright.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. #!/usr/bin/env python3
  2. # Copyright 2015 gRPC authors.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import argparse
  16. import datetime
  17. import os
  18. import re
  19. import subprocess
  20. import sys
  21. # find our home
  22. ROOT = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '../..'))
  23. os.chdir(ROOT)
  24. # parse command line
  25. argp = argparse.ArgumentParser(description='copyright checker')
  26. argp.add_argument('-o',
  27. '--output',
  28. default='details',
  29. choices=['list', 'details'])
  30. argp.add_argument('-s', '--skips', default=0, action='store_const', const=1)
  31. argp.add_argument('-a', '--ancient', default=0, action='store_const', const=1)
  32. argp.add_argument('--precommit', action='store_true')
  33. argp.add_argument('--fix', action='store_true')
  34. args = argp.parse_args()
  35. # open the license text
  36. with open('NOTICE.txt') as f:
  37. LICENSE_NOTICE = f.read().splitlines()
  38. # license format by file extension
  39. # key is the file extension, value is a format string
  40. # that given a line of license text, returns what should
  41. # be in the file
  42. LICENSE_PREFIX_RE = {
  43. '.bat': r'@rem\s*',
  44. '.c': r'\s*(?://|\*)\s*',
  45. '.cc': r'\s*(?://|\*)\s*',
  46. '.h': r'\s*(?://|\*)\s*',
  47. '.m': r'\s*\*\s*',
  48. '.mm': r'\s*\*\s*',
  49. '.php': r'\s*\*\s*',
  50. '.js': r'\s*\*\s*',
  51. '.py': r'#\s*',
  52. '.pyx': r'#\s*',
  53. '.pxd': r'#\s*',
  54. '.pxi': r'#\s*',
  55. '.rb': r'#\s*',
  56. '.sh': r'#\s*',
  57. '.proto': r'//\s*',
  58. '.cs': r'//\s*',
  59. '.mak': r'#\s*',
  60. '.bazel': r'#\s*',
  61. '.bzl': r'#\s*',
  62. 'Makefile': r'#\s*',
  63. 'Dockerfile': r'#\s*',
  64. 'BUILD': r'#\s*',
  65. }
  66. # The key is the file extension, while the value is a tuple of fields
  67. # (header, prefix, footer).
  68. # For example, for javascript multi-line comments, the header will be '/*', the
  69. # prefix will be '*' and the footer will be '*/'.
  70. # If header and footer are irrelevant for a specific file extension, they are
  71. # set to None.
  72. LICENSE_PREFIX_TEXT = {
  73. '.bat': (None, '@rem', None),
  74. '.c': (None, '//', None),
  75. '.cc': (None, '//', None),
  76. '.h': (None, '//', None),
  77. '.m': ('/**', ' *', ' */'),
  78. '.mm': ('/**', ' *', ' */'),
  79. '.php': ('/**', ' *', ' */'),
  80. '.js': ('/**', ' *', ' */'),
  81. '.py': (None, '#', None),
  82. '.pyx': (None, '#', None),
  83. '.pxd': (None, '#', None),
  84. '.pxi': (None, '#', None),
  85. '.rb': (None, '#', None),
  86. '.sh': (None, '#', None),
  87. '.proto': (None, '//', None),
  88. '.cs': (None, '//', None),
  89. '.mak': (None, '#', None),
  90. '.bazel': (None, '#', None),
  91. '.bzl': (None, '#', None),
  92. 'Makefile': (None, '#', None),
  93. 'Dockerfile': (None, '#', None),
  94. 'BUILD': (None, '#', None),
  95. }
  96. _EXEMPT = frozenset((
  97. # Generated protocol compiler output.
  98. 'examples/python/helloworld/helloworld_pb2.py',
  99. 'examples/python/helloworld/helloworld_pb2_grpc.py',
  100. 'examples/python/multiplex/helloworld_pb2.py',
  101. 'examples/python/multiplex/helloworld_pb2_grpc.py',
  102. 'examples/python/multiplex/route_guide_pb2.py',
  103. 'examples/python/multiplex/route_guide_pb2_grpc.py',
  104. 'examples/python/route_guide/route_guide_pb2.py',
  105. 'examples/python/route_guide/route_guide_pb2_grpc.py',
  106. # Generated doxygen config file
  107. 'tools/doxygen/Doxyfile.php',
  108. # An older file originally from outside gRPC.
  109. 'src/php/tests/bootstrap.php',
  110. # census.proto copied from github
  111. 'tools/grpcz/census.proto',
  112. # status.proto copied from googleapis
  113. 'src/proto/grpc/status/status.proto',
  114. # Gradle wrappers used to build for Android
  115. 'examples/android/helloworld/gradlew.bat',
  116. 'src/android/test/interop/gradlew.bat',
  117. # Designer-generated source
  118. 'examples/csharp/HelloworldXamarin/Droid/Resources/Resource.designer.cs',
  119. 'examples/csharp/HelloworldXamarin/iOS/ViewController.designer.cs',
  120. # BoringSSL generated header. It has commit version information at the head
  121. # of the file so we cannot check the license info.
  122. 'src/boringssl/boringssl_prefix_symbols.h',
  123. ))
  124. RE_YEAR = r'Copyright (?P<first_year>[0-9]+\-)?(?P<last_year>[0-9]+) ([Tt]he )?gRPC [Aa]uthors(\.|)'
  125. RE_LICENSE = dict(
  126. (k, r'\n'.join(LICENSE_PREFIX_RE[k] +
  127. (RE_YEAR if re.search(RE_YEAR, line) else re.escape(line))
  128. for line in LICENSE_NOTICE))
  129. for k, v in list(LICENSE_PREFIX_RE.items()))
  130. YEAR = datetime.datetime.now().year
  131. LICENSE_YEAR = f'Copyright {YEAR} gRPC authors.'
  132. def join_license_text(header, prefix, footer, notice):
  133. text = (header + '\n') if header else ""
  134. def add_prefix(prefix, line):
  135. # Don't put whitespace between prefix and empty line to avoid having
  136. # trailing whitespaces.
  137. return prefix + ('' if len(line) == 0 else ' ') + line
  138. text += '\n'.join(
  139. add_prefix(prefix, (LICENSE_YEAR if re.search(RE_YEAR, line) else line))
  140. for line in LICENSE_NOTICE)
  141. text += '\n'
  142. if footer:
  143. text += footer + '\n'
  144. return text
  145. LICENSE_TEXT = dict(
  146. (k,
  147. join_license_text(LICENSE_PREFIX_TEXT[k][0], LICENSE_PREFIX_TEXT[k][1],
  148. LICENSE_PREFIX_TEXT[k][2], LICENSE_NOTICE))
  149. for k, v in list(LICENSE_PREFIX_TEXT.items()))
  150. if args.precommit:
  151. FILE_LIST_COMMAND = 'git status -z | grep -Poz \'(?<=^[MARC][MARCD ] )[^\s]+\''
  152. else:
  153. FILE_LIST_COMMAND = 'git ls-tree -r --name-only -r HEAD | ' \
  154. 'grep -v ^third_party/ |' \
  155. 'grep -v "\(ares_config.h\|ares_build.h\)"'
  156. def load(name):
  157. with open(name) as f:
  158. return f.read()
  159. def save(name, text):
  160. with open(name, 'w') as f:
  161. f.write(text)
  162. assert (re.search(RE_LICENSE['Makefile'], load('Makefile')))
  163. def log(cond, why, filename):
  164. if not cond:
  165. return
  166. if args.output == 'details':
  167. print(('%s: %s' % (why, filename)))
  168. else:
  169. print(filename)
  170. # scan files, validate the text
  171. ok = True
  172. filename_list = []
  173. try:
  174. filename_list = subprocess.check_output(FILE_LIST_COMMAND,
  175. shell=True).decode().splitlines()
  176. except subprocess.CalledProcessError:
  177. sys.exit(0)
  178. for filename in filename_list:
  179. if filename in _EXEMPT:
  180. continue
  181. # Skip check for upb generated code.
  182. if (filename.endswith('.upb.h') or filename.endswith('.upb.c') or
  183. filename.endswith('.upbdefs.h') or filename.endswith('.upbdefs.c')):
  184. continue
  185. ext = os.path.splitext(filename)[1]
  186. base = os.path.basename(filename)
  187. if ext in RE_LICENSE:
  188. re_license = RE_LICENSE[ext]
  189. license_text = LICENSE_TEXT[ext]
  190. elif base in RE_LICENSE:
  191. re_license = RE_LICENSE[base]
  192. license_text = LICENSE_TEXT[base]
  193. else:
  194. log(args.skips, 'skip', filename)
  195. continue
  196. try:
  197. text = load(filename)
  198. except:
  199. continue
  200. m = re.search(re_license, text)
  201. if m:
  202. pass
  203. elif 'DO NOT EDIT' not in text:
  204. if args.fix:
  205. text = license_text + '\n' + text
  206. open(filename, 'w').write(text)
  207. log(1, 'copyright missing (fixed)', filename)
  208. else:
  209. log(1, 'copyright missing', filename)
  210. ok = False
  211. if not ok and not args.fix:
  212. print(
  213. 'You may use following command to automatically fix copyright headers:')
  214. print(' tools/distrib/check_copyright.py --fix')
  215. sys.exit(0 if ok else 1)