From 855997b7d7e90a28ec6259a5160a92ea8e062fe9 Mon Sep 17 00:00:00 2001 From: alt-glitch Date: Thu, 1 Oct 2026 16:47:57 +0530 Subject: [PATCH] fix(pm): give Store a long-path root on Windows Windows cuts plain file calls at 260 characters when LongPathsEnabled is 0, which is the default. PM's copy, walk, digest, rename and delete calls under the store then fail or silently skip files (#130242). Store now spells its root in the extended-length form (\\?\C:\...). Every path built from it with entry() or / inherits that spelling, so the file calls beneath the root no longer hit the limit. Relative paths, and so digests, are unchanged. On POSIX the root is untouched. native() returns the ordinary spelling for text that leaves PM's own file calls (argv, environment values, hashed identities, records and messages). The call sites adopt it in follow-up commits. --- pm/filesystem.py | 32 ++++++++++++++++++++++++++++++++ pm/store.py | 4 ++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/pm/filesystem.py b/pm/filesystem.py index 37d3e04de5..eb829818e3 100644 --- a/pm/filesystem.py +++ b/pm/filesystem.py @@ -7,6 +7,7 @@ from __future__ import annotations import errno import hashlib +import ntpath import os from pathlib import Path import shutil @@ -23,6 +24,37 @@ def is_junction(path: Path) -> bool: return os.name == "nt" and path.lstat().st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT +_VERBATIM = "\\\\?\\" +_VERBATIM_UNC = "\\\\?\\UNC\\" + + +def _long_form(text: str) -> str: + if text.startswith(_VERBATIM): + return text + # Windows stops normalising a verbatim path, so it must already be absolute, + # backslashed and free of `..` before the prefix goes on. + text = ntpath.abspath(text) + if text.startswith("\\\\"): + return _VERBATIM_UNC + text[2:] + return _VERBATIM + text + + +def long_root(path: Path) -> Path: + """Spell a PM-owned root so Windows file calls beneath it are not cut at + MAX_PATH when LongPathsEnabled is 0 (the default). Every path joined onto the + result inherits the spelling; relative paths and digests are unchanged.""" + return Path(_long_form(os.fspath(path))) if os.name == "nt" else path + + +def native(path: str | os.PathLike[str]) -> str: + """The ordinary spelling, for text that leaves PM's own file calls: argv, cwd, + environment values, hashed identities, records and messages.""" + text = os.fspath(path) + if text.startswith(_VERBATIM_UNC): + return "\\\\" + text[len(_VERBATIM_UNC):] + return text[len(_VERBATIM):] if text.startswith(_VERBATIM) else text + + def lock_fd(fd: int, *, wait: bool, timeout: float | None = None) -> bool: """Take the byte lock; ``timeout`` bounds the retry loop (None waits forever, 0 tries once).""" deadline = None if timeout is None else time.monotonic() + timeout diff --git a/pm/store.py b/pm/store.py index b6754651e6..ac0d63ce52 100644 --- a/pm/store.py +++ b/pm/store.py @@ -14,7 +14,7 @@ from contextlib import contextmanager from pathlib import Path from typing import IO -from pm.filesystem import is_junction, retry_held +from pm.filesystem import is_junction, long_root, retry_held @@ -348,7 +348,7 @@ class Store: Hash-keyed archives survive failed installs. Publication releases them.""" def __init__(self, root: Path): - self.root = root + self.root = long_root(root) def entry(self, name: str) -> Path: return self.root / name