mount: tell why FUSE support is unavailable, see #8657

- fuse_impl: record the import error of each FUSE implementation that
  failed to load. also catch non-ImportError failures: mfusepy raises
  OSError at import time if libfuse is missing (and AttributeError /
  NotImplementedError in other cases), which previously crashed borg
  with a traceback instead of a clean error.
- borg mount: show the recorded errors, so users see the actual cause
  (e.g. a missing shared library) instead of just "no FUSE support".
- sysinfo: report mfusepy as the active FUSE implementation (it showed
  "fuse: None" although mounting worked) and include the recorded
  import errors if no implementation loaded, so debug output pasted
  into issues shows why.
- docs: note that third-party binaries might lack FUSE support.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Thomas Waldmann
2026-08-21 12:40:38 +02:00
co-authored by Claude Fable 5
parent d6e64fd98b
commit 2927349210
5 changed files with 61 additions and 13 deletions
+6
View File
@@ -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``::
+4 -2
View File
@@ -18,10 +18,12 @@ class MountMixIn:
"""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**")
+9 -6
View File
@@ -12,13 +12,16 @@ BORG_FUSE_IMPL = os.environ.get("BORG_FUSE_IMPL", "mfusepy,pyfuse3,llfuse")
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 @@ for FUSE_IMPL in BORG_FUSE_IMPL.split(","):
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 @@ for FUSE_IMPL in BORG_FUSE_IMPL.split(","):
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
+8 -5
View File
@@ -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
from ..fuse_impl import hlfuse, llfuse, BORG_FUSE_IMPL, fuse_import_errors
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}]"
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()))
+34
View File
@@ -5,7 +5,11 @@ can not convert as a ValueError - both FUSE adapters have to turn these into the
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