#!/usr/bin/env python3
'''Usage: makemake90 [src=...] [obj=...] [mod=...] [bin=...]
Optional arguments specify places for source (.f90), object (.o), module
(.mod), and executable files. They all default to the currect directory.
There is no need to specify them again when updating existing makefiles.
'''
__version__ = '0.3'
import os
import re
import sys
[docs]
def parameters(*filenames, **args):
"""Fetch parameters from existing makefile and command arguments.
Parameters
----------
*filenames : str
Possible names of makefile.
**args : str
Default command arguments.
Returns
-------
str
Name of makefile.
str
Static makefile header.
str
Static makefile footer.
dict of str
Command arguments.
"""
if not filenames:
filenames = ['GNUmakefile', 'makefile', 'Makefile']
preamble = ''
epilogue = ''
for filename in filenames:
if os.path.exists(filename):
with open(filename) as makefile:
for line in makefile:
if 'generated by makemake' in line:
for arg in re.findall(r'\w+=[^\s:=]+', line):
sys.argv.insert(1, arg)
break
preamble += line
else:
raise SystemExit('Unknown Makefile already exists')
for line in makefile:
if 'not generated by makemake' in line:
break
for line in makefile:
epilogue += line
break
for arg in sys.argv[1:]:
if '=' in arg:
key, value = arg.split('=')
args[key] = value
else:
raise SystemExit(__doc__.rstrip())
preamble = preamble.strip()
epilogue = epilogue.strip()
return filename, preamble, epilogue, args
[docs]
def dependencies(src='.', obj='.', bin='.', **ignore):
"""Determine dependencies of Fortran project.
Parameters
----------
src : str, default '.'
Directory with source files.
obj : str, default '.'
Directory for object files.
bin : str, default '.'
Directory for executable binary files.
Returns
-------
dict of set
Programs with direct and indirect dependencies.
dict of set
Objects with direct dependencies.
set
All objects used.
"""
references = {}
companions = {}
components = {}
folders = [src]
def preprocess(lines):
for line in lines:
line = re.sub('!.*', '', line).strip()
if line:
yield line
for folder in folders:
for file in os.listdir(folder):
if file.startswith('.'):
continue
path = folder + '/' + file
if os.path.isdir(path):
folders.append(path)
elif path.endswith('.f90'):
doto = re.sub('^%s/' % src, '%s/' % obj, path)
doto = re.sub(r'\.f90$', '.o', doto)
references[doto] = set()
in_module = False
with open(path) as code:
code = preprocess(code)
for line in code:
while line.endswith('&'):
line = line.rstrip('&')
line += next(code).lstrip('&')
match = re.match(r'(use(?:\b.*::)?|program|module)'
r'\s+(\w+)\s*(?:$|,)', line, re.I)
if match:
statement, name = match.groups()
if statement == 'use':
references[doto].add(name)
elif statement == 'module':
companions[name] = doto
in_module = True
elif statement == 'program':
components['%s/%s' % (bin,
name.replace('_dot_', '.'))] = {doto}
match = re.match(r'.*\bexternal\b.*::(.*)', line, re.I)
if match:
for name in re.findall(r'\w+', match.group(1)):
references[doto].add(name)
if in_module:
if re.match('end module', line, re.I):
in_module = False
else:
match = re.match('(subroutine|function)'
r'\s+(\w+)', line, re.I)
if match:
companions[match.group(2)] = doto
for target, modules in references.items():
references[target] = set(companions[name]
for name in modules if name in companions)
related = set()
for doto in components.values():
todo = list(doto)
for target in todo:
new = references[target] - doto
doto |= new
todo += new
related |= doto
for target in list(references.keys()):
if target not in related:
del references[target]
else:
references[target].discard(target)
return components, references, related
[docs]
def makefile(filename='Makefile', components={}, references={}, related=None,
preamble='', epilogue='', src='.', obj='.', mod='.', bin='.', **ignore):
"""Create makefile.
Parameters
----------
filename : str, default 'Makefile'
Name of makefile.
components : dict of set, default {}
Programs with direct and indirect dependencies.
references : dict of set, default {}
Objects with direct dependencies.
related : set, default None
All objects used. Inferred from `components` if absent.
preamble : str, default ''
Static makefile header.
epilogue : str, default ''
Static makefile footer.
src : str, default '.'
Directory with source files.
obj : str, default '.'
Directory for object files.
mod : str, default '.'
Directory for module files.
bin : str, default '.'
Directory for executable binary files.
"""
if related is None:
related = set().union(*components.values())
args = dict(src=src, obj=obj, mod=mod, bin=bin)
def join(these):
return ' '.join(sorted(these))
programs = join(components)
adjuncts = join(related)
def listing(dependencies):
return '\n'.join(target + ': ' + join(doto)
for target, doto in sorted(dependencies.items()) if doto)
components = listing(components)
references = listing(references)
arguments = ''.join(' %s=%s' % (key, value)
for key, value in sorted(args.items()) if value != '.')
modules_flag = '' if mod == '.' else '''
modules_gfortran = -J{0}
modules_ifort = -module {0}
modules_ifx = ${{modules_ifort}}
override FFLAGS += ${{modules_$(FC)}}
'''.format(mod)
content = '''
# generated by makemake90{arguments}:
{modules_flag}
needless += {adjuncts} {mod}/*.mod
programs = {programs}
.PHONY: all clean cleaner
all: $(programs)
clean:
\trm -f $(needless)
cleaner: clean
\trm -f $(programs)
$(programs):
\t$(FC) $(FFLAGS) -o $@ $^ $(LDLIBS)
{obj}/%.o: {src}/%.f90
\t$(FC) $(FFLAGS) -c $< -o $@
{components}
{references}
'''.format(**vars())
content = re.sub(r'(^|\s)\./', r'\1', content)
with open(filename, 'w') as makefile:
if preamble:
makefile.write(preamble)
else:
makefile.write('''
FC = gfortran
flags_gfortran = -std=f2008 -pedantic -Wall -Wno-maybe-uninitialized
flags_ifort = -O0 -stand f08 -warn all
flags_ifx = ${flags_ifort}
FFLAGS = ${flags_$(FC)}
# dependent_program: LDLIBS = -llapack -lblas
'''.strip())
makefile.write(content)
if epilogue:
makefile.write('''
# not generated by makemake90:
{epilogue}
'''.format(**vars()))
for mkdir in set(args.values()) | set(map(os.path.dirname, related)):
if not os.path.isdir(mkdir):
os.makedirs(os.path.realpath(mkdir))
print('Created folder "%s" required to make project'
% os.path.normpath(mkdir))
def main():
filename, preamble, epilogue, args = parameters()
components, references, related = dependencies(**args)
makefile(filename, components, references, related, preamble, epilogue,
**args)
if __name__ == '__main__':
main()