diff options
Diffstat (limited to 'lib/spack/spack/spec.py')
-rw-r--r-- | lib/spack/spack/spec.py | 426 |
1 files changed, 301 insertions, 125 deletions
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py index db8dcf61a8..7bc0dce12a 100644 --- a/lib/spack/spack/spec.py +++ b/lib/spack/spack/spec.py @@ -108,6 +108,10 @@ from six import StringIO from six import string_types from six import iteritems +from llnl.util.filesystem import find_headers, find_libraries, is_exe +from llnl.util.lang import * +from llnl.util.tty.color import * + import spack import spack.architecture import spack.compilers as compilers @@ -117,9 +121,6 @@ import spack.store import spack.util.spack_json as sjson import spack.util.spack_yaml as syaml -from llnl.util.filesystem import find_headers, find_libraries, is_exe -from llnl.util.lang import * -from llnl.util.tty.color import * from spack.util.module_cmd import get_path_from_module, load_module from spack.error import SpecError, UnsatisfiableSpecError from spack.provider_index import ProviderIndex @@ -136,7 +137,6 @@ __all__ = [ 'Spec', 'alldeps', 'canonical_deptype', - 'validate_deptype', 'parse', 'parse_anonymous_spec', 'SpecError', @@ -149,7 +149,6 @@ __all__ = [ 'DuplicateArchitectureError', 'InconsistentSpecError', 'InvalidDependencyError', - 'InvalidDependencyTypeError', 'NoProviderError', 'MultipleProviderError', 'UnsatisfiableSpecError', @@ -166,21 +165,20 @@ __all__ = [ 'NoSuchHashError', 'RedundantSpecError'] -# Valid pattern for an identifier in Spack +#: Valid pattern for an identifier in Spack identifier_re = r'\w[\w-]*' -# Convenient names for color formats so that other things can use them -compiler_color = '@g' -version_color = '@c' -architecture_color = '@m' -enabled_variant_color = '@B' -disabled_variant_color = '@r' -dependency_color = '@.' -hash_color = '@K' - -"""This map determines the coloring of specs when using color output. - We make the fields different colors to enhance readability. - See spack.color for descriptions of the color codes. """ +compiler_color = '@g' #: color for highlighting compilers +version_color = '@c' #: color for highlighting versions +architecture_color = '@m' #: color for highlighting architectures +enabled_variant_color = '@B' #: color for highlighting enabled variants +disabled_variant_color = '@r' #: color for highlighting disabled varaints +dependency_color = '@.' #: color for highlighting dependencies +hash_color = '@K' #: color for highlighting package hashes + +#: This map determines the coloring of specs when using color output. +#: We make the fields different colors to enhance readability. +#: See spack.color for descriptions of the color codes. color_formats = {'%': compiler_color, '@': version_color, '=': architecture_color, @@ -189,51 +187,36 @@ color_formats = {'%': compiler_color, '^': dependency_color, '#': hash_color} -"""Regex used for splitting by spec field separators.""" -_separators = '[%s]' % ''.join(color_formats.keys()) +#: Regex used for splitting by spec field separators. +#: These need to be escaped to avoid metacharacters in +#: ``color_formats.keys()``. +_separators = '[\\%s]' % '\\'.join(color_formats.keys()) -"""Versionlist constant so we don't have to build a list - every time we call str()""" +#: Versionlist constant so we don't have to build a list +#: every time we call str() _any_version = VersionList([':']) -# Special types of dependencies. +#: Types of dependencies that Spack understands. alldeps = ('build', 'link', 'run') -norun = ('link', 'build') -special_types = { - 'alldeps': alldeps, - 'all': alldeps, # allow "all" as string but not symbol. - 'norun': norun, -} -legal_deps = tuple(special_types) + alldeps - -"""Max integer helps avoid passing too large a value to cyaml.""" +#: Max integer helps avoid passing too large a value to cyaml. maxint = 2 ** (ctypes.sizeof(ctypes.c_int) * 8 - 1) - 1 -def validate_deptype(deptype): - if isinstance(deptype, str): - if deptype not in legal_deps: - raise InvalidDependencyTypeError( - "Invalid dependency type: %s" % deptype) - - elif isinstance(deptype, (list, tuple)): - for t in deptype: - validate_deptype(t) - - elif deptype is None: - raise InvalidDependencyTypeError("deptype cannot be None!") - - def canonical_deptype(deptype): - if deptype is None: + if deptype in (None, 'all', all): return alldeps elif isinstance(deptype, string_types): - return special_types.get(deptype, (deptype,)) + if deptype not in alldeps: + raise ValueError('Invalid dependency type: %s' % deptype) + return (deptype,) elif isinstance(deptype, (tuple, list)): - return (sum((canonical_deptype(d) for d in deptype), ())) + invalid = next((d for d in deptype if d not in alldeps), None) + if invalid: + raise ValueError('Invalid dependency type: %s' % invalid) + return tuple(sorted(deptype)) return deptype @@ -820,7 +803,18 @@ def _libs_default_handler(descriptor, spec, cls): Raises: RuntimeError: If no libraries are found """ - name = 'lib' + spec.name + + # Variable 'name' is passed to function 'find_libraries', which supports + # glob characters. For example, we have a package with a name 'abc-abc'. + # Now, we don't know if the original name of the package is 'abc_abc' + # (and it generates a library 'libabc_abc.so') or 'abc-abc' (and it + # generates a library 'libabc-abc.so'). So, we tell the function + # 'find_libraries' to give us anything that matches 'libabc?abc' and it + # gives us either 'libabc-abc.so' or 'libabc_abc.so' (or an error) + # depending on which one exists (there is a possibility, of course, to + # get something like 'libabcXabc.so, but for now we consider this + # unlikely). + name = 'lib' + spec.name.replace('-', '?') if '+shared' in spec: libs = find_libraries( @@ -977,7 +971,159 @@ class SpecBuildInterface(ObjectWrapper): @key_ordering class Spec(object): - def __init__(self, spec_like, *dep_like, **kwargs): + @staticmethod + def from_literal(spec_dict, normal=True): + """Builds a Spec from a dictionary containing the spec literal. + + The dictionary must have a single top level key, representing the root, + and as many secondary level keys as needed in the spec. + + The keys can be either a string or a Spec or a tuple containing the + Spec and the dependency types. + + Args: + spec_dict (dict): the dictionary containing the spec literal + normal (bool): if True the same key appearing at different levels + of the ``spec_dict`` will map to the same object in memory. + + Examples: + A simple spec ``foo`` with no dependencies: + + .. code-block:: python + + {'foo': None} + + A spec ``foo`` with a ``(build, link)`` dependency ``bar``: + + .. code-block:: python + + {'foo': + {'bar:build,link': None}} + + A spec with a diamond dependency and various build types: + + .. code-block:: python + + {'dt-diamond': { + 'dt-diamond-left:build,link': { + 'dt-diamond-bottom:build': None + }, + 'dt-diamond-right:build,link': { + 'dt-diamond-bottom:build,link,run': None + } + }} + + The same spec with a double copy of ``dt-diamond-bottom`` and + no diamond structure: + + .. code-block:: python + + {'dt-diamond': { + 'dt-diamond-left:build,link': { + 'dt-diamond-bottom:build': None + }, + 'dt-diamond-right:build,link': { + 'dt-diamond-bottom:build,link,run': None + } + }, normal=False} + + Constructing a spec using a Spec object as key: + + .. code-block:: python + + mpich = Spec('mpich') + libelf = Spec('libelf@1.8.11') + expected_normalized = Spec.from_literal({ + 'mpileaks': { + 'callpath': { + 'dyninst': { + 'libdwarf': {libelf: None}, + libelf: None + }, + mpich: None + }, + mpich: None + }, + }) + + """ + + # Maps a literal to a Spec, to be sure we are reusing the same object + spec_cache = LazySpecCache() + + def spec_builder(d): + # The invariant is that the top level dictionary must have + # only one key + assert len(d) == 1 + + # Construct the top-level spec + spec_like, dep_like = next(iter(d.items())) + + # If the requirements was for unique nodes (default) + # then re-use keys from the local cache. Otherwise build + # a new node every time. + if not isinstance(spec_like, Spec): + spec = spec_cache[spec_like] if normal else Spec(spec_like) + else: + spec = spec_like + + if dep_like is None: + return spec + + def name_and_dependency_types(s): + """Given a key in the dictionary containing the literal, + extracts the name of the spec and its dependency types. + + Args: + s (str): key in the dictionary containing the literal + + """ + t = s.split(':') + + if len(t) > 2: + msg = 'more than one ":" separator in key "{0}"' + raise KeyError(msg.format(s)) + + n = t[0] + if len(t) == 2: + dtypes = tuple(dt.strip() for dt in t[1].split(',')) + else: + dtypes = () + + return n, dtypes + + def spec_and_dependency_types(s): + """Given a non-string key in the literal, extracts the spec + and its dependency types. + + Args: + s (spec or tuple): either a Spec object or a tuple + composed of a Spec object and a string with the + dependency types + + """ + if isinstance(s, Spec): + return s, () + + spec_obj, dtypes = s + return spec_obj, tuple(dt.strip() for dt in dtypes.split(',')) + + # Recurse on dependencies + for s, s_dependencies in dep_like.items(): + + if isinstance(s, string_types): + dag_node, dependency_types = name_and_dependency_types(s) + else: + dag_node, dependency_types = spec_and_dependency_types(s) + + dependency_spec = spec_builder({dag_node: s_dependencies}) + spec._add_dependency(dependency_spec, dependency_types) + + return spec + + return spec_builder(spec_dict) + + def __init__(self, spec_like, **kwargs): # Copy if spec_like is a Spec. if isinstance(spec_like, Spec): self._dup(spec_like) @@ -987,55 +1133,24 @@ class Spec(object): if not isinstance(spec_like, string_types): raise TypeError("Can't make spec out of %s" % type(spec_like)) - spec_list = SpecParser().parse(spec_like) + # parse string types *into* this spec + spec_list = SpecParser(self).parse(spec_like) if len(spec_list) > 1: raise ValueError("More than one spec in string: " + spec_like) if len(spec_list) < 1: raise ValueError("String contains no specs: " + spec_like) - # Take all the attributes from the first parsed spec without copying. - # This is safe b/c we throw out the parsed spec. It's a bit nasty, - # but it's nastier to implement the constructor so that the parser - # writes directly into this Spec object. - other = spec_list[0] - self.name = other.name - self.versions = other.versions - self.architecture = other.architecture - self.compiler = other.compiler - self.compiler_flags = other.compiler_flags - self.compiler_flags.spec = self - self._dependencies = other._dependencies - self._dependents = other._dependents - self.variants = other.variants - self.variants.spec = self - self.namespace = other.namespace - self._hash = other._hash - self._cmp_key_cache = other._cmp_key_cache - - # Specs are by default not assumed to be normal or concrete. - self._normal = False - self._concrete = False + # Specs are by default not assumed to be normal, but in some + # cases we've read them from a file want to assume normal. + # This allows us to manipulate specs that Spack doesn't have + # package.py files for. + self._normal = kwargs.get('normal', False) + self._concrete = kwargs.get('concrete', False) # Allow a spec to be constructed with an external path. self.external_path = kwargs.get('external_path', None) self.external_module = kwargs.get('external_module', None) - # This allows users to construct a spec DAG with literals. - # Note that given two specs a and b, Spec(a) copies a, but - # Spec(a, b) will copy a but just add b as a dep. - deptypes = () - for dep in dep_like: - - if isinstance(dep, (list, tuple)): - # Literals can be deptypes -- if there are tuples in the - # list, they will be used as deptypes for the following Spec. - deptypes = tuple(dep) - continue - - spec = dep if isinstance(dep, Spec) else Spec(dep) - self._add_dependency(spec, deptypes) - deptypes = () - @property def external(self): return bool(self.external_path) or bool(self.external_module) @@ -1363,9 +1478,8 @@ class Spec(object): @property def cshort_spec(self): - """Returns a version of the spec with the dependencies hashed - instead of completely enumerated.""" - return self.format('$_$@$%@$+$=$/', color=True) + """Returns an auto-colorized version of ``self.short_spec``.""" + return self.cformat('$_$@$%@$+$=$/') @property def prefix(self): @@ -1805,9 +1919,9 @@ class Spec(object): for x in self.traverse(): for conflict_spec, when_list in x.package.conflicts.items(): if x.satisfies(conflict_spec): - for when_spec in when_list: + for when_spec, msg in when_list: if x.satisfies(when_spec): - matches.append((x, conflict_spec, when_spec)) + matches.append((x, conflict_spec, when_spec, msg)) if matches: raise ConflictsInSpecError(self, matches) @@ -1887,6 +2001,7 @@ class Spec(object): pkg = spack.repo.get(self.fullname) conditions = pkg.dependencies[name] + substitute_abstract_variants(self) # evaluate when specs to figure out constraints on the dependency. dep = None for when_spec, dep_spec in conditions.items(): @@ -1992,10 +2107,19 @@ class Spec(object): try: changed |= spec_deps[dep.name].constrain(dep) except UnsatisfiableSpecError as e: - e.message = "Invalid spec: '%s'. " - e.message += "Package %s requires %s %s, but spec asked for %s" - e.message %= (spec_deps[dep.name], dep.name, - e.constraint_type, e.required, e.provided) + fmt = 'An unsatisfiable {0}'.format(e.constraint_type) + fmt += ' constraint has been detected for spec:' + fmt += '\n\n{0}\n\n'.format(spec_deps[dep.name].tree(indent=4)) + fmt += 'while trying to concretize the partial spec:' + fmt += '\n\n{0}\n\n'.format(self.tree(indent=4)) + fmt += '{0} requires {1} {2} {3}, but spec asked for {4}' + e.message = fmt.format( + self.name, + dep.name, + e.constraint_type, + e.required, + e.provided + ) raise e # Add merged spec to my deps and recurse @@ -2419,15 +2543,24 @@ class Spec(object): def _dup(self, other, deps=True, cleardeps=True): """Copy the spec other into self. This is an overwriting - copy. It does not copy any dependents (parents), but by default - copies dependencies. + copy. It does not copy any dependents (parents), but by default + copies dependencies. - To duplicate an entire DAG, call _dup() on the root of the DAG. + To duplicate an entire DAG, call _dup() on the root of the DAG. + + Args: + other (Spec): spec to be copied onto ``self`` + deps (bool or Sequence): if True copies all the dependencies. If + False copies None. If a sequence of dependency types copy + only those types. + cleardeps (bool): if True clears the dependencies of ``self``, + before possibly copying the dependencies of ``other`` onto + ``self`` + + Returns: + True if ``self`` changed because of the copy operation, + False otherwise. - Options: - dependencies[=True] - Whether deps should be copied too. Set to False to copy a - spec but not its dependencies. """ # We don't count dependencies as changes here changed = True @@ -2453,6 +2586,7 @@ class Spec(object): self._dependents = DependencyMap(self) self._dependencies = DependencyMap(self) self.compiler_flags = other.compiler_flags.copy() + self.compiler_flags.spec = self self.variants = other.variants.copy() self.variants.spec = self self.external_path = other.external_path @@ -2725,11 +2859,8 @@ class Spec(object): named_str = fmt = '' def write(s, c): - if color: - f = color_formats[c] + cescape(s) + '@.' - cwrite(f, stream=out, color=color) - else: - out.write(s) + f = color_formats[c] + cescape(s) + '@.' + cwrite(f, stream=out, color=color) iterator = enumerate(format_string) for i, c in iterator: @@ -2821,7 +2952,7 @@ class Spec(object): write(fmt % str(self.variants), '+') elif named_str == 'ARCHITECTURE': if self.architecture and str(self.architecture): - write(fmt % str(self.architecture), ' arch=') + write(fmt % str(self.architecture), '=') elif named_str == 'SHA1': if self.dependencies: out.write(fmt % str(self.dag_hash(7))) @@ -2852,6 +2983,12 @@ class Spec(object): result = out.getvalue() return result + def cformat(self, *args, **kwargs): + """Same as format, but color defaults to auto instead of False.""" + kwargs = kwargs.copy() + kwargs.setdefault('color', None) + return self.format(*args, **kwargs) + def dep_string(self): return ''.join("^" + dep.format() for dep in self.sorted_deps()) @@ -2882,7 +3019,7 @@ class Spec(object): def tree(self, **kwargs): """Prints out this spec and its dependencies, tree-formatted with indentation.""" - color = kwargs.pop('color', False) + color = kwargs.pop('color', get_color_when()) depth = kwargs.pop('depth', False) hashes = kwargs.pop('hashes', False) hlen = kwargs.pop('hashlen', None) @@ -2938,6 +3075,19 @@ class Spec(object): return str(self) +class LazySpecCache(collections.defaultdict): + """Cache for Specs that uses a spec_like as key, and computes lazily + the corresponding value ``Spec(spec_like``. + """ + def __init__(self): + super(LazySpecCache, self).__init__(Spec) + + def __missing__(self, key): + value = self.default_factory(key) + self[key] = value + return value + + # # These are possible token types in the spec grammar. # @@ -2976,9 +3126,17 @@ _lexer = SpecLexer() class SpecParser(spack.parse.Parser): - def __init__(self): + def __init__(self, initial_spec=None): + """Construct a new SpecParser. + + Args: + initial_spec (Spec, optional): provide a Spec that we'll parse + directly into. This is used to avoid construction of a + superfluous Spec object in the Spec constructor. + """ super(SpecParser, self).__init__(_lexer) self.previous = None + self._initial = initial_spec def do_parse(self): specs = [] @@ -3102,8 +3260,14 @@ class SpecParser(spack.parse.Parser): spec_namespace = None spec_name = None - # This will init the spec without calling __init__. - spec = Spec.__new__(Spec) + if self._initial is None: + # This will init the spec without calling Spec.__init__ + spec = Spec.__new__(Spec) + else: + # this is used by Spec.__init__ + spec = self._initial + self._initial = None + spec.name = spec_name spec.versions = VersionList() spec.variants = VariantMap(spec) @@ -3161,7 +3325,7 @@ class SpecParser(spack.parse.Parser): # Get spec by hash and confirm it matches what we already have hash_spec = self.spec_by_hash() if hash_spec.satisfies(spec): - spec = hash_spec + spec._dup(hash_spec) break else: raise InvalidHashError(spec, hash_spec.dag_hash()) @@ -3332,10 +3496,6 @@ class InvalidDependencyError(SpecError): of the package.""" -class InvalidDependencyTypeError(SpecError): - """Raised when a dependency type is not a legal Spack dep type.""" - - class NoProviderError(SpecError): """Raised when there is no package that provides a particular virtual dependency. @@ -3445,8 +3605,24 @@ class ConflictsInSpecError(SpecError, RuntimeError): message = 'Conflicts in concretized spec "{0}"\n'.format( spec.short_spec ) - long_message = 'List of matching conflicts:\n\n' - match_fmt = '{0}. "{1}" conflicts with "{2}" in spec "{3}"\n' - for idx, (s, c, w) in enumerate(matches): - long_message += match_fmt.format(idx + 1, c, w, s) + + visited = set() + + long_message = '' + + match_fmt_default = '{0}. "{1}" conflicts with "{2}"\n' + match_fmt_custom = '{0}. "{1}" conflicts with "{2}" [{3}]\n' + + for idx, (s, c, w, msg) in enumerate(matches): + + if s not in visited: + visited.add(s) + long_message += 'List of matching conflicts for spec:\n\n' + long_message += s.tree(indent=4) + '\n' + + if msg is None: + long_message += match_fmt_default.format(idx + 1, c, w) + else: + long_message += match_fmt_custom.format(idx + 1, c, w, msg) + super(ConflictsInSpecError, self).__init__(message, long_message) |