From a02fe71ae37d212078ce41d3515e46fbd7eb0a55 Mon Sep 17 00:00:00 2001 From: Mohammed Alkindi Date: Thu, 10 Sep 2026 00:40:45 +0400 Subject: [PATCH] fix: fall back to an absolute path when relpath crosses drives os.path.relpath raises ValueError when the two paths are on different drives or UNC mounts, so _resolve_relative_to crashed sam build whenever the template and the build directory were not on the same drive. Return the absolute path in that case, which is what a relative path cannot express anyway. Closes #3474. --- samcli/commands/_utils/template.py | 18 +++++++++++++----- tests/unit/commands/_utils/test_template.py | 17 ++++++++++++++++- 2 files changed, 29 insertions(+), 6 deletions(-) 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)