Repository: spack/spack

Issue:
Print or log a warning when downgrading targets
As a user I would like a warning to be either printed to screen or logged somewhere in debug mode when targets are downgraded by Spack so that I can spot deviations from the preferences expressed in e.g. configuration files more easily

### Rationale

Following a discussion happened on public Spack's slack in the #general channel. A `spack.yaml` file like the following:
```yaml
spack:
  specs:
  - mvapich2 fabrics=mrail
  packages:
    openssl:
      buildable: false
      paths:
        openssl@1.0.2: /usr
    all:
      target: [skylake-avx512]
  concretization: together
```
was causing all the specs to concretize as `x86_64`. This is because there's a typo in the preferred target that contains a dash where it should have an underscore i.e. `skylake_avx512`. 

What was happening is that Spack was falling back to a safe target after having tried to use a "non-existing" preference. Being a typo the error was hard to spot and probably it would have been much easier to debug if Spack prompted a visible warning about its decision to downgrade the target.

### Description

Make it such that Spack prompts a warning when it decides to downgrade targets. The warning should be as informative as possible on where and why this decision was taken.


### Additional information
No additional information


Discussion:

Resolving patch:
diff --git a/lib/spack/spack/concretize.py b/lib/spack/spack/concretize.py
--- a/lib/spack/spack/concretize.py
+++ b/lib/spack/spack/concretize.py
@@ -281,25 +281,7 @@ def concretize_architecture(self, spec):
             else:
                 # To get default platform, consider package prefs
                 if PackagePrefs.has_preferred_targets(spec.name):
-                    target_prefs = PackagePrefs(spec.name, 'target')
-                    target_specs = [spack.spec.Spec('target=%s' % tname)
-                                    for tname in cpu.targets]
-
-                    def tspec_filter(s):
-                        # Filter target specs by whether the architecture
-                        # family is the current machine type. This ensures
-                        # we only consider x86_64 targets when on an
-                        # x86_64 machine, etc. This may need to change to
-                        # enable setting cross compiling as a default
-                        target = cpu.targets[str(s.architecture.target)]
-                        arch_family_name = target.family.name
-                        return arch_family_name == platform.machine()
-
-                    # Sort filtered targets by package prefs
-                    target_specs = list(filter(tspec_filter, target_specs))
-                    target_specs.sort(key=target_prefs)
-
-                    new_target = target_specs[0].architecture.target
+                    new_target = self.target_from_package_preferences(spec)
                 else:
                     new_target = new_plat.target('default_target')
 
@@ -310,6 +292,33 @@ def tspec_filter(s):
         spec.architecture = new_arch
         return spec_changed
 
+    def target_from_package_preferences(self, spec):
+        """Returns the preferred target from the package preferences if
+        there's any.
+
+        Args:
+            spec: abstract spec to be concretized
+        """
+        target_prefs = PackagePrefs(spec.name, 'target')
+        target_specs = [spack.spec.Spec('target=%s' % tname)
+                        for tname in cpu.targets]
+
+        def tspec_filter(s):
+            # Filter target specs by whether the architecture
+            # family is the current machine type. This ensures
+            # we only consider x86_64 targets when on an
+            # x86_64 machine, etc. This may need to change to
+            # enable setting cross compiling as a default
+            target = cpu.targets[str(s.architecture.target)]
+            arch_family_name = target.family.name
+            return arch_family_name == platform.machine()
+
+        # Sort filtered targets by package prefs
+        target_specs = list(filter(tspec_filter, target_specs))
+        target_specs.sort(key=target_prefs)
+        new_target = target_specs[0].architecture.target
+        return new_target
+
     def concretize_variants(self, spec):
         """If the spec already has variants filled in, return.  Otherwise, add
            the user preferences from packages.yaml or the default variants from
@@ -526,7 +535,12 @@ def _adjust_target(self, spec):
         current_platform = spack.architecture.get_platform(
             spec.architecture.platform
         )
-        if current_target != current_platform.target('default_target') or \
+
+        default_target = current_platform.target('default_target')
+        if PackagePrefs.has_preferred_targets(spec.name):
+            default_target = self.target_from_package_preferences(spec)
+
+        if current_target != default_target or \
             (self.abstract_spec.architecture is not None and
              self.abstract_spec.architecture.target is not None):
             return False
@@ -544,6 +558,11 @@ def _adjust_target(self, spec):
                     continue
 
                 if candidate is not None:
+                    msg = ('{0.name}@{0.version} cannot build optimized '
+                           'binaries for "{1}". Using best target possible: '
+                           '"{2}"')
+                    msg = msg.format(spec.compiler, current_target, candidate)
+                    tty.warn(msg)
                     spec.architecture.target = candidate
                     return True
             else:


Test patch:
diff --git a/lib/spack/spack/test/concretize.py b/lib/spack/spack/test/concretize.py
--- a/lib/spack/spack/test/concretize.py
+++ b/lib/spack/spack/test/concretize.py
@@ -11,6 +11,7 @@
 import spack.repo
 
 from spack.concretize import find_spec, NoValidVersionError
+from spack.package_prefs import PackagePrefs
 from spack.spec import Spec, CompilerSpec
 from spack.spec import ConflictsInSpecError, SpecError
 from spack.version import ver
@@ -83,13 +84,25 @@ def spec(request):
 
 
 @pytest.fixture(params=[
-    'haswell', 'broadwell', 'skylake', 'icelake'
+    # Mocking the host detection
+    'haswell', 'broadwell', 'skylake', 'icelake',
+    # Using preferred targets from packages.yaml
+    'icelake-preference', 'cannonlake-preference'
 ])
 def current_host(request, monkeypatch):
-    target = llnl.util.cpu.targets[request.param]
-    monkeypatch.setattr(llnl.util.cpu, 'host', lambda: target)
-    monkeypatch.setattr(spack.platforms.test.Test, 'default', request.param)
-    return target
+    # is_preference is not empty if we want to supply the
+    # preferred target via packages.yaml
+    cpu, _, is_preference = request.param.partition('-')
+    target = llnl.util.cpu.targets[cpu]
+    if not is_preference:
+        monkeypatch.setattr(llnl.util.cpu, 'host', lambda: target)
+        monkeypatch.setattr(spack.platforms.test.Test, 'default', cpu)
+        yield target
+    else:
+        # There's a cache that needs to be cleared for unit tests
+        PackagePrefs._packages_config_cache = None
+        with spack.config.override('packages:all', {'target': [cpu]}):
+            yield target
 
 
 @pytest.mark.usefixtures('config', 'mock_packages')