Compare commits

...
Sign in to create a new pull request.

4 commits

Author SHA1 Message Date
Daniel Han
d901b70c0f Tighten the setup.sh fetch helper comments 2026-07-29 09:06:47 +00:00
Daniel Han
a979cee564 Bound the wget version check by wall clock, not per operation
wget's --timeout is per network operation, so a response that dribbles a byte
inside every interval never ends: against a local drip server the check was
still running after 45s, where curl's --max-time 5 stops at 5s. Wrapping wget
in coreutils timeout gives the same ceiling (5s, rc 124 in the same repro).

timeout is not in a base macOS install, so its absence falls back to the
per-operation bound instead of dropping the check; that path only matters
without curl, which macOS ships.
2026-07-29 08:55:13 +00:00
Daniel Han
3d314ff7a7 Cap the wget version check at one attempt
wget's --timeout is per operation and it retries 20 times by default, so a
server that accepts the request and then stalls stretched the bounded PyPI
version check to 245s against a local stalling server; --tries=1 brings it
back to the 5s curl's --max-time gives.

Adds tests/sh/test_setup_http_get.sh covering both helpers: curl preferred,
wget accepted, neither is a non-zero return, and the wget flags.
2026-07-29 08:45:45 +00:00
Daniel Han
9c3e14acc0 Accept wget in studio/setup.sh, as install.sh already does
install.sh takes either transport everywhere: download() and _http_get both try
curl then wget, and the transport gate only fires when both are missing. A
wget-only box therefore installs fine, reaches studio/setup.sh, and finds curl
as the only way to fetch anything there.

Two sites: the uv bootstrap, where the fallback is silent (USE_UV stays false and
fast_install degrades to python -m pip), and the PyPI version check. Both now go
through a helper with install.sh's preference order.

Verified in a PATH containing wget but no curl: both helpers return 0 and the uv
installer is fetched, while the bare `curl -LsSf` this replaces exits 127.
2026-07-29 08:35:20 +00:00
2 changed files with 142 additions and 3 deletions

View file

@ -972,14 +972,47 @@ install_python_stack() {
python "$SCRIPT_DIR/install_python_stack.py"
}
# ── HTTP GET to stdout (supports curl and wget) ──
# install.sh takes either transport everywhere, so a wget-only box installs fine
# and then stalled here, where curl was the only way to fetch anything.
_setup_http_get() {
if command -v curl >/dev/null 2>&1; then
curl -LsSf "$1"
elif command -v wget >/dev/null 2>&1; then
wget -qO- "$1"
else
return 1
fi
}
# Same, with a deadline, for the checks that must not hang the install.
# wget has nothing like curl's total-transfer --max-time: --timeout is per
# operation and it retries 20 times, so a stalled server took minutes and a slow
# drip never ended. --tries=1 plus an outer `timeout` restores the 5s ceiling;
# without timeout (base macOS, which ships curl anyway) the per-operation bound
# stands rather than the check being dropped.
_setup_http_get_timed() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --max-time 5 "$1"
elif command -v wget >/dev/null 2>&1; then
if command -v timeout >/dev/null 2>&1; then
timeout 5 wget -qO- --timeout=5 --tries=1 "$1"
else
wget -qO- --timeout=5 --tries=1 "$1"
fi
else
return 1
fi
}
USE_UV=false
if command -v uv &>/dev/null; then
USE_UV=true
elif {
if _is_verbose; then
curl -LsSf https://astral.sh/uv/install.sh | sh
_setup_http_get https://astral.sh/uv/install.sh | sh
else
curl -LsSf https://astral.sh/uv/install.sh | sh > /dev/null 2>&1
_setup_http_get https://astral.sh/uv/install.sh | sh > /dev/null 2>&1
fi
}; then
export PATH="$HOME/.local/bin:$PATH"
@ -1020,7 +1053,7 @@ import sys; from importlib.metadata import version
print(version(sys.argv[1]))
" "$_PKG_NAME" 2>/dev/null || echo "")
LATEST_VER=$(curl -fsSL --max-time 5 "https://pypi.org/pypi/$_PKG_NAME/json" 2>/dev/null \
LATEST_VER=$(_setup_http_get_timed "https://pypi.org/pypi/$_PKG_NAME/json" 2>/dev/null \
| "$VENV_DIR/bin/python" -c "import sys,json; print(json.load(sys.stdin)['info']['version'])" 2>/dev/null \
|| echo "")

View file

@ -0,0 +1,106 @@
#!/bin/bash
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
# studio/setup.sh fetch helpers: curl preferred, wget accepted, neither is an error.
# install.sh takes either transport everywhere, so a wget-only box installs fine
# and used to stall in setup.sh, where curl was the only way to fetch anything.
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SETUP_SH="$SCRIPT_DIR/../../studio/setup.sh"
PASS=0
FAIL=0
assert_eq() {
_label="$1"; _expected="$2"; _actual="$3"
if [ "$_actual" = "$_expected" ]; then
echo " PASS: $_label"
PASS=$((PASS + 1))
else
echo " FAIL: $_label (expected '$_expected', got '$_actual')"
FAIL=$((FAIL + 1))
fi
}
assert_contains() {
_label="$1"; _haystack="$2"; _needle="$3"
if echo "$_haystack" | grep -qF -- "$_needle"; then
echo " PASS: $_label"
PASS=$((PASS + 1))
else
echo " FAIL: $_label (expected to find '$_needle' in '$_haystack')"
FAIL=$((FAIL + 1))
fi
}
# ── Extract the two helpers from setup.sh ──
_FN_FILE=$(mktemp)
sed -n '/^_setup_http_get()/,/^}/p' "$SETUP_SH" > "$_FN_FILE"
sed -n '/^_setup_http_get_timed()/,/^}/p' "$SETUP_SH" >> "$_FN_FILE"
for _fn in _setup_http_get _setup_http_get_timed; do
grep -q "^$_fn()" "$_FN_FILE" || { echo " FAIL: $_fn not found in setup.sh"; exit 1; }
done
_MOCK=$(mktemp -d)
_LOG="$_MOCK/argv.log"
_make_shim() {
cat > "$_MOCK/$1" <<EOF
#!/bin/sh
echo "$1 \$*" >> "$_LOG"
echo "$1-body"
EOF
chmod +x "$_MOCK/$1"
}
# PATH holds only the shims, so an absent shim is genuinely absent (not a stub
# that command -v still finds).
_run() {
_have="$1"; _call="$2"
rm -f "$_MOCK"/curl "$_MOCK"/wget "$_MOCK"/timeout "$_LOG"
for _t in $_have; do _make_shim "$_t"; done
( PATH="$_MOCK"; export PATH; . "$_FN_FILE"; $_call "https://example.invalid/x" ) 2>/dev/null
}
_argv() { cat "$_LOG" 2>/dev/null | tr '\n' ' '; }
echo "=== _setup_http_get ==="
assert_eq "curl preferred when both exist" "curl-body" "$(_run 'curl wget' _setup_http_get)"
assert_contains "curl call keeps -LsSf" "$(_argv)" "curl -LsSf"
assert_eq "wget used when curl is missing" "wget-body" "$(_run 'wget' _setup_http_get)"
assert_contains "wget call writes to stdout" "$(_argv)" "wget -qO-"
_out=$(_run '' _setup_http_get || true)
assert_eq "no transport: empty output" "" "$_out"
_rc=0; _run '' _setup_http_get >/dev/null 2>&1 || _rc=$?
assert_eq "no transport: non-zero exit" "1" "$_rc"
echo ""
echo "=== _setup_http_get_timed ==="
assert_eq "curl preferred when both exist" "curl-body" "$(_run 'curl wget' _setup_http_get_timed)"
assert_contains "curl bounds the whole transfer" "$(_argv)" "--max-time 5"
assert_eq "wget used when curl is missing" "wget-body" "$(_run 'wget' _setup_http_get_timed)"
assert_contains "wget sets the timeout" "$(_argv)" "--timeout=5"
# wget's --timeout is per operation and it retries 20 times by default, so
# without --tries=1 a stalling server turns this bounded check into minutes.
assert_contains "wget limited to one attempt" "$(_argv)" "--tries=1"
# --timeout is per operation, so a drip response never ends the transfer; the
# outer timeout is what actually matches curl's --max-time.
assert_eq "timeout wraps wget when available" "timeout-body" "$(_run 'wget timeout' _setup_http_get_timed)"
assert_contains "wall clock deadline on wget" "$(_argv)" "timeout 5 wget -qO- --timeout=5 --tries=1"
assert_eq "no timeout binary: wget still runs" "wget-body" "$(_run 'wget' _setup_http_get_timed)"
_rc=0; _run '' _setup_http_get_timed >/dev/null 2>&1 || _rc=$?
assert_eq "no transport: non-zero exit" "1" "$_rc"
rm -rf "$_MOCK" "$_FN_FILE"
echo ""
echo "Results: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1