mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-06-28 07:10:21 +00:00
f7ca93d12c
* feat: Add basic PWA support and service worker for offline caching * feat: Vite PWA implementation WIP * feat: Improve PWA icons generation * feat: Add PWA workbox to server routes * feat: Include `version.json` in static assets * feat: Add HTTP cache headers for PWA static assets * feat: Update app name for `apple-mobile-web-app-title` * feat: Implement PWA versioning and automatic update detection * chore: Update `.gitignore` files * feat: Splash Screens * feat: Add dark mode favicon support * refactor: Cleanup * fix: Use dark logo for dark splash screens * refactor: Simplify favicons SVG code * fix: Adjust caching and polling for reliable service worker updates * fix: Add missing favicon entry * fix: Align PWA service worker configuration with SvelteKit build structure * fix: Replace hashed bundle paths with versioned static paths * test: Add PWA tests * ci: Add build output for unit tests * refactor: Cleanup * fix: Server build & release versioning * chore: Update package-lock.json * chore: Increase PWA cache size * chore: Update packages * feat: Update favicons * refactor: Post-merge fix * feat: support explicit build version for PWA cache busting * fix: CI * feat: Improve PWA Refresh Alert UI * feat: Add toggleable build version display * refactor: Cleanup * feat: Add version mismatch detection and manual app reload * refactor: replace dynamic imports with static * refactor: Cleanup * feat: Add safe space for `pwa-<size>.png` rendered icons * fix: use relative paths for PWA assets to support base path deployment * feat: add PWA mode detection via URL query parameter * feat: Use ?cache=true for SW-cached PWA assets * refactor: Build process cleanup * refactor: Decouple PWA versioning and remove ?cache=true workaround * chore: Update README logo * feat: Include PWA Assets generation in build script * refactor: `usePwa` hook for core layout * fix: Relativize base vite plugin * fix: remove unnecessary backslash escapes in test regexes * test: update static asset paths for API Key test * refactor: Move SvelteKit PWA Options config to constants * ui: fix update notification never appearing Keep the PWA hook object intact instead of destructuring needRefreshByStorage, which freezes the reactive getter. Also exclude loading.html from PWA precache to prevent 404 errors and broken SW installation.
455 lines
15 KiB
CMake
455 lines
15 KiB
CMake
# Provision UI assets and generate ui.cpp/ui.h.
|
|
#
|
|
# Asset provisioning priority:
|
|
# 1. Pre-built assets in SRC_DIST_DIR (manually built by user)
|
|
# 2. If BUILD_UI=ON: npm build
|
|
# 3. If above did not produce assets and HF_ENABLED=ON: HF Bucket download
|
|
|
|
cmake_minimum_required(VERSION 3.16)
|
|
|
|
set(UI_SOURCE_DIR "" CACHE STRING "UI source directory (to run npm build)")
|
|
set(UI_BINARY_DIR "" CACHE STRING "UI binary directory (to store generated files)")
|
|
set(LLAMA_SOURCE_DIR "" CACHE STRING "Project source root (to resolve version from git)")
|
|
set(HF_BUCKET "" CACHE STRING "Hugging Face bucket name")
|
|
set(HF_VERSION "" CACHE STRING "Version to download (empty = resolve from git)")
|
|
set(HF_ENABLED "" CACHE STRING "Whether to allow HF Bucket download (ON/OFF)")
|
|
set(BUILD_UI "" CACHE STRING "Build UI via npm (ON/OFF)")
|
|
set(LLAMA_UI_EMBED "" CACHE STRING "Path to llama-ui-embed helper")
|
|
|
|
# IMPORTANT: When adding PWA assets, sync across all 3 places:
|
|
# 1. tools/ui/src/lib/constants/pwa.ts (APPLE_DEVICES, PUBLIC_ENDPOINTS)
|
|
# 2. tools/server/server-http.cpp (public_endpoints)
|
|
# 3. scripts/ui-assets.cmake (ASSETS list)
|
|
# - C++ (server-http.cpp) - public endpoints (splash screens generated via helper)
|
|
# - TypeScript (constants/pwa.ts) - APPLE_DEVICES, PWA_MANIFEST, PUBLIC_ENDPOINTS
|
|
#
|
|
# When adding/changing PWA assets, update tools/ui/src/lib/constants/pwa.ts first,
|
|
# then sync any new file names here and in server-http.cpp.
|
|
set(ASSETS
|
|
index.html
|
|
loading.html
|
|
# PWA assets
|
|
favicon.ico
|
|
favicon-dark.ico
|
|
favicon.svg
|
|
favicon-dark.svg
|
|
pwa-64x64.png
|
|
pwa-192x192.png
|
|
pwa-512x512.png
|
|
maskable-icon-512x512.png
|
|
apple-touch-icon-180x180.png
|
|
# iOS splash screens
|
|
apple-splash-portrait-640x1136.png
|
|
apple-splash-landscape-1136x640.png
|
|
apple-splash-portrait-750x1334.png
|
|
apple-splash-landscape-1334x750.png
|
|
apple-splash-portrait-1170x2532.png
|
|
apple-splash-landscape-2532x1170.png
|
|
apple-splash-portrait-1179x2556.png
|
|
apple-splash-landscape-2556x1179.png
|
|
apple-splash-portrait-1206x2622.png
|
|
apple-splash-landscape-2622x1206.png
|
|
apple-splash-portrait-1284x2778.png
|
|
apple-splash-landscape-2778x1284.png
|
|
apple-splash-portrait-1290x2796.png
|
|
apple-splash-landscape-2796x1290.png
|
|
apple-splash-portrait-1320x2868.png
|
|
apple-splash-landscape-2868x1320.png
|
|
apple-splash-portrait-1488x2266.png
|
|
apple-splash-landscape-2266x1488.png
|
|
apple-splash-portrait-1640x2360.png
|
|
apple-splash-landscape-2360x1640.png
|
|
apple-splash-portrait-1668x2388.png
|
|
apple-splash-landscape-2388x1668.png
|
|
apple-splash-portrait-2048x2732.png
|
|
apple-splash-landscape-2732x2048.png
|
|
# iOS dark splash screens
|
|
apple-splash-portrait-dark-640x1136.png
|
|
apple-splash-landscape-dark-1136x640.png
|
|
apple-splash-portrait-dark-750x1334.png
|
|
apple-splash-landscape-dark-1334x750.png
|
|
apple-splash-portrait-dark-1170x2532.png
|
|
apple-splash-landscape-dark-2532x1170.png
|
|
apple-splash-portrait-dark-1179x2556.png
|
|
apple-splash-landscape-dark-2556x1179.png
|
|
apple-splash-portrait-dark-1206x2622.png
|
|
apple-splash-landscape-dark-2622x1206.png
|
|
apple-splash-portrait-dark-1284x2778.png
|
|
apple-splash-landscape-dark-2778x1284.png
|
|
apple-splash-portrait-dark-1290x2796.png
|
|
apple-splash-landscape-dark-2796x1290.png
|
|
apple-splash-portrait-dark-1320x2868.png
|
|
apple-splash-landscape-dark-2868x1320.png
|
|
apple-splash-portrait-dark-1640x2360.png
|
|
apple-splash-landscape-dark-2360x1640.png
|
|
apple-splash-portrait-dark-1668x2388.png
|
|
apple-splash-landscape-dark-2388x1668.png
|
|
apple-splash-portrait-dark-2048x2732.png
|
|
apple-splash-landscape-dark-2732x2048.png
|
|
manifest.webmanifest
|
|
sw.js
|
|
_app/version.json
|
|
build.json
|
|
)
|
|
|
|
set(DIST_DIR "${UI_BINARY_DIR}/dist")
|
|
set(SRC_DIST_DIR "${UI_SOURCE_DIR}/dist")
|
|
set(STAMP_FILE "${UI_BINARY_DIR}/.ui-stamp")
|
|
set(UI_CPP "${UI_BINARY_DIR}/ui.cpp")
|
|
set(UI_H "${UI_BINARY_DIR}/ui.h")
|
|
|
|
function(assets_present out_var)
|
|
set(present TRUE)
|
|
foreach(asset ${ASSETS})
|
|
if(NOT EXISTS "${DIST_DIR}/${asset}")
|
|
set(present FALSE)
|
|
break()
|
|
endif()
|
|
endforeach()
|
|
set(${out_var} ${present} PARENT_SCOPE)
|
|
endfunction()
|
|
|
|
function(copy_src_dist out_var)
|
|
set(${out_var} FALSE PARENT_SCOPE)
|
|
|
|
foreach(asset ${ASSETS})
|
|
if(NOT EXISTS "${SRC_DIST_DIR}/${asset}")
|
|
return()
|
|
endif()
|
|
endforeach()
|
|
|
|
file(MAKE_DIRECTORY "${DIST_DIR}")
|
|
message(STATUS "UI: using pre-built assets from ${SRC_DIST_DIR}")
|
|
foreach(asset ${ASSETS})
|
|
execute_process(
|
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
|
"${SRC_DIST_DIR}/${asset}" "${DIST_DIR}/${asset}"
|
|
)
|
|
endforeach()
|
|
set(${out_var} TRUE PARENT_SCOPE)
|
|
endfunction()
|
|
|
|
function(npm_build_should_skip out_var)
|
|
set(${out_var} FALSE PARENT_SCOPE)
|
|
|
|
assets_present(present)
|
|
if(NOT present)
|
|
return()
|
|
endif()
|
|
|
|
if(EXISTS "${STAMP_FILE}")
|
|
return()
|
|
endif()
|
|
|
|
if(NOT EXISTS "${UI_SOURCE_DIR}/sources.cmake")
|
|
return()
|
|
endif()
|
|
include("${UI_SOURCE_DIR}/sources.cmake")
|
|
|
|
set(globs "")
|
|
foreach(g ${UI_SOURCE_GLOBS})
|
|
list(APPEND globs "${UI_SOURCE_DIR}/${g}")
|
|
endforeach()
|
|
file(GLOB_RECURSE sources ${globs})
|
|
foreach(f ${UI_SOURCE_FILES})
|
|
list(APPEND sources "${UI_SOURCE_DIR}/${f}")
|
|
endforeach()
|
|
|
|
file(TIMESTAMP "${DIST_DIR}/index.html" out_ts)
|
|
|
|
foreach(s ${sources})
|
|
if(NOT EXISTS "${s}")
|
|
continue()
|
|
endif()
|
|
file(TIMESTAMP "${s}" s_ts)
|
|
if(s_ts STRGREATER out_ts)
|
|
return()
|
|
endif()
|
|
endforeach()
|
|
|
|
set(${out_var} TRUE PARENT_SCOPE)
|
|
endfunction()
|
|
|
|
function(npm_build out_var)
|
|
set(${out_var} FALSE PARENT_SCOPE)
|
|
|
|
if(NOT EXISTS "${UI_SOURCE_DIR}/package.json")
|
|
message(STATUS "UI: ${UI_SOURCE_DIR}/package.json not found, skipping npm")
|
|
return()
|
|
endif()
|
|
|
|
npm_build_should_skip(skip)
|
|
if(skip)
|
|
message(STATUS "UI: npm output up-to-date, skipping build")
|
|
set(${out_var} TRUE PARENT_SCOPE)
|
|
return()
|
|
endif()
|
|
|
|
if(CMAKE_HOST_WIN32)
|
|
find_program(NPM_EXECUTABLE NAMES npm.cmd npm.bat npm)
|
|
else()
|
|
find_program(NPM_EXECUTABLE npm)
|
|
endif()
|
|
if(NOT NPM_EXECUTABLE)
|
|
message(STATUS "UI: npm not found, skipping npm build")
|
|
return()
|
|
endif()
|
|
|
|
# npm writes node_modules/.package-lock.json on every successful install,
|
|
# so a package-lock.json newer than this marker means node_modules is stale
|
|
set(NPM_MARKER "${UI_SOURCE_DIR}/node_modules/.package-lock.json")
|
|
set(need_install FALSE)
|
|
if(NOT EXISTS "${NPM_MARKER}")
|
|
set(need_install TRUE)
|
|
else()
|
|
file(TIMESTAMP "${UI_SOURCE_DIR}/package-lock.json" lock_ts)
|
|
file(TIMESTAMP "${NPM_MARKER}" marker_ts)
|
|
if(lock_ts STRGREATER marker_ts)
|
|
set(need_install TRUE)
|
|
endif()
|
|
endif()
|
|
|
|
if(need_install)
|
|
message(STATUS "UI: running npm install")
|
|
execute_process(
|
|
COMMAND ${NPM_EXECUTABLE} install
|
|
WORKING_DIRECTORY "${UI_SOURCE_DIR}"
|
|
RESULT_VARIABLE rc
|
|
ERROR_VARIABLE err
|
|
)
|
|
if(NOT rc EQUAL 0)
|
|
message(STATUS "UI: npm install failed (${rc})")
|
|
message(STATUS " stderr: ${err}")
|
|
return()
|
|
endif()
|
|
endif()
|
|
|
|
file(MAKE_DIRECTORY "${DIST_DIR}")
|
|
|
|
message(STATUS "UI: running npm run build, output -> ${DIST_DIR}")
|
|
execute_process(
|
|
COMMAND ${CMAKE_COMMAND} -E env "LLAMA_UI_OUT_DIR=${DIST_DIR}" "LLAMA_UI_VERSION=${HF_VERSION}" "LLAMA_BUILD_NUMBER=${LLAMA_BUILD_NUMBER}"
|
|
${NPM_EXECUTABLE} run build
|
|
WORKING_DIRECTORY "${UI_SOURCE_DIR}"
|
|
RESULT_VARIABLE rc
|
|
ERROR_VARIABLE err
|
|
)
|
|
if(NOT rc EQUAL 0)
|
|
message(STATUS "UI: npm run build failed (${rc})")
|
|
message(STATUS " stderr: ${err}")
|
|
return()
|
|
endif()
|
|
|
|
assets_present(present)
|
|
if(NOT present)
|
|
message(STATUS "UI: npm build finished but assets missing in ${DIST_DIR}")
|
|
return()
|
|
endif()
|
|
|
|
message(STATUS "UI: npm build succeeded")
|
|
file(REMOVE "${STAMP_FILE}")
|
|
set(${out_var} TRUE PARENT_SCOPE)
|
|
endfunction()
|
|
|
|
function(resolve_version out_var)
|
|
if(NOT "${HF_VERSION}" STREQUAL "")
|
|
set(${out_var} "${HF_VERSION}" PARENT_SCOPE)
|
|
return()
|
|
endif()
|
|
|
|
if(EXISTS "${LLAMA_SOURCE_DIR}/cmake/build-info.cmake")
|
|
include("${LLAMA_SOURCE_DIR}/cmake/build-info.cmake")
|
|
if(NOT "${BUILD_NUMBER}" STREQUAL "" AND NOT BUILD_NUMBER EQUAL 0)
|
|
set(${out_var} "b${BUILD_NUMBER}" PARENT_SCOPE)
|
|
return()
|
|
endif()
|
|
endif()
|
|
|
|
set(${out_var} "" PARENT_SCOPE)
|
|
endfunction()
|
|
|
|
function(hf_download version out_var out_resolved)
|
|
set(${out_var} FALSE PARENT_SCOPE)
|
|
set(${out_resolved} "" PARENT_SCOPE)
|
|
|
|
file(MAKE_DIRECTORY "${DIST_DIR}")
|
|
|
|
set(candidates "")
|
|
if(NOT "${version}" STREQUAL "")
|
|
list(APPEND candidates "${version}")
|
|
endif()
|
|
list(APPEND candidates "latest")
|
|
|
|
foreach(resolved ${candidates})
|
|
set(base "https://huggingface.co/buckets/ggml-org/${HF_BUCKET}/resolve/${resolved}")
|
|
|
|
message(STATUS "UI: downloading from ${resolved}: ${base}")
|
|
|
|
set(ok TRUE)
|
|
foreach(asset ${ASSETS})
|
|
file(DOWNLOAD "${base}/${asset}?download=true" "${DIST_DIR}/${asset}"
|
|
STATUS status TIMEOUT 60
|
|
)
|
|
list(GET status 0 rc)
|
|
if(NOT rc EQUAL 0)
|
|
list(GET status 1 errmsg)
|
|
message(STATUS "UI: download ${asset} from ${resolved} failed: ${errmsg}")
|
|
set(ok FALSE)
|
|
break()
|
|
endif()
|
|
message(STATUS "UI: downloaded ${asset}")
|
|
endforeach()
|
|
|
|
if(NOT ok)
|
|
continue()
|
|
endif()
|
|
|
|
# Best-effort checksum verification
|
|
file(DOWNLOAD "${base}/checksums.txt?download=true" "${DIST_DIR}/checksums.txt"
|
|
STATUS cs_status TIMEOUT 30
|
|
)
|
|
list(GET cs_status 0 cs_rc)
|
|
if(cs_rc EQUAL 0)
|
|
message(STATUS "UI: verifying checksums")
|
|
file(STRINGS "${DIST_DIR}/checksums.txt" cs_lines)
|
|
foreach(asset ${ASSETS})
|
|
file(SHA256 "${DIST_DIR}/${asset}" h)
|
|
string(TOLOWER "${h}" h)
|
|
string(REGEX MATCH "${h}[ \t]+${asset}" m "${cs_lines}")
|
|
if(NOT m)
|
|
message(WARNING "UI: checksum verification failed for ${asset}")
|
|
set(ok FALSE)
|
|
break()
|
|
endif()
|
|
endforeach()
|
|
if(ok)
|
|
message(STATUS "UI: all checksums verified")
|
|
endif()
|
|
endif()
|
|
|
|
if(ok)
|
|
set(${out_var} TRUE PARENT_SCOPE)
|
|
set(${out_resolved} "${resolved}" PARENT_SCOPE)
|
|
return()
|
|
endif()
|
|
endforeach()
|
|
endfunction()
|
|
|
|
function(emit_files)
|
|
assets_present(present)
|
|
|
|
set(args "${UI_CPP}" "${UI_H}")
|
|
if(present)
|
|
foreach(asset ${ASSETS})
|
|
list(APPEND args "${asset}" "${DIST_DIR}/${asset}")
|
|
endforeach()
|
|
|
|
# Bundle files live in _app/immutable/ — vanilla SvelteKit output, no plugin
|
|
# rewriting. Embedded names must match the exact _app/ paths that index.html
|
|
# and sw.js reference.
|
|
file(GLOB_RECURSE detected_bundle_js "${DIST_DIR}/_app/immutable/bundle.*.js")
|
|
file(GLOB_RECURSE detected_bundle_css "${DIST_DIR}/_app/immutable/assets/bundle.*.css")
|
|
file(GLOB_RECURSE detected_workbox "${DIST_DIR}/workbox-*.js")
|
|
# Compute relative path from DIST_DIR to each found file.
|
|
# e.g. /path/to/build/tools/ui/dist/_app/immutable/bundle.XXX.js
|
|
# -> _app/immutable/bundle.XXX.js
|
|
foreach(f ${detected_bundle_js})
|
|
string(REPLACE "${DIST_DIR}/" "" rel "${f}")
|
|
list(APPEND args "${rel}" "${f}")
|
|
endforeach()
|
|
foreach(f ${detected_bundle_css})
|
|
string(REPLACE "${DIST_DIR}/" "" rel "${f}")
|
|
list(APPEND args "${rel}" "${f}")
|
|
endforeach()
|
|
foreach(f ${detected_workbox})
|
|
string(REPLACE "${DIST_DIR}/" "" rel "${f}")
|
|
list(APPEND args "${rel}" "${f}")
|
|
endforeach()
|
|
endif()
|
|
|
|
# Create build.json with the llama.cpp build number for UI version display.
|
|
# This is separate from SvelteKit's _app/version.json (used for SW cache invalidation).
|
|
# build.json is generated by the vite plugin (buildInfoPlugin) during npm build.
|
|
# CMake just embeds it from the dist that npm produced.
|
|
|
|
execute_process(
|
|
COMMAND "${LLAMA_UI_EMBED}" ${args}
|
|
RESULT_VARIABLE rc
|
|
)
|
|
if(NOT rc EQUAL 0)
|
|
message(FATAL_ERROR "UI: llama-ui-embed failed (${rc})")
|
|
endif()
|
|
endfunction()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Priority 1: pre-built assets supplied in tools/ui/dist
|
|
# ---------------------------------------------------------------------------
|
|
copy_src_dist(SRC_OK)
|
|
if(SRC_OK)
|
|
emit_files()
|
|
return()
|
|
endif()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Priority 2: npm build (if BUILD_UI=ON)
|
|
# ---------------------------------------------------------------------------
|
|
set(provisioned FALSE)
|
|
|
|
if(BUILD_UI)
|
|
# Resolve version from git build-info if not explicitly set
|
|
resolve_version(HF_VERSION)
|
|
npm_build(NPM_OK)
|
|
if(NPM_OK)
|
|
set(provisioned TRUE)
|
|
endif()
|
|
endif()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. Priority 3: HF Bucket download (if npm did not produce assets and HF_ENABLED=ON)
|
|
# ---------------------------------------------------------------------------
|
|
if(NOT provisioned AND HF_ENABLED)
|
|
resolve_version(VERSION)
|
|
|
|
set(stamp_ok FALSE)
|
|
if(EXISTS "${STAMP_FILE}" AND NOT "${VERSION}" STREQUAL "")
|
|
file(READ "${STAMP_FILE}" stamped)
|
|
string(STRIP "${stamped}" stamped)
|
|
if("${stamped}" STREQUAL "${VERSION}")
|
|
set(stamp_ok TRUE)
|
|
endif()
|
|
endif()
|
|
|
|
assets_present(have_assets)
|
|
if(stamp_ok AND have_assets)
|
|
message(STATUS "UI: HF stamp '${stamped}' matches version, skipping HF fetch")
|
|
set(provisioned TRUE)
|
|
else()
|
|
hf_download("${VERSION}" HF_OK HF_RESOLVED)
|
|
if(HF_OK)
|
|
file(WRITE "${STAMP_FILE}" "${HF_RESOLVED}")
|
|
message(STATUS "UI: HF download succeeded, stamp updated (${HF_RESOLVED})")
|
|
set(provisioned TRUE)
|
|
else()
|
|
message(STATUS "UI: HF download failed")
|
|
endif()
|
|
endif()
|
|
endif()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. Fallback: warn about stale or missing assets, then emit whatever we have
|
|
# ---------------------------------------------------------------------------
|
|
if(NOT provisioned)
|
|
assets_present(have_assets)
|
|
if(have_assets)
|
|
message(WARNING "UI: provisioning failed; embedding stale assets from ${DIST_DIR}")
|
|
else()
|
|
message(WARNING "UI: no assets available - building without an embedded UI. "
|
|
"In a disconnected environment, download the pre-built UI "
|
|
"from a llama.cpp release at "
|
|
"https://github.com/ggml-org/llama.cpp/releases and "
|
|
"extract to tools/ui/dist.")
|
|
endif()
|
|
endif()
|
|
|
|
emit_files()
|