• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import contextlib
2import distutils.ccompiler
3import logging
4import shlex
5import subprocess
6import sys
7
8from ..info import FileInfo, SourceLine
9from .errors import (
10    PreprocessorFailure,
11    ErrorDirectiveError,
12    MissingDependenciesError,
13    OSMismatchError,
14)
15
16
17logger = logging.getLogger(__name__)
18
19
20# XXX Add aggregate "source" class(es)?
21#  * expose all lines as single text string
22#  * expose all lines as sequence
23#  * iterate all lines
24
25
26def run_cmd(argv, *,
27            #capture_output=True,
28            stdout=subprocess.PIPE,
29            #stderr=subprocess.STDOUT,
30            stderr=subprocess.PIPE,
31            text=True,
32            check=True,
33            **kwargs
34            ):
35    if isinstance(stderr, str) and stderr.lower() == 'stdout':
36        stderr = subprocess.STDOUT
37
38    kw = dict(locals())
39    kw.pop('argv')
40    kw.pop('kwargs')
41    kwargs.update(kw)
42
43    proc = subprocess.run(argv, **kwargs)
44    return proc.stdout
45
46
47def preprocess(tool, filename, **kwargs):
48    argv = _build_argv(tool, filename, **kwargs)
49    logger.debug(' '.join(shlex.quote(v) for v in argv))
50
51    # Make sure the OS is supported for this file.
52    if (_expected := is_os_mismatch(filename)):
53        error = None
54        raise OSMismatchError(filename, _expected, argv, error, TOOL)
55
56    # Run the command.
57    with converted_error(tool, argv, filename):
58        # We use subprocess directly here, instead of calling the
59        # distutil compiler object's preprocess() method, since that
60        # one writes to stdout/stderr and it's simpler to do it directly
61        # through subprocess.
62        return run_cmd(argv)
63
64
65def _build_argv(
66    tool,
67    filename,
68    incldirs=None,
69    macros=None,
70    preargs=None,
71    postargs=None,
72    executable=None,
73    compiler=None,
74):
75    compiler = distutils.ccompiler.new_compiler(
76        compiler=compiler or tool,
77    )
78    if executable:
79        compiler.set_executable('preprocessor', executable)
80
81    argv = None
82    def _spawn(_argv):
83        nonlocal argv
84        argv = _argv
85    compiler.spawn = _spawn
86    compiler.preprocess(
87        filename,
88        macros=[tuple(v) for v in macros or ()],
89        include_dirs=incldirs or (),
90        extra_preargs=preargs or (),
91        extra_postargs=postargs or (),
92    )
93    return argv
94
95
96@contextlib.contextmanager
97def converted_error(tool, argv, filename):
98    try:
99        yield
100    except subprocess.CalledProcessError as exc:
101        convert_error(
102            tool,
103            argv,
104            filename,
105            exc.stderr,
106            exc.returncode,
107        )
108
109
110def convert_error(tool, argv, filename, stderr, rc):
111    error = (stderr.splitlines()[0], rc)
112    if (_expected := is_os_mismatch(filename, stderr)):
113        logger.debug(stderr.strip())
114        raise OSMismatchError(filename, _expected, argv, error, tool)
115    elif (_missing := is_missing_dep(stderr)):
116        logger.debug(stderr.strip())
117        raise MissingDependenciesError(filename, (_missing,), argv, error, tool)
118    elif '#error' in stderr:
119        # XXX Ignore incompatible files.
120        error = (stderr.splitlines()[1], rc)
121        logger.debug(stderr.strip())
122        raise ErrorDirectiveError(filename, argv, error, tool)
123    else:
124        # Try one more time, with stderr written to the terminal.
125        try:
126            output = run_cmd(argv, stderr=None)
127        except subprocess.CalledProcessError:
128            raise PreprocessorFailure(filename, argv, error, tool)
129
130
131def is_os_mismatch(filename, errtext=None):
132    # See: https://docs.python.org/3/library/sys.html#sys.platform
133    actual = sys.platform
134    if actual == 'unknown':
135        raise NotImplementedError
136
137    if errtext is not None:
138        if (missing := is_missing_dep(errtext)):
139            matching = get_matching_oses(missing, filename)
140            if actual not in matching:
141                return matching
142    return False
143
144
145def get_matching_oses(missing, filename):
146    # OSX
147    if 'darwin' in filename or 'osx' in filename:
148        return ('darwin',)
149    elif missing == 'SystemConfiguration/SystemConfiguration.h':
150        return ('darwin',)
151
152    # Windows
153    elif missing in ('windows.h', 'winsock2.h'):
154        return ('win32',)
155
156    # other
157    elif missing == 'sys/ldr.h':
158        return ('aix',)
159    elif missing == 'dl.h':
160        # XXX The existence of Python/dynload_dl.c implies others...
161        # Note that hpux isn't actual supported any more.
162        return ('hpux', '???')
163
164    # unrecognized
165    else:
166        return ()
167
168
169def is_missing_dep(errtext):
170    if 'No such file or directory' in errtext:
171        missing = errtext.split(': No such file or directory')[0].split()[-1]
172        return missing
173    return False
174