blob: 4a991dc9cbd56b5824d94fdaabe7d516fbf79951 [file] [log] [blame]
#!/usr/bin/env python3
import os
import pprint
import re
import sys
import textwrap
from collections import defaultdict
def k(s):
if s is True:
s = ''
return s
def srepr(s):
r = repr(s)
if len(r) > 100:
r = r[:100] + '...'
return r
IGNORE_EXT = [
'magic.lef',
'netlist.tsv',
'svg',
]
def files_by_extension(dname):
files = set()
ext2base = defaultdict(set)
ext2file = defaultdict(set)
for fname in os.listdir(dname):
fpath = os.path.join(dname, fname)
if not os.path.isfile(fpath):
continue
files.add(fname)
assert '.' in fname, (fname, dname)
base, ext = fname.split('.', 1)
lib, base = base.split('__', 1)
assert lib.startswith('sky130_'), lib
if '__' in base:
cellname, base = base.split('__', 1)
else:
base = ''
if ext in IGNORE_EXT:
continue
ext2base[ext].add(base)
ext2file[ext].add(fname)
oext2base = {}
for k in ext2base:
oext2base[k] = list(sorted(ext2base[k]))
oext2file = {}
for k in ext2file:
oext2file[k] = list(sorted(ext2file[k]))
ofiles = [os.path.abspath(os.path.join(dname, f)) for f in sorted(files)]
return oext2base, oext2file, ofiles
RE_SUBCKT = re.compile('^.subckt +(?P<subckt>[^ ]+) (?P<ports>.*)$', flags=re.M)
RE_INCLUDE = re.compile('^.inc((lude +["\'](?P<f>[^"\']+)["\'] *)|(?P<broken>.*))$', flags=re.M)
EXTRA_SUBCKTS = [
'base',
'subcell',
]
ERROR_HEADERS = {
'gds-missing' : 'No GDS file found for:',
'gds-example-only' : 'Only example GDS files found for:',
'spice-model-missing' : 'No .model.spice or .pm3.spice file found for:',
'spice-model-multiple' : 'Found both a .model.spice and a .pm3.spice file for:',
'spice-bins-missing' : 'Found a .pm3.spice file but no bins.csv file for:',
'spice-include-missing' : 'Missing files included in spice file for:',
'spice-include-error' : 'Invalid include statements in file for:',
'spice-subckt-missing' : 'No subckt found in files for:',
'spice-subckt-multiple' : 'Multiple subckts found in files for:',
'spice-subckt-incorrect': 'Incorrect subckt found in files for:',
}
ERROR_STRINGS = {
'gds-missing' : '',
'gds-example-only' : '',
'spice-model-missing' : '',
'spice-model-multiple' : '',
'spice-bins-missing' : '',
'spice-include-missing' : '{e}',
'spice-include-error' : '{e}',
'spice-subckt-missing' : '{file}: No subckt',
'spice-subckt-incorrect': '{file}: Incorrect subckt value for {t} - got: {g!r} (from {s!r}), wanted: {w!r} (from {f!r})',
}
ERROR_TYPES = set(ERROR_HEADERS.keys())
def emsg(etype, ekw, inctype=True, indent=''):
m = ERROR_STRINGS[etype].format(**ekw)
if inctype:
if m:
m = etype+': '+m
return ('\n'+indent+' '*(len(etype)+2)).join(m.split('\n'))
else:
return etype
return ('\n'+indent).join(m.split('\n'))
def flatten(f):
fdir = os.path.dirname(f)
data = open(f).read()
odata = []
oend = 0
for m in RE_INCLUDE.finditer(data):
odata.append(data[oend:m.start(0)])
incf = m.group('f')
if not incf:
raise IOError('invalid-include: {!r} in {!r}'.format(m.group(0), f))
incpath = os.path.abspath(os.path.join(fdir, incf))
if not os.path.isfile(incpath):
raise FileNotFoundError('invalid-include: {!r} in {!r}'.format(incpath, f))
try:
odata.append(flatten(incpath))
except IOError as e:
raise e.__class__(str(e) + '\n(included in {!r} byte:{})'.format(f, m.start(0)))
oend = m.end(0)
odata.append(data[oend:])
odata = ''.join(odata)
m = RE_INCLUDE.search(odata)
assert not m, m
return odata
def audit(ext2base, ext2file, files):
errors = []
warnings = []
def add_error(etype, **kw):
assert etype in ERROR_TYPES, (etype, kw)
ERROR_STRINGS[etype].format(**kw)
errors.append((etype, kw))
def add_warning(etype, **kw):
assert etype in ERROR_TYPES, (etype, kw)
ERROR_STRINGS[etype].format(**kw)
warnings.append((etype, kw))
if 'gds' not in ext2base:
add_error('gds-missing')
elif '' not in ext2base['gds']:
add_error('gds-example-only')
if 'model.spice' not in ext2base and 'pm3.spice' not in ext2base:
add_error('spice-model-missing')
if 'model.spice' in ext2base and 'pm3.spice' in ext2base:
if '' in ext2base['model.spice'] and '' in ext2base['pm3.spice']:
add_error('spice-model-multiple')
if 'pm3.spice' in ext2base and 'bins.csv' not in ext2base:
add_error('spice-bins-missing')
for fpath in files:
if not fpath.endswith('.spice'):
continue
fname = fpath.rsplit('/', 1)[-1]
fbase, ext = fname.split('.', 1)
flib, fcell = fbase.split('__', 1)
fextra = None
if '__' in fcell:
fcell, fextra = fcell.split('__', 1)
try:
data = flatten(fpath)
except FileNotFoundError as e:
add_error('spice-include-missing', e=str(e))
continue
except IOError as e:
add_error('spice-include-error', e=str(e))
continue
found_subckts = [m.group('subckt') for m in RE_SUBCKT.finditer(data)]
if not found_subckts:
if 'mismatch' in fpath:
continue
add_error('spice-subckt-missing', file=fname)
continue
for subckt in found_subckts:
if '__' not in subckt and fbase != subckt:
add_error(
'spice-subckt-incorrect',
file=fpath,
t='full', w=fbase, g=subckt,
f='', s='')
continue
subckt_lib, subckt_cell = subckt.split('__', 1)
subckt_extra = None
if '__' in subckt_cell:
subckt_cell, subckt_extra = subckt_cell.split('__', 1)
if flib != subckt_lib:
add_error(
'spice-subckt-incorrect',
file=fpath,
t='lib', w=flib, g=subckt_lib,
f=fbase, s=subckt)
if subckt_cell != fcell:
if subckt_cell.startswith(fcell):
subckt_variant = subckt_cell[len(fcell)+1:]
if subckt_variant.startswith('_'):
subckt_variant = subckt_variant[1:]
if '_' in subckt_variant:
add_error(
'spice-subckt-incorrect',
file=fpath,
t='variant', w='no _', g=subckt_variant,
f=fbase, s=subckt)
else:
add_warning(
'spice-subckt-incorrect',
file=fpath,
t='variant', w='seperate file', g=subckt_variant,
f=fbase, s=subckt)
else:
add_error(
'spice-subckt-incorrect',
file=fpath,
t='cell', w=fcell, g=subckt_cell,
f=fbase, s=subckt)
if subckt_extra in EXTRA_SUBCKTS:
continue
elif fextra is not None and subckt_extra is not None:
add_error(
'spice-subckt-incorrect',
file=fpath,
t='extra', w=fextra, g=subckt_extra,
f=fbase, s=subckt)
return errors, warnings
GOOD = 0
ERROR = 1
def main(argv):
data = defaultdict(dict)
libs = []
for a in argv:
assert os.path.exists(a), a
assert os.path.isdir(a), a
apath = os.path.abspath(a)
print()
print(a)
print('='*75)
allgood = []
witherrors = {}
libs.append((apath, (allgood, witherrors)))
for dname in sorted(os.listdir(apath)):
dpath = os.path.join(a, dname)
if not os.path.isdir(dpath):
print('Not directory', dpath)
continue
ext2base, ext2file, files = files_by_extension(dpath)
data[dname] = ext2base
errors, warnings = audit(ext2base, ext2file, files)
if not errors:
print()
print(dname, 'is all good')
allgood.append(dname)
continue
else:
witherrors[dname] = errors
print()
print(dname)
print(textwrap.indent(pprint.pformat(files), ' '))
print()
if errors:
print('Errors:')
for etype, ekw in errors:
print(' *', emsg(etype, ekw, indent=' '))
print()
if warnings:
print('Warnings:')
for etype, ekw in warnings:
print(' *', emsg(etype, ekw, indent=' '))
continue
print('-'*10)
for k, v in sorted(ext2base.items()):
base_found = '' in v
examples = [i[len('example_'):] for i in v if i.startswith('example_')]
nonexamples = [i for i in v if i and not i.startswith('example_')]
print(' *', '%15s,' % k, ['Not Found', ' Found'][base_found], end='')
if examples:
print(', Examples:', srepr(examples), end='')
if nonexamples:
print(', Other:', srepr(nonexamples), end='')
print()
print('-'*10)
#pprint.pprint(ext2file)
#print('-'*10)
#pprint.pprint(ext2base)
numfiles = len(allgood)+len(witherrors)
def g(f):
if len(libs) < 2:
return ''
if f in libs[-2][1][GOOD]:
return '(still good)'
elif f in libs[-2][1][ERROR]:
return '(previously broken)'
else:
return '(new)'
def e(f):
if len(libs) < 2:
return ''
if f in libs[-2][1][ERROR]:
return '(still broken)'
elif f in libs[-2][1][GOOD]:
return '(previously good)'
else:
return '(new)'
def p(l):
return '(%02.f%%)' % (len(l)/numfiles*100)
if len(libs) < 2:
all_previous = []
else:
all_previous = set(list(libs[-2][1][ERROR])+list(libs[-2][1][GOOD]))
print()
print()
print()
print("Summary for:", a)
print('='*75)
print('All good', len(allgood), 'cells', p(allgood))
print('-'*20)
for f in allgood:
print(' ', '%20s' % g(f), f)
if f in all_previous:
all_previous.remove(f)
print('-'*20)
print()
print('With errors', len(witherrors), 'cells', p(witherrors))
byerrors = defaultdict(list)
for f in witherrors:
for etype, ekw in witherrors[f]:
byerrors[etype].append((f,ekw))
if f in all_previous:
all_previous.remove(f)
def k(v):
f, ekw = v
return f, list(ekw.items())
print('-'*20)
for etype, v in byerrors.items():
print()
print(ERROR_HEADERS[etype], len(v), p(v))
for f, ekw in sorted(v, key=k):
m = emsg(etype, ekw, inctype=False, indent=' '*(24+len(f)+4))
if m:
m = ' - '+m
m = m.replace(apath, '')
print(' ', '%20s' % e(f), f, m)
print('-'*20)
if all_previous:
print()
print('Removed', len(all_previous), 'cells')
print('-'*20)
for f in sorted(all_previous):
print(' ', '%20s' % '(removed)', f)
print('-'*20)
else:
print('None removed')
print('='*75)
print()
print()
return 0
if __name__ == "__main__":
try:
sys.exit(main(sys.argv[1:]))
except SystemExit as e:
raise
except:
import traceback
traceback.print_exc()
sys.exit(1)