Felipe97/llama-cpp-compiled
01.2k
1# Provision UI assets and generate ui.cpp/ui.h.2#3# Asset provisioning priority:4# 1. Pre-built assets in SRC_DIST_DIR (manually built by user)5# 2. If BUILD_UI=ON: npm build6# 3. If above did not produce assets and HF_ENABLED=ON: HF Bucket download7# of dist.tar.gz (verified against dist.tar.gz.sha256)8 9cmake_minimum_required(VERSION 3.18)10 11set(UI_SOURCE_DIR "" CACHE STRING "UI source directory (to run npm build)")12set(UI_BINARY_DIR "" CACHE STRING "UI binary directory (to store generated files)")13set(LLAMA_SOURCE_DIR "" CACHE STRING "Project source root (to resolve version from git)")14set(HF_BUCKET "" CACHE STRING "Hugging Face bucket name")15set(HF_VERSION "" CACHE STRING "Version to download (empty = resolve from git)")16set(HF_ENABLED "" CACHE STRING "Whether to allow HF Bucket download (ON/OFF)")17set(BUILD_UI "" CACHE STRING "Build UI via npm (ON/OFF)")18set(LLAMA_UI_GZIP "" CACHE STRING "Apply gzip compress to assets to save bandwidth")19 20set(DIST_DIR "${UI_BINARY_DIR}/dist")21set(SRC_DIST_DIR "${UI_SOURCE_DIR}/dist")22set(WORK_DIR "${UI_BINARY_DIR}/ui-src")23set(STAMP_FILE "${UI_BINARY_DIR}/.ui-stamp")24set(EMBED_STAMP "${UI_BINARY_DIR}/.ui-embed.sha256")25set(UI_CPP "${UI_BINARY_DIR}/ui.cpp")26set(UI_H "${UI_BINARY_DIR}/ui.h")27 28function(mime_from_ext name out_var)29 string(FIND "${name}" "." ext REVERSE)30 if(ext GREATER -1)31 string(SUBSTRING "${name}" ${ext} -1 ext_full)32 string(SUBSTRING "${ext_full}" 1 -1 ext_str)33 else()34 set(ext_str "")35 endif()36 if(ext_str STREQUAL "html")37 set(m "text/html; charset=utf-8")38 elseif(ext_str STREQUAL "css")39 set(m "text/css")40 elseif(ext_str STREQUAL "js")41 set(m "application/javascript")42 elseif(ext_str STREQUAL "json")43 set(m "application/json")44 elseif(ext_str STREQUAL "webmanifest")45 set(m "application/manifest+json")46 elseif(ext_str STREQUAL "svg")47 set(m "image/svg+xml")48 elseif(ext_str STREQUAL "png")49 set(m "image/png")50 elseif(ext_str STREQUAL "jpg" OR ext_str STREQUAL "jpeg")51 set(m "image/jpeg")52 elseif(ext_str STREQUAL "ico")53 set(m "image/x-icon")54 elseif(ext_str STREQUAL "woff")55 set(m "font/woff")56 elseif(ext_str STREQUAL "woff2")57 set(m "font/woff2")58 else()59 set(m "application/octet-stream")60 endif()61 set(${out_var} "${m}" PARENT_SCOPE)62endfunction()63 64# Fail when a dist tree is present but is missing files the UI needs at65# runtime; catches truncated/stale asset trees early with a useful message.66function(ui_validate_assets files in_dir)67 list(LENGTH files n_assets)68 if(n_assets EQUAL 0)69 return()70 endif()71 72 set(found_index FALSE)73 set(found_manifest FALSE)74 set(found_sw FALSE)75 set(found_build_json FALSE)76 set(found_version_json FALSE)77 set(found_bundle_js FALSE)78 set(found_bundle_css FALSE)79 set(found_workbox_js FALSE)80 81 foreach(f ${files})82 get_filename_component(base "${f}" NAME)83 if(base STREQUAL "index.html")84 set(found_index TRUE)85 elseif(base STREQUAL "manifest.webmanifest")86 set(found_manifest TRUE)87 elseif(base STREQUAL "sw.js")88 set(found_sw TRUE)89 elseif(base STREQUAL "build.json")90 set(found_build_json TRUE)91 elseif(base STREQUAL "version.json")92 set(found_version_json TRUE)93 elseif(base MATCHES "^bundle.*\\.js$")94 set(found_bundle_js TRUE)95 elseif(base MATCHES "^bundle.*\\.css$")96 set(found_bundle_css TRUE)97 elseif(base MATCHES "^workbox.*\\.js$")98 set(found_workbox_js TRUE)99 endif()100 endforeach()101 102 set(missing "")103 if(NOT found_index)104 list(APPEND missing "index.html")105 endif()106 if(NOT found_manifest)107 list(APPEND missing "manifest.webmanifest")108 endif()109 if(NOT found_sw)110 list(APPEND missing "sw.js")111 endif()112 if(NOT found_build_json)113 list(APPEND missing "build.json")114 endif()115 if(NOT found_version_json)116 list(APPEND missing "version.json")117 endif()118 if(NOT found_bundle_js)119 list(APPEND missing "bundle[hash].js")120 endif()121 if(NOT found_bundle_css)122 list(APPEND missing "bundle[hash].css")123 endif()124 if(NOT found_workbox_js)125 list(APPEND missing "workbox[hash].js")126 endif()127 128 if(missing)129 set(listing "")130 foreach(f ${files})131 string(APPEND listing " ${f}\n")132 endforeach()133 set(missing_list "")134 foreach(m ${missing})135 string(APPEND missing_list " ${m}\n")136 endforeach()137 message(FATAL_ERROR138 "UI: current asset files:\n${listing}"139 "UI: missing required asset(s):\n${missing_list}"140 "UI: hint: try cleaning your build directory: ${in_dir}")141 endif()142endfunction()143 144# Generate ui.cpp/ui.h embedding every file of ${dist_dir} (empty table when145# it has no index.html), gzip-compressed when LLAMA_UI_GZIP is enabled.146function(emit_files dist_dir)147 set(UI_TEMPLATE_DIR "${LLAMA_SOURCE_DIR}/tools/ui")148 149 # Collect the asset list once and reuse it for the fingerprint,150 # validation, compression and embedding.151 set(assets "")152 if(EXISTS "${dist_dir}/index.html")153 file(GLOB_RECURSE assets154 LIST_DIRECTORIES false155 RELATIVE "${dist_dir}"156 "${dist_dir}/*")157 list(FILTER assets EXCLUDE REGEX "^_gzip/")158 list(SORT assets)159 endif()160 161 if(LLAMA_UI_GZIP AND NOT DEFINED ENV{SOURCE_DATE_EPOCH})162 # Zero the gzip header timestamp so identical inputs give identical163 # bytes (and therefore stable ETags) on every machine.164 set(ENV{SOURCE_DATE_EPOCH} 0)165 endif()166 167 # Fingerprint of every input that determines ui.cpp/ui.h: compression168 # settings, the asset tree (names + SHA-256) and this script + templates.169 set(fp "${LLAMA_UI_GZIP}|$ENV{SOURCE_DATE_EPOCH}|${CMAKE_VERSION}\n")170 foreach(f ${assets})171 file(SHA256 "${dist_dir}/${f}" h)172 string(APPEND fp "${f} ${h}\n")173 endforeach()174 foreach(g175 "${CMAKE_CURRENT_FUNCTION_LIST_FILE}"176 "${UI_TEMPLATE_DIR}/ui.h.in"177 "${UI_TEMPLATE_DIR}/ui.cpp.in")178 file(SHA256 "${g}" h)179 string(APPEND fp "gen ${h}\n")180 endforeach()181 string(SHA256 fingerprint "${fp}")182 183 if(EXISTS "${EMBED_STAMP}" AND EXISTS "${UI_CPP}" AND EXISTS "${UI_H}")184 file(READ "${EMBED_STAMP}" fp_saved)185 string(STRIP "${fp_saved}" fp_saved)186 if(fp_saved STREQUAL "${fingerprint}")187 message(STATUS "UI: assets unchanged, skipping embedding")188 return()189 endif()190 endif()191 192 # Drop the old stamp up front so a crash mid-generation cannot leave193 # outputs and stamp out of sync.194 file(REMOVE "${EMBED_STAMP}")195 196 ui_validate_assets("${assets}" "${dist_dir}")197 198 set(embed_dir "${dist_dir}")199 set(use_gzip FALSE)200 201 if(EXISTS "${dist_dir}/index.html")202 if(EXISTS "${dist_dir}/_gzip")203 # a _gzip tree inside dist_dir can only be a leftover from an204 # older version of this script that staged it there205 file(REMOVE_RECURSE "${dist_dir}/_gzip")206 message(STATUS "UI: removed stale gzip tree ${dist_dir}/_gzip")207 endif()208 if(LLAMA_UI_GZIP)209 # Compress every asset into a parallel _gzip/ tree under the build210 # directory, served with Content-Encoding: gzip.211 set(gzip_root "${UI_BINARY_DIR}/ui-gzip")212 set(gzip_dir "${gzip_root}/_gzip")213 file(REMOVE_RECURSE "${gzip_root}")214 foreach(f IN LISTS assets)215 get_filename_component(asset_path "${dist_dir}/${f}" REALPATH)216 get_filename_component(dst_dir "${gzip_dir}/${f}" DIRECTORY)217 file(MAKE_DIRECTORY "${dst_dir}")218 file(ARCHIVE_CREATE219 OUTPUT "${gzip_dir}/${f}"220 PATHS "${asset_path}"221 FORMAT raw222 COMPRESSION GZip223 )224 endforeach()225 message(STATUS "UI: gzip compression applied (${gzip_dir})")226 set(embed_dir "${gzip_dir}")227 set(use_gzip TRUE)228 endif()229 endif()230 231 list(LENGTH assets n_assets)232 233 # Per-asset arrays and table rows go into the ui.h.in / ui.cpp.in templates;234 # configure_file only rewrites on content change, avoiding needless recompiles.235 set(ASSET_ARRAYS "")236 set(ASSET_TABLE "")237 set(idx 0)238 239 foreach(f IN LISTS assets)240 file(READ "${embed_dir}/${f}" hex HEX)241 if(hex STREQUAL "")242 message(FATAL_ERROR "UI: empty file: ${embed_dir}/${f}")243 endif()244 245 string(REGEX REPLACE "(..)" "0x\\1," bytes "${hex}")246 file(SHA256 "${embed_dir}/${f}" etag)247 mime_from_ext("${f}" mime)248 249 string(APPEND ASSET_ARRAYS250 "static const unsigned char asset_${idx}[] = {${bytes}};\n")251 252 string(APPEND ASSET_TABLE253 " { \"${f}\", asset_${idx}, sizeof(asset_${idx}), \"\\\"${etag}\\\"\", \"${mime}\" },\n")254 255 math(EXPR idx "${idx} + 1")256 endforeach()257 258 set(LLAMA_UI_HAS_ASSETS 0)259 if(n_assets GREATER 0)260 set(LLAMA_UI_HAS_ASSETS 1)261 endif()262 set(N_ASSETS "${n_assets}")263 set(USE_GZIP false)264 if(use_gzip)265 set(USE_GZIP true)266 endif()267 268 configure_file("${UI_TEMPLATE_DIR}/ui.h.in" "${UI_H}" @ONLY)269 configure_file("${UI_TEMPLATE_DIR}/ui.cpp.in" "${UI_CPP}" @ONLY)270 271 # Write the embed stamp last, after both generated files succeeded.272 file(WRITE "${EMBED_STAMP}" "${fingerprint}")273 message(STATUS "UI: embedded ${n_assets} assets")274endfunction()275 276function(npm_build_should_skip out_var)277 set(${out_var} FALSE PARENT_SCOPE)278 279 if(NOT EXISTS "${DIST_DIR}/index.html")280 return()281 endif()282 283 if(EXISTS "${STAMP_FILE}")284 return()285 endif()286 287 if(NOT EXISTS "${UI_SOURCE_DIR}/sources.cmake")288 return()289 endif()290 include("${UI_SOURCE_DIR}/sources.cmake")291 292 set(globs "")293 foreach(g ${UI_SOURCE_GLOBS})294 list(APPEND globs "${UI_SOURCE_DIR}/${g}")295 endforeach()296 file(GLOB_RECURSE sources ${globs})297 foreach(f ${UI_SOURCE_FILES})298 list(APPEND sources "${UI_SOURCE_DIR}/${f}")299 endforeach()300 301 file(TIMESTAMP "${DIST_DIR}/index.html" out_ts)302 303 foreach(s ${sources})304 if(NOT EXISTS "${s}")305 continue()306 endif()307 file(TIMESTAMP "${s}" s_ts)308 if(s_ts STRGREATER out_ts)309 return()310 endif()311 endforeach()312 313 set(${out_var} TRUE PARENT_SCOPE)314endfunction()315 316function(stage_sources)317 if(EXISTS "${WORK_DIR}")318 file(GLOB staged RELATIVE "${WORK_DIR}" "${WORK_DIR}/*")319 list(REMOVE_ITEM staged "node_modules")320 foreach(entry ${staged})321 file(REMOVE_RECURSE "${WORK_DIR}/${entry}")322 endforeach()323 endif()324 325 file(COPY "${UI_SOURCE_DIR}/"326 DESTINATION "${WORK_DIR}"327 NO_SOURCE_PERMISSIONS328 PATTERN "node_modules" EXCLUDE329 )330endfunction()331 332function(npm_build out_var)333 set(${out_var} FALSE PARENT_SCOPE)334 335 if(NOT EXISTS "${UI_SOURCE_DIR}/package.json")336 message(STATUS "UI: ${UI_SOURCE_DIR}/package.json not found, skipping npm")337 return()338 endif()339 340 npm_build_should_skip(skip)341 if(skip)342 message(STATUS "UI: npm output up-to-date, skipping build")343 set(${out_var} TRUE PARENT_SCOPE)344 return()345 endif()346 347 if(CMAKE_HOST_WIN32)348 find_program(NPM_EXECUTABLE NAMES npm.cmd npm.bat npm)349 else()350 find_program(NPM_EXECUTABLE npm)351 endif()352 if(NOT NPM_EXECUTABLE)353 message(STATUS "UI: npm not found, skipping npm build")354 return()355 endif()356 357 stage_sources()358 359 # npm writes node_modules/.package-lock.json on every successful install,360 # so a package-lock.json newer than this marker means node_modules is stale361 set(NPM_MARKER "${WORK_DIR}/node_modules/.package-lock.json")362 set(need_install FALSE)363 if(NOT EXISTS "${NPM_MARKER}")364 set(need_install TRUE)365 else()366 file(TIMESTAMP "${WORK_DIR}/package-lock.json" lock_ts)367 file(TIMESTAMP "${NPM_MARKER}" marker_ts)368 if(lock_ts STRGREATER marker_ts)369 set(need_install TRUE)370 endif()371 endif()372 373 if(need_install)374 message(STATUS "UI: running npm ci")375 execute_process(376 COMMAND ${NPM_EXECUTABLE} ci377 WORKING_DIRECTORY "${WORK_DIR}"378 RESULT_VARIABLE rc379 ERROR_VARIABLE err380 )381 if(NOT rc EQUAL 0)382 message(STATUS "UI: npm ci failed (${rc})")383 message(STATUS " stderr: ${err}")384 return()385 endif()386 endif()387 388 file(MAKE_DIRECTORY "${DIST_DIR}")389 390 message(STATUS "UI: running npm run build, output -> ${DIST_DIR}")391 execute_process(392 COMMAND ${CMAKE_COMMAND} -E env "LLAMA_UI_OUT_DIR=${DIST_DIR}" "LLAMA_UI_VERSION=${HF_VERSION}" "LLAMA_BUILD_NUMBER=${LLAMA_BUILD_NUMBER}"393 ${NPM_EXECUTABLE} run build394 WORKING_DIRECTORY "${WORK_DIR}"395 RESULT_VARIABLE rc396 ERROR_VARIABLE err397 )398 if(NOT rc EQUAL 0)399 message(STATUS "UI: npm run build failed (${rc})")400 message(STATUS " stderr: ${err}")401 return()402 endif()403 404 if(NOT EXISTS "${DIST_DIR}/index.html")405 message(STATUS "UI: npm build finished but assets missing in ${DIST_DIR}")406 return()407 endif()408 409 message(STATUS "UI: npm build succeeded")410 file(REMOVE "${STAMP_FILE}")411 set(${out_var} TRUE PARENT_SCOPE)412endfunction()413 414function(resolve_version out_var)415 if(NOT "${HF_VERSION}" STREQUAL "")416 set(${out_var} "${HF_VERSION}" PARENT_SCOPE)417 return()418 endif()419 420 if(EXISTS "${LLAMA_SOURCE_DIR}/cmake/build-info.cmake")421 include("${LLAMA_SOURCE_DIR}/cmake/build-info.cmake")422 if(NOT "${BUILD_NUMBER}" STREQUAL "" AND NOT BUILD_NUMBER EQUAL 0)423 set(${out_var} "b${BUILD_NUMBER}" PARENT_SCOPE)424 return()425 endif()426 endif()427 428 set(${out_var} "" PARENT_SCOPE)429endfunction()430 431function(hf_download version out_var out_resolved)432 set(${out_var} FALSE PARENT_SCOPE)433 set(${out_resolved} "" PARENT_SCOPE)434 435 set(archive "${UI_BINARY_DIR}/dist.tar.gz")436 437 # Use HF_TOKEN to benefit from higher rate limits438 set(auth_headers "")439 if(DEFINED ENV{HF_TOKEN} AND NOT "$ENV{HF_TOKEN}" STREQUAL "")440 list(APPEND auth_headers "HTTPHEADER" "Authorization: Bearer $ENV{HF_TOKEN}")441 endif()442 443 set(candidates "")444 if(NOT "${version}" STREQUAL "")445 list(APPEND candidates "${version}")446 endif()447 list(APPEND candidates "latest")448 449 foreach(resolved ${candidates})450 set(base "https://huggingface.co/buckets/${HF_BUCKET}/resolve/${resolved}")451 452 message(STATUS "UI: downloading from ${resolved}: ${base}/dist.tar.gz")453 454 # Fetch the checksum first: when the archive we already have matches455 # it, the expensive download is skipped and only extraction repeats.456 file(DOWNLOAD "${base}/dist.tar.gz.sha256?download=true" "${archive}.sha256"457 STATUS status TIMEOUT 30 ${auth_headers}458 )459 list(GET status 0 rc)460 if(NOT rc EQUAL 0)461 list(GET status 1 errmsg)462 message(STATUS "UI: download dist.tar.gz.sha256 from ${resolved} failed: ${errmsg}")463 continue()464 endif()465 466 # Validate the sha256 checksum: reject anything that is not a full467 # 64-hex-digit digest before touching the archive.468 file(READ "${archive}.sha256" expected)469 string(REGEX MATCH "^[0-9a-fA-F]+" expected "${expected}")470 string(TOLOWER "${expected}" expected)471 string(LENGTH "${expected}" expected_len)472 if(NOT expected_len EQUAL 64)473 message(STATUS "UI: invalid checksum from ${resolved}")474 continue()475 endif()476 477 set(actual "")478 if(EXISTS "${archive}")479 file(SHA256 "${archive}" actual)480 endif()481 482 if("${actual}" STREQUAL "${expected}")483 message(STATUS "UI: local dist.tar.gz matches checksum from ${resolved}, skipping download")484 else()485 file(DOWNLOAD "${base}/dist.tar.gz?download=true" "${archive}"486 STATUS status TIMEOUT 300 ${auth_headers}487 )488 list(GET status 0 rc)489 if(NOT rc EQUAL 0)490 list(GET status 1 errmsg)491 message(STATUS "UI: download dist.tar.gz from ${resolved} failed: ${errmsg}")492 continue()493 endif()494 495 file(SHA256 "${archive}" actual)496 if(NOT "${actual}" STREQUAL "${expected}")497 message(STATUS "UI: checksum mismatch for dist.tar.gz from ${resolved}")498 continue()499 endif()500 endif()501 502 # Remove the stamp with the dist tree it describes, together.503 file(REMOVE "${STAMP_FILE}")504 file(REMOVE_RECURSE "${DIST_DIR}")505 506 file(ARCHIVE_EXTRACT INPUT "${archive}" DESTINATION "${DIST_DIR}")507 508 if(NOT EXISTS "${DIST_DIR}/index.html")509 message(STATUS "UI: archive from ${resolved} is missing required assets")510 continue()511 endif()512 513 message(STATUS "UI: archive verified and extracted")514 set(${out_var} TRUE PARENT_SCOPE)515 set(${out_resolved} "${resolved}" PARENT_SCOPE)516 return()517 endforeach()518endfunction()519 520# ---------------------------------------------------------------------------521# 1. Priority 1: pre-built assets supplied in tools/ui/dist522# ---------------------------------------------------------------------------523if(EXISTS "${SRC_DIST_DIR}/index.html")524 message(STATUS "UI: using pre-built assets from ${SRC_DIST_DIR}")525 emit_files("${SRC_DIST_DIR}")526 return()527endif()528 529# ---------------------------------------------------------------------------530# 2. Priority 2: npm build (if BUILD_UI=ON)531# ---------------------------------------------------------------------------532set(provisioned FALSE)533 534if(BUILD_UI)535 # Resolve version from git build-info if not explicitly set536 resolve_version(HF_VERSION)537 npm_build(NPM_OK)538 if(NPM_OK)539 set(provisioned TRUE)540 endif()541endif()542 543# ---------------------------------------------------------------------------544# 3. Priority 3: HF Bucket download (if npm did not produce assets and HF_ENABLED=ON)545# ---------------------------------------------------------------------------546if(NOT provisioned AND HF_ENABLED)547 resolve_version(VERSION)548 549 # Stamp a successful HF download: records bucket + requested version and550 # lets later steps distinguish downloaded assets from locally built ones.551 set(stamp_key "${HF_BUCKET}|${VERSION}")552 553 set(stamp_ok FALSE)554 if(EXISTS "${STAMP_FILE}" AND EXISTS "${DIST_DIR}/index.html" AND NOT "${VERSION}" STREQUAL "")555 file(READ "${STAMP_FILE}" stamped)556 string(STRIP "${stamped}" stamped)557 if(stamped STREQUAL "${stamp_key}")558 set(stamp_ok TRUE)559 endif()560 endif()561 562 if(stamp_ok)563 message(STATUS "UI: HF stamp matches '${stamp_key}', skipping HF fetch")564 set(provisioned TRUE)565 else()566 hf_download("${VERSION}" HF_OK HF_RESOLVED)567 if(HF_OK)568 file(WRITE "${STAMP_FILE}" "${stamp_key}")569 message(STATUS "UI: HF download succeeded, stamp updated (${stamp_key}, resolved: ${HF_RESOLVED})")570 set(provisioned TRUE)571 else()572 message(STATUS "UI: HF download failed")573 endif()574 endif()575endif()576 577# ---------------------------------------------------------------------------578# 4. Fallback: warn about stale or missing assets, then emit whatever we have579# ---------------------------------------------------------------------------580if(NOT provisioned)581 if(EXISTS "${DIST_DIR}/index.html")582 message(WARNING "UI: provisioning failed; embedding stale assets from ${DIST_DIR}")583 else()584 message(WARNING "UI: no assets available - building without an embedded UI. "585 "In a disconnected environment, download the pre-built UI "586 "from a llama.cpp release at "587 "https://github.com/ggml-org/llama.cpp/releases and "588 "extract to tools/ui/dist.")589 endif()590endif()591 592emit_files("${DIST_DIR}")593 