File indexing completed on 2025-08-03 08:09:26
0001
0002 import argparse
0003 import os
0004 import sys
0005 from subprocess import check_output
0006 import re
0007 import difflib
0008 from datetime import datetime
0009 from fnmatch import fnmatch
0010
0011 EXCLUDE = []
0012
0013
0014 class bcolors:
0015 HEADER = "\033[95m"
0016 OKBLUE = "\033[94m"
0017 OKGREEN = "\033[92m"
0018 WARNING = "\033[93m"
0019 FAIL = "\033[91m"
0020 ENDC = "\033[0m"
0021 BOLD = "\033[1m"
0022 UNDERLINE = "\033[4m"
0023
0024
0025 CROSS_SYMBOL = "\u2717"
0026
0027
0028 def err(string):
0029 if sys.stdout.isatty():
0030 return bcolors.FAIL + bcolors.BOLD + string + bcolors.ENDC
0031 else:
0032 return string
0033
0034
0035 class CommitInfo:
0036 date = None
0037 year = None
0038 author = None
0039 subject = None
0040 body = None
0041
0042
0043 def check_git_dates(src):
0044 output = (
0045 check_output(["git", "log", "--format={{{%an|%ad|%s|%b}}}", "--", src])
0046 .decode("utf-8")
0047 .strip()
0048 )
0049
0050
0051 commits = re.findall(r"{{{((?:.|\n)*?)}}}", output)
0052 commits = [c for c in commits if "[ignore-license]" not in c]
0053 commits = [c.split("|") for c in commits]
0054
0055
0056 mod = commits[0]
0057 add = commits[-1]
0058
0059 madd = re.match(r".*\d{2}:\d{2}:\d{2} (\d{4})", add[1])
0060 assert madd != None, "Regex did not match git log output"
0061 mmod = re.match(r".*\d{2}:\d{2}:\d{2} (\d{4})", mod[1])
0062 assert mmod != None, "Regex did not match git log output"
0063
0064 addcommit = CommitInfo()
0065 addcommit.date = add[1]
0066 addcommit.year = int(madd.group(1))
0067 addcommit.author = add[0]
0068 addcommit.subject = add[2]
0069 addcommit.body = add[3]
0070
0071 modcommit = CommitInfo()
0072 modcommit.date = mod[1]
0073 modcommit.year = int(mmod.group(1))
0074 modcommit.author = mod[0]
0075 modcommit.subject = mod[2]
0076 modcommit.body = mod[3]
0077
0078 return addcommit, modcommit
0079
0080
0081 def main():
0082 p = argparse.ArgumentParser()
0083 p.add_argument("input")
0084 p.add_argument(
0085 "--fix", action="store_true", help="Attempt to fix any license issues found."
0086 )
0087 p.add_argument(
0088 "--check-years",
0089 action="store_true",
0090 help="Check the license year info using git info for each file.",
0091 )
0092 p.add_argument(
0093 "--fail-year-mismatch",
0094 action="store_true",
0095 help="Fail if year in license statement is not valid.",
0096 )
0097 p.add_argument("--exclude", "-e", action="append", default=EXCLUDE)
0098
0099 args = p.parse_args()
0100 print(args.exclude)
0101
0102 if os.path.isdir(args.input):
0103 srcs = (
0104 str(
0105 check_output(
0106 [
0107 "find",
0108 args.input,
0109 "-iname",
0110 "*.cpp",
0111 "-or",
0112 "-iname",
0113 "*.hpp",
0114 "-or",
0115 "-iname",
0116 "*.ipp",
0117 ]
0118 ),
0119 "utf-8",
0120 )
0121 .strip()
0122 .split("\n")
0123 )
0124 srcs = filter(lambda p: not p.startswith("./build"), srcs)
0125 else:
0126 srcs = [args.input]
0127
0128 year = int(datetime.now().strftime("%Y"))
0129
0130 raw = """// This file is part of the Acts project.
0131 //
0132 // Copyright (C) {year} CERN for the benefit of the Acts project
0133 //
0134 // This Source Code Form is subject to the terms of the Mozilla Public
0135 // License, v. 2.0. If a copy of the MPL was not distributed with this
0136 // file, You can obtain one at http://mozilla.org/MPL/2.0/."""
0137
0138 reg = (
0139 r"\A// This file is part of the Acts project.\n"
0140 + r"//\n"
0141 + r"// Copyright \(C\) (?P<year>.*) CERN for the benefit of the Acts project\n"
0142 + r"//\n"
0143 + r"// This Source Code Form is subject to the terms of the Mozilla Public\n"
0144 + r"// License, v\. 2\.0\. If a copy of the MPL was not distributed with this\n"
0145 + r"// file, You can obtain one at http://mozilla.org/MPL/2.0/.\Z"
0146 )
0147
0148 ref = re.compile(reg, re.M)
0149 clean_re = re.compile(r"(\(C\)) (.*) (CERN)", re.M)
0150 year_re = re.compile(r"^(?P<year1>20\d{2}|(?P<year2>20\d{2})-(?P<year3>20\d{2}))$")
0151 extract_re = re.compile(r"(20\d{2})-?(20\d{2})?")
0152
0153 def clean(s):
0154 return clean_re.sub(r"\1 XXXX \3", s)
0155
0156 def get_clean_lines(s):
0157 return [clean(l) + "\n" for l in s.split("\n")]
0158
0159 def validate_years(year1, year2):
0160 if year1 and year2:
0161 year1 = int(year1)
0162 year2 = int(year2)
0163 if year1 >= year2:
0164 return False
0165 if year1 > year or year2 > year:
0166 return False
0167 else:
0168 theyear = int(year1 if year1 else year2)
0169 if theyear > year:
0170 return False
0171 return True
0172
0173 error_summary = ""
0174 info_summary = ""
0175
0176 def eprint(*args):
0177 nonlocal error_summary
0178 error_summary += " ".join(map(str, args)) + "\n"
0179
0180 def year_print(*pargs):
0181 nonlocal error_summary
0182 nonlocal info_summary
0183 string = " ".join(map(str, pargs)) + "\n"
0184 if args.fail_year_mismatch:
0185 error_summary += string
0186 else:
0187 info_summary += string
0188
0189 exit = 0
0190 srcs = list(srcs)
0191 nsrcs = len(srcs)
0192 step = int(nsrcs / 20)
0193 for i, src in enumerate(srcs):
0194 if any([fnmatch(src, e) for e in args.exclude]):
0195 continue
0196
0197 if nsrcs > 1 and i % step == 0:
0198 string = "{}/{} -> {:.2f}%".format(i, nsrcs, i / float(nsrcs) * 100.0)
0199 if sys.stdout.isatty():
0200 sys.stdout.write(string + "\r")
0201 else:
0202 print(string)
0203
0204 with open(src, "r+") as f:
0205 license = ""
0206 for x in range(len(raw)):
0207 line = f.readline()
0208 if not line.startswith("//"):
0209 break
0210 license += line
0211 license = ("".join(license)).strip()
0212 m = ref.search(license)
0213
0214 if m == None:
0215 eprint("Invalid / missing license in " + src + "")
0216
0217 exp = [l + "\n" for l in raw.format(year="XXXX").split("\n")]
0218 act = get_clean_lines(license)
0219
0220 diff = difflib.unified_diff(exp, act)
0221 eprint("".join(diff))
0222 eprint()
0223
0224 if args.fix:
0225 eprint("-> fixing file (prepend)")
0226 f.seek(0)
0227 file_content = f.read()
0228 f.seek(0)
0229 stmnt = raw.format(year=year)
0230 f.write(stmnt + "\n\n")
0231 f.write(file_content)
0232
0233 exit = 1
0234 else:
0235
0236
0237 if args.check_years:
0238 git_add_commit, git_mod_commit = check_git_dates(src)
0239 git_add_year = git_add_commit.year
0240 git_mod_year = git_mod_commit.year
0241 year_act = m.group("year")
0242 ym = year_re.match(year_act)
0243 valid = True
0244 if not ym:
0245 eprint("Year string does not match format in {}".format(src))
0246 eprint("Expected: YYYY or YYYY-YYYY (year or year range)")
0247 eprint("Actual: {}\n".format(year_act))
0248
0249 if args.fix:
0250 extract = extract_re.search(year_act)
0251 year1 = extract.group(1)
0252 year2 = extract.group(2)
0253
0254 exit = 1
0255 valid = False
0256
0257 else:
0258 extract = extract_re.search(year_act)
0259 year1 = extract.group(1)
0260 year2 = extract.group(2)
0261
0262 if not validate_years(year1, year2):
0263 eprint("Year string is not valid in {}".format(src))
0264 eprint("Year string is: " + year_act + "\n")
0265 exit = 1
0266 valid = False
0267
0268 if args.check_years:
0269 if git_add_year != git_mod_year:
0270
0271 if not (year1 and year2):
0272 year_print("File: {}".format(src))
0273
0274
0275 year_print(
0276 "- File was added in {}".format(git_add_year)
0277 )
0278 year_print(
0279 "- File was modified on {} by {}:\n{}".format(
0280 git_mod_commit.date,
0281 git_mod_commit.author,
0282 git_mod_commit.subject + git_mod_commit.body,
0283 )
0284 )
0285 year_print(
0286 "=> License should say {}-{}".format(
0287 git_add_year, git_mod_year
0288 )
0289 )
0290
0291 act_year = year1 if year1 else year2
0292 year_print(
0293 err(
0294 "{} But says: {}".format(CROSS_SYMBOL, act_year)
0295 )
0296 )
0297
0298 if args.fail_year_mismatch:
0299 exit = 1
0300 year_print("\n")
0301 else:
0302 year_print("This is not treated as an error\n")
0303 valid = False
0304 else:
0305 if (
0306 int(year1) != git_add_year
0307 or int(year2) != git_mod_year
0308 ):
0309 year_print("File: {}".format(src))
0310 year_print(
0311 "Year range {}-{} does not match range from git {}-{}".format(
0312 year1, year2, git_add_year, git_mod_year
0313 )
0314 )
0315 year_print(
0316 "- File was added in {}".format(git_add_year)
0317 )
0318 year_print(
0319 "- File was modified on {} by {}:\n{}".format(
0320 git_mod_commit.date,
0321 git_mod_commit.author,
0322 git_mod_commit.subject
0323 + git_mod_commit.body,
0324 )
0325 )
0326 year_print(
0327 "=> License should say {}-{}".format(
0328 git_add_year, git_mod_year
0329 )
0330 )
0331 year_print(
0332 err(
0333 "{} But says: {}-{}".format(
0334 CROSS_SYMBOL, year1, year2
0335 )
0336 )
0337 )
0338 if args.fail_year_mismatch:
0339 exit = 1
0340 year_print("\n")
0341 else:
0342 year_print("This is not treated as an error\n")
0343 valid = False
0344
0345 else:
0346 if int(year1) < git_mod_year:
0347 year_print("File: {}".format(src))
0348 year_print(
0349 "- Year {} does not match git modification year {}".format(
0350 year1, git_mod_year
0351 )
0352 )
0353 year_print(
0354 "- File was modified on {} by {}:\n{}".format(
0355 git_mod_commit.date,
0356 git_mod_commit.author,
0357 git_mod_commit.subject + git_mod_commit.body,
0358 )
0359 )
0360 year_print(
0361 "=> License should say {}".format(git_mod_year)
0362 )
0363 year_print(
0364 err("{} But says: {}".format(CROSS_SYMBOL, year1))
0365 )
0366 if args.fail_year_mismatch:
0367 exit = 1
0368 year_print("\n")
0369 else:
0370 year_print("This is not treated as an error\n")
0371 valid = False
0372
0373 if args.fix and not valid:
0374 eprint("-> fixing file (patch year)")
0375 year_str = "2016-{}".format(year)
0376 if args.check_years:
0377 if git_add_year == git_mod_year:
0378 year_str = "{}".format(git_add_year)
0379 else:
0380 year_str = "{}-{}".format(git_add_year, git_mod_year)
0381 new_license = raw.format(year=year_str)
0382
0383
0384 if args.check_years:
0385 old_license_len = len(license)
0386 f.seek(old_license_len)
0387 file_body = f.read()
0388 f.seek(0)
0389 f.truncate()
0390
0391 f.seek(0)
0392 f.write(new_license)
0393
0394 if args.check_years:
0395 f.write(file_body)
0396
0397 print("\n--- INFO ---\n")
0398 print(info_summary)
0399 print("\n--- ERROR ---\n")
0400 print(error_summary)
0401
0402 if exit != 0 and not args.fix:
0403 print("License problems found. You can try running again with --fix")
0404
0405 sys.exit(exit)
0406
0407
0408 if "__main__" == __name__:
0409 main()