"""Main pymake function, :code:`pymake.main()`, that is called when pymake is
run from the command line. :code:`pymake.main()` can also be called directly
from a script in combination with :code:`pymake.parser()`.
.. code-block:: python
import pymake
args = pymake.parser()
pymake.main(
args.srcdir,
args.target,
fc=args.fc,
cc=args.cc,
makeclean=args.makeclean,
expedite=args.expedite,
dryrun=args.dryrun,
double=args.double,
debug=args.debug,
include_subdirs=args.subdirs,
fflags=args.fflags,
cflags=args.cflags,
arch=args.arch,
syslibs=args.syslibs,
makefile=args.makefile,
srcdir2=args.commonsrc,
extrafiles=args.extrafiles,
excludefiles=args.excludefiles,
sharedobject=args.sharedobject,
appdir=args.appdir,
verbose=args.verbose,
inplace=args.inplace,
)
The script could be run from the command line using:
.. code-block:: bash
python myscript.py ../src myapp -fc=ifort -cc=icc
"""
import inspect
import os
import pathlib as pl
import shutil
import sys
import traceback
from .config import __version__
from .utils._compiler_language_files import (
_get_c_files,
_get_fortran_files,
_get_ordered_srcfiles,
_get_srcfiles,
_preprocess_file,
)
from .utils._compiler_switches import (
_get_c_flags,
_get_fortran_flags,
_get_linker_flags,
_get_optlevel,
_get_os_macro,
_get_osname,
)
from .utils._file_utils import _get_extra_exclude_files
from .utils._meson_build import _meson_build
from .utils._Popen_wrapper import (
_process_Popen_command,
_process_Popen_communicate,
_process_Popen_initialize,
_process_Popen_stdout,
)
[docs]def main(
srcdir=None,
target=None,
fc="gfortran",
cc="gcc",
makeclean=True,
expedite=False,
dryrun=False,
double=False,
debug=False,
include_subdirs=False,
fflags=None,
cflags=None,
syslibs=None,
arch="intel64",
makefile=False,
makefiledir=".",
srcdir2=None,
extrafiles=None,
excludefiles=None,
sharedobject=False,
appdir=None,
verbose=False,
inplace=False,
networkx=False,
meson=False,
mesondir=".",
):
"""Main pymake function.
Parameters
----------
srcdir : str
path for directory containing source files
target : str
executable name or path for executable to create
fc : str
fortran compiler
cc : str
c or cpp compiler
makeclean : bool
boolean indicating if intermediate files should be cleaned up
after successful build
expedite : bool
boolean indicating if only out of date source files will be compiled.
Clean must not have been used on previous build.
dryrun : bool
boolean indicating if source files should be compiled. Files will be
deleted, if makeclean is True.
double : bool
boolean indicating a compiler switch will be used to create an
executable with double precision real variables.
debug : bool
boolean indicating is a debug executable will be built
include_subdirs : bool
boolean indicating source files in srcdir subdirectories should be
included in the build
fflags : list
user provided list of fortran compiler flags
cflags : list
user provided list of c or cpp compiler flags
syslibs : list
user provided syslibs
arch : str
Architecture to use for Intel Compilers on Windows (default is intel64)
makefile : bool
boolean indicating if a GNU make makefile should be created
makefiledir : str
GNU make makefile path
srcdir2 : str
additional directory with common source files.
extrafiles : str
path for extrafiles file that contains paths to additional source
files to include
excludefiles : str
path for excludefiles file that contains filename of source files
to exclude from the build
sharedobject : bool
boolean indicating a shared object will be built
appdir : str
path for executable
verbose : bool
boolean indicating if output will be printed to the terminal
inplace : bool
boolean indicating that the source files in srcdir, srcdir2, and
defined in extrafiles will be used directly. If inplace is False,
source files will be copied to a directory named srcdir_temp.
(default is False)
networkx : bool
boolean indicating that the NetworkX python package will be used to
create the Directed Acyclic Graph (DAG) used to determine the order
source files are compiled in. The NetworkX package tends to result in
a unique DAG more often than the standard algorithm used in pymake.
(default is False)
meson : bool
boolean indicating that the executable should be built using the
meson build system. (default is False)
mesondir : str
Main meson.build file path
Returns
-------
returncode : int
return code
"""
if meson:
if not inplace:
inplace = True
print(
f"Using meson to build {os.path.basename(target)}, "
+ "ressetting inplace to True"
)
if srcdir is not None and target is not None:
objdir_temp, moddir_temp, srcdir_temp = get_temporary_directories(
appdir=appdir,
target=pl.Path(target).stem,
)
if inplace:
srcdir_temp = srcdir
# process appdir
if appdir is not None:
target = os.path.join(appdir, target)
# make appdir if it does not exist
if not os.path.isdir(appdir):
os.makedirs(appdir)
else:
target = os.path.join(".", target)
# set fc and cc to None if they are passed as 'none'
if fc == "none":
fc = None
if cc == "none":
cc = None
if fc is None and cc is None:
msg = (
"Nothing to do the fortran (-fc) and c/c++ compilers (-cc)"
+ "are both 'none'."
)
raise ValueError(msg)
# convert fflags, cflags, and syslibs to lists
if fflags is None:
fflags = []
elif isinstance(fflags, str):
fflags = fflags.split()
if cflags is None:
cflags = []
elif isinstance(cflags, str):
cflags = cflags.split()
if syslibs is None:
syslibs = []
elif isinstance(syslibs, str):
syslibs = syslibs.split()
# write summary information
if verbose:
print(f"\nsource files are in:\n {srcdir}\n")
print(f"executable name to be created:\n {target}\n")
if srcdir2 is not None:
msg = "additional source files are in:\n" + f" {srcdir2}\n"
print(msg)
# make sure the path for the target exists
pth = os.path.dirname(target)
if pth == "":
pth = "."
if not os.path.exists(pth):
print(f"creating target path - {pth}\n")
os.makedirs(pth)
# initialize
srcfiles = _pymake_initialize(
srcdir,
target,
srcdir2,
extrafiles,
excludefiles,
include_subdirs,
objdir_temp,
moddir_temp,
srcdir_temp,
meson,
)
# get ordered list of files to compile
srcfiles = _get_ordered_srcfiles(srcfiles, networkx)
# set intelwin flag to True in compiling on windows with
# Intel compilers
intelwin = False
if not meson:
if _get_osname() == "win32":
if fc is not None:
if fc in ["ifort", "mpiifort"]:
intelwin = True
if cc is not None:
if cc in ["cl", "icl"]:
intelwin = True
# update openspec files based on intelwin
if not intelwin:
_create_openspec(srcfiles, verbose)
# compile the executable
if meson:
returncode = _meson_build(
target,
srcdir,
srcdir2,
extrafiles,
srcfiles,
debug,
double,
fc,
cc,
fflags,
cflags,
syslibs,
sharedobject,
mesondir,
verbose,
)
else:
returncode = _pymake_compile(
srcfiles,
target,
fc,
cc,
expedite,
dryrun,
double,
debug,
fflags,
cflags,
syslibs,
arch,
intelwin,
sharedobject,
verbose,
)
# create makefile
if makefile:
_create_makefile(
target,
srcdir,
srcdir2,
extrafiles,
srcfiles,
debug,
double,
fc,
cc,
fflags,
cflags,
syslibs,
sharedobject,
makefiledir,
verbose,
)
# clean up temporary files
if makeclean and returncode == 0:
_clean_temp_files(
target,
intelwin,
inplace,
objdir_temp,
moddir_temp,
srcdir_temp,
meson,
mesondir,
verbose,
)
else:
msg = (
f"Nothing to do, the srcdir ({srcdir}) "
+ f"and/or target ({target}) "
+ "are not specified."
)
raise ValueError(msg)
return returncode
def _pymake_initialize(
srcdir,
target,
commonsrc,
extrafiles,
excludefiles,
include_subdirs,
objdir_temp,
moddir_temp,
srcdir_temp,
meson,
):
"""Remove temp source directory and target, and then copy source into
source temp directory.
Parameters
----------
srcdir : str
path for directory containing source files
target : str
path for executable to create
commonsrc : str
additional directory with common source files.
extrafiles : str
path for extrafiles file that contains paths to additional source
files to include
excludefiles : str
path for excludefiles file that contains filename of source files
to exclude from the build
include_subdirs : bool
boolean indicating source files in srcdir subdirectories should be
included in the build
objdir_temp : str
path for temporary directory that will contain the object files.
moddir_temp : str
path for temporary directory that will contain the module files.
srcdir_temp : str
path for directory that will contain the source files. If
srcdir_temp is the same as srcdir then the original source files
will be used.
Returns
-------
srcfiles : list
list of source files for build
"""
# remove the target if it already exists
if os.path.isfile(target):
os.remove(target)
inplace = False
if srcdir == srcdir_temp:
inplace = True
# if exclude is not None, then it is a text file with a list of
# source files that need to be excluded from srctemp.
excludefiles = _get_extra_exclude_files(excludefiles)
if excludefiles:
for idx, exclude_file in enumerate(excludefiles):
excludefiles[idx] = os.path.basename(exclude_file)
# remove srcdir_temp and copy in srcdir
if not inplace:
if os.path.isdir(srcdir_temp):
shutil.rmtree(srcdir_temp)
if excludefiles:
shutil.copytree(
srcdir,
srcdir_temp,
ignore=shutil.ignore_patterns(*excludefiles),
)
else:
shutil.copytree(srcdir, srcdir_temp)
# get a list of source files in srcdir_temp to include
srcfiles = _get_srcfiles(srcdir_temp, include_subdirs)
# copy files from a specified common source directory if
# commonsrc is not None
if commonsrc is not None:
if not inplace:
src = os.path.relpath(commonsrc, os.getcwd())
dst = os.path.join(
srcdir_temp, os.path.basename(os.path.normpath(commonsrc))
)
if excludefiles:
shutil.copytree(
src,
dst,
ignore=shutil.ignore_patterns(*excludefiles),
)
else:
shutil.copytree(src, dst)
else:
dst = os.path.relpath(os.path.abspath(os.path.abspath(commonsrc)))
srcfiles += _get_srcfiles(dst, include_subdirs)
# if extrafiles is not None, then it is a text file with a list of
# additional source files that need to be copied into srctemp and
# compiled.
files = _get_extra_exclude_files(extrafiles)
if files is None:
files = []
for fpth in files:
if not os.path.isfile(fpth):
# check if fpp file has been replaced by a free format file
if fpth.endswith(".fpp"):
fpth2 = fpth.replace(".fpp", ".f90")
if os.path.isfile(fpth):
fpth = fpth2
else:
msg = f"Current working directory: {os.getcwd()}\n"
msg += f"Error in extrafiles: {extrafiles}\n"
msg += f"Could not find file: {fpth}"
raise FileNotFoundError(msg)
if inplace:
dst = os.path.normpath(os.path.relpath(fpth, os.getcwd()))
else:
dst = os.path.join(srcdir_temp, os.path.basename(fpth))
if os.path.isfile(dst):
raise ValueError(
"Error with extrafile. Name conflicts with "
f"an existing source file: {dst}"
)
if not inplace:
shutil.copy(fpth, dst)
# add extrafiles to srcfiles
srcfiles.append(dst)
# remove exclude files from srcfiles list
if excludefiles:
remove_list = []
for fpth in srcfiles:
if os.path.basename(fpth) in excludefiles:
remove_list.append(fpth)
for fpth in remove_list:
srcfiles.remove(fpth)
# if they don't exist and not compiling with meson,
# create directories for objects and module (*.mod) files.
if not meson:
if not os.path.exists(objdir_temp):
os.makedirs(objdir_temp)
if not os.path.exists(moddir_temp):
os.makedirs(moddir_temp)
return srcfiles
[docs]def get_temporary_directories(appdir=None, target=None):
"""Get paths to temporary object, module, and source files
Parameters
----------
appdir : str
path for executable
target : str
target name to be appended to the temporary directories.
Default is None
Returns
-------
obj_temp : str
path to temporary object files
mod_temp : str
path to temporary module files
src_temp : str
path to temporary source files
"""
if appdir is None:
base_pth = "."
else:
base_pth = appdir
if target is None:
target = "temp"
return (
os.path.join(base_pth, f"obj_{target}"),
os.path.join(base_pth, f"mod_{target}"),
os.path.join(base_pth, f"src_{target}"),
)
def _clean_temp_files(
target,
intelwin,
inplace,
objdir_temp,
moddir_temp,
srcdir_temp,
meson,
mesondir,
verbose=False,
):
"""Cleanup intermediate files. Remove mod and object files, and remove the
temporary source directory.
Parameters
----------
target : str
path for executable to create
intelwin : bool
boolean indicating if pymake was used to compile source code on
Windows using Intel compilers
inplace : bool
boolean indicating that the source files in srcdir, srcdir2, and
defined in extrafiles will be used directly. If inplace is True,
source files will be copied to a directory named srcdir_temp.
(default is False)
objdir_temp : str
path for temporary directory that will contain the object files.
moddir_temp : str
path for temporary directory that will contain the module files.
srcdir_temp : str
path for directory that will contain the source files. If
srcdir_temp is the same as srcdir then the original source files
will be used.
meson : bool
boolean indicating that the executable should be built using the
meson build system. (default is False)
mesondir : str
Main meson.build file path
verbose : bool
boolean indicating if output will be printed to the terminal
Returns
-------
None
"""
# set object extension
if intelwin:
objext = ".obj"
else:
objext = ".o"
# clean things up
if verbose:
print("\nCleaning up temporary source, object, and module files...")
filelist = os.listdir(".")
delext = [".mod", objext]
for f in filelist:
for ext in delext:
if f.endswith(ext):
if verbose:
print(f" removing...{f}")
os.remove(f)
# shared object intermediate files
if verbose:
print("\nCleaning up intermediate shared object files...")
delext = [".exp", ".lib"]
dpth = os.path.dirname(os.path.abspath(target))
for f in os.listdir(dpth):
fpth = os.path.join(dpth, f)
for ext in delext:
if fpth.endswith(ext):
if verbose:
print(f" removing...'{fpth}'")
os.remove(fpth)
# remove temporary directories
if verbose:
msg = (
"\nCleaning up temporary source, object, "
+ "and module directories..."
)
print(msg)
if not inplace:
if os.path.isdir(srcdir_temp):
if verbose:
print(f"removing...'{srcdir_temp}'")
shutil.rmtree(srcdir_temp)
if os.path.isdir(objdir_temp):
if verbose:
print(f"removing...'{objdir_temp}'")
shutil.rmtree(objdir_temp)
if os.path.isdir(moddir_temp):
if verbose:
print(f"removing...'{moddir_temp}'")
shutil.rmtree(moddir_temp)
if meson:
meson_builddir = os.path.join(mesondir, "_build")
if os.path.isdir(meson_builddir):
if verbose:
print(f"removing...'{meson_builddir}'")
shutil.rmtree(meson_builddir)
main_meson_file = os.path.join(mesondir, "meson.build")
if os.path.isfile(main_meson_file):
if verbose:
print(f"removing...'{main_meson_file}'")
os.remove(main_meson_file)
# remove the windows batchfile
batch_file = "compile.bat"
if intelwin and os.path.isfile(batch_file):
os.remove(batch_file)
return
def _create_openspec(srcfiles, verbose):
"""Create new openspec.inc, FILESPEC.INC, and filespec.inc files that uses
STREAM ACCESS. This is specific to MODFLOW and MT3D based targets. Source
directories are scanned and files defining file access are replaced.
Parameters
----------
Returns
-------
None
"""
# list of files to replace
files = ["openspec.inc", "filespec.inc"]
# build list of directory paths from srcfiles
dpths = []
for fpth in srcfiles:
dpth = os.path.dirname(fpth)
if dpth not in dpths:
dpths.append(dpth)
# replace files in directory paths if they exist
for dpth in dpths:
for file in files:
fpth = os.path.join(dpth, file)
if os.path.isfile(fpth):
if verbose:
print(f'replacing..."{fpth}"')
f = open(fpth, "w")
line = (
"c -- created by pymake_base.py\n"
+ " CHARACTER*20 ACCESS,FORM,ACTION(2)\n"
+ " DATA ACCESS/'STREAM'/\n"
+ " DATA FORM/'UNFORMATTED'/\n"
+ " DATA (ACTION(I),I=1,2)/'READ','READWRITE'/\n"
+ "c -- end of include file\n"
)
f.write(line)
f.close()
return
def _check_out_of_date(srcfile, objfile):
"""Check if existing object files are current with the existing source
files.
Parameters
----------
srcfile : str
source file path
objfile : str
object file path
Returns
-------
stale : bool
boolean indicating if the object file is current
"""
stale = True
if os.path.exists(objfile):
t1 = os.path.getmtime(objfile)
t2 = os.path.getmtime(srcfile)
if t1 > t2:
stale = False
return stale
def _pymake_compile(
srcfiles,
target,
fc,
cc,
expedite,
dryrun,
double,
debug,
fflags,
cflags,
syslibs,
arch,
intelwin,
sharedobject,
verbose,
):
"""Standard compile method.
Parameters
-------
srcfiles : list
list of source file names
target : str
path for executable to create
fc : str
fortran compiler
cc : str
c or cpp compiler
expedite : bool
boolean indicating if only out of date source files will be compiled.
Clean must not have been used on previous build.
dryrun : bool
boolean indicating if source files should be compiled. Files will be
deleted, if makeclean is True.
double : bool
boolean indicating a compiler switch will be used to create an
executable with double precision real variables.
debug : bool
boolean indicating is a debug executable will be built
fflags : list
user provided list of fortran compiler flags
cflags : list
user provided list of c or cpp compiler flags
syslibs : list
user provided syslibs
arch : str
architecture to use for Intel Compilers on Windows (default is intel64)
intelwin : bool
boolean indicating if pymake was used to compile source code on
Windows using Intel compilers
sharedobject : bool
boolean indicating a shared object will be built
verbose : bool
boolean indicating if output will be printed to the terminal
Returns
-------
returncode : int
returncode
"""
# write pymake setting
if verbose:
msg = f"\nPymake settings in {_pymake_compile.__name__}\n" + 40 * "-"
print(msg)
frame = inspect.currentframe()
fnargs, _, _, values = inspect.getargvalues(frame)
for arg in fnargs:
value = values[arg]
if not value:
value = "None"
elif isinstance(value, list):
value = ", ".join(value)
print(f" {arg}={value}")
# initialize returncode
returncode = 0
# initialize ilink
ilink = 0
# get temporary object and module directories
objdir_temp, moddir_temp, _ = get_temporary_directories(
os.path.dirname(target),
target=pl.Path(target).stem,
)
# set optimization levels
optlevel = _get_optlevel(
target, fc, cc, debug, fflags, cflags, verbose=verbose
)
# get fortran and c compiler switches
tfflags = _get_fortran_flags(
target,
fc,
fflags,
debug,
double,
sharedobject=sharedobject,
verbose=verbose,
)
tcflags = _get_c_flags(
target,
cc,
cflags,
debug,
srcfiles,
sharedobject=sharedobject,
verbose=verbose,
)
# get linker flags and syslibs
lc, tlflags = _get_linker_flags(
target,
fc,
cc,
syslibs,
srcfiles,
sharedobject=sharedobject,
verbose=verbose,
)
# clean exe prior to build so that test for exe below can return a
# non-zero error code
if os.path.isfile(target):
if verbose:
msg = f"removing existing target with same name: {target}"
print(msg)
os.remove(target)
if intelwin:
# update compiler names if necessary
ext = ".exe"
if fc is not None:
if ext not in fc:
fc += ext
if cc is not None:
if ext not in cc:
cc += ext
if ext not in lc:
lc += ext
# update target extension
if sharedobject:
program_path, ext = os.path.splitext(target)
if ext.lower() != ".dll":
target = program_path + ".dll"
else:
if ext not in target:
target += ext
# delete the batch file if it exists
batchfile = "compile.bat"
if os.path.isfile(batchfile):
try:
os.remove(batchfile)
except:
if verbose:
print(f"could not remove '{batchfile}'")
# Create target using a batch file on Windows
try:
_create_win_batch(
batchfile,
fc,
cc,
lc,
optlevel,
tfflags,
tcflags,
tlflags,
objdir_temp,
moddir_temp,
srcfiles,
target,
arch,
sharedobject,
)
# build the command list for the Windows batch file
cmdlists = [
batchfile,
]
except:
errmsg = f"Could not make x64 target: {target}\n"
errmsg += traceback.print_exc()
print(errmsg)
else:
if sharedobject:
program_path, ext = os.path.splitext(target)
if _get_osname() == "win32":
if ext.lower() != ".dll":
target = program_path + ".dll"
elif _get_osname() == "darwin":
if ext.lower() != ".dylib":
target = program_path + ".dylib"
else:
if ext.lower() != ".so":
target = program_path + ".so"
# initialize the commands and object files list
cmdlists = []
objfiles = []
# assume that header files may be in other folders, so make a list
searchdir = []
for f in srcfiles:
dirname = os.path.dirname(f)
if dirname not in searchdir:
searchdir.append(dirname)
# build the command for each source file and add to the
# list of commands
for srcfile in srcfiles:
cmdlist = []
iscfile = False
ext = os.path.splitext(srcfile)[1].lower()
if ext in [".c", ".cpp"]: # mja
iscfile = True
cmdlist.append(cc) # mja
cmdlist.append(optlevel)
for switch in tcflags: # mja
cmdlist.append(switch) # mja
else: # mja
# build command list
cmdlist.append(fc)
cmdlist.append(optlevel)
for switch in tfflags:
cmdlist.append(switch)
# add preprocessor option, if necessary
if _preprocess_file(srcfile):
if os.path.basename(fc) == "gfortran":
pp_tag = "-cpp"
else:
pp_tag = "-fpp"
cmdlist.append(pp_tag)
# add search path for any c and c++ header files
if iscfile:
for sd in searchdir:
cmdlist.append(f"-I{sd}")
# put object files and module files in objdir_temp and moddir_temp
else:
cmdlist.append(f"-I{objdir_temp}")
if fc in ["ifort", "mpiifort"]:
cmdlist.append("-module")
cmdlist.append(moddir_temp + "/")
else:
cmdlist.append(f"-J{moddir_temp}")
cmdlist.append("-c")
cmdlist.append(srcfile)
# object file name and location
srcname, srcext = os.path.splitext(srcfile)
srcname = srcname.split(os.path.sep)[-1]
objfile = os.path.join(objdir_temp, srcname + ".o")
cmdlist.append("-o")
cmdlist.append(objfile)
# Save the name of the object file for linker
objfiles.append(objfile)
# If expedited, then check if object file is out of date, if it
# exists. No need to compile if object file is newer.
compilefile = True
if expedite:
if not _check_out_of_date(srcfile, objfile):
compilefile = False
if compilefile:
cmdlists.append(cmdlist)
# Build the link command and then link to create the executable
ilink = len(cmdlists)
if ilink > 0:
cmdlist = [lc, optlevel]
cmdlist.append("-o")
cmdlist.append(target)
for objfile in objfiles:
cmdlist.append(objfile)
# linker switches
for switch in tlflags:
cmdlist.append(switch)
# add linker command to the commands list
cmdlists.append(cmdlist)
# execute each command in cmdlists
if not dryrun:
for idx, cmdlist in enumerate(cmdlists):
if idx == 0:
if intelwin:
msg = (
f"\nCompiling '{os.path.basename(target)}' "
+ "for Windows using Intel compilers..."
)
else:
msg = (
"\nCompiling object files for "
+ f"'{os.path.basename(target)}'"
)
print(msg)
if idx > 0 and idx == ilink:
msg = (
"\nLinking object files "
+ f"to make '{os.path.basename(target)}'..."
)
print(msg)
# write the command to the terminal
_process_Popen_command(False, cmdlist)
# run the command using Popen
proc = _process_Popen_initialize(cmdlist, intelwin)
# write batch file execution to terminal
if intelwin:
_process_Popen_stdout(proc)
# establish communicator to report errors
else:
_process_Popen_communicate(proc)
# evaluate return code
returncode = proc.returncode
if returncode != 0:
msg = f"compilation failed on '{' '.join(cmdlist)}'"
print(msg)
break
# print blank line separator after all commands in cmdlist are executed
print("")
# return
return returncode
def _create_win_batch(
batchfile,
fc,
cc,
lc,
optlevel,
fflags,
cflags,
lflags,
objdir_temp,
moddir_temp,
srcfiles,
target,
arch,
sharedobject,
):
"""Make an intel compiler batch file for compiling on windows.
Parameters
-------
batchfile : str
batch file name to create
fc : str
fortran compiler
cc : str
c or cpp compiler
lc : str
compiler to use for linking
optlevel : str
compiler optimization switch
fflags : list
user provided list of fortran compiler flags
cflags : list
user provided list of c or cpp compiler flags
lflags : list
linker compiler flags, which are a combination of user provided list
of compiler flags for the compiler to used for linking
objdir_temp : str
path for temporary directory that will contain the object files.
moddir_temp : str
path for temporary directory that will contain the module files.
srcfiles : list
list of source file names
target : str
path for executable to create
arch : str
architecture to use for Intel Compilers on Windows (default is intel64)
Returns
-------
"""
# determine intel version
intel_setvars = None
# oneAPI
oneapi_list = ("LATEST_VERSION", "ONEAPI_ROOT")
for on_env_var in oneapi_list:
latest_version = os.environ.get(on_env_var)
if latest_version is not None:
if on_env_var == oneapi_list[0]:
cpvars = (
"C:\\Program Files (x86)\\Intel\\oneAPI\\compiler\\"
+ f"{latest_version}\\env\\vars.bat"
)
else:
cpvars = (
"C:\\Program Files (x86)\\Intel\\oneAPI\\" + "setvars.bat"
)
if not os.path.isfile(cpvars):
raise Exception(f"Could not find cpvars: {cpvars}")
intel_setvars = f'"{cpvars}"'
break
# stand alone intel installation
if intel_setvars is None:
iflist = [f"IFORT_COMPILER{i}" for i in range(30, 12, -1)]
for ift in iflist:
stand_alone_intel = os.environ.get(ift)
if stand_alone_intel is not None:
cpvars = os.path.join(
stand_alone_intel, "bin", "compilervars.bat"
)
if not os.path.isfile(cpvars):
raise Exception(f"Could not find cpvars: {cpvars}")
intel_setvars = '"' + os.path.normpath(cpvars) + '" ' + arch
break
# check if either OneAPI or stand alone intel is installed
if intel_setvars is None:
err_msg = (
"OneAPI or stand alone version of Intel compilers "
+ "is not installed"
)
raise ValueError(err_msg)
# open the batch file
f = open(batchfile, "w")
# only write the compilervars batch command to batchfile if env vars aren't already configured
# https://www.intel.com/content/www/us/en/develop/documentation/oneapi-programming-guide/top/oneapi-development-environment-setup/use-the-setvars-script-with-windows.html
if os.environ.get("SETVARS_COMPLETED") != "1":
line = "call " + intel_setvars + "\n"
f.write(line)
# assume that header files may be in other folders, so make a list
searchdir = []
for s in srcfiles:
dirname = os.path.dirname(s)
if dirname not in searchdir:
searchdir.append(dirname)
# write commands to build object files
line = (
"echo Creating object files to create '"
+ os.path.basename(target)
+ "'\n"
)
f.write(line)
for srcfile in srcfiles:
if srcfile.endswith(".c") or srcfile.endswith(".cpp"):
cmd = cc + " " + optlevel + " "
for switch in cflags:
cmd += switch + " "
cmd += "/c" + " "
# add search path for any header files
for sd in searchdir:
cmd += f"/I{sd} "
obj = os.path.join(
objdir_temp,
os.path.splitext(os.path.basename(srcfile))[0] + ".obj",
)
cmd += "/Fo:" + obj + " "
cmd += srcfile
else:
cmd = fc + " " + optlevel + " "
for switch in fflags:
cmd += switch + " "
# add preprocessor option, if necessary
if _preprocess_file(srcfile):
cmd += "/fpp" + " "
cmd += "/c" + " "
cmd += f"/module:{moddir_temp}\\ "
cmd += f"/object:{objdir_temp}\\ "
cmd += srcfile
f.write(f"echo {cmd}\n")
f.write(cmd + "\n")
# write commands to link
line = (
"echo Linking object files to create '"
+ os.path.basename(target)
+ "'\n"
)
f.write(line)
# assemble the link command
cmd = lc + " " + optlevel
cmd += " " + "-o" + " " + target + " " + objdir_temp + "\\*.obj"
for switch in lflags:
cmd += " " + switch
cmd += "\n"
f.write(f"echo {cmd}\n")
f.write(cmd)
# close the batch file
f.close()
return
def _create_makefile(
target,
srcdir,
srcdir2,
extrafiles,
srcfiles,
debug,
double,
fc,
cc,
fflags,
cflags,
syslibs,
sharedobject,
makefiledir,
verbose,
makedefaults="makedefaults",
):
"""
Parameters
----------
target : str
path for executable to create
srcdir : str
path for directory containing source files
srcdir2 : str
additional directory with common source files.
extrafiles : str
path for extrafiles file that contains paths to additional source
files to include
srcfiles : list
ordered list of source files to include in the makefile
debug : bool
boolean indicating is a debug executable will be built
double : bool
boolean indicating a compiler switch will be used to create an
executable with double precision real variables.
fc : str
fortran compiler
cc : str
c or cpp compiler
fflags : list
user provided list of fortran compiler flags
cflags : list
user provided list of c or cpp compiler flags
syslibs : list
user provided syslibs
sharedobject : bool
boolean indicating a shared object will be built
makefiledir : str
GNU make makefile path
verbose : bool
boolean indicating if output will be printed to the terminal
makedefaults : str
name of the makedefaults file to create with makefile (default is
makedefaults)
Returns
-------
"""
# write a message
if verbose:
msg = f"\nWriting makefile and {makedefaults}"
print(msg)
# set executable extension
if sharedobject:
win_ext = ".dll"
macos_ext = ".dylib"
linux_ext = ".so"
else:
win_ext = ".exe"
macos_ext = ""
linux_ext = ""
# set makefile directory
make_dir = makefiledir
# get temporary directories
objdir_temp, moddir_temp, _ = get_temporary_directories(make_dir)
# set object extension
objext = ".o"
# get list of unique fortran and c/c++ file extensions
fext = _get_fortran_files(srcfiles, extensions=True)
cext = _get_c_files(srcfiles, extensions=True)
# determine if the fortran file should be preprocessed
if fext is None:
preprocess = False
else:
preprocess = _preprocess_file(_get_fortran_files(srcfiles))
# set exe_name
exe_name = os.path.splitext(os.path.basename(target))[0]
# build heading
heading = (
f"# makefile created by pymake (version {__version__}) "
f"for the '{exe_name}' executable.\n"
)
# open makefile
f = open(os.path.join(make_dir, "makefile"), "w")
# write header
f.write(heading + "\n")
# write include file
line = f"\ninclude ./{makedefaults}\n\n"
f.write(line)
# determine the directories with source files
# source files in sdir and sdir2
dirs = [d[0].replace("\\", "/") for d in os.walk(srcdir)]
if srcdir2 is not None:
dirs2 = [d[0].replace("\\", "/") for d in os.walk(srcdir2)]
dirs = dirs + dirs2
# source files in extrafiles
files = _get_extra_exclude_files(extrafiles)
if files is not None:
for ef in files:
fdir = os.path.dirname(ef)
rdir = os.path.relpath(fdir, os.getcwd())
rdir = rdir.replace("\\", "/")
if rdir not in dirs:
dirs.append(rdir)
# write directories with source files and create vpath data
line = "# Define the source file directories\n"
f.write(line)
vpaths = []
for idx, source_dir in enumerate(dirs):
rel_source_dir = os.path.relpath(source_dir, make_dir)
vpaths.append(f"SOURCEDIR{idx + 1}")
line = f"{vpaths[idx]}={rel_source_dir}\n"
f.write(line)
f.write("\n")
# write vpath
f.write("VPATH = \\\n")
for idx, sd in enumerate(vpaths):
f.write("${" + f"{sd}" + "} ")
if idx + 1 < len(vpaths):
f.write("\\")
f.write("\n")
f.write("\n")
# write file extensions
line = ".SUFFIXES: "
if fext is not None:
for ext in fext:
line += f"{ext} "
if cext is not None:
for ext in cext:
line += f"{ext} "
line += objext
f.write(f"{line}\n")
f.write("\n")
f.write("OBJECTS = \\\n")
for idx, srcfile in enumerate(srcfiles):
objpth = os.path.splitext(os.path.basename(srcfile))[0] + objext
f.write(f"$(OBJDIR)/{objpth}")
if idx + 1 < len(srcfiles):
f.write(" \\")
f.write("\n")
f.write("\n")
f.write("# Define the objects that make up the program\n")
f.write("$(PROGRAM) : $(OBJECTS)\n")
if fext is None:
line = "\t-$(CC) $(OPTLEVEL) -o $@ $(OBJECTS) $(LDFLAGS)\n"
else:
line = "\t-$(FC) $(OPTLEVEL) -o $@ $(OBJECTS) $(LDFLAGS)\n"
f.write(f"{line}\n")
if fext is not None:
for ext in fext:
f.write(f"$(OBJDIR)/%{objext} : %{ext}\n")
f.write("\t@mkdir -p $(@D)\n")
line = (
"\t$(FC) $(OPTLEVEL) $(FFLAGS) -c $< -o $@ "
+ "$(INCSWITCH) $(MODSWITCH)\n"
)
f.write(f"{line}\n")
if cext is not None:
for ext in cext:
f.write(f"$(OBJDIR)/%{objext} : %{ext}\n")
f.write("\t@mkdir -p $(@D)\n")
line = (
"\t$(CC) $(OPTLEVEL) $(CFLAGS) -c $< -o $@ " + "$(INCSWITCH)\n"
)
f.write(f"{line}\n")
# close the makefile
f.close()
# open makedefaults
f = open(os.path.join(make_dir, makedefaults), "w")
# replace makefile in heading with makedefaults
heading = heading.replace("makefile", makedefaults)
# write header
f.write(heading + "\n")
# write OS evaluation
line = "# determine OS\n"
line += "ifeq ($(OS), Windows_NT)\n"
line += "\tdetected_OS = Windows\n"
line += "\tOS_macro = -D_WIN32\n"
line += "else\n"
line += (
"\tdetected_OS = $(shell sh -c 'uname 2>/dev/null "
+ "|| echo Unknown')\n"
)
line += "\tifeq ($(detected_OS), Darwin)\n"
line += "\t\tOS_macro = -D__APPLE__\n"
line += "\telse\n"
line += "\t\tOS_macro = -D__LINUX__\n"
line += "\tendif\n"
line += "endif\n\n"
f.write(line)
# get path to executable
dpth = os.path.dirname(target)
if len(dpth) > 0:
dpth = os.path.relpath(dpth, make_dir)
else:
dpth = "."
# write header
line = (
"# Define the directories for the object and module files\n"
+ "# and the executable and its path.\n"
)
tpth = dpth.replace("\\", "/")
line += f"BINDIR = {tpth}\n"
tpth = os.path.relpath(objdir_temp.replace("\\", "/"), make_dir)
line += f"OBJDIR = {tpth}\n"
tpth = os.path.relpath(moddir_temp.replace("\\", "/"), make_dir)
line += f"MODDIR = {tpth}\n"
line += "INCSWITCH = -I $(OBJDIR)\n"
line += "MODSWITCH = -J $(MODDIR)\n\n"
f.write(line)
line = "# define os dependent program name\n"
line += "ifeq ($(detected_OS), Windows)\n"
line += f"\tPROGRAM = $(BINDIR)/{exe_name}{win_ext}\n"
line += "else ifeq ($(detected_OS), Darwin)\n"
line += f"\tPROGRAM = $(BINDIR)/{exe_name}{macos_ext}\n"
line += "else\n"
line += f"\tPROGRAM = $(BINDIR)/{exe_name}{linux_ext}\n"
line += "endif\n\n"
f.write(line)
# reassign compilers if the defined compilers do not exist
line = "# use GNU compilers if defined compilers do not exist\n"
line += "ifeq ($(detected_OS), Windows)\n"
line += "\tWHICH = where\n"
line += "else\n"
line += "\tWHICH = which\n"
line += "endif\n"
if fext is not None:
line += "ifeq (, $(shell $(WHICH) $(FC)))\n"
line += "\tFC = gfortran\n"
line += "endif\n"
if cext is not None:
line += "ifeq (, $(shell $(WHICH) $(CC)))\n"
line += "\tCC = gcc\n"
line += "endif\n"
line += "\n"
f.write(line)
# set gfortran as fortran compiler if it is f77
if fext is not None:
line = "# set fortran compiler to gfortran if it is f77\n"
line += "ifeq ($(FC), f77)\n"
line += "\tFC = gfortran\n"
line += "\t# set c compiler to gcc if not passed on the command line\n"
line += '\tifneq ($(origin CC), "command line")\n'
line += "\t\tifneq ($(CC), gcc)\n"
line += "\t\t\tCC = gcc\n"
line += "\t\tendif\n"
line += "\tendif\n"
line += "endif\n\n"
f.write(line)
else:
line = "# set cc compiler to gcc if it is cc\n"
line += "ifeq ($(CC), cc)\n"
line += "\tCC = gcc\n"
line += "endif\n\n"
f.write(line)
# optimization level
optlevel = _get_optlevel(
target, fc, cc, debug, fflags, cflags, verbose=verbose
)
line = "# set the optimization level (OPTLEVEL) if not defined\n"
line += f"OPTLEVEL ?= {optlevel.replace('/', '-')}\n\n"
f.write(line)
# fortran flags
if fext is not None:
# remove existing os_macro for machine OS from fflags prior to
# adding os_macro for specific OS
tag = "-" + _get_os_macro()
if tag in fflags:
fflags.remove(tag)
# build fortran flags for each os
line = "# set the fortran flags\n"
line += "ifeq ($(detected_OS), Windows)\n"
line += "\tifeq ($(FC), gfortran)\n"
tfflags = _get_fortran_flags(
target,
"gfortran",
[],
debug,
double,
osname="win32",
sharedobject=sharedobject,
verbose=verbose,
)
for idx, flag in enumerate(tfflags):
if "-D_" in flag:
tfflags[idx] = "$(OS_macro)"
if preprocess:
tfflags.append("-cpp")
line += f"\t\tFFLAGS ?= {' '.join(tfflags)}\n"
line += "\tendif\n"
line += "else\n"
line += "\tifeq ($(FC), gfortran)\n"
tfflags = _get_fortran_flags(
target,
"gfortran",
[],
debug,
double,
osname="linux",
sharedobject=sharedobject,
verbose=verbose,
)
for idx, flag in enumerate(tfflags):
if "-D__" in flag:
tfflags[idx] = "$(OS_macro)"
if preprocess:
tfflags.append("-cpp")
line += f"\t\tFFLAGS ?= {' '.join(tfflags)}\n"
line += "\tendif\n"
line += "\tifeq ($(FC), $(filter $(FC), ifort mpiifort))\n"
tfflags = _get_fortran_flags(
target,
"ifort",
[],
debug,
double,
osname="linux",
sharedobject=sharedobject,
verbose=verbose,
)
for idx, flag in enumerate(tfflags):
if "-D__" in flag:
tfflags[idx] = "$(OS_macro)"
if preprocess:
tfflags.append("-fpp")
line += f"\t\tFFLAGS ?= {' '.join(tfflags)}\n"
line += "\t\tMODSWITCH = -module $(MODDIR)\n"
line += "\tendif\n"
line += "\tifeq ($(FC), $(filter $(FC), ftn))\n"
tfflags = _get_fortran_flags(
target,
"ftn",
[],
debug,
double,
osname="linux",
sharedobject=sharedobject,
verbose=verbose,
)
for idx, flag in enumerate(tfflags):
if "-D__" in flag:
tfflags[idx] = "$(OS_macro)"
line += f"\t\tFFLAGS ?= {' '.join(tfflags)}\n"
line += "\tendif\n"
line += "endif\n\n"
f.write(line)
# c/c++ flags
if cext is not None:
line = "# set the c/c++ flags\n"
line += "ifeq ($(detected_OS), Windows)\n"
line += "\tifeq ($(CC), $(filter $(CC), gcc g++))\n"
tcflags = _get_c_flags(
target,
"gcc",
fflags,
debug,
srcfiles,
osname="win32",
sharedobject=sharedobject,
verbose=verbose,
)
line += f"\t\tCFLAGS ?= {' '.join(tcflags)}\n"
line += "\tendif\n"
line += "\tifeq ($(CC), $(filter $(CC), clang clang++))\n"
tcflags = _get_c_flags(
target,
"clang",
fflags,
debug,
srcfiles,
osname="win32",
sharedobject=sharedobject,
verbose=verbose,
)
line += f"\t\tCFLAGS ?= {' '.join(tcflags)}\n"
line += "\tendif\n"
line += "else\n"
line += "\tifeq ($(CC), $(filter $(CC), gcc g++))\n"
tcflags = _get_c_flags(
target,
"gcc",
fflags,
debug,
srcfiles,
osname="linux",
sharedobject=sharedobject,
verbose=verbose,
)
line += f"\t\tCFLAGS ?= {' '.join(tcflags)}\n"
line += "\tendif\n"
line += "\tifeq ($(CC), $(filter $(CC), clang clang++))\n"
tcflags = _get_c_flags(
target,
"clang",
fflags,
debug,
srcfiles,
osname="linux",
sharedobject=sharedobject,
verbose=verbose,
)
line += f"\t\tCFLAGS ?= {' '.join(tcflags)}\n"
line += "\tendif\n"
line += "\tifeq ($(CC), $(filter $(CC), icc mpiicc icpc))\n"
tcflags = _get_c_flags(
target,
"icc",
fflags,
debug,
srcfiles,
osname="linux",
sharedobject=sharedobject,
verbose=verbose,
)
line += f"\t\tCFLAGS ?= {' '.join(tcflags)}\n"
line += "\tendif\n"
line += "endif\n\n"
f.write(line)
# syslibs
line = "# set the ldflgs\n"
# windows - gfortran only
line += "ifeq ($(detected_OS), Windows)\n"
# c/c++ compiler used for linking
if fext is None:
_, tsyslibs = _get_linker_flags(
target,
None,
"gcc",
[],
srcfiles,
osname="win32",
sharedobject=sharedobject,
verbose=verbose,
)
line += "\tifeq ($(CC), $(filter $(CC), gcc g++))\n"
line += f"\t\tLDFLAGS ?= {' '.join(tsyslibs)}\n"
line += "\tendif\n"
_, tsyslibs = _get_linker_flags(
target,
None,
"clang",
[],
srcfiles,
osname="win32",
sharedobject=sharedobject,
verbose=verbose,
)
line += "\tifeq ($(CC), $(filter $(CC), clang clang++))\n"
line += f"\t\tLDFLAGS ?= {' '.join(tsyslibs)}\n"
line += "\tendif\n"
# fortran compiler used for linking
else:
_, tsyslibs = _get_linker_flags(
target,
"gfortran",
"gcc",
[],
srcfiles,
osname="win32",
sharedobject=sharedobject,
verbose=verbose,
)
line += "\tifeq ($(FC), $(filter $(FC), gfortran))\n"
line += f"\t\tLDFLAGS ?= {' '.join(tsyslibs)}\n"
line += "\tendif\n"
# linux and osx
line += "else\n"
# c/c++ compiler used for linking
if fext is None:
_, tsyslibs = _get_linker_flags(
target,
None,
"gcc",
[],
srcfiles,
osname="linux",
sharedobject=sharedobject,
verbose=verbose,
)
line += "\tifeq ($(CC), $(filter $(CC), gcc g++))\n"
line += f"\t\tLDFLAGS ?= {' '.join(tsyslibs)}\n"
line += "\tendif\n"
_, tsyslibs = _get_linker_flags(
target,
None,
"clang",
[],
srcfiles,
osname="linux",
sharedobject=sharedobject,
verbose=verbose,
)
line += "\tifeq ($(CC), $(filter $(CC), clang clang++))\n"
line += f"\t\tLDFLAGS ?= {' '.join(tsyslibs)}\n"
line += "\tendif\n"
# fortran compiler used for linking
else:
# gfortran compiler
line += "\tifeq ($(FC), gfortran)\n"
_, tsyslibs = _get_linker_flags(
target,
"gfortran",
"gcc",
[],
srcfiles,
osname="linux",
sharedobject=sharedobject,
verbose=verbose,
)
line += f"\t\tLDFLAGS ?= {' '.join(tsyslibs)}\n"
line += "\tendif\n"
# ifort compiler
line += "\tifeq ($(FC), $(filter $(FC), ifort mpiifort))\n"
_, tsyslibs = _get_linker_flags(
target,
"ifort",
"icc",
[],
srcfiles,
osname="linux",
sharedobject=sharedobject,
verbose=verbose,
)
line += f"\t\tLDFLAGS ?= {' '.join(tsyslibs)}\n"
line += "\tendif\n"
# ftn compiler
line += "\tifeq ($(FC), $(filter $(FC), ftn))\n"
_, tsyslibs = _get_linker_flags(
target,
"ftn",
"clang",
[],
srcfiles,
osname="linux",
sharedobject=sharedobject,
verbose=verbose,
)
line += f"\t\tLDFLAGS ?= {' '.join(tsyslibs)}\n"
line += "\tendif\n"
line += "endif\n\n"
f.write(line)
# check for windows error condition
line = "# check for Windows error condition\n"
line += "ifeq ($(detected_OS), Windows)\n"
if fext is not None:
line += "\tifeq ($(FC), $(filter $(FC), ifort mpiifort))\n"
line += "\t\tWINDOWSERROR = $(FC)\n"
line += "\tendif\n"
if cext is not None:
line += "\tifeq ($(CC), $(filter $(CC), icl))\n"
line += "\t\tWINDOWSERROR = $(CC)\n"
line += "\tendif\n"
line += "endif\n\n"
f.write(line)
# task functions
line = "# Define task functions\n"
line += "# Create the bin directory and compile and link the program\n"
line += "all: windowscheck makedirs | $(PROGRAM)\n\n"
line += "# test for windows error\n"
line += "windowscheck:\n"
line += "ifdef WINDOWSERROR\n"
line += "\t$(error cannot use makefile on windows with $(WINDOWSERROR))\n"
line += "endif\n\n"
line += "# Make the bin directory for the executable\n"
line += "makedirs:\n"
line += "\tmkdir -p $(BINDIR)\n"
line += "\tmkdir -p $(MODDIR)\n\n"
line += "# Write selected compiler settings\n"
line += ".PHONY: settings\n"
line += "settings:\n"
line += '\t@echo "Optimization level: $(OPTLEVEL)"\n'
if fext is not None:
line += '\t@echo "Fortran compiler: $(FC)"\n'
line += '\t@echo "Fortran flags: $(FFLAGS)"\n'
if cext is not None:
line += '\t@echo "C compiler: $(CC)"\n'
line += '\t@echo "C flags: $(CFLAGS)"\n'
if fext is None:
line += '\t@echo "Linker: $(CC)"\n'
else:
line += '\t@echo "Linker: $(FC)"\n'
line += '\t@echo "SYSLIBS: $(LDFLAGS)"\n\n'
line += "# Clean the object and module files and the executable\n"
line += ".PHONY: clean\n"
line += "clean:\n"
line += "\t-rm -rf $(OBJDIR)\n"
line += "\t-rm -rf $(MODDIR)\n"
line += "\t-rm -rf $(PROGRAM)\n\n"
line += "# Clean the object and module files\n"
line += ".PHONY: cleanobj\n"
line += "cleanobj:\n"
line += "\t-rm -rf $(OBJDIR)\n"
line += "\t-rm -rf $(MODDIR)\n\n"
f.write(line)
# close the makedefaults
f.close()
# replace windows line endings
if sys.platform == "win32":
windows_line_ending = b"\r\n"
unix_line_ending = b"\n"
for file in (
os.path.join(make_dir, "makefile"),
os.path.join(make_dir, makedefaults),
):
with open(file, "rb") as f:
content = f.read()
# replace windows line endings
content = content.replace(windows_line_ending, unix_line_ending)
# rewrite the file
with open(file, "wb") as f:
f.write(content)
return