ui : embed assets directly with CMake (#28445)
Remove the build-time C++ helper and external gzip dependency, simplifying cross-compilation. Keep the generated C++ in templates for readability and preserve fully embedded UI assets. Signed-off-by: Adrien Gallouët <angt@huggingface.co>
This commit is contained in:
+217
-43
@@ -15,7 +15,6 @@ set(HF_BUCKET "" CACHE STRING "Hugging Face bucket name")
|
|||||||
set(HF_VERSION "" CACHE STRING "Version to download (empty = resolve from git)")
|
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(HF_ENABLED "" CACHE STRING "Whether to allow HF Bucket download (ON/OFF)")
|
||||||
set(BUILD_UI "" CACHE STRING "Build UI via npm (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")
|
|
||||||
set(LLAMA_UI_GZIP "" CACHE STRING "Apply gzip compress to assets to save bandwidth")
|
set(LLAMA_UI_GZIP "" CACHE STRING "Apply gzip compress to assets to save bandwidth")
|
||||||
|
|
||||||
set(DIST_DIR "${UI_BINARY_DIR}/dist")
|
set(DIST_DIR "${UI_BINARY_DIR}/dist")
|
||||||
@@ -25,6 +24,223 @@ set(STAMP_FILE "${UI_BINARY_DIR}/.ui-stamp")
|
|||||||
set(UI_CPP "${UI_BINARY_DIR}/ui.cpp")
|
set(UI_CPP "${UI_BINARY_DIR}/ui.cpp")
|
||||||
set(UI_H "${UI_BINARY_DIR}/ui.h")
|
set(UI_H "${UI_BINARY_DIR}/ui.h")
|
||||||
|
|
||||||
|
function(mime_from_ext name out_var)
|
||||||
|
string(FIND "${name}" "." ext REVERSE)
|
||||||
|
if(ext GREATER -1)
|
||||||
|
string(SUBSTRING "${name}" ${ext} -1 ext_full)
|
||||||
|
string(SUBSTRING "${ext_full}" 1 -1 ext_str)
|
||||||
|
else()
|
||||||
|
set(ext_str "")
|
||||||
|
endif()
|
||||||
|
if(ext_str STREQUAL "html")
|
||||||
|
set(m "text/html; charset=utf-8")
|
||||||
|
elseif(ext_str STREQUAL "css")
|
||||||
|
set(m "text/css")
|
||||||
|
elseif(ext_str STREQUAL "js")
|
||||||
|
set(m "application/javascript")
|
||||||
|
elseif(ext_str STREQUAL "json")
|
||||||
|
set(m "application/json")
|
||||||
|
elseif(ext_str STREQUAL "webmanifest")
|
||||||
|
set(m "application/manifest+json")
|
||||||
|
elseif(ext_str STREQUAL "svg")
|
||||||
|
set(m "image/svg+xml")
|
||||||
|
elseif(ext_str STREQUAL "png")
|
||||||
|
set(m "image/png")
|
||||||
|
elseif(ext_str STREQUAL "jpg" OR ext_str STREQUAL "jpeg")
|
||||||
|
set(m "image/jpeg")
|
||||||
|
elseif(ext_str STREQUAL "ico")
|
||||||
|
set(m "image/x-icon")
|
||||||
|
elseif(ext_str STREQUAL "woff")
|
||||||
|
set(m "font/woff")
|
||||||
|
elseif(ext_str STREQUAL "woff2")
|
||||||
|
set(m "font/woff2")
|
||||||
|
else()
|
||||||
|
set(m "application/octet-stream")
|
||||||
|
endif()
|
||||||
|
set(${out_var} "${m}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Fail when a dist tree is present but is missing files the UI needs at
|
||||||
|
# runtime; catches truncated/stale asset trees early with a useful message.
|
||||||
|
function(ui_validate_assets files in_dir)
|
||||||
|
list(LENGTH files n_assets)
|
||||||
|
if(n_assets EQUAL 0)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(found_index FALSE)
|
||||||
|
set(found_manifest FALSE)
|
||||||
|
set(found_sw FALSE)
|
||||||
|
set(found_build_json FALSE)
|
||||||
|
set(found_version_json FALSE)
|
||||||
|
set(found_bundle_js FALSE)
|
||||||
|
set(found_bundle_css FALSE)
|
||||||
|
set(found_workbox_js FALSE)
|
||||||
|
|
||||||
|
foreach(f ${files})
|
||||||
|
get_filename_component(base "${f}" NAME)
|
||||||
|
if(base STREQUAL "index.html")
|
||||||
|
set(found_index TRUE)
|
||||||
|
elseif(base STREQUAL "manifest.webmanifest")
|
||||||
|
set(found_manifest TRUE)
|
||||||
|
elseif(base STREQUAL "sw.js")
|
||||||
|
set(found_sw TRUE)
|
||||||
|
elseif(base STREQUAL "build.json")
|
||||||
|
set(found_build_json TRUE)
|
||||||
|
elseif(base STREQUAL "version.json")
|
||||||
|
set(found_version_json TRUE)
|
||||||
|
elseif(base MATCHES "^bundle.*\\.js$")
|
||||||
|
set(found_bundle_js TRUE)
|
||||||
|
elseif(base MATCHES "^bundle.*\\.css$")
|
||||||
|
set(found_bundle_css TRUE)
|
||||||
|
elseif(base MATCHES "^workbox.*\\.js$")
|
||||||
|
set(found_workbox_js TRUE)
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
set(missing "")
|
||||||
|
if(NOT found_index)
|
||||||
|
list(APPEND missing "index.html")
|
||||||
|
endif()
|
||||||
|
if(NOT found_manifest)
|
||||||
|
list(APPEND missing "manifest.webmanifest")
|
||||||
|
endif()
|
||||||
|
if(NOT found_sw)
|
||||||
|
list(APPEND missing "sw.js")
|
||||||
|
endif()
|
||||||
|
if(NOT found_build_json)
|
||||||
|
list(APPEND missing "build.json")
|
||||||
|
endif()
|
||||||
|
if(NOT found_version_json)
|
||||||
|
list(APPEND missing "version.json")
|
||||||
|
endif()
|
||||||
|
if(NOT found_bundle_js)
|
||||||
|
list(APPEND missing "bundle[hash].js")
|
||||||
|
endif()
|
||||||
|
if(NOT found_bundle_css)
|
||||||
|
list(APPEND missing "bundle[hash].css")
|
||||||
|
endif()
|
||||||
|
if(NOT found_workbox_js)
|
||||||
|
list(APPEND missing "workbox[hash].js")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(missing)
|
||||||
|
set(listing "")
|
||||||
|
foreach(f ${files})
|
||||||
|
string(APPEND listing " ${f}\n")
|
||||||
|
endforeach()
|
||||||
|
set(missing_list "")
|
||||||
|
foreach(m ${missing})
|
||||||
|
string(APPEND missing_list " ${m}\n")
|
||||||
|
endforeach()
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"UI: current asset files:\n${listing}"
|
||||||
|
"UI: missing required asset(s):\n${missing_list}"
|
||||||
|
"UI: hint: try cleaning your build directory: ${in_dir}")
|
||||||
|
endif()
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Generate ui.cpp/ui.h embedding every file of ${dist_dir} (empty table when
|
||||||
|
# it has no index.html). When LLAMA_UI_GZIP is enabled, assets are compressed
|
||||||
|
# first and served pre-gzipped (llama_ui_use_gzip()).
|
||||||
|
function(emit_files dist_dir)
|
||||||
|
set(embed_dir "${dist_dir}")
|
||||||
|
set(use_gzip FALSE)
|
||||||
|
|
||||||
|
if(EXISTS "${dist_dir}/index.html")
|
||||||
|
if(EXISTS "${dist_dir}/_gzip")
|
||||||
|
# a _gzip tree inside dist_dir can only be a leftover from an
|
||||||
|
# older version of this script that staged it there
|
||||||
|
file(REMOVE_RECURSE "${dist_dir}/_gzip")
|
||||||
|
message(STATUS "UI: removed stale gzip tree ${dist_dir}/_gzip")
|
||||||
|
endif()
|
||||||
|
if(LLAMA_UI_GZIP)
|
||||||
|
# Compress every asset into a parallel _gzip/ tree under the build
|
||||||
|
# directory (never write into the source or dist tree); the
|
||||||
|
# structure stays the same: /abc/def --> /_gzip/abc/def.
|
||||||
|
# FORMAT raw produces a bare gzip stream (no archive container)
|
||||||
|
# that can be served with Content-Encoding: gzip. SOURCE_DATE_EPOCH
|
||||||
|
# zeroes the header timestamp so identical inputs give identical
|
||||||
|
# bytes (and therefore stable ETags) on every machine.
|
||||||
|
if(NOT DEFINED ENV{SOURCE_DATE_EPOCH})
|
||||||
|
set(ENV{SOURCE_DATE_EPOCH} 0)
|
||||||
|
endif()
|
||||||
|
set(gzip_root "${UI_BINARY_DIR}/ui-gzip")
|
||||||
|
set(gzip_dir "${gzip_root}/_gzip")
|
||||||
|
file(REMOVE_RECURSE "${gzip_root}")
|
||||||
|
file(GLOB_RECURSE all_files RELATIVE "${dist_dir}" "${dist_dir}/*")
|
||||||
|
list(FILTER all_files EXCLUDE REGEX "^_gzip/")
|
||||||
|
foreach(f ${all_files})
|
||||||
|
get_filename_component(asset_path "${dist_dir}/${f}" REALPATH)
|
||||||
|
get_filename_component(dst_dir "${gzip_dir}/${f}" DIRECTORY)
|
||||||
|
file(MAKE_DIRECTORY "${dst_dir}")
|
||||||
|
file(ARCHIVE_CREATE
|
||||||
|
OUTPUT "${gzip_dir}/${f}"
|
||||||
|
PATHS "${asset_path}"
|
||||||
|
FORMAT raw
|
||||||
|
COMPRESSION GZip
|
||||||
|
)
|
||||||
|
endforeach()
|
||||||
|
message(STATUS "UI: gzip compression applied (${gzip_dir})")
|
||||||
|
set(embed_dir "${gzip_dir}")
|
||||||
|
set(use_gzip TRUE)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(assets "")
|
||||||
|
if(EXISTS "${embed_dir}/index.html")
|
||||||
|
file(GLOB_RECURSE assets RELATIVE "${embed_dir}" "${embed_dir}/*")
|
||||||
|
list(FILTER assets EXCLUDE REGEX "^_gzip/")
|
||||||
|
list(SORT assets)
|
||||||
|
ui_validate_assets("${assets}" "${embed_dir}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
list(LENGTH assets n_assets)
|
||||||
|
|
||||||
|
# Only the per-asset data arrays and table rows are built here; all
|
||||||
|
# static C++ lives in the ui.h.in / ui.cpp.in templates. configure_file
|
||||||
|
# rewrites an output only when its contents change, so the library is
|
||||||
|
# not recompiled needlessly. @ONLY keeps ${...} in the content literal;
|
||||||
|
# mime types come from a fixed list.
|
||||||
|
set(ASSET_ARRAYS "")
|
||||||
|
set(ASSET_TABLE "")
|
||||||
|
set(idx 0)
|
||||||
|
|
||||||
|
foreach(f IN LISTS assets)
|
||||||
|
file(READ "${embed_dir}/${f}" hex HEX)
|
||||||
|
if(hex STREQUAL "")
|
||||||
|
message(FATAL_ERROR "UI: empty file: ${embed_dir}/${f}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
string(REGEX REPLACE "(..)" "0x\\1," bytes "${hex}")
|
||||||
|
file(SHA256 "${embed_dir}/${f}" etag)
|
||||||
|
mime_from_ext("${f}" mime)
|
||||||
|
|
||||||
|
string(APPEND ASSET_ARRAYS
|
||||||
|
"static const unsigned char asset_${idx}[] = {${bytes}};\n")
|
||||||
|
|
||||||
|
string(APPEND ASSET_TABLE
|
||||||
|
" { \"${f}\", asset_${idx}, sizeof(asset_${idx}), \"\\\"${etag}\\\"\", \"${mime}\" },\n")
|
||||||
|
|
||||||
|
math(EXPR idx "${idx} + 1")
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
set(LLAMA_UI_HAS_ASSETS 0)
|
||||||
|
if(n_assets GREATER 0)
|
||||||
|
set(LLAMA_UI_HAS_ASSETS 1)
|
||||||
|
endif()
|
||||||
|
set(N_ASSETS "${n_assets}")
|
||||||
|
set(USE_GZIP false)
|
||||||
|
if(use_gzip)
|
||||||
|
set(USE_GZIP true)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(UI_TEMPLATE_DIR "${LLAMA_SOURCE_DIR}/tools/ui")
|
||||||
|
configure_file("${UI_TEMPLATE_DIR}/ui.h.in" "${UI_H}" @ONLY)
|
||||||
|
configure_file("${UI_TEMPLATE_DIR}/ui.cpp.in" "${UI_CPP}" @ONLY)
|
||||||
|
message(STATUS "UI: embedded ${n_assets} assets")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
function(npm_build_should_skip out_var)
|
function(npm_build_should_skip out_var)
|
||||||
set(${out_var} FALSE PARENT_SCOPE)
|
set(${out_var} FALSE PARENT_SCOPE)
|
||||||
|
|
||||||
@@ -250,48 +466,6 @@ function(hf_download version out_var out_resolved)
|
|||||||
endforeach()
|
endforeach()
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
function(emit_files dist_dir)
|
|
||||||
# If gzip is requested, compress every asset into a parallel _gzip/ tree
|
|
||||||
# the structure stays the same; for ex: /abc/def --> /_gzip/abc/def
|
|
||||||
# embed.cpp will check for _gzip and will pick it up
|
|
||||||
if(LLAMA_UI_GZIP AND EXISTS "${dist_dir}/index.html")
|
|
||||||
find_program(GZIP_EXECUTABLE gzip)
|
|
||||||
if(NOT GZIP_EXECUTABLE)
|
|
||||||
message(WARNING "UI: LLAMA_UI_GZIP requested but gzip not found, embedding uncompressed")
|
|
||||||
else()
|
|
||||||
set(gzip_dir "${dist_dir}/_gzip")
|
|
||||||
file(REMOVE_RECURSE "${gzip_dir}")
|
|
||||||
file(GLOB_RECURSE all_files RELATIVE "${dist_dir}" "${dist_dir}/*")
|
|
||||||
foreach(f ${all_files})
|
|
||||||
get_filename_component(dst_dir "${gzip_dir}/${f}" DIRECTORY)
|
|
||||||
file(MAKE_DIRECTORY "${dst_dir}")
|
|
||||||
execute_process(
|
|
||||||
COMMAND "${GZIP_EXECUTABLE}" -c "${dist_dir}/${f}"
|
|
||||||
OUTPUT_FILE "${gzip_dir}/${f}"
|
|
||||||
RESULT_VARIABLE gz_rc
|
|
||||||
)
|
|
||||||
if(NOT gz_rc EQUAL 0)
|
|
||||||
message(FATAL_ERROR "UI: gzip failed for ${f}")
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
message(STATUS "UI: gzip compression applied (${gzip_dir})")
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(args "${UI_CPP}" "${UI_H}")
|
|
||||||
if(EXISTS "${dist_dir}/index.html")
|
|
||||||
list(APPEND args "${dist_dir}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
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
|
# 1. Priority 1: pre-built assets supplied in tools/ui/dist
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ target_include_directories(${TARGET} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
|||||||
target_include_directories(${TARGET} PRIVATE ../mtmd ${CMAKE_SOURCE_DIR})
|
target_include_directories(${TARGET} PRIVATE ../mtmd ${CMAKE_SOURCE_DIR})
|
||||||
target_link_libraries(${TARGET} PUBLIC server-context llama-ui cpp-httplib ${CMAKE_THREAD_LIBS_INIT})
|
target_link_libraries(${TARGET} PUBLIC server-context llama-ui cpp-httplib ${CMAKE_THREAD_LIBS_INIT})
|
||||||
|
|
||||||
|
add_dependencies(${TARGET} llama-ui-assets)
|
||||||
|
|
||||||
if(LLAMA_TOOLS_INSTALL)
|
if(LLAMA_TOOLS_INSTALL)
|
||||||
install(TARGETS ${TARGET} LIBRARY)
|
install(TARGETS ${TARGET} LIBRARY)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
+5
-57
@@ -36,60 +36,11 @@ endif()
|
|||||||
set(UI_CPP "${CMAKE_CURRENT_BINARY_DIR}/ui.cpp")
|
set(UI_CPP "${CMAKE_CURRENT_BINARY_DIR}/ui.cpp")
|
||||||
set(UI_H "${CMAKE_CURRENT_BINARY_DIR}/ui.h")
|
set(UI_H "${CMAKE_CURRENT_BINARY_DIR}/ui.h")
|
||||||
|
|
||||||
if(CMAKE_CROSSCOMPILING)
|
# Provision assets and generate ui.cpp/ui.h natively in CMake at build time.
|
||||||
find_program(HOST_CXX_COMPILER NAMES g++ clang++ NO_CMAKE_FIND_ROOT_PATH)
|
# The generated sources are compiled by the regular target toolchain; no
|
||||||
if(NOT HOST_CXX_COMPILER)
|
# build-time host executable is needed (works in any cross-compile setup).
|
||||||
message(FATAL_ERROR "UI: no host C++ compiler (g++/clang++) found to build llama-ui-embed; set -DHOST_CXX_COMPILER=<path>")
|
# The script uses copy_if_different semantics, so the library below only
|
||||||
endif()
|
# recompiles when the generated contents actually change.
|
||||||
message(STATUS "UI: building llama-ui-embed with host compiler ${HOST_CXX_COMPILER}")
|
|
||||||
|
|
||||||
if(CMAKE_HOST_WIN32)
|
|
||||||
set(LLAMA_UI_EMBED_EXE "${CMAKE_CURRENT_BINARY_DIR}/llama-ui-embed-host.exe")
|
|
||||||
else()
|
|
||||||
set(LLAMA_UI_EMBED_EXE "${CMAKE_CURRENT_BINARY_DIR}/llama-ui-embed-host")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_custom_command(
|
|
||||||
OUTPUT "${LLAMA_UI_EMBED_EXE}"
|
|
||||||
COMMAND "${HOST_CXX_COMPILER}" -O2 -std=c++17
|
|
||||||
-o "${LLAMA_UI_EMBED_EXE}" "${CMAKE_CURRENT_SOURCE_DIR}/embed.cpp"
|
|
||||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/embed.cpp"
|
|
||||||
COMMENT "Building llama-ui-embed (host)"
|
|
||||||
VERBATIM
|
|
||||||
)
|
|
||||||
|
|
||||||
# phony target to tie it into the dependency graph
|
|
||||||
add_custom_target(llama-ui-embed DEPENDS "${LLAMA_UI_EMBED_EXE}")
|
|
||||||
else()
|
|
||||||
# exclude llama-ui-embed from sanitizer flags,
|
|
||||||
# it's a build-time-only tool, no need to instrument it
|
|
||||||
# this is to fix TSan "memory layout is incompatible" error on CI
|
|
||||||
get_directory_property(_llama_ui_dir_co COMPILE_OPTIONS)
|
|
||||||
get_directory_property(_llama_ui_dir_ll LINK_LIBRARIES)
|
|
||||||
set(_llama_ui_embed_co ${_llama_ui_dir_co})
|
|
||||||
set(_llama_ui_embed_ll ${_llama_ui_dir_ll})
|
|
||||||
list(FILTER _llama_ui_embed_co EXCLUDE REGEX ".*-fsanitize=.*")
|
|
||||||
list(FILTER _llama_ui_embed_ll EXCLUDE REGEX ".*-fsanitize=.*")
|
|
||||||
set_directory_properties(PROPERTIES
|
|
||||||
COMPILE_OPTIONS "${_llama_ui_embed_co}"
|
|
||||||
LINK_LIBRARIES "${_llama_ui_embed_ll}")
|
|
||||||
|
|
||||||
add_executable(llama-ui-embed embed.cpp)
|
|
||||||
target_compile_features(llama-ui-embed PRIVATE cxx_std_17)
|
|
||||||
set_target_properties(llama-ui-embed PROPERTIES
|
|
||||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
|
||||||
)
|
|
||||||
set(LLAMA_UI_EMBED_EXE "$<TARGET_FILE:llama-ui-embed>")
|
|
||||||
|
|
||||||
# restore so the llama-ui library below keeps sanitizer instrumentation
|
|
||||||
set_directory_properties(PROPERTIES
|
|
||||||
COMPILE_OPTIONS "${_llama_ui_dir_co}"
|
|
||||||
LINK_LIBRARIES "${_llama_ui_dir_ll}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Run the provisioning script every build so source changes in tools/ui/ are
|
|
||||||
# always picked up. The script uses copy_if_different for ui.cpp/ui.h, so the
|
|
||||||
# library only recompiles when contents actually change.
|
|
||||||
add_custom_target(llama-ui-assets ALL
|
add_custom_target(llama-ui-assets ALL
|
||||||
BYPRODUCTS ${UI_CPP} ${UI_H}
|
BYPRODUCTS ${UI_CPP} ${UI_H}
|
||||||
COMMAND ${CMAKE_COMMAND}
|
COMMAND ${CMAKE_COMMAND}
|
||||||
@@ -101,15 +52,12 @@ add_custom_target(llama-ui-assets ALL
|
|||||||
"-DHF_VERSION=${HF_UI_VERSION}"
|
"-DHF_VERSION=${HF_UI_VERSION}"
|
||||||
"-DHF_ENABLED=${LLAMA_USE_PREBUILT_UI}"
|
"-DHF_ENABLED=${LLAMA_USE_PREBUILT_UI}"
|
||||||
"-DBUILD_UI=${LLAMA_BUILD_UI}"
|
"-DBUILD_UI=${LLAMA_BUILD_UI}"
|
||||||
"-DLLAMA_UI_EMBED=${LLAMA_UI_EMBED_EXE}"
|
|
||||||
"-DLLAMA_UI_GZIP=${LLAMA_UI_GZIP}"
|
"-DLLAMA_UI_GZIP=${LLAMA_UI_GZIP}"
|
||||||
-P "${PROJECT_SOURCE_DIR}/scripts/ui-assets.cmake"
|
-P "${PROJECT_SOURCE_DIR}/scripts/ui-assets.cmake"
|
||||||
COMMENT "Provisioning UI assets"
|
COMMENT "Provisioning UI assets"
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
|
|
||||||
add_dependencies(llama-ui-assets llama-ui-embed)
|
|
||||||
|
|
||||||
set_source_files_properties(${UI_CPP} ${UI_H} PROPERTIES GENERATED TRUE)
|
set_source_files_properties(${UI_CPP} ${UI_H} PROPERTIES GENERATED TRUE)
|
||||||
|
|
||||||
add_library(${TARGET} STATIC ${UI_CPP} ${UI_H})
|
add_library(${TARGET} STATIC ${UI_CPP} ${UI_H})
|
||||||
|
|||||||
@@ -1,308 +0,0 @@
|
|||||||
// llama-ui-embed: generate ui.cpp / ui.h that embed UI assets as C arrays.
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
// llama-ui-embed <out_cpp> <out_h> [<asset_dir>]
|
|
||||||
//
|
|
||||||
// Recursively embeds every regular file under <asset_dir>.
|
|
||||||
// Asset names are relative paths from <asset_dir> (e.g. "_app/immutable/bundle.HASH.js").
|
|
||||||
// Without <asset_dir>, emits an empty asset table.
|
|
||||||
|
|
||||||
#include <inttypes.h>
|
|
||||||
#include <stdarg.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <fstream>
|
|
||||||
#include <functional>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
|
|
||||||
static const char * mime_from_ext(const std::string & name) {
|
|
||||||
auto ext = name.rfind('.');
|
|
||||||
if (ext == std::string::npos) return "application/octet-stream";
|
|
||||||
std::string e = name.substr(ext + 1);
|
|
||||||
if (e == "html") return "text/html; charset=utf-8";
|
|
||||||
if (e == "css") return "text/css";
|
|
||||||
if (e == "js") return "application/javascript";
|
|
||||||
if (e == "json") return "application/json";
|
|
||||||
if (e == "webmanifest") return "application/manifest+json";
|
|
||||||
if (e == "svg") return "image/svg+xml";
|
|
||||||
if (e == "png") return "image/png";
|
|
||||||
if (e == "jpg" ||
|
|
||||||
e == "jpeg") return "image/jpeg";
|
|
||||||
if (e == "ico") return "image/x-icon";
|
|
||||||
if (e == "woff") return "font/woff";
|
|
||||||
if (e == "woff2") return "font/woff2";
|
|
||||||
return "application/octet-stream";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Computes FNV-1a hash of the data
|
|
||||||
static uint64_t fnv_hash(const uint8_t * data, size_t len) {
|
|
||||||
const uint64_t fnv_prime = 0x100000001b3ULL;
|
|
||||||
uint64_t hash = 0xcbf29ce484222325ULL;
|
|
||||||
|
|
||||||
for (size_t i = 0; i < len; ++i) {
|
|
||||||
hash ^= data[i];
|
|
||||||
hash *= fnv_prime;
|
|
||||||
}
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool read_file(const std::filesystem::path & path, std::vector<unsigned char> & out) {
|
|
||||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
|
||||||
if (!f) {
|
|
||||||
fprintf(stderr, "embed: cannot open %s\n", path.string().c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const auto sz = f.tellg();
|
|
||||||
if (sz < 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
f.seekg(0);
|
|
||||||
out.resize(static_cast<size_t>(sz));
|
|
||||||
if (sz > 0 && !f.read(reinterpret_cast<char *>(out.data()), sz)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void append_bytes_hex(std::string & out, const std::vector<unsigned char> & bytes) {
|
|
||||||
static const char hex[] = "0123456789abcdef";
|
|
||||||
out.reserve(out.size() + bytes.size() * 5);
|
|
||||||
for (unsigned char b : bytes) {
|
|
||||||
out += '0';
|
|
||||||
out += 'x';
|
|
||||||
out += hex[b >> 4];
|
|
||||||
out += hex[b & 0xf];
|
|
||||||
out += ',';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool write_if_different(const std::string & path, const std::string & content) {
|
|
||||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
|
||||||
if (f) {
|
|
||||||
const auto sz = f.tellg();
|
|
||||||
if (sz >= 0 && static_cast<size_t>(sz) == content.size()) {
|
|
||||||
std::string existing(static_cast<size_t>(sz), '\0');
|
|
||||||
f.seekg(0);
|
|
||||||
if (sz == 0 || f.read(existing.data(), sz)) {
|
|
||||||
if (existing == content) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
|
||||||
if (!out) {
|
|
||||||
fprintf(stderr, "embed: cannot write %s\n", path.c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!content.empty()) {
|
|
||||||
out.write(content.data(), static_cast<std::streamsize>(content.size()));
|
|
||||||
}
|
|
||||||
bool ok = out.good();
|
|
||||||
if (ok) {
|
|
||||||
printf("embed: write output file %s\n", path.c_str());
|
|
||||||
}
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::string path_basename(const std::string & name) {
|
|
||||||
const size_t p = name.rfind('/');
|
|
||||||
return p == std::string::npos ? name : name.substr(p + 1);
|
|
||||||
}
|
|
||||||
static bool str_starts_with(const std::string & s, const char * prefix) {
|
|
||||||
const size_t n = strlen(prefix);
|
|
||||||
return s.size() >= n && s.compare(0, n, prefix) == 0;
|
|
||||||
}
|
|
||||||
static bool str_ends_with(const std::string & s, const char * suffix) {
|
|
||||||
const size_t n = strlen(suffix);
|
|
||||||
return s.size() >= n && s.compare(s.size() - n, n, suffix) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::string fmt(const char * pattern, ...) {
|
|
||||||
char tmp[512];
|
|
||||||
va_list ap;
|
|
||||||
va_start(ap, pattern);
|
|
||||||
const int n = vsnprintf(tmp, sizeof(tmp), pattern, ap);
|
|
||||||
va_end(ap);
|
|
||||||
return (n > 0) ? std::string(tmp, static_cast<size_t>(n)) : std::string();
|
|
||||||
}
|
|
||||||
|
|
||||||
struct asset_entry {
|
|
||||||
std::string name;
|
|
||||||
std::filesystem::path path;
|
|
||||||
};
|
|
||||||
|
|
||||||
int main(int argc, char ** argv) {
|
|
||||||
if (argc < 3 || argc > 4) {
|
|
||||||
fprintf(stderr, "usage: %s <out_cpp> <out_h> [<asset_dir>]\n", argv[0]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string out_cpp = argv[1];
|
|
||||||
const std::string out_h = argv[2];
|
|
||||||
const std::string asset_dir = (argc >= 4) ? argv[3] : std::string();
|
|
||||||
|
|
||||||
const bool use_gzip = !asset_dir.empty() && std::filesystem::exists(asset_dir + "/_gzip");
|
|
||||||
const std::string in_dir = use_gzip ? (asset_dir + "/_gzip") : asset_dir;
|
|
||||||
|
|
||||||
std::vector<asset_entry> assets;
|
|
||||||
if (!in_dir.empty()) {
|
|
||||||
const std::filesystem::path dir = in_dir;
|
|
||||||
|
|
||||||
std::error_code ec;
|
|
||||||
std::filesystem::recursive_directory_iterator it(dir, ec);
|
|
||||||
if (ec) {
|
|
||||||
fprintf(stderr, "embed: cannot iterate %s: %s\n", argv[3], ec.message().c_str());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
for (const auto & entry : it) {
|
|
||||||
if (!entry.is_regular_file()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// name is the relative path from dir, with forward slashes
|
|
||||||
const std::string name = entry.path().lexically_relative(dir).generic_string();
|
|
||||||
assets.push_back({ name, entry.path() });
|
|
||||||
}
|
|
||||||
|
|
||||||
// directory iteration order is unspecified; sort for reproducible output
|
|
||||||
std::sort(assets.begin(), assets.end(),
|
|
||||||
[](const asset_entry & a, const asset_entry & b) { return a.name < b.name; });
|
|
||||||
}
|
|
||||||
|
|
||||||
const int n_assets = static_cast<int>(assets.size());
|
|
||||||
|
|
||||||
if (n_assets > 0) {
|
|
||||||
using match_fn = std::function<bool(const std::string &)>;
|
|
||||||
auto exact = [](const char * name) -> match_fn {
|
|
||||||
return [name](const std::string & base) { return base == name; };
|
|
||||||
};
|
|
||||||
|
|
||||||
struct required_check { const char * label; match_fn match; bool found; };
|
|
||||||
required_check checks[] = {
|
|
||||||
{ "index.html", exact("index.html"), false },
|
|
||||||
{ "manifest.webmanifest", exact("manifest.webmanifest"), false },
|
|
||||||
{ "sw.js", exact("sw.js"), false },
|
|
||||||
{ "build.json", exact("build.json"), false },
|
|
||||||
{ "version.json", exact("version.json"), false },
|
|
||||||
{ "bundle[hash].js", [](const std::string & b) {
|
|
||||||
return str_starts_with(b, "bundle") && str_ends_with(b, ".js");
|
|
||||||
}, false },
|
|
||||||
{ "bundle[hash].css", [](const std::string & b) {
|
|
||||||
return str_starts_with(b, "bundle") && str_ends_with(b, ".css");
|
|
||||||
}, false },
|
|
||||||
{ "workbox[hash].js", [](const std::string & b) {
|
|
||||||
return str_starts_with(b, "workbox") && str_ends_with(b, ".js");
|
|
||||||
}, false },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto & a : assets) {
|
|
||||||
const std::string base = path_basename(a.name);
|
|
||||||
for (auto & c : checks) {
|
|
||||||
if (!c.found) { c.found = c.match(base); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<const char *> missing;
|
|
||||||
for (const auto & c : checks) {
|
|
||||||
if (!c.found) { missing.push_back(c.label); }
|
|
||||||
}
|
|
||||||
if (!missing.empty()) {
|
|
||||||
fprintf(stderr, "\ncurrent asset files:\n");
|
|
||||||
for (const auto & a : assets) {
|
|
||||||
fprintf(stderr, " %s\n", a.name.c_str());
|
|
||||||
}
|
|
||||||
fprintf(stderr, "missing required asset(s):\n");
|
|
||||||
for (const char * m : missing) {
|
|
||||||
fprintf(stderr, " %s\n", m);
|
|
||||||
}
|
|
||||||
fprintf(stderr, "hint: try cleaning your build directory: %s\n", in_dir.c_str());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string h;
|
|
||||||
h += "#pragma once\n\n#include <array>\n#include <string>\n\n";
|
|
||||||
if (n_assets > 0) {
|
|
||||||
h += "#define LLAMA_UI_HAS_ASSETS 1\n\n";
|
|
||||||
}
|
|
||||||
h +=
|
|
||||||
"struct llama_ui_asset {\n"
|
|
||||||
" std::string name;\n"
|
|
||||||
" const unsigned char * data;\n"
|
|
||||||
" std::size_t size;\n"
|
|
||||||
" std::string etag;\n"
|
|
||||||
" std::string type;\n"
|
|
||||||
"};\n\n"
|
|
||||||
"const llama_ui_asset * llama_ui_find_asset(const std::string & name);\n"
|
|
||||||
"bool llama_ui_use_gzip();\n";
|
|
||||||
h += fmt("const std::array<llama_ui_asset, %d> & llama_ui_get_assets();\n", n_assets);
|
|
||||||
|
|
||||||
std::string cpp;
|
|
||||||
cpp += "#include \"ui.h\"\n\n";
|
|
||||||
|
|
||||||
if (n_assets > 0) {
|
|
||||||
for (int i = 0; i < n_assets; i++) {
|
|
||||||
std::vector<unsigned char> bytes;
|
|
||||||
if (!read_file(assets[i].path, bytes)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (bytes.empty()) {
|
|
||||||
fprintf(stderr, "embed: empty file: %s\n", assets[i].path.generic_string().c_str());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
cpp += fmt("static const unsigned char asset_%d_data[] = {", i);
|
|
||||||
append_bytes_hex(cpp, bytes);
|
|
||||||
|
|
||||||
// note: this is a simple hash for cache busting, not a cryptographic hash; fnv is enough here
|
|
||||||
const auto hash = fnv_hash(bytes.data(), bytes.size());
|
|
||||||
|
|
||||||
cpp += fmt("};\nstatic const std::size_t asset_%d_size = %zu;\n",
|
|
||||||
i, bytes.size());
|
|
||||||
cpp += fmt("static const char asset_%d_etag[] = \"\\\"0x%016" PRIx64 "\\\"\";\n\n",
|
|
||||||
i, hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
cpp += fmt("static const std::array<llama_ui_asset, %d> g_assets = {{\n", n_assets);
|
|
||||||
for (int i = 0; i < n_assets; i++) {
|
|
||||||
const std::string & name = assets[i].name;
|
|
||||||
cpp += fmt(" { \"%s\", asset_%d_data, asset_%d_size, asset_%d_etag, \"%s\" },\n",
|
|
||||||
name.c_str(), i, i, i, mime_from_ext(name));
|
|
||||||
}
|
|
||||||
cpp += "}};\n\n";
|
|
||||||
|
|
||||||
cpp +=
|
|
||||||
"const llama_ui_asset * llama_ui_find_asset(const std::string & name) {\n"
|
|
||||||
" for (const auto & a : g_assets) {\n"
|
|
||||||
" if (a.name == name) {\n"
|
|
||||||
" return &a;\n"
|
|
||||||
" }\n"
|
|
||||||
" }\n"
|
|
||||||
" return nullptr;\n"
|
|
||||||
"}\n";
|
|
||||||
cpp += fmt("const std::array<llama_ui_asset, %d> & llama_ui_get_assets() {\n", n_assets);
|
|
||||||
cpp += " return g_assets;\n"
|
|
||||||
"}\n";
|
|
||||||
} else {
|
|
||||||
cpp +=
|
|
||||||
"const llama_ui_asset * llama_ui_find_asset(const std::string &) {\n"
|
|
||||||
" return nullptr;\n"
|
|
||||||
"}\n"
|
|
||||||
"const std::array<llama_ui_asset, 0> & llama_ui_get_assets() {\n"
|
|
||||||
" static const std::array<llama_ui_asset, 0> empty{};\n"
|
|
||||||
" return empty;\n"
|
|
||||||
"}\n";
|
|
||||||
}
|
|
||||||
cpp += fmt("bool llama_ui_use_gzip() { return %s; }\n", use_gzip ? "true" : "false");
|
|
||||||
|
|
||||||
bool ok = true;
|
|
||||||
ok = write_if_different(out_h, h) && ok;
|
|
||||||
ok = write_if_different(out_cpp, cpp) && ok;
|
|
||||||
return ok ? 0 : 1;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Generated by scripts/ui-assets.cmake - do not edit.
|
||||||
|
|
||||||
|
#include "ui.h"
|
||||||
|
|
||||||
|
@ASSET_ARRAYS@
|
||||||
|
#if defined(LLAMA_UI_HAS_ASSETS)
|
||||||
|
static const std::array<llama_ui_asset, @N_ASSETS@> g_assets = {{
|
||||||
|
@ASSET_TABLE@
|
||||||
|
}};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const llama_ui_asset * llama_ui_find_asset(const std::string & name) {
|
||||||
|
#if defined(LLAMA_UI_HAS_ASSETS)
|
||||||
|
for (const auto & a : g_assets) {
|
||||||
|
if (a.name == name) {
|
||||||
|
return &a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
(void) name;
|
||||||
|
#endif
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::array<llama_ui_asset, @N_ASSETS@> & llama_ui_get_assets() {
|
||||||
|
#if defined(LLAMA_UI_HAS_ASSETS)
|
||||||
|
return g_assets;
|
||||||
|
#else
|
||||||
|
static const std::array<llama_ui_asset, 0> empty{};
|
||||||
|
return empty;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool llama_ui_use_gzip() {
|
||||||
|
return @USE_GZIP@;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Generated by scripts/ui-assets.cmake - do not edit.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Defined as 1 only when assets were embedded (tools/server checks defined()).
|
||||||
|
#cmakedefine LLAMA_UI_HAS_ASSETS 1
|
||||||
|
|
||||||
|
struct llama_ui_asset {
|
||||||
|
std::string name;
|
||||||
|
const unsigned char * data;
|
||||||
|
std::size_t size;
|
||||||
|
std::string etag;
|
||||||
|
std::string type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const llama_ui_asset * llama_ui_find_asset(const std::string & name);
|
||||||
|
bool llama_ui_use_gzip();
|
||||||
|
const std::array<llama_ui_asset, @N_ASSETS@> & llama_ui_get_assets();
|
||||||
Reference in New Issue
Block a user