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']