Skip to content

Commit

Permalink
Add support for writing a Make compatible depfile
Browse files Browse the repository at this point in the history
This allows build systems that can consume them (Make and Ninja, in
particular) to figure out at build time which files are implicitly
included in the dependency graph of a fypp output.

This means for example if you have a target that includes two other
files, an incremental rebuild can be triggered automatically if one of
the included files are modified.
  • Loading branch information
dcbaker committed Apr 8, 2024
1 parent 6971698 commit 24bbeff
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 0 deletions.
37 changes: 37 additions & 0 deletions bin/fypp
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,13 @@ class Parser:
# Directory of current file
self._curdir = None

# All files that have been included
self._included_files = []


def get_dependencies(self):
return self._included_files


def parsefile(self, fobj):
'''Parses file or a file like object.
Expand All @@ -252,6 +259,9 @@ class Parser:


def _includefile(self, span, fobj, fname, curdir):
# Don't add the root file, only later includes
if self._curfile:
self._included_files.append(fname)
oldfile = self._curfile
olddir = self._curdir
self._curfile = fname
Expand Down Expand Up @@ -2413,6 +2423,10 @@ class Processor:
return self._render()


def get_dependencies(self):
return self._parser.get_dependencies()


def _render(self):
output = self._renderer.render(self._builder.tree)
self._builder.reset()
Expand Down Expand Up @@ -2574,6 +2588,16 @@ class Fypp:
return None


def write_dependencies(self, outfile, depfile):
def quote(text):
return text.replace('$', '$$').replace(' ', '\\ ').replace('#', '\\#')

dependencies = [quote(d) for d in self._preprocessor.get_dependencies()]

with open(depfile, 'w', encoding='utf-8') as f:
f.write('{}: {}'.format(quote(outfile), ' '.join(dependencies)))


def process_text(self, txt):
'''Processes a string.
Expand Down Expand Up @@ -2666,6 +2690,8 @@ class FyppOptions(optparse.Values):
setting.
create_parent_folder (bool): Whether the parent folder for the output
file should be created if it does not exist. Default: False.
depfile (str | None): If set, where to write a Makefile compatible
dependency file. Default: None.
'''

def __init__(self):
Expand All @@ -2685,6 +2711,7 @@ class FyppOptions(optparse.Values):
self.encoding = 'utf-8'
self.create_parent_folder = False
self.file_var_root = None
self.depfile = None


class FortranLineFolder:
Expand Down Expand Up @@ -2911,6 +2938,10 @@ def get_option_parser():
parser.add_option('--file-var-root', metavar='DIR', dest='file_var_root',
default=defs.file_var_root, help=msg)

msg = 'Write a Make-compatible dependency file to this location'
parser.add_option('--depfile', metavar='DEPFILE', dest='depfile',
default=defs.depfile, help=msg)

return parser


Expand All @@ -2921,9 +2952,15 @@ def run_fypp():
opts, leftover = optparser.parse_args(values=options)
infile = leftover[0] if len(leftover) > 0 else '-'
outfile = leftover[1] if len(leftover) > 1 else '-'

if outfile == '-' and opts.depfile:
raise optparse.OptParseError("--depfile cannot be used when writing to stdout")

try:
tool = Fypp(opts)
tool.process_file(infile, outfile)
if opts.depfile:
tool.write_dependencies(outfile, opts.depfile)
except FyppStopRequest as exc:
sys.stderr.write(_formatted_exception(exc))
sys.exit(USER_ERROR_EXIT_CODE)
Expand Down
1 change: 1 addition & 0 deletions test/include/escaped_includes.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#:include 'need$ #escape.inc'
2 changes: 2 additions & 0 deletions test/include/multi_includes.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#:include 'fypp1.inc'
#:include 'fypp2.inc'
1 change: 1 addition & 0 deletions test/include/subfolder/need$ #escape.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#:include 'fypp2.inc'
62 changes: 62 additions & 0 deletions test/test_fypp.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import platform
import sys
import tempfile
import unittest

# Allow for importing fypp
Expand Down Expand Up @@ -2948,6 +2949,27 @@ def _importmodule(module):
),
]

DEPFILE_TESTS = [
('basic',
([_incdir('include')],
'include/subfolder/include_fypp1.inc',
'{output}: include/fypp1.inc',
)
),
('multiple includes',
([_incdir('include/subfolder')],
'include/multi_includes.inc',
'{output}: include/fypp1.inc include/subfolder/fypp2.inc',
)
),
('escapes',
([_incdir('include'), _incdir('include/subfolder')],
'include/escaped_includes.inc',
'{output}: include/subfolder/need$$\\ \\#escape.inc include/subfolder/fypp2.inc',
)
),
]


def _get_test_output_method(args, inp, out):
'''Returns a test method for checking correctness of Fypp output.
Expand Down Expand Up @@ -2995,6 +3017,36 @@ def test_output_from_file_input(self):
return test_output_from_file_input


def _get_test_depfile_method(args, inputfile, expected):
'''Returns a test method for checking correctness of depfile.
Args:
args (list of str): Command-line arguments to pass to Fypp.
inputfile (str): Input file with Fypp directives.
out (str): Expected output.
Returns:
method: Method to test equality of depfile with result delivered by Fypp.
'''

def test_depfile(self):
'''Tests whether Fypp result matches expected output when input is in a file.'''
output = self._get_tempfile()
depfile = self._get_tempfile()

optparser = fypp.get_option_parser()
options, leftover = optparser.parse_args(args + ['--depfile', depfile])
self.assertEqual(len(leftover), 0)
tool = fypp.Fypp(options)
tool.process_file(inputfile, output)
tool.write_dependencies(output, depfile)

with open(depfile, 'r', encoding='utf-8') as f:
got = f.read().strip()
self.assertEqual(got, expected.format(output=output))
return test_depfile



def _get_test_exception_method(args, inp, exceptions):
'''Returns a test method for checking correctness of thrown exception.
Expand Down Expand Up @@ -3092,6 +3144,16 @@ class ExceptionTest(_TestContainer): pass
class ImportTest(_TestContainer): pass
ImportTest.add_test_methods(IMPORT_TESTS, _get_test_output_method)

class DepfileTest(_TestContainer):

def _get_tempfile(self):
_fd, output = tempfile.mkstemp()
os.close(_fd)
self.addCleanup(os.unlink, output)
return output

DepfileTest.add_test_methods(DEPFILE_TESTS, _get_test_depfile_method)


if __name__ == '__main__':
unittest.main()

0 comments on commit 24bbeff

Please sign in to comment.