Skip to content

Commit e23c4db

Browse files
authored
Merge pull request #78 from robotpy/msvc-preprocessor
Add MSVC compatible preprocessing function
2 parents d94df61 + 196e88b commit e23c4db

File tree

6 files changed

+140
-8
lines changed

6 files changed

+140
-8
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/options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import Callable, Optional
33

44
#: arguments are (filename, content)
5-
PreprocessorFunction = Callable[[str, str], str]
5+
PreprocessorFunction = Callable[[str, Optional[str]], str]
66

77

88
@dataclass

cxxheaderparser/parser.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,10 @@ class CxxParser:
7474
def __init__(
7575
self,
7676
filename: str,
77-
content: str,
77+
content: typing.Optional[str],
7878
visitor: CxxVisitor,
7979
options: typing.Optional[ParserOptions] = None,
80+
encoding: typing.Optional[str] = None,
8081
) -> None:
8182
self.visitor = visitor
8283
self.filename = filename
@@ -85,6 +86,13 @@ def __init__(
8586
if options and options.preprocessor is not None:
8687
content = options.preprocessor(filename, content)
8788

89+
if content is None:
90+
if encoding is None:
91+
encoding = "utf-8-sig"
92+
93+
with open(filename, "r", encoding=encoding) as fp:
94+
content = fp.read()
95+
8896
self.lex: lexer.TokenStream = lexer.LexerTokenStream(filename, content)
8997

9098
global_ns = NamespaceDecl([], False)

cxxheaderparser/preprocessor.py

Lines changed: 113 additions & 2 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
@@ -74,7 +75,7 @@ def make_gcc_preprocessor(
7475
if not encoding:
7576
encoding = "utf-8"
7677

77-
def _preprocess_file(filename: str, content: str) -> str:
78+
def _preprocess_file(filename: str, content: typing.Optional[str]) -> str:
7879
cmd = gcc_args + ["-w", "-E", "-C"]
7980

8081
for p in include_paths:
@@ -86,6 +87,8 @@ def _preprocess_file(filename: str, content: str) -> str:
8687
if filename == "<str>":
8788
cmd.append("-")
8889
filename = "<stdin>"
90+
if content is None:
91+
raise PreprocessorError("no content specified for stdin")
8992
kwargs["input"] = content
9093
else:
9194
cmd.append(filename)
@@ -102,6 +105,110 @@ def _preprocess_file(filename: str, content: str) -> str:
102105
return _preprocess_file
103106

104107

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+
105212
#
106213
# PCPP preprocessor support (not installed by default)
107214
#
@@ -191,7 +298,7 @@ def make_pcpp_preprocessor(
191298
if pcpp is None:
192299
raise PreprocessorError("pcpp is not installed")
193300

194-
def _preprocess_file(filename: str, content: str) -> str:
301+
def _preprocess_file(filename: str, content: typing.Optional[str]) -> str:
195302
pp = _CustomPreprocessor(encoding, passthru_includes)
196303
if include_paths:
197304
for p in include_paths:
@@ -203,6 +310,10 @@ def _preprocess_file(filename: str, content: str) -> str:
203310
if not retain_all_content:
204311
pp.line_directive = "#line"
205312

313+
if content is None:
314+
with open(filename, "r", encoding=encoding) as fp:
315+
content = fp.read()
316+
206317
pp.parse(content, filename)
207318

208319
if pp.errors:

cxxheaderparser/simple.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,10 @@ def parse_file(
348348
if filename == "-":
349349
content = sys.stdin.read()
350350
else:
351-
with open(filename, encoding=encoding) as fp:
352-
content = fp.read()
351+
content = None
353352

354-
return parse_string(content, filename=filename, options=options)
353+
visitor = SimpleCxxVisitor()
354+
parser = CxxParser(filename, content, visitor, options)
355+
parser.parse()
356+
357+
return visitor.data

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)