|
| 1 | +# cmake/Emscripten.cmake |
| 2 | +# Emscripten/WebAssembly build configuration |
| 3 | + |
| 4 | +# Common paths for web assets |
| 5 | +set(cons_expr_WEB_DIR "${CMAKE_SOURCE_DIR}/web") |
| 6 | +set(cons_expr_COI_WORKER "${cons_expr_WEB_DIR}/coi-serviceworker.min.js") |
| 7 | +set(cons_expr_SHELL_TEMPLATE "${cons_expr_WEB_DIR}/shell_template.html.in") |
| 8 | +set(cons_expr_INDEX_TEMPLATE "${cons_expr_WEB_DIR}/index_template.html.in") |
| 9 | + |
| 10 | +# Helper function to escape HTML special characters |
| 11 | +function(escape_html output_var input) |
| 12 | + set(result "${input}") |
| 13 | + string(REPLACE "&" "&" result "${result}") |
| 14 | + string(REPLACE "<" "<" result "${result}") |
| 15 | + string(REPLACE ">" ">" result "${result}") |
| 16 | + string(REPLACE "\"" """ result "${result}") |
| 17 | + set(${output_var} "${result}" PARENT_SCOPE) |
| 18 | +endfunction() |
| 19 | + |
| 20 | +# Detect if we're building with Emscripten |
| 21 | +if(EMSCRIPTEN) |
| 22 | + message(STATUS "Emscripten build detected - configuring for WebAssembly") |
| 23 | + |
| 24 | + # Set WASM build flag |
| 25 | + set(cons_expr_WASM_BUILD ON CACHE BOOL "Building for WebAssembly" FORCE) |
| 26 | + |
| 27 | + # Sanitizers don't work with Emscripten |
| 28 | + foreach(sanitizer ADDRESS LEAK UNDEFINED THREAD MEMORY) |
| 29 | + set(cons_expr_ENABLE_SANITIZER_${sanitizer} OFF CACHE BOOL "Not supported with Emscripten") |
| 30 | + endforeach() |
| 31 | + |
| 32 | + # Disable static analysis and strict warnings for Emscripten builds |
| 33 | + foreach(option CLANG_TIDY CPPCHECK WARNINGS_AS_ERRORS) |
| 34 | + set(cons_expr_ENABLE_${option} OFF CACHE BOOL "Disabled for Emscripten") |
| 35 | + endforeach() |
| 36 | + |
| 37 | + # Disable testing - no way to execute WASM test targets |
| 38 | + set(BUILD_TESTING OFF CACHE BOOL "No test runner for WASM") |
| 39 | + |
| 40 | + # WASM runtime configuration - tunable performance parameters |
| 41 | + set(cons_expr_WASM_INITIAL_MEMORY "33554432" CACHE STRING |
| 42 | + "Initial WASM memory in bytes (default: 32MB)") |
| 43 | + set(cons_expr_WASM_PTHREAD_POOL_SIZE "4" CACHE STRING |
| 44 | + "Pthread pool size for WASM builds (default: 4)") |
| 45 | + set(cons_expr_WASM_ASYNCIFY_STACK_SIZE "65536" CACHE STRING |
| 46 | + "Asyncify stack size in bytes (default: 64KB)") |
| 47 | + |
| 48 | + # For Emscripten WASM builds, FTXUI requires pthreads and native exception handling |
| 49 | + # Set these flags early so they propagate to all dependencies |
| 50 | + add_compile_options(-pthread -fwasm-exceptions) |
| 51 | + add_link_options(-pthread -fwasm-exceptions) |
| 52 | +endif() |
| 53 | + |
| 54 | +# Function to apply WASM settings to a target |
| 55 | +function(cons_expr_configure_wasm_target target) |
| 56 | + if(EMSCRIPTEN) |
| 57 | + # Parse optional named arguments |
| 58 | + set(options "") |
| 59 | + set(oneValueArgs TITLE DESCRIPTION RESOURCES_DIR) |
| 60 | + set(multiValueArgs "") |
| 61 | + cmake_parse_arguments(WASM "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) |
| 62 | + |
| 63 | + # Set defaults if not provided |
| 64 | + if(NOT WASM_TITLE) |
| 65 | + set(WASM_TITLE "${target}") |
| 66 | + endif() |
| 67 | + |
| 68 | + if(NOT WASM_DESCRIPTION) |
| 69 | + set(WASM_DESCRIPTION "WebAssembly application") |
| 70 | + endif() |
| 71 | + |
| 72 | + # Get the actual output name (may differ from target name) |
| 73 | + get_target_property(OUTPUT_NAME ${target} OUTPUT_NAME) |
| 74 | + if(NOT OUTPUT_NAME) |
| 75 | + set(OUTPUT_NAME "${target}") |
| 76 | + endif() |
| 77 | + |
| 78 | + # Register this target in the global WASM targets list |
| 79 | + set_property(GLOBAL APPEND PROPERTY cons_expr_WASM_TARGETS "${target}") |
| 80 | + set_property(GLOBAL PROPERTY cons_expr_WASM_TARGET_${target}_TITLE "${WASM_TITLE}") |
| 81 | + set_property(GLOBAL PROPERTY cons_expr_WASM_TARGET_${target}_DESCRIPTION "${WASM_DESCRIPTION}") |
| 82 | + set_property(GLOBAL PROPERTY cons_expr_WASM_TARGET_${target}_OUTPUT_NAME "${OUTPUT_NAME}") |
| 83 | + |
| 84 | + target_compile_definitions(${target} PRIVATE cons_expr_WASM_BUILD=1) |
| 85 | + |
| 86 | + # Emscripten link flags |
| 87 | + target_link_options(${target} PRIVATE |
| 88 | + # Enable pthreads - REQUIRED by FTXUI's WASM implementation |
| 89 | + "-sUSE_PTHREADS=1" |
| 90 | + "-sPROXY_TO_PTHREAD=1" |
| 91 | + "-sPTHREAD_POOL_SIZE=${cons_expr_WASM_PTHREAD_POOL_SIZE}" |
| 92 | + # Enable asyncify for emscripten_sleep and async operations |
| 93 | + "-sASYNCIFY=1" |
| 94 | + "-sASYNCIFY_STACK_SIZE=${cons_expr_WASM_ASYNCIFY_STACK_SIZE}" |
| 95 | + # Memory configuration |
| 96 | + "-sALLOW_MEMORY_GROWTH=1" |
| 97 | + "-sINITIAL_MEMORY=${cons_expr_WASM_INITIAL_MEMORY}" |
| 98 | + # Environment - need both web and worker for pthread support |
| 99 | + "-sENVIRONMENT=web,worker" |
| 100 | + # Export runtime methods for JavaScript interop |
| 101 | + "-sEXPORTED_RUNTIME_METHODS=['FS','ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8']" |
| 102 | + # Export malloc/free for MAIN_THREAD_EM_ASM usage |
| 103 | + "-sEXPORTED_FUNCTIONS=['_main','_malloc','_free']" |
| 104 | + # Debug: enable assertions for better error messages |
| 105 | + "-sASSERTIONS=1" |
| 106 | + ) |
| 107 | + |
| 108 | + # Embed resources into WASM binary (optional, per-target) |
| 109 | + if(WASM_RESOURCES_DIR AND EXISTS "${WASM_RESOURCES_DIR}") |
| 110 | + # Convert to absolute path to avoid issues with Emscripten path resolution |
| 111 | + get_filename_component(ABS_RESOURCES_DIR "${WASM_RESOURCES_DIR}" ABSOLUTE BASE_DIR "${CMAKE_SOURCE_DIR}") |
| 112 | + |
| 113 | + target_link_options(${target} PRIVATE |
| 114 | + "--embed-file=${ABS_RESOURCES_DIR}@/resources" |
| 115 | + ) |
| 116 | + message(STATUS "Embedding resources for ${target} from ${ABS_RESOURCES_DIR}") |
| 117 | + endif() |
| 118 | + |
| 119 | + # Configure the shell HTML template for this target |
| 120 | + set(TARGET_NAME "${OUTPUT_NAME}") |
| 121 | + set(TARGET_TITLE "${WASM_TITLE}") |
| 122 | + set(TARGET_DESCRIPTION "${WASM_DESCRIPTION}") |
| 123 | + set(AT "@") # For escaping @ in npm package URLs |
| 124 | + set(CONFIGURED_SHELL "${CMAKE_BINARY_DIR}/web/${target}_shell.html") |
| 125 | + |
| 126 | + # Generate target-specific shell file (configure_file creates parent directories automatically) |
| 127 | + if(EXISTS "${cons_expr_SHELL_TEMPLATE}") |
| 128 | + configure_file( |
| 129 | + "${cons_expr_SHELL_TEMPLATE}" |
| 130 | + "${CONFIGURED_SHELL}" |
| 131 | + @ONLY |
| 132 | + ) |
| 133 | + |
| 134 | + # Use the generated shell file |
| 135 | + target_link_options(${target} PRIVATE |
| 136 | + "--shell-file=${CONFIGURED_SHELL}" |
| 137 | + ) |
| 138 | + |
| 139 | + # Add both template and configured file as link dependencies |
| 140 | + set_property(TARGET ${target} APPEND PROPERTY LINK_DEPENDS |
| 141 | + "${cons_expr_SHELL_TEMPLATE}" |
| 142 | + "${CONFIGURED_SHELL}" |
| 143 | + ) |
| 144 | + |
| 145 | + message(STATUS "Configured WASM shell for ${target}: ${CONFIGURED_SHELL}") |
| 146 | + else() |
| 147 | + message(FATAL_ERROR "Shell template not found: ${cons_expr_SHELL_TEMPLATE}") |
| 148 | + endif() |
| 149 | + |
| 150 | + # Copy service worker to target build directory for standalone target builds |
| 151 | + if(EXISTS "${cons_expr_COI_WORKER}") |
| 152 | + add_custom_command(TARGET ${target} POST_BUILD |
| 153 | + COMMAND ${CMAKE_COMMAND} -E copy_if_different |
| 154 | + "${cons_expr_COI_WORKER}" |
| 155 | + "$<TARGET_FILE_DIR:${target}>/coi-serviceworker.min.js" |
| 156 | + COMMENT "Copying coi-serviceworker.min.js to ${target} build directory" |
| 157 | + ) |
| 158 | + endif() |
| 159 | + |
| 160 | + # Set output suffix to .html |
| 161 | + set_target_properties(${target} PROPERTIES SUFFIX ".html") |
| 162 | + |
| 163 | + message(STATUS "Configured ${target} for WebAssembly") |
| 164 | + endif() |
| 165 | +endfunction() |
| 166 | + |
| 167 | +# Create a unified web deployment directory with all WASM targets |
| 168 | +function(cons_expr_create_web_dist) |
| 169 | + if(NOT EMSCRIPTEN) |
| 170 | + return() |
| 171 | + endif() |
| 172 | + |
| 173 | + # Define output directory |
| 174 | + set(WEB_DIST_DIR "${CMAKE_BINARY_DIR}/web-dist") |
| 175 | + |
| 176 | + # Get list of all WASM targets |
| 177 | + get_property(WASM_TARGETS GLOBAL PROPERTY cons_expr_WASM_TARGETS) |
| 178 | + |
| 179 | + if(NOT WASM_TARGETS) |
| 180 | + message(WARNING "No WASM targets registered. Skipping web-dist generation.") |
| 181 | + return() |
| 182 | + endif() |
| 183 | + |
| 184 | + # Generate HTML for app cards |
| 185 | + set(WASM_APPS_HTML "") |
| 186 | + foreach(target ${WASM_TARGETS}) |
| 187 | + get_property(TITLE GLOBAL PROPERTY cons_expr_WASM_TARGET_${target}_TITLE) |
| 188 | + get_property(DESCRIPTION GLOBAL PROPERTY cons_expr_WASM_TARGET_${target}_DESCRIPTION) |
| 189 | + |
| 190 | + # Escape HTML special characters to prevent injection |
| 191 | + escape_html(TITLE_ESCAPED "${TITLE}") |
| 192 | + escape_html(DESC_ESCAPED "${DESCRIPTION}") |
| 193 | + |
| 194 | + string(APPEND WASM_APPS_HTML |
| 195 | +" <a href=\"${target}/\" class=\"app-card\"> |
| 196 | + <h3>${TITLE_ESCAPED}</h3> |
| 197 | + <p>${DESC_ESCAPED}</p> |
| 198 | + </a> |
| 199 | +") |
| 200 | + endforeach() |
| 201 | + |
| 202 | + # Generate index.html from template |
| 203 | + set(INDEX_OUTPUT "${WEB_DIST_DIR}/index.html") |
| 204 | + |
| 205 | + if(EXISTS "${cons_expr_INDEX_TEMPLATE}") |
| 206 | + configure_file("${cons_expr_INDEX_TEMPLATE}" "${INDEX_OUTPUT}" @ONLY) |
| 207 | + else() |
| 208 | + message(WARNING "Index template not found: ${cons_expr_INDEX_TEMPLATE}") |
| 209 | + endif() |
| 210 | + |
| 211 | + # Build list of copy commands |
| 212 | + set(COPY_COMMANDS "") |
| 213 | + |
| 214 | + # For each WASM target, copy artifacts to subdirectory |
| 215 | + # Each target gets its own service worker copy for standalone deployment |
| 216 | + foreach(target ${WASM_TARGETS}) |
| 217 | + get_target_property(TARGET_BINARY_DIR ${target} BINARY_DIR) |
| 218 | + get_property(OUTPUT_NAME GLOBAL PROPERTY cons_expr_WASM_TARGET_${target}_OUTPUT_NAME) |
| 219 | + set(TARGET_DIST_DIR "${WEB_DIST_DIR}/${target}") |
| 220 | + |
| 221 | + # Copy WASM artifacts: .html (as index.html), .js, .wasm, and service worker |
| 222 | + # Use OUTPUT_NAME instead of target name for file names |
| 223 | + list(APPEND COPY_COMMANDS |
| 224 | + COMMAND ${CMAKE_COMMAND} -E make_directory "${TARGET_DIST_DIR}" |
| 225 | + COMMAND ${CMAKE_COMMAND} -E copy_if_different |
| 226 | + "${TARGET_BINARY_DIR}/${OUTPUT_NAME}.html" |
| 227 | + "${TARGET_DIST_DIR}/index.html" |
| 228 | + COMMAND ${CMAKE_COMMAND} -E copy_if_different |
| 229 | + "${TARGET_BINARY_DIR}/${OUTPUT_NAME}.js" |
| 230 | + "${TARGET_DIST_DIR}/${OUTPUT_NAME}.js" |
| 231 | + COMMAND ${CMAKE_COMMAND} -E copy_if_different |
| 232 | + "${TARGET_BINARY_DIR}/${OUTPUT_NAME}.wasm" |
| 233 | + "${TARGET_DIST_DIR}/${OUTPUT_NAME}.wasm" |
| 234 | + COMMAND ${CMAKE_COMMAND} -E copy_if_different |
| 235 | + "${cons_expr_COI_WORKER}" |
| 236 | + "${TARGET_DIST_DIR}/coi-serviceworker.min.js" |
| 237 | + ) |
| 238 | + endforeach() |
| 239 | + |
| 240 | + # Create custom target with all commands (part of ALL so it builds by default) |
| 241 | + add_custom_target(web-dist ALL |
| 242 | + COMMAND ${CMAKE_COMMAND} -E make_directory "${WEB_DIST_DIR}" |
| 243 | + ${COPY_COMMANDS} |
| 244 | + COMMENT "Creating unified web deployment directory" |
| 245 | + ) |
| 246 | + |
| 247 | + # Ensure web-dist runs after all WASM targets are built |
| 248 | + add_dependencies(web-dist ${WASM_TARGETS}) |
| 249 | + |
| 250 | + message(STATUS "Configured web-dist target with ${WASM_TARGETS}") |
| 251 | +endfunction() |
0 commit comments