stacks: initial implementation of stacks on environments
- stack syntax in env schema - switch environment specs over to SpecList object - add stack functionality to environments - handle definition extensions through stack.yaml and SpecList - implement conditional definitions - tests
This commit is contained in:
parent
5be1ff83d1
commit
d450a2fce2
@ -17,6 +17,9 @@
|
||||
|
||||
|
||||
def setup_parser(subparser):
|
||||
subparser.add_argument('-l', '--list-name',
|
||||
dest='list_name', default='specs',
|
||||
help="name of the list to add specs to")
|
||||
subparser.add_argument(
|
||||
'specs', nargs=argparse.REMAINDER, help="specs of packages to add")
|
||||
|
||||
@ -25,7 +28,7 @@ def add(parser, args):
|
||||
env = ev.get_env(args, 'add', required=True)
|
||||
|
||||
for spec in spack.cmd.parse_specs(args.specs):
|
||||
if not env.add(spec):
|
||||
if not env.add(spec, args.list_name):
|
||||
tty.msg("Package {0} was already added to {1}"
|
||||
.format(spec.name, env.name))
|
||||
else:
|
||||
|
@ -175,6 +175,7 @@ def find(parser, args):
|
||||
tty.msg('No root specs')
|
||||
else:
|
||||
tty.msg('Root specs')
|
||||
# TODO: Change this to not print extraneous deps and variants
|
||||
display_specs(
|
||||
env.user_specs, args,
|
||||
decorator=lambda s, f: color.colorize('@*{%s}' % f))
|
||||
|
@ -20,6 +20,9 @@ def setup_parser(subparser):
|
||||
subparser.add_argument(
|
||||
'-a', '--all', action='store_true',
|
||||
help="remove all specs from (clear) the environment")
|
||||
subparser.add_argument('-l', '--list-name',
|
||||
dest='list_name', default='specs',
|
||||
help="name of the list to remove specs from")
|
||||
subparser.add_argument(
|
||||
'-f', '--force', action='store_true',
|
||||
help="remove concretized spec (if any) immediately")
|
||||
@ -35,5 +38,5 @@ def remove(parser, args):
|
||||
else:
|
||||
for spec in spack.cmd.parse_specs(args.specs):
|
||||
tty.msg('Removing %s from environment %s' % (spec, env.name))
|
||||
env.remove(spec, force=args.force)
|
||||
env.remove(spec, args.list_name, force=args.force)
|
||||
env.write()
|
||||
|
@ -7,10 +7,13 @@
|
||||
import re
|
||||
import sys
|
||||
import shutil
|
||||
import copy
|
||||
|
||||
import ruamel.yaml
|
||||
import six
|
||||
|
||||
from ordereddict_backport import OrderedDict
|
||||
|
||||
import llnl.util.filesystem as fs
|
||||
import llnl.util.tty as tty
|
||||
from llnl.util.tty.color import colorize
|
||||
@ -21,10 +24,13 @@
|
||||
import spack.spec
|
||||
import spack.util.spack_json as sjson
|
||||
import spack.config
|
||||
from spack.spec import Spec
|
||||
from spack.filesystem_view import YamlFilesystemView
|
||||
|
||||
from spack.util.environment import EnvironmentModifications
|
||||
import spack.architecture as architecture
|
||||
from spack.spec import Spec
|
||||
from spack.spec_list import SpecList, InvalidSpecConstraintError
|
||||
from spack.variant import UnknownVariantError
|
||||
from spack.util.executable import which
|
||||
|
||||
#: environment variable used to indicate the active environment
|
||||
spack_env_var = 'SPACK_ENV'
|
||||
@ -385,6 +391,29 @@ def _write_yaml(data, str_or_file):
|
||||
default_flow_style=False)
|
||||
|
||||
|
||||
def _eval_conditional(string):
|
||||
"""Evaluate conditional definitions using restricted variable scope."""
|
||||
arch = architecture.Arch(
|
||||
architecture.platform(), 'default_os', 'default_target')
|
||||
valid_variables = {
|
||||
'target': str(arch.target),
|
||||
'os': str(arch.platform_os),
|
||||
'platform': str(arch.platform),
|
||||
'arch': str(arch),
|
||||
'architecture': str(arch),
|
||||
're': re,
|
||||
'env': os.environ,
|
||||
}
|
||||
hostname_bin = which('hostname')
|
||||
if hostname_bin:
|
||||
hostname = str(hostname_bin(output=str, error=str)).strip()
|
||||
valid_variables['hostname'] = hostname
|
||||
else:
|
||||
tty.warn("Spack was unable to find the executable `hostname`"
|
||||
" hostname will be unavailable in conditionals")
|
||||
return eval(string, valid_variables)
|
||||
|
||||
|
||||
class Environment(object):
|
||||
def __init__(self, path, init_file=None, with_view=None):
|
||||
"""Create a new environment.
|
||||
@ -425,6 +454,18 @@ def __init__(self, path, init_file=None, with_view=None):
|
||||
if default_manifest:
|
||||
self._set_user_specs_from_lockfile()
|
||||
|
||||
if os.path.exists(self.manifest_path):
|
||||
# read the spack.yaml file, if exists
|
||||
with open(self.manifest_path) as f:
|
||||
self._read_manifest(f)
|
||||
elif self.concretized_user_specs:
|
||||
# if not, take user specs from the lockfile
|
||||
self._set_user_specs_from_lockfile()
|
||||
self.yaml = _read_yaml(default_manifest_yaml)
|
||||
else:
|
||||
# if there's no manifest or lockfile, use the default
|
||||
self._read_manifest(default_manifest_yaml)
|
||||
|
||||
if with_view is False:
|
||||
self._view_path = None
|
||||
elif isinstance(with_view, six.string_types):
|
||||
@ -435,9 +476,38 @@ def __init__(self, path, init_file=None, with_view=None):
|
||||
def _read_manifest(self, f):
|
||||
"""Read manifest file and set up user specs."""
|
||||
self.yaml = _read_yaml(f)
|
||||
|
||||
self.read_specs = OrderedDict()
|
||||
|
||||
for item in list(self.yaml.values())[0].get('definitions', []):
|
||||
entry = copy.deepcopy(item)
|
||||
when = _eval_conditional(entry.pop('when', 'True'))
|
||||
assert len(entry) == 1
|
||||
if when:
|
||||
name, spec_list = list(entry.items())[0]
|
||||
user_specs = SpecList(name, spec_list, self.read_specs.copy())
|
||||
if name in self.read_specs:
|
||||
self.read_specs[name].extend(user_specs)
|
||||
else:
|
||||
self.read_specs[name] = user_specs
|
||||
|
||||
spec_list = config_dict(self.yaml).get('specs')
|
||||
if spec_list:
|
||||
self.user_specs = [Spec(s) for s in spec_list if s]
|
||||
user_specs = SpecList('specs', [s for s in spec_list if s],
|
||||
self.read_specs.copy())
|
||||
self.read_specs['specs'] = user_specs
|
||||
|
||||
enable_view = config_dict(self.yaml).get('view')
|
||||
# enable_view can be boolean, string, or None
|
||||
if enable_view is True or enable_view is None:
|
||||
self._view_path = self.default_view_path
|
||||
elif isinstance(enable_view, six.string_types):
|
||||
self._view_path = enable_view
|
||||
else:
|
||||
self._view_path = None
|
||||
|
||||
@property
|
||||
def user_specs(self):
|
||||
return self.read_specs['specs']
|
||||
|
||||
enable_view = config_dict(self.yaml).get('view')
|
||||
# enable_view can be true/false, a string, or None (if the manifest did
|
||||
@ -452,10 +522,14 @@ def _read_manifest(self, f):
|
||||
|
||||
def _set_user_specs_from_lockfile(self):
|
||||
"""Copy user_specs from a read-in lockfile."""
|
||||
self.user_specs = [Spec(s) for s in self.concretized_user_specs]
|
||||
self.read_specs = {
|
||||
'specs': SpecList(
|
||||
'specs', [Spec(s) for s in self.concretized_user_specs]
|
||||
)
|
||||
}
|
||||
|
||||
def clear(self):
|
||||
self.user_specs = [] # current user specs
|
||||
self.read_specs = {'specs': SpecList()} # specs read from yaml
|
||||
self.concretized_user_specs = [] # user specs from last concretize
|
||||
self.concretized_order = [] # roots of last concretize, in order
|
||||
self.specs_by_hash = {} # concretized specs by hash
|
||||
@ -578,7 +652,7 @@ def destroy(self):
|
||||
"""Remove this environment from Spack entirely."""
|
||||
shutil.rmtree(self.path)
|
||||
|
||||
def add(self, user_spec):
|
||||
def add(self, user_spec, list_name='specs'):
|
||||
"""Add a single user_spec (non-concretized) to the Environment
|
||||
|
||||
Returns:
|
||||
@ -587,47 +661,86 @@ def add(self, user_spec):
|
||||
|
||||
"""
|
||||
spec = Spec(user_spec)
|
||||
if not spec.name:
|
||||
raise SpackEnvironmentError(
|
||||
'cannot add anonymous specs to an environment!')
|
||||
elif not spack.repo.path.exists(spec.name):
|
||||
raise SpackEnvironmentError('no such package: %s' % spec.name)
|
||||
|
||||
existing = set(s for s in self.user_specs if s.name == spec.name)
|
||||
if not existing:
|
||||
self.user_specs.append(spec)
|
||||
if list_name not in self.read_specs:
|
||||
raise SpackEnvironmentError(
|
||||
'No list %s exists in environment %s' % (list_name, self.name)
|
||||
)
|
||||
|
||||
if list_name == 'specs':
|
||||
if not spec.name:
|
||||
raise SpackEnvironmentError(
|
||||
'cannot add anonymous specs to an environment!')
|
||||
elif not spack.repo.path.exists(spec.name):
|
||||
raise SpackEnvironmentError('no such package: %s' % spec.name)
|
||||
|
||||
added = False
|
||||
existing = False
|
||||
for i, (name, speclist) in enumerate(self.read_specs.items()):
|
||||
# Iterate over all named lists from an OrderedDict()
|
||||
if name == list_name:
|
||||
# We need to modify this list
|
||||
# TODO: Add conditional which reimplements name-level checking
|
||||
existing = str(spec) in speclist.yaml_list
|
||||
if not existing:
|
||||
speclist.add(str(spec))
|
||||
added = True
|
||||
elif added:
|
||||
# We've already modified a list, so all later lists need to
|
||||
# have their references updated.
|
||||
new_reference = dict((n, self.read_specs[n])
|
||||
for n in list(self.read_specs.keys())[:i])
|
||||
speclist.update_reference(new_reference)
|
||||
return bool(not existing)
|
||||
|
||||
def remove(self, query_spec, force=False):
|
||||
def remove(self, query_spec, list_name='specs', force=False):
|
||||
"""Remove specs from an environment that match a query_spec"""
|
||||
query_spec = Spec(query_spec)
|
||||
|
||||
# try abstract specs first
|
||||
matches = []
|
||||
if not query_spec.concrete:
|
||||
matches = [s for s in self.user_specs if s.satisfies(query_spec)]
|
||||
removed = False
|
||||
for i, (name, speclist) in enumerate(self.read_specs.items()):
|
||||
# Iterate over all named lists from an OrderedDict()
|
||||
if name == list_name:
|
||||
# We need to modify this list
|
||||
# try abstract specs first
|
||||
matches = []
|
||||
|
||||
if not matches:
|
||||
# concrete specs match against concrete specs in the env
|
||||
specs_hashes = zip(
|
||||
self.concretized_user_specs, self.concretized_order)
|
||||
matches = [
|
||||
s for s, h in specs_hashes if query_spec.dag_hash() == h]
|
||||
if not query_spec.concrete:
|
||||
matches = [s for s in speclist if s.satisfies(query_spec)]
|
||||
|
||||
if not matches:
|
||||
raise SpackEnvironmentError("Not found: {0}".format(query_spec))
|
||||
if not matches:
|
||||
# concrete specs match against concrete specs in the env
|
||||
specs_hashes = zip(
|
||||
self.concretized_user_specs, self.concretized_order)
|
||||
matches = [
|
||||
s for s, h in specs_hashes
|
||||
if query_spec.dag_hash() == h
|
||||
]
|
||||
|
||||
for spec in matches:
|
||||
if spec in self.user_specs:
|
||||
self.user_specs.remove(spec)
|
||||
if not matches:
|
||||
raise SpackEnvironmentError(
|
||||
"Not found: {0}".format(query_spec))
|
||||
|
||||
if force and spec in self.concretized_user_specs:
|
||||
i = self.concretized_user_specs.index(spec)
|
||||
del self.concretized_user_specs[i]
|
||||
for spec in matches:
|
||||
if spec in speclist:
|
||||
speclist.remove(spec)
|
||||
removed = True
|
||||
|
||||
dag_hash = self.concretized_order[i]
|
||||
del self.concretized_order[i]
|
||||
del self.specs_by_hash[dag_hash]
|
||||
if force and spec in self.concretized_user_specs:
|
||||
i = self.concretized_user_specs.index(spec)
|
||||
del self.concretized_user_specs[i]
|
||||
|
||||
dag_hash = self.concretized_order[i]
|
||||
del self.concretized_order[i]
|
||||
del self.specs_by_hash[dag_hash]
|
||||
removed = True
|
||||
|
||||
elif removed:
|
||||
# We've already modified one list, so all later lists need
|
||||
# their references updated.
|
||||
new_reference = dict((n, self.read_specs[n])
|
||||
for n in list(self.read_specs.keys())[:i])
|
||||
speclist.update_reference(new_reference)
|
||||
|
||||
def concretize(self, force=False):
|
||||
"""Concretize user_specs in this environment.
|
||||
@ -663,10 +776,11 @@ def concretize(self, force=False):
|
||||
self._add_concrete_spec(s, concrete, new=False)
|
||||
|
||||
# concretize any new user specs that we haven't concretized yet
|
||||
for uspec in self.user_specs:
|
||||
for uspec, uspec_constraints in zip(
|
||||
self.user_specs, self.user_specs.specs_as_constraints):
|
||||
if uspec not in old_concretized_user_specs:
|
||||
tty.msg('Concretizing %s' % uspec)
|
||||
concrete = uspec.concretized()
|
||||
concrete = _concretize_from_constraints(uspec_constraints)
|
||||
self._add_concrete_spec(uspec, concrete)
|
||||
|
||||
# Display concretized spec to the user
|
||||
@ -689,7 +803,8 @@ def install(self, user_spec, concrete_spec=None, **install_args):
|
||||
self._add_concrete_spec(spec, concrete)
|
||||
else:
|
||||
# spec might be in the user_specs, but not installed.
|
||||
spec = next(s for s in self.user_specs if s.name == spec.name)
|
||||
# TODO: Redo name-based comparison for old style envs
|
||||
spec = next(s for s in self.user_specs if s.satisfies(user_spec))
|
||||
concrete = self.specs_by_hash.get(spec.dag_hash())
|
||||
if not concrete:
|
||||
concrete = spec.concretized()
|
||||
@ -1018,10 +1133,16 @@ def write(self):
|
||||
# invalidate _repo cache
|
||||
self._repo = None
|
||||
|
||||
# put any changes in the definitions in the YAML
|
||||
named_speclists = list(self.read_specs.items())
|
||||
for i, (name, speclist) in enumerate(named_speclists[:-1]):
|
||||
conf = config_dict(self.yaml)
|
||||
yaml_list = conf.get('definitions', [])[i].setdefault(name, [])
|
||||
yaml_list[:] = speclist.yaml_list
|
||||
|
||||
# put the new user specs in the YAML
|
||||
yaml_dict = config_dict(self.yaml)
|
||||
yaml_spec_list = yaml_dict.setdefault('specs', [])
|
||||
yaml_spec_list[:] = [str(s) for s in self.user_specs]
|
||||
yaml_spec_list = config_dict(self.yaml).setdefault('specs', [])
|
||||
yaml_spec_list[:] = self.user_specs.yaml_list
|
||||
|
||||
if self._view_path == self.default_view_path:
|
||||
view = True
|
||||
@ -1057,6 +1178,48 @@ def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
activate(self._previous_active)
|
||||
|
||||
|
||||
def _concretize_from_constraints(spec_constraints):
|
||||
# Accept only valid constraints from list and concretize spec
|
||||
# Get the named spec even if out of order
|
||||
root_spec = [s for s in spec_constraints if s.name]
|
||||
if len(root_spec) != 1:
|
||||
m = 'The constraints %s are not a valid spec ' % spec_constraints
|
||||
m += 'concretization target. all specs must have a single name '
|
||||
m += 'constraint for concretization.'
|
||||
raise InvalidSpecConstraintError(m)
|
||||
spec_constraints.remove(root_spec[0])
|
||||
|
||||
invalid_constraints = []
|
||||
while True:
|
||||
# Attach all anonymous constraints to one named spec
|
||||
s = root_spec[0].copy()
|
||||
for c in spec_constraints:
|
||||
if c not in invalid_constraints:
|
||||
s.constrain(c)
|
||||
try:
|
||||
return s.concretized()
|
||||
except spack.spec.InvalidDependencyError as e:
|
||||
dep_index = e.message.index('depend on ') + len('depend on ')
|
||||
invalid_msg = e.message[dep_index:]
|
||||
invalid_deps_string = ['^' + d.strip(',')
|
||||
for d in invalid_msg.split()
|
||||
if d != 'or']
|
||||
invalid_deps = [c for c in spec_constraints
|
||||
if any(c.satisfies(invd)
|
||||
for invd in invalid_deps_string)]
|
||||
if len(invalid_deps) != len(invalid_deps_string):
|
||||
raise e
|
||||
invalid_constraints.extend(invalid_deps)
|
||||
except UnknownVariantError as e:
|
||||
invalid_variants = re.findall(r"'(\w+)'", e.message)
|
||||
invalid_deps = [c for c in spec_constraints
|
||||
if any(name in c.variants
|
||||
for name in invalid_variants)]
|
||||
if len(invalid_deps) != len(invalid_variants):
|
||||
raise e
|
||||
invalid_constraints.extend(invalid_deps)
|
||||
|
||||
|
||||
def make_repo_path(root):
|
||||
"""Make a RepoPath from the repo subdirectories in an environment."""
|
||||
path = spack.repo.RepoPath()
|
||||
|
@ -11,8 +11,41 @@
|
||||
from llnl.util.lang import union_dicts
|
||||
|
||||
import spack.schema.merged
|
||||
import spack.schema.projections
|
||||
|
||||
|
||||
spec_list_schema = {
|
||||
'type': 'array',
|
||||
'default': [],
|
||||
'items': {
|
||||
'anyOf': [
|
||||
{'type': 'object',
|
||||
'additionalProperties': False,
|
||||
'properties': {
|
||||
'matrix': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'string',
|
||||
}
|
||||
}
|
||||
},
|
||||
'exclude': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'string'
|
||||
}
|
||||
}
|
||||
}},
|
||||
{'type': 'string'},
|
||||
{'type': 'null'}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
projections_scheme = spack.schema.projections.properties['projections']
|
||||
|
||||
schema = {
|
||||
'$schema': 'http://json-schema.org/schema#',
|
||||
'title': 'Spack environment file schema',
|
||||
@ -34,22 +67,52 @@
|
||||
'type': 'string'
|
||||
},
|
||||
},
|
||||
'specs': {
|
||||
# Specs is a list of specs, which can have
|
||||
# optional additional properties in a sub-dict
|
||||
'type': 'array',
|
||||
'default': [],
|
||||
'additionalProperties': False,
|
||||
'items': {
|
||||
'anyOf': [
|
||||
{'type': 'string'},
|
||||
{'type': 'null'},
|
||||
{'type': 'object'},
|
||||
]
|
||||
}
|
||||
},
|
||||
'view': {
|
||||
'type': ['boolean', 'string']
|
||||
},
|
||||
'definitions': {
|
||||
'type': 'array',
|
||||
'default': [],
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'when': {
|
||||
'type': 'string'
|
||||
}
|
||||
},
|
||||
'patternProperties': {
|
||||
'^(?!when$)\w*': spec_list_schema
|
||||
}
|
||||
}
|
||||
},
|
||||
'specs': spec_list_schema,
|
||||
'view': {
|
||||
'anyOf': [
|
||||
{'type': 'boolean'},
|
||||
{'type': 'string'},
|
||||
{'type': 'object',
|
||||
'required': ['root'],
|
||||
'additionalProperties': False,
|
||||
'properties': {
|
||||
'root': {
|
||||
'type': 'string'
|
||||
},
|
||||
'select': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'string'
|
||||
}
|
||||
},
|
||||
'exclude': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'string'
|
||||
}
|
||||
},
|
||||
'projections': projections_scheme
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -2494,7 +2494,7 @@ def _constrain_dependencies(self, other):
|
||||
"""Apply constraints of other spec's dependencies to this spec."""
|
||||
other = self._autospec(other)
|
||||
|
||||
if not self._dependencies or not other._dependencies:
|
||||
if not other._dependencies:
|
||||
return False
|
||||
|
||||
# TODO: might want more detail than this, e.g. specific deps
|
||||
|
168
lib/spack/spack/spec_list.py
Normal file
168
lib/spack/spack/spec_list.py
Normal file
@ -0,0 +1,168 @@
|
||||
# Copyright 2013-2019 Lawrence Livermore National Security, LLC and other
|
||||
# Spack Project Developers. See the top-level COPYRIGHT file for details.
|
||||
#
|
||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||
import itertools
|
||||
from six import string_types
|
||||
|
||||
from spack.spec import Spec
|
||||
from spack.error import SpackError
|
||||
|
||||
|
||||
def spec_ordering_key(s):
|
||||
if s.startswith('^'):
|
||||
return 5
|
||||
elif s.startswith('/'):
|
||||
return 4
|
||||
elif s.startswith('%'):
|
||||
return 3
|
||||
elif any(s.startswith(c) for c in '~-+@') or '=' in s:
|
||||
return 2
|
||||
else:
|
||||
return 1
|
||||
|
||||
|
||||
class SpecList(object):
|
||||
|
||||
def __init__(self, name='specs', yaml_list=[], reference={}):
|
||||
self.name = name
|
||||
self._reference = reference # TODO: Do we need defensive copy here?
|
||||
|
||||
self.yaml_list = yaml_list[:]
|
||||
|
||||
# Expansions can be expensive to compute and difficult to keep updated
|
||||
# We cache results and invalidate when self.yaml_list changes
|
||||
self._expanded_list = None
|
||||
self._constraints = None
|
||||
self._specs = None
|
||||
|
||||
@property
|
||||
def specs_as_yaml_list(self):
|
||||
if self._expanded_list is None:
|
||||
self._expanded_list = self._expand_references(self.yaml_list)
|
||||
return self._expanded_list
|
||||
|
||||
@property
|
||||
def specs_as_constraints(self):
|
||||
if self._constraints is None:
|
||||
constraints = []
|
||||
for item in self.specs_as_yaml_list:
|
||||
if isinstance(item, dict): # matrix of specs
|
||||
excludes = item.get('exclude', [])
|
||||
for combo in itertools.product(*(item['matrix'])):
|
||||
# Test against the excludes using a single spec
|
||||
ordered_combo = sorted(combo, key=spec_ordering_key)
|
||||
test_spec = Spec(' '.join(ordered_combo))
|
||||
if any(test_spec.satisfies(x) for x in excludes):
|
||||
continue
|
||||
|
||||
# Add as list of constraints
|
||||
constraints.append([Spec(x) for x in ordered_combo])
|
||||
else: # individual spec
|
||||
constraints.append([Spec(item)])
|
||||
self._constraints = constraints
|
||||
|
||||
return self._constraints
|
||||
|
||||
@property
|
||||
def specs(self):
|
||||
if self._specs is None:
|
||||
specs = []
|
||||
# This could be slightly faster done directly from yaml_list,
|
||||
# but this way is easier to maintain.
|
||||
for constraint_list in self.specs_as_constraints:
|
||||
spec = constraint_list[0].copy()
|
||||
for const in constraint_list[1:]:
|
||||
spec.constrain(const)
|
||||
specs.append(spec)
|
||||
self._specs = specs
|
||||
|
||||
return self._specs
|
||||
|
||||
def add(self, spec):
|
||||
self.yaml_list.append(str(spec))
|
||||
|
||||
# expanded list can be updated without invalidation
|
||||
if self._expanded_list is not None:
|
||||
self._expanded_list.append(str(spec))
|
||||
|
||||
# Invalidate cache variables when we change the list
|
||||
self._constraints = None
|
||||
self._specs = None
|
||||
|
||||
def remove(self, spec):
|
||||
# Get spec to remove from list
|
||||
remove = [s for s in self.yaml_list
|
||||
if (isinstance(s, string_types) and not s.startswith('$'))
|
||||
and Spec(s) == Spec(spec)]
|
||||
if not remove:
|
||||
msg = 'Cannot remove %s from SpecList %s\n' % (spec, self.name)
|
||||
msg += 'Either %s is not in %s or %s is ' % (spec, self.name, spec)
|
||||
msg += 'expanded from a matrix and cannot be removed directly.'
|
||||
raise SpecListError(msg)
|
||||
assert len(remove) == 1
|
||||
self.yaml_list.remove(remove[0])
|
||||
|
||||
# invalidate cache variables when we change the list
|
||||
self._expanded_list = None
|
||||
self._constraints = None
|
||||
self._specs = None
|
||||
|
||||
def extend(self, other, copy_reference=True):
|
||||
self.yaml_list.extend(other.yaml_list)
|
||||
self._expanded_list = None
|
||||
self._constraints = None
|
||||
self._specs = None
|
||||
|
||||
if copy_reference:
|
||||
self._reference = other._reference
|
||||
|
||||
def update_reference(self, reference):
|
||||
self._reference = reference
|
||||
self._expanded_list = None
|
||||
self._constraints = None
|
||||
self._specs = None
|
||||
|
||||
def _expand_references(self, yaml):
|
||||
if isinstance(yaml, list):
|
||||
for idx, item in enumerate(yaml):
|
||||
if isinstance(item, string_types) and item.startswith('$'):
|
||||
name = item[1:]
|
||||
if name in self._reference:
|
||||
ret = [self._expand_references(i) for i in yaml[:idx]]
|
||||
ret += self._reference[name].specs_as_yaml_list
|
||||
ret += [self._expand_references(i)
|
||||
for i in yaml[idx + 1:]]
|
||||
return ret
|
||||
else:
|
||||
msg = 'SpecList %s refers to ' % self.name
|
||||
msg = 'named list %s ' % name
|
||||
msg += 'which does not appear in its reference dict'
|
||||
raise UndefinedReferenceError(msg)
|
||||
# No references in this
|
||||
return [self._expand_references(item) for item in yaml]
|
||||
elif isinstance(yaml, dict):
|
||||
# There can't be expansions in dicts
|
||||
return dict((name, self._expand_references(val))
|
||||
for (name, val) in yaml.items())
|
||||
else:
|
||||
# Strings are just returned
|
||||
return yaml
|
||||
|
||||
def __len__(self):
|
||||
return len(self.specs)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.specs[key]
|
||||
|
||||
|
||||
class SpecListError(SpackError):
|
||||
"""Error class for all errors related to SpecList objects."""
|
||||
|
||||
|
||||
class UndefinedReferenceError(SpecListError):
|
||||
"""Error class for undefined references in Spack stacks."""
|
||||
|
||||
|
||||
class InvalidSpecConstraintError(SpecListError):
|
||||
"""Error class for invalid spec constraints at concretize time."""
|
@ -15,6 +15,7 @@
|
||||
from spack.cmd.env import _env_create
|
||||
from spack.spec import Spec
|
||||
from spack.main import SpackCommand
|
||||
from spack.spec_list import SpecListError
|
||||
|
||||
|
||||
# everything here uses the mock_env_path
|
||||
@ -763,3 +764,271 @@ def test_env_activate_view_fails(
|
||||
"""Sanity check on env activate to make sure it requires shell support"""
|
||||
out = env('activate', 'test')
|
||||
assert "To initialize spack's shell commands:" in out
|
||||
|
||||
|
||||
def test_stack_yaml_definitions(tmpdir):
|
||||
filename = str(tmpdir.join('spack.yaml'))
|
||||
with open(filename, 'w') as f:
|
||||
f.write("""\
|
||||
env:
|
||||
definitions:
|
||||
- packages: [mpileaks, callpath]
|
||||
specs:
|
||||
- $packages
|
||||
""")
|
||||
with tmpdir.as_cwd():
|
||||
env('create', 'test', './spack.yaml')
|
||||
test = ev.read('test')
|
||||
|
||||
assert Spec('mpileaks') in test.user_specs
|
||||
assert Spec('callpath') in test.user_specs
|
||||
|
||||
|
||||
def test_stack_yaml_add_to_list(tmpdir):
|
||||
filename = str(tmpdir.join('spack.yaml'))
|
||||
with open(filename, 'w') as f:
|
||||
f.write("""\
|
||||
env:
|
||||
definitions:
|
||||
- packages: [mpileaks, callpath]
|
||||
specs:
|
||||
- $packages
|
||||
""")
|
||||
with tmpdir.as_cwd():
|
||||
env('create', 'test', './spack.yaml')
|
||||
with ev.read('test'):
|
||||
add('-l', 'packages', 'libelf')
|
||||
|
||||
test = ev.read('test')
|
||||
|
||||
assert Spec('libelf') in test.user_specs
|
||||
assert Spec('mpileaks') in test.user_specs
|
||||
assert Spec('callpath') in test.user_specs
|
||||
|
||||
|
||||
def test_stack_yaml_remove_from_list(tmpdir):
|
||||
filename = str(tmpdir.join('spack.yaml'))
|
||||
with open(filename, 'w') as f:
|
||||
f.write("""\
|
||||
env:
|
||||
definitions:
|
||||
- packages: [mpileaks, callpath]
|
||||
specs:
|
||||
- $packages
|
||||
""")
|
||||
with tmpdir.as_cwd():
|
||||
env('create', 'test', './spack.yaml')
|
||||
with ev.read('test'):
|
||||
remove('-l', 'packages', 'mpileaks')
|
||||
|
||||
test = ev.read('test')
|
||||
|
||||
assert Spec('mpileaks') not in test.user_specs
|
||||
assert Spec('callpath') in test.user_specs
|
||||
|
||||
|
||||
def test_stack_yaml_attempt_remove_from_matrix(tmpdir):
|
||||
filename = str(tmpdir.join('spack.yaml'))
|
||||
with open(filename, 'w') as f:
|
||||
f.write("""\
|
||||
env:
|
||||
definitions:
|
||||
- packages:
|
||||
- matrix:
|
||||
- [mpileaks, callpath]
|
||||
- [target=be]
|
||||
specs:
|
||||
- $packages
|
||||
""")
|
||||
with tmpdir.as_cwd():
|
||||
env('create', 'test', './spack.yaml')
|
||||
with pytest.raises(SpecListError):
|
||||
with ev.read('test'):
|
||||
remove('-l', 'packages', 'mpileaks')
|
||||
|
||||
|
||||
def test_stack_concretize_extraneous_deps(tmpdir, config, mock_packages):
|
||||
filename = str(tmpdir.join('spack.yaml'))
|
||||
with open(filename, 'w') as f:
|
||||
f.write("""\
|
||||
env:
|
||||
definitions:
|
||||
- packages: [libelf, mpileaks]
|
||||
- install:
|
||||
- matrix:
|
||||
- [$packages]
|
||||
- ['^zmpi', '^mpich']
|
||||
specs:
|
||||
- $install
|
||||
""")
|
||||
with tmpdir.as_cwd():
|
||||
env('create', 'test', './spack.yaml')
|
||||
with ev.read('test'):
|
||||
concretize()
|
||||
|
||||
test = ev.read('test')
|
||||
|
||||
for user, concrete in test.concretized_specs():
|
||||
assert concrete.concrete
|
||||
assert not user.concrete
|
||||
if user.name == 'libelf':
|
||||
assert not concrete.satisfies('^mpi', strict=True)
|
||||
elif user.name == 'mpileaks':
|
||||
assert concrete.satisfies('^mpi', strict=True)
|
||||
|
||||
|
||||
def test_stack_concretize_extraneous_variants(tmpdir, config, mock_packages):
|
||||
filename = str(tmpdir.join('spack.yaml'))
|
||||
with open(filename, 'w') as f:
|
||||
f.write("""\
|
||||
env:
|
||||
definitions:
|
||||
- packages: [libelf, mpileaks]
|
||||
- install:
|
||||
- matrix:
|
||||
- [$packages]
|
||||
- ['~shared', '+shared']
|
||||
specs:
|
||||
- $install
|
||||
""")
|
||||
with tmpdir.as_cwd():
|
||||
env('create', 'test', './spack.yaml')
|
||||
with ev.read('test'):
|
||||
concretize()
|
||||
|
||||
test = ev.read('test')
|
||||
|
||||
for user, concrete in test.concretized_specs():
|
||||
assert concrete.concrete
|
||||
assert not user.concrete
|
||||
if user.name == 'libelf':
|
||||
assert 'shared' not in concrete.variants
|
||||
if user.name == 'mpileaks':
|
||||
assert (concrete.variants['shared'].value ==
|
||||
user.variants['shared'].value)
|
||||
|
||||
|
||||
def test_stack_definition_extension(tmpdir):
|
||||
filename = str(tmpdir.join('spack.yaml'))
|
||||
with open(filename, 'w') as f:
|
||||
f.write("""\
|
||||
env:
|
||||
definitions:
|
||||
- packages: [libelf, mpileaks]
|
||||
- packages: [callpath]
|
||||
specs:
|
||||
- $packages
|
||||
""")
|
||||
with tmpdir.as_cwd():
|
||||
env('create', 'test', './spack.yaml')
|
||||
|
||||
test = ev.read('test')
|
||||
|
||||
assert Spec('libelf') in test.user_specs
|
||||
assert Spec('mpileaks') in test.user_specs
|
||||
assert Spec('callpath') in test.user_specs
|
||||
|
||||
|
||||
def test_stack_definition_conditional_false(tmpdir):
|
||||
filename = str(tmpdir.join('spack.yaml'))
|
||||
with open(filename, 'w') as f:
|
||||
f.write("""\
|
||||
env:
|
||||
definitions:
|
||||
- packages: [libelf, mpileaks]
|
||||
- packages: [callpath]
|
||||
when: 'False'
|
||||
specs:
|
||||
- $packages
|
||||
""")
|
||||
with tmpdir.as_cwd():
|
||||
env('create', 'test', './spack.yaml')
|
||||
|
||||
test = ev.read('test')
|
||||
|
||||
assert Spec('libelf') in test.user_specs
|
||||
assert Spec('mpileaks') in test.user_specs
|
||||
assert Spec('callpath') not in test.user_specs
|
||||
|
||||
|
||||
def test_stack_definition_conditional_true(tmpdir):
|
||||
filename = str(tmpdir.join('spack.yaml'))
|
||||
with open(filename, 'w') as f:
|
||||
f.write("""\
|
||||
env:
|
||||
definitions:
|
||||
- packages: [libelf, mpileaks]
|
||||
- packages: [callpath]
|
||||
when: 'True'
|
||||
specs:
|
||||
- $packages
|
||||
""")
|
||||
with tmpdir.as_cwd():
|
||||
env('create', 'test', './spack.yaml')
|
||||
|
||||
test = ev.read('test')
|
||||
|
||||
assert Spec('libelf') in test.user_specs
|
||||
assert Spec('mpileaks') in test.user_specs
|
||||
assert Spec('callpath') in test.user_specs
|
||||
|
||||
|
||||
def test_stack_definition_conditional_with_variable(tmpdir):
|
||||
filename = str(tmpdir.join('spack.yaml'))
|
||||
with open(filename, 'w') as f:
|
||||
f.write("""\
|
||||
env:
|
||||
definitions:
|
||||
- packages: [libelf, mpileaks]
|
||||
- packages: [callpath]
|
||||
when: platform == 'test'
|
||||
specs:
|
||||
- $packages
|
||||
""")
|
||||
with tmpdir.as_cwd():
|
||||
env('create', 'test', './spack.yaml')
|
||||
|
||||
test = ev.read('test')
|
||||
|
||||
assert Spec('libelf') in test.user_specs
|
||||
assert Spec('mpileaks') in test.user_specs
|
||||
assert Spec('callpath') in test.user_specs
|
||||
|
||||
|
||||
def test_stack_definition_complex_conditional(tmpdir):
|
||||
filename = str(tmpdir.join('spack.yaml'))
|
||||
with open(filename, 'w') as f:
|
||||
f.write("""\
|
||||
env:
|
||||
definitions:
|
||||
- packages: [libelf, mpileaks]
|
||||
- packages: [callpath]
|
||||
when: re.search(r'foo', hostname) and env['test'] == 'THISSHOULDBEFALSE'
|
||||
specs:
|
||||
- $packages
|
||||
""")
|
||||
with tmpdir.as_cwd():
|
||||
env('create', 'test', './spack.yaml')
|
||||
|
||||
test = ev.read('test')
|
||||
|
||||
assert Spec('libelf') in test.user_specs
|
||||
assert Spec('mpileaks') in test.user_specs
|
||||
assert Spec('callpath') not in test.user_specs
|
||||
|
||||
|
||||
def test_stack_definition_conditional_invalid_variable(tmpdir):
|
||||
filename = str(tmpdir.join('spack.yaml'))
|
||||
with open(filename, 'w') as f:
|
||||
f.write("""\
|
||||
env:
|
||||
definitions:
|
||||
- packages: [libelf, mpileaks]
|
||||
- packages: [callpath]
|
||||
when: bad_variable == 'test'
|
||||
specs:
|
||||
- $packages
|
||||
""")
|
||||
with tmpdir.as_cwd():
|
||||
with pytest.raises(NameError):
|
||||
env('create', 'test', './spack.yaml')
|
||||
|
144
lib/spack/spack/test/spec_list.py
Normal file
144
lib/spack/spack/test/spec_list.py
Normal file
@ -0,0 +1,144 @@
|
||||
# Copyright 2013-2019 Lawrence Livermore National Security, LLC and other
|
||||
# Spack Project Developers. See the top-level COPYRIGHT file for details.
|
||||
#
|
||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||
from spack.spec_list import SpecList
|
||||
from spack.spec import Spec
|
||||
|
||||
|
||||
class TestSpecList(object):
|
||||
default_input = ['mpileaks', '$mpis',
|
||||
{'matrix': [['hypre'], ['$gccs', '%clang@3.3']]},
|
||||
'libelf']
|
||||
|
||||
default_reference = {'gccs': SpecList('gccs', ['%gcc@4.5.0']),
|
||||
'mpis': SpecList('mpis', ['zmpi@1.0', 'mpich@3.0'])}
|
||||
|
||||
default_expansion = ['mpileaks', 'zmpi@1.0', 'mpich@3.0',
|
||||
{'matrix': [
|
||||
['hypre'],
|
||||
['%gcc@4.5.0', '%clang@3.3'],
|
||||
]},
|
||||
'libelf']
|
||||
|
||||
default_constraints = [[Spec('mpileaks')],
|
||||
[Spec('zmpi@1.0')],
|
||||
[Spec('mpich@3.0')],
|
||||
[Spec('hypre'), Spec('%gcc@4.5.0')],
|
||||
[Spec('hypre'), Spec('%clang@3.3')],
|
||||
[Spec('libelf')]]
|
||||
|
||||
default_specs = [Spec('mpileaks'), Spec('zmpi@1.0'),
|
||||
Spec('mpich@3.0'), Spec('hypre%gcc@4.5.0'),
|
||||
Spec('hypre%clang@3.3'), Spec('libelf')]
|
||||
|
||||
def test_spec_list_expansions(self):
|
||||
speclist = SpecList('specs', self.default_input,
|
||||
self.default_reference)
|
||||
|
||||
assert speclist.specs_as_yaml_list == self.default_expansion
|
||||
assert speclist.specs_as_constraints == self.default_constraints
|
||||
assert speclist.specs == self.default_specs
|
||||
|
||||
def test_spec_list_constraint_ordering(self):
|
||||
specs = [{'matrix': [
|
||||
['^zmpi'],
|
||||
['%gcc@4.5.0'],
|
||||
['hypre', 'libelf'],
|
||||
['~shared'],
|
||||
['cflags=-O3', 'cflags="-g -O0"'],
|
||||
['^foo']
|
||||
]}]
|
||||
|
||||
speclist = SpecList('specs', specs)
|
||||
|
||||
expected_specs = [
|
||||
Spec('hypre cflags=-O3 ~shared %gcc@4.5.0 ^foo ^zmpi'),
|
||||
Spec('hypre cflags="-g -O0" ~shared %gcc@4.5.0 ^foo ^zmpi'),
|
||||
Spec('libelf cflags=-O3 ~shared %gcc@4.5.0 ^foo ^zmpi'),
|
||||
Spec('libelf cflags="-g -O0" ~shared %gcc@4.5.0 ^foo ^zmpi'),
|
||||
]
|
||||
assert speclist.specs == expected_specs
|
||||
|
||||
def test_spec_list_add(self):
|
||||
speclist = SpecList('specs', self.default_input,
|
||||
self.default_reference)
|
||||
|
||||
assert speclist.specs_as_yaml_list == self.default_expansion
|
||||
assert speclist.specs_as_constraints == self.default_constraints
|
||||
assert speclist.specs == self.default_specs
|
||||
|
||||
speclist.add('libdwarf')
|
||||
|
||||
assert speclist.specs_as_yaml_list == self.default_expansion + [
|
||||
'libdwarf']
|
||||
assert speclist.specs_as_constraints == self.default_constraints + [
|
||||
[Spec('libdwarf')]]
|
||||
assert speclist.specs == self.default_specs + [Spec('libdwarf')]
|
||||
|
||||
def test_spec_list_remove(self):
|
||||
speclist = SpecList('specs', self.default_input,
|
||||
self.default_reference)
|
||||
|
||||
assert speclist.specs_as_yaml_list == self.default_expansion
|
||||
assert speclist.specs_as_constraints == self.default_constraints
|
||||
assert speclist.specs == self.default_specs
|
||||
|
||||
speclist.remove('libelf')
|
||||
|
||||
assert speclist.specs_as_yaml_list + [
|
||||
'libelf'
|
||||
] == self.default_expansion
|
||||
|
||||
assert speclist.specs_as_constraints + [
|
||||
[Spec('libelf')]
|
||||
] == self.default_constraints
|
||||
|
||||
assert speclist.specs + [Spec('libelf')] == self.default_specs
|
||||
|
||||
def test_spec_list_update_reference(self):
|
||||
speclist = SpecList('specs', self.default_input,
|
||||
self.default_reference)
|
||||
|
||||
assert speclist.specs_as_yaml_list == self.default_expansion
|
||||
assert speclist.specs_as_constraints == self.default_constraints
|
||||
assert speclist.specs == self.default_specs
|
||||
|
||||
new_mpis = SpecList('mpis', self.default_reference['mpis'].yaml_list)
|
||||
new_mpis.add('mpich@3.3')
|
||||
new_reference = self.default_reference.copy()
|
||||
new_reference['mpis'] = new_mpis
|
||||
|
||||
speclist.update_reference(new_reference)
|
||||
|
||||
expansion = list(self.default_expansion)
|
||||
expansion.insert(3, 'mpich@3.3')
|
||||
constraints = list(self.default_constraints)
|
||||
constraints.insert(3, [Spec('mpich@3.3')])
|
||||
specs = list(self.default_specs)
|
||||
specs.insert(3, Spec('mpich@3.3'))
|
||||
|
||||
assert speclist.specs_as_yaml_list == expansion
|
||||
assert speclist.specs_as_constraints == constraints
|
||||
assert speclist.specs == specs
|
||||
|
||||
def test_spec_list_extension(self):
|
||||
speclist = SpecList('specs', self.default_input,
|
||||
self.default_reference)
|
||||
|
||||
assert speclist.specs_as_yaml_list == self.default_expansion
|
||||
assert speclist.specs_as_constraints == self.default_constraints
|
||||
assert speclist.specs == self.default_specs
|
||||
|
||||
new_ref = self.default_reference.copy()
|
||||
otherlist = SpecList('specs',
|
||||
['zlib', {'matrix': [['callpath'],
|
||||
['%intel@18']]}],
|
||||
new_ref)
|
||||
|
||||
speclist.extend(otherlist)
|
||||
|
||||
assert speclist.specs_as_yaml_list == (self.default_expansion +
|
||||
otherlist.specs_as_yaml_list)
|
||||
assert speclist.specs == self.default_specs + otherlist.specs
|
||||
assert speclist._reference is new_ref
|
Loading…
Reference in New Issue
Block a user