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>
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
109128static 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, " \n current 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 ());
0 commit comments