Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 232 additions & 5 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

149 changes: 149 additions & 0 deletions .github/workflows/wasm-ladder.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
name: WASM ladder gate

# Compile gate for the application ladder's WebAssembly clients — the one
# examples/TESTING.md's CI tiering promises ("the WASM compile gate for the
# affected rungs") and the only thing in this repository that can actually
# verify them: no Emscripten toolchain was available where rung 0's WASM-remote
# spike (examples/common/wasm_spike) or rung 1's WASM client
# (examples/pastebin/gui_wasm) were authored, so both shipped structurally
# complete and never compiled. Until this job runs green, treat every WASM
# target here as unverified.
#
# Deliberately separate from wasm-demo.yml (bank's WASM GUI): different sources,
# different path filter, and nothing here is deployed anywhere — this builds and
# stops. Single-threaded Qt-for-WASM, same as that workflow.

on:
push:
branches:
- master
paths:
- 'examples/common/**'
- 'examples/pastebin/**'
- 'examples/bookmarks/**'
- 'examples/polls/**'
- 'examples/kanban/**'
- 'examples/CMakeLists.txt'
- 'cmake/**'
- 'include/morph/**'
- 'src/qt/**'
- 'CMakeLists.txt'
- '.github/workflows/wasm-ladder.yml'
pull_request:
branches:
- master
paths:
- 'examples/common/**'
- 'examples/pastebin/**'
- 'examples/bookmarks/**'
- 'examples/polls/**'
- 'examples/kanban/**'
- 'examples/CMakeLists.txt'
- 'cmake/**'
- 'include/morph/**'
- 'src/qt/**'
- 'CMakeLists.txt'
- '.github/workflows/wasm-ladder.yml'

concurrency:
group: wasm-ladder-${{ github.ref }}
cancel-in-progress: true

env:
QT_VERSION: 6.8.3
EMSDK_VERSION: 3.1.56 # the emscripten Qt 6.8 was built against

jobs:
build-ladder-wasm:
name: Build the ladder's WASM clients
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install build tools
run: |
sudo apt-get update -q
sudo apt-get install -y ninja-build

# aqtinstall gives a matched host + wasm Qt pair (same cmake glue), so no
# host/target version skew. qtwebsockets on *both*: morph::qt links
# Qt6::WebSockets, and a ladder WASM client is a remote client by rule
# (examples/IMPLEMENTATION.md rule 4's WASM clause), so the transport is
# not optional here the way it is for bank's local-only demo.
- name: Install Qt (host desktop)
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: linux
target: desktop
arch: linux_gcc_64
modules: qtwebsockets
dir: ${{ runner.temp }}/qt

- name: Install Qt (wasm, single-threaded)
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: all_os
target: wasm
arch: wasm_singlethread
modules: qtwebsockets
dir: ${{ runner.temp }}/qt

- name: Set up emsdk
uses: mymindstorm/setup-emsdk@v14
with:
version: ${{ env.EMSDK_VERSION }}
actions-cache-folder: emsdk-ladder-cache

# MORPH_CLIENT_ONLY is mandatory, not a tuning knob: a rung's presenters
# are BridgeHandler<Model> templates, so the client names its model type
# even though it never hosts one — and without this option morph still
# emits the registrars that closure over that model's ODBC-backed
# execute() bodies, which cannot link in a browser
# (docs/spec/core/registry.md). morph_add_rung() fails the configure with
# that explanation if it is missing.
#
# MORPH_BUILD_TESTS=OFF: Catch2 binaries are not browser artifacts, and
# examples/common/CMakeLists.txt returns before its Catch2/Lightweight
# section under Emscripten for exactly that reason.
- name: Configure
run: |
export EM_CACHE="$PWD/.emcache"
mkdir -p "$EM_CACHE"
HOST=${{ runner.temp }}/qt/Qt/${{ env.QT_VERSION }}/gcc_64
WASM=${{ runner.temp }}/qt/Qt/${{ env.QT_VERSION }}/wasm_singlethread
# The all_os/wasm package extracts its scripts without the exec bit.
chmod +x "$WASM"/bin/* || true
"$WASM/bin/qt-cmake" -S . -B build-wasm-ladder -G Ninja \
-DQT_HOST_PATH="$HOST" \
-DMORPH_BUILD_QT=ON \
-DMORPH_BUILD_FORMS_QML=ON \
-DMORPH_BUILD_LADDER=ON \
-DMORPH_LADDER_RUNGS=all \
-DMORPH_CLIENT_ONLY=ON \
-DMORPH_BUILD_TESTS=OFF \
-DMORPH_BUILD_EXAMPLES=OFF

# The rung-0 spike and rungs 1-3's clients, built by name so a target
# that silently stops being generated (morph_add_rung() skips a rung's
# gui_wasm when its prerequisites are missing, announcing why) fails this
# job instead of passing it vacuously. The plain build that follows
# covers any further rung automatically, so this file does not need
# editing again just to add another named target.
- name: Build the WASM-remote spike and every rung's WASM client
run: |
export EM_CACHE="$PWD/.emcache"
cmake --build build-wasm-ladder --target morph_ladder_wasm_spike
cmake --build build-wasm-ladder --target ladder_pastebin_gui_wasm
cmake --build build-wasm-ladder --target ladder_bookmarks_gui_wasm
cmake --build build-wasm-ladder --target ladder_polls_gui_wasm
# Catches any further rung's WASM client too, without editing this
# file again -- closing the gap rung 1's own final review flagged.
cmake --build build-wasm-ladder

# Informational: the build steps above are the gate. Listed rather than
# asserted by path, since where Qt drops a wasm bundle is Qt's business.
- name: Show the produced artifacts
run: find build-wasm-ladder -name '*.wasm' -o -name '*.html' | sort
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
/build-wasm/
/out/
*.db
*.profraw
/.cache/
/compile_commands.json
*.user
*.suo
.vs/
bv-clang/

# superpowers subagent-driven-development scratch workspace (ledgers, briefs, review packages)
/.superpowers/
42 changes: 36 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,24 @@ option(MORPH_BUILD_BANK_GUI "Build the Qt 6 GUI for the bank example" OFF)
option(MORPH_BUILD_HMAC_EXAMPLES "Build vetted-HMAC adapter examples (libsodium/OpenSSL, heavy deps)" OFF)
option(MORPH_BUILD_FORMS_QML "Build the shipped Qt/QML forms renderer module (MorphForms) and its demo" OFF)

# The application ladder (examples/LADDER.md): a shared testkit + GUI
# architecture consumed by every ladder rung. Off by default like the other
# heavy-dependency example options; needs MORPH_BUILD_QT and MORPH_BUILD_TESTS
# (checked inside examples/common/CMakeLists.txt with a clear FATAL_ERROR).
option(MORPH_BUILD_LADDER "Build the application ladder's shared testkit/GUI infrastructure and enabled rungs" OFF)

# Cache list of rungs to build when MORPH_BUILD_LADDER=ON. "all" builds every
# rung with a CMakeLists.txt under examples/<rung>/; a semicolon-separated
# subset (e.g. "pastebin;bookmarks") builds only those. Rung 0 has no rung
# folders yet, so this option exists but has nothing to select until rung 1
# lands (see examples/TESTING.md, "Build system and CI").
set(MORPH_LADDER_RUNGS "all" CACHE STRING "Semicolon-separated list of ladder rungs to build, or \"all\"")

if(MORPH_BUILD_HMAC_EXAMPLES AND NOT MORPH_BUILD_EXAMPLES)
message(WARNING "MORPH_BUILD_HMAC_EXAMPLES is ignored: it lives under examples/vetted_hmac, "
"which needs MORPH_BUILD_EXAMPLES=ON.")
endif()

if(MORPH_BUILD_FORMS_QML AND EMSCRIPTEN)
message(WARNING "MORPH_BUILD_FORMS_QML is ignored: the Qt/QML forms renderer needs a "
"non-Emscripten toolchain.")
endif()
option(MORPH_BUILD_QT "Build Qt6 WebSocket backend and tests" OFF)
option(MORPH_BUILD_NET "Build the morph::net raw-socket WebSocket transport (POSIX only; see docs/spec/core/backend.md)" OFF)
option(MORPH_BUILD_FUZZERS "Build libFuzzer harnesses over wire::decode/dispatchExecute (Clang only)" OFF)
Expand Down Expand Up @@ -214,7 +223,15 @@ target_sources(morph
# deferred to just after the "Tests" section further below, since Catch2 is
# only found/fetched there and its test executable names Catch2::Catch2
# directly.
if(MORPH_BUILD_FORMS_QML AND NOT EMSCRIPTEN)
# Emscripten builds this too. MorphForms is a plain Qt Quick QML module over
# header-only morph code — nothing in it is host-only — and a WASM ladder
# client has to render the *same* schema-driven Main.qml the desktop client
# does (examples/TESTING.md's "same client code"), which imports MorphForms.
# This block used to carry a `NOT EMSCRIPTEN` guard plus a "needs a
# non-Emscripten toolchain" warning, written when no WASM target consumed the
# renderer; that claim was never tested. Its one host-only piece, the QuickTest
# suite, is guarded inside src/qt/forms/CMakeLists.txt instead.
if(MORPH_BUILD_FORMS_QML)
# 6.5 is a hard floor, not a preference: qt_standard_project_setup's
# REQUIRES keyword and QQmlApplicationEngine::loadFromModule (used by the
# demo) both arrive in 6.5. Stating it here turns "your Qt is too old" into
Expand Down Expand Up @@ -285,6 +302,19 @@ if(MORPH_BUILD_TESTS)
add_subdirectory(tests)
endif()

# ── Application ladder (optional) ───────────────────────────────────────────
# Deferred to here (after the Tests section above), the same way
# MORPH_BUILD_FORMS_QML's src/qt/forms subdirectory is deferred further below:
# examples/common/CMakeLists.txt calls find_package(Catch2 3 CONFIG QUIET) and
# treats "not found" as a hard FATAL_ERROR (its own Catch2 does not get
# fetched -- it relies on MORPH_BUILD_TESTS=ON having already resolved one).
# Adding examples/ before this point would let that find_package() run before
# the Tests section's FetchContent fallback ever executes, breaking the
# no-system-Catch2 case even though MORPH_BUILD_TESTS=ON.
if(MORPH_BUILD_LADDER)
add_subdirectory(examples)
endif()

# ── Qt/QML forms renderer (optional) ─────────────────────────────────────────
# The actual MorphForms module/plugin (src/qt/forms), deferred to here (after
# Catch2 is found/fetched above) since its own CMakeLists.txt links a Catch2
Expand All @@ -294,7 +324,7 @@ endif()
# examples/forms/gui_qml (a consumer, added above) only forward-references
# the plain (non-namespaced) morph_forms_moduleplugin target this creates,
# which CMake resolves once this subdirectory is processed.
if(MORPH_BUILD_FORMS_QML AND NOT EMSCRIPTEN)
if(MORPH_BUILD_FORMS_QML)
add_subdirectory(src/qt/forms)
endif()

Expand Down
3 changes: 3 additions & 0 deletions cmake/compiler_options.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ function(apply_warnings target)
target_compile_options(${target} PRIVATE
# ── MSVC ──────────────────────────────────────────────────────────────
$<$<CXX_COMPILER_ID:MSVC>:
/bigobj # heavy template instantiation (BRIDGE_REGISTER_ACTION chains,
# examples/forms/main.cpp) exceeds the default object-file
# section limit (C1128) without this
/W4
/permissive-
/w14062 # enumerator not handled in switch
Expand Down
Loading
Loading