From 9c57e86ad60f0dbe5a28a95e3c7787a81238b715 Mon Sep 17 00:00:00 2001 From: Vladimir Belitskiy Date: Mon, 17 Aug 2026 18:16:05 +0000 Subject: [PATCH 1/9] fix(windows): use extended paths in Python bootstraps Implicit long-path support is not universal across the Win32 API. The documented set of APIs covered by the long-path opt-in does not include DLL loading functions, e.g., LoadLibraryExW: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation#functions-without-max_path-restrictions Always use extended-length paths in Windows bootstrap code and correctly convert UNC paths to the \\?\UNC\ form. --- python/private/python_bootstrap_template.txt | 27 ++++++------------- python/private/site_init_template.py | 28 ++++++-------------- python/private/stage2_bootstrap_template.py | 27 ++++++------------- python/private/zipapp/zip_main_template.py | 20 ++++++-------- 4 files changed, 32 insertions(+), 70 deletions(-) diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt index 482918c038..8b8f095999 100644 --- a/python/private/python_bootstrap_template.txt +++ b/python/private/python_bootstrap_template.txt @@ -141,33 +141,22 @@ def get_windows_path_with_unc_prefix(path): if not IS_WINDOWS or sys.version_info[0] < 3: return path - # Starting in Windows 10, version 1607(OS build 14393), MAX_PATH limitations have been - # removed from common Win32 file and directory functions. - # Related doc: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later - import platform - win32_version = None - # Windows 2022 with Python 3.12.8 gives flakey errors, so try a couple times. - for _ in range(3): - try: - win32_version = platform.win32_ver()[1] - break - except (ValueError, KeyError): - pass - if win32_version and win32_version >= '10.0.14393': - return path - # import sysconfig only now to maintain python 2.6 compatibility import sysconfig if sysconfig.get_platform() == 'mingw': return path - # Lets start the unicode fun - unicode_prefix = '\\\\?\\' - if path.startswith(unicode_prefix): + # Implicit long-path support is not universal across the Win32 API. For + # example, DLL loading still requires an explicit extended-length prefix. + extended_path_prefix = '\\\\?\\' + if path.startswith(extended_path_prefix): return path # abspath returns a normalized absolute path - return unicode_prefix + abspath(path) + path = abspath(path) + if path.startswith('\\\\'): + return extended_path_prefix + 'UNC\\' + path[2:] + return extended_path_prefix + path def search_path(name): """Finds a file in a given search path.""" diff --git a/python/private/site_init_template.py b/python/private/site_init_template.py index 12be98eb57..c324981dea 100644 --- a/python/private/site_init_template.py +++ b/python/private/site_init_template.py @@ -93,35 +93,23 @@ def _get_windows_path_with_unc_prefix(path): if not _is_windows() or sys.version_info[0] < 3: return path - # Starting in Windows 10, version 1607(OS build 14393), MAX_PATH limitations have been - # removed from common Win32 file and directory functions. - # Related doc: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later - import platform - - win32_version = None - # Windows 2022 with Python 3.12.8 gives flakey errors, so try a couple times. - for _ in range(3): - try: - win32_version = platform.win32_ver()[1] - break - except (ValueError, KeyError): - pass - if win32_version and win32_version >= "10.0.14393": - return path - # import sysconfig only now to maintain python 2.6 compatibility import sysconfig if sysconfig.get_platform() == "mingw": return path - # Lets start the unicode fun - unicode_prefix = "\\\\?\\" - if path.startswith(unicode_prefix): + # Implicit long-path support is not universal across the Win32 API. For + # example, DLL loading still requires an explicit extended-length prefix. + extended_path_prefix = "\\\\?\\" + if path.startswith(extended_path_prefix): return path # os.path.abspath returns a normalized absolute path - return unicode_prefix + os.path.abspath(path) + path = os.path.abspath(path) + if path.startswith("\\\\"): + return extended_path_prefix + "UNC\\" + path[2:] + return extended_path_prefix + path def _search_path(name): diff --git a/python/private/stage2_bootstrap_template.py b/python/private/stage2_bootstrap_template.py index f445ad2b6a..cb064dc0a6 100644 --- a/python/private/stage2_bootstrap_template.py +++ b/python/private/stage2_bootstrap_template.py @@ -113,34 +113,23 @@ def get_windows_path_with_unc_prefix(path): if not IS_WINDOWS or sys.version_info[0] < 3: return path - # Starting in Windows 10, version 1607(OS build 14393), MAX_PATH limitations have been - # removed from common Win32 file and directory functions. - # Related doc: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later - import platform - - win32_version = None - # Windows 2022 with Python 3.12.8 gives flakey errors, so try a couple times. - for _ in range(3): - try: - win32_version = platform.win32_ver()[1] - break - except (ValueError, KeyError): - pass - if win32_version and win32_version >= "10.0.14393": - return path - # import sysconfig only now to maintain python 2.6 compatibility import sysconfig if sysconfig.get_platform() == "mingw": return path - # Lets start the unicode fun - if path.startswith(unicode_prefix): # noqa: F821 + # Implicit long-path support is not universal across the Win32 API. For + # example, DLL loading still requires an explicit extended-length prefix. + extended_path_prefix = "\\\\?\\" + if path.startswith(extended_path_prefix): return path # os.path.abspath returns a normalized absolute path - return unicode_prefix + os.path.abspath(path) # noqa: F821 + path = os.path.abspath(path) + if path.startswith("\\\\"): + return extended_path_prefix + "UNC\\" + path[2:] + return extended_path_prefix + path def print_verbose(*args, mapping=None, values=None): diff --git a/python/private/zipapp/zip_main_template.py b/python/private/zipapp/zip_main_template.py index 709e08815c..d5e2e74943 100644 --- a/python/private/zipapp/zip_main_template.py +++ b/python/private/zipapp/zip_main_template.py @@ -103,27 +103,23 @@ def get_windows_path_with_unc_prefix(path): if not IS_WINDOWS or sys.version_info[0] < 3: return path - # Starting in Windows 10, version 1607(OS build 14393), MAX_PATH limitations have been - # removed from common Win32 file and directory functions. - # Related doc: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later - import platform - - if platform.win32_ver()[1] >= "10.0.14393": - return path - # import sysconfig only now to maintain python 2.6 compatibility import sysconfig if sysconfig.get_platform() == "mingw": return path - # Lets start the unicode fun - unicode_prefix = "\\\\?\\" - if path.startswith(unicode_prefix): + # Implicit long-path support is not universal across the Win32 API. For + # example, DLL loading still requires an explicit extended-length prefix. + extended_path_prefix = "\\\\?\\" + if path.startswith(extended_path_prefix): return path # os.path.abspath returns a normalized absolute path - return unicode_prefix + os.path.abspath(path) + path = os.path.abspath(path) + if path.startswith("\\\\"): + return extended_path_prefix + "UNC\\" + path[2:] + return extended_path_prefix + path def search_path(name): From 52ef9398b22803d299bf1f0eee4f576b667d4a9e Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 18 Aug 2026 20:22:51 -0700 Subject: [PATCH 2/9] fix(windows): normalize extended path prefixes in runfiles and fix bootstrap path order Extended-length path prefixes (\\?\ and \\?\UNC\) added by bootstrap templates caused Runfiles.CurrentRepository() to fail with a mount mismatch ValueError when computing relative paths against standard drive roots. Additionally, forward-slash paths could be corrupted when checking the prefix before converting to an absolute path. - Normalize \\?\ and \\?\UNC\ prefixes and path separators in Runfiles._normalize_windows_path() and use ntpath on Windows. - Call abspath() before checking extended path prefixes across all bootstrap templates. - Fix inverted Windows logic in zip_main_template.py create_runfiles_root(). - Add unit tests for Windows path normalization in tests/runfiles/runfiles_test.py. - Add news fragment news/4071.fixed.md. --- news/4071.fixed.md | 3 + python/private/python_bootstrap_template.txt | 4 +- python/private/site_init_template.py | 4 +- python/private/stage2_bootstrap_template.py | 4 +- python/private/zipapp/zip_main_template.py | 6 +- python/runfiles/runfiles.py | 65 +++++++++++--------- tests/runfiles/runfiles_test.py | 58 ++++++++++++++++- 7 files changed, 105 insertions(+), 39 deletions(-) create mode 100644 news/4071.fixed.md diff --git a/news/4071.fixed.md b/news/4071.fixed.md new file mode 100644 index 0000000000..ecf29b760d --- /dev/null +++ b/news/4071.fixed.md @@ -0,0 +1,3 @@ +(windows) Fixed long path DLL loading in {obj}`py_binary` and +{obj}`py_test` bootstraps +([#4071](https://github.com/bazel-contrib/rules_python/pull/4071)). diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt index 8b8f095999..68e3687aeb 100644 --- a/python/private/python_bootstrap_template.txt +++ b/python/private/python_bootstrap_template.txt @@ -148,12 +148,12 @@ def get_windows_path_with_unc_prefix(path): # Implicit long-path support is not universal across the Win32 API. For # example, DLL loading still requires an explicit extended-length prefix. + # abspath returns a normalized absolute path + path = abspath(path) extended_path_prefix = '\\\\?\\' if path.startswith(extended_path_prefix): return path - # abspath returns a normalized absolute path - path = abspath(path) if path.startswith('\\\\'): return extended_path_prefix + 'UNC\\' + path[2:] return extended_path_prefix + path diff --git a/python/private/site_init_template.py b/python/private/site_init_template.py index c324981dea..557e2e115b 100644 --- a/python/private/site_init_template.py +++ b/python/private/site_init_template.py @@ -101,12 +101,12 @@ def _get_windows_path_with_unc_prefix(path): # Implicit long-path support is not universal across the Win32 API. For # example, DLL loading still requires an explicit extended-length prefix. + # os.path.abspath returns a normalized absolute path + path = os.path.abspath(path) extended_path_prefix = "\\\\?\\" if path.startswith(extended_path_prefix): return path - # os.path.abspath returns a normalized absolute path - path = os.path.abspath(path) if path.startswith("\\\\"): return extended_path_prefix + "UNC\\" + path[2:] return extended_path_prefix + path diff --git a/python/private/stage2_bootstrap_template.py b/python/private/stage2_bootstrap_template.py index cb064dc0a6..f9ae176683 100644 --- a/python/private/stage2_bootstrap_template.py +++ b/python/private/stage2_bootstrap_template.py @@ -121,12 +121,12 @@ def get_windows_path_with_unc_prefix(path): # Implicit long-path support is not universal across the Win32 API. For # example, DLL loading still requires an explicit extended-length prefix. + # os.path.abspath returns a normalized absolute path + path = os.path.abspath(path) extended_path_prefix = "\\\\?\\" if path.startswith(extended_path_prefix): return path - # os.path.abspath returns a normalized absolute path - path = os.path.abspath(path) if path.startswith("\\\\"): return extended_path_prefix + "UNC\\" + path[2:] return extended_path_prefix + path diff --git a/python/private/zipapp/zip_main_template.py b/python/private/zipapp/zip_main_template.py index d5e2e74943..67539a1d24 100644 --- a/python/private/zipapp/zip_main_template.py +++ b/python/private/zipapp/zip_main_template.py @@ -111,12 +111,12 @@ def get_windows_path_with_unc_prefix(path): # Implicit long-path support is not universal across the Win32 API. For # example, DLL loading still requires an explicit extended-length prefix. + # os.path.abspath returns a normalized absolute path + path = os.path.abspath(path) extended_path_prefix = "\\\\?\\" if path.startswith(extended_path_prefix): return path - # os.path.abspath returns a normalized absolute path - path = os.path.abspath(path) if path.startswith("\\\\"): return extended_path_prefix + "UNC\\" + path[2:] return extended_path_prefix + path @@ -219,10 +219,10 @@ def create_runfiles_root(): extract_root = join(EXTRACT_ROOT, extract_dir, hash_dir) else: extract_root = join(EXTRACT_ROOT, EXTRACT_DIR, APP_HASH) - extract_root = get_windows_path_with_unc_prefix(extract_root) else: extract_root = tempfile.mkdtemp("", "Bazel.runfiles_") + extract_root = get_windows_path_with_unc_prefix(extract_root) extract_zip(dirname(__file__), extract_root) print_verbose("extracted to:", extract_root) # IMPORTANT: Later code does `rm -fr` on dirname(runfiles_root) -- it's diff --git a/python/runfiles/runfiles.py b/python/runfiles/runfiles.py index 1c6dca6088..bc619d857c 100644 --- a/python/runfiles/runfiles.py +++ b/python/runfiles/runfiles.py @@ -26,6 +26,7 @@ from __future__ import annotations import inspect +import ntpath import os import pathlib import posixpath @@ -496,6 +497,18 @@ def EnvVars(self) -> dict[str, str]: } +def _normalize_windows_path(path: str) -> str: + """Strip extended path prefixes (\\\\?\\ and \\\\?\\UNC\\) and normalize separators on Windows.""" + if not path: + return path + path = path.replace("/", "\\") + if path.startswith("\\\\?\\UNC\\"): + return "\\\\" + path[8:] + if path.startswith("\\\\?\\"): + return path[4:] + return path + + class Runfiles: """Returns the runtime location of runfiles. @@ -640,28 +653,31 @@ def CurrentRepository(self, frame: int = 1) -> str: path is not contained in the Python runfiles tree """ try: - # pylint: disable-next=protected-access caller_path = inspect.getfile(sys._getframe(frame)) except (TypeError, ValueError) as exc: raise ValueError("failed to determine caller's file path") from exc - caller_runfiles_path = os.path.relpath(caller_path, self._python_runfiles_root) - if caller_runfiles_path.startswith(".." + os.path.sep): - # With Python 3.10 and earlier, sys.path contains the directory - # of the script, which can result in a module being loaded from - # outside the runfiles tree. In this case, assume that the module is - # located in the main repository. - # With Python 3.11 and higher, the Python launcher sets - # PYTHONSAFEPATH, which prevents this behavior. - # On Windows, the current toolchain being used has a buggy zip file - # bootstrap, which leaves RUNFILES_DIR pointing at the first stage - # path and not the module path. In this case too, assume that the - # module is located in the main repository. - # TODO: This doesn't cover the case of a script being run from an - # external repository, which could be heuristically detected - # by parsing the script's path. - if (sys.version_info.minor <= 10 or sys.platform == "win32") and sys.path[ - 0 - ] != self._python_runfiles_root: + path_mod = ntpath if (sys.platform == "win32" or os.name == "nt") else os.path + python_runfiles_root = self._python_runfiles_root + if sys.platform == "win32" or os.name == "nt": + caller_path = _normalize_windows_path(caller_path) + python_runfiles_root = _normalize_windows_path(python_runfiles_root) + try: + caller_runfiles_path = path_mod.relpath(caller_path, python_runfiles_root) + except ValueError: + caller_runfiles_path = ".." + path_mod.sep + if caller_runfiles_path.startswith(".." + path_mod.sep): + sys_path_0 = sys.path[0] if sys.path else "" + if sys.platform == "win32" or os.name == "nt": + sys_path_0 = _normalize_windows_path(sys_path_0) + if ( + sys.version_info.minor <= 10 + or sys.platform == "win32" + or os.name == "nt" + ) and path_mod.normcase(sys_path_0) != path_mod.normcase( + python_runfiles_root + ): + return "" + elif (sys.version_info.minor <= 10) and sys_path_0 != python_runfiles_root: return "" raise ValueError( "{} does not lie under the runfiles root {}".format( @@ -669,18 +685,9 @@ def CurrentRepository(self, frame: int = 1) -> str: ) ) - caller_runfiles_directory = caller_runfiles_path[ - : caller_runfiles_path.find(os.path.sep) - ] - # With Bzlmod, the runfiles directory of the main repository is always - # named "_main". Without Bzlmod, the value returned by this function is - # never used, so we just assume Bzlmod is enabled. + caller_runfiles_directory = caller_runfiles_path.split(path_mod.sep, 1)[0] if caller_runfiles_directory == "_main": - # The canonical name of the main repository (also known as the - # workspace) is the empty string. return "" - # For all other repositories, the name of the runfiles directory is the - # canonical name. return caller_runfiles_directory # TODO: Update return type to Self when 3.11 is the min version diff --git a/tests/runfiles/runfiles_test.py b/tests/runfiles/runfiles_test.py index 78e2554aa3..5921d539ab 100644 --- a/tests/runfiles/runfiles_test.py +++ b/tests/runfiles/runfiles_test.py @@ -20,9 +20,10 @@ import tempfile import unittest from typing import Any +from unittest import mock from python.runfiles import runfiles -from python.runfiles.runfiles import _RepositoryMapping +from python.runfiles.runfiles import _normalize_windows_path, _RepositoryMapping class RunfilesTest(unittest.TestCase): @@ -766,6 +767,61 @@ def testCurrentRepository(self) -> None: assert r is not None # type assert self.assertEqual(r.CurrentRepository(), expected) + def testNormalizeWindowsPath(self) -> None: + self.assertEqual(_normalize_windows_path(r"\\?\C:\foo\bar"), r"C:\foo\bar") + self.assertEqual(_normalize_windows_path(r"\\?\c:\foo\bar"), r"c:\foo\bar") + self.assertEqual(_normalize_windows_path(r"//?/C:/foo/bar"), r"C:\foo\bar") + self.assertEqual( + _normalize_windows_path(r"\\?\UNC\server\share\path"), + r"\\server\share\path", + ) + self.assertEqual( + _normalize_windows_path(r"//?/UNC/server/share/path"), + r"\\server\share\path", + ) + self.assertEqual(_normalize_windows_path(r"C:/foo/bar"), r"C:\foo\bar") + self.assertEqual( + _normalize_windows_path(r"\\server\share\path"), + r"\\server\share\path", + ) + self.assertEqual(_normalize_windows_path(""), "") + + def testCurrentRepositoryWindowsExtendedPathNormalization(self) -> None: + with _MockFile(name="MANIFEST", contents=["_repo_mapping "]) as mf: + r = runfiles.Create( + { + "RUNFILES_MANIFEST_FILE": mf.Path(), + } + ) + assert r is not None # type assert + # Mock _python_runfiles_root and caller_path with Windows extended path + r._python_runfiles_root = r"C:\execroot\rules_python\bin.runfiles" + with mock.patch.object( + runfiles.inspect, + "getfile", + return_value=r"\\?\C:\execroot\rules_python\bin.runfiles\_main\pkg\app.py", + ): + with mock.patch.object(runfiles.sys, "platform", "win32"): + self.assertEqual(r.CurrentRepository(), "") + + with mock.patch.object( + runfiles.inspect, + "getfile", + return_value=r"\\?\C:\execroot\rules_python\bin.runfiles\my_module\pkg\app.py", + ): + with mock.patch.object(runfiles.sys, "platform", "win32"): + self.assertEqual(r.CurrentRepository(), "my_module") + + # Also test when runfiles_root itself has \\?\ prefix and caller_path has C:\ + r._python_runfiles_root = r"\\?\C:\execroot\rules_python\bin.runfiles" + with mock.patch.object( + runfiles.inspect, + "getfile", + return_value=r"C:\execroot\rules_python\bin.runfiles\my_module\pkg\app.py", + ): + with mock.patch.object(runfiles.sys, "platform", "win32"): + self.assertEqual(r.CurrentRepository(), "my_module") + @staticmethod def IsWindows() -> bool: return os.name == "nt" From ab75d94ee9c1c272a3e894c8ca3f2d173d9c987c Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 18 Aug 2026 20:38:13 -0700 Subject: [PATCH 3/9] fix(windows): limit extended path prefixing to paths exceeding MAX_PATH Unconditionally prepending \\?\ to short Windows paths broke cmd.exe batch wrapper execution, relative path resolution (..) in importlib.metadata, and zipapp runfiles discovery. Restrict extended path prefixing in bootstrap templates to paths that exceed MAX_PATH (260 characters). --- python/private/python_bootstrap_template.txt | 7 +++++++ python/private/site_init_template.py | 7 +++++++ python/private/stage2_bootstrap_template.py | 7 +++++++ python/private/zipapp/zip_main_template.py | 7 +++++++ 4 files changed, 28 insertions(+) diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt index 68e3687aeb..f37dcb8ba5 100644 --- a/python/private/python_bootstrap_template.txt +++ b/python/private/python_bootstrap_template.txt @@ -154,6 +154,13 @@ def get_windows_path_with_unc_prefix(path): if path.startswith(extended_path_prefix): return path + # Only prepend extended path prefix for paths that exceed MAX_PATH (260 chars). + # Win32 APIs (e.g. LoadLibraryExW) only require \\?\ for long paths, whereas + # unconditionally prefixing short paths breaks cmd.exe batch script execution, + # relative .. traversal in importlib, and zipapp runfiles discovery. + if len(path) < 260: + return path + if path.startswith('\\\\'): return extended_path_prefix + 'UNC\\' + path[2:] return extended_path_prefix + path diff --git a/python/private/site_init_template.py b/python/private/site_init_template.py index 557e2e115b..1155729f5d 100644 --- a/python/private/site_init_template.py +++ b/python/private/site_init_template.py @@ -107,6 +107,13 @@ def _get_windows_path_with_unc_prefix(path): if path.startswith(extended_path_prefix): return path + # Only prepend extended path prefix for paths that exceed MAX_PATH (260 chars). + # Win32 APIs (e.g. LoadLibraryExW) only require \\?\ for long paths, whereas + # unconditionally prefixing short paths breaks cmd.exe batch script execution, + # relative .. traversal in importlib, and zipapp runfiles discovery. + if len(path) < 260: + return path + if path.startswith("\\\\"): return extended_path_prefix + "UNC\\" + path[2:] return extended_path_prefix + path diff --git a/python/private/stage2_bootstrap_template.py b/python/private/stage2_bootstrap_template.py index f9ae176683..c40a0451f2 100644 --- a/python/private/stage2_bootstrap_template.py +++ b/python/private/stage2_bootstrap_template.py @@ -127,6 +127,13 @@ def get_windows_path_with_unc_prefix(path): if path.startswith(extended_path_prefix): return path + # Only prepend extended path prefix for paths that exceed MAX_PATH (260 chars). + # Win32 APIs (e.g. LoadLibraryExW) only require \\?\ for long paths, whereas + # unconditionally prefixing short paths breaks cmd.exe batch script execution, + # relative .. traversal in importlib, and zipapp runfiles discovery. + if len(path) < 260: + return path + if path.startswith("\\\\"): return extended_path_prefix + "UNC\\" + path[2:] return extended_path_prefix + path diff --git a/python/private/zipapp/zip_main_template.py b/python/private/zipapp/zip_main_template.py index 67539a1d24..9691704b48 100644 --- a/python/private/zipapp/zip_main_template.py +++ b/python/private/zipapp/zip_main_template.py @@ -117,6 +117,13 @@ def get_windows_path_with_unc_prefix(path): if path.startswith(extended_path_prefix): return path + # Only prepend extended path prefix for paths that exceed MAX_PATH (260 chars). + # Win32 APIs (e.g. LoadLibraryExW) only require \\?\ for long paths, whereas + # unconditionally prefixing short paths breaks cmd.exe batch script execution, + # relative .. traversal in importlib, and zipapp runfiles discovery. + if len(path) < 260: + return path + if path.startswith("\\\\"): return extended_path_prefix + "UNC\\" + path[2:] return extended_path_prefix + path From 7e76d7ea230c049985f609b97a62cafa1ed7b4cc Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 18 Aug 2026 21:02:49 -0700 Subject: [PATCH 4/9] fix(windows): resolve extended path compatibility in bootstrap and runfiles Unconditionally apply extended-length path prefixing on Windows without arbitrary MAX_PATH length checks. Normalize relative path separators in find_runfiles_root across bootstrap templates so os.path.exists checks succeed under \\?\ paths. Ensure zipapp symlink extraction resolves relative targets to absolute paths on Windows, and normalize RECORD paths in importlib metadata tests. --- python/private/python_bootstrap_template.txt | 10 +++------ python/private/site_init_template.py | 22 +++++++------------ python/private/stage2_bootstrap_template.py | 10 +++------ python/private/zipapp/zip_main_template.py | 9 ++------ .../importlib_metadata_test.py | 3 ++- .../whl_scripts_runnable_test.py | 7 +++++- 6 files changed, 24 insertions(+), 37 deletions(-) diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt index f37dcb8ba5..9a2cb38a70 100644 --- a/python/private/python_bootstrap_template.txt +++ b/python/private/python_bootstrap_template.txt @@ -154,13 +154,6 @@ def get_windows_path_with_unc_prefix(path): if path.startswith(extended_path_prefix): return path - # Only prepend extended path prefix for paths that exceed MAX_PATH (260 chars). - # Win32 APIs (e.g. LoadLibraryExW) only require \\?\ for long paths, whereas - # unconditionally prefixing short paths breaks cmd.exe batch script execution, - # relative .. traversal in importlib, and zipapp runfiles discovery. - if len(path) < 260: - return path - if path.startswith('\\\\'): return extended_path_prefix + 'UNC\\' + path[2:] return extended_path_prefix + path @@ -233,6 +226,9 @@ def find_runfiles_root(main_rel_path): # argv[0] may no longer point to a location inside the runfiles # directory. We should therefore respect RUNFILES_DIR and # RUNFILES_MANIFEST_FILE set by the caller. + if IS_WINDOWS and main_rel_path: + main_rel_path = main_rel_path.replace('/', os.sep) + runfiles_dir = os.environ.get('RUNFILES_DIR', None) if not runfiles_dir: runfiles_manifest_file = os.environ.get('RUNFILES_MANIFEST_FILE', '') diff --git a/python/private/site_init_template.py b/python/private/site_init_template.py index 1155729f5d..ffc57ff75e 100644 --- a/python/private/site_init_template.py +++ b/python/private/site_init_template.py @@ -35,6 +35,10 @@ def _is_verbose(): return bool(os.environ.get("RULES_PYTHON_BOOTSTRAP_VERBOSE")) +def _is_windows(): + return os.name == "nt" + + def _print_verbose_coverage(*args): if os.environ.get("VERBOSE_COVERAGE") or _is_verbose(): _print_verbose(*args) @@ -66,9 +70,10 @@ def _find_runfiles_root(): # Be defensive: the runfiles dir should contain ourselves. If it doesn't, # then it must not be our runfiles directory. - if runfiles_dir and os.path.exists( - os.path.join(runfiles_dir, _SELF_RUNFILES_RELATIVE_PATH) - ): + self_path = _SELF_RUNFILES_RELATIVE_PATH + if _is_windows(): + self_path = self_path.replace("/", os.sep) + if runfiles_dir and os.path.exists(os.path.join(runfiles_dir, self_path)): return runfiles_dir num_dirs_to_runfiles_root = _SELF_RUNFILES_RELATIVE_PATH.count("/") + 1 @@ -83,10 +88,6 @@ def _find_runfiles_root(): _print_verbose("runfiles_root:", _RUNFILES_ROOT) -def _is_windows(): - return os.name == "nt" - - def _get_windows_path_with_unc_prefix(path): path = path.strip() # No need to add prefix for non-Windows platforms. @@ -107,13 +108,6 @@ def _get_windows_path_with_unc_prefix(path): if path.startswith(extended_path_prefix): return path - # Only prepend extended path prefix for paths that exceed MAX_PATH (260 chars). - # Win32 APIs (e.g. LoadLibraryExW) only require \\?\ for long paths, whereas - # unconditionally prefixing short paths breaks cmd.exe batch script execution, - # relative .. traversal in importlib, and zipapp runfiles discovery. - if len(path) < 260: - return path - if path.startswith("\\\\"): return extended_path_prefix + "UNC\\" + path[2:] return extended_path_prefix + path diff --git a/python/private/stage2_bootstrap_template.py b/python/private/stage2_bootstrap_template.py index c40a0451f2..21ddfdb3a7 100644 --- a/python/private/stage2_bootstrap_template.py +++ b/python/private/stage2_bootstrap_template.py @@ -127,13 +127,6 @@ def get_windows_path_with_unc_prefix(path): if path.startswith(extended_path_prefix): return path - # Only prepend extended path prefix for paths that exceed MAX_PATH (260 chars). - # Win32 APIs (e.g. LoadLibraryExW) only require \\?\ for long paths, whereas - # unconditionally prefixing short paths breaks cmd.exe batch script execution, - # relative .. traversal in importlib, and zipapp runfiles discovery. - if len(path) < 260: - return path - if path.startswith("\\\\"): return extended_path_prefix + "UNC\\" + path[2:] return extended_path_prefix + path @@ -182,6 +175,9 @@ def find_runfiles_root(main_rel_path): # argv[0] may no longer point to a location inside the runfiles # directory. We should therefore respect RUNFILES_DIR and # RUNFILES_MANIFEST_FILE set by the caller. + if IS_WINDOWS and main_rel_path: + main_rel_path = main_rel_path.replace("/", os.sep) + runfiles_dir = os.environ.get("RUNFILES_DIR", None) if not runfiles_dir: runfiles_manifest_file = os.environ.get("RUNFILES_MANIFEST_FILE", "") diff --git a/python/private/zipapp/zip_main_template.py b/python/private/zipapp/zip_main_template.py index 9691704b48..ffcaa6e348 100644 --- a/python/private/zipapp/zip_main_template.py +++ b/python/private/zipapp/zip_main_template.py @@ -117,13 +117,6 @@ def get_windows_path_with_unc_prefix(path): if path.startswith(extended_path_prefix): return path - # Only prepend extended path prefix for paths that exceed MAX_PATH (260 chars). - # Win32 APIs (e.g. LoadLibraryExW) only require \\?\ for long paths, whereas - # unconditionally prefixing short paths breaks cmd.exe batch script execution, - # relative .. traversal in importlib, and zipapp runfiles discovery. - if len(path) < 260: - return path - if path.startswith("\\\\"): return extended_path_prefix + "UNC\\" + path[2:] return extended_path_prefix + path @@ -207,6 +200,8 @@ def extract_zip(zip_path, dest_dir): # Directories aren't stored in zips, so a missing # target means it points to a directory. target_is_directory = True + target = os.path.abspath(join(dirname(file_path), target)) + target = get_windows_path_with_unc_prefix(target) else: target_is_directory = False os.symlink(target, file_path, target_is_directory=target_is_directory) diff --git a/tests/venv_site_packages_libs/importlib_metadata_test.py b/tests/venv_site_packages_libs/importlib_metadata_test.py index eb16a6b6af..204bad7246 100644 --- a/tests/venv_site_packages_libs/importlib_metadata_test.py +++ b/tests/venv_site_packages_libs/importlib_metadata_test.py @@ -1,4 +1,5 @@ import importlib.metadata +import os import pathlib import sys import unittest @@ -61,7 +62,7 @@ def test_importlib_metadata_files(self): self.assertEqual(file_paths, expected_paths) for f in files: - resolved = pathlib.Path(f.locate()) + resolved = pathlib.Path(os.path.normpath(str(f.locate()))) if resolved.exists(): self.assertTrue( resolved.is_file(), diff --git a/tests/venv_site_packages_libs/whl_scripts_runnable_test.py b/tests/venv_site_packages_libs/whl_scripts_runnable_test.py index a0c4210f71..b14594c458 100644 --- a/tests/venv_site_packages_libs/whl_scripts_runnable_test.py +++ b/tests/venv_site_packages_libs/whl_scripts_runnable_test.py @@ -14,7 +14,12 @@ class WhlScriptsRunnableTest(unittest.TestCase): def _get_script_path(self, name): is_windows = sys.platform == "win32" if is_windows: - bin_dir = Path(sys.prefix) / "Scripts" + prefix = sys.prefix + if prefix.startswith("\\\\?\\UNC\\"): + prefix = "\\\\" + prefix[8:] + elif prefix.startswith("\\\\?\\"): + prefix = prefix[4:] + bin_dir = Path(prefix) / "Scripts" pathexts = os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";") for ext in [""] + [e.lower() for e in pathexts]: script_path = bin_dir / f"{name}{ext}" From 7075049eb0de0363a5bab7be6faf2f54d3af316d Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 18 Aug 2026 21:09:13 -0700 Subject: [PATCH 5/9] test(cc): add py_extension test at long path exceeding MAX_PATH Add a test exercising Python C extension dynamic loading at a path longer than 260 characters (MAX_PATH). On Windows, this exercises the extended-length path prefix (\\?\) in sys.path and runfiles required by LoadLibraryExW. --- .../BUILD.bazel | 21 +++++++++++ .../ext_long_path.c | 22 +++++++++++ .../py_extension_long_path_test.py | 37 +++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/BUILD.bazel create mode 100644 tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/ext_long_path.c create mode 100644 tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/py_extension_long_path_test.py diff --git a/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/BUILD.bazel b/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/BUILD.bazel new file mode 100644 index 0000000000..002f58c6b5 --- /dev/null +++ b/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/BUILD.bazel @@ -0,0 +1,21 @@ +load("//python:py_test.bzl", "py_test") +load("//python/cc:py_extension.bzl", "py_extension") + +package( + default_visibility = ["//tests/cc/py_extension:__subpackages__"], +) + +licenses(["notice"]) + +py_extension( + name = "ext_long_path", + srcs = ["ext_long_path.c"], +) + +py_test( + name = "py_extension_long_path_test", + srcs = ["py_extension_long_path_test.py"], + deps = [ + ":ext_long_path", + ], +) diff --git a/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/ext_long_path.c b/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/ext_long_path.c new file mode 100644 index 0000000000..89ea6e5743 --- /dev/null +++ b/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/ext_long_path.c @@ -0,0 +1,22 @@ +#include + +static PyObject* get_magic_number(PyObject* self, PyObject* args) { + return PyLong_FromLong(42); +} + +static PyMethodDef ModuleMethods[] = { + {"get_magic_number", get_magic_number, METH_NOARGS, "Returns 42."}, + {NULL, NULL, 0, NULL} +}; + +static struct PyModuleDef ext_long_path_module = { + PyModuleDef_HEAD_INIT, + "ext_long_path", + NULL, + -1, + ModuleMethods +}; + +PyMODINIT_FUNC PyInit_ext_long_path(void) { + return PyModule_Create(&ext_long_path_module); +} diff --git a/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/py_extension_long_path_test.py b/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/py_extension_long_path_test.py new file mode 100644 index 0000000000..b7440af7d9 --- /dev/null +++ b/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/py_extension_long_path_test.py @@ -0,0 +1,37 @@ +import importlib +import os +import unittest + +# Import path for the C extension located at a path exceeding MAX_PATH (260). +# On Windows, loading this triggers LoadLibraryExW which requires \\?\ +# extended-length path prefixing in sys.path / runfiles to load DLLs. +_EXT_MODULE_PATH = ( + "tests.cc.py_extension.long_path" + ".subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1" + ".subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2" + ".subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3" + ".subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4" + ".ext_long_path" +) + + +class PyExtensionLongPathTest(unittest.TestCase): + def test_extension_loaded_from_long_path(self): + ext_long_path = importlib.import_module(_EXT_MODULE_PATH) + self.assertEqual(ext_long_path.get_magic_number(), 42) + + def test_path_length_exceeds_max_path(self): + ext_long_path = importlib.import_module(_EXT_MODULE_PATH) + self.assertIsNotNone(ext_long_path.__file__) + assert ext_long_path.__file__ is not None + ext_file = os.path.abspath(ext_long_path.__file__) + self.assertGreater( + len(ext_file), + 260, + f"Expected extension path length to exceed 260 chars, got {len(ext_file)}: " + f"{ext_file}", + ) + + +if __name__ == "__main__": + unittest.main() From 7b2a693a012557e2ca4834ad52e76269116767ad Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 18 Aug 2026 21:14:12 -0700 Subject: [PATCH 6/9] test(cc): restrict py_extension long path test to Windows platform LoadLibraryExW is a Windows-specific API and long path names are restricted to Windows target compatibility to avoid POSIX path length limits on other operating systems. --- .../BUILD.bazel | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/BUILD.bazel b/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/BUILD.bazel index 002f58c6b5..7dd38599c7 100644 --- a/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/BUILD.bazel +++ b/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/BUILD.bazel @@ -10,11 +10,13 @@ licenses(["notice"]) py_extension( name = "ext_long_path", srcs = ["ext_long_path.c"], + target_compatible_with = ["@platforms//os:windows"], ) py_test( name = "py_extension_long_path_test", srcs = ["py_extension_long_path_test.py"], + target_compatible_with = ["@platforms//os:windows"], deps = [ ":ext_long_path", ], From ebd52d9735fb5241ae2af631fcf3023a695b1990 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 18 Aug 2026 21:15:26 -0700 Subject: [PATCH 7/9] test(bootstrap): move long path py_extension test under tests/bootstrap_impls Move the extended-length path test from tests/cc/py_extension to tests/bootstrap_impls/long_path to better reflect that it validates bootstrap path handling and LoadLibraryExW runtime behavior. --- .../BUILD.bazel | 2 +- .../ext_long_path.c | 0 .../py_extension_long_path_test.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename tests/{cc/py_extension => bootstrap_impls}/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/BUILD.bazel (88%) rename tests/{cc/py_extension => bootstrap_impls}/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/ext_long_path.c (100%) rename tests/{cc/py_extension => bootstrap_impls}/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/py_extension_long_path_test.py (97%) diff --git a/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/BUILD.bazel b/tests/bootstrap_impls/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/BUILD.bazel similarity index 88% rename from tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/BUILD.bazel rename to tests/bootstrap_impls/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/BUILD.bazel index 7dd38599c7..56b7f45f5b 100644 --- a/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/BUILD.bazel +++ b/tests/bootstrap_impls/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/BUILD.bazel @@ -2,7 +2,7 @@ load("//python:py_test.bzl", "py_test") load("//python/cc:py_extension.bzl", "py_extension") package( - default_visibility = ["//tests/cc/py_extension:__subpackages__"], + default_visibility = ["//tests/bootstrap_impls:__subpackages__"], ) licenses(["notice"]) diff --git a/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/ext_long_path.c b/tests/bootstrap_impls/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/ext_long_path.c similarity index 100% rename from tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/ext_long_path.c rename to tests/bootstrap_impls/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/ext_long_path.c diff --git a/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/py_extension_long_path_test.py b/tests/bootstrap_impls/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/py_extension_long_path_test.py similarity index 97% rename from tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/py_extension_long_path_test.py rename to tests/bootstrap_impls/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/py_extension_long_path_test.py index b7440af7d9..d737d7a381 100644 --- a/tests/cc/py_extension/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/py_extension_long_path_test.py +++ b/tests/bootstrap_impls/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/py_extension_long_path_test.py @@ -6,7 +6,7 @@ # On Windows, loading this triggers LoadLibraryExW which requires \\?\ # extended-length path prefixing in sys.path / runfiles to load DLLs. _EXT_MODULE_PATH = ( - "tests.cc.py_extension.long_path" + "tests.bootstrap_impls.long_path" ".subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1" ".subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2" ".subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3" From ed525930c1660ab151ecccb74a8413b55ccac304 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 18 Aug 2026 21:28:23 -0700 Subject: [PATCH 8/9] test(bootstrap): balance long path directory length for MSVC cl.exe compatibility Shorten the intermediate directory segments so that the compile-time cl.exe params file path remains under MAX_PATH (213 characters), while the full runtime path in Bazel runfiles on Windows exceeds MAX_PATH (388 characters), exercising LoadLibraryExW with extended path prefixing. --- .../BUILD.bazel | 0 .../ext_long_path.c | 0 .../py_extension_long_path_test.py | 13 ++++++------- 3 files changed, 6 insertions(+), 7 deletions(-) rename tests/bootstrap_impls/long_path/{subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4 => p1_pkg_dir_exceeding_max_path_limit_0123456789/p2_pkg_dir_exceeding_max_path_limit_0123456789}/BUILD.bazel (100%) rename tests/bootstrap_impls/long_path/{subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4 => p1_pkg_dir_exceeding_max_path_limit_0123456789/p2_pkg_dir_exceeding_max_path_limit_0123456789}/ext_long_path.c (100%) rename tests/bootstrap_impls/long_path/{subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4 => p1_pkg_dir_exceeding_max_path_limit_0123456789/p2_pkg_dir_exceeding_max_path_limit_0123456789}/py_extension_long_path_test.py (61%) diff --git a/tests/bootstrap_impls/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/BUILD.bazel b/tests/bootstrap_impls/long_path/p1_pkg_dir_exceeding_max_path_limit_0123456789/p2_pkg_dir_exceeding_max_path_limit_0123456789/BUILD.bazel similarity index 100% rename from tests/bootstrap_impls/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/BUILD.bazel rename to tests/bootstrap_impls/long_path/p1_pkg_dir_exceeding_max_path_limit_0123456789/p2_pkg_dir_exceeding_max_path_limit_0123456789/BUILD.bazel diff --git a/tests/bootstrap_impls/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/ext_long_path.c b/tests/bootstrap_impls/long_path/p1_pkg_dir_exceeding_max_path_limit_0123456789/p2_pkg_dir_exceeding_max_path_limit_0123456789/ext_long_path.c similarity index 100% rename from tests/bootstrap_impls/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/ext_long_path.c rename to tests/bootstrap_impls/long_path/p1_pkg_dir_exceeding_max_path_limit_0123456789/p2_pkg_dir_exceeding_max_path_limit_0123456789/ext_long_path.c diff --git a/tests/bootstrap_impls/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/py_extension_long_path_test.py b/tests/bootstrap_impls/long_path/p1_pkg_dir_exceeding_max_path_limit_0123456789/p2_pkg_dir_exceeding_max_path_limit_0123456789/py_extension_long_path_test.py similarity index 61% rename from tests/bootstrap_impls/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/py_extension_long_path_test.py rename to tests/bootstrap_impls/long_path/p1_pkg_dir_exceeding_max_path_limit_0123456789/p2_pkg_dir_exceeding_max_path_limit_0123456789/py_extension_long_path_test.py index d737d7a381..d71bca09f0 100644 --- a/tests/bootstrap_impls/long_path/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3/subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4/py_extension_long_path_test.py +++ b/tests/bootstrap_impls/long_path/p1_pkg_dir_exceeding_max_path_limit_0123456789/p2_pkg_dir_exceeding_max_path_limit_0123456789/py_extension_long_path_test.py @@ -2,15 +2,14 @@ import os import unittest -# Import path for the C extension located at a path exceeding MAX_PATH (260). -# On Windows, loading this triggers LoadLibraryExW which requires \\?\ -# extended-length path prefixing in sys.path / runfiles to load DLLs. +# Import path for the C extension located at a path where the full runtime path +# exceeds MAX_PATH (260 characters) on Windows. Loading this triggers +# LoadLibraryExW, which requires \\?\ extended-length path prefixing in +# sys.path / runfiles to load DLLs. _EXT_MODULE_PATH = ( "tests.bootstrap_impls.long_path" - ".subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_1" - ".subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_2" - ".subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_3" - ".subpath_longer_than_eighty_chars_to_exercise_windows_extended_path_handling_4" + ".p1_pkg_dir_exceeding_max_path_limit_0123456789" + ".p2_pkg_dir_exceeding_max_path_limit_0123456789" ".ext_long_path" ) From 7a8ef717b767bbf258059c1ada6ce727f4d827a1 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 18 Aug 2026 21:35:35 -0700 Subject: [PATCH 9/9] test(venv): handle extended path prefixes in venv metadata and executable tests Normalize sys.executable paths when comparing across processes, and read metadata files with relative '..' components via resolved paths to prevent Win32 invalid argument errors under extended-length prefixes. --- .../importlib_metadata_test.py | 14 +++++++++---- .../whl_scripts_runnable_test.py | 20 +++++++++++++++++-- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/tests/venv_site_packages_libs/importlib_metadata_test.py b/tests/venv_site_packages_libs/importlib_metadata_test.py index 204bad7246..3e4818504c 100644 --- a/tests/venv_site_packages_libs/importlib_metadata_test.py +++ b/tests/venv_site_packages_libs/importlib_metadata_test.py @@ -69,11 +69,17 @@ def test_importlib_metadata_files(self): f"Expected {resolved} to be a regular file", ) - # Verify file content can be read both as binary and as text - content = f.read_binary() + # Verify file content can be read both as binary and as text. + # Note: f.read_binary() uses f.locate() without normpath; on Windows + # when sys.path contains extended \\?\ prefixes, relative '..' segments + # cannot be opened directly via f.locate(), so read via resolved path. + if str(f).startswith(".."): + content = resolved.read_bytes() + text = resolved.read_text(encoding="utf-8") + else: + content = f.read_binary() + text = f.read_text(encoding="utf-8") self.assertIsNotNone(content) - - text = f.read_text(encoding="utf-8") self.assertIsNotNone(text) else: # On Windows, venv bin scripts have a .bat extension appended. diff --git a/tests/venv_site_packages_libs/whl_scripts_runnable_test.py b/tests/venv_site_packages_libs/whl_scripts_runnable_test.py index b14594c458..62b265c357 100644 --- a/tests/venv_site_packages_libs/whl_scripts_runnable_test.py +++ b/tests/venv_site_packages_libs/whl_scripts_runnable_test.py @@ -31,6 +31,16 @@ def _get_script_path(self, name): script_path = bin_dir / name return script_path + @staticmethod + def _normalize_exe_path(path_str): + if sys.platform == "win32": + if path_str.startswith("\\\\?\\UNC\\"): + path_str = "\\\\" + path_str[8:] + elif path_str.startswith("\\\\?\\"): + path_str = path_str[4:] + return os.path.normcase(os.path.normpath(path_str)) + return os.path.normpath(path_str) + def test_script_is_runnable(self): script_path = self._get_script_path("whl_with_data1_script") self.assertTrue(script_path.exists(), f"Script not found at {script_path}") @@ -49,7 +59,10 @@ def test_script_is_runnable(self): # Depending on how it's invoked, it might have more output, # but the user said it prints the hello message AND sys.executable. script_executable = output[-1].strip() - self.assertEqual(script_executable, sys.executable) + self.assertEqual( + self._normalize_exe_path(script_executable), + self._normalize_exe_path(sys.executable), + ) def test_entry_point_is_runnable(self): script_path = self._get_script_path("whl_with_data2_bin") @@ -66,7 +79,10 @@ def test_entry_point_is_runnable(self): self.assertIn("hello from whl_with_data2_bin", output) script_executable = output[-1].strip() - self.assertEqual(script_executable, sys.executable) + self.assertEqual( + self._normalize_exe_path(script_executable), + self._normalize_exe_path(sys.executable), + ) def test_pythonw_script(self): script_path = self._get_script_path("whl_with_data1_pythonw")