Tutorial setup
If you have not done the prior sections, you’ll need to start the docker image:
docker run -it ghcr.io/spack/tutorial:sc25
and then set Spack up like this:
git clone --depth=2 --branch=releases/v1.1 https://github.com/spack/spack
. spack/share/spack/setup-env.sh
spack repo update builtin --tag v2025.11.0
spack tutorial -y
spack bootstrap now
spack compiler find
See the Basic Installation Tutorial for full details on setup.
For more help, join us in the #tutorial channel on Slack – get an invitation at slack.spack.io
Spack Package Build Systems¶
You may begin to notice after writing a couple of package template files that a pattern emerges for some packages.
For example, you may find yourself writing an install() method that invokes: configure, cmake, make, make install.
You may also find yourself writing "prefix=" + prefix as an argument to configure or cmake.
Rather than having you repeat these lines for all packages, Spack has classes that can take care of these patterns.
In addition, these package files allow for finer-grained control of these build systems.
In this section, we will describe each build system and give examples on how these can be used to simplify packaging.
Package Class Hierarchy¶
The above diagram gives a high-level view of the class hierarchy and how each package relates.
Each build system specific class inherits from the PackageBase superclass.
The bulk of the common work is done in this superclass which includes fetching, extracting to a staging directory and the install process.
Each subclass then adds additional build-system-specific functionality.
In the following sections, we will go over examples of how to utilize each subclass and see how powerful these abstractions are when packaging.
Package¶
We’ve already seen examples of using the generic Package class in our walkthrough for writing package files, so we won’t be spending much time with it here.
Briefly, the Package class allows for arbitrary control over the build process, whereas subclasses rely on certain patterns (e.g. configure make make install) to be useful.
The generic Package class is particularly useful for packages that have a non-conventional build process, as it allows the packager to use Spack’s helper functions to customize the building and installing of a package fully.
Autotools¶
As we have seen earlier, packages using Autotools use configure, make and make install commands to execute the build and install process.
In our Package class, your typical build incantation will consist of the following:
def install(self, spec, prefix):
configure("--prefix=" + prefix)
make()
make("install")
You’ll see that this looks similar to what we wrote in our packaging tutorial.
The AutotoolsPackage subclass aims to simplify writing package files for Autotools-based software and provides convenient methods to manipulate each of the different phases for an Autotools build system.
AutotoolsPackage builds consist of four main phases, each cooresponding to a method that can be overridden:
autoreconf()configure()build()install()
Each of these phases has sensible defaults.
Let’s take a quick look at some of the internals of the Autotools class:
$ spack edit --build-system autotools
This will open the AutotoolsPackage file in your text editor.
Note
The examples showing code for these classes are abridged to avoid having long examples. We only show what is relevant to the packager.
1 "force_autoreconf",
2 "autoreconf_extra_args",
3 "install_libtool_archives",
4 "patch_config_files",
5 "configure_directory",
6 "configure_abs_path",
7 "build_directory",
8 "autoreconf_search_path_args",
9 )
10
11 #: Whether to update ``libtool`` (e.g. for Arm/Clang/Fujitsu/NVHPC compilers)
12 patch_libtool = True
13
14 #: Targets for ``make`` during the :py:meth:`~.AutotoolsBuilder.build` phase
15 build_targets: List[str] = []
16 #: Targets for ``make`` during the :py:meth:`~.AutotoolsBuilder.install` phase
17 install_targets = ["install"]
18
19 #: Callback names for build-time test
20 build_time_test_callbacks = ["check"]
21
22 def autoreconf(self, pkg: AutotoolsPackage, spec: Spec, prefix: Prefix) -> None:
23 """Not needed usually, configure should be already there"""
24
25 # If configure exists nothing needs to be done
26 if os.path.exists(self.configure_abs_path):
27 return
28
29 # Else try to regenerate it, which requires a few build dependencies
30 ensure_build_dependencies_or_raise(
31 spec=spec,
32 dependencies=["autoconf", "automake", "libtool"],
33 error_msg="Cannot generate configure",
34 )
35
36 tty.msg("Configure script not found: trying to generate it")
37 tty.warn("*********************************************************")
38 tty.warn("* If the default procedure fails, consider implementing *")
39 tty.warn("* a custom AUTORECONF phase in the package *")
40 tty.warn("*********************************************************")
41 with working_dir(self.configure_directory):
42 # This line is what is needed most of the time
43 # --install, --verbose, --force
44 autoreconf_args = ["-ivf"]
45 autoreconf_args += self.autoreconf_search_path_args
46 autoreconf_args += self.autoreconf_extra_args
47 self.pkg.module.autoreconf(*autoreconf_args)
48
49 def configure(self, pkg: AutotoolsPackage, spec: Spec, prefix: Prefix) -> None:
50 """Run "configure", with the arguments specified by the builder and an
Important to note are the highlighted lines.
These properties allow the packager to set what build targets and install targets they want for their package.
If, for example, we wanted to add as our build target foo then we can append to our build_targets list:
build_targets = ["foo"]
Which is similar to invoking make in our Package
make("foo")
This is useful if we have packages that ignore environment variables and need a command line argument.
Another thing to take note of is in the configure() method in AutotoolsPackage.
Here we see that the --prefix argument is already included since it is a common pattern amongst packages using Autotools.
Therefore, we typically only need to override the configure_args() method to return a list of additional arguments.
The configure() method will then append these to the standard arguments.
Packagers also have the option to run autoreconf in case a package needs to update the build system and generate a new configure.
However, for the most part this will be unnecessary.
Let’s look at the mpileaks package.py file that we worked on earlier:
$ spack edit mpileaks
Notice that mpileaks was originally written as a generic Package but uses the Autotools build system.
Although this package is acceptable, let’s covert it to an AutotoolsPackage to simplify it further.
1# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
2# Spack Project Developers. See the top-level COPYRIGHT file for details.
3#
4# SPDX-License-Identifier: (Apache-2.0 OR MIT)
5
6from spack import *
7
8
9class Mpileaks(AutotoolsPackage):
10 """Tool to detect and report leaked MPI objects like MPI_Requests and
11 MPI_Datatypes."""
12
13 homepage = "https://github.com/hpc/mpileaks"
14 url = "https://github.com/hpc/mpileaks/releases/download/v1.0/mpileaks-1.0.tar.gz"
15
16 version('1.0', '8838c574b39202a57d7c2d68692718aa')
17
18 depends_on("mpi")
19 depends_on("adept-utils")
20 depends_on("callpath")
21
22 def install(self, spec, prefix):
23 configure("--prefix=" + prefix,
24 "--with-adept-utils=" + spec['adept-utils'].prefix,
25 "--with-callpath=" + spec['callpath'].prefix)
26 make()
27 make("install")
We first inherit from the AutotoolsPackage class.
Although we could keep the install() method, most of it can be handled by the AutotoolsPackage base class.
In fact, the only thing that needs to be overridden is configure_args().
1# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
2# Spack Project Developers. See the top-level COPYRIGHT file for details.
3#
4# SPDX-License-Identifier: (Apache-2.0 OR MIT)
5
6from spack import *
7
8
9class Mpileaks(AutotoolsPackage):
10 """Tool to detect and report leaked MPI objects like MPI_Requests and
11 MPI_Datatypes."""
12
13 homepage = "https://github.com/hpc/mpileaks"
14 url = "https://github.com/hpc/mpileaks/releases/download/v1.0/mpileaks-1.0.tar.gz"
15
16 version('1.0', '8838c574b39202a57d7c2d68692718aa')
17
18 variant("stackstart", values=int, default=0,
19 description="Specify the number of stack frames to truncate")
20
21 depends_on("mpi")
22 depends_on("adept-utils")
23 depends_on("callpath")
24
25 def configure_args(self):
26 stackstart = int(self.spec.variants['stackstart'].value)
27 args = ["--with-adept-utils=" + spec['adept-utils'].prefix,
28 "--with-callpath=" + spec['callpath'].prefix]
29 if stackstart:
30 args.extend([f'--with-stack-start-c={stackstart}',
31 f'--with-stack-start-fortran={stackstart}'])
32 return args
Since Spack’s AutotoolsPackage takes care of setting the prefix for us, we can exclude that as an argument to configure.
Our package file looks simpler, and we don’t need to worry about whether we have properly included configure and make.
This version of the mpileaks package installs the same as the previous, but the AutotoolsPackage class lets us do it with a cleaner looking package file.
Makefile¶
Packages that utilize Make or a Makefile usually require you to edit a Makefile to set up platform and compiler-specific variables.
These packages are handled by the MakefilePackage subclass which provides convenience methods to help write these types of packages.
A MakefilePackage build has three phases that can be overridden by the packager:
edit()build()install()
Packagers then have the ability to control how a Makefile is edited, and what targets to include for the build phase or install phase.
Let’s also take a look inside the MakefilePackage class:
$ spack edit --build-system makefile
Take note of the following:
1
2@register_builder("makefile")
3class MakefileBuilder(BuilderWithDefaults):
4 """The Makefile builder encodes the most common way of building software with
5 Makefiles. It has three phases that can be overridden, if need be:
6
7 1. :py:meth:`~.MakefileBuilder.edit`
8 2. :py:meth:`~.MakefileBuilder.build`
9 3. :py:meth:`~.MakefileBuilder.install`
10
11 It is usually necessary to override the :py:meth:`~.MakefileBuilder.edit`
12 phase (which is by default a no-op), while the other two have sensible defaults.
13
14 For a finer tuning you may override:
15
16 +-----------------------------------------------+--------------------+
17 | **Method** | **Purpose** |
18 +===============================================+====================+
19 | :py:attr:`~.MakefileBuilder.build_targets` | Specify ``make`` |
20 | | targets for the |
21 | | build phase |
22 +-----------------------------------------------+--------------------+
23 | :py:attr:`~.MakefileBuilder.install_targets` | Specify ``make`` |
24 | | targets for the |
25 | | install phase |
26 +-----------------------------------------------+--------------------+
27 | :py:meth:`~.MakefileBuilder.build_directory` | Directory where the|
28 | | Makefile is located|
29 +-----------------------------------------------+--------------------+
30 """
31
32 phases = ("edit", "build", "install")
33
34 #: Names associated with package methods in the old build-system format
35 package_methods = ("check", "installcheck")
36
37 #: Names associated with package attributes in the old build-system format
38 package_attributes = (
39 "build_targets",
40 "install_targets",
41 "build_time_test_callbacks",
42 "install_time_test_callbacks",
43 "build_directory",
44 )
45
46 #: Targets for ``make`` during the :py:meth:`~.MakefileBuilder.build` phase
47 build_targets: List[str] = []
48 #: Targets for ``make`` during the :py:meth:`~.MakefileBuilder.install` phase
49 install_targets = ["install"]
50
51 #: Callback names for build-time test
52 build_time_test_callbacks = ["check"]
53
54 #: Callback names for install-time test
55 install_time_test_callbacks = ["installcheck"]
56
57 @property
58 def build_directory(self) -> str:
59 """Return the directory containing the main Makefile."""
60 return self.pkg.stage.source_path
61
62 def edit(self, pkg: MakefilePackage, spec: Spec, prefix: Prefix) -> None:
63 """Edit the Makefile before calling make. The default is a no-op."""
64 pass
65
66 def build(self, pkg: MakefilePackage, spec: Spec, prefix: Prefix) -> None:
67 """Run "make" on the build targets specified by the builder."""
68 with working_dir(self.build_directory):
69 pkg.module.make(*self.build_targets)
70
71 def install(self, pkg: MakefilePackage, spec: Spec, prefix: Prefix) -> None:
72 """Run "make" on the install targets specified by the builder."""
Similar to Autotools, MakefilePackage class has properties that can be set by the packager.
We can also override the different methods highlighted.
Let’s try to recreate the Bowtie package:
$ spack create -f https://downloads.sourceforge.net/project/bowtie-bio/bowtie/1.2.1.1/bowtie-1.2.1.1-src.zip
==> This looks like a URL for bowtie
==> Found 1 version of bowtie:
1.2.1.1 https://downloads.sourceforge.net/project/bowtie-bio/bowtie/1.2.1.1/bowtie-1.2.1.1-src.zip
==> How many would you like to checksum? (default is 1, q to abort) 1
==> Downloading...
==> Fetching https://downloads.sourceforge.net/project/bowtie-bio/bowtie/1.2.1.1/bowtie-1.2.1.1-src.zip
######################################################################## 100.0%
==> Checksummed 1 version of bowtie
==> This package looks like it uses the makefile build system
==> Created template for bowtie package
==> Created package file: /Users/mamelara/spack/var/spack/repos/builtin/packages/bowtie/package.py
Once the fetching is completed, Spack will open up your text editor in the usual fashion and create a template of a MakefilePackage package.py.
1# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
2# Spack Project Developers. See the top-level COPYRIGHT file for details.
3#
4# SPDX-License-Identifier: (Apache-2.0 OR MIT)
5
6from spack import *
7
8
9class Bowtie(MakefilePackage):
10 """FIXME: Put a proper description of your package here."""
11
12 # FIXME: Add a proper url for your package's homepage here.
13 homepage = "http://www.example.com"
14 url = "https://downloads.sourceforge.net/project/bowtie-bio/bowtie/1.2.1.1/bowtie-1.2.1.1-src.zip"
15
16 version('1.2.1.1', 'ec06265730c5f587cd58bcfef6697ddf')
17
18 # FIXME: Add dependencies if required.
19 # depends_on('foo')
20
21 def edit(self, spec, prefix):
22 # FIXME: Edit the Makefile if necessary
23 # FIXME: If not needed delete this function
24 # makefile = FileFilter('Makefile')
25 # makefile.filter('CC = .*', 'CC = cc')
26 return
Spack was successfully able to detect that Bowtie uses Makefiles.
Let’s add in the rest of our details for our package:
1# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
2# Spack Project Developers. See the top-level COPYRIGHT file for details.
3#
4# SPDX-License-Identifier: (Apache-2.0 OR MIT)
5
6from spack import *
7
8
9class Bowtie(MakefilePackage):
10 """Bowtie is an ultrafast, memory efficient short read aligner
11 for short DNA sequences (reads) from next-gen sequencers."""
12
13 homepage = "https://sourceforge.net/projects/bowtie-bio/"
14 url = "https://downloads.sourceforge.net/project/bowtie-bio/bowtie/1.2.1.1/bowtie-1.2.1.1-src.zip"
15
16 version('1.2.1.1', 'ec06265730c5f587cd58bcfef6697ddf')
17
18 variant("tbb", default=False, description="Use Intel thread building block")
19
20 depends_on("tbb", when="+tbb")
21
22 def edit(self, spec, prefix):
23 # FIXME: Edit the Makefile if necessary
24 # FIXME: If not needed delete this function
25 # makefile = FileFilter('Makefile')
26 # makefile.filter('CC = .*', 'CC = cc')
27 return
As we mentioned earlier, most packages using a Makefile have hardcoded variables that must be edited.
These variables are fine if you happen to not care about setup or types of compilers used, but Spack is designed to work with any compiler.
The MakefilePackage subclass makes it easy to edit these Makefiles by having an edit() method that can be overridden.
Let’s take a look at the default Makefile that Bowtie provides.
If we look inside, we see that CC and CXX point to our GNU compiler:
$ spack stage bowtie
Note
- As usual make sure you have shell support activated with Spack:
source /path/to/spack/share/spack/setup-env.sh
$ spack cd -s bowtie
$ cd spack-src
$ vim Makefile
CPP = g++ -w
CXX = $(CPP)
CC = gcc
LIBS = $(LDFLAGS) -lz
HEADERS = $(wildcard *.h)
To fix this, we need to use the edit() method to modify the Makefile.
1# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
2# Spack Project Developers. See the top-level COPYRIGHT file for details.
3#
4# SPDX-License-Identifier: (Apache-2.0 OR MIT)
5
6from spack import *
7
8
9class Bowtie(MakefilePackage):
10 """Bowtie is an ultrafast, memory efficient short read aligner
11 for short DNA sequences (reads) from next-gen sequencers."""
12
13 homepage = "https://sourceforge.net/projects/bowtie-bio/"
14 url = "https://downloads.sourceforge.net/project/bowtie-bio/bowtie/1.2.1.1/bowtie-1.2.1.1-src.zip"
15
16 version('1.2.1.1', 'ec06265730c5f587cd58bcfef6697ddf')
17
18 variant("tbb", default=False, description="Use Intel thread building block")
19
20 depends_on("tbb", when="+tbb")
21
22 def edit(self, spec, prefix):
23 makefile = FileFilter("Makefile")
24 makefile.filter('CC= .*', 'CC = ' + env['CC'])
25 makefile.filter('CXX = .*', 'CXX = ' + env['CXX'])
Here we use a FileFilter object (a Spack utility) to edit our Makefile.
It takes a regular expression to find lines (e.g., assignments to CC and CXX) and then replaces them with values derived from Spack’s build environment (e.g., self.compiler.cc and self.compiler.cxx).
This allows us to build Bowtie with whatever compiler we specify through Spack’s spec syntax.
Let’s change the build and install phases of our package:
1# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
2# Spack Project Developers. See the top-level COPYRIGHT file for details.
3#
4# SPDX-License-Identifier: (Apache-2.0 OR MIT)
5
6from spack import *
7
8
9class Bowtie(MakefilePackage):
10 """Bowtie is an ultrafast, memory efficient short read aligner
11 for short DNA sequences (reads) from next-gen sequencers."""
12
13 homepage = "https://sourceforge.net/projects/bowtie-bio/"
14 url = "https://downloads.sourceforge.net/project/bowtie-bio/bowtie/1.2.1.1/bowtie-1.2.1.1-src.zip"
15
16 version('1.2.1.1', 'ec06265730c5f587cd58bcfef6697ddf')
17
18 variant("tbb", default=False, description="Use Intel thread building block")
19
20 depends_on("tbb", when="+tbb")
21
22 def edit(self, spec, prefix):
23 makefile = FileFilter("Makefile")
24 makefile.filter('CC= .*', 'CC = ' + env['CC'])
25 makefile.filter('CXX = .*', 'CXX = ' + env['CXX'])
26
27 @property
28 def build_targets(self):
29 if "+tbb" in spec:
30 return []
31 else:
32 return ["NO_TBB=1"]
33
34 @property
35 def install_targets(self):
36 return [f'prefix={self.prefix}', 'install']
Here we demonstrate another strategy that we can use to manipulate our package’s build.
We can provide command line arguments to make().
Since Bowtie can use tbb we can either add NO_TBB=1 as a argument to prevent tbb support, or we can invoke make with no arguments if TBB is desired and found by its build system.
Bowtie requires our install_target to provide a path to the install directory.
We can do this by providing prefix= as a command line argument to make().
Let’s look at a couple of other examples and go through them:
$ spack edit esmf
Some packages allow environment variables to be set and will honor them.
Packages that use ?= for assignment in their Makefile can be set using environment variables.
In our esmf example we set two environment variables in our edit() method:
def edit(self, spec, prefix):
for var in os.environ:
if var.startswith("ESMF_"):
os.environ.pop(var)
# More code ...
if self.compiler.name == "gcc":
os.environ["ESMF_COMPILER"] = "gfortran"
elif self.compiler.name == "intel":
os.environ["ESMF_COMPILER"] = "intel"
elif self.compiler.name == "clang":
os.environ["ESMF_COMPILER"] = "gfortranclang"
elif self.compiler.name == "nag":
os.environ["ESMF_COMPILER"] = "nag"
elif self.compiler.name == "pgi":
os.environ["ESMF_COMPILER"] = "pgi"
else:
msg = "The compiler you are building with, "
msg += "'{0}', is not supported by ESMF."
raise InstallError(msg.format(self.compiler.name))
As you may have noticed, we didn’t really write anything to the Makefile but rather we set environment variables that will override variables set in the Makefile.
Some packages include a configuration file that sets certain compiler variables, platform specific variables, and the location of dependencies or libraries.
If the file is simple and only requires a couple of changes, we can replace those entries with our FileFilter object.
If the configuration involves complex changes, we can write a new configuration file from scratch within the edit() method.
Let’s look at an example of this in the elk package:
$ spack edit elk
def edit(self, spec, prefix):
# Dictionary of configuration options
config = {"MAKE": "make", "AR": "ar"}
# Compiler-specific flags
flags = ""
if self.compiler.name == "intel":
flags = "-O3 -ip -unroll -no-prec-div"
elif self.compiler.name == "gcc":
flags = "-O3 -ffast-math -funroll-loops"
elif self.compiler.name == "pgi":
flags = "-O3 -lpthread"
elif self.compiler.name == "g95":
flags = "-O3 -fno-second-underscore"
elif self.compiler.name == "nag":
flags = "-O4 -kind=byte -dusty -dcfuns"
elif self.compiler.name == "xl":
flags = "-O3"
config["F90_OPTS"] = flags
config["F77_OPTS"] = flags
# BLAS/LAPACK support
# Note: BLAS/LAPACK must be compiled with OpenMP support
# if the +openmp variant is chosen
blas = "blas.a"
lapack = "lapack.a"
if "+blas" in spec:
blas = spec["blas"].libs.joined()
if "+lapack" in spec:
lapack = spec["lapack"].libs.joined()
# lapack must come before blas
config["LIB_LPK"] = " ".join([lapack, blas])
# FFT support
if "+fft" in spec:
config["LIB_FFT"] = join_path(spec["fftw"].prefix.lib, "libfftw3.so")
config["SRC_FFT"] = "zfftifc_fftw.f90"
else:
config["LIB_FFT"] = "fftlib.a"
config["SRC_FFT"] = "zfftifc.f90"
# MPI support
if "+mpi" in spec:
config["F90"] = spec["mpi"].mpifc
config["F77"] = spec["mpi"].mpif77
else:
config["F90"] = spack_fc
config["F77"] = spack_f77
config["SRC_MPI"] = "mpi_stub.f90"
# OpenMP support
if "+openmp" in spec:
config["F90_OPTS"] += " " + self.compiler.openmp_flag
config["F77_OPTS"] += " " + self.compiler.openmp_flag
else:
config["SRC_OMP"] = "omp_stub.f90"
# Libxc support
if "+libxc" in spec:
config["LIB_libxc"] = " ".join(
[
join_path(spec["libxc"].prefix.lib, "libxcf90.so"),
join_path(spec["libxc"].prefix.lib, "libxc.so"),
]
)
config["SRC_libxc"] = " ".join(["libxc_funcs.f90", "libxc.f90", "libxcifc.f90"])
else:
config["SRC_libxc"] = "libxcifc_stub.f90"
# Write configuration options to include file
with open("make.inc", "w") as inc:
for key in config:
inc.write("{0} = {1}\n".format(key, config[key]))
config is just a Python dictionary that we populate with key-value pairs.
By the end of the edit() method, we write the contents of our dictionary to the make.inc file, which the package’s Makefile then includes.
CMake¶
CMake is another common build system that has been gaining popularity.
It works in a similar manner to Autotools but with differences in variable names, the number of configuration options available, and the handling of shared libraries.
Typical build incantations look like this:
def install(self, spec, prefix):
cmake("-DCMAKE_INSTALL_PREFIX:PATH=/path/to/install_dir ..")
make()
make("install")
As you can see from the example above, it’s very similar to invoking configure and make in an Autotools build system.
However, the variable names and options differ.
Most options in CMake are prefixed with a '-D' flag to indicate a configuration setting.
In the CMakePackage class, we can override the following build phases:
cmake()build()install()
The CMakePackage class also provides sensible defaults, so often we only need to override cmake_args() to pass package-specific options.
Let’s look at these defaults in the CMakePackage class in the _std_args() method:
$ spack edit --build-system cmake
1
2class CMakePackage(PackageBase):
3 """Specialized class for packages built using CMake
4
5 For more information on the CMake build system, see:
6 https://cmake.org/cmake/help/latest/
7 """
8
9 #: This attribute is used in UI queries that need to know the build
10 #: system base class
11 build_system_class = "CMakePackage"
12
13 #: Legacy buildsystem attribute used to deserialize and install old specs
14 default_buildsystem = "cmake"
15
16 #: When this package depends on Python and ``find_python_hints`` is set to True, pass the
17 #: defines {Python3,Python,PYTHON}_EXECUTABLE explicitly, so that CMake locates the right
18 #: Python in its builtin FindPython3, FindPython, and FindPythonInterp modules. Spack does
19 #: CMake's job because CMake's modules by default only search for Python versions known at the
20 #: time of release.
21 find_python_hints = True
22
23 build_system("cmake")
24
25 with when("build_system=cmake"):
26 # https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html
27 # See https://github.com/spack/spack/pull/36679 and related issues for a
28 # discussion of the trade-offs between Release and RelWithDebInfo for default
29 # builds. Release is chosen to maximize performance and reduce disk-space burden,
30 # at the cost of more difficulty in debugging.
31 variant(
32 "build_type",
33 default="Release",
34 description="CMake build type",
35 values=("Debug", "Release", "RelWithDebInfo", "MinSizeRel"),
36 )
37 # CMAKE_INTERPROCEDURAL_OPTIMIZATION only exists for CMake >= 3.9
38 # https://cmake.org/cmake/help/latest/variable/CMAKE_INTERPROCEDURAL_OPTIMIZATION.html
39 variant(
40 "ipo",
41 default=False,
42 when="^cmake@3.9:",
43 description="CMake interprocedural optimization",
44 )
45
46 if sys.platform == "win32":
47 generator("ninja")
48 else:
49 generator("ninja", "make", default="make")
50
51 depends_on("cmake", type="build")
52 depends_on("gmake", type="build", when="generator=make")
53 depends_on("ninja", type="build", when="generator=ninja")
54
55 def flags_to_build_system_args(self, flags):
56 """Return a list of all command line arguments to pass the specified
57 compiler flags to cmake. Note CMAKE does not have a cppflags option,
58 so cppflags will be added to cflags, cxxflags, and fflags to mimic the
59 behavior in other tools.
60 """
61 # Has to be dynamic attribute due to caching
62 setattr(self, "cmake_flag_args", [])
63
64 flag_string = "-DCMAKE_{0}_FLAGS={1}"
65 langs = {"C": "c", "CXX": "cxx", "Fortran": "f"}
66
67 # Handle language compiler flags
68 for lang, pre in langs.items():
69 flag = pre + "flags"
70 # cmake has no explicit cppflags support -> add it to all langs
71 lang_flags = " ".join(flags.get(flag, []) + flags.get("cppflags", []))
72 if lang_flags:
73 self.cmake_flag_args.append(flag_string.format(lang, lang_flags))
74
75 # Cmake has different linker arguments for different build types.
76 # We specify for each of them.
77 if flags["ldflags"]:
78 ldflags = " ".join(flags["ldflags"])
79 # cmake has separate linker arguments for types of builds.
80 self.cmake_flag_args.append(f"-DCMAKE_EXE_LINKER_FLAGS={ldflags}")
81 self.cmake_flag_args.append(f"-DCMAKE_MODULE_LINKER_FLAGS={ldflags}")
82 self.cmake_flag_args.append(f"-DCMAKE_SHARED_LINKER_FLAGS={ldflags}")
83
84 # CMake has libs options separated by language. Apply ours to each.
85 if flags["ldlibs"]:
86 libs_flags = " ".join(flags["ldlibs"])
87 libs_string = "-DCMAKE_{0}_STANDARD_LIBRARIES={1}"
88 for lang in langs:
89 self.cmake_flag_args.append(libs_string.format(lang, libs_flags))
90
91 # Legacy methods (used by too many packages to change them,
92 # need to forward to the builder)
93 def define(self, cmake_var: str, value: Any) -> str:
94 return define(cmake_var, value)
95
96 def define_from_variant(self, cmake_var: str, variant: Optional[str] = None) -> str:
97 return define_from_variant(self, cmake_var, variant)
98
99
100@register_builder("cmake")
101class CMakeBuilder(BuilderWithDefaults):
102 """The cmake builder encodes the default way of building software with CMake. IT
103 has three phases that can be overridden:
104
105 1. :py:meth:`~.CMakeBuilder.cmake`
106 2. :py:meth:`~.CMakeBuilder.build`
107 3. :py:meth:`~.CMakeBuilder.install`
108
109 They all have sensible defaults and for many packages the only thing
110 necessary will be to override :py:meth:`~.CMakeBuilder.cmake_args`.
111
112 For a finer tuning you may also override:
113
114 +-----------------------------------------------+--------------------+
115 | **Method** | **Purpose** |
116 +===============================================+====================+
117 | :py:meth:`~.CMakeBuilder.root_cmakelists_dir` | Location of the |
118 | | root CMakeLists.txt|
119 +-----------------------------------------------+--------------------+
120 | :py:meth:`~.CMakeBuilder.build_directory` | Directory where to |
121 | | build the package |
122 +-----------------------------------------------+--------------------+
123 """
124
125 #: Phases of a CMake package
126 phases: Tuple[str, ...] = ("cmake", "build", "install")
127
128 #: Names associated with package methods in the old build-system format
129 package_methods: Tuple[str, ...] = ("cmake_args", "check")
130
131 #: Names associated with package attributes in the old build-system format
132 package_attributes: Tuple[str, ...] = (
133 "build_targets",
134 "install_targets",
Some CMake packages use different generators.
Spack is able to support Unix-Makefile generators as well as Ninja generators.
If no generator is specified, Spack will default to Unix Makefiles.
Next we setup the build type.
In CMake you can specify the build type that you want.
Options include:
emptyDebugReleaseRelWithDebInfoMinSizeRel
With these options you can specify whether you want your executable to have the debug version only, release version or the release with debug information.
Release executables tend to be more optimized than Debug versions.
In Spack, we set the default as Release unless otherwise specified through a variant (e.g., build_type=Debug).
Spack then automatically sets up the -DCMAKE_INSTALL_PREFIX path, appends the build type (defaulting to RelWithDebInfo), and enables a verbose Makefile output by default.
Next we add the rpaths to -DCMAKE_INSTALL_RPATH:STRING.
Finally we add to -DCMAKE_PREFIX_PATH:STRING the locations of all our dependencies so that CMake can find them.
In the end our cmake line will look like this (example is xrootd):
$ cmake $HOME/spack/var/spack/stage/xrootd-4.6.0-4ydm74kbrp4xmcgda5upn33co5pwddyk/xrootd-4.6.0 -G Unix Makefiles -DCMAKE_INSTALL_PREFIX:PATH=$HOME/spack/opt/spack/darwin-sierra-x86_64/clang-9.0.0-apple/xrootd-4.6.0-4ydm74kbrp4xmcgda5upn33co5pwddyk -DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON -DCMAKE_FIND_FRAMEWORK:STRING=LAST -DCMAKE_INSTALL_RPATH_USE_LINK_PATH:BOOL=FALSE -DCMAKE_INSTALL_RPATH:STRING=$HOME/spack/opt/spack/darwin-sierra-x86_64/clang-9.0.0-apple/xrootd-4.6.0-4ydm74kbrp4xmcgda5upn33co5pwddyk/lib:$HOME/spack/opt/spack/darwin-sierra-x86_64/clang-9.0.0-apple/xrootd-4.6.0-4ydm74kbrp4xmcgda5upn33co5pwddyk/lib64 -DCMAKE_PREFIX_PATH:STRING=$HOME/spack/opt/spack/darwin-sierra-x86_64/clang-9.0.0-apple/cmake-3.9.4-hally3vnbzydiwl3skxcxcbzsscaasx5
We can see now how CMake takes care of a lot of the boilerplate code that would have to be otherwise typed in.
Let’s try to recreate callpath:
$ spack create -f https://github.com/llnl/callpath/archive/v1.0.3.tar.gz
==> This looks like a URL for callpath
==> Found 4 versions of callpath:
1.0.3 https://github.com/LLNL/callpath/archive/v1.0.3.tar.gz
1.0.2 https://github.com/LLNL/callpath/archive/v1.0.2.tar.gz
1.0.1 https://github.com/LLNL/callpath/archive/v1.0.1.tar.gz
1.0 https://github.com/LLNL/callpath/archive/v1.0.tar.gz
==> How many would you like to checksum? (default is 1, q to abort) 1
==> Downloading...
==> Fetching https://github.com/LLNL/callpath/archive/v1.0.3.tar.gz
######################################################################## 100.0%
==> Checksummed 1 version of callpath
==> This package looks like it uses the cmake build system
==> Created template for callpath package
==> Created package file: /Users/mamelara/spack/var/spack/repos/builtin/packages/callpath/package.py
which then produces the following template:
1# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
2# Spack Project Developers. See the top-level COPYRIGHT file for details.
3#
4# SPDX-License-Identifier: (Apache-2.0 OR MIT)
5
6#
7# This is a template package file for Spack. We've put "FIXME"
8# next to all the things you'll want to change. Once you've handled
9# them, you can save this file and test your package like this:
10#
11# spack install callpath
12#
13# You can edit this file again by typing:
14#
15# spack edit callpath
16#
17# See the Spack documentation for more information on packaging.
18# If you submit this package back to Spack as a pull request,
19# please first remove this boilerplate and all FIXME comments.
20#
21from spack import *
22
23
24class Callpath(CMakePackage):
25 """FIXME: Put a proper description of your package here."""
26
27 # FIXME: Add a proper url for your package's homepage here.
28 homepage = "http://www.example.com"
29 url = "https://github.com/llnl/callpath/archive/v1.0.1.tar.gz"
30
31 version('1.0.3', 'c89089b3f1c1ba47b09b8508a574294a')
32
33 # FIXME: Add dependencies if required.
34 # depends_on('foo')
35
36 def cmake_args(self):
37 # FIXME: Add arguments other than
38 # FIXME: CMAKE_INSTALL_PREFIX and CMAKE_BUILD_TYPE
39 # FIXME: If not needed delete this function
40 args = []
41 return args
Again we fill in the details:
1# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
2# Spack Project Developers. See the top-level COPYRIGHT file for details.
3#
4# SPDX-License-Identifier: (Apache-2.0 OR MIT)
5
6from spack import *
7
8
9class Callpath(CMakePackage):
10 """Library for representing callpaths consistently in
11 distributed-memory performance tools."""
12
13 homepage = "https://github.com/llnl/callpath"
14 url = "https://github.com/llnl/callpath/archive/v1.0.3.tar.gz"
15
16 version('1.0.3', 'c89089b3f1c1ba47b09b8508a574294a')
17
18 depends_on("elf", type="link")
19 depends_on("libdwarf")
20 depends_on("dyninst")
21 depends_on("adept-utils")
22 depends_on("mpi")
23 depends_on("cmake@2.8:", type="build")
As mentioned earlier, Spack’s CMakePackage uses sensible defaults to reduce boilerplate and simplify writing package files for CMake-based software.
In callpath, we want to control options like CALLPATH_WALKER or add specific compiler flags.
We can return these options from cmake_args() like so:
1# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
2# Spack Project Developers. See the top-level COPYRIGHT file for details.
3#
4# SPDX-License-Identifier: (Apache-2.0 OR MIT)
5
6from spack import *
7
8
9class Callpath(CMakePackage):
10 """Library for representing callpaths consistently in
11 distributed-memory performance tools."""
12
13 homepage = "https://github.com/llnl/callpath"
14 url = "https://github.com/llnl/callpath/archive/v1.0.3.tar.gz"
15
16 version('1.0.3', 'c89089b3f1c1ba47b09b8508a574294a')
17
18 depends_on("elf", type="link")
19 depends_on("libdwarf")
20 depends_on("dyninst")
21 depends_on("adept-utils")
22 depends_on("mpi")
23 depends_on("cmake@2.8:", type="build")
24
25 def cmake_args(self):
26 args = ["-DCALLPATH_WALKER=dyninst"]
27
28 if self.spec.satisfies("^dyninst@9.3.0:"):
29 std_flag = self.compiler.cxx_flag
30 args.append(f"-DCMAKE_CXX_FLAGS='{std_flag}' -fpermissive'")
31
32 return args
Now we can control our build options using cmake_args().
If defaults are sufficient enough for the package, we can leave this method out.
CMakePackage classes allow for control of other features in the build system.
For example, you can specify the path to the “out of source” build directory and also point to the root of the CMakeLists.txt file if it is placed in a non-standard location.
A good example of a package that has its CMakeLists.txt file located at a different location is found in spades.
$ spack edit spades
root_cmakelists_dir = "src"
Here root_cmakelists_dir will tell Spack where to find the location of CMakeLists.txt.
In this example, it is located a directory level below in the src directory.
Some CMake packages also require the install phase to be overridden.
For example, let’s take a look at sniffles.
$ spack edit sniffles
In the install() method, we have to manually install our targets so we override the install() method to do it for us:
# the build process doesn't actually install anything, do it by hand
def install(self, spec, prefix):
mkdir(prefix.bin)
src = "bin/sniffles-core-{0}".format(spec.version.dotted)
binaries = ["sniffles", "sniffles-debug"]
for b in binaries:
install(join_path(src, b), join_path(prefix.bin, b))
PythonPackage¶
Python extensions and modules are built differently from source than most applications. These modules are usually installed using the following line:
$ pip install .
We can write package files for Python packages using the Package class, but the class brings with it a lot of methods that are useless for Python packages.
Instead, Spack has a PythonPackage subclass that allows packagers of Python modules to be able to invoke pip.
We will write a package file for Pandas:
$ spack create -f https://pypi.io/packages/source/p/pandas/pandas-0.19.0.tar.gz
==> This looks like a URL for pandas
==> Warning: Spack was unable to fetch url list due to a certificate verification problem. You can try running spack -k, which will not check SSL certificates. Use this at your own risk.
==> Found 1 version of pandas:
0.19.0 https://pypi.io/packages/source/p/pandas/pandas-0.19.0.tar.gz
==> How many would you like to checksum? (default is 1, q to abort) 1
==> Downloading...
==> Fetching https://pypi.io/packages/source/p/pandas/pandas-0.19.0.tar.gz
######################################################################## 100.0%
==> Checksummed 1 version of pandas
==> This package looks like it uses the python build system
==> Changing package name from pandas to py-pandas
==> Created template for py-pandas package
==> Created package file: /Users/mamelara/spack/var/spack/repos/builtin/packages/py-pandas/package.py
And we are left with the following template:
1# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
2# Spack Project Developers. See the top-level COPYRIGHT file for details.
3#
4# SPDX-License-Identifier: (Apache-2.0 OR MIT)
5
6#
7# This is a template package file for Spack. We've put "FIXME"
8# next to all the things you'll want to change. Once you've handled
9# them, you can save this file and test your package like this:
10#
11# spack install py-pandas
12#
13# You can edit this file again by typing:
14#
15# spack edit py-pandas
16#
17# See the Spack documentation for more information on packaging.
18# If you submit this package back to Spack as a pull request,
19# please first remove this boilerplate and all FIXME comments.
20#
21from spack import *
22
23
24class PyPandas(PythonPackage):
25 """FIXME: Put a proper description of your package here."""
26
27 # FIXME: Add a proper url for your package's homepage here.
28 homepage = "http://www.example.com"
29 url = "https://pypi.io/packages/source/p/pandas/pandas-0.19.0.tar.gz"
30
31 version('0.19.0', 'bc9bb7188e510b5d44fbdd249698a2c3')
32
33 # FIXME: Add dependencies if required.
34 # depends_on('py-setuptools', type='build')
35 # depends_on('py-foo', type=('build', 'run'))
36
37 def build_args(self, spec, prefix):
38 # FIXME: Add arguments other than --prefix
39 # FIXME: If not needed delete this function
40 args = []
41 return args
As you can see this is not any different than any package template that we have written. We have the choice of providing build options or using the sensible defaults.
Luckily for us, there is no need to provide build args.
Next we need to find the dependencies of a package.
Dependencies are usually listed in setup.py.
You can find the dependencies by searching for install_requires keyword in that file.
Here it is for Pandas:
# ... code
if sys.version_info[0] >= 3:
setuptools_kwargs = {
'zip_safe': False,
'install_requires': ['python-dateutil >= 2',
'pytz >= 2011k',
'numpy >= %s' % min_numpy_ver],
'setup_requires': ['numpy >= %s' % min_numpy_ver],
}
if not _have_setuptools:
sys.exit("need setuptools/distribute for Py3k"
"\n$ pip install distribute")
# ... more code
You can find a more comprehensive list at the Pandas documentation.
By reading the documentation and setup.py we found that Pandas depends on python-dateutil, pytz, and numpy, numexpr, and finally bottleneck.
Here is the completed Pandas script:
1# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
2# Spack Project Developers. See the top-level COPYRIGHT file for details.
3#
4# SPDX-License-Identifier: (Apache-2.0 OR MIT)
5
6from spack import *
7
8
9class PyPandas(PythonPackage):
10 """pandas is a Python package providing fast, flexible, and expressive
11 data structures designed to make working with relational or
12 labeled data both easy and intuitive. It aims to be the
13 fundamental high-level building block for doing practical, real
14 world data analysis in Python. Additionally, it has the broader
15 goal of becoming the most powerful and flexible open source data
16 analysis / manipulation tool available in any language.
17 """
18 homepage = "http://pandas.pydata.org/"
19 url = "https://pypi.io/packages/source/p/pandas/pandas-0.19.0.tar.gz"
20
21 version('0.19.0', 'bc9bb7188e510b5d44fbdd249698a2c3')
22 version('0.18.0', 'f143762cd7a59815e348adf4308d2cf6')
23 version('0.16.1', 'fac4f25748f9610a3e00e765474bdea8')
24 version('0.16.0', 'bfe311f05dc0c351f8955fbd1e296e73')
25
26 depends_on('py-dateutil', type=('build', 'run'))
27 depends_on('py-numpy', type=('build', 'run'))
28 depends_on('py-setuptools', type='build')
29 depends_on('py-cython', type='build')
30 depends_on('py-pytz', type=('build', 'run'))
31 depends_on('py-numexpr', type=('build', 'run'))
32 depends_on('py-bottleneck', type=('build', 'run'))
It is quite important to declare all the dependencies of a Python package. Spack can “activate” Python packages to prevent the user from having to load each dependency module explicitly. If a dependency is missed, Spack will be unable to properly activate the package and it will cause an issue. To learn more about extensions go to spack extensions.
From this example, you can see that building Python modules is made easy through the PythonPackage class.
Other Build Systems¶
Although we won’t get in depth with any of the other build systems that Spack supports, it is worth mentioning that Spack does provide subclasses for the following build systems:
IntelPackageSconsPackageWafPackageRPackagePerlPackageQMakePackage
Each of these classes have their own abstractions to help assist in writing package files.
For whatever doesn’t fit nicely into the other build systems, you can use the Package class.
Hopefully by now you can see how we aim to make packaging simple and robust through these classes. If you want to learn more about these build systems, check out Overview of the installation procedure in the Packaging Guide.