From d0b179962b87470edbaf4e05e41e748bebe27a3d Mon Sep 17 00:00:00 2001
From: Todd Gamblin <tgamblin@llnl.gov>
Date: Mon, 4 Aug 2014 07:40:53 -0700
Subject: [PATCH] find and uninstall work when installed package is no longer
 in spack.

- Make switching between git branches easier.
- Make future removal of packages easier.
---
 lib/spack/spack/cmd/uninstall.py    | 11 ++++++-
 lib/spack/spack/directory_layout.py |  3 +-
 lib/spack/spack/package.py          |  9 +++---
 lib/spack/spack/spec.py             | 45 ++++++++++++++++++++++++-----
 4 files changed, 54 insertions(+), 14 deletions(-)

diff --git a/lib/spack/spack/cmd/uninstall.py b/lib/spack/spack/cmd/uninstall.py
index 28a2f659ae..73c98a203b 100644
--- a/lib/spack/spack/cmd/uninstall.py
+++ b/lib/spack/spack/cmd/uninstall.py
@@ -28,6 +28,7 @@
 
 import spack
 import spack.cmd
+import spack.packages
 
 description="Remove an installed package"
 
@@ -68,7 +69,15 @@ def uninstall(parser, args):
         if len(matching_specs) == 0:
             tty.die("%s does not match any installed packages." % spec)
 
-        pkgs.extend(spack.db.get(s) for s in matching_specs)
+        for s in matching_specs:
+            try:
+                # should work if package is known to spack
+                pkgs.append(spack.db.get(s))
+
+            except spack.packages.UnknownPackageError, e:
+                # The package.py file has gone away -- but still want to uninstall.
+                spack.Package(s).do_uninstall(force=True)
+
 
     # Sort packages to be uninstalled by the number of installed dependents
     # This ensures we do things in the right order
diff --git a/lib/spack/spack/directory_layout.py b/lib/spack/spack/directory_layout.py
index 28719521d2..4fc00d536e 100644
--- a/lib/spack/spack/directory_layout.py
+++ b/lib/spack/spack/directory_layout.py
@@ -155,7 +155,8 @@ def read_spec(self, path):
         """Read the contents of a file and parse them as a spec"""
         with closing(open(path)) as spec_file:
             string = spec_file.read().replace('\n', '')
-            return Spec(string)
+            # Specs from files are assumed normal and concrete
+            return Spec(string, concrete=True)
 
 
     def make_path_for_spec(self, spec):
diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py
index 90e77b5e82..8df658e660 100644
--- a/lib/spack/spack/package.py
+++ b/lib/spack/spack/package.py
@@ -322,9 +322,6 @@ class SomePackage(Package):
 
 
     def __init__(self, spec):
-        # These attributes are required for all packages.
-        attr_required(self.__class__, 'homepage')
-
         # this determines how the package should be built.
         self.spec = spec
 
@@ -396,9 +393,13 @@ def stage(self):
             raise ValueError("Can only get a stage for a concrete package.")
 
         if self._stage is None:
+            if not self.url:
+                raise PackageVersionError(self.version)
+
             # TODO: move this logic into a mirror module.
             mirror_path = "%s/%s" % (self.name, "%s-%s.%s" % (
                 self.name, self.version, extension(self.url)))
+
             self._stage = Stage(
                 self.url, mirror_path=mirror_path, name=self.spec.short_spec)
         return self._stage
@@ -531,8 +532,6 @@ def nearest_url(version):
                     break
                 if self.versions[v].url:
                     url = self.versions[v].url
-            if not url:
-                raise PackageVersionError(v)
             return url
 
         if version in self.versions:
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
index 1e2da10dcc..45c3402617 100644
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -345,6 +345,17 @@ def __init__(self, spec_like, *dep_like, **kwargs):
         self.compiler = other.compiler
         self.dependencies = other.dependencies
 
+        # 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)
+
+        # Specs cannot be concrete and non-normal.
+        if self._concrete:
+            self._normal = True
+
         # 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.
@@ -432,11 +443,15 @@ def concrete(self):
            If any of the name, version, architecture, compiler, or depdenencies
            are ambiguous,then it is not concrete.
         """
-        return bool(not self.virtual
-                    and self.versions.concrete
-                    and self.architecture
-                    and self.compiler and self.compiler.concrete
-                    and self.dependencies.concrete)
+        if self._concrete:
+            return True
+
+        self._concrete = bool(not self.virtual
+                              and self.versions.concrete
+                              and self.architecture
+                              and self.compiler and self.compiler.concrete
+                              and self.dependencies.concrete)
+        return self._concrete
 
 
     def preorder_traversal(self, visited=None, d=0, **kwargs):
@@ -606,7 +621,7 @@ def _expand_virtual_packages(self):
 
             # If there are duplicate providers or duplicate provider deps, this
             # consolidates them and merges constraints.
-            self.normalize()
+            self.normalize(force=True)
 
 
     def concretize(self):
@@ -621,9 +636,13 @@ def concretize(self):
            with requirements of its pacakges.  See flatten() and normalize() for
            more details on this.
         """
+        if self._concrete:
+            return
+
         self.normalize()
         self._expand_virtual_packages()
         self._concretize_helper()
+        self._concrete = True
 
 
     def concretized(self):
@@ -754,7 +773,7 @@ def _normalize_helper(self, visited, spec_deps, provider_index):
             dependency._normalize_helper(visited, spec_deps, provider_index)
 
 
-    def normalize(self):
+    def normalize(self, **kwargs):
         """When specs are parsed, any dependencies specified are hanging off
            the root, and ONLY the ones that were explicitly provided are there.
            Normalization turns a partial flat spec into a DAG, where:
@@ -772,6 +791,9 @@ def normalize(self):
            TODO: normalize should probably implement some form of cycle detection,
            to ensure that the spec is actually a DAG.
         """
+        if self._normal and not kwargs.get('force', False):
+            return
+
         # Ensure first that all packages & compilers in the DAG exist.
         self.validate_names()
 
@@ -805,6 +827,9 @@ def normalize(self):
             raise InvalidDependencyException(
                 self.name + " does not depend on " + comma_or(extra))
 
+        # Mark the spec as normal once done.
+        self._normal = True
+
 
     def normalized(self):
         """Return a normalized copy of this spec without modifying this spec."""
@@ -1009,6 +1034,9 @@ def _dup(self, other, **kwargs):
         else:
             self.dependencies = DependencyMap()
 
+        self._normal = other._normal
+        self._concrete = other._concrete
+
 
     def copy(self, **kwargs):
         """Return a copy of this spec.
@@ -1261,6 +1289,9 @@ def spec(self):
         spec.dependents   = DependencyMap()
         spec.dependencies = DependencyMap()
 
+        spec._normal = False
+        spec._concrete = False
+
         # record this so that we know whether version is
         # unspecified or not.
         added_version = False
-- 
GitLab