|
| 1 | +# cmake/Emscripten.cmake |
| 2 | +# Emscripten/WebAssembly build configuration |
| 3 | + |
| 4 | +# Common paths for web assets |
| 5 | +set(myproject_WEB_DIR "${CMAKE_SOURCE_DIR}/web") |
| 6 | +set(myproject_COI_WORKER "${myproject_WEB_DIR}/coi-serviceworker.min.js") |
| 7 | +set(myproject_SHELL_TEMPLATE "${myproject_WEB_DIR}/shell_template.html.in") |
| 8 | +set(myproject_INDEX_TEMPLATE "${myproject_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(myproject_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(myproject_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(myproject_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(myproject_WASM_INITIAL_MEMORY "33554432" CACHE STRING |
| 42 | + "Initial WASM memory in bytes (default: 32MB)") |
| 43 | + set(myproject_WASM_PTHREAD_POOL_SIZE "4" CACHE STRING |
| 44 | + "Pthread pool size for WASM builds (default: 4)") |
| 45 | + set(myproject_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(myproject_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 | + # Register this target in the global WASM targets list |
| 73 | + set_property(GLOBAL APPEND PROPERTY myproject_WASM_TARGETS "${target}") |
| 74 | + set_property(GLOBAL PROPERTY myproject_WASM_TARGET_${target}_TITLE "${WASM_TITLE}") |
| 75 | + set_property(GLOBAL PROPERTY myproject_WASM_TARGET_${target}_DESCRIPTION "${WASM_DESCRIPTION}") |
| 76 | + |
| 77 | + target_compile_definitions(${target} PRIVATE myproject_WASM_BUILD=1) |
| 78 | + |
| 79 | + # Emscripten link flags |
| 80 | + target_link_options(${target} PRIVATE |
| 81 | + # Enable pthreads - REQUIRED by FTXUI's WASM implementation |
| 82 | + "-sUSE_PTHREADS=1" |
| 83 | + "-sPROXY_TO_PTHREAD=1" |
| 84 | + "-sPTHREAD_POOL_SIZE=${myproject_WASM_PTHREAD_POOL_SIZE}" |
| 85 | + # Enable asyncify for emscripten_sleep and async operations |
| 86 | + "-sASYNCIFY=1" |
| 87 | + "-sASYNCIFY_STACK_SIZE=${myproject_WASM_ASYNCIFY_STACK_SIZE}" |
| 88 | + # Memory configuration |
| 89 | + "-sALLOW_MEMORY_GROWTH=1" |
| 90 | + "-sINITIAL_MEMORY=${myproject_WASM_INITIAL_MEMORY}" |
| 91 | + # Environment - need both web and worker for pthread support |
| 92 | + "-sENVIRONMENT=web,worker" |
| 93 | + # Export runtime methods for JavaScript interop |
| 94 | + "-sEXPORTED_RUNTIME_METHODS=['FS','ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8']" |
| 95 | + # Export malloc/free for MAIN_THREAD_EM_ASM usage |
| 96 | + "-sEXPORTED_FUNCTIONS=['_main','_malloc','_free']" |
| 97 | + # Debug: enable assertions for better error messages |
| 98 | + "-sASSERTIONS=1" |
| 99 | + ) |
| 100 | + |
| 101 | + # Embed resources into WASM binary (optional, per-target) |
| 102 | + if(WASM_RESOURCES_DIR AND EXISTS "${WASM_RESOURCES_DIR}") |
| 103 | + # Convert to absolute path to avoid issues with Emscripten path resolution |
| 104 | + get_filename_component(ABS_RESOURCES_DIR "${WASM_RESOURCES_DIR}" ABSOLUTE BASE_DIR "${CMAKE_SOURCE_DIR}") |
| 105 | + |
| 106 | + target_link_options(${target} PRIVATE |
| 107 | + "--embed-file=${ABS_RESOURCES_DIR}@/resources" |
| 108 | + ) |
| 109 | + message(STATUS "Embedding resources for ${target} from ${ABS_RESOURCES_DIR}") |
| 110 | + endif() |
| 111 | + |
| 112 | + # Configure the shell HTML template for this target |
| 113 | + set(TARGET_NAME "${target}") |
| 114 | + set(TARGET_TITLE "${WASM_TITLE}") |
| 115 | + set(TARGET_DESCRIPTION "${WASM_DESCRIPTION}") |
| 116 | + set(AT "@") # For escaping @ in npm package URLs |
| 117 | + set(CONFIGURED_SHELL "${CMAKE_BINARY_DIR}/web/${target}_shell.html") |
| 118 | + |
| 119 | + # Generate target-specific shell file (configure_file creates parent directories automatically) |
| 120 | + if(EXISTS "${myproject_SHELL_TEMPLATE}") |
| 121 | + configure_file( |
| 122 | + "${myproject_SHELL_TEMPLATE}" |
| 123 | + "${CONFIGURED_SHELL}" |
| 124 | + @ONLY |
| 125 | + ) |
| 126 | + |
| 127 | + # Use the generated shell file |
| 128 | + target_link_options(${target} PRIVATE |
| 129 | + "--shell-file=${CONFIGURED_SHELL}" |
| 130 | + ) |
| 131 | + |
| 132 | + # Add both template and configured file as link dependencies |
| 133 | + set_property(TARGET ${target} APPEND PROPERTY LINK_DEPENDS |
| 134 | + "${myproject_SHELL_TEMPLATE}" |
| 135 | + "${CONFIGURED_SHELL}" |
| 136 | + ) |
| 137 | + |
| 138 | + message(STATUS "Configured WASM shell for ${target}: ${CONFIGURED_SHELL}") |
| 139 | + else() |
| 140 | + message(FATAL_ERROR "Shell template not found: ${myproject_SHELL_TEMPLATE}") |
| 141 | + endif() |
| 142 | + |
| 143 | + # Copy service worker to target build directory for standalone target builds |
| 144 | + if(EXISTS "${myproject_COI_WORKER}") |
| 145 | + add_custom_command(TARGET ${target} POST_BUILD |
| 146 | + COMMAND ${CMAKE_COMMAND} -E copy_if_different |
| 147 | + "${myproject_COI_WORKER}" |
| 148 | + "$<TARGET_FILE_DIR:${target}>/coi-serviceworker.min.js" |
| 149 | + COMMENT "Copying coi-serviceworker.min.js to ${target} build directory" |
| 150 | + ) |
| 151 | + endif() |
| 152 | + |
| 153 | + # Set output suffix to .html |
| 154 | + set_target_properties(${target} PROPERTIES SUFFIX ".html") |
| 155 | + |
| 156 | + message(STATUS "Configured ${target} for WebAssembly") |
| 157 | + endif() |
| 158 | +endfunction() |
| 159 | + |
| 160 | +# Create a unified web deployment directory with all WASM targets |
| 161 | +function(myproject_create_web_dist) |
| 162 | + if(NOT EMSCRIPTEN) |
| 163 | + return() |
| 164 | + endif() |
| 165 | + |
| 166 | + # Define output directory |
| 167 | + set(WEB_DIST_DIR "${CMAKE_BINARY_DIR}/web-dist") |
| 168 | + |
| 169 | + # Get list of all WASM targets |
| 170 | + get_property(WASM_TARGETS GLOBAL PROPERTY myproject_WASM_TARGETS) |
| 171 | + |
| 172 | + if(NOT WASM_TARGETS) |
| 173 | + message(WARNING "No WASM targets registered. Skipping web-dist generation.") |
| 174 | + return() |
| 175 | + endif() |
| 176 | + |
| 177 | + # Generate HTML for app cards |
| 178 | + set(WASM_APPS_HTML "") |
| 179 | + foreach(target ${WASM_TARGETS}) |
| 180 | + get_property(TITLE GLOBAL PROPERTY myproject_WASM_TARGET_${target}_TITLE) |
| 181 | + get_property(DESCRIPTION GLOBAL PROPERTY myproject_WASM_TARGET_${target}_DESCRIPTION) |
| 182 | + |
| 183 | + # Escape HTML special characters to prevent injection |
| 184 | + escape_html(TITLE_ESCAPED "${TITLE}") |
| 185 | + escape_html(DESC_ESCAPED "${DESCRIPTION}") |
| 186 | + |
| 187 | + string(APPEND WASM_APPS_HTML |
| 188 | +" <a href=\"${target}/\" class=\"app-card\"> |
| 189 | + <div class=\"app-title\">${TITLE_ESCAPED}</div> |
| 190 | + <div class=\"app-description\">${DESC_ESCAPED}</div> |
| 191 | + </a> |
| 192 | +") |
| 193 | + endforeach() |
| 194 | + |
| 195 | + # Generate index.html from template |
| 196 | + set(INDEX_OUTPUT "${WEB_DIST_DIR}/index.html") |
| 197 | + |
| 198 | + if(EXISTS "${myproject_INDEX_TEMPLATE}") |
| 199 | + configure_file("${myproject_INDEX_TEMPLATE}" "${INDEX_OUTPUT}" @ONLY) |
| 200 | + else() |
| 201 | + message(WARNING "Index template not found: ${myproject_INDEX_TEMPLATE}") |
| 202 | + endif() |
| 203 | + |
| 204 | + # Build list of copy commands |
| 205 | + set(COPY_COMMANDS "") |
| 206 | + |
| 207 | + # For each WASM target, copy artifacts to subdirectory |
| 208 | + # Each target gets its own service worker copy for standalone deployment |
| 209 | + foreach(target ${WASM_TARGETS}) |
| 210 | + get_target_property(TARGET_BINARY_DIR ${target} BINARY_DIR) |
| 211 | + set(TARGET_DIST_DIR "${WEB_DIST_DIR}/${target}") |
| 212 | + |
| 213 | + # Copy WASM artifacts: .html (as index.html), .js, .wasm, and service worker |
| 214 | + list(APPEND COPY_COMMANDS |
| 215 | + COMMAND ${CMAKE_COMMAND} -E make_directory "${TARGET_DIST_DIR}" |
| 216 | + COMMAND ${CMAKE_COMMAND} -E copy_if_different |
| 217 | + "${TARGET_BINARY_DIR}/${target}.html" |
| 218 | + "${TARGET_DIST_DIR}/index.html" |
| 219 | + COMMAND ${CMAKE_COMMAND} -E copy_if_different |
| 220 | + "${TARGET_BINARY_DIR}/${target}.js" |
| 221 | + "${TARGET_DIST_DIR}/${target}.js" |
| 222 | + COMMAND ${CMAKE_COMMAND} -E copy_if_different |
| 223 | + "${TARGET_BINARY_DIR}/${target}.wasm" |
| 224 | + "${TARGET_DIST_DIR}/${target}.wasm" |
| 225 | + COMMAND ${CMAKE_COMMAND} -E copy_if_different |
| 226 | + "${myproject_COI_WORKER}" |
| 227 | + "${TARGET_DIST_DIR}/coi-serviceworker.min.js" |
| 228 | + ) |
| 229 | + endforeach() |
| 230 | + |
| 231 | + # Create custom target with all commands (part of ALL so it builds by default) |
| 232 | + add_custom_target(web-dist ALL |
| 233 | + COMMAND ${CMAKE_COMMAND} -E make_directory "${WEB_DIST_DIR}" |
| 234 | + ${COPY_COMMANDS} |
| 235 | + COMMENT "Creating unified web deployment directory" |
| 236 | + ) |
| 237 | + |
| 238 | + # Ensure web-dist runs after all WASM targets are built |
| 239 | + add_dependencies(web-dist ${WASM_TARGETS}) |
| 240 | + |
| 241 | + message(STATUS "Configured web-dist target with ${WASM_TARGETS}") |
| 242 | +endfunction() |
0 commit comments