diff --git a/samcli/commands/_utils/template.py b/samcli/commands/_utils/template.py index a0056e35863..3a0a7695db4 100644 --- a/samcli/commands/_utils/template.py +++ b/samcli/commands/_utils/template.py @@ -399,11 +399,19 @@ def _resolve_relative_to(path, original_root, new_root): return None # Value is definitely a relative path. Change it relative to the destination directory - return os.path.relpath( - # Resolve the paths to take care of symlinks - os.path.normpath(os.path.join(pathlib.Path(original_root).resolve(), path)), - pathlib.Path(new_root).resolve(), # Absolute original path w.r.t ``original_root`` - ) # Resolve the original path with respect to ``new_root`` + # Resolve the paths to take care of symlinks + absolute_path = os.path.normpath(os.path.join(pathlib.Path(original_root).resolve(), path)) + try: + return os.path.relpath( + absolute_path, + pathlib.Path(new_root).resolve(), # Absolute original path w.r.t ``original_root`` + ) # Resolve the original path with respect to ``new_root`` + except ValueError: + # os.path.relpath raises ValueError when the two paths are on different drives or + # UNC mounts, which happens on Windows when the template and the build directory + # are not on the same drive. A relative path cannot express that, so fall back to + # the absolute path rather than letting the exception escape. + return absolute_path def get_template_parameters(template_file): diff --git a/tests/unit/commands/_utils/test_template.py b/tests/unit/commands/_utils/test_template.py index ae535c62433..83a95832832 100644 --- a/tests/unit/commands/_utils/test_template.py +++ b/tests/unit/commands/_utils/test_template.py @@ -1,7 +1,9 @@ import copy import os +import pathlib +import platform import tempfile -from unittest import TestCase +from unittest import TestCase, skipIf from unittest.mock import patch, mock_open, MagicMock import shutil @@ -803,6 +805,19 @@ def test_must_resolve_relative_to_symlinked_original_root_and_new_root(self): self.assertEqual(result, expected_result) + @skipIf(platform.system() != "Windows", "Different drives only exist on Windows") + def test_must_resolve_relative_to_across_different_drives(self): + # os.path.relpath raises ValueError when the two paths are on different drives, + # so a template on one drive and a --build-dir on another cannot be expressed + # relatively. Fall back to the absolute path rather than crashing. + original_root = os.path.join("C:" + os.sep, "src") + new_root = os.path.join("D:" + os.sep, "destination") + expected_result = os.path.normpath(os.path.join(pathlib.Path(original_root).resolve(), self.curpath)) + + result = _resolve_relative_to(self.curpath, original_root, new_root) + + self.assertEqual(result, expected_result) + def create_symlink(self, src, dest): os.makedirs(src) os.symlink(src, dest)