Repository: spack/spack

Issue:
Add label in concretizer for external packages 
I'd like ability to see external packages in the concretized output so its easy to know what packages are external and which are not

### Rationale

This is used to identify package preference and help pinpoint before doing `spack install` what are the issues.

### Description

In this concretized output, I have specified `m4` as an external. The symbol on left `-` is not clear if its an external or installed from source. The `[+]` is clear that package is installed.

```
[+]  yxi7z3o                  ^libtool@2.4.6%gcc@10.2.0 arch=cray-sles15-zen2
 -   wlj6ezo                      ^m4@1.4.18%gcc@10.2.0+sigsegv patches=3877ab548f88597ab2327a2230ee048d2d07ace1062efe81fc92e91b7f39cd00,fc9b61654a3ba1a8d6cd78ce087e7c96366c290bc8d2c299f09828d793b853c8 arch=cray-sles15-zen2
```
I would propose we have a `[E]` for external package. We can potentially extend this to other names during concretizer for labels to help see concretized output

This also relates to https://github.com/spack/spack/issues/18588 when trying to analyze output of `spack concretize` to make it easy to digest the information considering when we concretize a huge software stack. These changes to concretized output will be appreciated.

This was discussed in https://spackpm.slack.com/archives/C5W7NKZJT/p1625194987305100


Discussion:
hi @becker33 can we consider having a `[E]` option or `[e]` to show external packages in concretized output. See https://spackpm.slack.com/archives/C5W7NKZJT/p1625232749310400?thread_ts=1625197335.306700&cid=C5W7NKZJT for our conversation, perhaps some others may have idea.

Resolving patch:
diff --git a/lib/spack/spack/relocate.py b/lib/spack/spack/relocate.py
--- a/lib/spack/spack/relocate.py
+++ b/lib/spack/spack/relocate.py
@@ -676,7 +676,7 @@ def is_relocatable(spec):
     Raises:
         ValueError: if the spec is not installed
     """
-    if not spec.install_status():
+    if not spec.installed:
         raise ValueError("spec is not installed [{0}]".format(str(spec)))
 
     if spec.external or spec.virtual:
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -50,6 +50,7 @@
 """
 import collections
 import collections.abc
+import enum
 import io
 import itertools
 import os
@@ -173,6 +174,16 @@
 SPECFILE_FORMAT_VERSION = 3
 
 
+# InstallStatus is used to map install statuses to symbols for display
+# Options are artificially disjoint for dispay purposes
+class InstallStatus(enum.Enum):
+    installed = "@g{[+]}  "
+    upstream = "@g{[^]}  "
+    external = "@g{[e]}  "
+    absent = "@K{ - }  "
+    missing = "@r{[-]}  "
+
+
 def colorize_spec(spec):
     """Returns a spec colorized according to the colors specified in
     color_formats."""
@@ -4401,12 +4412,20 @@ def __str__(self):
     def install_status(self):
         """Helper for tree to print DB install status."""
         if not self.concrete:
-            return None
-        try:
-            record = spack.store.db.get_record(self)
-            return record.installed
-        except KeyError:
-            return None
+            return InstallStatus.absent
+
+        if self.external:
+            return InstallStatus.external
+
+        upstream, record = spack.store.db.query_by_spec_hash(self.dag_hash())
+        if not record:
+            return InstallStatus.absent
+        elif upstream and record.installed:
+            return InstallStatus.upstream
+        elif record.installed:
+            return InstallStatus.installed
+        else:
+            return InstallStatus.missing
 
     def _installed_explicitly(self):
         """Helper for tree to print DB install status."""
@@ -4420,7 +4439,10 @@ def _installed_explicitly(self):
 
     def tree(self, **kwargs):
         """Prints out this spec and its dependencies, tree-formatted
-        with indentation."""
+        with indentation.
+
+        Status function may either output a boolean or an InstallStatus
+        """
         color = kwargs.pop("color", clr.get_color_when())
         depth = kwargs.pop("depth", False)
         hashes = kwargs.pop("hashes", False)
@@ -4452,14 +4474,12 @@ def tree(self, **kwargs):
 
             if status_fn:
                 status = status_fn(node)
-                if node.installed_upstream:
-                    out += clr.colorize("@g{[^]}  ", color=color)
-                elif status is None:
-                    out += clr.colorize("@K{ - }  ", color=color)  # !installed
+                if status in list(InstallStatus):
+                    out += clr.colorize(status.value, color=color)
                 elif status:
-                    out += clr.colorize("@g{[+]}  ", color=color)  # installed
+                    out += clr.colorize("@g{[+]}  ", color=color)
                 else:
-                    out += clr.colorize("@r{[-]}  ", color=color)  # missing
+                    out += clr.colorize("@r{[-]}  ", color=color)
 
             if hashes:
                 out += clr.colorize("@K{%s}  ", color=color) % node.dag_hash(hlen)


Test patch: