|
| 1 | +#!/bin/bash |
| 2 | +# Fetch the prebuilt llama.cpp web UI assets from Hugging Face. |
| 3 | +# |
| 4 | +# Upstream's tools/ui/scripts/ui-assets.cmake pulls the Svelte build outputs |
| 5 | +# from the ggml-org/llama-ui HF bucket. We do the same here so the cosmocc |
| 6 | +# build never has to run a JS toolchain. If the fetch fails (no network, |
| 7 | +# version not yet published, HF down) we leave tools/ui/dist empty; BUILD.mk's |
| 8 | +# embed step then generates a no-asset ui.cpp and the server still works, just |
| 9 | +# without the web UI. |
| 10 | +# |
| 11 | +# Run by apply-patches.sh (i.e. `make setup`); also safe to run standalone to |
| 12 | +# re-fetch the UI without re-applying patches. |
| 13 | + |
| 14 | +set -e |
| 15 | + |
| 16 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 17 | +LLAMA_DIR="$SCRIPT_DIR/../llama.cpp" |
| 18 | + |
| 19 | +HF_BUCKET="${LLAMAFILE_UI_HF_BUCKET:-llama-ui}" |
| 20 | +HF_BASE="https://huggingface.co/buckets/ggml-org/${HF_BUCKET}/resolve" |
| 21 | +HF_TREE_API="https://huggingface.co/api/buckets/ggml-org/${HF_BUCKET}/tree" |
| 22 | +UI_DIST="$LLAMA_DIR/tools/ui/dist" |
| 23 | +UI_ASSETS=(bundle.css bundle.js index.html loading.html) |
| 24 | + |
| 25 | +# Echo the highest bNNNN tag in the bucket whose build number is <= $1, or |
| 26 | +# nothing. The tree API returns directories in ascending order, 100 per page, |
| 27 | +# with a `Link: <...>; rel="next"` header for the following page. |
| 28 | +pick_ui_tag() { |
| 29 | + local cur="$1" |
| 30 | + local url="${HF_TREE_API}?limit=100&recursive=false" |
| 31 | + local hdrs best="" n saw_newer guard=0 body |
| 32 | + hdrs="$(mktemp)" |
| 33 | + while [ -n "$url" ] && [ "$guard" -lt 100 ]; do |
| 34 | + guard=$((guard + 1)) |
| 35 | + body="$(curl -fsSL --max-time 30 -D "$hdrs" "$url" 2>/dev/null)" || break |
| 36 | + saw_newer=0 |
| 37 | + for n in $(printf '%s' "$body" | grep -oE '"path":"b[0-9]+"' \ |
| 38 | + | grep -oE '[0-9]+'); do |
| 39 | + if [ "$n" -le "$cur" ]; then |
| 40 | + if [ -z "$best" ] || [ "$n" -gt "$best" ]; then |
| 41 | + best="$n" |
| 42 | + fi |
| 43 | + else |
| 44 | + saw_newer=1 |
| 45 | + fi |
| 46 | + done |
| 47 | + # Once a page holds a tag newer than us, all later pages are newer too, |
| 48 | + # so there is nothing better to find. |
| 49 | + if [ "$saw_newer" -eq 1 ]; then |
| 50 | + break |
| 51 | + fi |
| 52 | + url="$(grep -i '^link:' "$hdrs" \ |
| 53 | + | grep -oE '<[^>]+>; *rel="next"' | grep -oE 'https?://[^>]+' | head -1)" |
| 54 | + done |
| 55 | + rm -f "$hdrs" |
| 56 | + [ -n "$best" ] && printf 'b%s' "$best" |
| 57 | +} |
| 58 | + |
| 59 | +echo "" |
| 60 | +echo "Fetching prebuilt web UI assets from Hugging Face..." |
| 61 | + |
| 62 | +# Pick the version to download. Upstream's CMake just tries the exact build |
| 63 | +# number and then "latest", but for llamafile that's fragile: the exact tag for |
| 64 | +# our pinned commit is often not published yet, and "latest" can be built |
| 65 | +# against a newer backend than the llama.cpp we've pinned. Instead we enumerate |
| 66 | +# the tags actually present in the bucket and pick the newest one that is <= our |
| 67 | +# build number. "latest" is kept only as a last-resort fallback (enumeration |
| 68 | +# failed, e.g. offline / API change, or our commit predates every published |
| 69 | +# tag) so the build can still get *some* UI rather than none. |
| 70 | +UI_CUR_BUILD="$(cd "$LLAMA_DIR" && git describe --tags --always 2>/dev/null \ |
| 71 | + | grep -oE '^b[0-9]+' | grep -oE '[0-9]+' || true)" |
| 72 | + |
| 73 | +UI_CANDIDATES=() |
| 74 | +if [ -n "$UI_CUR_BUILD" ]; then |
| 75 | + echo " resolving newest UI tag <= b$UI_CUR_BUILD ..." |
| 76 | + UI_BEST_TAG="$(pick_ui_tag "$UI_CUR_BUILD")" |
| 77 | + if [ -n "$UI_BEST_TAG" ]; then |
| 78 | + echo " selected UI tag $UI_BEST_TAG" |
| 79 | + UI_CANDIDATES+=("$UI_BEST_TAG") |
| 80 | + else |
| 81 | + echo " no UI tag <= b$UI_CUR_BUILD found in bucket; will try 'latest'" |
| 82 | + fi |
| 83 | +fi |
| 84 | +UI_CANDIDATES+=("latest") |
| 85 | + |
| 86 | +mkdir -p "$UI_DIST" |
| 87 | +ui_ok=false |
| 88 | +for v in "${UI_CANDIDATES[@]}"; do |
| 89 | + echo " trying $HF_BASE/$v ..." |
| 90 | + fail=false |
| 91 | + for asset in "${UI_ASSETS[@]}" checksums.txt; do |
| 92 | + if ! curl -fsSL --max-time 60 -o "$UI_DIST/$asset" \ |
| 93 | + "$HF_BASE/$v/$asset?download=true"; then |
| 94 | + fail=true |
| 95 | + break |
| 96 | + fi |
| 97 | + done |
| 98 | + if $fail; then |
| 99 | + continue |
| 100 | + fi |
| 101 | + |
| 102 | + # Best-effort sha256 verification against checksums.txt (one "<hash> <name>" |
| 103 | + # line per asset). Skip if shasum/sha256sum isn't around. |
| 104 | + if command -v shasum >/dev/null 2>&1; then |
| 105 | + sha_cmd="shasum -a 256" |
| 106 | + elif command -v sha256sum >/dev/null 2>&1; then |
| 107 | + sha_cmd="sha256sum" |
| 108 | + else |
| 109 | + sha_cmd="" |
| 110 | + fi |
| 111 | + if [ -n "$sha_cmd" ] && [ -f "$UI_DIST/checksums.txt" ]; then |
| 112 | + bad=false |
| 113 | + for asset in "${UI_ASSETS[@]}"; do |
| 114 | + want=$(awk -v a="$asset" '$2 == a { print $1 }' "$UI_DIST/checksums.txt") |
| 115 | + got=$($sha_cmd "$UI_DIST/$asset" | awk '{print $1}') |
| 116 | + if [ -z "$want" ] || [ "$want" != "$got" ]; then |
| 117 | + echo " checksum mismatch for $asset (want=$want got=$got)" |
| 118 | + bad=true |
| 119 | + break |
| 120 | + fi |
| 121 | + done |
| 122 | + if $bad; then |
| 123 | + continue |
| 124 | + fi |
| 125 | + fi |
| 126 | + |
| 127 | + echo " fetched UI assets from $v" |
| 128 | + ui_ok=true |
| 129 | + break |
| 130 | +done |
| 131 | + |
| 132 | +if ! $ui_ok; then |
| 133 | + echo " warning: could not download UI assets; server will build without the web UI" |
| 134 | + rm -f "$UI_DIST"/* |
| 135 | +fi |
0 commit comments