Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions reframe/core/buildsystems.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -782,6 +799,100 @@ def generated_modules(self):
return self._eb_modules


class Spack(BuildSystem):
'''A build system for building test code using `Spack
<https://spack.io/>`__.

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 <environment directory>
#:
#: 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)
Expand Down
13 changes: 12 additions & 1 deletion reframe/core/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
41 changes: 41 additions & 0 deletions unittests/test_buildsystems.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down