diff --git a/reframe/core/buildsystems.py b/reframe/core/buildsystems.py
index 8d5cf6e37e..88ab054111 100644
--- a/reframe/core/buildsystems.py
+++ b/reframe/core/buildsystems.py
@@ -12,6 +12,13 @@
from reframe.core.exceptions import BuildSystemError
+class _UndefinedType:
+ '''Used as an initial value for undefined values instead of None.'''
+
+
+_Undefined = _UndefinedType()
+
+
class BuildSystem(abc.ABC):
'''The abstract base class of any build system.
@@ -157,6 +164,16 @@ def post_build(self, buildjob):
'''
+ def prepare_cmds(self):
+ '''Callback function that the framework will call before run.
+
+ Build systems may use this information to add commands to the run
+ script before anything set by the user.
+
+ :meta private:
+ '''
+ return []
+
def _resolve_flags(self, flags, environ):
_flags = getattr(self, flags)
if _flags:
@@ -782,6 +799,100 @@ def generated_modules(self):
return self._eb_modules
+class Spack(BuildSystem):
+ '''A build system for building test code using `Spack
+ `__.
+
+ ReFrame will use a user-provided Spack environment in order to build and
+ test a set of specs.
+
+ .. versionadded:: 3.6.1
+
+ '''
+
+ #: The Spack environment to use for building this test.
+ #:
+ #: ReFrame will activate and install this environment.
+ #: This environment will also be used to run the test.
+ #:
+ #: .. code-block:: bash
+ #:
+ #: spack env activate -V -d
+ #:
+ #: ReFrame looks for environments in the test's
+ #: :attr:`~reframe.core.pipeline.RegressionTest.sourcesdir`.
+ #:
+ #: This field is required.
+ #:
+ #: :type: :class:`str` or :class:`None`
+ #: :default: :class:`None`
+ environment = fields.TypedField(typ.Str[r'\S+'], _UndefinedType)
+
+ #: The list of specs to build and install within the given environment.
+ #:
+ #: ReFrame will add the specs to the active environment by emititing the
+ #: following command:
+ #:
+ #: .. code-block:: bash
+ #:
+ #: spack add spec1 spec2 ... specN
+ #:
+ #: If no spec is passed, ReFrame will simply install what is prescribed by
+ #: the environment.
+ #:
+ #: :type: :class:`List[str]`
+ #: :default: ``[]``
+ specs = fields.TypedField(typ.List[str])
+
+ #: Emit the necessary ``spack load`` commands before running the test.
+ #:
+ #: :type: :class:`bool`
+ #: :default: :obj:`True`
+ emit_load_cmds = fields.TypedField(bool)
+
+ #: Options to pass to ``spack install``
+ #:
+ #: :type: :class:`List[str]`
+ #: :default: ``[]``
+ install_opts = fields.TypedField(typ.List[str])
+
+ def __init__(self):
+ super().__init__()
+ self.specs = []
+ self.environment = _Undefined
+ self.emit_load_cmds = True
+ self.install_opts = []
+ self._prefix_save = None
+
+ def emit_build_commands(self, environ):
+ self._prefix_save = os.getcwd()
+ if self.environment is _Undefined:
+ raise BuildSystemError(f'no Spack environment is defined')
+
+ ret = self._env_activate_cmds()
+ if self.specs:
+ specs_str = ' '.join(self.specs)
+ ret.append(f'spack add {specs_str}')
+
+ install_cmd = 'spack install'
+ if self.install_opts:
+ install_cmd += ' ' + ' '.join(self.install_opts)
+
+ ret.append(install_cmd)
+ return ret
+
+ def _env_activate_cmds(self):
+ return [f'. $SPACK_ROOT/share/spack/setup-env.sh',
+ f'spack env activate -V -d {self.environment}']
+
+ def prepare_cmds(self):
+ cmds = self._env_activate_cmds()
+ if self.specs and self.emit_load_cmds:
+ cmds.append('spack load ' + ' '.join(s for s in self.specs))
+
+ return cmds
+
+
class BuildSystemField(fields.TypedField):
def __init__(self, fieldname, *other_types):
super().__init__(fieldname, BuildSystem, *other_types)
diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py
index f01bfe628d..aeea2ce53a 100644
--- a/reframe/core/pipeline.py
+++ b/reframe/core/pipeline.py
@@ -1339,7 +1339,18 @@ def run(self):
)
exec_cmd = [self.job.launcher.run_command(self.job),
self.executable, *self.executable_opts]
- commands = [*self.prerun_cmds, ' '.join(exec_cmd), *self.postrun_cmds]
+
+ if self.build_system:
+ prepare_cmds = self.build_system.prepare_cmds()
+ else:
+ prepare_cmds = []
+
+ commands = [
+ *prepare_cmds,
+ *self.prerun_cmds,
+ ' '.join(exec_cmd),
+ *self.postrun_cmds
+ ]
user_environ = env.Environment(type(self).__name__,
self.modules, self.variables.items())
environs = [
diff --git a/unittests/test_buildsystems.py b/unittests/test_buildsystems.py
index 1fa25a8225..45c2d975c4 100644
--- a/unittests/test_buildsystems.py
+++ b/unittests/test_buildsystems.py
@@ -243,6 +243,47 @@ def test_singlesource_unknown_language():
build_system.emit_build_commands(ProgEnvironment('testenv'))
+def test_spack(environ, tmp_path):
+ build_system = bs.Spack()
+ build_system.environment = 'spack_env'
+ build_system.install_opts = ['-j 10']
+ with osext.change_dir(tmp_path):
+ assert build_system.emit_build_commands(environ) == [
+ f'. $SPACK_ROOT/share/spack/setup-env.sh',
+ f'spack env activate -V -d {build_system.environment}',
+ f'spack install -j 10'
+ ]
+ assert build_system.prepare_cmds() == [
+ f'. $SPACK_ROOT/share/spack/setup-env.sh',
+ f'spack env activate -V -d {build_system.environment}',
+ ]
+
+
+def test_spack_with_spec(environ, tmp_path):
+ build_system = bs.Spack()
+ build_system.environment = 'spack_env'
+ build_system.specs = ['spec1@version1', 'spec2@version2']
+ specs_str = ' '.join(build_system.specs)
+ with osext.change_dir(tmp_path):
+ assert build_system.emit_build_commands(environ) == [
+ f'. $SPACK_ROOT/share/spack/setup-env.sh',
+ f'spack env activate -V -d {build_system.environment}',
+ f'spack add {specs_str}',
+ f'spack install'
+ ]
+ assert build_system.prepare_cmds() == [
+ f'. $SPACK_ROOT/share/spack/setup-env.sh',
+ f'spack env activate -V -d {build_system.environment}',
+ f'spack load {specs_str}',
+ ]
+
+
+def test_spack_no_env(environ, tmp_path):
+ build_system = bs.Spack()
+ with pytest.raises(BuildSystemError):
+ build_system.emit_build_commands(environ)
+
+
def test_easybuild(environ, tmp_path):
build_system = bs.EasyBuild()
build_system.easyconfigs = ['ec1.eb', 'ec2.eb']