char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Adrien Gallouëtui : add cache (#28802)3057bb66c

char/aliceai-t5-moe
20.1 KiB592 linesraw
1# Provision UI assets and generate ui.cpp/ui.h.
2#
3# Asset provisioning priority:
4#   1. Pre-built assets in SRC_DIST_DIR (manually built by user)
5#   2. If BUILD_UI=ON: npm build
6#   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)
8
9cmake_minimum_required(VERSION 3.18)
10
11set(UI_SOURCE_DIR     "" CACHE STRING "UI source directory (to run npm build)")
12set(UI_BINARY_DIR     "" CACHE STRING "UI binary directory (to store generated files)")
13set(LLAMA_SOURCE_DIR  "" CACHE STRING "Project source root (to resolve version from git)")
14set(HF_BUCKET         "" CACHE STRING "Hugging Face bucket name")
15set(HF_VERSION        "" CACHE STRING "Version to download (empty = resolve from git)")
16set(HF_ENABLED        "" CACHE STRING "Whether to allow HF Bucket download (ON/OFF)")
17set(BUILD_UI          "" CACHE STRING "Build UI via npm (ON/OFF)")
18set(LLAMA_UI_GZIP     "" CACHE STRING "Apply gzip compress to assets to save bandwidth")
19
20set(DIST_DIR     "${UI_BINARY_DIR}/dist")
21set(SRC_DIST_DIR "${UI_SOURCE_DIR}/dist")
22set(WORK_DIR     "${UI_BINARY_DIR}/ui-src")
23set(STAMP_FILE   "${UI_BINARY_DIR}/.ui-stamp")
24set(EMBED_STAMP  "${UI_BINARY_DIR}/.ui-embed.sha256")
25set(UI_CPP       "${UI_BINARY_DIR}/ui.cpp")
26set(UI_H         "${UI_BINARY_DIR}/ui.h")
27
28function(mime_from_ext name out_var)
29    string(FIND "${name}" "." ext REVERSE)
30    if(ext GREATER -1)
31        string(SUBSTRING "${name}" ${ext} -1 ext_full)
32        string(SUBSTRING "${ext_full}" 1 -1 ext_str)
33    else()
34        set(ext_str "")
35    endif()
36    if(ext_str STREQUAL "html")
37        set(m "text/html; charset=utf-8")
38    elseif(ext_str STREQUAL "css")
39        set(m "text/css")
40    elseif(ext_str STREQUAL "js")
41        set(m "application/javascript")
42    elseif(ext_str STREQUAL "json")
43        set(m "application/json")
44    elseif(ext_str STREQUAL "webmanifest")
45        set(m "application/manifest+json")
46    elseif(ext_str STREQUAL "svg")
47        set(m "image/svg+xml")
48    elseif(ext_str STREQUAL "png")
49        set(m "image/png")
50    elseif(ext_str STREQUAL "jpg" OR ext_str STREQUAL "jpeg")
51        set(m "image/jpeg")
52    elseif(ext_str STREQUAL "ico")
53        set(m "image/x-icon")
54    elseif(ext_str STREQUAL "woff")
55        set(m "font/woff")
56    elseif(ext_str STREQUAL "woff2")
57        set(m "font/woff2")
58    else()
59        set(m "application/octet-stream")
60    endif()
61    set(${out_var} "${m}" PARENT_SCOPE)
62endfunction()
63
64# Fail when a dist tree is present but is missing files the UI needs at
65# runtime; catches truncated/stale asset trees early with a useful message.
66function(ui_validate_assets files in_dir)
67    list(LENGTH files n_assets)
68    if(n_assets EQUAL 0)
69        return()
70    endif()
71
72    set(found_index FALSE)
73    set(found_manifest FALSE)
74    set(found_sw FALSE)
75    set(found_build_json FALSE)
76    set(found_version_json FALSE)
77    set(found_bundle_js FALSE)
78    set(found_bundle_css FALSE)
79    set(found_workbox_js FALSE)
80
81    foreach(f ${files})
82        get_filename_component(base "${f}" NAME)
83        if(base STREQUAL "index.html")
84            set(found_index TRUE)
85        elseif(base STREQUAL "manifest.webmanifest")
86            set(found_manifest TRUE)
87        elseif(base STREQUAL "sw.js")
88            set(found_sw TRUE)
89        elseif(base STREQUAL "build.json")
90            set(found_build_json TRUE)
91        elseif(base STREQUAL "version.json")
92            set(found_version_json TRUE)
93        elseif(base MATCHES "^bundle.*\\.js$")
94            set(found_bundle_js TRUE)
95        elseif(base MATCHES "^bundle.*\\.css$")
96            set(found_bundle_css TRUE)
97        elseif(base MATCHES "^workbox.*\\.js$")
98            set(found_workbox_js TRUE)
99        endif()
100    endforeach()
101
102    set(missing "")
103    if(NOT found_index)
104        list(APPEND missing "index.html")
105    endif()
106    if(NOT found_manifest)
107        list(APPEND missing "manifest.webmanifest")
108    endif()
109    if(NOT found_sw)
110        list(APPEND missing "sw.js")
111    endif()
112    if(NOT found_build_json)
113        list(APPEND missing "build.json")
114    endif()
115    if(NOT found_version_json)
116        list(APPEND missing "version.json")
117    endif()
118    if(NOT found_bundle_js)
119        list(APPEND missing "bundle[hash].js")
120    endif()
121    if(NOT found_bundle_css)
122        list(APPEND missing "bundle[hash].css")
123    endif()
124    if(NOT found_workbox_js)
125        list(APPEND missing "workbox[hash].js")
126    endif()
127
128    if(missing)
129        set(listing "")
130        foreach(f ${files})
131            string(APPEND listing "    ${f}\n")
132        endforeach()
133        set(missing_list "")
134        foreach(m ${missing})
135            string(APPEND missing_list "    ${m}\n")
136        endforeach()
137        message(FATAL_ERROR
138            "UI: current asset files:\n${listing}"
139            "UI: missing required asset(s):\n${missing_list}"
140            "UI: hint: try cleaning your build directory: ${in_dir}")
141    endif()
142endfunction()
143
144# Generate ui.cpp/ui.h embedding every file of ${dist_dir} (empty table when
145# it has no index.html), gzip-compressed when LLAMA_UI_GZIP is enabled.
146function(emit_files dist_dir)
147    set(UI_TEMPLATE_DIR "${LLAMA_SOURCE_DIR}/tools/ui")
148
149    # Collect the asset list once and reuse it for the fingerprint,
150    # validation, compression and embedding.
151    set(assets "")
152    if(EXISTS "${dist_dir}/index.html")
153        file(GLOB_RECURSE assets
154            LIST_DIRECTORIES false
155            RELATIVE "${dist_dir}"
156            "${dist_dir}/*")
157        list(FILTER assets EXCLUDE REGEX "^_gzip/")
158        list(SORT assets)
159    endif()
160
161    if(LLAMA_UI_GZIP AND NOT DEFINED ENV{SOURCE_DATE_EPOCH})
162        # Zero the gzip header timestamp so identical inputs give identical
163        # bytes (and therefore stable ETags) on every machine.
164        set(ENV{SOURCE_DATE_EPOCH} 0)
165    endif()
166
167    # Fingerprint of every input that determines ui.cpp/ui.h: compression
168    # settings, the asset tree (names + SHA-256) and this script + templates.
169    set(fp "${LLAMA_UI_GZIP}|$ENV{SOURCE_DATE_EPOCH}|${CMAKE_VERSION}\n")
170    foreach(f ${assets})
171        file(SHA256 "${dist_dir}/${f}" h)
172        string(APPEND fp "${f} ${h}\n")
173    endforeach()
174    foreach(g
175            "${CMAKE_CURRENT_FUNCTION_LIST_FILE}"
176            "${UI_TEMPLATE_DIR}/ui.h.in"
177            "${UI_TEMPLATE_DIR}/ui.cpp.in")
178        file(SHA256 "${g}" h)
179        string(APPEND fp "gen ${h}\n")
180    endforeach()
181    string(SHA256 fingerprint "${fp}")
182
183    if(EXISTS "${EMBED_STAMP}" AND EXISTS "${UI_CPP}" AND EXISTS "${UI_H}")
184        file(READ "${EMBED_STAMP}" fp_saved)
185        string(STRIP "${fp_saved}" fp_saved)
186        if(fp_saved STREQUAL "${fingerprint}")
187            message(STATUS "UI: assets unchanged, skipping embedding")
188            return()
189        endif()
190    endif()
191
192    # Drop the old stamp up front so a crash mid-generation cannot leave
193    # outputs and stamp out of sync.
194    file(REMOVE "${EMBED_STAMP}")
195
196    ui_validate_assets("${assets}" "${dist_dir}")
197
198    set(embed_dir "${dist_dir}")
199    set(use_gzip FALSE)
200
201    if(EXISTS "${dist_dir}/index.html")
202        if(EXISTS "${dist_dir}/_gzip")
203            # a _gzip tree inside dist_dir can only be a leftover from an
204            # older version of this script that staged it there
205            file(REMOVE_RECURSE "${dist_dir}/_gzip")
206            message(STATUS "UI: removed stale gzip tree ${dist_dir}/_gzip")
207        endif()
208        if(LLAMA_UI_GZIP)
209            # Compress every asset into a parallel _gzip/ tree under the build
210            # directory, served with Content-Encoding: gzip.
211            set(gzip_root "${UI_BINARY_DIR}/ui-gzip")
212            set(gzip_dir  "${gzip_root}/_gzip")
213            file(REMOVE_RECURSE "${gzip_root}")
214            foreach(f IN LISTS assets)
215                get_filename_component(asset_path "${dist_dir}/${f}" REALPATH)
216                get_filename_component(dst_dir "${gzip_dir}/${f}" DIRECTORY)
217                file(MAKE_DIRECTORY "${dst_dir}")
218                file(ARCHIVE_CREATE
219                    OUTPUT "${gzip_dir}/${f}"
220                    PATHS "${asset_path}"
221                    FORMAT raw
222                    COMPRESSION GZip
223                )
224            endforeach()
225            message(STATUS "UI: gzip compression applied (${gzip_dir})")
226            set(embed_dir "${gzip_dir}")
227            set(use_gzip TRUE)
228        endif()
229    endif()
230
231    list(LENGTH assets n_assets)
232
233    # Per-asset arrays and table rows go into the ui.h.in / ui.cpp.in templates;
234    # configure_file only rewrites on content change, avoiding needless recompiles.
235    set(ASSET_ARRAYS "")
236    set(ASSET_TABLE "")
237    set(idx 0)
238
239    foreach(f IN LISTS assets)
240        file(READ "${embed_dir}/${f}" hex HEX)
241        if(hex STREQUAL "")
242            message(FATAL_ERROR "UI: empty file: ${embed_dir}/${f}")
243        endif()
244
245        string(REGEX REPLACE "(..)" "0x\\1," bytes "${hex}")
246        file(SHA256 "${embed_dir}/${f}" etag)
247        mime_from_ext("${f}" mime)
248
249        string(APPEND ASSET_ARRAYS
250            "static const unsigned char asset_${idx}[] = {${bytes}};\n")
251
252        string(APPEND ASSET_TABLE
253            "    { \"${f}\", asset_${idx}, sizeof(asset_${idx}), \"\\\"${etag}\\\"\", \"${mime}\" },\n")
254
255        math(EXPR idx "${idx} + 1")
256    endforeach()
257
258    set(LLAMA_UI_HAS_ASSETS 0)
259    if(n_assets GREATER 0)
260        set(LLAMA_UI_HAS_ASSETS 1)
261    endif()
262    set(N_ASSETS "${n_assets}")
263    set(USE_GZIP false)
264    if(use_gzip)
265        set(USE_GZIP true)
266    endif()
267
268    configure_file("${UI_TEMPLATE_DIR}/ui.h.in"   "${UI_H}"   @ONLY)
269    configure_file("${UI_TEMPLATE_DIR}/ui.cpp.in" "${UI_CPP}" @ONLY)
270
271    # Write the embed stamp last, after both generated files succeeded.
272    file(WRITE "${EMBED_STAMP}" "${fingerprint}")
273    message(STATUS "UI: embedded ${n_assets} assets")
274endfunction()
275
276function(npm_build_should_skip out_var)
277    set(${out_var} FALSE PARENT_SCOPE)
278
279    if(NOT EXISTS "${DIST_DIR}/index.html")
280        return()
281    endif()
282
283    if(EXISTS "${STAMP_FILE}")
284        return()
285    endif()
286
287    if(NOT EXISTS "${UI_SOURCE_DIR}/sources.cmake")
288        return()
289    endif()
290    include("${UI_SOURCE_DIR}/sources.cmake")
291
292    set(globs "")
293    foreach(g ${UI_SOURCE_GLOBS})
294        list(APPEND globs "${UI_SOURCE_DIR}/${g}")
295    endforeach()
296    file(GLOB_RECURSE sources ${globs})
297    foreach(f ${UI_SOURCE_FILES})
298        list(APPEND sources "${UI_SOURCE_DIR}/${f}")
299    endforeach()
300
301    file(TIMESTAMP "${DIST_DIR}/index.html" out_ts)
302
303    foreach(s ${sources})
304        if(NOT EXISTS "${s}")
305            continue()
306        endif()
307        file(TIMESTAMP "${s}" s_ts)
308        if(s_ts STRGREATER out_ts)
309            return()
310        endif()
311    endforeach()
312
313    set(${out_var} TRUE PARENT_SCOPE)
314endfunction()
315
316function(stage_sources)
317    if(EXISTS "${WORK_DIR}")
318        file(GLOB staged RELATIVE "${WORK_DIR}" "${WORK_DIR}/*")
319        list(REMOVE_ITEM staged "node_modules")
320        foreach(entry ${staged})
321            file(REMOVE_RECURSE "${WORK_DIR}/${entry}")
322        endforeach()
323    endif()
324
325    file(COPY "${UI_SOURCE_DIR}/"
326        DESTINATION "${WORK_DIR}"
327        NO_SOURCE_PERMISSIONS
328        PATTERN "node_modules" EXCLUDE
329    )
330endfunction()
331
332function(npm_build out_var)
333    set(${out_var} FALSE PARENT_SCOPE)
334
335    if(NOT EXISTS "${UI_SOURCE_DIR}/package.json")
336        message(STATUS "UI: ${UI_SOURCE_DIR}/package.json not found, skipping npm")
337        return()
338    endif()
339
340    npm_build_should_skip(skip)
341    if(skip)
342        message(STATUS "UI: npm output up-to-date, skipping build")
343        set(${out_var} TRUE PARENT_SCOPE)
344        return()
345    endif()
346
347    if(CMAKE_HOST_WIN32)
348        find_program(NPM_EXECUTABLE NAMES npm.cmd npm.bat npm)
349    else()
350        find_program(NPM_EXECUTABLE npm)
351    endif()
352    if(NOT NPM_EXECUTABLE)
353        message(STATUS "UI: npm not found, skipping npm build")
354        return()
355    endif()
356
357    stage_sources()
358
359    # npm writes node_modules/.package-lock.json on every successful install,
360    # so a package-lock.json newer than this marker means node_modules is stale
361    set(NPM_MARKER "${WORK_DIR}/node_modules/.package-lock.json")
362    set(need_install FALSE)
363    if(NOT EXISTS "${NPM_MARKER}")
364        set(need_install TRUE)
365    else()
366        file(TIMESTAMP "${WORK_DIR}/package-lock.json" lock_ts)
367        file(TIMESTAMP "${NPM_MARKER}" marker_ts)
368        if(lock_ts STRGREATER marker_ts)
369            set(need_install TRUE)
370        endif()
371    endif()
372
373    if(need_install)
374        message(STATUS "UI: running npm ci")
375        execute_process(
376            COMMAND ${NPM_EXECUTABLE} ci
377            WORKING_DIRECTORY "${WORK_DIR}"
378            RESULT_VARIABLE rc
379            ERROR_VARIABLE  err
380        )
381        if(NOT rc EQUAL 0)
382            message(STATUS "UI: npm ci failed (${rc})")
383            message(STATUS "  stderr: ${err}")
384            return()
385        endif()
386    endif()
387
388    file(MAKE_DIRECTORY "${DIST_DIR}")
389
390    message(STATUS "UI: running npm run build, output -> ${DIST_DIR}")
391    execute_process(
392        COMMAND ${CMAKE_COMMAND} -E env "LLAMA_UI_OUT_DIR=${DIST_DIR}" "LLAMA_UI_VERSION=${HF_VERSION}" "LLAMA_BUILD_NUMBER=${LLAMA_BUILD_NUMBER}"
393                ${NPM_EXECUTABLE} run build
394        WORKING_DIRECTORY "${WORK_DIR}"
395        RESULT_VARIABLE rc
396        ERROR_VARIABLE  err
397    )
398    if(NOT rc EQUAL 0)
399        message(STATUS "UI: npm run build failed (${rc})")
400        message(STATUS "  stderr: ${err}")
401        return()
402    endif()
403
404    if(NOT EXISTS "${DIST_DIR}/index.html")
405        message(STATUS "UI: npm build finished but assets missing in ${DIST_DIR}")
406        return()
407    endif()
408
409    message(STATUS "UI: npm build succeeded")
410    file(REMOVE "${STAMP_FILE}")
411    set(${out_var} TRUE PARENT_SCOPE)
412endfunction()
413
414function(resolve_version out_var)
415    if(NOT "${HF_VERSION}" STREQUAL "")
416        set(${out_var} "${HF_VERSION}" PARENT_SCOPE)
417        return()
418    endif()
419
420    if(EXISTS "${LLAMA_SOURCE_DIR}/cmake/build-info.cmake")
421        include("${LLAMA_SOURCE_DIR}/cmake/build-info.cmake")
422        if(NOT "${BUILD_NUMBER}" STREQUAL "" AND NOT BUILD_NUMBER EQUAL 0)
423            set(${out_var} "b${BUILD_NUMBER}" PARENT_SCOPE)
424            return()
425        endif()
426    endif()
427
428    set(${out_var} "" PARENT_SCOPE)
429endfunction()
430
431function(hf_download version out_var out_resolved)
432    set(${out_var}      FALSE PARENT_SCOPE)
433    set(${out_resolved} ""    PARENT_SCOPE)
434
435    set(archive "${UI_BINARY_DIR}/dist.tar.gz")
436
437    # Use HF_TOKEN to benefit from higher rate limits
438    set(auth_headers "")
439    if(DEFINED ENV{HF_TOKEN} AND NOT "$ENV{HF_TOKEN}" STREQUAL "")
440        list(APPEND auth_headers "HTTPHEADER" "Authorization: Bearer $ENV{HF_TOKEN}")
441    endif()
442
443    set(candidates "")
444    if(NOT "${version}" STREQUAL "")
445        list(APPEND candidates "${version}")
446    endif()
447    list(APPEND candidates "latest")
448
449    foreach(resolved ${candidates})
450        set(base "https://huggingface.co/buckets/${HF_BUCKET}/resolve/${resolved}")
451
452        message(STATUS "UI: downloading from ${resolved}: ${base}/dist.tar.gz")
453
454        # Fetch the checksum first: when the archive we already have matches
455        # it, the expensive download is skipped and only extraction repeats.
456        file(DOWNLOAD "${base}/dist.tar.gz.sha256?download=true" "${archive}.sha256"
457            STATUS status TIMEOUT 30 ${auth_headers}
458        )
459        list(GET status 0 rc)
460        if(NOT rc EQUAL 0)
461            list(GET status 1 errmsg)
462            message(STATUS "UI: download dist.tar.gz.sha256 from ${resolved} failed: ${errmsg}")
463            continue()
464        endif()
465
466        # Validate the sha256 checksum: reject anything that is not a full
467        # 64-hex-digit digest before touching the archive.
468        file(READ "${archive}.sha256" expected)
469        string(REGEX MATCH "^[0-9a-fA-F]+" expected "${expected}")
470        string(TOLOWER "${expected}" expected)
471        string(LENGTH "${expected}" expected_len)
472        if(NOT expected_len EQUAL 64)
473            message(STATUS "UI: invalid checksum from ${resolved}")
474            continue()
475        endif()
476
477        set(actual "")
478        if(EXISTS "${archive}")
479            file(SHA256 "${archive}" actual)
480        endif()
481
482        if("${actual}" STREQUAL "${expected}")
483            message(STATUS "UI: local dist.tar.gz matches checksum from ${resolved}, skipping download")
484        else()
485            file(DOWNLOAD "${base}/dist.tar.gz?download=true" "${archive}"
486                STATUS status TIMEOUT 300 ${auth_headers}
487            )
488            list(GET status 0 rc)
489            if(NOT rc EQUAL 0)
490                list(GET status 1 errmsg)
491                message(STATUS "UI: download dist.tar.gz from ${resolved} failed: ${errmsg}")
492                continue()
493            endif()
494
495            file(SHA256 "${archive}" actual)
496            if(NOT "${actual}" STREQUAL "${expected}")
497                message(STATUS "UI: checksum mismatch for dist.tar.gz from ${resolved}")
498                continue()
499            endif()
500        endif()
501
502        # Remove the stamp with the dist tree it describes, together.
503        file(REMOVE "${STAMP_FILE}")
504        file(REMOVE_RECURSE "${DIST_DIR}")
505
506        file(ARCHIVE_EXTRACT INPUT "${archive}" DESTINATION "${DIST_DIR}")
507
508        if(NOT EXISTS "${DIST_DIR}/index.html")
509            message(STATUS "UI: archive from ${resolved} is missing required assets")
510            continue()
511        endif()
512
513        message(STATUS "UI: archive verified and extracted")
514        set(${out_var}      TRUE          PARENT_SCOPE)
515        set(${out_resolved} "${resolved}" PARENT_SCOPE)
516        return()
517    endforeach()
518endfunction()
519
520# ---------------------------------------------------------------------------
521# 1. Priority 1: pre-built assets supplied in tools/ui/dist
522# ---------------------------------------------------------------------------
523if(EXISTS "${SRC_DIST_DIR}/index.html")
524    message(STATUS "UI: using pre-built assets from ${SRC_DIST_DIR}")
525    emit_files("${SRC_DIST_DIR}")
526    return()
527endif()
528
529# ---------------------------------------------------------------------------
530# 2. Priority 2: npm build (if BUILD_UI=ON)
531# ---------------------------------------------------------------------------
532set(provisioned FALSE)
533
534if(BUILD_UI)
535    # Resolve version from git build-info if not explicitly set
536    resolve_version(HF_VERSION)
537    npm_build(NPM_OK)
538    if(NPM_OK)
539        set(provisioned TRUE)
540    endif()
541endif()
542
543# ---------------------------------------------------------------------------
544# 3. Priority 3: HF Bucket download (if npm did not produce assets and HF_ENABLED=ON)
545# ---------------------------------------------------------------------------
546if(NOT provisioned AND HF_ENABLED)
547    resolve_version(VERSION)
548
549    # Stamp a successful HF download: records bucket + requested version and
550    # lets later steps distinguish downloaded assets from locally built ones.
551    set(stamp_key "${HF_BUCKET}|${VERSION}")
552
553    set(stamp_ok FALSE)
554    if(EXISTS "${STAMP_FILE}" AND EXISTS "${DIST_DIR}/index.html" AND NOT "${VERSION}" STREQUAL "")
555        file(READ "${STAMP_FILE}" stamped)
556        string(STRIP "${stamped}" stamped)
557        if(stamped STREQUAL "${stamp_key}")
558            set(stamp_ok TRUE)
559        endif()
560    endif()
561
562    if(stamp_ok)
563        message(STATUS "UI: HF stamp matches '${stamp_key}', skipping HF fetch")
564        set(provisioned TRUE)
565    else()
566        hf_download("${VERSION}" HF_OK HF_RESOLVED)
567        if(HF_OK)
568            file(WRITE "${STAMP_FILE}" "${stamp_key}")
569            message(STATUS "UI: HF download succeeded, stamp updated (${stamp_key}, resolved: ${HF_RESOLVED})")
570            set(provisioned TRUE)
571        else()
572            message(STATUS "UI: HF download failed")
573        endif()
574    endif()
575endif()
576
577# ---------------------------------------------------------------------------
578# 4. Fallback: warn about stale or missing assets, then emit whatever we have
579# ---------------------------------------------------------------------------
580if(NOT provisioned)
581    if(EXISTS "${DIST_DIR}/index.html")
582        message(WARNING "UI: provisioning failed; embedding stale assets from ${DIST_DIR}")
583    else()
584        message(WARNING "UI: no assets available - building without an embedded UI. "
585                        "In a disconnected environment, download the pre-built UI "
586                        "from a llama.cpp release at "
587                        "https://github.com/ggml-org/llama.cpp/releases and "
588                        "extract to tools/ui/dist.")
589    endif()
590endif()
591
592emit_files("${DIST_DIR}")