diff --git a/dvc/info.py b/dvc/info.py index 5885aea11..d7d213847 100644 --- a/dvc/info.py +++ b/dvc/info.py @@ -1,3 +1,4 @@ +import errno import itertools import os import pathlib @@ -7,7 +8,7 @@ import uuid import psutil from dvc import __version__ -from dvc.exceptions import DvcException, NotDvcRepoError +from dvc.exceptions import NotDvcRepoError from dvc.fs import FS_MAP, get_fs_cls, get_fs_config from dvc.repo import Repo from dvc.scm.base import SCMError @@ -103,7 +104,9 @@ def _get_linktype_support_info(repo): if is_link and not is_link(dst): status = "broken" os.unlink(dst) - except DvcException: + except OSError as exc: + if exc.errno != errno.ENOTSUP: + raise status = "not supported" if status == "supported": diff --git a/dvc/objects/checkout.py b/dvc/objects/checkout.py index 020df6a6a..92cfc97a5 100644 --- a/dvc/objects/checkout.py +++ b/dvc/objects/checkout.py @@ -1,3 +1,4 @@ +import errno import logging from itertools import chain @@ -77,7 +78,9 @@ def _try_links(cache, from_info, to_info, link_types): _verify_link(cache, to_info, link_types[0]) return - except DvcException as exc: + except OSError as exc: + if exc.errno not in [errno.EXDEV, errno.ENOTSUP]: + raise logger.debug( "Cache type '%s' is not supported: %s", link_types[0], exc ) diff --git a/dvc/system.py b/dvc/system.py index fc9180c56..43a0e4e18 100644 --- a/dvc/system.py +++ b/dvc/system.py @@ -3,34 +3,29 @@ import logging import os import platform -from dvc.exceptions import DvcException - logger = logging.getLogger(__name__) class System: @staticmethod def hardlink(source, link_name): - try: - os.link(source, link_name) - except OSError as exc: - raise DvcException("failed to link") from exc + os.link(source, link_name) @staticmethod def symlink(source, link_name): - try: - os.symlink(source, link_name) - except OSError as exc: - raise DvcException("failed to symlink") from exc + os.symlink(source, link_name) @staticmethod def _reflink_darwin(src, dst): import ctypes + def _cdll(name): + return ctypes.CDLL(name, use_errno=True) + LIBC = "libc.dylib" LIBC_FALLBACK = "/usr/lib/libSystem.dylib" try: - clib = ctypes.CDLL(LIBC) + clib = _cdll(LIBC) except OSError as exc: logger.debug( "unable to access '{}' (errno '{}'). " @@ -39,40 +34,43 @@ class System: if exc.errno != errno.ENOENT: raise # NOTE: trying to bypass System Integrity Protection (SIP) - clib = ctypes.CDLL(LIBC_FALLBACK) + clib = _cdll(LIBC_FALLBACK) if not hasattr(clib, "clonefile"): - return -1 + raise OSError( + errno.ENOTSUP, + "'clonefile' not supported by the standard library", + ) clonefile = clib.clonefile clonefile.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int] clonefile.restype = ctypes.c_int - return clonefile( + ret = clonefile( ctypes.c_char_p(os.fsencode(src)), ctypes.c_char_p(os.fsencode(dst)), ctypes.c_int(0), ) - - @staticmethod - def _reflink_windows(_src, _dst): - return -1 + if ret: + err = ctypes.get_errno() + msg = os.strerror(err) + raise OSError(err, msg) @staticmethod def _reflink_linux(src, dst): import fcntl # pylint: disable=import-error + from funcy import suppress + FICLONE = 0x40049409 try: - ret = 255 with open(src, "rb") as s, open(dst, "wb+") as d: - ret = fcntl.ioctl(d.fileno(), FICLONE, s.fileno()) - finally: - if ret != 0: + fcntl.ioctl(d.fileno(), FICLONE, s.fileno()) + except OSError: + with suppress(OSError): os.unlink(dst) - - return ret + raise @staticmethod def reflink(source, link_name): @@ -81,20 +79,15 @@ class System: source, link_name = os.fspath(source), os.fspath(link_name) system = platform.system() - try: - if system == "Windows": - ret = System._reflink_windows(source, link_name) - elif system == "Darwin": - ret = System._reflink_darwin(source, link_name) - elif system == "Linux": - ret = System._reflink_linux(source, link_name) - else: - ret = -1 - except OSError: - ret = -1 - - if ret != 0: - raise DvcException("reflink is not supported") + if system == "Darwin": + System._reflink_darwin(source, link_name) + elif system == "Linux": + System._reflink_linux(source, link_name) + else: + raise OSError( + errno.ENOTSUP, + f"reflink is not supported on '{system}'", + ) # NOTE: reflink has a new inode, but has the same mode as the src, # so we need to chmod it to look like a normal copy. diff --git a/dvc/utils/fs.py b/dvc/utils/fs.py index 15be5013d..ef4860c51 100644 --- a/dvc/utils/fs.py +++ b/dvc/utils/fs.py @@ -206,7 +206,7 @@ def copyfile(src, dest, callback=None, no_progress_bar=False, name=None): try: System.reflink(src, dest) - except DvcException: + except OSError: from dvc.progress import tdqm_or_callback_wrapped with open(src, "rb") as fsrc, open(dest, "wb+") as fdest: diff --git a/tests/func/test_add.py b/tests/func/test_add.py index eacc44a51..fc504e361 100644 --- a/tests/func/test_add.py +++ b/tests/func/test_add.py @@ -958,7 +958,7 @@ def test_add_with_cache_link_error(tmp_dir, dvc, mocker, capsys): mocker.patch( "dvc.objects.checkout._do_link", - side_effect=DvcException("link failed"), + side_effect=OSError(errno.ENOTSUP, "link failed"), ) dvc.add("foo") err = capsys.readouterr()[1]