Skip to content

Commit aec7db5

Browse files
committed
Add emscripten capability from cmake_template project
1 parent 8266192 commit aec7db5

File tree

10 files changed

+751
-33
lines changed

10 files changed

+751
-33
lines changed

CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ project(
2323
LANGUAGES CXX C)
2424

2525
include(cmake/PreventInSourceBuilds.cmake)
26+
include(cmake/Emscripten.cmake)
2627
include(ProjectOptions.cmake)
2728

2829

@@ -67,6 +68,11 @@ add_subdirectory(src)
6768

6869
add_subdirectory(examples)
6970

71+
# Create unified web deployment directory (for WASM builds)
72+
if(EMSCRIPTEN)
73+
cons_expr_create_web_dist()
74+
endif()
75+
7076
# Don't even look at tests if we're not top level
7177
if(NOT PROJECT_IS_TOP_LEVEL)
7278
return()

ProjectOptions.cmake

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,57 @@ include(CMakeDependentOption)
44
include(CheckCXXCompilerFlag)
55

66

7+
include(CheckCXXSourceCompiles)
8+
9+
710
macro(cons_expr_supports_sanitizers)
8-
if((CMAKE_CXX_COMPILER_ID MATCHES ".*Clang.*" OR CMAKE_CXX_COMPILER_ID MATCHES ".*GNU.*") AND NOT WIN32)
9-
set(SUPPORTS_UBSAN ON)
11+
# Emscripten doesn't support sanitizers
12+
if(EMSCRIPTEN)
13+
set(SUPPORTS_UBSAN OFF)
14+
set(SUPPORTS_ASAN OFF)
15+
elseif((CMAKE_CXX_COMPILER_ID MATCHES ".*Clang.*" OR CMAKE_CXX_COMPILER_ID MATCHES ".*GNU.*") AND NOT WIN32)
16+
17+
message(STATUS "Sanity checking UndefinedBehaviorSanitizer, it should be supported on this platform")
18+
set(TEST_PROGRAM "int main() { return 0; }")
19+
20+
# Check if UndefinedBehaviorSanitizer works at link time
21+
set(CMAKE_REQUIRED_FLAGS "-fsanitize=undefined")
22+
set(CMAKE_REQUIRED_LINK_OPTIONS "-fsanitize=undefined")
23+
check_cxx_source_compiles("${TEST_PROGRAM}" HAS_UBSAN_LINK_SUPPORT)
24+
25+
if(HAS_UBSAN_LINK_SUPPORT)
26+
message(STATUS "UndefinedBehaviorSanitizer is supported at both compile and link time.")
27+
set(SUPPORTS_UBSAN ON)
28+
else()
29+
message(WARNING "UndefinedBehaviorSanitizer is NOT supported at link time.")
30+
set(SUPPORTS_UBSAN OFF)
31+
endif()
1032
else()
1133
set(SUPPORTS_UBSAN OFF)
1234
endif()
1335

1436
if((CMAKE_CXX_COMPILER_ID MATCHES ".*Clang.*" OR CMAKE_CXX_COMPILER_ID MATCHES ".*GNU.*") AND WIN32)
1537
set(SUPPORTS_ASAN OFF)
1638
else()
17-
set(SUPPORTS_ASAN ON)
39+
if (NOT WIN32)
40+
message(STATUS "Sanity checking AddressSanitizer, it should be supported on this platform")
41+
set(TEST_PROGRAM "int main() { return 0; }")
42+
43+
# Check if AddressSanitizer works at link time
44+
set(CMAKE_REQUIRED_FLAGS "-fsanitize=address")
45+
set(CMAKE_REQUIRED_LINK_OPTIONS "-fsanitize=address")
46+
check_cxx_source_compiles("${TEST_PROGRAM}" HAS_ASAN_LINK_SUPPORT)
47+
48+
if(HAS_ASAN_LINK_SUPPORT)
49+
message(STATUS "AddressSanitizer is supported at both compile and link time.")
50+
set(SUPPORTS_ASAN ON)
51+
else()
52+
message(WARNING "AddressSanitizer is NOT supported at link time.")
53+
set(SUPPORTS_ASAN OFF)
54+
endif()
55+
else()
56+
set(SUPPORTS_ASAN ON)
57+
endif()
1858
endif()
1959
endmacro()
2060

@@ -54,8 +94,8 @@ macro(cons_expr_setup_options)
5494
option(cons_expr_ENABLE_SANITIZER_THREAD "Enable thread sanitizer" OFF)
5595
option(cons_expr_ENABLE_SANITIZER_MEMORY "Enable memory sanitizer" OFF)
5696
option(cons_expr_ENABLE_UNITY_BUILD "Enable unity builds" OFF)
57-
option(cons_expr_ENABLE_CLANG_TIDY "Enable clang-tidy" OFF)
58-
option(cons_expr_ENABLE_CPPCHECK "Enable cpp-check analysis" OFF)
97+
option(cons_expr_ENABLE_CLANG_TIDY "Enable clang-tidy" ON)
98+
option(cons_expr_ENABLE_CPPCHECK "Enable cpp-check analysis" ON)
5999
option(cons_expr_ENABLE_PCH "Enable precompiled headers" OFF)
60100
option(cons_expr_ENABLE_CACHE "Enable ccache" ON)
61101
endif()
@@ -130,19 +170,22 @@ macro(cons_expr_local_options)
130170
""
131171
"")
132172

133-
if(cons_expr_ENABLE_USER_LINKER)
134-
include(cmake/Linker.cmake)
135-
configure_linker(cons_expr_options)
136-
endif()
173+
# Linker and sanitizers not supported in Emscripten
174+
if(NOT EMSCRIPTEN)
175+
if(cons_expr_ENABLE_USER_LINKER)
176+
include(cmake/Linker.cmake)
177+
cons_expr_configure_linker(cons_expr_options)
178+
endif()
137179

138-
include(cmake/Sanitizers.cmake)
139-
cons_expr_enable_sanitizers(
140-
cons_expr_options
141-
${cons_expr_ENABLE_SANITIZER_ADDRESS}
142-
${cons_expr_ENABLE_SANITIZER_LEAK}
143-
${cons_expr_ENABLE_SANITIZER_UNDEFINED}
144-
${cons_expr_ENABLE_SANITIZER_THREAD}
145-
${cons_expr_ENABLE_SANITIZER_MEMORY})
180+
include(cmake/Sanitizers.cmake)
181+
cons_expr_enable_sanitizers(
182+
cons_expr_options
183+
${cons_expr_ENABLE_SANITIZER_ADDRESS}
184+
${cons_expr_ENABLE_SANITIZER_LEAK}
185+
${cons_expr_ENABLE_SANITIZER_UNDEFINED}
186+
${cons_expr_ENABLE_SANITIZER_THREAD}
187+
${cons_expr_ENABLE_SANITIZER_MEMORY})
188+
endif()
146189

147190
set_target_properties(cons_expr_options PROPERTIES UNITY_BUILD ${cons_expr_ENABLE_UNITY_BUILD})
148191

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,15 @@
3232
* For C++23
3333
* Currently only known to work with GCC 13.1.
3434

35+
* WebAssembly build support with automatic GitHub Pages deployment
3536

37+
**Live Demo:** If you enable GitHub Pages in your project created from this template, you'll have a working example like this:
38+
39+
- Main: [https://cpp-best-practices.github.io/cmake_template/](https://cpp-best-practices.github.io/cmake_template/)
40+
- Develop: [https://cpp-best-practices.github.io/cmake_template/develop/](https://cpp-best-practices.github.io/cmake_template/develop/)
41+
42+
The `main` branch deploys to the root, `develop` to `/develop/`, and tags to `/tagname/`.
43+
3644
## Command Line Inspection Tool
3745

3846
`ccons_expr` can be used to execute scripts and inspect the state of the runtime system live

cmake/Emscripten.cmake

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
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 "<" "&lt;" result "${result}")
15+
string(REPLACE ">" "&gt;" result "${result}")
16+
string(REPLACE "\"" "&quot;" 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

Comments
 (0)