Skip to content

Commit d7bbcfb

Browse files
committed
archiver: warn about MSYS2 path translation, fixes #9339
This adds a runtime warning when running under MSYS2/Git Bash without the necessary environment variables to disable automatic path translation. The documentation is also updated to explain this behavior and how to mitigate it.
1 parent 1706e0d commit d7bbcfb

5 files changed

Lines changed: 80 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,8 @@ jobs:
626626

627627
env:
628628
PY_COLORS: 1
629+
MSYS2_ARG_CONV_EXCL: "*"
630+
MSYS2_ENV_CONV_EXCL: "*"
629631

630632
defaults:
631633
run:

docs/installation.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,29 @@ Install the dependencies with the provided script::
326326

327327
./scripts/msys2-install-deps
328328

329+
.. _msys2_path_translation:
330+
331+
MSYS2 Path Translation
332+
++++++++++++++++++++++
333+
334+
When running Borg within an MSYS2 environment, the shell
335+
automatically translates POSIX-style paths (like ``/tmp`` or ``/C/Users``) to
336+
Windows paths (like ``C:\msys64\tmp`` or ``C:\Users``) before they reach the
337+
Borg process.
338+
339+
This behavior can result in absolute Windows paths being stored in your backups,
340+
which may not be what you intended if you use POSIX paths for portability.
341+
342+
To disable this automatic translation for Borg, you can use environment variables
343+
to exclude everything from conversion. Similarly, MSYS2 also translates
344+
environment variables that look like paths. To disable this generally for Borg,
345+
set both variables::
346+
347+
export MSYS2_ARG_CONV_EXCL="*"
348+
export MSYS2_ENV_CONV_EXCL="*"
349+
350+
For more details, see the `MSYS2 documentation on filesystem paths <https://www.msys2.org/docs/filesystem-paths/>`__.
351+
329352
Windows 10's Linux Subsystem
330353
++++++++++++++++++++++++++++
331354

src/borg/archiver/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from ..helpers import msgpack
4747
from ..helpers import sig_int
4848
from ..helpers import get_config_dir
49+
from ..platformflags import is_msystem
4950
from ..remote import RemoteRepository
5051
from ..selftest import selftest
5152
except BaseException:
@@ -397,7 +398,16 @@ def get_func(self, args, parser):
397398
return functools.partial(self.do_maincommand_help, parser)
398399

399400
def prerun_checks(self, logger, is_serve):
400-
401+
if (
402+
not is_serve
403+
and is_msystem
404+
and ("MSYS2_ARG_CONV_EXCL" not in os.environ or "MSYS2_ENV_CONV_EXCL" not in os.environ)
405+
):
406+
logger.warning(
407+
"MSYS2 path translation is active. This can cause POSIX paths to be mangled into "
408+
"Windows paths in archives. Consider setting MSYS2_ARG_CONV_EXCL='*' and "
409+
"MSYS2_ENV_CONV_EXCL='*'. See https://www.msys2.org/docs/filesystem-paths/ for details."
410+
)
401411
selftest(logger)
402412

403413
def _setup_implied_logging(self, args):

src/borg/platformflags.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
Use these flags instead of sys.platform.startswith('<os>') or try/except.
55
"""
66

7+
import os
78
import sys
89

910
is_win32 = sys.platform.startswith("win32")
@@ -15,3 +16,6 @@
1516
is_openbsd = sys.platform.startswith("openbsd")
1617
is_darwin = sys.platform.startswith("darwin")
1718
is_haiku = sys.platform.startswith("haiku")
19+
20+
# MSYS2 (on Windows)
21+
is_msystem = is_win32 and "MSYSTEM" in os.environ

src/borg/testsuite/archiver/create_cmd_test.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from ...constants import zeros
1515
from ...manifest import Manifest
1616
from ...platform import is_win32
17+
from ...platformflags import is_msystem
1718
from ...repository import Repository
1819
from ...helpers import CommandError, BackupPermissionError
1920
from .. import has_lchflags, has_mknod
@@ -150,6 +151,45 @@ def test_archived_paths(archivers, request):
150151
assert expected_paths == sorted([json.loads(line)["path"] for line in archive_list.splitlines() if line])
151152

152153

154+
@pytest.mark.skipif(not is_msystem, reason="only for msystem")
155+
def test_create_msys2_path_translation_warning(archivers, request, monkeypatch):
156+
archiver = request.getfixturevalue(archivers)
157+
cmd(archiver, "repo-create", RK_ENCRYPTION)
158+
create_regular_file(archiver.input_path, "test")
159+
160+
# When MSYS2 path translation is active (variables NOT set), a warning should be emitted.
161+
monkeypatch.delenv("MSYS2_ARG_CONV_EXCL", raising=False)
162+
monkeypatch.delenv("MSYS2_ENV_CONV_EXCL", raising=False)
163+
output = cmd(archiver, "create", "test1", "input", fork=True)
164+
assert "MSYS2 path translation is active." in output
165+
166+
# When the variables ARE set, the warning should not be emitted,
167+
# and /tmp should be archived properly without being translated to msys64/tmp.
168+
monkeypatch.setenv("MSYS2_ARG_CONV_EXCL", "*")
169+
monkeypatch.setenv("MSYS2_ENV_CONV_EXCL", "*")
170+
171+
# We must create a real /tmp directory to avoid file not found errors,
172+
# since we will pass '/tmp' directly to Borg
173+
tmp_path = os.path.abspath("/tmp")
174+
os.makedirs(tmp_path, exist_ok=True)
175+
test_filepath = os.path.join(tmp_path, "borg_msys2_test_file")
176+
with open(test_filepath, "w") as f:
177+
f.write("test")
178+
179+
try:
180+
output2 = cmd(archiver, "create", "test2", "/tmp", fork=True)
181+
assert "MSYS2 path translation is active." not in output2
182+
183+
archive_list = cmd(archiver, "list", "test2", "--json-lines")
184+
paths = [json.loads(line)["path"] for line in archive_list.splitlines() if line]
185+
186+
# Verify that msys64 is not present and paths start with tmp/
187+
assert not any("msys64" in p for p in paths)
188+
assert any(p.startswith("tmp/borg_msys2_test_file") for p in paths)
189+
finally:
190+
os.unlink(test_filepath)
191+
192+
153193
@requires_hardlinks
154194
def test_create_duplicate_root(archivers, request):
155195
archiver = request.getfixturevalue(archivers)

0 commit comments

Comments
 (0)