Skip to content

Commit 2306800

Browse files
committed
Add support for generating depfiles for pcpp/gcc
1 parent 131d92d commit 2306800

File tree

3 files changed

+133
-9
lines changed

3 files changed

+133
-9
lines changed

cxxheaderparser/dump.py

+35-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import argparse
22
import dataclasses
33
import json
4+
import pathlib
45
import pprint
56
import subprocess
67
import sys
@@ -28,23 +29,50 @@ def dumpmain() -> None:
2829
parser.add_argument(
2930
"--pcpp", default=False, action="store_true", help="Use pcpp preprocessor"
3031
)
32+
parser.add_argument(
33+
"--gcc", default=False, action="store_true", help="Use GCC as preprocessor"
34+
)
35+
parser.add_argument(
36+
"--depfile",
37+
default=None,
38+
type=pathlib.Path,
39+
help="Generate a depfile (requires preprocessor)",
40+
)
41+
parser.add_argument(
42+
"--deptarget", default=[], action="append", help="depfile target"
43+
)
3144
parser.add_argument(
3245
"--encoding", default=None, help="Use this encoding to open the file"
3346
)
3447

3548
args = parser.parse_args()
3649

50+
pp_kwargs = dict(encoding=args.encoding)
51+
52+
if args.depfile:
53+
if not (args.pcpp or args.gcc):
54+
parser.error("--depfile requires either --pcpp or --gcc")
55+
56+
pp_kwargs["depfile"] = args.depfile
57+
pp_kwargs["deptarget"] = args.deptarget
58+
3759
preprocessor = None
38-
if args.pcpp or args.mode == "pponly":
60+
if args.gcc:
61+
from .preprocessor import make_gcc_preprocessor
62+
63+
preprocessor = make_gcc_preprocessor(**pp_kwargs)
64+
65+
if args.pcpp or (args.mode == "pponly" and preprocessor is None):
3966
from .preprocessor import make_pcpp_preprocessor
4067

41-
preprocessor = make_pcpp_preprocessor(encoding=args.encoding)
68+
preprocessor = make_pcpp_preprocessor(**pp_kwargs)
4269

43-
if args.mode == "pponly":
44-
with open(args.header, "r", encoding=args.encoding) as fp:
45-
pp_content = preprocessor(args.header, fp.read())
46-
sys.stdout.write(pp_content)
47-
sys.exit(0)
70+
if args.mode == "pponly":
71+
assert preprocessor is not None
72+
with open(args.header, "r", encoding=args.encoding) as fp:
73+
pp_content = preprocessor(args.header, fp.read())
74+
sys.stdout.write(pp_content)
75+
sys.exit(0)
4876

4977
options = ParserOptions(verbose=args.verbose, preprocessor=preprocessor)
5078
data = parse_file(args.header, encoding=args.encoding, options=options)

cxxheaderparser/preprocessor.py

+54-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
"""
44

55
import io
6+
import pathlib
67
import re
78
import os
9+
import os.path
810
import subprocess
911
import sys
1012
import tempfile
@@ -48,6 +50,8 @@ def make_gcc_preprocessor(
4850
encoding: typing.Optional[str] = None,
4951
gcc_args: typing.List[str] = ["g++"],
5052
print_cmd: bool = True,
53+
depfile: typing.Optional[pathlib.Path] = None,
54+
deptarget: typing.Optional[typing.List[str]] = None,
5155
) -> PreprocessorFunction:
5256
"""
5357
Creates a preprocessor function that uses g++ to preprocess the input text.
@@ -62,6 +66,9 @@ def make_gcc_preprocessor(
6266
:param encoding: If specified any include files are opened with this encoding
6367
:param gcc_args: This is the path to G++ and any extra args you might want
6468
:param print_cmd: Prints the gcc command as its executed
69+
:param depfile: If specified, will generate a preprocessor depfile that contains
70+
a list of include files that were parsed. Must also specify deptarget.
71+
:param deptarget: List of targets to put in the depfile
6572
6673
.. code-block:: python
6774
@@ -93,6 +100,16 @@ def _preprocess_file(filename: str, content: typing.Optional[str]) -> str:
93100
else:
94101
cmd.append(filename)
95102

103+
if depfile is not None:
104+
if deptarget is None:
105+
raise PreprocessorError(
106+
"must specify deptarget if depfile is specified"
107+
)
108+
cmd.append("-MD")
109+
for target in deptarget:
110+
cmd += ["-MQ", target]
111+
cmd += ["-MF", str(depfile)]
112+
96113
if print_cmd:
97114
print("+", " ".join(cmd), file=sys.stderr)
98115

@@ -242,7 +259,9 @@ def on_comment(self, *ignored):
242259
pcpp = None
243260

244261

245-
def _pcpp_filter(fname: str, fp: typing.TextIO) -> str:
262+
def _pcpp_filter(
263+
fname: str, fp: typing.TextIO, deps: typing.Optional[typing.Dict[str, bool]]
264+
) -> str:
246265
# the output of pcpp includes the contents of all the included files, which
247266
# isn't what a typical user of cxxheaderparser would want, so we strip out
248267
# the line directives and any content that isn't in our original file
@@ -255,6 +274,9 @@ def _pcpp_filter(fname: str, fp: typing.TextIO) -> str:
255274
for line in fp:
256275
if line.startswith("#line"):
257276
keep = line.endswith(line_ending)
277+
if deps is not None:
278+
start = line.find('"')
279+
deps[line[start + 1 : -2]] = True
258280

259281
if keep:
260282
new_output.write(line)
@@ -270,6 +292,8 @@ def make_pcpp_preprocessor(
270292
retain_all_content: bool = False,
271293
encoding: typing.Optional[str] = None,
272294
passthru_includes: typing.Optional["re.Pattern"] = None,
295+
depfile: typing.Optional[pathlib.Path] = None,
296+
deptarget: typing.Optional[typing.List[str]] = None,
273297
) -> PreprocessorFunction:
274298
"""
275299
Creates a preprocessor function that uses pcpp (which must be installed
@@ -285,6 +309,10 @@ def make_pcpp_preprocessor(
285309
:param encoding: If specified any include files are opened with this encoding
286310
:param passthru_includes: If specified any #include directives that match the
287311
compiled regex pattern will be part of the output.
312+
:param depfile: If specified, will generate a preprocessor depfile that contains
313+
a list of include files that were parsed. Must also specify deptarget.
314+
Not compatible with retain_all_content
315+
:param deptarget: List of targets to put in the depfile
288316
289317
.. code-block:: python
290318
@@ -309,6 +337,8 @@ def _preprocess_file(filename: str, content: typing.Optional[str]) -> str:
309337

310338
if not retain_all_content:
311339
pp.line_directive = "#line"
340+
elif depfile:
341+
raise PreprocessorError("retain_all_content and depfile not compatible")
312342

313343
if content is None:
314344
with open(filename, "r", encoding=encoding) as fp:
@@ -327,6 +357,16 @@ def _preprocess_file(filename: str, content: typing.Optional[str]) -> str:
327357
if retain_all_content:
328358
return fp.read()
329359
else:
360+
deps: typing.Optional[typing.Dict[str, bool]] = None
361+
target = None
362+
if depfile:
363+
deps = {}
364+
if not deptarget:
365+
base, _ = os.path.splitext(filename)
366+
target = f"{base}.o"
367+
else:
368+
target = " ".join(deptarget)
369+
330370
# pcpp emits the #line directive using the filename you pass in
331371
# but will rewrite it if it's on the include path it uses. This
332372
# is copied from pcpp:
@@ -339,6 +379,18 @@ def _preprocess_file(filename: str, content: typing.Optional[str]) -> str:
339379
filename = filename.replace(os.sep, "/")
340380
break
341381

342-
return _pcpp_filter(filename, fp)
382+
filtered = _pcpp_filter(filename, fp, deps)
383+
384+
if depfile is not None:
385+
assert deps is not None
386+
with open(depfile, "w") as fp:
387+
fp.write(f"{target}:")
388+
for dep in reversed(list(deps.keys())):
389+
dep = dep.replace("\\", "\\\\")
390+
dep = dep.replace(" ", "\\ ")
391+
fp.write(f" \\\n {dep}")
392+
fp.write("\n")
393+
394+
return filtered
343395

344396
return _preprocess_file

tests/test_preprocessor.py

+44
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,47 @@ def test_preprocessor_passthru_includes(tmp_path: pathlib.Path) -> None:
202202
assert data == ParsedData(
203203
namespace=NamespaceScope(), includes=[Include(filename='"t2.h"')]
204204
)
205+
206+
207+
def test_preprocessor_depfile(
208+
make_pp: typing.Callable[..., PreprocessorFunction],
209+
tmp_path: pathlib.Path,
210+
) -> None:
211+
212+
tmp_path = tmp_path / "hard path"
213+
tmp_path.mkdir(parents=True, exist_ok=True)
214+
215+
# not supported
216+
if make_pp is preprocessor.make_msvc_preprocessor:
217+
return
218+
219+
h_content = '#include "t2.h"' "\n" "int x = X;\n"
220+
h2_content = '#include "t3.h"\n' "#define X 2\n" "int omitted = 1;\n"
221+
h3_content = "int h3;"
222+
223+
with open(tmp_path / "t1.h", "w") as fp:
224+
fp.write(h_content)
225+
226+
with open(tmp_path / "t2.h", "w") as fp:
227+
fp.write(h2_content)
228+
229+
with open(tmp_path / "t3.h", "w") as fp:
230+
fp.write(h3_content)
231+
232+
depfile = tmp_path / "t1.d"
233+
deptarget = ["tgt"]
234+
235+
options = ParserOptions(preprocessor=make_pp(depfile=depfile, deptarget=deptarget))
236+
parse_file(tmp_path / "t1.h", options=options)
237+
238+
with open(depfile) as fp:
239+
depcontent = fp.read()
240+
241+
assert depcontent.startswith("tgt:")
242+
deps = [d.strip() for d in depcontent[4:].strip().split("\\\n")]
243+
deps = [d.replace("\\ ", " ") for d in deps if d]
244+
245+
# gcc will insert extra paths of predefined stuff, so just make sure this is sane
246+
assert str(tmp_path / "t1.h") in deps
247+
assert str(tmp_path / "t2.h") in deps
248+
assert str(tmp_path / "t3.h") in deps

0 commit comments

Comments
 (0)