diff --git a/docs/installation.rst b/docs/installation.rst index 62c326098f..2012e89561 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -118,6 +118,12 @@ are available on the releases_ page for the following platforms: ARM binaries are built by Johann Bauer, see: https://borg.bauerj.eu/ +.. note:: ``borg mount`` only works if the binary was built with FUSE support + (third-party binaries might lack it) and your OS has FUSE installed + (e.g. the ``fuse3`` package on Linux, macFUSE on macOS). + If ``borg mount`` fails, the error message will tell for each FUSE + implementation why it could not be loaded. + To install such a binary, just drop it into a directory in your ``PATH``, make borg readable and executable for its users and then you can run ``borg``:: diff --git a/src/borg/archiver/mount_cmds.py b/src/borg/archiver/mount_cmds.py index 162b053e4f..27071fee73 100644 --- a/src/borg/archiver/mount_cmds.py +++ b/src/borg/archiver/mount_cmds.py @@ -18,10 +18,12 @@ def do_mount(self, args): """Mounts an archive or an entire repository as a FUSE filesystem.""" # Perform these checks before opening the repository and asking for a passphrase. - from ..fuse_impl import llfuse, has_mfusepy, BORG_FUSE_IMPL + from ..fuse_impl import llfuse, has_mfusepy, BORG_FUSE_IMPL, fuse_import_errors if llfuse is None and not has_mfusepy: - raise RTError("borg mount not available: no FUSE support, BORG_FUSE_IMPL=%s." % BORG_FUSE_IMPL) + msg = "borg mount not available: no FUSE support, BORG_FUSE_IMPL=%s." % BORG_FUSE_IMPL + msg += "".join(f"\nimport of {impl} failed: {err}" for impl, err in fuse_import_errors.items()) + raise RTError(msg) if not os.path.isdir(args.mountpoint): raise RTError(f"{args.mountpoint}: Mountpoint must be an **existing directory**") diff --git a/src/borg/fuse_impl.py b/src/borg/fuse_impl.py index 6e761d9265..0ea2c67850 100644 --- a/src/borg/fuse_impl.py +++ b/src/borg/fuse_impl.py @@ -12,13 +12,16 @@ hlfuse: types.ModuleType | None = None llfuse: types.ModuleType | None = None +# import failures by implementation name, see issue #8657 +fuse_import_errors: dict[str, str] = {} + for FUSE_IMPL in BORG_FUSE_IMPL.split(","): FUSE_IMPL = FUSE_IMPL.strip() if FUSE_IMPL == "pyfuse3": try: import pyfuse3 - except ImportError: - pass + except Exception as err: + fuse_import_errors[FUSE_IMPL] = str(err) else: llfuse = pyfuse3 has_llfuse = False @@ -30,8 +33,8 @@ elif FUSE_IMPL == "llfuse": try: import llfuse as llfuse_module - except ImportError: - pass + except Exception as err: + fuse_import_errors[FUSE_IMPL] = str(err) else: llfuse = llfuse_module has_llfuse = True @@ -43,8 +46,8 @@ elif FUSE_IMPL == "mfusepy": try: import mfusepy - except ImportError: - pass + except Exception as err: # mfusepy raises OSError (not ImportError) if libfuse is missing + fuse_import_errors[FUSE_IMPL] = str(err) else: hlfuse = mfusepy has_llfuse = False diff --git a/src/borg/helpers/misc.py b/src/borg/helpers/misc.py index 4418671b5b..d6a027b632 100644 --- a/src/borg/helpers/misc.py +++ b/src/borg/helpers/misc.py @@ -36,11 +36,14 @@ def sysinfo(): msgpack_version = ".".join(str(v) for v in msgpack.version) except: # noqa msgpack_version = "unknown" - from ..fuse_impl import llfuse, BORG_FUSE_IMPL - - llfuse_name = llfuse.__name__ if llfuse else "None" - llfuse_version = (" %s" % llfuse.__version__) if llfuse else "" - llfuse_info = f"{llfuse_name}{llfuse_version} [{BORG_FUSE_IMPL}]" + from ..fuse_impl import hlfuse, llfuse, BORG_FUSE_IMPL, fuse_import_errors + + fuse_mod = hlfuse or llfuse + fuse_name = fuse_mod.__name__ if fuse_mod else "None" + fuse_version = getattr(fuse_mod, "__version__", "") if fuse_mod else "" + fuse_version = " %s" % fuse_version if fuse_version else "" + fuse_errors = "".join(f" {impl}: {err}." for impl, err in fuse_import_errors.items()) if not fuse_mod else "" + fuse_info = f"{fuse_name}{fuse_version} [{BORG_FUSE_IMPL}]{fuse_errors}" info = [] if uname is not None: info.append("Platform: {}".format(" ".join(uname))) @@ -48,7 +51,7 @@ def sysinfo(): info.append("Linux: %s %s %s" % linux_distribution) info.append( "Borg: {} Python: {} {} msgpack: {} fuse: {}".format( - borg_version, python_implementation, python_version, msgpack_version, llfuse_info + borg_version, python_implementation, python_version, msgpack_version, fuse_info ) ) info.append("PID: %d CWD: %s" % (os.getpid(), os.getcwd())) diff --git a/src/borg/testsuite/fuse_test.py b/src/borg/testsuite/fuse_test.py index e18440dc24..b16dbd14a3 100644 --- a/src/borg/testsuite/fuse_test.py +++ b/src/borg/testsuite/fuse_test.py @@ -5,7 +5,11 @@ errno, which is what we check here without mounting anything. """ +import builtins import errno +import importlib +import os +from unittest.mock import patch import pytest @@ -126,3 +130,33 @@ def test_mfusepy_getxattr_broken_acl(): with pytest.raises(hlfuse.FuseOSError) as excinfo: getxattr(ops, "/file", "system.posix_acl_access") assert excinfo.value.errno == errno.EIO + + +def test_fuse_import_errors_recorded(): + """A FUSE impl failing to import must not crash borg and must record why, see #8657.""" + import borg.fuse_impl + + real_import = builtins.__import__ + + def failing_import(name, *args, **kwargs): + if name == "mfusepy": + raise OSError("Unable to find libfuse") # mfusepy raises OSError, not ImportError + if name in ("pyfuse3", "llfuse"): + raise ImportError(f"No module named '{name}'") + return real_import(name, *args, **kwargs) + + try: + with ( + patch.object(builtins, "__import__", failing_import), + patch.dict(os.environ, {"BORG_FUSE_IMPL": "mfusepy,pyfuse3,llfuse"}), + ): + fuse_impl = importlib.reload(borg.fuse_impl) + assert not fuse_impl.has_any_fuse + assert fuse_impl.hlfuse is None and fuse_impl.llfuse is None + assert fuse_impl.fuse_import_errors == { + "mfusepy": "Unable to find libfuse", + "pyfuse3": "No module named 'pyfuse3'", + "llfuse": "No module named 'llfuse'", + } + finally: + importlib.reload(borg.fuse_impl) # restore the real state