summaryrefslogtreecommitdiff
path: root/lib/spack/spack/spec.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/spack/spack/spec.py')
-rw-r--r--lib/spack/spack/spec.py292
1 files changed, 174 insertions, 118 deletions
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
index edc97c7c3b..2731c344ab 100644
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -67,6 +67,7 @@ from StringIO import StringIO
import tty
import spack.parse
import spack.error
+import spack.concretize
import spack.compilers
import spack.compilers.gcc
import spack.packages as packages
@@ -137,9 +138,8 @@ class Compiler(object):
def constrain(self, other):
- if not self.satisfies(other.compiler):
- raise UnsatisfiableCompilerSpecError(
- "%s does not satisfy %s" % (self.compiler, other.compiler))
+ if not self.satisfies(other):
+ raise UnsatisfiableCompilerSpecError(self, other)
self.versions.intersect(other.versions)
@@ -149,23 +149,6 @@ class Compiler(object):
return self.versions.concrete
- def _concretize(self):
- """If this spec could describe more than one version, variant, or build
- of a package, this will resolve it to be concrete.
- """
- # TODO: support compilers other than GCC.
- if self.concrete:
- return
- gcc_version = spack.compilers.gcc.get_version()
- self.versions = VersionList([gcc_version])
-
-
- def concretized(self):
- clone = self.copy()
- clone._concretize()
- return clone
-
-
@property
def version(self):
if not self.concrete:
@@ -243,13 +226,34 @@ class DependencyMap(HashableMap):
@key_ordering
class Spec(object):
- def __init__(self, name):
- self.name = name
- self.versions = VersionList()
- self.variants = VariantMap()
- self.architecture = None
- self.compiler = None
- self.dependencies = DependencyMap()
+ def __init__(self, spec_like):
+ # Copy if spec_like is a Spec.
+ if type(spec_like) == Spec:
+ self._dup(spec_like)
+ return
+
+ # Parse if the spec_like is a string.
+ if type(spec_like) != str:
+ raise TypeError("Can't make spec out of %s" % type(spec_like))
+
+ spec_list = SpecParser().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 a little bit nasty, but it's nastier to make the parser
+ # write directly into this Spec object.
+ other = spec_list[0]
+ self.name = other.name
+ self.parent = other.parent
+ self.versions = other.versions
+ self.variants = other.variants
+ self.architecture = other.architecture
+ self.compiler = other.compiler
+ self.dependencies = other.dependencies
+
#
# Private routines here are called by the parser when building a spec.
@@ -285,6 +289,21 @@ class Spec(object):
if dep.name in self.dependencies:
raise DuplicateDependencyError("Cannot depend on '%s' twice" % dep)
self.dependencies[dep.name] = dep
+ dep.parent = self
+
+
+ @property
+ def root(self):
+ """Follow parent links and find the root of this spec's DAG."""
+ root = self
+ while root.parent is not None:
+ root = root.parent
+ return root
+
+
+ @property
+ def package(self):
+ return packages.get(self.name)
@property
@@ -296,6 +315,20 @@ class Spec(object):
and self.dependencies.concrete)
+ def preorder_traversal(self, visited=None):
+ if visited is None:
+ visited = set()
+
+ if id(self) in visited:
+ return
+ visited.add(id(self))
+
+ yield self
+ for dep in self.dependencies.itervalues():
+ for spec in dep.preorder_traversal(visited):
+ yield spec
+
+
def _concretize(self):
"""A spec is concrete if it describes one build of a package uniquely.
This will ensure that this spec is concrete.
@@ -327,30 +360,40 @@ class Spec(object):
# TODO: handle variants.
- pkg = packages.get(self.name)
-
# Take the highest version in a range
if not self.versions.concrete:
- preferred = self.versions.highest() or pkg.version
+ preferred = self.versions.highest() or self.package.version
self.versions = VersionList([preferred])
# Ensure dependencies have right versions
- @property
- def traverse_deps(self, visited=None):
- """Yields dependencies in depth-first order"""
- if not visited:
- visited = set()
+ def flatten(self):
+ """Pull all dependencies up to the root (this spec).
+ Merge constraints for dependencies with the same name, and if they
+ conflict, throw an exception. """
+ # This ensures that the package descriptions themselves are consistent
+ self.package.validate_dependencies()
+
+ # Once that is guaranteed, we know any constraint violations are due
+ # to the spec -- so they're the user's fault, not Spack's.
+ flat_deps = DependencyMap()
+ try:
+ for spec in self.preorder_traversal():
+ if spec.name not in flat_deps:
+ flat_deps[spec.name] = spec
+ else:
+ flat_deps[spec.name].constrain(spec)
- for name in sorted(self.dependencies.keys()):
- dep = dependencies[name]
- if dep in visited:
- continue
+ except UnsatisfiableSpecError, e:
+ # This REALLY shouldn't happen unless something is wrong in spack.
+ # It means we got a spec DAG with two instances of the same package
+ # that had inconsistent constraints. There's no way for a user to
+ # produce a spec like this (the parser adds all deps to the root),
+ # so this means OUR code is not sane!
+ raise InconsistentSpecError("Invalid Spec DAG: %s" % e.message)
- for d in dep.traverse_deps(seen):
- yield d
- yield dep
+ self.dependencies = flat_deps
def _normalize_helper(self, visited, spec_deps):
@@ -362,9 +405,7 @@ class Spec(object):
# Combine constraints from package dependencies with
# information in this spec's dependencies.
pkg = packages.get(self.name)
- for pkg_dep in pkg.dependencies:
- name = pkg_dep.name
-
+ for name, pkg_dep in self.package.dependencies.iteritems():
if name not in spec_deps:
# Clone the spec from the package
spec_deps[name] = pkg_dep.copy()
@@ -372,23 +413,29 @@ class Spec(object):
try:
# intersect package information with spec info
spec_deps[name].constrain(pkg_dep)
+
except UnsatisfiableSpecError, e:
- error_type = type(e)
- raise error_type(
- "Violated depends_on constraint from package %s: %s"
- % (self.name, e.message))
+ e.message = "Invalid spec: '%s'. "
+ e.message += "Package %s requires %s %s, but spec asked for %s"
+ e.message %= (spec_deps[name], name, e.constraint_type,
+ e.required, e.provided)
+ raise e
# Add merged spec to my deps and recurse
- self.dependencies[name] = spec_deps[name]
+ self._add_dependency(spec_deps[name])
self.dependencies[name]._normalize_helper(visited, spec_deps)
def normalize(self):
- if any(dep.dependencies for dep in self.dependencies.values()):
- raise SpecError("Spec has already been normalized.")
-
+ # Ensure first that all packages exist.
self.validate_package_names()
+ # Then ensure that the packages mentioned are sane, that the
+ # provided spec is sane, and that all dependency specs are in the
+ # root node of the spec. Flatten will do this for us.
+ self.flatten()
+
+ # Now that we're flat we can get all our dependencies at once.
spec_deps = self.dependencies
self.dependencies = DependencyMap()
@@ -404,29 +451,25 @@ class Spec(object):
def validate_package_names(self):
- for name in self.dependencies:
- packages.get(name)
+ packages.get(self.name)
+ for name, dep in self.dependencies.iteritems():
+ dep.validate_package_names()
def constrain(self, other):
if not self.versions.overlaps(other.versions):
- raise UnsatisfiableVersionSpecError(
- "%s does not satisfy %s" % (self.versions, other.versions))
+ raise UnsatisfiableVersionSpecError(self.versions, other.versions)
- conflicting_variants = [
- v for v in other.variants if v in self.variants and
- self.variants[v].enabled != other.variants[v].enabled]
-
- if conflicting_variants:
- raise UnsatisfiableVariantSpecError(comma_and(
- "%s does not satisfy %s" % (self.variants[v], other.variants[v])
- for v in conflicting_variants))
+ for v in other.variants:
+ if (v in self.variants and
+ self.variants[v].enabled != other.variants[v].enabled):
+ raise UnsatisfiableVariantSpecError(self.variants[v],
+ other.variants[v])
if self.architecture is not None and other.architecture is not None:
if self.architecture != other.architecture:
- raise UnsatisfiableArchitectureSpecError(
- "Asked for architecture %s, but required %s"
- % (self.architecture, other.architecture))
+ raise UnsatisfiableArchitectureSpecError(self.architecture,
+ other.architecture)
if self.compiler is not None and other.compiler is not None:
self.compiler.constrain(other.compiler)
@@ -457,16 +500,23 @@ class Spec(object):
return clone
+ def _dup(self, other):
+ """Copy the spec other into self. This is a
+ first-party, overwriting copy."""
+ # TODO: this needs to handle DAGs.
+ self.name = other.name
+ self.versions = other.versions.copy()
+ self.variants = other.variants.copy()
+ self.architecture = other.architecture
+ self.compiler = None
+ if other.compiler:
+ self.compiler = other.compiler.copy()
+ self.dependencies = other.dependencies.copy()
+
+
def copy(self):
- clone = Spec(self.name)
- clone.versions = self.versions.copy()
- clone.variants = self.variants.copy()
- clone.architecture = self.architecture
- clone.compiler = None
- if self.compiler:
- clone.compiler = self.compiler.copy()
- clone.dependencies = self.dependencies.copy()
- return clone
+ """Return a deep copy of this spec."""
+ return Spec(self)
@property
@@ -478,7 +528,7 @@ class Spec(object):
def _cmp_key(self):
return (self.name, self.versions, self.variants,
- self.architecture, self.compiler)
+ self.architecture, self.compiler, self.dependencies)
def colorized(self):
@@ -505,7 +555,7 @@ class Spec(object):
def tree(self, indent=""):
"""Prints out this spec and its dependencies, tree-formatted
- with indentation."""
+ with indentation. Each node also has an id."""
out = indent + self.str_without_deps()
for dep in sorted(self.dependencies.keys()):
out += "\n" + self.dependencies[dep].tree(indent + " ")
@@ -566,8 +616,22 @@ class SpecParser(spack.parse.Parser):
def spec(self):
+ """Parse a spec out of the input. If a spec is supplied, then initialize
+ and return it instead of creating a new one."""
self.check_identifier()
- spec = Spec(self.token.value)
+
+ # This will init the spec without calling __init__.
+ spec = Spec.__new__(Spec)
+ spec.name = self.token.value
+ spec.parent = None
+ spec.versions = VersionList()
+ spec.variants = VariantMap()
+ spec.architecture = None
+ spec.compiler = None
+ spec.dependencies = DependencyMap()
+
+ # record this so that we know whether version is
+ # unspecified or not.
added_version = False
while self.next:
@@ -661,34 +725,10 @@ class SpecParser(spack.parse.Parser):
def parse(string):
- """Returns a list of specs from an input string."""
- return SpecParser().parse(string)
-
-
-def parse_one(string):
- """Parses a string containing only one spec, then returns that
- spec. If more than one spec is found, raises a ValueError.
+ """Returns a list of specs from an input string.
+ For creating one spec, see Spec() constructor.
"""
- spec_list = parse(string)
- if len(spec_list) > 1:
- raise ValueError("string contains more than one spec!")
- elif len(spec_list) < 1:
- raise ValueError("string contains no specs!")
- return spec_list[0]
-
-
-def make_spec(spec_like):
- if type(spec_like) == str:
- specs = parse(spec_like)
- if len(specs) != 1:
- raise ValueError("String contains multiple specs: '%s'" % spec_like)
- return specs[0]
-
- elif type(spec_like) == Spec:
- return spec_like
-
- else:
- raise TypeError("Can't make spec out of %s" % type(spec_like))
+ return SpecParser().parse(string)
class SpecError(spack.error.SpackError):
@@ -728,6 +768,13 @@ class DuplicateArchitectureError(SpecError):
super(DuplicateArchitectureError, self).__init__(message)
+class InconsistentSpecError(SpecError):
+ """Raised when two nodes in the same spec DAG have inconsistent
+ constraints."""
+ def __init__(self, message):
+ super(InconsistentSpecError, self).__init__(message)
+
+
class InvalidDependencyException(SpecError):
"""Raised when a dependency in a spec is not actually a dependency
of the package."""
@@ -736,30 +783,39 @@ class InvalidDependencyException(SpecError):
class UnsatisfiableSpecError(SpecError):
- """Raised when a spec conflicts with package constraints."""
- def __init__(self, message):
- super(UnsatisfiableSpecError, self).__init__(message)
+ """Raised when a spec conflicts with package constraints.
+ Provide the requirement that was violated when raising."""
+ def __init__(self, provided, required, constraint_type):
+ super(UnsatisfiableSpecError, self).__init__(
+ "%s does not satisfy %s" % (provided, required))
+ self.provided = provided
+ self.required = required
+ self.constraint_type = constraint_type
class UnsatisfiableVersionSpecError(UnsatisfiableSpecError):
"""Raised when a spec version conflicts with package constraints."""
- def __init__(self, message):
- super(UnsatisfiableVersionSpecError, self).__init__(message)
+ def __init__(self, provided, required):
+ super(UnsatisfiableVersionSpecError, self).__init__(
+ provided, required, "version")
class UnsatisfiableCompilerSpecError(UnsatisfiableSpecError):
"""Raised when a spec comiler conflicts with package constraints."""
- def __init__(self, message):
- super(UnsatisfiableCompilerSpecError, self).__init__(message)
+ def __init__(self, provided, required):
+ super(UnsatisfiableCompilerSpecError, self).__init__(
+ provided, required, "compiler")
class UnsatisfiableVariantSpecError(UnsatisfiableSpecError):
"""Raised when a spec variant conflicts with package constraints."""
- def __init__(self, message):
- super(UnsatisfiableVariantSpecError, self).__init__(message)
+ def __init__(self, provided, required):
+ super(UnsatisfiableVariantSpecError, self).__init__(
+ provided, required, "variant")
class UnsatisfiableArchitectureSpecError(UnsatisfiableSpecError):
"""Raised when a spec architecture conflicts with package constraints."""
- def __init__(self, message):
- super(UnsatisfiableArchitectureSpecError, self).__init__(message)
+ def __init__(self, provided, required):
+ super(UnsatisfiableArchitectureSpecError, self).__init__(
+ provided, required, "architecture")