summaryrefslogtreecommitdiffstats
path: root/Tools/c-analyzer/c_parser/preprocessor/common.py
blob: dbe1edeef3852760de38ef4d13454d74e191edea (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import contextlib
import distutils.ccompiler
import logging
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)

    proc = subprocess.run(argv, **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