# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os
from typing import Tuple
import llnl.util.filesystem as fs
import llnl.util.tty as tty
import spack.builder
from .cmake import CMakeBuilder, CMakePackage
[docs]def cmake_cache_path(name, value, comment=""):
"""Generate a string for a cmake cache variable"""
return 'set({0} "{1}" CACHE PATH "{2}")\n'.format(name, value, comment)
[docs]def cmake_cache_string(name, value, comment=""):
"""Generate a string for a cmake cache variable"""
return 'set({0} "{1}" CACHE STRING "{2}")\n'.format(name, value, comment)
[docs]def cmake_cache_option(name, boolean_value, comment=""):
"""Generate a string for a cmake configuration option"""
value = "ON" if boolean_value else "OFF"
return 'set({0} {1} CACHE BOOL "{2}")\n'.format(name, value, comment)
[docs]class CachedCMakeBuilder(CMakeBuilder):
#: Phases of a Cached CMake package
#: Note: the initconfig phase is used for developer builds as a final phase to stop on
phases: Tuple[str, ...] = ("initconfig", "cmake", "build", "install")
#: Names associated with package methods in the old build-system format
legacy_methods: Tuple[str, ...] = CMakeBuilder.legacy_methods + (
"initconfig_compiler_entries",
"initconfig_mpi_entries",
"initconfig_hardware_entries",
"std_initconfig_entries",
"initconfig_package_entries",
)
#: Names associated with package attributes in the old build-system format
legacy_attributes: Tuple[str, ...] = CMakeBuilder.legacy_attributes + (
"cache_name",
"cache_path",
)
@property
def cache_name(self):
return "{0}-{1}-{2}@{3}.cmake".format(
self.pkg.name,
self.pkg.spec.architecture,
self.pkg.spec.compiler.name,
self.pkg.spec.compiler.version,
)
@property
def cache_path(self):
return os.path.join(self.pkg.stage.source_path, self.cache_name)
[docs] def initconfig_compiler_entries(self):
# This will tell cmake to use the Spack compiler wrappers when run
# through Spack, but use the underlying compiler when run outside of
# Spack
spec = self.pkg.spec
# Fortran compiler is optional
if "FC" in os.environ:
spack_fc_entry = cmake_cache_path("CMAKE_Fortran_COMPILER", os.environ["FC"])
system_fc_entry = cmake_cache_path("CMAKE_Fortran_COMPILER", self.pkg.compiler.fc)
else:
spack_fc_entry = "# No Fortran compiler defined in spec"
system_fc_entry = "# No Fortran compiler defined in spec"
entries = [
"#------------------{0}".format("-" * 60),
"# Compilers",
"#------------------{0}".format("-" * 60),
"# Compiler Spec: {0}".format(spec.compiler),
"#------------------{0}".format("-" * 60),
"if(DEFINED ENV{SPACK_CC})\n",
" " + cmake_cache_path("CMAKE_C_COMPILER", os.environ["CC"]),
" " + cmake_cache_path("CMAKE_CXX_COMPILER", os.environ["CXX"]),
" " + spack_fc_entry,
"else()\n",
" " + cmake_cache_path("CMAKE_C_COMPILER", self.pkg.compiler.cc),
" " + cmake_cache_path("CMAKE_CXX_COMPILER", self.pkg.compiler.cxx),
" " + system_fc_entry,
"endif()\n",
]
flags = spec.compiler_flags
# use global spack compiler flags
cppflags = " ".join(flags["cppflags"])
if cppflags:
# avoid always ending up with " " with no flags defined
cppflags += " "
cflags = cppflags + " ".join(flags["cflags"])
if cflags:
entries.append(cmake_cache_string("CMAKE_C_FLAGS", cflags))
cxxflags = cppflags + " ".join(flags["cxxflags"])
if cxxflags:
entries.append(cmake_cache_string("CMAKE_CXX_FLAGS", cxxflags))
fflags = " ".join(flags["fflags"])
if fflags:
entries.append(cmake_cache_string("CMAKE_Fortran_FLAGS", fflags))
# Cmake has different linker arguments for different build types.
# We specify for each of them.
if flags["ldflags"]:
ld_flags = " ".join(flags["ldflags"])
ld_format_string = "CMAKE_{0}_LINKER_FLAGS"
# CMake has separate linker arguments for types of builds.
for ld_type in ["EXE", "MODULE", "SHARED", "STATIC"]:
ld_string = ld_format_string.format(ld_type)
entries.append(cmake_cache_string(ld_string, ld_flags))
# CMake has libs options separated by language. Apply ours to each.
if flags["ldlibs"]:
libs_flags = " ".join(flags["ldlibs"])
libs_format_string = "CMAKE_{0}_STANDARD_LIBRARIES"
langs = ["C", "CXX", "Fortran"]
for lang in langs:
libs_string = libs_format_string.format(lang)
entries.append(cmake_cache_string(libs_string, libs_flags))
return entries
[docs] def initconfig_mpi_entries(self):
spec = self.pkg.spec
if not spec.satisfies("^mpi"):
return []
entries = [
"#------------------{0}".format("-" * 60),
"# MPI",
"#------------------{0}\n".format("-" * 60),
]
entries.append(cmake_cache_path("MPI_C_COMPILER", spec["mpi"].mpicc))
entries.append(cmake_cache_path("MPI_CXX_COMPILER", spec["mpi"].mpicxx))
entries.append(cmake_cache_path("MPI_Fortran_COMPILER", spec["mpi"].mpifc))
# Check for slurm
using_slurm = False
slurm_checks = ["+slurm", "schedulers=slurm", "process_managers=slurm"]
if any(spec["mpi"].satisfies(variant) for variant in slurm_checks):
using_slurm = True
# Determine MPIEXEC
if using_slurm:
if spec["mpi"].external:
# Heuristic until we have dependents on externals
mpiexec = "/usr/bin/srun"
else:
mpiexec = os.path.join(spec["slurm"].prefix.bin, "srun")
else:
mpiexec = os.path.join(spec["mpi"].prefix.bin, "mpirun")
if not os.path.exists(mpiexec):
mpiexec = os.path.join(spec["mpi"].prefix.bin, "mpiexec")
if not os.path.exists(mpiexec):
msg = "Unable to determine MPIEXEC, %s tests may fail" % self.pkg.name
entries.append("# {0}\n".format(msg))
tty.warn(msg)
else:
# starting with cmake 3.10, FindMPI expects MPIEXEC_EXECUTABLE
# vs the older versions which expect MPIEXEC
if self.pkg.spec["cmake"].satisfies("@3.10:"):
entries.append(cmake_cache_path("MPIEXEC_EXECUTABLE", mpiexec))
else:
entries.append(cmake_cache_path("MPIEXEC", mpiexec))
# Determine MPIEXEC_NUMPROC_FLAG
if using_slurm:
entries.append(cmake_cache_string("MPIEXEC_NUMPROC_FLAG", "-n"))
else:
entries.append(cmake_cache_string("MPIEXEC_NUMPROC_FLAG", "-np"))
return entries
[docs] def initconfig_hardware_entries(self):
spec = self.pkg.spec
entries = [
"#------------------{0}".format("-" * 60),
"# Hardware",
"#------------------{0}\n".format("-" * 60),
]
if spec.satisfies("^cuda"):
entries.append("#------------------{0}".format("-" * 30))
entries.append("# Cuda")
entries.append("#------------------{0}\n".format("-" * 30))
cudatoolkitdir = spec["cuda"].prefix
entries.append(cmake_cache_path("CUDA_TOOLKIT_ROOT_DIR", cudatoolkitdir))
cudacompiler = "${CUDA_TOOLKIT_ROOT_DIR}/bin/nvcc"
entries.append(cmake_cache_path("CMAKE_CUDA_COMPILER", cudacompiler))
entries.append(cmake_cache_path("CMAKE_CUDA_HOST_COMPILER", "${CMAKE_CXX_COMPILER}"))
return entries
[docs] def std_initconfig_entries(self):
return [
"#------------------{0}".format("-" * 60),
"# !!!! This is a generated file, edit at own risk !!!!",
"#------------------{0}".format("-" * 60),
"# CMake executable path: {0}".format(self.pkg.spec["cmake"].command.path),
"#------------------{0}\n".format("-" * 60),
]
[docs] def initconfig_package_entries(self):
"""This method is to be overwritten by the package"""
return []
[docs] def initconfig(self, pkg, spec, prefix):
cache_entries = (
self.std_initconfig_entries()
+ self.initconfig_compiler_entries()
+ self.initconfig_mpi_entries()
+ self.initconfig_hardware_entries()
+ self.initconfig_package_entries()
)
with open(self.cache_name, "w") as f:
for entry in cache_entries:
f.write("%s\n" % entry)
f.write("\n")
@property
def std_cmake_args(self):
args = super(CachedCMakeBuilder, self).std_cmake_args
args.extend(["-C", self.cache_path])
return args
[docs] @spack.builder.run_after("install")
def install_cmake_cache(self):
fs.mkdirp(self.pkg.spec.prefix.share.cmake)
fs.install(self.cache_path, self.pkg.spec.prefix.share.cmake)
[docs]class CachedCMakePackage(CMakePackage):
"""Specialized class for packages built using CMake initial cache.
This feature of CMake allows packages to increase reproducibility,
especially between Spack- and manual builds. It also allows packages to
sidestep certain parsing bugs in extremely long ``cmake`` commands, and to
avoid system limits on the length of the command line.
"""
CMakeBuilder = CachedCMakeBuilder
[docs] def flag_handler(self, name, flags):
if name in ("cflags", "cxxflags", "cppflags", "fflags"):
return None, None, None # handled in the cmake cache
return flags, None, None