Merge pull request #78 from robotpy/msvc-preprocessor
Add MSVC compatible preprocessing function
This commit is contained in:
commit
e23c4db96d
4
.github/workflows/dist.yml
vendored
4
.github/workflows/dist.yml
vendored
@ -112,6 +112,10 @@ jobs:
|
|||||||
- name: Install test dependencies
|
- name: Install test dependencies
|
||||||
run: python -m pip --disable-pip-version-check install -r tests/requirements.txt
|
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
|
- name: Test wheel
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
@ -2,7 +2,7 @@ from dataclasses import dataclass
|
|||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
#: arguments are (filename, content)
|
#: arguments are (filename, content)
|
||||||
PreprocessorFunction = Callable[[str, str], str]
|
PreprocessorFunction = Callable[[str, Optional[str]], str]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -74,9 +74,10 @@ class CxxParser:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
filename: str,
|
filename: str,
|
||||||
content: str,
|
content: typing.Optional[str],
|
||||||
visitor: CxxVisitor,
|
visitor: CxxVisitor,
|
||||||
options: typing.Optional[ParserOptions] = None,
|
options: typing.Optional[ParserOptions] = None,
|
||||||
|
encoding: typing.Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.visitor = visitor
|
self.visitor = visitor
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
@ -85,6 +86,13 @@ class CxxParser:
|
|||||||
if options and options.preprocessor is not None:
|
if options and options.preprocessor is not None:
|
||||||
content = options.preprocessor(filename, content)
|
content = options.preprocessor(filename, content)
|
||||||
|
|
||||||
|
if content is None:
|
||||||
|
if encoding is None:
|
||||||
|
encoding = "utf-8-sig"
|
||||||
|
|
||||||
|
with open(filename, "r", encoding=encoding) as fp:
|
||||||
|
content = fp.read()
|
||||||
|
|
||||||
self.lex: lexer.TokenStream = lexer.LexerTokenStream(filename, content)
|
self.lex: lexer.TokenStream = lexer.LexerTokenStream(filename, content)
|
||||||
|
|
||||||
global_ns = NamespaceDecl([], False)
|
global_ns = NamespaceDecl([], False)
|
||||||
|
@ -7,6 +7,7 @@ import re
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from .options import PreprocessorFunction
|
from .options import PreprocessorFunction
|
||||||
@ -74,7 +75,7 @@ def make_gcc_preprocessor(
|
|||||||
if not encoding:
|
if not encoding:
|
||||||
encoding = "utf-8"
|
encoding = "utf-8"
|
||||||
|
|
||||||
def _preprocess_file(filename: str, content: str) -> str:
|
def _preprocess_file(filename: str, content: typing.Optional[str]) -> str:
|
||||||
cmd = gcc_args + ["-w", "-E", "-C"]
|
cmd = gcc_args + ["-w", "-E", "-C"]
|
||||||
|
|
||||||
for p in include_paths:
|
for p in include_paths:
|
||||||
@ -86,6 +87,8 @@ def make_gcc_preprocessor(
|
|||||||
if filename == "<str>":
|
if filename == "<str>":
|
||||||
cmd.append("-")
|
cmd.append("-")
|
||||||
filename = "<stdin>"
|
filename = "<stdin>"
|
||||||
|
if content is None:
|
||||||
|
raise PreprocessorError("no content specified for stdin")
|
||||||
kwargs["input"] = content
|
kwargs["input"] = content
|
||||||
else:
|
else:
|
||||||
cmd.append(filename)
|
cmd.append(filename)
|
||||||
@ -102,6 +105,110 @@ def make_gcc_preprocessor(
|
|||||||
return _preprocess_file
|
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()
|
||||||
|
|
||||||
|
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)
|
# PCPP preprocessor support (not installed by default)
|
||||||
#
|
#
|
||||||
@ -191,7 +298,7 @@ def make_pcpp_preprocessor(
|
|||||||
if pcpp is None:
|
if pcpp is None:
|
||||||
raise PreprocessorError("pcpp is not installed")
|
raise PreprocessorError("pcpp is not installed")
|
||||||
|
|
||||||
def _preprocess_file(filename: str, content: str) -> str:
|
def _preprocess_file(filename: str, content: typing.Optional[str]) -> str:
|
||||||
pp = _CustomPreprocessor(encoding, passthru_includes)
|
pp = _CustomPreprocessor(encoding, passthru_includes)
|
||||||
if include_paths:
|
if include_paths:
|
||||||
for p in include_paths:
|
for p in include_paths:
|
||||||
@ -203,6 +310,10 @@ def make_pcpp_preprocessor(
|
|||||||
if not retain_all_content:
|
if not retain_all_content:
|
||||||
pp.line_directive = "#line"
|
pp.line_directive = "#line"
|
||||||
|
|
||||||
|
if content is None:
|
||||||
|
with open(filename, "r", encoding=encoding) as fp:
|
||||||
|
content = fp.read()
|
||||||
|
|
||||||
pp.parse(content, filename)
|
pp.parse(content, filename)
|
||||||
|
|
||||||
if pp.errors:
|
if pp.errors:
|
||||||
|
@ -348,7 +348,10 @@ def parse_file(
|
|||||||
if filename == "-":
|
if filename == "-":
|
||||||
content = sys.stdin.read()
|
content = sys.stdin.read()
|
||||||
else:
|
else:
|
||||||
with open(filename, encoding=encoding) as fp:
|
content = None
|
||||||
content = fp.read()
|
|
||||||
|
|
||||||
return parse_string(content, filename=filename, options=options)
|
visitor = SimpleCxxVisitor()
|
||||||
|
parser = CxxParser(filename, content, visitor, options)
|
||||||
|
parser.parse()
|
||||||
|
|
||||||
|
return visitor.data
|
||||||
|
@ -26,7 +26,7 @@ from cxxheaderparser.types import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=["gcc", "pcpp"])
|
@pytest.fixture(params=["gcc", "msvc", "pcpp"])
|
||||||
def make_pp(request) -> typing.Callable[..., PreprocessorFunction]:
|
def make_pp(request) -> typing.Callable[..., PreprocessorFunction]:
|
||||||
param = request.param
|
param = request.param
|
||||||
if param == "gcc":
|
if param == "gcc":
|
||||||
@ -36,6 +36,12 @@ def make_pp(request) -> typing.Callable[..., PreprocessorFunction]:
|
|||||||
|
|
||||||
subprocess.run([gcc_path, "--version"])
|
subprocess.run([gcc_path, "--version"])
|
||||||
return preprocessor.make_gcc_preprocessor
|
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":
|
elif param == "pcpp":
|
||||||
if preprocessor.pcpp is None:
|
if preprocessor.pcpp is None:
|
||||||
pytest.skip("pcpp not installed")
|
pytest.skip("pcpp not installed")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user