ECCE @ EIC Software
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
check_license.py
Go to the documentation of this file. Or view the newest version in sPHENIX GitHub for file check_license.py
1 #!/usr/bin/env python3
2 import argparse
3 import os
4 import sys
5 from subprocess import check_output
6 import re
7 import difflib
8 from datetime import datetime
9 from fnmatch import fnmatch
10 
11 EXCLUDE = []
12 
13 
14 class bcolors:
15  HEADER = "\033[95m"
16  OKBLUE = "\033[94m"
17  OKGREEN = "\033[92m"
18  WARNING = "\033[93m"
19  FAIL = "\033[91m"
20  ENDC = "\033[0m"
21  BOLD = "\033[1m"
22  UNDERLINE = "\033[4m"
23 
24 
25 CROSS_SYMBOL = u"\u2717"
26 
27 
28 def err(string):
29  if sys.stdout.isatty():
30  return bcolors.FAIL + bcolors.BOLD + string + bcolors.ENDC
31  else:
32  return string
33 
34 
35 class CommitInfo:
36  date = None
37  year = None
38  author = None
39  subject = None
40  body = None
41 
42 
43 def check_git_dates(src):
44  output = (
45  check_output(["git", "log", "--format={{{%an|%ad|%s|%b}}}", "--", src])
46  .decode("utf-8")
47  .strip()
48  )
49 
50  # find single outputs
51  commits = re.findall(r"{{{((?:.|\n)*?)}}}", output)
52  commits = [c for c in commits if "[ignore-license]" not in c]
53  commits = [c.split("|") for c in commits]
54 
55  # print(output)
56  mod = commits[0]
57  add = commits[-1]
58 
59  madd = re.match(r".*\d{2}:\d{2}:\d{2} (\d{4})", add[1])
60  assert madd != None, "Regex did not match git log output"
61  mmod = re.match(r".*\d{2}:\d{2}:\d{2} (\d{4})", mod[1])
62  assert mmod != None, "Regex did not match git log output"
63 
64  addcommit = CommitInfo()
65  addcommit.date = add[1]
66  addcommit.year = int(madd.group(1))
67  addcommit.author = add[0]
68  addcommit.subject = add[2]
69  addcommit.body = add[3]
70 
71  modcommit = CommitInfo()
72  modcommit.date = mod[1]
73  modcommit.year = int(mmod.group(1))
74  modcommit.author = mod[0]
75  modcommit.subject = mod[2]
76  modcommit.body = mod[3]
77 
78  return addcommit, modcommit
79 
80 
81 def main():
82  p = argparse.ArgumentParser()
83  p.add_argument("input")
84  p.add_argument(
85  "--fix", action="store_true", help="Attempt to fix any license issues found."
86  )
87  p.add_argument(
88  "--check-years",
89  action="store_true",
90  help="Check the license year info using git info for each file.",
91  )
92  p.add_argument(
93  "--fail-year-mismatch",
94  action="store_true",
95  help="Fail if year in license statement is not valid.",
96  )
97  p.add_argument("--exclude", "-e", action="append", default=EXCLUDE)
98 
99  args = p.parse_args()
100  print(args.exclude)
101 
102  if os.path.isdir(args.input):
103  srcs = (
104  str(
105  check_output(
106  [
107  "find",
108  args.input,
109  "-iname",
110  "*.cpp",
111  "-or",
112  "-iname",
113  "*.hpp",
114  "-or",
115  "-iname",
116  "*.ipp",
117  ]
118  ),
119  "utf-8",
120  )
121  .strip()
122  .split("\n")
123  )
124  srcs = filter(lambda p: not p.startswith("./build"), srcs)
125  else:
126  srcs = [args.input]
127 
128  year = int(datetime.now().strftime("%Y"))
129 
130  raw = """// This file is part of the Acts project.
131 //
132 // Copyright (C) {year} CERN for the benefit of the Acts project
133 //
134 // This Source Code Form is subject to the terms of the Mozilla Public
135 // License, v. 2.0. If a copy of the MPL was not distributed with this
136 // file, You can obtain one at http://mozilla.org/MPL/2.0/."""
137 
138  reg = (
139  r"\A// This file is part of the Acts project.\n"
140  + r"//\n"
141  + r"// Copyright \(C\) (?P<year>.*) CERN for the benefit of the Acts project\n"
142  + r"//\n"
143  + r"// This Source Code Form is subject to the terms of the Mozilla Public\n"
144  + r"// License, v\. 2\.0\. If a copy of the MPL was not distributed with this\n"
145  + r"// file, You can obtain one at http://mozilla.org/MPL/2.0/.\Z"
146  )
147 
148  ref = re.compile(reg, re.M)
149  clean_re = re.compile(r"(\(C\)) (.*) (Acts)", re.M)
150  year_re = re.compile(r"^(?P<year1>20\d{2}|(?P<year2>20\d{2})-(?P<year3>20\d{2}))$")
151  extract_re = re.compile(r"(20\d{2})-?(20\d{2})?")
152 
153  def clean(s):
154  return clean_re.sub(r"\1 XXXX \3", s)
155 
156  def get_clean_lines(s):
157  return [clean(l) + "\n" for l in s.split("\n")]
158 
159  def validate_years(year1, year2):
160  if year1 and year2:
161  year1 = int(year1)
162  year2 = int(year2)
163  if year1 >= year2:
164  return False
165  if year1 > year or year2 > year:
166  return False
167  else:
168  theyear = int(year1 if year1 else year2)
169  if theyear > year:
170  return False
171  return True
172 
173  error_summary = ""
174  info_summary = ""
175 
176  def eprint(*args):
177  nonlocal error_summary
178  error_summary += " ".join(map(str, args)) + "\n"
179 
180  def year_print(*pargs):
181  nonlocal error_summary
182  nonlocal info_summary
183  string = " ".join(map(str, pargs)) + "\n"
184  if args.fail_year_mismatch:
185  error_summary += string
186  else:
187  info_summary += string
188 
189  exit = 0
190  srcs = list(srcs)
191  nsrcs = len(srcs)
192  step = int(nsrcs / 20)
193  for i, src in enumerate(srcs):
194 
195  if any([fnmatch(src, e) for e in args.exclude]):
196  continue
197 
198  if nsrcs > 1 and i % step == 0:
199  string = "{}/{} -> {:.2f}%".format(i, nsrcs, i / float(nsrcs) * 100.0)
200  if sys.stdout.isatty():
201  sys.stdout.write(string + "\r")
202  else:
203  print(string)
204 
205  with open(src, "r+") as f:
206  license = ""
207  for x in range(len(raw)):
208  line = f.readline()
209  if not line.startswith("//"):
210  break
211  license += line
212  license = ("".join(license)).strip()
213  m = ref.search(license)
214 
215  if m == None:
216  eprint("Invalid / missing license in " + src + "")
217 
218  exp = [l + "\n" for l in raw.format(year="XXXX").split("\n")]
219  act = get_clean_lines(license)
220 
221  diff = difflib.unified_diff(exp, act)
222  eprint("".join(diff))
223  eprint()
224 
225  if args.fix:
226  eprint("-> fixing file (prepend)")
227  f.seek(0)
228  file_content = f.read()
229  f.seek(0)
230  stmnt = raw.format(year=year)
231  f.write(stmnt + "\n\n")
232  f.write(file_content)
233 
234  exit = 1
235  else:
236  # we have a match, need to verify year string is right
237 
238  if args.check_years:
239  git_add_commit, git_mod_commit = check_git_dates(src)
240  git_add_year = git_add_commit.year
241  git_mod_year = git_mod_commit.year
242  year_act = m.group("year")
243  ym = year_re.match(year_act)
244  valid = True
245  if not ym:
246  eprint("Year string does not match format in {}".format(src))
247  eprint("Expected: YYYY or YYYY-YYYY (year or year range)")
248  eprint("Actual: {}\n".format(year_act))
249 
250  if args.fix:
251  extract = extract_re.search(year_act)
252  year1 = extract.group(1)
253  year2 = extract.group(2)
254 
255  exit = 1
256  valid = False
257 
258  else:
259  extract = extract_re.search(year_act)
260  year1 = extract.group(1)
261  year2 = extract.group(2)
262 
263  if not validate_years(year1, year2):
264  eprint("Year string is not valid in {}".format(src))
265  eprint("Year string is: " + year_act + "\n")
266  exit = 1
267  valid = False
268 
269  if args.check_years:
270 
271  if git_add_year != git_mod_year:
272  # need year range in licence
273  if not (year1 and year2):
274  year_print("File: {}".format(src))
275  # year_print("o File was modified in a different year ({}) than it was added ({})."
276  # .format(git_mod_year, git_add_year))
277  year_print(
278  "- File was added in {}".format(git_add_year)
279  )
280  year_print(
281  "- File was modified on {} by {}:\n{}".format(
282  git_mod_commit.date,
283  git_mod_commit.author,
284  git_mod_commit.subject + git_mod_commit.body,
285  )
286  )
287  year_print(
288  "=> License should say {}-{}".format(
289  git_add_year, git_mod_year
290  )
291  )
292 
293  act_year = year1 if year1 else year2
294  year_print(
295  err(
296  "{} But says: {}".format(CROSS_SYMBOL, act_year)
297  )
298  )
299 
300  if args.fail_year_mismatch:
301  exit = 1
302  year_print("\n")
303  else:
304  year_print("This is not treated as an error\n")
305  valid = False
306  else:
307  if (
308  int(year1) != git_add_year
309  or int(year2) != git_mod_year
310  ):
311 
312  year_print("File: {}".format(src))
313  year_print(
314  "Year range {}-{} does not match range from git {}-{}".format(
315  year1, year2, git_add_year, git_mod_year
316  )
317  )
318  year_print(
319  "- File was added in {}".format(git_add_year)
320  )
321  year_print(
322  "- File was modified on {} by {}:\n{}".format(
323  git_mod_commit.date,
324  git_mod_commit.author,
325  git_mod_commit.subject
326  + git_mod_commit.body,
327  )
328  )
329  year_print(
330  "=> License should say {}-{}".format(
331  git_add_year, git_mod_year
332  )
333  )
334  year_print(
335  err(
336  "{} But says: {}-{}".format(
337  CROSS_SYMBOL, year1, year2
338  )
339  )
340  )
341  if args.fail_year_mismatch:
342  exit = 1
343  year_print("\n")
344  else:
345  year_print("This is not treated as an error\n")
346  valid = False
347 
348  else:
349  if int(year1) < git_mod_year:
350  year_print("File: {}".format(src))
351  year_print(
352  "- Year {} does not match git modification year {}".format(
353  year1, git_mod_year
354  )
355  )
356  year_print(
357  "- File was modified on {} by {}:\n{}".format(
358  git_mod_commit.date,
359  git_mod_commit.author,
360  git_mod_commit.subject + git_mod_commit.body,
361  )
362  )
363  year_print(
364  "=> License should say {}".format(git_mod_year)
365  )
366  year_print(
367  err("{} But says: {}".format(CROSS_SYMBOL, year1))
368  )
369  if args.fail_year_mismatch:
370  exit = 1
371  year_print("\n")
372  else:
373  year_print("This is not treated as an error\n")
374  valid = False
375 
376  if args.fix and not valid:
377  eprint("-> fixing file (patch year)")
378  year_str = "2016-{}".format(year)
379  if args.check_years:
380  if git_add_year == git_mod_year:
381  year_str = "{}".format(git_add_year)
382  else:
383  year_str = "{}-{}".format(git_add_year, git_mod_year)
384  new_license = raw.format(year=year_str)
385 
386  # preserve rest of file as is
387  if args.check_years:
388  old_license_len = len(license)
389  f.seek(old_license_len)
390  file_body = f.read()
391  f.seek(0)
392  f.truncate()
393 
394  f.seek(0)
395  f.write(new_license)
396 
397  if args.check_years:
398  f.write(file_body)
399 
400  print("\n--- INFO ---\n")
401  print(info_summary)
402  print("\n--- ERROR ---\n")
403  print(error_summary)
404 
405  if exit != 0 and not args.fix:
406  print("License problems found. You can try running again with --fix")
407 
408  sys.exit(exit)
409 
410 
411 if "__main__" == __name__:
412  main()