modules: configurable module defaults (#24367)
Any spec satisfying a default will be symlinked to `default` If multiple specs have modulefiles in the same directory and satisfy configured module defaults, then whichever was written last will be default.
This commit is contained in:
		| @@ -449,6 +449,36 @@ that are already in the LMod hierarchy. | ||||
|    For hierarchies that are deeper than three layers ``lmod spider`` may have some issues. | ||||
|    See `this discussion on the LMod project <https://github.com/TACC/Lmod/issues/114>`_. | ||||
|  | ||||
| """""""""""""""""""""" | ||||
| Select default modules | ||||
| """""""""""""""""""""" | ||||
|  | ||||
| By default, when multiple modules of the same name share a directory, | ||||
| the highest version number will be the default module. This behavior | ||||
| of the ``module`` command can be overridden with a symlink named | ||||
| ``default`` to the desired default module. If you wish to configure | ||||
| default modules with Spack, add a ``defaults`` key to your modules | ||||
| configuration: | ||||
|  | ||||
| .. code-block:: yaml | ||||
|  | ||||
|   modules: | ||||
|     my-module-set: | ||||
|       tcl: | ||||
|         defaults: | ||||
|         - gcc@10.2.1 | ||||
|         - hdf5@1.2.10+mpi+hl%gcc | ||||
|  | ||||
| These defaults may be arbitrarily specific. For any package that | ||||
| satisfies a default, Spack will generate the module file in the | ||||
| appropriate path, and will generate a default symlink to the module | ||||
| file as well. | ||||
|  | ||||
| .. warning::  | ||||
|   If Spack is configured to generate multiple default packages in the | ||||
|   same directory, the last modulefile to be generated will be the | ||||
|   default module. | ||||
|  | ||||
| .. _customize-env-modifications: | ||||
|  | ||||
| """"""""""""""""""""""""""""""""""" | ||||
|   | ||||
| @@ -209,6 +209,10 @@ def merge_config_rules(configuration, spec): | ||||
|     verbose = module_specific_configuration.get('verbose', False) | ||||
|     spec_configuration['verbose'] = verbose | ||||
| 
 | ||||
|     # module defaults per-package | ||||
|     defaults = module_specific_configuration.get('defaults', []) | ||||
|     spec_configuration['defaults'] = defaults | ||||
| 
 | ||||
|     return spec_configuration | ||||
| 
 | ||||
| 
 | ||||
| @@ -453,6 +457,11 @@ def template(self): | ||||
|         """ | ||||
|         return self.conf.get('template', None) | ||||
| 
 | ||||
|     @property | ||||
|     def defaults(self): | ||||
|         """Returns the specs configured as defaults or [].""" | ||||
|         return self.conf.get('defaults', []) | ||||
| 
 | ||||
|     @property | ||||
|     def env(self): | ||||
|         """List of environment modifications that should be done in the | ||||
| @@ -891,6 +900,18 @@ def write(self, overwrite=False): | ||||
|         if os.path.exists(self.layout.filename): | ||||
|             fp.set_permissions_by_spec(self.layout.filename, self.spec) | ||||
| 
 | ||||
|         # Symlink defaults if needed | ||||
|         if any(self.spec.satisfies(default) for default in self.conf.defaults): | ||||
|             # This spec matches a default, it needs to be symlinked to default | ||||
|             # Symlink to a tmp location first and move, so that existing | ||||
|             # symlinks do not cause an error. | ||||
|             default_path = os.path.join(os.path.dirname(self.layout.filename), | ||||
|                                         'default') | ||||
|             default_tmp = os.path.join(os.path.dirname(self.layout.filename), | ||||
|                                        '.tmp_spack_default') | ||||
|             os.symlink(self.layout.filename, default_tmp) | ||||
|             os.rename(default_tmp, default_path) | ||||
| 
 | ||||
|     def remove(self): | ||||
|         """Deletes the module file.""" | ||||
|         mod_file = self.layout.filename | ||||
|   | ||||
| @@ -17,8 +17,8 @@ | ||||
| #: THIS NEEDS TO BE UPDATED FOR EVERY NEW KEYWORD THAT | ||||
| #: IS ADDED IMMEDIATELY BELOW THE MODULE TYPE ATTRIBUTE | ||||
| spec_regex = r'(?!hierarchy|core_specs|verbose|hash_length|whitelist|' \ | ||||
|              r'blacklist|projections|naming_scheme|core_compilers|all)' \ | ||||
|              r'(^\w[\w-]*)' | ||||
|              r'blacklist|projections|naming_scheme|core_compilers|all|' \ | ||||
|              r'defaults)(^\w[\w-]*)' | ||||
| 
 | ||||
| #: Matches a valid name for a module set | ||||
| # Banned names are valid entries at that level in the previous schema | ||||
| @@ -99,6 +99,7 @@ | ||||
|                 'type': 'boolean', | ||||
|                 'default': False | ||||
|             }, | ||||
|             'defaults': array_of_strings, | ||||
|             'naming_scheme': { | ||||
|                 'type': 'string'  # Can we be more specific here? | ||||
|             }, | ||||
|   | ||||
| @@ -46,13 +46,28 @@ def test_update_dictionary_extending_list(): | ||||
| @pytest.fixture() | ||||
| def mock_module_filename(monkeypatch, tmpdir): | ||||
|     filename = str(tmpdir.join('module')) | ||||
|     monkeypatch.setattr(spack.modules.common.BaseFileLayout, | ||||
|     # Set for both module types so we can test both | ||||
|     monkeypatch.setattr(spack.modules.lmod.LmodFileLayout, | ||||
|                         'filename', | ||||
|                         filename) | ||||
|     monkeypatch.setattr(spack.modules.tcl.TclFileLayout, | ||||
|                         'filename', | ||||
|                         filename) | ||||
| 
 | ||||
|     yield filename | ||||
| 
 | ||||
| 
 | ||||
| @pytest.fixture() | ||||
| def mock_module_defaults(monkeypatch): | ||||
|     def impl(*args): | ||||
|         # No need to patch both types because neither override base | ||||
|         monkeypatch.setattr(spack.modules.common.BaseConfiguration, | ||||
|                             'defaults', | ||||
|                             [arg for arg in args]) | ||||
| 
 | ||||
|     return impl | ||||
| 
 | ||||
| 
 | ||||
| @pytest.fixture() | ||||
| def mock_package_perms(monkeypatch): | ||||
|     perms = stat.S_IRGRP | stat.S_IWGRP | ||||
| @@ -77,6 +92,22 @@ def test_modules_written_with_proper_permissions(mock_module_filename, | ||||
|         mock_module_filename).st_mode == mock_package_perms | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize('module_type', ['tcl', 'lmod']) | ||||
| def test_modules_default_symlink( | ||||
|         module_type, mock_packages, mock_module_filename, mock_module_defaults, config | ||||
| ): | ||||
|     spec = spack.spec.Spec('mpileaks@2.3').concretized() | ||||
|     mock_module_defaults(spec.format('{name}{@version}')) | ||||
| 
 | ||||
|     generator_cls = spack.modules.module_types[module_type] | ||||
|     generator = generator_cls(spec, 'default') | ||||
|     generator.write() | ||||
| 
 | ||||
|     link_path = os.path.join(os.path.dirname(mock_module_filename), 'default') | ||||
|     assert os.path.islink(link_path) | ||||
|     assert os.readlink(link_path) == mock_module_filename | ||||
| 
 | ||||
| 
 | ||||
| class MockDb(object): | ||||
|     def __init__(self, db_ids, spec_hash_to_db): | ||||
|         self.upstream_dbs = db_ids | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Greg Becker
					Greg Becker