Skip to content

Commit 196e88b

Browse files
committed
Add MSVC preprocessor support
1 parent 3d23375 commit 196e88b

File tree

3 files changed

+116
-1
lines changed

3 files changed

+116
-1
lines changed

.github/workflows/dist.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ jobs:
112112
- name: Install test dependencies
113113
run: python -m pip --disable-pip-version-check install -r tests/requirements.txt
114114

115+
- name: Setup MSVC compiler
116+
uses: ilammy/msvc-dev-cmd@v1
117+
if: matrix.os == 'windows-latest'
118+
115119
- name: Test wheel
116120
shell: bash
117121
run: |

cxxheaderparser/preprocessor.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import os
88
import subprocess
99
import sys
10+
import tempfile
1011
import typing
1112

1213
from .options import PreprocessorFunction
@@ -104,6 +105,110 @@ def _preprocess_file(filename: str, content: typing.Optional[str]) -> str:
104105
return _preprocess_file
105106

106107

108+
#
109+
# Microsoft Visual Studio preprocessor support
110+
#
111+
112+
113+
def _msvc_filter(fp: typing.TextIO) -> str:
114+
# MSVC outputs the original file as the very first #line directive
115+
# so we just use that
116+
new_output = io.StringIO()
117+
keep = True
118+
119+
first = fp.readline()
120+
assert first.startswith("#line")
121+
fname = first[first.find('"') :]
122+
123+
for line in fp:
124+
if line.startswith("#line"):
125+
keep = line.endswith(fname)
126+
127+
if keep:
128+
new_output.write(line)
129+
130+
new_output.seek(0)
131+
return new_output.read()
132+
133+
134+
def make_msvc_preprocessor(
135+
*,
136+
defines: typing.List[str] = [],
137+
include_paths: typing.List[str] = [],
138+
retain_all_content: bool = False,
139+
encoding: typing.Optional[str] = None,
140+
msvc_args: typing.List[str] = ["cl.exe"],
141+
print_cmd: bool = True,
142+
) -> PreprocessorFunction:
143+
"""
144+
Creates a preprocessor function that uses cl.exe from Microsoft Visual Studio
145+
to preprocess the input text. cl.exe is not typically on the path, so you
146+
may need to open the correct developer tools shell or pass in the correct path
147+
to cl.exe in the `msvc_args` parameter.
148+
149+
cl.exe will throw an error if a file referenced by an #include directive is not found.
150+
151+
:param defines: list of #define macros specified as "key value"
152+
:param include_paths: list of directories to search for included files
153+
:param retain_all_content: If False, only the parsed file content will be retained
154+
:param encoding: If specified any include files are opened with this encoding
155+
:param msvc_args: This is the path to cl.exe and any extra args you might want
156+
:param print_cmd: Prints the command as its executed
157+
158+
.. code-block:: python
159+
160+
pp = make_msvc_preprocessor()
161+
options = ParserOptions(preprocessor=pp)
162+
163+
parse_file(content, options=options)
164+
165+
"""
166+
167+
if not encoding:
168+
encoding = "utf-8"
169+
170+
def _preprocess_file(filename: str, content: typing.Optional[str]) -> str:
171+
cmd = msvc_args + ["/nologo", "/E", "/C"]
172+
173+
for p in include_paths:
174+
cmd.append(f"/I{p}")
175+
for d in defines:
176+
cmd.append(f"/D{d.replace(' ', '=')}")
177+
178+
tfpname = None
179+
180+
try:
181+
kwargs = {"encoding": encoding}
182+
if filename == "<str>":
183+
if content is None:
184+
raise PreprocessorError("no content specified for stdin")
185+
186+
tfp = tempfile.NamedTemporaryFile(
187+
mode="w", encoding=encoding, suffix=".h", delete=False
188+
)
189+
tfpname = tfp.name
190+
tfp.write(content)
191+
tfp.close()
192+
193+
cmd.append(tfpname)
194+
else:
195+
cmd.append(filename)
196+
197+
if print_cmd:
198+
print("+", " ".join(cmd), file=sys.stderr)
199+
200+
result: str = subprocess.check_output(cmd, **kwargs) # type: ignore
201+
if not retain_all_content:
202+
result = _msvc_filter(io.StringIO(result))
203+
finally:
204+
if tfpname:
205+
os.unlink(tfpname)
206+
207+
return result
208+
209+
return _preprocess_file
210+
211+
107212
#
108213
# PCPP preprocessor support (not installed by default)
109214
#

tests/test_preprocessor.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
)
2727

2828

29-
@pytest.fixture(params=["gcc", "pcpp"])
29+
@pytest.fixture(params=["gcc", "msvc", "pcpp"])
3030
def make_pp(request) -> typing.Callable[..., PreprocessorFunction]:
3131
param = request.param
3232
if param == "gcc":
@@ -36,6 +36,12 @@ def make_pp(request) -> typing.Callable[..., PreprocessorFunction]:
3636

3737
subprocess.run([gcc_path, "--version"])
3838
return preprocessor.make_gcc_preprocessor
39+
elif param == "msvc":
40+
gcc_path = shutil.which("cl.exe")
41+
if not gcc_path:
42+
pytest.skip("cl.exe not found")
43+
44+
return preprocessor.make_msvc_preprocessor
3945
elif param == "pcpp":
4046
if preprocessor.pcpp is None:
4147
pytest.skip("pcpp not installed")

0 commit comments

Comments
 (0)