"""Classes used to write build system files.
The classes here are used to write out settings for use by Makefile and CMake
build systems. The two relevant classes are CMakeMacroWriter and
MakeMacroWriter, which encapsulate the information necessary to write CMake and
Makefile formatted text, respectively. See the docstrings for those classes for
more.
"""
# This is not the most useful check.
# pylint: disable=invalid-name
import os
from abc import ABCMeta, abstractmethod
from CIME.XML.standard_module_setup import *
from CIME.utils import get_cime_root
from six import add_metaclass
logger = logging.getLogger(__name__)
def _get_components(value):
"""
>>> value = '-something ${shell ${NETCDF_PATH}/bin/nf-config --flibs} -lblas -llapack'
>>> _get_components(value)
[(False, '-something'), (True, '${NETCDF_PATH}/bin/nf-config --flibs'), (False, '-lblas -llapack')]
>>> value = '${shell ${NETCDF_PATH}/bin/nf-config --flibs} -lblas -llapack'
>>> _get_components(value)
[(True, '${NETCDF_PATH}/bin/nf-config --flibs'), (False, '-lblas -llapack')]
>>> value = '${shell ${NETCDF_PATH}/bin/nf-config --flibs}'
>>> _get_components(value)
[(True, '${NETCDF_PATH}/bin/nf-config --flibs')]
"""
value = value.strip()
components = []
curr_comp = ""
idx = 0
while idx < len(value):
if value[idx:idx+8] == "${shell ":
if curr_comp:
components.append((False, curr_comp.strip()))
curr_comp = ""
idx += 8
brace_cnt = 0
done = False
while not done:
if value[idx] == "{":
brace_cnt += 1
curr_comp += value[idx]
elif value[idx] == "}":
if brace_cnt == 0:
done = True
else:
brace_cnt -= 1
curr_comp += value[idx]
else:
curr_comp += value[idx]
idx += 1
components.append((True, curr_comp.strip()))
curr_comp = ""
else:
curr_comp += value[idx]
idx += 1
if curr_comp:
components.append((False, curr_comp.strip()))
return components
[docs]@add_metaclass(ABCMeta)
class MacroWriterBase(object):
"""Abstract base class for macro file writers.
The methods here come in three flavors:
1. indent_left/indent_right change the level of indent used internally by
the class.
2. The various methods ending in "_string" return strings relevant to the
build system.
3. The other methods write information to the file handle associated with
an individual writer instance.
Public attributes:
indent_increment - Number of spaces to indent if blocks (does not apply
to format-specific indentation, e.g. cases where
Makefiles must use tabs).
output - File-like object that output is written to.
Public methods:
indent_string
indent_left
indent_right
write_line
environment_variable_string
shell_command_string
variable_string
set_variable
append_variable
start_ifeq
end_ifeq
"""
indent_increment = 2
def __init__(self, output):
"""Initialize a macro writer.
Arguments:
output - File-like object (probably an io.TextIOWrapper), which
will be written to.
"""
self.output = output
self._indent_num = 0
[docs] def indent_string(self):
"""Return an appropriate number of spaces for the indent."""
return ' ' * self._indent_num
[docs] def indent_left(self):
"""Decrease the amount of line indent."""
self._indent_num -= 2
[docs] def indent_right(self):
"""Increase the amount of line indent."""
self._indent_num += 2
[docs] def write_line(self, line):
"""Write a single line of output, appropriately indented.
A trailing newline is added, whether or not the input has one.
"""
self.output.write(str(self.indent_string() + line + "\n"))
[docs] @abstractmethod
def environment_variable_string(self, name):
"""Return an environment variable reference."""
[docs] @abstractmethod
def shell_command_strings(self, command):
"""Return strings used to get the output of a shell command.
Implementations should return a tuple of three strings:
1. A line that is needed to get the output of the command (or None,
if a command can be run inline).
2. A string that can be used within a line to refer to the output.
3. A line that does any cleanup of temporary variables (or None, if
no cleanup is necessary).
Example usage:
# Get strings and write initial command.
(pre, var, post) = writer.shell_command_strings(command)
if pre is not None:
writer.write(pre)
# Use the variable to write an if block.
writer.start_ifeq(var, "TRUE")
writer.set_variable("foo", "bar")
writer.end_ifeq()
# Cleanup
if post is not None:
writer.write(post)
"""
[docs] @abstractmethod
def variable_string(self, name):
"""Return a string to refer to a variable with the given name."""
[docs] @abstractmethod
def set_variable(self, name, value):
"""Write out a statement setting a variable to some value."""
[docs] def append_variable(self, name, value):
"""Write out a statement appending a value to a string variable."""
var_string = self.variable_string(name)
self.set_variable(name, var_string + " " + value)
[docs] @abstractmethod
def start_ifeq(self, left, right):
"""Write out a statement to start a conditional block.
The arguments to this method are compared, and the block is entered
only if they are equal.
"""
[docs] @abstractmethod
def end_ifeq(self):
"""Write out a statement to end a block started with start_ifeq."""
# None class based method for version 1.0
[docs]def write_macros_file_v1(macros, compiler, os_, machine, macros_file="Macros", output_format="make"):
"""
Parse the config_compiler.xml file into a Macros file for the
given machine and compiler.
"""
# A few things can be used from environ if not in XML
for item in ["MPI_PATH", "NETCDF_PATH"]:
if not item in macros and item in os.environ:
logger.warning("Setting {} from Environment".format(item))
macros[item] = os.environ[item]
with open(macros_file, "w") as fd:
fd.write(
"""#
# COMPILER={}
# OS={}
# MACH={}
""".format(compiler, os_, machine)
)
if output_format == "make":
fd.write("#\n# Makefile Macros \n")
# print the settings out to the Macros file
for key, value in sorted(macros.items()):
if key == "_COND_":
pass
elif key.startswith("ADD_"):
fd.write("{}+={}\n\n".format(key[4:], value))
else:
fd.write("{}:={}\n\n".format(key, value))
elif output_format == "cmake":
fd.write(
'''#
# cmake Macros generated from $compiler_file
#
set(CMAKE_MODULE_PATH %s)
include(Compilers)
set(CMAKE_C_FLAGS_RELEASE "" CACHE STRING "Flags used by c compiler." FORCE)
set(CMAKE_C_FLAGS_DEBUG "" CACHE STRING "Flags used by c compiler." FORCE)
set(CMAKE_Fortran_FLAGS_RELEASE "" CACHE STRING "Flags used by Fortran compiler." FORCE)
set(CMAKE_Fortran_FLAGS_DEBUG "" CACHE STRING "Flags used by Fortran compiler." FORCE)
set(all_build_types "None Debug Release RelWithDebInfo MinSizeRel")
set(CMAKE_BUILD_TYPE "${CMAKE_BUILD_TYPE}" CACHE STRING "Choose the type of build, options are: ${all_build_types}." FORCE)
''' % os.path.join(get_cime_root(), "src", "CMake"))
# print the settings out to the Macros file, do it in
# two passes so that path values appear first in the
# file.
for key, value in sorted(macros.items()):
if key == "_COND_":
pass
else:
value = value.replace("(", "{").replace(")", "}")
if key.endswith("_PATH"):
if value.startswith("$"):
value = "$ENV{}".format(value[1:])
cmake_var = key.replace("NETCDF_PATH", "NetCDF_PATH").replace("PNETCDF_PATH", "Pnetcdf_PATH")
fd.write("set({} {})\n".format(cmake_var, value))
fd.write("list(APPEND CMAKE_PREFIX_PATH {})\n\n".format(value))
for key, value in sorted(macros.items()):
if key == "_COND_":
pass
else:
value = value.replace("(", "{").replace(")", "}")
if "CFLAGS" in key or "FFLAGS" in key or "CPPDEFS" in key or "SLIBS" in key or "LDFLAGS" in key:
if "shell " in value:
components = _get_components(value)
idx = 0
for is_shell, component in components:
component = component.replace("NETCDF", "NetCDF").replace("PNETCDF_PATH", "Pnetcdf_PATH")
if is_shell:
fd.write('execute_process(COMMAND {} OUTPUT_VARIABLE TEMP{:d})\n'.format(component, idx))
fd.write('string(REGEX REPLACE "\\n$" "" TEMP{0:d} "${{TEMP{0:d}}}")\n'.format(idx))
else:
fd.write('set(TEMP{:d} "{}")\n'.format(idx, component))
idx += 1
fd.write('set(TEMP "{}")\n'.format(" ".join(["${{TEMP{:d}}}".format(i) for i in range(idx)])))
else:
fd.write('set(TEMP "{}")\n'.format(value))
if "CFLAGS" in key:
fd.write("add_flags(CFLAGS ${TEMP})\n\n")
elif "FFLAGS" in key:
fd.write("add_flags(FFLAGS ${TEMP})\n\n")
elif "CPPDEFS" in key:
fd.write("list(APPEND CPPDEFS ${TEMP})\n\n")
elif "SLIBS" in key or "LDFLAGS" in key:
fd.write("add_flags(LDFLAGS ${TEMP})\n\n")
# Recursively print the conditionals, combining tests to avoid repetition
_parse_hash(macros["_COND_"], fd, 0, output_format)
def _parse_hash(macros, fd, depth, output_format, cmakedebug=""):
width = 2 * depth
for key, value in macros.items():
if type(value) is dict:
if output_format == "make" or "DEBUG" in key:
for key2, value2 in value.items():
if output_format == "make":
fd.write("{}ifeq ($({}), {}) \n".format(" " * width, key, key2))
_parse_hash(value2, fd, depth + 1, output_format, key2)
else:
if output_format == "make":
if key.startswith("ADD_"):
fd.write("{} {} += {}\n".format(" " * width, key[4:], value))
else:
fd.write("{} {} += {}\n".format(" " * width, key, value))
else:
value = value.replace("(", "{").replace(")", "}")
release = "DEBUG" if "TRUE" in cmakedebug else "RELEASE"
if "CFLAGS" in key:
fd.write("add_flags(CMAKE_C_FLAGS_{} {})\n\n".format(release, value))
elif "FFLAGS" in key:
fd.write("add_flags(CMAKE_Fortran_FLAGS_{} {})\n\n".format(release, value))
elif "CPPDEF" in key:
fd.write("add_config_definitions({} {})\n\n".format(release, value))
elif "SLIBS" in key or "LDFLAGS" in key:
fd.write("add_flags(CMAKE_EXE_LINKER_FLAGS_{} {})\n\n".format(release, value))
width -= 2
if output_format == "make" and depth > 0:
fd.write("{}endif\n\n".format(" " * width))