install: keep pinned repairs off UV_TORCH_BACKEND and narrow setup.ps1's rocm pin match
Round 2 of the pinned-index hardening: 1. _build_uv_cmd converted UV_TORCH_BACKEND into --torch-backend before the new env isolation could act, and uv's torch backend redirects torch resolution to its own per-backend index even when --index-url is given (verified: a cu128-pinned dry run with UV_TORCH_BACKEND=cpu resolves torch 2.13.0+cpu). Pinned-index commands now never receive the flag and UV_TORCH_BACKEND joins the stripped env vars, so uv cannot re-read it. 2. setup.ps1's pinned reroute had the same bare rocm* glob install.ps1 had: a custom find-links leaf like rocm-rel-7.2.1 was routed through the ROCm --index-url path instead of the verbatim unknown-pin path. Now requires a digit after rocm, matching install.ps1, install.sh and _is_pip_rocm_family_leaf. 3. The marker test's case-normalization checks used -eq, which is case-insensitive in PowerShell, making them vacuous, and the unknown-leaf expectation was written lowercased while the implementation deliberately preserves custom-leaf case. Tightened to -ceq with the case-preserving expected value. Adds unit + parity tests for 1 and 2 (5 new tests).
This commit is contained in:
parent
e382a180f2
commit
f8bde5e387
5 changed files with 76 additions and 13 deletions
|
|
@ -2560,9 +2560,13 @@ def _build_uv_cmd(args: tuple[str, ...]) -> list[str]:
|
|||
cmd.extend(_translate_pip_args_for_uv(args))
|
||||
# Torch is pre-installed by install.sh/setup.ps1. Do not add
|
||||
# --torch-backend by default -- it can cause solver dead-ends on CPU-only
|
||||
# machines. Callers that need it can set UV_TORCH_BACKEND.
|
||||
# machines. Callers that need it can set UV_TORCH_BACKEND. Never add it to a
|
||||
# pinned-index command: uv's torch backend redirects torch resolution to its
|
||||
# own per-backend index (verified: --index-url .../cu128 with UV_TORCH_BACKEND
|
||||
# =cpu resolves torch+cpu), which would defeat the explicit pin exactly like
|
||||
# the index env vars _install_env_for_cmd() strips.
|
||||
_tb = os.environ.get("UV_TORCH_BACKEND", "")
|
||||
if _tb:
|
||||
if _tb and not _is_pinned_index_cmd(cmd):
|
||||
cmd.append(f"--torch-backend={_tb}")
|
||||
return cmd
|
||||
|
||||
|
|
@ -2576,7 +2580,22 @@ def _build_uv_cmd(args: tuple[str, ...]) -> list[str]:
|
|||
# actually used. install.sh (run_install_cmd, #6898), install.ps1 and setup.ps1
|
||||
# already neutralise these vars for their pinned installs; do the same here so the
|
||||
# pin wins on every platform. Non-pinned installs (no --index-url) keep the mirror.
|
||||
_UV_INDEX_ENV_VARS = ("UV_DEFAULT_INDEX", "UV_INDEX_URL", "UV_INDEX", "UV_EXTRA_INDEX_URL")
|
||||
# UV_TORCH_BACKEND is stripped too: uv's torch backend redirects torch resolution
|
||||
# to its own per-backend index even when a --index-url is given, so it defeats a
|
||||
# pin the same way ( _build_uv_cmd also refuses to turn it into a --torch-backend
|
||||
# flag for pinned commands; this covers uv reading the env var directly).
|
||||
_UV_INDEX_ENV_VARS = (
|
||||
"UV_DEFAULT_INDEX",
|
||||
"UV_INDEX_URL",
|
||||
"UV_INDEX",
|
||||
"UV_EXTRA_INDEX_URL",
|
||||
"UV_TORCH_BACKEND",
|
||||
)
|
||||
|
||||
|
||||
def _is_pinned_index_cmd(cmd: "list[str] | tuple[str, ...]") -> bool:
|
||||
"""True when the command pins an index via --index-url / --default-index."""
|
||||
return any(arg in ("--index-url", "--default-index") for arg in cmd)
|
||||
|
||||
|
||||
def _install_env_for_cmd(cmd: "list[str]") -> "dict[str, str] | None":
|
||||
|
|
@ -2585,11 +2604,12 @@ def _install_env_for_cmd(cmd: "list[str]") -> "dict[str, str] | None":
|
|||
Returns None (inherit the caller's environment unchanged) when the command
|
||||
does NOT pin an index, so ordinary installs still honour a user's UV_INDEX /
|
||||
UV_EXTRA_INDEX_URL mirror. When the command passes --index-url / --default-index
|
||||
(the torch repair paths), the four uv index env vars are removed so the pinned
|
||||
index is not overridden by an inherited mirror (uv treats the default index as
|
||||
lowest priority). Mirrors install.sh's run_install_cmd gate (#6898).
|
||||
(the torch repair paths), the uv index env vars (and UV_TORCH_BACKEND) are
|
||||
removed so the pinned index is not overridden by an inherited mirror or torch
|
||||
backend (uv treats the default index as lowest priority). Mirrors install.sh's
|
||||
run_install_cmd gate (#6898).
|
||||
"""
|
||||
if not any(arg in ("--index-url", "--default-index") for arg in cmd):
|
||||
if not _is_pinned_index_cmd(cmd):
|
||||
return None
|
||||
env = os.environ.copy()
|
||||
for name in _UV_INDEX_ENV_VARS:
|
||||
|
|
|
|||
|
|
@ -3168,10 +3168,15 @@ if ($TorchIndexPinned -and -not $ROCmIndexUrl -and $PinnedTorchIndexUrl) {
|
|||
$ROCmVisionSpec = "torchvision>=0.26.0,<0.27.0"
|
||||
$ROCmAudioSpec = "torchaudio>=2.11.0,<2.12.0"
|
||||
substep "pinned ROCm index ($_pinLeaf) -- enforcing $ROCmTorchSpec" "Cyan"
|
||||
} elseif ($_pinLeaf -like 'gfx*' -or $_pinLeaf -like 'rocm*') {
|
||||
} elseif ($_pinLeaf -like 'gfx*' -or $_pinLeaf -match '^rocm\d') {
|
||||
# Other gfx per-arch indexes and older rocm (<=7.1) ship torch <2.11;
|
||||
# route via the ROCm path with bare specs (matches the automatic path's
|
||||
# bare floor for these arches).
|
||||
# Require a DIGIT after rocm (rocm7.1, not rocm-rel-7.2.1 / rocm-current):
|
||||
# only rocm<digit> and gfx* are pip --index-url (PEP 503) families; a
|
||||
# rocm-<nondigit> leaf is a custom/find-links URL that must stay on the
|
||||
# verbatim unknown-pin path. Mirrors install.ps1 and
|
||||
# install_python_stack.py's _is_pip_rocm_family_leaf (^rocm\d).
|
||||
$ROCmIndexUrl = $PinnedTorchIndexUrl
|
||||
$ROCmTorchSpec = "torch"
|
||||
$ROCmVisionSpec = "torchvision"
|
||||
|
|
|
|||
|
|
@ -485,6 +485,17 @@ class TestPinnedRocmLeafDigitParity:
|
|||
"-like 'rocm*'" not in text
|
||||
), "install.ps1 must not route a pinned index on a bare -like 'rocm*' glob"
|
||||
|
||||
def test_setup_ps1_pinned_reroute_requires_rocm_digit(self):
|
||||
text = SETUP_PS1.read_text(encoding = "utf-8")
|
||||
assert "-match '^rocm\\d'" in text, (
|
||||
"setup.ps1 pinned-index reroute must use -match '^rocm\\d' (not a bare "
|
||||
"-like 'rocm*' glob) so custom find-links leaves stay on the verbatim path"
|
||||
)
|
||||
pinned_block = text[text.find("$_pinGfx211 = Test-RocmGfx211Leaf") :][:2000]
|
||||
assert "-like 'rocm*'" not in pinned_block, (
|
||||
"setup.ps1 pinned reroute must not route on a bare -like 'rocm*' glob"
|
||||
)
|
||||
|
||||
def test_install_sh_repairable_requires_rocm_digit(self):
|
||||
text = INSTALL_SH.read_text(encoding = "utf-8")
|
||||
assert re.search(
|
||||
|
|
|
|||
|
|
@ -54,6 +54,24 @@ class TestBuildUvCmdTorchBackend:
|
|||
a.startswith("--torch-backend") for a in cmd
|
||||
), f"Empty UV_TORCH_BACKEND should not add flag, got: {cmd}"
|
||||
|
||||
def test_uv_torch_backend_skipped_for_pinned_index(self):
|
||||
"""A pinned-index command must NOT get --torch-backend: uv's torch backend
|
||||
redirects torch resolution to its own per-backend index even when
|
||||
--index-url is given (verified: cu128 pin + backend cpu installs
|
||||
torch+cpu), defeating the pin."""
|
||||
for pin_flag in ("--index-url", "--default-index"):
|
||||
with mock.patch.dict(os.environ, {"UV_TORCH_BACKEND": "cpu"}):
|
||||
cmd = self._call(("torch", pin_flag, "https://download.pytorch.org/whl/cu128"))
|
||||
assert not any(
|
||||
a.startswith("--torch-backend") for a in cmd
|
||||
), f"{pin_flag} command must not carry --torch-backend, got: {cmd}"
|
||||
|
||||
def test_uv_torch_backend_kept_for_unpinned(self):
|
||||
"""Non-pinned commands still honour UV_TORCH_BACKEND."""
|
||||
with mock.patch.dict(os.environ, {"UV_TORCH_BACKEND": "cpu"}):
|
||||
cmd = self._call(("somepackage",))
|
||||
assert "--torch-backend=cpu" in cmd
|
||||
|
||||
|
||||
class TestUvSafePath:
|
||||
"""_uv_safe_path hands uv a space-free `-c`/`-r` path (issue #6503)."""
|
||||
|
|
@ -216,3 +234,10 @@ class TestPinnedIndexClearsUvEnv:
|
|||
env = ips._install_env_for_cmd(cmd)
|
||||
assert env is not None
|
||||
assert env.get("PATH_SENTINEL_XYZ") == "keepme", "only uv index vars are removed"
|
||||
|
||||
def test_pinned_cmd_strips_uv_torch_backend(self):
|
||||
"""UV_TORCH_BACKEND is stripped for pinned commands so uv cannot read it
|
||||
from the environment and reroute torch off the pinned index."""
|
||||
with mock.patch.dict(os.environ, {"UV_TORCH_BACKEND": "cpu"}):
|
||||
env = ips._install_env_for_cmd(["uv", "pip", "install", "torch", "--index-url", "https://x/cu128"])
|
||||
assert env is not None and "UV_TORCH_BACKEND" not in env
|
||||
|
|
|
|||
|
|
@ -39,12 +39,14 @@ function Check($name, $cond) {
|
|||
}
|
||||
|
||||
Write-Host "Get-NormalizedIndexUrl (trim / strip trailing slash / lowercase leaf)"
|
||||
Check "trailing slashes + leaf lowered" `
|
||||
((Get-NormalizedIndexUrl "https://repo.amd.com/rocm/whl/gfx120X-all///") -eq "https://repo.amd.com/rocm/whl/gfx120x-all")
|
||||
# -ceq: PowerShell -eq is case-INsensitive for strings, which would make these
|
||||
# case-normalization checks vacuous (any casing would pass).
|
||||
Check "trailing slashes + known family leaf lowered" `
|
||||
((Get-NormalizedIndexUrl "https://repo.amd.com/rocm/whl/gfx120X-all///") -ceq "https://repo.amd.com/rocm/whl/gfx120x-all")
|
||||
Check "whitespace trimmed" `
|
||||
((Get-NormalizedIndexUrl " https://download.pytorch.org/whl/cu128 ") -eq "https://download.pytorch.org/whl/cu128")
|
||||
Check "host case preserved, leaf lowered" `
|
||||
((Get-NormalizedIndexUrl "https://Mirror.Local/Simple/") -eq "https://Mirror.Local/simple")
|
||||
((Get-NormalizedIndexUrl " https://download.pytorch.org/whl/cu128 ") -ceq "https://download.pytorch.org/whl/cu128")
|
||||
Check "host case preserved, unknown custom leaf keeps case" `
|
||||
((Get-NormalizedIndexUrl "https://Mirror.Local/Simple/") -ceq "https://Mirror.Local/Simple")
|
||||
Check "gfx120X-all == gfx120x-all after normalize" `
|
||||
((Get-NormalizedIndexUrl "https://repo.amd.com/rocm/whl/gfx120X-all") -eq (Get-NormalizedIndexUrl "https://repo.amd.com/rocm/whl/gfx120x-all"))
|
||||
Check "empty -> null" ($null -eq (Get-NormalizedIndexUrl " "))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue