• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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