Skip to content

Commit

Permalink
Add MSVC preprocessor support
Browse files Browse the repository at this point in the history
  • Loading branch information
virtuald committed Oct 8, 2023
1 parent 3d23375 commit f568590
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 1 deletion.
4 changes: 4 additions & 0 deletions .github/workflows/dist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ jobs:
- name: Install test dependencies
run: python -m pip --disable-pip-version-check install -r tests/requirements.txt

- name: Setup MSVC compiler
uses: ilammy/msvc-dev-cmd@v1
if: matrix.os == 'windows-latest'

- name: Test wheel
shell: bash
run: |
Expand Down
105 changes: 105 additions & 0 deletions cxxheaderparser/preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import os
import subprocess
import sys
import tempfile
import typing

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


#
# Microsoft Visual Studio preprocessor support
#


def _msvc_filter(fp: typing.TextIO) -> str:
# MSVC outputs the original file as the very first #line directive
# so we just use that
new_output = io.StringIO()
keep = True

first = fp.readline()
assert first.startswith("#line")
fname = first[first.find('"') :]

for line in fp:
if line.startswith("#line"):
keep = line.endswith(fname)

if keep:
new_output.write(line)

new_output.seek(0)
return new_output.read()


def make_msvc_preprocessor(
*,
defines: typing.List[str] = [],
include_paths: typing.List[str] = [],
retain_all_content: bool = False,
encoding: typing.Optional[str] = None,
msvc_args: typing.List[str] = ["cl.exe"],
print_cmd: bool = True,
) -> PreprocessorFunction:
"""
Creates a preprocessor function that uses cl.exe from Microsoft Visual Studio
to preprocess the input text. cl.exe is not typically on the path, so you
may need to open the correct developer tools shell or pass in the correct path
to cl.exe in the `msvc_args` parameter.
cl.exe will throw an error if a file referenced by an #include directive is not found.
:param defines: list of #define macros specified as "key value"
:param include_paths: list of directories to search for included files
:param retain_all_content: If False, only the parsed file content will be retained
:param encoding: If specified any include files are opened with this encoding
:param msvc_args: This is the path to cl.exe and any extra args you might want
:param print_cmd: Prints the command as its executed
.. code-block:: python
pp = make_msvc_preprocessor()
options = ParserOptions(preprocessor=pp)
parse_file(content, options=options)
"""

if not encoding:
encoding = "utf-8"

def _preprocess_file(filename: str, content: typing.Optional[str]) -> str:
cmd = msvc_args + ["/nologo", "/E", "/C"]

for p in include_paths:
cmd.append(f"/I{p}")
for d in defines:
cmd.append(f"/D{d.replace(' ', '=')}")

tfpname = None

try:
kwargs = {"encoding": encoding}
if filename == "<str>":
if content is None:
raise PreprocessorError("no content specified for stdin")

tfp = tempfile.NamedTemporaryFile(
mode="w", encoding=encoding, suffix=".h", delete=False
)
tfpname = tfp.name
tfp.write(content)
tfp.close()
# tfp.seek(0)
cmd.append(tfpname)
else:
cmd.append(filename)

if print_cmd:
print("+", " ".join(cmd), file=sys.stderr)

result: str = subprocess.check_output(cmd, **kwargs) # type: ignore
if not retain_all_content:
result = _msvc_filter(io.StringIO(result))
finally:
if tfpname:
os.unlink(tfpname)

return result

return _preprocess_file


#
# PCPP preprocessor support (not installed by default)
#
Expand Down
8 changes: 7 additions & 1 deletion tests/test_preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
)


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

subprocess.run([gcc_path, "--version"])
return preprocessor.make_gcc_preprocessor
elif param == "msvc":
gcc_path = shutil.which("cl.exe")
if not gcc_path:
pytest.skip("cl.exe not found")

return preprocessor.make_msvc_preprocessor
elif param == "pcpp":
if preprocessor.pcpp is None:
pytest.skip("pcpp not installed")
Expand Down

0 comments on commit f568590

Please sign in to comment.