mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-06-27 23:50:20 -05:00
ui: keep original file name and path (#24568)
* ui: keep original file name and path * fix nocache
This commit is contained in:
parent
57fe1f07c3
commit
597b6672e8
@ -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. "
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
}
|
||||
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");
|
||||
for (const auto & a : assets) {
|
||||
fprintf(stderr, " %s\n", a.name.c_str());
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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');
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user