diff --git a/studio/setup.sh b/studio/setup.sh index f623c9ab0b..4916f9b0c0 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -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 "") diff --git a/tests/sh/test_setup_http_get.sh b/tests/sh/test_setup_http_get.sh new file mode 100644 index 0000000000..5f35f84de4 --- /dev/null +++ b/tests/sh/test_setup_http_get.sh @@ -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" <> "$_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