import os
import subprocess
from logging import debug
from shutil import copy, copytree, ignore_patterns, rmtree
[docs]
def compile(self):
"""Compile (or copy) the CHIMERE forward, TL, and adjoint executables.
Follows a two-strategy approach:
1. **Copy pre-compiled executables** (default) — copies
``fwdchimere.e``, ``tlchimere.e``, and ``achimere.e`` from
``self.direxec`` into ``$workdir/model/``. Skipped when
``force-recompile = True``.
2. **Compile from sources** — triggered when ``auto-recompile = True``
(or when the pre-compiled executables are not found). Clones the
source tree from ``self.dir_sources`` (or ``self.direxec``),
patches the Makefiles for the configured compiler and library paths
(NetCDF, GRIB, LDFLAGS), and runs ``make`` for each of the three
build variants (forward, TL, adjoint) in PROD or DEBUG mode.
Args:
self (Plugin): CHIMERE model plugin instance (carries ``workdir``,
``direxec``, ``dir_sources``, compiler flags, and
``force-recompile`` / ``auto-recompile`` flags).
Raises:
RuntimeError: if ``auto-recompile`` is False and the executables
cannot be found.
"""
comp_dir = f"{self.workdir}/model"
# Copying the executables
try:
# If force-recompile = True, return IOError to avoid copying
if getattr(self, "force-recompile"):
raise IOError
# Otherwise, try copying executables
source = f"{self.direxec}/src/fwdchimere.e"
copy(source, comp_dir)
source = f"{self.direxec}/src_tl/tlchimere.e"
copy(source, comp_dir)
source = f"{self.direxec}/src_ad/achimere.e"
copy(source, comp_dir)
return
except IOError as e:
if not getattr(self, "auto-recompile"):
raise RuntimeError(
f"CHIMERE could not find executables ({self.direxec}) and was not asked to "
"compile them; specify auto-recompile = True in Yaml to do so"
) from e
# Otherwise, re-compile
# Copying sources locally; overwrite folder if exists
if os.path.isdir(f"{comp_dir}/sources"):
rmtree(f"{comp_dir}/sources")
dir_sources = self.dir_sources if self.dir_sources != "" else self.direxec
copytree(
dir_sources,
f"{comp_dir}/sources",
ignore=ignore_patterns("*.a"),
)
# Modifying LDFLAGS if needed
if hasattr(self, "LDFLAGS"):
for fld in ["src", "src_tl", "src_ad"]:
old_lines = open(
f"{comp_dir}/sources/{fld}/Makefile_chimere", "r"
).readlines()
old_lines = [ln.rstrip() for ln in old_lines]
for k, ln in enumerate(old_lines):
if ln[:7] == "LDFLAGS":
old_lines[k] = f"LDFLAGS =\t{self.LDFLAGS}"
with open(f"{comp_dir}/sources/{fld}/Makefile_chimere", "w") as f:
f.write("\n".join(old_lines))
# Modifying Makefile header if needed
makefile_sed = os.path.join(comp_dir, "sources", "Makefile.hdr.sed")
with open(makefile_sed, "r") as f:
lines = [line.strip() for line in f.readlines()]
# Join lines that are split with a backslash (keep backslash and newline characters)
joined_lines = []
join = False
for line in lines:
if join:
joined_lines[-1] = joined_lines[-1] + "\n" + line # append line to previous
else:
joined_lines.append(line) # new line
# Join next line if current line ends with a backslash
join = line.endswith("\\")
gfortran_line = False
new_lines = []
for line in joined_lines:
if gfortran_line:
gfortran_line = False # Reset flag
if line.startswith("MF77") and hasattr(self, "gfortran_executable"):
line = f"MF77\t=\t{self.gfortran_executable}" # Replace line with new value
elif line.startswith("F77FLAGS1") and hasattr(self, "COMPILOPTIONS"):
line = line + " " + self.COMPILOPTIONS # append value to line
elif line == "REALFC\t=\tgfortran":
gfortran_line = True # Replace next line
else:
for varname in ("NETCDFLIB", "NETCDFINC", "GRIBLIB", "GRIBINC"):
if line.startswith(varname) and hasattr(self, varname):
line = f"{varname}\t=\t{getattr(self, varname)}" # Replace line with new value
break
new_lines.append(line)
with open(makefile_sed, "w") as f:
f.write("\n".join(new_lines))
# Now compiling
comp_mode = "" if getattr(self, "compile-mode") == "PROD" else "--debug"
comp_clean = "--clean" if getattr(self, "compile-clean") else ""
compiler = f"--{getattr(self, 'compiler', 'ifort')}"
for mode, pref, suff in zip(
["A", "L", "D"], ["a", "tl", "fwd"], ["_ad", "_tl", ""]
):
if not mode in getattr(self, "compile-only"):
continue
debug(f"Compiling CHIMERE {mode}")
with open(f"{comp_dir}/CHIMERE_compiling_{mode}.log", "w") as log:
process = subprocess.Popen(
f"./compile-chimere -m "
f"{mode} {comp_mode} {comp_clean} {compiler}",
shell=True,
stdout=log,
cwd=f"{comp_dir}/sources/",
stderr=subprocess.PIPE,
)
_, stderr = process.communicate()
# Print errors if no executable created (or if forced to)
exe_file = f"{comp_dir}/sources/src{suff}/{pref}chimere.e"
if not os.path.isfile(exe_file) or getattr(self, "force-compile-stderr"):
debug("CHIMERE returned errors during compiling.")
debug("There might be some bugs in the Fortran or in the "
"libraries loaded in the system:")
debug("### START OF CHIMERE ERROR MESSAGE ###")
for ln in stderr.decode().split("\n"):
debug(ln)
debug("### END OF CHIMERE ERROR MESSAGE ###")
# Copy executable at model root directory
copy(exe_file, comp_dir)