]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
ui: keep original file name and path (#24568)
authorXuan-Son Nguyen <redacted>
Sat, 13 Jun 2026 12:31:41 +0000 (14:31 +0200)
committerGitHub <redacted>
Sat, 13 Jun 2026 12:31:41 +0000 (14:31 +0200)
* ui: keep original file name and path

* fix nocache

scripts/ui-assets.cmake
tools/server/server-http.cpp
tools/ui/embed.cpp
tools/ui/package.json
tools/ui/scripts/post-build.js [deleted file]

index e5b47863ecf596cf082a022c650cf56c3be553c2..89f22b14ac4e2127062a0f21b6db2912c74fe940 100644 (file)
@@ -17,45 +17,16 @@ set(HF_ENABLED        "" CACHE STRING "Whether to allow HF Bucket download (ON/O
 set(BUILD_UI          "" CACHE STRING "Build UI via npm (ON/OFF)")
 set(LLAMA_UI_EMBED    "" CACHE STRING "Path to llama-ui-embed helper")
 
-# IMPORTANT: When adding PWA assets, sync:
-#   - tools/ui/src/lib/constants/pwa.ts   (APPLE_DEVICES, PWA_MANIFEST)
-#
-# The HTTP server registers routes and public endpoints for every embedded asset.
-set(REQUIRED_ASSETS
-    index.html
-    loading.html
-    manifest.webmanifest
-    sw.js
-    build.json
-    # post-build.js flattens and dehashes these to fixed names in the dist dir
-    bundle.js
-    bundle.css
-    workbox.js
-    version.json
-)
-
 set(DIST_DIR     "${UI_BINARY_DIR}/dist")
 set(SRC_DIST_DIR "${UI_SOURCE_DIR}/dist")
 set(STAMP_FILE   "${UI_BINARY_DIR}/.ui-stamp")
 set(UI_CPP       "${UI_BINARY_DIR}/ui.cpp")
 set(UI_H         "${UI_BINARY_DIR}/ui.h")
 
-function(assets_present dir out_var)
-    set(present TRUE)
-    foreach(asset ${REQUIRED_ASSETS})
-        if(NOT EXISTS "${dir}/${asset}")
-            set(present FALSE)
-            break()
-        endif()
-    endforeach()
-    set(${out_var} ${present} PARENT_SCOPE)
-endfunction()
-
 function(npm_build_should_skip out_var)
     set(${out_var} FALSE PARENT_SCOPE)
 
-    assets_present("${DIST_DIR}" present)
-    if(NOT present)
+    if(NOT EXISTS "${DIST_DIR}/index.html")
         return()
     endif()
 
@@ -162,8 +133,7 @@ function(npm_build out_var)
         return()
     endif()
 
-    assets_present("${DIST_DIR}" present)
-    if(NOT present)
+    if(NOT EXISTS "${DIST_DIR}/index.html")
         message(STATUS "UI: npm build finished but assets missing in ${DIST_DIR}")
         return()
     endif()
@@ -242,8 +212,7 @@ function(hf_download version out_var out_resolved)
 
         file(ARCHIVE_EXTRACT INPUT "${archive}" DESTINATION "${DIST_DIR}")
 
-        assets_present("${DIST_DIR}" present)
-        if(NOT present)
+        if(NOT EXISTS "${DIST_DIR}/index.html")
             message(STATUS "UI: archive from ${resolved} is missing required assets")
             continue()
         endif()
@@ -256,11 +225,8 @@ function(hf_download version out_var out_resolved)
 endfunction()
 
 function(emit_files dist_dir)
-    assets_present("${dist_dir}" present)
-
     set(args "${UI_CPP}" "${UI_H}")
-    if(present)
-        # llama-ui-embed embeds every top-level file in dist_dir
+    if(EXISTS "${dist_dir}/index.html")
         list(APPEND args "${dist_dir}")
     endif()
 
@@ -276,8 +242,7 @@ endfunction()
 # ---------------------------------------------------------------------------
 # 1. Priority 1: pre-built assets supplied in tools/ui/dist
 # ---------------------------------------------------------------------------
-assets_present("${SRC_DIST_DIR}" SRC_OK)
-if(SRC_OK)
+if(EXISTS "${SRC_DIST_DIR}/index.html")
     message(STATUS "UI: using pre-built assets from ${SRC_DIST_DIR}")
     emit_files("${SRC_DIST_DIR}")
     return()
@@ -312,7 +277,10 @@ if(NOT provisioned AND HF_ENABLED)
         endif()
     endif()
 
-    assets_present("${DIST_DIR}" have_assets)
+    set(have_assets FALSE)
+    if(EXISTS "${DIST_DIR}/index.html")
+        set(have_assets TRUE)
+    endif()
     if(stamp_ok AND have_assets)
         message(STATUS "UI: HF stamp '${stamped}' matches version, skipping HF fetch")
         set(provisioned TRUE)
@@ -332,8 +300,7 @@ endif()
 # 4. Fallback: warn about stale or missing assets, then emit whatever we have
 # ---------------------------------------------------------------------------
 if(NOT provisioned)
-    assets_present("${DIST_DIR}" have_assets)
-    if(have_assets)
+    if(EXISTS "${DIST_DIR}/index.html")
         message(WARNING "UI: provisioning failed; embedding stale assets from ${DIST_DIR}")
     else()
         message(WARNING "UI: no assets available - building without an embedded UI. "
index bb63cc5985766e93ddd3f6dc6c8cb1b0a280889c..e79f0cbab837ef2a35b018d599ab199f5601cf78 100644 (file)
@@ -26,52 +26,6 @@ server_http_context::server_http_context()
 
 server_http_context::~server_http_context() = default;
 
-// transform path --> asset name ; rules:
-// delete "_app/" prefix
-// delete hash, for ex: bundle.HCjcCZFH.css --> bundle.css
-//                      workbox-12bd46aa.js --> workbox.js
-static std::string asset_name_from_path(const std::string & path) {
-    // Strip leading slash
-    std::string s = (!path.empty() && path[0] == '/') ? path.substr(1) : path;
-    // Strip _app/ prefix
-    if (s.size() > 5 && s.compare(0, 5, "_app/") == 0) {
-        s = s.substr(5);
-    }
-    // Strip hash segment from filename:
-    //   bundle.HCjcCZFH.css -> bundle.css  (name.HASH.ext)
-    //   workbox-12bd46aa.js -> workbox.js  (name-HEXHASH.ext)
-    size_t slash = s.rfind('/');
-    std::string dir  = (slash != std::string::npos) ? s.substr(0, slash + 1) : "";
-    std::string file = (slash != std::string::npos) ? s.substr(slash + 1)    : s;
-
-    auto is_alnum_hash = [](const std::string & h) {
-        if (h.size() < 6 || h.size() > 16) return false;
-        for (char c : h) { if (!isalnum((unsigned char)c)) return false; }
-        return true;
-    };
-    auto is_hex_hash = [](const std::string & h) {
-        if (h.size() < 6 || h.size() > 16) return false;
-        for (char c : h) { if (!isxdigit((unsigned char)c)) return false; }
-        return true;
-    };
-
-    size_t dot1 = file.find('.');
-    if (dot1 != std::string::npos) {
-        size_t dot2 = file.find('.', dot1 + 1);
-        if (dot2 != std::string::npos && is_alnum_hash(file.substr(dot1 + 1, dot2 - dot1 - 1))) {
-            file = file.substr(0, dot1) + file.substr(dot2);
-        } else {
-            size_t dot = file.rfind('.');
-            size_t dash = file.rfind('-', dot);
-            if (dash != std::string::npos && is_hex_hash(file.substr(dash + 1, dot - dash - 1))) {
-                file = file.substr(0, dash) + file.substr(dot);
-            }
-        }
-    }
-
-    return dir + file;
-}
-
 static void log_server_request(const httplib::Request & req, const httplib::Response & res) {
     // skip logging requests that are regularly sent, to avoid log spam
     if (req.path == "/health"
@@ -240,9 +194,8 @@ bool server_http_context::init(const common_params & params) {
             return true;
         }
 
-        // If path is public or a UI asset (including hashed paths like /_app/bundle.XXX.js),
-        // skip validation
-        if (get_public_endpoints.count("/" + asset_name_from_path(req.path))) {
+        // If path is public or a UI asset, skip validation
+        if (get_public_endpoints.count(req.path)) {
             return true;
         }
 
@@ -399,17 +352,9 @@ bool server_http_context::init(const common_params & params) {
                 };
             };
 
-            // Hashed routes: browser requests contain the build hash, assets are stored without.
-            auto serve_hashed = [serve_asset_cached](const std::string & name) {
-                return serve_asset_cached(name, false);
-            };
-            srv->Get(params.api_prefix + R"(/_app/immutable/bundle\.[^/]+\.js)",         serve_hashed("bundle.js"));
-            srv->Get(params.api_prefix + R"(/_app/immutable/assets/bundle\.[^/]+\.css)", serve_hashed("bundle.css"));
-            srv->Get(params.api_prefix + R"(/workbox-[^/]+\.js)",                        serve_hashed("workbox.js"));
-
-            // SPA entry — also aliased at "/_app/version.json" (referenced by the service worker)
-            srv->Get(params.api_prefix + "/",                  serve_asset_cached ("index.html",   true));
-            srv->Get(params.api_prefix + "/_app/version.json", serve_asset_nocache("version.json"));
+            // main index file
+            srv->Get(params.api_prefix + "/",           serve_asset_cached("index.html", true));
+            srv->Get(params.api_prefix + "/index.html", serve_asset_cached("index.html", true));
 
             // All remaining assets registered directly from the embedded asset table.
             // PWA revalidation files (sw.js, manifest, version.json) use no-cache;
@@ -417,15 +362,14 @@ bool server_http_context::init(const common_params & params) {
             static const std::unordered_set<std::string> no_cache_names = {
                 "sw.js",
                 "manifest.webmanifest",
-                "version.json",
+                "_app/version.json",
                 "build.json"
             };
-            // index.html also accessible at /index.html (with the same isolation headers as /)
-            srv->Get(params.api_prefix + "/index.html", serve_asset_cached("index.html", true));
 
             for (const auto & a : llama_ui_get_assets()) {
                 if (a.name == "index.html") continue;  // served at "/" and "/index.html" above
                 if (no_cache_names.count(a.name)) {
+                    SRV_DBG("serve nocache for %s\n", a.name.c_str());
                     srv->Get(params.api_prefix + "/" + a.name, serve_asset_nocache(a.name));
                 } else {
                     srv->Get(params.api_prefix + "/" + a.name, serve_asset_cached(a.name, false));
index 24617e05fc84ad4384695e8439375f59fc2ad4ad..2ca022208f847a15b91d6398621ebb4921e736ba 100644 (file)
@@ -3,7 +3,8 @@
 // Usage:
 //   llama-ui-embed <out_cpp> <out_h> [<asset_dir>]
 //
-// Embeds every regular file directly under <asset_dir> (non-recursive).
+// 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>
@@ -15,6 +16,7 @@
 #include <algorithm>
 #include <filesystem>
 #include <fstream>
+#include <functional>
 #include <string>
 #include <vector>
 
@@ -103,7 +105,24 @@ static bool write_if_different(const std::string & path, const std::string & con
     if (!content.empty()) {
         out.write(content.data(), static_cast<std::streamsize>(content.size()));
     }
-    return out.good();
+    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, ...) {
@@ -128,13 +147,14 @@ int main(int argc, char ** argv) {
 
     const std::string out_cpp = argv[1];
     const std::string out_h   = argv[2];
+    const std::string in_dir  = argv[3];
 
     std::vector<asset_entry> assets;
     if (argc == 4) {
-        const std::filesystem::path dir = argv[3];
+        const std::filesystem::path dir = in_dir;
 
         std::error_code ec;
-        std::filesystem::directory_iterator it(dir, 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;
@@ -143,7 +163,9 @@ int main(int argc, char ** argv) {
             if (!entry.is_regular_file()) {
                 continue;
             }
-            assets.push_back({ entry.path().filename().generic_string(), entry.path() });
+            // 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
@@ -154,18 +176,51 @@ int main(int argc, char ** argv) {
     const int n_assets = static_cast<int>(assets.size());
 
     if (n_assets > 0) {
-        bool has_index = false, has_bundle_js = false, has_bundle_css = false, has_version = false;
+        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 },
+            { "loading.html",         exact("loading.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) {
-            if (a.name == "index.html")   has_index      = true;
-            if (a.name == "bundle.js")    has_bundle_js  = true;
-            if (a.name == "bundle.css")   has_bundle_css = true;
-            if (a.name == "version.json") has_version    = true;
+            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 (!has_index || !has_bundle_js || !has_bundle_css || !has_version) {
-            fprintf(stderr, "embed: missing required assets (need index.html, bundle.js, bundle.css, version.json); got:\n");
+        if (!missing.empty()) {
+            fprintf(stderr, "\ncurrent asset files:\n");
             for (const auto & a : assets) {
-                fprintf(stderr, "  %s\n", a.name.c_str());
+                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;
         }
     }
@@ -195,6 +250,10 @@ int main(int argc, char ** argv) {
             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);
             const auto hash = fnv_hash(bytes.data(), bytes.size());
index 15dca7888692ee639050c7680e66c28c2897ffcf..6c750431fe6c8965250b3f9ccc8951384bbbe598 100644 (file)
@@ -4,7 +4,7 @@
        "version": "1.0.0",
        "type": "module",
        "scripts": {
-               "build": "npm run build-pwa-assets && vite build && node scripts/post-build.js",
+               "build": "npm run build-pwa-assets && vite build",
                "build-pwa-assets": "npx @vite-pwa/assets-generator --root . --config pwa-assets.config.ts && npx @vite-pwa/assets-generator --root . --config pwa-assets-dark.config.ts && node scripts/make-icons-circular.js",
                "dev": "bash scripts/dev.sh",
                "preview": "vite preview",
diff --git a/tools/ui/scripts/post-build.js b/tools/ui/scripts/post-build.js
deleted file mode 100644 (file)
index f2e72ce..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/usr/bin/env node
-// Post-build: copy hashed/nested assets to predictable flat names.
-// No file content is modified — the C++ server handles routing hashed URLs
-// to the correct stored asset at runtime.
-//
-// Copies:
-//   _app/immutable/bundle.HASH.js         -> bundle.js
-//   _app/immutable/assets/bundle.HASH.css -> bundle.css
-//   workbox-HEXHASH.js                    -> workbox.js
-//   _app/version.json                     -> version.json
-
-import fs from 'fs';
-import path from 'path';
-
-const outDir = process.env.LLAMA_UI_OUT_DIR ?? './dist';
-
-function findOne(dir, pattern) {
-       const files = fs.readdirSync(dir).filter((f) => pattern.test(f));
-       if (files.length === 0) throw new Error(`post-build: no file matching ${pattern} in ${dir}`);
-       return path.join(dir, files[0]);
-}
-
-function copyFlat(src, destName) {
-       const dest = path.join(outDir, destName);
-       fs.copyFileSync(src, dest);
-       console.log(`post-build: ${path.relative(outDir, src)} -> ${destName}`);
-}
-
-const bundleJs = findOne(path.join(outDir, '_app/immutable'), /^bundle\.[^.]+\.js$/);
-const bundleCss = findOne(path.join(outDir, '_app/immutable/assets'), /^bundle\.[^.]+\.css$/);
-const workbox = findOne(outDir, /^workbox-[0-9a-f]+\.js$/);
-
-copyFlat(bundleJs, 'bundle.js');
-copyFlat(bundleCss, 'bundle.css');
-copyFlat(workbox, 'workbox.js');
-
-const versionSrc = path.join(outDir, '_app/version.json');
-if (fs.existsSync(versionSrc)) {
-       copyFlat(versionSrc, 'version.json');
-}