From 04a0ffca42edeea4c71dd32e1411247a68baa4bc Mon Sep 17 00:00:00 2001 From: Jason Turner Date: Tue, 30 Dec 2025 18:36:39 -0700 Subject: [PATCH 01/14] Add WebAssembly build infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port WASM infrastructure from travels project to enable building the intro executable for web browsers using Emscripten. Core WASM infrastructure: - cmake/Emscripten.cmake: Emscripten/WASM build configuration - Detects Emscripten builds and disables incompatible features - Configures pthread support (required by FTXUI) - Provides myproject_configure_wasm_target() function - Optional resource embedding support via INTRO_RESOURCES_DIR - web/shell.html: Custom HTML shell with xterm.js terminal - Dark-themed terminal UI with WebGL rendering - Handles stdin/stdout/stderr buffering for WASM - Supports ?version URL parameter - web/coi-serviceworker.min.js: Service worker for COOP/COEP headers - Required for SharedArrayBuffer/pthread support on GitHub Pages - .github/workflows/wasm.yml: CI/CD workflow - Builds WASM on all CI runs - Deploys to GitHub Pages for main/develop branches and tags CMake integration: - CMakeLists.txt: Include Emscripten.cmake before ProjectOptions - ProjectOptions.cmake: Add EMSCRIPTEN detection to disable sanitizers - src/ftxui_sample/CMakeLists.txt: Apply WASM configuration to intro Critical bug fixes included: - cmake/Hardening.cmake: Fix CMake list handling and global flag propagation (proper add_compile_options() usage) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/wasm.yml | 61 +++++++ CMakeLists.txt | 1 + ProjectOptions.cmake | 33 ++-- cmake/Emscripten.cmake | 109 +++++++++++++ cmake/Hardening.cmake | 6 +- src/ftxui_sample/CMakeLists.txt | 5 + web/coi-serviceworker.min.js | 2 + web/shell.html | 272 ++++++++++++++++++++++++++++++++ 8 files changed, 473 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/wasm.yml create mode 100644 cmake/Emscripten.cmake create mode 100644 web/coi-serviceworker.min.js create mode 100644 web/shell.html diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml new file mode 100644 index 00000000..39616e3f --- /dev/null +++ b/.github/workflows/wasm.yml @@ -0,0 +1,61 @@ +name: Build Intro WASM and Deploy to GitHub Pages + +on: + pull_request: + release: + types: [published] + push: + branches: [main, develop] + tags: ['**'] + +permissions: + contents: write + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Emscripten + uses: mymindstorm/setup-emsdk@v14 + with: + version: 'latest' + + - name: Install Ninja + run: sudo apt-get install -y ninja-build + + - name: Configure CMake + run: emcmake cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: emmake cmake --build build --target intro + + - name: Prepare artifacts + run: | + mkdir -p dist + cp build/src/ftxui_sample/intro.html dist/index.html + cp build/src/ftxui_sample/intro.js dist/ + cp build/src/ftxui_sample/intro.wasm dist/ + cp build/src/ftxui_sample/coi-serviceworker.min.js dist/ + + - name: Determine deploy path + id: deploy-path + if: github.event_name != 'pull_request' && github.event_name != 'release' + run: | + if [[ "$GITHUB_REF" == refs/tags/* ]]; then + echo "path=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + elif [[ "$GITHUB_REF" == refs/heads/main ]]; then + echo "path=." >> $GITHUB_OUTPUT + elif [[ "$GITHUB_REF" == refs/heads/develop ]]; then + echo "path=develop" >> $GITHUB_OUTPUT + fi + + - name: Deploy to GitHub Pages + if: github.event_name != 'pull_request' && github.event_name != 'release' + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist + destination_dir: ${{ steps.deploy-path.outputs.path }} + keep_files: true diff --git a/CMakeLists.txt b/CMakeLists.txt index a5ac5435..83bcb2dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ project( LANGUAGES CXX C) include(cmake/PreventInSourceBuilds.cmake) +include(cmake/Emscripten.cmake) include(ProjectOptions.cmake) diff --git a/ProjectOptions.cmake b/ProjectOptions.cmake index 3d260944..fcd472bd 100644 --- a/ProjectOptions.cmake +++ b/ProjectOptions.cmake @@ -8,7 +8,11 @@ include(CheckCXXSourceCompiles) macro(myproject_supports_sanitizers) - if((CMAKE_CXX_COMPILER_ID MATCHES ".*Clang.*" OR CMAKE_CXX_COMPILER_ID MATCHES ".*GNU.*") AND NOT WIN32) + # Emscripten doesn't support sanitizers + if(EMSCRIPTEN) + set(SUPPORTS_UBSAN OFF) + set(SUPPORTS_ASAN OFF) + elseif((CMAKE_CXX_COMPILER_ID MATCHES ".*Clang.*" OR CMAKE_CXX_COMPILER_ID MATCHES ".*GNU.*") AND NOT WIN32) message(STATUS "Sanity checking UndefinedBehaviorSanitizer, it should be supported on this platform") set(TEST_PROGRAM "int main() { return 0; }") @@ -166,19 +170,22 @@ macro(myproject_local_options) "" "") - if(myproject_ENABLE_USER_LINKER) - include(cmake/Linker.cmake) - myproject_configure_linker(myproject_options) - endif() + # Linker and sanitizers not supported in Emscripten + if(NOT EMSCRIPTEN) + if(myproject_ENABLE_USER_LINKER) + include(cmake/Linker.cmake) + myproject_configure_linker(myproject_options) + endif() - include(cmake/Sanitizers.cmake) - myproject_enable_sanitizers( - myproject_options - ${myproject_ENABLE_SANITIZER_ADDRESS} - ${myproject_ENABLE_SANITIZER_LEAK} - ${myproject_ENABLE_SANITIZER_UNDEFINED} - ${myproject_ENABLE_SANITIZER_THREAD} - ${myproject_ENABLE_SANITIZER_MEMORY}) + include(cmake/Sanitizers.cmake) + myproject_enable_sanitizers( + myproject_options + ${myproject_ENABLE_SANITIZER_ADDRESS} + ${myproject_ENABLE_SANITIZER_LEAK} + ${myproject_ENABLE_SANITIZER_UNDEFINED} + ${myproject_ENABLE_SANITIZER_THREAD} + ${myproject_ENABLE_SANITIZER_MEMORY}) + endif() set_target_properties(myproject_options PROPERTIES UNITY_BUILD ${myproject_ENABLE_UNITY_BUILD}) diff --git a/cmake/Emscripten.cmake b/cmake/Emscripten.cmake new file mode 100644 index 00000000..6e580329 --- /dev/null +++ b/cmake/Emscripten.cmake @@ -0,0 +1,109 @@ +# cmake/Emscripten.cmake +# Emscripten/WebAssembly build configuration + +# Detect if we're building with Emscripten +if(EMSCRIPTEN) + message(STATUS "Emscripten build detected - configuring for WebAssembly") + + # Set WASM build flag + set(MYPROJECT_WASM_BUILD ON CACHE BOOL "Building for WebAssembly" FORCE) + + # Sanitizers don't work with Emscripten + set(myproject_ENABLE_SANITIZER_ADDRESS OFF CACHE BOOL "Not supported with Emscripten") + set(myproject_ENABLE_SANITIZER_LEAK OFF CACHE BOOL "Not supported with Emscripten") + set(myproject_ENABLE_SANITIZER_UNDEFINED OFF CACHE BOOL "Not supported with Emscripten") + set(myproject_ENABLE_SANITIZER_THREAD OFF CACHE BOOL "Not supported with Emscripten") + set(myproject_ENABLE_SANITIZER_MEMORY OFF CACHE BOOL "Not supported with Emscripten") + + # Disable static analysis and strict warnings for Emscripten builds + set(myproject_ENABLE_CLANG_TIDY OFF CACHE BOOL "Disabled for Emscripten") + set(myproject_ENABLE_CPPCHECK OFF CACHE BOOL "Disabled for Emscripten") + set(myproject_WARNINGS_AS_ERRORS OFF CACHE BOOL "Disabled for Emscripten") + + # Disable testing - no way to execute WASM test targets + set(BUILD_TESTING OFF CACHE BOOL "No test runner for WASM") + + # Resource embedding path (optional) + set(INTRO_RESOURCES_DIR "" CACHE PATH "Resources directory (optional)") + + # For Emscripten WASM builds, FTXUI requires pthreads + # Set these flags early so they propagate to all dependencies + + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pthread") + + # Enable native WebAssembly exception handling + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fwasm-exceptions") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fwasm-exceptions") + + +else() + set(MYPROJECT_WASM_BUILD OFF CACHE BOOL "Building for WebAssembly" FORCE) +endif() + +# Function to apply WASM settings to a target +function(myproject_configure_wasm_target target) + if(EMSCRIPTEN) + target_compile_definitions(${target} PRIVATE MYPROJECT_WASM_BUILD=1) + + # Emscripten link flags + target_link_options(${target} PRIVATE + # Enable pthreads - REQUIRED by FTXUI's WASM implementation + "-sUSE_PTHREADS=1" + "-sPROXY_TO_PTHREAD=1" + "-sPTHREAD_POOL_SIZE=4" + # Enable asyncify for emscripten_sleep and async operations + "-sASYNCIFY=1" + "-sASYNCIFY_STACK_SIZE=65536" + # Memory configuration + "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=33554432" + # Environment - need both web and worker for pthread support + "-sENVIRONMENT=web,worker" + # Export runtime methods for JavaScript interop + "-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8']" + # Export malloc/free for MAIN_THREAD_EM_ASM usage + "-sEXPORTED_FUNCTIONS=['_main','_malloc','_free']" + # Enable native WebAssembly exception handling + "-fwasm-exceptions" + # Debug: enable assertions for better error messages + "-sASSERTIONS=1" + ) + + # Embed resources into WASM binary (optional) + if(INTRO_RESOURCES_DIR AND EXISTS "${INTRO_RESOURCES_DIR}") + target_link_options(${target} PRIVATE + "--embed-file=${INTRO_RESOURCES_DIR}@/resources" + ) + message(STATUS "Embedding resources from ${INTRO_RESOURCES_DIR}") + else() + message(STATUS "No resources directory configured, skipping resource embedding") + endif() + + # Use custom HTML shell if it exists + set(SHELL_FILE "${CMAKE_SOURCE_DIR}/web/shell.html") + if(EXISTS "${SHELL_FILE}") + target_link_options(${target} PRIVATE + "--shell-file=${SHELL_FILE}" + ) + # Add shell file as a link dependency so changes trigger rebuild + set_property(TARGET ${target} APPEND PROPERTY LINK_DEPENDS "${SHELL_FILE}") + endif() + + # Copy service worker for COOP/COEP headers (needed for GitHub Pages) + set(COI_WORKER "${CMAKE_SOURCE_DIR}/web/coi-serviceworker.min.js") + if(EXISTS "${COI_WORKER}") + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${COI_WORKER}" + "$/coi-serviceworker.min.js" + COMMENT "Copying coi-serviceworker.min.js for COOP/COEP headers" + ) + endif() + + # Set output suffix to .html + set_target_properties(${target} PROPERTIES SUFFIX ".html") + + message(STATUS "Configured ${target} for WebAssembly") + endif() +endfunction() diff --git a/cmake/Hardening.cmake b/cmake/Hardening.cmake index fc8d0e1a..e14d51b2 100644 --- a/cmake/Hardening.cmake +++ b/cmake/Hardening.cmake @@ -90,9 +90,9 @@ macro( if(${global}) message(STATUS "** Setting hardening options globally for all dependencies") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${NEW_COMPILE_OPTIONS}") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${NEW_LINK_OPTIONS}") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${NEW_CXX_DEFINITIONS}") + add_compile_options(${NEW_COMPILE_OPTIONS}) + add_compile_definitions(${NEW_CXX_DEFINITIONS}) + add_link_options(${NEW_LINK_OPTIONS}) else() target_compile_options(${target} INTERFACE ${NEW_COMPILE_OPTIONS}) target_link_options(${target} INTERFACE ${NEW_LINK_OPTIONS}) diff --git a/src/ftxui_sample/CMakeLists.txt b/src/ftxui_sample/CMakeLists.txt index fc5c65e2..0df51275 100644 --- a/src/ftxui_sample/CMakeLists.txt +++ b/src/ftxui_sample/CMakeLists.txt @@ -17,3 +17,8 @@ target_link_system_libraries( ftxui::component) target_include_directories(intro PRIVATE "${CMAKE_BINARY_DIR}/configured_files/include") + +# Apply WASM-specific configuration +if(EMSCRIPTEN) + myproject_configure_wasm_target(intro) +endif() diff --git a/web/coi-serviceworker.min.js b/web/coi-serviceworker.min.js new file mode 100644 index 00000000..33fddc0f --- /dev/null +++ b/web/coi-serviceworker.min.js @@ -0,0 +1,2 @@ +/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */ +let coepCredentialless=!1;"undefined"==typeof window?(self.addEventListener("install",(()=>self.skipWaiting())),self.addEventListener("activate",(e=>e.waitUntil(self.clients.claim()))),self.addEventListener("message",(e=>{e.data&&("deregister"===e.data.type?self.registration.unregister().then((()=>self.clients.matchAll())).then((e=>{e.forEach((e=>e.navigate(e.url)))})):"coepCredentialless"===e.data.type&&(coepCredentialless=e.data.value))})),self.addEventListener("fetch",(function(e){const o=e.request;if("only-if-cached"===o.cache&&"same-origin"!==o.mode)return;const s=coepCredentialless&&"no-cors"===o.mode?new Request(o,{credentials:"omit"}):o;e.respondWith(fetch(s).then((e=>{if(0===e.status)return e;const o=new Headers(e.headers);return o.set("Cross-Origin-Embedder-Policy",coepCredentialless?"credentialless":"require-corp"),coepCredentialless||o.set("Cross-Origin-Resource-Policy","cross-origin"),o.set("Cross-Origin-Opener-Policy","same-origin"),new Response(e.body,{status:e.status,statusText:e.statusText,headers:o})})).catch((e=>console.error(e))))}))):(()=>{const e=window.sessionStorage.getItem("coiReloadedBySelf");window.sessionStorage.removeItem("coiReloadedBySelf");const o="coepdegrade"==e,s={shouldRegister:()=>!e,shouldDeregister:()=>!1,coepCredentialless:()=>!0,coepDegrade:()=>!0,doReload:()=>window.location.reload(),quiet:!1,...window.coi},r=navigator,t=r.serviceWorker&&r.serviceWorker.controller;t&&!window.crossOriginIsolated&&window.sessionStorage.setItem("coiCoepHasFailed","true");const i=window.sessionStorage.getItem("coiCoepHasFailed");if(t){const e=s.coepDegrade()&&!(o||window.crossOriginIsolated);r.serviceWorker.controller.postMessage({type:"coepCredentialless",value:!(e||i&&s.coepDegrade())&&s.coepCredentialless()}),e&&(!s.quiet&&console.log("Reloading page to degrade COEP."),window.sessionStorage.setItem("coiReloadedBySelf","coepdegrade"),s.doReload("coepdegrade")),s.shouldDeregister()&&r.serviceWorker.controller.postMessage({type:"deregister"})}!1===window.crossOriginIsolated&&s.shouldRegister()&&(window.isSecureContext?r.serviceWorker?r.serviceWorker.register(window.document.currentScript.src).then((e=>{!s.quiet&&console.log("COOP/COEP Service Worker registered",e.scope),e.addEventListener("updatefound",(()=>{!s.quiet&&console.log("Reloading page to make use of updated COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","updatefound"),s.doReload()})),e.active&&!r.serviceWorker.controller&&(!s.quiet&&console.log("Reloading page to make use of COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","notcontrolling"),s.doReload())}),(e=>{!s.quiet&&console.error("COOP/COEP Service Worker failed to register:",e)})):!s.quiet&&console.error("COOP/COEP Service Worker not registered, perhaps due to private mode."):!s.quiet&&console.log("COOP/COEP Service Worker not registered, a secure context is required."))})(); diff --git a/web/shell.html b/web/shell.html new file mode 100644 index 00000000..a70d55eb --- /dev/null +++ b/web/shell.html @@ -0,0 +1,272 @@ + + + + + + Intro - FTXUI Sample + + + + + + + +
+

Intro

+ +
+
+

Loading application...

+

+
+ + + +
+
+ + + + + + {{{ SCRIPT }}} + + From 072266dd0e31849e651bb3700646cc74a2040472 Mon Sep 17 00:00:00 2001 From: Jason Turner Date: Tue, 30 Dec 2025 19:33:23 -0700 Subject: [PATCH 02/14] Improve URL parameter to CLI argument handling in shell.html MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Support any URL parameter as command-line argument - Smart prefix detection: single-char → -, multi-char → -- - Handle both boolean flags and parameters with values - Add visible usage guide on web page explaining conversion rules Examples: ?version → --version ?v → -v ?file=test.txt → --file test.txt ?verbose&config=app.cfg → --verbose --config app.cfg 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- web/shell.html | 82 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/web/shell.html b/web/shell.html index a70d55eb..20939d56 100644 --- a/web/shell.html +++ b/web/shell.html @@ -91,6 +91,31 @@

Intro

+ +
+

Command-Line Arguments via URL

+

Pass command-line arguments to the application using URL parameters:

+ +
+ Conversion Rules: +
    +
  • • Single-character params → short flags: ?v → -v
  • +
  • • Multi-character params → long flags: ?version → --version
  • +
  • • Params with values: ?file=test.txt → --file test.txt
  • +
  • • Multiple params: ?v&help → -v --help
  • +
+
+ +
+ Examples: +
    +
  • intro.html?version
  • +
  • intro.html?h
  • +
  • intro.html?file=data.txt
  • +
  • intro.html?verbose&config=settings.json
  • +
+
+
@@ -178,9 +203,20 @@

Intro

const args = []; const urlParams = new URLSearchParams(window.location.search); - // Support ?version flag - if (urlParams.has('version')) { - args.push('--version'); + // Convert all URL parameters to command-line arguments + for (const [key, value] of urlParams.entries()) { + // Single character params become short flags: ?v → -v + // Multi-character params become long flags: ?version → --version + const prefix = key.length === 1 ? '-' : '--'; + + if (value === '' || value === null) { + // Boolean flag without value: ?help → --help + args.push(prefix + key); + } else { + // Parameter with value: ?file=test.txt → --file test.txt + args.push(prefix + key); + args.push(value); + } } console.log('Module.arguments:', args); @@ -268,5 +304,45 @@

Intro

}); {{{ SCRIPT }}} + + From bbca6b62673167b9907ae4669dbe03773cdec1f4 Mon Sep 17 00:00:00 2001 From: Jason Turner Date: Tue, 30 Dec 2025 19:35:05 -0700 Subject: [PATCH 03/14] Upgrade xterm to 6.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update xterm from 5.3.0 to 6.0.0 - Update addon-fit from 0.8.0 to 0.10.0 - Update addon-webgl from 0.16.0 to 0.18.0 - Use new @xterm scoped package names for addons 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- web/shell.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/shell.html b/web/shell.html index 20939d56..c07f87ce 100644 --- a/web/shell.html +++ b/web/shell.html @@ -4,7 +4,7 @@ Intro - FTXUI Sample - + + + +
+

@PROJECT_NAME@

+

WebAssembly Applications

+ +
+ @WASM_APPS_HTML@ +
+ + +
+ + From 9ba37373f4ce315c5e467758ec1515f85fbe7489 Mon Sep 17 00:00:00 2001 From: Jason Turner Date: Wed, 31 Dec 2025 14:26:02 -0700 Subject: [PATCH 07/14] Fix hardening options for MSVC --- cmake/Hardening.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/Hardening.cmake b/cmake/Hardening.cmake index 92ab0b0a..905fc453 100644 --- a/cmake/Hardening.cmake +++ b/cmake/Hardening.cmake @@ -9,9 +9,9 @@ macro( message(STATUS "** Enabling Hardening (Target ${target}) **") if(MSVC) - set(NEW_COMPILE_OPTIONS "${NEW_COMPILE_OPTIONS} /sdl /DYNAMICBASE /guard:cf") + list(APPEND NEW_COMPILE_OPTIONS /sdl /DYNAMICBASE /guard:cf) message(STATUS "*** MSVC flags: /sdl /DYNAMICBASE /guard:cf /NXCOMPAT /CETCOMPAT") - set(NEW_LINK_OPTIONS "${NEW_LINK_OPTIONS} /NXCOMPAT /CETCOMPAT") + list(APPEND NEW_LINK_OPTIONS /NXCOMPAT /CETCOMPAT) elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang|GNU") list(APPEND NEW_CXX_DEFINITIONS -D_GLIBCXX_ASSERTIONS) From 626d4b4737c46a6f5aa90798a97c40804b093645 Mon Sep 17 00:00:00 2001 From: Jason Turner Date: Wed, 31 Dec 2025 16:10:13 -0700 Subject: [PATCH 08/14] Clean up and improve WASM build configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1: Critical Fixes - Remove redundant -fwasm-exceptions flag (already in global CMAKE_CXX_FLAGS) - Fix HTML injection vulnerability by escaping special characters in titles/descriptions - Absolutify resource paths to prevent Emscripten path resolution issues - Ensure service worker copied to both individual targets and web-dist Phase 2: Configuration Improvements - Add configurable WASM runtime parameters (memory, thread pool, asyncify stack) - MYPROJECT_WASM_INITIAL_MEMORY (default: 32MB) - MYPROJECT_WASM_PTHREAD_POOL_SIZE (default: 4) - MYPROJECT_WASM_ASYNCIFY_STACK_SIZE (default: 64KB) - Replace INTRO_RESOURCES_DIR with per-target RESOURCES_DIR parameter - Make each target directory fully standalone with its own service worker Benefits: - Users can tune WASM performance without editing CMake files - HTML injection vulnerability fixed - Resource paths work correctly with relative paths - Both individual targets and web-dist deployments are self-contained - More flexible per-target resource embedding 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- cmake/Emscripten.cmake | 64 ++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/cmake/Emscripten.cmake b/cmake/Emscripten.cmake index c2fa50c9..5c369ddc 100644 --- a/cmake/Emscripten.cmake +++ b/cmake/Emscripten.cmake @@ -23,8 +23,13 @@ if(EMSCRIPTEN) # Disable testing - no way to execute WASM test targets set(BUILD_TESTING OFF CACHE BOOL "No test runner for WASM") - # Resource embedding path (optional) - set(INTRO_RESOURCES_DIR "" CACHE PATH "Resources directory (optional)") + # WASM runtime configuration - tunable performance parameters + set(MYPROJECT_WASM_INITIAL_MEMORY "33554432" CACHE STRING + "Initial WASM memory in bytes (default: 32MB)") + set(MYPROJECT_WASM_PTHREAD_POOL_SIZE "4" CACHE STRING + "Pthread pool size for WASM builds (default: 4)") + set(MYPROJECT_WASM_ASYNCIFY_STACK_SIZE "65536" CACHE STRING + "Asyncify stack size in bytes (default: 64KB)") # For Emscripten WASM builds, FTXUI requires pthreads # Set these flags early so they propagate to all dependencies @@ -46,7 +51,7 @@ function(myproject_configure_wasm_target target) if(EMSCRIPTEN) # Parse optional named arguments set(options "") - set(oneValueArgs TITLE DESCRIPTION) + set(oneValueArgs TITLE DESCRIPTION RESOURCES_DIR) set(multiValueArgs "") cmake_parse_arguments(WASM "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) @@ -71,33 +76,33 @@ function(myproject_configure_wasm_target target) # Enable pthreads - REQUIRED by FTXUI's WASM implementation "-sUSE_PTHREADS=1" "-sPROXY_TO_PTHREAD=1" - "-sPTHREAD_POOL_SIZE=4" + "-sPTHREAD_POOL_SIZE=${MYPROJECT_WASM_PTHREAD_POOL_SIZE}" # Enable asyncify for emscripten_sleep and async operations "-sASYNCIFY=1" - "-sASYNCIFY_STACK_SIZE=65536" + "-sASYNCIFY_STACK_SIZE=${MYPROJECT_WASM_ASYNCIFY_STACK_SIZE}" # Memory configuration "-sALLOW_MEMORY_GROWTH=1" - "-sINITIAL_MEMORY=33554432" + "-sINITIAL_MEMORY=${MYPROJECT_WASM_INITIAL_MEMORY}" # Environment - need both web and worker for pthread support "-sENVIRONMENT=web,worker" # Export runtime methods for JavaScript interop "-sEXPORTED_RUNTIME_METHODS=['FS','ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8']" # Export malloc/free for MAIN_THREAD_EM_ASM usage "-sEXPORTED_FUNCTIONS=['_main','_malloc','_free']" - # Enable native WebAssembly exception handling - "-fwasm-exceptions" + # Note: -fwasm-exceptions already set in global CMAKE_CXX_FLAGS (line 36) # Debug: enable assertions for better error messages "-sASSERTIONS=1" ) - # Embed resources into WASM binary (optional) - if(INTRO_RESOURCES_DIR AND EXISTS "${INTRO_RESOURCES_DIR}") + # Embed resources into WASM binary (optional, per-target) + if(WASM_RESOURCES_DIR AND EXISTS "${WASM_RESOURCES_DIR}") + # Convert to absolute path to avoid issues with Emscripten path resolution + get_filename_component(ABS_RESOURCES_DIR "${WASM_RESOURCES_DIR}" ABSOLUTE BASE_DIR "${CMAKE_SOURCE_DIR}") + target_link_options(${target} PRIVATE - "--embed-file=${INTRO_RESOURCES_DIR}@/resources" + "--embed-file=${ABS_RESOURCES_DIR}@/resources" ) - message(STATUS "Embedding resources from ${INTRO_RESOURCES_DIR}") - else() - message(STATUS "No resources directory configured, skipping resource embedding") + message(STATUS "Embedding resources for ${target} from ${ABS_RESOURCES_DIR}") endif() # Configure the shell HTML template for this target @@ -135,14 +140,14 @@ function(myproject_configure_wasm_target target) message(WARNING "Shell template not found: ${TEMPLATE_FILE}") endif() - # Copy service worker for COOP/COEP headers (needed for GitHub Pages) + # Copy service worker to target build directory for standalone target builds set(COI_WORKER "${CMAKE_SOURCE_DIR}/web/coi-serviceworker.min.js") if(EXISTS "${COI_WORKER}") add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${COI_WORKER}" "$/coi-serviceworker.min.js" - COMMENT "Copying coi-serviceworker.min.js for COOP/COEP headers" + COMMENT "Copying coi-serviceworker.min.js to ${target} build directory" ) endif() @@ -176,10 +181,21 @@ function(myproject_create_web_dist) get_property(TITLE GLOBAL PROPERTY MYPROJECT_WASM_TARGET_${target}_TITLE) get_property(DESCRIPTION GLOBAL PROPERTY MYPROJECT_WASM_TARGET_${target}_DESCRIPTION) + # Escape HTML special characters to prevent injection + string(REPLACE "&" "&" TITLE_ESCAPED "${TITLE}") + string(REPLACE "<" "<" TITLE_ESCAPED "${TITLE_ESCAPED}") + string(REPLACE ">" ">" TITLE_ESCAPED "${TITLE_ESCAPED}") + string(REPLACE "\"" """ TITLE_ESCAPED "${TITLE_ESCAPED}") + + string(REPLACE "&" "&" DESC_ESCAPED "${DESCRIPTION}") + string(REPLACE "<" "<" DESC_ESCAPED "${DESC_ESCAPED}") + string(REPLACE ">" ">" DESC_ESCAPED "${DESC_ESCAPED}") + string(REPLACE "\"" """ DESC_ESCAPED "${DESC_ESCAPED}") + string(APPEND WASM_APPS_HTML " -
${TITLE}
-
${DESCRIPTION}
+
${TITLE_ESCAPED}
+
${DESC_ESCAPED}
") endforeach() @@ -197,17 +213,11 @@ function(myproject_create_web_dist) # Build list of copy commands set(COPY_COMMANDS "") - # Copy service worker (shared by all apps) + # Service worker location set(COI_WORKER "${CMAKE_SOURCE_DIR}/web/coi-serviceworker.min.js") - if(EXISTS "${COI_WORKER}") - list(APPEND COPY_COMMANDS - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${COI_WORKER}" - "${WEB_DIST_DIR}/coi-serviceworker.min.js" - ) - endif() # For each WASM target, copy artifacts to subdirectory + # Each target gets its own service worker copy for standalone deployment foreach(target ${WASM_TARGETS}) # Determine source directory (where target was built) get_target_property(TARGET_BINARY_DIR ${target} BINARY_DIR) @@ -227,7 +237,7 @@ function(myproject_create_web_dist) "${TARGET_BINARY_DIR}/${target}.wasm" "${TARGET_DIST_DIR}/${target}.wasm" COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${TARGET_BINARY_DIR}/coi-serviceworker.min.js" + "${COI_WORKER}" "${TARGET_DIST_DIR}/coi-serviceworker.min.js" ) endforeach() From ccfec09f0eac363cc2a43ea3a3684179902494f3 Mon Sep 17 00:00:00 2001 From: Jason Turner Date: Wed, 31 Dec 2025 16:40:41 -0700 Subject: [PATCH 09/14] Simplify WASM build infrastructure code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduces code duplication and improves maintainability while preserving all functionality. Changes include: JavaScript simplifications (web/shell_template.html.in): - Remove 39 lines of duplicate URL parameter documentation - Consolidate error handlers with shared showError() helper - Extract buffer writer factory to eliminate stdout/stderr duplication - Simplify URL parameter parsing using forEach API - Total reduction: ~70 lines CMake simplifications (cmake/Emscripten.cmake): - Add reusable escape_html() helper function - Loop sanitizer configuration instead of 5 separate set() calls - Use string(APPEND) for compiler flags - Use escape_html() helper to eliminate duplicate escaping logic - Remove redundant else clause for WASM_BUILD flag - Remove redundant directory creation (configure_file creates parents) - Change template missing warning to fatal error (fail fast) - Total reduction: ~12 lines Overall impact: ~82 fewer lines, improved readability, easier maintenance. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- cmake/Emscripten.cmake | 51 ++++++--------- web/shell_template.html.in | 129 +++++++++++-------------------------- 2 files changed, 56 insertions(+), 124 deletions(-) diff --git a/cmake/Emscripten.cmake b/cmake/Emscripten.cmake index 5c369ddc..680c5fce 100644 --- a/cmake/Emscripten.cmake +++ b/cmake/Emscripten.cmake @@ -1,6 +1,16 @@ # cmake/Emscripten.cmake # Emscripten/WebAssembly build configuration +# Helper function to escape HTML special characters +function(escape_html output_var input) + set(result "${input}") + string(REPLACE "&" "&" result "${result}") + string(REPLACE "<" "<" result "${result}") + string(REPLACE ">" ">" result "${result}") + string(REPLACE "\"" """ result "${result}") + set(${output_var} "${result}" PARENT_SCOPE) +endfunction() + # Detect if we're building with Emscripten if(EMSCRIPTEN) message(STATUS "Emscripten build detected - configuring for WebAssembly") @@ -9,11 +19,9 @@ if(EMSCRIPTEN) set(MYPROJECT_WASM_BUILD ON CACHE BOOL "Building for WebAssembly" FORCE) # Sanitizers don't work with Emscripten - set(myproject_ENABLE_SANITIZER_ADDRESS OFF CACHE BOOL "Not supported with Emscripten") - set(myproject_ENABLE_SANITIZER_LEAK OFF CACHE BOOL "Not supported with Emscripten") - set(myproject_ENABLE_SANITIZER_UNDEFINED OFF CACHE BOOL "Not supported with Emscripten") - set(myproject_ENABLE_SANITIZER_THREAD OFF CACHE BOOL "Not supported with Emscripten") - set(myproject_ENABLE_SANITIZER_MEMORY OFF CACHE BOOL "Not supported with Emscripten") + foreach(sanitizer ADDRESS LEAK UNDEFINED THREAD MEMORY) + set(myproject_ENABLE_SANITIZER_${sanitizer} OFF CACHE BOOL "Not supported with Emscripten") + endforeach() # Disable static analysis and strict warnings for Emscripten builds set(myproject_ENABLE_CLANG_TIDY OFF CACHE BOOL "Disabled for Emscripten") @@ -31,19 +39,10 @@ if(EMSCRIPTEN) set(MYPROJECT_WASM_ASYNCIFY_STACK_SIZE "65536" CACHE STRING "Asyncify stack size in bytes (default: 64KB)") - # For Emscripten WASM builds, FTXUI requires pthreads + # For Emscripten WASM builds, FTXUI requires pthreads and native exception handling # Set these flags early so they propagate to all dependencies - - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pthread") - - # Enable native WebAssembly exception handling - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fwasm-exceptions") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fwasm-exceptions") - - -else() - set(MYPROJECT_WASM_BUILD OFF CACHE BOOL "Building for WebAssembly" FORCE) + string(APPEND CMAKE_CXX_FLAGS " -pthread -fwasm-exceptions") + string(APPEND CMAKE_C_FLAGS " -pthread -fwasm-exceptions") endif() # Function to apply WASM settings to a target @@ -113,10 +112,7 @@ function(myproject_configure_wasm_target target) set(TEMPLATE_FILE "${CMAKE_SOURCE_DIR}/web/shell_template.html.in") set(CONFIGURED_SHELL "${CMAKE_BINARY_DIR}/web/${target}_shell.html") - # Create output directory - file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/web") - - # Generate target-specific shell file + # Generate target-specific shell file (configure_file creates parent directories automatically) if(EXISTS "${TEMPLATE_FILE}") configure_file( "${TEMPLATE_FILE}" @@ -137,7 +133,7 @@ function(myproject_configure_wasm_target target) message(STATUS "Configured WASM shell for ${target}: ${CONFIGURED_SHELL}") else() - message(WARNING "Shell template not found: ${TEMPLATE_FILE}") + message(FATAL_ERROR "Shell template not found: ${TEMPLATE_FILE}") endif() # Copy service worker to target build directory for standalone target builds @@ -182,15 +178,8 @@ function(myproject_create_web_dist) get_property(DESCRIPTION GLOBAL PROPERTY MYPROJECT_WASM_TARGET_${target}_DESCRIPTION) # Escape HTML special characters to prevent injection - string(REPLACE "&" "&" TITLE_ESCAPED "${TITLE}") - string(REPLACE "<" "<" TITLE_ESCAPED "${TITLE_ESCAPED}") - string(REPLACE ">" ">" TITLE_ESCAPED "${TITLE_ESCAPED}") - string(REPLACE "\"" """ TITLE_ESCAPED "${TITLE_ESCAPED}") - - string(REPLACE "&" "&" DESC_ESCAPED "${DESCRIPTION}") - string(REPLACE "<" "<" DESC_ESCAPED "${DESC_ESCAPED}") - string(REPLACE ">" ">" DESC_ESCAPED "${DESC_ESCAPED}") - string(REPLACE "\"" """ DESC_ESCAPED "${DESC_ESCAPED}") + escape_html(TITLE_ESCAPED "${TITLE}") + escape_html(DESC_ESCAPED "${DESCRIPTION}") string(APPEND WASM_APPS_HTML " diff --git a/web/shell_template.html.in b/web/shell_template.html.in index d6927cd9..6b98449a 100644 --- a/web/shell_template.html.in +++ b/web/shell_template.html.in @@ -124,26 +124,13 @@ // execute asynchronously and Emscripten needs Module to exist immediately. var Module = { // Parse URL parameters and convert to CLI-style arguments + // Single char params → short flags (?v → -v), multi-char → long flags (?version → --version) arguments: (function() { const args = []; - const urlParams = new URLSearchParams(window.location.search); - - // Convert all URL parameters to command-line arguments - for (const [key, value] of urlParams.entries()) { - // Single character params become short flags: ?v → -v - // Multi-character params become long flags: ?version → --version - const prefix = key.length === 1 ? '-' : '--'; - - if (value === '' || value === null) { - // Boolean flag without value: ?help → --help - args.push(prefix + key); - } else { - // Parameter with value: ?file=test.txt → --file test.txt - args.push(prefix + key); - args.push(value); - } - } - + new URLSearchParams(window.location.search).forEach((value, key) => { + args.push((key.length === 1 ? '-' : '--') + key); + if (value) args.push(value); + }); console.log('Module.arguments:', args); return args; })(), @@ -235,37 +222,32 @@ return stdin_buffer.shift() || 0; } - // stdout: buffer bytes, flush on null byte (0) or newline (10) - function stdout(code) { - if (code === 0 || code === 10) { - // Flush buffer to terminal - if (code === 10) { - stdout_buffer.push(code); // Include the newline - } - if (stdout_buffer.length > 0) { - var text = new TextDecoder().decode(new Uint8Array(stdout_buffer)); - term.write(text); - stdout_buffer = []; + // Factory function to create buffered output writers + function createBufferWriter(buffer, writeHandler) { + return function(code) { + if (code === 0 || code === 10) { + if (code === 10) buffer.push(code); // Include newline in buffer + if (buffer.length > 0) { + var text = new TextDecoder().decode(new Uint8Array(buffer)); + writeHandler(text); + buffer.length = 0; // Clear buffer + } + } else { + buffer.push(code); } - } else { - stdout_buffer.push(code); - } + }; } - // stderr: buffer bytes, flush on null or newline - function stderr(code) { - if (code === 0 || code === 10) { - if (stderr_buffer.length > 0) { - var text = new TextDecoder().decode(new Uint8Array(stderr_buffer)); - console.error(text); - // Also write to visible terminal so users can see errors - term.write(text); - stderr_buffer = []; - } - } else { - stderr_buffer.push(code); - } - } + // stdout: buffer bytes, flush to terminal on null or newline + var stdout = createBufferWriter(stdout_buffer, function(text) { + term.write(text); + }); + + // stderr: buffer bytes, flush to console and terminal on null or newline + var stderr = createBufferWriter(stderr_buffer, function(text) { + console.error(text); + term.write(text); + }); // Hook into Module's preRun to set up terminal before WASM starts Module.preRun.push(function() { @@ -307,20 +289,21 @@ } }); - window.onerror = function(message, source, lineno, colno, error) { + // Consolidated error display helper + function showError(message, details) { var errorDiv = document.getElementById('error'); errorDiv.textContent = 'Error: ' + message; errorDiv.style.display = 'block'; document.getElementById('loading').classList.add('hidden'); - console.error('Error:', message, source, lineno, colno, error); + if (details) console.error(message, details); + } + + window.onerror = function(message, source, lineno, colno, error) { + showError(message, { source: source, lineno: lineno, colno: colno, error: error }); }; window.addEventListener('unhandledrejection', function(event) { - var errorDiv = document.getElementById('error'); - errorDiv.textContent = 'Error: ' + event.reason; - errorDiv.style.display = 'block'; - document.getElementById('loading').classList.add('hidden'); - console.error('Unhandled rejection:', event.reason); + showError(event.reason); }); // Export to window for debugging @@ -328,45 +311,5 @@ window.fitAddon = fitAddon; {{{ SCRIPT }}} - - From 84629a15b206cde5673629041100857273099725 Mon Sep 17 00:00:00 2001 From: Jason Turner Date: Wed, 31 Dec 2025 16:50:06 -0700 Subject: [PATCH 10/14] Use add_compile_options and add_link_options for Emscripten flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace direct CMAKE_CXX_FLAGS/CMAKE_C_FLAGS manipulation with add_compile_options() and add_link_options(), which is more idiomatic modern CMake and handles flag propagation correctly. Both -pthread and -fwasm-exceptions need to be specified at compile and link time for Emscripten builds. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- cmake/Emscripten.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/Emscripten.cmake b/cmake/Emscripten.cmake index 680c5fce..31d33a1f 100644 --- a/cmake/Emscripten.cmake +++ b/cmake/Emscripten.cmake @@ -41,8 +41,8 @@ if(EMSCRIPTEN) # For Emscripten WASM builds, FTXUI requires pthreads and native exception handling # Set these flags early so they propagate to all dependencies - string(APPEND CMAKE_CXX_FLAGS " -pthread -fwasm-exceptions") - string(APPEND CMAKE_C_FLAGS " -pthread -fwasm-exceptions") + add_compile_options(-pthread -fwasm-exceptions) + add_link_options(-pthread -fwasm-exceptions) endif() # Function to apply WASM settings to a target From b338c9e0109a5b5462c7e30202168493b0d842b3 Mon Sep 17 00:00:00 2001 From: Jason Turner Date: Wed, 31 Dec 2025 16:54:52 -0700 Subject: [PATCH 11/14] Apply DRY principles to Emscripten configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminates code duplication and improves maintainability: - Define common web asset paths as constants at top of file (MYPROJECT_WEB_DIR, MYPROJECT_COI_WORKER, etc.) instead of duplicating "${CMAKE_SOURCE_DIR}/web/..." throughout - Loop static analysis tool options (CLANG_TIDY, CPPCHECK, WARNINGS_AS_ERRORS) instead of three separate set() calls - Use path constants consistently in both myproject_configure_wasm_target and myproject_create_web_dist functions - Remove outdated comment reference to old line number - Add clarifying comment about WASM artifact files being copied Net result: 3 fewer lines, single source of truth for file paths, more consistent code structure. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- cmake/Emscripten.cmake | 43 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/cmake/Emscripten.cmake b/cmake/Emscripten.cmake index 31d33a1f..8f97d494 100644 --- a/cmake/Emscripten.cmake +++ b/cmake/Emscripten.cmake @@ -1,6 +1,12 @@ # cmake/Emscripten.cmake # Emscripten/WebAssembly build configuration +# Common paths for web assets +set(MYPROJECT_WEB_DIR "${CMAKE_SOURCE_DIR}/web") +set(MYPROJECT_COI_WORKER "${MYPROJECT_WEB_DIR}/coi-serviceworker.min.js") +set(MYPROJECT_SHELL_TEMPLATE "${MYPROJECT_WEB_DIR}/shell_template.html.in") +set(MYPROJECT_INDEX_TEMPLATE "${MYPROJECT_WEB_DIR}/index_template.html.in") + # Helper function to escape HTML special characters function(escape_html output_var input) set(result "${input}") @@ -24,9 +30,9 @@ if(EMSCRIPTEN) endforeach() # Disable static analysis and strict warnings for Emscripten builds - set(myproject_ENABLE_CLANG_TIDY OFF CACHE BOOL "Disabled for Emscripten") - set(myproject_ENABLE_CPPCHECK OFF CACHE BOOL "Disabled for Emscripten") - set(myproject_WARNINGS_AS_ERRORS OFF CACHE BOOL "Disabled for Emscripten") + foreach(option CLANG_TIDY CPPCHECK WARNINGS_AS_ERRORS) + set(myproject_ENABLE_${option} OFF CACHE BOOL "Disabled for Emscripten") + endforeach() # Disable testing - no way to execute WASM test targets set(BUILD_TESTING OFF CACHE BOOL "No test runner for WASM") @@ -88,7 +94,6 @@ function(myproject_configure_wasm_target target) "-sEXPORTED_RUNTIME_METHODS=['FS','ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8']" # Export malloc/free for MAIN_THREAD_EM_ASM usage "-sEXPORTED_FUNCTIONS=['_main','_malloc','_free']" - # Note: -fwasm-exceptions already set in global CMAKE_CXX_FLAGS (line 36) # Debug: enable assertions for better error messages "-sASSERTIONS=1" ) @@ -109,13 +114,12 @@ function(myproject_configure_wasm_target target) set(TARGET_TITLE "${WASM_TITLE}") set(TARGET_DESCRIPTION "${WASM_DESCRIPTION}") set(AT "@") # For escaping @ in npm package URLs - set(TEMPLATE_FILE "${CMAKE_SOURCE_DIR}/web/shell_template.html.in") set(CONFIGURED_SHELL "${CMAKE_BINARY_DIR}/web/${target}_shell.html") # Generate target-specific shell file (configure_file creates parent directories automatically) - if(EXISTS "${TEMPLATE_FILE}") + if(EXISTS "${MYPROJECT_SHELL_TEMPLATE}") configure_file( - "${TEMPLATE_FILE}" + "${MYPROJECT_SHELL_TEMPLATE}" "${CONFIGURED_SHELL}" @ONLY ) @@ -127,21 +131,20 @@ function(myproject_configure_wasm_target target) # Add both template and configured file as link dependencies set_property(TARGET ${target} APPEND PROPERTY LINK_DEPENDS - "${TEMPLATE_FILE}" + "${MYPROJECT_SHELL_TEMPLATE}" "${CONFIGURED_SHELL}" ) message(STATUS "Configured WASM shell for ${target}: ${CONFIGURED_SHELL}") else() - message(FATAL_ERROR "Shell template not found: ${TEMPLATE_FILE}") + message(FATAL_ERROR "Shell template not found: ${MYPROJECT_SHELL_TEMPLATE}") endif() # Copy service worker to target build directory for standalone target builds - set(COI_WORKER "${CMAKE_SOURCE_DIR}/web/coi-serviceworker.min.js") - if(EXISTS "${COI_WORKER}") + if(EXISTS "${MYPROJECT_COI_WORKER}") add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${COI_WORKER}" + "${MYPROJECT_COI_WORKER}" "$/coi-serviceworker.min.js" COMMENT "Copying coi-serviceworker.min.js to ${target} build directory" ) @@ -190,30 +193,24 @@ function(myproject_create_web_dist) endforeach() # Generate index.html from template - set(INDEX_TEMPLATE "${CMAKE_SOURCE_DIR}/web/index_template.html.in") set(INDEX_OUTPUT "${WEB_DIST_DIR}/index.html") - if(EXISTS "${INDEX_TEMPLATE}") - configure_file("${INDEX_TEMPLATE}" "${INDEX_OUTPUT}" @ONLY) + if(EXISTS "${MYPROJECT_INDEX_TEMPLATE}") + configure_file("${MYPROJECT_INDEX_TEMPLATE}" "${INDEX_OUTPUT}" @ONLY) else() - message(WARNING "Index template not found: ${INDEX_TEMPLATE}") + message(WARNING "Index template not found: ${MYPROJECT_INDEX_TEMPLATE}") endif() # Build list of copy commands set(COPY_COMMANDS "") - # Service worker location - set(COI_WORKER "${CMAKE_SOURCE_DIR}/web/coi-serviceworker.min.js") - # For each WASM target, copy artifacts to subdirectory # Each target gets its own service worker copy for standalone deployment foreach(target ${WASM_TARGETS}) - # Determine source directory (where target was built) get_target_property(TARGET_BINARY_DIR ${target} BINARY_DIR) - - # Create target subdirectory set(TARGET_DIST_DIR "${WEB_DIST_DIR}/${target}") + # Copy WASM artifacts: .html (as index.html), .js, .wasm, and service worker list(APPEND COPY_COMMANDS COMMAND ${CMAKE_COMMAND} -E make_directory "${TARGET_DIST_DIR}" COMMAND ${CMAKE_COMMAND} -E copy_if_different @@ -226,7 +223,7 @@ function(myproject_create_web_dist) "${TARGET_BINARY_DIR}/${target}.wasm" "${TARGET_DIST_DIR}/${target}.wasm" COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${COI_WORKER}" + "${MYPROJECT_COI_WORKER}" "${TARGET_DIST_DIR}/coi-serviceworker.min.js" ) endforeach() From 9d11452fb9c9cfcd42a6002468bd453a25413960 Mon Sep 17 00:00:00 2001 From: Jason Turner Date: Wed, 31 Dec 2025 17:18:03 -0700 Subject: [PATCH 12/14] Update MYPROJECT to myproject for proper replacement --- cmake/Emscripten.cmake | 56 +++++++++++++++++----------------- configured_files/config.hpp.in | 4 +-- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/cmake/Emscripten.cmake b/cmake/Emscripten.cmake index 8f97d494..e6115ab8 100644 --- a/cmake/Emscripten.cmake +++ b/cmake/Emscripten.cmake @@ -2,10 +2,10 @@ # Emscripten/WebAssembly build configuration # Common paths for web assets -set(MYPROJECT_WEB_DIR "${CMAKE_SOURCE_DIR}/web") -set(MYPROJECT_COI_WORKER "${MYPROJECT_WEB_DIR}/coi-serviceworker.min.js") -set(MYPROJECT_SHELL_TEMPLATE "${MYPROJECT_WEB_DIR}/shell_template.html.in") -set(MYPROJECT_INDEX_TEMPLATE "${MYPROJECT_WEB_DIR}/index_template.html.in") +set(myproject_WEB_DIR "${CMAKE_SOURCE_DIR}/web") +set(myproject_COI_WORKER "${myproject_WEB_DIR}/coi-serviceworker.min.js") +set(myproject_SHELL_TEMPLATE "${myproject_WEB_DIR}/shell_template.html.in") +set(myproject_INDEX_TEMPLATE "${myproject_WEB_DIR}/index_template.html.in") # Helper function to escape HTML special characters function(escape_html output_var input) @@ -22,7 +22,7 @@ if(EMSCRIPTEN) message(STATUS "Emscripten build detected - configuring for WebAssembly") # Set WASM build flag - set(MYPROJECT_WASM_BUILD ON CACHE BOOL "Building for WebAssembly" FORCE) + set(myproject_WASM_BUILD ON CACHE BOOL "Building for WebAssembly" FORCE) # Sanitizers don't work with Emscripten foreach(sanitizer ADDRESS LEAK UNDEFINED THREAD MEMORY) @@ -38,11 +38,11 @@ if(EMSCRIPTEN) set(BUILD_TESTING OFF CACHE BOOL "No test runner for WASM") # WASM runtime configuration - tunable performance parameters - set(MYPROJECT_WASM_INITIAL_MEMORY "33554432" CACHE STRING + set(myproject_WASM_INITIAL_MEMORY "33554432" CACHE STRING "Initial WASM memory in bytes (default: 32MB)") - set(MYPROJECT_WASM_PTHREAD_POOL_SIZE "4" CACHE STRING + set(myproject_WASM_PTHREAD_POOL_SIZE "4" CACHE STRING "Pthread pool size for WASM builds (default: 4)") - set(MYPROJECT_WASM_ASYNCIFY_STACK_SIZE "65536" CACHE STRING + set(myproject_WASM_ASYNCIFY_STACK_SIZE "65536" CACHE STRING "Asyncify stack size in bytes (default: 64KB)") # For Emscripten WASM builds, FTXUI requires pthreads and native exception handling @@ -70,24 +70,24 @@ function(myproject_configure_wasm_target target) endif() # Register this target in the global WASM targets list - set_property(GLOBAL APPEND PROPERTY MYPROJECT_WASM_TARGETS "${target}") - set_property(GLOBAL PROPERTY MYPROJECT_WASM_TARGET_${target}_TITLE "${WASM_TITLE}") - set_property(GLOBAL PROPERTY MYPROJECT_WASM_TARGET_${target}_DESCRIPTION "${WASM_DESCRIPTION}") + set_property(GLOBAL APPEND PROPERTY myproject_WASM_TARGETS "${target}") + set_property(GLOBAL PROPERTY myproject_WASM_TARGET_${target}_TITLE "${WASM_TITLE}") + set_property(GLOBAL PROPERTY myproject_WASM_TARGET_${target}_DESCRIPTION "${WASM_DESCRIPTION}") - target_compile_definitions(${target} PRIVATE MYPROJECT_WASM_BUILD=1) + target_compile_definitions(${target} PRIVATE myproject_WASM_BUILD=1) # Emscripten link flags target_link_options(${target} PRIVATE # Enable pthreads - REQUIRED by FTXUI's WASM implementation "-sUSE_PTHREADS=1" "-sPROXY_TO_PTHREAD=1" - "-sPTHREAD_POOL_SIZE=${MYPROJECT_WASM_PTHREAD_POOL_SIZE}" + "-sPTHREAD_POOL_SIZE=${myproject_WASM_PTHREAD_POOL_SIZE}" # Enable asyncify for emscripten_sleep and async operations "-sASYNCIFY=1" - "-sASYNCIFY_STACK_SIZE=${MYPROJECT_WASM_ASYNCIFY_STACK_SIZE}" + "-sASYNCIFY_STACK_SIZE=${myproject_WASM_ASYNCIFY_STACK_SIZE}" # Memory configuration "-sALLOW_MEMORY_GROWTH=1" - "-sINITIAL_MEMORY=${MYPROJECT_WASM_INITIAL_MEMORY}" + "-sINITIAL_MEMORY=${myproject_WASM_INITIAL_MEMORY}" # Environment - need both web and worker for pthread support "-sENVIRONMENT=web,worker" # Export runtime methods for JavaScript interop @@ -117,9 +117,9 @@ function(myproject_configure_wasm_target target) set(CONFIGURED_SHELL "${CMAKE_BINARY_DIR}/web/${target}_shell.html") # Generate target-specific shell file (configure_file creates parent directories automatically) - if(EXISTS "${MYPROJECT_SHELL_TEMPLATE}") + if(EXISTS "${myproject_SHELL_TEMPLATE}") configure_file( - "${MYPROJECT_SHELL_TEMPLATE}" + "${myproject_SHELL_TEMPLATE}" "${CONFIGURED_SHELL}" @ONLY ) @@ -131,20 +131,20 @@ function(myproject_configure_wasm_target target) # Add both template and configured file as link dependencies set_property(TARGET ${target} APPEND PROPERTY LINK_DEPENDS - "${MYPROJECT_SHELL_TEMPLATE}" + "${myproject_SHELL_TEMPLATE}" "${CONFIGURED_SHELL}" ) message(STATUS "Configured WASM shell for ${target}: ${CONFIGURED_SHELL}") else() - message(FATAL_ERROR "Shell template not found: ${MYPROJECT_SHELL_TEMPLATE}") + message(FATAL_ERROR "Shell template not found: ${myproject_SHELL_TEMPLATE}") endif() # Copy service worker to target build directory for standalone target builds - if(EXISTS "${MYPROJECT_COI_WORKER}") + if(EXISTS "${myproject_COI_WORKER}") add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${MYPROJECT_COI_WORKER}" + "${myproject_COI_WORKER}" "$/coi-serviceworker.min.js" COMMENT "Copying coi-serviceworker.min.js to ${target} build directory" ) @@ -167,7 +167,7 @@ function(myproject_create_web_dist) set(WEB_DIST_DIR "${CMAKE_BINARY_DIR}/web-dist") # Get list of all WASM targets - get_property(WASM_TARGETS GLOBAL PROPERTY MYPROJECT_WASM_TARGETS) + get_property(WASM_TARGETS GLOBAL PROPERTY myproject_WASM_TARGETS) if(NOT WASM_TARGETS) message(WARNING "No WASM targets registered. Skipping web-dist generation.") @@ -177,8 +177,8 @@ function(myproject_create_web_dist) # Generate HTML for app cards set(WASM_APPS_HTML "") foreach(target ${WASM_TARGETS}) - get_property(TITLE GLOBAL PROPERTY MYPROJECT_WASM_TARGET_${target}_TITLE) - get_property(DESCRIPTION GLOBAL PROPERTY MYPROJECT_WASM_TARGET_${target}_DESCRIPTION) + get_property(TITLE GLOBAL PROPERTY myproject_WASM_TARGET_${target}_TITLE) + get_property(DESCRIPTION GLOBAL PROPERTY myproject_WASM_TARGET_${target}_DESCRIPTION) # Escape HTML special characters to prevent injection escape_html(TITLE_ESCAPED "${TITLE}") @@ -195,10 +195,10 @@ function(myproject_create_web_dist) # Generate index.html from template set(INDEX_OUTPUT "${WEB_DIST_DIR}/index.html") - if(EXISTS "${MYPROJECT_INDEX_TEMPLATE}") - configure_file("${MYPROJECT_INDEX_TEMPLATE}" "${INDEX_OUTPUT}" @ONLY) + if(EXISTS "${myproject_INDEX_TEMPLATE}") + configure_file("${myproject_INDEX_TEMPLATE}" "${INDEX_OUTPUT}" @ONLY) else() - message(WARNING "Index template not found: ${MYPROJECT_INDEX_TEMPLATE}") + message(WARNING "Index template not found: ${myproject_INDEX_TEMPLATE}") endif() # Build list of copy commands @@ -223,7 +223,7 @@ function(myproject_create_web_dist) "${TARGET_BINARY_DIR}/${target}.wasm" "${TARGET_DIST_DIR}/${target}.wasm" COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${MYPROJECT_COI_WORKER}" + "${myproject_COI_WORKER}" "${TARGET_DIST_DIR}/coi-serviceworker.min.js" ) endforeach() diff --git a/configured_files/config.hpp.in b/configured_files/config.hpp.in index 86d5cb38..e8086136 100644 --- a/configured_files/config.hpp.in +++ b/configured_files/config.hpp.in @@ -1,5 +1,5 @@ -#ifndef MYPROJECT_CONFIG_HPP -#define MYPROJECT_CONFIG_HPP +#ifndef myproject_CONFIG_HPP +#define myproject_CONFIG_HPP // this is a basic example of how a CMake configured file might look // in this particular case, we are using it to set the version number of our executable From ad7a4f9fe0224cace334e488e69cefe0aa6f13b1 Mon Sep 17 00:00:00 2001 From: Jason Turner Date: Wed, 31 Dec 2025 17:32:34 -0700 Subject: [PATCH 13/14] Document GitHub Pages deployment hierarchy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note that main deploys to root, develop to /develop/, and tags to /tagname/ subdirectories. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .github/template/README.md | 6 ++++++ README.md | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/.github/template/README.md b/.github/template/README.md index c7b5b7e4..cd9c4086 100644 --- a/.github/template/README.md +++ b/.github/template/README.md @@ -7,6 +7,12 @@ ## About %%myproject%% %%description%% +## WebAssembly Demo + +Try the live WebAssembly demo: [https://%%myorg%%.github.io/%%myproject%%/](https://%%myorg%%.github.io/%%myproject%%/) + +The `main` branch deploys to the root, `develop` to `/develop/`, and tags to `/tagname/`. + ## More Details diff --git a/README.md b/README.md index a860cb0b..c06f9b89 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,11 @@ It includes * a basic CLI example * examples for fuzz, unit, and constexpr testing * large GitHub action testing matrix + * WebAssembly build support with automatic GitHub Pages deployment + +**Live Demo:** If you enable GitHub Pages in your project created from this template, you'll have a working example like this: [https://cpp-best-practices.github.io/cmake_template/](https://cpp-best-practices.github.io/cmake_template/) + +The `main` branch deploys to the root, `develop` to `/develop/`, and tags to `/tagname/`. It requires From bd7ad9497a0d77e4bf1c03b78ecdc877d7e62c1b Mon Sep 17 00:00:00 2001 From: Jason Turner Date: Wed, 31 Dec 2025 18:05:44 -0700 Subject: [PATCH 14/14] Add develop branch link to WebAssembly demo section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Include links to both main and develop deployments so users can see both stable and development versions of the demo. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .github/template/README.md | 4 +++- README.md | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/template/README.md b/.github/template/README.md index cd9c4086..fcda5aea 100644 --- a/.github/template/README.md +++ b/.github/template/README.md @@ -9,7 +9,9 @@ ## WebAssembly Demo -Try the live WebAssembly demo: [https://%%myorg%%.github.io/%%myproject%%/](https://%%myorg%%.github.io/%%myproject%%/) +Try the live WebAssembly demo: +- Main: [https://%%myorg%%.github.io/%%myproject%%/](https://%%myorg%%.github.io/%%myproject%%/) +- Develop: [https://%%myorg%%.github.io/%%myproject%%/develop/](https://%%myorg%%.github.io/%%myproject%%/develop/) The `main` branch deploys to the root, `develop` to `/develop/`, and tags to `/tagname/`. diff --git a/README.md b/README.md index c06f9b89..eeedbbb8 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,9 @@ It includes * large GitHub action testing matrix * WebAssembly build support with automatic GitHub Pages deployment -**Live Demo:** If you enable GitHub Pages in your project created from this template, you'll have a working example like this: [https://cpp-best-practices.github.io/cmake_template/](https://cpp-best-practices.github.io/cmake_template/) +**Live Demo:** If you enable GitHub Pages in your project created from this template, you'll have a working example like this: +- Main: [https://cpp-best-practices.github.io/cmake_template/](https://cpp-best-practices.github.io/cmake_template/) +- Develop: [https://cpp-best-practices.github.io/cmake_template/develop/](https://cpp-best-practices.github.io/cmake_template/develop/) The `main` branch deploys to the root, `develop` to `/develop/`, and tags to `/tagname/`.