1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
|
#!/usr/bin/env python3
# encoding: utf-8
# Bálint Aradi, 2016-2021
'''General module for using Fypp as preprocessor.
This module implements the general framework for the Fypp preprocessor, but does
not bind it to any task generator. If you want to use it to preprocessor Fortran
source files (.fpp -> .f90), use the fypp_fortran module instead. Otherwise,
you can generate your own binding as ususal::
def build(bld):
bld(features='fypp', source=['test.fypp'])
from waflib import TaskGen
@TaskGen.extension('.fypp')
def process(self, node):
tsk = self.create_task('fypp_preprocessor', [node], node.change_ext('.out'))
The preprocessor understands the following uselib attributes:
* ``includes``: Directory/directories to search for include files
* ``modules``: Python module(s) to import before preprocessing starts
* ``defines``: Definition(s) to apply before preprocessing starts
* ``inifiles``: Python file(s) to execute before preprosessing starts
The example below demonstrates this::
def build(bld):
bld(features='fypp',
source=['trash.fypp'],
includes='include',
modules=['myfypp1', 'myfypp2'],
defines='TEST=1 QUIET',
inifiles='fyppini.py')
'''
import re
import os.path
from waflib import Configure, Logs, Task, TaskGen, Tools, Errors
try:
import fypp
except ImportError:
fypp = None
Tools.ccroot.USELIB_VARS['fypp'] = set([ 'DEFINES', 'INCLUDES', 'MODULES',
'INIFILES' ])
FYPP_INCPATH_ST = '-I%s'
FYPP_DEFINES_ST = '-D%s'
FYPP_LINENUM_FLAG = '-n'
FYPP_MODULES_ST = '-m%s'
FYPP_INIFILES_ST = '-i%s'
class FyppPreprocError(Errors.WafError):
pass
################################################################################
# Configure
################################################################################
def configure(conf):
fypp_check(conf)
fypp_add_user_flags(conf)
@Configure.conf
def fypp_add_user_flags(conf):
'''Import user settings for Fypp.'''
conf.add_os_flags('FYPP_FLAGS', dup=False)
@Configure.conf
def fypp_check(conf):
'''Check for Fypp.'''
conf.start_msg('Checking for fypp module')
if fypp is None:
conf.fatal('Python module \'fypp\' could not be imported.')
version = fypp.VERSION
version_regexp = re.compile(r'^(?P<major>\d+)\.(?P<minor>\d+)'\
'(?:\.(?P<patch>\d+))?$')
match = version_regexp.search(version)
if not match:
conf.fatal('cannot parse fypp version string')
version = (match.group('major'), match.group('minor'))
conf.env['FYPP_VERSION'] = version
conf.end_msg('found (version %s.%s)' % version)
################################################################################
# Build
################################################################################
class fypp_preprocessor(Task.Task):
def keyword(self):
return 'Preprocessing'
def run(self):
argparser = fypp.get_option_parser()
args = [FYPP_LINENUM_FLAG]
args += self.env.FYPP_FLAGS
args += [FYPP_DEFINES_ST % ss for ss in self.env['DEFINES']]
args += [FYPP_INCPATH_ST % ss for ss in self.env['INCLUDES']]
args += [FYPP_INIFILES_ST % ss for ss in self.env['INIFILES']]
args += [FYPP_MODULES_ST % ss for ss in self.env['MODULES']]
opts, leftover = argparser.parse_args(args)
infile = self.inputs[0].abspath()
outfile = self.outputs[0].abspath()
if Logs.verbose:
Logs.debug('runner: fypp.Fypp %r %r %r'
% (args, infile, outfile))
tool = fypp.Fypp(opts)
try:
tool.process_file(infile, outfile)
except fypp.FyppError as err:
msg = ("%s [%s:%d]"
% (err.msg, err.fname, err.span[0] + 1))
raise FyppPreprocError(msg)
return 0
def scan(self):
parser = FyppIncludeParser(self.generator.includes_nodes)
nodes, names = parser.parse(self.inputs[0])
if Logs.verbose:
Logs.debug('deps: deps for %r: %r; unresolved: %r'
% (self.inputs, nodes, names))
return (nodes, names)
TaskGen.feature('fypp')(Tools.ccroot.propagate_uselib_vars)
TaskGen.feature('fypp')(Tools.ccroot.apply_incpaths)
################################################################################
# Helper routines
################################################################################
class FyppIncludeParser(object):
'''Parser for include directives in files preprocessed by Fypp.
It can not handle conditional includes.
'''
# Include file pattern, opening and closing quoute must be replaced inside.
INCLUDE_PATTERN = re.compile(r'^\s*#:include\s*(["\'])(?P<incfile>.+?)\1',
re.MULTILINE)
def __init__(self, incpaths):
'''Initializes the parser.
:param quotes: Tuple containing the opening and closing quote sign.
:type quotes: tuple
'''
# Nodes still to be processed
self._waiting = []
# Files we have already processed
self._processed = set()
# List of dependent nodes
self._dependencies = []
# List of unresolved dependencies
self._unresolved = set()
# Paths to consider when checking for includes
self._incpaths = incpaths
def parse(self, node):
'''Parser the includes in a given node.
:return: Tuple with two elements: list of dependent nodes and list of
unresolved depencies.
'''
self._waiting = [ node, ]
# self._waiting is eventually extended during _process() -> iterate
while self._waiting:
curnode = self._waiting.pop(0)
self._process(curnode)
return (self._dependencies, list(self._unresolved))
def _process(self, node):
incfiles = self._get_include_files(node)
for incfile in incfiles:
if incfile in self._processed:
continue
self._processed.add(incfile)
incnode = self._find_include_node(node, incfile)
if incnode:
self._dependencies.append(incnode)
self._waiting.append(incnode)
else:
self._unresolved.add(incfile)
def _get_include_files(self, node):
txt = node.read()
matches = self.INCLUDE_PATTERN.finditer(txt)
incs = [ match.group('incfile') for match in matches ]
return incs
def _find_include_node(self, node, filename):
for incpath in self._incpaths:
incnode = incpath.find_resource(filename)
if incnode:
break
else:
incnode = node.parent.find_resource(filename)
return incnode
|