diff options
-rw-r--r-- | lib/spack/docs/module_file_support.rst | 30 | ||||
-rw-r--r-- | lib/spack/spack/modules/common.py | 21 | ||||
-rw-r--r-- | lib/spack/spack/schema/modules.py | 5 | ||||
-rw-r--r-- | lib/spack/spack/test/modules/common.py | 33 |
4 files changed, 86 insertions, 3 deletions
diff --git a/lib/spack/docs/module_file_support.rst b/lib/spack/docs/module_file_support.rst index 029c6dbbbd..960d5ebdaa 100644 --- a/lib/spack/docs/module_file_support.rst +++ b/lib/spack/docs/module_file_support.rst @@ -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: """"""""""""""""""""""""""""""""""" diff --git a/lib/spack/spack/modules/common.py b/lib/spack/spack/modules/common.py index 7c34a5338e..e09b0f1b6b 100644 --- a/lib/spack/spack/modules/common.py +++ b/lib/spack/spack/modules/common.py @@ -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 @@ -454,6 +458,11 @@ class BaseConfiguration(object): 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 module. @@ -891,6 +900,18 @@ class BaseModuleFileWriter(object): 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 diff --git a/lib/spack/spack/schema/modules.py b/lib/spack/spack/schema/modules.py index 07a495af13..d3619b1823 100644 --- a/lib/spack/spack/schema/modules.py +++ b/lib/spack/spack/schema/modules.py @@ -17,8 +17,8 @@ import spack.schema.projections #: 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 @@ module_type_configuration = { 'type': 'boolean', 'default': False }, + 'defaults': array_of_strings, 'naming_scheme': { 'type': 'string' # Can we be more specific here? }, diff --git a/lib/spack/spack/test/modules/common.py b/lib/spack/spack/test/modules/common.py index 61303cc7a6..ae156ede9e 100644 --- a/lib/spack/spack/test/modules/common.py +++ b/lib/spack/spack/test/modules/common.py @@ -46,7 +46,11 @@ 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) @@ -54,6 +58,17 @@ def mock_module_filename(monkeypatch, tmpdir): @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 monkeypatch.setattr(spack.package_prefs, @@ -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 |