tcl/board/calao-usb-a9260: fix and refactor broken support
[openocd.git] / tools / scripts / spdxcheck.py
1 #!/usr/bin/env python3
2 # SPDX-License-Identifier: GPL-2.0
3 # Copyright Thomas Gleixner <tglx@linutronix.de>
4
5 from argparse import ArgumentParser
6 from ply import lex, yacc
7 import locale
8 import traceback
9 import fnmatch
10 import sys
11 import git
12 import re
13 import os
14
15 class ParserException(Exception):
16 def __init__(self, tok, txt):
17 self.tok = tok
18 self.txt = txt
19
20 class SPDXException(Exception):
21 def __init__(self, el, txt):
22 self.el = el
23 self.txt = txt
24
25 class SPDXdata(object):
26 def __init__(self):
27 self.license_files = 0
28 self.exception_files = 0
29 self.licenses = [ ]
30 self.exceptions = { }
31
32 class dirinfo(object):
33 def __init__(self):
34 self.missing = 0
35 self.total = 0
36 self.files = []
37
38 def update(self, fname, basedir, miss):
39 self.total += 1
40 self.missing += miss
41 if miss:
42 fname = './' + fname
43 bdir = os.path.dirname(fname)
44 if bdir == basedir.rstrip('/'):
45 self.files.append(fname)
46
47 # Read the spdx data from the LICENSES directory
48 def read_spdxdata(repo):
49
50 # The subdirectories of LICENSES in the kernel source
51 # Note: exceptions needs to be parsed as last directory.
52 # OpenOCD specific: Begin
53 license_dirs = [ "preferred", "stand-alone", "exceptions" ]
54 # OpenOCD specific: End
55 lictree = repo.head.commit.tree['LICENSES']
56
57 spdx = SPDXdata()
58
59 for d in license_dirs:
60 for el in lictree[d].traverse():
61 if not os.path.isfile(el.path):
62 continue
63
64 exception = None
65 for l in open(el.path, encoding="utf-8").readlines():
66 if l.startswith('Valid-License-Identifier:'):
67 lid = l.split(':')[1].strip().upper()
68 if lid in spdx.licenses:
69 raise SPDXException(el, 'Duplicate License Identifier: %s' %lid)
70 else:
71 spdx.licenses.append(lid)
72
73 elif l.startswith('SPDX-Exception-Identifier:'):
74 exception = l.split(':')[1].strip().upper()
75 spdx.exceptions[exception] = []
76
77 elif l.startswith('SPDX-Licenses:'):
78 for lic in l.split(':')[1].upper().strip().replace(' ', '').replace('\t', '').split(','):
79 if not lic in spdx.licenses:
80 raise SPDXException(None, 'Exception %s missing license %s' %(exception, lic))
81 spdx.exceptions[exception].append(lic)
82
83 elif l.startswith("License-Text:"):
84 if exception:
85 if not len(spdx.exceptions[exception]):
86 raise SPDXException(el, 'Exception %s is missing SPDX-Licenses' %exception)
87 spdx.exception_files += 1
88 else:
89 spdx.license_files += 1
90 break
91 return spdx
92
93 class id_parser(object):
94
95 reserved = [ 'AND', 'OR', 'WITH' ]
96 tokens = [ 'LPAR', 'RPAR', 'ID', 'EXC' ] + reserved
97
98 precedence = ( ('nonassoc', 'AND', 'OR'), )
99
100 t_ignore = ' \t'
101
102 def __init__(self, spdx):
103 self.spdx = spdx
104 self.lasttok = None
105 self.lastid = None
106 self.lexer = lex.lex(module = self, reflags = re.UNICODE)
107 # Initialize the parser. No debug file and no parser rules stored on disk
108 # The rules are small enough to be generated on the fly
109 self.parser = yacc.yacc(module = self, write_tables = False, debug = False)
110 self.lines_checked = 0
111 self.checked = 0
112 self.excluded = 0
113 self.spdx_valid = 0
114 self.spdx_errors = 0
115 self.spdx_dirs = {}
116 self.dirdepth = -1
117 self.basedir = '.'
118 self.curline = 0
119 self.deepest = 0
120
121 def set_dirinfo(self, basedir, dirdepth):
122 if dirdepth >= 0:
123 self.basedir = basedir
124 bdir = basedir.lstrip('./').rstrip('/')
125 if bdir != '':
126 parts = bdir.split('/')
127 else:
128 parts = []
129 self.dirdepth = dirdepth + len(parts)
130
131 # Validate License and Exception IDs
132 def validate(self, tok):
133 id = tok.value.upper()
134 if tok.type == 'ID':
135 if not id in self.spdx.licenses:
136 raise ParserException(tok, 'Invalid License ID')
137 self.lastid = id
138 elif tok.type == 'EXC':
139 if id not in self.spdx.exceptions:
140 raise ParserException(tok, 'Invalid Exception ID')
141 if self.lastid not in self.spdx.exceptions[id]:
142 raise ParserException(tok, 'Exception not valid for license %s' %self.lastid)
143 self.lastid = None
144 elif tok.type != 'WITH':
145 self.lastid = None
146
147 # Lexer functions
148 def t_RPAR(self, tok):
149 r'\)'
150 self.lasttok = tok.type
151 return tok
152
153 def t_LPAR(self, tok):
154 r'\('
155 self.lasttok = tok.type
156 return tok
157
158 def t_ID(self, tok):
159 r'[A-Za-z.0-9\-+]+'
160
161 if self.lasttok == 'EXC':
162 print(tok)
163 raise ParserException(tok, 'Missing parentheses')
164
165 tok.value = tok.value.strip()
166 val = tok.value.upper()
167
168 if val in self.reserved:
169 tok.type = val
170 elif self.lasttok == 'WITH':
171 tok.type = 'EXC'
172
173 self.lasttok = tok.type
174 self.validate(tok)
175 return tok
176
177 def t_error(self, tok):
178 raise ParserException(tok, 'Invalid token')
179
180 def p_expr(self, p):
181 '''expr : ID
182 | ID WITH EXC
183 | expr AND expr
184 | expr OR expr
185 | LPAR expr RPAR'''
186 pass
187
188 def p_error(self, p):
189 if not p:
190 raise ParserException(None, 'Unfinished license expression')
191 else:
192 raise ParserException(p, 'Syntax error')
193
194 def parse(self, expr):
195 self.lasttok = None
196 self.lastid = None
197 self.parser.parse(expr, lexer = self.lexer)
198
199 def parse_lines(self, fd, maxlines, fname):
200 self.checked += 1
201 self.curline = 0
202 fail = 1
203 try:
204 for line in fd:
205 line = line.decode(locale.getpreferredencoding(False), errors='ignore')
206 self.curline += 1
207 if self.curline > maxlines:
208 break
209 self.lines_checked += 1
210 if line.find("SPDX-License-Identifier:") < 0:
211 continue
212 expr = line.split(':')[1].strip()
213 # Remove trailing comment closure
214 if line.strip().endswith('*/'):
215 expr = expr.rstrip('*/').strip()
216 # Remove trailing xml comment closure
217 if line.strip().endswith('-->'):
218 expr = expr.rstrip('-->').strip()
219 # Special case for SH magic boot code files
220 if line.startswith('LIST \"'):
221 expr = expr.rstrip('\"').strip()
222 self.parse(expr)
223 self.spdx_valid += 1
224 #
225 # Should we check for more SPDX ids in the same file and
226 # complain if there are any?
227 #
228 fail = 0
229 break
230
231 except ParserException as pe:
232 if pe.tok:
233 col = line.find(expr) + pe.tok.lexpos
234 tok = pe.tok.value
235 sys.stdout.write('%s: %d:%d %s: %s\n' %(fname, self.curline, col, pe.txt, tok))
236 else:
237 sys.stdout.write('%s: %d:0 %s\n' %(fname, self.curline, pe.txt))
238 self.spdx_errors += 1
239
240 if fname == '-':
241 return
242
243 base = os.path.dirname(fname)
244 if self.dirdepth > 0:
245 parts = base.split('/')
246 i = 0
247 base = '.'
248 while i < self.dirdepth and i < len(parts) and len(parts[i]):
249 base += '/' + parts[i]
250 i += 1
251 elif self.dirdepth == 0:
252 base = self.basedir
253 else:
254 base = './' + base.rstrip('/')
255 base += '/'
256
257 di = self.spdx_dirs.get(base, dirinfo())
258 di.update(fname, base, fail)
259 self.spdx_dirs[base] = di
260
261 class pattern(object):
262 def __init__(self, line):
263 self.pattern = line
264 self.match = self.match_file
265 if line == '.*':
266 self.match = self.match_dot
267 elif line.endswith('/'):
268 self.pattern = line[:-1]
269 self.match = self.match_dir
270 elif line.startswith('/'):
271 self.pattern = line[1:]
272 self.match = self.match_fn
273
274 def match_dot(self, fpath):
275 return os.path.basename(fpath).startswith('.')
276
277 def match_file(self, fpath):
278 return os.path.basename(fpath) == self.pattern
279
280 def match_fn(self, fpath):
281 return fnmatch.fnmatchcase(fpath, self.pattern)
282
283 def match_dir(self, fpath):
284 if self.match_fn(os.path.dirname(fpath)):
285 return True
286 return fpath.startswith(self.pattern)
287
288 def exclude_file(fpath):
289 for rule in exclude_rules:
290 if rule.match(fpath):
291 return True
292 return False
293
294 def scan_git_tree(tree, basedir, dirdepth):
295 parser.set_dirinfo(basedir, dirdepth)
296 for el in tree.traverse():
297 if not os.path.isfile(el.path):
298 continue
299 if exclude_file(el.path):
300 parser.excluded += 1
301 continue
302 with open(el.path, 'rb') as fd:
303 parser.parse_lines(fd, args.maxlines, el.path)
304
305 def scan_git_subtree(tree, path, dirdepth):
306 for p in path.strip('/').split('/'):
307 tree = tree[p]
308 scan_git_tree(tree, path.strip('/'), dirdepth)
309
310 def read_exclude_file(fname):
311 rules = []
312 if not fname:
313 return rules
314 with open(fname) as fd:
315 for line in fd:
316 line = line.strip()
317 if line.startswith('#'):
318 continue
319 if not len(line):
320 continue
321 rules.append(pattern(line))
322 return rules
323
324 if __name__ == '__main__':
325
326 ap = ArgumentParser(description='SPDX expression checker')
327 ap.add_argument('path', nargs='*', help='Check path or file. If not given full git tree scan. For stdin use "-"')
328 ap.add_argument('-d', '--dirs', action='store_true',
329 help='Show [sub]directory statistics.')
330 ap.add_argument('-D', '--depth', type=int, default=-1,
331 help='Directory depth for -d statistics. Default: unlimited')
332 ap.add_argument('-e', '--exclude',
333 help='File containing file patterns to exclude. Default: scripts/spdxexclude')
334 ap.add_argument('-f', '--files', action='store_true',
335 help='Show files without SPDX.')
336 ap.add_argument('-m', '--maxlines', type=int, default=15,
337 help='Maximum number of lines to scan in a file. Default 15')
338 ap.add_argument('-v', '--verbose', action='store_true', help='Verbose statistics output')
339 args = ap.parse_args()
340
341 # Sanity check path arguments
342 if '-' in args.path and len(args.path) > 1:
343 sys.stderr.write('stdin input "-" must be the only path argument\n')
344 sys.exit(1)
345
346 try:
347 # Use git to get the valid license expressions
348 repo = git.Repo(os.getcwd())
349 assert not repo.bare
350
351 # Initialize SPDX data
352 spdx = read_spdxdata(repo)
353
354 # Initialize the parser
355 parser = id_parser(spdx)
356
357 except SPDXException as se:
358 if se.el:
359 sys.stderr.write('%s: %s\n' %(se.el.path, se.txt))
360 else:
361 sys.stderr.write('%s\n' %se.txt)
362 sys.exit(1)
363
364 except Exception as ex:
365 sys.stderr.write('FAIL: %s\n' %ex)
366 sys.stderr.write('%s\n' %traceback.format_exc())
367 sys.exit(1)
368
369 try:
370 fname = args.exclude
371 if not fname:
372 fname = os.path.join(os.path.dirname(__file__), 'spdxexclude')
373 exclude_rules = read_exclude_file(fname)
374 except Exception as ex:
375 sys.stderr.write('FAIL: Reading exclude file %s: %s\n' %(fname, ex))
376 sys.exit(1)
377
378 try:
379 if len(args.path) and args.path[0] == '-':
380 stdin = os.fdopen(sys.stdin.fileno(), 'rb')
381 parser.parse_lines(stdin, args.maxlines, '-')
382 else:
383 if args.path:
384 for p in args.path:
385 if os.path.isfile(p):
386 parser.parse_lines(open(p, 'rb'), args.maxlines, p)
387 elif os.path.isdir(p):
388 scan_git_subtree(repo.head.reference.commit.tree, p,
389 args.depth)
390 else:
391 sys.stderr.write('path %s does not exist\n' %p)
392 sys.exit(1)
393 else:
394 # Full git tree scan
395 scan_git_tree(repo.head.commit.tree, '.', args.depth)
396
397 ndirs = len(parser.spdx_dirs)
398 dirsok = 0
399 if ndirs:
400 for di in parser.spdx_dirs.values():
401 if not di.missing:
402 dirsok += 1
403
404 if args.verbose:
405 sys.stderr.write('\n')
406 sys.stderr.write('License files: %12d\n' %spdx.license_files)
407 sys.stderr.write('Exception files: %12d\n' %spdx.exception_files)
408 sys.stderr.write('License IDs %12d\n' %len(spdx.licenses))
409 sys.stderr.write('Exception IDs %12d\n' %len(spdx.exceptions))
410 sys.stderr.write('\n')
411 sys.stderr.write('Files excluded: %12d\n' %parser.excluded)
412 sys.stderr.write('Files checked: %12d\n' %parser.checked)
413 sys.stderr.write('Lines checked: %12d\n' %parser.lines_checked)
414 if parser.checked:
415 pc = int(100 * parser.spdx_valid / parser.checked)
416 sys.stderr.write('Files with SPDX: %12d %3d%%\n' %(parser.spdx_valid, pc))
417 sys.stderr.write('Files with errors: %12d\n' %parser.spdx_errors)
418 if ndirs:
419 sys.stderr.write('\n')
420 sys.stderr.write('Directories accounted: %8d\n' %ndirs)
421 pc = int(100 * dirsok / ndirs)
422 sys.stderr.write('Directories complete: %8d %3d%%\n' %(dirsok, pc))
423
424 if ndirs and ndirs != dirsok and args.dirs:
425 if args.verbose:
426 sys.stderr.write('\n')
427 sys.stderr.write('Incomplete directories: SPDX in Files\n')
428 for f in sorted(parser.spdx_dirs.keys()):
429 di = parser.spdx_dirs[f]
430 if di.missing:
431 valid = di.total - di.missing
432 pc = int(100 * valid / di.total)
433 sys.stderr.write(' %-80s: %5d of %5d %3d%%\n' %(f, valid, di.total, pc))
434
435 if ndirs and ndirs != dirsok and args.files:
436 if args.verbose or args.dirs:
437 sys.stderr.write('\n')
438 sys.stderr.write('Files without SPDX:\n')
439 for f in sorted(parser.spdx_dirs.keys()):
440 di = parser.spdx_dirs[f]
441 for f in sorted(di.files):
442 sys.stderr.write(' %s\n' %f)
443
444 sys.exit(0)
445
446 except Exception as ex:
447 sys.stderr.write('FAIL: %s\n' %ex)
448 sys.stderr.write('%s\n' %traceback.format_exc())
449 sys.exit(1)

Linking to existing account procedure

If you already have an account and want to add another login method you MUST first sign in with your existing account and then change URL to read https://review.openocd.org/login/?link to get to this page again but this time it'll work for linking. Thank you.

SSH host keys fingerprints

1024 SHA256:YKx8b7u5ZWdcbp7/4AeXNaqElP49m6QrwfXaqQGJAOk gerrit-code-review@openocd.zylin.com (DSA)
384 SHA256:jHIbSQa4REvwCFG4cq5LBlBLxmxSqelQPem/EXIrxjk gerrit-code-review@openocd.org (ECDSA)
521 SHA256:UAOPYkU9Fjtcao0Ul/Rrlnj/OsQvt+pgdYSZ4jOYdgs gerrit-code-review@openocd.org (ECDSA)
256 SHA256:A13M5QlnozFOvTllybRZH6vm7iSt0XLxbA48yfc2yfY gerrit-code-review@openocd.org (ECDSA)
256 SHA256:spYMBqEYoAOtK7yZBrcwE8ZpYt6b68Cfh9yEVetvbXg gerrit-code-review@openocd.org (ED25519)
+--[ED25519 256]--+
|=..              |
|+o..   .         |
|*.o   . .        |
|+B . . .         |
|Bo. = o S        |
|Oo.+ + =         |
|oB=.* = . o      |
| =+=.+   + E     |
|. .=o   . o      |
+----[SHA256]-----+
2048 SHA256:0Onrb7/PHjpo6iVZ7xQX2riKN83FJ3KGU0TvI0TaFG4 gerrit-code-review@openocd.zylin.com (RSA)