Skip to content

Commit 57fe1f0

Browse files
ngxsonaldehir
andauthored
server: clean up static assets handling (ggml-org#24550)
* server: clean up static assets handling * nits * simplify file name handling, use static file name everywhere * cmake/ui : bundle UI assets in an archive * ui : run prettier on post-build.js --------- Co-authored-by: Alde Rojas <hello@alde.dev>
1 parent d8a24cc commit 57fe1f0

10 files changed

Lines changed: 317 additions & 421 deletions

File tree

.github/workflows/ui-build-self-hosted.yml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,6 @@ jobs:
2828
run: npm run build
2929
working-directory: tools/ui
3030

31-
- name: Generate checksums
32-
run: |
33-
cd tools/ui/dist
34-
for f in *; do
35-
sha256sum "$f" | awk '{print $1, $2}' >> checksums.txt
36-
done
37-
3831
- name: Upload built UI
3932
uses: actions/upload-artifact@v6
4033
with:

.github/workflows/ui-build.yml

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,14 @@ jobs:
3232
- name: Build application
3333
env:
3434
HF_UI_VERSION: ${{ inputs.hf_ui_version || '' }}
35-
LLAMA_UI_VERSION: ${{ inputs.hf_ui_version || 'b0000' }}
35+
LLAMA_BUILD_NUMBER: ${{ inputs.hf_ui_version || 'b0000' }}
3636
run: npm run build
3737
working-directory: tools/ui
3838

3939
- name: Run PWA unit tests (versioned build output)
4040
run: npx vitest --project=unit --run tests/unit/pwa.spec.ts
4141
working-directory: tools/ui
4242

43-
- name: Generate checksums
44-
run: |
45-
cd tools/ui/dist
46-
for f in *; do
47-
sha256sum "$f" | awk '{print $1, $2}' >> checksums.txt
48-
done
49-
5043
- name: Upload built UI
5144
uses: actions/upload-artifact@v6
5245
with:

.github/workflows/ui-publish.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ jobs:
4040
name: ui-build
4141
path: tools/ui/dist/
4242

43+
- name: Create distribution archive
44+
run: |
45+
tar -czf dist.tar.gz -C tools/ui/dist .
46+
sha256sum dist.tar.gz > dist.tar.gz.sha256
47+
mv dist.tar.gz dist.tar.gz.sha256 tools/ui/dist/
48+
4349
- name: Install Hugging Face Hub CLI
4450
run: pip install -U huggingface_hub
4551

scripts/ui-assets.cmake

Lines changed: 69 additions & 176 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
# 1. Pre-built assets in SRC_DIST_DIR (manually built by user)
55
# 2. If BUILD_UI=ON: npm build
66
# 3. If above did not produce assets and HF_ENABLED=ON: HF Bucket download
7+
# of dist.tar.gz (verified against dist.tar.gz.sha256)
78

8-
cmake_minimum_required(VERSION 3.16)
9+
cmake_minimum_required(VERSION 3.18)
910

1011
set(UI_SOURCE_DIR "" CACHE STRING "UI source directory (to run npm build)")
1112
set(UI_BINARY_DIR "" CACHE STRING "UI binary directory (to store generated files)")
@@ -16,80 +17,21 @@ set(HF_ENABLED "" CACHE STRING "Whether to allow HF Bucket download (ON/O
1617
set(BUILD_UI "" CACHE STRING "Build UI via npm (ON/OFF)")
1718
set(LLAMA_UI_EMBED "" CACHE STRING "Path to llama-ui-embed helper")
1819

19-
# IMPORTANT: When adding PWA assets, sync across all 3 places:
20-
# 1. tools/ui/src/lib/constants/pwa.ts (APPLE_DEVICES, PUBLIC_ENDPOINTS)
21-
# 2. tools/server/server-http.cpp (public_endpoints)
22-
# 3. scripts/ui-assets.cmake (ASSETS list)
23-
# - C++ (server-http.cpp) - public endpoints (splash screens generated via helper)
24-
# - TypeScript (constants/pwa.ts) - APPLE_DEVICES, PWA_MANIFEST, PUBLIC_ENDPOINTS
20+
# IMPORTANT: When adding PWA assets, sync:
21+
# - tools/ui/src/lib/constants/pwa.ts (APPLE_DEVICES, PWA_MANIFEST)
2522
#
26-
# When adding/changing PWA assets, update tools/ui/src/lib/constants/pwa.ts first,
27-
# then sync any new file names here and in server-http.cpp.
28-
set(ASSETS
23+
# The HTTP server registers routes and public endpoints for every embedded asset.
24+
set(REQUIRED_ASSETS
2925
index.html
3026
loading.html
31-
# PWA assets
32-
favicon.ico
33-
favicon-dark.ico
34-
favicon.svg
35-
favicon-dark.svg
36-
pwa-64x64.png
37-
pwa-192x192.png
38-
pwa-512x512.png
39-
maskable-icon-512x512.png
40-
apple-touch-icon-180x180.png
41-
# iOS splash screens
42-
apple-splash-portrait-640x1136.png
43-
apple-splash-landscape-1136x640.png
44-
apple-splash-portrait-750x1334.png
45-
apple-splash-landscape-1334x750.png
46-
apple-splash-portrait-1170x2532.png
47-
apple-splash-landscape-2532x1170.png
48-
apple-splash-portrait-1179x2556.png
49-
apple-splash-landscape-2556x1179.png
50-
apple-splash-portrait-1206x2622.png
51-
apple-splash-landscape-2622x1206.png
52-
apple-splash-portrait-1284x2778.png
53-
apple-splash-landscape-2778x1284.png
54-
apple-splash-portrait-1290x2796.png
55-
apple-splash-landscape-2796x1290.png
56-
apple-splash-portrait-1320x2868.png
57-
apple-splash-landscape-2868x1320.png
58-
apple-splash-portrait-1488x2266.png
59-
apple-splash-landscape-2266x1488.png
60-
apple-splash-portrait-1640x2360.png
61-
apple-splash-landscape-2360x1640.png
62-
apple-splash-portrait-1668x2388.png
63-
apple-splash-landscape-2388x1668.png
64-
apple-splash-portrait-2048x2732.png
65-
apple-splash-landscape-2732x2048.png
66-
# iOS dark splash screens
67-
apple-splash-portrait-dark-640x1136.png
68-
apple-splash-landscape-dark-1136x640.png
69-
apple-splash-portrait-dark-750x1334.png
70-
apple-splash-landscape-dark-1334x750.png
71-
apple-splash-portrait-dark-1170x2532.png
72-
apple-splash-landscape-dark-2532x1170.png
73-
apple-splash-portrait-dark-1179x2556.png
74-
apple-splash-landscape-dark-2556x1179.png
75-
apple-splash-portrait-dark-1206x2622.png
76-
apple-splash-landscape-dark-2622x1206.png
77-
apple-splash-portrait-dark-1284x2778.png
78-
apple-splash-landscape-dark-2778x1284.png
79-
apple-splash-portrait-dark-1290x2796.png
80-
apple-splash-landscape-dark-2796x1290.png
81-
apple-splash-portrait-dark-1320x2868.png
82-
apple-splash-landscape-dark-2868x1320.png
83-
apple-splash-portrait-dark-1640x2360.png
84-
apple-splash-landscape-dark-2360x1640.png
85-
apple-splash-portrait-dark-1668x2388.png
86-
apple-splash-landscape-dark-2388x1668.png
87-
apple-splash-portrait-dark-2048x2732.png
88-
apple-splash-landscape-dark-2732x2048.png
8927
manifest.webmanifest
9028
sw.js
91-
_app/version.json
9229
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
9335
)
9436

9537
set(DIST_DIR "${UI_BINARY_DIR}/dist")
@@ -98,41 +40,21 @@ set(STAMP_FILE "${UI_BINARY_DIR}/.ui-stamp")
9840
set(UI_CPP "${UI_BINARY_DIR}/ui.cpp")
9941
set(UI_H "${UI_BINARY_DIR}/ui.h")
10042

101-
function(assets_present out_var)
43+
function(assets_present dir out_var)
10244
set(present TRUE)
103-
foreach(asset ${ASSETS})
104-
if(NOT EXISTS "${DIST_DIR}/${asset}")
45+
foreach(asset ${REQUIRED_ASSETS})
46+
if(NOT EXISTS "${dir}/${asset}")
10547
set(present FALSE)
10648
break()
10749
endif()
10850
endforeach()
10951
set(${out_var} ${present} PARENT_SCOPE)
11052
endfunction()
11153

112-
function(copy_src_dist out_var)
113-
set(${out_var} FALSE PARENT_SCOPE)
114-
115-
foreach(asset ${ASSETS})
116-
if(NOT EXISTS "${SRC_DIST_DIR}/${asset}")
117-
return()
118-
endif()
119-
endforeach()
120-
121-
file(MAKE_DIRECTORY "${DIST_DIR}")
122-
message(STATUS "UI: using pre-built assets from ${SRC_DIST_DIR}")
123-
foreach(asset ${ASSETS})
124-
execute_process(
125-
COMMAND ${CMAKE_COMMAND} -E copy_if_different
126-
"${SRC_DIST_DIR}/${asset}" "${DIST_DIR}/${asset}"
127-
)
128-
endforeach()
129-
set(${out_var} TRUE PARENT_SCOPE)
130-
endfunction()
131-
13254
function(npm_build_should_skip out_var)
13355
set(${out_var} FALSE PARENT_SCOPE)
13456

135-
assets_present(present)
57+
assets_present("${DIST_DIR}" present)
13658
if(NOT present)
13759
return()
13860
endif()
@@ -240,7 +162,7 @@ function(npm_build out_var)
240162
return()
241163
endif()
242164

243-
assets_present(present)
165+
assets_present("${DIST_DIR}" present)
244166
if(NOT present)
245167
message(STATUS "UI: npm build finished but assets missing in ${DIST_DIR}")
246168
return()
@@ -272,7 +194,7 @@ function(hf_download version out_var out_resolved)
272194
set(${out_var} FALSE PARENT_SCOPE)
273195
set(${out_resolved} "" PARENT_SCOPE)
274196

275-
file(MAKE_DIRECTORY "${DIST_DIR}")
197+
set(archive "${UI_BINARY_DIR}/dist.tar.gz")
276198

277199
set(candidates "")
278200
if(NOT "${version}" STREQUAL "")
@@ -281,97 +203,67 @@ function(hf_download version out_var out_resolved)
281203
list(APPEND candidates "latest")
282204

283205
foreach(resolved ${candidates})
284-
set(base "https://huggingface.co/buckets/ggml-org/${HF_BUCKET}/resolve/${resolved}")
285-
286-
message(STATUS "UI: downloading from ${resolved}: ${base}")
287-
288-
set(ok TRUE)
289-
foreach(asset ${ASSETS})
290-
file(DOWNLOAD "${base}/${asset}?download=true" "${DIST_DIR}/${asset}"
291-
STATUS status TIMEOUT 60
292-
)
293-
list(GET status 0 rc)
294-
if(NOT rc EQUAL 0)
295-
list(GET status 1 errmsg)
296-
message(STATUS "UI: download ${asset} from ${resolved} failed: ${errmsg}")
297-
set(ok FALSE)
298-
break()
299-
endif()
300-
message(STATUS "UI: downloaded ${asset}")
301-
endforeach()
302-
303-
if(NOT ok)
206+
set(base "https://huggingface.co/buckets/${HF_BUCKET}/resolve/${resolved}")
207+
208+
message(STATUS "UI: downloading from ${resolved}: ${base}/dist.tar.gz")
209+
210+
file(DOWNLOAD "${base}/dist.tar.gz?download=true" "${archive}"
211+
STATUS status TIMEOUT 300
212+
)
213+
list(GET status 0 rc)
214+
if(NOT rc EQUAL 0)
215+
list(GET status 1 errmsg)
216+
message(STATUS "UI: download dist.tar.gz from ${resolved} failed: ${errmsg}")
304217
continue()
305218
endif()
306219

307-
# Best-effort checksum verification
308-
file(DOWNLOAD "${base}/checksums.txt?download=true" "${DIST_DIR}/checksums.txt"
309-
STATUS cs_status TIMEOUT 30
220+
file(DOWNLOAD "${base}/dist.tar.gz.sha256?download=true" "${archive}.sha256"
221+
STATUS status TIMEOUT 30
310222
)
311-
list(GET cs_status 0 cs_rc)
312-
if(cs_rc EQUAL 0)
313-
message(STATUS "UI: verifying checksums")
314-
file(STRINGS "${DIST_DIR}/checksums.txt" cs_lines)
315-
foreach(asset ${ASSETS})
316-
file(SHA256 "${DIST_DIR}/${asset}" h)
317-
string(TOLOWER "${h}" h)
318-
string(REGEX MATCH "${h}[ \t]+${asset}" m "${cs_lines}")
319-
if(NOT m)
320-
message(WARNING "UI: checksum verification failed for ${asset}")
321-
set(ok FALSE)
322-
break()
323-
endif()
324-
endforeach()
325-
if(ok)
326-
message(STATUS "UI: all checksums verified")
327-
endif()
223+
list(GET status 0 rc)
224+
if(NOT rc EQUAL 0)
225+
list(GET status 1 errmsg)
226+
message(STATUS "UI: download dist.tar.gz.sha256 from ${resolved} failed: ${errmsg}")
227+
continue()
328228
endif()
329229

330-
if(ok)
331-
set(${out_var} TRUE PARENT_SCOPE)
332-
set(${out_resolved} "${resolved}" PARENT_SCOPE)
333-
return()
230+
# Validate sha256 checkums
231+
file(READ "${archive}.sha256" expected)
232+
string(REGEX MATCH "^[0-9a-fA-F]+" expected "${expected}")
233+
string(TOLOWER "${expected}" expected)
234+
file(SHA256 "${archive}" actual)
235+
if("${expected}" STREQUAL "" OR NOT "${actual}" STREQUAL "${expected}")
236+
message(STATUS "UI: checksum mismatch for dist.tar.gz from ${resolved}")
237+
continue()
238+
endif()
239+
240+
# Clear DIST_DIR to remove stale files first
241+
file(REMOVE_RECURSE "${DIST_DIR}")
242+
243+
file(ARCHIVE_EXTRACT INPUT "${archive}" DESTINATION "${DIST_DIR}")
244+
245+
assets_present("${DIST_DIR}" present)
246+
if(NOT present)
247+
message(STATUS "UI: archive from ${resolved} is missing required assets")
248+
continue()
334249
endif()
250+
251+
message(STATUS "UI: archive verified and extracted")
252+
set(${out_var} TRUE PARENT_SCOPE)
253+
set(${out_resolved} "${resolved}" PARENT_SCOPE)
254+
return()
335255
endforeach()
336256
endfunction()
337257

338-
function(emit_files)
339-
assets_present(present)
258+
function(emit_files dist_dir)
259+
assets_present("${dist_dir}" present)
340260

341261
set(args "${UI_CPP}" "${UI_H}")
342262
if(present)
343-
foreach(asset ${ASSETS})
344-
list(APPEND args "${asset}" "${DIST_DIR}/${asset}")
345-
endforeach()
346-
347-
# Bundle files live in _app/immutable/ — vanilla SvelteKit output, no plugin
348-
# rewriting. Embedded names must match the exact _app/ paths that index.html
349-
# and sw.js reference.
350-
file(GLOB_RECURSE detected_bundle_js "${DIST_DIR}/_app/immutable/bundle.*.js")
351-
file(GLOB_RECURSE detected_bundle_css "${DIST_DIR}/_app/immutable/assets/bundle.*.css")
352-
file(GLOB_RECURSE detected_workbox "${DIST_DIR}/workbox-*.js")
353-
# Compute relative path from DIST_DIR to each found file.
354-
# e.g. /path/to/build/tools/ui/dist/_app/immutable/bundle.XXX.js
355-
# -> _app/immutable/bundle.XXX.js
356-
foreach(f ${detected_bundle_js})
357-
string(REPLACE "${DIST_DIR}/" "" rel "${f}")
358-
list(APPEND args "${rel}" "${f}")
359-
endforeach()
360-
foreach(f ${detected_bundle_css})
361-
string(REPLACE "${DIST_DIR}/" "" rel "${f}")
362-
list(APPEND args "${rel}" "${f}")
363-
endforeach()
364-
foreach(f ${detected_workbox})
365-
string(REPLACE "${DIST_DIR}/" "" rel "${f}")
366-
list(APPEND args "${rel}" "${f}")
367-
endforeach()
263+
# llama-ui-embed embeds every top-level file in dist_dir
264+
list(APPEND args "${dist_dir}")
368265
endif()
369266

370-
# Create build.json with the llama.cpp build number for UI version display.
371-
# This is separate from SvelteKit's _app/version.json (used for SW cache invalidation).
372-
# build.json is generated by the vite plugin (buildInfoPlugin) during npm build.
373-
# CMake just embeds it from the dist that npm produced.
374-
375267
execute_process(
376268
COMMAND "${LLAMA_UI_EMBED}" ${args}
377269
RESULT_VARIABLE rc
@@ -384,9 +276,10 @@ endfunction()
384276
# ---------------------------------------------------------------------------
385277
# 1. Priority 1: pre-built assets supplied in tools/ui/dist
386278
# ---------------------------------------------------------------------------
387-
copy_src_dist(SRC_OK)
279+
assets_present("${SRC_DIST_DIR}" SRC_OK)
388280
if(SRC_OK)
389-
emit_files()
281+
message(STATUS "UI: using pre-built assets from ${SRC_DIST_DIR}")
282+
emit_files("${SRC_DIST_DIR}")
390283
return()
391284
endif()
392285

@@ -419,7 +312,7 @@ if(NOT provisioned AND HF_ENABLED)
419312
endif()
420313
endif()
421314

422-
assets_present(have_assets)
315+
assets_present("${DIST_DIR}" have_assets)
423316
if(stamp_ok AND have_assets)
424317
message(STATUS "UI: HF stamp '${stamped}' matches version, skipping HF fetch")
425318
set(provisioned TRUE)
@@ -439,7 +332,7 @@ endif()
439332
# 4. Fallback: warn about stale or missing assets, then emit whatever we have
440333
# ---------------------------------------------------------------------------
441334
if(NOT provisioned)
442-
assets_present(have_assets)
335+
assets_present("${DIST_DIR}" have_assets)
443336
if(have_assets)
444337
message(WARNING "UI: provisioning failed; embedding stale assets from ${DIST_DIR}")
445338
else()
@@ -451,4 +344,4 @@ if(NOT provisioned)
451344
endif()
452345
endif()
453346

454-
emit_files()
347+
emit_files("${DIST_DIR}")

0 commit comments

Comments
 (0)