From a2b4146e104fc00442b847306df2240d2bb6c0b6 Mon Sep 17 00:00:00 2001 From: Todd Gamblin Date: Mon, 19 Dec 2016 09:09:53 -0800 Subject: [PATCH] Fixes for various hash issues (#2626) * Better output for disambiguate_specs() * Fix wrong exception name. * Fix satsifies(): concrete specs require matching by hash. - Fixes uninstall by hash and other places where we need to match a specific spec. - Fix an error in provider_index (satisfies() call was backwards) - Fix an error in satisfies_dependencies(): checks were too shallow. * Fix default args in Spec.tree() * Move installed_dependents() to DB to avoid unknown package error. * Make `spack find` and `sapck.store.db.query()` faster for hashes. * Add a test to ensure satisfies() respects concrete Specs' hashes. --- lib/spack/spack/cmd/__init__.py | 4 ++- lib/spack/spack/cmd/common/arguments.py | 18 +++++++++---- lib/spack/spack/cmd/dependents.py | 14 ++++++---- lib/spack/spack/cmd/find.py | 3 +++ lib/spack/spack/cmd/uninstall.py | 8 ++++-- lib/spack/spack/database.py | 21 +++++++++++++++ lib/spack/spack/package.py | 20 +------------- lib/spack/spack/provider_index.py | 3 ++- lib/spack/spack/spec.py | 35 +++++++------------------ lib/spack/spack/test/spec_semantics.py | 15 +++++++++++ 10 files changed, 83 insertions(+), 58 deletions(-) diff --git a/lib/spack/spack/cmd/__init__.py b/lib/spack/spack/cmd/__init__.py index 8000c9f1f2..e712ba8e1d 100644 --- a/lib/spack/spack/cmd/__init__.py +++ b/lib/spack/spack/cmd/__init__.py @@ -144,7 +144,9 @@ def disambiguate_spec(spec): elif len(matching_specs) > 1: args = ["%s matches multiple packages." % spec, "Matching packages:"] - args += [" " + str(s) for s in matching_specs] + color = sys.stdout.isatty() + args += [colorize(" @K{%s} " % s.dag_hash(7), color=color) + + s.format('$_$@$%@$=', color=color) for s in matching_specs] args += ["Use a more specific spec."] tty.die(*args) diff --git a/lib/spack/spack/cmd/common/arguments.py b/lib/spack/spack/cmd/common/arguments.py index 1839132218..53e75a4f2c 100644 --- a/lib/spack/spack/cmd/common/arguments.py +++ b/lib/spack/spack/cmd/common/arguments.py @@ -25,6 +25,7 @@ import argparse +import spack.cmd import spack.store import spack.modules from spack.util.pattern import Args @@ -59,11 +60,18 @@ def __call__(self, parser, namespace, values, option_string=None): namespace.specs = self._specs def _specs(self, **kwargs): - specs = [s for s in spack.store.db.query(**kwargs)] - values = ' '.join(self.values) - if values: - specs = [x for x in specs if x.satisfies(values, strict=True)] - return specs + qspecs = spack.cmd.parse_specs(self.values) + + # return everything for an empty query. + if not qspecs: + return spack.store.db.query() + + # Return only matching stuff otherwise. + specs = set() + for spec in qspecs: + for s in spack.store.db.query(spec, **kwargs): + specs.add(s) + return sorted(specs) _arguments['constraint'] = Args( diff --git a/lib/spack/spack/cmd/dependents.py b/lib/spack/spack/cmd/dependents.py index 7729105e62..dc2ee658ac 100644 --- a/lib/spack/spack/cmd/dependents.py +++ b/lib/spack/spack/cmd/dependents.py @@ -27,6 +27,7 @@ import llnl.util.tty as tty import spack +import spack.store import spack.cmd description = "Show installed packages that depend on another." @@ -39,11 +40,14 @@ def setup_parser(subparser): def dependents(parser, args): - specs = spack.cmd.parse_specs(args.spec, concretize=True) + specs = spack.cmd.parse_specs(args.spec) if len(specs) != 1: tty.die("spack dependents takes only one spec.") + spec = spack.cmd.disambiguate_spec(specs[0]) - fmt = '$_$@$%@$+$=$#' - deps = [d.format(fmt, color=True) - for d in specs[0].package.installed_dependents] - tty.msg("Dependents of %s" % specs[0].format(fmt, color=True), *deps) + tty.msg("Dependents of %s" % spec.format('$_$@$%@$#', color=True)) + deps = spack.store.db.installed_dependents(spec) + if deps: + spack.cmd.display_specs(deps) + else: + print "No dependents" diff --git a/lib/spack/spack/cmd/find.py b/lib/spack/spack/cmd/find.py index 9f1759afc0..ab0b29c30e 100644 --- a/lib/spack/spack/cmd/find.py +++ b/lib/spack/spack/cmd/find.py @@ -114,14 +114,17 @@ def query_arguments(args): def find(parser, args): q_args = query_arguments(args) query_specs = args.specs(**q_args) + # Exit early if no package matches the constraint if not query_specs and args.constraint: msg = "No package matches the query: {0}".format(args.constraint) tty.msg(msg) return + # Display the result if sys.stdout.isatty(): tty.msg("%d installed packages." % len(query_specs)) + display_specs(query_specs, mode=args.mode, long=args.long, diff --git a/lib/spack/spack/cmd/uninstall.py b/lib/spack/spack/cmd/uninstall.py index 2f7e15ba55..0fc22ce538 100644 --- a/lib/spack/spack/cmd/uninstall.py +++ b/lib/spack/spack/cmd/uninstall.py @@ -124,7 +124,8 @@ def installed_dependents(specs): """ dependents = {} for item in specs: - lst = [x for x in item.package.installed_dependents if x not in specs] + lst = [x for x in spack.store.db.installed_dependents(item) + if x not in specs] if lst: lst = list(set(lst)) dependents[item] = lst @@ -152,7 +153,7 @@ def do_uninstall(specs, force): # Sort packages to be uninstalled by the number of installed dependents # This ensures we do things in the right order def num_installed_deps(pkg): - return len(pkg.installed_dependents) + return len(spack.store.db.installed_dependents(pkg.spec)) packages.sort(key=num_installed_deps) for item in packages: @@ -163,11 +164,14 @@ def get_uninstall_list(args): specs = [any] if args.packages: specs = spack.cmd.parse_specs(args.packages) + # Gets the list of installed specs that match the ones give via cli # takes care of '-a' is given in the cli uninstall_list = concretize_specs(specs, args.all, args.force) + # Takes care of '-d' dependent_list = installed_dependents(uninstall_list) + # Process dependent_list and update uninstall_list has_error = False if dependent_list and not args.dependents and not args.force: diff --git a/lib/spack/spack/database.py b/lib/spack/spack/database.py index a01795ca0f..3a0c028d5b 100644 --- a/lib/spack/spack/database.py +++ b/lib/spack/spack/database.py @@ -604,6 +604,15 @@ def remove(self, spec): with self.write_transaction(): return self._remove(spec) + @_autospec + def installed_dependents(self, spec): + """List the installed specs that depend on this one.""" + dependents = set() + for spec in self.query(spec): + for dependent in spec.traverse(direction='parents', root=False): + dependents.add(dependent) + return dependents + @_autospec def installed_extensions_for(self, extendee_spec): """ @@ -655,6 +664,18 @@ def query(self, query_spec=any, known=any, installed=True, explicit=any): """ with self.read_transaction(): + # Just look up concrete specs with hashes; no fancy search. + if (isinstance(query_spec, spack.spec.Spec) and + query_spec._concrete): + + hash_key = query_spec.dag_hash() + if hash_key in self._data: + return [self._data[hash_key].spec] + else: + return [] + + # Abstract specs require more work -- currently we test + # against everything. results = [] for key, rec in self._data.items(): if installed is not any and rec.installed != installed: diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py index 3502eb2931..0c924cfcbc 100644 --- a/lib/spack/spack/package.py +++ b/lib/spack/spack/package.py @@ -855,24 +855,6 @@ def provides(self, vpkg_name): def installed(self): return os.path.isdir(self.prefix) - @property - def installed_dependents(self): - """Return a list of the specs of all installed packages that depend - on this one. - - TODO: move this method to database.py? - """ - dependents = [] - for spec in spack.store.db.query(): - if self.name == spec.name: - continue - # XXX(deptype): Should build dependencies not count here? - # for dep in spec.traverse(deptype=('run')): - for dep in spec.traverse(deptype=spack.alldeps): - if self.spec == dep: - dependents.append(spec) - return dependents - @property def prefix_lock(self): """Prefix lock is a byte range lock on the nth byte of a file. @@ -1528,7 +1510,7 @@ def do_uninstall(self, force=False): raise InstallError(str(self.spec) + " is not installed.") if not force: - dependents = self.installed_dependents + dependents = spack.store.db.installed_dependents(self.spec) if dependents: raise PackageStillNeededError(self.spec, dependents) diff --git a/lib/spack/spack/provider_index.py b/lib/spack/spack/provider_index.py index 6bcf98009e..3911947e4a 100644 --- a/lib/spack/spack/provider_index.py +++ b/lib/spack/spack/provider_index.py @@ -100,7 +100,8 @@ def update(self, spec): for provided_spec, provider_spec in pkg.provided.iteritems(): # We want satisfaction other than flags provider_spec.compiler_flags = spec.compiler_flags.copy() - if provider_spec.satisfies(spec, deps=False): + + if spec.satisfies(provider_spec, deps=False): provided_name = provided_spec.name provider_map = self.providers.setdefault(provided_name, {}) diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py index 238eb07e1f..a8377e324f 100644 --- a/lib/spack/spack/spec.py +++ b/lib/spack/spack/spec.py @@ -818,7 +818,7 @@ def get_dependency(self, name): dep = self._dependencies.get(name) if dep is not None: return dep - raise InvalidDependencyException( + raise InvalidDependencyError( self.name + " does not depend on " + comma_or(name)) def _find_deps(self, where, deptype): @@ -1395,26 +1395,6 @@ def _replace_with(self, concrete): dependent._add_dependency(concrete, deptypes, dep_spec.default_deptypes) - def _replace_node(self, replacement): - """Replace this spec with another. - - Connects all dependents of this spec to its replacement, and - disconnects this spec from any dependencies it has. New spec - will have any dependencies the replacement had, and may need - to be normalized. - - """ - 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, dep_spec.default_deptypes) - - 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, and normalize again to include the provider's (potentially virtual) @@ -2043,6 +2023,10 @@ def satisfies(self, other, deps=True, strict=False): """ other = self._autospec(other) + # The only way to satisfy a concrete spec is to match its hash exactly. + if other._concrete: + return self._concrete and self.dag_hash() == other.dag_hash() + # A concrete provider can satisfy a virtual dependency. if not self.virtual and other.virtual: pkg = spack.repo.get(self.fullname) @@ -2113,8 +2097,9 @@ def satisfies_dependencies(self, other, strict=False): if other._dependencies and not self._dependencies: return False - if not all(dep in self._dependencies - for dep in other._dependencies): + alldeps = set(d.name for d in self.traverse(root=False)) + if not all(dep.name in alldeps + for dep in other.traverse(root=False)): return False elif not self._dependencies or not other._dependencies: @@ -2620,9 +2605,9 @@ def tree(self, **kwargs): with indentation.""" color = kwargs.pop('color', False) depth = kwargs.pop('depth', False) - hashes = kwargs.pop('hashes', True) + hashes = kwargs.pop('hashes', False) hlen = kwargs.pop('hashlen', None) - install_status = kwargs.pop('install_status', True) + install_status = kwargs.pop('install_status', False) cover = kwargs.pop('cover', 'nodes') indent = kwargs.pop('indent', 0) fmt = kwargs.pop('format', '$_$@$%@+$+$=') diff --git a/lib/spack/spack/test/spec_semantics.py b/lib/spack/spack/test/spec_semantics.py index c165934948..16d6121dea 100644 --- a/lib/spack/spack/test/spec_semantics.py +++ b/lib/spack/spack/test/spec_semantics.py @@ -312,6 +312,21 @@ def test_satisfies_virtual_dep_with_virtual_constraint(self): Spec('netlib-lapack ^netlib-blas').satisfies( 'netlib-lapack ^netlib-blas')) + def test_satisfies_same_spec_with_different_hash(self): + """Ensure that concrete specs are matched *exactly* by hash.""" + s1 = Spec('mpileaks').concretized() + s2 = s1.copy() + + self.assertTrue(s1.satisfies(s2)) + self.assertTrue(s2.satisfies(s1)) + + # Simulate specs that were installed before and after a change to + # Spack's hashing algorithm. This just reverses s2's hash. + s2._hash = s1.dag_hash()[-1::-1] + + self.assertFalse(s1.satisfies(s2)) + self.assertFalse(s2.satisfies(s1)) + # ======================================================================== # Indexing specs # ========================================================================