diff --git a/cli/src/mpftp/sidecar.py b/cli/src/mpftp/sidecar.py index 958e069..1991ec1 100644 --- a/cli/src/mpftp/sidecar.py +++ b/cli/src/mpftp/sidecar.py @@ -204,6 +204,17 @@ def friendly_exec_timeout_message(detail: str) -> str: ) + +def _windows_spelling(path: str) -> Optional[str]: + """``C:\\x\\y`` for a WSL mount path ``/mnt/c/x/y`` when running on + Windows; None when there's no such spelling.""" + if os.name != "nt" or not path.startswith("/mnt/") or len(path) < 6: + return None + drive, rest = path[5], path[6:] + if not drive.isalpha() or (rest and not rest.startswith("/")): + return None + return drive.upper() + ":\\" + rest.lstrip("/").replace("/", "\\") + class FollowTimeoutError(RuntimeError): """Follow-exec timed out; ``partial_output`` is what the board printed first. @@ -3619,7 +3630,12 @@ def _cp_local_to_remote( ) -> None: src_p = Path(src) if not src_p.exists(): - raise RuntimeError(f"local path not found: {src}") + # Under WSL the CLI is Linux and this sidecar is Windows Python, + # so a path the CLI resolved (/mnt/c/...) needs Windows' spelling. + alt = _windows_spelling(src) + if alt is None or not Path(alt).exists(): + raise RuntimeError(f"local path not found: {src}") + src, src_p = alt, Path(alt) msc_root = self._circuitpy_msc_root() if msc_root is not None: diff --git a/cli/tests/test_put.py b/cli/tests/test_put.py index 2f77a74..5738c68 100644 --- a/cli/tests/test_put.py +++ b/cli/tests/test_put.py @@ -83,3 +83,22 @@ def test_put_file_still_uses_fs_write(self): if __name__ == "__main__": unittest.main() + + +class WindowsSpellingTests(unittest.TestCase): + """A recursive put from WSL hands the Windows sidecar a /mnt/ path.""" + + def test_mount_paths_get_a_drive_letter_on_windows(self): + from mpftp import sidecar + + with mock.patch.object(sidecar.os, "name", "nt"): + self.assertEqual(sidecar._windows_spelling("/mnt/c/Users/me/lib"), "C:\\Users\\me\\lib") + self.assertEqual(sidecar._windows_spelling("/mnt/d"), "D:\\") + self.assertIsNone(sidecar._windows_spelling("/home/me/lib")) + self.assertIsNone(sidecar._windows_spelling("/mnt/cd/x")) + + def test_nothing_changes_off_windows(self): + from mpftp import sidecar + + with mock.patch.object(sidecar.os, "name", "posix"): + self.assertIsNone(sidecar._windows_spelling("/mnt/c/Users/me/lib"))