summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/spack/docs/module_file_support.rst30
-rw-r--r--lib/spack/spack/modules/common.py21
-rw-r--r--lib/spack/spack/schema/modules.py5
-rw-r--r--lib/spack/spack/test/modules/common.py33
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