Skip to content

Commit 597b667

Browse files
authored
ui: keep original file name and path (ggml-org#24568)
* ui: keep original file name and path * fix nocache
1 parent 57fe1f0 commit 597b667

5 files changed

Lines changed: 90 additions & 160 deletions

File tree

scripts/ui-assets.cmake

Lines changed: 10 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,45 +17,16 @@ set(HF_ENABLED "" CACHE STRING "Whether to allow HF Bucket download (ON/O
1717
set(BUILD_UI "" CACHE STRING "Build UI via npm (ON/OFF)")
1818
set(LLAMA_UI_EMBED "" CACHE STRING "Path to llama-ui-embed helper")
1919

20-
# IMPORTANT: When adding PWA assets, sync:
21-
# - tools/ui/src/lib/constants/pwa.ts (APPLE_DEVICES, PWA_MANIFEST)
22-
#
23-
# The HTTP server registers routes and public endpoints for every embedded asset.
24-
set(REQUIRED_ASSETS
25-
index.html
26-
loading.html
27-
manifest.webmanifest
28-
sw.js
29-
build.json
30-
# post-build.js flattens and dehashes these to fixed names in the dist dir
31-
bundle.js
32-
bundle.css
33-
workbox.js
34-
version.json
35-
)
36-
3720
set(DIST_DIR "${UI_BINARY_DIR}/dist")
3821
set(SRC_DIST_DIR "${UI_SOURCE_DIR}/dist")
3922
set(STAMP_FILE "${UI_BINARY_DIR}/.ui-stamp")
4023
set(UI_CPP "${UI_BINARY_DIR}/ui.cpp")
4124
set(UI_H "${UI_BINARY_DIR}/ui.h")
4225

43-
function(assets_present dir out_var)
44-
set(present TRUE)
45-
foreach(asset ${REQUIRED_ASSETS})
46-
if(NOT EXISTS "${dir}/${asset}")
47-
set(present FALSE)
48-
break()
49-
endif()
50-
endforeach()
51-
set(${out_var} ${present} PARENT_SCOPE)
52-
endfunction()
53-
5426
function(npm_build_should_skip out_var)
5527
set(${out_var} FALSE PARENT_SCOPE)
5628

57-
assets_present("${DIST_DIR}" present)
58-
if(NOT present)
29+
if(NOT EXISTS "${DIST_DIR}/index.html")
5930
return()
6031
endif()
6132

@@ -162,8 +133,7 @@ function(npm_build out_var)
162133
return()
163134
endif()
164135

165-
assets_present("${DIST_DIR}" present)
166-
if(NOT present)
136+
if(NOT EXISTS "${DIST_DIR}/index.html")
167137
message(STATUS "UI: npm build finished but assets missing in ${DIST_DIR}")
168138
return()
169139
endif()
@@ -242,8 +212,7 @@ function(hf_download version out_var out_resolved)
242212

243213
file(ARCHIVE_EXTRACT INPUT "${archive}" DESTINATION "${DIST_DIR}")
244214

245-
assets_present("${DIST_DIR}" present)
246-
if(NOT present)
215+
if(NOT EXISTS "${DIST_DIR}/index.html")
247216
message(STATUS "UI: archive from ${resolved} is missing required assets")
248217
continue()
249218
endif()
@@ -256,11 +225,8 @@ function(hf_download version out_var out_resolved)
256225
endfunction()
257226

258227
function(emit_files dist_dir)
259-
assets_present("${dist_dir}" present)
260-
261228
set(args "${UI_CPP}" "${UI_H}")
262-
if(present)
263-
# llama-ui-embed embeds every top-level file in dist_dir
229+
if(EXISTS "${dist_dir}/index.html")
264230
list(APPEND args "${dist_dir}")
265231
endif()
266232

@@ -276,8 +242,7 @@ endfunction()
276242
# ---------------------------------------------------------------------------
277243
# 1. Priority 1: pre-built assets supplied in tools/ui/dist
278244
# ---------------------------------------------------------------------------
279-
assets_present("${SRC_DIST_DIR}" SRC_OK)
280-
if(SRC_OK)
245+
if(EXISTS "${SRC_DIST_DIR}/index.html")
281246
message(STATUS "UI: using pre-built assets from ${SRC_DIST_DIR}")
282247
emit_files("${SRC_DIST_DIR}")
283248
return()
@@ -312,7 +277,10 @@ if(NOT provisioned AND HF_ENABLED)
312277
endif()
313278
endif()
314279

315-
assets_present("${DIST_DIR}" have_assets)
280+
set(have_assets FALSE)
281+
if(EXISTS "${DIST_DIR}/index.html")
282+
set(have_assets TRUE)
283+
endif()
316284
if(stamp_ok AND have_assets)
317285
message(STATUS "UI: HF stamp '${stamped}' matches version, skipping HF fetch")
318286
set(provisioned TRUE)
@@ -332,8 +300,7 @@ endif()
332300
# 4. Fallback: warn about stale or missing assets, then emit whatever we have
333301
# ---------------------------------------------------------------------------
334302
if(NOT provisioned)
335-
assets_present("${DIST_DIR}" have_assets)
336-
if(have_assets)
303+
if(EXISTS "${DIST_DIR}/index.html")
337304
message(WARNING "UI: provisioning failed; embedding stale assets from ${DIST_DIR}")
338305
else()
339306
message(WARNING "UI: no assets available - building without an embedded UI. "

tools/server/server-http.cpp

Lines changed: 7 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -26,52 +26,6 @@ server_http_context::server_http_context()
2626

2727
server_http_context::~server_http_context() = default;
2828

29-
// transform path --> asset name ; rules:
30-
// delete "_app/" prefix
31-
// delete hash, for ex: bundle.HCjcCZFH.css --> bundle.css
32-
// workbox-12bd46aa.js --> workbox.js
33-
static std::string asset_name_from_path(const std::string & path) {
34-
// Strip leading slash
35-
std::string s = (!path.empty() && path[0] == '/') ? path.substr(1) : path;
36-
// Strip _app/ prefix
37-
if (s.size() > 5 && s.compare(0, 5, "_app/") == 0) {
38-
s = s.substr(5);
39-
}
40-
// Strip hash segment from filename:
41-
// bundle.HCjcCZFH.css -> bundle.css (name.HASH.ext)
42-
// workbox-12bd46aa.js -> workbox.js (name-HEXHASH.ext)
43-
size_t slash = s.rfind('/');
44-
std::string dir = (slash != std::string::npos) ? s.substr(0, slash + 1) : "";
45-
std::string file = (slash != std::string::npos) ? s.substr(slash + 1) : s;
46-
47-
auto is_alnum_hash = [](const std::string & h) {
48-
if (h.size() < 6 || h.size() > 16) return false;
49-
for (char c : h) { if (!isalnum((unsigned char)c)) return false; }
50-
return true;
51-
};
52-
auto is_hex_hash = [](const std::string & h) {
53-
if (h.size() < 6 || h.size() > 16) return false;
54-
for (char c : h) { if (!isxdigit((unsigned char)c)) return false; }
55-
return true;
56-
};
57-
58-
size_t dot1 = file.find('.');
59-
if (dot1 != std::string::npos) {
60-
size_t dot2 = file.find('.', dot1 + 1);
61-
if (dot2 != std::string::npos && is_alnum_hash(file.substr(dot1 + 1, dot2 - dot1 - 1))) {
62-
file = file.substr(0, dot1) + file.substr(dot2);
63-
} else {
64-
size_t dot = file.rfind('.');
65-
size_t dash = file.rfind('-', dot);
66-
if (dash != std::string::npos && is_hex_hash(file.substr(dash + 1, dot - dash - 1))) {
67-
file = file.substr(0, dash) + file.substr(dot);
68-
}
69-
}
70-
}
71-
72-
return dir + file;
73-
}
74-
7529
static void log_server_request(const httplib::Request & req, const httplib::Response & res) {
7630
// skip logging requests that are regularly sent, to avoid log spam
7731
if (req.path == "/health"
@@ -240,9 +194,8 @@ bool server_http_context::init(const common_params & params) {
240194
return true;
241195
}
242196

243-
// If path is public or a UI asset (including hashed paths like /_app/bundle.XXX.js),
244-
// skip validation
245-
if (get_public_endpoints.count("/" + asset_name_from_path(req.path))) {
197+
// If path is public or a UI asset, skip validation
198+
if (get_public_endpoints.count(req.path)) {
246199
return true;
247200
}
248201

@@ -399,33 +352,24 @@ bool server_http_context::init(const common_params & params) {
399352
};
400353
};
401354

402-
// Hashed routes: browser requests contain the build hash, assets are stored without.
403-
auto serve_hashed = [serve_asset_cached](const std::string & name) {
404-
return serve_asset_cached(name, false);
405-
};
406-
srv->Get(params.api_prefix + R"(/_app/immutable/bundle\.[^/]+\.js)", serve_hashed("bundle.js"));
407-
srv->Get(params.api_prefix + R"(/_app/immutable/assets/bundle\.[^/]+\.css)", serve_hashed("bundle.css"));
408-
srv->Get(params.api_prefix + R"(/workbox-[^/]+\.js)", serve_hashed("workbox.js"));
409-
410-
// SPA entry — also aliased at "/_app/version.json" (referenced by the service worker)
411-
srv->Get(params.api_prefix + "/", serve_asset_cached ("index.html", true));
412-
srv->Get(params.api_prefix + "/_app/version.json", serve_asset_nocache("version.json"));
355+
// main index file
356+
srv->Get(params.api_prefix + "/", serve_asset_cached("index.html", true));
357+
srv->Get(params.api_prefix + "/index.html", serve_asset_cached("index.html", true));
413358

414359
// All remaining assets registered directly from the embedded asset table.
415360
// PWA revalidation files (sw.js, manifest, version.json) use no-cache;
416361
// everything else is immutable.
417362
static const std::unordered_set<std::string> no_cache_names = {
418363
"sw.js",
419364
"manifest.webmanifest",
420-
"version.json",
365+
"_app/version.json",
421366
"build.json"
422367
};
423-
// index.html also accessible at /index.html (with the same isolation headers as /)
424-
srv->Get(params.api_prefix + "/index.html", serve_asset_cached("index.html", true));
425368

426369
for (const auto & a : llama_ui_get_assets()) {
427370
if (a.name == "index.html") continue; // served at "/" and "/index.html" above
428371
if (no_cache_names.count(a.name)) {
372+
SRV_DBG("serve nocache for %s\n", a.name.c_str());
429373
srv->Get(params.api_prefix + "/" + a.name, serve_asset_nocache(a.name));
430374
} else {
431375
srv->Get(params.api_prefix + "/" + a.name, serve_asset_cached(a.name, false));

tools/ui/embed.cpp

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
// Usage:
44
// llama-ui-embed <out_cpp> <out_h> [<asset_dir>]
55
//
6-
// Embeds every regular file directly under <asset_dir> (non-recursive).
6+
// Recursively embeds every regular file under <asset_dir>.
7+
// Asset names are relative paths from <asset_dir> (e.g. "_app/immutable/bundle.HASH.js").
78
// Without <asset_dir>, emits an empty asset table.
89

910
#include <inttypes.h>
@@ -15,6 +16,7 @@
1516
#include <algorithm>
1617
#include <filesystem>
1718
#include <fstream>
19+
#include <functional>
1820
#include <string>
1921
#include <vector>
2022

@@ -103,7 +105,24 @@ static bool write_if_different(const std::string & path, const std::string & con
103105
if (!content.empty()) {
104106
out.write(content.data(), static_cast<std::streamsize>(content.size()));
105107
}
106-
return out.good();
108+
bool ok = out.good();
109+
if (ok) {
110+
printf("embed: write output file %s\n", path.c_str());
111+
}
112+
return ok;
113+
}
114+
115+
static std::string path_basename(const std::string & name) {
116+
const size_t p = name.rfind('/');
117+
return p == std::string::npos ? name : name.substr(p + 1);
118+
}
119+
static bool str_starts_with(const std::string & s, const char * prefix) {
120+
const size_t n = strlen(prefix);
121+
return s.size() >= n && s.compare(0, n, prefix) == 0;
122+
}
123+
static bool str_ends_with(const std::string & s, const char * suffix) {
124+
const size_t n = strlen(suffix);
125+
return s.size() >= n && s.compare(s.size() - n, n, suffix) == 0;
107126
}
108127

109128
static std::string fmt(const char * pattern, ...) {
@@ -128,13 +147,14 @@ int main(int argc, char ** argv) {
128147

129148
const std::string out_cpp = argv[1];
130149
const std::string out_h = argv[2];
150+
const std::string in_dir = argv[3];
131151

132152
std::vector<asset_entry> assets;
133153
if (argc == 4) {
134-
const std::filesystem::path dir = argv[3];
154+
const std::filesystem::path dir = in_dir;
135155

136156
std::error_code ec;
137-
std::filesystem::directory_iterator it(dir, ec);
157+
std::filesystem::recursive_directory_iterator it(dir, ec);
138158
if (ec) {
139159
fprintf(stderr, "embed: cannot iterate %s: %s\n", argv[3], ec.message().c_str());
140160
return 1;
@@ -143,7 +163,9 @@ int main(int argc, char ** argv) {
143163
if (!entry.is_regular_file()) {
144164
continue;
145165
}
146-
assets.push_back({ entry.path().filename().generic_string(), entry.path() });
166+
// name is the relative path from dir, with forward slashes
167+
const std::string name = entry.path().lexically_relative(dir).generic_string();
168+
assets.push_back({ name, entry.path() });
147169
}
148170

149171
// directory iteration order is unspecified; sort for reproducible output
@@ -154,18 +176,51 @@ int main(int argc, char ** argv) {
154176
const int n_assets = static_cast<int>(assets.size());
155177

156178
if (n_assets > 0) {
157-
bool has_index = false, has_bundle_js = false, has_bundle_css = false, has_version = false;
179+
using match_fn = std::function<bool(const std::string &)>;
180+
auto exact = [](const char * name) -> match_fn {
181+
return [name](const std::string & base) { return base == name; };
182+
};
183+
184+
struct required_check { const char * label; match_fn match; bool found; };
185+
required_check checks[] = {
186+
{ "index.html", exact("index.html"), false },
187+
{ "loading.html", exact("loading.html"), false },
188+
{ "manifest.webmanifest", exact("manifest.webmanifest"), false },
189+
{ "sw.js", exact("sw.js"), false },
190+
{ "build.json", exact("build.json"), false },
191+
{ "version.json", exact("version.json"), false },
192+
{ "bundle[hash].js", [](const std::string & b) {
193+
return str_starts_with(b, "bundle") && str_ends_with(b, ".js");
194+
}, false },
195+
{ "bundle[hash].css", [](const std::string & b) {
196+
return str_starts_with(b, "bundle") && str_ends_with(b, ".css");
197+
}, false },
198+
{ "workbox[hash].js", [](const std::string & b) {
199+
return str_starts_with(b, "workbox") && str_ends_with(b, ".js");
200+
}, false },
201+
};
202+
158203
for (const auto & a : assets) {
159-
if (a.name == "index.html") has_index = true;
160-
if (a.name == "bundle.js") has_bundle_js = true;
161-
if (a.name == "bundle.css") has_bundle_css = true;
162-
if (a.name == "version.json") has_version = true;
204+
const std::string base = path_basename(a.name);
205+
for (auto & c : checks) {
206+
if (!c.found) { c.found = c.match(base); }
207+
}
208+
}
209+
210+
std::vector<const char *> missing;
211+
for (const auto & c : checks) {
212+
if (!c.found) { missing.push_back(c.label); }
163213
}
164-
if (!has_index || !has_bundle_js || !has_bundle_css || !has_version) {
165-
fprintf(stderr, "embed: missing required assets (need index.html, bundle.js, bundle.css, version.json); got:\n");
214+
if (!missing.empty()) {
215+
fprintf(stderr, "\ncurrent asset files:\n");
166216
for (const auto & a : assets) {
167-
fprintf(stderr, " %s\n", a.name.c_str());
217+
fprintf(stderr, " %s\n", a.name.c_str());
168218
}
219+
fprintf(stderr, "missing required asset(s):\n");
220+
for (const char * m : missing) {
221+
fprintf(stderr, " %s\n", m);
222+
}
223+
fprintf(stderr, "hint: try cleaning your build directory: %s\n", in_dir.c_str());
169224
return 1;
170225
}
171226
}
@@ -195,6 +250,10 @@ int main(int argc, char ** argv) {
195250
if (!read_file(assets[i].path, bytes)) {
196251
return 1;
197252
}
253+
if (bytes.empty()) {
254+
fprintf(stderr, "embed: empty file: %s\n", assets[i].path.generic_string().c_str());
255+
return 1;
256+
}
198257
cpp += fmt("static const unsigned char asset_%d_data[] = {", i);
199258
append_bytes_hex(cpp, bytes);
200259
const auto hash = fnv_hash(bytes.data(), bytes.size());

tools/ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"version": "1.0.0",
55
"type": "module",
66
"scripts": {
7-
"build": "npm run build-pwa-assets && vite build && node scripts/post-build.js",
7+
"build": "npm run build-pwa-assets && vite build",
88
"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",
99
"dev": "bash scripts/dev.sh",
1010
"preview": "vite preview",

0 commit comments

Comments
 (0)