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:hpcic24
and then set Spack up like this:
git clone --depth=100 --branch=releases/v0.22 https://github.com/spack/spack
. spack/share/spack/setup-env.sh
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 a
pattern emerge 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 manipulated to install a package.
Package Class Hierarchy
The above diagram gives a high level view of the class hierarchy and how each
package relates. Each subclass inherits from the PackageBaseClass
super class. The bulk of the work is done in this super class which includes
fetching, extracting to a staging directory and installing. 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 to see
how powerful these abstractions are when packaging.
Package
We’ve already seen examples of a Package
class in our walkthrough for writing
package files, so we won’t be spending much time with them 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. Package
classes are particularly useful
for packages that have a non-conventional way of being built since the packager
can utilize some of Spack’s helper functions to customize the building and
installing of a package.
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 Autotools
subclass aims to simplify writing package files and provides
convenience methods to manipulate each of the different phases for a Autotools
build system.
Autotools
packages consist of four phases:
autoreconf()
configure()
build()
install()
Each of these phases have sensible defaults. Let’s take a quick look at some
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 is abridged to avoid having long examples. We only show what is relevant to the packager.
1 patch_libtool = True
2
3 #: Targets for ``make`` during the :py:meth:`~.AutotoolsBuilder.build` phase
4 build_targets = [] # type: List[str]
5 #: Targets for ``make`` during the :py:meth:`~.AutotoolsBuilder.install` phase
6 install_targets = ["install"]
7
8 #: Callback names for build-time test
9 build_time_test_callbacks = ["check"]
10
11 #: Callback names for install-time test
12 install_time_test_callbacks = ["installcheck"]
13
14 #: Set to true to force the autoreconf step even if configure is present
15 force_autoreconf = False
16
17 #: Options to be passed to autoreconf when using the default implementation
18 autoreconf_extra_args = [] # type: List[str]
19
20 #: If False deletes all the .la files in the prefix folder after the installation.
21 #: If True instead it installs them.
22 inspect.getmodule(self.pkg).configure = Executable(self.configure_abs_path)
23
24 def configure_args(self):
25 """Return the list of all the arguments that must be passed to configure,
26 except ``--prefix`` which will be pre-pended to the list.
27 """
28 return []
29
30 def configure(self, pkg, spec, prefix):
31 """Run "configure", with the arguments specified by the builder and an
32 appropriately set prefix.
33 """
34 options = getattr(self.pkg, "configure_flag_args", [])
35 options += ["--prefix={0}".format(prefix)]
36 options += self.configure_args()
37
38 with fs.working_dir(self.build_directory, create=True):
39 inspect.getmodule(self.pkg).configure(*options)
40
41 def build(self, pkg, spec, prefix):
42 """Run "make" on the build targets specified by the builder."""
43 # See https://autotools.io/automake/silent.html
44 params = ["V=1"]
45 params += self.build_targets
46 with fs.working_dir(self.build_directory):
47 inspect.getmodule(self.pkg).make(*params)
48
49 def install(self, pkg, spec, prefix):
50 """Run "make" on the install targets specified by the builder."""
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
property:
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.
Here we see that the prefix
argument is already included since it is a
common pattern amongst packages using Autotools
. We then only have to
override configure_args()
, which will then return it’s output to
to configure()
. Then, configure()
will append the common
arguments
Packagers also have the option to run autoreconf
in case a package
needs to update the build system and generate a new configure
. Though,
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 is a Package
class but uses the Autotools
build system. Although this package is acceptable let’s make this into an
AutotoolsPackage
class and 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 takes care of setting the prefix for us we can exclude that as
an argument to configure
. Our packages look simpler, and the packager
does not need to worry about whether they 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 Makefile
subclass which provides
convenience methods to help write these types of packages.
A MakefilePackage
class has three phases that can be overridden. These include:
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 1. :py:meth:`~.MakefileBuilder.edit`
2 2. :py:meth:`~.MakefileBuilder.build`
3 3. :py:meth:`~.MakefileBuilder.install`
4
5 It is usually necessary to override the :py:meth:`~.MakefileBuilder.edit`
6 phase (which is by default a no-op), while the other two have sensible defaults.
7
8 For a finer tuning you may override:
9
10 +-----------------------------------------------+--------------------+
11 | **Method** | **Purpose** |
12 +===============================================+====================+
13 | :py:attr:`~.MakefileBuilder.build_targets` | Specify ``make`` |
14 | | targets for the |
15 | | build phase |
16 +-----------------------------------------------+--------------------+
17 | :py:attr:`~.MakefileBuilder.install_targets` | Specify ``make`` |
18 | | targets for the |
19 | | install phase |
20 +-----------------------------------------------+--------------------+
21 | :py:meth:`~.MakefileBuilder.build_directory` | Directory where the|
22 | | Makefile is located|
23 +-----------------------------------------------+--------------------+
24 """
25
26 phases = ("edit", "build", "install")
27
28 #: Names associated with package methods in the old build-system format
29 legacy_methods = ("check", "installcheck")
30
31 #: Names associated with package attributes in the old build-system format
32 legacy_attributes = (
33 "build_targets",
34 "install_targets",
35 "build_time_test_callbacks",
36 "install_time_test_callbacks",
37 "build_directory",
38 )
39
40 #: Targets for ``make`` during the :py:meth:`~.MakefileBuilder.build` phase
41 build_targets = [] # type: List[str]
42 #: Targets for ``make`` during the :py:meth:`~.MakefileBuilder.install` phase
43 install_targets = ["install"]
44
45 #: Callback names for build-time test
46 build_time_test_callbacks = ["check"]
47
48 #: Callback names for install-time test
49 install_time_test_callbacks = ["installcheck"]
50
51 @property
52 def build_directory(self):
53 """Return the directory containing the main Makefile."""
54 return self.pkg.stage.source_path
55
56 def edit(self, pkg, spec, prefix):
57 """Edit the Makefile before calling make. The default is a no-op."""
58 pass
59
60 def build(self, pkg, spec, prefix):
61 """Run "make" on the build targets specified by the builder."""
62 with fs.working_dir(self.build_directory):
63 inspect.getmodule(self.pkg).make(*self.build_targets)
64
65 def install(self, pkg, spec, prefix):
66 """Run "make" on the install targets specified by the builder."""
67 with fs.working_dir(self.build_directory):
68 inspect.getmodule(self.pkg).make(*self.install_targets)
69
70 spack.builder.run_after("build")(execute_build_time_tests)
71
72 def check(self):
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 Make
.
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 hard-coded
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_root/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 write our custom
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 to edit our Makefile
. It takes
in a regular expression and then replaces CC
and CXX
to whatever
Spack sets CC
and CXX
environment variables to. 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 demonstrate another strategy that we can use to manipulate our package
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 just invoke make
with no arguments.
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 overwrite
those entries with our FileFilter
object. If the configuration involves
complex changes, we can write a new configuration file from scratch.
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 dictionary that we can add key-value pairs to. By the
end of the edit()
method we write the contents of our dictionary to
make.inc
.
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 phases:
cmake()
build()
install()
The CMakePackage
class also provides sensible defaults so we only need to
override cmake_args()
.
Let’s look at these defaults in the CMakePackage
class in the _std_args()
method:
$ spack edit --build-system cmake
1 "install_targets",
2 "build_time_test_callbacks",
3 "archive_files",
4 "root_cmakelists_dir",
5 "std_cmake_args",
6 "build_dirname",
7 "build_directory",
8 ) # type: Tuple[str, ...]
9
10 #: The build system generator to use.
11 #:
12 #: See ``cmake --help`` for a list of valid generators.
13 #: Currently, "Unix Makefiles" and "Ninja" are the only generators
14 #: that Spack supports. Defaults to "Unix Makefiles".
15 #:
16 #: See https://cmake.org/cmake/help/latest/manual/cmake-generators.7.html
17 #: for more information.
18 generator = "Ninja" if sys.platform == "win32" else "Unix Makefiles"
19
20 #: Targets to be used during the build phase
21 build_targets = [] # type: List[str]
22 #: Targets to be used during the install phase
23 install_targets = ["install"]
24 #: Callback names for build-time test
25 build_time_test_callbacks = ["check"]
26
27 @property
28 def archive_files(self):
29 """Files to archive for packages based on CMake"""
30 return [os.path.join(self.build_directory, "CMakeCache.txt")]
31
32 @property
33 def root_cmakelists_dir(self):
34 """The relative path to the directory containing CMakeLists.txt
35
36 This path is relative to the root of the extracted tarball,
37 not to the ``build_directory``. Defaults to the current directory.
38 """
39 return self.pkg.stage.source_path
40
41 @property
42 def std_cmake_args(self):
43 """Standard cmake arguments provided as a property for
44 convenience of package writers
45 """
46 # standard CMake arguments
47 std_cmake_args = CMakeBuilder.std_args(self.pkg, generator=self.generator)
48 std_cmake_args += getattr(self.pkg, "cmake_flag_args", [])
49 return std_cmake_args
50
51 @staticmethod
52 def std_args(pkg, generator=None):
53 """Computes the standard cmake arguments for a generic package"""
54 generator = generator or "Unix Makefiles"
55 valid_primary_generators = ["Unix Makefiles", "Ninja"]
56 primary_generator = _extract_primary_generator(generator)
57 if primary_generator not in valid_primary_generators:
58 msg = "Invalid CMake generator: '{0}'\n".format(generator)
59 msg += "CMakePackage currently supports the following "
60 msg += "primary generators: '{0}'".format("', '".join(valid_primary_generators))
61 raise spack.package_base.InstallError(msg)
62
63 try:
64 build_type = pkg.spec.variants["build_type"].value
65 except KeyError:
66 build_type = "RelWithDebInfo"
67
68 try:
69 ipo = pkg.spec.variants["ipo"].value
70 except KeyError:
71 ipo = False
72
73 define = CMakeBuilder.define
74 args = [
75 "-G",
76 generator,
77 define("CMAKE_INSTALL_PREFIX", pkg.prefix),
78 define("CMAKE_BUILD_TYPE", build_type),
79 define("BUILD_TESTING", pkg.run_tests),
80 ]
81
82 # CMAKE_INTERPROCEDURAL_OPTIMIZATION only exists for CMake >= 3.9
83 if pkg.spec.satisfies("^cmake@3.9:"):
84 args.append(define("CMAKE_INTERPROCEDURAL_OPTIMIZATION", ipo))
85
86 if primary_generator == "Unix Makefiles":
87 args.append(define("CMAKE_VERBOSE_MAKEFILE", True))
88
89 if platform.mac_ver()[0]:
90 args.extend(
91 [
92 define("CMAKE_FIND_FRAMEWORK", "LAST"),
93 define("CMAKE_FIND_APPBUNDLE", "LAST"),
94 ]
95 )
96
97 # Set up CMake rpath
98 args.extend(
99 [
100 define("CMAKE_INSTALL_RPATH_USE_LINK_PATH", True),
101 define("CMAKE_INSTALL_RPATH", spack.build_environment.get_rpaths(pkg)),
102 define("CMAKE_PREFIX_PATH", spack.build_environment.get_cmake_prefix_path(pkg)),
103 ]
104 )
105 return args
106
107 @staticmethod
108 def define(cmake_var, value):
109 """Return a CMake command line argument that defines a variable.
110
111 The resulting argument will convert boolean values to OFF/ON
112 and lists/tuples to CMake semicolon-separated string lists. All other
113 values will be interpreted as strings.
114
115 Examples:
116
117 .. code-block:: python
118
119 [define('BUILD_SHARED_LIBS', True),
120 define('CMAKE_CXX_STANDARD', 14),
121 define('swr', ['avx', 'avx2'])]
122
123 will generate the following configuration options:
124
125 .. code-block:: console
126
127 ["-DBUILD_SHARED_LIBS:BOOL=ON",
128 "-DCMAKE_CXX_STANDARD:STRING=14",
129 "-DSWR:STRING=avx;avx2]
130
131 """
132 # Create a list of pairs. Each pair includes a configuration
133 # option and whether or not that option is activated
134 if isinstance(value, bool):
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:
empty
Debug
Release
RelWithDebInfo
MinSizeRel
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. In Spack, we set the default as Release unless otherwise specified through a variant.
Spack then automatically sets up the -DCMAKE_INSTALL_PREFIX
path,
appends the build type (RelWithDebInfo
default), and then specifies a verbose
Makefile
.
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 will use sensible defaults to prevent repeated code
and to make writing CMake
package files simpler.
In callpath, we want to add options to CALLPATH_WALKER
as well as add
compiler flags. We add the following options 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:
IntelPackage
SconsPackage
WafPackage
RPackage
PerlPackage
QMakePackage
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 Implementing the installation procedure in the Packaging Guide.