Team Ai
Modelpublic

Felipe97/llama-cpp-compiled

sourceHugging Faceupdated 21d agoView on Hugging Face
0likes1.2kdownloads
ui-assets.cmake593 linesDownload Raw Back to scripts
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