| 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | # ################################################################ |
| 4 | # Copyright (c) Meta Platforms, Inc. and affiliates. |
| 5 | # All rights reserved. |
| 6 | # |
| 7 | # This source code is licensed under both the BSD-style license (found in the |
| 8 | # LICENSE file in the root directory of this source tree) and the GPLv2 (found |
| 9 | # in the COPYING file in the root directory of this source tree). |
| 10 | # You may select, at your option, one of the above-listed licenses. |
| 11 | # ################################################################ |
| 12 | |
| 13 | import enum |
| 14 | import glob |
| 15 | import os |
| 16 | import re |
| 17 | import sys |
| 18 | |
| 19 | ROOT = os.path.join(os.path.dirname(__file__), "..") |
| 20 | |
| 21 | RELDIRS = [ |
| 22 | "doc", |
| 23 | "examples", |
| 24 | "lib", |
| 25 | "programs", |
| 26 | "tests", |
| 27 | "contrib/linux-kernel", |
| 28 | ] |
| 29 | |
| 30 | REL_EXCLUDES = [ |
| 31 | "contrib/linux-kernel/test/include", |
| 32 | ] |
| 33 | |
| 34 | def to_abs(d): |
| 35 | return os.path.normpath(os.path.join(ROOT, d)) + "/" |
| 36 | |
| 37 | DIRS = [to_abs(d) for d in RELDIRS] |
| 38 | EXCLUDES = [to_abs(d) for d in REL_EXCLUDES] |
| 39 | |
| 40 | SUFFIXES = [ |
| 41 | ".c", |
| 42 | ".h", |
| 43 | "Makefile", |
| 44 | ".mk", |
| 45 | ".py", |
| 46 | ".S", |
| 47 | ] |
| 48 | |
| 49 | # License should certainly be in the first 10 KB. |
| 50 | MAX_BYTES = 10000 |
| 51 | MAX_LINES = 50 |
| 52 | |
| 53 | LICENSE_LINES = [ |
| 54 | "This source code is licensed under both the BSD-style license (found in the", |
| 55 | "LICENSE file in the root directory of this source tree) and the GPLv2 (found", |
| 56 | "in the COPYING file in the root directory of this source tree).", |
| 57 | "You may select, at your option, one of the above-listed licenses.", |
| 58 | ] |
| 59 | |
| 60 | COPYRIGHT_EXCEPTIONS = { |
| 61 | # From zstdmt |
| 62 | "threading.c", |
| 63 | "threading.h", |
| 64 | # From divsufsort |
| 65 | "divsufsort.c", |
| 66 | "divsufsort.h", |
| 67 | } |
| 68 | |
| 69 | LICENSE_EXCEPTIONS = { |
| 70 | # From divsufsort |
| 71 | "divsufsort.c", |
| 72 | "divsufsort.h", |
| 73 | # License is slightly different because it references GitHub |
| 74 | "linux_zstd.h", |
| 75 | } |
| 76 | |
| 77 | |
| 78 | def valid_copyright(lines): |
| 79 | YEAR_REGEX = re.compile("\d\d\d\d|present") |
| 80 | for line in lines: |
| 81 | line = line.strip() |
| 82 | if "Copyright" not in line: |
| 83 | continue |
| 84 | if "present" in line: |
| 85 | return (False, f"Copyright line '{line}' contains 'present'!") |
| 86 | if "Meta Platforms, Inc" not in line: |
| 87 | return (False, f"Copyright line '{line}' does not contain 'Meta Platforms, Inc'") |
| 88 | year = YEAR_REGEX.search(line) |
| 89 | if year is not None: |
| 90 | return (False, f"Copyright line '{line}' contains {year.group(0)}; it should be yearless") |
| 91 | if " (c) " not in line: |
| 92 | return (False, f"Copyright line '{line}' does not contain ' (c) '!") |
| 93 | return (True, "") |
| 94 | return (False, "Copyright not found!") |
| 95 | |
| 96 | |
| 97 | def valid_license(lines): |
| 98 | for b in range(len(lines)): |
| 99 | if LICENSE_LINES[0] not in lines[b]: |
| 100 | continue |
| 101 | for l in range(len(LICENSE_LINES)): |
| 102 | if LICENSE_LINES[l] not in lines[b + l]: |
| 103 | message = f"""Invalid license line found starting on line {b + l}! |
| 104 | Expected: '{LICENSE_LINES[l]}' |
| 105 | Actual: '{lines[b + l]}'""" |
| 106 | return (False, message) |
| 107 | return (True, "") |
| 108 | return (False, "License not found!") |
| 109 | |
| 110 | |
| 111 | def valid_file(filename): |
| 112 | with open(filename, "r") as f: |
| 113 | lines = f.readlines(MAX_BYTES) |
| 114 | lines = lines[:min(len(lines), MAX_LINES)] |
| 115 | |
| 116 | ok = True |
| 117 | if os.path.basename(filename) not in COPYRIGHT_EXCEPTIONS: |
| 118 | c_ok, c_msg = valid_copyright(lines) |
| 119 | if not c_ok: |
| 120 | print(f"{filename}: {c_msg}", file=sys.stderr) |
| 121 | ok = False |
| 122 | if os.path.basename(filename) not in LICENSE_EXCEPTIONS: |
| 123 | l_ok, l_msg = valid_license(lines) |
| 124 | if not l_ok: |
| 125 | print(f"{filename}: {l_msg}", file=sys.stderr) |
| 126 | ok = False |
| 127 | return ok |
| 128 | |
| 129 | |
| 130 | def exclude(filename): |
| 131 | for x in EXCLUDES: |
| 132 | if filename.startswith(x): |
| 133 | return True |
| 134 | return False |
| 135 | |
| 136 | def main(): |
| 137 | invalid_files = [] |
| 138 | for directory in DIRS: |
| 139 | for suffix in SUFFIXES: |
| 140 | files = set(glob.glob(f"{directory}/**/*{suffix}", recursive=True)) |
| 141 | for filename in files: |
| 142 | if exclude(filename): |
| 143 | continue |
| 144 | if not valid_file(filename): |
| 145 | invalid_files.append(filename) |
| 146 | if len(invalid_files) > 0: |
| 147 | print("Fail!", file=sys.stderr) |
| 148 | for f in invalid_files: |
| 149 | print(f) |
| 150 | return 1 |
| 151 | else: |
| 152 | print("Pass!", file=sys.stderr) |
| 153 | return 0 |
| 154 | |
| 155 | if __name__ == "__main__": |
| 156 | sys.exit(main()) |