ci: reproduce default-Windows wheel install to guard MAX_PATH (#29597)

* ci: reproduce default-Windows wheel install to guard MAX_PATH

The existing using_litellm_on_windows job installs the project with
`uv sync`, an editable source install that never copies package files
into a deep site-packages path, so it cannot see the 260-char MAX_PATH
overflow that breaks `pip install litellm` on default Windows. The
content-filter benchmark fixtures have hit that limit three times
(#21941, #22039, #29536), each caught only after release.

This adds a guard to the same job that builds the wheel and installs it
the way an end user would: into a venv whose site-packages prefix is
padded to a realistic worst-case Windows length (~100 chars), then
asserts the install completes and litellm imports. Any packaged path
long enough to bust MAX_PATH at that prefix is reported up front, so the
check is deterministic regardless of the runner's long-path setting,
while the real install also covers failure modes a length heuristic
cannot (half-unpacked packages, reserved names, case collisions).

This commit is the guard only; on the current tree it correctly fails
because nine fixtures still exceed the limit. The rename that brings
them back under it follows on this branch.

* fix(packaging): shorten content-filter benchmark fixtures under MAX_PATH

The 10 content-filter benchmark result fixtures used the legacy
block_{topic}_-_contentfilter_({yaml}).json naming, up to 176 chars
inside the wheel, which busts the Windows 260-char MAX_PATH limit once
extracted under a realistic site-packages prefix and aborts
`pip install litellm` on default Windows.

Rename them to the short {topic}_cf.json scheme that
_save_confusion_results already emits today (it splits the label on the
em-dash and writes f"{topic}_cf"), matching the insults_cf.json and
investment_cf.json files fixed earlier. Re-running the eval suite now
regenerates these same short names rather than recreating the long ones.

This drops the longest packaged path from 176 to 128, so the guard added
in the previous commit goes from red to green with a 32-char margin.

* test(windows): tidy MAX_PATH guard per review

Close the wheel zip via a context manager rather than leaning on
refcount collection, and select the wheel under dist/ by newest mtime so
a stale artifact from an earlier build cannot be tested instead of the
one just produced. Also pin down the venv-depth formula with a short
note: the +2 is the separator joining the venv root to "Lib" plus the
trailing separator before the entry, which lands the simulated
site-packages prefix at exactly 100 chars.
This commit is contained in:
yuneng-jiang 2026-06-03 11:28:08 -07:00 committed by GitHub
parent 53a206a179
commit 34293fa80a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 120 additions and 1 deletions

View File

@ -182,7 +182,14 @@ jobs:
- run:
name: Run Windows-specific test
command: |
uv run --no-sync python -m pytest tests/windows_tests/test_litellm_on_windows.py -v
uv run --no-sync python -m pytest tests/windows_tests/ -v
- run:
name: Guard against MAX_PATH-busting packaged wheel paths
environment:
UV_HTTP_TIMEOUT: "300"
command: |
uv build --wheel --out-dir dist
uv run --no-sync python tests/windows_tests/check_windows_wheel_install.py
local_testing_part1:
docker:

View File

@ -0,0 +1,76 @@
"""Reproduce a default-Windows ``pip install litellm`` to catch the 260-char
MAX_PATH regression that content-filter benchmark fixtures keep reintroducing
(#21941, #22039, #29536). Run after ``uv build --wheel --out-dir dist``.
"""
import glob
import os
import subprocess
import sys
import zipfile
MAX_PATH = 260
# Worst-case Windows site-packages prefix: long profile name + roaming AppData venv.
WORST_CASE_PREFIX = 100
def overlong_install_paths(wheel, prefix_len=WORST_CASE_PREFIX, max_path=MAX_PATH):
with zipfile.ZipFile(wheel) as zf:
names = zf.namelist()
return sorted(
(n for n in names if prefix_len + len(n) > max_path), key=len, reverse=True
)
def _deep_venv_dir(target_prefix=WORST_CASE_PREFIX):
drive = os.path.splitdrive(os.getcwd())[0] or "C:"
root = drive + os.sep + "lmwin" + os.sep
# +2: the sep joining the venv root to "Lib", plus the trailing sep before the entry
suffix = len(os.path.join("Lib", "site-packages")) + 2
return root + "x" * (target_prefix - suffix - len(root))
def _run(cmd):
print("+ " + subprocess.list2cmdline(cmd), flush=True)
return subprocess.call(cmd)
def main():
wheels = glob.glob(os.path.join("dist", "*.whl"))
if not wheels:
print("::error::no wheel in dist/; run `uv build --wheel --out-dir dist` first")
return 1
wheel = max(wheels, key=os.path.getmtime)
offenders = overlong_install_paths(wheel)
if offenders:
print(
f"::error::{len(offenders)} packaged path(s) bust the Windows MAX_PATH limit "
f"at a {WORST_CASE_PREFIX}-char install prefix:"
)
for n in offenders[:15]:
print(f" on-disk {WORST_CASE_PREFIX + len(n):4} {n}")
return 1
venv = _deep_venv_dir()
os.makedirs(os.path.dirname(venv), exist_ok=True)
if _run(["uv", "venv", venv]) != 0:
return 1
python = os.path.join(venv, "Scripts", "python.exe")
if _run(["uv", "pip", "install", "--python", python, wheel]) != 0:
print(
f"::error::installing {os.path.basename(wheel)} into a deep prefix failed"
)
return 1
if _run([python, "-c", "import litellm; import litellm.types.utils"]) != 0:
print("::error::litellm did not import after install (half-unpacked package)")
return 1
print(
f"ok: {os.path.basename(wheel)} installs into a worst-case prefix and imports"
)
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,36 @@
import zipfile
from check_windows_wheel_install import (
MAX_PATH,
WORST_CASE_PREFIX,
overlong_install_paths,
)
def _wheel(tmp_path, *entry_names):
path = tmp_path / "pkg.whl"
with zipfile.ZipFile(path, "w") as zf:
for name in entry_names:
zf.writestr(name, "{}")
return str(path)
def test_flags_entry_one_char_over_budget(tmp_path):
busts = "a" * (MAX_PATH - WORST_CASE_PREFIX + 1)
assert overlong_install_paths(_wheel(tmp_path, busts)) == [busts]
def test_allows_entry_exactly_at_budget(tmp_path):
at_limit = "a" * (MAX_PATH - WORST_CASE_PREFIX)
assert (
overlong_install_paths(_wheel(tmp_path, at_limit, "litellm/__init__.py")) == []
)
def test_orders_offenders_longest_first(tmp_path):
longer = "a" * (MAX_PATH - WORST_CASE_PREFIX + 5)
shorter = "b" * (MAX_PATH - WORST_CASE_PREFIX + 1)
assert overlong_install_paths(_wheel(tmp_path, shorter, longer)) == [
longer,
shorter,
]