Repository: spack/spack

Issue:
Cannot apply a gunzipped patch file
Spack should support applying compressed patches from remote, but currently fails on `.gz` files. The issue seems that `gunzip` always decompress the file in place, while Spack expects it to extract in the current working directory:

https://github.com/spack/spack/blob/58edc361be0aec47bfed24ef310ffa808153c74c/lib/spack/spack/fetch_strategy.py#L465-L472


### Steps to reproduce the issue
Using #22788 modified to retrieve the patch from remote:
```console
$ spack -d patch ncbi-rmblastn
```

### Error Message

<details>

<summary>$ spack -d patch ncbi-rmblastn </summary>

```console
$ spack -d patch ncbi-rmblastn
==> [2021-04-07-10:16:42.722900] Imported patch from built-in commands
==> [2021-04-07-10:16:42.723910] Imported patch from built-in commands
==> [2021-04-07-10:16:42.725527] Reading config file /home/culpo/PycharmProjects/spack/etc/spack/defaults/config.yaml
==> [2021-04-07-10:16:42.750447] Reading config file /home/culpo/PycharmProjects/spack/etc/spack/defaults/repos.yaml
==> [2021-04-07-10:16:42.821071] Reading config file /home/culpo/PycharmProjects/spack/etc/spack/defaults/packages.yaml
==> [2021-04-07-10:16:42.860430] Reading config file /home/culpo/.spack/packages.yaml
==> [2021-04-07-10:16:42.866125] Reading config file /home/culpo/.spack/linux/compilers.yaml
==> [2021-04-07-10:16:42.971083] Ordered hashes [ncbi-rmblastn]: ncbi-rmblastn/0/ce985abd3512834adb9ad3e4078fbf9608a33a2ee6538a1e94b641490c92f899
==> [2021-04-07-10:16:42.971715] DATABASE LOCK TIMEOUT: 3s
==> [2021-04-07-10:16:42.971787] PACKAGE LOCK TIMEOUT: No timeout
==> [2021-04-07-10:16:42.985895] Creating stage lock spack-stage-ncbi-rmblastn-2.9.0-7v3skwjselu63o5avqqaez3qslpizpr4
==> [2021-04-07-10:16:42.986535] Reading config file /home/culpo/PycharmProjects/spack/etc/spack/defaults/mirrors.yaml
==> [2021-04-07-10:16:42.987343] Reading config file /home/culpo/.spack/mirrors.yaml
==> [2021-04-07-10:16:43.073409] Using cached archive: /home/culpo/PycharmProjects/spack/var/spack/cache/_source-cache/archive/a3/a390cc2d7a09422759fc178db84de9def822cbe485916bbb2ec0d215dacdc257.tar.gz
==> [2021-04-07-10:16:43.187731] Creating stage lock spack-stage-g8t8mduw
==> [2021-04-07-10:16:43.188243] Checking existence of http://www.repeatmasker.org/isb-2.11.0+-rmblast.patch.gz
==> [2021-04-07-10:16:43.188815] '/usr/bin/curl' '--stderr' '-' '-s' '-f' '-r' '0-0' 'http://www.repeatmasker.org/isb-2.11.0+-rmblast.patch.gz'
==> [2021-04-07-10:16:43.714717] Fetching http://www.repeatmasker.org/isb-2.11.0+-rmblast.patch.gz
==> [2021-04-07-10:16:43.716420] '/usr/bin/curl' '-C' '-' '-o' '/tmp/culpo/spack-stage/spack-stage-g8t8mduw/isb-2.11.0+-rmblast.patch.gz.part' '-f' '-D' '-' '-L' 'http://www.repeatmasker.org/isb-2.11.0+-rmblast.patch.gz' '-#' '--connect-timeout' '10'
########################################################################################################################################### 100,0%
==> [2021-04-07-10:16:44.404490] Staging archive: /tmp/culpo/spack-stage/spack-stage-g8t8mduw/isb-2.11.0+-rmblast.patch.gz
==> [2021-04-07-10:16:44.407331] '/bin/gunzip' '/tmp/culpo/spack-stage/spack-stage-g8t8mduw/isb-2.11.0+-rmblast.patch.gz'
/tmp/culpo/spack-stage/spack-stage-g8t8mduw/spack-src
==> [2021-04-07-10:16:44.417239] Created stage in /tmp/culpo/spack-stage/spack-stage-g8t8mduw
/tmp/culpo/spack-stage/spack-stage-g8t8mduw/spack-src
==> [2021-04-07-10:16:44.417716] NoSuchPatchError: Archive was empty: http://www.repeatmasker.org/isb-2.11.0+-rmblast.patch.gz
==> [2021-04-07-10:16:44.417856] Error: Archive was empty: http://www.repeatmasker.org/isb-2.11.0+-rmblast.patch.gz
Traceback (most recent call last):
  File "/home/culpo/PycharmProjects/spack/lib/spack/spack/main.py", line 772, in main
    return _invoke_command(command, parser, args, unknown)
  File "/home/culpo/PycharmProjects/spack/lib/spack/spack/main.py", line 496, in _invoke_command
    return_val = command(parser, args)
  File "/home/culpo/PycharmProjects/spack/lib/spack/spack/cmd/patch.py", line 36, in patch
    package.do_patch()
  File "/home/culpo/PycharmProjects/spack/lib/spack/spack/package.py", line 1397, in do_patch
    self.do_stage()
  File "/home/culpo/PycharmProjects/spack/lib/spack/spack/package.py", line 1382, in do_stage
    self.do_fetch(mirror_only)
  File "/home/culpo/PycharmProjects/spack/lib/spack/spack/package.py", line 1370, in do_fetch
    patch.fetch()
  File "/home/culpo/PycharmProjects/spack/lib/spack/spack/patch.py", line 206, in fetch
    "Archive was empty: %s" % self.url)
spack.patch.NoSuchPatchError: Archive was empty: http://www.repeatmasker.org/isb-2.11.0+-rmblast.patch.gz
```

</details>

### Information on your system

* **Spack:** 0.16.1-2091-c188575cfe
* **Python:** 3.6.9
* **Platform:** linux-ubuntu18.04-broadwell
* **Concretizer:** original


### Additional information

The following diff seems to solve the issue:

```diff
diff --git a/lib/spack/spack/util/compression.py b/lib/spack/spack/util/compression.py
index 2f190e94e1..03a830109a 100644
--- a/lib/spack/spack/util/compression.py
+++ b/lib/spack/spack/util/compression.py
@@ -22,6 +22,21 @@ def allowed_archive(path):
     return any(path.endswith(t) for t in ALLOWED_ARCHIVE_TYPES)
 
 
+def _gunzip(archive_file):
+    """Like gunzip, but extracts in cwd instead of in-place.
+
+    Args:
+        archive_file (str): absolute path of the file to be decompressed
+    """
+    import gzip
+    decompressed_file = os.path.basename(archive_file.strip('.gz'))
+    working_dir = os.getcwd()
+    destination_abspath = os.path.join(working_dir, decompressed_file)
+    with gzip.open(archive_file, "rb") as f_in:
+        with open(destination_abspath, "wb") as f_out:
+            f_out.write(f_in.read())
+
+
 def decompressor_for(path, extension=None):
     """Get the appropriate decompressor for a path."""
     if ((extension and re.match(r'\.?zip$', extension)) or
@@ -30,8 +45,7 @@ def decompressor_for(path, extension=None):
         unzip.add_default_arg('-q')
         return unzip
     if extension and re.match(r'gz', extension):
-        gunzip = which('gunzip', required=True)
-        return gunzip
+        return _gunzip
     if extension and re.match(r'bz2', extension):
         bunzip2 = which('bunzip2', required=True)
         return bunzip2

```


- [x] I have run `spack debug report` and reported the version of Spack/Python/Platform
- [x] I have searched the issues of this repo and believe this is not a duplicate
- [x] I have run the failing commands in debug mode and reported the output



Discussion:

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
@@ -467,6 +467,8 @@ def expand(self):
         tarball_container = os.path.join(self.stage.path,
                                          "spack-expanded-archive")
 
+        # Below we assume that the command to decompress expand the
+        # archive in the current working directory
         mkdirp(tarball_container)
         with working_dir(tarball_container):
             decompress(self.archive_file)
diff --git a/lib/spack/spack/util/compression.py b/lib/spack/spack/util/compression.py
--- a/lib/spack/spack/util/compression.py
+++ b/lib/spack/spack/util/compression.py
@@ -22,6 +22,22 @@ def allowed_archive(path):
     return any(path.endswith(t) for t in ALLOWED_ARCHIVE_TYPES)
 
 
+def _gunzip(archive_file):
+    """Like gunzip, but extracts in the current working directory
+    instead of in-place.
+
+    Args:
+        archive_file (str): absolute path of the file to be decompressed
+    """
+    import gzip
+    decompressed_file = os.path.basename(archive_file.strip('.gz'))
+    working_dir = os.getcwd()
+    destination_abspath = os.path.join(working_dir, decompressed_file)
+    with gzip.open(archive_file, "rb") as f_in:
+        with open(destination_abspath, "wb") as f_out:
+            f_out.write(f_in.read())
+
+
 def decompressor_for(path, extension=None):
     """Get the appropriate decompressor for a path."""
     if ((extension and re.match(r'\.?zip$', extension)) or
@@ -30,8 +46,7 @@ def decompressor_for(path, extension=None):
         unzip.add_default_arg('-q')
         return unzip
     if extension and re.match(r'gz', extension):
-        gunzip = which('gunzip', required=True)
-        return gunzip
+        return _gunzip
     if extension and re.match(r'bz2', extension):
         bunzip2 = which('bunzip2', required=True)
         return bunzip2
diff --git a/var/spack/repos/builtin/packages/ncbi-rmblastn/package.py b/var/spack/repos/builtin/packages/ncbi-rmblastn/package.py
--- a/var/spack/repos/builtin/packages/ncbi-rmblastn/package.py
+++ b/var/spack/repos/builtin/packages/ncbi-rmblastn/package.py
@@ -22,8 +22,18 @@ class NcbiRmblastn(AutotoolsPackage):
     #   isb-2.11.0+-rmblast.patch.gz
     # The patch is downloaded and unzipped in the ncbi-rmblastn Spack package
     # directory to make it available for the patch directive.
-    patch('isb-2.11.0+-rmblast.patch', when="@2.11.0")
-    patch('isb-2.9.0+-rmblast-p1.patch', when="@2.9.0")
+    patch(
+        'http://www.repeatmasker.org/isb-2.11.0+-rmblast.patch.gz',
+        sha256='ce985abd3512834adb9ad3e4078fbf9608a33a2ee6538a1e94b641490c92f899',
+        archive_sha256='0fc27781c2ea2f17645247e2f3775b5d18c56f0b62761a865347be745ea4f6be',
+        when='@2.11.0'
+    )
+    patch(
+        'http://www.repeatmasker.org/isb-2.9.0+-rmblast.patch.gz',
+        sha256='ffa0845801aed11f4215b452532f3ff5b3dcb49ac8c14169568aaa585b9450ed',
+        archive_sha256='e746ee480ade608052306fd21f015c8a323f27029f65399275216f9a4c882d59',
+        when='@2.9.0'
+    )
 
     configure_directory = 'c++'
 


Test patch: