Allow for packages with multiple build-systems (#30738)

This commit extends the DSL that can be used in packages
to allow declaring that a package uses different build-systems
under different conditions.

It requires each spec to have a `build_system` single valued
variant. The variant can be used in many context to query, manipulate
or select the build system associated with a concrete spec.

The knowledge to build a package has been moved out of the
PackageBase hierarchy, into a new Builder hierarchy. Customization
of the default behavior for a given builder can be obtained by
coding a new derived builder in package.py.

The "run_after" and "run_before" decorators are now applied to
methods on the builder. They can also incorporate a "when="
argument to specify that a method is run only when certain
conditions apply.

For packages that do not define their own builder, forwarding logic
is added between the builder and package (methods not found in one
will be retrieved from the other); this PR is expected to be fully
backwards compatible with unmodified packages that use a single
build system.
This commit is contained in:
Massimiliano Culpo
2022-10-26 20:17:32 +02:00
committed by GitHub
parent 83ee500108
commit 30c9ff50dd
155 changed files with 7460 additions and 3568 deletions

View File

@@ -65,7 +65,6 @@ on these ideas for each distinct build system that Spack supports:
build_systems/custompackage build_systems/custompackage
build_systems/inteloneapipackage build_systems/inteloneapipackage
build_systems/intelpackage build_systems/intelpackage
build_systems/multiplepackage
build_systems/rocmpackage build_systems/rocmpackage
build_systems/sourceforgepackage build_systems/sourceforgepackage

View File

@@ -5,9 +5,9 @@
.. _autotoolspackage: .. _autotoolspackage:
---------------- ---------
AutotoolsPackage Autotools
---------------- ---------
Autotools is a GNU build system that provides a build-script generator. Autotools is a GNU build system that provides a build-script generator.
By running the platform-independent ``./configure`` script that comes By running the platform-independent ``./configure`` script that comes
@@ -17,7 +17,7 @@ with the package, you can generate a platform-dependent Makefile.
Phases Phases
^^^^^^ ^^^^^^
The ``AutotoolsPackage`` base class comes with the following phases: The ``AutotoolsBuilder`` and ``AutotoolsPackage`` base classes come with the following phases:
#. ``autoreconf`` - generate the configure script #. ``autoreconf`` - generate the configure script
#. ``configure`` - generate the Makefiles #. ``configure`` - generate the Makefiles

View File

@@ -5,9 +5,9 @@
.. _bundlepackage: .. _bundlepackage:
------------- ------
BundlePackage Bundle
------------- ------
``BundlePackage`` represents a set of packages that are expected to work well ``BundlePackage`` represents a set of packages that are expected to work well
together, such as a collection of commonly used software libraries. The together, such as a collection of commonly used software libraries. The

View File

@@ -5,9 +5,9 @@
.. _cmakepackage: .. _cmakepackage:
------------ -----
CMakePackage CMake
------------ -----
Like Autotools, CMake is a widely-used build-script generator. Designed Like Autotools, CMake is a widely-used build-script generator. Designed
by Kitware, CMake is the most popular build system for new C, C++, and by Kitware, CMake is the most popular build system for new C, C++, and
@@ -21,7 +21,7 @@ whereas Autotools is Unix-only.
Phases Phases
^^^^^^ ^^^^^^
The ``CMakePackage`` base class comes with the following phases: The ``CMakeBuilder`` and ``CMakePackage`` base classes come with the following phases:
#. ``cmake`` - generate the Makefile #. ``cmake`` - generate the Makefile
#. ``build`` - build the package #. ``build`` - build the package
@@ -130,8 +130,8 @@ Adding flags to cmake
To add additional flags to the ``cmake`` call, simply override the To add additional flags to the ``cmake`` call, simply override the
``cmake_args`` function. The following example defines values for the flags ``cmake_args`` function. The following example defines values for the flags
``WHATEVER``, ``ENABLE_BROKEN_FEATURE``, ``DETECT_HDF5``, and ``THREADS`` with ``WHATEVER``, ``ENABLE_BROKEN_FEATURE``, ``DETECT_HDF5``, and ``THREADS`` with
and without the :meth:`~spack.build_systems.cmake.CMakePackage.define` and and without the :meth:`~spack.build_systems.cmake.CMakeBuilder.define` and
:meth:`~spack.build_systems.cmake.CMakePackage.define_from_variant` helper functions: :meth:`~spack.build_systems.cmake.CMakeBuilder.define_from_variant` helper functions:
.. code-block:: python .. code-block:: python

View File

@@ -5,11 +5,11 @@
.. _luapackage: .. _luapackage:
------------ ---
LuaPackage Lua
------------ ---
LuaPackage is a helper for the common case of Lua packages that provide The ``Lua`` build-system is a helper for the common case of Lua packages that provide
a rockspec file. This is not meant to take a rock archive, but to build a rockspec file. This is not meant to take a rock archive, but to build
a source archive or repository that provides a rockspec, which should cover a source archive or repository that provides a rockspec, which should cover
most lua packages. In the case a Lua package builds by Make rather than most lua packages. In the case a Lua package builds by Make rather than
@@ -19,7 +19,7 @@ luarocks, prefer MakefilePackage.
Phases Phases
^^^^^^ ^^^^^^
The ``LuaPackage`` base class comes with the following phases: The ``LuaBuilder`` and `LuaPackage`` base classes come with the following phases:
#. ``unpack`` - if using a rock, unpacks the rock and moves into the source directory #. ``unpack`` - if using a rock, unpacks the rock and moves into the source directory
#. ``preprocess`` - adjust sources or rockspec to fix build #. ``preprocess`` - adjust sources or rockspec to fix build

View File

@@ -5,9 +5,9 @@
.. _makefilepackage: .. _makefilepackage:
--------------- --------
MakefilePackage Makefile
--------------- --------
The most primitive build system a package can use is a plain Makefile. The most primitive build system a package can use is a plain Makefile.
Makefiles are simple to write for small projects, but they usually Makefiles are simple to write for small projects, but they usually
@@ -18,7 +18,7 @@ variables.
Phases Phases
^^^^^^ ^^^^^^
The ``MakefilePackage`` base class comes with 3 phases: The ``MakefileBuilder`` and ``MakefilePackage`` base classes come with 3 phases:
#. ``edit`` - edit the Makefile #. ``edit`` - edit the Makefile
#. ``build`` - build the project #. ``build`` - build the project

View File

@@ -5,9 +5,9 @@
.. _mavenpackage: .. _mavenpackage:
------------ -----
MavenPackage Maven
------------ -----
Apache Maven is a general-purpose build system that does not rely Apache Maven is a general-purpose build system that does not rely
on Makefiles to build software. It is designed for building and on Makefiles to build software. It is designed for building and
@@ -17,7 +17,7 @@ managing and Java-based project.
Phases Phases
^^^^^^ ^^^^^^
The ``MavenPackage`` base class comes with the following phases: The ``MavenBuilder`` and ``MavenPackage`` base classes come with the following phases:
#. ``build`` - compile code and package into a JAR file #. ``build`` - compile code and package into a JAR file
#. ``install`` - copy to installation prefix #. ``install`` - copy to installation prefix

View File

@@ -5,9 +5,9 @@
.. _mesonpackage: .. _mesonpackage:
------------ -----
MesonPackage Meson
------------ -----
Much like Autotools and CMake, Meson is a build system. But it is Much like Autotools and CMake, Meson is a build system. But it is
meant to be both fast and as user friendly as possible. GNOME's goal meant to be both fast and as user friendly as possible. GNOME's goal
@@ -17,7 +17,7 @@ is to port modules to use the Meson build system.
Phases Phases
^^^^^^ ^^^^^^
The ``MesonPackage`` base class comes with the following phases: The ``MesonBuilder`` and ``MesonPackage`` base classes come with the following phases:
#. ``meson`` - generate ninja files #. ``meson`` - generate ninja files
#. ``build`` - build the project #. ``build`` - build the project

View File

@@ -1,350 +0,0 @@
.. Copyright 2013-2022 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)
.. _multiplepackage:
----------------------
Multiple Build Systems
----------------------
Quite frequently, a package will change build systems from one version to the
next. For example, a small project that once used a single Makefile to build
may now require Autotools to handle the increased number of files that need to
be compiled. Or, a package that once used Autotools may switch to CMake for
Windows support. In this case, it becomes a bit more challenging to write a
single build recipe for this package in Spack.
There are several ways that this can be handled in Spack:
#. Subclass the new build system, and override phases as needed (preferred)
#. Subclass ``Package`` and implement ``install`` as needed
#. Create separate ``*-cmake``, ``*-autotools``, etc. packages for each build system
#. Rename the old package to ``*-legacy`` and create a new package
#. Move the old package to a ``legacy`` repository and create a new package
#. Drop older versions that only support the older build system
Of these options, 1 is preferred, and will be demonstrated in this
documentation. Options 3-5 have issues with concretization, so shouldn't be
used. Options 4-5 also don't support more than two build systems. Option 6 only
works if the old versions are no longer needed. Option 1 is preferred over 2
because it makes it easier to drop the old build system entirely.
The exact syntax of the package depends on which build systems you need to
support. Below are a couple of common examples.
^^^^^^^^^^^^^^^^^^^^^
Makefile -> Autotools
^^^^^^^^^^^^^^^^^^^^^
Let's say we have the following package:
.. code-block:: python
class Foo(MakefilePackage):
version("1.2.0", sha256="...")
def edit(self, spec, prefix):
filter_file("CC=", "CC=" + spack_cc, "Makefile")
def install(self, spec, prefix):
install_tree(".", prefix)
The package subclasses from :ref:`makefilepackage`, which has three phases:
#. ``edit`` (does nothing by default)
#. ``build`` (runs ``make`` by default)
#. ``install`` (runs ``make install`` by default)
In this case, the ``install`` phase needed to be overridden because the
Makefile did not have an install target. We also modify the Makefile to use
Spack's compiler wrappers. The default ``build`` phase is not changed.
Starting with version 1.3.0, we want to use Autotools to build instead.
:ref:`autotoolspackage` has four phases:
#. ``autoreconf`` (does not if a configure script already exists)
#. ``configure`` (runs ``./configure --prefix=...`` by default)
#. ``build`` (runs ``make`` by default)
#. ``install`` (runs ``make install`` by default)
If the only version we need to support is 1.3.0, the package would look as
simple as:
.. code-block:: python
class Foo(AutotoolsPackage):
version("1.3.0", sha256="...")
def configure_args(self):
return ["--enable-shared"]
In this case, we use the default methods for each phase and only override
``configure_args`` to specify additional flags to pass to ``./configure``.
If we wanted to write a single package that supports both versions 1.2.0 and
1.3.0, it would look something like:
.. code-block:: python
class Foo(AutotoolsPackage):
version("1.3.0", sha256="...")
version("1.2.0", sha256="...", deprecated=True)
def configure_args(self):
return ["--enable-shared"]
# Remove the following once version 1.2.0 is dropped
@when("@:1.2")
def patch(self):
filter_file("CC=", "CC=" + spack_cc, "Makefile")
@when("@:1.2")
def autoreconf(self, spec, prefix):
pass
@when("@:1.2")
def configure(self, spec, prefix):
pass
@when("@:1.2")
def install(self, spec, prefix):
install_tree(".", prefix)
There are a few interesting things to note here:
* We added ``deprecated=True`` to version 1.2.0. This signifies that version
1.2.0 is deprecated and shouldn't be used. However, if a user still relies
on version 1.2.0, it's still there and builds just fine.
* We moved the contents of the ``edit`` phase to the ``patch`` function. Since
``AutotoolsPackage`` doesn't have an ``edit`` phase, the only way for this
step to be executed is to move it to the ``patch`` function, which always
gets run.
* The ``autoreconf`` and ``configure`` phases become no-ops. Since the old
Makefile-based build system doesn't use these, we ignore these phases when
building ``foo@1.2.0``.
* The ``@when`` decorator is used to override these phases only for older
versions. The default methods are used for ``foo@1.3:``.
Once a new Spack release comes out, version 1.2.0 and everything below the
comment can be safely deleted. The result is the same as if we had written a
package for version 1.3.0 from scratch.
^^^^^^^^^^^^^^^^^^
Autotools -> CMake
^^^^^^^^^^^^^^^^^^
Let's say we have the following package:
.. code-block:: python
class Bar(AutotoolsPackage):
version("1.2.0", sha256="...")
def configure_args(self):
return ["--enable-shared"]
The package subclasses from :ref:`autotoolspackage`, which has four phases:
#. ``autoreconf`` (does not if a configure script already exists)
#. ``configure`` (runs ``./configure --prefix=...`` by default)
#. ``build`` (runs ``make`` by default)
#. ``install`` (runs ``make install`` by default)
In this case, we use the default methods for each phase and only override
``configure_args`` to specify additional flags to pass to ``./configure``.
Starting with version 1.3.0, we want to use CMake to build instead.
:ref:`cmakepackage` has three phases:
#. ``cmake`` (runs ``cmake ...`` by default)
#. ``build`` (runs ``make`` by default)
#. ``install`` (runs ``make install`` by default)
If the only version we need to support is 1.3.0, the package would look as
simple as:
.. code-block:: python
class Bar(CMakePackage):
version("1.3.0", sha256="...")
def cmake_args(self):
return [self.define("BUILD_SHARED_LIBS", True)]
In this case, we use the default methods for each phase and only override
``cmake_args`` to specify additional flags to pass to ``cmake``.
If we wanted to write a single package that supports both versions 1.2.0 and
1.3.0, it would look something like:
.. code-block:: python
class Bar(CMakePackage):
version("1.3.0", sha256="...")
version("1.2.0", sha256="...", deprecated=True)
def cmake_args(self):
return [self.define("BUILD_SHARED_LIBS", True)]
# Remove the following once version 1.2.0 is dropped
def configure_args(self):
return ["--enable-shared"]
@when("@:1.2")
def cmake(self, spec, prefix):
configure("--prefix=" + prefix, *self.configure_args())
There are a few interesting things to note here:
* We added ``deprecated=True`` to version 1.2.0. This signifies that version
1.2.0 is deprecated and shouldn't be used. However, if a user still relies
on version 1.2.0, it's still there and builds just fine.
* Since CMake and Autotools are so similar, we only need to override the
``cmake`` phase, we can use the default ``build`` and ``install`` phases.
* We override ``cmake`` to run ``./configure`` for older versions.
``configure_args`` remains the same.
* The ``@when`` decorator is used to override these phases only for older
versions. The default methods are used for ``bar@1.3:``.
Once a new Spack release comes out, version 1.2.0 and everything below the
comment can be safely deleted. The result is the same as if we had written a
package for version 1.3.0 from scratch.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Multiple build systems for the same version
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
During the transition from one build system to another, developers often
support multiple build systems at the same time. Spack can only use a single
build system for a single version. To decide which build system to use for a
particular version, take the following things into account:
1. If the developers explicitly state that one build system is preferred over
another, use that one.
2. If one build system is considered "experimental" while another is considered
"stable", use the stable build system.
3. Otherwise, use the newer build system.
The developer preference for which build system to use can change over time as
a newer build system becomes stable/recommended.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Dropping support for old build systems
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When older versions of a package don't support a newer build system, it can be
tempting to simply delete them from a package. This significantly reduces
package complexity and makes the build recipe much easier to maintain. However,
other packages or Spack users may rely on these older versions. The recommended
approach is to first support both build systems (as demonstrated above),
:ref:`deprecate <deprecate>` versions that rely on the old build system, and
remove those versions and any phases that needed to be overridden in the next
Spack release.
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Three or more build systems
^^^^^^^^^^^^^^^^^^^^^^^^^^^
In rare cases, a package may change build systems multiple times. For example,
a package may start with Makefiles, then switch to Autotools, then switch to
CMake. The same logic used above can be extended to any number of build systems.
For example:
.. code-block:: python
class Baz(CMakePackage):
version("1.4.0", sha256="...") # CMake
version("1.3.0", sha256="...") # Autotools
version("1.2.0", sha256="...") # Makefile
def cmake_args(self):
return [self.define("BUILD_SHARED_LIBS", True)]
# Remove the following once version 1.3.0 is dropped
def configure_args(self):
return ["--enable-shared"]
@when("@1.3")
def cmake(self, spec, prefix):
configure("--prefix=" + prefix, *self.configure_args())
# Remove the following once version 1.2.0 is dropped
@when("@:1.2")
def patch(self):
filter_file("CC=", "CC=" + spack_cc, "Makefile")
@when("@:1.2")
def cmake(self, spec, prefix):
pass
@when("@:1.2")
def install(self, spec, prefix):
install_tree(".", prefix)
^^^^^^^^^^^^^^^^^^^
Additional examples
^^^^^^^^^^^^^^^^^^^
When writing new packages, it often helps to see examples of existing packages.
Here is an incomplete list of existing Spack packages that have changed build
systems before:
================ ===================== ================
Package Previous Build System New Build System
================ ===================== ================
amber custom CMake
arpack-ng Autotools CMake
atk Autotools Meson
blast None Autotools
dyninst Autotools CMake
evtgen Autotools CMake
fish Autotools CMake
gdk-pixbuf Autotools Meson
glib Autotools Meson
glog Autotools CMake
gmt Autotools CMake
gtkplus Autotools Meson
hpl Makefile Autotools
interproscan Perl Maven
jasper Autotools CMake
kahip SCons CMake
kokkos Makefile CMake
kokkos-kernels Makefile CMake
leveldb Makefile CMake
libdrm Autotools Meson
libjpeg-turbo Autotools CMake
mesa Autotools Meson
metis None CMake
mpifileutils Autotools CMake
muparser Autotools CMake
mxnet Makefile CMake
nest Autotools CMake
neuron Autotools CMake
nsimd CMake nsconfig
opennurbs Makefile CMake
optional-lite None CMake
plasma Makefile CMake
preseq Makefile Autotools
protobuf Autotools CMake
py-pygobject Autotools Python
singularity Autotools Makefile
span-lite None CMake
ssht Makefile CMake
string-view-lite None CMake
superlu Makefile CMake
superlu-dist Makefile CMake
uncrustify Autotools CMake
================ ===================== ================
Packages that support multiple build systems can be a bit confusing to write.
Don't hesitate to open an issue or draft pull request and ask for advice from
other Spack developers!

View File

@@ -5,9 +5,9 @@
.. _octavepackage: .. _octavepackage:
------------- ------
OctavePackage Octave
------------- ------
Octave has its own build system for installing packages. Octave has its own build system for installing packages.
@@ -15,7 +15,7 @@ Octave has its own build system for installing packages.
Phases Phases
^^^^^^ ^^^^^^
The ``OctavePackage`` base class has a single phase: The ``OctaveBuilder`` and ``OctavePackage`` base classes have a single phase:
#. ``install`` - install the package #. ``install`` - install the package

View File

@@ -5,9 +5,9 @@
.. _perlpackage: .. _perlpackage:
----------- ----
PerlPackage Perl
----------- ----
Much like Octave, Perl has its own language-specific Much like Octave, Perl has its own language-specific
build system. build system.
@@ -16,7 +16,7 @@ build system.
Phases Phases
^^^^^^ ^^^^^^
The ``PerlPackage`` base class comes with 3 phases that can be overridden: The ``PerlBuilder`` and ``PerlPackage`` base classes come with 3 phases that can be overridden:
#. ``configure`` - configure the package #. ``configure`` - configure the package
#. ``build`` - build the package #. ``build`` - build the package

View File

@@ -5,9 +5,9 @@
.. _qmakepackage: .. _qmakepackage:
------------ -----
QMakePackage QMake
------------ -----
Much like Autotools and CMake, QMake is a build-script generator Much like Autotools and CMake, QMake is a build-script generator
designed by the developers of Qt. In its simplest form, Spack's designed by the developers of Qt. In its simplest form, Spack's
@@ -29,7 +29,7 @@ variables or edit ``*.pro`` files to get things working properly.
Phases Phases
^^^^^^ ^^^^^^
The ``QMakePackage`` base class comes with the following phases: The ``QMakeBuilder`` and ``QMakePackage`` base classes come with the following phases:
#. ``qmake`` - generate Makefiles #. ``qmake`` - generate Makefiles
#. ``build`` - build the project #. ``build`` - build the project

View File

@@ -5,9 +5,9 @@
.. _racketpackage: .. _racketpackage:
------------- ------
RacketPackage Racket
------------- ------
Much like Python, Racket packages and modules have their own special build system. Much like Python, Racket packages and modules have their own special build system.
To learn more about the specifics of Racket package system, please refer to the To learn more about the specifics of Racket package system, please refer to the
@@ -17,7 +17,7 @@ To learn more about the specifics of Racket package system, please refer to the
Phases Phases
^^^^^^ ^^^^^^
The ``RacketPackage`` base class provides an ``install`` phase that The ``RacketBuilder`` and ``RacketPackage`` base classes provides an ``install`` phase that
can be overridden, corresponding to the use of: can be overridden, corresponding to the use of:
.. code-block:: console .. code-block:: console

View File

@@ -19,7 +19,7 @@ new Spack packages for.
Phases Phases
^^^^^^ ^^^^^^
The ``RPackage`` base class has a single phase: The ``RBuilder`` and ``RPackage`` base classes have a single phase:
#. ``install`` - install the package #. ``install`` - install the package

View File

@@ -5,9 +5,9 @@
.. _rubypackage: .. _rubypackage:
----------- ----
RubyPackage Ruby
----------- ----
Like Perl, Python, and R, Ruby has its own build system for Like Perl, Python, and R, Ruby has its own build system for
installing Ruby gems. installing Ruby gems.
@@ -16,7 +16,7 @@ installing Ruby gems.
Phases Phases
^^^^^^ ^^^^^^
The ``RubyPackage`` base class provides the following phases that The ``RubyBuilder`` and ``RubyPackage`` base classes provide the following phases that
can be overridden: can be overridden:
#. ``build`` - build everything needed to install #. ``build`` - build everything needed to install

View File

@@ -5,9 +5,9 @@
.. _sconspackage: .. _sconspackage:
------------ -----
SConsPackage SCons
------------ -----
SCons is a general-purpose build system that does not rely on SCons is a general-purpose build system that does not rely on
Makefiles to build software. SCons is written in Python, and handles Makefiles to build software. SCons is written in Python, and handles
@@ -42,7 +42,7 @@ As previously mentioned, SCons allows developers to add subcommands like
$ scons install $ scons install
To facilitate this, the ``SConsPackage`` base class provides the To facilitate this, the ``SConsBuilder`` and ``SconsPackage`` base classes provide the
following phases: following phases:
#. ``build`` - build the package #. ``build`` - build the package

View File

@@ -5,9 +5,9 @@
.. _sippackage: .. _sippackage:
---------- ---
SIPPackage SIP
---------- ---
SIP is a tool that makes it very easy to create Python bindings for C and C++ SIP is a tool that makes it very easy to create Python bindings for C and C++
libraries. It was originally developed to create PyQt, the Python bindings for libraries. It was originally developed to create PyQt, the Python bindings for
@@ -22,7 +22,7 @@ provides support functions to the automatically generated code.
Phases Phases
^^^^^^ ^^^^^^
The ``SIPPackage`` base class comes with the following phases: The ``SIPBuilder`` and ``SIPPackage`` base classes come with the following phases:
#. ``configure`` - configure the package #. ``configure`` - configure the package
#. ``build`` - build the package #. ``build`` - build the package

View File

@@ -5,9 +5,9 @@
.. _wafpackage: .. _wafpackage:
---------- ---
WafPackage Waf
---------- ---
Like SCons, Waf is a general-purpose build system that does not rely Like SCons, Waf is a general-purpose build system that does not rely
on Makefiles to build software. on Makefiles to build software.
@@ -16,7 +16,7 @@ on Makefiles to build software.
Phases Phases
^^^^^^ ^^^^^^
The ``WafPackage`` base class comes with the following phases: The ``WafBuilder`` and ``WafPackage`` base classes come with the following phases:
#. ``configure`` - configure the project #. ``configure`` - configure the project
#. ``build`` - build the project #. ``build`` - build the project

View File

@@ -209,6 +209,7 @@ def setup(sphinx):
# Spack classes that are private and we don't want to expose # Spack classes that are private and we don't want to expose
("py:class", "spack.provider_index._IndexBase"), ("py:class", "spack.provider_index._IndexBase"),
("py:class", "spack.repo._PrependFileLoader"), ("py:class", "spack.repo._PrependFileLoader"),
("py:class", "spack.build_systems._checks.BaseBuilder"),
# Spack classes that intersphinx is unable to resolve # Spack classes that intersphinx is unable to resolve
("py:class", "spack.version.VersionBase"), ("py:class", "spack.version.VersionBase"),
] ]

View File

@@ -149,11 +149,9 @@ grouped by functionality.
Package-related modules Package-related modules
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
:mod:`spack.package` :mod:`spack.package_base`
Contains the :class:`~spack.package_base.Package` class, which Contains the :class:`~spack.package_base.PackageBase` class, which
is the superclass for all packages in Spack. Methods on ``Package`` is the superclass for all packages in Spack.
implement all phases of the :ref:`package lifecycle
<package-lifecycle>` and manage the build process.
:mod:`spack.util.naming` :mod:`spack.util.naming`
Contains functions for mapping between Spack package names, Contains functions for mapping between Spack package names,

View File

@@ -98,40 +98,42 @@ For example, this command:
.. code-block:: console .. code-block:: console
$ spack create http://www.mr511.de/software/libelf-0.8.13.tar.gz $ spack create https://ftp.osuosl.org/pub/blfs/conglomeration/libelf/libelf-0.8.13.tar.gz
creates a simple python file: creates a simple python file:
.. code-block:: python .. code-block:: python
from spack import * from spack.package import *
class Libelf(Package): class Libelf(AutotoolsPackage):
"""FIXME: Put a proper description of your package here.""" """FIXME: Put a proper description of your package here."""
# FIXME: Add a proper url for your package's homepage here. # FIXME: Add a proper url for your package's homepage here.
homepage = "http://www.example.com" homepage = "https://www.example.com"
url = "http://www.mr511.de/software/libelf-0.8.13.tar.gz" url = "https://ftp.osuosl.org/pub/blfs/conglomeration/libelf/libelf-0.8.13.tar.gz"
version('0.8.13', '4136d7b4c04df68b686570afa26988ac') # FIXME: Add a list of GitHub accounts to
# notify when the package is updated.
# maintainers = ["github_user1", "github_user2"]
version("0.8.13", sha256="591a9b4ec81c1f2042a97aa60564e0cb79d041c52faa7416acb38bc95bd2c76d")
# FIXME: Add dependencies if required. # FIXME: Add dependencies if required.
# depends_on('foo') # depends_on("foo")
def install(self, spec, prefix): def configure_args(self):
# FIXME: Modify the configure line to suit your build system here. # FIXME: Add arguments other than --prefix
configure('--prefix={0}'.format(prefix)) # FIXME: If not needed delete this function
args = []
# FIXME: Add logic to build and install here. return args
make()
make('install')
It doesn't take much python coding to get from there to a working It doesn't take much python coding to get from there to a working
package: package:
.. literalinclude:: _spack_root/var/spack/repos/builtin/packages/libelf/package.py .. literalinclude:: _spack_root/var/spack/repos/builtin/packages/libelf/package.py
:lines: 6- :lines: 5-
Spack also provides wrapper functions around common commands like Spack also provides wrapper functions around common commands like
``configure``, ``make``, and ``cmake`` to make writing packages ``configure``, ``make``, and ``cmake`` to make writing packages

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -34,24 +34,155 @@ ubiquitous in the scientific software community. Second, it's a modern
language and has many powerful features to help make package writing language and has many powerful features to help make package writing
easy. easy.
---------------------------
Creating & editing packages .. _installation_procedure:
---------------------------
--------------------------------------
Overview of the installation procedure
--------------------------------------
Whenever Spack installs software, it goes through a series of predefined steps:
.. image:: images/installation_pipeline.png
:scale: 60 %
:align: center
All these steps are influenced by the metadata in each ``package.py`` and
by the current Spack configuration.
Since build systems are different from one another, the execution of the
last block in the figure is further expanded in a build system specific way.
An example for ``CMake`` is, for instance:
.. image:: images/builder_phases.png
:align: center
:scale: 60 %
The predefined steps for each build system are called "phases".
In general, the name and order in which the phases will be executed can be
obtained by either reading the API docs at :py:mod:`~.spack.build_systems`, or
using the ``spack info`` command:
.. code-block:: console
:emphasize-lines: 13,14
$ spack info --phases m4
AutotoolsPackage: m4
Homepage: https://www.gnu.org/software/m4/m4.html
Safe versions:
1.4.17 ftp://ftp.gnu.org/gnu/m4/m4-1.4.17.tar.gz
Variants:
Name Default Description
sigsegv on Build the libsigsegv dependency
Installation Phases:
autoreconf configure build install
Build Dependencies:
libsigsegv
...
An extensive list of available build systems and phases is provided in :ref:`installation_process`.
------------------------
Writing a package recipe
------------------------
Since v0.19, Spack supports two ways of writing a package recipe. The most commonly used is to encode both the metadata
(directives, etc.) and the build behavior in a single class, like shown in the following example:
.. code-block:: python
class Openjpeg(CMakePackage):
"""OpenJPEG is an open-source JPEG 2000 codec written in C language"""
homepage = "https://github.com/uclouvain/openjpeg"
url = "https://github.com/uclouvain/openjpeg/archive/v2.3.1.tar.gz"
version("2.4.0", sha256="8702ba68b442657f11aaeb2b338443ca8d5fb95b0d845757968a7be31ef7f16d")
variant("codec", default=False, description="Build the CODEC executables")
depends_on("libpng", when="+codec")
def url_for_version(self, version):
if version >= Version("2.1.1"):
return super(Openjpeg, self).url_for_version(version)
url_fmt = "https://github.com/uclouvain/openjpeg/archive/version.{0}.tar.gz"
return url_fmt.format(version)
def cmake_args(self):
args = [
self.define_from_variant("BUILD_CODEC", "codec"),
self.define("BUILD_MJ2", False),
self.define("BUILD_THIRDPARTY", False),
]
return args
A package encoded with a single class is backward compatible with versions of Spack
lower than v0.19, and so are custom repositories containing only recipes of this kind.
The downside is that *this format doesn't allow packagers to use more than one build system in a single recipe*.
To do that, we have to resort to the second way Spack has of writing packages, which involves writing a
builder class explicitly. Using the same example as above, this reads:
.. code-block:: python
class Openjpeg(CMakePackage):
"""OpenJPEG is an open-source JPEG 2000 codec written in C language"""
homepage = "https://github.com/uclouvain/openjpeg"
url = "https://github.com/uclouvain/openjpeg/archive/v2.3.1.tar.gz"
version("2.4.0", sha256="8702ba68b442657f11aaeb2b338443ca8d5fb95b0d845757968a7be31ef7f16d")
variant("codec", default=False, description="Build the CODEC executables")
depends_on("libpng", when="+codec")
def url_for_version(self, version):
if version >= Version("2.1.1"):
return super(Openjpeg, self).url_for_version(version)
url_fmt = "https://github.com/uclouvain/openjpeg/archive/version.{0}.tar.gz"
return url_fmt.format(version)
class CMakeBuilder(spack.build_systems.cmake.CMakeBuilder):
def cmake_args(self):
args = [
self.define_from_variant("BUILD_CODEC", "codec"),
self.define("BUILD_MJ2", False),
self.define("BUILD_THIRDPARTY", False),
]
return args
This way of writing packages allows extending the recipe to support multiple build systems,
see :ref:`multiple_build_systems` for more details. The downside is that recipes of this kind
are only understood by Spack since v0.19+. More information on the internal architecture of
Spack can be found at :ref:`package_class_structure`.
.. note::
If a builder is implemented in ``package.py``, all build-specific methods must be moved
to the builder. This means that if you have a package like
.. code-block:: python
class Foo(CmakePackage):
def cmake_args(self):
...
and you add a builder to the ``package.py``, you must move ``cmake_args`` to the builder.
.. _cmd-spack-create: .. _cmd-spack-create:
^^^^^^^^^^^^^^^^ ---------------------
``spack create`` Creating new packages
^^^^^^^^^^^^^^^^ ---------------------
The ``spack create`` command creates a directory with the package name and To help creating a new package Spack provides a command that generates a ``package.py``
generates a ``package.py`` file with a boilerplate package template. If given file in an existing repository, with a boilerplate package template. Here's an example:
a URL pointing to a tarball or other software archive, ``spack create`` is
smart enough to determine basic information about the package, including its name
and build system. In most cases, ``spack create`` plus a few modifications is
all you need to get a package working.
Here's an example:
.. code-block:: console .. code-block:: console
@@ -87,23 +218,6 @@ You do not *have* to download all of the versions up front. You can
always choose to download just one tarball initially, and run always choose to download just one tarball initially, and run
:ref:`cmd-spack-checksum` later if you need more versions. :ref:`cmd-spack-checksum` later if you need more versions.
Let's say you download 3 tarballs:
.. code-block:: console
How many would you like to checksum? (default is 1, q to abort) 3
==> Downloading...
==> Fetching https://gmplib.org/download/gmp/gmp-6.1.2.tar.bz2
######################################################################## 100.0%
==> Fetching https://gmplib.org/download/gmp/gmp-6.1.1.tar.bz2
######################################################################## 100.0%
==> Fetching https://gmplib.org/download/gmp/gmp-6.1.0.tar.bz2
######################################################################## 100.0%
==> Checksummed 3 versions of gmp:
==> This package looks like it uses the autotools build system
==> Created template for gmp package
==> Created package file: /Users/Adam/spack/var/spack/repos/builtin/packages/gmp/package.py
Spack automatically creates a directory in the appropriate repository, Spack automatically creates a directory in the appropriate repository,
generates a boilerplate template for your package, and opens up the new generates a boilerplate template for your package, and opens up the new
``package.py`` in your favorite ``$EDITOR``: ``package.py`` in your favorite ``$EDITOR``:
@@ -111,6 +225,14 @@ generates a boilerplate template for your package, and opens up the new
.. code-block:: python .. code-block:: python
:linenos: :linenos:
# Copyright 2013-2022 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)
# ----------------------------------------------------------------------------
# If you submit this package back to Spack as a pull request,
# please first remove this boilerplate and all FIXME comments.
# #
# This is a template package file for Spack. We've put "FIXME" # This is a template package file for Spack. We've put "FIXME"
# next to all the things you'll want to change. Once you've handled # next to all the things you'll want to change. Once you've handled
@@ -123,9 +245,8 @@ generates a boilerplate template for your package, and opens up the new
# spack edit gmp # spack edit gmp
# #
# See the Spack documentation for more information on packaging. # See the Spack documentation for more information on packaging.
# If you submit this package back to Spack as a pull request, # ----------------------------------------------------------------------------
# please first remove this boilerplate and all FIXME comments. import spack.build_systems.autotools
#
from spack.package import * from spack.package import *
@@ -133,19 +254,17 @@ generates a boilerplate template for your package, and opens up the new
"""FIXME: Put a proper description of your package here.""" """FIXME: Put a proper description of your package here."""
# FIXME: Add a proper url for your package's homepage here. # FIXME: Add a proper url for your package's homepage here.
homepage = "http://www.example.com" homepage = "https://www.example.com"
url = "https://gmplib.org/download/gmp/gmp-6.1.2.tar.bz2" url = "https://gmplib.org/download/gmp/gmp-6.1.2.tar.bz2"
# FIXME: Add a list of GitHub accounts to # FIXME: Add a list of GitHub accounts to
# notify when the package is updated. # notify when the package is updated.
# maintainers = ['github_user1', 'github_user2'] # maintainers = ["github_user1", "github_user2"]
version('6.1.2', '8ddbb26dc3bd4e2302984debba1406a5') version("6.2.1", sha256="eae9326beb4158c386e39a356818031bd28f3124cf915f8c5b1dc4c7a36b4d7c")
version('6.1.1', '4c175f86e11eb32d8bf9872ca3a8e11d')
version('6.1.0', '86ee6e54ebfc4a90b643a65e402c4048')
# FIXME: Add dependencies if required. # FIXME: Add dependencies if required.
# depends_on('foo') # depends_on("foo")
def configure_args(self): def configure_args(self):
# FIXME: Add arguments other than --prefix # FIXME: Add arguments other than --prefix
@@ -154,15 +273,16 @@ generates a boilerplate template for your package, and opens up the new
return args return args
The tedious stuff (creating the class, checksumming archives) has been The tedious stuff (creating the class, checksumming archives) has been
done for you. You'll notice that ``spack create`` correctly detected that done for you. Spack correctly detected that ``gmp`` uses the ``autotools``
``gmp`` uses the Autotools build system. It created a new ``Gmp`` package build system, so it created a new ``Gmp`` package that subclasses the
that subclasses the ``AutotoolsPackage`` base class. This base class ``AutotoolsPackage`` base class.
provides basic installation methods common to all Autotools packages:
The default installation procedure for a package subclassing the ``AutotoolsPackage``
is to go through the typical process of:
.. code-block:: bash .. code-block:: bash
./configure --prefix=/path/to/installation/directory ./configure --prefix=/path/to/installation/directory
make make
make check make check
make install make install
@@ -209,12 +329,14 @@ The rest of the tasks you need to do are as follows:
Your new package may require specific flags during ``configure``. Your new package may require specific flags during ``configure``.
These can be added via ``configure_args``. Specifics will differ These can be added via ``configure_args``. Specifics will differ
depending on the package and its build system. depending on the package and its build system.
:ref:`Implementing the install method <install-method>` is :ref:`installation_process` is
covered in detail later. covered in detail later.
Passing a URL to ``spack create`` is a convenient and easy way to get ^^^^^^^^^^^^^^^^^^^^^^^^^
a basic package template, but what if your software is licensed and Non-downloadable software
cannot be downloaded from a URL? You can still create a boilerplate ^^^^^^^^^^^^^^^^^^^^^^^^^
If your software cannot be downloaded from a URL you can still create a boilerplate
``package.py`` by telling ``spack create`` what name you want to use: ``package.py`` by telling ``spack create`` what name you want to use:
.. code-block:: console .. code-block:: console
@@ -223,40 +345,23 @@ cannot be downloaded from a URL? You can still create a boilerplate
This will create a simple ``intel`` package with an ``install()`` This will create a simple ``intel`` package with an ``install()``
method that you can craft to install your package. method that you can craft to install your package.
Likewise, you can force the build system to be used with ``--template`` and,
What if ``spack create <url>`` guessed the wrong name or build system? in case it's needed, you can overwrite a package already in the repository
For example, if your package uses the Autotools build system but does with ``--force``:
not come with a ``configure`` script, Spack won't realize it uses
Autotools. You can overwrite the old package with ``--force`` and specify
a name with ``--name`` or a build system template to use with ``--template``:
.. code-block:: console .. code-block:: console
$ spack create --name gmp https://gmplib.org/download/gmp/gmp-6.1.2.tar.bz2 $ spack create --name gmp https://gmplib.org/download/gmp/gmp-6.1.2.tar.bz2
$ spack create --force --template autotools https://gmplib.org/download/gmp/gmp-6.1.2.tar.bz2 $ spack create --force --template autotools https://gmplib.org/download/gmp/gmp-6.1.2.tar.bz2
.. note::
If you are creating a package that uses the Autotools build system
but does not come with a ``configure`` script, you'll need to add an
``autoreconf`` method to your package that explains how to generate
the ``configure`` script. You may also need the following dependencies:
.. code-block:: python
depends_on('autoconf', type='build')
depends_on('automake', type='build')
depends_on('libtool', type='build')
depends_on('m4', type='build')
A complete list of available build system templates can be found by running A complete list of available build system templates can be found by running
``spack create --help``. ``spack create --help``.
.. _cmd-spack-edit: .. _cmd-spack-edit:
^^^^^^^^^^^^^^ -------------------------
``spack edit`` Editing existing packages
^^^^^^^^^^^^^^ -------------------------
One of the easiest ways to learn how to write packages is to look at One of the easiest ways to learn how to write packages is to look at
existing ones. You can edit a package file by name with the ``spack existing ones. You can edit a package file by name with the ``spack
@@ -266,10 +371,15 @@ edit`` command:
$ spack edit gmp $ spack edit gmp
So, if you used ``spack create`` to create a package, then saved and If you used ``spack create`` to create a package, you can get back to
closed the resulting file, you can get back to it with ``spack edit``. it later with ``spack edit``. For instance, the ``gmp`` package actually
The ``gmp`` package actually lives in lives in:
``$SPACK_ROOT/var/spack/repos/builtin/packages/gmp/package.py``,
.. code-block:: console
$ spack location -p gmp
${SPACK_ROOT}/var/spack/repos/builtin/packages/gmp/package.py
but ``spack edit`` provides a much simpler shortcut and saves you the but ``spack edit`` provides a much simpler shortcut and saves you the
trouble of typing the full path. trouble of typing the full path.
@@ -2422,7 +2532,7 @@ Spack provides a mechanism for dependencies to influence the
environment of their dependents by overriding the environment of their dependents by overriding the
:meth:`setup_dependent_run_environment <spack.package_base.PackageBase.setup_dependent_run_environment>` :meth:`setup_dependent_run_environment <spack.package_base.PackageBase.setup_dependent_run_environment>`
or the or the
:meth:`setup_dependent_build_environment <spack.package_base.PackageBase.setup_dependent_build_environment>` :meth:`setup_dependent_build_environment <spack.builder.Builder.setup_dependent_build_environment>`
methods. methods.
The Qt package, for instance, uses this call: The Qt package, for instance, uses this call:
@@ -3280,67 +3390,91 @@ the Python extensions provided by them: once for ``+python`` and once
for ``~python``. Other than using a little extra disk space, that for ``~python``. Other than using a little extra disk space, that
solution has no serious problems. solution has no serious problems.
.. _installation_procedure: .. _installation_process:
--------------------------------------- --------------------------------
Implementing the installation procedure Overriding build system defaults
--------------------------------------- --------------------------------
The last element of a package is its **installation procedure**. This is .. note::
where the real work of installation happens, and it's the main part of
the package you'll need to customize for each piece of software.
Defining an installation procedure means overriding a set of methods or attributes If you code a single class in ``package.py`` all the functions shown in the table below
that will be called at some point during the installation of the package. can be implemented with the same signature on the ``*Package`` instead of the corresponding builder.
The package base class, usually specialized for a given build system, determines the
actual set of entities available for overriding.
The classes that are currently provided by Spack are: Most of the time the default implementation of methods or attributes in build system base classes
is what a packager needs, and just a very few entities need to be overwritten. Typically we just
need to override methods like ``configure_args``:
.. code-block:: python
def configure_args(self):
args = ["--enable-cxx"] + self.enable_or_disable("libs")
if "libs=static" in self.spec:
args.append("--with-pic")
return args
The actual set of entities available for overriding in ``package.py`` depend on
the build system. The build systems currently supported by Spack are:
+----------------------------------------------------------+----------------------------------+ +----------------------------------------------------------+----------------------------------+
| **Base Class** | **Purpose** | | **API docs** | **Description** |
+==========================================================+==================================+ +==========================================================+==================================+
| :class:`~spack.package_base.Package` | General base class not | | :class:`~spack.build_systems.generic` | Generic build system without any |
| | specialized for any build system | | | base implementation |
+----------------------------------------------------------+----------------------------------+ +----------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.makefile.MakefilePackage` | Specialized class for packages | | :class:`~spack.build_systems.makefile` | Specialized build system for |
| | built invoking | | | software built invoking |
| | hand-written Makefiles | | | hand-written Makefiles |
+----------------------------------------------------------+----------------------------------+ +----------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.autotools.AutotoolsPackage` | Specialized class for packages | | :class:`~spack.build_systems.autotools` | Specialized build system for |
| | built using GNU Autotools | | | software built using |
| | GNU Autotools |
+----------------------------------------------------------+----------------------------------+ +----------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.cmake.CMakePackage` | Specialized class for packages | | :class:`~spack.build_systems.cmake` | Specialized build system for |
| | built using CMake | | | software built using CMake |
+----------------------------------------------------------+----------------------------------+ +----------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.cuda.CudaPackage` | A helper class for packages that | | :class:`~spack.build_systems.maven` | Specialized build system for |
| | use CUDA | | | software built using Maven |
+----------------------------------------------------------+----------------------------------+ +----------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.qmake.QMakePackage` | Specialized class for packages | | :class:`~spack.build_systems.meson` | Specialized build system for |
| | built using QMake | | | software built using Meson |
+----------------------------------------------------------+----------------------------------+ +----------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.rocm.ROCmPackage` | A helper class for packages that | | :class:`~spack.build_systems.nmake` | Specialized build system for |
| | use ROCm | | | software built using NMake |
+----------------------------------------------------------+----------------------------------+ +----------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.scons.SConsPackage` | Specialized class for packages | | :class:`~spack.build_systems.qmake` | Specialized build system for |
| | built using SCons | | | software built using QMake |
+----------------------------------------------------------+----------------------------------+ +----------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.waf.WafPackage` | Specialized class for packages | | :class:`~spack.build_systems.scons` | Specialized build system for |
| | built using Waf | | | software built using SCons |
+----------------------------------------------------------+----------------------------------+ +----------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.r.RPackage` | Specialized class for | | :class:`~spack.build_systems.waf` | Specialized build system for |
| | software built using Waf |
+----------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.r` | Specialized build system for |
| | R extensions | | | R extensions |
+----------------------------------------------------------+----------------------------------+ +----------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.octave.OctavePackage` | Specialized class for | | :class:`~spack.build_systems.octave` | Specialized build system for |
| | Octave packages | | | Octave packages |
+----------------------------------------------------------+----------------------------------+ +----------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.python.PythonPackage` | Specialized class for | | :class:`~spack.build_systems.python` | Specialized build system for |
| | Python extensions | | | Python extensions |
+----------------------------------------------------------+----------------------------------+ +----------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.perl.PerlPackage` | Specialized class for | | :class:`~spack.build_systems.perl` | Specialized build system for |
| | Perl extensions | | | Perl extensions |
+----------------------------------------------------------+----------------------------------+ +----------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.intel.IntelPackage` | Specialized class for licensed | | :class:`~spack.build_systems.ruby` | Specialized build system for |
| | Intel software | | | Ruby extensions |
+----------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.intel` | Specialized build system for |
| | licensed Intel software |
+----------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.oneapi` | Specialized build system for |
| | Intel onaAPI software |
+----------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.aspell_dict` | Specialized build system for |
| | Aspell dictionaries |
+----------------------------------------------------------+----------------------------------+ +----------------------------------------------------------+----------------------------------+
@@ -3353,69 +3487,17 @@ The classes that are currently provided by Spack are:
For example, a Python extension installed with CMake would ``extends('python')`` and For example, a Python extension installed with CMake would ``extends('python')`` and
subclass from :class:`~spack.build_systems.cmake.CMakePackage`. subclass from :class:`~spack.build_systems.cmake.CMakePackage`.
^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^
Installation pipeline Overriding builder methods
^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^
When a user runs ``spack install``, Spack: Build-system "phases" have default implementations that fit most of the common cases:
1. Fetches an archive for the correct version of the software.
2. Expands the archive.
3. Sets the current working directory to the root directory of the expanded archive.
Then, depending on the base class of the package under consideration, it will execute
a certain number of **phases** that reflect the way a package of that type is usually built.
The name and order in which the phases will be executed can be obtained either reading the API
docs at :py:mod:`~.spack.build_systems`, or using the ``spack info`` command:
.. code-block:: console
:emphasize-lines: 26-27
$ spack info --phases m4
AutotoolsPackage: m4
Description:
GNU M4 is an implementation of the traditional Unix macro processor.
Homepage: https://www.gnu.org/software/m4/m4.html
Preferred version:
1.4.19 https://ftpmirror.gnu.org/m4/m4-1.4.19.tar.gz
Safe versions:
1.4.19 https://ftpmirror.gnu.org/m4/m4-1.4.19.tar.gz
1.4.18 https://ftpmirror.gnu.org/m4/m4-1.4.18.tar.gz
1.4.17 https://ftpmirror.gnu.org/m4/m4-1.4.17.tar.gz
Deprecated versions:
None
Variants:
Name [Default] When Allowed values Description
============== ==== ============== ===============================
sigsegv [on] -- on, off Build the libsigsegv dependency
Installation Phases:
autoreconf configure build install
Build Dependencies:
diffutils gnuconfig libsigsegv
Link Dependencies:
libsigsegv
Run Dependencies:
None
Typically, phases have default implementations that fit most of the common cases:
.. literalinclude:: _spack_root/lib/spack/spack/build_systems/autotools.py .. literalinclude:: _spack_root/lib/spack/spack/build_systems/autotools.py
:pyobject: AutotoolsPackage.configure :pyobject: AutotoolsBuilder.configure
:linenos: :linenos:
It is thus just sufficient for a packager to override a few It is usually sufficient for a packager to override a few
build system specific helper methods or attributes to provide, for instance, build system specific helper methods or attributes to provide, for instance,
configure arguments: configure arguments:
@@ -3423,31 +3505,31 @@ configure arguments:
:pyobject: M4.configure_args :pyobject: M4.configure_args
:linenos: :linenos:
.. note:: Each specific build system has a list of attributes and methods that can be overridden to
Each specific build system has a list of attributes that can be overridden to fine-tune the installation of a package without overriding an entire phase. To
fine-tune the installation of a package without overriding an entire phase. To have more information on them the place to go is the API docs of the :py:mod:`~.spack.build_systems`
have more information on them the place to go is the API docs of the :py:mod:`~.spack.build_systems` module.
module.
^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^
Overriding an entire phase Overriding an entire phase
^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^
In extreme cases it may be necessary to override an entire phase. Regardless Sometimes it is necessary to override an entire phase. If the ``package.py`` contains
of the build system, the signature is the same. For example, the signature a single class recipe, see :ref:`package_class_structure`, then the signature for a
for the install phase is: phase is:
.. code-block:: python .. code-block:: python
class Foo(Package): class Openjpeg(CMakePackage):
def install(self, spec, prefix): def install(self, spec, prefix):
... ...
regardless of the build system. The arguments for the phase are:
``self`` ``self``
For those not used to Python instance methods, this is the This is the package object, which extends ``CMakePackage``.
package itself. In this case it's an instance of ``Foo``, which For API docs on Package objects, see
extends ``Package``. For API docs on Package objects, see :py:class:`Package <spack.package_base.PackageBase>`.
:py:class:`Package <spack.package_base.Package>`.
``spec`` ``spec``
This is the concrete spec object created by Spack from an This is the concrete spec object created by Spack from an
@@ -3462,12 +3544,111 @@ for the install phase is:
The arguments ``spec`` and ``prefix`` are passed only for convenience, as they always The arguments ``spec`` and ``prefix`` are passed only for convenience, as they always
correspond to ``self.spec`` and ``self.spec.prefix`` respectively. correspond to ``self.spec`` and ``self.spec.prefix`` respectively.
As mentioned in :ref:`install-environment`, you will usually not need to refer If the ``package.py`` encodes builders explicitly, the signature for a phase changes slightly:
to dependencies explicitly in your package file, as the compiler wrappers take care of most of
the heavy lifting here. There will be times, though, when you need to refer to .. code-block:: python
the install locations of dependencies, or when you need to do something different
depending on the version, compiler, dependencies, etc. that your package is class CMakeBuilder(spack.build_systems.cmake.CMakeBuilder):
built with. These parameters give you access to this type of information. def install(self, pkg, spec, prefix):
...
In this case the package is passed as the second argument, and ``self`` is the builder instance.
.. _multiple_build_systems:
^^^^^^^^^^^^^^^^^^^^^^
Multiple build systems
^^^^^^^^^^^^^^^^^^^^^^
There are cases where a software actively supports two build systems, or changes build systems
as it evolves, or needs different build systems on different platforms. Spack allows dealing with
these cases natively, if a recipe is written using builders explicitly.
For instance, software that supports two build systems unconditionally should derive from
both ``*Package`` base classes, and declare the possible use of multiple build systems using
a directive:
.. code-block:: python
class ArpackNg(CMakePackage, AutotoolsPackage):
build_system("cmake", "autotools", default="cmake")
In this case the software can be built with both ``autotools`` and ``cmake``. Since the package
supports multiple build systems, it is necessary to declare which one is the default. The ``package.py``
will likely contain some overriding of default builder methods:
.. code-block:: python
class CMakeBuilder(spack.build_systems.cmake.CMakeBuilder):
def cmake_args(self):
pass
class Autotoolsbuilder(spack.build_systems.autotools.AutotoolsBuilder):
def configure_args(self):
pass
In more complex cases it might happen that the build system changes according to certain conditions,
for instance across versions. That can be expressed with conditional variant values:
.. code-block:: python
class ArpackNg(CMakePackage, AutotoolsPackage):
build_system(
conditional("cmake", when="@0.64:"),
conditional("autotools", when="@:0.63"),
default="cmake",
)
In the example the directive impose a change from ``Autotools`` to ``CMake`` going
from ``v0.63`` to ``v0.64``.
^^^^^^^^^^^^^^^^^^
Mixin base classes
^^^^^^^^^^^^^^^^^^
Besides build systems, there are other cases where common metadata and behavior can be extracted
and reused by many packages. For instance, packages that depend on ``Cuda`` or ``Rocm``, share
common dependencies and constraints. To factor these attributes into a single place, Spack provides
a few mixin classes in the ``spack.build_systems`` module:
+---------------------------------------------------------------+----------------------------------+
| **API docs** | **Description** |
+===============================================================+==================================+
| :class:`~spack.build_systems.cuda.CudaPackage` | A helper class for packages that |
| | use CUDA |
+---------------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.rocm.ROCmPackage` | A helper class for packages that |
| | use ROCm |
+---------------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.gnu.GNUMirrorPackage` | A helper class for GNU packages |
+---------------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.python.PythonExtension` | A helper class for Python |
| | extensions |
+---------------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.sourceforge.SourceforgePackage` | A helper class for packages |
| | from sourceforge.org |
+---------------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.sourceware.SourcewarePackage` | A helper class for packages |
| | from sourceware.org |
+---------------------------------------------------------------+----------------------------------+
| :class:`~spack.build_systems.xorg.XorgPackage` | A helper class for x.org |
| | packages |
+---------------------------------------------------------------+----------------------------------+
These classes should be used by adding them to the inheritance tree of the package that needs them,
for instance:
.. code-block:: python
class Cp2k(MakefilePackage, CudaPackage):
"""CP2K is a quantum chemistry and solid state physics software package
that can perform atomistic simulations of solid state, liquid, molecular,
periodic, material, crystal, and biological systems
"""
In the example above ``Cp2k`` inherits all the conflicts and variants that ``CudaPackage`` defines.
.. _install-environment: .. _install-environment:
@@ -6116,3 +6297,82 @@ might write:
DWARF_PREFIX = $(spack location --install-dir libdwarf) DWARF_PREFIX = $(spack location --install-dir libdwarf)
CXXFLAGS += -I$DWARF_PREFIX/include CXXFLAGS += -I$DWARF_PREFIX/include
CXXFLAGS += -L$DWARF_PREFIX/lib CXXFLAGS += -L$DWARF_PREFIX/lib
.. _package_class_structure:
--------------------------
Package class architecture
--------------------------
.. note::
This section aims to provide a high-level knowledge of how the package class architecture evolved
in Spack, and provides some insights on the current design.
Packages in Spack were originally designed to support only a single build system. The overall
class structure for a package looked like:
.. image:: images/original_package_architecture.png
:scale: 60 %
:align: center
In this architecture the base class ``AutotoolsPackage`` was responsible for both the metadata
related to the ``autotools`` build system (e.g. dependencies or variants common to all packages
using it), and for encoding the default installation procedure.
In reality, a non-negligible number of packages are either changing their build system during the evolution of the
project, or using different build systems for different platforms. An architecture based on a single class
requires hacks or other workarounds to deal with these cases.
To support a model more adherent to reality, Spack v0.19 changed its internal design by extracting
the attributes and methods related to building a software into a separate hierarchy:
.. image:: images/builder_package_architecture.png
:scale: 60 %
:align: center
In this new format each ``package.py`` contains one ``*Package`` class that gathers all the metadata,
and one or more ``*Builder`` classes that encode the installation procedure. A specific builder object
is created just before the software is built, so at a time where Spack knows which build system needs
to be used for the current installation, and receives a ``package`` object during initialization.
^^^^^^^^^^^^^^^^^^^^^^^^
``build_system`` variant
^^^^^^^^^^^^^^^^^^^^^^^^
To allow imposing conditions based on the build system, each package must a have ``build_system`` variant,
which is usually inherited from base classes. This variant allows for writing metadata that is conditional
on the build system:
.. code-block:: python
with when("build_system=cmake"):
depends_on("cmake", type="build")
and also for selecting a specific build system from a spec literal, like in the following command:
.. code-block:: console
$ spack install arpack-ng build_system=autotools
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Compatibility with single-class format
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Internally, Spack always uses builders to perform operations related to the installation of a specific software.
The builders are created in the ``spack.builder.create`` function
.. literalinclude:: _spack_root/lib/spack/spack/builder.py
:pyobject: create
To achieve backward compatibility with the single-class format Spack creates in this function a special
"adapter builder", if no custom builder is detected in the recipe:
.. image:: images/adapter.png
:scale: 60 %
:align: center
Overall the role of the adapter is to route access to attributes of methods first through the ``*Package``
hierarchy, and then back to the base class builder. This is schematically shown in the diagram above, where
the adapter role is to "emulate" a method resolution order like the one represented by the red arrows.

View File

@@ -503,6 +503,33 @@ def invalid_sha256_digest(fetcher):
return errors return errors
@package_properties
def _ensure_env_methods_are_ported_to_builders(pkgs, error_cls):
"""Ensure that methods modifying the build environment are ported to builder classes."""
errors = []
for pkg_name in pkgs:
pkg_cls = spack.repo.path.get_pkg_class(pkg_name)
buildsystem_variant, _ = pkg_cls.variants["build_system"]
buildsystem_names = [getattr(x, "value", x) for x in buildsystem_variant.values]
builder_cls_names = [spack.builder.BUILDER_CLS[x].__name__ for x in buildsystem_names]
module = pkg_cls.module
has_builders_in_package_py = any(
getattr(module, name, False) for name in builder_cls_names
)
if not has_builders_in_package_py:
continue
for method_name in ("setup_build_environment", "setup_dependent_build_environment"):
if hasattr(pkg_cls, method_name):
msg = (
"Package '{}' need to move the '{}' method from the package class to the"
" appropriate builder class".format(pkg_name, method_name)
)
errors.append(error_cls(msg, []))
return errors
@package_https_directives @package_https_directives
def _linting_package_file(pkgs, error_cls): def _linting_package_file(pkgs, error_cls):
"""Check for correctness of links""" """Check for correctness of links"""
@@ -660,7 +687,13 @@ def _ensure_variant_defaults_are_parsable(pkgs, error_cls):
errors.append(error_cls(error_msg.format(variant_name, pkg_name), [])) errors.append(error_cls(error_msg.format(variant_name, pkg_name), []))
continue continue
vspec = variant.make_default() try:
vspec = variant.make_default()
except spack.variant.MultipleValuesInExclusiveVariantError:
error_msg = "Cannot create a default value for the variant '{}' in package '{}'"
errors.append(error_cls(error_msg.format(variant_name, pkg_name), []))
continue
try: try:
variant.validate_or_raise(vspec, pkg_cls=pkg_cls) variant.validate_or_raise(vspec, pkg_cls=pkg_cls)
except spack.variant.InvalidVariantValueError: except spack.variant.InvalidVariantValueError:

View File

@@ -52,6 +52,7 @@
import spack.build_systems.cmake import spack.build_systems.cmake
import spack.build_systems.meson import spack.build_systems.meson
import spack.builder
import spack.config import spack.config
import spack.install_test import spack.install_test
import spack.main import spack.main
@@ -558,9 +559,9 @@ def _set_variables_for_single_module(pkg, module):
if sys.platform == "win32": if sys.platform == "win32":
m.nmake = Executable("nmake") m.nmake = Executable("nmake")
# Standard CMake arguments # Standard CMake arguments
m.std_cmake_args = spack.build_systems.cmake.CMakePackage._std_args(pkg) m.std_cmake_args = spack.build_systems.cmake.CMakeBuilder.std_args(pkg)
m.std_meson_args = spack.build_systems.meson.MesonPackage._std_args(pkg) m.std_meson_args = spack.build_systems.meson.MesonBuilder.std_args(pkg)
m.std_pip_args = spack.build_systems.python.PythonPackage._std_args(pkg) m.std_pip_args = spack.build_systems.python.PythonPipBuilder.std_args(pkg)
# Put spack compiler paths in module scope. # Put spack compiler paths in module scope.
link_dir = spack.paths.build_env_path link_dir = spack.paths.build_env_path
@@ -727,38 +728,6 @@ def get_rpaths(pkg):
return list(dedupe(filter_system_paths(rpaths))) return list(dedupe(filter_system_paths(rpaths)))
def get_std_cmake_args(pkg):
"""List of standard arguments used if a package is a CMakePackage.
Returns:
list: standard arguments that would be used if this
package were a CMakePackage instance.
Args:
pkg (spack.package_base.PackageBase): package under consideration
Returns:
list: arguments for cmake
"""
return spack.build_systems.cmake.CMakePackage._std_args(pkg)
def get_std_meson_args(pkg):
"""List of standard arguments used if a package is a MesonPackage.
Returns:
list: standard arguments that would be used if this
package were a MesonPackage instance.
Args:
pkg (spack.package_base.PackageBase): package under consideration
Returns:
list: arguments for meson
"""
return spack.build_systems.meson.MesonPackage._std_args(pkg)
def parent_class_modules(cls): def parent_class_modules(cls):
""" """
Get list of superclass modules that descend from spack.package_base.PackageBase Get list of superclass modules that descend from spack.package_base.PackageBase
@@ -819,7 +788,8 @@ def setup_package(pkg, dirty, context="build"):
platform.setup_platform_environment(pkg, env_mods) platform.setup_platform_environment(pkg, env_mods)
if context == "build": if context == "build":
pkg.setup_build_environment(env_mods) builder = spack.builder.create(pkg)
builder.setup_build_environment(env_mods)
if (not dirty) and (not env_mods.is_unset("CPATH")): if (not dirty) and (not env_mods.is_unset("CPATH")):
tty.debug( tty.debug(
@@ -1015,7 +985,8 @@ def add_modifications_for_dep(dep):
module.__dict__.update(changes.__dict__) module.__dict__.update(changes.__dict__)
if context == "build": if context == "build":
dpkg.setup_dependent_build_environment(env, spec) builder = spack.builder.create(dpkg)
builder.setup_dependent_build_environment(env, spec)
else: else:
dpkg.setup_dependent_run_environment(env, spec) dpkg.setup_dependent_run_environment(env, spec)
@@ -1117,8 +1088,20 @@ def _setup_pkg_and_run(
pkg.test_suite.stage, spack.install_test.TestSuite.test_log_name(pkg.spec) pkg.test_suite.stage, spack.install_test.TestSuite.test_log_name(pkg.spec)
) )
error_msg = str(exc)
if isinstance(exc, (spack.multimethod.NoSuchMethodError, AttributeError)):
error_msg = (
"The '{}' package cannot find an attribute while trying to build "
"from sources. This might be due to a change in Spack's package format "
"to support multiple build-systems for a single package. You can fix this "
"by updating the build recipe, and you can also report the issue as a bug. "
"More information at https://spack.readthedocs.io/en/latest/packaging_guide.html#installation-procedure"
).format(pkg.name)
error_msg = colorize("@*R{{{}}}".format(error_msg))
error_msg = "{}\n\n{}".format(str(exc), error_msg)
# make a pickleable exception to send to parent. # make a pickleable exception to send to parent.
msg = "%s: %s" % (exc_type.__name__, str(exc)) msg = "%s: %s" % (exc_type.__name__, error_msg)
ce = ChildError( ce = ChildError(
msg, msg,

View File

@@ -0,0 +1,124 @@
# Copyright 2013-2022 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
import six
import llnl.util.lang
import spack.builder
import spack.installer
import spack.relocate
import spack.store
def sanity_check_prefix(builder):
"""Check that specific directories and files are created after installation.
The files to be checked are in the ``sanity_check_is_file`` attribute of the
package object, while the directories are in the ``sanity_check_is_dir``.
Args:
builder (spack.builder.Builder): builder that installed the package
"""
pkg = builder.pkg
def check_paths(path_list, filetype, predicate):
if isinstance(path_list, six.string_types):
path_list = [path_list]
for path in path_list:
abs_path = os.path.join(pkg.prefix, path)
if not predicate(abs_path):
msg = "Install failed for {0}. No such {1} in prefix: {2}"
msg = msg.format(pkg.name, filetype, path)
raise spack.installer.InstallError(msg)
check_paths(pkg.sanity_check_is_file, "file", os.path.isfile)
check_paths(pkg.sanity_check_is_dir, "directory", os.path.isdir)
ignore_file = llnl.util.lang.match_predicate(spack.store.layout.hidden_file_regexes)
if all(map(ignore_file, os.listdir(pkg.prefix))):
msg = "Install failed for {0}. Nothing was installed!"
raise spack.installer.InstallError(msg.format(pkg.name))
def apply_macos_rpath_fixups(builder):
"""On Darwin, make installed libraries more easily relocatable.
Some build systems (handrolled, autotools, makefiles) can set their own
rpaths that are duplicated by spack's compiler wrapper. This fixup
interrogates, and postprocesses if necessary, all libraries installed
by the code.
It should be added as a @run_after to packaging systems (or individual
packages) that do not install relocatable libraries by default.
Args:
builder (spack.builder.Builder): builder that installed the package
"""
spack.relocate.fixup_macos_rpaths(builder.spec)
def ensure_build_dependencies_or_raise(spec, dependencies, error_msg):
"""Ensure that some build dependencies are present in the concrete spec.
If not, raise a RuntimeError with a helpful error message.
Args:
spec (spack.spec.Spec): concrete spec to be checked.
dependencies (list of spack.spec.Spec): list of abstract specs to be satisfied
error_msg (str): brief error message to be prepended to a longer description
Raises:
RuntimeError: when the required build dependencies are not found
"""
assert spec.concrete, "Can ensure build dependencies only on concrete specs"
build_deps = [d.name for d in spec.dependencies(deptype="build")]
missing_deps = [x for x in dependencies if x not in build_deps]
if not missing_deps:
return
# Raise an exception on missing deps.
msg = (
"{0}: missing dependencies: {1}.\n\nPlease add "
"the following lines to the package:\n\n".format(error_msg, ", ".join(missing_deps))
)
for dep in missing_deps:
msg += " depends_on('{0}', type='build', when='@{1} {2}')\n".format(
dep, spec.version, "build_system=autotools"
)
msg += "\nUpdate the version (when='@{0}') as needed.".format(spec.version)
raise RuntimeError(msg)
def execute_build_time_tests(builder):
"""Execute the build-time tests prescribed by builder.
Args:
builder (Builder): builder prescribing the test callbacks. The name of the callbacks is
stored as a list of strings in the ``build_time_test_callbacks`` attribute.
"""
builder.pkg.run_test_callbacks(builder, builder.build_time_test_callbacks, "build")
def execute_install_time_tests(builder):
"""Execute the install-time tests prescribed by builder.
Args:
builder (Builder): builder prescribing the test callbacks. The name of the callbacks is
stored as a list of strings in the ``install_time_test_callbacks`` attribute.
"""
builder.pkg.run_test_callbacks(builder, builder.install_time_test_callbacks, "install")
class BaseBuilder(spack.builder.Builder):
"""Base class for builders to register common checks"""
# Check that self.prefix is there after installation
spack.builder.run_after("install")(sanity_check_prefix)

View File

@@ -2,18 +2,36 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import llnl.util.filesystem as fs
# Why doesn't this work for me? import spack.directives
# from spack import * import spack.package_base
from llnl.util.filesystem import filter_file import spack.util.executable
from spack.build_systems.autotools import AutotoolsPackage from .autotools import AutotoolsBuilder, AutotoolsPackage
from spack.directives import extends
from spack.package_base import ExtensionError
from spack.util.executable import which class AspellBuilder(AutotoolsBuilder):
"""The Aspell builder is close enough to an autotools builder to allow
specializing the builder class, so to use variables that are specific
to the Aspell extensions.
"""
def configure(self, pkg, spec, prefix):
aspell = spec["aspell"].prefix.bin.aspell
prezip = spec["aspell"].prefix.bin.prezip
destdir = prefix
sh = spack.util.executable.which("sh")
sh(
"./configure",
"--vars",
"ASPELL={0}".format(aspell),
"PREZIP={0}".format(prezip),
"DESTDIR={0}".format(destdir),
)
#
# Aspell dictionaries install their bits into their prefix.lib # Aspell dictionaries install their bits into their prefix.lib
# and when activated they'll get symlinked into the appropriate aspell's # and when activated they'll get symlinked into the appropriate aspell's
# dict dir (see aspell's {de,}activate methods). # dict dir (see aspell's {de,}activate methods).
@@ -23,12 +41,17 @@
class AspellDictPackage(AutotoolsPackage): class AspellDictPackage(AutotoolsPackage):
"""Specialized class for building aspell dictionairies.""" """Specialized class for building aspell dictionairies."""
extends("aspell") spack.directives.extends("aspell", when="build_system=autotools")
#: Override the default autotools builder
AutotoolsBuilder = AspellBuilder
def view_destination(self, view): def view_destination(self, view):
aspell_spec = self.spec["aspell"] aspell_spec = self.spec["aspell"]
if view.get_projection_for_spec(aspell_spec) != aspell_spec.prefix: if view.get_projection_for_spec(aspell_spec) != aspell_spec.prefix:
raise ExtensionError("aspell does not support non-global extensions") raise spack.package_base.ExtensionError(
"aspell does not support non-global extensions"
)
aspell = aspell_spec.command aspell = aspell_spec.command
return aspell("dump", "config", "dict-dir", output=str).strip() return aspell("dump", "config", "dict-dir", output=str).strip()
@@ -36,19 +59,5 @@ def view_source(self):
return self.prefix.lib return self.prefix.lib
def patch(self): def patch(self):
filter_file(r"^dictdir=.*$", "dictdir=/lib", "configure") fs.filter_file(r"^dictdir=.*$", "dictdir=/lib", "configure")
filter_file(r"^datadir=.*$", "datadir=/lib", "configure") fs.filter_file(r"^datadir=.*$", "datadir=/lib", "configure")
def configure(self, spec, prefix):
aspell = spec["aspell"].prefix.bin.aspell
prezip = spec["aspell"].prefix.bin.prezip
destdir = prefix
sh = which("sh")
sh(
"./configure",
"--vars",
"ASPELL={0}".format(aspell),
"PREZIP={0}".format(prezip),
"DESTDIR={0}".format(destdir),
)

View File

@@ -6,87 +6,140 @@
import os import os
import os.path import os.path
import stat import stat
from subprocess import PIPE, check_call import subprocess
from typing import List # novm from typing import List # novm
import llnl.util.filesystem as fs import llnl.util.filesystem as fs
import llnl.util.tty as tty import llnl.util.tty as tty
from llnl.util.filesystem import force_remove, working_dir
from spack.build_environment import InstallError import spack.build_environment
from spack.directives import conflicts, depends_on import spack.builder
import spack.package_base
from spack.directives import build_system, conflicts, depends_on
from spack.multimethod import when
from spack.operating_systems.mac_os import macos_version from spack.operating_systems.mac_os import macos_version
from spack.package_base import PackageBase, run_after, run_before
from spack.util.executable import Executable from spack.util.executable import Executable
from spack.version import Version from spack.version import Version
from ._checks import (
BaseBuilder,
apply_macos_rpath_fixups,
ensure_build_dependencies_or_raise,
execute_build_time_tests,
execute_install_time_tests,
)
class AutotoolsPackage(PackageBase):
"""Specialized class for packages built using GNU Autotools.
This class provides four phases that can be overridden: class AutotoolsPackage(spack.package_base.PackageBase):
"""Specialized class for packages built using GNU Autotools."""
1. :py:meth:`~.AutotoolsPackage.autoreconf` #: This attribute is used in UI queries that need to know the build
2. :py:meth:`~.AutotoolsPackage.configure` #: system base class
3. :py:meth:`~.AutotoolsPackage.build` build_system_class = "AutotoolsPackage"
4. :py:meth:`~.AutotoolsPackage.install`
#: Legacy buildsystem attribute used to deserialize and install old specs
legacy_buildsystem = "autotools"
build_system("autotools")
with when("build_system=autotools"):
depends_on("gnuconfig", type="build", when="target=ppc64le:")
depends_on("gnuconfig", type="build", when="target=aarch64:")
depends_on("gnuconfig", type="build", when="target=riscv64:")
conflicts("platform=windows")
def flags_to_build_system_args(self, flags):
"""Produces a list of all command line arguments to pass specified
compiler flags to configure."""
# Has to be dynamic attribute due to caching.
setattr(self, "configure_flag_args", [])
for flag, values in flags.items():
if values:
values_str = "{0}={1}".format(flag.upper(), " ".join(values))
self.configure_flag_args.append(values_str)
# Spack's fflags are meant for both F77 and FC, therefore we
# additionaly set FCFLAGS if required.
values = flags.get("fflags", None)
if values:
values_str = "FCFLAGS={0}".format(" ".join(values))
self.configure_flag_args.append(values_str)
# Legacy methods (used by too many packages to change them,
# need to forward to the builder)
def enable_or_disable(self, *args, **kwargs):
return self.builder.enable_or_disable(*args, **kwargs)
def with_or_without(self, *args, **kwargs):
return self.builder.with_or_without(*args, **kwargs)
@spack.builder.builder("autotools")
class AutotoolsBuilder(BaseBuilder):
"""The autotools builder encodes the default way of installing software built
with autotools. It has four phases that can be overridden, if need be:
1. :py:meth:`~.AutotoolsBuilder.autoreconf`
2. :py:meth:`~.AutotoolsBuilder.configure`
3. :py:meth:`~.AutotoolsBuilder.build`
4. :py:meth:`~.AutotoolsBuilder.install`
They all have sensible defaults and for many packages the only thing necessary
is to override the helper method
:meth:`~spack.build_systems.autotools.AutotoolsBuilder.configure_args`.
They all have sensible defaults and for many packages the only thing
necessary will be to override the helper method
:meth:`~spack.build_systems.autotools.AutotoolsPackage.configure_args`.
For a finer tuning you may also override: For a finer tuning you may also override:
+-----------------------------------------------+--------------------+ +-----------------------------------------------+--------------------+
| **Method** | **Purpose** | | **Method** | **Purpose** |
+===============================================+====================+ +===============================================+====================+
| :py:attr:`~.AutotoolsPackage.build_targets` | Specify ``make`` | | :py:attr:`~.AutotoolsBuilder.build_targets` | Specify ``make`` |
| | targets for the | | | targets for the |
| | build phase | | | build phase |
+-----------------------------------------------+--------------------+ +-----------------------------------------------+--------------------+
| :py:attr:`~.AutotoolsPackage.install_targets` | Specify ``make`` | | :py:attr:`~.AutotoolsBuilder.install_targets` | Specify ``make`` |
| | targets for the | | | targets for the |
| | install phase | | | install phase |
+-----------------------------------------------+--------------------+ +-----------------------------------------------+--------------------+
| :py:meth:`~.AutotoolsPackage.check` | Run build time | | :py:meth:`~.AutotoolsBuilder.check` | Run build time |
| | tests if required | | | tests if required |
+-----------------------------------------------+--------------------+ +-----------------------------------------------+--------------------+
""" """
#: Phases of a GNU Autotools package #: Phases of a GNU Autotools package
phases = ["autoreconf", "configure", "build", "install"] phases = ("autoreconf", "configure", "build", "install")
#: This attribute is used in UI queries that need to know the build
#: system base class
build_system_class = "AutotoolsPackage"
@property #: Names associated with package methods in the old build-system format
def patch_config_files(self): legacy_methods = (
""" "configure_args",
Whether or not to update old ``config.guess`` and ``config.sub`` files "check",
distributed with the tarball. This currently only applies to "installcheck",
``ppc64le:``, ``aarch64:``, and ``riscv64`` target architectures. The )
substitutes are taken from the ``gnuconfig`` package, which is
automatically added as a build dependency for these architectures. In
case system versions of these config files are required, the
``gnuconfig`` package can be marked external with a prefix pointing to
the directory containing the system ``config.guess`` and ``config.sub``
files.
"""
return (
self.spec.satisfies("target=ppc64le:")
or self.spec.satisfies("target=aarch64:")
or self.spec.satisfies("target=riscv64:")
)
#: Whether or not to update ``libtool`` #: Names associated with package attributes in the old build-system format
#: (currently only for Arm/Clang/Fujitsu/NVHPC compilers) legacy_attributes = (
"archive_files",
"patch_libtool",
"build_targets",
"install_targets",
"build_time_test_callbacks",
"install_time_test_callbacks",
"force_autoreconf",
"autoreconf_extra_args",
"install_libtool_archives",
"patch_config_files",
"configure_directory",
"configure_abs_path",
"build_directory",
"autoreconf_search_path_args",
)
#: Whether to update ``libtool`` (e.g. for Arm/Clang/Fujitsu/NVHPC compilers)
patch_libtool = True patch_libtool = True
#: Targets for ``make`` during the :py:meth:`~.AutotoolsPackage.build` #: Targets for ``make`` during the :py:meth:`~.AutotoolsBuilder.build` phase
#: phase
build_targets = [] # type: List[str] build_targets = [] # type: List[str]
#: Targets for ``make`` during the :py:meth:`~.AutotoolsPackage.install` #: Targets for ``make`` during the :py:meth:`~.AutotoolsBuilder.install` phase
#: phase
install_targets = ["install"] install_targets = ["install"]
#: Callback names for build-time test #: Callback names for build-time test
@@ -97,24 +150,40 @@ def patch_config_files(self):
#: Set to true to force the autoreconf step even if configure is present #: Set to true to force the autoreconf step even if configure is present
force_autoreconf = False force_autoreconf = False
#: Options to be passed to autoreconf when using the default implementation #: Options to be passed to autoreconf when using the default implementation
autoreconf_extra_args = [] # type: List[str] autoreconf_extra_args = [] # type: List[str]
#: If False deletes all the .la files in the prefix folder #: If False deletes all the .la files in the prefix folder after the installation.
#: after the installation. If True instead it installs them. #: If True instead it installs them.
install_libtool_archives = False install_libtool_archives = False
depends_on("gnuconfig", type="build", when="target=ppc64le:") @property
depends_on("gnuconfig", type="build", when="target=aarch64:") def patch_config_files(self):
depends_on("gnuconfig", type="build", when="target=riscv64:") """Whether to update old ``config.guess`` and ``config.sub`` files
conflicts("platform=windows") distributed with the tarball.
This currently only applies to ``ppc64le:``, ``aarch64:``, and
``riscv64`` target architectures.
The substitutes are taken from the ``gnuconfig`` package, which is
automatically added as a build dependency for these architectures. In case
system versions of these config files are required, the ``gnuconfig`` package
can be marked external, with a prefix pointing to the directory containing the
system ``config.guess`` and ``config.sub`` files.
"""
return (
self.pkg.spec.satisfies("target=ppc64le:")
or self.pkg.spec.satisfies("target=aarch64:")
or self.pkg.spec.satisfies("target=riscv64:")
)
@property @property
def _removed_la_files_log(self): def _removed_la_files_log(self):
"""File containing the list of remove libtool archives""" """File containing the list of removed libtool archives"""
build_dir = self.build_directory build_dir = self.build_directory
if not os.path.isabs(self.build_directory): if not os.path.isabs(self.build_directory):
build_dir = os.path.join(self.stage.path, build_dir) build_dir = os.path.join(self.pkg.stage.path, build_dir)
return os.path.join(build_dir, "removed_la_files.txt") return os.path.join(build_dir, "removed_la_files.txt")
@property @property
@@ -125,13 +194,13 @@ def archive_files(self):
files.append(self._removed_la_files_log) files.append(self._removed_la_files_log)
return files return files
@run_after("autoreconf") @spack.builder.run_after("autoreconf")
def _do_patch_config_files(self): def _do_patch_config_files(self):
"""Some packages ship with older config.guess/config.sub files and """Some packages ship with older config.guess/config.sub files and need to
need to have these updated when installed on a newer architecture. have these updated when installed on a newer architecture.
In particular, config.guess fails for PPC64LE for version prior
to a 2013-06-10 build date (automake 1.13.4) and for ARM (aarch64) and In particular, config.guess fails for PPC64LE for version prior to a
RISC-V (riscv64). 2013-06-10 build date (automake 1.13.4) and for AArch64 and RISC-V.
""" """
if not self.patch_config_files: if not self.patch_config_files:
return return
@@ -139,11 +208,11 @@ def _do_patch_config_files(self):
# TODO: Expand this to select the 'config.sub'-compatible architecture # TODO: Expand this to select the 'config.sub'-compatible architecture
# for each platform (e.g. 'config.sub' doesn't accept 'power9le', but # for each platform (e.g. 'config.sub' doesn't accept 'power9le', but
# does accept 'ppc64le'). # does accept 'ppc64le').
if self.spec.satisfies("target=ppc64le:"): if self.pkg.spec.satisfies("target=ppc64le:"):
config_arch = "ppc64le" config_arch = "ppc64le"
elif self.spec.satisfies("target=aarch64:"): elif self.pkg.spec.satisfies("target=aarch64:"):
config_arch = "aarch64" config_arch = "aarch64"
elif self.spec.satisfies("target=riscv64:"): elif self.pkg.spec.satisfies("target=riscv64:"):
config_arch = "riscv64" config_arch = "riscv64"
else: else:
config_arch = "local" config_arch = "local"
@@ -155,7 +224,7 @@ def runs_ok(script_abs_path):
args = [script_abs_path] + additional_args.get(script_name, []) args = [script_abs_path] + additional_args.get(script_name, [])
try: try:
check_call(args, stdout=PIPE, stderr=PIPE) subprocess.check_call(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except Exception as e: except Exception as e:
tty.debug(e) tty.debug(e)
return False return False
@@ -163,7 +232,7 @@ def runs_ok(script_abs_path):
return True return True
# Get the list of files that needs to be patched # Get the list of files that needs to be patched
to_be_patched = fs.find(self.stage.path, files=["config.sub", "config.guess"]) to_be_patched = fs.find(self.pkg.stage.path, files=["config.sub", "config.guess"])
to_be_patched = [f for f in to_be_patched if not runs_ok(f)] to_be_patched = [f for f in to_be_patched if not runs_ok(f)]
# If there are no files to be patched, return early # If there are no files to be patched, return early
@@ -171,22 +240,21 @@ def runs_ok(script_abs_path):
return return
# Otherwise, require `gnuconfig` to be a build dependency # Otherwise, require `gnuconfig` to be a build dependency
self._require_build_deps( ensure_build_dependencies_or_raise(
pkgs=["gnuconfig"], spec=self.spec, err="Cannot patch config files" spec=self.pkg.spec, dependencies=["gnuconfig"], error_msg="Cannot patch config files"
) )
# Get the config files we need to patch (config.sub / config.guess). # Get the config files we need to patch (config.sub / config.guess).
to_be_found = list(set(os.path.basename(f) for f in to_be_patched)) to_be_found = list(set(os.path.basename(f) for f in to_be_patched))
gnuconfig = self.spec["gnuconfig"] gnuconfig = self.pkg.spec["gnuconfig"]
gnuconfig_dir = gnuconfig.prefix gnuconfig_dir = gnuconfig.prefix
# An external gnuconfig may not not have a prefix. # An external gnuconfig may not not have a prefix.
if gnuconfig_dir is None: if gnuconfig_dir is None:
raise InstallError( raise spack.build_environment.InstallError(
"Spack could not find substitutes for GNU config " "Spack could not find substitutes for GNU config files because no "
"files because no prefix is available for the " "prefix is available for the `gnuconfig` package. Make sure you set a "
"`gnuconfig` package. Make sure you set a prefix " "prefix path instead of modules for external `gnuconfig`."
"path instead of modules for external `gnuconfig`."
) )
candidates = fs.find(gnuconfig_dir, files=to_be_found, recursive=False) candidates = fs.find(gnuconfig_dir, files=to_be_found, recursive=False)
@@ -203,7 +271,7 @@ def runs_ok(script_abs_path):
msg += ( msg += (
" or the `gnuconfig` package prefix is misconfigured as" " an external package" " or the `gnuconfig` package prefix is misconfigured as" " an external package"
) )
raise InstallError(msg) raise spack.build_environment.InstallError(msg)
# Filter working substitutes # Filter working substitutes
candidates = [f for f in candidates if runs_ok(f)] candidates = [f for f in candidates if runs_ok(f)]
@@ -228,7 +296,9 @@ def runs_ok(script_abs_path):
and set the prefix to the directory containing the `config.guess` and and set the prefix to the directory containing the `config.guess` and
`config.sub` files. `config.sub` files.
""" """
raise InstallError(msg.format(", ".join(to_be_found), self.name)) raise spack.build_environment.InstallError(
msg.format(", ".join(to_be_found), self.name)
)
# Copy the good files over the bad ones # Copy the good files over the bad ones
for abs_path in to_be_patched: for abs_path in to_be_patched:
@@ -238,7 +308,7 @@ def runs_ok(script_abs_path):
fs.copy(substitutes[name], abs_path) fs.copy(substitutes[name], abs_path)
os.chmod(abs_path, mode) os.chmod(abs_path, mode)
@run_before("configure") @spack.builder.run_before("configure")
def _patch_usr_bin_file(self): def _patch_usr_bin_file(self):
"""On NixOS file is not available in /usr/bin/file. Patch configure """On NixOS file is not available in /usr/bin/file. Patch configure
scripts to use file from path.""" scripts to use file from path."""
@@ -250,7 +320,7 @@ def _patch_usr_bin_file(self):
with fs.keep_modification_time(*x.filenames): with fs.keep_modification_time(*x.filenames):
x.filter(regex="/usr/bin/file", repl="file", string=True) x.filter(regex="/usr/bin/file", repl="file", string=True)
@run_before("configure") @spack.builder.run_before("configure")
def _set_autotools_environment_variables(self): def _set_autotools_environment_variables(self):
"""Many autotools builds use a version of mknod.m4 that fails when """Many autotools builds use a version of mknod.m4 that fails when
running as root unless FORCE_UNSAFE_CONFIGURE is set to 1. running as root unless FORCE_UNSAFE_CONFIGURE is set to 1.
@@ -261,11 +331,10 @@ def _set_autotools_environment_variables(self):
Without it, configure just fails halfway through, but it can Without it, configure just fails halfway through, but it can
still run things *before* this check. Forcing this just removes a still run things *before* this check. Forcing this just removes a
nuisance -- this is not circumventing any real protection. nuisance -- this is not circumventing any real protection.
""" """
os.environ["FORCE_UNSAFE_CONFIGURE"] = "1" os.environ["FORCE_UNSAFE_CONFIGURE"] = "1"
@run_before("configure") @spack.builder.run_before("configure")
def _do_patch_libtool_configure(self): def _do_patch_libtool_configure(self):
"""Patch bugs that propagate from libtool macros into "configure" and """Patch bugs that propagate from libtool macros into "configure" and
further into "libtool". Note that patches that can be fixed by patching further into "libtool". Note that patches that can be fixed by patching
@@ -293,7 +362,7 @@ def _do_patch_libtool_configure(self):
# Support Libtool 2.4.2 and older: # Support Libtool 2.4.2 and older:
x.filter(regex=r'^(\s*test \$p = "-R")(; then\s*)$', repl=r'\1 || test x-l = x"$p"\2') x.filter(regex=r'^(\s*test \$p = "-R")(; then\s*)$', repl=r'\1 || test x-l = x"$p"\2')
@run_after("configure") @spack.builder.run_after("configure")
def _do_patch_libtool(self): def _do_patch_libtool(self):
"""If configure generates a "libtool" script that does not correctly """If configure generates a "libtool" script that does not correctly
detect the compiler (and patch_libtool is set), patch in the correct detect the compiler (and patch_libtool is set), patch in the correct
@@ -328,31 +397,33 @@ def _do_patch_libtool(self):
markers[tag] = "LIBTOOL TAG CONFIG: {0}".format(tag.upper()) markers[tag] = "LIBTOOL TAG CONFIG: {0}".format(tag.upper())
# Replace empty linker flag prefixes: # Replace empty linker flag prefixes:
if self.compiler.name == "nag": if self.pkg.compiler.name == "nag":
# Nag is mixed with gcc and g++, which are recognized correctly. # Nag is mixed with gcc and g++, which are recognized correctly.
# Therefore, we change only Fortran values: # Therefore, we change only Fortran values:
for tag in ["fc", "f77"]: for tag in ["fc", "f77"]:
marker = markers[tag] marker = markers[tag]
x.filter( x.filter(
regex='^wl=""$', regex='^wl=""$',
repl='wl="{0}"'.format(self.compiler.linker_arg), repl='wl="{0}"'.format(self.pkg.compiler.linker_arg),
start_at="# ### BEGIN {0}".format(marker), start_at="# ### BEGIN {0}".format(marker),
stop_at="# ### END {0}".format(marker), stop_at="# ### END {0}".format(marker),
) )
else: else:
x.filter(regex='^wl=""$', repl='wl="{0}"'.format(self.compiler.linker_arg)) x.filter(regex='^wl=""$', repl='wl="{0}"'.format(self.pkg.compiler.linker_arg))
# Replace empty PIC flag values: # Replace empty PIC flag values:
for cc, marker in markers.items(): for cc, marker in markers.items():
x.filter( x.filter(
regex='^pic_flag=""$', regex='^pic_flag=""$',
repl='pic_flag="{0}"'.format(getattr(self.compiler, "{0}_pic_flag".format(cc))), repl='pic_flag="{0}"'.format(
getattr(self.pkg.compiler, "{0}_pic_flag".format(cc))
),
start_at="# ### BEGIN {0}".format(marker), start_at="# ### BEGIN {0}".format(marker),
stop_at="# ### END {0}".format(marker), stop_at="# ### END {0}".format(marker),
) )
# Other compiler-specific patches: # Other compiler-specific patches:
if self.compiler.name == "fj": if self.pkg.compiler.name == "fj":
x.filter(regex="-nostdlib", repl="", string=True) x.filter(regex="-nostdlib", repl="", string=True)
rehead = r"/\S*/" rehead = r"/\S*/"
for o in [ for o in [
@@ -365,12 +436,12 @@ def _do_patch_libtool(self):
"crtendS.o", "crtendS.o",
]: ]:
x.filter(regex=(rehead + o), repl="", string=True) x.filter(regex=(rehead + o), repl="", string=True)
elif self.compiler.name == "dpcpp": elif self.pkg.compiler.name == "dpcpp":
# Hack to filter out spurious predep_objects when building with Intel dpcpp # Hack to filter out spurious predep_objects when building with Intel dpcpp
# (see https://github.com/spack/spack/issues/32863): # (see https://github.com/spack/spack/issues/32863):
x.filter(regex=r"^(predep_objects=.*)/tmp/conftest-[0-9A-Fa-f]+\.o", repl=r"\1") x.filter(regex=r"^(predep_objects=.*)/tmp/conftest-[0-9A-Fa-f]+\.o", repl=r"\1")
x.filter(regex=r"^(predep_objects=.*)/tmp/a-[0-9A-Fa-f]+\.o", repl=r"\1") x.filter(regex=r"^(predep_objects=.*)/tmp/a-[0-9A-Fa-f]+\.o", repl=r"\1")
elif self.compiler.name == "nag": elif self.pkg.compiler.name == "nag":
for tag in ["fc", "f77"]: for tag in ["fc", "f77"]:
marker = markers[tag] marker = markers[tag]
start_at = "# ### BEGIN {0}".format(marker) start_at = "# ### BEGIN {0}".format(marker)
@@ -446,11 +517,8 @@ def _do_patch_libtool(self):
@property @property
def configure_directory(self): def configure_directory(self):
"""Returns the directory where 'configure' resides. """Return the directory where 'configure' resides."""
return self.pkg.stage.source_path
:return: directory where to find configure
"""
return self.stage.source_path
@property @property
def configure_abs_path(self): def configure_abs_path(self):
@@ -463,34 +531,12 @@ def build_directory(self):
"""Override to provide another place to build the package""" """Override to provide another place to build the package"""
return self.configure_directory return self.configure_directory
@run_before("autoreconf") @spack.builder.run_before("autoreconf")
def delete_configure_to_force_update(self): def delete_configure_to_force_update(self):
if self.force_autoreconf: if self.force_autoreconf:
force_remove(self.configure_abs_path) fs.force_remove(self.configure_abs_path)
def _require_build_deps(self, pkgs, spec, err): def autoreconf(self, pkg, spec, prefix):
"""Require `pkgs` to be direct build dependencies of `spec`. Raises a
RuntimeError with a helpful error messages when any dep is missing."""
build_deps = [d.name for d in spec.dependencies(deptype="build")]
missing_deps = [x for x in pkgs if x not in build_deps]
if not missing_deps:
return
# Raise an exception on missing deps.
msg = (
"{0}: missing dependencies: {1}.\n\nPlease add "
"the following lines to the package:\n\n".format(err, ", ".join(missing_deps))
)
for dep in missing_deps:
msg += " depends_on('{0}', type='build', when='@{1}')\n".format(dep, spec.version)
msg += "\nUpdate the version (when='@{0}') as needed.".format(spec.version)
raise RuntimeError(msg)
def autoreconf(self, spec, prefix):
"""Not needed usually, configure should be already there""" """Not needed usually, configure should be already there"""
# If configure exists nothing needs to be done # If configure exists nothing needs to be done
@@ -498,8 +544,10 @@ def autoreconf(self, spec, prefix):
return return
# Else try to regenerate it, which reuquires a few build dependencies # Else try to regenerate it, which reuquires a few build dependencies
self._require_build_deps( ensure_build_dependencies_or_raise(
pkgs=["autoconf", "automake", "libtool"], spec=spec, err="Cannot generate configure" spec=spec,
dependencies=["autoconf", "automake", "libtool"],
error_msg="Cannot generate configure",
) )
tty.msg("Configure script not found: trying to generate it") tty.msg("Configure script not found: trying to generate it")
@@ -507,8 +555,8 @@ def autoreconf(self, spec, prefix):
tty.warn("* If the default procedure fails, consider implementing *") tty.warn("* If the default procedure fails, consider implementing *")
tty.warn("* a custom AUTORECONF phase in the package *") tty.warn("* a custom AUTORECONF phase in the package *")
tty.warn("*********************************************************") tty.warn("*********************************************************")
with working_dir(self.configure_directory): with fs.working_dir(self.configure_directory):
m = inspect.getmodule(self) m = inspect.getmodule(self.pkg)
# This line is what is needed most of the time # This line is what is needed most of the time
# --install, --verbose, --force # --install, --verbose, --force
autoreconf_args = ["-ivf"] autoreconf_args = ["-ivf"]
@@ -524,98 +572,66 @@ def autoreconf_search_path_args(self):
spack dependencies.""" spack dependencies."""
return _autoreconf_search_path_args(self.spec) return _autoreconf_search_path_args(self.spec)
@run_after("autoreconf") @spack.builder.run_after("autoreconf")
def set_configure_or_die(self): def set_configure_or_die(self):
"""Checks the presence of a ``configure`` file after the """Ensure the presence of a "configure" script, or raise. If the "configure"
autoreconf phase. If it is found sets a module attribute is found, a module level attribute is set.
appropriately, otherwise raises an error.
:raises RuntimeError: if a configure script is not found in Raises:
:py:meth:`~AutotoolsPackage.configure_directory` RuntimeError: if the "configure" script is not found
""" """
# Check if a configure script is there. If not raise a RuntimeError. # Check if the "configure" script is there. If not raise a RuntimeError.
if not os.path.exists(self.configure_abs_path): if not os.path.exists(self.configure_abs_path):
msg = "configure script not found in {0}" msg = "configure script not found in {0}"
raise RuntimeError(msg.format(self.configure_directory)) raise RuntimeError(msg.format(self.configure_directory))
# Monkey-patch the configure script in the corresponding module # Monkey-patch the configure script in the corresponding module
inspect.getmodule(self).configure = Executable(self.configure_abs_path) inspect.getmodule(self.pkg).configure = Executable(self.configure_abs_path)
def configure_args(self): def configure_args(self):
"""Produces a list containing all the arguments that must be passed to """Return the list of all the arguments that must be passed to configure,
configure, except ``--prefix`` which will be pre-pended to the list. except ``--prefix`` which will be pre-pended to the list.
:return: list of arguments for configure
""" """
return [] return []
def flags_to_build_system_args(self, flags): def configure(self, pkg, spec, prefix):
"""Produces a list of all command line arguments to pass specified """Run "configure", with the arguments specified by the builder and an
compiler flags to configure.""" appropriately set prefix.
# Has to be dynamic attribute due to caching.
setattr(self, "configure_flag_args", [])
for flag, values in flags.items():
if values:
values_str = "{0}={1}".format(flag.upper(), " ".join(values))
self.configure_flag_args.append(values_str)
# Spack's fflags are meant for both F77 and FC, therefore we
# additionaly set FCFLAGS if required.
values = flags.get("fflags", None)
if values:
values_str = "FCFLAGS={0}".format(" ".join(values))
self.configure_flag_args.append(values_str)
def configure(self, spec, prefix):
"""Runs configure with the arguments specified in
:meth:`~spack.build_systems.autotools.AutotoolsPackage.configure_args`
and an appropriately set prefix.
""" """
options = getattr(self, "configure_flag_args", []) options = getattr(self.pkg, "configure_flag_args", [])
options += ["--prefix={0}".format(prefix)] options += ["--prefix={0}".format(prefix)]
options += self.configure_args() options += self.configure_args()
with working_dir(self.build_directory, create=True): with fs.working_dir(self.build_directory, create=True):
inspect.getmodule(self).configure(*options) inspect.getmodule(self.pkg).configure(*options)
def setup_build_environment(self, env): def build(self, pkg, spec, prefix):
if self.spec.platform == "darwin" and macos_version() >= Version("11"): """Run "make" on the build targets specified by the builder."""
# Many configure files rely on matching '10.*' for macOS version
# detection and fail to add flags if it shows as version 11.
env.set("MACOSX_DEPLOYMENT_TARGET", "10.16")
def build(self, spec, prefix):
"""Makes the build targets specified by
:py:attr:``~.AutotoolsPackage.build_targets``
"""
# See https://autotools.io/automake/silent.html # See https://autotools.io/automake/silent.html
params = ["V=1"] params = ["V=1"]
params += self.build_targets params += self.build_targets
with working_dir(self.build_directory): with fs.working_dir(self.build_directory):
inspect.getmodule(self).make(*params) inspect.getmodule(self.pkg).make(*params)
def install(self, spec, prefix): def install(self, pkg, spec, prefix):
"""Makes the install targets specified by """Run "make" on the install targets specified by the builder."""
:py:attr:``~.AutotoolsPackage.install_targets`` with fs.working_dir(self.build_directory):
""" inspect.getmodule(self.pkg).make(*self.install_targets)
with working_dir(self.build_directory):
inspect.getmodule(self).make(*self.install_targets)
run_after("build")(PackageBase._run_default_build_time_test_callbacks) spack.builder.run_after("build")(execute_build_time_tests)
def check(self): def check(self):
"""Searches the Makefile for targets ``test`` and ``check`` """Run "make" on the ``test`` and ``check`` targets, if found."""
and runs them if found. with fs.working_dir(self.build_directory):
""" self.pkg._if_make_target_execute("test")
with working_dir(self.build_directory): self.pkg._if_make_target_execute("check")
self._if_make_target_execute("test")
self._if_make_target_execute("check")
def _activate_or_not( def _activate_or_not(
self, name, activation_word, deactivation_word, activation_value=None, variant=None self, name, activation_word, deactivation_word, activation_value=None, variant=None
): ):
"""This function contains the current implementation details of """This function contain the current implementation details of
:meth:`~spack.build_systems.autotools.AutotoolsPackage.with_or_without` and :meth:`~spack.build_systems.autotools.AutotoolsBuilder.with_or_without` and
:meth:`~spack.build_systems.autotools.AutotoolsPackage.enable_or_disable`. :meth:`~spack.build_systems.autotools.AutotoolsBuilder.enable_or_disable`.
Args: Args:
name (str): name of the option that is being activated or not name (str): name of the option that is being activated or not
@@ -671,7 +687,7 @@ def _activate_or_not(
Raises: Raises:
KeyError: if name is not among known variants KeyError: if name is not among known variants
""" """
spec = self.spec spec = self.pkg.spec
args = [] args = []
if activation_value == "prefix": if activation_value == "prefix":
@@ -681,16 +697,16 @@ def _activate_or_not(
# Defensively look that the name passed as argument is among # Defensively look that the name passed as argument is among
# variants # variants
if variant not in self.variants: if variant not in self.pkg.variants:
msg = '"{0}" is not a variant of "{1}"' msg = '"{0}" is not a variant of "{1}"'
raise KeyError(msg.format(variant, self.name)) raise KeyError(msg.format(variant, self.pkg.name))
if variant not in spec.variants: if variant not in spec.variants:
return [] return []
# Create a list of pairs. Each pair includes a configuration # Create a list of pairs. Each pair includes a configuration
# option and whether or not that option is activated # option and whether or not that option is activated
variant_desc, _ = self.variants[variant] variant_desc, _ = self.pkg.variants[variant]
if set(variant_desc.values) == set((True, False)): if set(variant_desc.values) == set((True, False)):
# BoolValuedVariant carry information about a single option. # BoolValuedVariant carry information about a single option.
# Nonetheless, for uniformity of treatment we'll package them # Nonetheless, for uniformity of treatment we'll package them
@@ -718,14 +734,18 @@ def _activate_or_not(
override_name = "{0}_or_{1}_{2}".format( override_name = "{0}_or_{1}_{2}".format(
activation_word, deactivation_word, option_value activation_word, deactivation_word, option_value
) )
line_generator = getattr(self, override_name, None) line_generator = getattr(self, override_name, None) or getattr(
self.pkg, override_name, None
)
# If not available use a sensible default # If not available use a sensible default
if line_generator is None: if line_generator is None:
def _default_generator(is_activated): def _default_generator(is_activated):
if is_activated: if is_activated:
line = "--{0}-{1}".format(activation_word, option_value) line = "--{0}-{1}".format(activation_word, option_value)
if activation_value is not None and activation_value(option_value): if activation_value is not None and activation_value(
option_value
): # NOQA=ignore=E501
line += "={0}".format(activation_value(option_value)) line += "={0}".format(activation_value(option_value))
return line return line
return "--{0}-{1}".format(deactivation_word, option_value) return "--{0}-{1}".format(deactivation_word, option_value)
@@ -764,7 +784,7 @@ def with_or_without(self, name, activation_value=None, variant=None):
def enable_or_disable(self, name, activation_value=None, variant=None): def enable_or_disable(self, name, activation_value=None, variant=None):
"""Same as """Same as
:meth:`~spack.build_systems.autotools.AutotoolsPackage.with_or_without` :meth:`~spack.build_systems.autotools.AutotoolsBuilder.with_or_without`
but substitute ``with`` with ``enable`` and ``without`` with ``disable``. but substitute ``with`` with ``enable`` and ``without`` with ``disable``.
Args: Args:
@@ -781,19 +801,14 @@ def enable_or_disable(self, name, activation_value=None, variant=None):
""" """
return self._activate_or_not(name, "enable", "disable", activation_value, variant) return self._activate_or_not(name, "enable", "disable", activation_value, variant)
run_after("install")(PackageBase._run_default_install_time_test_callbacks) spack.builder.run_after("install")(execute_install_time_tests)
def installcheck(self): def installcheck(self):
"""Searches the Makefile for an ``installcheck`` target """Run "make" on the ``installcheck`` target, if found."""
and runs it if found. with fs.working_dir(self.build_directory):
""" self.pkg._if_make_target_execute("installcheck")
with working_dir(self.build_directory):
self._if_make_target_execute("installcheck")
# Check that self.prefix is there after installation @spack.builder.run_after("install")
run_after("install")(PackageBase.sanity_check_prefix)
@run_after("install")
def remove_libtool_archives(self): def remove_libtool_archives(self):
"""Remove all .la files in prefix sub-folders if the package sets """Remove all .la files in prefix sub-folders if the package sets
``install_libtool_archives`` to be False. ``install_libtool_archives`` to be False.
@@ -803,14 +818,20 @@ def remove_libtool_archives(self):
return return
# Remove the files and create a log of what was removed # Remove the files and create a log of what was removed
libtool_files = fs.find(str(self.prefix), "*.la", recursive=True) libtool_files = fs.find(str(self.pkg.prefix), "*.la", recursive=True)
with fs.safe_remove(*libtool_files): with fs.safe_remove(*libtool_files):
fs.mkdirp(os.path.dirname(self._removed_la_files_log)) fs.mkdirp(os.path.dirname(self._removed_la_files_log))
with open(self._removed_la_files_log, mode="w") as f: with open(self._removed_la_files_log, mode="w") as f:
f.write("\n".join(libtool_files)) f.write("\n".join(libtool_files))
def setup_build_environment(self, env):
if self.spec.platform == "darwin" and macos_version() >= Version("11"):
# Many configure files rely on matching '10.*' for macOS version
# detection and fail to add flags if it shows as version 11.
env.set("MACOSX_DEPLOYMENT_TARGET", "10.16")
# On macOS, force rpaths for shared library IDs and remove duplicate rpaths # On macOS, force rpaths for shared library IDs and remove duplicate rpaths
run_after("install")(PackageBase.apply_macos_rpath_fixups) spack.builder.run_after("install", when="platform=darwin")(apply_macos_rpath_fixups)
def _autoreconf_search_path_args(spec): def _autoreconf_search_path_args(spec):

View File

@@ -0,0 +1,31 @@
# Copyright 2013-2022 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 spack.builder
import spack.directives
import spack.package_base
class BundlePackage(spack.package_base.PackageBase):
"""General purpose bundle, or no-code, package class."""
#: This attribute is used in UI queries that require to know which
#: build-system class we are using
build_system_class = "BundlePackage"
#: Legacy buildsystem attribute used to deserialize and install old specs
legacy_buildsystem = "bundle"
#: Bundle packages do not have associated source or binary code.
has_code = False
spack.directives.build_system("bundle")
@spack.builder.builder("bundle")
class BundleBuilder(spack.builder.Builder):
phases = ("install",)
def install(self, pkg, spec, prefix):
pass

View File

@@ -3,12 +3,14 @@
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os import os
from typing import Tuple
import llnl.util.filesystem as fs
import llnl.util.tty as tty import llnl.util.tty as tty
from llnl.util.filesystem import install, mkdirp
from spack.build_systems.cmake import CMakePackage import spack.builder
from spack.package_base import run_after
from .cmake import CMakeBuilder, CMakePackage
def cmake_cache_path(name, value, comment=""): def cmake_cache_path(name, value, comment=""):
@@ -28,44 +30,46 @@ def cmake_cache_option(name, boolean_value, comment=""):
return 'set({0} {1} CACHE BOOL "{2}")\n'.format(name, value, comment) return 'set({0} {1} CACHE BOOL "{2}")\n'.format(name, value, comment)
class CachedCMakePackage(CMakePackage): class CachedCMakeBuilder(CMakeBuilder):
"""Specialized class for packages built using CMake initial cache.
This feature of CMake allows packages to increase reproducibility, #: Names associated with package methods in the old build-system format
especially between Spack- and manual builds. It also allows packages to legacy_methods = CMakeBuilder.legacy_methods + (
sidestep certain parsing bugs in extremely long ``cmake`` commands, and to "initconfig_compiler_entries",
avoid system limits on the length of the command line.""" "initconfig_mpi_entries",
"initconfig_hardware_entries",
"std_initconfig_entries",
"initconfig_package_entries",
) # type: Tuple[str, ...]
phases = ["initconfig", "cmake", "build", "install"] #: Names associated with package attributes in the old build-system format
legacy_attributes = CMakeBuilder.legacy_attributes + (
"cache_name",
"cache_path",
) # type: Tuple[str, ...]
@property @property
def cache_name(self): def cache_name(self):
return "{0}-{1}-{2}@{3}.cmake".format( return "{0}-{1}-{2}@{3}.cmake".format(
self.name, self.pkg.name,
self.spec.architecture, self.pkg.spec.architecture,
self.spec.compiler.name, self.pkg.spec.compiler.name,
self.spec.compiler.version, self.pkg.spec.compiler.version,
) )
@property @property
def cache_path(self): def cache_path(self):
return os.path.join(self.stage.source_path, self.cache_name) return os.path.join(self.pkg.stage.source_path, self.cache_name)
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)
def initconfig_compiler_entries(self): def initconfig_compiler_entries(self):
# This will tell cmake to use the Spack compiler wrappers when run # This will tell cmake to use the Spack compiler wrappers when run
# through Spack, but use the underlying compiler when run outside of # through Spack, but use the underlying compiler when run outside of
# Spack # Spack
spec = self.spec spec = self.pkg.spec
# Fortran compiler is optional # Fortran compiler is optional
if "FC" in os.environ: if "FC" in os.environ:
spack_fc_entry = cmake_cache_path("CMAKE_Fortran_COMPILER", os.environ["FC"]) spack_fc_entry = cmake_cache_path("CMAKE_Fortran_COMPILER", os.environ["FC"])
system_fc_entry = cmake_cache_path("CMAKE_Fortran_COMPILER", self.compiler.fc) system_fc_entry = cmake_cache_path("CMAKE_Fortran_COMPILER", self.pkg.compiler.fc)
else: else:
spack_fc_entry = "# No Fortran compiler defined in spec" spack_fc_entry = "# No Fortran compiler defined in spec"
system_fc_entry = "# No Fortran compiler defined in spec" system_fc_entry = "# No Fortran compiler defined in spec"
@@ -81,8 +85,8 @@ def initconfig_compiler_entries(self):
" " + cmake_cache_path("CMAKE_CXX_COMPILER", os.environ["CXX"]), " " + cmake_cache_path("CMAKE_CXX_COMPILER", os.environ["CXX"]),
" " + spack_fc_entry, " " + spack_fc_entry,
"else()\n", "else()\n",
" " + cmake_cache_path("CMAKE_C_COMPILER", self.compiler.cc), " " + cmake_cache_path("CMAKE_C_COMPILER", self.pkg.compiler.cc),
" " + cmake_cache_path("CMAKE_CXX_COMPILER", self.compiler.cxx), " " + cmake_cache_path("CMAKE_CXX_COMPILER", self.pkg.compiler.cxx),
" " + system_fc_entry, " " + system_fc_entry,
"endif()\n", "endif()\n",
] ]
@@ -126,7 +130,7 @@ def initconfig_compiler_entries(self):
return entries return entries
def initconfig_mpi_entries(self): def initconfig_mpi_entries(self):
spec = self.spec spec = self.pkg.spec
if not spec.satisfies("^mpi"): if not spec.satisfies("^mpi"):
return [] return []
@@ -160,13 +164,13 @@ def initconfig_mpi_entries(self):
mpiexec = os.path.join(spec["mpi"].prefix.bin, "mpiexec") mpiexec = os.path.join(spec["mpi"].prefix.bin, "mpiexec")
if not os.path.exists(mpiexec): if not os.path.exists(mpiexec):
msg = "Unable to determine MPIEXEC, %s tests may fail" % self.name msg = "Unable to determine MPIEXEC, %s tests may fail" % self.pkg.name
entries.append("# {0}\n".format(msg)) entries.append("# {0}\n".format(msg))
tty.warn(msg) tty.warn(msg)
else: else:
# starting with cmake 3.10, FindMPI expects MPIEXEC_EXECUTABLE # starting with cmake 3.10, FindMPI expects MPIEXEC_EXECUTABLE
# vs the older versions which expect MPIEXEC # vs the older versions which expect MPIEXEC
if self.spec["cmake"].satisfies("@3.10:"): if self.pkg.spec["cmake"].satisfies("@3.10:"):
entries.append(cmake_cache_path("MPIEXEC_EXECUTABLE", mpiexec)) entries.append(cmake_cache_path("MPIEXEC_EXECUTABLE", mpiexec))
else: else:
entries.append(cmake_cache_path("MPIEXEC", mpiexec)) entries.append(cmake_cache_path("MPIEXEC", mpiexec))
@@ -180,7 +184,7 @@ def initconfig_mpi_entries(self):
return entries return entries
def initconfig_hardware_entries(self): def initconfig_hardware_entries(self):
spec = self.spec spec = self.pkg.spec
entries = [ entries = [
"#------------------{0}".format("-" * 60), "#------------------{0}".format("-" * 60),
@@ -212,7 +216,7 @@ def std_initconfig_entries(self):
"#------------------{0}".format("-" * 60), "#------------------{0}".format("-" * 60),
"# !!!! This is a generated file, edit at own risk !!!!", "# !!!! This is a generated file, edit at own risk !!!!",
"#------------------{0}".format("-" * 60), "#------------------{0}".format("-" * 60),
"# CMake executable path: {0}".format(self.spec["cmake"].command.path), "# CMake executable path: {0}".format(self.pkg.spec["cmake"].command.path),
"#------------------{0}\n".format("-" * 60), "#------------------{0}\n".format("-" * 60),
] ]
@@ -220,7 +224,8 @@ def initconfig_package_entries(self):
"""This method is to be overwritten by the package""" """This method is to be overwritten by the package"""
return [] return []
def initconfig(self, spec, prefix): @spack.builder.run_before("cmake")
def initconfig(self):
cache_entries = ( cache_entries = (
self.std_initconfig_entries() self.std_initconfig_entries()
+ self.initconfig_compiler_entries() + self.initconfig_compiler_entries()
@@ -236,11 +241,28 @@ def initconfig(self, spec, prefix):
@property @property
def std_cmake_args(self): def std_cmake_args(self):
args = super(CachedCMakePackage, self).std_cmake_args args = super(CachedCMakeBuilder, self).std_cmake_args
args.extend(["-C", self.cache_path]) args.extend(["-C", self.cache_path])
return args return args
@run_after("install") @spack.builder.run_after("install")
def install_cmake_cache(self): def install_cmake_cache(self):
mkdirp(self.spec.prefix.share.cmake) fs.mkdirp(self.pkg.spec.prefix.share.cmake)
install(self.cache_path, self.spec.prefix.share.cmake) fs.install(self.cache_path, self.pkg.spec.prefix.share.cmake)
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
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

View File

@@ -2,23 +2,26 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import inspect import inspect
import os import os
import platform import platform
import re import re
import sys import sys
from typing import List from typing import List, Tuple
import six import six
import llnl.util.filesystem as fs
from llnl.util.compat import Sequence from llnl.util.compat import Sequence
from llnl.util.filesystem import working_dir
import spack.build_environment import spack.build_environment
from spack.directives import conflicts, depends_on, variant import spack.builder
from spack.package_base import InstallError, PackageBase, run_after import spack.package_base
import spack.util.path
from spack.directives import build_system, depends_on, variant
from spack.multimethod import when
from ._checks import BaseBuilder, execute_build_time_tests
# Regex to extract the primary generator from the CMake generator # Regex to extract the primary generator from the CMake generator
# string. # string.
@@ -34,56 +37,141 @@ def _extract_primary_generator(generator):
return primary_generator return primary_generator
class CMakePackage(PackageBase): class CMakePackage(spack.package_base.PackageBase):
"""Specialized class for packages built using CMake """Specialized class for packages built using CMake
For more information on the CMake build system, see: For more information on the CMake build system, see:
https://cmake.org/cmake/help/latest/ https://cmake.org/cmake/help/latest/
"""
This class provides three phases that can be overridden: #: This attribute is used in UI queries that need to know the build
#: system base class
build_system_class = "CMakePackage"
1. :py:meth:`~.CMakePackage.cmake` #: Legacy buildsystem attribute used to deserialize and install old specs
2. :py:meth:`~.CMakePackage.build` legacy_buildsystem = "cmake"
3. :py:meth:`~.CMakePackage.install`
build_system("cmake")
with when("build_system=cmake"):
# https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html
variant(
"build_type",
default="RelWithDebInfo",
description="CMake build type",
values=("Debug", "Release", "RelWithDebInfo", "MinSizeRel"),
)
# CMAKE_INTERPROCEDURAL_OPTIMIZATION only exists for CMake >= 3.9
# https://cmake.org/cmake/help/latest/variable/CMAKE_INTERPROCEDURAL_OPTIMIZATION.html
variant(
"ipo",
default=False,
when="^cmake@3.9:",
description="CMake interprocedural optimization",
)
depends_on("cmake", type="build")
depends_on("ninja", type="build", when="platform=windows")
def flags_to_build_system_args(self, flags):
"""Return a list of all command line arguments to pass the specified
compiler flags to cmake. Note CMAKE does not have a cppflags option,
so cppflags will be added to cflags, cxxflags, and fflags to mimic the
behavior in other tools.
"""
# Has to be dynamic attribute due to caching
setattr(self, "cmake_flag_args", [])
flag_string = "-DCMAKE_{0}_FLAGS={1}"
langs = {"C": "c", "CXX": "cxx", "Fortran": "f"}
# Handle language compiler flags
for lang, pre in langs.items():
flag = pre + "flags"
# cmake has no explicit cppflags support -> add it to all langs
lang_flags = " ".join(flags.get(flag, []) + flags.get("cppflags", []))
if lang_flags:
self.cmake_flag_args.append(flag_string.format(lang, lang_flags))
# Cmake has different linker arguments for different build types.
# We specify for each of them.
if flags["ldflags"]:
ldflags = " ".join(flags["ldflags"])
ld_string = "-DCMAKE_{0}_LINKER_FLAGS={1}"
# cmake has separate linker arguments for types of builds.
for type in ["EXE", "MODULE", "SHARED", "STATIC"]:
self.cmake_flag_args.append(ld_string.format(type, ldflags))
# CMake has libs options separated by language. Apply ours to each.
if flags["ldlibs"]:
libs_flags = " ".join(flags["ldlibs"])
libs_string = "-DCMAKE_{0}_STANDARD_LIBRARIES={1}"
for lang in langs:
self.cmake_flag_args.append(libs_string.format(lang, libs_flags))
# Legacy methods (used by too many packages to change them,
# need to forward to the builder)
def define(self, *args, **kwargs):
return self.builder.define(*args, **kwargs)
def define_from_variant(self, *args, **kwargs):
return self.builder.define_from_variant(*args, **kwargs)
@spack.builder.builder("cmake")
class CMakeBuilder(BaseBuilder):
"""The cmake builder encodes the default way of building software with CMake. IT
has three phases that can be overridden:
1. :py:meth:`~.CMakeBuilder.cmake`
2. :py:meth:`~.CMakeBuilder.build`
3. :py:meth:`~.CMakeBuilder.install`
They all have sensible defaults and for many packages the only thing They all have sensible defaults and for many packages the only thing
necessary will be to override :py:meth:`~.CMakePackage.cmake_args`. necessary will be to override :py:meth:`~.CMakeBuilder.cmake_args`.
For a finer tuning you may also override: For a finer tuning you may also override:
+-----------------------------------------------+--------------------+ +-----------------------------------------------+--------------------+
| **Method** | **Purpose** | | **Method** | **Purpose** |
+===============================================+====================+ +===============================================+====================+
| :py:meth:`~.CMakePackage.root_cmakelists_dir` | Location of the | | :py:meth:`~.CMakeBuilder.root_cmakelists_dir` | Location of the |
| | root CMakeLists.txt| | | root CMakeLists.txt|
+-----------------------------------------------+--------------------+ +-----------------------------------------------+--------------------+
| :py:meth:`~.CMakePackage.build_directory` | Directory where to | | :py:meth:`~.CMakeBuilder.build_directory` | Directory where to |
| | build the package | | | build the package |
+-----------------------------------------------+--------------------+ +-----------------------------------------------+--------------------+
The generator used by CMake can be specified by providing the ``generator``
The generator used by CMake can be specified by providing the attribute. Per
generator attribute. Per
https://cmake.org/cmake/help/git-master/manual/cmake-generators.7.html, https://cmake.org/cmake/help/git-master/manual/cmake-generators.7.html,
the format is: [<secondary-generator> - ]<primary_generator>. The the format is: [<secondary-generator> - ]<primary_generator>.
full list of primary and secondary generators supported by CMake may
be found in the documentation for the version of CMake used; The full list of primary and secondary generators supported by CMake may be found
however, at this time Spack supports only the primary generators in the documentation for the version of CMake used; however, at this time Spack
"Unix Makefiles" and "Ninja." Spack's CMake support is agnostic with supports only the primary generators "Unix Makefiles" and "Ninja." Spack's CMake
respect to primary generators. Spack will generate a runtime error support is agnostic with respect to primary generators. Spack will generate a
if the generator string does not follow the prescribed format, or if runtime error if the generator string does not follow the prescribed format, or if
the primary generator is not supported. the primary generator is not supported.
""" """
#: Phases of a CMake package #: Phases of a CMake package
phases = ["cmake", "build", "install"] phases = ("cmake", "build", "install")
#: This attribute is used in UI queries that need to know the build
#: system base class
build_system_class = "CMakePackage"
build_targets = [] # type: List[str] #: Names associated with package methods in the old build-system format
install_targets = ["install"] legacy_methods = ("cmake_args", "check") # type: Tuple[str, ...]
build_time_test_callbacks = ["check"] #: Names associated with package attributes in the old build-system format
legacy_attributes = (
"generator",
"build_targets",
"install_targets",
"build_time_test_callbacks",
"archive_files",
"root_cmakelists_dir",
"std_cmake_args",
"build_dirname",
"build_directory",
) # type: Tuple[str, ...]
#: The build system generator to use. #: The build system generator to use.
#: #:
@@ -93,27 +181,14 @@ class CMakePackage(PackageBase):
#: #:
#: See https://cmake.org/cmake/help/latest/manual/cmake-generators.7.html #: See https://cmake.org/cmake/help/latest/manual/cmake-generators.7.html
#: for more information. #: for more information.
generator = "Ninja" if sys.platform == "win32" else "Unix Makefiles"
generator = "Unix Makefiles" #: Targets to be used during the build phase
build_targets = [] # type: List[str]
if sys.platform == "win32": #: Targets to be used during the install phase
generator = "Ninja" install_targets = ["install"]
depends_on("ninja") #: Callback names for build-time test
build_time_test_callbacks = ["check"]
# https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html
variant(
"build_type",
default="RelWithDebInfo",
description="CMake build type",
values=("Debug", "Release", "RelWithDebInfo", "MinSizeRel"),
)
# https://cmake.org/cmake/help/latest/variable/CMAKE_INTERPROCEDURAL_OPTIMIZATION.html
variant("ipo", default=False, description="CMake interprocedural optimization")
# CMAKE_INTERPROCEDURAL_OPTIMIZATION only exists for CMake >= 3.9
conflicts("+ipo", when="^cmake@:3.8", msg="+ipo is not supported by CMake < 3.9")
depends_on("cmake", type="build")
@property @property
def archive_files(self): def archive_files(self):
@@ -126,40 +201,30 @@ def root_cmakelists_dir(self):
This path is relative to the root of the extracted tarball, This path is relative to the root of the extracted tarball,
not to the ``build_directory``. Defaults to the current directory. not to the ``build_directory``. Defaults to the current directory.
:return: directory containing CMakeLists.txt
""" """
return self.stage.source_path return self.pkg.stage.source_path
@property @property
def std_cmake_args(self): def std_cmake_args(self):
"""Standard cmake arguments provided as a property for """Standard cmake arguments provided as a property for
convenience of package writers convenience of package writers
:return: standard cmake arguments
""" """
# standard CMake arguments # standard CMake arguments
std_cmake_args = CMakePackage._std_args(self) std_cmake_args = CMakeBuilder.std_args(self.pkg, generator=self.generator)
std_cmake_args += getattr(self, "cmake_flag_args", []) std_cmake_args += getattr(self.pkg, "cmake_flag_args", [])
return std_cmake_args return std_cmake_args
@staticmethod @staticmethod
def _std_args(pkg): def std_args(pkg, generator=None):
"""Computes the standard cmake arguments for a generic package""" """Computes the standard cmake arguments for a generic package"""
generator = generator or "Unix Makefiles"
try:
generator = pkg.generator
except AttributeError:
generator = CMakePackage.generator
# Make sure a valid generator was chosen
valid_primary_generators = ["Unix Makefiles", "Ninja"] valid_primary_generators = ["Unix Makefiles", "Ninja"]
primary_generator = _extract_primary_generator(generator) primary_generator = _extract_primary_generator(generator)
if primary_generator not in valid_primary_generators: if primary_generator not in valid_primary_generators:
msg = "Invalid CMake generator: '{0}'\n".format(generator) msg = "Invalid CMake generator: '{0}'\n".format(generator)
msg += "CMakePackage currently supports the following " msg += "CMakePackage currently supports the following "
msg += "primary generators: '{0}'".format("', '".join(valid_primary_generators)) msg += "primary generators: '{0}'".format("', '".join(valid_primary_generators))
raise InstallError(msg) raise spack.package_base.InstallError(msg)
try: try:
build_type = pkg.spec.variants["build_type"].value build_type = pkg.spec.variants["build_type"].value
@@ -171,7 +236,7 @@ def _std_args(pkg):
except KeyError: except KeyError:
ipo = False ipo = False
define = CMakePackage.define define = CMakeBuilder.define
args = [ args = [
"-G", "-G",
generator, generator,
@@ -251,7 +316,7 @@ def define_from_variant(self, cmake_var, variant=None):
of ``cmake_var``. of ``cmake_var``.
This utility function is similar to This utility function is similar to
:meth:`~spack.build_systems.autotools.AutotoolsPackage.with_or_without`. :meth:`~spack.build_systems.autotools.AutotoolsBuilder.with_or_without`.
Examples: Examples:
@@ -291,122 +356,75 @@ def define_from_variant(self, cmake_var, variant=None):
if variant is None: if variant is None:
variant = cmake_var.lower() variant = cmake_var.lower()
if variant not in self.variants: if variant not in self.pkg.variants:
raise KeyError('"{0}" is not a variant of "{1}"'.format(variant, self.name)) raise KeyError('"{0}" is not a variant of "{1}"'.format(variant, self.pkg.name))
if variant not in self.spec.variants: if variant not in self.pkg.spec.variants:
return "" return ""
value = self.spec.variants[variant].value value = self.pkg.spec.variants[variant].value
if isinstance(value, (tuple, list)): if isinstance(value, (tuple, list)):
# Sort multi-valued variants for reproducibility # Sort multi-valued variants for reproducibility
value = sorted(value) value = sorted(value)
return self.define(cmake_var, value) return self.define(cmake_var, value)
def flags_to_build_system_args(self, flags):
"""Produces a list of all command line arguments to pass the specified
compiler flags to cmake. Note CMAKE does not have a cppflags option,
so cppflags will be added to cflags, cxxflags, and fflags to mimic the
behavior in other tools."""
# Has to be dynamic attribute due to caching
setattr(self, "cmake_flag_args", [])
flag_string = "-DCMAKE_{0}_FLAGS={1}"
langs = {"C": "c", "CXX": "cxx", "Fortran": "f"}
# Handle language compiler flags
for lang, pre in langs.items():
flag = pre + "flags"
# cmake has no explicit cppflags support -> add it to all langs
lang_flags = " ".join(flags.get(flag, []) + flags.get("cppflags", []))
if lang_flags:
self.cmake_flag_args.append(flag_string.format(lang, lang_flags))
# Cmake has different linker arguments for different build types.
# We specify for each of them.
if flags["ldflags"]:
ldflags = " ".join(flags["ldflags"])
ld_string = "-DCMAKE_{0}_LINKER_FLAGS={1}"
# cmake has separate linker arguments for types of builds.
for type in ["EXE", "MODULE", "SHARED", "STATIC"]:
self.cmake_flag_args.append(ld_string.format(type, ldflags))
# CMake has libs options separated by language. Apply ours to each.
if flags["ldlibs"]:
libs_flags = " ".join(flags["ldlibs"])
libs_string = "-DCMAKE_{0}_STANDARD_LIBRARIES={1}"
for lang in langs:
self.cmake_flag_args.append(libs_string.format(lang, libs_flags))
@property @property
def build_dirname(self): def build_dirname(self):
"""Returns the directory name to use when building the package """Directory name to use when building the package."""
return "spack-build-%s" % self.pkg.spec.dag_hash(7)
:return: name of the subdirectory for building the package
"""
return "spack-build-%s" % self.spec.dag_hash(7)
@property @property
def build_directory(self): def build_directory(self):
"""Returns the directory to use when building the package """Full-path to the directory to use when building the package."""
return os.path.join(self.pkg.stage.path, self.build_dirname)
:return: directory where to build the package
"""
return os.path.join(self.stage.path, self.build_dirname)
def cmake_args(self): def cmake_args(self):
"""Produces a list containing all the arguments that must be passed to """List of all the arguments that must be passed to cmake, except:
cmake, except:
* CMAKE_INSTALL_PREFIX * CMAKE_INSTALL_PREFIX
* CMAKE_BUILD_TYPE * CMAKE_BUILD_TYPE
* BUILD_TESTING * BUILD_TESTING
which will be set automatically. which will be set automatically.
:return: list of arguments for cmake
""" """
return [] return []
def cmake(self, spec, prefix): def cmake(self, pkg, spec, prefix):
"""Runs ``cmake`` in the build directory""" """Runs ``cmake`` in the build directory"""
options = self.std_cmake_args options = self.std_cmake_args
options += self.cmake_args() options += self.cmake_args()
options.append(os.path.abspath(self.root_cmakelists_dir)) options.append(os.path.abspath(self.root_cmakelists_dir))
with working_dir(self.build_directory, create=True): with fs.working_dir(self.build_directory, create=True):
inspect.getmodule(self).cmake(*options) inspect.getmodule(self.pkg).cmake(*options)
def build(self, spec, prefix): def build(self, pkg, spec, prefix):
"""Make the build targets""" """Make the build targets"""
with working_dir(self.build_directory): with fs.working_dir(self.build_directory):
if self.generator == "Unix Makefiles": if self.generator == "Unix Makefiles":
inspect.getmodule(self).make(*self.build_targets) inspect.getmodule(self.pkg).make(*self.build_targets)
elif self.generator == "Ninja": elif self.generator == "Ninja":
self.build_targets.append("-v") self.build_targets.append("-v")
inspect.getmodule(self).ninja(*self.build_targets) inspect.getmodule(self.pkg).ninja(*self.build_targets)
def install(self, spec, prefix): def install(self, pkg, spec, prefix):
"""Make the install targets""" """Make the install targets"""
with working_dir(self.build_directory): with fs.working_dir(self.build_directory):
if self.generator == "Unix Makefiles": if self.generator == "Unix Makefiles":
inspect.getmodule(self).make(*self.install_targets) inspect.getmodule(self.pkg).make(*self.install_targets)
elif self.generator == "Ninja": elif self.generator == "Ninja":
inspect.getmodule(self).ninja(*self.install_targets) inspect.getmodule(self.pkg).ninja(*self.install_targets)
run_after("build")(PackageBase._run_default_build_time_test_callbacks) spack.builder.run_after("build")(execute_build_time_tests)
def check(self): def check(self):
"""Searches the CMake-generated Makefile for the target ``test`` """Search the CMake-generated files for the targets ``test`` and ``check``,
and runs it if found. and runs them if found.
""" """
with working_dir(self.build_directory): with fs.working_dir(self.build_directory):
if self.generator == "Unix Makefiles": if self.generator == "Unix Makefiles":
self._if_make_target_execute("test", jobs_env="CTEST_PARALLEL_LEVEL") self.pkg._if_make_target_execute("test", jobs_env="CTEST_PARALLEL_LEVEL")
self._if_make_target_execute("check") self.pkg._if_make_target_execute("check")
elif self.generator == "Ninja": elif self.generator == "Ninja":
self._if_ninja_target_execute("test", jobs_env="CTEST_PARALLEL_LEVEL") self.pkg._if_ninja_target_execute("test", jobs_env="CTEST_PARALLEL_LEVEL")
self._if_ninja_target_execute("check") self.pkg._if_ninja_target_execute("check")
# Check that self.prefix is there after installation
run_after("install")(PackageBase.sanity_check_prefix)

View File

@@ -0,0 +1,44 @@
# Copyright 2013-2022 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)
from typing import Tuple
import spack.builder
import spack.directives
import spack.package_base
from ._checks import BaseBuilder, apply_macos_rpath_fixups
class Package(spack.package_base.PackageBase):
"""General purpose class with a single ``install`` phase that needs to be
coded by packagers.
"""
#: This attribute is used in UI queries that require to know which
#: build-system class we are using
build_system_class = "Package"
#: Legacy buildsystem attribute used to deserialize and install old specs
legacy_buildsystem = "generic"
spack.directives.build_system("generic")
@spack.builder.builder("generic")
class GenericBuilder(BaseBuilder):
"""A builder for a generic build system, that require packagers
to implement an "install" phase.
"""
#: A generic package has only the "install" phase
phases = ("install",)
#: Names associated with package methods in the old build-system format
legacy_methods = () # type: Tuple[str, ...]
#: Names associated with package attributes in the old build-system format
legacy_attributes = ("archive_files",) # type: Tuple[str, ...]
# On macOS, force rpaths for shared library IDs and remove duplicate rpaths
spack.builder.run_after("install", when="platform=darwin")(apply_macos_rpath_fixups)

View File

@@ -2,8 +2,6 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import glob import glob
import inspect import inspect
import os import os
@@ -26,12 +24,14 @@
import spack.error import spack.error
from spack.build_environment import dso_suffix from spack.build_environment import dso_suffix
from spack.package_base import InstallError, PackageBase, run_after from spack.package_base import InstallError
from spack.util.environment import EnvironmentModifications from spack.util.environment import EnvironmentModifications
from spack.util.executable import Executable from spack.util.executable import Executable
from spack.util.prefix import Prefix from spack.util.prefix import Prefix
from spack.version import Version, ver from spack.version import Version, ver
from .generic import Package
# A couple of utility functions that might be useful in general. If so, they # A couple of utility functions that might be useful in general. If so, they
# should really be defined elsewhere, unless deemed heretical. # should really be defined elsewhere, unless deemed heretical.
# (Or na"ive on my part). # (Or na"ive on my part).
@@ -86,7 +86,7 @@ def _expand_fields(s):
return s return s
class IntelPackage(PackageBase): class IntelPackage(Package):
"""Specialized class for licensed Intel software. """Specialized class for licensed Intel software.
This class provides two phases that can be overridden: This class provides two phases that can be overridden:
@@ -99,9 +99,6 @@ class IntelPackage(PackageBase):
to set the appropriate environment variables. to set the appropriate environment variables.
""" """
#: Phases of an Intel package
phases = ["configure", "install"]
#: This attribute is used in UI queries that need to know the build #: This attribute is used in UI queries that need to know the build
#: system base class #: system base class
build_system_class = "IntelPackage" build_system_class = "IntelPackage"
@@ -1184,12 +1181,13 @@ def _determine_license_type(self):
debug_print(license_type) debug_print(license_type)
return license_type return license_type
def configure(self, spec, prefix): @spack.builder.run_before("install")
def configure(self):
"""Generates the silent.cfg file to pass to installer.sh. """Generates the silent.cfg file to pass to installer.sh.
See https://software.intel.com/en-us/articles/configuration-file-format See https://software.intel.com/en-us/articles/configuration-file-format
""" """
prefix = self.prefix
# Both tokens AND values of the configuration file are validated during # Both tokens AND values of the configuration file are validated during
# the run of the underlying binary installer. Any unknown token or # the run of the underlying binary installer. Any unknown token or
# unacceptable value will cause that installer to fail. Notably, this # unacceptable value will cause that installer to fail. Notably, this
@@ -1270,7 +1268,7 @@ def install(self, spec, prefix):
for f in glob.glob("%s/intel*log" % tmpdir): for f in glob.glob("%s/intel*log" % tmpdir):
install(f, dst) install(f, dst)
@run_after("install") @spack.builder.run_after("install")
def validate_install(self): def validate_install(self):
# Sometimes the installer exits with an error but doesn't pass a # Sometimes the installer exits with an error but doesn't pass a
# non-zero exit code to spack. Check for the existence of a 'bin' # non-zero exit code to spack. Check for the existence of a 'bin'
@@ -1278,7 +1276,7 @@ def validate_install(self):
if not os.path.exists(self.prefix.bin): if not os.path.exists(self.prefix.bin):
raise InstallError("The installer has failed to install anything.") raise InstallError("The installer has failed to install anything.")
@run_after("install") @spack.builder.run_after("install")
def configure_rpath(self): def configure_rpath(self):
if "+rpath" not in self.spec: if "+rpath" not in self.spec:
return return
@@ -1296,7 +1294,7 @@ def configure_rpath(self):
with open(compiler_cfg, "w") as fh: with open(compiler_cfg, "w") as fh:
fh.write("-Xlinker -rpath={0}\n".format(compilers_lib_dir)) fh.write("-Xlinker -rpath={0}\n".format(compilers_lib_dir))
@run_after("install") @spack.builder.run_after("install")
def configure_auto_dispatch(self): def configure_auto_dispatch(self):
if self._has_compilers: if self._has_compilers:
if "auto_dispatch=none" in self.spec: if "auto_dispatch=none" in self.spec:
@@ -1320,7 +1318,7 @@ def configure_auto_dispatch(self):
with open(compiler_cfg, "a") as fh: with open(compiler_cfg, "a") as fh:
fh.write("-ax{0}\n".format(",".join(ad))) fh.write("-ax{0}\n".format(",".join(ad)))
@run_after("install") @spack.builder.run_after("install")
def filter_compiler_wrappers(self): def filter_compiler_wrappers(self):
if ("+mpi" in self.spec or self.provides("mpi")) and "~newdtags" in self.spec: if ("+mpi" in self.spec or self.provides("mpi")) and "~newdtags" in self.spec:
bin_dir = self.component_bin_dir("mpi") bin_dir = self.component_bin_dir("mpi")
@@ -1328,7 +1326,7 @@ def filter_compiler_wrappers(self):
f = os.path.join(bin_dir, f) f = os.path.join(bin_dir, f)
filter_file("-Xlinker --enable-new-dtags", " ", f, string=True) filter_file("-Xlinker --enable-new-dtags", " ", f, string=True)
@run_after("install") @spack.builder.run_after("install")
def uninstall_ism(self): def uninstall_ism(self):
# The "Intel(R) Software Improvement Program" [ahem] gets installed, # The "Intel(R) Software Improvement Program" [ahem] gets installed,
# apparently regardless of PHONEHOME_SEND_USAGE_DATA. # apparently regardless of PHONEHOME_SEND_USAGE_DATA.
@@ -1360,7 +1358,7 @@ def base_lib_dir(self):
debug_print(d) debug_print(d)
return d return d
@run_after("install") @spack.builder.run_after("install")
def modify_LLVMgold_rpath(self): def modify_LLVMgold_rpath(self):
"""Add libimf.so and other required libraries to the RUNPATH of LLVMgold.so. """Add libimf.so and other required libraries to the RUNPATH of LLVMgold.so.
@@ -1391,6 +1389,3 @@ def modify_LLVMgold_rpath(self):
] ]
) )
patchelf("--set-rpath", rpath, lib) patchelf("--set-rpath", rpath, lib)
# Check that self.prefix is there after installation
run_after("install")(PackageBase.sanity_check_prefix)

View File

@@ -2,59 +2,79 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os import os
from llnl.util.filesystem import find from llnl.util.filesystem import find
from spack.directives import depends_on, extends import spack.builder
import spack.package_base
import spack.util.executable
from spack.directives import build_system, depends_on, extends
from spack.multimethod import when from spack.multimethod import when
from spack.package_base import PackageBase
from spack.util.executable import Executable
class LuaPackage(PackageBase): class LuaPackage(spack.package_base.PackageBase):
"""Specialized class for lua packages""" """Specialized class for lua packages"""
phases = ["unpack", "generate_luarocks_config", "preprocess", "install"]
#: This attribute is used in UI queries that need to know the build #: This attribute is used in UI queries that need to know the build
#: system base class #: system base class
build_system_class = "LuaPackage" build_system_class = "LuaPackage"
list_depth = 1 # LuaRocks requires at least one level of spidering to find versions list_depth = 1 # LuaRocks requires at least one level of spidering to find versions
depends_on("lua-lang")
extends("lua", when="^lua")
with when("^lua-luajit"):
extends("lua-luajit")
depends_on("luajit")
depends_on("lua-luajit+lualinks")
with when("^lua-luajit-openresty"):
extends("lua-luajit-openresty")
depends_on("luajit")
depends_on("lua-luajit-openresty+lualinks")
def unpack(self, spec, prefix): build_system("lua")
if os.path.splitext(self.stage.archive_file)[1] == ".rock":
directory = self.luarocks("unpack", self.stage.archive_file, output=str) with when("build_system=lua"):
depends_on("lua-lang")
extends("lua", when="^lua")
with when("^lua-luajit"):
extends("lua-luajit")
depends_on("luajit")
depends_on("lua-luajit+lualinks")
with when("^lua-luajit-openresty"):
extends("lua-luajit-openresty")
depends_on("luajit")
depends_on("lua-luajit-openresty+lualinks")
@property
def lua(self):
return spack.util.executable.Executable(self.spec["lua-lang"].prefix.bin.lua)
@property
def luarocks(self):
lr = spack.util.executable.Executable(self.spec["lua-lang"].prefix.bin.luarocks)
return lr
@spack.builder.builder("lua")
class LuaBuilder(spack.builder.Builder):
phases = ("unpack", "generate_luarocks_config", "preprocess", "install")
#: Names associated with package methods in the old build-system format
legacy_methods = ("luarocks_args",)
#: Names associated with package attributes in the old build-system format
legacy_attributes = ()
def unpack(self, pkg, spec, prefix):
if os.path.splitext(pkg.stage.archive_file)[1] == ".rock":
directory = pkg.luarocks("unpack", pkg.stage.archive_file, output=str)
dirlines = directory.split("\n") dirlines = directory.split("\n")
# TODO: figure out how to scope this better # TODO: figure out how to scope this better
os.chdir(dirlines[2]) os.chdir(dirlines[2])
def _generate_tree_line(self, name, prefix): @staticmethod
def _generate_tree_line(name, prefix):
return """{{ name = "{name}", root = "{prefix}" }};""".format( return """{{ name = "{name}", root = "{prefix}" }};""".format(
name=name, name=name,
prefix=prefix, prefix=prefix,
) )
def _luarocks_config_path(self): def generate_luarocks_config(self, pkg, spec, prefix):
return os.path.join(self.stage.source_path, "spack_luarocks.lua") spec = self.pkg.spec
def generate_luarocks_config(self, spec, prefix):
spec = self.spec
table_entries = [] table_entries = []
for d in spec.traverse(deptypes=("build", "run"), deptype_query="run"): for d in spec.traverse(deptypes=("build", "run"), deptype_query="run"):
if d.package.extends(self.extendee_spec): if d.package.extends(self.pkg.extendee_spec):
table_entries.append(self._generate_tree_line(d.name, d.prefix)) table_entries.append(self._generate_tree_line(d.name, d.prefix))
path = self._luarocks_config_path() path = self._luarocks_config_path()
@@ -71,30 +91,24 @@ def generate_luarocks_config(self, spec, prefix):
) )
return path return path
def setup_build_environment(self, env): def preprocess(self, pkg, spec, prefix):
env.set("LUAROCKS_CONFIG", self._luarocks_config_path())
def preprocess(self, spec, prefix):
"""Override this to preprocess source before building with luarocks""" """Override this to preprocess source before building with luarocks"""
pass pass
@property
def lua(self):
return Executable(self.spec["lua-lang"].prefix.bin.lua)
@property
def luarocks(self):
lr = Executable(self.spec["lua-lang"].prefix.bin.luarocks)
return lr
def luarocks_args(self): def luarocks_args(self):
return [] return []
def install(self, spec, prefix): def install(self, pkg, spec, prefix):
rock = "." rock = "."
specs = find(".", "*.rockspec", recursive=False) specs = find(".", "*.rockspec", recursive=False)
if specs: if specs:
rock = specs[0] rock = specs[0]
rocks_args = self.luarocks_args() rocks_args = self.luarocks_args()
rocks_args.append(rock) rocks_args.append(rock)
self.luarocks("--tree=" + prefix, "make", *rocks_args) self.pkg.luarocks("--tree=" + prefix, "make", *rocks_args)
def _luarocks_config_path(self):
return os.path.join(self.pkg.stage.source_path, "spack_luarocks.lua")
def setup_build_environment(self, env):
env.set("LUAROCKS_CONFIG", self._luarocks_config_path())

View File

@@ -2,62 +2,85 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import inspect import inspect
from typing import List # novm from typing import List # novm
import llnl.util.tty as tty import llnl.util.filesystem as fs
from llnl.util.filesystem import working_dir
from spack.directives import conflicts import spack.builder
from spack.package_base import PackageBase, run_after import spack.package_base
from spack.directives import build_system, conflicts
from ._checks import (
BaseBuilder,
apply_macos_rpath_fixups,
execute_build_time_tests,
execute_install_time_tests,
)
class MakefilePackage(PackageBase): class MakefilePackage(spack.package_base.PackageBase):
"""Specialized class for packages that are built using editable Makefiles """Specialized class for packages built using a Makefiles."""
This class provides three phases that can be overridden: #: This attribute is used in UI queries that need to know the build
#: system base class
build_system_class = "MakefilePackage"
#: Legacy buildsystem attribute used to deserialize and install old specs
legacy_buildsystem = "makefile"
1. :py:meth:`~.MakefilePackage.edit` build_system("makefile")
2. :py:meth:`~.MakefilePackage.build` conflicts("platform=windows", when="build_system=makefile")
3. :py:meth:`~.MakefilePackage.install`
@spack.builder.builder("makefile")
class MakefileBuilder(BaseBuilder):
"""The Makefile builder encodes the most common way of building software with
Makefiles. It has three phases that can be overridden, if need be:
1. :py:meth:`~.MakefileBuilder.edit`
2. :py:meth:`~.MakefileBuilder.build`
3. :py:meth:`~.MakefileBuilder.install`
It is usually necessary to override the :py:meth:`~.MakefileBuilder.edit`
phase (which is by default a no-op), while the other two have sensible defaults.
It is usually necessary to override the :py:meth:`~.MakefilePackage.edit`
phase, while :py:meth:`~.MakefilePackage.build` and
:py:meth:`~.MakefilePackage.install` have sensible defaults.
For a finer tuning you may override: For a finer tuning you may override:
+-----------------------------------------------+--------------------+ +-----------------------------------------------+--------------------+
| **Method** | **Purpose** | | **Method** | **Purpose** |
+===============================================+====================+ +===============================================+====================+
| :py:attr:`~.MakefilePackage.build_targets` | Specify ``make`` | | :py:attr:`~.MakefileBuilder.build_targets` | Specify ``make`` |
| | targets for the | | | targets for the |
| | build phase | | | build phase |
+-----------------------------------------------+--------------------+ +-----------------------------------------------+--------------------+
| :py:attr:`~.MakefilePackage.install_targets` | Specify ``make`` | | :py:attr:`~.MakefileBuilder.install_targets` | Specify ``make`` |
| | targets for the | | | targets for the |
| | install phase | | | install phase |
+-----------------------------------------------+--------------------+ +-----------------------------------------------+--------------------+
| :py:meth:`~.MakefilePackage.build_directory` | Directory where the| | :py:meth:`~.MakefileBuilder.build_directory` | Directory where the|
| | Makefile is located| | | Makefile is located|
+-----------------------------------------------+--------------------+ +-----------------------------------------------+--------------------+
""" """
#: Phases of a package that is built with an hand-written Makefile phases = ("edit", "build", "install")
phases = ["edit", "build", "install"]
#: This attribute is used in UI queries that need to know the build
#: system base class
build_system_class = "MakefilePackage"
#: Targets for ``make`` during the :py:meth:`~.MakefilePackage.build` #: Names associated with package methods in the old build-system format
#: phase legacy_methods = ("check", "installcheck")
#: Names associated with package attributes in the old build-system format
legacy_attributes = (
"build_targets",
"install_targets",
"build_time_test_callbacks",
"install_time_test_callbacks",
"build_directory",
)
#: Targets for ``make`` during the :py:meth:`~.MakefileBuilder.build` phase
build_targets = [] # type: List[str] build_targets = [] # type: List[str]
#: Targets for ``make`` during the :py:meth:`~.MakefilePackage.install` #: Targets for ``make`` during the :py:meth:`~.MakefileBuilder.install` phase
#: phase
install_targets = ["install"] install_targets = ["install"]
conflicts("platform=windows")
#: Callback names for build-time test #: Callback names for build-time test
build_time_test_callbacks = ["check"] build_time_test_callbacks = ["check"]
@@ -66,53 +89,39 @@ class MakefilePackage(PackageBase):
@property @property
def build_directory(self): def build_directory(self):
"""Returns the directory containing the main Makefile """Return the directory containing the main Makefile."""
return self.pkg.stage.source_path
:return: build directory def edit(self, pkg, spec, prefix):
""" """Edit the Makefile before calling make. The default is a no-op."""
return self.stage.source_path pass
def edit(self, spec, prefix): def build(self, pkg, spec, prefix):
"""Edits the Makefile before calling make. This phase cannot """Run "make" on the build targets specified by the builder."""
be defaulted. with fs.working_dir(self.build_directory):
""" inspect.getmodule(self.pkg).make(*self.build_targets)
tty.msg("Using default implementation: skipping edit phase.")
def build(self, spec, prefix): def install(self, pkg, spec, prefix):
"""Calls make, passing :py:attr:`~.MakefilePackage.build_targets` """Run "make" on the install targets specified by the builder."""
as targets. with fs.working_dir(self.build_directory):
""" inspect.getmodule(self.pkg).make(*self.install_targets)
with working_dir(self.build_directory):
inspect.getmodule(self).make(*self.build_targets)
def install(self, spec, prefix): spack.builder.run_after("build")(execute_build_time_tests)
"""Calls make, passing :py:attr:`~.MakefilePackage.install_targets`
as targets.
"""
with working_dir(self.build_directory):
inspect.getmodule(self).make(*self.install_targets)
run_after("build")(PackageBase._run_default_build_time_test_callbacks)
def check(self): def check(self):
"""Searches the Makefile for targets ``test`` and ``check`` """Run "make" on the ``test`` and ``check`` targets, if found."""
and runs them if found. with fs.working_dir(self.build_directory):
""" self.pkg._if_make_target_execute("test")
with working_dir(self.build_directory): self.pkg._if_make_target_execute("check")
self._if_make_target_execute("test")
self._if_make_target_execute("check")
run_after("install")(PackageBase._run_default_install_time_test_callbacks) spack.builder.run_after("install")(execute_install_time_tests)
def installcheck(self): def installcheck(self):
"""Searches the Makefile for an ``installcheck`` target """Searches the Makefile for an ``installcheck`` target
and runs it if found. and runs it if found.
""" """
with working_dir(self.build_directory): with fs.working_dir(self.build_directory):
self._if_make_target_execute("installcheck") self.pkg._if_make_target_execute("installcheck")
# Check that self.prefix is there after installation
run_after("install")(PackageBase.sanity_check_prefix)
# On macOS, force rpaths for shared library IDs and remove duplicate rpaths # On macOS, force rpaths for shared library IDs and remove duplicate rpaths
run_after("install")(PackageBase.apply_macos_rpath_fixups) spack.builder.run_after("install", when="platform=darwin")(apply_macos_rpath_fixups)

View File

@@ -2,60 +2,73 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import llnl.util.filesystem as fs
import spack.builder
from llnl.util.filesystem import install_tree, working_dir import spack.package_base
from spack.directives import build_system, depends_on
from spack.directives import depends_on from spack.multimethod import when
from spack.package_base import PackageBase, run_after
from spack.util.executable import which from spack.util.executable import which
from ._checks import BaseBuilder
class MavenPackage(PackageBase):
class MavenPackage(spack.package_base.PackageBase):
"""Specialized class for packages that are built using the """Specialized class for packages that are built using the
Maven build system. See https://maven.apache.org/index.html Maven build system. See https://maven.apache.org/index.html
for more information. for more information.
This class provides the following phases that can be overridden:
* build
* install
""" """
# Default phases
phases = ["build", "install"]
# To be used in UI queries that require to know which # To be used in UI queries that require to know which
# build-system class we are using # build-system class we are using
build_system_class = "MavenPackage" build_system_class = "MavenPackage"
depends_on("java", type=("build", "run")) #: Legacy buildsystem attribute used to deserialize and install old specs
depends_on("maven", type="build") legacy_buildsystem = "maven"
build_system("maven")
with when("build_system=maven"):
depends_on("java", type=("build", "run"))
depends_on("maven", type="build")
@spack.builder.builder("maven")
class MavenBuilder(BaseBuilder):
"""The Maven builder encodes the default way to build software with Maven.
It has two phases that can be overridden, if need be:
1. :py:meth:`~.MavenBuilder.build`
2. :py:meth:`~.MavenBuilder.install`
"""
phases = ("build", "install")
#: Names associated with package methods in the old build-system format
legacy_methods = ("build_args",)
#: Names associated with package attributes in the old build-system format
legacy_attributes = ("build_directory",)
@property @property
def build_directory(self): def build_directory(self):
"""The directory containing the ``pom.xml`` file.""" """The directory containing the ``pom.xml`` file."""
return self.stage.source_path return self.pkg.stage.source_path
def build_args(self): def build_args(self):
"""List of args to pass to build phase.""" """List of args to pass to build phase."""
return [] return []
def build(self, spec, prefix): def build(self, pkg, spec, prefix):
"""Compile code and package into a JAR file.""" """Compile code and package into a JAR file."""
with fs.working_dir(self.build_directory):
with working_dir(self.build_directory):
mvn = which("mvn") mvn = which("mvn")
if self.run_tests: if self.pkg.run_tests:
mvn("verify", *self.build_args()) mvn("verify", *self.build_args())
else: else:
mvn("package", "-DskipTests", *self.build_args()) mvn("package", "-DskipTests", *self.build_args())
def install(self, spec, prefix): def install(self, pkg, spec, prefix):
"""Copy to installation prefix.""" """Copy to installation prefix."""
with fs.working_dir(self.build_directory):
with working_dir(self.build_directory): fs.install_tree(".", prefix)
install_tree(".", prefix)
# Check that self.prefix is there after installation
run_after("install")(PackageBase.sanity_check_prefix)

View File

@@ -2,76 +2,104 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import inspect import inspect
import os import os
from typing import List # novm from typing import List # novm
from llnl.util.filesystem import working_dir import llnl.util.filesystem as fs
from spack.directives import depends_on, variant import spack.builder
from spack.package_base import PackageBase, run_after import spack.package_base
from spack.directives import build_system, depends_on, variant
from spack.multimethod import when
from ._checks import BaseBuilder, execute_build_time_tests
class MesonPackage(PackageBase): class MesonPackage(spack.package_base.PackageBase):
"""Specialized class for packages built using Meson """Specialized class for packages built using Meson. For more information
on the Meson build system, see https://mesonbuild.com/
"""
For more information on the Meson build system, see: #: This attribute is used in UI queries that need to know the build
https://mesonbuild.com/ #: system base class
build_system_class = "MesonPackage"
This class provides three phases that can be overridden: #: Legacy buildsystem attribute used to deserialize and install old specs
legacy_buildsystem = "meson"
1. :py:meth:`~.MesonPackage.meson` build_system("meson")
2. :py:meth:`~.MesonPackage.build`
3. :py:meth:`~.MesonPackage.install` with when("build_system=meson"):
variant(
"buildtype",
default="debugoptimized",
description="Meson build type",
values=("plain", "debug", "debugoptimized", "release", "minsize"),
)
variant(
"default_library",
default="shared",
values=("shared", "static"),
multi=True,
description="Build shared libs, static libs or both",
)
variant("strip", default=False, description="Strip targets on install")
depends_on("meson", type="build")
depends_on("ninja", type="build")
def flags_to_build_system_args(self, flags):
"""Produces a list of all command line arguments to pass the specified
compiler flags to meson."""
# Has to be dynamic attribute due to caching
setattr(self, "meson_flag_args", [])
@spack.builder.builder("meson")
class MesonBuilder(BaseBuilder):
"""The Meson builder encodes the default way to build software with Meson.
The builder has three phases that can be overridden, if need be:
1. :py:meth:`~.MesonBuilder.meson`
2. :py:meth:`~.MesonBuilder.build`
3. :py:meth:`~.MesonBuilder.install`
They all have sensible defaults and for many packages the only thing They all have sensible defaults and for many packages the only thing
necessary will be to override :py:meth:`~.MesonPackage.meson_args`. necessary will be to override :py:meth:`~.MesonBuilder.meson_args`.
For a finer tuning you may also override: For a finer tuning you may also override:
+-----------------------------------------------+--------------------+ +-----------------------------------------------+--------------------+
| **Method** | **Purpose** | | **Method** | **Purpose** |
+===============================================+====================+ +===============================================+====================+
| :py:meth:`~.MesonPackage.root_mesonlists_dir` | Location of the | | :py:meth:`~.MesonBuilder.root_mesonlists_dir` | Location of the |
| | root MesonLists.txt| | | root MesonLists.txt|
+-----------------------------------------------+--------------------+ +-----------------------------------------------+--------------------+
| :py:meth:`~.MesonPackage.build_directory` | Directory where to | | :py:meth:`~.MesonBuilder.build_directory` | Directory where to |
| | build the package | | | build the package |
+-----------------------------------------------+--------------------+ +-----------------------------------------------+--------------------+
""" """
#: Phases of a Meson package phases = ("meson", "build", "install")
phases = ["meson", "build", "install"]
#: This attribute is used in UI queries that need to know the build #: Names associated with package methods in the old build-system format
#: system base class legacy_methods = ("meson_args", "check")
build_system_class = "MesonPackage"
#: Names associated with package attributes in the old build-system format
legacy_attributes = (
"build_targets",
"install_targets",
"build_time_test_callbacks",
"root_mesonlists_dir",
"std_meson_args",
"build_directory",
)
build_targets = [] # type: List[str] build_targets = [] # type: List[str]
install_targets = ["install"] install_targets = ["install"]
build_time_test_callbacks = ["check"] build_time_test_callbacks = ["check"]
variant(
"buildtype",
default="debugoptimized",
description="Meson build type",
values=("plain", "debug", "debugoptimized", "release", "minsize"),
)
variant(
"default_library",
default="shared",
values=("shared", "static"),
multi=True,
description="Build shared libs, static libs or both",
)
variant("strip", default=False, description="Strip targets on install")
depends_on("meson", type="build")
depends_on("ninja", type="build")
@property @property
def archive_files(self): def archive_files(self):
"""Files to archive for packages based on Meson""" """Files to archive for packages based on Meson"""
@@ -79,31 +107,26 @@ def archive_files(self):
@property @property
def root_mesonlists_dir(self): def root_mesonlists_dir(self):
"""The relative path to the directory containing meson.build """Relative path to the directory containing meson.build
This path is relative to the root of the extracted tarball, This path is relative to the root of the extracted tarball,
not to the ``build_directory``. Defaults to the current directory. not to the ``build_directory``. Defaults to the current directory.
:return: directory containing meson.build
""" """
return self.stage.source_path return self.pkg.stage.source_path
@property @property
def std_meson_args(self): def std_meson_args(self):
"""Standard meson arguments provided as a property for """Standard meson arguments provided as a property for convenience
convenience of package writers of package writers.
:return: standard meson arguments
""" """
# standard Meson arguments # standard Meson arguments
std_meson_args = MesonPackage._std_args(self) std_meson_args = MesonBuilder.std_args(self.pkg)
std_meson_args += getattr(self, "meson_flag_args", []) std_meson_args += getattr(self, "meson_flag_args", [])
return std_meson_args return std_meson_args
@staticmethod @staticmethod
def _std_args(pkg): def std_args(pkg):
"""Computes the standard meson arguments for a generic package""" """Standard meson arguments for a generic package."""
try: try:
build_type = pkg.spec.variants["buildtype"].value build_type = pkg.spec.variants["buildtype"].value
except KeyError: except KeyError:
@@ -132,31 +155,18 @@ def _std_args(pkg):
return args return args
def flags_to_build_system_args(self, flags):
"""Produces a list of all command line arguments to pass the specified
compiler flags to meson."""
# Has to be dynamic attribute due to caching
setattr(self, "meson_flag_args", [])
@property @property
def build_dirname(self): def build_dirname(self):
"""Returns the directory name to use when building the package """Returns the directory name to use when building the package."""
return "spack-build-{}".format(self.spec.dag_hash(7))
:return: name of the subdirectory for building the package
"""
return "spack-build-%s" % self.spec.dag_hash(7)
@property @property
def build_directory(self): def build_directory(self):
"""Returns the directory to use when building the package """Directory to use when building the package."""
return os.path.join(self.pkg.stage.path, self.build_dirname)
:return: directory where to build the package
"""
return os.path.join(self.stage.path, self.build_dirname)
def meson_args(self): def meson_args(self):
"""Produces a list containing all the arguments that must be passed to """List of arguments that must be passed to meson, except:
meson, except:
* ``--prefix`` * ``--prefix``
* ``--libdir`` * ``--libdir``
@@ -165,40 +175,33 @@ def meson_args(self):
* ``--default_library`` * ``--default_library``
which will be set automatically. which will be set automatically.
:return: list of arguments for meson
""" """
return [] return []
def meson(self, spec, prefix): def meson(self, pkg, spec, prefix):
"""Runs ``meson`` in the build directory""" """Run ``meson`` in the build directory"""
options = [os.path.abspath(self.root_mesonlists_dir)] options = [os.path.abspath(self.root_mesonlists_dir)]
options += self.std_meson_args options += self.std_meson_args
options += self.meson_args() options += self.meson_args()
with working_dir(self.build_directory, create=True): with fs.working_dir(self.build_directory, create=True):
inspect.getmodule(self).meson(*options) inspect.getmodule(self.pkg).meson(*options)
def build(self, spec, prefix): def build(self, pkg, spec, prefix):
"""Make the build targets""" """Make the build targets"""
options = ["-v"] options = ["-v"]
options += self.build_targets options += self.build_targets
with working_dir(self.build_directory): with fs.working_dir(self.build_directory):
inspect.getmodule(self).ninja(*options) inspect.getmodule(self.pkg).ninja(*options)
def install(self, spec, prefix): def install(self, pkg, spec, prefix):
"""Make the install targets""" """Make the install targets"""
with working_dir(self.build_directory): with fs.working_dir(self.build_directory):
inspect.getmodule(self).ninja(*self.install_targets) inspect.getmodule(self.pkg).ninja(*self.install_targets)
run_after("build")(PackageBase._run_default_build_time_test_callbacks) spack.builder.run_after("build")(execute_build_time_tests)
def check(self): def check(self):
"""Searches the Meson-generated file for the target ``test`` """Search Meson-generated files for the target ``test`` and run it if found."""
and runs it if found. with fs.working_dir(self.build_directory):
"""
with working_dir(self.build_directory):
self._if_ninja_target_execute("test") self._if_ninja_target_execute("test")
self._if_ninja_target_execute("check") self._if_ninja_target_execute("check")
# Check that self.prefix is there after installation
run_after("install")(PackageBase.sanity_check_prefix)

View File

@@ -0,0 +1,102 @@
# Copyright 2013-2022 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 inspect
from typing import List # novm
import llnl.util.filesystem as fs
import spack.builder
import spack.package_base
from spack.directives import build_system, conflicts
from ._checks import BaseBuilder
class NMakePackage(spack.package_base.PackageBase):
"""Specialized class for packages built using a Makefiles."""
#: This attribute is used in UI queries that need to know the build
#: system base class
build_system_class = "NmakePackage"
build_system("nmake")
conflicts("platform=linux", when="build_system=nmake")
conflicts("platform=darwin", when="build_system=nmake")
conflicts("platform=cray", when="build_system=nmake")
@spack.builder.builder("nmake")
class NMakeBuilder(BaseBuilder):
"""The NMake builder encodes the most common way of building software with
NMake on Windows. It has three phases that can be overridden, if need be:
1. :py:meth:`~.NMakeBuilder.edit`
2. :py:meth:`~.NMakeBuilder.build`
3. :py:meth:`~.NMakeBuilder.install`
It is usually necessary to override the :py:meth:`~.NMakeBuilder.edit`
phase (which is by default a no-op), while the other two have sensible defaults.
For a finer tuning you may override:
+--------------------------------------------+--------------------+
| **Method** | **Purpose** |
+============================================+====================+
| :py:attr:`~.NMakeBuilder.build_targets` | Specify ``nmake`` |
| | targets for the |
| | build phase |
+--------------------------------------------+--------------------+
| :py:attr:`~.NMakeBuilder.install_targets` | Specify ``nmake`` |
| | targets for the |
| | install phase |
+--------------------------------------------+--------------------+
| :py:meth:`~.NMakeBuilder.build_directory` | Directory where the|
| | Makefile is located|
+--------------------------------------------+--------------------+
"""
phases = ("edit", "build", "install")
#: Names associated with package methods in the old build-system format
legacy_methods = ("check", "installcheck")
#: Names associated with package attributes in the old build-system format
legacy_attributes = (
"build_targets",
"install_targets",
"build_time_test_callbacks",
"install_time_test_callbacks",
"build_directory",
)
#: Targets for ``make`` during the :py:meth:`~.NMakeBuilder.build` phase
build_targets = [] # type: List[str]
#: Targets for ``make`` during the :py:meth:`~.NMakeBuilder.install` phase
install_targets = ["install"]
#: Callback names for build-time test
build_time_test_callbacks = ["check"]
#: Callback names for install-time test
install_time_test_callbacks = ["installcheck"]
@property
def build_directory(self):
"""Return the directory containing the main Makefile."""
return self.pkg.stage.source_path
def edit(self, pkg, spec, prefix):
"""Edit the Makefile before calling make. The default is a no-op."""
pass
def build(self, pkg, spec, prefix):
"""Run "make" on the build targets specified by the builder."""
with fs.working_dir(self.build_directory):
inspect.getmodule(self.pkg).nmake(*self.build_targets)
def install(self, pkg, spec, prefix):
"""Run "make" on the install targets specified by the builder."""
with fs.working_dir(self.build_directory):
inspect.getmodule(self.pkg).nmake(*self.install_targets)

View File

@@ -2,51 +2,62 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import inspect import inspect
from spack.directives import extends import spack.builder
from spack.package_base import PackageBase, run_after import spack.package_base
from spack.directives import build_system, extends
from spack.multimethod import when
from ._checks import BaseBuilder
class OctavePackage(PackageBase): class OctavePackage(spack.package_base.PackageBase):
"""Specialized class for Octave packages. See """Specialized class for Octave packages. See
https://www.gnu.org/software/octave/doc/v4.2.0/Installing-and-Removing-Packages.html https://www.gnu.org/software/octave/doc/v4.2.0/Installing-and-Removing-Packages.html
for more information. for more information.
This class provides the following phases that can be overridden:
1. :py:meth:`~.OctavePackage.install`
""" """
# Default phases
phases = ["install"]
# To be used in UI queries that require to know which # To be used in UI queries that require to know which
# build-system class we are using # build-system class we are using
build_system_class = "OctavePackage" build_system_class = "OctavePackage"
#: Legacy buildsystem attribute used to deserialize and install old specs
legacy_buildsystem = "octave"
extends("octave") build_system("octave")
with when("build_system=octave"):
extends("octave")
@spack.builder.builder("octave")
class OctaveBuilder(BaseBuilder):
"""The octave builder provides the following phases that can be overridden:
1. :py:meth:`~.OctaveBuilder.install`
"""
phases = ("install",)
#: Names associated with package methods in the old build-system format
legacy_methods = ()
#: Names associated with package attributes in the old build-system format
legacy_attributes = ()
def install(self, pkg, spec, prefix):
"""Install the package from the archive file"""
inspect.getmodule(self.pkg).octave(
"--quiet",
"--norc",
"--built-in-docstrings-file=/dev/null",
"--texi-macros-file=/dev/null",
"--eval",
"pkg prefix %s; pkg install %s" % (prefix, self.pkg.stage.archive_file),
)
def setup_build_environment(self, env): def setup_build_environment(self, env):
# octave does not like those environment variables to be set: # octave does not like those environment variables to be set:
env.unset("CC") env.unset("CC")
env.unset("CXX") env.unset("CXX")
env.unset("FC") env.unset("FC")
def install(self, spec, prefix):
"""Install the package from the archive file"""
inspect.getmodule(self).octave(
"--quiet",
"--norc",
"--built-in-docstrings-file=/dev/null",
"--texi-macros-file=/dev/null",
"--eval",
"pkg prefix %s; pkg install %s" % (prefix, self.stage.archive_file),
)
# Testing
# Check that self.prefix is there after installation
run_after("install")(PackageBase.sanity_check_prefix)

View File

@@ -2,11 +2,7 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""Common utilities for managing intel oneapi packages."""
"""Common utilities for managing intel oneapi packages.
"""
import getpass import getpass
import platform import platform
import shutil import shutil
@@ -14,18 +10,17 @@
from llnl.util.filesystem import find_headers, find_libraries, join_path from llnl.util.filesystem import find_headers, find_libraries, join_path
from spack.package_base import Package
from spack.util.environment import EnvironmentModifications from spack.util.environment import EnvironmentModifications
from spack.util.executable import Executable from spack.util.executable import Executable
from .generic import Package
class IntelOneApiPackage(Package): class IntelOneApiPackage(Package):
"""Base class for Intel oneAPI packages.""" """Base class for Intel oneAPI packages."""
homepage = "https://software.intel.com/oneapi" homepage = "https://software.intel.com/oneapi"
phases = ["install"]
# oneAPI license does not allow mirroring outside of the # oneAPI license does not allow mirroring outside of the
# organization (e.g. University/Company). # organization (e.g. University/Company).
redistribute_source = False redistribute_source = False

View File

@@ -2,73 +2,87 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import inspect import inspect
import os import os
from llnl.util.filesystem import filter_file from llnl.util.filesystem import filter_file
from spack.directives import extends import spack.builder
from spack.package_base import PackageBase, run_after import spack.package_base
from spack.directives import build_system, extends
from spack.package_base import PackageBase
from spack.util.executable import Executable from spack.util.executable import Executable
from ._checks import BaseBuilder, execute_build_time_tests
class PerlPackage(PackageBase): class PerlPackage(PackageBase):
"""Specialized class for packages that are built using Perl. """Specialized class for packages that are built using Perl."""
This class provides four phases that can be overridden if required: #: This attribute is used in UI queries that need to know the build
#: system base class
build_system_class = "PerlPackage"
#: Legacy buildsystem attribute used to deserialize and install old specs
legacy_buildsystem = "perl"
1. :py:meth:`~.PerlPackage.configure` build_system("perl")
2. :py:meth:`~.PerlPackage.build`
3. :py:meth:`~.PerlPackage.check` extends("perl", when="build_system=perl")
4. :py:meth:`~.PerlPackage.install`
@spack.builder.builder("perl")
class PerlBuilder(BaseBuilder):
"""The perl builder provides four phases that can be overridden, if required:
1. :py:meth:`~.PerlBuilder.configure`
2. :py:meth:`~.PerlBuilder.build`
3. :py:meth:`~.PerlBuilder.check`
4. :py:meth:`~.PerlBuilder.install`
The default methods use, in order of preference: The default methods use, in order of preference:
(1) Makefile.PL, (1) Makefile.PL,
(2) Build.PL. (2) Build.PL.
Some packages may need to override Some packages may need to override :py:meth:`~.PerlBuilder.configure_args`,
:py:meth:`~.PerlPackage.configure_args`, which produces a list of arguments for :py:meth:`~.PerlBuilder.configure`.
which produces a list of arguments for
:py:meth:`~.PerlPackage.configure`.
Arguments should not include the installation base directory. Arguments should not include the installation base directory.
""" """
#: Phases of a Perl package #: Phases of a Perl package
phases = ["configure", "build", "install"] phases = ("configure", "build", "install")
#: This attribute is used in UI queries that need to know the build #: Names associated with package methods in the old build-system format
#: system base class legacy_methods = ("configure_args", "check")
build_system_class = "PerlPackage"
#: Names associated with package attributes in the old build-system format
legacy_attributes = ()
#: Callback names for build-time test #: Callback names for build-time test
build_time_test_callbacks = ["check"] build_time_test_callbacks = ["check"]
extends("perl")
def configure_args(self): def configure_args(self):
"""Produces a list containing the arguments that must be passed to """List of arguments passed to :py:meth:`~.PerlBuilder.configure`.
:py:meth:`~.PerlPackage.configure`. Arguments should not include
the installation base directory, which is prepended automatically.
:return: list of arguments for Makefile.PL or Build.PL Arguments should not include the installation base directory, which
is prepended automatically.
""" """
return [] return []
def configure(self, spec, prefix): def configure(self, pkg, spec, prefix):
"""Runs Makefile.PL or Build.PL with arguments consisting of """Run Makefile.PL or Build.PL with arguments consisting of
an appropriate installation base directory followed by the an appropriate installation base directory followed by the
list returned by :py:meth:`~.PerlPackage.configure_args`. list returned by :py:meth:`~.PerlBuilder.configure_args`.
:raise RuntimeError: if neither Makefile.PL or Build.PL exist Raises:
RuntimeError: if neither Makefile.PL nor Build.PL exist
""" """
if os.path.isfile("Makefile.PL"): if os.path.isfile("Makefile.PL"):
self.build_method = "Makefile.PL" self.build_method = "Makefile.PL"
self.build_executable = inspect.getmodule(self).make self.build_executable = inspect.getmodule(self.pkg).make
elif os.path.isfile("Build.PL"): elif os.path.isfile("Build.PL"):
self.build_method = "Build.PL" self.build_method = "Build.PL"
self.build_executable = Executable(os.path.join(self.stage.source_path, "Build")) self.build_executable = Executable(os.path.join(self.pkg.stage.source_path, "Build"))
else: else:
raise RuntimeError("Unknown build_method for perl package") raise RuntimeError("Unknown build_method for perl package")
@@ -78,33 +92,30 @@ def configure(self, spec, prefix):
options = ["Build.PL", "--install_base", prefix] options = ["Build.PL", "--install_base", prefix]
options += self.configure_args() options += self.configure_args()
inspect.getmodule(self).perl(*options) inspect.getmodule(self.pkg).perl(*options)
# It is possible that the shebang in the Build script that is created from # It is possible that the shebang in the Build script that is created from
# Build.PL may be too long causing the build to fail. Patching the shebang # Build.PL may be too long causing the build to fail. Patching the shebang
# does not happen until after install so set '/usr/bin/env perl' here in # does not happen until after install so set '/usr/bin/env perl' here in
# the Build script. # the Build script.
@run_after("configure") @spack.builder.run_after("configure")
def fix_shebang(self): def fix_shebang(self):
if self.build_method == "Build.PL": if self.build_method == "Build.PL":
pattern = "#!{0}".format(self.spec["perl"].command.path) pattern = "#!{0}".format(self.spec["perl"].command.path)
repl = "#!/usr/bin/env perl" repl = "#!/usr/bin/env perl"
filter_file(pattern, repl, "Build", backup=False) filter_file(pattern, repl, "Build", backup=False)
def build(self, spec, prefix): def build(self, pkg, spec, prefix):
"""Builds a Perl package.""" """Builds a Perl package."""
self.build_executable() self.build_executable()
# Ensure that tests run after build (if requested): # Ensure that tests run after build (if requested):
run_after("build")(PackageBase._run_default_build_time_test_callbacks) spack.builder.run_after("build")(execute_build_time_tests)
def check(self): def check(self):
"""Runs built-in tests of a Perl package.""" """Runs built-in tests of a Perl package."""
self.build_executable("test") self.build_executable("test")
def install(self, spec, prefix): def install(self, pkg, spec, prefix):
"""Installs a Perl package.""" """Installs a Perl package."""
self.build_executable("install") self.build_executable("install")
# Check that self.prefix is there after installation
run_after("install")(PackageBase.sanity_check_prefix)

View File

@@ -8,93 +8,22 @@
import shutil import shutil
from typing import Optional from typing import Optional
import llnl.util.filesystem as fs
import llnl.util.lang as lang
import llnl.util.tty as tty import llnl.util.tty as tty
from llnl.util.filesystem import (
filter_file,
find,
find_all_headers,
find_libraries,
is_nonsymlink_exe_with_shebang,
path_contains_subdirectory,
same_path,
working_dir,
)
from llnl.util.lang import classproperty, match_predicate
from spack.directives import depends_on, extends import spack.builder
import spack.multimethod
import spack.package_base
from spack.directives import build_system, depends_on, extends
from spack.error import NoHeadersError, NoLibrariesError, SpecError from spack.error import NoHeadersError, NoLibrariesError, SpecError
from spack.package_base import PackageBase, run_after
from spack.version import Version from spack.version import Version
from ._checks import BaseBuilder, execute_install_time_tests
class PythonPackage(PackageBase):
"""Specialized class for packages that are built using pip."""
#: Package name, version, and extension on PyPI class PythonExtension(spack.package_base.PackageBase):
pypi = None # type: Optional[str] maintainers = ["adamjstewart"]
maintainers = ["adamjstewart", "pradyunsg"]
# Default phases
phases = ["install"]
# To be used in UI queries that require to know which
# build-system class we are using
build_system_class = "PythonPackage"
#: Callback names for install-time test
install_time_test_callbacks = ["test"]
extends("python")
depends_on("py-pip", type="build")
# FIXME: technically wheel is only needed when building from source, not when
# installing a downloaded wheel, but I don't want to add wheel as a dep to every
# package manually
depends_on("py-wheel", type="build")
py_namespace = None # type: Optional[str]
@staticmethod
def _std_args(cls):
return [
# Verbose
"-vvv",
# Disable prompting for input
"--no-input",
# Disable the cache
"--no-cache-dir",
# Don't check to see if pip is up-to-date
"--disable-pip-version-check",
# Install packages
"install",
# Don't install package dependencies
"--no-deps",
# Overwrite existing packages
"--ignore-installed",
# Use env vars like PYTHONPATH
"--no-build-isolation",
# Don't warn that prefix.bin is not in PATH
"--no-warn-script-location",
# Ignore the PyPI package index
"--no-index",
]
@classproperty
def homepage(cls):
if cls.pypi:
name = cls.pypi.split("/")[0]
return "https://pypi.org/project/" + name + "/"
@classproperty
def url(cls):
if cls.pypi:
return "https://files.pythonhosted.org/packages/source/" + cls.pypi[0] + "/" + cls.pypi
@classproperty
def list_url(cls):
if cls.pypi:
name = cls.pypi.split("/")[0]
return "https://pypi.org/simple/" + name + "/"
@property @property
def import_modules(self): def import_modules(self):
@@ -124,7 +53,7 @@ def import_modules(self):
# Some Python libraries are packages: collections of modules # Some Python libraries are packages: collections of modules
# distributed in directories containing __init__.py files # distributed in directories containing __init__.py files
for path in find(root, "__init__.py", recursive=True): for path in fs.find(root, "__init__.py", recursive=True):
modules.append( modules.append(
path.replace(root + os.sep, "", 1) path.replace(root + os.sep, "", 1)
.replace(os.sep + "__init__.py", "") .replace(os.sep + "__init__.py", "")
@@ -133,7 +62,7 @@ def import_modules(self):
# Some Python libraries are modules: individual *.py files # Some Python libraries are modules: individual *.py files
# found in the site-packages directory # found in the site-packages directory
for path in find(root, "*.py", recursive=False): for path in fs.find(root, "*.py", recursive=False):
modules.append( modules.append(
path.replace(root + os.sep, "", 1).replace(".py", "").replace("/", ".") path.replace(root + os.sep, "", 1).replace(".py", "").replace("/", ".")
) )
@@ -160,6 +89,208 @@ def skip_modules(self):
""" """
return [] return []
def view_file_conflicts(self, view, merge_map):
"""Report all file conflicts, excepting special cases for python.
Specifically, this does not report errors for duplicate
__init__.py files for packages in the same namespace.
"""
conflicts = list(dst for src, dst in merge_map.items() if os.path.exists(dst))
if conflicts and self.py_namespace:
ext_map = view.extensions_layout.extension_map(self.extendee_spec)
namespaces = set(x.package.py_namespace for x in ext_map.values())
namespace_re = r"site-packages/{0}/__init__.py".format(self.py_namespace)
find_namespace = lang.match_predicate(namespace_re)
if self.py_namespace in namespaces:
conflicts = list(x for x in conflicts if not find_namespace(x))
return conflicts
def add_files_to_view(self, view, merge_map, skip_if_exists=True):
bin_dir = self.spec.prefix.bin
python_prefix = self.extendee_spec.prefix
python_is_external = self.extendee_spec.external
global_view = fs.same_path(python_prefix, view.get_projection_for_spec(self.spec))
for src, dst in merge_map.items():
if os.path.exists(dst):
continue
elif global_view or not fs.path_contains_subdirectory(src, bin_dir):
view.link(src, dst)
elif not os.path.islink(src):
shutil.copy2(src, dst)
is_script = fs.is_nonsymlink_exe_with_shebang(src)
if is_script and not python_is_external:
fs.filter_file(
python_prefix,
os.path.abspath(view.get_projection_for_spec(self.spec)),
dst,
)
else:
orig_link_target = os.path.realpath(src)
new_link_target = os.path.abspath(merge_map[orig_link_target])
view.link(new_link_target, dst)
def remove_files_from_view(self, view, merge_map):
ignore_namespace = False
if self.py_namespace:
ext_map = view.extensions_layout.extension_map(self.extendee_spec)
remaining_namespaces = set(
spec.package.py_namespace for name, spec in ext_map.items() if name != self.name
)
if self.py_namespace in remaining_namespaces:
namespace_init = lang.match_predicate(
r"site-packages/{0}/__init__.py".format(self.py_namespace)
)
ignore_namespace = True
bin_dir = self.spec.prefix.bin
global_view = self.extendee_spec.prefix == view.get_projection_for_spec(self.spec)
to_remove = []
for src, dst in merge_map.items():
if ignore_namespace and namespace_init(dst):
continue
if global_view or not fs.path_contains_subdirectory(src, bin_dir):
to_remove.append(dst)
else:
os.remove(dst)
view.remove_files(to_remove)
def test(self):
"""Attempts to import modules of the installed package."""
# Make sure we are importing the installed modules,
# not the ones in the source directory
for module in self.import_modules:
self.run_test(
inspect.getmodule(self).python.path,
["-c", "import {0}".format(module)],
purpose="checking import of {0}".format(module),
work_dir="spack-test",
)
class PythonPackage(PythonExtension):
"""Specialized class for packages that are built using pip."""
#: Package name, version, and extension on PyPI
pypi = None # type: Optional[str]
maintainers = ["adamjstewart", "pradyunsg"]
# To be used in UI queries that require to know which
# build-system class we are using
build_system_class = "PythonPackage"
#: Legacy buildsystem attribute used to deserialize and install old specs
legacy_buildsystem = "python_pip"
#: Callback names for install-time test
install_time_test_callbacks = ["test"]
build_system("python_pip")
with spack.multimethod.when("build_system=python_pip"):
extends("python")
depends_on("py-pip", type="build")
# FIXME: technically wheel is only needed when building from source, not when
# installing a downloaded wheel, but I don't want to add wheel as a dep to every
# package manually
depends_on("py-wheel", type="build")
py_namespace = None # type: Optional[str]
@lang.classproperty
def homepage(cls):
if cls.pypi:
name = cls.pypi.split("/")[0]
return "https://pypi.org/project/" + name + "/"
@lang.classproperty
def url(cls):
if cls.pypi:
return "https://files.pythonhosted.org/packages/source/" + cls.pypi[0] + "/" + cls.pypi
@lang.classproperty
def list_url(cls):
if cls.pypi:
name = cls.pypi.split("/")[0]
return "https://pypi.org/simple/" + name + "/"
@property
def headers(self):
"""Discover header files in platlib."""
# Headers may be in either location
include = self.prefix.join(self.spec["python"].package.include)
platlib = self.prefix.join(self.spec["python"].package.platlib)
headers = fs.find_all_headers(include) + fs.find_all_headers(platlib)
if headers:
return headers
msg = "Unable to locate {} headers in {} or {}"
raise NoHeadersError(msg.format(self.spec.name, include, platlib))
@property
def libs(self):
"""Discover libraries in platlib."""
# Remove py- prefix in package name
library = "lib" + self.spec.name[3:].replace("-", "?")
root = self.prefix.join(self.spec["python"].package.platlib)
for shared in [True, False]:
libs = fs.find_libraries(library, root, shared=shared, recursive=True)
if libs:
return libs
msg = "Unable to recursively locate {} libraries in {}"
raise NoLibrariesError(msg.format(self.spec.name, root))
@spack.builder.builder("python_pip")
class PythonPipBuilder(BaseBuilder):
phases = ("install",)
#: Names associated with package methods in the old build-system format
legacy_methods = ("test",)
#: Same as legacy_methods, but the signature is different
legacy_long_methods = ("install_options", "global_options", "config_settings")
#: Names associated with package attributes in the old build-system format
legacy_attributes = ("build_directory", "install_time_test_callbacks")
#: Callback names for install-time test
install_time_test_callbacks = ["test"]
@staticmethod
def std_args(cls):
return [
# Verbose
"-vvv",
# Disable prompting for input
"--no-input",
# Disable the cache
"--no-cache-dir",
# Don't check to see if pip is up-to-date
"--disable-pip-version-check",
# Install packages
"install",
# Don't install package dependencies
"--no-deps",
# Overwrite existing packages
"--ignore-installed",
# Use env vars like PYTHONPATH
"--no-build-isolation",
# Don't warn that prefix.bin is not in PATH
"--no-warn-script-location",
# Ignore the PyPI package index
"--no-index",
]
@property @property
def build_directory(self): def build_directory(self):
"""The root directory of the Python package. """The root directory of the Python package.
@@ -170,11 +301,10 @@ def build_directory(self):
* ``setup.cfg`` * ``setup.cfg``
* ``setup.py`` * ``setup.py``
""" """
return self.stage.source_path return self.pkg.stage.source_path
def config_settings(self, spec, prefix): def config_settings(self, spec, prefix):
"""Configuration settings to be passed to the PEP 517 build backend. """Configuration settings to be passed to the PEP 517 build backend.
Requires pip 22.1+, which requires Python 3.7+. Requires pip 22.1+, which requires Python 3.7+.
Args: Args:
@@ -211,10 +341,10 @@ def global_options(self, spec, prefix):
""" """
return [] return []
def install(self, spec, prefix): def install(self, pkg, spec, prefix):
"""Install everything from build directory.""" """Install everything from build directory."""
args = PythonPackage._std_args(self) + ["--prefix=" + prefix] args = PythonPipBuilder.std_args(pkg) + ["--prefix=" + prefix]
for key, value in self.config_settings(spec, prefix).items(): for key, value in self.config_settings(spec, prefix).items():
if spec["py-pip"].version < Version("22.1"): if spec["py-pip"].version < Version("22.1"):
@@ -223,137 +353,21 @@ def install(self, spec, prefix):
"pip 22.1+. Add the following line to the package to fix this:\n\n" "pip 22.1+. Add the following line to the package to fix this:\n\n"
' depends_on("py-pip@22.1:", type="build")'.format(spec.name) ' depends_on("py-pip@22.1:", type="build")'.format(spec.name)
) )
args.append("--config-settings={}={}".format(key, value)) args.append("--config-settings={}={}".format(key, value))
for option in self.install_options(spec, prefix): for option in self.install_options(spec, prefix):
args.append("--install-option=" + option) args.append("--install-option=" + option)
for option in self.global_options(spec, prefix): for option in self.global_options(spec, prefix):
args.append("--global-option=" + option) args.append("--global-option=" + option)
if self.stage.archive_file and self.stage.archive_file.endswith(".whl"): if pkg.stage.archive_file and pkg.stage.archive_file.endswith(".whl"):
args.append(self.stage.archive_file) args.append(pkg.stage.archive_file)
else: else:
args.append(".") args.append(".")
pip = inspect.getmodule(self).pip pip = inspect.getmodule(pkg).pip
with working_dir(self.build_directory): with fs.working_dir(self.build_directory):
pip(*args) pip(*args)
@property spack.builder.run_after("install")(execute_install_time_tests)
def headers(self):
"""Discover header files in platlib."""
# Headers may be in either location
include = self.prefix.join(self.spec["python"].package.include)
platlib = self.prefix.join(self.spec["python"].package.platlib)
headers = find_all_headers(include) + find_all_headers(platlib)
if headers:
return headers
msg = "Unable to locate {} headers in {} or {}"
raise NoHeadersError(msg.format(self.spec.name, include, platlib))
@property
def libs(self):
"""Discover libraries in platlib."""
# Remove py- prefix in package name
library = "lib" + self.spec.name[3:].replace("-", "?")
root = self.prefix.join(self.spec["python"].package.platlib)
for shared in [True, False]:
libs = find_libraries(library, root, shared=shared, recursive=True)
if libs:
return libs
msg = "Unable to recursively locate {} libraries in {}"
raise NoLibrariesError(msg.format(self.spec.name, root))
# Testing
def test(self):
"""Attempts to import modules of the installed package."""
# Make sure we are importing the installed modules,
# not the ones in the source directory
for module in self.import_modules:
self.run_test(
inspect.getmodule(self).python.path,
["-c", "import {0}".format(module)],
purpose="checking import of {0}".format(module),
work_dir="spack-test",
)
run_after("install")(PackageBase._run_default_install_time_test_callbacks)
# Check that self.prefix is there after installation
run_after("install")(PackageBase.sanity_check_prefix)
def view_file_conflicts(self, view, merge_map):
"""Report all file conflicts, excepting special cases for python.
Specifically, this does not report errors for duplicate
__init__.py files for packages in the same namespace.
"""
conflicts = list(dst for src, dst in merge_map.items() if os.path.exists(dst))
if conflicts and self.py_namespace:
ext_map = view.extensions_layout.extension_map(self.extendee_spec)
namespaces = set(x.package.py_namespace for x in ext_map.values())
namespace_re = r"site-packages/{0}/__init__.py".format(self.py_namespace)
find_namespace = match_predicate(namespace_re)
if self.py_namespace in namespaces:
conflicts = list(x for x in conflicts if not find_namespace(x))
return conflicts
def add_files_to_view(self, view, merge_map, skip_if_exists=True):
bin_dir = self.spec.prefix.bin
python_prefix = self.extendee_spec.prefix
python_is_external = self.extendee_spec.external
global_view = same_path(python_prefix, view.get_projection_for_spec(self.spec))
for src, dst in merge_map.items():
if os.path.exists(dst):
continue
elif global_view or not path_contains_subdirectory(src, bin_dir):
view.link(src, dst)
elif not os.path.islink(src):
shutil.copy2(src, dst)
is_script = is_nonsymlink_exe_with_shebang(src)
if is_script and not python_is_external:
filter_file(
python_prefix,
os.path.abspath(view.get_projection_for_spec(self.spec)),
dst,
)
else:
orig_link_target = os.path.realpath(src)
new_link_target = os.path.abspath(merge_map[orig_link_target])
view.link(new_link_target, dst)
def remove_files_from_view(self, view, merge_map):
ignore_namespace = False
if self.py_namespace:
ext_map = view.extensions_layout.extension_map(self.extendee_spec)
remaining_namespaces = set(
spec.package.py_namespace for name, spec in ext_map.items() if name != self.name
)
if self.py_namespace in remaining_namespaces:
namespace_init = match_predicate(
r"site-packages/{0}/__init__.py".format(self.py_namespace)
)
ignore_namespace = True
bin_dir = self.spec.prefix.bin
global_view = self.extendee_spec.prefix == view.get_projection_for_spec(self.spec)
to_remove = []
for src, dst in merge_map.items():
if ignore_namespace and namespace_init(dst):
continue
if global_view or not path_contains_subdirectory(src, bin_dir):
to_remove.append(dst)
else:
os.remove(dst)
view.remove_files(to_remove)

View File

@@ -2,82 +2,85 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import inspect import inspect
from llnl.util.filesystem import working_dir from llnl.util.filesystem import working_dir
from spack.directives import depends_on import spack.builder
from spack.package_base import PackageBase, run_after import spack.package_base
from spack.directives import build_system, depends_on
from ._checks import BaseBuilder, execute_build_time_tests
class QMakePackage(PackageBase): class QMakePackage(spack.package_base.PackageBase):
"""Specialized class for packages built using qmake. """Specialized class for packages built using qmake.
For more information on the qmake build system, see: For more information on the qmake build system, see:
http://doc.qt.io/qt-5/qmake-manual.html http://doc.qt.io/qt-5/qmake-manual.html
This class provides three phases that can be overridden:
1. :py:meth:`~.QMakePackage.qmake`
2. :py:meth:`~.QMakePackage.build`
3. :py:meth:`~.QMakePackage.install`
They all have sensible defaults and for many packages the only thing
necessary will be to override :py:meth:`~.QMakePackage.qmake_args`.
""" """
#: Phases of a qmake package
phases = ["qmake", "build", "install"]
#: This attribute is used in UI queries that need to know the build #: This attribute is used in UI queries that need to know the build
#: system base class #: system base class
build_system_class = "QMakePackage" build_system_class = "QMakePackage"
#: Legacy buildsystem attribute used to deserialize and install old specs
legacy_buildsystem = "qmake"
build_system("qmake")
depends_on("qt", type="build", when="build_system=qmake")
@spack.builder.builder("qmake")
class QMakeBuilder(BaseBuilder):
"""The qmake builder provides three phases that can be overridden:
1. :py:meth:`~.QMakeBuilder.qmake`
2. :py:meth:`~.QMakeBuilder.build`
3. :py:meth:`~.QMakeBuilder.install`
They all have sensible defaults and for many packages the only thing
necessary will be to override :py:meth:`~.QMakeBuilder.qmake_args`.
"""
phases = ("qmake", "build", "install")
#: Names associated with package methods in the old build-system format
legacy_methods = ("qmake_args", "check")
#: Names associated with package attributes in the old build-system format
legacy_attributes = ("build_directory", "build_time_test_callbacks")
#: Callback names for build-time test #: Callback names for build-time test
build_time_test_callbacks = ["check"] build_time_test_callbacks = ["check"]
depends_on("qt", type="build")
@property @property
def build_directory(self): def build_directory(self):
"""The directory containing the ``*.pro`` file.""" """The directory containing the ``*.pro`` file."""
return self.stage.source_path return self.stage.source_path
def qmake_args(self): def qmake_args(self):
"""Produces a list containing all the arguments that must be passed to """List of arguments passed to qmake."""
qmake
"""
return [] return []
def qmake(self, spec, prefix): def qmake(self, pkg, spec, prefix):
"""Run ``qmake`` to configure the project and generate a Makefile.""" """Run ``qmake`` to configure the project and generate a Makefile."""
with working_dir(self.build_directory): with working_dir(self.build_directory):
inspect.getmodule(self).qmake(*self.qmake_args()) inspect.getmodule(self.pkg).qmake(*self.qmake_args())
def build(self, spec, prefix): def build(self, pkg, spec, prefix):
"""Make the build targets""" """Make the build targets"""
with working_dir(self.build_directory): with working_dir(self.build_directory):
inspect.getmodule(self).make() inspect.getmodule(self.pkg).make()
def install(self, spec, prefix): def install(self, pkg, spec, prefix):
"""Make the install targets""" """Make the install targets"""
with working_dir(self.build_directory): with working_dir(self.build_directory):
inspect.getmodule(self).make("install") inspect.getmodule(self.pkg).make("install")
# Tests
def check(self): def check(self):
"""Searches the Makefile for a ``check:`` target and runs it if found.""" """Search the Makefile for a ``check:`` target and runs it if found."""
with working_dir(self.build_directory): with working_dir(self.build_directory):
self._if_make_target_execute("check") self._if_make_target_execute("check")
run_after("build")(PackageBase._run_default_build_time_test_callbacks) spack.builder.run_after("build")(execute_build_time_tests)
# Check that self.prefix is there after installation
run_after("install")(PackageBase.sanity_check_prefix)

View File

@@ -3,30 +3,64 @@
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import inspect import inspect
from typing import Optional from typing import Optional, Tuple
import llnl.util.lang as lang import llnl.util.lang as lang
from spack.directives import extends from spack.directives import extends
from spack.package_base import PackageBase, run_after
from .generic import GenericBuilder, Package
class RPackage(PackageBase): class RBuilder(GenericBuilder):
"""The R builder provides a single phase that can be overridden:
1. :py:meth:`~.RBuilder.install`
It has sensible defaults, and for many packages the only thing
necessary will be to add dependencies.
"""
#: Names associated with package methods in the old build-system format
legacy_methods = (
"configure_args",
"configure_vars",
) + GenericBuilder.legacy_methods # type: Tuple[str, ...]
def configure_args(self):
"""Arguments to pass to install via ``--configure-args``."""
return []
def configure_vars(self):
"""Arguments to pass to install via ``--configure-vars``."""
return []
def install(self, pkg, spec, prefix):
"""Installs an R package."""
config_args = self.configure_args()
config_vars = self.configure_vars()
args = ["--vanilla", "CMD", "INSTALL"]
if config_args:
args.append("--configure-args={0}".format(" ".join(config_args)))
if config_vars:
args.append("--configure-vars={0}".format(" ".join(config_vars)))
args.extend(["--library={0}".format(self.pkg.module.r_lib_dir), self.stage.source_path])
inspect.getmodule(self.pkg).R(*args)
class RPackage(Package):
"""Specialized class for packages that are built using R. """Specialized class for packages that are built using R.
For more information on the R build system, see: For more information on the R build system, see:
https://stat.ethz.ch/R-manual/R-devel/library/utils/html/INSTALL.html https://stat.ethz.ch/R-manual/R-devel/library/utils/html/INSTALL.html
This class provides a single phase that can be overridden:
1. :py:meth:`~.RPackage.install`
It has sensible defaults, and for many packages the only thing
necessary will be to add dependencies
""" """
phases = ["install"]
# package attributes that can be expanded to set the homepage, url, # package attributes that can be expanded to set the homepage, url,
# list_url, and git values # list_url, and git values
# For CRAN packages # For CRAN packages
@@ -35,6 +69,8 @@ class RPackage(PackageBase):
# For Bioconductor packages # For Bioconductor packages
bioc = None # type: Optional[str] bioc = None # type: Optional[str]
GenericBuilder = RBuilder
maintainers = ["glennpj"] maintainers = ["glennpj"]
#: This attribute is used in UI queries that need to know the build #: This attribute is used in UI queries that need to know the build
@@ -70,32 +106,3 @@ def list_url(cls):
def git(self): def git(self):
if self.bioc: if self.bioc:
return "https://git.bioconductor.org/packages/" + self.bioc return "https://git.bioconductor.org/packages/" + self.bioc
def configure_args(self):
"""Arguments to pass to install via ``--configure-args``."""
return []
def configure_vars(self):
"""Arguments to pass to install via ``--configure-vars``."""
return []
def install(self, spec, prefix):
"""Installs an R package."""
config_args = self.configure_args()
config_vars = self.configure_vars()
args = ["--vanilla", "CMD", "INSTALL"]
if config_args:
args.append("--configure-args={0}".format(" ".join(config_args)))
if config_vars:
args.append("--configure-vars={0}".format(" ".join(config_vars)))
args.extend(["--library={0}".format(self.module.r_lib_dir), self.stage.source_path])
inspect.getmodule(self).R(*args)
# Check that self.prefix is there after installation
run_after("install")(PackageBase.sanity_check_prefix)

View File

@@ -3,14 +3,15 @@
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os import os
from typing import Optional from typing import Optional, Tuple
import llnl.util.filesystem as fs
import llnl.util.lang as lang import llnl.util.lang as lang
import llnl.util.tty as tty import llnl.util.tty as tty
from llnl.util.filesystem import working_dir
import spack.builder
from spack.build_environment import SPACK_NO_PARALLEL_MAKE, determine_number_of_jobs from spack.build_environment import SPACK_NO_PARALLEL_MAKE, determine_number_of_jobs
from spack.directives import extends from spack.directives import build_system, extends
from spack.package_base import PackageBase from spack.package_base import PackageBase
from spack.util.environment import env_flag from spack.util.environment import env_flag
from spack.util.executable import Executable, ProcessError from spack.util.executable import Executable, ProcessError
@@ -19,34 +20,52 @@
class RacketPackage(PackageBase): class RacketPackage(PackageBase):
"""Specialized class for packages that are built using Racket's """Specialized class for packages that are built using Racket's
`raco pkg install` and `raco setup` commands. `raco pkg install` and `raco setup` commands.
This class provides the following phases that can be overridden:
* install
* setup
""" """
#: Package name, version, and extension on PyPI #: Package name, version, and extension on PyPI
maintainers = ["elfprince13"] maintainers = ["elfprince13"]
# Default phases
phases = ["install"]
# To be used in UI queries that require to know which # To be used in UI queries that require to know which
# build-system class we are using # build-system class we are using
build_system_class = "RacketPackage" build_system_class = "RacketPackage"
#: Legacy buildsystem attribute used to deserialize and install old specs
legacy_buildsystem = "racket"
extends("racket") build_system("racket")
extends("racket", when="build_system=racket")
pkgs = False
subdirectory = None # type: Optional[str]
racket_name = None # type: Optional[str] racket_name = None # type: Optional[str]
parallel = True parallel = True
@lang.classproperty @lang.classproperty
def homepage(cls): def homepage(cls):
if cls.pkgs: if cls.racket_name:
return "https://pkgs.racket-lang.org/package/{0}".format(cls.racket_name) return "https://pkgs.racket-lang.org/package/{0}".format(cls.racket_name)
return None
@spack.builder.builder("racket")
class RacketBuilder(spack.builder.Builder):
"""The Racket builder provides an ``install`` phase that can be overridden."""
phases = ("install",)
#: Names associated with package methods in the old build-system format
legacy_methods = tuple() # type: Tuple[str, ...]
#: Names associated with package attributes in the old build-system format
legacy_attributes = ("build_directory", "build_time_test_callbacks", "subdirectory")
#: Callback names for build-time test
build_time_test_callbacks = ["check"]
racket_name = None # type: Optional[str]
@property
def subdirectory(self):
if self.racket_name:
return "pkgs/{0}".format(self.pkg.racket_name)
return None
@property @property
def build_directory(self): def build_directory(self):
@@ -55,25 +74,25 @@ def build_directory(self):
ret = os.path.join(ret, self.subdirectory) ret = os.path.join(ret, self.subdirectory)
return ret return ret
def install(self, spec, prefix): def install(self, pkg, spec, prefix):
"""Install everything from build directory.""" """Install everything from build directory."""
raco = Executable("raco") raco = Executable("raco")
with working_dir(self.build_directory): with fs.working_dir(self.build_directory):
allow_parallel = self.parallel and (not env_flag(SPACK_NO_PARALLEL_MAKE)) parallel = self.pkg.parallel and (not env_flag(SPACK_NO_PARALLEL_MAKE))
args = [ args = [
"pkg", "pkg",
"install", "install",
"-t", "-t",
"dir", "dir",
"-n", "-n",
self.racket_name, self.pkg.racket_name,
"--deps", "--deps",
"fail", "fail",
"--ignore-implies", "--ignore-implies",
"--copy", "--copy",
"-i", "-i",
"-j", "-j",
str(determine_number_of_jobs(allow_parallel)), str(determine_number_of_jobs(parallel)),
"--", "--",
os.getcwd(), os.getcwd(),
] ]
@@ -82,9 +101,8 @@ def install(self, spec, prefix):
except ProcessError: except ProcessError:
args.insert(-2, "--skip-installed") args.insert(-2, "--skip-installed")
raco(*args) raco(*args)
tty.warn( msg = (
( "Racket package {0} was already installed, uninstalling via "
"Racket package {0} was already installed, uninstalling via " "Spack may make someone unhappy!"
"Spack may make someone unhappy!"
).format(self.racket_name)
) )
tty.warn(msg.format(self.pkg.racket_name))

View File

@@ -2,35 +2,49 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import glob import glob
import inspect import inspect
from spack.directives import extends import spack.builder
from spack.package_base import PackageBase, run_after import spack.package_base
from spack.directives import build_system, extends
from ._checks import BaseBuilder
class RubyPackage(PackageBase): class RubyPackage(spack.package_base.PackageBase):
"""Specialized class for building Ruby gems. """Specialized class for building Ruby gems."""
This class provides two phases that can be overridden if required:
#. :py:meth:`~.RubyPackage.build`
#. :py:meth:`~.RubyPackage.install`
"""
maintainers = ["Kerilk"] maintainers = ["Kerilk"]
#: Phases of a Ruby package
phases = ["build", "install"]
#: This attribute is used in UI queries that need to know the build #: This attribute is used in UI queries that need to know the build
#: system base class #: system base class
build_system_class = "RubyPackage" build_system_class = "RubyPackage"
#: Legacy buildsystem attribute used to deserialize and install old specs
legacy_buildsystem = "ruby"
extends("ruby") build_system("ruby")
def build(self, spec, prefix): extends("ruby", when="build_system=ruby")
@spack.builder.builder("ruby")
class RubyBuilder(BaseBuilder):
"""The Ruby builder provides two phases that can be overridden if required:
#. :py:meth:`~.RubyBuilder.build`
#. :py:meth:`~.RubyBuilder.install`
"""
phases = ("build", "install")
#: Names associated with package methods in the old build-system format
legacy_methods = ()
#: Names associated with package attributes in the old build-system format
legacy_attributes = ()
def build(self, pkg, spec, prefix):
"""Build a Ruby gem.""" """Build a Ruby gem."""
# ruby-rake provides both rake.gemspec and Rakefile, but only # ruby-rake provides both rake.gemspec and Rakefile, but only
@@ -38,15 +52,15 @@ def build(self, spec, prefix):
gemspecs = glob.glob("*.gemspec") gemspecs = glob.glob("*.gemspec")
rakefiles = glob.glob("Rakefile") rakefiles = glob.glob("Rakefile")
if gemspecs: if gemspecs:
inspect.getmodule(self).gem("build", "--norc", gemspecs[0]) inspect.getmodule(self.pkg).gem("build", "--norc", gemspecs[0])
elif rakefiles: elif rakefiles:
jobs = inspect.getmodule(self).make_jobs jobs = inspect.getmodule(self.pkg).make_jobs
inspect.getmodule(self).rake("package", "-j{0}".format(jobs)) inspect.getmodule(self.pkg).rake("package", "-j{0}".format(jobs))
else: else:
# Some Ruby packages only ship `*.gem` files, so nothing to build # Some Ruby packages only ship `*.gem` files, so nothing to build
pass pass
def install(self, spec, prefix): def install(self, pkg, spec, prefix):
"""Install a Ruby gem. """Install a Ruby gem.
The ruby package sets ``GEM_HOME`` to tell gem where to install to.""" The ruby package sets ``GEM_HOME`` to tell gem where to install to."""
@@ -56,9 +70,6 @@ def install(self, spec, prefix):
# if --install-dir is not used, GEM_PATH is deleted from the # if --install-dir is not used, GEM_PATH is deleted from the
# environement, and Gems required to build native extensions will # environement, and Gems required to build native extensions will
# not be found. Those extensions are built during `gem install`. # not be found. Those extensions are built during `gem install`.
inspect.getmodule(self).gem( inspect.getmodule(self.pkg).gem(
"install", "--norc", "--ignore-dependencies", "--install-dir", prefix, gems[0] "install", "--norc", "--ignore-dependencies", "--install-dir", prefix, gems[0]
) )
# Check that self.prefix is there after installation
run_after("install")(PackageBase.sanity_check_prefix)

View File

@@ -2,63 +2,75 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import inspect import inspect
from spack.directives import depends_on import spack.builder
from spack.package_base import PackageBase, run_after import spack.package_base
from spack.directives import build_system, depends_on
from ._checks import BaseBuilder, execute_build_time_tests
class SConsPackage(PackageBase): class SConsPackage(spack.package_base.PackageBase):
"""Specialized class for packages built using SCons. """Specialized class for packages built using SCons.
See http://scons.org/documentation.html for more information. See http://scons.org/documentation.html for more information.
This class provides the following phases that can be overridden:
1. :py:meth:`~.SConsPackage.build`
2. :py:meth:`~.SConsPackage.install`
Packages that use SCons as a build system are less uniform than packages
that use other build systems. Developers can add custom subcommands or
variables that control the build. You will likely need to override
:py:meth:`~.SConsPackage.build_args` to pass the appropriate variables.
""" """
#: Phases of a SCons package
phases = ["build", "install"]
#: To be used in UI queries that require to know which #: To be used in UI queries that require to know which
#: build-system class we are using #: build-system class we are using
build_system_class = "SConsPackage" build_system_class = "SConsPackage"
#: Callback names for build-time test #: Callback names for build-time test
build_time_test_callbacks = ["build_test"] build_time_test_callbacks = ["build_test"]
#: Legacy buildsystem attribute used to deserialize and install old specs
legacy_buildsystem = "scons"
depends_on("scons", type="build") build_system("scons")
def build_args(self, spec, prefix): depends_on("scons", type="build", when="build_system=scons")
@spack.builder.builder("scons")
class SConsBuilder(BaseBuilder):
"""The Scons builder provides the following phases that can be overridden:
1. :py:meth:`~.SConsBuilder.build`
2. :py:meth:`~.SConsBuilder.install`
Packages that use SCons as a build system are less uniform than packages that use
other build systems. Developers can add custom subcommands or variables that
control the build. You will likely need to override
:py:meth:`~.SConsBuilder.build_args` to pass the appropriate variables.
"""
#: Phases of a SCons package
phases = ("build", "install")
#: Names associated with package methods in the old build-system format
legacy_methods = ("build_args", "install_args", "build_test")
#: Names associated with package attributes in the old build-system format
legacy_attributes = ()
def build_args(self):
"""Arguments to pass to build.""" """Arguments to pass to build."""
return [] return []
def build(self, spec, prefix): def build(self, pkg, spec, prefix):
"""Build the package.""" """Build the package."""
args = self.build_args(spec, prefix) args = self.build_args()
inspect.getmodule(self.pkg).scons(*args)
inspect.getmodule(self).scons(*args) def install_args(self):
def install_args(self, spec, prefix):
"""Arguments to pass to install.""" """Arguments to pass to install."""
return [] return []
def install(self, spec, prefix): def install(self, pkg, spec, prefix):
"""Install the package.""" """Install the package."""
args = self.install_args(spec, prefix) args = self.install_args()
inspect.getmodule(self).scons("install", *args) inspect.getmodule(self.pkg).scons("install", *args)
# Testing
def build_test(self): def build_test(self):
"""Run unit tests after build. """Run unit tests after build.
@@ -68,7 +80,4 @@ def build_test(self):
""" """
pass pass
run_after("build")(PackageBase._run_default_build_time_test_callbacks) spack.builder.run_after("build")(execute_build_time_tests)
# Check that self.prefix is there after installation
run_after("install")(PackageBase.sanity_check_prefix)

View File

@@ -2,7 +2,6 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import inspect import inspect
import os import os
import re import re
@@ -10,28 +9,20 @@
import llnl.util.tty as tty import llnl.util.tty as tty
from llnl.util.filesystem import find, join_path, working_dir from llnl.util.filesystem import find, join_path, working_dir
from spack.directives import depends_on, extends import spack.builder
from spack.package_base import PackageBase, run_after import spack.package_base
from spack.directives import build_system, depends_on, extends
from spack.multimethod import when
from ._checks import BaseBuilder, execute_install_time_tests
class SIPPackage(PackageBase): class SIPPackage(spack.package_base.PackageBase):
"""Specialized class for packages that are built using the """Specialized class for packages that are built using the
SIP build system. See https://www.riverbankcomputing.com/software/sip/intro SIP build system. See https://www.riverbankcomputing.com/software/sip/intro
for more information. for more information.
This class provides the following phases that can be overridden:
* configure
* build
* install
The configure phase already adds a set of default flags. To see more
options, run ``python configure.py --help``.
""" """
# Default phases
phases = ["configure", "build", "install"]
# To be used in UI queries that require to know which # To be used in UI queries that require to know which
# build-system class we are using # build-system class we are using
build_system_class = "SIPPackage" build_system_class = "SIPPackage"
@@ -41,11 +32,15 @@ class SIPPackage(PackageBase):
#: Callback names for install-time test #: Callback names for install-time test
install_time_test_callbacks = ["test"] install_time_test_callbacks = ["test"]
#: Legacy buildsystem attribute used to deserialize and install old specs
legacy_buildsystem = "sip"
extends("python") build_system("sip")
depends_on("qt") with when("build_system=sip"):
depends_on("py-sip") extends("python")
depends_on("qt")
depends_on("py-sip")
@property @property
def import_modules(self): def import_modules(self):
@@ -95,11 +90,51 @@ def python(self, *args, **kwargs):
"""The python ``Executable``.""" """The python ``Executable``."""
inspect.getmodule(self).python(*args, **kwargs) inspect.getmodule(self).python(*args, **kwargs)
def test(self):
"""Attempts to import modules of the installed package."""
# Make sure we are importing the installed modules,
# not the ones in the source directory
for module in self.import_modules:
self.run_test(
inspect.getmodule(self).python.path,
["-c", "import {0}".format(module)],
purpose="checking import of {0}".format(module),
work_dir="spack-test",
)
@spack.builder.builder("sip")
class SIPBuilder(BaseBuilder):
"""The SIP builder provides the following phases that can be overridden:
* configure
* build
* install
The configure phase already adds a set of default flags. To see more
options, run ``python configure.py --help``.
"""
phases = ("configure", "build", "install")
#: Names associated with package methods in the old build-system format
legacy_methods = ("configure_file", "configure_args", "build_args", "install_args")
#: Names associated with package attributes in the old build-system format
legacy_attributes = (
"build_targets",
"install_targets",
"build_time_test_callbacks",
"install_time_test_callbacks",
"build_directory",
)
def configure_file(self): def configure_file(self):
"""Returns the name of the configure file to use.""" """Returns the name of the configure file to use."""
return "configure.py" return "configure.py"
def configure(self, spec, prefix): def configure(self, pkg, spec, prefix):
"""Configure the package.""" """Configure the package."""
configure = self.configure_file() configure = self.configure_file()
@@ -118,7 +153,7 @@ def configure(self, spec, prefix):
"--bindir", "--bindir",
prefix.bin, prefix.bin,
"--destdir", "--destdir",
inspect.getmodule(self).python_platlib, inspect.getmodule(self.pkg).python_platlib,
] ]
) )
@@ -128,53 +163,35 @@ def configure_args(self):
"""Arguments to pass to configure.""" """Arguments to pass to configure."""
return [] return []
def build(self, spec, prefix): def build(self, pkg, spec, prefix):
"""Build the package.""" """Build the package."""
args = self.build_args() args = self.build_args()
inspect.getmodule(self).make(*args) inspect.getmodule(self.pkg).make(*args)
def build_args(self): def build_args(self):
"""Arguments to pass to build.""" """Arguments to pass to build."""
return [] return []
def install(self, spec, prefix): def install(self, pkg, spec, prefix):
"""Install the package.""" """Install the package."""
args = self.install_args() args = self.install_args()
inspect.getmodule(self).make("install", parallel=False, *args) inspect.getmodule(self.pkg).make("install", parallel=False, *args)
def install_args(self): def install_args(self):
"""Arguments to pass to install.""" """Arguments to pass to install."""
return [] return []
# Testing spack.builder.run_after("install")(execute_install_time_tests)
def test(self): @spack.builder.run_after("install")
"""Attempts to import modules of the installed package."""
# Make sure we are importing the installed modules,
# not the ones in the source directory
for module in self.import_modules:
self.run_test(
inspect.getmodule(self).python.path,
["-c", "import {0}".format(module)],
purpose="checking import of {0}".format(module),
work_dir="spack-test",
)
run_after("install")(PackageBase._run_default_install_time_test_callbacks)
# Check that self.prefix is there after installation
run_after("install")(PackageBase.sanity_check_prefix)
@run_after("install")
def extend_path_setup(self): def extend_path_setup(self):
# See github issue #14121 and PR #15297 # See github issue #14121 and PR #15297
module = self.spec["py-sip"].variants["module"].value module = self.pkg.spec["py-sip"].variants["module"].value
if module != "sip": if module != "sip":
module = module.split(".")[0] module = module.split(".")[0]
with working_dir(inspect.getmodule(self).python_platlib): with working_dir(inspect.getmodule(self.pkg).python_platlib):
with open(os.path.join(module, "__init__.py"), "a") as f: with open(os.path.join(module, "__init__.py"), "a") as f:
f.write("from pkgutil import extend_path\n") f.write("from pkgutil import extend_path\n")
f.write("__path__ = extend_path(__path__, __name__)\n") f.write("__path__ = extend_path(__path__, __name__)\n")

View File

@@ -2,21 +2,38 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import inspect import inspect
from llnl.util.filesystem import working_dir from llnl.util.filesystem import working_dir
from spack.directives import depends_on import spack.builder
from spack.package_base import PackageBase, run_after import spack.package_base
from spack.directives import build_system, depends_on
from ._checks import BaseBuilder, execute_build_time_tests, execute_install_time_tests
class WafPackage(PackageBase): class WafPackage(spack.package_base.PackageBase):
"""Specialized class for packages that are built using the """Specialized class for packages that are built using the
Waf build system. See https://waf.io/book/ for more information. Waf build system. See https://waf.io/book/ for more information.
"""
This class provides the following phases that can be overridden: # To be used in UI queries that require to know which
# build-system class we are using
build_system_class = "WafPackage"
#: Legacy buildsystem attribute used to deserialize and install old specs
legacy_buildsystem = "waf"
build_system("waf")
# Much like AutotoolsPackage does not require automake and autoconf
# to build, WafPackage does not require waf to build. It only requires
# python to run the waf build script.
depends_on("python@2.5:", type="build", when="build_system=waf")
@spack.builder.builder("waf")
class WafBuilder(BaseBuilder):
"""The WAF builder provides the following phases that can be overridden:
* configure * configure
* build * build
@@ -40,12 +57,25 @@ class WafPackage(PackageBase):
function, which passes ``--prefix=/path/to/installation/prefix``. function, which passes ``--prefix=/path/to/installation/prefix``.
""" """
# Default phases phases = ("configure", "build", "install")
phases = ["configure", "build", "install"]
# To be used in UI queries that require to know which #: Names associated with package methods in the old build-system format
# build-system class we are using legacy_methods = (
build_system_class = "WafPackage" "build_test",
"install_test",
"configure_args",
"build_args",
"install_args",
"build_test",
"install_test",
)
#: Names associated with package attributes in the old build-system format
legacy_attributes = (
"build_time_test_callbacks",
"build_time_test_callbacks",
"build_directory",
)
# Callback names for build-time test # Callback names for build-time test
build_time_test_callbacks = ["build_test"] build_time_test_callbacks = ["build_test"]
@@ -53,11 +83,6 @@ class WafPackage(PackageBase):
# Callback names for install-time test # Callback names for install-time test
install_time_test_callbacks = ["install_test"] install_time_test_callbacks = ["install_test"]
# Much like AutotoolsPackage does not require automake and autoconf
# to build, WafPackage does not require waf to build. It only requires
# python to run the waf build script.
depends_on("python@2.5:", type="build")
@property @property
def build_directory(self): def build_directory(self):
"""The directory containing the ``waf`` file.""" """The directory containing the ``waf`` file."""
@@ -65,18 +90,18 @@ def build_directory(self):
def python(self, *args, **kwargs): def python(self, *args, **kwargs):
"""The python ``Executable``.""" """The python ``Executable``."""
inspect.getmodule(self).python(*args, **kwargs) inspect.getmodule(self.pkg).python(*args, **kwargs)
def waf(self, *args, **kwargs): def waf(self, *args, **kwargs):
"""Runs the waf ``Executable``.""" """Runs the waf ``Executable``."""
jobs = inspect.getmodule(self).make_jobs jobs = inspect.getmodule(self.pkg).make_jobs
with working_dir(self.build_directory): with working_dir(self.build_directory):
self.python("waf", "-j{0}".format(jobs), *args, **kwargs) self.python("waf", "-j{0}".format(jobs), *args, **kwargs)
def configure(self, spec, prefix): def configure(self, pkg, spec, prefix):
"""Configures the project.""" """Configures the project."""
args = ["--prefix={0}".format(self.prefix)] args = ["--prefix={0}".format(self.pkg.prefix)]
args += self.configure_args() args += self.configure_args()
self.waf("configure", *args) self.waf("configure", *args)
@@ -85,7 +110,7 @@ def configure_args(self):
"""Arguments to pass to configure.""" """Arguments to pass to configure."""
return [] return []
def build(self, spec, prefix): def build(self, pkg, spec, prefix):
"""Executes the build.""" """Executes the build."""
args = self.build_args() args = self.build_args()
@@ -95,7 +120,7 @@ def build_args(self):
"""Arguments to pass to build.""" """Arguments to pass to build."""
return [] return []
def install(self, spec, prefix): def install(self, pkg, spec, prefix):
"""Installs the targets on the system.""" """Installs the targets on the system."""
args = self.install_args() args = self.install_args()
@@ -105,8 +130,6 @@ def install_args(self):
"""Arguments to pass to install.""" """Arguments to pass to install."""
return [] return []
# Testing
def build_test(self): def build_test(self):
"""Run unit tests after build. """Run unit tests after build.
@@ -115,7 +138,7 @@ def build_test(self):
""" """
pass pass
run_after("build")(PackageBase._run_default_build_time_test_callbacks) spack.builder.run_after("build")(execute_build_time_tests)
def install_test(self): def install_test(self):
"""Run unit tests after install. """Run unit tests after install.
@@ -125,7 +148,4 @@ def install_test(self):
""" """
pass pass
run_after("install")(PackageBase._run_default_install_time_test_callbacks) spack.builder.run_after("install")(execute_install_time_tests)
# Check that self.prefix is there after installation
run_after("install")(PackageBase.sanity_check_prefix)

574
lib/spack/spack/builder.py Normal file
View File

@@ -0,0 +1,574 @@
# Copyright 2013-2022 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 collections
import copy
import functools
import inspect
from typing import List, Optional, Tuple
import six
import llnl.util.compat
import spack.build_environment
#: Builder classes, as registered by the "builder" decorator
BUILDER_CLS = {}
#: An object of this kind is a shared global state used to collect callbacks during
#: class definition time, and is flushed when the class object is created at the end
#: of the class definition
#:
#: Args:
#: attribute_name (str): name of the attribute that will be attached to the builder
#: callbacks (list): container used to temporarily aggregate the callbacks
CallbackTemporaryStage = collections.namedtuple(
"CallbackTemporaryStage", ["attribute_name", "callbacks"]
)
#: Shared global state to aggregate "@run_before" callbacks
_RUN_BEFORE = CallbackTemporaryStage(attribute_name="run_before_callbacks", callbacks=[])
#: Shared global state to aggregate "@run_after" callbacks
_RUN_AFTER = CallbackTemporaryStage(attribute_name="run_after_callbacks", callbacks=[])
#: Map id(pkg) to a builder, to avoid creating multiple
#: builders for the same package object.
_BUILDERS = {}
def builder(build_system_name):
"""Class decorator used to register the default builder
for a given build-system.
Args:
build_system_name (str): name of the build-system
"""
def _decorator(cls):
cls.build_system = build_system_name
BUILDER_CLS[build_system_name] = cls
return cls
return _decorator
def create(pkg):
"""Given a package object with an associated concrete spec,
return the builder object that can install it.
Args:
pkg (spack.package_base.PackageBase): package for which we want the builder
"""
if id(pkg) not in _BUILDERS:
_BUILDERS[id(pkg)] = _create(pkg)
return _BUILDERS[id(pkg)]
class _PhaseAdapter(object):
def __init__(self, builder, phase_fn):
self.builder = builder
self.phase_fn = phase_fn
def __call__(self, spec, prefix):
return self.phase_fn(self.builder.pkg, spec, prefix)
def _create(pkg):
"""Return a new builder object for the package object being passed as argument.
The function inspects the build-system used by the package object and try to:
1. Return a custom builder, if any is defined in the same ``package.py`` file.
2. Return a customization of more generic builders, if any is defined in the
class hierarchy (look at AspellDictPackage for an example of that)
3. Return a run-time generated adapter builder otherwise
The run-time generated adapter builder is capable of adapting an old-style package
to the new architecture, where the installation procedure has been extracted from
the ``*Package`` hierarchy into a ``*Builder`` hierarchy. This means that the
adapter looks for attribute or method overrides preferably in the ``*Package``
before using the default builder implementation.
Note that in case a builder is explicitly coded in ``package.py``, no attempt is made
to look for build-related methods in the ``*Package``.
Args:
pkg (spack.package_base.PackageBase): package object for which we need a builder
"""
package_module = inspect.getmodule(pkg)
package_buildsystem = buildsystem_name(pkg)
default_builder_cls = BUILDER_CLS[package_buildsystem]
builder_cls_name = default_builder_cls.__name__
builder_cls = getattr(package_module, builder_cls_name, None)
if builder_cls:
return builder_cls(pkg)
# Specialized version of a given buildsystem can subclass some
# base classes and specialize certain phases or methods or attributes.
# In that case they can store their builder class as a class level attribute.
# See e.g. AspellDictPackage as an example.
base_cls = getattr(pkg, builder_cls_name, default_builder_cls)
# From here on we define classes to construct a special builder that adapts to the
# old, single class, package format. The adapter forwards any call or access to an
# attribute related to the installation procedure to a package object wrapped in
# a class that falls-back on calling the base builder if no override is found on the
# package. The semantic should be the same as the method in the base builder were still
# present in the base class of the package.
class _ForwardToBaseBuilder(object):
def __init__(self, wrapped_pkg_object, root_builder):
self.wrapped_package_object = wrapped_pkg_object
self.root_builder = root_builder
package_cls = type(wrapped_pkg_object)
wrapper_cls = type(self)
bases = (package_cls, wrapper_cls)
new_cls_name = package_cls.__name__ + "Wrapper"
new_cls = type(new_cls_name, bases, {})
new_cls.__module__ = package_cls.__module__
self.__class__ = new_cls
self.__dict__.update(wrapped_pkg_object.__dict__)
def __getattr__(self, item):
result = getattr(super(type(self.root_builder), self.root_builder), item)
if item in super(type(self.root_builder), self.root_builder).phases:
result = _PhaseAdapter(self.root_builder, result)
return result
def forward_method_to_getattr(fn_name):
def __forward(self, *args, **kwargs):
return self.__getattr__(fn_name)(*args, **kwargs)
return __forward
# Add fallback methods for the Package object to refer to the builder. If a method
# with the same name is defined in the Package, it will override this definition
# (when _ForwardToBaseBuilder is initialized)
for method_name in (
base_cls.phases
+ base_cls.legacy_methods
+ getattr(base_cls, "legacy_long_methods", tuple())
+ ("setup_build_environment", "setup_dependent_build_environment")
):
setattr(_ForwardToBaseBuilder, method_name, forward_method_to_getattr(method_name))
def forward_property_to_getattr(property_name):
def __forward(self):
return self.__getattr__(property_name)
return __forward
for attribute_name in base_cls.legacy_attributes:
setattr(
_ForwardToBaseBuilder,
attribute_name,
property(forward_property_to_getattr(attribute_name)),
)
class Adapter(six.with_metaclass(_PackageAdapterMeta, base_cls)):
def __init__(self, pkg):
# Deal with custom phases in packages here
if hasattr(pkg, "phases"):
self.phases = pkg.phases
for phase in self.phases:
setattr(Adapter, phase, _PackageAdapterMeta.phase_method_adapter(phase))
# Attribute containing the package wrapped in dispatcher with a `__getattr__`
# method that will forward certain calls to the default builder.
self.pkg_with_dispatcher = _ForwardToBaseBuilder(pkg, root_builder=self)
super(Adapter, self).__init__(pkg)
# These two methods don't follow the (self, spec, prefix) signature of phases nor
# the (self) signature of methods, so they are added explicitly to avoid using a
# catch-all (*args, **kwargs)
def setup_build_environment(self, env):
return self.pkg_with_dispatcher.setup_build_environment(env)
def setup_dependent_build_environment(self, env, dependent_spec):
return self.pkg_with_dispatcher.setup_dependent_build_environment(env, dependent_spec)
return Adapter(pkg)
def buildsystem_name(pkg):
"""Given a package object with an associated concrete spec,
return the name of its build system.
Args:
pkg (spack.package_base.PackageBase): package for which we want
the build system name
"""
try:
return pkg.spec.variants["build_system"].value
except KeyError:
# We are reading an old spec without the build_system variant
return pkg.legacy_buildsystem
class PhaseCallbacksMeta(type):
"""Permit to register arbitrary functions during class definition and run them
later, before or after a given install phase.
Each method decorated with ``run_before`` or ``run_after`` gets temporarily
stored in a global shared state when a class being defined is parsed by the Python
interpreter. At class definition time that temporary storage gets flushed and a list
of callbacks is attached to the class being defined.
"""
def __new__(mcs, name, bases, attr_dict):
for temporary_stage in (_RUN_BEFORE, _RUN_AFTER):
staged_callbacks = temporary_stage.callbacks
# We don't have callbacks in this class, move on
if not staged_callbacks:
continue
# If we are here we have callbacks. To get a complete list, get first what
# was attached to parent classes, then prepend what we have registered here.
#
# The order should be:
# 1. Callbacks are registered in order within the same class
# 2. Callbacks defined in derived classes precede those defined in base
# classes
for base in bases:
callbacks_from_base = getattr(base, temporary_stage.attribute_name, None)
if callbacks_from_base:
break
callbacks_from_base = callbacks_from_base or []
# Set the callbacks in this class and flush the temporary stage
attr_dict[temporary_stage.attribute_name] = staged_callbacks[:] + callbacks_from_base
del temporary_stage.callbacks[:]
return super(PhaseCallbacksMeta, mcs).__new__(mcs, name, bases, attr_dict)
@staticmethod
def run_after(phase, when=None):
"""Decorator to register a function for running after a given phase.
Args:
phase (str): phase after which the function must run.
when (str): condition under which the function is run (if None, it is always run).
"""
def _decorator(fn):
key = (phase, when)
item = (key, fn)
_RUN_AFTER.callbacks.append(item)
return fn
return _decorator
@staticmethod
def run_before(phase, when=None):
"""Decorator to register a function for running before a given phase.
Args:
phase (str): phase before which the function must run.
when (str): condition under which the function is run (if None, it is always run).
"""
def _decorator(fn):
key = (phase, when)
item = (key, fn)
_RUN_BEFORE.callbacks.append(item)
return fn
return _decorator
class BuilderMeta(PhaseCallbacksMeta, type(llnl.util.compat.Sequence)): # type: ignore
pass
class _PackageAdapterMeta(BuilderMeta):
"""Metaclass to adapt old-style packages to the new architecture based on builders
for the installation phase.
This class does the necessary mangling to function argument so that a call to a
builder object can delegate to a package object.
"""
@staticmethod
def phase_method_adapter(phase_name):
def _adapter(self, pkg, spec, prefix):
phase_fn = getattr(self.pkg_with_dispatcher, phase_name)
return phase_fn(spec, prefix)
return _adapter
@staticmethod
def legacy_long_method_adapter(method_name):
def _adapter(self, spec, prefix):
bind_method = getattr(self.pkg_with_dispatcher, method_name)
return bind_method(spec, prefix)
return _adapter
@staticmethod
def legacy_method_adapter(method_name):
def _adapter(self):
bind_method = getattr(self.pkg_with_dispatcher, method_name)
return bind_method()
return _adapter
@staticmethod
def legacy_attribute_adapter(attribute_name):
def _adapter(self):
return getattr(self.pkg_with_dispatcher, attribute_name)
return property(_adapter)
@staticmethod
def combine_callbacks(pipeline_attribute_name):
"""This function combines callbacks from old-style packages with callbacks that might
be registered for the default builder.
It works by:
1. Extracting the callbacks from the old-style package
2. Transforming those callbacks by adding an adapter that receives a builder as argument
and calls the wrapped function with ``builder.pkg``
3. Combining the list of transformed callbacks with those that might be present in the
default builder
"""
def _adapter(self):
def unwrap_pkg(fn):
@functools.wraps(fn)
def _wrapped(builder):
return fn(builder.pkg_with_dispatcher)
return _wrapped
# Concatenate the current list with the one from package
callbacks_from_package = getattr(self.pkg, pipeline_attribute_name, [])
callbacks_from_package = [(key, unwrap_pkg(x)) for key, x in callbacks_from_package]
callbacks_from_builder = getattr(super(type(self), self), pipeline_attribute_name, [])
return callbacks_from_package + callbacks_from_builder
return property(_adapter)
def __new__(mcs, name, bases, attr_dict):
# Add ways to intercept methods and attribute calls and dispatch
# them first to a package object
default_builder_cls = bases[0]
for phase_name in default_builder_cls.phases:
attr_dict[phase_name] = _PackageAdapterMeta.phase_method_adapter(phase_name)
for method_name in default_builder_cls.legacy_methods:
attr_dict[method_name] = _PackageAdapterMeta.legacy_method_adapter(method_name)
# These exist e.g. for Python, see discussion in https://github.com/spack/spack/pull/32068
for method_name in getattr(default_builder_cls, "legacy_long_methods", []):
attr_dict[method_name] = _PackageAdapterMeta.legacy_long_method_adapter(method_name)
for attribute_name in default_builder_cls.legacy_attributes:
attr_dict[attribute_name] = _PackageAdapterMeta.legacy_attribute_adapter(
attribute_name
)
combine_callbacks = _PackageAdapterMeta.combine_callbacks
attr_dict[_RUN_BEFORE.attribute_name] = combine_callbacks(_RUN_BEFORE.attribute_name)
attr_dict[_RUN_AFTER.attribute_name] = combine_callbacks(_RUN_AFTER.attribute_name)
return super(_PackageAdapterMeta, mcs).__new__(mcs, name, bases, attr_dict)
class InstallationPhase(object):
"""Manages a single phase of the installation.
This descriptor stores at creation time the name of the method it should
search for execution. The method is retrieved at __get__ time, so that
it can be overridden by subclasses of whatever class declared the phases.
It also provides hooks to execute arbitrary callbacks before and after
the phase.
"""
def __init__(self, name, builder):
self.name = name
self.builder = builder
self.phase_fn = self._select_phase_fn()
self.run_before = self._make_callbacks(_RUN_BEFORE.attribute_name)
self.run_after = self._make_callbacks(_RUN_AFTER.attribute_name)
def _make_callbacks(self, callbacks_attribute):
result = []
callbacks = getattr(self.builder, callbacks_attribute, [])
for (phase, condition), fn in callbacks:
# Same if it is for another phase
if phase != self.name:
continue
# If we have no condition or the callback satisfies a condition, register it
if condition is None or self.builder.pkg.spec.satisfies(condition):
result.append(fn)
return result
def __str__(self):
msg = '{0}: executing "{1}" phase'
return msg.format(self.builder, self.name)
def execute(self):
pkg = self.builder.pkg
self._on_phase_start(pkg)
for callback in self.run_before:
callback(self.builder)
self.phase_fn(pkg, pkg.spec, pkg.prefix)
for callback in self.run_after:
callback(self.builder)
self._on_phase_exit(pkg)
def _select_phase_fn(self):
phase_fn = getattr(self.builder, self.name, None)
if not phase_fn:
msg = (
'unexpected error: package "{0.fullname}" must implement an '
'"{1}" phase for the "{2}" build system'
)
raise RuntimeError(msg.format(self.builder.pkg, self.name, self.builder.build_system))
return phase_fn
def _on_phase_start(self, instance):
# If a phase has a matching stop_before_phase attribute,
# stop the installation process raising a StopPhase
if getattr(instance, "stop_before_phase", None) == self.name:
raise spack.build_environment.StopPhase(
"Stopping before '{0}' phase".format(self.name)
)
def _on_phase_exit(self, instance):
# If a phase has a matching last_phase attribute,
# stop the installation process raising a StopPhase
if getattr(instance, "last_phase", None) == self.name:
raise spack.build_environment.StopPhase("Stopping at '{0}' phase".format(self.name))
def copy(self):
return copy.deepcopy(self)
class Builder(six.with_metaclass(BuilderMeta, llnl.util.compat.Sequence)):
"""A builder is a class that, given a package object (i.e. associated with
concrete spec), knows how to install it.
The builder behaves like a sequence, and when iterated over return the
"phases" of the installation in the correct order.
Args:
pkg (spack.package_base.PackageBase): package object to be built
"""
#: Sequence of phases. Must be defined in derived classes
phases = None # type: Optional[Tuple[str, ...]]
#: Build system name. Must also be defined in derived classes.
build_system = None # type: Optional[str]
legacy_methods = () # type: Tuple[str, ...]
legacy_attributes = () # type: Tuple[str, ...]
#: List of glob expressions. Each expression must either be
#: absolute or relative to the package source path.
#: Matching artifacts found at the end of the build process will be
#: copied in the same directory tree as _spack_build_logfile and
#: _spack_build_envfile.
archive_files = [] # type: List[str]
def __init__(self, pkg):
self.pkg = pkg
self.callbacks = {}
for phase in self.phases:
self.callbacks[phase] = InstallationPhase(phase, self)
@property
def spec(self):
return self.pkg.spec
@property
def stage(self):
return self.pkg.stage
@property
def prefix(self):
return self.pkg.prefix
def test(self):
# Defer tests to virtual and concrete packages
pass
def setup_build_environment(self, env):
"""Sets up the build environment for a package.
This method will be called before the current package prefix exists in
Spack's store.
Args:
env (spack.util.environment.EnvironmentModifications): environment
modifications to be applied when the package is built. Package authors
can call methods on it to alter the build environment.
"""
if not hasattr(super(Builder, self), "setup_build_environment"):
return
super(Builder, self).setup_build_environment(env)
def setup_dependent_build_environment(self, env, dependent_spec):
"""Sets up the build environment of packages that depend on this one.
This is similar to ``setup_build_environment``, but it is used to
modify the build environments of packages that *depend* on this one.
This gives packages like Python and others that follow the extension
model a way to implement common environment or compile-time settings
for dependencies.
This method will be called before the dependent package prefix exists
in Spack's store.
Examples:
1. Installing python modules generally requires ``PYTHONPATH``
to point to the ``lib/pythonX.Y/site-packages`` directory in the
module's install prefix. This method could be used to set that
variable.
Args:
env (spack.util.environment.EnvironmentModifications): environment
modifications to be applied when the dependent package is built.
Package authors can call methods on it to alter the build environment.
dependent_spec (spack.spec.Spec): the spec of the dependent package
about to be built. This allows the extendee (self) to query
the dependent's state. Note that *this* package's spec is
available as ``self.spec``
"""
if not hasattr(super(Builder, self), "setup_dependent_build_environment"):
return
super(Builder, self).setup_dependent_build_environment(env, dependent_spec)
def __getitem__(self, idx):
key = self.phases[idx]
return self.callbacks[key]
def __len__(self):
return len(self.phases)
def __repr__(self):
msg = "{0}({1})"
return msg.format(type(self).__name__, self.pkg.spec.format("{name}/{hash:7}"))
def __str__(self):
msg = '"{0}" builder for "{1}"'
return msg.format(type(self).build_system, self.pkg.spec.format("{name}/{hash:7}"))
# Export these names as standalone to be used in packages
run_after = PhaseCallbacksMeta.run_after
run_before = PhaseCallbacksMeta.run_before

View File

@@ -210,11 +210,11 @@ def print_maintainers(pkg):
def print_phases(pkg): def print_phases(pkg):
"""output installation phases""" """output installation phases"""
if hasattr(pkg, "phases") and pkg.phases: if hasattr(pkg.builder, "phases") and pkg.builder.phases:
color.cprint("") color.cprint("")
color.cprint(section_title("Installation Phases:")) color.cprint(section_title("Installation Phases:"))
phase_str = "" phase_str = ""
for phase in pkg.phases: for phase in pkg.builder.phases:
phase_str += " {0}".format(phase) phase_str += " {0}".format(phase)
color.cprint(phase_str) color.cprint(phase_str)

View File

@@ -228,7 +228,7 @@ def do_uninstall(env, specs, force):
except spack.repo.UnknownEntityError: except spack.repo.UnknownEntityError:
# The package.py file has gone away -- but still # The package.py file has gone away -- but still
# want to uninstall. # want to uninstall.
spack.package_base.Package.uninstall_by_spec(item, force=True) spack.package_base.PackageBase.uninstall_by_spec(item, force=True)
# A package is ready to be uninstalled when nothing else references it, # A package is ready to be uninstalled when nothing else references it,
# unless we are requested to force uninstall it. # unless we are requested to force uninstall it.

View File

@@ -228,7 +228,7 @@ def compute_windows_program_path_for_package(pkg):
program files location, return list of best guesses program files location, return list of best guesses
Args: Args:
pkg (spack.package_base.Package): package for which pkg (spack.package_base.PackageBase): package for which
Program Files location is to be computed Program Files location is to be computed
""" """
if not is_windows: if not is_windows:

View File

@@ -17,6 +17,7 @@ class OpenMpi(Package):
The available directives are: The available directives are:
* ``build_system``
* ``conflicts`` * ``conflicts``
* ``depends_on`` * ``depends_on``
* ``extends`` * ``extends``
@@ -59,13 +60,15 @@ class OpenMpi(Package):
"patch", "patch",
"variant", "variant",
"resource", "resource",
"build_system",
] ]
#: These are variant names used by Spack internally; packages can't use them #: These are variant names used by Spack internally; packages can't use them
reserved_names = ["patches", "dev_path"] reserved_names = ["patches", "dev_path"]
#: Names of possible directives. This list is populated elsewhere in the file. #: Names of possible directives. This list is mostly populated using the @directive decorator.
directive_names = [] #: Some directives leverage others and in that case are not automatically added.
directive_names = ["build_system"]
_patch_order_index = 0 _patch_order_index = 0
@@ -758,6 +761,17 @@ def _execute_resource(pkg):
return _execute_resource return _execute_resource
def build_system(*values, **kwargs):
default = kwargs.get("default", None) or values[0]
return variant(
"build_system",
values=tuple(values),
description="Build systems supported by the package",
default=default,
multi=False,
)
class DirectiveError(spack.error.SpackError): class DirectiveError(spack.error.SpackError):
"""This is raised when something is wrong with a package directive.""" """This is raised when something is wrong with a package directive."""

View File

@@ -112,14 +112,15 @@ def _check_last_phase(pkg):
Raises: Raises:
``BadInstallPhase`` if stop_before or last phase is invalid ``BadInstallPhase`` if stop_before or last phase is invalid
""" """
if pkg.stop_before_phase and pkg.stop_before_phase not in pkg.phases: phases = pkg.builder.phases
if pkg.stop_before_phase and pkg.stop_before_phase not in phases:
raise BadInstallPhase(pkg.name, pkg.stop_before_phase) raise BadInstallPhase(pkg.name, pkg.stop_before_phase)
if pkg.last_phase and pkg.last_phase not in pkg.phases: if pkg.last_phase and pkg.last_phase not in phases:
raise BadInstallPhase(pkg.name, pkg.last_phase) raise BadInstallPhase(pkg.name, pkg.last_phase)
# If we got a last_phase, make sure it's not already last # If we got a last_phase, make sure it's not already last
if pkg.last_phase and pkg.last_phase == pkg.phases[-1]: if pkg.last_phase and pkg.last_phase == phases[-1]:
pkg.last_phase = None pkg.last_phase = None
@@ -129,7 +130,7 @@ def _handle_external_and_upstream(pkg, explicit):
database if it is external package. database if it is external package.
Args: Args:
pkg (spack.package_base.Package): the package whose installation is under pkg (spack.package_base.PackageBase): the package whose installation is under
consideration consideration
explicit (bool): the package was explicitly requested by the user explicit (bool): the package was explicitly requested by the user
Return: Return:
@@ -559,7 +560,7 @@ def log(pkg):
Copy provenance into the install directory on success Copy provenance into the install directory on success
Args: Args:
pkg (spack.package_base.Package): the package that was built and installed pkg (spack.package_base.PackageBase): the package that was built and installed
""" """
packages_dir = spack.store.layout.build_packages_path(pkg.spec) packages_dir = spack.store.layout.build_packages_path(pkg.spec)
@@ -596,7 +597,7 @@ def log(pkg):
errors = six.StringIO() errors = six.StringIO()
target_dir = os.path.join(spack.store.layout.metadata_path(pkg.spec), "archived-files") target_dir = os.path.join(spack.store.layout.metadata_path(pkg.spec), "archived-files")
for glob_expr in pkg.archive_files: for glob_expr in pkg.builder.archive_files:
# Check that we are trying to copy things that are # Check that we are trying to copy things that are
# in the stage tree (not arbitrary files) # in the stage tree (not arbitrary files)
abs_expr = os.path.realpath(glob_expr) abs_expr = os.path.realpath(glob_expr)
@@ -810,7 +811,7 @@ def _add_init_task(self, pkg, request, is_compiler, all_deps):
Creates and queus the initial build task for the package. Creates and queus the initial build task for the package.
Args: Args:
pkg (spack.package_base.Package): the package to be built and installed pkg (spack.package_base.PackageBase): the package to be built and installed
request (BuildRequest or None): the associated install request request (BuildRequest or None): the associated install request
where ``None`` can be used to indicate the package was where ``None`` can be used to indicate the package was
explicitly requested by the user explicitly requested by the user
@@ -1404,7 +1405,7 @@ def _setup_install_dir(self, pkg):
Write a small metadata file with the current spack environment. Write a small metadata file with the current spack environment.
Args: Args:
pkg (spack.package_base.Package): the package to be built and installed pkg (spack.package_base.PackageBase): the package to be built and installed
""" """
if not os.path.exists(pkg.spec.prefix): if not os.path.exists(pkg.spec.prefix):
path = spack.util.path.debug_padded_filter(pkg.spec.prefix) path = spack.util.path.debug_padded_filter(pkg.spec.prefix)
@@ -1477,8 +1478,8 @@ def _flag_installed(self, pkg, dependent_ids=None):
known dependents. known dependents.
Args: Args:
pkg (spack.package_base.Package): Package that has been installed locally, pkg (spack.package_base.PackageBase): Package that has been installed
externally or upstream locally, externally or upstream
dependent_ids (list or None): list of the package's dependent_ids (list or None): list of the package's
dependent ids, or None if the dependent ids are limited to dependent ids, or None if the dependent ids are limited to
those maintained in the package (dependency DAG) those maintained in the package (dependency DAG)
@@ -1562,11 +1563,7 @@ def _install_action(self, task):
return InstallAction.OVERWRITE return InstallAction.OVERWRITE
def install(self): def install(self):
""" """Install the requested package(s) and or associated dependencies."""
Install the requested package(s) and or associated dependencies.
Args:
pkg (spack.package_base.Package): the package to be built and installed"""
self._init_queue() self._init_queue()
fail_fast_err = "Terminating after first install failure" fail_fast_err = "Terminating after first install failure"
@@ -1951,6 +1948,8 @@ def _install_source(self):
fs.install_tree(pkg.stage.source_path, src_target) fs.install_tree(pkg.stage.source_path, src_target)
def _real_install(self): def _real_install(self):
import spack.builder
pkg = self.pkg pkg = self.pkg
# Do the real install in the source directory. # Do the real install in the source directory.
@@ -1981,13 +1980,11 @@ def _real_install(self):
# Spawn a daemon that reads from a pipe and redirects # Spawn a daemon that reads from a pipe and redirects
# everything to log_path, and provide the phase for logging # everything to log_path, and provide the phase for logging
for i, (phase_name, phase_attr) in enumerate( builder = spack.builder.create(pkg)
zip(pkg.phases, pkg._InstallPhase_phases) for i, phase_fn in enumerate(builder):
):
# Keep a log file for each phase # Keep a log file for each phase
log_dir = os.path.dirname(pkg.log_path) log_dir = os.path.dirname(pkg.log_path)
log_file = "spack-build-%02d-%s-out.txt" % (i + 1, phase_name.lower()) log_file = "spack-build-%02d-%s-out.txt" % (i + 1, phase_fn.name.lower())
log_file = os.path.join(log_dir, log_file) log_file = os.path.join(log_dir, log_file)
try: try:
@@ -2005,20 +2002,20 @@ def _real_install(self):
with logger.force_echo(): with logger.force_echo():
inner_debug_level = tty.debug_level() inner_debug_level = tty.debug_level()
tty.set_debug(debug_level) tty.set_debug(debug_level)
tty.msg("{0} Executing phase: '{1}'".format(self.pre, phase_name)) msg = "{0} Executing phase: '{1}'"
tty.msg(msg.format(self.pre, phase_fn.name))
tty.set_debug(inner_debug_level) tty.set_debug(inner_debug_level)
# Redirect stdout and stderr to daemon pipe # Redirect stdout and stderr to daemon pipe
phase = getattr(pkg, phase_attr) self.timer.phase(phase_fn.name)
self.timer.phase(phase_name)
# Catch any errors to report to logging # Catch any errors to report to logging
phase(pkg.spec, pkg.prefix) phase_fn.execute()
spack.hooks.on_phase_success(pkg, phase_name, log_file) spack.hooks.on_phase_success(pkg, phase_fn.name, log_file)
except BaseException: except BaseException:
combine_phase_logs(pkg.phase_log_files, pkg.log_path) combine_phase_logs(pkg.phase_log_files, pkg.log_path)
spack.hooks.on_phase_error(pkg, phase_name, log_file) spack.hooks.on_phase_error(pkg, phase_fn.name, log_file)
# phase error indicates install error # phase error indicates install error
spack.hooks.on_install_failure(pkg.spec) spack.hooks.on_install_failure(pkg.spec)
@@ -2094,7 +2091,7 @@ def __init__(self, pkg, request, compiler, start, attempts, status, installed):
Instantiate a build task for a package. Instantiate a build task for a package.
Args: Args:
pkg (spack.package_base.Package): the package to be built and installed pkg (spack.package_base.PackageBase): the package to be built and installed
request (BuildRequest or None): the associated install request request (BuildRequest or None): the associated install request
where ``None`` can be used to indicate the package was where ``None`` can be used to indicate the package was
explicitly requested by the user explicitly requested by the user
@@ -2310,7 +2307,7 @@ def __init__(self, pkg, install_args):
Instantiate a build request for a package. Instantiate a build request for a package.
Args: Args:
pkg (spack.package_base.Package): the package to be built and installed pkg (spack.package_base.PackageBase): the package to be built and installed
install_args (dict): the install arguments associated with ``pkg`` install_args (dict): the install arguments associated with ``pkg``
""" """
# Ensure dealing with a package that has a concrete spec # Ensure dealing with a package that has a concrete spec

View File

@@ -6,10 +6,9 @@
"""This module contains additional behavior that can be attached to any given """This module contains additional behavior that can be attached to any given
package. package.
""" """
import collections
import os import os
import sys import sys
from typing import Callable, DefaultDict, Dict, List # novm from typing import Callable, DefaultDict, List # novm
if sys.version_info >= (3, 5): if sys.version_info >= (3, 5):
CallbackDict = DefaultDict[str, List[Callable]] CallbackDict = DefaultDict[str, List[Callable]]
@@ -18,105 +17,7 @@
import llnl.util.filesystem import llnl.util.filesystem
__all__ = [ import spack.builder
"filter_compiler_wrappers",
"PackageMixinsMeta",
]
class PackageMixinsMeta(type):
"""This metaclass serves the purpose of implementing a declarative syntax
for package mixins.
Mixins are implemented below in the form of a function. Each one of them
needs to register a callable that takes a single argument to be run
before or after a certain phase. This callable is basically a method that
gets implicitly attached to the package class by calling the mixin.
"""
_methods_to_be_added = {} # type: Dict[str, Callable]
_add_method_before = collections.defaultdict(list) # type: CallbackDict
_add_method_after = collections.defaultdict(list) # type: CallbackDict
@staticmethod
def register_method_before(fn, phase): # type: (Callable, str) -> None
"""Registers a method to be run before a certain phase.
Args:
fn: function taking a single argument (self)
phase (str): phase before which fn must run
"""
PackageMixinsMeta._methods_to_be_added[fn.__name__] = fn
PackageMixinsMeta._add_method_before[phase].append(fn)
@staticmethod
def register_method_after(fn, phase): # type: (Callable, str) -> None
"""Registers a method to be run after a certain phase.
Args:
fn: function taking a single argument (self)
phase (str): phase after which fn must run
"""
PackageMixinsMeta._methods_to_be_added[fn.__name__] = fn
PackageMixinsMeta._add_method_after[phase].append(fn)
def __init__(cls, name, bases, attr_dict):
# Add the methods to the class being created
if PackageMixinsMeta._methods_to_be_added:
attr_dict.update(PackageMixinsMeta._methods_to_be_added)
PackageMixinsMeta._methods_to_be_added.clear()
attr_fmt = "_InstallPhase_{0}"
# Copy the phases that needs it to the most derived classes
# in order not to interfere with other packages in the hierarchy
phases_to_be_copied = list(PackageMixinsMeta._add_method_before.keys())
phases_to_be_copied += list(PackageMixinsMeta._add_method_after.keys())
for phase in phases_to_be_copied:
attr_name = attr_fmt.format(phase)
# Here we want to get the attribute directly from the class (not
# from the instance), so that we can modify it and add the mixin
# method to the pipeline.
phase = getattr(cls, attr_name)
# Due to MRO, we may have taken a method from a parent class
# and modifying it may influence other packages in unwanted
# manners. Solve the problem by copying the phase into the most
# derived class.
setattr(cls, attr_name, phase.copy())
# Insert the methods in the appropriate position
# in the installation pipeline.
for phase in PackageMixinsMeta._add_method_before:
attr_name = attr_fmt.format(phase)
phase_obj = getattr(cls, attr_name)
fn_list = PackageMixinsMeta._add_method_after[phase]
for f in fn_list:
phase_obj.run_before.append(f)
# Flush the dictionary for the next class
PackageMixinsMeta._add_method_before.clear()
for phase in PackageMixinsMeta._add_method_after:
attr_name = attr_fmt.format(phase)
phase_obj = getattr(cls, attr_name)
fn_list = PackageMixinsMeta._add_method_after[phase]
for f in fn_list:
phase_obj.run_after.append(f)
# Flush the dictionary for the next class
PackageMixinsMeta._add_method_after.clear()
super(PackageMixinsMeta, cls).__init__(name, bases, attr_dict)
def filter_compiler_wrappers(*files, **kwargs): def filter_compiler_wrappers(*files, **kwargs):
@@ -216,4 +117,4 @@ def _filter_compiler_wrappers_impl(self):
if self.compiler.name == "nag": if self.compiler.name == "nag":
x.filter("-Wl,--enable-new-dtags", "", **filter_kwargs) x.filter("-Wl,--enable-new-dtags", "", **filter_kwargs)
PackageMixinsMeta.register_method_after(_filter_compiler_wrappers_impl, after) spack.builder.run_after(after)(_filter_compiler_wrappers_impl)

View File

@@ -120,32 +120,36 @@ def _get_method_by_spec(self, spec):
return method return method
return self.default or None return self.default or None
def __call__(self, package_self, *args, **kwargs): def __call__(self, package_or_builder_self, *args, **kwargs):
"""Find the first method with a spec that matches the """Find the first method with a spec that matches the
package's spec. If none is found, call the default package's spec. If none is found, call the default
or if there is none, then raise a NoSuchMethodError. or if there is none, then raise a NoSuchMethodError.
""" """
spec_method = self._get_method_by_spec(package_self.spec) spec_method = self._get_method_by_spec(package_or_builder_self.spec)
if spec_method: if spec_method:
return spec_method(package_self, *args, **kwargs) return spec_method(package_or_builder_self, *args, **kwargs)
# Unwrap the MRO of `package_self by hand. Note that we can't # Unwrap the MRO of `package_self by hand. Note that we can't
# use `super()` here, because using `super()` recursively # use `super()` here, because using `super()` recursively
# requires us to know the class of `package_self`, as well as # requires us to know the class of `package_self`, as well as
# its superclasses for successive calls. We don't have that # its superclasses for successive calls. We don't have that
# information within `SpecMultiMethod`, because it is not # information within `SpecMultiMethod`, because it is not
# associated with the package class. # associated with the package class.
for cls in inspect.getmro(package_self.__class__)[1:]: for cls in inspect.getmro(package_or_builder_self.__class__)[1:]:
superself = cls.__dict__.get(self.__name__, None) superself = cls.__dict__.get(self.__name__, None)
if isinstance(superself, SpecMultiMethod): if isinstance(superself, SpecMultiMethod):
# Check parent multimethod for method for spec. # Check parent multimethod for method for spec.
superself_method = superself._get_method_by_spec(package_self.spec) superself_method = superself._get_method_by_spec(package_or_builder_self.spec)
if superself_method: if superself_method:
return superself_method(package_self, *args, **kwargs) return superself_method(package_or_builder_self, *args, **kwargs)
elif superself: elif superself:
return superself(package_self, *args, **kwargs) return superself(package_or_builder_self, *args, **kwargs)
raise NoSuchMethodError( raise NoSuchMethodError(
type(package_self), self.__name__, package_self.spec, [m[0] for m in self.method_list] type(package_or_builder_self),
self.__name__,
package_or_builder_self.spec,
[m[0] for m in self.method_list],
) )

View File

@@ -17,6 +17,7 @@
import spack.util.executable import spack.util.executable
from spack.build_systems.aspell_dict import AspellDictPackage from spack.build_systems.aspell_dict import AspellDictPackage
from spack.build_systems.autotools import AutotoolsPackage from spack.build_systems.autotools import AutotoolsPackage
from spack.build_systems.bundle import BundlePackage
from spack.build_systems.cached_cmake import ( from spack.build_systems.cached_cmake import (
CachedCMakePackage, CachedCMakePackage,
cmake_cache_option, cmake_cache_option,
@@ -25,12 +26,14 @@
) )
from spack.build_systems.cmake import CMakePackage from spack.build_systems.cmake import CMakePackage
from spack.build_systems.cuda import CudaPackage from spack.build_systems.cuda import CudaPackage
from spack.build_systems.generic import Package
from spack.build_systems.gnu import GNUMirrorPackage from spack.build_systems.gnu import GNUMirrorPackage
from spack.build_systems.intel import IntelPackage from spack.build_systems.intel import IntelPackage
from spack.build_systems.lua import LuaPackage from spack.build_systems.lua import LuaPackage
from spack.build_systems.makefile import MakefilePackage from spack.build_systems.makefile import MakefilePackage
from spack.build_systems.maven import MavenPackage from spack.build_systems.maven import MavenPackage
from spack.build_systems.meson import MesonPackage from spack.build_systems.meson import MesonPackage
from spack.build_systems.nmake import NMakePackage
from spack.build_systems.octave import OctavePackage from spack.build_systems.octave import OctavePackage
from spack.build_systems.oneapi import ( from spack.build_systems.oneapi import (
IntelOneApiLibraryPackage, IntelOneApiLibraryPackage,
@@ -38,7 +41,7 @@
IntelOneApiStaticLibraryList, IntelOneApiStaticLibraryList,
) )
from spack.build_systems.perl import PerlPackage from spack.build_systems.perl import PerlPackage
from spack.build_systems.python import PythonPackage from spack.build_systems.python import PythonExtension, PythonPackage
from spack.build_systems.qmake import QMakePackage from spack.build_systems.qmake import QMakePackage
from spack.build_systems.r import RPackage from spack.build_systems.r import RPackage
from spack.build_systems.racket import RacketPackage from spack.build_systems.racket import RacketPackage
@@ -50,6 +53,7 @@
from spack.build_systems.sourceware import SourcewarePackage from spack.build_systems.sourceware import SourcewarePackage
from spack.build_systems.waf import WafPackage from spack.build_systems.waf import WafPackage
from spack.build_systems.xorg import XorgPackage from spack.build_systems.xorg import XorgPackage
from spack.builder import run_after, run_before
from spack.dependency import all_deptypes from spack.dependency import all_deptypes
from spack.directives import * from spack.directives import *
from spack.install_test import get_escaped_text_output from spack.install_test import get_escaped_text_output
@@ -62,17 +66,13 @@
from spack.mixins import filter_compiler_wrappers from spack.mixins import filter_compiler_wrappers
from spack.multimethod import when from spack.multimethod import when
from spack.package_base import ( from spack.package_base import (
BundlePackage,
DependencyConflictError, DependencyConflictError,
Package,
build_system_flags, build_system_flags,
env_flags, env_flags,
flatten_dependencies, flatten_dependencies,
inject_flags, inject_flags,
install_dependency_symlinks, install_dependency_symlinks,
on_package_attributes, on_package_attributes,
run_after,
run_before,
) )
from spack.spec import InvalidSpecDetected, Spec from spack.spec import InvalidSpecDetected, Spec
from spack.util.executable import * from spack.util.executable import *

View File

@@ -33,7 +33,7 @@
import llnl.util.filesystem as fsys import llnl.util.filesystem as fsys
import llnl.util.tty as tty import llnl.util.tty as tty
from llnl.util.lang import classproperty, match_predicate, memoized, nullcontext from llnl.util.lang import classproperty, memoized, nullcontext
from llnl.util.link_tree import LinkTree from llnl.util.link_tree import LinkTree
import spack.compilers import spack.compilers
@@ -104,7 +104,7 @@ def deprecated_version(pkg, version):
"""Return True if the version is deprecated, False otherwise. """Return True if the version is deprecated, False otherwise.
Arguments: Arguments:
pkg (Package): The package whose version is to be checked. pkg (PackageBase): The package whose version is to be checked.
version (str or spack.version.VersionBase): The version being checked version (str or spack.version.VersionBase): The version being checked
""" """
if not isinstance(version, VersionBase): if not isinstance(version, VersionBase):
@@ -122,7 +122,7 @@ def preferred_version(pkg):
Returns a sorted list of the preferred versions of the package. Returns a sorted list of the preferred versions of the package.
Arguments: Arguments:
pkg (Package): The package whose versions are to be assessed. pkg (PackageBase): The package whose versions are to be assessed.
""" """
# Here we sort first on the fact that a version is marked # Here we sort first on the fact that a version is marked
# as preferred in the package, then on the fact that the # as preferred in the package, then on the fact that the
@@ -131,77 +131,6 @@ def preferred_version(pkg):
return sorted(pkg.versions, key=key_fn).pop() return sorted(pkg.versions, key=key_fn).pop()
class InstallPhase(object):
"""Manages a single phase of the installation.
This descriptor stores at creation time the name of the method it should
search for execution. The method is retrieved at __get__ time, so that
it can be overridden by subclasses of whatever class declared the phases.
It also provides hooks to execute arbitrary callbacks before and after
the phase.
"""
def __init__(self, name):
self.name = name
self.run_before = []
self.run_after = []
def __get__(self, instance, owner):
# The caller is a class that is trying to customize
# my behavior adding something
if instance is None:
return self
# If instance is there the caller wants to execute the
# install phase, thus return a properly set wrapper
phase = getattr(instance, self.name)
@functools.wraps(phase)
def phase_wrapper(spec, prefix):
# Check instance attributes at the beginning of a phase
self._on_phase_start(instance)
# Execute phase pre-conditions,
# and give them the chance to fail
for callback in self.run_before:
callback(instance)
phase(spec, prefix)
# Execute phase sanity_checks,
# and give them the chance to fail
for callback in self.run_after:
callback(instance)
# Check instance attributes at the end of a phase
self._on_phase_exit(instance)
return phase_wrapper
def _on_phase_start(self, instance):
# If a phase has a matching stop_before_phase attribute,
# stop the installation process raising a StopPhase
if getattr(instance, "stop_before_phase", None) == self.name:
from spack.build_environment import StopPhase
raise StopPhase("Stopping before '{0}' phase".format(self.name))
def _on_phase_exit(self, instance):
# If a phase has a matching last_phase attribute,
# stop the installation process raising a StopPhase
if getattr(instance, "last_phase", None) == self.name:
from spack.build_environment import StopPhase
raise StopPhase("Stopping at '{0}' phase".format(self.name))
def copy(self):
try:
return copy.deepcopy(self)
except TypeError:
# This bug-fix was not back-ported in Python 2.6
# http://bugs.python.org/issue1515
other = InstallPhase(self.name)
other.run_before.extend(self.run_before)
other.run_after.extend(self.run_after)
return other
class WindowsRPathMeta(object): class WindowsRPathMeta(object):
"""Collection of functionality surrounding Windows RPATH specific features """Collection of functionality surrounding Windows RPATH specific features
@@ -368,23 +297,18 @@ def determine_variants(cls, objs, version_str):
class PackageMeta( class PackageMeta(
spack.builder.PhaseCallbacksMeta,
DetectablePackageMeta, DetectablePackageMeta,
spack.directives.DirectiveMeta, spack.directives.DirectiveMeta,
spack.mixins.PackageMixinsMeta,
spack.multimethod.MultiMethodMeta, spack.multimethod.MultiMethodMeta,
): ):
""" """
Package metaclass for supporting directives (e.g., depends_on) and phases Package metaclass for supporting directives (e.g., depends_on) and phases
""" """
phase_fmt = "_InstallPhase_{0}"
# These are accessed only through getattr, by name
_InstallPhase_run_before = {} # type: Dict[str, List[Callable]]
_InstallPhase_run_after = {} # type: Dict[str, List[Callable]]
def __new__(cls, name, bases, attr_dict): def __new__(cls, name, bases, attr_dict):
""" """
FIXME: REWRITE
Instance creation is preceded by phase attribute transformations. Instance creation is preceded by phase attribute transformations.
Conveniently transforms attributes to permit extensible phases by Conveniently transforms attributes to permit extensible phases by
@@ -392,70 +316,10 @@ def __new__(cls, name, bases, attr_dict):
InstallPhase attributes in the class that will be initialized in InstallPhase attributes in the class that will be initialized in
__init__. __init__.
""" """
if "phases" in attr_dict:
# Turn the strings in 'phases' into InstallPhase instances
# and add them as private attributes
_InstallPhase_phases = [PackageMeta.phase_fmt.format(x) for x in attr_dict["phases"]]
for phase_name, callback_name in zip(_InstallPhase_phases, attr_dict["phases"]):
attr_dict[phase_name] = InstallPhase(callback_name)
attr_dict["_InstallPhase_phases"] = _InstallPhase_phases
def _flush_callbacks(check_name):
# Name of the attribute I am going to check it exists
check_attr = PackageMeta.phase_fmt.format(check_name)
checks = getattr(cls, check_attr)
if checks:
for phase_name, funcs in checks.items():
phase_attr = PackageMeta.phase_fmt.format(phase_name)
try:
# Search for the phase in the attribute dictionary
phase = attr_dict[phase_attr]
except KeyError:
# If it is not there it's in the bases
# and we added a check. We need to copy
# and extend
for base in bases:
phase = getattr(base, phase_attr, None)
if phase is not None:
break
phase = attr_dict[phase_attr] = phase.copy()
getattr(phase, check_name).extend(funcs)
# Clear the attribute for the next class
setattr(cls, check_attr, {})
_flush_callbacks("run_before")
_flush_callbacks("run_after")
# Reset names for packages that inherit from another
# package with a different name
attr_dict["_name"] = None attr_dict["_name"] = None
return super(PackageMeta, cls).__new__(cls, name, bases, attr_dict) return super(PackageMeta, cls).__new__(cls, name, bases, attr_dict)
@staticmethod
def register_callback(check_type, *phases):
def _decorator(func):
attr_name = PackageMeta.phase_fmt.format(check_type)
check_list = getattr(PackageMeta, attr_name)
for item in phases:
checks = check_list.setdefault(item, [])
checks.append(func)
setattr(PackageMeta, attr_name, check_list)
return func
return _decorator
def run_before(*phases):
"""Registers a method of a package to be run before a given phase"""
return PackageMeta.register_callback("run_before", *phases)
def run_after(*phases):
"""Registers a method of a package to be run after a given phase"""
return PackageMeta.register_callback("run_after", *phases)
def on_package_attributes(**attr_dict): def on_package_attributes(**attr_dict):
"""Decorator: executes instance function only if object has attr valuses. """Decorator: executes instance function only if object has attr valuses.
@@ -475,7 +339,9 @@ def _wrapper(instance, *args, **kwargs):
has_all_attributes = all([hasattr(instance, key) for key in attr_dict]) has_all_attributes = all([hasattr(instance, key) for key in attr_dict])
if has_all_attributes: if has_all_attributes:
has_the_right_values = all( has_the_right_values = all(
[getattr(instance, key) == value for key, value in attr_dict.items()] [
getattr(instance, key) == value for key, value in attr_dict.items()
] # NOQA: ignore=E501
) )
if has_the_right_values: if has_the_right_values:
func(instance, *args, **kwargs) func(instance, *args, **kwargs)
@@ -687,13 +553,6 @@ class PackageBase(six.with_metaclass(PackageMeta, WindowsRPathMeta, PackageViewM
#: directories, sanity checks will fail. #: directories, sanity checks will fail.
sanity_check_is_dir = [] # type: List[str] sanity_check_is_dir = [] # type: List[str]
#: List of glob expressions. Each expression must either be
#: absolute or relative to the package source path.
#: Matching artifacts found at the end of the build process will be
#: copied in the same directory tree as _spack_build_logfile and
#: _spack_build_envfile.
archive_files = [] # type: List[str]
#: Boolean. Set to ``True`` for packages that require a manual download. #: Boolean. Set to ``True`` for packages that require a manual download.
#: This is currently used by package sanity tests and generation of a #: This is currently used by package sanity tests and generation of a
#: more meaningful fetch failure error. #: more meaningful fetch failure error.
@@ -1038,7 +897,7 @@ def all_urls_for_version(self, version):
version (spack.version.Version): the version for which a URL is sought version (spack.version.Version): the version for which a URL is sought
""" """
uf = None uf = None
if type(self).url_for_version != Package.url_for_version: if type(self).url_for_version != PackageBase.url_for_version:
uf = self.url_for_version uf = self.url_for_version
return self._implement_all_urls_for_version(version, uf) return self._implement_all_urls_for_version(version, uf)
@@ -1959,9 +1818,9 @@ def do_install(self, **kwargs):
even with exceptions. even with exceptions.
restage (bool): Force spack to restage the package source. restage (bool): Force spack to restage the package source.
skip_patch (bool): Skip patch stage of build if True. skip_patch (bool): Skip patch stage of build if True.
stop_before (InstallPhase): stop execution before this stop_before (str): stop execution before this
installation phase (or None) installation phase (or None)
stop_at (InstallPhase): last installation phase to be executed stop_at (str): last installation phase to be executed
(or None) (or None)
tests (bool or list or set): False to run no tests, True to test tests (bool or list or set): False to run no tests, True to test
all packages, or a list of package names to run tests for some all packages, or a list of package names to run tests for some
@@ -2191,46 +2050,6 @@ def unit_test_check(self):
""" """
return True return True
def sanity_check_prefix(self):
"""This function checks whether install succeeded."""
def check_paths(path_list, filetype, predicate):
if isinstance(path_list, six.string_types):
path_list = [path_list]
for path in path_list:
abs_path = os.path.join(self.prefix, path)
if not predicate(abs_path):
raise InstallError(
"Install failed for %s. No such %s in prefix: %s"
% (self.name, filetype, path)
)
check_paths(self.sanity_check_is_file, "file", os.path.isfile)
check_paths(self.sanity_check_is_dir, "directory", os.path.isdir)
ignore_file = match_predicate(spack.store.layout.hidden_file_regexes)
if all(map(ignore_file, os.listdir(self.prefix))):
raise InstallError("Install failed for %s. Nothing was installed!" % self.name)
def apply_macos_rpath_fixups(self):
"""On Darwin, make installed libraries more easily relocatable.
Some build systems (handrolled, autotools, makefiles) can set their own
rpaths that are duplicated by spack's compiler wrapper. This fixup
interrogates, and postprocesses if necessary, all libraries installed
by the code.
It should be added as a @run_after to packaging systems (or individual
packages) that do not install relocatable libraries by default.
"""
if "platform=darwin" not in self.spec:
return
from spack.relocate import fixup_macos_rpaths
fixup_macos_rpaths(self.spec)
@property @property
def build_log_path(self): def build_log_path(self):
""" """
@@ -2268,19 +2087,6 @@ def build_system_flags(cls, name, flags):
""" """
return None, None, flags return None, None, flags
def setup_build_environment(self, env):
"""Sets up the build environment for a package.
This method will be called before the current package prefix exists in
Spack's store.
Args:
env (spack.util.environment.EnvironmentModifications): environment
modifications to be applied when the package is built. Package authors
can call methods on it to alter the build environment.
"""
pass
def setup_run_environment(self, env): def setup_run_environment(self, env):
"""Sets up the run environment for a package. """Sets up the run environment for a package.
@@ -2291,37 +2097,6 @@ def setup_run_environment(self, env):
""" """
pass pass
def setup_dependent_build_environment(self, env, dependent_spec):
"""Sets up the build environment of packages that depend on this one.
This is similar to ``setup_build_environment``, but it is used to
modify the build environments of packages that *depend* on this one.
This gives packages like Python and others that follow the extension
model a way to implement common environment or compile-time settings
for dependencies.
This method will be called before the dependent package prefix exists
in Spack's store.
Examples:
1. Installing python modules generally requires ``PYTHONPATH``
to point to the ``lib/pythonX.Y/site-packages`` directory in the
module's install prefix. This method could be used to set that
variable.
Args:
env (spack.util.environment.EnvironmentModifications): environment
modifications to be applied when the dependent package is built.
Package authors can call methods on it to alter the build environment.
dependent_spec (spack.spec.Spec): the spec of the dependent package
about to be built. This allows the extendee (self) to query
the dependent's state. Note that *this* package's spec is
available as ``self.spec``
"""
pass
def setup_dependent_run_environment(self, env, dependent_spec): def setup_dependent_run_environment(self, env, dependent_spec):
"""Sets up the run environment of packages that depend on this one. """Sets up the run environment of packages that depend on this one.
@@ -2508,7 +2283,7 @@ def uninstall_by_spec(spec, force=False, deprecator=None):
def do_uninstall(self, force=False): def do_uninstall(self, force=False):
"""Uninstall this package by spec.""" """Uninstall this package by spec."""
# delegate to instance-less method. # delegate to instance-less method.
Package.uninstall_by_spec(self.spec, force) PackageBase.uninstall_by_spec(self.spec, force)
def do_deprecate(self, deprecator, link_fn): def do_deprecate(self, deprecator, link_fn):
"""Deprecate this package in favor of deprecator spec""" """Deprecate this package in favor of deprecator spec"""
@@ -2560,7 +2335,7 @@ def do_deprecate(self, deprecator, link_fn):
deprecated.package.do_deprecate(deprecator, link_fn) deprecated.package.do_deprecate(deprecator, link_fn)
# Now that we've handled metadata, uninstall and replace with link # Now that we've handled metadata, uninstall and replace with link
Package.uninstall_by_spec(spec, force=True, deprecator=deprecator) PackageBase.uninstall_by_spec(spec, force=True, deprecator=deprecator)
link_fn(deprecator.prefix, spec.prefix) link_fn(deprecator.prefix, spec.prefix)
def _check_extendable(self): def _check_extendable(self):
@@ -2811,21 +2586,25 @@ def rpath_args(self):
""" """
return " ".join("-Wl,-rpath,%s" % p for p in self.rpath) return " ".join("-Wl,-rpath,%s" % p for p in self.rpath)
def _run_test_callbacks(self, method_names, callback_type="install"): @property
def builder(self):
return spack.builder.create(self)
@staticmethod
def run_test_callbacks(builder, method_names, callback_type="install"):
"""Tries to call all of the listed methods, returning immediately """Tries to call all of the listed methods, returning immediately
if the list is None.""" if the list is None."""
if method_names is None: if not builder.pkg.run_tests or method_names is None:
return return
fail_fast = spack.config.get("config:fail_fast", False) fail_fast = spack.config.get("config:fail_fast", False)
with builder.pkg._setup_test(verbose=False, externals=False) as logger:
with self._setup_test(verbose=False, externals=False) as logger:
# Report running each of the methods in the build log # Report running each of the methods in the build log
print_test_message(logger, "Running {0}-time tests".format(callback_type), True) print_test_message(logger, "Running {0}-time tests".format(callback_type), True)
for name in method_names: for name in method_names:
try: try:
fn = getattr(self, name) fn = getattr(builder, name)
msg = ("RUN-TESTS: {0}-time tests [{1}]".format(callback_type, name),) msg = ("RUN-TESTS: {0}-time tests [{1}]".format(callback_type, name),)
print_test_message(logger, msg, True) print_test_message(logger, msg, True)
@@ -2835,27 +2614,13 @@ def _run_test_callbacks(self, method_names, callback_type="install"):
msg = ("RUN-TESTS: method not implemented [{0}]".format(name),) msg = ("RUN-TESTS: method not implemented [{0}]".format(name),)
print_test_message(logger, msg, True) print_test_message(logger, msg, True)
self.test_failures.append((e, msg)) builder.pkg.test_failures.append((e, msg))
if fail_fast: if fail_fast:
break break
# Raise any collected failures here # Raise any collected failures here
if self.test_failures: if builder.pkg.test_failures:
raise TestFailure(self.test_failures) raise TestFailure(builder.pkg.test_failures)
@on_package_attributes(run_tests=True)
def _run_default_build_time_test_callbacks(self):
"""Tries to call all the methods that are listed in the attribute
``build_time_test_callbacks`` if ``self.run_tests is True``.
"""
self._run_test_callbacks(self.build_time_test_callbacks, "build")
@on_package_attributes(run_tests=True)
def _run_default_install_time_test_callbacks(self):
"""Tries to call all the methods that are listed in the attribute
``install_time_test_callbacks`` if ``self.run_tests is True``.
"""
self._run_test_callbacks(self.install_time_test_callbacks, "install")
def has_test_method(pkg): def has_test_method(pkg):
@@ -2979,37 +2744,6 @@ def test_process(pkg, kwargs):
build_system_flags = PackageBase.build_system_flags build_system_flags = PackageBase.build_system_flags
class BundlePackage(PackageBase):
"""General purpose bundle, or no-code, package class."""
#: There are no phases by default but the property is required to support
#: post-install hooks (e.g., for module generation).
phases = [] # type: List[str]
#: This attribute is used in UI queries that require to know which
#: build-system class we are using
build_system_class = "BundlePackage"
#: Bundle packages do not have associated source or binary code.
has_code = False
class Package(PackageBase):
"""General purpose class with a single ``install``
phase that needs to be coded by packagers.
"""
#: The one and only phase
phases = ["install"]
#: This attribute is used in UI queries that require to know which
#: build-system class we are using
build_system_class = "Package"
# This will be used as a registration decorator in user
# packages, if need be
run_after("install")(PackageBase.sanity_check_prefix)
# On macOS, force rpaths for shared library IDs and remove duplicate rpaths
run_after("install")(PackageBase.apply_macos_rpath_fixups)
def install_dependency_symlinks(pkg, spec, prefix): def install_dependency_symlinks(pkg, spec, prefix):
""" """
Execute a dummy install and flatten dependencies. Execute a dummy install and flatten dependencies.

View File

@@ -352,7 +352,7 @@ def patch_for_package(self, sha256, pkg):
Arguments: Arguments:
sha256 (str): sha256 hash to look up sha256 (str): sha256 hash to look up
pkg (spack.package_base.Package): Package object to get patch for. pkg (spack.package_base.PackageBase): Package object to get patch for.
We build patch objects lazily because building them requires that We build patch objects lazily because building them requires that
we have information about the package's location in its repo. we have information about the package's location in its repo.

View File

@@ -22,7 +22,7 @@
# This package has a GitHub patch URL without full_index=1 # This package has a GitHub patch URL without full_index=1
(["invalid-github-patch-url"], ["PKG-DIRECTIVES", "PKG-PROPERTIES"]), (["invalid-github-patch-url"], ["PKG-DIRECTIVES", "PKG-PROPERTIES"]),
# This package has a stand-alone 'test' method in build-time callbacks # This package has a stand-alone 'test' method in build-time callbacks
(["test-build-callbacks"], ["PKG-DIRECTIVES", "PKG-PROPERTIES"]), (["fail-test-audit"], ["PKG-DIRECTIVES", "PKG-PROPERTIES"]),
# This package has no issues # This package has no issues
(["mpileaks"], None), (["mpileaks"], None),
# This package has a conflict with a trigger which cannot constrain the constraint # This package has a conflict with a trigger which cannot constrain the constraint

View File

@@ -13,10 +13,11 @@
import llnl.util.filesystem as fs import llnl.util.filesystem as fs
import spack.build_systems.autotools import spack.build_systems.autotools
import spack.build_systems.cmake
import spack.environment import spack.environment
import spack.platforms import spack.platforms
import spack.repo import spack.repo
from spack.build_environment import ChildError, get_std_cmake_args, setup_package from spack.build_environment import ChildError, setup_package
from spack.spec import Spec from spack.spec import Spec
from spack.util.executable import which from spack.util.executable import which
@@ -144,7 +145,7 @@ def test_libtool_archive_files_are_deleted_by_default(self, mutable_database):
# Assert the libtool archive is not there and we have # Assert the libtool archive is not there and we have
# a log of removed files # a log of removed files
assert not os.path.exists(s.package.libtool_archive_file) assert not os.path.exists(s.package.builder.libtool_archive_file)
search_directory = os.path.join(s.prefix, ".spack") search_directory = os.path.join(s.prefix, ".spack")
libtool_deletion_log = fs.find(search_directory, "removed_la_files.txt", recursive=True) libtool_deletion_log = fs.find(search_directory, "removed_la_files.txt", recursive=True)
assert libtool_deletion_log assert libtool_deletion_log
@@ -155,11 +156,11 @@ def test_libtool_archive_files_might_be_installed_on_demand(
# Install a package that creates a mock libtool archive, # Install a package that creates a mock libtool archive,
# patch its package to preserve the installation # patch its package to preserve the installation
s = Spec("libtool-deletion").concretized() s = Spec("libtool-deletion").concretized()
monkeypatch.setattr(s.package, "install_libtool_archives", True) monkeypatch.setattr(type(s.package.builder), "install_libtool_archives", True)
s.package.do_install(explicit=True) s.package.do_install(explicit=True)
# Assert libtool archives are installed # Assert libtool archives are installed
assert os.path.exists(s.package.libtool_archive_file) assert os.path.exists(s.package.builder.libtool_archive_file)
def test_autotools_gnuconfig_replacement(self, mutable_database): def test_autotools_gnuconfig_replacement(self, mutable_database):
""" """
@@ -253,22 +254,23 @@ class TestCMakePackage(object):
def test_cmake_std_args(self): def test_cmake_std_args(self):
# Call the function on a CMakePackage instance # Call the function on a CMakePackage instance
s = Spec("cmake-client").concretized() s = Spec("cmake-client").concretized()
assert s.package.std_cmake_args == get_std_cmake_args(s.package) expected = spack.build_systems.cmake.CMakeBuilder.std_args(s.package)
assert s.package.builder.std_cmake_args == expected
# Call it on another kind of package # Call it on another kind of package
s = Spec("mpich").concretized() s = Spec("mpich").concretized()
assert get_std_cmake_args(s.package) assert spack.build_systems.cmake.CMakeBuilder.std_args(s.package)
def test_cmake_bad_generator(self): def test_cmake_bad_generator(self, monkeypatch):
s = Spec("cmake-client").concretized() s = Spec("cmake-client").concretized()
s.package.generator = "Yellow Sticky Notes" monkeypatch.setattr(type(s.package), "generator", "Yellow Sticky Notes", raising=False)
with pytest.raises(spack.package_base.InstallError): with pytest.raises(spack.package_base.InstallError):
get_std_cmake_args(s.package) s.package.builder.std_cmake_args
def test_cmake_secondary_generator(config, mock_packages): def test_cmake_secondary_generator(config, mock_packages):
s = Spec("cmake-client").concretized() s = Spec("cmake-client").concretized()
s.package.generator = "CodeBlocks - Unix Makefiles" s.package.generator = "CodeBlocks - Unix Makefiles"
assert get_std_cmake_args(s.package) assert s.package.builder.std_cmake_args
def test_define(self): def test_define(self):
s = Spec("cmake-client").concretized() s = Spec("cmake-client").concretized()
@@ -361,7 +363,7 @@ def test_autotools_args_from_conditional_variant(config, mock_packages):
is not met. When this is the case, the variant is not set in the spec.""" is not met. When this is the case, the variant is not set in the spec."""
s = Spec("autotools-conditional-variants-test").concretized() s = Spec("autotools-conditional-variants-test").concretized()
assert "example" not in s.variants assert "example" not in s.variants
assert len(s.package._activate_or_not("example", "enable", "disable")) == 0 assert len(s.package.builder._activate_or_not("example", "enable", "disable")) == 0
def test_autoreconf_search_path_args_multiple(config, mock_packages, tmpdir): def test_autoreconf_search_path_args_multiple(config, mock_packages, tmpdir):

View File

@@ -0,0 +1,123 @@
# Copyright 2013-2022 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.path
import pytest
import spack.paths
@pytest.fixture()
def builder_test_repository():
builder_test_path = os.path.join(spack.paths.repos_path, "builder.test")
with spack.repo.use_repositories(builder_test_path) as mock_repo:
yield mock_repo
@pytest.mark.parametrize(
"spec_str,expected_values",
[
(
"callbacks@2.0",
[
("BEFORE_INSTALL_1_CALLED", "1"),
("BEFORE_INSTALL_2_CALLED", "1"),
("CALLBACKS_INSTALL_CALLED", "1"),
("AFTER_INSTALL_1_CALLED", "1"),
("TEST_VALUE", "3"),
("INSTALL_VALUE", "CALLBACKS"),
],
),
# The last callback is conditional on "@1.0", check it's being executed
(
"callbacks@1.0",
[
("BEFORE_INSTALL_1_CALLED", "1"),
("BEFORE_INSTALL_2_CALLED", "1"),
("CALLBACKS_INSTALL_CALLED", "1"),
("AFTER_INSTALL_1_CALLED", "1"),
("AFTER_INSTALL_2_CALLED", "1"),
("TEST_VALUE", "4"),
("INSTALL_VALUE", "CALLBACKS"),
],
),
# The package below adds to "callbacks" using inheritance, test that using super()
# works with builder hierarchies
(
"inheritance@1.0",
[
("DERIVED_BEFORE_INSTALL_CALLED", "1"),
("BEFORE_INSTALL_1_CALLED", "1"),
("BEFORE_INSTALL_2_CALLED", "1"),
("CALLBACKS_INSTALL_CALLED", "1"),
("INHERITANCE_INSTALL_CALLED", "1"),
("AFTER_INSTALL_1_CALLED", "1"),
("AFTER_INSTALL_2_CALLED", "1"),
("TEST_VALUE", "4"),
("INSTALL_VALUE", "INHERITANCE"),
],
),
# Generate custom phases using a GenericBuilder
(
"custom-phases",
[
("CONFIGURE_CALLED", "1"),
("INSTALL_CALLED", "1"),
("LAST_PHASE", "INSTALL"),
],
),
# Old-style package, with phase defined in base builder
(
"old-style-autotools@1.0",
[
("AFTER_AUTORECONF_1_CALLED", "1"),
],
),
(
"old-style-autotools@2.0",
[
("AFTER_AUTORECONF_2_CALLED", "1"),
],
),
(
"old-style-custom-phases",
[
("AFTER_CONFIGURE_CALLED", "1"),
("TEST_VALUE", "0"),
],
),
],
)
@pytest.mark.usefixtures("builder_test_repository", "config")
@pytest.mark.disable_clean_stage_check
def test_callbacks_and_installation_procedure(spec_str, expected_values, working_env):
"""Test the correct execution of callbacks and installation procedures for packages."""
s = spack.spec.Spec(spec_str).concretized()
builder = spack.builder.create(s.package)
for phase_fn in builder:
phase_fn.execute()
# Check calls have produced the expected side effects
for var_name, expected in expected_values:
assert os.environ[var_name] == expected, os.environ
@pytest.mark.usefixtures("builder_test_repository", "config")
@pytest.mark.parametrize(
"spec_str,method_name,expected",
[
# Call a function defined on the package, which calls the same function defined
# on the super(builder)
("old-style-autotools", "configure_args", ["--with-foo"]),
# Call a function defined on the package, which calls the same function defined on the
# super(pkg), which calls the same function defined in the super(builder)
("old-style-derived", "configure_args", ["--with-bar", "--with-foo"]),
],
)
def test_old_style_compatibility_with_super(spec_str, method_name, expected):
s = spack.spec.Spec(spec_str).concretized()
builder = spack.builder.create(s.package)
value = getattr(builder, method_name)()
assert value == expected

View File

@@ -43,7 +43,7 @@ def test_it_just_runs(pkg):
def test_info_noversion(mock_packages, print_buffer): def test_info_noversion(mock_packages, print_buffer):
"""Check that a mock package with no versions or variants outputs None.""" """Check that a mock package with no versions outputs None."""
info("noversion") info("noversion")
line_iter = iter(print_buffer) line_iter = iter(print_buffer)
@@ -52,7 +52,7 @@ def test_info_noversion(mock_packages, print_buffer):
has = [desc in line for desc in ["Preferred", "Safe", "Deprecated"]] has = [desc in line for desc in ["Preferred", "Safe", "Deprecated"]]
if not any(has): if not any(has):
continue continue
elif "Variants" not in line: else:
continue continue
assert "None" in next(line_iter).strip() assert "None" in next(line_iter).strip()

View File

@@ -1087,7 +1087,7 @@ def test_install_empty_env(
("test-install-callbacks", "undefined-install-test"), ("test-install-callbacks", "undefined-install-test"),
], ],
) )
def test_install_callbacks_fail(install_mockery, mock_fetch, name, method): def test_installation_fail_tests(install_mockery, mock_fetch, name, method):
output = install("--test=root", "--no-cache", name, fail_on_error=False) output = install("--test=root", "--no-cache", name, fail_on_error=False)
assert output.count(method) == 2 assert output.count(method) == 2

View File

@@ -237,8 +237,7 @@ def test_test_list_all(mock_packages):
"simple-standalone-test", "simple-standalone-test",
"test-error", "test-error",
"test-fail", "test-fail",
"test-build-callbacks", "fail-test-audit",
"test-install-callbacks",
] ]
) )

View File

@@ -228,7 +228,7 @@ def test_concretize(self, spec):
check_concretize(spec) check_concretize(spec)
def test_concretize_mention_build_dep(self): def test_concretize_mention_build_dep(self):
spec = check_concretize("cmake-client ^cmake@3.4.3") spec = check_concretize("cmake-client ^cmake@3.21.3")
# Check parent's perspective of child # Check parent's perspective of child
to_dependencies = spec.edges_to_dependencies(name="cmake") to_dependencies = spec.edges_to_dependencies(name="cmake")

View File

@@ -7,6 +7,7 @@
import pytest import pytest
import spack.build_systems.generic
import spack.config import spack.config
import spack.repo import spack.repo
import spack.util.spack_yaml as syaml import spack.util.spack_yaml as syaml
@@ -102,7 +103,7 @@ def fake_installs(monkeypatch, tmpdir):
stage_path = str(tmpdir.ensure("fake-stage", dir=True)) stage_path = str(tmpdir.ensure("fake-stage", dir=True))
universal_unused_stage = spack.stage.DIYStage(stage_path) universal_unused_stage = spack.stage.DIYStage(stage_path)
monkeypatch.setattr( monkeypatch.setattr(
spack.package_base.Package, "_make_stage", MakeStage(universal_unused_stage) spack.build_systems.generic.Package, "_make_stage", MakeStage(universal_unused_stage)
) )

View File

@@ -18,6 +18,7 @@
import spack.config import spack.config
import spack.environment as ev import spack.environment as ev
import spack.main import spack.main
import spack.package_base
import spack.paths import spack.paths
import spack.repo import spack.repo
import spack.schema.compilers import spack.schema.compilers
@@ -1170,13 +1171,13 @@ def test_license_dir_config(mutable_config, mock_packages):
"""Ensure license directory is customizable""" """Ensure license directory is customizable"""
expected_dir = spack.paths.default_license_dir expected_dir = spack.paths.default_license_dir
assert spack.config.get("config:license_dir") == expected_dir assert spack.config.get("config:license_dir") == expected_dir
assert spack.package.Package.global_license_dir == expected_dir assert spack.package_base.PackageBase.global_license_dir == expected_dir
assert spack.repo.path.get_pkg_class("a").global_license_dir == expected_dir assert spack.repo.path.get_pkg_class("a").global_license_dir == expected_dir
rel_path = os.path.join(os.path.sep, "foo", "bar", "baz") rel_path = os.path.join(os.path.sep, "foo", "bar", "baz")
spack.config.set("config:license_dir", rel_path) spack.config.set("config:license_dir", rel_path)
assert spack.config.get("config:license_dir") == rel_path assert spack.config.get("config:license_dir") == rel_path
assert spack.package.Package.global_license_dir == rel_path assert spack.package_base.PackageBase.global_license_dir == rel_path
assert spack.repo.path.get_pkg_class("a").global_license_dir == rel_path assert spack.repo.path.get_pkg_class("a").global_license_dir == rel_path

View File

@@ -103,6 +103,19 @@ def test_ascii_graph_mpileaks(config, mock_packages, monkeypatch):
| o libdwarf | o libdwarf
|/ |/
o libelf o libelf
"""
or graph_str
== r"""o mpileaks
|\
o | callpath
|\|
| o mpich
|
o dyninst
|\
o | libdwarf
|/
o libelf
""" """
) )

View File

@@ -144,13 +144,12 @@ def __getattr__(self, attr):
return getattr(self.wrapped_stage, attr) return getattr(self.wrapped_stage, attr)
def test_partial_install_delete_prefix_and_stage(install_mockery, mock_fetch): def test_partial_install_delete_prefix_and_stage(install_mockery, mock_fetch, working_env):
s = Spec("canfail").concretized() s = Spec("canfail").concretized()
instance_rm_prefix = s.package.remove_prefix instance_rm_prefix = s.package.remove_prefix
try: try:
s.package.succeed = False
s.package.remove_prefix = mock_remove_prefix s.package.remove_prefix = mock_remove_prefix
with pytest.raises(MockInstallError): with pytest.raises(MockInstallError):
s.package.do_install() s.package.do_install()
@@ -161,7 +160,7 @@ def test_partial_install_delete_prefix_and_stage(install_mockery, mock_fetch):
# must clear failure markings for the package before re-installing it # must clear failure markings for the package before re-installing it
spack.store.db.clear_failure(s, True) spack.store.db.clear_failure(s, True)
s.package.succeed = True s.package.set_install_succeed()
s.package.stage = MockStage(s.package.stage) s.package.stage = MockStage(s.package.stage)
s.package.do_install(restage=True) s.package.do_install(restage=True)
@@ -174,18 +173,20 @@ def test_partial_install_delete_prefix_and_stage(install_mockery, mock_fetch):
@pytest.mark.disable_clean_stage_check @pytest.mark.disable_clean_stage_check
def test_failing_overwrite_install_should_keep_previous_installation(mock_fetch, install_mockery): def test_failing_overwrite_install_should_keep_previous_installation(
mock_fetch, install_mockery, working_env
):
""" """
Make sure that whenever `spack install --overwrite` fails, spack restores Make sure that whenever `spack install --overwrite` fails, spack restores
the original install prefix instead of cleaning it. the original install prefix instead of cleaning it.
""" """
# Do a successful install # Do a successful install
s = Spec("canfail").concretized() s = Spec("canfail").concretized()
s.package.succeed = True s.package.set_install_succeed()
# Do a failing overwrite install # Do a failing overwrite install
s.package.do_install() s.package.do_install()
s.package.succeed = False s.package.set_install_fail()
kwargs = {"overwrite": [s.dag_hash()]} kwargs = {"overwrite": [s.dag_hash()]}
with pytest.raises(Exception): with pytest.raises(Exception):
@@ -238,13 +239,11 @@ def test_install_dependency_symlinks_pkg(install_mockery, mock_fetch, mutable_mo
def test_install_times(install_mockery, mock_fetch, mutable_mock_repo): def test_install_times(install_mockery, mock_fetch, mutable_mock_repo):
"""Test install times added.""" """Test install times added."""
spec = Spec("dev-build-test-install-phases") spec = Spec("dev-build-test-install-phases").concretized()
spec.concretize() spec.package.do_install()
pkg = spec.package
pkg.do_install()
# Ensure dependency directory exists after the installation. # Ensure dependency directory exists after the installation.
install_times = os.path.join(pkg.prefix, ".spack", "install_times.json") install_times = os.path.join(spec.package.prefix, ".spack", "install_times.json")
assert os.path.isfile(install_times) assert os.path.isfile(install_times)
# Ensure the phases are included # Ensure the phases are included
@@ -346,12 +345,11 @@ def test_installed_upstream(install_upstream, mock_fetch):
@pytest.mark.disable_clean_stage_check @pytest.mark.disable_clean_stage_check
def test_partial_install_keep_prefix(install_mockery, mock_fetch, monkeypatch): def test_partial_install_keep_prefix(install_mockery, mock_fetch, monkeypatch, working_env):
s = Spec("canfail").concretized() s = Spec("canfail").concretized()
# If remove_prefix is called at any point in this test, that is an error # If remove_prefix is called at any point in this test, that is an error
s.package.succeed = False # make the build fail monkeypatch.setattr(spack.package_base.PackageBase, "remove_prefix", mock_remove_prefix)
monkeypatch.setattr(spack.package_base.Package, "remove_prefix", mock_remove_prefix)
with pytest.raises(spack.build_environment.ChildError): with pytest.raises(spack.build_environment.ChildError):
s.package.do_install(keep_prefix=True) s.package.do_install(keep_prefix=True)
assert os.path.exists(s.package.prefix) assert os.path.exists(s.package.prefix)
@@ -359,7 +357,7 @@ def test_partial_install_keep_prefix(install_mockery, mock_fetch, monkeypatch):
# must clear failure markings for the package before re-installing it # must clear failure markings for the package before re-installing it
spack.store.db.clear_failure(s, True) spack.store.db.clear_failure(s, True)
s.package.succeed = True # make the build succeed s.package.set_install_succeed()
s.package.stage = MockStage(s.package.stage) s.package.stage = MockStage(s.package.stage)
s.package.do_install(keep_prefix=True) s.package.do_install(keep_prefix=True)
assert s.package.spec.installed assert s.package.spec.installed
@@ -368,14 +366,14 @@ def test_partial_install_keep_prefix(install_mockery, mock_fetch, monkeypatch):
def test_second_install_no_overwrite_first(install_mockery, mock_fetch, monkeypatch): def test_second_install_no_overwrite_first(install_mockery, mock_fetch, monkeypatch):
s = Spec("canfail").concretized() s = Spec("canfail").concretized()
monkeypatch.setattr(spack.package_base.Package, "remove_prefix", mock_remove_prefix) monkeypatch.setattr(spack.package_base.PackageBase, "remove_prefix", mock_remove_prefix)
s.package.succeed = True s.package.set_install_succeed()
s.package.do_install() s.package.do_install()
assert s.package.spec.installed assert s.package.spec.installed
# If Package.install is called after this point, it will fail # If Package.install is called after this point, it will fail
s.package.succeed = False s.package.set_install_fail()
s.package.do_install() s.package.do_install()
@@ -589,7 +587,9 @@ def _install(src, dest):
source = spec.package.stage.source_path source = spec.package.stage.source_path
config = os.path.join(source, "config.log") config = os.path.join(source, "config.log")
fs.touchp(config) fs.touchp(config)
spec.package.archive_files = ["missing", "..", config] monkeypatch.setattr(
type(spec.package), "archive_files", ["missing", "..", config], raising=False
)
spack.installer.log(spec.package) spack.installer.log(spec.package)

View File

@@ -113,14 +113,14 @@ def test_absolute_import_spack_packages_as_python_modules(mock_packages):
assert hasattr(spack.pkg.builtin.mock, "mpileaks") assert hasattr(spack.pkg.builtin.mock, "mpileaks")
assert hasattr(spack.pkg.builtin.mock.mpileaks, "Mpileaks") assert hasattr(spack.pkg.builtin.mock.mpileaks, "Mpileaks")
assert isinstance(spack.pkg.builtin.mock.mpileaks.Mpileaks, spack.package_base.PackageMeta) assert isinstance(spack.pkg.builtin.mock.mpileaks.Mpileaks, spack.package_base.PackageMeta)
assert issubclass(spack.pkg.builtin.mock.mpileaks.Mpileaks, spack.package_base.Package) assert issubclass(spack.pkg.builtin.mock.mpileaks.Mpileaks, spack.package_base.PackageBase)
def test_relative_import_spack_packages_as_python_modules(mock_packages): def test_relative_import_spack_packages_as_python_modules(mock_packages):
from spack.pkg.builtin.mock.mpileaks import Mpileaks from spack.pkg.builtin.mock.mpileaks import Mpileaks
assert isinstance(Mpileaks, spack.package_base.PackageMeta) assert isinstance(Mpileaks, spack.package_base.PackageMeta)
assert issubclass(Mpileaks, spack.package_base.Package) assert issubclass(Mpileaks, spack.package_base.PackageBase)
def test_all_virtual_packages_have_default_providers(): def test_all_virtual_packages_have_default_providers():

View File

@@ -64,7 +64,7 @@ def __init__(self, spec):
# list of URL attributes and metadata attributes # list of URL attributes and metadata attributes
# these will be removed from packages. # these will be removed from packages.
self.metadata_attrs = [s.url_attr for s in spack.fetch_strategy.all_strategies] self.metadata_attrs = [s.url_attr for s in spack.fetch_strategy.all_strategies]
self.metadata_attrs += spack.package_base.Package.metadata_attrs self.metadata_attrs += spack.package_base.PackageBase.metadata_attrs
self.spec = spec self.spec = spec
self.in_classdef = False # used to avoid nested classdefs self.in_classdef = False # used to avoid nested classdefs
@@ -158,6 +158,7 @@ def __init__(self, spec):
def visit_FunctionDef(self, func): def visit_FunctionDef(self, func):
conditions = [] conditions = []
for dec in func.decorator_list: for dec in func.decorator_list:
if isinstance(dec, ast.Call) and dec.func.id == "when": if isinstance(dec, ast.Call) and dec.func.id == "when":
try: try:

View File

@@ -784,7 +784,7 @@ def find_versions_of_archive(
list_depth (int): max depth to follow links on list_url pages. list_depth (int): max depth to follow links on list_url pages.
Defaults to 0. Defaults to 0.
concurrency (int): maximum number of concurrent requests concurrency (int): maximum number of concurrent requests
reference_package (spack.package_base.Package or None): a spack package reference_package (spack.package_base.PackageBase or None): a spack package
used as a reference for url detection. Uses the url_for_version used as a reference for url detection. Uses the url_for_version
method on the package to produce reference urls which, if found, method on the package to produce reference urls which, if found,
are preferred. are preferred.

View File

@@ -96,7 +96,7 @@ def validate_or_raise(self, vspec, pkg_cls=None):
Args: Args:
vspec (Variant): instance to be validated vspec (Variant): instance to be validated
pkg_cls (spack.package_base.Package): the package class pkg_cls (spack.package_base.PackageBase): the package class
that required the validation, if available that required the validation, if available
Raises: Raises:

View File

@@ -24,6 +24,8 @@ spack:
- openmpi - openmpi
- mpich - mpich
variants: +mpi variants: +mpi
tbb:
require: "intel-tbb"
binutils: binutils:
variants: +ld +gold +headers +libiberty ~nls variants: +ld +gold +headers +libiberty ~nls
version: version:

View File

@@ -24,6 +24,8 @@ spack:
- openmpi - openmpi
- mpich - mpich
variants: +mpi variants: +mpi
tbb:
require: "intel-tbb"
binutils: binutils:
variants: +ld +gold +headers +libiberty ~nls variants: +ld +gold +headers +libiberty ~nls
version: version:

View File

@@ -24,6 +24,8 @@ spack:
- openmpi - openmpi
- mpich - mpich
variants: +mpi variants: +mpi
tbb:
require: "intel-tbb"
binutils: binutils:
variants: +ld +gold +headers +libiberty ~nls variants: +ld +gold +headers +libiberty ~nls
version: version:

View File

@@ -14,6 +14,8 @@ spack:
definitions: definitions:
- default_specs: - default_specs:
- 'uncrustify build_system=autotools'
- 'uncrustify build_system=cmake'
- lz4 # MakefilePackage - lz4 # MakefilePackage
- mpich~fortran # AutotoolsPackage - mpich~fortran # AutotoolsPackage
- py-setuptools # PythonPackage - py-setuptools # PythonPackage

View File

@@ -21,6 +21,8 @@ spack:
mpi: [mpich] mpi: [mpich]
target: [x86_64] target: [x86_64]
variants: +mpi variants: +mpi
tbb:
require: "intel-tbb"
binutils: binutils:
variants: +ld +gold +headers +libiberty ~nls variants: +ld +gold +headers +libiberty ~nls
cuda: cuda:

View File

@@ -18,6 +18,8 @@ spack:
packages: packages:
all: all:
target: [x86_64] target: [x86_64]
tbb:
require: 'intel-tbb'
definitions: definitions:
- gcc_system_packages: - gcc_system_packages:

View File

@@ -0,0 +1,45 @@
# Copyright 2013-2022 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
import spack.build_systems.generic
from spack.package import *
class Callbacks(Package):
"""Package used to verify that callbacks on phases work correctly, including conditions"""
homepage = "http://www.example.com"
url = "http://www.example.com/a-1.0.tar.gz"
version("2.0", "abcdef0123456789abcdef0123456789")
version("1.0", "0123456789abcdef0123456789abcdef")
class GenericBuilder(spack.build_systems.generic.GenericBuilder):
def install(self, pkg, spec, prefix):
os.environ["CALLBACKS_INSTALL_CALLED"] = "1"
os.environ["INSTALL_VALUE"] = "CALLBACKS"
mkdirp(prefix.bin)
@run_before("install")
def before_install_1(self):
os.environ["BEFORE_INSTALL_1_CALLED"] = "1"
os.environ["TEST_VALUE"] = "1"
@run_before("install")
def before_install_2(self):
os.environ["BEFORE_INSTALL_2_CALLED"] = "1"
os.environ["TEST_VALUE"] = "2"
@run_after("install")
def after_install_1(self):
os.environ["AFTER_INSTALL_1_CALLED"] = "1"
os.environ["TEST_VALUE"] = "3"
@run_after("install", when="@1.0")
def after_install_2(self):
os.environ["AFTER_INSTALL_2_CALLED"] = "1"
os.environ["TEST_VALUE"] = "4"

View File

@@ -0,0 +1,31 @@
# Copyright 2013-2022 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
import spack.build_systems.generic
from spack.package import *
class CustomPhases(Package):
"""Package used to verify that we can set custom phases on builders"""
homepage = "http://www.example.com"
url = "http://www.example.com/a-1.0.tar.gz"
version("2.0", "abcdef0123456789abcdef0123456789")
version("1.0", "0123456789abcdef0123456789abcdef")
class GenericBuilder(spack.build_systems.generic.GenericBuilder):
phases = ["configure", "install"]
def configure(self, pkg, spec, prefix):
os.environ["CONFIGURE_CALLED"] = "1"
os.environ["LAST_PHASE"] = "CONFIGURE"
def install(self, pkg, spec, prefix):
os.environ["INSTALL_CALLED"] = "1"
os.environ["LAST_PHASE"] = "INSTALL"
mkdirp(prefix.bin)

View File

@@ -0,0 +1,15 @@
# Copyright 2013-2022 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)
from spack.package import *
class Gnuconfig(Package):
"""This package is needed to allow mocking AutotoolsPackage objects"""
homepage = "http://www.example.com"
url = "http://www.example.com/a-1.0.tar.gz"
version("2.0", "abcdef0123456789abcdef0123456789")
version("1.0", "0123456789abcdef0123456789abcdef")

View File

@@ -0,0 +1,26 @@
# Copyright 2013-2022 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
import spack.pkg.builder.test.callbacks
from spack.package import *
class Inheritance(spack.pkg.builder.test.callbacks.Callbacks):
"""Package used to verify that inheritance among packages work as expected"""
pass
class GenericBuilder(spack.pkg.builder.test.callbacks.GenericBuilder):
def install(self, pkg, spec, prefix):
super(GenericBuilder, self).install(pkg, spec, prefix)
os.environ["INHERITANCE_INSTALL_CALLED"] = "1"
os.environ["INSTALL_VALUE"] = "INHERITANCE"
@run_before("install")
def derived_before_install(self):
os.environ["DERIVED_BEFORE_INSTALL_CALLED"] = "1"
os.environ["TEST_VALUE"] = "0"

View File

@@ -0,0 +1,50 @@
# Copyright 2013-2022 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 spack.package import *
class OldStyleAutotools(AutotoolsPackage):
"""Package used to verify that old-style packages work correctly when executing the
installation procedure.
"""
homepage = "http://www.example.com"
url = "http://www.example.com/a-1.0.tar.gz"
version("2.0", "abcdef0123456789abcdef0123456789")
version("1.0", "0123456789abcdef0123456789abcdef")
def configure(self, spec, prefix):
pass
def build(self, spec, prefix):
pass
def install(self, spec, prefix):
mkdirp(prefix.bin)
def configure_args(self):
"""This override a function in the builder and construct the result using a method
defined in this class and a super method defined in the builder.
"""
return [self.foo()] + super(OldStyleAutotools, self).configure_args()
def foo(self):
return "--with-foo"
@run_before("autoreconf")
def create_configure(self):
mkdirp(self.configure_directory)
touch(self.configure_abs_path)
@run_after("autoreconf", when="@1.0")
def after_autoreconf_1(self):
os.environ["AFTER_AUTORECONF_1_CALLED"] = "1"
@run_after("autoreconf", when="@2.0")
def after_autoreconf_2(self):
os.environ["AFTER_AUTORECONF_2_CALLED"] = "1"

View File

@@ -0,0 +1,34 @@
# Copyright 2013-2022 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 spack.package import *
class OldStyleCustomPhases(AutotoolsPackage):
"""Package used to verify that old-style packages work correctly when defining custom
phases (though it's not recommended for packagers to do so).
"""
homepage = "http://www.example.com"
url = "http://www.example.com/a-1.0.tar.gz"
version("2.0", "abcdef0123456789abcdef0123456789")
version("1.0", "0123456789abcdef0123456789abcdef")
phases = ["configure"]
def configure(self, spec, prefix):
mkdirp(prefix.bin)
@run_after("configure")
def after_configure(self):
os.environ["AFTER_CONFIGURE_CALLED"] = "1"
os.environ["TEST_VALUE"] = "0"
@run_after("install")
def after_install(self):
os.environ["AFTER_INSTALL_CALLED"] = "1"
os.environ["TEST_VALUE"] = "1"

View File

@@ -0,0 +1,21 @@
# Copyright 2013-2022 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 spack.pkg.builder.test.old_style_autotools
from spack.package import *
class OldStyleDerived(spack.pkg.builder.test.old_style_autotools.OldStyleAutotools):
"""Package used to verify that old-style packages work correctly when executing the
installation procedure.
"""
homepage = "http://www.example.com"
url = "http://www.example.com/a-1.0.tar.gz"
version("2.0", "abcdef0123456789abcdef0123456789")
version("1.0", "0123456789abcdef0123456789abcdef")
def configure_args(self):
return ["--with-bar"] + super(OldStyleDerived, self).configure_args()

View File

@@ -0,0 +1,2 @@
repo:
namespace: builder.test

View File

@@ -2,7 +2,7 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import spack.build_systems.autotools
from spack.package import * from spack.package import *
@@ -32,21 +32,23 @@ class A(AutotoolsPackage):
parallel = False parallel = False
class AutotoolsBuilder(spack.build_systems.autotools.AutotoolsBuilder):
def with_or_without_fee(self, activated): def with_or_without_fee(self, activated):
if not activated: if not activated:
return "--no-fee" return "--no-fee"
return "--fee-all-the-time" return "--fee-all-the-time"
def autoreconf(self, spec, prefix): def autoreconf(self, pkg, spec, prefix):
pass pass
def configure(self, spec, prefix): def configure(self, pkg, spec, prefix):
pass pass
def build(self, spec, prefix): def build(self, pkg, spec, prefix):
pass pass
def install(self, spec, prefix): def install(self, pkg, spec, prefix):
# sanity_check_prefix requires something in the install directory # sanity_check_prefix requires something in the install directory
# Test requires overriding the one provided by `AutotoolsPackage` # Test requires overriding the one provided by `AutotoolsPackage`
mkdirp(prefix.bin) mkdirp(prefix.bin)

View File

@@ -8,7 +8,6 @@
class AttributesFoo(BundlePackage): class AttributesFoo(BundlePackage):
phases = ["install"]
version("1.0") version("1.0")
provides("bar") provides("bar")

View File

@@ -2,6 +2,7 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os
from spack.package import * from spack.package import *
@@ -14,7 +15,16 @@ class Canfail(Package):
version("1.0", "0123456789abcdef0123456789abcdef") version("1.0", "0123456789abcdef0123456789abcdef")
succeed = False def set_install_succeed(self):
os.environ["CANFAIL_SUCCEED"] = "1"
def set_install_fail(self):
os.environ.pop("CANFAIL_SUCCEED", None)
@property
def succeed(self):
result = True if "CANFAIL_SUCCEED" in os.environ else False
return result
def install(self, spec, prefix): def install(self, spec, prefix):
if not self.succeed: if not self.succeed:

View File

@@ -15,7 +15,7 @@ def check(condition, msg):
class CmakeClient(CMakePackage): class CmakeClient(CMakePackage):
"""A dumy package that uses cmake.""" """A dummy package that uses cmake."""
homepage = "https://www.example.com" homepage = "https://www.example.com"
url = "https://www.example.com/cmake-client-1.0.tar.gz" url = "https://www.example.com/cmake-client-1.0.tar.gz"
@@ -38,14 +38,16 @@ class CmakeClient(CMakePackage):
did_something = False did_something = False
@run_after("cmake") @run_after("cmake")
@run_before("cmake", "build", "install") @run_before("cmake")
@run_before("build")
@run_before("install")
def increment(self): def increment(self):
self.callback_counter += 1 CmakeClient.callback_counter += 1
@run_after("cmake") @run_after("cmake")
@on_package_attributes(run_this=True, check_this_is_none=None) @on_package_attributes(run_this=True, check_this_is_none=None)
def flip(self): def flip(self):
self.flipped = True CmakeClient.flipped = True
@run_after("cmake") @run_after("cmake")
@on_package_attributes(does_not_exist=None) @on_package_attributes(does_not_exist=None)

View File

@@ -18,11 +18,16 @@ def check(condition, msg):
class Cmake(Package): class Cmake(Package):
"""A dumy package for the cmake build system.""" """A dummy package for the cmake build system."""
homepage = "https://www.cmake.org" homepage = "https://www.cmake.org"
url = "https://cmake.org/files/v3.4/cmake-3.4.3.tar.gz" url = "https://cmake.org/files/v3.4/cmake-3.4.3.tar.gz"
version(
"3.23.1",
"4cb3ff35b2472aae70f542116d616e63",
url="https://cmake.org/files/v3.4/cmake-3.4.3.tar.gz",
)
version( version(
"3.4.3", "3.4.3",
"4cb3ff35b2472aae70f542116d616e63", "4cb3ff35b2472aae70f542116d616e63",

View File

@@ -7,14 +7,12 @@
from spack.package import * from spack.package import *
class DevBuildTestDependent(Package): class DevBuildTestDependent(MakefilePackage):
homepage = "example.com" homepage = "example.com"
url = "fake.com" url = "fake.com"
version("0.0.0", sha256="0123456789abcdef0123456789abcdef") version("0.0.0", sha256="0123456789abcdef0123456789abcdef")
phases = ["edit", "install"]
filename = "dev-build-test-file.txt" filename = "dev-build-test-file.txt"
original_string = "This file should be edited" original_string = "This file should be edited"
replacement_string = "This file has been edited" replacement_string = "This file has been edited"
@@ -28,5 +26,8 @@ def edit(self, spec, prefix):
f.truncate() f.truncate()
f.write(self.replacement_string) f.write(self.replacement_string)
def build(self, spec, prefix):
pass
def install(self, spec, prefix): def install(self, spec, prefix):
install(self.filename, prefix) install(self.filename, prefix)

Some files were not shown because too many files have changed in this diff Show More