| import contextlib |
| import distutils.ccompiler |
| import logging |
| import os |
| import shlex |
| import subprocess |
| import sys |
| |
| from ..info import FileInfo, SourceLine |
| from .errors import ( |
| PreprocessorFailure, |
| ErrorDirectiveError, |
| MissingDependenciesError, |
| OSMismatchError, |
| ) |
| |
| |
| logger = logging.getLogger(__name__) |
| |
| |
| # XXX Add aggregate "source" class(es)? |
| # * expose all lines as single text string |
| # * expose all lines as sequence |
| # * iterate all lines |
| |
| |
| def run_cmd(argv, *, |
| #capture_output=True, |
| stdout=subprocess.PIPE, |
| #stderr=subprocess.STDOUT, |
| stderr=subprocess.PIPE, |
| text=True, |
| check=True, |
| **kwargs |
| ): |
| if isinstance(stderr, str) and stderr.lower() == 'stdout': |
| stderr = subprocess.STDOUT |
| |
| kw = dict(locals()) |
| kw.pop('argv') |
| kw.pop('kwargs') |
| kwargs.update(kw) |
| |
| # Remove LANG environment variable: the C parser doesn't support GCC |
| # localized messages |
| env = dict(os.environ) |
| env.pop('LANG', None) |
| |
| proc = subprocess.run(argv, env=env, **kwargs) |
| return proc.stdout |
| |
| |
| def preprocess(tool, filename, cwd=None, **kwargs): |
| argv = _build_argv(tool, filename, **kwargs) |
| logger.debug(' '.join(shlex.quote(v) for v in argv)) |
| |
| # Make sure the OS is supported for this file. |
| if (_expected := is_os_mismatch(filename)): |
| error = None |
| raise OSMismatchError(filename, _expected, argv, error, TOOL) |
| |
| # Run the command. |
| with converted_error(tool, argv, filename): |
| # We use subprocess directly here, instead of calling the |
| # distutil compiler object's preprocess() method, since that |
| # one writes to stdout/stderr and it's simpler to do it directly |
| # through subprocess. |
| return run_cmd(argv, cwd=cwd) |
| |
| |
| def _build_argv( |
| tool, |
| filename, |
| incldirs=None, |
| includes=None, |
| macros=None, |
| preargs=None, |
| postargs=None, |
| executable=None, |
| compiler=None, |
| ): |
| if includes: |
| includes = tuple(f'-include{i}' for i in includes) |
| postargs = (includes + postargs) if postargs else includes |
| |
| compiler = distutils.ccompiler.new_compiler( |
| compiler=compiler or tool, |
| ) |
| if executable: |
| compiler.set_executable('preprocessor', executable) |
| |
| argv = None |
| def _spawn(_argv): |
| nonlocal argv |
| argv = _argv |
| compiler.spawn = _spawn |
| compiler.preprocess( |
| filename, |
| macros=[tuple(v) for v in macros or ()], |
| include_dirs=incldirs or (), |
| extra_preargs=preargs or (), |
| extra_postargs=postargs or (), |
| ) |
| return argv |
| |
| |
| @contextlib.contextmanager |
| def converted_error(tool, argv, filename): |
| try: |
| yield |
| except subprocess.CalledProcessError as exc: |
| convert_error( |
| tool, |
| argv, |
| filename, |
| exc.stderr, |
| exc.returncode, |
| ) |
| |
| |
| def convert_error(tool, argv, filename, stderr, rc): |
| error = (stderr.splitlines()[0], rc) |
| if (_expected := is_os_mismatch(filename, stderr)): |
| logger.info(stderr.strip()) |
| raise OSMismatchError(filename, _expected, argv, error, tool) |
| elif (_missing := is_missing_dep(stderr)): |
| logger.info(stderr.strip()) |
| raise MissingDependenciesError(filename, (_missing,), argv, error, tool) |
| elif '#error' in stderr: |
| # XXX Ignore incompatible files. |
| error = (stderr.splitlines()[1], rc) |
| logger.info(stderr.strip()) |
| raise ErrorDirectiveError(filename, argv, error, tool) |
| else: |
| # Try one more time, with stderr written to the terminal. |
| try: |
| output = run_cmd(argv, stderr=None) |
| except subprocess.CalledProcessError: |
| raise PreprocessorFailure(filename, argv, error, tool) |
| |
| |
| def is_os_mismatch(filename, errtext=None): |
| # See: https://docs.python.org/3/library/sys.html#sys.platform |
| actual = sys.platform |
| if actual == 'unknown': |
| raise NotImplementedError |
| |
| if errtext is not None: |
| if (missing := is_missing_dep(errtext)): |
| matching = get_matching_oses(missing, filename) |
| if actual not in matching: |
| return matching |
| return False |
| |
| |
| def get_matching_oses(missing, filename): |
| # OSX |
| if 'darwin' in filename or 'osx' in filename: |
| return ('darwin',) |
| elif missing == 'SystemConfiguration/SystemConfiguration.h': |
| return ('darwin',) |
| |
| # Windows |
| elif missing in ('windows.h', 'winsock2.h'): |
| return ('win32',) |
| |
| # other |
| elif missing == 'sys/ldr.h': |
| return ('aix',) |
| elif missing == 'dl.h': |
| # XXX The existence of Python/dynload_dl.c implies others... |
| # Note that hpux isn't actual supported any more. |
| return ('hpux', '???') |
| |
| # unrecognized |
| else: |
| return () |
| |
| |
| def is_missing_dep(errtext): |
| if 'No such file or directory' in errtext: |
| missing = errtext.split(': No such file or directory')[0].split()[-1] |
| return missing |
| return False |