summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorHarmen Stoppels <me@harmenstoppels.nl>2024-05-27 13:37:04 +0200
committerGitHub <noreply@github.com>2024-05-27 13:37:04 +0200
commite461234865f0a6a5add54815eee3b83fefc6627f (patch)
treec2fc6b5bed65ee3f23bac1135f0032b681cfc736 /lib
parent2c1d5f9844574c1f99bb38305e9f5feae3bc898b (diff)
downloadspack-e461234865f0a6a5add54815eee3b83fefc6627f.tar.gz
spack-e461234865f0a6a5add54815eee3b83fefc6627f.tar.bz2
spack-e461234865f0a6a5add54815eee3b83fefc6627f.tar.xz
spack-e461234865f0a6a5add54815eee3b83fefc6627f.zip
link: directly bind to os.* on non-windows (#44400)
The windows wrappers for basic functions like `os.symlink`, `os.readlink` and `os.path.islink` in the `llnl.util.symlink` module have bugs, and trigger more file system operations on non-windows than they should. This commit just binds `llnl.util.symlink.symlink = os.symlink` etc so built-in functions are used on non-windows
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/llnl/util/filesystem.py22
-rw-r--r--lib/spack/llnl/util/symlink.py131
-rw-r--r--lib/spack/spack/installer.py8
-rw-r--r--lib/spack/spack/test/llnl/util/filesystem.py4
-rw-r--r--lib/spack/spack/test/llnl/util/symlink.py32
5 files changed, 80 insertions, 117 deletions
diff --git a/lib/spack/llnl/util/filesystem.py b/lib/spack/llnl/util/filesystem.py
index f233828df8..6b2ba50c0e 100644
--- a/lib/spack/llnl/util/filesystem.py
+++ b/lib/spack/llnl/util/filesystem.py
@@ -766,7 +766,6 @@ def copy_tree(
src: str,
dest: str,
symlinks: bool = True,
- allow_broken_symlinks: bool = sys.platform != "win32",
ignore: Optional[Callable[[str], bool]] = None,
_permissions: bool = False,
):
@@ -789,8 +788,6 @@ def copy_tree(
src (str): the directory to copy
dest (str): the destination directory
symlinks (bool): whether or not to preserve symlinks
- allow_broken_symlinks (bool): whether or not to allow broken (dangling) symlinks,
- On Windows, setting this to True will raise an exception. Defaults to true on unix.
ignore (typing.Callable): function indicating which files to ignore
_permissions (bool): for internal use only
@@ -798,8 +795,6 @@ def copy_tree(
IOError: if *src* does not match any files or directories
ValueError: if *src* is a parent directory of *dest*
"""
- if allow_broken_symlinks and sys.platform == "win32":
- raise llnl.util.symlink.SymlinkError("Cannot allow broken symlinks on Windows!")
if _permissions:
tty.debug("Installing {0} to {1}".format(src, dest))
else:
@@ -872,16 +867,14 @@ def copy_tree(
copy_mode(s, d)
for target, d, s in links:
- symlink(target, d, allow_broken_symlinks=allow_broken_symlinks)
+ symlink(target, d)
if _permissions:
set_install_permissions(d)
copy_mode(s, d)
@system_path_filter
-def install_tree(
- src, dest, symlinks=True, ignore=None, allow_broken_symlinks=sys.platform != "win32"
-):
+def install_tree(src, dest, symlinks=True, ignore=None):
"""Recursively install an entire directory tree rooted at *src*.
Same as :py:func:`copy_tree` with the addition of setting proper
@@ -892,21 +885,12 @@ def install_tree(
dest (str): the destination directory
symlinks (bool): whether or not to preserve symlinks
ignore (typing.Callable): function indicating which files to ignore
- allow_broken_symlinks (bool): whether or not to allow broken (dangling) symlinks,
- On Windows, setting this to True will raise an exception.
Raises:
IOError: if *src* does not match any files or directories
ValueError: if *src* is a parent directory of *dest*
"""
- copy_tree(
- src,
- dest,
- symlinks=symlinks,
- allow_broken_symlinks=allow_broken_symlinks,
- ignore=ignore,
- _permissions=True,
- )
+ copy_tree(src, dest, symlinks=symlinks, ignore=ignore, _permissions=True)
@system_path_filter
diff --git a/lib/spack/llnl/util/symlink.py b/lib/spack/llnl/util/symlink.py
index 934aba552b..be758c4d13 100644
--- a/lib/spack/llnl/util/symlink.py
+++ b/lib/spack/llnl/util/symlink.py
@@ -8,6 +8,7 @@ import shutil
import subprocess
import sys
import tempfile
+from typing import Union
from llnl.util import lang, tty
@@ -16,92 +17,66 @@ from ..path import sanitize_win_longpath, system_path_filter
if sys.platform == "win32":
from win32file import CreateHardLink
-is_windows = sys.platform == "win32"
+def _windows_symlink(
+ src: str, dst: str, target_is_directory: bool = False, *, dir_fd: Union[int, None] = None
+):
+ """On Windows with System Administrator privileges this will be a normal symbolic link via
+ os.symlink. On Windows without privledges the link will be a junction for a directory and a
+ hardlink for a file. On Windows the various link types are:
-def symlink(source_path: str, link_path: str, allow_broken_symlinks: bool = not is_windows):
- """
- Create a link.
-
- On non-Windows and Windows with System Administrator
- privleges this will be a normal symbolic link via
- os.symlink.
-
- On Windows without privledges the link will be a
- junction for a directory and a hardlink for a file.
- On Windows the various link types are:
-
- Symbolic Link: A link to a file or directory on the
- same or different volume (drive letter) or even to
- a remote file or directory (using UNC in its path).
- Need System Administrator privileges to make these.
-
- Hard Link: A link to a file on the same volume (drive
- letter) only. Every file (file's data) has at least 1
- hard link (file's name). But when this method creates
- a new hard link there will be 2. Deleting all hard
- links effectively deletes the file. Don't need System
- Administrator privileges.
-
- Junction: A link to a directory on the same or different
- volume (drive letter) but not to a remote directory. Don't
- need System Administrator privileges.
-
- Parameters:
- source_path (str): The real file or directory that the link points to.
- Must be absolute OR relative to the link.
- link_path (str): The path where the link will exist.
- allow_broken_symlinks (bool): On Linux or Mac, don't raise an exception if the source_path
- doesn't exist. This will still raise an exception on Windows.
- """
- source_path = os.path.normpath(source_path)
- win_source_path = source_path
- link_path = os.path.normpath(link_path)
+ Symbolic Link: A link to a file or directory on the same or different volume (drive letter) or
+ even to a remote file or directory (using UNC in its path). Need System Administrator
+ privileges to make these.
- # Never allow broken links on Windows.
- if sys.platform == "win32" and allow_broken_symlinks:
- raise ValueError("allow_broken_symlinks parameter cannot be True on Windows.")
+ Hard Link: A link to a file on the same volume (drive letter) only. Every file (file's data)
+ has at least 1 hard link (file's name). But when this method creates a new hard link there will
+ be 2. Deleting all hard links effectively deletes the file. Don't need System Administrator
+ privileges.
- if not allow_broken_symlinks:
- # Perform basic checks to make sure symlinking will succeed
- if os.path.lexists(link_path):
- raise AlreadyExistsError(
- f"Link path ({link_path}) already exists. Cannot create link."
+ Junction: A link to a directory on the same or different volume (drive letter) but not to a
+ remote directory. Don't need System Administrator privileges."""
+ source_path = os.path.normpath(src)
+ win_source_path = source_path
+ link_path = os.path.normpath(dst)
+
+ # Perform basic checks to make sure symlinking will succeed
+ if os.path.lexists(link_path):
+ raise AlreadyExistsError(f"Link path ({link_path}) already exists. Cannot create link.")
+
+ if not os.path.exists(source_path):
+ if os.path.isabs(source_path):
+ # An absolute source path that does not exist will result in a broken link.
+ raise SymlinkError(
+ f"Source path ({source_path}) is absolute but does not exist. Resulting "
+ f"link would be broken so not making link."
)
-
- if not os.path.exists(source_path):
- if os.path.isabs(source_path) and not allow_broken_symlinks:
- # An absolute source path that does not exist will result in a broken link.
+ else:
+ # os.symlink can create a link when the given source path is relative to
+ # the link path. Emulate this behavior and check to see if the source exists
+ # relative to the link path ahead of link creation to prevent broken
+ # links from being made.
+ link_parent_dir = os.path.dirname(link_path)
+ relative_path = os.path.join(link_parent_dir, source_path)
+ if os.path.exists(relative_path):
+ # In order to work on windows, the source path needs to be modified to be
+ # relative because hardlink/junction dont resolve relative paths the same
+ # way as os.symlink. This is ignored on other operating systems.
+ win_source_path = relative_path
+ else:
raise SymlinkError(
- f"Source path ({source_path}) is absolute but does not exist. Resulting "
- f"link would be broken so not making link."
+ f"The source path ({source_path}) is not relative to the link path "
+ f"({link_path}). Resulting link would be broken so not making link."
)
- else:
- # os.symlink can create a link when the given source path is relative to
- # the link path. Emulate this behavior and check to see if the source exists
- # relative to the link path ahead of link creation to prevent broken
- # links from being made.
- link_parent_dir = os.path.dirname(link_path)
- relative_path = os.path.join(link_parent_dir, source_path)
- if os.path.exists(relative_path):
- # In order to work on windows, the source path needs to be modified to be
- # relative because hardlink/junction dont resolve relative paths the same
- # way as os.symlink. This is ignored on other operating systems.
- win_source_path = relative_path
- elif not allow_broken_symlinks:
- raise SymlinkError(
- f"The source path ({source_path}) is not relative to the link path "
- f"({link_path}). Resulting link would be broken so not making link."
- )
# Create the symlink
- if sys.platform == "win32" and not _windows_can_symlink():
+ if not _windows_can_symlink():
_windows_create_link(win_source_path, link_path)
else:
os.symlink(source_path, link_path, target_is_directory=os.path.isdir(source_path))
-def islink(path: str) -> bool:
+def _windows_islink(path: str) -> bool:
"""Override os.islink to give correct answer for spack logic.
For Non-Windows: a link can be determined with the os.path.islink method.
@@ -269,7 +244,7 @@ def _windows_create_hard_link(path: str, link: str):
CreateHardLink(link, path)
-def readlink(path: str, *, dir_fd=None):
+def _windows_readlink(path: str, *, dir_fd=None):
"""Spack utility to override of os.readlink method to work cross platform"""
if _windows_is_hardlink(path):
return _windows_read_hard_link(path)
@@ -338,6 +313,16 @@ def resolve_link_target_relative_to_the_link(link):
return os.path.join(link_dir, target)
+if sys.platform == "win32":
+ symlink = _windows_symlink
+ readlink = _windows_readlink
+ islink = _windows_islink
+else:
+ symlink = os.symlink
+ readlink = os.readlink
+ islink = os.path.islink
+
+
class SymlinkError(RuntimeError):
"""Exception class for errors raised while creating symlinks,
junctions and hard links
diff --git a/lib/spack/spack/installer.py b/lib/spack/spack/installer.py
index 09eeecaca5..dcfa0c2269 100644
--- a/lib/spack/spack/installer.py
+++ b/lib/spack/spack/installer.py
@@ -600,9 +600,7 @@ def dump_packages(spec: "spack.spec.Spec", path: str) -> None:
if node is spec:
spack.repo.PATH.dump_provenance(node, dest_pkg_dir)
elif source_pkg_dir:
- fs.install_tree(
- source_pkg_dir, dest_pkg_dir, allow_broken_symlinks=(sys.platform != "win32")
- )
+ fs.install_tree(source_pkg_dir, dest_pkg_dir)
def get_dependent_ids(spec: "spack.spec.Spec") -> List[str]:
@@ -2380,9 +2378,7 @@ class BuildProcessInstaller:
src_target = os.path.join(pkg.spec.prefix, "share", pkg.name, "src")
tty.debug(f"{self.pre} Copying source to {src_target}")
- fs.install_tree(
- pkg.stage.source_path, src_target, allow_broken_symlinks=(sys.platform != "win32")
- )
+ fs.install_tree(pkg.stage.source_path, src_target)
def _real_install(self) -> None:
import spack.builder
diff --git a/lib/spack/spack/test/llnl/util/filesystem.py b/lib/spack/spack/test/llnl/util/filesystem.py
index ea17a8fc8a..d9e34d5009 100644
--- a/lib/spack/spack/test/llnl/util/filesystem.py
+++ b/lib/spack/spack/test/llnl/util/filesystem.py
@@ -278,8 +278,8 @@ class TestInstallTree:
def test_allow_broken_symlinks(self, stage):
"""Test installing with a broken symlink."""
with fs.working_dir(str(stage)):
- symlink("nonexistant.txt", "source/broken", allow_broken_symlinks=True)
- fs.install_tree("source", "dest", symlinks=True, allow_broken_symlinks=True)
+ symlink("nonexistant.txt", "source/broken")
+ fs.install_tree("source", "dest", symlinks=True)
assert os.path.islink("dest/broken")
assert not os.path.exists(readlink("dest/broken"))
diff --git a/lib/spack/spack/test/llnl/util/symlink.py b/lib/spack/spack/test/llnl/util/symlink.py
index 6e34c97fc4..73a9d05e97 100644
--- a/lib/spack/spack/test/llnl/util/symlink.py
+++ b/lib/spack/spack/test/llnl/util/symlink.py
@@ -20,7 +20,7 @@ def test_symlink_file(tmpdir):
fd, real_file = tempfile.mkstemp(prefix="real", suffix=".txt", dir=test_dir)
link_file = str(tmpdir.join("link.txt"))
assert os.path.exists(link_file) is False
- symlink.symlink(source_path=real_file, link_path=link_file)
+ symlink.symlink(real_file, link_file)
assert os.path.exists(link_file)
assert symlink.islink(link_file)
@@ -32,11 +32,12 @@ def test_symlink_dir(tmpdir):
real_dir = os.path.join(test_dir, "real_dir")
link_dir = os.path.join(test_dir, "link_dir")
os.mkdir(real_dir)
- symlink.symlink(source_path=real_dir, link_path=link_dir)
+ symlink.symlink(real_dir, link_dir)
assert os.path.exists(link_dir)
assert symlink.islink(link_dir)
+@pytest.mark.skipif(sys.platform != "win32", reason="Test is only for Windows")
def test_symlink_source_not_exists(tmpdir):
"""Test the symlink.symlink method for the case where a source path does not exist"""
with tmpdir.as_cwd():
@@ -44,7 +45,7 @@ def test_symlink_source_not_exists(tmpdir):
real_dir = os.path.join(test_dir, "real_dir")
link_dir = os.path.join(test_dir, "link_dir")
with pytest.raises(symlink.SymlinkError):
- symlink.symlink(source_path=real_dir, link_path=link_dir, allow_broken_symlinks=False)
+ symlink._windows_symlink(real_dir, link_dir)
def test_symlink_src_relative_to_link(tmpdir):
@@ -61,18 +62,16 @@ def test_symlink_src_relative_to_link(tmpdir):
fd, real_file = tempfile.mkstemp(prefix="real", suffix=".txt", dir=subdir_2)
link_file = os.path.join(subdir_1, "link.txt")
- symlink.symlink(
- source_path=f"b/{os.path.basename(real_file)}",
- link_path=f"a/{os.path.basename(link_file)}",
- )
+ symlink.symlink(f"b/{os.path.basename(real_file)}", f"a/{os.path.basename(link_file)}")
assert os.path.exists(link_file)
assert symlink.islink(link_file)
# Check dirs
assert not os.path.lexists(link_dir)
- symlink.symlink(source_path="b", link_path="a/c")
+ symlink.symlink("b", "a/c")
assert os.path.lexists(link_dir)
+@pytest.mark.skipif(sys.platform != "win32", reason="Test is only for Windows")
def test_symlink_src_not_relative_to_link(tmpdir):
"""Test the symlink.symlink functionality where the source value does not exist relative to
the link and not relative to the cwd. NOTE that this symlink api call is EXPECTED to raise
@@ -88,19 +87,18 @@ def test_symlink_src_not_relative_to_link(tmpdir):
link_file = str(tmpdir.join("link.txt"))
# Expected SymlinkError because source path does not exist relative to link path
with pytest.raises(symlink.SymlinkError):
- symlink.symlink(
- source_path=f"d/{os.path.basename(real_file)}",
- link_path=f"a/{os.path.basename(link_file)}",
- allow_broken_symlinks=False,
+ symlink._windows_symlink(
+ f"d/{os.path.basename(real_file)}", f"a/{os.path.basename(link_file)}"
)
assert not os.path.exists(link_file)
# Check dirs
assert not os.path.lexists(link_dir)
with pytest.raises(symlink.SymlinkError):
- symlink.symlink(source_path="d", link_path="a/c", allow_broken_symlinks=False)
+ symlink._windows_symlink("d", "a/c")
assert not os.path.lexists(link_dir)
+@pytest.mark.skipif(sys.platform != "win32", reason="Test is only for Windows")
def test_symlink_link_already_exists(tmpdir):
"""Test the symlink.symlink method for the case where a link already exists"""
with tmpdir.as_cwd():
@@ -108,10 +106,10 @@ def test_symlink_link_already_exists(tmpdir):
real_dir = os.path.join(test_dir, "real_dir")
link_dir = os.path.join(test_dir, "link_dir")
os.mkdir(real_dir)
- symlink.symlink(real_dir, link_dir, allow_broken_symlinks=False)
+ symlink._windows_symlink(real_dir, link_dir)
assert os.path.exists(link_dir)
with pytest.raises(symlink.SymlinkError):
- symlink.symlink(source_path=real_dir, link_path=link_dir, allow_broken_symlinks=False)
+ symlink._windows_symlink(real_dir, link_dir)
@pytest.mark.skipif(not symlink._windows_can_symlink(), reason="Test requires elevated privileges")
@@ -122,7 +120,7 @@ def test_symlink_win_file(tmpdir):
test_dir = str(tmpdir)
fd, real_file = tempfile.mkstemp(prefix="real", suffix=".txt", dir=test_dir)
link_file = str(tmpdir.join("link.txt"))
- symlink.symlink(source_path=real_file, link_path=link_file)
+ symlink.symlink(real_file, link_file)
# Verify that all expected conditions are met
assert os.path.exists(link_file)
assert symlink.islink(link_file)
@@ -140,7 +138,7 @@ def test_symlink_win_dir(tmpdir):
real_dir = os.path.join(test_dir, "real")
link_dir = os.path.join(test_dir, "link")
os.mkdir(real_dir)
- symlink.symlink(source_path=real_dir, link_path=link_dir)
+ symlink.symlink(real_dir, link_dir)
# Verify that all expected conditions are met
assert os.path.exists(link_dir)
assert symlink.islink(link_dir)