from collections import namedtuple import glob import os.path import re import shutil import sys import subprocess VERBOSITY = 2 C_GLOBALS_DIR = os.path.abspath(os.path.dirname(__file__)) TOOLS_DIR = os.path.dirname(C_GLOBALS_DIR) ROOT_DIR = os.path.dirname(TOOLS_DIR) GLOBALS_FILE = os.path.join(C_GLOBALS_DIR, 'ignored-globals.txt') SOURCE_DIRS = ['Include', 'Objects', 'Modules', 'Parser', 'Python'] CAPI_REGEX = re.compile(r'^ *PyAPI_DATA\([^)]*\) \W*(_?Py\w+(?:, \w+)*\w).*;.*$') IGNORED_VARS = { '_DYNAMIC', '_GLOBAL_OFFSET_TABLE_', '__JCR_LIST__', '__JCR_END__', '__TMC_END__', '__bss_start', '__data_start', '__dso_handle', '_edata', '_end', } def find_capi_vars(root): capi_vars = {} for dirname in SOURCE_DIRS: for filename in glob.glob(os.path.join(ROOT_DIR, dirname, '**/*.[hc]'), recursive=True): with open(filename) as file: for name in _find_capi_vars(file): if name in capi_vars: assert not filename.endswith('.c') assert capi_vars[name].endswith('.c') capi_vars[name] = filename return capi_vars def _find_capi_vars(lines): for line in lines: if not line.startswith('PyAPI_DATA'): continue assert '{' not in line match = CAPI_REGEX.match(line) assert match names, = match.groups() for name in names.split(', '): yield name def _read_global_names(filename): # These variables are shared between all interpreters in the process. with open(filename) as file: return {line.partition('#')[0].strip() for line in file if line.strip() and not line.startswith('#')} def _is_global_var(name, globalnames): if _is_autogen_var(name): return True if _is_type_var(name): return True if _is_module(name): return True if _is_exception(name): return True if _is_compiler(name): return True return name in globalnames def _is_autogen_var(name): return ( name.startswith('PyId_') or '.' in name or # Objects/typeobject.c name.startswith('op_id.') or name.startswith('rop_id.') or # Python/graminit.c name.startswith('arcs_') or name.startswith('states_') ) def _is_type_var(name): if name.endswith(('Type', '_Type', '_type')): # XXX Always a static type? return True if name.endswith('_desc'): # for structseq types return True return ( name.startswith('doc_') or name.endswith(('_doc', '__doc__', '_docstring')) or name.endswith('_methods') or name.endswith('_fields') or name.endswith(('_memberlist', '_members')) or name.endswith('_slots') or name.endswith(('_getset', '_getsets', '_getsetlist')) or name.endswith('_as_mapping') or name.endswith('_as_number') or name.endswith('_as_sequence') or name.endswith('_as_buffer') or name.endswith('_as_async') ) def _is_module(name): if name.endswith(('_functions', 'Methods', '_Methods')): return True if name == 'module_def': return True if name == 'initialized': return True return name.endswith(('module', '_Module')) def _is_exception(name): # Other vars are enumerated in globals-core.txt. if not name.startswith(('PyExc_', '_PyExc_')): return False return name.endswith(('Error', 'Warning')) def _is_compiler(name): return ( # Python/Python-ast.c name.endswith('_type') or name.endswith('_singleton') or name.endswith('_attributes') ) class Var(namedtuple('Var', 'name kind scope capi filename')): @classmethod def parse_nm(cls, line, expected, ignored, capi_vars, globalnames): _, _, line = line.partition(' ') # strip off the address line = line.strip() kind, _, line = line.partition(' ') if kind in ignored or (): return None elif kind not in expected or (): raise RuntimeError('unsupported NM type {!r}'.format(kind)) name, _, filename = line.partition('\t') name = name.strip() if _is_autogen_var(name): return None if _is_global_var(name, globalnames): scope = 'global' else: scope = None capi = (name in capi_vars or ()) if filename: filename = os.path.relpath(filename.partition(':')[0]) return cls(name, kind, scope, capi, filename or '~???~') @property def external(self): return self.kind.isupper() def find_vars(root, globals_filename=GLOBALS_FILE): python = os.path.join(root, 'python') if not os.path.exists(python): raise RuntimeError('python binary missing (need to build it first?)') capi_vars = find_capi_vars(root) globalnames = _read_global_names(globals_filename) nm = shutil.which('nm') if nm is None: # XXX Use dumpbin.exe /SYMBOLS on Windows. raise NotImplementedError else: yield from (var for var in _find_var_symbols(python, nm, capi_vars, globalnames) if var.name not in IGNORED_VARS) NM_FUNCS = set('Tt') NM_PUBLIC_VARS = set('BD') NM_PRIVATE_VARS = set('bd') NM_VARS = NM_PUBLIC_VARS | NM_PRIVATE_VARS NM_DATA = set('Rr') NM_OTHER = set('ACGgiINpSsuUVvWw-?') NM_IGNORED = NM_FUNCS | NM_DATA | NM_OTHER def _find_var_symbols(python, nm, capi_vars, globalnames): args = [nm, '--line-numbers', python] out = subprocess.check_output(args) for line in out.decode('utf-8').splitlines(): var = Var.parse_nm(line, NM_VARS, NM_IGNORED, capi_vars, globalnames) if var is None: continue yield var ####################################### class Filter(namedtuple('Filter', 'name op value action')): @classmethod def parse(cls, raw): action = '+' if raw.startswith(('+', '-')): action = raw[0] raw = raw[1:] # XXX Support < and >? name, op, value = raw.partition('=') return cls(name, op, value, action) def check(self, var): value = getattr(var, self.name, None) if not self.op: matched = bool(value) elif self.op == '=': matched = (value == self.value) else: raise NotImplementedError if self.action == '+': return matched elif self.action == '-': return not matched else: raise NotImplementedError def filter_var(var, filters): for filter in filters: if not filter.check(var): return False return True def make_sort_key(spec): columns = [(col.strip('_'), '_' if col.startswith('_') else '') for col in spec] def sort_key(var): return tuple(getattr(var, col).lstrip(prefix) for col, prefix in columns) return sort_key def make_groups(allvars, spec): group = spec groups = {} for var in allvars: value = getattr(var, group) key = '{}: {}'.format(group, value) try: groupvars = groups[key] except KeyError: groupvars = groups[key] = [] groupvars.append(var) return groups def format_groups(groups, columns, fmts, widths): for group in sorted(groups): groupvars = groups[group] yield '', 0 yield ' # {}'.format(group), 0 yield from format_vars(groupvars, columns, fmts, widths) def format_vars(allvars, columns, fmts, widths): fmt = ' '.join(fmts[col] for col in columns) fmt = ' ' + fmt.replace(' ', ' ') + ' ' # for div margin header = fmt.replace(':', ':^').format(*(col.upper() for col in columns)) yield header, 0 div = ' '.join('-'*(widths[col]+2) for col in columns) yield div, 0 for var in allvars: values = (getattr(var, col) for col in columns) row = fmt.format(*('X' if val is True else val or '' for val in values)) yield row, 1 yield div, 0 ####################################### COLUMNS = 'name,external,capi,scope,filename' COLUMN_NAMES = COLUMNS.split(',') COLUMN_WIDTHS = {col: len(col) for col in COLUMN_NAMES} COLUMN_WIDTHS.update({ 'name': 50, 'scope': 7, 'filename': 40, }) COLUMN_FORMATS = {col: '{:%s}' % width for col, width in COLUMN_WIDTHS.items()} for col in COLUMN_FORMATS: if COLUMN_WIDTHS[col] == len(col): COLUMN_FORMATS[col] = COLUMN_FORMATS[col].replace(':', ':^') def _parse_filters_arg(raw, error): filters = [] for value in raw.split(','): value=value.strip() if not value: continue try: filter = Filter.parse(value) if filter.name not in COLUMN_NAMES: raise Exception('unsupported column {!r}'.format(filter.name)) except Exception as e: error('bad filter {!r}: {}'.format(raw, e)) filters.append(filter) return filters def _parse_columns_arg(raw, error): columns = raw.split(',') for column in columns: if column not in COLUMN_NAMES: error('unsupported column {!r}'.format(column)) return columns def _parse_sort_arg(raw, error): sort = raw.split(',') for column in sort: if column.lstrip('_') not in COLUMN_NAMES: error('unsupported column {!r}'.format(column)) return sort def _parse_group_arg(raw, error): if not raw: return raw group = raw if group not in COLUMN_NAMES: error('unsupported column {!r}'.format(group)) if group != 'filename': error('unsupported group {!r}'.format(group)) return group def parse_args(argv=None): if argv is None: argv = sys.argv[1:] import argparse parser = argparse.ArgumentParser() parser.add_argument('-v', '--verbose', action='count', default=0) parser.add_argument('-q', '--quiet', action='count', default=0) parser.add_argument('--filters', default='-scope', help='[[-][=]] ...') parser.add_argument('--columns', default=COLUMNS, help='a comma-separated list of columns to show') parser.add_argument('--sort', default='filename,_name', help='a comma-separated list of columns to sort') parser.add_argument('--group', help='group by the given column name (- to not group)') parser.add_argument('--rc-on-match', dest='rc', type=int) parser.add_argument('filename', nargs='?', default=GLOBALS_FILE) args = parser.parse_args(argv) verbose = vars(args).pop('verbose', 0) quiet = vars(args).pop('quiet', 0) args.verbosity = max(0, VERBOSITY + verbose - quiet) if args.sort.startswith('filename') and not args.group: args.group = 'filename' if args.rc is None: if '-scope=core' in args.filters or 'core' not in args.filters: args.rc = 0 else: args.rc = 1 args.filters = _parse_filters_arg(args.filters, parser.error) args.columns = _parse_columns_arg(args.columns, parser.error) args.sort = _parse_sort_arg(args.sort, parser.error) args.group = _parse_group_arg(args.group, parser.error) return args def main(root=ROOT_DIR, filename=GLOBALS_FILE, filters=None, columns=COLUMN_NAMES, sort=None, group=None, verbosity=VERBOSITY, rc=1): log = lambda msg: ... if verbosity >= 2: log = lambda msg: print(msg) allvars = (var for var in find_vars(root, filename) if filter_var(var, filters)) if sort: allvars = sorted(allvars, key=make_sort_key(sort)) if group: try: columns.remove(group) except ValueError: pass grouped = make_groups(allvars, group) lines = format_groups(grouped, columns, COLUMN_FORMATS, COLUMN_WIDTHS) else: lines = format_vars(allvars, columns, COLUMN_FORMATS, COLUMN_WIDTHS) total = 0 for line, count in lines: total += count log(line) log('\ntotal: {}'.format(total)) if total and rc: print('ERROR: found unsafe globals', file=sys.stderr) return rc return 0 if __name__ == '__main__': args = parse_args() sys.exit( main(**vars(args)))