Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions routes/cookbook_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1204,6 +1204,41 @@ def _safe_env_prefix(ep: str | None) -> str | None:
return f'[ -f "{path}" ] && source "{path}" || true'


def _local_windows_bash_env_prefix(ep: str | None) -> str | None:
"""Convert a frontend PowerShell venv prefix for the local Git Bash runner."""
if not ep:
return ep

prefix = ep.strip()
if not prefix.startswith("&"):
return ep

raw_path = prefix[1:].lstrip()
if not raw_path:
return ep
if raw_path.startswith("'"):
if len(raw_path) < 2 or not raw_path.endswith("'"):
return ep
quoted_path = raw_path[1:-1]
if "'" in quoted_path.replace("''", ""):
return ep
path = quoted_path.replace("''", "'")
else:
path = raw_path.rstrip()
if "'" in path or '"' in path:
return ep
if any(c in path for c in "\r\n;&|`$<>"):
return ep
if not path.replace("\\", "/").casefold().endswith("/scripts/activate.ps1"):
return ep

bash_path = _git_bash_path(path)
if "\\" in bash_path:
return ep
bash_path = bash_path[: -len("Activate.ps1")] + "activate"
return "source " + shlex.quote(bash_path)


def _ssh_ps(host, script_path, port=None):
"""Build SSH command to run a PowerShell script on a Windows remote."""
pf = f"-p {port} " if port and port != "22" else ""
Expand Down
6 changes: 3 additions & 3 deletions routes/cookbook_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
_SESSION_ID_RE, _validate_repo_id, _validate_serve_model_id, _validate_include, _validate_token,
_validate_local_dir, _validate_gpus, _shell_path,
_ps_squote, _bash_squote, _validate_serve_cmd, _parse_serve_phase, OLLAMA_MISSING_HINT,
_safe_env_prefix, _local_tooling_path_export, _append_serve_preflight_exit_lines,
_safe_env_prefix, _local_windows_bash_env_prefix, _local_tooling_path_export, _append_serve_preflight_exit_lines,
_append_serve_exit_code_lines, _append_llama_cpp_linux_accel_build_lines, _cached_model_scan_script,
load_stored_hf_token,
_append_vllm_linux_preflight_lines, _ollama_bind_from_cmd, _pip_install_fallback_chain,
Expand Down Expand Up @@ -1336,7 +1336,7 @@ async def model_download(request: Request, req: ModelDownloadRequest):
# Local: run hf download in the background (tmux on POSIX, a detached
# process + logfile on Windows where tmux doesn't exist).
if req.env_prefix:
lines.append(_safe_env_prefix(req.env_prefix))
lines.append(_safe_env_prefix(_local_windows_bash_env_prefix(req.env_prefix) if local_windows else req.env_prefix))
else:
lines.append("deactivate 2>/dev/null; hash -r")
# Show whether the HF token reached this run (masked) — tells a gated
Expand Down Expand Up @@ -2166,7 +2166,7 @@ async def model_serve(request: Request, req: ServeRequest):
if req.gpus:
runner_lines.append(f"export CUDA_VISIBLE_DEVICES='{req.gpus}'")
if req.env_prefix:
runner_lines.append(_safe_env_prefix(req.env_prefix))
runner_lines.append(_safe_env_prefix(_local_windows_bash_env_prefix(req.env_prefix) if local_windows else req.env_prefix))
else:
runner_lines.append("deactivate 2>/dev/null; hash -r")
_append_venv_nvidia_library_path_lines(runner_lines, cmd=req.cmd)
Expand Down
4 changes: 3 additions & 1 deletion static/js/cookbookDownload.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ let _getPlatform;
let _serverByVal;
let _isWindows;
let _buildEnvPrefix;
let _psQuote;
let _buildServeCmd;
let _detectBackend;
let _detectToolParser;
Expand Down Expand Up @@ -538,7 +539,7 @@ export async function _runModelDownload(panel, model, backend, hostOverride) {
if (srv.downloadDir) payload.local_dir = srv.downloadDir;
if (isWin) {
if (env === 'venv' && envPath) {
payload.env_prefix = '& ' + (envPath.endsWith('\\Scripts\\Activate.ps1') ? envPath : envPath + '\\Scripts\\Activate.ps1');
payload.env_prefix = '& ' + _psQuote(envPath.endsWith('\\Scripts\\Activate.ps1') ? envPath : envPath + '\\Scripts\\Activate.ps1');
} else if (env === 'conda' && envPath) {
payload.env_prefix = 'conda activate ' + envPath;
}
Expand Down Expand Up @@ -652,6 +653,7 @@ export function initDownload(shared) {
_serverByVal = shared._serverByVal;
_isWindows = shared._isWindows;
_buildEnvPrefix = shared._buildEnvPrefix;
_psQuote = shared._psQuote;
_buildServeCmd = shared._buildServeCmd;
_detectBackend = shared._detectBackend;
_detectToolParser = shared._detectToolParser;
Expand Down
4 changes: 3 additions & 1 deletion static/js/cookbookRunning.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ let _sshPrefix;
let _getPlatform;
let _isWindows;
let _buildEnvPrefix;
let _psQuote;
let _loadPresets;
let _savePresets;
let _copyText;
Expand Down Expand Up @@ -1971,7 +1972,7 @@ export async function _launchServeTask(shortName, repo, cmd, fields, hostOverrid
let envPrefix = '';
if (_isWindows()) {
if (_envState.env === 'venv' && _envState.envPath) {
envPrefix = '& ' + (_envState.envPath.endsWith('\\Scripts\\Activate.ps1') ? _envState.envPath : _envState.envPath + '\\Scripts\\Activate.ps1');
envPrefix = '& ' + _psQuote(_envState.envPath.endsWith('\\Scripts\\Activate.ps1') ? _envState.envPath : _envState.envPath + '\\Scripts\\Activate.ps1');
} else if (_envState.env === 'conda' && _envState.envPath) {
envPrefix = 'conda activate ' + _envState.envPath;
}
Expand Down Expand Up @@ -4402,6 +4403,7 @@ export function initRunning(shared) {
_getPlatform = shared._getPlatform;
_isWindows = shared._isWindows;
_buildEnvPrefix = shared._buildEnvPrefix;
_psQuote = shared._psQuote;
_loadPresets = shared._loadPresets;
_savePresets = shared._savePresets;
_copyText = shared._copyText;
Expand Down
65 changes: 65 additions & 0 deletions tests/test_cookbook_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
_llama_cpp_rebuild_cmd,
_append_vllm_linux_preflight_lines,
_local_tooling_path_export,
_local_windows_bash_env_prefix,
_pip_install_attempt,
_pip_install_fallback_chain,
_ollama_bind_from_cmd,
Expand Down Expand Up @@ -107,6 +108,70 @@ def test_safe_env_prefix_accepts_powershell_activation_path():
)


@pytest.mark.parametrize(
("prefix", "expected"),
[
("& 'C:\\Users\\me\\venv\\Scripts\\Activate.ps1'", "source /c/Users/me/venv/Scripts/activate"),
(r"& C:\Users\me\venv\Scripts\Activate.ps1", "source /c/Users/me/venv/Scripts/activate"),
(
r"& C:\Users\me\My Envs\venv\Scripts\Activate.ps1",
"source '/c/Users/me/My Envs/venv/Scripts/activate'",
),
(
"& 'C:\\Users\\me\\My Envs\\venv\\Scripts\\Activate.ps1'",
"source '/c/Users/me/My Envs/venv/Scripts/activate'",
),
(r"& D:/Envs/venv/Scripts/Activate.ps1", "source /d/Envs/venv/Scripts/activate"),
],
)
def test_local_windows_bash_env_prefix_converts_powershell_venv_activation(prefix, expected):
converted = _local_windows_bash_env_prefix(prefix)

assert converted == expected
assert _safe_env_prefix(converted).startswith('[ -f "')


@pytest.mark.parametrize(
"prefix",
[
None,
"",
"source /home/me/venv/bin/activate",
"conda activate qwen35",
'eval "$(conda shell.bash hook)" && conda activate qwen35',
r"& \\server\share\venv\Scripts\Activate.ps1",
],
)
def test_local_windows_bash_env_prefix_leaves_other_prefixes_unchanged(prefix):
assert _local_windows_bash_env_prefix(prefix) == prefix


def test_local_windows_bash_env_prefix_handles_long_whitespace_input():
prefix = "\t" * 100_000

assert _local_windows_bash_env_prefix(prefix) == prefix


@pytest.mark.parametrize(
"relative_path",
["static/js/cookbookRunning.js", "static/js/cookbookDownload.js"],
)
def test_primary_windows_venv_emitters_quote_activation_path(relative_path):
source = (Path(__file__).resolve().parents[1] / relative_path).read_text(encoding="utf-8")

assert "'& ' + _psQuote(" in source
assert "_psQuote = shared._psQuote;" in source


def test_windows_venv_conversion_stays_scoped_to_local_git_bash_runners():
source = (Path(__file__).resolve().parents[1] / "routes/cookbook_routes.py").read_text(encoding="utf-8")
guarded_conversion = (
"_local_windows_bash_env_prefix(req.env_prefix) if local_windows else req.env_prefix"
)

assert source.count(guarded_conversion) == 2


def test_validate_local_dir_accepts_external_drive_paths_with_spaces():
path = "/Volumes/T7 2TB/AI Models/llamacpp"

Expand Down
Loading