diff --git a/lib/ramble/ramble/application_types/spack.py b/lib/ramble/ramble/application_types/spack.py index 5fa92bac6..da16591bb 100644 --- a/lib/ramble/ramble/application_types/spack.py +++ b/lib/ramble/ramble/application_types/spack.py @@ -71,6 +71,12 @@ def __init__(self, file_path): def _long_print(self): out_str = super()._long_print() + if hasattr(self, 'package_manager_configs'): + out_str.append('\n') + out_str.append(section_title('Package Manager Configs:\n')) + for name, config in self.package_manager_configs.items(): + out_str.append(f'\t{name} = {config}\n') + for group in self._spec_groups: if hasattr(self, group[0]): out_str.append('\n') @@ -135,6 +141,14 @@ def _create_spack_env(self, workspace): else: workspace.add_to_cache(cache_tupl) + package_manager_config_dicts = [self.package_manager_configs] + for mod_inst in self._modifier_instances: + package_manager_config_dicts.append(mod_inst.package_manager_configs) + + for config_dict in package_manager_config_dicts: + for _, config in config_dict.items(): + self.spack_runner.add_config(config) + try: self.spack_runner.set_dry_run(workspace.dry_run) self.spack_runner.create_env(self.expander.expand_var('{spack_env}')) diff --git a/lib/ramble/ramble/language/shared_language.py b/lib/ramble/ramble/language/shared_language.py index 791475368..2566175cd 100644 --- a/lib/ramble/ramble/language/shared_language.py +++ b/lib/ramble/ramble/language/shared_language.py @@ -154,6 +154,21 @@ def _execute_software_spec(obj): return _execute_software_spec +@shared_directive('package_manager_configs') +def package_manager_config(name, config, **kwargs): + """Defines a config option to set within a package manager + + Define a new config which will be passed to a package manager. The + resulting experiment instance will pass the config to the package manager, + which will control the logic of applying it. + """ + + def _execute_package_manager_config(obj): + obj.package_manager_configs[name] = config + + return _execute_package_manager_config + + @shared_directive('required_packages') def required_package(name): """Defines a new spack package that is required for this object diff --git a/lib/ramble/ramble/modifier.py b/lib/ramble/ramble/modifier.py index 9fb0aa4fe..bf988d4d7 100644 --- a/lib/ramble/ramble/modifier.py +++ b/lib/ramble/ramble/modifier.py @@ -130,6 +130,12 @@ def _long_print(self): out_str.append(rucolor.section_title('Executable Modifiers:\n')) out_str.append('\t' + colified(self.executable_modifiers.keys(), tty=True) + '\n') + if hasattr(self, 'package_manager_configs'): + out_str.append(rucolor.section_title('Package Manager Configs:\n')) + for name, config in self.package_manager_configs.items(): + out_str.append(f'\t{name} = {config}\n') + out_str.append('\n') + if hasattr(self, 'default_compilers'): out_str.append(rucolor.section_title('Default Compilers:\n')) for comp_name, comp_def in self.default_compilers.items(): diff --git a/lib/ramble/ramble/spack_runner.py b/lib/ramble/ramble/spack_runner.py index cd79722fd..8f71c782a 100644 --- a/lib/ramble/ramble/spack_runner.py +++ b/lib/ramble/ramble/spack_runner.py @@ -99,6 +99,8 @@ def __init__(self, shell='bash', dry_run=False): self.dry_run = dry_run self.concretized = False self.compiler_config_dir = None + self.configs = [] + self.configs_applied = False def get_version(self): """Get spack's version""" @@ -180,6 +182,12 @@ def configure_env(self, path): # Ensure subsequent commands use the created env now. self.env_path = path + def add_config(self, config): + """ + Add a config option to this spack environment. + """ + self.configs.append(config) + def create_env(self, path, output=None, error=None): """ Ensure a spack environment is created, and set the path to it within @@ -375,6 +383,30 @@ def add_include_file(self, include_file): if file_name in self._allowed_config_files: self.includes.append(include_file) + def apply_configs(self): + """ + Add all defined configs to the environment + """ + + if self.configs_applied: + return + + self._check_active() + + config_args = [ + 'config', + 'add' + ] + + for config in self.configs: + args = config_args.copy() + args.append(config) + self.exe(*args) + if self.dry_run: + self._dry_run_print(args) + + self.configs_applied = True + def copy_from_external_env(self, env_name_or_path): """ Copy an external spack environment file into the generated environment. @@ -419,6 +451,9 @@ def copy_from_external_env(self, env_name_or_path): shutil.copyfile(conf_file, os.path.join(self.env_path, 'spack.yaml')) + if self.configs: + self.apply_configs() + self.concretized = found_lock def generate_env_file(self): @@ -465,6 +500,9 @@ def generate_env_file(self): with open(os.path.join(self.env_path, 'spack.yaml'), 'w+') as f: syaml.dump_config(env_file, f, default_flow_style=False) + if self.configs: + self.apply_configs() + def concretize(self): """ Concretize a spack environment. @@ -526,29 +564,6 @@ def install(self): 'install' ] args.extend(install_flags.split()) - if not self.dry_run: - self.exe(*args) - else: - self._dry_run_print(args) - - for mod_type in ['tcl', 'lmod']: - args = [ - 'module', - mod_type, - 'refresh', - '-y' - ] - - if not self.dry_run: - self.exe(*args) - else: - self._dry_run_print(args) - - args = [ - 'env', - 'loads' - ] - if not self.dry_run: self.exe(*args) else: diff --git a/lib/ramble/ramble/test/application_language.py b/lib/ramble/ramble/test/application_language.py index a9a3a2e30..5cf41ada5 100644 --- a/lib/ramble/ramble/test/application_language.py +++ b/lib/ramble/ramble/test/application_language.py @@ -32,6 +32,7 @@ def test_application_type_features(app_class): assert hasattr(test_app, 'software_specs') assert hasattr(test_app, 'required_packages') assert hasattr(test_app, 'maintainers') + assert hasattr(test_app, 'package_manager_configs') def add_workload(app_inst, wl_num=1): diff --git a/lib/ramble/ramble/test/cmd/info.py b/lib/ramble/ramble/test/cmd/info.py index b5e5a799e..c6e64bc6d 100644 --- a/lib/ramble/ramble/test/cmd/info.py +++ b/lib/ramble/ramble/test/cmd/info.py @@ -78,7 +78,25 @@ def test_spack_info_software(app_query): 'Tags:', 'spack_spec =', 'compiler =', + ) + + out = info(app_query) + + for field in expected_fields: + assert field in out + +@pytest.mark.parametrize('app_query', [ + 'zlib-configs', +]) +def test_mock_spack_info_software(mock_applications, app_query): + expected_fields = ( + 'Description:', + 'Setup Pipeline Phases:', + 'Analyze Pipeline Phases:', + 'Tags:', + 'Package Manager Configs:', + 'spack_spec =', ) out = info(app_query) diff --git a/lib/ramble/ramble/test/cmd/mods.py b/lib/ramble/ramble/test/cmd/mods.py index 145314623..7ddf8339e 100644 --- a/lib/ramble/ramble/test/cmd/mods.py +++ b/lib/ramble/ramble/test/cmd/mods.py @@ -16,7 +16,7 @@ def check_info(output): expected_sections = ['Tags', 'Mode', 'Builtin Executables', 'Executable Modifiers', 'Default Compilers', - 'Software Specs'] + 'Software Specs', 'Package Manager Configs'] for section in expected_sections: assert section in output diff --git a/lib/ramble/ramble/test/end_to_end/package_manager_config.py b/lib/ramble/ramble/test/end_to_end/package_manager_config.py new file mode 100644 index 000000000..26e92deb6 --- /dev/null +++ b/lib/ramble/ramble/test/end_to_end/package_manager_config.py @@ -0,0 +1,71 @@ +# Copyright 2022-2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 or the MIT license +# , at your +# option. This file may not be copied, modified, or distributed +# except according to those terms. + +import os + +import pytest + +import ramble.workspace +import ramble.config +import ramble.software_environments +from ramble.main import RambleCommand + + +pytestmark = pytest.mark.usefixtures('mutable_config', + 'mutable_mock_workspace_path') + +workspace = RambleCommand('workspace') + + +def test_package_manager_config_zlib(mock_applications): + test_config = """ +ramble: + variables: + mpi_command: '' + batch_submit: 'batch_submit {execute_experiment}' + processes_per_node: '1' + n_ranks: '1' + applications: + zlib-configs: + workloads: + ensure_installed: + experiments: + test: + variables: {} + spack: + concretized: true + packages: + zlib: + spack_spec: 'zlib' + environments: + zlib-configs: + packages: + - zlib +""" + + workspace_name = 'test_package_manager_config_zlib' + with ramble.workspace.create(workspace_name) as ws: + ws.write() + + config_path = os.path.join(ws.config_dir, ramble.workspace.config_file_name) + + with open(config_path, 'w+') as f: + f.write(test_config) + ws._re_read() + + workspace('setup', '--dry-run', global_args=['-w', workspace_name]) + + spack_yaml = os.path.join(ws.software_dir, 'zlib-configs.ensure_installed', + 'spack.yaml') + + assert os.path.isfile(spack_yaml) + + with open(spack_yaml, 'r') as f: + data = f.read() + assert 'config:' in data + assert 'debug: true' in data diff --git a/lib/ramble/ramble/test/modifier_functionality/mock_modifier_spack_configs.py b/lib/ramble/ramble/test/modifier_functionality/mock_modifier_spack_configs.py new file mode 100644 index 000000000..4efb0750f --- /dev/null +++ b/lib/ramble/ramble/test/modifier_functionality/mock_modifier_spack_configs.py @@ -0,0 +1,63 @@ +# Copyright 2022-2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 or the MIT license +# , at your +# option. This file may not be copied, modified, or distributed +# except according to those terms. + +import os + +import pytest + +from ramble.test.dry_run_helpers import dry_run_config, SCOPES +import ramble.test.modifier_functionality.modifier_helpers as modifier_helpers +import ramble.workspace +from ramble.main import RambleCommand + +workspace = RambleCommand('workspace') + + +@pytest.mark.parametrize( + 'scope', + [ + SCOPES.workspace, + SCOPES.application, + SCOPES.workload, + SCOPES.experiment, + ] +) +def test_gromacs_mock_spack_config_mod(mutable_mock_workspace_path, + mutable_applications, + mock_modifiers, + scope): + workspace_name = 'test_gromacs_mock_spack_config_mod' + + test_modifiers = [ + (scope, modifier_helpers.named_modifier('spack-mod')), + ] + + with ramble.workspace.create(workspace_name) as ws1: + ws1.write() + + config_path = os.path.join(ws1.config_dir, ramble.workspace.config_file_name) + + dry_run_config('modifiers', test_modifiers, config_path, 'gromacs', 'water_bare') + + ws1._re_read() + + workspace('concretize', global_args=['-D', ws1.root]) + workspace('setup', '--dry-run', global_args=['-D', ws1.root]) + exp_script = os.path.join(ws1.experiment_dir, 'gromacs', 'water_bare', + 'test_exp', 'execute_experiment') + + assert os.path.isfile(exp_script) + + spack_yaml = os.path.join(ws1.software_dir, 'gromacs.water_bare', + 'spack.yaml') + assert os.path.isfile(spack_yaml) + + with open(spack_yaml, 'r') as f: + data = f.read() + + assert 'debug: true' in data diff --git a/lib/ramble/ramble/test/modifier_language.py b/lib/ramble/ramble/test/modifier_language.py index ab9f7b713..5aac97ed3 100644 --- a/lib/ramble/ramble/test/modifier_language.py +++ b/lib/ramble/ramble/test/modifier_language.py @@ -48,6 +48,7 @@ def test_modifier_type_features(mod_class): assert hasattr(test_mod, 'executable_modifiers') assert hasattr(test_mod, 'env_var_modifications') assert hasattr(test_mod, 'maintainers') + assert hasattr(test_mod, 'package_manager_configs') def add_mode(mod_inst, mode_num=1): diff --git a/lib/ramble/ramble/test/spack_runner.py b/lib/ramble/ramble/test/spack_runner.py index 64d682c0c..8fdfc47c7 100644 --- a/lib/ramble/ramble/test/spack_runner.py +++ b/lib/ramble/ramble/test/spack_runner.py @@ -120,7 +120,6 @@ def test_env_install(tmpdir, capsys): captured = capsys.readouterr() assert "with args: ['install'" in captured.out - assert "with args: ['env', 'loads']" in captured.out sr.deactivate() @@ -135,6 +134,35 @@ def test_env_install(tmpdir, capsys): pytest.skip('%s' % e) +def test_env_configs_apply(tmpdir, capsys): + try: + env_path = str(tmpdir.join('spack-env')) + # Dry run so we don't actually install zlib + sr = ramble.spack_runner.SpackRunner(dry_run=True) + sr.create_env(env_path) + sr.activate() + sr.add_spec('zlib') + sr.add_config('config:debug:true') + sr.generate_env_file() + + captured = capsys.readouterr() + assert "with args: ['config', 'add', 'config:debug:true']" in captured.out + + sr.deactivate() + + env_file = os.path.join(env_path, 'spack.yaml') + + assert os.path.exists(env_file) + + with open(env_file, 'r') as f: + data = f.read() + assert 'zlib' in data + assert 'debug: true' in data + + except ramble.spack_runner.RunnerError as e: + pytest.skip('%s' % e) + + def test_default_concretize_flags(tmpdir, capsys): try: env_path = tmpdir.join('spack-env') @@ -376,6 +404,34 @@ def test_external_env_copies(tmpdir): pytest.skip('%s' % e) +def test_configs_apply_to_external_env(tmpdir): + src_spack_yaml = """ +spack: + specs: [ 'zlib' ] +""" + with tmpdir.as_cwd(): + with open(os.path.join(os.getcwd(), 'spack.yaml'), 'w+') as f: + f.write(src_spack_yaml) + + try: + sr = ramble.spack_runner.SpackRunner(dry_run=True) + generated_env = os.path.join(os.getcwd(), 'dest_env') + sr.create_env(os.path.join(generated_env)) + sr.activate() + sr.add_config('config:debug:true') + sr.copy_from_external_env(os.getcwd()) + + assert os.path.exists(os.path.join(generated_env, 'spack.yaml')) + + with open(os.path.join(generated_env, 'spack.yaml'), 'r') as f: + data = f.read() + assert 'zlib' in data + assert 'config:' in data + assert 'debug: true' in data + except ramble.spack_runner.RunnerError as e: + pytest.skip('%s' % e) + + def test_invalid_external_env_errors(tmpdir): with tmpdir.as_cwd(): try: diff --git a/var/ramble/repos/builtin.mock/applications/zlib-configs/application.py b/var/ramble/repos/builtin.mock/applications/zlib-configs/application.py new file mode 100644 index 000000000..fa3cb4375 --- /dev/null +++ b/var/ramble/repos/builtin.mock/applications/zlib-configs/application.py @@ -0,0 +1,28 @@ +# Copyright 2022-2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 or the MIT license +# , at your +# option. This file may not be copied, modified, or distributed +# except according to those terms. + +from ramble.appkit import * + + +class ZlibConfigs(SpackApplication): + name = "zlib-configs" + + software_spec('zlib', spack_spec='zlib') + + executable('list_lib', 'ls {zlib}/lib', use_mpi=False) + + workload('ensure_installed', executable='list_lib') + + package_manager_config('enable_debug', 'config:debug:true') + + figure_of_merit('zlib_installed', + fom_regex=r'(?Plibz.so.*)', group_name='lib_name', + units='') + + success_criteria('zlib_installed', mode='string', + match=r'libz.so', file='{log_file}') diff --git a/var/ramble/repos/builtin.mock/modifiers/spack-mod/modifier.py b/var/ramble/repos/builtin.mock/modifiers/spack-mod/modifier.py index 580dbba3f..865ff780a 100644 --- a/var/ramble/repos/builtin.mock/modifiers/spack-mod/modifier.py +++ b/var/ramble/repos/builtin.mock/modifiers/spack-mod/modifier.py @@ -17,6 +17,8 @@ class SpackMod(SpackModifier): mode('default', description='This is the default mode for the spack-mod') + package_manager_config('enable_debug', 'config:debug:true') + default_compiler('mod_compiler', spack_spec='mod_compiler@1.1 target=x86_64', compiler_spec='mod_compiler@1.1')