|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
# Copyright 2015 gRPC authors.
|
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import datetime
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import subprocess
|
|
|
|
import sys
|
|
|
|
|
|
|
|
# find our home
|
|
|
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "../.."))
|
|
|
|
os.chdir(ROOT)
|
|
|
|
|
|
|
|
# parse command line
|
|
|
|
argp = argparse.ArgumentParser(description="copyright checker")
|
|
|
|
argp.add_argument(
|
|
|
|
"-o", "--output", default="details", choices=["list", "details"]
|
|
|
|
)
|
|
|
|
argp.add_argument("-s", "--skips", default=0, action="store_const", const=1)
|
|
|
|
argp.add_argument("-a", "--ancient", default=0, action="store_const", const=1)
|
|
|
|
argp.add_argument("--precommit", action="store_true")
|
|
|
|
argp.add_argument("--fix", action="store_true")
|
|
|
|
args = argp.parse_args()
|
|
|
|
|
|
|
|
# open the license text
|
|
|
|
with open("NOTICE.txt") as f:
|
|
|
|
LICENSE_NOTICE = f.read().splitlines()
|
|
|
|
|
|
|
|
# license format by file extension
|
|
|
|
# key is the file extension, value is a format string
|
|
|
|
# that given a line of license text, returns what should
|
|
|
|
# be in the file
|
|
|
|
LICENSE_PREFIX_RE = {
|
|
|
|
".bat": r"@rem\s*",
|
|
|
|
".c": r"\s*(?://|\*)\s*",
|
|
|
|
".cc": r"\s*(?://|\*)\s*",
|
|
|
|
".h": r"\s*(?://|\*)\s*",
|
|
|
|
".m": r"\s*\*\s*",
|
|
|
|
".mm": r"\s*\*\s*",
|
|
|
|
".php": r"\s*\*\s*",
|
|
|
|
".js": r"\s*\*\s*",
|
|
|
|
".py": r"#\s*",
|
|
|
|
".pyx": r"#\s*",
|
|
|
|
".pxd": r"#\s*",
|
|
|
|
".pxi": r"#\s*",
|
|
|
|
".rb": r"#\s*",
|
|
|
|
".sh": r"#\s*",
|
|
|
|
".proto": r"//\s*",
|
|
|
|
".cs": r"//\s*",
|
|
|
|
".mak": r"#\s*",
|
|
|
|
".bazel": r"#\s*",
|
|
|
|
".bzl": r"#\s*",
|
|
|
|
"Makefile": r"#\s*",
|
|
|
|
"Dockerfile": r"#\s*",
|
|
|
|
"BUILD": r"#\s*",
|
|
|
|
}
|
|
|
|
|
|
|
|
# The key is the file extension, while the value is a tuple of fields
|
|
|
|
# (header, prefix, footer).
|
|
|
|
# For example, for javascript multi-line comments, the header will be '/*', the
|
|
|
|
# prefix will be '*' and the footer will be '*/'.
|
|
|
|
# If header and footer are irrelevant for a specific file extension, they are
|
|
|
|
# set to None.
|
|
|
|
LICENSE_PREFIX_TEXT = {
|
|
|
|
".bat": (None, "@rem", None),
|
|
|
|
".c": (None, "//", None),
|
|
|
|
".cc": (None, "//", None),
|
|
|
|
".h": (None, "//", None),
|
|
|
|
".m": ("/**", " *", " */"),
|
|
|
|
".mm": ("/**", " *", " */"),
|
|
|
|
".php": ("/**", " *", " */"),
|
|
|
|
".js": ("/**", " *", " */"),
|
|
|
|
".py": (None, "#", None),
|
|
|
|
".pyx": (None, "#", None),
|
|
|
|
".pxd": (None, "#", None),
|
|
|
|
".pxi": (None, "#", None),
|
|
|
|
".rb": (None, "#", None),
|
|
|
|
".sh": (None, "#", None),
|
|
|
|
".proto": (None, "//", None),
|
|
|
|
".cs": (None, "//", None),
|
|
|
|
".mak": (None, "#", None),
|
|
|
|
".bazel": (None, "#", None),
|
|
|
|
".bzl": (None, "#", None),
|
|
|
|
"Makefile": (None, "#", None),
|
|
|
|
"Dockerfile": (None, "#", None),
|
|
|
|
"BUILD": (None, "#", None),
|
|
|
|
}
|
|
|
|
|
|
|
|
_EXEMPT = frozenset(
|
|
|
|
(
|
|
|
|
# Generated protocol compiler output.
|
|
|
|
"examples/python/helloworld/helloworld_pb2.py",
|
|
|
|
"examples/python/helloworld/helloworld_pb2_grpc.py",
|
|
|
|
"examples/python/multiplex/helloworld_pb2.py",
|
|
|
|
"examples/python/multiplex/helloworld_pb2_grpc.py",
|
|
|
|
"examples/python/multiplex/route_guide_pb2.py",
|
|
|
|
"examples/python/multiplex/route_guide_pb2_grpc.py",
|
|
|
|
"examples/python/route_guide/route_guide_pb2.py",
|
|
|
|
"examples/python/route_guide/route_guide_pb2_grpc.py",
|
|
|
|
# Generated doxygen config file
|
|
|
|
"tools/doxygen/Doxyfile.php",
|
|
|
|
# An older file originally from outside gRPC.
|
|
|
|
"src/php/tests/bootstrap.php",
|
|
|
|
# census.proto copied from github
|
|
|
|
"tools/grpcz/census.proto",
|
|
|
|
# status.proto copied from googleapis
|
|
|
|
"src/proto/grpc/status/status.proto",
|
|
|
|
# Gradle wrappers used to build for Android
|
|
|
|
"examples/android/helloworld/gradlew.bat",
|
|
|
|
"src/android/test/interop/gradlew.bat",
|
|
|
|
# Designer-generated source
|
|
|
|
"examples/csharp/HelloworldXamarin/Droid/Resources/Resource.designer.cs",
|
|
|
|
"examples/csharp/HelloworldXamarin/iOS/ViewController.designer.cs",
|
|
|
|
# BoringSSL generated header. It has commit version information at the head
|
|
|
|
# of the file so we cannot check the license info.
|
|
|
|
"src/boringssl/boringssl_prefix_symbols.h",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
_ENFORCE_CPP_STYLE_COMMENT_PATH_PREFIX = tuple(
|
|
|
|
[
|
|
|
|
"include/grpc++/",
|
|
|
|
"include/grpcpp/",
|
|
|
|
"src/core/",
|
|
|
|
"src/cpp/",
|
|
|
|
"test/core/",
|
|
|
|
"test/cpp/",
|
|
|
|
"fuzztest/",
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
RE_YEAR = (
|
|
|
|
r"Copyright (?P<first_year>[0-9]+\-)?(?P<last_year>[0-9]+) ([Tt]he )?gRPC"
|
|
|
|
r" [Aa]uthors(\.|)"
|
|
|
|
)
|
|
|
|
RE_LICENSE = dict(
|
|
|
|
(
|
|
|
|
k,
|
|
|
|
r"\n".join(
|
|
|
|
LICENSE_PREFIX_RE[k]
|
|
|
|
+ (RE_YEAR if re.search(RE_YEAR, line) else re.escape(line))
|
|
|
|
for line in LICENSE_NOTICE
|
|
|
|
),
|
|
|
|
)
|
|
|
|
for k, v in list(LICENSE_PREFIX_RE.items())
|
|
|
|
)
|
|
|
|
|
|
|
|
RE_C_STYLE_COMMENT_START = r"^/\*\s*\n"
|
|
|
|
RE_C_STYLE_COMMENT_OPTIONAL_LINE = r"(?:\s*\*\s*\n)*"
|
|
|
|
RE_C_STYLE_COMMENT_END = r"\s*\*/"
|
|
|
|
RE_C_STYLE_COMMENT_LICENSE = (
|
|
|
|
RE_C_STYLE_COMMENT_START
|
|
|
|
+ RE_C_STYLE_COMMENT_OPTIONAL_LINE
|
|
|
|
+ r"\n".join(
|
|
|
|
r"\s*(?:\*)\s*"
|
|
|
|
+ (RE_YEAR if re.search(RE_YEAR, line) else re.escape(line))
|
|
|
|
for line in LICENSE_NOTICE
|
|
|
|
)
|
|
|
|
+ r"\n"
|
|
|
|
+ RE_C_STYLE_COMMENT_OPTIONAL_LINE
|
|
|
|
+ RE_C_STYLE_COMMENT_END
|
|
|
|
)
|
|
|
|
RE_CPP_STYLE_COMMENT_LICENSE = r"\n".join(
|
|
|
|
r"\s*(?://)\s*" + (RE_YEAR if re.search(RE_YEAR, line) else re.escape(line))
|
|
|
|
for line in LICENSE_NOTICE
|
|
|
|
)
|
|
|
|
|
|
|
|
YEAR = datetime.datetime.now().year
|
|
|
|
|
|
|
|
LICENSE_YEAR = f"Copyright {YEAR} gRPC authors."
|
|
|
|
|
|
|
|
|
|
|
|
def join_license_text(header, prefix, footer, notice):
|
|
|
|
text = (header + "\n") if header else ""
|
|
|
|
|
|
|
|
def add_prefix(prefix, line):
|
|
|
|
# Don't put whitespace between prefix and empty line to avoid having
|
|
|
|
# trailing whitespaces.
|
|
|
|
return prefix + ("" if len(line) == 0 else " ") + line
|
|
|
|
|
|
|
|
text += "\n".join(
|
|
|
|
add_prefix(prefix, (LICENSE_YEAR if re.search(RE_YEAR, line) else line))
|
|
|
|
for line in LICENSE_NOTICE
|
|
|
|
)
|
|
|
|
text += "\n"
|
|
|
|
if footer:
|
|
|
|
text += footer + "\n"
|
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
|
|
LICENSE_TEXT = dict(
|
|
|
|
(
|
|
|
|
k,
|
|
|
|
join_license_text(
|
|
|
|
LICENSE_PREFIX_TEXT[k][0],
|
|
|
|
LICENSE_PREFIX_TEXT[k][1],
|
|
|
|
LICENSE_PREFIX_TEXT[k][2],
|
|
|
|
LICENSE_NOTICE,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
for k, v in list(LICENSE_PREFIX_TEXT.items())
|
|
|
|
)
|
|
|
|
|
|
|
|
if args.precommit:
|
|
|
|
FILE_LIST_COMMAND = (
|
|
|
|
"git status -z | grep -Poz '(?<=^[MARC][MARCD ] )[^\s]+'"
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
FILE_LIST_COMMAND = (
|
|
|
|
"git ls-tree -r --name-only -r HEAD | "
|
|
|
|
"grep -v ^third_party/ |"
|
|
|
|
'grep -v "\(ares_config.h\|ares_build.h\)"'
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def load(name):
|
|
|
|
with open(name) as f:
|
|
|
|
return f.read()
|
|
|
|
|
|
|
|
|
|
|
|
def save(name, text):
|
|
|
|
with open(name, "w") as f:
|
|
|
|
f.write(text)
|
|
|
|
|
|
|
|
|
|
|
|
assert re.search(RE_LICENSE["Makefile"], load("Makefile"))
|
|
|
|
|
|
|
|
|
|
|
|
def log(cond, why, filename):
|
|
|
|
if not cond:
|
|
|
|
return
|
|
|
|
if args.output == "details":
|
|
|
|
print(("%s: %s" % (why, filename)))
|
|
|
|
else:
|
|
|
|
print(filename)
|
|
|
|
|
|
|
|
|
|
|
|
def write_copyright(license_text, file_text, filename):
|
|
|
|
shebang = ""
|
|
|
|
lines = file_text.split("\n")
|
|
|
|
if lines and lines[0].startswith("#!"):
|
|
|
|
shebang = lines[0] + "\n"
|
|
|
|
file_text = file_text[len(shebang) :]
|
|
|
|
|
|
|
|
rewritten_text = shebang + license_text + "\n" + file_text
|
|
|
|
with open(filename, "w") as f:
|
|
|
|
f.write(rewritten_text)
|
|
|
|
|
|
|
|
|
|
|
|
def replace_copyright(license_text, file_text, filename):
|
|
|
|
m = re.search(RE_C_STYLE_COMMENT_LICENSE, text)
|
|
|
|
if m:
|
|
|
|
rewritten_text = license_text + file_text[m.end() :]
|
|
|
|
with open(filename, "w") as f:
|
|
|
|
f.write(rewritten_text)
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
# scan files, validate the text
|
|
|
|
ok = True
|
|
|
|
filename_list = []
|
|
|
|
try:
|
|
|
|
filename_list = (
|
|
|
|
subprocess.check_output(FILE_LIST_COMMAND, shell=True)
|
|
|
|
.decode()
|
|
|
|
.splitlines()
|
|
|
|
)
|
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
|
|
for filename in filename_list:
|
|
|
|
enforce_cpp_style_comment = False
|
|
|
|
if filename in _EXEMPT:
|
|
|
|
continue
|
|
|
|
# Skip check for upb generated code.
|
|
|
|
if (
|
|
|
|
filename.endswith(".upb.h")
|
|
|
|
or filename.endswith(".upbdefs.h")
|
|
|
|
or filename.endswith(".upbdefs.c")
|
|
|
|
or filename.endswith(".upb_minitable.h")
|
|
|
|
or filename.endswith(".upb_minitable.c")
|
|
|
|
):
|
|
|
|
continue
|
|
|
|
# Allow empty __init__.py files for code generated by xds_protos
|
|
|
|
if filename.startswith("tools/distrib/python/xds_protos") and (
|
|
|
|
filename.endswith("__init__.py")
|
|
|
|
or filename.endswith("generated_file_import_test.py")
|
|
|
|
):
|
|
|
|
continue
|
|
|
|
ext = os.path.splitext(filename)[1]
|
|
|
|
base = os.path.basename(filename)
|
|
|
|
if filename.startswith(_ENFORCE_CPP_STYLE_COMMENT_PATH_PREFIX) and ext in [
|
|
|
|
".cc",
|
|
|
|
".h",
|
|
|
|
]:
|
|
|
|
enforce_cpp_style_comment = True
|
|
|
|
re_license = RE_CPP_STYLE_COMMENT_LICENSE
|
|
|
|
license_text = LICENSE_TEXT[ext]
|
|
|
|
elif ext in RE_LICENSE:
|
|
|
|
re_license = RE_LICENSE[ext]
|
|
|
|
license_text = LICENSE_TEXT[ext]
|
|
|
|
elif base in RE_LICENSE:
|
|
|
|
re_license = RE_LICENSE[base]
|
|
|
|
license_text = LICENSE_TEXT[base]
|
|
|
|
else:
|
|
|
|
log(args.skips, "skip", filename)
|
|
|
|
continue
|
|
|
|
try:
|
|
|
|
text = load(filename)
|
|
|
|
except:
|
|
|
|
continue
|
|
|
|
m = re.search(re_license, text)
|
|
|
|
if m:
|
|
|
|
pass
|
|
|
|
elif enforce_cpp_style_comment:
|
|
|
|
log(
|
|
|
|
1,
|
|
|
|
"copyright missing or does not use cpp-style copyright header",
|
|
|
|
filename,
|
|
|
|
)
|
|
|
|
if args.fix:
|
|
|
|
# Attempt fix: search for c-style copyright header and replace it
|
|
|
|
# with cpp-style copyright header. If that doesn't work
|
|
|
|
# (e.g. missing copyright header), write cpp-style copyright header.
|
|
|
|
if not replace_copyright(license_text, text, filename):
|
|
|
|
write_copyright(license_text, text, filename)
|
|
|
|
ok = False
|
|
|
|
elif "DO NOT EDIT" not in text:
|
|
|
|
if args.fix:
|
|
|
|
write_copyright(license_text, text, filename)
|
|
|
|
log(1, "copyright missing (fixed)", filename)
|
|
|
|
else:
|
|
|
|
log(1, "copyright missing", filename)
|
|
|
|
ok = False
|
|
|
|
|
|
|
|
if not ok and not args.fix:
|
|
|
|
print(
|
|
|
|
"You may use following command to automatically fix copyright headers:"
|
|
|
|
)
|
|
|
|
print(" tools/distrib/check_copyright.py --fix")
|
|
|
|
|
|
|
|
sys.exit(0 if ok else 1)
|