Use Spec.format for token substitution in modules (#1848)

This replaces a custom token-based substitution format with calls to
Spec.format in modules.py

This also resolves a couple issues:

- LmodModules set configuration globally instead of in its initializer
  which meant test-specific configuration was not applied
- Added support for setting hash_length=0 for LmodModules. This only
  affects the module filename and not the directory names for the
  hierarchy tokens in the path. This includes an additional unit test.
This commit is contained in:
scheibelp 2016-10-29 13:56:34 -07:00 committed by Todd Gamblin
parent c82985cb5e
commit 23683c65de
3 changed files with 87 additions and 65 deletions

View File

@ -131,15 +131,13 @@ def dependencies(spec, request='all'):
# FIXME : step among nodes that refer to the same package? # FIXME : step among nodes that refer to the same package?
seen = set() seen = set()
seen_add = seen.add seen_add = seen.add
l = [xx l = sorted(
for xx in sorted( spec.traverse(order='post',
spec.traverse(order='post', cover='nodes',
depth=True, deptype=('link', 'run'),
cover='nodes', root=False),
deptype=('link', 'run'), reverse=True)
root=False), return [x for x in l if not (x in seen or seen_add(x))]
reverse=True)]
return [xx for ii, xx in l if not (xx in seen or seen_add(xx))]
def update_dictionary_extending_lists(target, update): def update_dictionary_extending_lists(target, update):
@ -238,6 +236,10 @@ def filter_blacklisted(specs, module_name):
yield x yield x
def format_env_var_name(name):
return name.replace('-', '_').upper()
class EnvModule(object): class EnvModule(object):
name = 'env_module' name = 'env_module'
formats = {} formats = {}
@ -274,51 +276,30 @@ def naming_scheme(self):
naming_scheme = self.default_naming_format naming_scheme = self.default_naming_format
return naming_scheme return naming_scheme
@property
def tokens(self):
"""Tokens that can be substituted in environment variable values
and naming schemes
"""
tokens = {
'name': self.spec.name,
'version': self.spec.version,
'compiler': self.spec.compiler,
'prefix': self.spec.package.prefix
}
return tokens
@property
def upper_tokens(self):
"""Tokens that can be substituted in environment variable names"""
upper_tokens = {
'name': self.spec.name.replace('-', '_').upper()
}
return upper_tokens
@property @property
def use_name(self): def use_name(self):
""" """
Subclasses should implement this to return the name the module command Subclasses should implement this to return the name the module command
uses to refer to the package. uses to refer to the package.
""" """
naming_tokens = self.tokens name = self.spec.format(self.naming_scheme)
naming_scheme = self.naming_scheme
name = naming_scheme.format(**naming_tokens)
# Not everybody is working on linux... # Not everybody is working on linux...
parts = name.split('/') parts = name.split('/')
name = join_path(*parts) name = join_path(*parts)
# Add optional suffixes based on constraints # Add optional suffixes based on constraints
path_elements = [name] + self._get_suffixes()
return '-'.join(path_elements)
def _get_suffixes(self):
configuration, _ = parse_config_options(self) configuration, _ = parse_config_options(self)
suffixes = [name] suffixes = []
for constraint, suffix in configuration.get('suffixes', {}).items(): for constraint, suffix in configuration.get('suffixes', {}).items():
if constraint in self.spec: if constraint in self.spec:
suffixes.append(suffix) suffixes.append(suffix)
# Always append the hash to make the module file unique hash_length = configuration.get('hash_length', 7)
hash_length = configuration.pop('hash_length', 7)
if hash_length != 0: if hash_length != 0:
suffixes.append(self.spec.dag_hash(length=hash_length)) suffixes.append(self.spec.dag_hash(length=hash_length))
name = '-'.join(suffixes) return suffixes
return name
@property @property
def category(self): def category(self):
@ -456,8 +437,9 @@ def prerequisite(self, spec):
def process_environment_command(self, env): def process_environment_command(self, env):
for command in env: for command in env:
# Token expansion from configuration file # Token expansion from configuration file
name = command.args.get('name', '').format(**self.upper_tokens) name = format_env_var_name(
value = str(command.args.get('value', '')).format(**self.tokens) self.spec.format(command.args.get('name', '')))
value = self.spec.format(str(command.args.get('value', '')))
command.update_args(name=name, value=value) command.update_args(name=name, value=value)
# Format the line int the module file # Format the line int the module file
try: try:
@ -501,7 +483,7 @@ class Dotkit(EnvModule):
autoload_format = 'dk_op {module_file}\n' autoload_format = 'dk_op {module_file}\n'
default_naming_format = \ default_naming_format = \
'{name}-{version}-{compiler.name}-{compiler.version}' '${PACKAGE}-${VERSION}-${COMPILERNAME}-${COMPILERVER}'
@property @property
def file_name(self): def file_name(self):
@ -544,7 +526,7 @@ class TclModule(EnvModule):
prerequisite_format = 'prereq {module_file}\n' prerequisite_format = 'prereq {module_file}\n'
default_naming_format = \ default_naming_format = \
'{name}-{version}-{compiler.name}-{compiler.version}' '${PACKAGE}-${VERSION}-${COMPILERNAME}-${COMPILERVER}'
@property @property
def file_name(self): def file_name(self):
@ -595,8 +577,9 @@ def process_environment_command(self, env):
} }
for command in env: for command in env:
# Token expansion from configuration file # Token expansion from configuration file
name = command.args.get('name', '').format(**self.upper_tokens) name = format_env_var_name(
value = str(command.args.get('value', '')).format(**self.tokens) self.spec.format(command.args.get('name', '')))
value = self.spec.format(str(command.args.get('value', '')))
command.update_args(name=name, value=value) command.update_args(name=name, value=value)
# Format the line int the module file # Format the line int the module file
try: try:
@ -614,7 +597,6 @@ def process_environment_command(self, env):
tty.warn(details.format(**command.args)) tty.warn(details.format(**command.args))
def module_specific_content(self, configuration): def module_specific_content(self, configuration):
naming_tokens = self.tokens
# Conflict # Conflict
conflict_format = configuration.get('conflict', []) conflict_format = configuration.get('conflict', [])
f = string.Formatter() f = string.Formatter()
@ -635,7 +617,7 @@ def module_specific_content(self, configuration):
nformat=self.naming_scheme, nformat=self.naming_scheme,
cformat=item)) cformat=item))
raise SystemExit('Module generation aborted.') raise SystemExit('Module generation aborted.')
line = line.format(**naming_tokens) line = self.spec.format(line)
yield line yield line
# To construct an arbitrary hierarchy of module files: # To construct an arbitrary hierarchy of module files:
@ -672,24 +654,24 @@ class LmodModule(EnvModule):
family_format = 'family("{family}")\n' family_format = 'family("{family}")\n'
path_part_with_hash = join_path('{token.name}', '{token.version}-{token.hash}') # NOQA: ignore=E501
path_part_without_hash = join_path('{token.name}', '{token.version}') path_part_without_hash = join_path('{token.name}', '{token.version}')
# TODO : Check that extra tokens specified in configuration file
# TODO : are actually virtual dependencies
configuration = CONFIGURATION.get('lmod', {})
hierarchy_tokens = configuration.get('hierarchical_scheme', [])
hierarchy_tokens = hierarchy_tokens + ['mpi', 'compiler']
def __init__(self, spec=None): def __init__(self, spec=None):
super(LmodModule, self).__init__(spec) super(LmodModule, self).__init__(spec)
self.configuration = CONFIGURATION.get('lmod', {})
hierarchy_tokens = self.configuration.get('hierarchical_scheme', [])
# TODO : Check that the extra hierarchy tokens specified in the
# TODO : configuration file are actually virtual dependencies
self.hierarchy_tokens = hierarchy_tokens + ['mpi', 'compiler']
# Sets the root directory for this architecture # Sets the root directory for this architecture
self.modules_root = join_path(LmodModule.path, self.spec.architecture) self.modules_root = join_path(LmodModule.path, self.spec.architecture)
# Retrieve core compilers # Retrieve core compilers
self.core_compilers = self.configuration.get('core_compilers', []) self.core_compilers = self.configuration.get('core_compilers', [])
# Keep track of the requirements that this package has in terms # Keep track of the requirements that this package has in terms
# of virtual packages # of virtual packages that participate in the hierarchical structure
# that participate in the hierarchical structure
self.requires = {'compiler': self.spec.compiler} self.requires = {'compiler': self.spec.compiler}
# For each virtual dependency in the hierarchy # For each virtual dependency in the hierarchy
for x in self.hierarchy_tokens: for x in self.hierarchy_tokens:
@ -740,10 +722,10 @@ def token_to_path(self, name, value):
# CompilerSpec does not have an hash # CompilerSpec does not have an hash
if name == 'compiler': if name == 'compiler':
return self.path_part_without_hash.format(token=value) return self.path_part_without_hash.format(token=value)
# For virtual providers add a small part of the hash # In this case the hierarchy token refers to a virtual provider
# to distinguish among different variants in a directory hierarchy path = self.path_part_without_hash.format(token=value)
value.hash = value.dag_hash(length=6) path = '-'.join([path, value.dag_hash(length=7)])
return self.path_part_with_hash.format(token=value) return path
@property @property
def file_name(self): def file_name(self):
@ -756,7 +738,10 @@ def file_name(self):
@property @property
def use_name(self): def use_name(self):
return self.token_to_path('', self.spec) path_elements = [self.spec.format("${PACKAGE}/${VERSION}")]
# The remaining elements are filename suffixes
path_elements.extend(self._get_suffixes())
return '-'.join(path_elements)
def modulepath_modifications(self): def modulepath_modifications(self):
# What is available is what we require plus what we provide # What is available is what we require plus what we provide

View File

@ -2201,10 +2201,12 @@ def format(self, format_string='$_$@$%@+$+$=', **kwargs):
${OPTIONS} Options ${OPTIONS} Options
${ARCHITECTURE} Architecture ${ARCHITECTURE} Architecture
${SHA1} Dependencies 8-char sha1 prefix ${SHA1} Dependencies 8-char sha1 prefix
${HASH:len} DAG hash with optional length specifier
${SPACK_ROOT} The spack root directory ${SPACK_ROOT} The spack root directory
${SPACK_INSTALL} The default spack install directory, ${SPACK_INSTALL} The default spack install directory,
${SPACK_PREFIX}/opt ${SPACK_PREFIX}/opt
${PREFIX} The package prefix
Optionally you can provide a width, e.g. ``$20_`` for a 20-wide name. Optionally you can provide a width, e.g. ``$20_`` for a 20-wide name.
Like printf, you can provide '-' for left justification, e.g. Like printf, you can provide '-' for left justification, e.g.
@ -2327,6 +2329,15 @@ def write(s, c):
out.write(fmt % spack.prefix) out.write(fmt % spack.prefix)
elif named_str == 'SPACK_INSTALL': elif named_str == 'SPACK_INSTALL':
out.write(fmt % spack.install_path) out.write(fmt % spack.install_path)
elif named_str == 'PREFIX':
out.write(fmt % self.prefix)
elif named_str.startswith('HASH'):
if named_str.startswith('HASH:'):
_, hashlen = named_str.split(':')
hashlen = int(hashlen)
else:
hashlen = None
out.write(fmt % (self.dag_hash(hashlen)))
named = False named = False

View File

@ -27,6 +27,7 @@
import StringIO import StringIO
import spack.modules import spack.modules
import spack.spec
from spack.test.mock_packages_test import MockPackagesTest from spack.test.mock_packages_test import MockPackagesTest
FILE_REGISTRY = collections.defaultdict(StringIO.StringIO) FILE_REGISTRY = collections.defaultdict(StringIO.StringIO)
@ -167,7 +168,7 @@ class TclTests(ModuleFileGeneratorTests):
'all': { 'all': {
'filter': {'environment_blacklist': ['CMAKE_PREFIX_PATH']}, 'filter': {'environment_blacklist': ['CMAKE_PREFIX_PATH']},
'environment': { 'environment': {
'set': {'{name}_ROOT': '{prefix}'} 'set': {'${PACKAGE}_ROOT': '${PREFIX}'}
} }
}, },
'platform=test target=x86_64': { 'platform=test target=x86_64': {
@ -196,9 +197,9 @@ class TclTests(ModuleFileGeneratorTests):
configuration_conflicts = { configuration_conflicts = {
'enable': ['tcl'], 'enable': ['tcl'],
'tcl': { 'tcl': {
'naming_scheme': '{name}/{version}-{compiler.name}', 'naming_scheme': '${PACKAGE}/${VERSION}-${COMPILERNAME}',
'all': { 'all': {
'conflict': ['{name}', 'intel/14.0.1'] 'conflict': ['${PACKAGE}', 'intel/14.0.1']
} }
} }
} }
@ -206,9 +207,9 @@ class TclTests(ModuleFileGeneratorTests):
configuration_wrong_conflicts = { configuration_wrong_conflicts = {
'enable': ['tcl'], 'enable': ['tcl'],
'tcl': { 'tcl': {
'naming_scheme': '{name}/{version}-{compiler.name}', 'naming_scheme': '${PACKAGE}/${VERSION}-${COMPILERNAME}',
'all': { 'all': {
'conflict': ['{name}/{compiler.name}'] 'conflict': ['${PACKAGE}/${COMPILERNAME}']
} }
} }
} }
@ -371,6 +372,13 @@ class LmodTests(ModuleFileGeneratorTests):
} }
} }
configuration_no_hash = {
'enable': ['lmod'],
'lmod': {
'hash_length': 0
}
}
configuration_alter_environment = { configuration_alter_environment = {
'enable': ['lmod'], 'enable': ['lmod'],
'lmod': { 'lmod': {
@ -455,6 +463,24 @@ def test_blacklist(self):
len([x for x in content if 'if not isloaded(' in x]), 1) len([x for x in content if 'if not isloaded(' in x]), 1)
self.assertEqual(len([x for x in content if 'load(' in x]), 1) self.assertEqual(len([x for x in content if 'load(' in x]), 1)
def test_no_hash(self):
# Make sure that virtual providers (in the hierarchy) always
# include a hash. Make sure that the module file for the spec
# does not include a hash if hash_length is 0.
spack.modules.CONFIGURATION = self.configuration_no_hash
spec = spack.spec.Spec(mpileaks_spec_string)
spec.concretize()
module = spack.modules.LmodModule(spec)
path = module.file_name
mpiSpec = spec['mpi']
mpiElement = "{0}/{1}-{2}/".format(
mpiSpec.name, mpiSpec.version, mpiSpec.dag_hash(length=7))
self.assertTrue(mpiElement in path)
mpileaksSpec = spec
mpileaksElement = "{0}/{1}.lua".format(
mpileaksSpec.name, mpileaksSpec.version)
self.assertTrue(path.endswith(mpileaksElement))
class DotkitTests(MockPackagesTest): class DotkitTests(MockPackagesTest):