Repository: spack/spack

Issue:
Symbolic links in binarycache bundles need to be relocated.
<!-- Explain, in a clear and concise way, the command you ran and the result you were trying to achieve.
Example: "I ran `spack find` to list all the installed packages and ..." -->

### Steps to reproduce the issue

```console
$ spack install cudnn
$ spack binarycache create -d <location> cudnn
<look at the incorrect links in the binary package>
```

### Error Message

I untarred the .spack file, then ran `tar tzvf *.tar.gz`.  I see this:
```
lrwxrwxrwx blake14/blake14     0 2020-10-15 14:33 cudnn-7.6.5.32-10.1-linux-ppc64le-vbhtwfg4trkseuelk4e6626u6fjajapp/include -> /usr/WS2/blake14/deploy.spack/opt/spack/linux-rhel7-power9le/gcc-8.3.1/cudnn-7.6.5.32-10.1-linux-ppc64le-vbhtwfg4trkseuelk4e6626u6fjajapp/targets/ppc64le-linux/include
lrwxrwxrwx blake14/blake14     0 2020-10-15 14:33 cudnn-7.6.5.32-10.1-linux-ppc64le-vbhtwfg4trkseuelk4e6626u6fjajapp/lib -> /usr/WS2/blake14/deploy.spack/opt/spack/linux-rhel7-power9le/gcc-8.3.1/cudnn-7.6.5.32-10.1-linux-ppc64le-vbhtwfg4trkseuelk4e6626u6fjajapp/targets/ppc64le-linux/lib
```

These symbolic links point to my personal spack install directory.  They need to be relocated just like the rest of the paths.


### Information on your system

* **Spack:** 0.15.4-1651-978d8ad
* **Python:** 3.7.2
* **Platform:** linux-rhel7-haswell

### Additional information

<!-- These boxes can be checked by replacing [ ] with [x] or by clicking them after submitting the issue. -->
- [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
- [ ] I have run the failing commands in debug mode and reported the output

<!-- We encourage you to try, as much as possible, to reduce your problem to the minimal example that still reproduces the issue. That would help us a lot in fixing it quickly and effectively!

If you want to ask a question about the tool (how to use it, what it can currently do, etc.), try the `#general` channel on our Slack first. We have a welcoming community and chances are you'll get your reply faster and without opening an issue.

Other than that, thanks for taking the time to contribute to Spack! -->



Discussion:
@haampie Can you give a look at this, and check whether it should stay in v0.20 or be moved?
I've fixed this an eternity ago in a PR that didn't get merged :( #32306 

Resolving patch:
diff --git a/lib/spack/spack/filesystem_view.py b/lib/spack/spack/filesystem_view.py
--- a/lib/spack/spack/filesystem_view.py
+++ b/lib/spack/spack/filesystem_view.py
@@ -3,13 +3,14 @@
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
-import collections
 import functools as ft
 import itertools
 import os
 import re
 import shutil
+import stat
 import sys
+from typing import Optional
 
 from llnl.util import tty
 from llnl.util.filesystem import (
@@ -32,12 +33,14 @@
 
 import spack.config
 import spack.projections
+import spack.relocate
 import spack.schema.projections
 import spack.spec
 import spack.store
 import spack.util.spack_json as s_json
 import spack.util.spack_yaml as s_yaml
 from spack.error import SpackError
+from spack.hooks import sbang
 
 __all__ = ["FilesystemView", "YamlFilesystemView"]
 
@@ -57,50 +60,47 @@ def view_hardlink(src, dst, **kwargs):
     os.link(src, dst)
 
 
-def view_copy(src, dst, view, spec=None):
+def view_copy(src: str, dst: str, view, spec: Optional[spack.spec.Spec] = None):
     """
     Copy a file from src to dst.
 
     Use spec and view to generate relocations
     """
-    shutil.copy2(src, dst)
-    if spec and not spec.external:
-        # Not metadata, we have to relocate it
+    shutil.copy2(src, dst, follow_symlinks=False)
 
-        # Get information on where to relocate from/to
+    # No need to relocate if no metadata or external.
+    if not spec or spec.external:
+        return
 
-        # This is vestigial code for the *old* location of sbang. Previously,
-        # sbang was a bash script, and it lived in the spack prefix. It is
-        # now a POSIX script that lives in the install prefix. Old packages
-        # will have the old sbang location in their shebangs.
-        # TODO: Not sure which one to use...
-        import spack.hooks.sbang as sbang
+    # Order of this dict is somewhat irrelevant
+    prefix_to_projection = {
+        s.prefix: view.get_projection_for_spec(s)
+        for s in spec.traverse(root=True, order="breadth")
+        if not s.external
+    }
 
-        # Break a package include cycle
-        import spack.relocate
+    src_stat = os.lstat(src)
 
-        orig_sbang = "#!/bin/bash {0}/bin/sbang".format(spack.paths.spack_root)
-        new_sbang = sbang.sbang_shebang_line()
+    # TODO: change this into a bulk operation instead of a per-file operation
 
-        prefix_to_projection = collections.OrderedDict(
-            {spec.prefix: view.get_projection_for_spec(spec)}
-        )
+    if stat.S_ISLNK(src_stat.st_mode):
+        spack.relocate.relocate_links(links=[dst], prefix_to_prefix=prefix_to_projection)
+    elif spack.relocate.is_binary(dst):
+        spack.relocate.relocate_text_bin(binaries=[dst], prefixes=prefix_to_projection)
+    else:
+        prefix_to_projection[spack.store.layout.root] = view._root
 
-        for dep in spec.traverse():
-            if not dep.external:
-                prefix_to_projection[dep.prefix] = view.get_projection_for_spec(dep)
+        # This is vestigial code for the *old* location of sbang.
+        prefix_to_projection[
+            "#!/bin/bash {0}/bin/sbang".format(spack.paths.spack_root)
+        ] = sbang.sbang_shebang_line()
 
-        if spack.relocate.is_binary(dst):
-            spack.relocate.relocate_text_bin(binaries=[dst], prefixes=prefix_to_projection)
-        else:
-            prefix_to_projection[spack.store.layout.root] = view._root
-            prefix_to_projection[orig_sbang] = new_sbang
-            spack.relocate.relocate_text(files=[dst], prefixes=prefix_to_projection)
-        try:
-            stat = os.stat(src)
-            os.chown(dst, stat.st_uid, stat.st_gid)
-        except OSError:
-            tty.debug("Can't change the permissions for %s" % dst)
+        spack.relocate.relocate_text(files=[dst], prefixes=prefix_to_projection)
+
+    try:
+        os.chown(dst, src_stat.st_uid, src_stat.st_gid)
+    except OSError:
+        tty.debug("Can't change the permissions for %s" % dst)
 
 
 def view_func_parser(parsed_name):


Test patch: