Repository navigation
Expand file tree
/
Copy pathCMakeLists.txt
More file actions
1193 lines (1117 loc) · 60.7 KB
/
Copy pathCMakeLists.txt
File metadata and controls
1193 lines (1117 loc) · 60.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
cmake_minimum_required(VERSION 3.25)
project(morph
VERSION 0.1.0
DESCRIPTION "Bridge/Model/Executor async framework"
LANGUAGES CXX
)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Global, build-wide: the Lightweight ORM (fetched into examples/common's
# ladder testkit and examples/bank) includes <Windows.h> from SqlStatement.hpp
# with no NOMINMAX/WIN32_LEAN_AND_MEAN guard of its own, so its `min`/`max`
# macros leak into every translation unit that (transitively) includes it and
# break any later std::min/std::max/std::numeric_limits<T>::min() call in the
# same TU (e.g. examples/common/clock.hpp). Defined here, before any
# subdirectory is added, so it reaches every target that could end up in such
# a TU, not just the ones that call apply_warnings().
if(WIN32)
add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN)
endif()
option(MORPH_BUILD_TESTS "Build tests" ON)
option(MORPH_BUILD_EXAMPLES "Build examples" ON)
option(MORPH_BUILD_BANK_EXAMPLE "Build the SQLite/Lightweight bank example (heavy deps)" OFF)
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()
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)
option(MORPH_BUILD_LOAD_TESTS "Build the soak + throughput/latency benchmark targets" OFF)
# Opt-in reference durable IOfflineQueue backed by SQLite (raw sqlite3 C API).
# Resolved via CMake's bundled FindSQLite3 module against a system SQLite3 dev
# package -- deliberately NOT added to vcpkg.json, so vcpkg-toolchain presets
# do not install it unconditionally. Off by default: the standard build tree
# never needs SQLite3 to be present. See docs/spec/offline/offline.md,
# "SqliteOfflineQueue".
option(MORPH_BUILD_OFFLINE_SQLITE "Build the SQLite-backed durable IOfflineQueue reference implementation (needs SQLite3)" OFF)
option(MORPH_BUILD_DOCUMENTATION "Build doxygen docs" OFF)
option(MORPH_BUILD_CLANG_TIDY "Enable clang-tidy checks (warnings-as-errors)" OFF)
option(MORPH_ENABLE_STRICT_COMPILATION
"Enable strict compilation: treat all warnings as errors with extra diagnostics"
ON
)
# Opt-in production guard: when ON, session_auth.hpp drops the `mac = hmacSha256`
# default argument on TokenIssuer/TokenVerifier/SigningAuthorizer, so any
# construction relying on the reference HMAC's default fails to compile and a
# vetted MacFunction (see examples/vetted_hmac/) must be injected explicitly.
# Keys on a build configuration the deployer chooses, not on CMAKE_BUILD_TYPE,
# so a zero-dependency local/low-stakes deployment can keep the reference impl
# by leaving this off. See docs/spec/security.md, "MAC-primitive recommended
# wiring". Off by default: the standard build is unaffected.
option(MORPH_REQUIRE_VETTED_HMAC
"Fail to build if a call site relies on the reference hmacSha256 default; forces an explicit vetted MacFunction"
OFF
)
# Opt-in linker-footprint guard for a pure client -- one that dispatches every
# action to a remote peer and never constructs a model locally. Registering a
# model normally emits two registrars (registerModelOnce, registerActionOnce)
# whose bodies call the model's constructor and Model::execute -- pulling in
# implementations (a database driver, a native UI framework, an OS-specific
# API) a client target may not even have a link path for, and definitely never
# calls. The third registrar (registerActionExecutorOnce) routes through
# Bridge::executeVia -> IBackend::execute (fully abstract) and needs only the
# action's JSON codecs plus Model::execute's *declaration* -- never its body
# -- so it stays emitted regardless. See docs/spec/core/registry.md,
# "MORPH_CLIENT_ONLY".
#
# On the INTERFACE of the morph target, not per-consumer: the macro changes
# which registrars a model header emits, so two translation units disagreeing
# about it would violate ODR. Off by default: the standard build is unaffected.
#
# NEVER enable this for a process that hosts models (a server, or any Bridge
# running LocalBackend) -- it would silently register nothing, failing at
# runtime with "unknown model type" instead of at compile/link time.
option(MORPH_CLIENT_ONLY
"Suppress the model-owning registrars so a pure remote client need not link model constructor/execute bodies. NEVER enable for a process that hosts models."
OFF
)
# Both Qt consumers (the WebSocket backend and the forms QML renderer) need
# Qt6 auto-detected on Windows before their find_package(Qt6 ...) calls run.
if(WIN32 AND (MORPH_BUILD_QT OR MORPH_BUILD_FORMS_QML))
include(cmake/FindQt.cmake)
endif()
include(cmake/compiler_options.cmake)
# Compiler-cache launcher selection: fastcache-cc (when a fastcached daemon
# answers) -> sccache -> ccache -> none. Included before any target or fetched
# dependency (glaze and core-cpp, through CPM, below) so those get cached too. See
# cmake/CompileCache.cmake for the full selection logic; it declares its own
# USE_COMPILER_CACHE/FASTCACHE_ADDR options.
include(cmake/CompileCache.cmake)
# ── clang-tidy integration ─────────────────────────────────────────────────
if(MORPH_BUILD_CLANG_TIDY)
find_program(CLANG_TIDY_BIN clang-tidy REQUIRED)
set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_BIN};--warnings-as-errors=*;--header-filter=include/morph/.*")
message(STATUS "clang-tidy: ${CLANG_TIDY_BIN} (warnings as errors)")
endif()
# Registered before any subdirectory so demo-level add_test calls (examples/
# forms) land in the test set alongside the Catch2 suite.
if(MORPH_BUILD_TESTS)
enable_testing()
endif()
# ── Dependencies: CPM, over one shared source cache ─────────────────────────
# Every dependency morph fetches goes through CPMAddPackage, and CPM keeps the
# sources it clones in CPM_SOURCE_CACHE, keyed on the package and its revision.
# A second configure -- another preset, another build directory, the install
# check's scratch tree -- then clones nothing, and CI restores the directory
# from actions/cache rather than cloning glaze, Catch2, Lightweight and
# doxygen-awesome-css again on every job: a burst of anonymous clones from one
# egress address is answered with 401 by github.com, which git reports as
# "could not read Username". A cache miss still fetches.
#
# The default is inside the checkout (.cache/ is ignored), so it needs no
# setup and is never shared between checkouts by surprise. An explicit
# -DCPM_SOURCE_CACHE or the environment variable of the same name wins.
if(NOT DEFINED CPM_SOURCE_CACHE AND NOT DEFINED ENV{CPM_SOURCE_CACHE})
set(CPM_SOURCE_CACHE "${CMAKE_CURRENT_SOURCE_DIR}/.cache/cpm" CACHE PATH
"Where CPM keeps the sources of every fetched dependency")
endif()
# CPM itself is loaded only when something has to be fetched: its bootstrap
# downloads CPM.cmake, so loading it unconditionally would put the network on
# the path of a configure whose every dependency is already installed -- the
# shape a package-manager build (vcpkg, Conan, a distribution) has, where
# reaching the network is not allowed. Each fetch site calls
# morph_use_cpm(<what> [<why>]) before its CPMAddPackage; the first one to
# load CPM names itself in a STATUS line, so a failed bootstrap download says
# which dependency asked for it. A parent project that has loaded CPM already
# is reused, not loaded again.
#
# A macro, not a function, so CPM's own non-cache variables land in the
# caller's scope rather than vanishing with a function's.
set(MORPH_CPM_BOOTSTRAP "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CPM.cmake")
macro(morph_use_cpm _morph_cpm_for)
if(NOT COMMAND CPMAddPackage)
if(${ARGC} GREATER 1)
set(_morph_cpm_why "${ARGV1}")
else()
set(_morph_cpm_why "not found installed (install it and point CMAKE_PREFIX_PATH at it to configure without the network)")
endif()
message(STATUS "morph: loading CPM to fetch ${_morph_cpm_for}: ${_morph_cpm_why}")
unset(_morph_cpm_why)
include("${MORPH_CPM_BOOTSTRAP}")
endif()
endmacro()
# ── glaze ────────────────────────────────────────────────────────────────────
# On Windows vcpkg provides glaze. Elsewhere CPM fetches it, so that CI does
# not require vcpkg. The version bound matters: without it an older
# system/user install (e.g. a stray 4.x in ~/.local) silently shadows the
# pinned v7.4.0 and breaks schema generation.
# One place for the bound, because morphConfig.cmake has to state it too: an
# installed morph whose package config asks for a different glaze than the
# build used would resolve a header set the build never compiled against.
#
# find_package first and CPMAddPackage only when it finds nothing, rather than
# CPM_USE_LOCAL_PACKAGES: that option is global, so it would also change how
# Lightweight's own CPM dependencies resolve, and it passes the fetched pin as
# the version it asks find_package for -- which for Catch2 below would refuse
# the distribution's 3.4.0 that the clang-tidy leg is pinned to analyse.
set(MORPH_GLAZE_VERSION 7.4)
find_package(glaze ${MORPH_GLAZE_VERSION} CONFIG QUIET)
if(NOT glaze_FOUND)
morph_use_cpm(glaze)
CPMAddPackage(
NAME glaze
GITHUB_REPOSITORY stephenberry/glaze
GIT_TAG v7.4.0
OPTIONS "glaze_ENABLE_TESTS OFF")
endif()
# ── Tracy (optional) ────────────────────────────────────────────────────────
# MORPH_ENABLE_TRACY turns morph's MORPH_ZONE/MORPH_PLOT/... macros
# (include/morph/core/profiler.hpp) into Tracy calls. Found first, as glaze
# is, and fetched only when no installed Tracy is found.
#
# Before core-cpp, because core-cpp has Tracy instrumentation of its own
# (CORE_CPP_WITH_TRACY) and, with morph passing CORE_CPP_FETCH_DEPS OFF, takes
# the Tracy::TracyClient its parent has already provided. Added after it, a
# build with both on would have no client for core-cpp to find.
#
# The tag and the CPM name are Lightweight's, deliberately: an application
# that links both libraries must end up with exactly one TracyClient, because
# two copies of the client -- let alone two versions -- in one process do not
# work. With the same NAME, whichever of the two adds Tracy first fetches it
# and the other's CPMAddPackage reuses that copy; with the same tag, neither
# side is handed a version it did not ask for. TRACY_ON_DEMAND is
# Lightweight's choice too, and the one a library should make: without it the
# client buffers every event from process start until a profiler connects,
# which in a process nobody profiles is a leak. MORPH_TRACY_ON_DEMAND=OFF is
# for a capture that must see a program from its first zone -- the nightly
# capture job -- and, like the tag, applies only when morph is the one that
# fetches Tracy. It is a morph option rather than TRACY_ON_DEMAND itself
# because CPM passes OPTIONS as normal variables, which a -D on the command
# line cannot override.
#
# The define and the link are INTERFACE: morph is header-only, so the macros
# expand in the consumer's own translation units, and every one of them has
# to agree on MORPH_TRACY_ENABLED (see profiler.hpp on why that is an ODR
# matter).
option(MORPH_ENABLE_TRACY "Instrument morph's hot paths with Tracy profiler zones (fetches Tracy if not installed)" OFF)
option(MORPH_TRACY_ON_DEMAND "Build a fetched Tracy client that records only while a profiler is connected" ON)
mark_as_advanced(MORPH_TRACY_ON_DEMAND)
set(MORPH_TRACY_GIT_TAG v0.13.1)
if(MORPH_ENABLE_TRACY)
find_package(Tracy CONFIG QUIET)
if(NOT Tracy_FOUND AND NOT TARGET Tracy::TracyClient)
morph_use_cpm(Tracy)
CPMAddPackage(
NAME tracy
GITHUB_REPOSITORY wolfpld/tracy
GIT_TAG ${MORPH_TRACY_GIT_TAG}
EXCLUDE_FROM_ALL YES
SYSTEM YES
OPTIONS "TRACY_ENABLE ON" "TRACY_ON_DEMAND ${MORPH_TRACY_ON_DEMAND}")
endif()
# A TracyClient built without TRACY_ENABLE compiles every Tracy macro to
# nothing: the build would report profiling on and record no zone at all.
get_target_property(_morph_tracy_defs Tracy::TracyClient INTERFACE_COMPILE_DEFINITIONS)
if(NOT "${_morph_tracy_defs}" MATCHES "TRACY_ENABLE")
message(FATAL_ERROR
"MORPH_ENABLE_TRACY=ON, but the Tracy::TracyClient this configure found does not define "
"TRACY_ENABLE in its interface, so every zone would compile to nothing. Use a Tracy "
"built with TRACY_ENABLE=ON, or let morph fetch ${MORPH_TRACY_GIT_TAG}.")
endif()
unset(_morph_tracy_defs)
endif()
# ── core-cpp ────────────────────────────────────────────────────────────────
# The shared C++23 foundation of the Contour Terminal projects: morph takes
# its event loop and timers, base64 and the wakeup primitive from it. Only the
# modules morph links are built (EXCLUDE_FROM_ALL, unless morph installs; see
# below), none of core-cpp's own tests, examples, TUI or TLS, and nothing is
# fetched on its behalf: with those off it needs no dependency but the thread
# library. Under Emscripten it
# builds its single-threaded WebAssembly subset, which is what morph uses
# there. SYSTEM, so morph's warning set does not apply to core-cpp's headers.
#
# morph stays a header-only INTERFACE target, but core::base, core::net and
# core::platform are static libraries, so every consumer of morph now builds
# them -- or links an installed core-cpp's.
#
# Found first, as glaze is, and fetched only when no installed core-cpp
# satisfies the bound. MORPH_CORE_CPP_VERSION is that bound, stated once
# because morphConfig.cmake asks for the same one; core-cpp's package accepts
# only its own minor version while it is 0.x.
#
# Except under a sanitizer: an installed core-cpp was compiled without morph's
# instrumentation, and a ThreadSanitizer that cannot see core::net's side of
# TimeoutScheduler's hand-off reports races that are not there and misses ones
# that are. A sanitizer leg therefore always builds core-cpp in-tree, so the
# block below can instrument it.
#
# morph's install exports morph::morph, which links core-cpp's modules, so an
# install of morph with a fetched core-cpp has to install core-cpp too: CMake
# refuses an export that names a target in no export set. CORE_CPP_INSTALL
# follows MORPH_INSTALL for that, and morphConfig.cmake finds the installed
# core-cpp package in turn. A found core-cpp is already installed, its targets
# are imported, and the export names them without installing anything again.
# MORPH_INSTALL is described with the install rules below. EXCLUDE_FROM_ALL
# is off while morph installs: CMake leaves an excluded subdirectory's install
# rules out of the parent's install, so `cmake --install` would install
# morph's package without the core-cpp package it depends on.
set(MORPH_CORE_CPP_VERSION 0.5)
option(MORPH_INSTALL "Generate morph's install and export rules" ${PROJECT_IS_TOP_LEVEL})
if(NOT DEFINED AF_SANITIZER)
find_package(core-cpp ${MORPH_CORE_CPP_VERSION} CONFIG QUIET)
endif()
if(NOT core-cpp_FOUND)
if(MORPH_INSTALL)
set(_morph_core_cpp_exclude_from_all NO)
else()
set(_morph_core_cpp_exclude_from_all YES)
endif()
if(DEFINED AF_SANITIZER)
morph_use_cpm(core-cpp "AF_SANITIZER builds it in-tree so that it can be instrumented")
else()
morph_use_cpm(core-cpp)
endif()
CPMAddPackage(
NAME core-cpp
GITHUB_REPOSITORY contour-terminal/core-cpp
GIT_TAG v0.5.0
VERSION 0.5.0
SYSTEM YES
EXCLUDE_FROM_ALL ${_morph_core_cpp_exclude_from_all}
OPTIONS "CORE_CPP_TESTING OFF" "CORE_CPP_BUILD_EXAMPLES OFF" "CORE_CPP_WITH_TUI OFF"
"CORE_CPP_WITH_TLS OFF" "CORE_CPP_FETCH_DEPS OFF" "CORE_CPP_INSTALL ${MORPH_INSTALL}")
unset(_morph_core_cpp_exclude_from_all)
endif()
# A sanitizer leg instruments core-cpp's compiled modules with morph's own
# targets (see above for why). core-cpp lists those targets in the
# CORE_CPP_TARGETS global property for exactly this. The property is set only
# by an in-tree core-cpp; an empty one here means nothing would be
# instrumented while the configure reported success, so it stops instead --
# e.g. a parent project that brought core-cpp in some other way.
if(DEFINED AF_SANITIZER)
get_property(_morph_core_cpp_targets GLOBAL PROPERTY CORE_CPP_TARGETS)
if(NOT _morph_core_cpp_targets)
message(FATAL_ERROR
"AF_SANITIZER=${AF_SANITIZER} but core-cpp lists no targets to instrument "
"(the CORE_CPP_TARGETS global property is empty): core-cpp did not come from an "
"in-tree build, so its compiled modules would run uninstrumented.")
endif()
foreach(_morph_core_cpp_target IN LISTS _morph_core_cpp_targets)
apply_sanitizers(${_morph_core_cpp_target} ${AF_SANITIZER})
endforeach()
unset(_morph_core_cpp_targets)
unset(_morph_core_cpp_target)
endif()
# Emscripten's single-threaded build has no pthreads; requiring Threads there
# would force -pthread (SharedArrayBuffer), which we deliberately avoid so the
# WASM bundle can be served from plain GitHub Pages with no COOP/COEP headers.
if(NOT WIN32 AND NOT EMSCRIPTEN)
find_package(Threads REQUIRED)
endif()
# ── Header-only library target ───────────────────────────────────────────────
add_library(morph INTERFACE)
add_library(morph::morph ALIAS morph)
target_include_directories(morph INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_link_libraries(morph INTERFACE glaze::glaze core::base core::async core::net)
if(NOT EMSCRIPTEN)
target_link_libraries(morph INTERFACE core::platform)
endif()
if(NOT WIN32 AND NOT EMSCRIPTEN)
target_link_libraries(morph INTERFACE Threads::Threads)
endif()
if(MORPH_REQUIRE_VETTED_HMAC)
target_compile_definitions(morph INTERFACE MORPH_REQUIRE_VETTED_HMAC)
endif()
if(MORPH_CLIENT_ONLY)
target_compile_definitions(morph INTERFACE MORPH_CLIENT_ONLY)
endif()
if(MORPH_ENABLE_TRACY)
target_compile_definitions(morph INTERFACE MORPH_TRACY_ENABLED)
target_link_libraries(morph INTERFACE Tracy::TracyClient)
endif()
set_target_properties(morph PROPERTIES
VERIFY_INTERFACE_HEADER_SETS ON
)
target_sources(morph
INTERFACE
FILE_SET HEADERS
BASE_DIRS include
FILES
include/morph/version.hpp
include/morph/attributes.hpp
include/morph/core/logger.hpp
include/morph/core/observability.hpp
include/morph/core/profiler.hpp
include/morph/core/executor.hpp
include/morph/core/strand.hpp
include/morph/core/owner_strand.hpp
include/morph/core/completion.hpp
include/morph/core/coroutine.hpp
include/morph/core/callback_scope.hpp
include/morph/core/async.hpp
include/morph/core/io_loop.hpp
include/morph/core/timeout_scheduler.hpp
include/morph/core/model.hpp
include/morph/core/registry.hpp
include/morph/core/backend.hpp
include/morph/core/remote.hpp
include/morph/core/bridge.hpp
include/morph/core/wire.hpp
include/morph/journal/action_log.hpp
include/morph/journal/action_log_json.hpp
include/morph/journal/journal.hpp
include/morph/journal/file_action_log.hpp
include/morph/offline/network_monitor.hpp
include/morph/offline/offline_queue.hpp
include/morph/offline/sync_worker.hpp
include/morph/offline/file_offline_queue.hpp
include/morph/offline/replay_ledger.hpp
include/morph/session/session.hpp
include/morph/forms/forms.hpp
include/morph/forms/choice.hpp
include/morph/forms/i18n.hpp
include/morph/util/rational.hpp
include/morph/util/quantity.hpp
include/morph/util/datetime.hpp
include/morph/reactive/runtime.hpp
include/morph/reactive/signal.hpp
include/morph/reactive/scope.hpp
include/morph/reactive/store.hpp
include/morph/reactive/scheduler.hpp
include/morph/reactive/testing/manual_scheduler.hpp
include/morph/reactive/control.hpp
include/morph/ui/view.hpp
include/morph/ui/backend.hpp
include/morph/ui/mount.hpp
include/morph/ui/frontend.hpp
include/morph/ui/testing/recording_backend.hpp
include/morph/ui/testing/backend_conformance.hpp
include/morph/render/i18n.hpp
include/morph/render/locale_format.hpp
include/morph/core/file_io_ops.hpp
include/morph/core/model_key.hpp
include/morph/core/payload_schema.hpp
include/morph/core/payload_shape_tag.hpp
include/morph/testing/step_executor.hpp
include/morph/testing/owner_probe_recorder.hpp
include/morph/forms/app.hpp
include/morph/forms/flows.hpp
include/morph/forms/layout.hpp
include/morph/forms/sections.hpp
include/morph/forms/views.hpp
include/morph/forms/widget_hints.hpp
include/morph/journal/outbox.hpp
include/morph/offline/reconnect_coordinator.hpp
include/morph/session/session_auth.hpp
include/morph/util/tagged.hpp
include/morph/forms/instance_constraints.hpp
include/morph/table/data_source.hpp
include/morph/table/sort.hpp
include/morph/table/filter.hpp
include/morph/table/view_change.hpp
include/morph/table/engine.hpp
include/morph/table/query.hpp
include/morph/table/selection.hpp
include/morph/table/edits.hpp
)
# The detail/ headers that public headers include. They are not public surface
# and are deliberately outside the verified set -- standalone compilation is a
# promise made about morph's public headers, and a detail/ header is free to be
# a fragment included partway down the one that owns it -- but they must still
# ship, or an installed morph/util/quantity.hpp cannot find fixed_string.hpp
# and the install compiles nowhere (morph#232). A second FILE_SET gets them
# installed without adding them to what is standalone-compiled;
# INTERFACE_HEADER_SETS_TO_VERIFY, which otherwise defaults to *every*
# interface header set, keeps the verification list exactly as it was.
#
# As it happens every detail/ header compiles standalone today (morph#574 made
# the last one that did not, quantity_equation.hpp, self-contained). That is a
# convenience, not the rule this exclusion encodes, which is why
# scripts/test_check_install_export.sh plants its own broken header rather than
# leaning on one of these staying broken.
#
# Adding a detail/ header here is easy to forget, and forgetting is silent:
# `cmake --install` still exits 0, and the drift guard above skips detail/ by
# design. That is how morph#540 shipped -- `forms/detail/session_common.hpp`
# was added with morph#515 and never listed, so an installed
# `morph/forms/app.hpp`, `flows.hpp` and `sections.hpp` could not be compiled
# at all. What catches it now is not vigilance here but
# scripts/check_install_export.sh, whose consumer TU includes every installed
# *public* header generated from the prefix: a public header that needs an
# uninstalled detail/ header fails to compile there, whatever this list says.
target_sources(morph
INTERFACE
FILE_SET morph_detail_headers TYPE HEADERS
BASE_DIRS include
FILES
include/morph/detail/fixed_string.hpp
include/morph/detail/quantity_equation.hpp
include/morph/detail/reflected_member.hpp
include/morph/core/detail/completion_awaiter.hpp
include/morph/core/detail/task_handler.hpp
include/morph/core/detail/instance_directory.hpp
include/morph/core/detail/owned_state.hpp
include/morph/core/detail/owner_affinity.hpp
include/morph/reactive/detail/graph.hpp
include/morph/core/detail/owner_probe.hpp
include/morph/core/detail/reply_router.hpp
include/morph/core/detail/subscription_registry.hpp
include/morph/forms/detail/session_common.hpp
include/morph/forms/detail/schema_name.hpp
)
set_target_properties(morph PROPERTIES INTERFACE_HEADER_SETS_TO_VERIFY HEADERS)
# ── Guard: every public header must belong to some target's FILE_SET ─────────
#
# VERIFY_INTERFACE_HEADER_SETS compiles each *listed* header standalone and is
# silent about the rest, so a header omitted from a FILE_SET is not checked
# loosely -- it is not checked at all, while the target keeps reporting success.
# That is the "control that reports success while measuring nothing" shape
# AGENTS.md names, and it had already happened here: 13 public headers were
# unlisted when morph#230 was filed, and a 14th (payload_schema.hpp) was added
# and left unlisted before the issue was worked. A hand-maintained list drifts;
# this turns the drift into a configure error.
#
# Two kinds of header are deliberately out of scope:
# * anything under a detail/ directory -- not public surface;
# * headers owned by an optional component whose target this configuration
# did not create (morph::net, morph::qt, morph::qt_forms,
# morph::offline_sqlite). Those have their own FILE_SETs and now carry
# VERIFY_INTERFACE_HEADER_SETS themselves, so they are genuinely checked
# whenever their option is ON -- they were not before morph#230, which is
# the same blindness one level down. The mapping below is by *component*,
# not by header,
# so adding a header to an existing component still gets caught -- which is
# the drift that actually happens.
set(_morph_optional_components
"include/morph/net/:morph_net"
"include/morph/qt/forms/:morph_qt_forms"
"include/morph/qt/:morph_qt"
"include/morph/offline/sqlite_offline_queue.hpp:morph_offline_sqlite")
file(GLOB_RECURSE _morph_public_headers
RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}"
CONFIGURE_DEPENDS "include/morph/*.hpp")
# Union of every header set this configuration actually created.
set(_morph_listed_headers "")
foreach(_morph_target IN ITEMS morph morph_net morph_qt morph_qt_forms morph_offline_sqlite)
if(TARGET ${_morph_target})
get_target_property(_morph_target_headers ${_morph_target} HEADER_SET)
if(_morph_target_headers)
list(APPEND _morph_listed_headers ${_morph_target_headers})
endif()
endif()
endforeach()
set(_morph_unlisted_headers "")
foreach(_morph_header IN LISTS _morph_public_headers)
if(_morph_header MATCHES "(^|/)detail/")
continue()
endif()
# Skip a header whose optional component was not configured.
set(_morph_skip FALSE)
foreach(_morph_entry IN LISTS _morph_optional_components)
string(REGEX REPLACE "^(.*):([^:]+)$" "\\1" _morph_prefix "${_morph_entry}")
string(REGEX REPLACE "^(.*):([^:]+)$" "\\2" _morph_owner "${_morph_entry}")
if(_morph_header MATCHES "^${_morph_prefix}" AND NOT TARGET ${_morph_owner})
set(_morph_skip TRUE)
break()
endif()
endforeach()
if(_morph_skip)
continue()
endif()
if(NOT "${CMAKE_CURRENT_SOURCE_DIR}/${_morph_header}" IN_LIST _morph_listed_headers)
list(APPEND _morph_unlisted_headers "${_morph_header}")
endif()
endforeach()
if(_morph_unlisted_headers)
string(REPLACE ";" "\n " _morph_unlisted_pretty "${_morph_unlisted_headers}")
message(FATAL_ERROR
"morph: these public headers belong to no target's FILE_SET HEADERS, so "
"VERIFY_INTERFACE_HEADER_SETS never compiles them standalone and would keep "
"passing if they stopped being self-contained (morph#230):\n "
"${_morph_unlisted_pretty}\n"
"Add each to the FILES list of the target that owns it, or move it under a "
"detail/ directory if it is not public surface.")
endif()
unset(_morph_public_headers)
unset(_morph_listed_headers)
unset(_morph_unlisted_headers)
unset(_morph_target_headers)
unset(_morph_optional_components)
# ── SQLite-backed durable offline queue: library target (optional) ──────────
# Only the morph::offline_sqlite target itself is created here, deliberately
# ahead of "Application ladder"'s add_subdirectory(examples) further below:
# examples/kanban/CMakeLists.txt links morph::offline_sqlite (a namespaced
# target, which CMake requires to already exist at the point it is named --
# see the identical note on morph::qt_forms vs the plain
# morph_forms_moduleplugin just below). The tests/offline_sqlite subdirectory
# that also depends on MORPH_BUILD_OFFLINE_SQLITE stays deferred to its
# original spot, after the "Tests" section, since it links
# Catch2::Catch2WithMain and that target only exists once Catch2 has been
# found/fetched there.
if(MORPH_BUILD_OFFLINE_SQLITE)
find_package(SQLite3 REQUIRED)
# CMake's FindSQLite3 locates the header by searching for sqlite3.h, and on
# macOS finds it in the SDK's *bare* /usr/include -- the whole C library's
# include directory, not a SQLite-specific prefix. Propagating that through
# an imported target's INTERFACE_INCLUDE_DIRECTORIES turns it into
# `-isystem <sdk>/usr/include`, which puts it *ahead of libc++* in the
# search order. Every translation unit that reaches a SQLite header then
# compiles the C library's own headers without their prerequisites:
#
# .../MacOSX.sdk/usr/include/_types.h:46:9:
# error: unknown type name '__uint32_t'
#
# AppleClang is unaffected, because that directory is already where it
# looks; only a non-Apple clang breaks -- which is the compiler this
# project's own -Weverything guidance points at, so `morph_offline_sqlite`
# could not be built by the developers most likely to build it (morph#172).
#
# The repair is to *drop* the directory, not to relocate it: sqlite3.h is
# already on the compiler's default search path via -isysroot, at the
# correct position, so it resolves with no flag at all. Measured both ways
# on Homebrew clang 22 -- the same TU compiles clean with no flag and
# produces 20 errors with the -isystem.
#
# Scoped to a directory that is a C-library catch-all (detected by stdio.h
# living in it), so a real SQLite prefix -- Homebrew's, MacPorts', or one
# given explicitly via -DSQLite3_INCLUDE_DIR -- is still honoured.
function(_morph_strip_catchall_includes in_var out_var)
set(_kept "")
foreach(_dir IN LISTS ${in_var})
if(APPLE AND EXISTS "${_dir}/stdio.h" AND EXISTS "${_dir}/sqlite3.h")
message(STATUS
"morph: dropping catch-all SQLite3 include dir '${_dir}' -- it is the C library's own "
"include directory, and forcing it ahead of libc++ breaks every TU that reaches a "
"SQLite header (morph#172). sqlite3.h still resolves via the SDK default search path.")
else()
list(APPEND _kept "${_dir}")
endif()
endforeach()
set(${out_var} "${_kept}" PARENT_SCOPE)
endfunction()
_morph_strip_catchall_includes(SQLite3_INCLUDE_DIRS _morph_sqlite_includes)
# FindSQLite3 reports success but does not always leave `SQLite3::SQLite3`
# resolvable at generate time (observed on the ubuntu-24.04 runner, which
# fails with "the link interface ... contains SQLite3::SQLite3 but the
# target was not found" while printing "Found SQLite3" moments earlier).
# Synthesise the target from the variables the module *does* set, and
# promote it to GLOBAL either way so every subdirectory that consumes
# morph::offline_sqlite can see it.
if(NOT TARGET SQLite3::SQLite3)
add_library(SQLite3::SQLite3 UNKNOWN IMPORTED GLOBAL)
set_target_properties(SQLite3::SQLite3 PROPERTIES
IMPORTED_LOCATION "${SQLite3_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${_morph_sqlite_includes}")
# IMPORTED_LOCATION takes exactly one path to a library file, which is
# what SQLite3_LIBRARY (singular) is. SQLite3_LIBRARIES (plural) is
# FindSQLite3's link-requirements variable and can carry extra
# entries beyond the library itself (e.g. `dl` on platforms where
# SQLite needs libdl) -- a shape that only INTERFACE_LINK_LIBRARIES
# accepts. Thread anything beyond the library path itself through
# there instead of concatenating it into IMPORTED_LOCATION, which
# only resolves a single file (morph#440).
set(_morph_sqlite_extra_libs "${SQLite3_LIBRARIES}")
if(_morph_sqlite_extra_libs)
list(REMOVE_ITEM _morph_sqlite_extra_libs "${SQLite3_LIBRARY}")
endif()
if(_morph_sqlite_extra_libs)
set_target_properties(SQLite3::SQLite3 PROPERTIES
INTERFACE_LINK_LIBRARIES "${_morph_sqlite_extra_libs}")
endif()
else()
set_target_properties(SQLite3::SQLite3 PROPERTIES IMPORTED_GLOBAL TRUE)
# The module built the target itself, so strip the same directory from
# the interface it populated -- otherwise the filter above only covers
# the branch where we synthesise the target.
get_target_property(_morph_sqlite_target_includes SQLite3::SQLite3 INTERFACE_INCLUDE_DIRECTORIES)
if(_morph_sqlite_target_includes)
_morph_strip_catchall_includes(_morph_sqlite_target_includes _morph_sqlite_target_kept)
set_target_properties(SQLite3::SQLite3 PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_morph_sqlite_target_kept}")
endif()
endif()
add_library(morph_offline_sqlite INTERFACE)
add_library(morph::offline_sqlite ALIAS morph_offline_sqlite)
# Same standalone-compile check the core target gets: a listed header that
# stops being self-contained must fail here, not silently pass (morph#230).
set_target_properties(morph_offline_sqlite PROPERTIES VERIFY_INTERFACE_HEADER_SETS ON)
target_link_libraries(morph_offline_sqlite INTERFACE morph SQLite3::SQLite3)
target_sources(morph_offline_sqlite
INTERFACE
FILE_SET HEADERS
BASE_DIRS include
FILES
include/morph/offline/sqlite_offline_queue.hpp
)
endif()
# ── Qt/QML forms renderer setup (optional) ───────────────────────────────────
# Ships the reference Qt/QML renderer (MorphForms, src/qt/forms) as a reusable
# component, independent of MORPH_BUILD_EXAMPLES. examples/forms/gui_qml
# (added from examples/forms/CMakeLists.txt, itself still gated on
# MORPH_BUILD_EXAMPLES, added further below in "Demo executable") is a
# *consumer* of this module, not its home -- the find_package/
# qt_standard_project_setup() call and the morph::qt_forms interface target
# below must both exist before the "Demo executable" section runs, since that
# demo's CMakeLists.txt relies on qt_add_library/qt_add_qml_module/
# qt_add_executable already being defined and links against morph::qt_forms
# (a namespaced target, which -- unlike a plain target name such as
# morph_forms_moduleplugin below -- CMake requires to already exist at the
# point it is named). add_subdirectory(src/qt/forms) itself (which builds the
# actual MorphForms module/plugin, plus a Catch2-linked test executable) is
# deferred to just after the "Tests" section further below, since Catch2 is
# only found/fetched there and its test executable names Catch2::Catch2
# directly.
# 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
# one clear configure-time message instead of a confusing missing-member
# compile error deep in the build (Ubuntu 24.04 still ships 6.4.2).
find_package(Qt6 6.5 REQUIRED COMPONENTS Core Gui Qml Quick)
qt_standard_project_setup(REQUIRES 6.5)
# Header-only, model-agnostic controller core (no Q_OBJECT -- Qt cannot
# register a class template for QML); an app's own QObject/QML_ELEMENT
# controller subclass forwards to it. See
# include/morph/qt/forms/forms_controller_core.hpp.
add_library(morph_qt_forms INTERFACE)
add_library(morph::qt_forms ALIAS morph_qt_forms)
# Same standalone-compile check the core target gets: a listed header that
# stops being self-contained must fail here, not silently pass (morph#230).
set_target_properties(morph_qt_forms PROPERTIES VERIFY_INTERFACE_HEADER_SETS ON)
target_link_libraries(morph_qt_forms INTERFACE morph Qt6::Core)
# qt_executor.hpp and generic_model_bridge_core.hpp are listed here too,
# not only under morph::qt: forms_controller_core.hpp includes both, and
# morph::qt (MORPH_BUILD_QT, which needs Qt WebSockets) is not part of
# every install that ships this header. Neither needs anything beyond
# QtCore. multi_model_bridge_core.hpp/multi_model_forms_controller_core.hpp
# and the owned_local_bridge.hpp detail header they (and the single-model
# pair) share are the same story.
target_sources(morph_qt_forms
INTERFACE
FILE_SET HEADERS
BASE_DIRS include
FILES
include/morph/qt/bridge/detail/owned_local_bridge.hpp
include/morph/qt/bridge/generic_model_bridge_core.hpp
include/morph/qt/bridge/multi_model_bridge_core.hpp
include/morph/qt/forms/forms_controller_core.hpp
include/morph/qt/forms/multi_model_forms_controller_core.hpp
include/morph/qt/qt_executor.hpp
)
endif()
# ── Demo executable ──────────────────────────────────────────────────────────
if(MORPH_BUILD_EXAMPLES)
# The console demo uses the thread-pool executor; skip it for the
# single-threaded WASM build (which only wants the bank GUI target).
if(NOT EMSCRIPTEN)
add_executable(morph_example src/main.cpp)
target_link_libraries(morph_example PRIVATE morph)
apply_warnings(morph_example)
if(DEFINED AF_SANITIZER)
apply_sanitizers(morph_example ${AF_SANITIZER})
endif()
if(AF_COVERAGE)
apply_coverage(morph_example)
endif()
add_subdirectory(examples/forms)
add_subdirectory(examples/concepts)
endif()
if(MORPH_BUILD_BANK_EXAMPLE)
add_subdirectory(examples/bank)
endif()
if(MORPH_BUILD_HMAC_EXAMPLES)
add_subdirectory(examples/vetted_hmac)
endif()
endif()
# ── Tests ────────────────────────────────────────────────────────────────────
if(MORPH_BUILD_TESTS)
find_package(Catch2 CONFIG QUIET)
if(NOT Catch2_FOUND)
morph_use_cpm(Catch2)
CPMAddPackage(
NAME Catch2
GITHUB_REPOSITORY catchorg/Catch2
GIT_TAG v3.8.1)
# Catch2 hands its `extras/` (Catch.cmake, catch_discover_tests) to
# its parent scope, which under CPM is the CPMAddPackage function
# rather than this directory; tests/ includes Catch from here.
list(APPEND CMAKE_MODULE_PATH "${Catch2_SOURCE_DIR}/extras")
endif()
# ── The `--log-level` gate, shared by every Catch2 suite ────────────────
#
# morph::log used to default to LogLevel::debug, so a plain `morph_tests`
# run wrote 2,949 lines of framework tracing to stderr against 23 lines of
# actual Catch2 output. The library default is now `warn`; these two targets
# are what take a *test* run down to silent while keeping the stream one
# switch away.
#
# A Catch2 event listener would have been less machinery, but listeners are
# constructed after the command line is parsed and cannot add an option to
# it. So every suite owns its `main` (morph_test_main, or its own
# QCoreApplication-owning one) and routes through the same helper.
#
# Defined here rather than in tests/CMakeLists.txt because
# examples/{concepts,bank,vetted_hmac} are add_subdirectory()'d *above*,
# before this Tests block runs, and link these targets;
# target_link_libraries() resolution is deferred to generate time, which is
# the same property those directories already rely on for
# Catch2::Catch2WithMain (see the note in examples/concepts/CMakeLists.txt).
add_library(morph_test_log_level INTERFACE)
target_include_directories(morph_test_log_level INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/tests)
target_link_libraries(morph_test_log_level INTERFACE morph::morph Catch2::Catch2)
# On Windows a failed assert(), an abort() or a crash in a test opens a
# modal dialog (CRT assert, abort, Windows Error Reporting) and waits for a
# click: under ctest nobody clicks, so the test holds the run until its
# timeout, and on a desktop it lands on the user's screen. core-cpp's
# core::testing_dialogs is one object whose static initialiser calls
# core::testing::suppressWindowsDialogs() before main() runs: CRT reports
# and abort()'s message go to stderr, abort() asks for no fault report,
# Windows Error Reporting shows no UI, and the process exits, so the test
# fails loudly instead. It is linked into test executables only, never into
# morph or the example applications.
#
# Every Catch2 suite reaches it through morph_test_log_level, which each of
# them links (through morph_test_main or directly, for the ones that own a
# QCoreApplication main). An OBJECT library's objects reach only the target
# that links it directly, so they are named as an interface link item too,
# as core-cpp's own core::testing_main does. Test executables without
# Catch2 call morph_suppress_test_dialogs() themselves.
function(morph_suppress_test_dialogs target)
if(WIN32 AND TARGET core-cpp-testing_dialogs)
target_link_libraries(${target} PRIVATE core::testing_dialogs)
endif()
endfunction()
if(WIN32 AND TARGET core-cpp-testing_dialogs)
target_link_libraries(morph_test_log_level INTERFACE
core::testing_dialogs "$<TARGET_OBJECTS:core-cpp-testing_dialogs>")
endif()
# STATIC, not OBJECT: the only symbol it carries is `main`, which is always
# an undefined symbol in the linking executable, so the archive member is
# always extracted. (An OBJECT library would work too; a listener-based
# design would have *needed* one, since a static initializer with nothing
# referencing it is exactly what a linker is free to drop.)
add_library(morph_test_main STATIC tests/testkit/catch_main.cpp)
target_link_libraries(morph_test_main PUBLIC morph_test_log_level)
apply_warnings(morph_test_main)
# Deliberately NOT apply_sanitizers(). That helper adds -fsanitize to both
# compile *and* link options as PRIVATE, and PRIVATE link options do not
# propagate out of a static library -- so instrumenting this one left
# `__asan_init`/`__ubsan_handle_type_mismatch_v1` undefined in every
# consumer that is not itself sanitized, which is how the ladder's ASan leg
# failed to link. A partially-instrumented binary is fine (no system
# library is instrumented either), and an eight-line `main` is not where a
# sanitizer finding was ever going to come from. Catch2::Catch2WithMain,
# which this replaced, was likewise an uninstrumented prebuilt archive.
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 CPM 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
# test executable directly against Catch2::Catch2 when MORPH_BUILD_TESTS is
# ON -- the same condition that guards both. Qt6 Quick/Qml was already found
# and morph::qt_forms already created earlier (before "Demo executable"), so
# 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)
add_subdirectory(src/qt/forms)
endif()
# ── Fuzz harnesses (optional) ────────────────────────────────────────────────
if(MORPH_BUILD_FUZZERS)
add_subdirectory(tests/fuzz)
endif()
# ── Soak + load/latency benchmark targets (optional) ─────────────────────────
if(MORPH_BUILD_LOAD_TESTS)
if(NOT MORPH_BUILD_TESTS)
message(FATAL_ERROR
"MORPH_BUILD_LOAD_TESTS requires MORPH_BUILD_TESTS=ON (the soak/bench "
"targets link Catch2, fetched under that option).")
endif()
add_subdirectory(tests/soak)
add_subdirectory(tests/bench)
endif()
# ── Qt WebSocket backend (optional) ─────────────────────────────────────────
if(MORPH_BUILD_QT)
find_package(Qt6 COMPONENTS WebSockets REQUIRED)
# Header-only interface for consumers
add_library(morph_qt INTERFACE)
add_library(morph::qt ALIAS morph_qt)
# Same standalone-compile check the core target gets: a listed header that
# stops being self-contained must fail here, not silently pass (morph#230).
set_target_properties(morph_qt PROPERTIES VERIFY_INTERFACE_HEADER_SETS ON)
target_link_libraries(morph_qt INTERFACE morph Qt6::WebSockets)
target_sources(morph_qt
INTERFACE
FILE_SET HEADERS
BASE_DIRS include
FILES
include/morph/qt/qt_executor.hpp
include/morph/qt/qt_tls.hpp
include/morph/qt/qt_websocket_server.hpp
include/morph/qt/qt_websocket_backend.hpp
)
# Compiled implementation (QObject subclass needs MOC via a .cpp file)
add_library(morph_qt_impl STATIC
src/qt/qt_websocket_server.cpp
src/qt/qt_websocket_backend.cpp
include/morph/qt/qt_websocket_server.hpp
)
target_link_libraries(morph_qt_impl PUBLIC morph_qt)
set_target_properties(morph_qt_impl PROPERTIES AUTOMOC ON)
# The Qt WebSocket server and backend are the only *compiled* translation
# units in morph's own transport: everything else a Qt test exercises is
# header-only and so gets instrumented with the test target. Left out, a
# sanitizer leg watched every caller of this code and none of the code
# (morph#542).
#
# Safe here, unlike morph_test_main above, because every consumer of this
# archive is itself instrumented on a sanitizer preset -- the ladder
# targets, examples/common, examples/bank, tests/qt,
# tests/net_qt_interop and examples/qt_tls_client all carry an
# AF_SANITIZER block. apply_sanitizers()'s link options are PRIVATE and do
# not propagate out of a static library, so a consumer that did not would
# fail to link with `__asan_init` undefined rather than quietly lose its
# instrumentation. (The WASM spike links it too and is never built with
# AF_SANITIZER defined -- Emscripten has no such preset here.)
if(DEFINED AF_SANITIZER)
apply_sanitizers(morph_qt_impl ${AF_SANITIZER})
endif()
if(MORPH_BUILD_TESTS)
add_subdirectory(tests/qt)
endif()
if(MORPH_BUILD_EXAMPLES)
add_subdirectory(examples/qt_tls_client)
endif()
endif()
# ── morph::net raw-socket WebSocket transport (optional, POSIX only) ───────
if(MORPH_BUILD_NET)
if(WIN32)
message(WARNING "MORPH_BUILD_NET is ignored on Windows: morph::net is POSIX-only "
"(BSD sockets) today; Winsock2 support is documented future work.")
else()
add_library(morph_net INTERFACE)
add_library(morph::net ALIAS morph_net)
# Same standalone-compile check the core target gets: a listed header that
# stops being self-contained must fail here, not silently pass (morph#230).
set_target_properties(morph_net PROPERTIES VERIFY_INTERFACE_HEADER_SETS ON)
target_link_libraries(morph_net INTERFACE morph)
target_sources(morph_net
INTERFACE
FILE_SET HEADERS
BASE_DIRS include