1  import contextlib
       2  import distutils.ccompiler
       3  import logging
       4  import os.path
       5  
       6  from c_common.fsutil import match_glob as _match_glob
       7  from c_common.tables import parse_table as _parse_table
       8  from ..source import (
       9      resolve as _resolve_source,
      10      good_file as _good_file,
      11  )
      12  from . import errors as _errors
      13  from . import (
      14      pure as _pure,
      15      gcc as _gcc,
      16  )
      17  
      18  
      19  logger = 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  
      34  def 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  
      71  def 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  
     109  def _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  
     116  def _parse_macros(macros):
     117      for row, srcfile in _parse_table(macros, '\t', 'glob\tname\tvalue', rawsep='=', default=None):
     118          yield row
     119  
     120  
     121  def _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
     132  def 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  
     171  def _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  
     183  from .errors import (
     184      PreprocessorError,
     185      PreprocessorFailure,
     186      ErrorDirectiveError,
     187      MissingDependenciesError,
     188      OSMismatchError,
     189  )
     190  from .common import FileInfo, SourceLine