From c457e3bf7fa88a7ccfa31f1dbf358ac1ebd90e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Gallou=C3=ABt?= Date: Sun, 6 Sep 2026 07:49:39 +0200 Subject: [PATCH] ui : embed assets directly with CMake (#28445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- scripts/ui-assets.cmake | 260 +++++++++++++++++++++++++----- tools/server/CMakeLists.txt | 2 + tools/ui/CMakeLists.txt | 62 +------- tools/ui/embed.cpp | 308 ------------------------------------ tools/ui/ui.cpp.in | 36 +++++ tools/ui/ui.h.in | 21 +++ 6 files changed, 281 insertions(+), 408 deletions(-) delete mode 100644 tools/ui/embed.cpp create mode 100644 tools/ui/ui.cpp.in create mode 100644 tools/ui/ui.h.in diff --git a/scripts/ui-assets.cmake b/scripts/ui-assets.cmake index 0c1c4de555..402f95bd4f 100644 --- a/scripts/ui-assets.cmake +++ b/scripts/ui-assets.cmake @@ -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_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") set(LLAMA_UI_GZIP "" CACHE STRING "Apply gzip compress to assets to save bandwidth") 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_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) set(${out_var} FALSE PARENT_SCOPE) @@ -250,48 +466,6 @@ function(hf_download version out_var out_resolved) endforeach() 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 # --------------------------------------------------------------------------- diff --git a/tools/server/CMakeLists.txt b/tools/server/CMakeLists.txt index 280bd9e19d..43c2456333 100644 --- a/tools/server/CMakeLists.txt +++ b/tools/server/CMakeLists.txt @@ -50,6 +50,8 @@ target_include_directories(${TARGET} PUBLIC ${CMAKE_CURRENT_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}) +add_dependencies(${TARGET} llama-ui-assets) + if(LLAMA_TOOLS_INSTALL) install(TARGETS ${TARGET} LIBRARY) endif() diff --git a/tools/ui/CMakeLists.txt b/tools/ui/CMakeLists.txt index 208b46a5c1..79ffe9fc17 100644 --- a/tools/ui/CMakeLists.txt +++ b/tools/ui/CMakeLists.txt @@ -36,60 +36,11 @@ endif() set(UI_CPP "${CMAKE_CURRENT_BINARY_DIR}/ui.cpp") set(UI_H "${CMAKE_CURRENT_BINARY_DIR}/ui.h") -if(CMAKE_CROSSCOMPILING) - find_program(HOST_CXX_COMPILER NAMES g++ clang++ NO_CMAKE_FIND_ROOT_PATH) - if(NOT HOST_CXX_COMPILER) - message(FATAL_ERROR "UI: no host C++ compiler (g++/clang++) found to build llama-ui-embed; set -DHOST_CXX_COMPILER=") - endif() - 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 "$") - - # 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. +# Provision assets and generate ui.cpp/ui.h natively in CMake at build time. +# The generated sources are compiled by the regular target toolchain; no +# build-time host executable is needed (works in any cross-compile setup). +# The script uses copy_if_different semantics, so the library below only +# recompiles when the generated contents actually change. add_custom_target(llama-ui-assets ALL BYPRODUCTS ${UI_CPP} ${UI_H} COMMAND ${CMAKE_COMMAND} @@ -101,15 +52,12 @@ add_custom_target(llama-ui-assets ALL "-DHF_VERSION=${HF_UI_VERSION}" "-DHF_ENABLED=${LLAMA_USE_PREBUILT_UI}" "-DBUILD_UI=${LLAMA_BUILD_UI}" - "-DLLAMA_UI_EMBED=${LLAMA_UI_EMBED_EXE}" "-DLLAMA_UI_GZIP=${LLAMA_UI_GZIP}" -P "${PROJECT_SOURCE_DIR}/scripts/ui-assets.cmake" COMMENT "Provisioning UI assets" VERBATIM ) -add_dependencies(llama-ui-assets llama-ui-embed) - set_source_files_properties(${UI_CPP} ${UI_H} PROPERTIES GENERATED TRUE) add_library(${TARGET} STATIC ${UI_CPP} ${UI_H}) diff --git a/tools/ui/embed.cpp b/tools/ui/embed.cpp deleted file mode 100644 index b76c9047f2..0000000000 --- a/tools/ui/embed.cpp +++ /dev/null @@ -1,308 +0,0 @@ -// llama-ui-embed: generate ui.cpp / ui.h that embed UI assets as C arrays. -// -// Usage: -// llama-ui-embed [] -// -// Recursively embeds every regular file under . -// Asset names are relative paths from (e.g. "_app/immutable/bundle.HASH.js"). -// Without , emits an empty asset table. - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - - -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 & 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(sz)); - if (sz > 0 && !f.read(reinterpret_cast(out.data()), sz)) { - return false; - } - return true; -} - -static void append_bytes_hex(std::string & out, const std::vector & 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(sz) == content.size()) { - std::string existing(static_cast(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(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(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 []\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 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(assets.size()); - - if (n_assets > 0) { - using match_fn = std::function; - 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 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 \n#include \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_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 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 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_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_get_assets() {\n" - " static const std::array 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; -} diff --git a/tools/ui/ui.cpp.in b/tools/ui/ui.cpp.in new file mode 100644 index 0000000000..7f91ef2a22 --- /dev/null +++ b/tools/ui/ui.cpp.in @@ -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 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_get_assets() { +#if defined(LLAMA_UI_HAS_ASSETS) + return g_assets; +#else + static const std::array empty{}; + return empty; +#endif +} + +bool llama_ui_use_gzip() { + return @USE_GZIP@; +} diff --git a/tools/ui/ui.h.in b/tools/ui/ui.h.in new file mode 100644 index 0000000000..4555b0dd52 --- /dev/null +++ b/tools/ui/ui.h.in @@ -0,0 +1,21 @@ +// Generated by scripts/ui-assets.cmake - do not edit. + +#pragma once + +#include +#include + +// 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_get_assets();