Repository: spack/spack

Issue:
non-explicit "All fetchers failed" error when curl is not available
When curl is not installed and I try to use spack, I get 
```
$ spack install zlib
==> Installing zlib
==> No binary for zlib found: installing from source
==> Error: FetchError: All fetchers failed

/home/f377482/spack_installs/spack/lib/spack/spack/package.py:1270, in do_fetch:
       1267                                 self.spec.format('{name}{@version}'), ck_msg)
       1268
       1269        self.stage.create()
  >>   1270        err_msg = None if not self.manual_download else self.download_instr
       1271        self.stage.fetch(mirror_only, err_msg=err_msg)
       1272        self._fetch_time = time.time() - start_time
       1273
```

Using pdb, I see 
that a ```spack.util.executable.CommandNotFoundError: spack requires 'curl'. Make sure it is in your path.``` error was generated, but silenced somewhere later.

This message is very helpful and should deserve to be displayed to the user.



Discussion:
@tldahlgren this might have happened when we made Spack less verbose...
> @tldahlgren this might have happened when we made Spack less verbose...

From the line it looks like the manual download change (https://github.com/spack/spack/pull/18242) was a contributor.
> > @tldahlgren this might have happened when we made Spack less verbose...
> 
> From the line it looks like the manual download change (#18242) was a contributor.

On closer inspection, you are correct Adam.

Resolving patch:
diff --git a/lib/spack/spack/fetch_strategy.py b/lib/spack/spack/fetch_strategy.py
--- a/lib/spack/spack/fetch_strategy.py
+++ b/lib/spack/spack/fetch_strategy.py
@@ -42,7 +42,7 @@
 from llnl.util.filesystem import (
     working_dir, mkdirp, temp_rename, temp_cwd, get_single_file)
 from spack.util.compression import decompressor_for, extension
-from spack.util.executable import which
+from spack.util.executable import which, CommandNotFoundError
 from spack.util.string import comma_and, quote
 from spack.version import Version, ver
 
@@ -267,7 +267,10 @@ def __init__(self, url=None, checksum=None, **kwargs):
     @property
     def curl(self):
         if not self._curl:
-            self._curl = which('curl', required=True)
+            try:
+                self._curl = which('curl', required=True)
+            except CommandNotFoundError as exc:
+                tty.error(str(exc))
         return self._curl
 
     def source_id(self):


Test patch:
diff --git a/lib/spack/spack/test/url_fetch.py b/lib/spack/spack/test/url_fetch.py
--- a/lib/spack/spack/test/url_fetch.py
+++ b/lib/spack/spack/test/url_fetch.py
@@ -18,6 +18,7 @@
 from spack.stage import Stage
 from spack.version import ver
 import spack.util.crypto as crypto
+import spack.util.executable
 
 
 @pytest.fixture(params=list(crypto.hashes.keys()))
@@ -225,3 +226,29 @@ def test_candidate_urls(pkg_factory, url, urls, version, expected):
     pkg = pkg_factory(url, urls, fetch_options={'timeout': 60})
     f = fs._from_merged_attrs(fs.URLFetchStrategy, pkg, version)
     assert f.extra_options == {'timeout': 60}
+
+
+@pytest.mark.regression('19673')
+def test_missing_curl(tmpdir, monkeypatch):
+    """Ensure a fetch involving missing curl package reports the error."""
+    err_fmt = 'No such command {0}'
+
+    def _which(*args, **kwargs):
+        err_msg = err_fmt.format(args[0])
+        raise spack.util.executable.CommandNotFoundError(err_msg)
+
+    # Patching the 'which' symbol imported by fetch_strategy works
+    # since it is too late in import processing to patch the defining
+    # (spack.util.executable) module's symbol.
+    monkeypatch.setattr(fs, 'which', _which)
+
+    testpath = str(tmpdir)
+    url = 'http://github.com/spack/spack'
+    fetcher = fs.URLFetchStrategy(url=url)
+    assert fetcher is not None
+
+    with pytest.raises(TypeError, match='object is not callable'):
+        with Stage(fetcher, path=testpath) as stage:
+            out = stage.fetch()
+
+        assert err_fmt.format('curl') in out