diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd7ba84d3c..2c0d33a244 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -626,6 +626,8 @@ jobs: env: PY_COLORS: 1 + MSYS2_ARG_CONV_EXCL: "*" + MSYS2_ENV_CONV_EXCL: "*" defaults: run: diff --git a/docs/installation.rst b/docs/installation.rst index 8272cd0831..f37609534c 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -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 `__. + Windows 10's Linux Subsystem ++++++++++++++++++++++++++++ diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index 591f02913c..dadb4c7d83 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -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: @@ -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): diff --git a/src/borg/platformflags.py b/src/borg/platformflags.py index 9abf7d0652..cc84f7c7ac 100644 --- a/src/borg/platformflags.py +++ b/src/borg/platformflags.py @@ -4,6 +4,7 @@ Use these flags instead of sys.platform.startswith('') or try/except. """ +import os import sys is_win32 = sys.platform.startswith("win32") @@ -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 diff --git a/src/borg/testsuite/archiver/create_cmd_test.py b/src/borg/testsuite/archiver/create_cmd_test.py index 3536adde63..2562aa3779 100644 --- a/src/borg/testsuite/archiver/create_cmd_test.py +++ b/src/borg/testsuite/archiver/create_cmd_test.py @@ -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 @@ -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)