77import os
88import subprocess
99import sys
10+ import tempfile
1011import typing
1112
1213from .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 :
0 commit comments