Expose Cookbook user-install CLIs in Docker (#887)
Ensure pip --user console scripts like vLLM are visible to Docker runtime and dependency probes by adding the user install bin directory to PATH.
This commit is contained in:
committed by
GitHub
parent
9a1893760d
commit
e58e4a185d
@@ -76,6 +76,10 @@ done
|
||||
# nvcc" even when the GPU itself is fully visible to the container.
|
||||
export VLLM_USE_FLASHINFER_SAMPLER="${VLLM_USE_FLASHINFER_SAMPLER:-0}"
|
||||
|
||||
# Make Cookbook-installed Python CLIs visible after `pip install --user`.
|
||||
# vLLM and helper scripts land here because /app is the non-root user's HOME.
|
||||
export PATH="/app/.local/bin:$PATH"
|
||||
|
||||
# Drop root and run the actual app. `gosu` is preferred over `su` /
|
||||
# `sudo` because it cleans up the process tree (no extra shell layer)
|
||||
# so signals (SIGTERM from `docker stop`) reach uvicorn directly.
|
||||
|
||||
@@ -183,13 +183,41 @@ def _package_status_note(name: str, probe: dict) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _prepend_user_install_bins_to_path() -> None:
|
||||
"""Make pip --user console scripts visible to dependency probes.
|
||||
|
||||
Docker Cookbook installs vLLM with `python -m pip install --user`, which
|
||||
drops the `vllm` CLI in /app/.local/bin. The running app process does not
|
||||
inherit that PATH update, so `shutil.which("vllm")` can report missing even
|
||||
after a successful install.
|
||||
"""
|
||||
try:
|
||||
import site
|
||||
|
||||
candidates = [os.path.join(site.USER_BASE, "bin")]
|
||||
except Exception:
|
||||
candidates = []
|
||||
candidates.append(os.path.expanduser("~/.local/bin"))
|
||||
|
||||
parts = os.environ.get("PATH", "").split(os.pathsep) if os.environ.get("PATH") else []
|
||||
changed = False
|
||||
for path in reversed([p for p in candidates if p]):
|
||||
if path not in parts:
|
||||
parts.insert(0, path)
|
||||
changed = True
|
||||
if changed:
|
||||
os.environ["PATH"] = os.pathsep.join(parts)
|
||||
|
||||
|
||||
def _package_probe_script(names: list[str]) -> str:
|
||||
names_lit = ",".join(repr(n) for n in names)
|
||||
return f"""
|
||||
import importlib.util
|
||||
import importlib.metadata as md
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import site
|
||||
|
||||
names=[{names_lit}]
|
||||
dist_names={{
|
||||
@@ -204,6 +232,24 @@ bin_names={{
|
||||
'llama_cpp':['llama-server'],
|
||||
}}
|
||||
|
||||
def add_user_install_bins_to_path():
|
||||
candidates = []
|
||||
try:
|
||||
candidates.append(os.path.join(site.USER_BASE, 'bin'))
|
||||
except Exception:
|
||||
pass
|
||||
candidates.append(os.path.expanduser('~/.local/bin'))
|
||||
parts = os.environ.get('PATH', '').split(os.pathsep) if os.environ.get('PATH') else []
|
||||
changed = False
|
||||
for path in reversed([p for p in candidates if p]):
|
||||
if path not in parts:
|
||||
parts.insert(0, path)
|
||||
changed = True
|
||||
if changed:
|
||||
os.environ['PATH'] = os.pathsep.join(parts)
|
||||
|
||||
add_user_install_bins_to_path()
|
||||
|
||||
def mod_status(n):
|
||||
spec = importlib.util.find_spec(n)
|
||||
loader = getattr(spec, 'loader', None) if spec else None
|
||||
@@ -793,6 +839,7 @@ def setup_shell_routes() -> APIRouter:
|
||||
_require_admin(request)
|
||||
_reject_cross_site(request)
|
||||
import importlib, importlib.metadata as importlib_metadata, shlex, json as _json
|
||||
_prepend_user_install_bins_to_path()
|
||||
if ssh_port and str(ssh_port).strip() not in ("", "22"):
|
||||
_port = str(ssh_port).strip()
|
||||
if not _SSH_PORT_RE.match(_port) or not (1 <= int(_port) <= 65535):
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import builtins
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
@@ -14,7 +15,9 @@ from routes.shell_routes import (
|
||||
_running_in_container,
|
||||
_docker_row_status,
|
||||
_package_installed_from_probe,
|
||||
_package_probe_script,
|
||||
_package_status_note,
|
||||
_prepend_user_install_bins_to_path,
|
||||
_reject_cross_site,
|
||||
_ssh_base_argv,
|
||||
_venv_activate_prefix,
|
||||
@@ -247,6 +250,26 @@ class TestPackageProbeStatus:
|
||||
assert _package_installed_from_probe("diffusers", missing_torch) is False
|
||||
assert _package_installed_from_probe("diffusers", ready) is True
|
||||
|
||||
def test_local_user_install_bin_is_added_to_path(self, monkeypatch, tmp_path):
|
||||
user_base = tmp_path / "user-base"
|
||||
monkeypatch.setattr("site.USER_BASE", str(user_base))
|
||||
monkeypatch.setenv("HOME", str(tmp_path / "home"))
|
||||
monkeypatch.setenv("PATH", "/usr/bin")
|
||||
|
||||
_prepend_user_install_bins_to_path()
|
||||
|
||||
parts = os.environ["PATH"].split(os.pathsep)
|
||||
assert str(user_base / "bin") in parts
|
||||
assert str(tmp_path / "home" / ".local" / "bin") in parts
|
||||
|
||||
def test_remote_package_probe_checks_user_install_bin(self):
|
||||
script = _package_probe_script(["vllm"])
|
||||
|
||||
assert "site.USER_BASE" in script
|
||||
assert "os.path.expanduser('~/.local/bin')" in script
|
||||
assert "add_user_install_bins_to_path()" in script
|
||||
assert "shutil.which(b)" in script
|
||||
|
||||
|
||||
class TestSshBaseArgv:
|
||||
def test_basic_host_no_port(self):
|
||||
|
||||
Reference in New Issue
Block a user