diff options
Diffstat (limited to 'Tools/c-analyzer/c_globals')
-rw-r--r-- | Tools/c-analyzer/c_globals/README | 72 | ||||
-rw-r--r-- | Tools/c-analyzer/c_globals/__init__.py | 0 | ||||
-rw-r--r-- | Tools/c-analyzer/c_globals/__main__.py | 209 | ||||
-rw-r--r-- | Tools/c-analyzer/c_globals/find.py | 95 | ||||
-rw-r--r-- | Tools/c-analyzer/c_globals/show.py | 16 | ||||
-rw-r--r-- | Tools/c-analyzer/c_globals/supported.py | 393 |
6 files changed, 0 insertions, 785 deletions
diff --git a/Tools/c-analyzer/c_globals/README b/Tools/c-analyzer/c_globals/README deleted file mode 100644 index 772b8be270..0000000000 --- a/Tools/c-analyzer/c_globals/README +++ /dev/null @@ -1,72 +0,0 @@ -####################################### -# C Globals and CPython Runtime State. - -CPython's C code makes extensive use of global variables (whether static -globals or static locals). Each such variable falls into one of several -categories: - -* strictly const data -* used exclusively in main or in the REPL -* process-global state (e.g. managing process-level resources - like signals and file descriptors) -* Python "global" runtime state -* per-interpreter runtime state - -The last one can be a problem as soon as anyone creates a second -interpreter (AKA "subinterpreter") in a process. It is definitely a -problem under subinterpreters if they are no longer sharing the GIL, -since the GIL protects us from a lot of race conditions. Keep in mind -that ultimately *all* objects (PyObject) should be treated as -per-interpreter state. This includes "static types", freelists, -_PyIdentifier, and singletons. Take that in for a second. It has -significant implications on where we use static variables! - -Be aware that module-global state (stored in C statics) is a kind of -per-interpreter state. There have been efforts across many years, and -still going, to provide extension module authors mechanisms to store -that state safely (see PEPs 3121, 489, etc.). - -(Note that there has been discussion around support for running multiple -Python runtimes in the same process. That would ends up with the same -problems, relative to static variables, that subinterpreters have.) - -Historically we have been bad at keeping per-interpreter state out of -static variables, mostly because until recently subinterpreters were -not widely used nor even factored in to solutions. However, the -feature is growing in popularity and use in the community. - -Mandate: "Eliminate use of static variables for per-interpreter state." - -The "c-statics.py" script in this directory, along with its accompanying -data files, are part of the effort to resolve existing problems with -our use of static variables and to prevent future problems. - -#------------------------- -## statics for actually-global state (and runtime state consolidation) - -In general, holding any kind of state in static variables -increases maintenance burden and increases the complexity of code (e.g. -we use TSS to identify the active thread state). So it is a good idea -to avoid using statics for state even if for the "global" runtime or -for process-global state. - -Relative to maintenance burden, one problem is where the runtime -state is spread throughout the codebase in dozens of individual -globals. Unlike the other globals, the runtime state represents a set -of values that are constantly shifting in a complex way. When they are -spread out it's harder to get a clear picture of what the runtime -involves. Furthermore, when they are spread out it complicates efforts -that change the runtime. - -Consequently, the globals for Python's runtime state have been -consolidated under a single top-level _PyRuntime global. No new globals -should be added for runtime state. Instead, they should be added to -_PyRuntimeState or one of its sub-structs. The tools in this directory -are run as part of the test suite to ensure that no new globals have -been added. The script can be run manually as well: - - ./python Lib/test/test_c_statics/c-statics.py check - -If it reports any globals then they should be resolved. If the globals -are runtime state then they should be folded into _PyRuntimeState. -Otherwise they should be marked as ignored. diff --git a/Tools/c-analyzer/c_globals/__init__.py b/Tools/c-analyzer/c_globals/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 --- a/Tools/c-analyzer/c_globals/__init__.py +++ /dev/null diff --git a/Tools/c-analyzer/c_globals/__main__.py b/Tools/c-analyzer/c_globals/__main__.py deleted file mode 100644 index 9570fb6a14..0000000000 --- a/Tools/c-analyzer/c_globals/__main__.py +++ /dev/null @@ -1,209 +0,0 @@ -import argparse -import os.path -import re -import sys - -from c_analyzer_common import SOURCE_DIRS, REPO_ROOT -from c_analyzer_common.info import UNKNOWN -from c_analyzer_common.known import ( - from_file as known_from_file, - DATA_FILE as KNOWN_FILE, - ) -from . import find, show -from .supported import is_supported, ignored_from_file, IGNORED_FILE, _is_object - - -def _match_unused_global(variable, knownvars, used): - found = [] - for varid in knownvars: - if varid in used: - continue - if varid.funcname is not None: - continue - if varid.name != variable.name: - continue - if variable.filename and variable.filename != UNKNOWN: - if variable.filename == varid.filename: - found.append(varid) - else: - found.append(varid) - return found - - -def _check_results(unknown, knownvars, used): - badknown = set() - for variable in sorted(unknown): - msg = None - if variable.funcname != UNKNOWN: - msg = f'could not find global symbol {variable.id}' - elif m := _match_unused_global(variable, knownvars, used): - assert isinstance(m, list) - badknown.update(m) - elif variable.name in ('completed', 'id'): # XXX Figure out where these variables are. - unknown.remove(variable) - else: - msg = f'could not find local symbol {variable.id}' - if msg: - #raise Exception(msg) - print(msg) - if badknown: - print('---') - print(f'{len(badknown)} globals in known.tsv, but may actually be local:') - for varid in sorted(badknown): - print(f'{varid.filename:30} {varid.name}') - unused = sorted(varid - for varid in set(knownvars) - used - if varid.name != 'id') # XXX Figure out where these variables are. - if unused: - print('---') - print(f'did not use {len(unused)} known vars:') - for varid in unused: - print(f'{varid.filename:30} {varid.funcname or "-":20} {varid.name}') - raise Exception('not all known symbols used') - if unknown: - print('---') - raise Exception('could not find all symbols') - - -def _find_globals(dirnames, known, ignored): - if dirnames == SOURCE_DIRS: - dirnames = [os.path.relpath(d, REPO_ROOT) for d in dirnames] - - ignored = ignored_from_file(ignored) - known = known_from_file(known) - - used = set() - unknown = set() - knownvars = (known or {}).get('variables') - for variable in find.globals_from_binary(knownvars=knownvars, - dirnames=dirnames): - #for variable in find.globals(dirnames, known, kind='platform'): - if variable.vartype == UNKNOWN: - unknown.add(variable) - continue - yield variable, is_supported(variable, ignored, known) - used.add(variable.id) - - #_check_results(unknown, knownvars, used) - - -def cmd_check(cmd, dirs=SOURCE_DIRS, *, - ignored=IGNORED_FILE, - known=KNOWN_FILE, - _find=_find_globals, - _show=show.basic, - _print=print, - ): - """ - Fail if there are unsupported globals variables. - - In the failure case, the list of unsupported variables - will be printed out. - """ - unsupported = [v for v, s in _find(dirs, known, ignored) if not s] - if not unsupported: - #_print('okay') - return - - _print('ERROR: found unsupported global variables') - _print() - _show(sorted(unsupported)) - _print(f' ({len(unsupported)} total)') - sys.exit(1) - - -def cmd_show(cmd, dirs=SOURCE_DIRS, *, - ignored=IGNORED_FILE, - known=KNOWN_FILE, - skip_objects=False, - _find=_find_globals, - _show=show.basic, - _print=print, - ): - """ - Print out the list of found global variables. - - The variables will be distinguished as "supported" or "unsupported". - """ - allsupported = [] - allunsupported = [] - for found, supported in _find(dirs, known, ignored): - if skip_objects: # XXX Support proper filters instead. - if _is_object(found.vartype): - continue - (allsupported if supported else allunsupported - ).append(found) - - _print('supported:') - _print('----------') - _show(sorted(allsupported)) - _print(f' ({len(allsupported)} total)') - _print() - _print('unsupported:') - _print('------------') - _show(sorted(allunsupported)) - _print(f' ({len(allunsupported)} total)') - - -############################# -# the script - -COMMANDS = { - 'check': cmd_check, - 'show': cmd_show, - } - -PROG = sys.argv[0] -PROG = 'c-globals.py' - - -def parse_args(prog=PROG, argv=sys.argv[1:], *, _fail=None): - common = argparse.ArgumentParser(add_help=False) - common.add_argument('--ignored', metavar='FILE', - default=IGNORED_FILE, - help='path to file that lists ignored vars') - common.add_argument('--known', metavar='FILE', - default=KNOWN_FILE, - help='path to file that lists known types') - common.add_argument('dirs', metavar='DIR', nargs='*', - default=SOURCE_DIRS, - help='a directory to check') - - parser = argparse.ArgumentParser( - prog=prog, - ) - subs = parser.add_subparsers(dest='cmd') - - check = subs.add_parser('check', parents=[common]) - - show = subs.add_parser('show', parents=[common]) - show.add_argument('--skip-objects', action='store_true') - - if _fail is None: - def _fail(msg): - parser.error(msg) - - # Now parse the args. - args = parser.parse_args(argv) - ns = vars(args) - - cmd = ns.pop('cmd') - if not cmd: - _fail('missing command') - - return cmd, ns - - -def main(cmd, cmdkwargs=None, *, _COMMANDS=COMMANDS): - try: - cmdfunc = _COMMANDS[cmd] - except KeyError: - raise ValueError( - f'unsupported cmd {cmd!r}' if cmd else 'missing cmd') - - cmdfunc(cmd, **cmdkwargs or {}) - - -if __name__ == '__main__': - cmd, cmdkwargs = parse_args() - main(cmd, cmdkwargs) diff --git a/Tools/c-analyzer/c_globals/find.py b/Tools/c-analyzer/c_globals/find.py deleted file mode 100644 index a51b947cbd..0000000000 --- a/Tools/c-analyzer/c_globals/find.py +++ /dev/null @@ -1,95 +0,0 @@ -from c_analyzer_common import SOURCE_DIRS -from c_analyzer_common.info import UNKNOWN -from c_symbols import ( - info as s_info, - binary as b_symbols, - source as s_symbols, - resolve, - ) -from c_parser import info, declarations - - -# XXX needs tests: -# * iter_variables - -def globals_from_binary(binfile=b_symbols.PYTHON, *, - knownvars=None, - dirnames=None, - _iter_symbols=b_symbols.iter_symbols, - _resolve=resolve.symbols_to_variables, - _get_symbol_resolver=resolve.get_resolver, - ): - """Yield a Variable for each found Symbol. - - Details are filled in from the given "known" variables and types. - """ - symbols = _iter_symbols(binfile, find_local_symbol=None) - #symbols = list(symbols) - for variable in _resolve(symbols, - resolve=_get_symbol_resolver(knownvars, dirnames), - ): - # Skip each non-global variable (unless we couldn't find it). - # XXX Drop the "UNKNOWN" condition? - if not variable.isglobal and variable.vartype != UNKNOWN: - continue - yield variable - - -def globals_from_declarations(dirnames=SOURCE_DIRS, *, - known=None, - ): - """Yield a Variable for each found declaration. - - Details are filled in from the given "known" variables and types. - """ - raise NotImplementedError - - -def iter_variables(kind='platform', *, - known=None, - dirnames=None, - _resolve_symbols=resolve.symbols_to_variables, - _get_symbol_resolver=resolve.get_resolver, - _symbols_from_binary=b_symbols.iter_symbols, - _symbols_from_source=s_symbols.iter_symbols, - _iter_raw=declarations.iter_all, - _iter_preprocessed=declarations.iter_preprocessed, - ): - """Yield a Variable for each one found (e.g. in files).""" - kind = kind or 'platform' - - if kind == 'symbols': - knownvars = (known or {}).get('variables') - yield from _resolve_symbols( - _symbols_from_source(dirnames, known), - resolve=_get_symbol_resolver(knownvars, dirnames), - ) - elif kind == 'platform': - knownvars = (known or {}).get('variables') - yield from _resolve_symbols( - _symbols_from_binary(find_local_symbol=None), - resolve=_get_symbol_resolver(knownvars, dirnames), - ) - elif kind == 'declarations': - for decl in _iter_raw(dirnames): - if not isinstance(decl, info.Variable): - continue - yield decl - elif kind == 'preprocessed': - for decl in _iter_preprocessed(dirnames): - if not isinstance(decl, info.Variable): - continue - yield decl - else: - raise ValueError(f'unsupported kind {kind!r}') - - -def globals(dirnames, known, *, - kind=None, # Use the default. - _iter_variables=iter_variables, - ): - """Return a list of (StaticVar, <supported>) for each found global var.""" - for found in _iter_variables(kind, known=known, dirnames=dirnames): - if not found.isglobal: - continue - yield found diff --git a/Tools/c-analyzer/c_globals/show.py b/Tools/c-analyzer/c_globals/show.py deleted file mode 100644 index f4298b17b6..0000000000 --- a/Tools/c-analyzer/c_globals/show.py +++ /dev/null @@ -1,16 +0,0 @@ - -def basic(globals, *, - _print=print): - """Print each row simply.""" - for variable in globals: - if variable.funcname: - line = f'{variable.filename}:{variable.funcname}():{variable.name}' - else: - line = f'{variable.filename}:{variable.name}' - vartype = variable.vartype - #if vartype.startswith('static '): - # vartype = vartype.partition(' ')[2] - #else: - # vartype = '=' + vartype - line = f'{line:<64} {vartype}' - _print(line) diff --git a/Tools/c-analyzer/c_globals/supported.py b/Tools/c-analyzer/c_globals/supported.py deleted file mode 100644 index d185daa246..0000000000 --- a/Tools/c-analyzer/c_globals/supported.py +++ /dev/null @@ -1,393 +0,0 @@ -import os.path -import re - -from c_analyzer_common import DATA_DIR -from c_analyzer_common.info import ID -from c_analyzer_common.util import read_tsv, write_tsv - - -IGNORED_FILE = os.path.join(DATA_DIR, 'ignored.tsv') - -IGNORED_COLUMNS = ('filename', 'funcname', 'name', 'kind', 'reason') -IGNORED_HEADER = '\t'.join(IGNORED_COLUMNS) - -# XXX Move these to ignored.tsv. -IGNORED = { - # global - 'PyImport_FrozenModules': 'process-global', - 'M___hello__': 'process-global', - 'inittab_copy': 'process-global', - 'PyHash_Func': 'process-global', - '_Py_HashSecret_Initialized': 'process-global', - '_TARGET_LOCALES': 'process-global', - - # startup (only changed before/during) - '_PyRuntime': 'runtime startup', - 'runtime_initialized': 'runtime startup', - 'static_arg_parsers': 'runtime startup', - 'orig_argv': 'runtime startup', - 'opt_ptr': 'runtime startup', - '_preinit_warnoptions': 'runtime startup', - '_Py_StandardStreamEncoding': 'runtime startup', - 'Py_FileSystemDefaultEncoding': 'runtime startup', - '_Py_StandardStreamErrors': 'runtime startup', - 'Py_FileSystemDefaultEncodeErrors': 'runtime startup', - 'Py_BytesWarningFlag': 'runtime startup', - 'Py_DebugFlag': 'runtime startup', - 'Py_DontWriteBytecodeFlag': 'runtime startup', - 'Py_FrozenFlag': 'runtime startup', - 'Py_HashRandomizationFlag': 'runtime startup', - 'Py_IgnoreEnvironmentFlag': 'runtime startup', - 'Py_InspectFlag': 'runtime startup', - 'Py_InteractiveFlag': 'runtime startup', - 'Py_IsolatedFlag': 'runtime startup', - 'Py_NoSiteFlag': 'runtime startup', - 'Py_NoUserSiteDirectory': 'runtime startup', - 'Py_OptimizeFlag': 'runtime startup', - 'Py_QuietFlag': 'runtime startup', - 'Py_UTF8Mode': 'runtime startup', - 'Py_UnbufferedStdioFlag': 'runtime startup', - 'Py_VerboseFlag': 'runtime startup', - '_Py_path_config': 'runtime startup', - '_PyOS_optarg': 'runtime startup', - '_PyOS_opterr': 'runtime startup', - '_PyOS_optind': 'runtime startup', - '_Py_HashSecret': 'runtime startup', - - # REPL - '_PyOS_ReadlineLock': 'repl', - '_PyOS_ReadlineTState': 'repl', - - # effectively const - 'tracemalloc_empty_traceback': 'const', - '_empty_bitmap_node': 'const', - 'posix_constants_pathconf': 'const', - 'posix_constants_confstr': 'const', - 'posix_constants_sysconf': 'const', - '_PySys_ImplCacheTag': 'const', - '_PySys_ImplName': 'const', - 'PyImport_Inittab': 'const', - '_PyImport_DynLoadFiletab': 'const', - '_PyParser_Grammar': 'const', - 'Py_hexdigits': 'const', - '_PyImport_Inittab': 'const', - '_PyByteArray_empty_string': 'const', - '_PyLong_DigitValue': 'const', - '_Py_SwappedOp': 'const', - 'PyStructSequence_UnnamedField': 'const', - - # signals are main-thread only - 'faulthandler_handlers': 'signals are main-thread only', - 'user_signals': 'signals are main-thread only', - 'wakeup': 'signals are main-thread only', - - # hacks - '_PySet_Dummy': 'only used as a placeholder', - } - -BENIGN = 'races here are benign and unlikely' - - -def is_supported(variable, ignored=None, known=None, *, - _ignored=(lambda *a, **k: _is_ignored(*a, **k)), - _vartype_okay=(lambda *a, **k: _is_vartype_okay(*a, **k)), - ): - """Return True if the given global variable is okay in CPython.""" - if _ignored(variable, - ignored and ignored.get('variables')): - return True - elif _vartype_okay(variable.vartype, - ignored.get('types')): - return True - else: - return False - - -def _is_ignored(variable, ignoredvars=None, *, - _IGNORED=IGNORED, - ): - """Return the reason if the variable is a supported global. - - Return None if the variable is not a supported global. - """ - if ignoredvars and (reason := ignoredvars.get(variable.id)): - return reason - - if variable.funcname is None: - if reason := _IGNORED.get(variable.name): - return reason - - # compiler - if variable.filename == 'Python/graminit.c': - if variable.vartype.startswith('static state '): - return 'compiler' - if variable.filename == 'Python/symtable.c': - if variable.vartype.startswith('static identifier '): - return 'compiler' - if variable.filename == 'Python/Python-ast.c': - # These should be const. - if variable.name.endswith('_field'): - return 'compiler' - if variable.name.endswith('_attribute'): - return 'compiler' - - # other - if variable.filename == 'Python/dtoa.c': - # guarded by lock? - if variable.name in ('p5s', 'freelist'): - return 'dtoa is thread-safe?' - if variable.name in ('private_mem', 'pmem_next'): - return 'dtoa is thread-safe?' - if variable.filename == 'Python/thread.c': - # Threads do not become an issue until after these have been set - # and these never get changed after that. - if variable.name in ('initialized', 'thread_debug'): - return 'thread-safe' - if variable.filename == 'Python/getversion.c': - if variable.name == 'version': - # Races are benign here, as well as unlikely. - return BENIGN - if variable.filename == 'Python/fileutils.c': - if variable.name == 'force_ascii': - return BENIGN - if variable.name == 'ioctl_works': - return BENIGN - if variable.name == '_Py_open_cloexec_works': - return BENIGN - if variable.filename == 'Python/codecs.c': - if variable.name == 'ucnhash_CAPI': - return BENIGN - if variable.filename == 'Python/bootstrap_hash.c': - if variable.name == 'getrandom_works': - return BENIGN - if variable.filename == 'Objects/unicodeobject.c': - if variable.name == 'ucnhash_CAPI': - return BENIGN - if variable.name == 'bloom_linebreak': - # *mostly* benign - return BENIGN - if variable.filename == 'Modules/getbuildinfo.c': - if variable.name == 'buildinfo': - # The static is used for pre-allocation. - return BENIGN - if variable.filename == 'Modules/posixmodule.c': - if variable.name == 'ticks_per_second': - return BENIGN - if variable.name == 'dup3_works': - return BENIGN - if variable.filename == 'Modules/timemodule.c': - if variable.name == 'ticks_per_second': - return BENIGN - if variable.filename == 'Objects/longobject.c': - if variable.name == 'log_base_BASE': - return BENIGN - if variable.name == 'convwidth_base': - return BENIGN - if variable.name == 'convmultmax_base': - return BENIGN - - return None - - -def _is_vartype_okay(vartype, ignoredtypes=None): - if _is_object(vartype): - return None - - if vartype.startswith('static const '): - return 'const' - if vartype.startswith('const '): - return 'const' - - # components for TypeObject definitions - for name in ('PyMethodDef', 'PyGetSetDef', 'PyMemberDef'): - if name in vartype: - return 'const' - for name in ('PyNumberMethods', 'PySequenceMethods', 'PyMappingMethods', - 'PyBufferProcs', 'PyAsyncMethods'): - if name in vartype: - return 'const' - for name in ('slotdef', 'newfunc'): - if name in vartype: - return 'const' - - # structseq - for name in ('PyStructSequence_Desc', 'PyStructSequence_Field'): - if name in vartype: - return 'const' - - # other definiitions - if 'PyModuleDef' in vartype: - return 'const' - - # thread-safe - if '_Py_atomic_int' in vartype: - return 'thread-safe' - if 'pthread_condattr_t' in vartype: - return 'thread-safe' - - # startup - if '_Py_PreInitEntry' in vartype: - return 'startup' - - # global -# if 'PyMemAllocatorEx' in vartype: -# return True - - # others -# if 'PyThread_type_lock' in vartype: -# return True - - # XXX ??? - # _Py_tss_t - # _Py_hashtable_t - # stack_t - # _PyUnicode_Name_CAPI - - # functions - if '(' in vartype and '[' not in vartype: - return 'function pointer' - - # XXX finish! - # * allow const values? - #raise NotImplementedError - return None - - -PYOBJECT_RE = re.compile(r''' - ^ - ( - # must start with "static " - static \s+ - ( - identifier - ) - \b - ) | - ( - # may start with "static " - ( static \s+ )? - ( - .* - ( - PyObject | - PyTypeObject | - _? Py \w+ Object | - _PyArg_Parser | - _Py_Identifier | - traceback_t | - PyAsyncGenASend | - _PyAsyncGenWrappedValue | - PyContext | - method_cache_entry - ) - \b - ) | - ( - ( - _Py_IDENTIFIER | - _Py_static_string - ) - [(] - ) - ) - ''', re.VERBOSE) - - -def _is_object(vartype): - if 'PyDictKeysObject' in vartype: - return False - if PYOBJECT_RE.match(vartype): - return True - if vartype.endswith((' _Py_FalseStruct', ' _Py_TrueStruct')): - return True - - # XXX Add more? - - #for part in vartype.split(): - # # XXX const is automatic True? - # if part == 'PyObject' or part.startswith('PyObject['): - # return True - return False - - -def ignored_from_file(infile, *, - _read_tsv=read_tsv, - ): - """Yield a Variable for each ignored var in the file.""" - ignored = { - 'variables': {}, - #'types': {}, - #'constants': {}, - #'macros': {}, - } - for row in _read_tsv(infile, IGNORED_HEADER): - filename, funcname, name, kind, reason = row - if not funcname or funcname == '-': - funcname = None - id = ID(filename, funcname, name) - if kind == 'variable': - values = ignored['variables'] - else: - raise ValueError(f'unsupported kind in row {row}') - values[id] = reason - return ignored - - -################################## -# generate - -def _get_row(varid, reason): - return ( - varid.filename, - varid.funcname or '-', - varid.name, - 'variable', - str(reason), - ) - - -def _get_rows(variables, ignored=None, *, - _as_row=_get_row, - _is_ignored=_is_ignored, - _vartype_okay=_is_vartype_okay, - ): - count = 0 - for variable in variables: - reason = _is_ignored(variable, - ignored and ignored.get('variables'), - ) - if not reason: - reason = _vartype_okay(variable.vartype, - ignored and ignored.get('types')) - if not reason: - continue - - print(' ', variable, repr(reason)) - yield _as_row(variable.id, reason) - count += 1 - print(f'total: {count}') - - -def _generate_ignored_file(variables, filename=None, *, - _generate_rows=_get_rows, - _write_tsv=write_tsv, - ): - if not filename: - filename = IGNORED_FILE + '.new' - rows = _generate_rows(variables) - _write_tsv(filename, IGNORED_HEADER, rows) - - -if __name__ == '__main__': - from c_analyzer_common import SOURCE_DIRS - from c_analyzer_common.known import ( - from_file as known_from_file, - DATA_FILE as KNOWN_FILE, - ) - from . import find - known = known_from_file(KNOWN_FILE) - knownvars = (known or {}).get('variables') - variables = find.globals_from_binary(knownvars=knownvars, - dirnames=SOURCE_DIRS) - - _generate_ignored_file(variables) |