import json
import os
import shutil
from buildtest.builders.base import BuilderBase
from buildtest.cli.compilers import BuildtestCompilers
from buildtest.exceptions import BuildTestError
from buildtest.tools.modules import get_module_commands
from buildtest.utils.file import resolve_path
from buildtest.utils.tools import deep_get
[docs]class CompilerBuilder(BuilderBase):
"""This is a subclass of BuilderBase used for building test that uses ``type: compiler`` in the buildspec."""
type = "compiler"
# Fortran Extensions Links:
# https://software.intel.com/content/www/us/en/develop/documentation/fortran-compiler-developer-guide-and-reference/top/compiler-setup/using-the-command-line/understanding-file-extensions.html
# Fortran Extensions: http://fortranwiki.org/fortran/show/File+extensions
lang_ext_table = {
".c": "C",
".cc": "C++",
".cxx": "C++",
".cpp": "C++",
".c++": "C++",
".f90": "Fortran",
".F90": "Fortran",
".f95": "Fortran",
".f03": "Fortran",
".f": "Fortran",
".F": "Fortran",
".FOR": "Fortran",
".for": "Fortran",
".FTN": "Fortran",
".ftn": "Fortran",
}
cc = None
cxx = None
fc = None
ldflags = None
cflags = None
cxxflags = None
fflags = None
cppflags = None
def __init__(
self,
name,
recipe,
buildspec,
buildexecutor,
executor,
configuration,
compiler=None,
testdir=None,
numprocs=None,
numnodes=None,
):
super().__init__(
name=name,
recipe=recipe,
buildspec=buildspec,
executor=executor,
buildexecutor=buildexecutor,
testdir=testdir,
numprocs=numprocs,
numnodes=numnodes,
compiler=compiler,
)
self.compiler = compiler
self.configuration = configuration
self.metadata["compiler"] = compiler
self.compiler_section = self.recipe["compilers"]
self.sourcefile = self.recipe["source"]
[docs] def setup(self):
"""The setup method is responsible for process compiler section, getting modules
pre_build, post_build, pre_run, post_run section and generate compilation
and run command. This method invokes other methods and set values in class
variables. This method is called by self.generate_script method.
"""
self._resolve_source()
self.lang = self._detect_lang(self.sourcefile)
# set executable name and assign to self.executable
self.executable = "%s.exe" % os.path.basename(self.sourcefile)
self.exec_variable = f"_EXEC={self.executable}"
self._process_compiler_config()
self.metrics = deep_get(
self.recipe, "executors", self.executor, "metrics"
) or self.recipe.get("metrics")
# get environment variables
self.envvars = (
deep_get(self.compiler_section, "config", self.compiler, "env")
or deep_get(self.compiler_section, "default", self.compiler_group, "env")
or deep_get(self.compiler_section, "default", "all", "env")
)
# get environment variables
self.vars = (
deep_get(self.compiler_section, "config", self.compiler, "vars")
or deep_get(self.compiler_section, "default", self.compiler_group, "vars")
or deep_get(self.compiler_section, "default", "all", "vars")
)
# get status
self.status = (
deep_get(self.compiler_section, "config", self.compiler, "status")
or deep_get(self.compiler_section, "default", self.compiler_group, "status")
or deep_get(self.compiler_section, "default", "all", "status")
)
# compiler set in compilers 'config' section, we try to get module lines using self._get_modules
self.modules = get_module_commands(
deep_get(self.compiler_section, "config", self.compiler, "module")
)
if not self.modules:
self.modules = get_module_commands(self.bc_compiler.get("module"))
self.pre_build = (
deep_get(self.compiler_section, "config", self.compiler, "pre_build")
or deep_get(
self.compiler_section, "default", self.compiler_group, "pre_build"
)
or deep_get(self.compiler_section, "default", "all", "pre_build")
)
self.post_build = (
deep_get(self.compiler_section, "config", self.compiler, "post_build")
or deep_get(
self.compiler_section, "default", self.compiler_group, "post_build"
)
or deep_get(self.compiler_section, "default", "all", "post_build")
)
self.pre_run = (
deep_get(self.compiler_section, "config", self.compiler, "pre_run")
or deep_get(
self.compiler_section, "default", self.compiler_group, "pre_run"
)
or deep_get(self.compiler_section, "default", "all", "pre_run")
)
self.post_run = (
deep_get(self.compiler_section, "config", self.compiler, "post_run")
or deep_get(
self.compiler_section, "default", self.compiler_group, "post_run"
)
or deep_get(self.compiler_section, "default", "all", "post_run")
)
self.compile_cmd = self._compile_cmd()
self.run_cmd = self._run_cmd()
# Compiler schema needs 'source' key in order to compile test if 'build' is specified. Schema needs
# 'binary' if one just wants to run test without compilation
# if mode: [build, run] - compile and run test
# if mode: [build ] - compile test
# if mode: [run ] - run test
[docs] def generate_script(self):
"""This method is responsible for generating test script for compiler schema.
The method ``generate_script`` is implemented in each subclass because
implementation on test generation differs across schema types.
This method will add the lines into list which comprise content
of test. The method will return a list containing lines of test script.
"""
self.setup()
# every test starts with shebang line
lines = [self.shebang]
batch_dict = {}
cray_dict = {}
# get sbatch, bsub, cobalt, pbs, batch property and store in batch dictionary.
# The order of lookup is in order of precedence
for batch in ["sbatch", "bsub", "cobalt", "pbs"]:
batch_dict[batch] = (
deep_get(self.compiler_section, "config", self.compiler, batch)
or deep_get(
self.compiler_section, "default", self.compiler_group, batch
)
or deep_get(self.compiler_section, "default", "all", batch)
)
# setting these values override values from Builder.sched_init() method
self.sbatch = batch_dict["sbatch"]
self.bsub = batch_dict["bsub"]
self.pbs = batch_dict["pbs"]
self.cobalt = batch_dict["cobalt"]
sched_lines = self.get_job_directives()
if sched_lines:
lines += sched_lines
# get cray burst buffer (BB) and datawarp (DW) fields in order of precedence.
for name in ["BB", "DW"]:
cray_dict[name] = (
deep_get(self.compiler_section, "config", self.compiler, name)
or deep_get(self.compiler_section, "default", self.compiler_group, name)
or deep_get(self.compiler_section, "default", "all", name)
)
burst_buffer_lines = self._get_burst_buffer(cray_dict["BB"])
if burst_buffer_lines:
lines.append("### START OF BURST BUFFER DIRECTIVES ###")
lines += burst_buffer_lines
lines.append("### END OF BURST BUFFER DIRECTIVES ###")
data_warp_lines = self._get_data_warp(cray_dict["DW"])
if data_warp_lines:
lines.append("### START OF DATAWARP DIRECTIVES ###")
lines += data_warp_lines
lines.append("### END OF DATAWARP DIRECTIVES ###")
lines.append("\n")
lines.append(self._emit_set_command())
lines.append("# name of executable")
lines += [self.exec_variable]
env_lines = self._get_environment(self.envvars)
if env_lines:
lines += env_lines
# get variables
var_lines = self._get_variables(self.vars)
if var_lines:
lines += var_lines
# if 'module' defined in Buildspec add modules to test
if self.modules:
lines.append("# Loading modules")
lines += self.modules
if self.pre_build:
lines.append("### START OF PRE BUILD SECTION ###")
lines.append(self.pre_build)
lines.append("### END OF PRE BUILD SECTION ###")
lines.append("\n")
lines.append("# Compilation Line")
lines.append(self.compile_cmd)
lines.append("\n")
if self.post_build:
lines.append("### START OF POST BUILD SECTION ###")
lines.append(self.post_build)
lines.append("### END OF POST BUILD SECTION ###")
lines.append("\n")
if self.pre_run:
lines.append("### START OF PRE RUN SECTION ###")
lines.append(self.pre_run)
lines.append("### END OF PRE RUN SECTION ###")
lines.append("\n")
# add run command
lines.append("# Run executable")
lines.append(self.run_cmd)
lines.append("\n")
if self.post_run:
lines.append("### START OF POST RUN SECTION ###")
lines.append(self.post_run)
lines.append("### END OF POST RUN SECTION ###")
lines.append("\n")
return lines
[docs] def _resolve_source(self):
"""This method resolves full path to source file, it checks for absolute
path first before checking relative path that is relative to
Buildspec recipe.
"""
# attempt to resolve path based on 'source' field.
# 1. The source file can be absolute path and if exists we use this
# 2. The source file can be relative path to where buildspec is located
self.abspath_sourcefile = resolve_path(self.sourcefile) or resolve_path(
os.path.join(os.path.dirname(self.buildspec), self.sourcefile)
)
# raise error if we can't find source file to compile
if not self.abspath_sourcefile:
raise BuildTestError(
f"Failed to resolve path specified in field 'source': {self.sourcefile}"
)
[docs] def _detect_lang(self, sourcefile):
"""This method will return the Programming Language based by looking up
file extension of source file.
"""
self.logger.debug(
f"[{self.name}]: Detecting programming language for source file: {sourcefile}"
)
ext = os.path.splitext(sourcefile)[1]
self.logger.debug(
f"Found file extension: {ext}, now we will attempt to lookup programming language based on extension"
)
# if ext not in self.lang_ext_table then raise an error. This table consist of all file extensions that map to a Programming Language
if ext not in self.lang_ext_table:
raise BuildTestError(
f"[{self.name}]: Unable to detect Program Language based on extension: {ext} in source: {sourcefile}"
)
# Set Programming Language based on ext. Programming Language could be (C, C++, Fortran)
lang = self.lang_ext_table[ext]
self.logger.debug(
f"[{self.name}]: Based on extension: {ext} the programming language is: {lang}"
)
return lang
[docs] def _compile_cmd(self):
"""This method generates the compilation line and returns the output as
a list. The compilation line depends on the the language detected
that is stored in variable ``self.lang``.
"""
cmd = []
# Generate C compilation line
if self.lang == "C":
cmd = [
self.cc,
self.cppflags,
self.cflags,
"-o $_EXEC",
self.abspath_sourcefile,
self.ldflags,
]
# Generate C++ compilation line
elif self.lang == "C++":
cmd = [
self.cxx,
self.cppflags,
self.cxxflags,
"-o $_EXEC",
self.abspath_sourcefile,
self.ldflags,
]
# Generate Fortran compilation line
elif self.lang == "Fortran":
cmd = [
self.fc,
self.cppflags,
self.fflags,
"-o $_EXEC",
self.abspath_sourcefile,
self.ldflags,
]
# remove any None from list
cmd = list(filter(None, cmd))
cmd = " ".join(cmd)
return cmd
[docs] def _run_cmd(self):
"""This method builds the run command which refers to how to run the
generated binary after compilation.
"""
# order of precedence on how to generate run line when executing binary.
# 1. Check in 'config' section within compiler
# 2. Check in 'default' section within compiler group
# 3. Check in 'default' section within 'all' section
# 4. Last resort run binary standalone
run_line = (
deep_get(self.compiler_section, "config", self.compiler, "run")
or deep_get(self.compiler_section, "default", self.compiler_group, "run")
or deep_get(self.compiler_section, "default", "all", "run")
or "./$_EXEC"
)
return run_line
[docs] def _process_compiler_config(self):
"""This method is responsible for setting cc, fc, cxx class variables based
on compiler selection. The order of precedence is ``config``, ``default``,
then buildtest setting. Compiler settings in 'config' takes highest precedence,
this overrides any configuration in 'default'. Finally we resort to compiler
configuration in buildtest setting if none defined. This method is responsible
for setting cc, fc, cxx, cflags, cxxflags, fflags, ldflags, and cppflags.
"""
bc = BuildtestCompilers(configuration=self.configuration)
self.compiler_group = bc.compiler_name_to_group[self.compiler]
self.logger.debug(
f"[{self.name}]: compiler: {self.compiler} belongs to compiler group: {self.compiler_group}"
)
# compiler from buildtest settings
self.bc_compiler = self.configuration.target_config["compilers"]["compiler"][
self.compiler_group
][self.compiler]
self.logger.debug(self.bc_compiler)
# set compiler values based on 'default' property in buildspec. This can override
# compiler setting defined in configuration file. If default is not set we load from buildtest settings for appropriate compiler.
# set compiler variables to ones defined in buildtest configuration
self.cc = self.bc_compiler["cc"]
self.cxx = self.bc_compiler["cxx"]
self.fc = self.bc_compiler["fc"]
self.logger.debug(
f"[{self.name}]: Compiler setting for {self.compiler} from configuration file"
)
self.logger.debug(
f"[{self.name}]: {self.compiler}: {json.dumps(self.bc_compiler, indent=2)}"
)
# if default compiler setting provided in buildspec let's assign it.
if deep_get(self.compiler_section, "default", self.compiler_group):
self.cc = (
self.compiler_section["default"][self.compiler_group].get("cc")
or self.cc
)
self.fc = (
self.compiler_section["default"][self.compiler_group].get("fc")
or self.fc
)
self.cxx = (
self.compiler_section["default"][self.compiler_group].get("cxx")
or self.cxx
)
self.cflags = self.compiler_section["default"][self.compiler_group].get(
"cflags"
)
self.cxxflags = self.compiler_section["default"][self.compiler_group].get(
"cxxflags"
)
self.fflags = self.compiler_section["default"][self.compiler_group].get(
"fflags"
)
self.ldflags = self.compiler_section["default"][self.compiler_group].get(
"ldflags"
)
self.cppflags = self.compiler_section["default"][self.compiler_group].get(
"cppflags"
)
# if compiler instance defined in config section read from buildspec. This overrides default section if specified
if deep_get(self.compiler_section, "config", self.compiler):
self.logger.debug(
f"[{self.name}]: Detected compiler: {self.compiler} in 'config' scope overriding default compiler group setting for: {self.compiler_group}"
)
self.cc = (
self.compiler_section["config"][self.compiler].get("cc") or self.cc
)
self.fc = (
self.compiler_section["config"][self.compiler].get("fc") or self.fc
)
self.cxx = (
self.compiler_section["config"][self.compiler].get("cxx") or self.cxx
)
self.cflags = (
self.compiler_section["config"][self.compiler].get("cflags")
or self.cflags
)
self.cxxflags = (
self.compiler_section["config"][self.compiler].get("cxxflags")
or self.cxxflags
)
self.fflags = (
self.compiler_section["config"][self.compiler].get("fflags")
or self.fflags
)
self.cppflags = (
self.compiler_section["config"][self.compiler].get("cppflags")
or self.cppflags
)
self.ldflags = (
self.compiler_section["config"][self.compiler].get("ldflags")
or self.ldflags
)
self.logger.debug(
f"cc: {self.cc}, cxx: {self.cxx} fc: {self.fc} cppflags: {self.cppflags} cflags: {self.cflags} fflags: {self.fflags} ldflags: {self.ldflags}"
)
# this condition is a safety check before compiling code to ensure if all C, C++, Fortran compiler not set we raise error
if not self.cc and not self.cxx and not self.fc:
raise BuildTestError(
"Unable to set C, C++, and Fortran compiler wrapper, please specify 'cc', 'cxx','fc' in your compiler settings in buildtest configuration or specify in buildspec file. "
)
[docs] def set_cc(self, cc):
self.cc = cc
[docs] def set_cxx(self, cxx):
self.cxx = cxx
[docs] def set_fc(self, fc):
self.fc = fc
[docs] def set_cflags(self, cflags):
self.cflags = cflags
[docs] def set_fflags(self, fflags):
self.fflags = fflags
[docs] def set_cxxflags(self, cxxflags):
self.cxxflags = cxxflags
[docs] def set_cppflags(self, cppflags):
self.cppflags = cppflags
[docs] def set_ldflags(self, ldflags):
self.ldflags = ldflags
[docs] def get_cc(self):
return self.cc
[docs] def get_cxx(self):
return self.cxx
[docs] def get_fc(self):
return self.fc
[docs] def get_cflags(self):
return self.cflags
[docs] def get_cxxflags(self):
return self.cxxflags
[docs] def get_fflags(self):
return self.fflags
[docs] def get_cppfilags(self):
return self.cppflags
[docs] def get_ldflags(self):
return self.ldflags
[docs] def get_path(self):
"""This method returns the full path for C, C++, Fortran compilers"""
path = {
self.cc: shutil.which(self.cc),
self.cxx: shutil.which(self.cxx),
self.fc: shutil.which(self.fc),
}
return path