Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,8 @@ jobs:

env:
PY_COLORS: 1
MSYS2_ARG_CONV_EXCL: "*"
MSYS2_ENV_CONV_EXCL: "*"

defaults:
run:
Expand Down
23 changes: 23 additions & 0 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,29 @@ Install the dependencies with the provided script::

./scripts/msys2-install-deps

.. _msys2_path_translation:

MSYS2 Path Translation
++++++++++++++++++++++

When running Borg within an MSYS2 environment, the shell
automatically translates POSIX-style paths (like ``/tmp`` or ``/C/Users``) to
Windows paths (like ``C:\msys64\tmp`` or ``C:\Users``) before they reach the
Borg process.

This behavior can result in absolute Windows paths being stored in your backups,
which may not be what you intended if you use POSIX paths for portability.

To disable this automatic translation for Borg, you can use environment variables
to exclude everything from conversion. Similarly, MSYS2 also translates
environment variables that look like paths. To disable this generally for Borg,
set both variables::

export MSYS2_ARG_CONV_EXCL="*"
export MSYS2_ENV_CONV_EXCL="*"

For more details, see the `MSYS2 documentation on filesystem paths <https://www.msys2.org/docs/filesystem-paths/>`__.

Windows 10's Linux Subsystem
++++++++++++++++++++++++++++

Expand Down
12 changes: 11 additions & 1 deletion src/borg/archiver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
from ..helpers import msgpack
from ..helpers import sig_int
from ..helpers import get_config_dir
from ..platformflags import is_msystem
from ..remote import RemoteRepository
from ..selftest import selftest
except BaseException:
Expand Down Expand Up @@ -397,7 +398,16 @@ def get_func(self, args, parser):
return functools.partial(self.do_maincommand_help, parser)

def prerun_checks(self, logger, is_serve):

if (
not is_serve
and is_msystem
and ("MSYS2_ARG_CONV_EXCL" not in os.environ or "MSYS2_ENV_CONV_EXCL" not in os.environ)
):
logger.warning(
"MSYS2 path translation is active. This can cause POSIX paths to be mangled into "
"Windows paths in archives. Consider setting MSYS2_ARG_CONV_EXCL='*' and "
"MSYS2_ENV_CONV_EXCL='*'. See https://www.msys2.org/docs/filesystem-paths/ for details."
)
selftest(logger)

def _setup_implied_logging(self, args):
Expand Down
4 changes: 4 additions & 0 deletions src/borg/platformflags.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Use these flags instead of sys.platform.startswith('<os>') or try/except.
"""

import os
import sys

is_win32 = sys.platform.startswith("win32")
Expand All @@ -15,3 +16,6 @@
is_openbsd = sys.platform.startswith("openbsd")
is_darwin = sys.platform.startswith("darwin")
is_haiku = sys.platform.startswith("haiku")

# MSYS2 (on Windows)
is_msystem = is_win32 and "MSYSTEM" in os.environ
40 changes: 40 additions & 0 deletions src/borg/testsuite/archiver/create_cmd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from ...constants import zeros
from ...manifest import Manifest
from ...platform import is_win32
from ...platformflags import is_msystem
from ...repository import Repository
from ...helpers import CommandError, BackupPermissionError
from .. import has_lchflags, has_mknod
Expand Down Expand Up @@ -150,6 +151,45 @@ def test_archived_paths(archivers, request):
assert expected_paths == sorted([json.loads(line)["path"] for line in archive_list.splitlines() if line])


@pytest.mark.skipif(not is_msystem, reason="only for msystem")
def test_create_msys2_path_translation_warning(archivers, request, monkeypatch):
archiver = request.getfixturevalue(archivers)
cmd(archiver, "repo-create", RK_ENCRYPTION)
create_regular_file(archiver.input_path, "test")

# When MSYS2 path translation is active (variables NOT set), a warning should be emitted.
monkeypatch.delenv("MSYS2_ARG_CONV_EXCL", raising=False)
monkeypatch.delenv("MSYS2_ENV_CONV_EXCL", raising=False)
output = cmd(archiver, "create", "test1", "input", fork=True)
assert "MSYS2 path translation is active." in output

# When the variables ARE set, the warning should not be emitted,
# and /tmp should be archived properly without being translated to msys64/tmp.
monkeypatch.setenv("MSYS2_ARG_CONV_EXCL", "*")
monkeypatch.setenv("MSYS2_ENV_CONV_EXCL", "*")

# We must create a real /tmp directory to avoid file not found errors,
# since we will pass '/tmp' directly to Borg
tmp_path = os.path.abspath("/tmp")
os.makedirs(tmp_path, exist_ok=True)
test_filepath = os.path.join(tmp_path, "borg_msys2_test_file")
with open(test_filepath, "w") as f:
f.write("test")

try:
output2 = cmd(archiver, "create", "test2", "/tmp", fork=True)
assert "MSYS2 path translation is active." not in output2

archive_list = cmd(archiver, "list", "test2", "--json-lines")
paths = [json.loads(line)["path"] for line in archive_list.splitlines() if line]

# Verify that msys64 is not present and paths start with tmp/
assert not any("msys64" in p for p in paths)
assert any(p.startswith("tmp/borg_msys2_test_file") for p in paths)
finally:
os.unlink(test_filepath)


@requires_hardlinks
def test_create_duplicate_root(archivers, request):
archiver = request.getfixturevalue(archivers)
Expand Down
Loading