summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/spack/llnl/util/filesystem.py34
-rw-r--r--lib/spack/spack/build_systems/python.py54
-rw-r--r--var/spack/repos/builtin/packages/python/package.py72
3 files changed, 97 insertions, 63 deletions
diff --git a/lib/spack/llnl/util/filesystem.py b/lib/spack/llnl/util/filesystem.py
index cff512a167..b13fc00bd0 100644
--- a/lib/spack/llnl/util/filesystem.py
+++ b/lib/spack/llnl/util/filesystem.py
@@ -920,28 +920,34 @@ def get_filetype(path_name):
return output.strip()
+def has_shebang(path):
+ """Returns whether a path has a shebang line. Returns False if the file cannot be opened."""
+ try:
+ with open(path, "rb") as f:
+ return f.read(2) == b"#!"
+ except OSError:
+ return False
+
+
@system_path_filter
def is_nonsymlink_exe_with_shebang(path):
- """
- Returns whether the path is an executable script with a shebang.
- Return False when the path is a *symlink* to an executable script.
- """
+ """Returns whether the path is an executable regular file with a shebang. Returns False too
+ when the path is a symlink to a script, and also when the file cannot be opened."""
try:
st = os.lstat(path)
- # Should not be a symlink
- if stat.S_ISLNK(st.st_mode):
- return False
+ except OSError:
+ return False
- # Should be executable
- if not st.st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH):
- return False
+ # Should not be a symlink
+ if stat.S_ISLNK(st.st_mode):
+ return False
- # Should start with a shebang
- with open(path, "rb") as f:
- return f.read(2) == b"#!"
- except (IOError, OSError):
+ # Should be executable
+ if not st.st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH):
return False
+ return has_shebang(path)
+
@system_path_filter(arg_slice=slice(1))
def chgrp_if_not_world_writable(path, group):
diff --git a/lib/spack/spack/build_systems/python.py b/lib/spack/spack/build_systems/python.py
index 39dc7519e8..824d634b3e 100644
--- a/lib/spack/spack/build_systems/python.py
+++ b/lib/spack/spack/build_systems/python.py
@@ -6,7 +6,8 @@ import inspect
import os
import re
import shutil
-from typing import Iterable, List, Mapping, Optional
+import stat
+from typing import Dict, Iterable, List, Mapping, Optional, Tuple
import archspec
@@ -136,31 +137,50 @@ class PythonExtension(spack.package_base.PackageBase):
return conflicts
def add_files_to_view(self, view, merge_map, skip_if_exists=True):
- if not self.extendee_spec:
+ # Patch up shebangs to the python linked in the view only if python is built by Spack.
+ if not self.extendee_spec or self.extendee_spec.external:
return super().add_files_to_view(view, merge_map, skip_if_exists)
+ # We only patch shebangs in the bin directory.
+ copied_files: Dict[Tuple[int, int], str] = {} # File identifier -> source
+ delayed_links: List[Tuple[str, str]] = [] # List of symlinks from merge map
+
bin_dir = self.spec.prefix.bin
python_prefix = self.extendee_spec.prefix
- python_is_external = self.extendee_spec.external
- global_view = fs.same_path(python_prefix, view.get_projection_for_spec(self.spec))
for src, dst in merge_map.items():
- if os.path.exists(dst):
+ if skip_if_exists and os.path.lexists(dst):
continue
- elif global_view or not fs.path_contains_subdirectory(src, bin_dir):
+
+ if not fs.path_contains_subdirectory(src, bin_dir):
view.link(src, dst)
- elif not os.path.islink(src):
+ continue
+
+ s = os.lstat(src)
+
+ # Symlink is delayed because we may need to re-target if its target is copied in view
+ if stat.S_ISLNK(s.st_mode):
+ delayed_links.append((src, dst))
+ continue
+
+ # If it's executable and has a shebang, copy and patch it.
+ if (s.st_mode & 0b111) and fs.has_shebang(src):
+ copied_files[(s.st_dev, s.st_ino)] = dst
shutil.copy2(src, dst)
- is_script = fs.is_nonsymlink_exe_with_shebang(src)
- if is_script and not python_is_external:
- fs.filter_file(
- python_prefix,
- os.path.abspath(view.get_projection_for_spec(self.spec)),
- dst,
- )
+ fs.filter_file(
+ python_prefix, os.path.abspath(view.get_projection_for_spec(self.spec)), dst
+ )
+
+ # Finally re-target the symlinks that point to copied files.
+ for src, dst in delayed_links:
+ try:
+ s = os.stat(src)
+ target = copied_files[(s.st_dev, s.st_ino)]
+ except (OSError, KeyError):
+ target = None
+ if target:
+ os.symlink(target, dst)
else:
- orig_link_target = os.path.realpath(src)
- new_link_target = os.path.abspath(merge_map[orig_link_target])
- view.link(new_link_target, dst)
+ view.link(src, dst, spec=self.spec)
def remove_files_from_view(self, view, merge_map):
ignore_namespace = False
diff --git a/var/spack/repos/builtin/packages/python/package.py b/var/spack/repos/builtin/packages/python/package.py
index 4ebf32693e..8ca289b196 100644
--- a/var/spack/repos/builtin/packages/python/package.py
+++ b/var/spack/repos/builtin/packages/python/package.py
@@ -8,10 +8,11 @@ import json
import os
import platform
import re
+import stat
import subprocess
import sys
from shutil import copy
-from typing import Dict, List
+from typing import Dict, List, Tuple
import llnl.util.tty as tty
from llnl.util.filesystem import is_nonsymlink_exe_with_shebang, path_contains_subdirectory
@@ -1265,42 +1266,49 @@ print(json.dumps(config))
module.python_purelib = join_path(dependent_spec.prefix, self.purelib)
def add_files_to_view(self, view, merge_map, skip_if_exists=True):
+ # The goal is to copy the `python` executable, so that its search paths are relative to the
+ # view instead of the install prefix. This is an obsolete way of creating something that
+ # resembles a virtual environnent. Also we copy scripts with shebang lines. Finally we need
+ # to re-target symlinks pointing to copied files.
bin_dir = self.spec.prefix.bin if sys.platform != "win32" else self.spec.prefix
+ copied_files: Dict[Tuple[int, int], str] = {} # File identifier -> source
+ delayed_links: List[Tuple[str, str]] = [] # List of symlinks from merge map
for src, dst in merge_map.items():
+ if skip_if_exists and os.path.lexists(dst):
+ continue
+
+ # Files not in the bin dir are linked the default way.
if not path_contains_subdirectory(src, bin_dir):
view.link(src, dst, spec=self.spec)
- elif not os.path.islink(src):
- copy(src, dst)
- if is_nonsymlink_exe_with_shebang(src):
- filter_file(
- self.spec.prefix,
- os.path.abspath(view.get_projection_for_spec(self.spec)),
- dst,
- backup=False,
- )
- else:
- # orig_link_target = os.path.realpath(src) is insufficient when
- # the spack install tree is located at a symlink or a
- # descendent of a symlink. What we need here is the real
- # relative path from the python prefix to src
- # TODO: generalize this logic in the link_tree object
- # add a method to resolve a link relative to the link_tree
- # object root.
- realpath_src = os.path.realpath(src)
- realpath_prefix = os.path.realpath(self.spec.prefix)
- realpath_rel = os.path.relpath(realpath_src, realpath_prefix)
- orig_link_target = os.path.join(self.spec.prefix, realpath_rel)
-
- new_link_target = os.path.abspath(merge_map[orig_link_target])
- view.link(new_link_target, dst, spec=self.spec)
-
- def remove_files_from_view(self, view, merge_map):
- bin_dir = self.spec.prefix.bin if sys.platform != "win32" else self.spec.prefix
- for src, dst in merge_map.items():
- if not path_contains_subdirectory(src, bin_dir):
- view.remove_file(src, dst)
+ continue
+
+ s = os.lstat(src)
+
+ # Symlink is delayed because we may need to re-target if its target is copied in view
+ if stat.S_ISLNK(s.st_mode):
+ delayed_links.append((src, dst))
+ continue
+
+ # Anything that's not a symlink gets copied. Scripts with shebangs are immediately
+ # updated when necessary.
+ copied_files[(s.st_dev, s.st_ino)] = dst
+ copy(src, dst)
+ if is_nonsymlink_exe_with_shebang(src):
+ filter_file(
+ self.spec.prefix, os.path.abspath(view.get_projection_for_spec(self.spec)), dst
+ )
+
+ # Finally re-target the symlinks that point to copied files.
+ for src, dst in delayed_links:
+ try:
+ s = os.stat(src)
+ target = copied_files[(s.st_dev, s.st_ino)]
+ except (OSError, KeyError):
+ target = None
+ if target:
+ os.symlink(target, dst)
else:
- os.remove(dst)
+ view.link(src, dst, spec=self.spec)
def test_hello_world(self):
"""run simple hello world program"""