| 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 |