diff options
Diffstat (limited to 'lib/spack/spack/spec.py')
-rw-r--r-- | lib/spack/spack/spec.py | 352 |
1 files changed, 249 insertions, 103 deletions
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py index 77bc57147d..d3a5f66e57 100644 --- a/lib/spack/spack/spec.py +++ b/lib/spack/spack/spec.py @@ -155,6 +155,9 @@ _separators = '[%s]' % ''.join(color_formats.keys()) every time we call str()""" _any_version = VersionList([':']) +alldeps = ('build', 'link', 'run') +nolink = ('build', 'run') + def index_specs(specs): """Take a list of specs and return a dict of lists. Dict is @@ -292,6 +295,27 @@ class CompilerSpec(object): @key_ordering +class DependencySpec(object): + """ + Dependencies have conditions in which they apply. + + This stores both what is depended on and why it is a dependency. + """ + def __init__(self, spec, deptypes): + self.spec = spec + self.deptypes = deptypes + + def _cmp_key(self): + return self.spec + + def copy(self): + return DependencySpec(self.spec.copy(), self.deptype) + + def __str__(self): + return str(self.spec) + + +@key_ordering class VariantSpec(object): """Variants are named, build-time options for a package. Names depend @@ -440,11 +464,11 @@ class DependencyMap(HashableMap): The DependencyMap is keyed by name. """ @property def concrete(self): - return all(d.concrete for d in self.values()) + return all(d.spec.concrete for d in self.values()) def __str__(self): return ''.join( - ["^" + str(self[name]) for name in sorted(self.keys())]) + ["^" + str(self[name].spec) for name in sorted(self.keys())]) @key_ordering @@ -472,13 +496,13 @@ class Spec(object): # writes directly into this Spec object. other = spec_list[0] self.name = other.name - self.dependents = other.dependents 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._dependencies = other._dependencies + self._dependents = other._dependents self.variants = other.variants self.variants.spec = self self.namespace = other.namespace @@ -500,7 +524,49 @@ class Spec(object): # Spec(a, b) will copy a but just add b as a dep. for dep in dep_like: spec = dep if isinstance(dep, Spec) else Spec(dep) - self._add_dependency(spec) + # XXX(deptype): default deptypes + self._add_dependency(spec, ('build', 'link')) + + def get_dependency(self, name): + dep = self._dependencies.get(name) + if dep is not None: + return dep + raise InvalidDependencyException( + self.name + " does not depend on " + comma_or(name)) + + def _deptype_norm(self, deptype): + if deptype is None: + return alldeps + # Force deptype to be a set object so that we can do set intersections. + if isinstance(deptype, str): + return (deptype,) + return deptype + + def _find_deps(self, where, deptype): + deptype = self._deptype_norm(deptype) + + return [dep.spec + for dep in where.values() + if deptype and any(d in deptype for d in dep.deptypes)] + + def dependencies(self, deptype=None): + return self._find_deps(self._dependencies, deptype) + + def dependents(self, deptype=None): + return self._find_deps(self._dependents, deptype) + + def _find_deps_dict(self, where, deptype): + deptype = self._deptype_norm(deptype) + + return [(dep.spec.name, dep) + for dep in where.values() + if deptype and any(d in deptype for d in dep.deptypes)] + + def dependencies_dict(self, deptype=None): + return dict(self._find_deps_dict(self._dependencies, deptype)) + + def dependents_dict(self, deptype=None): + return dict(self._find_deps_dict(self._dependents, deptype)) # # Private routines here are called by the parser when building a spec. @@ -609,13 +675,13 @@ class Spec(object): if self.architecture.platform: self.architecture.target = self.architecture.platform.target(value) - def _add_dependency(self, spec): + def _add_dependency(self, spec, deptypes): """Called by the parser to add another spec as a dependency.""" - if spec.name in self.dependencies: + if spec.name in self._dependencies: raise DuplicateDependencyError( "Cannot depend on '%s' twice" % spec) - self.dependencies[spec.name] = spec - spec.dependents[self.name] = self + self._dependencies[spec.name] = DependencySpec(spec, deptypes) + spec._dependents[self.name] = DependencySpec(self, deptypes) # # Public interface @@ -632,15 +698,15 @@ class Spec(object): installed). This will throw an assertion error if that is not the case. """ - if not self.dependents: + if not self._dependents: return self # If the spec has multiple dependents, ensure that they all # lead to the same place. Spack shouldn't deal with any DAGs # with multiple roots, so something's wrong if we find one. - depiter = iter(self.dependents.values()) - first_root = next(depiter).root - assert(all(first_root is d.root for d in depiter)) + depiter = iter(self._dependents.values()) + first_root = next(depiter).spec.root + assert(all(first_root is d.spec.root for d in depiter)) return first_root @property @@ -687,10 +753,21 @@ class Spec(object): and self.architecture.concrete and self.compiler and self.compiler.concrete and self.compiler_flags.concrete - and self.dependencies.concrete) + and self._dependencies.concrete) return self._concrete - def traverse(self, visited=None, d=0, **kwargs): + def traverse(self, visited=None, deptype=None, **kwargs): + traversal = self.traverse_with_deptype(visited=visited, + deptype=deptype, + **kwargs) + if kwargs.get('depth', False): + return [(s[0], s[1].spec) for s in traversal] + else: + return [s.spec for s in traversal] + + def traverse_with_deptype(self, visited=None, d=0, deptype=None, + deptype_query=None, _self_deptype=None, + **kwargs): """Generic traversal of the DAG represented by this spec. This will yield each node in the spec. Options: @@ -742,6 +819,12 @@ class Spec(object): direction = kwargs.get('direction', 'children') order = kwargs.get('order', 'pre') + if deptype is None: + deptype = alldeps + + if deptype_query is None: + deptype_query = ('link', 'run') + # Make sure kwargs have legal values; raise ValueError if not. def validate(name, val, allowed_values): if val not in allowed_values: @@ -759,30 +842,37 @@ class Spec(object): if key in visited and cover == 'nodes': return - # Determine whether and what to yield for this node. + def return_val(res): + return (d, res) if depth else res + yield_me = yield_root or d > 0 - result = (d, self) if depth else self # Preorder traversal yields before successors if yield_me and order == 'pre': - yield result + yield return_val(DependencySpec(self, _self_deptype)) + + deps = self.dependencies_dict(deptype) # Edge traversal yields but skips children of visited nodes if not (key in visited and cover == 'edges'): # This code determines direction and yields the children/parents - successors = self.dependencies + successors = deps if direction == 'parents': - successors = self.dependents + successors = self.dependents_dict() visited.add(key) for name in sorted(successors): child = successors[name] - for elt in child.traverse(visited, d + 1, **kwargs): + children = child.spec.traverse_with_deptype( + visited, d=d + 1, deptype=deptype_query, + deptype_query=deptype_query, + _self_deptype=child.deptypes, **kwargs) + for elt in children: yield elt # Postorder traversal yields after successors if yield_me and order == 'post': - yield result + yield return_val(DependencySpec(self, _self_deptype)) @property def short_spec(self): @@ -807,6 +897,7 @@ class Spec(object): if self._hash: return self._hash[:length] else: + # XXX(deptype): ignore 'build' dependencies here yaml_text = yaml.dump( self.to_node_dict(), default_flow_style=True, width=sys.maxint) sha = hashlib.sha1(yaml_text) @@ -819,11 +910,13 @@ class Spec(object): params = dict((name, v.value) for name, v in self.variants.items()) params.update(dict((name, value) for name, value in self.compiler_flags.items())) + deps = self.dependencies_dict(deptype=('link', 'run')) d = { - 'parameters' : params, - 'arch' : self.architecture, - 'dependencies' : dict((d, self.dependencies[d].dag_hash()) - for d in sorted(self.dependencies)) + 'parameters': params, + 'arch': self.architecture, + 'dependencies': dict((d, (deps[d].spec.dag_hash(), + deps[d].deptypes)) + for d in sorted(deps.keys())) } # Older concrete specs do not have a namespace. Omit for @@ -848,7 +941,7 @@ class Spec(object): def to_yaml(self, stream=None): node_list = [] - for s in self.traverse(order='pre'): + for s in self.traverse(order='pre', deptype=('link', 'run')): node = s.to_node_dict() node[s.name]['hash'] = s.dag_hash() node_list.append(node) @@ -889,6 +982,11 @@ class Spec(object): raise SpackRecordError( "Did not find a valid format for variants in YAML file") + # XXX(deptypes): why are dependencies not meant to be read here? + #for name, dep_info in node['dependencies'].items(): + # (dag_hash, deptypes) = dep_info + # spec._dependencies[name] = DependencySpec(dag_hash, deptypes) + return spec @staticmethod @@ -919,8 +1017,10 @@ class Spec(object): for node in yfile['spec']: name = next(iter(node)) - for dep_name in node[name]['dependencies']: - deps[name].dependencies[dep_name] = deps[dep_name] + for dep_name, (dep, deptypes) in \ + node[name]['dependencies'].items(): + deps[name]._dependencies[dep_name] = \ + DependencySpec(deps[dep_name], deptypes) return spec def _concretize_helper(self, presets=None, visited=None): @@ -940,8 +1040,9 @@ class Spec(object): changed = False # Concretize deps first -- this is a bottom-up process. - for name in sorted(self.dependencies.keys()): - changed |= self.dependencies[name]._concretize_helper(presets, visited) + for name in sorted(self._dependencies.keys()): + changed |= self._dependencies[ + name].spec._concretize_helper(presets, visited) if self.name in presets: changed |= self.constrain(presets[self.name]) @@ -965,13 +1066,16 @@ class Spec(object): def _replace_with(self, concrete): """Replace this virtual spec with a concrete spec.""" assert(self.virtual) - for name, dependent in self.dependents.items(): + for name, dep_spec in self._dependents.items(): + dependent = dep_spec.spec + deptypes = dep_spec.deptypes + # remove self from all dependents. - del dependent.dependencies[self.name] + del dependent._dependencies[self.name] # add the replacement, unless it is already a dep of dependent. - if concrete.name not in dependent.dependencies: - dependent._add_dependency(concrete) + if concrete.name not in dependent._dependencies: + dependent._add_dependency(concrete, deptypes) def _replace_node(self, replacement): """Replace this spec with another. @@ -982,13 +1086,15 @@ class Spec(object): to be normalized. """ - for name, dependent in self.dependents.items(): - del dependent.dependencies[self.name] - dependent._add_dependency(replacement) + for name, dep_spec in self._dependents.items(): + dependent = dep_spec.spec + deptypes = dep_spec.deptypes + del dependent._dependencies[self.name] + dependent._add_dependency(replacement, deptypes) - for name, dep in self.dependencies.items(): - del dep.dependents[self.name] - del self.dependencies[dep.name] + for name, dep_spec in self._dependencies.items(): + del dep_spec.spec.dependents[self.name] + del self._dependencies[dep.name] def _expand_virtual_packages(self): """Find virtual packages in this spec, replace them with providers, @@ -1008,13 +1114,14 @@ class Spec(object): a problem. """ # Make an index of stuff this spec already provides + # XXX(deptype): 'link' and 'run'? self_index = ProviderIndex(self.traverse(), restrict=True) changed = False done = False while not done: done = True - + # XXX(deptype): 'link' and 'run'? for spec in list(self.traverse()): replacement = None if spec.virtual: @@ -1054,10 +1161,10 @@ class Spec(object): # If replacement is external then trim the dependencies if replacement.external or replacement.external_module: - if (spec.dependencies): + if (spec._dependencies): changed = True - spec.dependencies = DependencyMap() - replacement.dependencies = DependencyMap() + spec._dependencies = DependencyMap() + replacement._dependencies = DependencyMap() replacement.architecture = self.architecture # TODO: could this and the stuff in _dup be cleaned up? @@ -1068,7 +1175,7 @@ class Spec(object): feq(replacement.versions, spec.versions) and feq(replacement.compiler, spec.compiler) and feq(replacement.architecture, spec.architecture) and - feq(replacement.dependencies, spec.dependencies) and + feq(replacement._dependencies, spec._dependencies) and feq(replacement.variants, spec.variants) and feq(replacement.external, spec.external) and feq(replacement.external_module, spec.external_module)): @@ -1116,7 +1223,7 @@ class Spec(object): changed = any(changes) force = True - for s in self.traverse(): + for s in self.traverse(deptype_query=alldeps): # After concretizing, assign namespaces to anything left. # Note that this doesn't count as a "change". The repository # configuration is constant throughout a spack run, and @@ -1146,7 +1253,7 @@ class Spec(object): Only for internal use -- client code should use "concretize" unless there is a need to force a spec to be concrete. """ - for s in self.traverse(): + for s in self.traverse(deptype_query=alldeps): s._normal = True s._concrete = True @@ -1159,6 +1266,13 @@ class Spec(object): return clone def flat_dependencies(self, **kwargs): + flat_deps = DependencyMap() + flat_deps_deptypes = self.flat_dependencies_with_deptype(**kwargs) + for name, depspec in flat_deps_deptypes.items(): + flat_deps[name] = depspec.spec + return flat_deps + + def flat_dependencies_with_deptype(self, **kwargs): """Return a DependencyMap containing all of this spec's dependencies with their constraints merged. @@ -1169,23 +1283,31 @@ class Spec(object): returns them. """ copy = kwargs.get('copy', True) + deptype_query = kwargs.get('deptype_query') flat_deps = DependencyMap() try: - for spec in self.traverse(root=False): + deptree = self.traverse_with_deptype(root=False, + deptype_query=deptype_query) + for depspec in deptree: + spec = depspec.spec + deptypes = depspec.deptypes + if spec.name not in flat_deps: if copy: - flat_deps[spec.name] = spec.copy(deps=False) + dep_spec = DependencySpec(spec.copy(deps=False), + deptypes) else: - flat_deps[spec.name] = spec + dep_spec = DependencySpec(spec, deptypes) + flat_deps[spec.name] = dep_spec else: - flat_deps[spec.name].constrain(spec) + flat_deps[spec.name].spec.constrain(spec) if not copy: - for dep in flat_deps.values(): - dep.dependencies.clear() - dep.dependents.clear() - self.dependencies.clear() + for depspec in flat_deps.values(): + depspec.spec._dependencies.clear() + depspec.spec._dependents.clear() + self._dependencies.clear() return flat_deps @@ -1200,17 +1322,11 @@ class Spec(object): """Return DependencyMap that points to all the dependencies in this spec.""" dm = DependencyMap() + # XXX(deptype): use a deptype kwarg. for spec in self.traverse(): dm[spec.name] = spec return dm - 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. """ - for dep in self.flat_dependencies(copy=False): - self._add_dependency(dep) - def _evaluate_dependency_conditions(self, name): """Evaluate all the conditions on a dependency with this name. @@ -1267,7 +1383,8 @@ class Spec(object): elif required: raise UnsatisfiableProviderSpecError(required[0], vdep) - def _merge_dependency(self, dep, visited, spec_deps, provider_index): + def _merge_dependency(self, dep, deptypes, visited, spec_deps, + provider_index): """Merge the dependency into this spec. This is the core of normalize(). There are some basic steps: @@ -1294,7 +1411,9 @@ class Spec(object): dep = provider else: index = ProviderIndex([dep], restrict=True) - for vspec in (v for v in spec_deps.values() if v.virtual): + for vspec in (v.spec + for v in spec_deps.values() + if v.spec.virtual): if index.providers_for(vspec): vspec._replace_with(dep) del spec_deps[vspec.name] @@ -1307,25 +1426,25 @@ class Spec(object): # If the spec isn't already in the set of dependencies, clone # it from the package description. if dep.name not in spec_deps: - spec_deps[dep.name] = dep.copy() + spec_deps[dep.name] = DependencySpec(dep.copy(), deptypes) changed = True # Constrain package information with spec info try: - changed |= spec_deps[dep.name].constrain(dep) + changed |= spec_deps[dep.name].spec.constrain(dep) except UnsatisfiableSpecError, 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) + e.message %= (spec_deps[dep.name].spec, dep.name, + e.constraint_type, e.required, e.provided) raise e # Add merged spec to my deps and recurse dependency = spec_deps[dep.name] - if dep.name not in self.dependencies: - self._add_dependency(dependency) + if dep.name not in self._dependencies: + self._add_dependency(dependency.spec, dependency.deptypes) - changed |= dependency._normalize_helper( + changed |= dependency.spec._normalize_helper( visited, spec_deps, provider_index) return changed @@ -1351,10 +1470,11 @@ class Spec(object): for dep_name in pkg.dependencies: # Do we depend on dep_name? If so pkg_dep is not None. pkg_dep = self._evaluate_dependency_conditions(dep_name) + deptypes = pkg._deptypes[dep_name] # If pkg_dep is a dependency, merge it. if pkg_dep: changed |= self._merge_dependency( - pkg_dep, visited, spec_deps, provider_index) + pkg_dep, deptypes, visited, spec_deps, provider_index) any_change |= changed return any_change @@ -1385,11 +1505,13 @@ class Spec(object): # Ensure first that all packages & compilers in the DAG exist. self.validate_names() # Get all the dependencies into one DependencyMap - spec_deps = self.flat_dependencies(copy=False) + spec_deps = self.flat_dependencies_with_deptype(copy=False, + deptype_query=alldeps) # Initialize index of virtual dependency providers if # concretize didn't pass us one already - provider_index = ProviderIndex(spec_deps.values(), restrict=True) + provider_index = ProviderIndex([s.spec for s in spec_deps.values()], + restrict=True) # traverse the package DAG and fill out dependencies according # to package files & their 'when' specs @@ -1510,7 +1632,7 @@ class Spec(object): """Apply constraints of other spec's dependencies to this spec.""" other = self._autospec(other) - if not self.dependencies or not other.dependencies: + if not self._dependencies or not other._dependencies: return False # TODO: might want more detail than this, e.g. specific deps @@ -1526,13 +1648,17 @@ class Spec(object): # Update with additional constraints from other spec for name in other.dep_difference(self): - self._add_dependency(other[name].copy()) + dep_spec_copy = other.get_dependency(name) + dep_copy = dep_spec_copy.spec + deptypes = dep_spec_copy.deptypes + self._add_dependency(dep_copy.copy(), deptypes) changed = True return changed def common_dependencies(self, other): """Return names of dependencies that self an other have in common.""" + # XXX(deptype): handle deptypes via deptype kwarg. common = set( s.name for s in self.traverse(root=False)) common.intersection_update( @@ -1657,13 +1783,14 @@ class Spec(object): other = self._autospec(other) if strict: - if other.dependencies and not self.dependencies: + if other._dependencies and not self._dependencies: return False - if not all(dep in self.dependencies for dep in other.dependencies): + if not all(dep in self._dependencies + for dep in other._dependencies): return False - elif not self.dependencies or not other.dependencies: + elif not self._dependencies or not other._dependencies: # if either spec doesn't restrict dependencies then both are # compatible. return True @@ -1726,8 +1853,8 @@ class Spec(object): self.architecture = other.architecture self.compiler = other.compiler.copy() if other.compiler else None if kwargs.get('cleardeps', True): - self.dependents = DependencyMap() - self.dependencies = DependencyMap() + self._dependents = DependencyMap() + self._dependencies = DependencyMap() self.compiler_flags = other.compiler_flags.copy() self.variants = other.variants.copy() self.variants.spec = self @@ -1739,15 +1866,30 @@ class Spec(object): # If we copy dependencies, preserve DAG structure in the new spec if kwargs.get('deps', True): # This copies the deps from other using _dup(deps=False) - new_nodes = other.flat_dependencies() + # XXX(deptype): We can keep different instances of specs here iff + # it is only a 'build' dependency (from its parent). + # All other instances must be shared (due to symbol + # and PATH contention). These should probably search + # for any existing installation which can satisfy the + # build and latch onto that because if 3 things need + # the same build dependency and it is *not* + # available, we only want to build it once. + new_nodes = other.flat_dependencies(deptype_query=alldeps) new_nodes[self.name] = self - # Hook everything up properly here by traversing. - for spec in other.traverse(cover='nodes'): - parent = new_nodes[spec.name] - for child in spec.dependencies: - if child not in parent.dependencies: - parent._add_dependency(new_nodes[child]) + stack = [other] + while stack: + cur_spec = stack.pop(0) + new_spec = new_nodes[cur_spec.name] + + for depspec in cur_spec._dependencies.values(): + stack.append(depspec.spec) + + # XXX(deptype): add any new deptypes that may have appeared + # here. + if depspec.spec.name not in new_spec._dependencies: + new_spec._add_dependency( + new_nodes[depspec.spec.name], depspec.deptypes) # Since we preserved structure, we can copy _normal safely. self._normal = other._normal @@ -1790,7 +1932,7 @@ class Spec(object): raise KeyError("No spec with name %s in %s" % (name, self)) def __contains__(self, spec): - """True if this spec satisfis the provided spec, or if any dependency + """True if this spec satisfies the provided spec, or if any dependency does. If the spec has no name, then we parse this one first. """ spec = self._autospec(spec) @@ -1814,13 +1956,13 @@ class Spec(object): if self.ne_node(other): return False - if len(self.dependencies) != len(other.dependencies): + if len(self._dependencies) != len(other._dependencies): return False - ssorted = [self.dependencies[name] - for name in sorted(self.dependencies)] - osorted = [other.dependencies[name] - for name in sorted(other.dependencies)] + ssorted = [self._dependencies[name].spec + for name in sorted(self._dependencies)] + osorted = [other._dependencies[name].spec + for name in sorted(other._dependencies)] for s, o in zip(ssorted, osorted): visited_s = id(s) in vs @@ -1874,9 +2016,10 @@ class Spec(object): 1. A tuple describing this node in the DAG. 2. The hash of each of this node's dependencies' cmp_keys. """ + dep_dict = self.dependencies_dict(deptype=('link', 'run')) return self._cmp_node() + ( - tuple(hash(self.dependencies[name]) - for name in sorted(self.dependencies)),) + tuple(hash(dep_dict[name]) + for name in sorted(dep_dict)),) def colorized(self): return colorize_spec(self) @@ -2081,8 +2224,8 @@ class Spec(object): self.architecture, other.architecture) #Dependency is not configurable - if self.dependencies != other.dependencies: - return -1 if self.dependencies < other.dependencies else 1 + if self._dependencies != other._dependencies: + return -1 if self._dependencies < other._dependencies else 1 #Equal specs return 0 @@ -2196,10 +2339,13 @@ class SpecParser(spack.parse.Parser): specs.append(self.spec(None)) self.previous = None if self.accept(HASH): - specs[-1]._add_dependency(self.spec_by_hash()) + dep = self.spec_by_hash() else: self.expect(ID) - specs[-1]._add_dependency(self.spec(self.token.value)) + dep = self.spec(self.token.value) + # XXX(deptype): default deptypes + def_deptypes = ('build', 'link') + specs[-1]._add_dependency(dep, def_deptypes) else: # Attempt to construct an anonymous spec, but check that @@ -2263,8 +2409,8 @@ class SpecParser(spack.parse.Parser): spec.external = None spec.external_module = None spec.compiler_flags = FlagMap(spec) - spec.dependents = DependencyMap() - spec.dependencies = DependencyMap() + spec._dependents = DependencyMap() + spec._dependencies = DependencyMap() spec.namespace = spec_namespace spec._hash = None |