Repository: spack/spack

Issue:
spack does not report error when compiler is missing
I went to clean up my aging installs, and deleted all the files.  I did not
update `~/.spack/linux/compilers.yaml`, so it still pointed to  several compilers that
no longer existed.

I went to do various installs, and they all failed with errors that did not indicate
the problem was with the compiler.  I think spack should report an error like:

```
    gcc@7.3.0 specified in ~/.spack/linux/compilers.yaml does not exist
```


Discussion:
Thanks @martyitz!
It would be pretty easy to do a quick check before we start the build to see whether the chosen compiler still exists.  I would rather not do a check on all compilers at startup, as it'll increase our already slow (due to Python modules) startup time.
I agree.  No need to check the compilers that are not about to be used.

> On Sep 25, 2019, at 9:48 AM, Todd Gamblin <notifications@github.com> wrote:
> 
> It would be pretty easy to do a quick check before we start the build to see whether the chosen compiler still exists. I would rather not do a check on all compilers at startup, as it'll increase our already slow (due to Python modules) startup time
> 
> 


There is actually code for this but it is not working, see #9845 (probably needs a rebase and some smaller changes).

Resolving patch:
diff --git a/lib/spack/spack/build_environment.py b/lib/spack/spack/build_environment.py
--- a/lib/spack/spack/build_environment.py
+++ b/lib/spack/spack/build_environment.py
@@ -198,6 +198,9 @@ def set_compiler_environment_variables(pkg, env):
     compiler = pkg.compiler
     spec = pkg.spec
 
+    # Make sure the executables for this compiler exist
+    compiler.verify_executables()
+
     # Set compiler variables used by CMake and autotools
     assert all(key in compiler.link_paths for key in (
         'cc', 'cxx', 'f77', 'fc'))
diff --git a/lib/spack/spack/compiler.py b/lib/spack/spack/compiler.py
--- a/lib/spack/spack/compiler.py
+++ b/lib/spack/spack/compiler.py
@@ -27,12 +27,6 @@
 __all__ = ['Compiler']
 
 
-def _verify_executables(*paths):
-    for path in paths:
-        if not os.path.isfile(path) and os.access(path, os.X_OK):
-            raise CompilerAccessError(path)
-
-
 @llnl.util.lang.memoized
 def get_compiler_version_output(compiler_path, version_arg, ignore_errors=()):
     """Invokes the compiler at a given path passing a single
@@ -271,20 +265,16 @@ def __init__(self, cspec, operating_system, target,
         self.extra_rpaths = extra_rpaths
         self.enable_implicit_rpaths = enable_implicit_rpaths
 
-        def check(exe):
-            if exe is None:
-                return None
-            _verify_executables(exe)
-            return exe
-
-        self.cc  = check(paths[0])
-        self.cxx = check(paths[1])
+        self.cc  = paths[0]
+        self.cxx = paths[1]
+        self.f77 = None
+        self.fc = None
         if len(paths) > 2:
-            self.f77 = check(paths[2])
+            self.f77 = paths[2]
             if len(paths) == 3:
                 self.fc = self.f77
             else:
-                self.fc  = check(paths[3])
+                self.fc  = paths[3]
 
         self.environment = environment
         self.extra_rpaths = extra_rpaths or []
@@ -298,6 +288,21 @@ def check(exe):
             if value is not None:
                 self.flags[flag] = tokenize_flags(value)
 
+    def verify_executables(self):
+        """Raise an error if any of the compiler executables is not valid.
+
+        This method confirms that for all of the compilers (cc, cxx, f77, fc)
+        that have paths, those paths exist and are executable by the current
+        user.
+        Raises a CompilerAccessError if any of the non-null paths for the
+        compiler are not accessible.
+        """
+        missing = [cmp for cmp in (self.cc, self.cxx, self.f77, self.fc)
+                   if cmp and not (os.path.isfile(cmp) and
+                                   os.access(cmp, os.X_OK))]
+        if missing:
+            raise CompilerAccessError(self, missing)
+
     @property
     def version(self):
         return self.spec.version
@@ -575,10 +580,10 @@ def _compiler_environment(self):
 
 
 class CompilerAccessError(spack.error.SpackError):
-
-    def __init__(self, path):
-        super(CompilerAccessError, self).__init__(
-            "'%s' is not a valid compiler." % path)
+    def __init__(self, compiler, paths):
+        msg = "Compiler '%s' has executables that are missing" % compiler.spec
+        msg += " or are not executable: %s" % paths
+        super(CompilerAccessError, self).__init__(msg)
 
 
 class InvalidCompilerError(spack.error.SpackError):


Test patch:
diff --git a/lib/spack/spack/test/compilers/basics.py b/lib/spack/spack/test/compilers/basics.py
--- a/lib/spack/spack/test/compilers/basics.py
+++ b/lib/spack/spack/test/compilers/basics.py
@@ -815,3 +815,33 @@ class MockPackage(object):
     pkg = MockPackage()
     with pytest.raises(OSError):
         compiler.setup_custom_environment(pkg, env)
+
+
+@pytest.mark.enable_compiler_verification
+def test_compiler_executable_verification_raises(tmpdir):
+    compiler = MockCompiler()
+    compiler.cc = '/this/path/does/not/exist'
+
+    with pytest.raises(spack.compiler.CompilerAccessError):
+        compiler.verify_executables()
+
+
+@pytest.mark.enable_compiler_verification
+def test_compiler_executable_verification_success(tmpdir):
+    def prepare_executable(name):
+        real = str(tmpdir.join('cc').ensure())
+        fs.set_executable(real)
+        setattr(compiler, name, real)
+
+    # setup mock compiler with real paths
+    compiler = MockCompiler()
+    for name in ('cc', 'cxx', 'f77', 'fc'):
+        prepare_executable(name)
+
+    # testing that this doesn't raise an error because the paths exist and
+    # are executable
+    compiler.verify_executables()
+
+    # Test that null entries don't fail
+    compiler.cc = None
+    compiler.verify_executables()
diff --git a/lib/spack/spack/test/conftest.py b/lib/spack/spack/test/conftest.py
--- a/lib/spack/spack/test/conftest.py
+++ b/lib/spack/spack/test/conftest.py
@@ -70,6 +70,25 @@ def clean_user_environment():
         ev.activate(active)
 
 
+#
+# Disable checks on compiler executable existence
+#
+@pytest.fixture(scope='function', autouse=True)
+def mock_compiler_executable_verification(request, monkeypatch):
+    """Mock the compiler executable verification to allow missing executables.
+
+    This fixture can be disabled for tests of the compiler verification
+    functionality by::
+
+        @pytest.mark.enable_compiler_verification
+
+    If a test is marked in that way this is a no-op."""
+    if 'enable_compiler_verification' not in request.keywords:
+        monkeypatch.setattr(spack.compiler.Compiler,
+                            'verify_executables',
+                            lambda x: None)
+
+
 # Hooks to add command line options or set other custom behaviors.
 # They must be placed here to be found by pytest. See:
 #