summaryrefslogtreecommitdiff
path: root/lib/spack/llnl/util/symlink.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/spack/llnl/util/symlink.py')
-rw-r--r--lib/spack/llnl/util/symlink.py336
1 files changed, 58 insertions, 278 deletions
diff --git a/lib/spack/llnl/util/symlink.py b/lib/spack/llnl/util/symlink.py
index f6f250c3eb..69aacaf9f0 100644
--- a/lib/spack/llnl/util/symlink.py
+++ b/lib/spack/llnl/util/symlink.py
@@ -2,185 +2,77 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+import errno
import os
-import re
import shutil
-import subprocess
import sys
import tempfile
+from os.path import exists, join
-from llnl.util import lang, tty
-
-from spack.error import SpackError
-from spack.util.path import system_path_filter
+from llnl.util import lang
if sys.platform == "win32":
from win32file import CreateHardLink
-def symlink(source_path: str, link_path: str, allow_broken_symlinks: bool = False):
+def symlink(real_path, link_path):
"""
- 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.
+ Create a symbolic link.
- 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.
+ On Windows, use junctions if os.symlink fails.
"""
- source_path = os.path.normpath(source_path)
- win_source_path = source_path
- link_path = os.path.normpath(link_path)
-
- # 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.")
-
- # Perform basic checks to make sure symlinking will succeed
- if os.path.lexists(link_path):
- raise SymlinkError(f"Link path ({link_path}) already exists. Cannot create 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.
- raise SymlinkError(
- f"Source path ({source_path}) is absolute but does not exist. Resulting "
- f"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 patg 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():
- _windows_create_link(win_source_path, link_path)
+ if sys.platform != "win32":
+ os.symlink(real_path, link_path)
+ elif _win32_can_symlink():
+ # Windows requires target_is_directory=True when the target is a dir.
+ os.symlink(real_path, link_path, target_is_directory=os.path.isdir(real_path))
else:
- os.symlink(source_path, link_path, target_is_directory=os.path.isdir(source_path))
-
-
-def 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.
- Windows-only methods will return false for other operating systems.
-
- For Windows: spack considers symlinks, hard links, and junctions to
- all be links, so if any of those are True, return True.
-
- Args:
- path (str): path to check if it is a link.
+ try:
+ # Try to use junctions
+ _win32_junction(real_path, link_path)
+ except OSError as e:
+ if e.errno == errno.EEXIST:
+ # EEXIST error indicates that file we're trying to "link"
+ # is already present, don't bother trying to copy which will also fail
+ # just raise
+ raise
+ else:
+ # If all else fails, fall back to copying files
+ shutil.copyfile(real_path, link_path)
- Returns:
- bool - whether the path is any kind link or not.
- """
- return any([os.path.islink(path), _windows_is_junction(path), _windows_is_hardlink(path)])
-
-
-def _windows_is_hardlink(path: str) -> bool:
- """Determines if a path is a windows hard link. This is accomplished
- by looking at the number of links using os.stat. A non-hard-linked file
- will have a st_nlink value of 1, whereas a hard link will have a value
- larger than 1. Note that both the original and hard-linked file will
- return True because they share the same inode.
-
- Args:
- path (str): Windows path to check for a hard link
-
- Returns:
- bool - Whether the path is a hard link or not.
- """
- if sys.platform != "win32" or os.path.islink(path) or not os.path.exists(path):
- return False
-
- return os.stat(path).st_nlink > 1
+def islink(path):
+ return os.path.islink(path) or _win32_is_junction(path)
-def _windows_is_junction(path: str) -> bool:
- """Determines if a path is a windows junction. A junction can be
- determined using a bitwise AND operation between the file's
- attribute bitmask and the known junction bitmask (0x400).
- Args:
- path (str): A non-file path
+# '_win32' functions based on
+# https://github.com/Erotemic/ubelt/blob/master/ubelt/util_links.py
+def _win32_junction(path, link):
+ # junctions require absolute paths
+ if not os.path.isabs(link):
+ link = os.path.abspath(link)
- Returns:
- bool - whether the path is a junction or not.
- """
- if sys.platform != "win32" or os.path.islink(path) or os.path.isfile(path):
- return False
-
- import ctypes.wintypes
-
- get_file_attributes = ctypes.windll.kernel32.GetFileAttributesW # type: ignore[attr-defined]
- get_file_attributes.argtypes = (ctypes.wintypes.LPWSTR,)
- get_file_attributes.restype = ctypes.wintypes.DWORD
+ # os.symlink will fail if link exists, emulate the behavior here
+ if exists(link):
+ raise OSError(errno.EEXIST, "File exists: %s -> %s" % (link, path))
- invalid_file_attributes = 0xFFFFFFFF
- reparse_point = 0x400
- file_attr = get_file_attributes(path)
+ if not os.path.isabs(path):
+ parent = os.path.join(link, os.pardir)
+ path = os.path.join(parent, path)
+ path = os.path.abspath(path)
- if file_attr == invalid_file_attributes:
- return False
-
- return file_attr & reparse_point > 0
+ CreateHardLink(link, path)
@lang.memoized
-def _windows_can_symlink() -> bool:
- """
- Determines if windows is able to make a symlink depending on
- the system configuration and the level of the user's permissions.
- """
- if sys.platform != "win32":
- tty.warn("windows_can_symlink method can't be used on non-Windows OS.")
- return False
-
+def _win32_can_symlink():
tempdir = tempfile.mkdtemp()
- dpath = os.path.join(tempdir, "dpath")
- fpath = os.path.join(tempdir, "fpath.txt")
+ dpath = join(tempdir, "dpath")
+ fpath = join(tempdir, "fpath.txt")
- dlink = os.path.join(tempdir, "dlink")
- flink = os.path.join(tempdir, "flink.txt")
+ dlink = join(tempdir, "dlink")
+ flink = join(tempdir, "flink.txt")
import llnl.util.filesystem as fs
@@ -204,136 +96,24 @@ def _windows_can_symlink() -> bool:
return can_symlink_directories and can_symlink_files
-def _windows_create_link(source: str, link: str):
- """
- Attempts to create a Hard Link or Junction as an alternative
- to a symbolic link. This is called when symbolic links cannot
- be created.
- """
- if sys.platform != "win32":
- raise SymlinkError("windows_create_link method can't be used on non-Windows OS.")
- elif os.path.isdir(source):
- _windows_create_junction(source=source, link=link)
- elif os.path.isfile(source):
- _windows_create_hard_link(path=source, link=link)
- else:
- raise SymlinkError(
- f"Cannot create link from {source}. It is neither a file nor a directory."
- )
-
-
-def _windows_create_junction(source: str, link: str):
- """Duly verify that the path and link are eligible to create a junction,
- then create the junction.
+def _win32_is_junction(path):
"""
- if sys.platform != "win32":
- raise SymlinkError("windows_create_junction method can't be used on non-Windows OS.")
- elif not os.path.exists(source):
- raise SymlinkError("Source path does not exist, cannot create a junction.")
- elif os.path.lexists(link):
- raise SymlinkError("Link path already exists, cannot create a junction.")
- elif not os.path.isdir(source):
- raise SymlinkError("Source path is not a directory, cannot create a junction.")
-
- import subprocess
-
- cmd = ["cmd", "/C", "mklink", "/J", link, source]
- proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- out, err = proc.communicate()
- tty.debug(out.decode())
- if proc.returncode != 0:
- err = err.decode()
- tty.error(err)
- raise SymlinkError("Make junction command returned a non-zero return code.", err)
-
-
-def _windows_create_hard_link(path: str, link: str):
- """Duly verify that the path and link are eligible to create a hard
- link, then create the hard link.
+ Determines if a path is a win32 junction
"""
- if sys.platform != "win32":
- raise SymlinkError("windows_create_hard_link method can't be used on non-Windows OS.")
- elif not os.path.exists(path):
- raise SymlinkError(f"File path {path} does not exist. Cannot create hard link.")
- elif os.path.lexists(link):
- raise SymlinkError(f"Link path ({link}) already exists. Cannot create hard link.")
- elif not os.path.isfile(path):
- raise SymlinkError(f"File path ({link}) is not a file. Cannot create hard link.")
- else:
- tty.debug(f"Creating hard link {link} pointing to {path}")
- CreateHardLink(link, path)
-
-
-def readlink(path: str):
- """Spack utility to override of os.readlink method to work cross platform"""
- if _windows_is_hardlink(path):
- return _windows_read_hard_link(path)
- elif _windows_is_junction(path):
- return _windows_read_junction(path)
- else:
- return os.readlink(path)
-
-
-def _windows_read_hard_link(link: str) -> str:
- """Find all of the files that point to the same inode as the link"""
- if sys.platform != "win32":
- raise SymlinkError("Can't read hard link on non-Windows OS.")
- link = os.path.abspath(link)
- fsutil_cmd = ["fsutil", "hardlink", "list", link]
- proc = subprocess.Popen(fsutil_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
- out, err = proc.communicate()
- if proc.returncode != 0:
- raise SymlinkError(f"An error occurred while reading hard link: {err.decode()}")
-
- # fsutil response does not include the drive name, so append it back to each linked file.
- drive, link_tail = os.path.splitdrive(os.path.abspath(link))
- links = set([os.path.join(drive, p) for p in out.decode().splitlines()])
- links.remove(link)
- if len(links) == 1:
- return links.pop()
- elif len(links) > 1:
- # TODO: How best to handle the case where 3 or more paths point to a single inode?
- raise SymlinkError(f"Found multiple paths pointing to the same inode {links}")
- else:
- raise SymlinkError("Cannot determine hard link source path.")
-
-
-def _windows_read_junction(link: str):
- """Find the path that a junction points to."""
- if sys.platform != "win32":
- raise SymlinkError("Can't read junction on non-Windows OS.")
+ if os.path.islink(path):
+ return False
- link = os.path.abspath(link)
- link_basename = os.path.basename(link)
- link_parent = os.path.dirname(link)
- fsutil_cmd = ["dir", "/a:l", link_parent]
- proc = subprocess.Popen(fsutil_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
- out, err = proc.communicate()
- if proc.returncode != 0:
- raise SymlinkError(f"An error occurred while reading junction: {err.decode()}")
- matches = re.search(rf"<JUNCTION>\s+{link_basename} \[(.*)]", out.decode())
- if matches:
- return matches.group(1)
- else:
- raise SymlinkError("Could not find junction path.")
+ if sys.platform == "win32":
+ import ctypes.wintypes
+ GetFileAttributes = ctypes.windll.kernel32.GetFileAttributesW
+ GetFileAttributes.argtypes = (ctypes.wintypes.LPWSTR,)
+ GetFileAttributes.restype = ctypes.wintypes.DWORD
-@system_path_filter
-def resolve_link_target_relative_to_the_link(link):
- """
- os.path.isdir uses os.path.exists, which for links will check
- the existence of the link target. If the link target is relative to
- the link, we need to construct a pathname that is valid from
- our cwd (which may not be the same as the link's directory)
- """
- target = readlink(link)
- if os.path.isabs(target):
- return target
- link_dir = os.path.dirname(os.path.abspath(link))
- return os.path.join(link_dir, target)
+ INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF
+ FILE_ATTRIBUTE_REPARSE_POINT = 0x400
+ res = GetFileAttributes(path)
+ return res != INVALID_FILE_ATTRIBUTES and bool(res & FILE_ATTRIBUTE_REPARSE_POINT)
-class SymlinkError(SpackError):
- """Exception class for errors raised while creating symlinks,
- junctions and hard links
- """
+ return False