1import contextlib 2import distutils.ccompiler 3import logging 4import os.path 5 6from c_common.fsutil import match_glob as _match_glob 7from c_common.tables import parse_table as _parse_table 8from ..source import ( 9 resolve as _resolve_source, 10 good_file as _good_file, 11) 12from . import errors as _errors 13from . import ( 14 pure as _pure, 15 gcc as _gcc, 16) 17 18 19logger = logging.getLogger(__name__) 20 21 22# Supported "source": 23# * filename (string) 24# * lines (iterable) 25# * text (string) 26# Supported return values: 27# * iterator of SourceLine 28# * sequence of SourceLine 29# * text (string) 30# * something that combines all those 31# XXX Add the missing support from above. 32# XXX Add more low-level functions to handle permutations? 33 34def preprocess(source, *, 35 incldirs=None, 36 macros=None, 37 samefiles=None, 38 filename=None, 39 tool=True, 40 ): 41 """... 42 43 CWD should be the project root and "source" should be relative. 44 """ 45 if tool: 46 logger.debug(f'CWD: {os.getcwd()!r}') 47 logger.debug(f'incldirs: {incldirs!r}') 48 logger.debug(f'macros: {macros!r}') 49 logger.debug(f'samefiles: {samefiles!r}') 50 _preprocess = _get_preprocessor(tool) 51 with _good_file(source, filename) as source: 52 return _preprocess(source, incldirs, macros, samefiles) or () 53 else: 54 source, filename = _resolve_source(source, filename) 55 # We ignore "includes", "macros", etc. 56 return _pure.preprocess(source, filename) 57 58 # if _run() returns just the lines: 59# text = _run(source) 60# lines = [line + os.linesep for line in text.splitlines()] 61# lines[-1] = lines[-1].splitlines()[0] 62# 63# conditions = None 64# for lno, line in enumerate(lines, 1): 65# kind = 'source' 66# directive = None 67# data = line 68# yield lno, kind, data, conditions 69 70 71def get_preprocessor(*, 72 file_macros=None, 73 file_incldirs=None, 74 file_same=None, 75 ignore_exc=False, 76 log_err=None, 77 ): 78 _preprocess = preprocess 79 if file_macros: 80 file_macros = tuple(_parse_macros(file_macros)) 81 if file_incldirs: 82 file_incldirs = tuple(_parse_incldirs(file_incldirs)) 83 if file_same: 84 file_same = tuple(file_same) 85 if not callable(ignore_exc): 86 ignore_exc = (lambda exc, _ig=ignore_exc: _ig) 87 88 def get_file_preprocessor(filename): 89 filename = filename.strip() 90 if file_macros: 91 macros = list(_resolve_file_values(filename, file_macros)) 92 if file_incldirs: 93 incldirs = [v for v, in _resolve_file_values(filename, file_incldirs)] 94 95 def preprocess(**kwargs): 96 if file_macros and 'macros' not in kwargs: 97 kwargs['macros'] = macros 98 if file_incldirs and 'incldirs' not in kwargs: 99 kwargs['incldirs'] = [v for v, in _resolve_file_values(filename, file_incldirs)] 100 if file_same and 'file_same' not in kwargs: 101 kwargs['samefiles'] = file_same 102 kwargs.setdefault('filename', filename) 103 with handling_errors(ignore_exc, log_err=log_err): 104 return _preprocess(filename, **kwargs) 105 return preprocess 106 return get_file_preprocessor 107 108 109def _resolve_file_values(filename, file_values): 110 # We expect the filename and all patterns to be absolute paths. 111 for pattern, *value in file_values or (): 112 if _match_glob(filename, pattern): 113 yield value 114 115 116def _parse_macros(macros): 117 for row, srcfile in _parse_table(macros, '\t', 'glob\tname\tvalue', rawsep='=', default=None): 118 yield row 119 120 121def _parse_incldirs(incldirs): 122 for row, srcfile in _parse_table(incldirs, '\t', 'glob\tdirname', default=None): 123 glob, dirname = row 124 if dirname is None: 125 # Match all files. 126 dirname = glob 127 row = ('*', dirname.strip()) 128 yield row 129 130 131@contextlib.contextmanager 132def handling_errors(ignore_exc=None, *, log_err=None): 133 try: 134 yield 135 except _errors.OSMismatchError as exc: 136 if not ignore_exc(exc): 137 raise # re-raise 138 if log_err is not None: 139 log_err(f'<OS mismatch (expected {" or ".join(exc.expected)})>') 140 return None 141 except _errors.MissingDependenciesError as exc: 142 if not ignore_exc(exc): 143 raise # re-raise 144 if log_err is not None: 145 log_err(f'<missing dependency {exc.missing}') 146 return None 147 except _errors.ErrorDirectiveError as exc: 148 if not ignore_exc(exc): 149 raise # re-raise 150 if log_err is not None: 151 log_err(exc) 152 return None 153 154 155################################## 156# tools 157 158_COMPILERS = { 159 # matching distutils.ccompiler.compiler_class: 160 'unix': _gcc.preprocess, 161 'msvc': None, 162 'cygwin': None, 163 'mingw32': None, 164 'bcpp': None, 165 # aliases/extras: 166 'gcc': _gcc.preprocess, 167 'clang': None, 168} 169 170 171def _get_preprocessor(tool): 172 if tool is True: 173 tool = distutils.ccompiler.get_default_compiler() 174 preprocess = _COMPILERS.get(tool) 175 if preprocess is None: 176 raise ValueError(f'unsupported tool {tool}') 177 return preprocess 178 179 180################################## 181# aliases 182 183from .errors import ( 184 PreprocessorError, 185 PreprocessorFailure, 186 ErrorDirectiveError, 187 MissingDependenciesError, 188 OSMismatchError, 189) 190from .common import FileInfo, SourceLine 191