#!/bin/bash # embed-frontend.sh — Convert built frontend assets into linkable object files. # # Usage: scripts/embed-frontend.sh # # For each file in dist_dir, creates: # 1. An object file via `ld -r -b binary` (raw bytes, zero bloat) # 2. A generated embedded_assets.c with a lookup table # # Symbols created per file: _binary__start, _binary__end # On macOS, these get a leading underscore: __binary__start set -euo pipefail DIST_DIR="${1:?Usage: embed-frontend.sh }" OUTPUT_DIR="${2:?Usage: embed-frontend.sh }" # Clean old embedded objects (asset hashes change on each build) rm -rf "$OUTPUT_DIR" mkdir -p "$OUTPUT_DIR" # Detect platform — Linux uses ld -r -b binary, everything else uses xxd+cc IS_LINUX=false if [[ "$(uname -s)" == "Linux" ]] && ! [[ "$(uname -s)" =~ MINGW|MSYS ]]; then IS_LINUX=true fi # Content-type detection content_type_for() { local f="$1" case "$f" in *.html) echo "text/html" ;; *.js) echo "application/javascript" ;; *.css) echo "text/css" ;; *.json) echo "application/json" ;; *.svg) echo "image/svg+xml" ;; *.png) echo "image/png" ;; *.ico) echo "image/x-icon" ;; *.woff2) echo "font/woff2" ;; *.woff) echo "font/woff" ;; *.map) echo "application/json" ;; *) echo "application/octet-stream" ;; esac } # Mangle filename to valid C symbol: replace non-alnum with _ mangle() { echo "$1" | sed 's/[^a-zA-Z0-9]/_/g' } # Collect all files FILES=() while IFS= read -r -d '' file; do FILES+=("$file") done < <(find "$DIST_DIR" -type f -print0 | sort -z) if [[ ${#FILES[@]} -eq 0 ]]; then echo "Error: no files found in $DIST_DIR" >&2 exit 1 fi echo "Embedding ${#FILES[@]} files from $DIST_DIR" # Generate object files OBJ_FILES=() for file in "${FILES[@]}"; do rel="${file#$DIST_DIR/}" mangled=$(mangle "$rel") obj="$OUTPUT_DIR/embed_${mangled}.o" if $IS_LINUX; then # Linux: ld -r -b binary (zero bloat, ELF only) abs_obj="$(cd "$(dirname "$0")/.." && pwd)/$obj" (cd "$DIST_DIR" && ld -r -b binary -o "$abs_obj" "$rel") else # macOS/Windows/MSYS2: generate C byte array + cc (no xxd dependency) local_c="$OUTPUT_DIR/embed_${mangled}.c" local_sym="_binary_${mangled}" echo "/* Generated from $rel */" > "$local_c" echo "const unsigned char ${local_sym}_data[] = {" >> "$local_c" # Use od (POSIX) to generate hex bytes — works everywhere without xxd/vim od -An -tx1 -v < "$file" | tr -s ' ' '\n' | grep -v '^$' | sed 's/^/0x/; s/$/,/' | paste -sd' ' - | fold -s -w 76 | sed 's/^/ /' >> "$local_c" echo "};" >> "$local_c" echo "const unsigned int ${local_sym}_size = sizeof(${local_sym}_data);" >> "$local_c" ${CC:-cc} -c -O2 -o "$obj" "$local_c" rm -f "$local_c" fi OBJ_FILES+=("$obj") echo " $rel -> $obj" done # Generate embedded_assets.c ASSETS_C="src/ui/embedded_assets.c" cat > "$ASSETS_C" <<'HEADER' /* * embedded_assets.c — Generated file mapping URL paths to embedded bytes. * DO NOT EDIT — regenerated by scripts/embed-frontend.sh */ #include "ui/embedded_assets.h" #include HEADER # Declare extern symbols for file in "${FILES[@]}"; do rel="${file#$DIST_DIR/}" mangled=$(mangle "$rel") sym="_binary_${mangled}" if ! $IS_LINUX; then echo "extern const unsigned char ${sym}_data[];" >> "$ASSETS_C" echo "extern const unsigned int ${sym}_size;" >> "$ASSETS_C" else echo "extern const unsigned char ${sym}_start[];" >> "$ASSETS_C" echo "extern const unsigned char ${sym}_end[];" >> "$ASSETS_C" fi done echo "" >> "$ASSETS_C" echo "cbm_embedded_file_t CBM_EMBEDDED_FILES[] = {" >> "$ASSETS_C" for file in "${FILES[@]}"; do rel="${file#$DIST_DIR/}" mangled=$(mangle "$rel") sym="_binary_${mangled}" ct=$(content_type_for "$rel") # URL path: /index.html for root, /assets/... for assets url_path="/$rel" if ! $IS_LINUX; then echo " {\"$url_path\", ${sym}_data, 0, \"$ct\"}," >> "$ASSETS_C" else echo " {\"$url_path\", ${sym}_start, 0, \"$ct\"}," >> "$ASSETS_C" fi done echo "};" >> "$ASSETS_C" echo "const int CBM_EMBEDDED_FILE_COUNT = ${#FILES[@]};" >> "$ASSETS_C" # Generate size fixup if $IS_LINUX; then # Linux: compute size from start/end pointers (ld -r -b binary symbols) cat >> "$ASSETS_C" <<'SIZEINIT' static void __attribute__((constructor)) init_embedded_sizes(void) { cbm_embedded_file_t *files = CBM_EMBEDDED_FILES; SIZEINIT for i in "${!FILES[@]}"; do rel="${FILES[$i]#$DIST_DIR/}" mangled=$(mangle "$rel") sym="_binary_${mangled}" echo " files[$i].size = (unsigned int)(${sym}_end - ${sym}_start);" >> "$ASSETS_C" done echo "}" >> "$ASSETS_C" else # macOS/Windows: use explicit _size vars from xxd-generated C arrays cat >> "$ASSETS_C" <<'SIZEINIT' static void __attribute__((constructor)) init_embedded_sizes(void) { cbm_embedded_file_t *files = CBM_EMBEDDED_FILES; SIZEINIT for i in "${!FILES[@]}"; do rel="${FILES[$i]#$DIST_DIR/}" mangled=$(mangle "$rel") sym="_binary_${mangled}" echo " files[$i].size = ${sym}_size;" >> "$ASSETS_C" done echo "}" >> "$ASSETS_C" fi # Add lookup function cat >> "$ASSETS_C" <<'LOOKUP' const cbm_embedded_file_t *cbm_embedded_lookup(const char *path) { for (int i = 0; i < CBM_EMBEDDED_FILE_COUNT; i++) { if (strcmp(CBM_EMBEDDED_FILES[i].path, path) == 0) { return &CBM_EMBEDDED_FILES[i]; } } return NULL; } LOOKUP echo "Generated $ASSETS_C with ${#FILES[@]} embedded files" echo "Object files in $OUTPUT_DIR:" printf ' %s\n' "${OBJ_FILES[@]}"