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