Repository navigation
Expand file tree
/
Copy pathci.sh
More file actions
executable file
·2181 lines (2073 loc) · 110 KB
/
Copy pathci.sh
File metadata and controls
executable file
·2181 lines (2073 loc) · 110 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
#!/usr/bin/env bash
# ci.sh — full aether_ui test pipeline as a CI job would run it.
#
# The AetherUIDriver control server is OUT of app builds by default (a listening
# socket must not ship in release binaries); the spec/CI pipeline opts in so the
# HTTP-driven tests work. This MUST be exported before any `aeb` build below.
export AETHER_UI_WITH_DRIVER=1
#
# Phases:
# 1. Build every example (catches C/Aether compile regressions).
# 2. Smoke-launch the non-driver examples to catch runtime crashes the
# HTTP-driven tests can't reach (widget wiring, reactive state init,
# platform-backend regressions). Each binary is launched, given 1.5s
# to render, then killed; still-alive = pass.
# 3. Launch example_calculator with the AetherUIDriver test server and
# run test_calculator.sh (11 assertions).
# 4. Launch example_testable and run test_automation.sh (17 assertions).
#
# Alongside those, Phase 1c3 runs the headless UI-logic tests under
# tests/<name>/: .ae programs that assert against ui/module.ae and exit
# non-zero, with no window and no driver. Everything else here needs a real
# window, so they are the only invariants a display-less box can check.
#
# Platform handling:
# macOS — runs directly (AppKit).
# Linux — runs directly if $DISPLAY or $WAYLAND_DISPLAY is set; otherwise
# auto-wraps with xvfb-run when available. Falls back to build-only.
# Windows — native Win32 backend. Aether-level examples are skipped (the
# DSL has a separate module-resolution issue on MINGW that
# blocks `ae build`). The C-level backend test suite
# (tests/test_widgets.c) and the HTTP driver test
# (tests/test_driver.sh) run instead — invoked via
# `make contrib-aether-ui-check`.
#
# Exits non-zero only when an implemented platform fails. Leaves no
# background processes.
#
# Usage: ./ci.sh [port]
set -u
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$SCRIPT_DIR"
# The driver port: $1, else $AETHER_UI_TEST_PORT, else 9222 (a box whose 9222
# is taken -- an `adb forward tcp:9222` to a phone -- picks another). Apps
# bind the AETHER_UI_TEST_PORT they are launched with, whatever port they
# pass enable_test_server (aether_ui_test_server_start), and the specs talk
# to it (run_spec.sh exports its numeric $1 for tests/lib/uidriver.ae).
PORT="${1:-${AETHER_UI_TEST_PORT:-9222}}"
cd "$ROOT"
mkdir -p build
# --- Aether toolchain resolution guard -----------------------------------
# aeb resolves the SDK include AND lib dirs relative to `dirname(command -v ae)`
# WITHOUT dereferencing symlinks. If `ae` is a symlinked shim beside a stale/
# partial ~/.local tree, aeb links the wrong libaether.a (an old one with no
# std.audio/std.worker → `undefined reference to aether_audio_*`) and/or misses
# the nested headers (`fatal error: aether_panic.h`). Point $AETHER at the
# *dereferenced* real binary so aeb's dir resolution lands on the actual
# versioned install for both halves; also pin $AETHER_INCLUDE (which aeb honors
# first) as a backstop. No-op on a canonical /usr/local install where `ae` is
# not a symlink; only applied when the resolved tree is real (has runtime/).
_ae_bin="$(command -v "${AETHER:-ae}" 2>/dev/null || true)"
if [ -n "$_ae_bin" ]; then
_ae_real="$(readlink -f "$_ae_bin" 2>/dev/null || true)"
[ -n "$_ae_real" ] || _ae_real="$_ae_bin" # BSD readlink lacks -f
_ae_root="$(dirname "$(dirname "$_ae_real")")"
if [ -x "$_ae_real" ] && [ -d "$_ae_root/include/aether/runtime" ]; then
[ -z "${AETHER:-}" ] && export AETHER="$_ae_real"
[ -z "${AETHER_INCLUDE:-}" ] && export AETHER_INCLUDE="$_ae_root/include/aether"
echo " aeb toolchain pinned: AETHER=$AETHER"
fi
fi
# -------------------------------------------------------------------------
# All examples that must compile in Phase 1.
EXAMPLES=(aecs_dark_demo disclosure_demo icons_demo pills_demo textpath_demo counter form picker styled system canvas testable calculator context_menu overlay_demo vg_tooltip each_demo rebuild_demo fileicon_demo scrollbg_demo scrollwide_demo keyhandler_demo imagefill_demo filedrop_demo barfill_demo listbox_demo table_demo transitions_demo split_demo bindings_demo tabs_demo scrollbar_demo secondary_demo typography_demo menu rbind_demo typo_demo multiselect_demo selmode_demo dblclick_demo tree_demo tabledeleg_demo weightclamp_demo flexround_demo shortcut_demo polish_demo vlist_demo wshortcut_demo multiwindow_demo timer_demo frametick_demo background_demo canvasscroll_demo canvasclip_demo canvasresetclip_demo styledbg_demo gpuview_demo gpuanim_demo native_view_demo panelcanvas_demo resizecb_demo quit_demo panelsize_demo pinsize_demo cssbatch_demo insets_demo blitborrow_demo groupalpha_demo hoverpaint_demo gradspread_demo placeholder_demo multikey_demo sheet_demo winmenu_demo reorder_demo reorderable_demo overlaytr_demo a11y_demo material_demo themes_demo csssem_demo zen_demo states_demo undo_demo roles_demo command_demo clipboard window_title openurl_demo prompt_demo)
# Examples without a test server — Phase 2 smoke-launches each.
# calculator and testable are exercised through their HTTP drivers in
# Phases 3-4, so they are not smoke-tested here.
SMOKE_EXAMPLES=(disclosure_demo icons_demo pills_demo textpath_demo counter form picker styled system canvas)
FAIL=0
OS="$(uname -s)"
case "$OS" in
Darwin) PLATFORM=macos ;;
Linux) PLATFORM=linux ;;
# The GTK4 backend on FreeBSD (ghostbsd): the Linux path, run under an
# Xvfb the caller starts (FreeBSD's Xvfb ships without xvfb-run).
FreeBSD) PLATFORM=linux ;;
MINGW*|MSYS*|CYGWIN*) PLATFORM=windows ;;
*) PLATFORM=unknown ;;
esac
# The host C compiler for the unit tests' links: $CC when set, else gcc
# (Linux CI, macOS's clang shim), else cc (FreeBSD has cc, not gcc).
HOST_CC="${CC:-gcc}"
command -v "$HOST_CC" >/dev/null 2>&1 || HOST_CC=cc
echo "=== aether_ui CI on $OS ($PLATFORM) ==="
# A crashing driver app leaves no explanation in its own output. Allow cores so
# a failing spec can print the faulting frame instead of a list of POSTs that
# stopped being answered. Each workflow step is a fresh shell, so this has to
# be set here rather than beside the core_pattern in the workflow.
if [ "$PLATFORM" = "linux" ]; then ulimit -c unlimited 2>/dev/null || true; fi
if [ "$PLATFORM" = "windows" ]; then
echo "NOTICE: Windows backend uses a separate test flow."
echo " Run: make contrib-aether-ui-check"
echo " (headless widget suite + HTTP driver integration test)"
exit 0
fi
if [ "$PLATFORM" = "unknown" ]; then
echo "ERROR: unrecognized platform '$OS'."
exit 1
fi
# Decide how to launch GUI binaries. On Linux CI runners without a display,
# wrap with xvfb-run so GTK4 has a framebuffer. The screen must be big enough
# that Xvfb's pointer (parked at screen centre) starts OUTSIDE the app
# window: GTK synthesizes crossing/motion events at the pointer position on
# every relayout, and a pointer sitting over the canvas fires the app's hover
# handler between test steps — rewriting the status line the assertions read.
launch_xvfb() { xvfb-run -a -s "-screen 0 3200x2000x24" "$@"; }
# Kill a launched app and everything under it. Through launch_xvfb, $! is the
# subshell running the function, so a plain kill left xvfb-run, its Xvfb and
# the app itself running: every smoke and driver phase leaked one app, and a
# full run's leftovers ran a 2 GB container out of memory (cc1 killed in the
# next build). pgrep -P is on Linux, macOS and MSYS2 alike.
tree_pids() {
local p="$1" c
for c in $(pgrep -P "$p" 2>/dev/null); do tree_pids "$c"; done
echo "$p"
}
# TERM first, then KILL what is left: an app that outlives TERM keeps
# xvfb-run (a /bin/sh script, which cannot act on TERM while it waits on
# that app) and its Xvfb alive too -- ae-x64 kept one of each per smoke app
# per run until this. Once the app is gone, xvfb-run reaps its own Xvfb.
kill_tree() {
local pids
pids="$(tree_pids "$1")"
kill $pids 2>/dev/null
sleep 0.3
kill -9 $pids 2>/dev/null
return 0
}
LAUNCH_PREFIX=""
if [ "$PLATFORM" = "linux" ]; then
if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]; then
if command -v xvfb-run > /dev/null 2>&1; then
LAUNCH_PREFIX="launch_xvfb"
echo "no display detected — wrapping GUI launches with xvfb-run"
else
echo "NOTICE: no display and xvfb-run missing — will build-only."
LAUNCH_PREFIX="SKIP_RUNTIME"
fi
fi
fi
# Run a binary that is expected to END ITSELF, and report its exit status.
#
# Neither `timeout` nor a bare launch works across this matrix: macOS ships no
# `timeout`, and GTK4 needs a framebuffer even in headless mode, so Linux has
# to go through $LAUNCH_PREFIX like every other phase. This waits on the
# process and only kills it if it overruns, so "never quit" is reported as 124
# the way `timeout` would have.
run_self_quitting() {
local bin="$1" name="$2" limit="${3:-30}"
local log="/tmp/ci_${name}.selfquit.log"
# Headless ONLY when there is no display to use. GTK4's headless path
# realizes without mapping, so nothing is ever allocated and get_width
# answers 0 — which is exactly what this reported on Linux. Under xvfb
# ($LAUNCH_PREFIX set) the window maps and the allocation is real. On
# macOS there is no xvfb, and headless is what keeps a local run from
# popping windows; allocation works there either way.
# A GTK box with a display of its own (DISPLAY already set, as on the
# FreeBSD lane, which starts its own Xvfb) maps the window too; headless
# there read every width as 0 (panelsize_demo, insets_demo).
if [ -n "${LAUNCH_PREFIX:-}" ]; then
$LAUNCH_PREFIX "$bin" > "$log" 2>&1 &
elif [ "$PLATFORM" = linux ] && [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]; then
"$bin" > "$log" 2>&1 &
else
AETHER_UI_HEADLESS=1 "$bin" > "$log" 2>&1 &
fi
local pid=$!
local ticks=0
while kill -0 "$pid" 2>/dev/null; do
if [ "$ticks" -ge "$((limit * 10))" ]; then
kill_tree "$pid"
wait "$pid" 2>/dev/null
return 124
fi
sleep 0.1
ticks=$((ticks + 1))
done
wait "$pid"
}
run_server_test() {
# Launch a binary with AETHER_UI_TEST_PORT set, wait for the test server,
# run the given test script against it, kill the binary, propagate status.
local bin="$1" script="$2" name="$3"
echo "--- launching $bin ---"
# Refuse to launch into a squatted port: a stray app from an earlier
# run answers the spec's requests and every assertion interrogates the
# WRONG process (stale binary, mutated state) — seen as inexplicable
# geometry failures. Fail loudly instead.
if curl -sf -o /dev/null "http://127.0.0.1:$PORT/widgets" 2>/dev/null; then
echo " FAIL: $name — port $PORT already answering (stray app?)"
return 1
fi
AETHER_UI_TEST_PORT="$PORT" $LAUNCH_PREFIX "$bin" > "/tmp/ci_${name}.app.log" 2>&1 &
local pid=$!
# Wait up to 6s for the server to come up.
local up=0
for _ in $(seq 1 30); do
if curl -sf -o /dev/null "http://127.0.0.1:$PORT/widgets"; then up=1; break; fi
sleep 0.2
done
if [ "$up" -ne 1 ]; then
echo " FAIL: $name test server never responded"
kill_tree "$pid"; wait "$pid" 2>/dev/null
tail -20 "/tmp/ci_${name}.app.log" | sed 's/^/ /'
return 1
fi
"$script" "$PORT"
local rc=$?
# Close the app PROPERLY: ask the driver to shut down (the app exits by
# the same path as the user closing the window). Signal-killing the
# xvfb-run wrapper is only a fallback — it can leave the app child alive
# and HOLDING THE PORT, and the next phase then interrogates the wrong
# app (a whole family of "impossible" test failures traced back to this).
curl -sf -m 2 -X POST "http://127.0.0.1:$PORT/shutdown" > /dev/null 2>&1
local freed=0
for _ in $(seq 1 25); do
if ! curl -sf -o /dev/null "http://127.0.0.1:$PORT/widgets"; then freed=1; break; fi
sleep 0.2
done
kill_tree "$pid"; wait "$pid" 2>/dev/null
if [ "$freed" -ne 1 ]; then
pkill -f "$bin" 2>/dev/null
for _ in $(seq 1 25); do
curl -sf -o /dev/null "http://127.0.0.1:$PORT/widgets" || break
sleep 0.2
done
fi
# A spec that fails AFTER the server answered leaves its diagnosis in the
# app's own output, which was printed only on the never-responded path.
# Without this, a mid-run crash reads as a list of "was not 2xx" lines
# with nothing saying the process had died.
if [ "$rc" -ne 0 ] && [ -s "/tmp/ci_${name}.app.log" ]; then
echo " --- $name app output (last 30 lines) ---"
tail -30 "/tmp/ci_${name}.app.log" | sed 's/^/ /'
fi
# A crash (SIGBUS/SIGSEGV) leaves nothing useful in the app's own output:
# the process is gone before it can say anything, and the spec only sees
# POSTs that stopped being answered. macOS files a report naming the
# faulting frame, so surface it here rather than leaving the next reader
# to guess from "was not 2xx".
if [ "$rc" -ne 0 ] && [ "$PLATFORM" = "linux" ] && command -v gdb > /dev/null 2>&1; then
local core
core="$(ls -t /tmp/core."$(basename "$bin")".* 2>/dev/null | head -1)"
if [ -n "$core" ]; then
echo " --- $name backtrace from $core ---"
gdb -batch -ex 'thread apply all bt' "$bin" "$core" 2>/dev/null \
| head -60 | sed 's/^/ /'
rm -f "$core"
fi
fi
if [ "$rc" -ne 0 ] && [ "$PLATFORM" = "macos" ]; then
local base report
base="$(basename "$bin")"
report="$(ls -t "$HOME/Library/Logs/DiagnosticReports/${base}"*.ips 2>/dev/null | head -1)"
if [ -n "$report" ]; then
echo " --- $name crash report: $(basename "$report") ---"
sed -n '1,80p' "$report" | sed 's/^/ /'
fi
fi
return $rc
}
run_smoke_test() {
# Launch a GUI binary, give it 1.5s to open its window, then kill it.
# Pass iff the process is still alive at the deadline. A process that
# exited early is a crash (missing widget impl, null deref on init,
# backend API mismatch) — propagate non-zero and dump the tail.
local bin="$1" name="$2"
$LAUNCH_PREFIX "$bin" > "/tmp/ci_smoke_${name}.log" 2>&1 &
local pid=$!
sleep 1.5
if kill -0 "$pid" 2>/dev/null; then
echo " OK $name (alive 1.5s)"
kill_tree "$pid"; wait "$pid" 2>/dev/null
return 0
fi
wait "$pid" 2>/dev/null; local rc=$?
echo " FAIL $name (exited early, rc=$rc)"
tail -15 "/tmp/ci_smoke_${name}.log" | sed 's/^/ /'
return 1
}
# AeVG port unit tests — pure Aether (no GTK/display), so they run even
# under SKIP_RUNTIME. Each is a self-contained `main()` that exits non-zero
# on the first failed assertion. Append new modules' tests here as they land.
AEVG_TESTS=(test_font test_text_path test_transform test_normalizer test_easing test_parser test_bbox test_region test_blur test_rasterize test_grammar_utils test_chrome test_grammar_context test_grammar_element test_grammar_rendering test_grammar_style test_grammar_shapes test_grammar_factories test_grammar_animations test_loader test_grammar_defs test_grammar_text test_grammar_css test_grammar_events test_path_builder test_polypath test_vg3d test_render_as_raster test_grammar_bind test_grammar_reactive test_gradient_transform test_font_family test_refresh test_reactive_bindpos test_backend_dispatch test_raster_roundtrip test_filter_routing test_gradient_fill test_vg test_transpiler test_grammar_interaction test_vg_interactive test_vg_when test_vg_bindto test_vg_bindpos test_vg_clock test_vg_hidpi test_vg_anim test_live_region test_effects test_behavior test_base64 test_vg_image test_vg_live_paint)
# Tests that exercise the REAL cairo text metrics — linked against the GTK4
# backend (the pure-Aether AEVG_TESTS link with $(ae cflags) only).
AEVG_GTK_TESTS=(test_text_metrics test_group_pixels)
# test_group_pixels rasterizes through the GTK backend, so it needs a display;
# test_text_metrics only measures text and does not. Running the display-bound
# one bare is what failed a headless runner with "Failed to open display" even
# though the workflow installs xvfb.
AEVG_GTK_NEEDS_DISPLAY=" test_group_pixels "
# `ae cflags --libs` emits the transitive deps that libaether.a was
# built with (PCRE2 / OpenSSL / zlib / nghttp2 — see Aether CHANGELOG
# [current], the cmd_cflags fix). So `$(ae cflags)` alone is enough
# for any module that imports std.regex / std.cryptography / std.http
# / std.zlib. The earlier per-dep workaround is no longer needed.
# ---------------------------------------------------------------------------
# Guard: every spec must RETURN its run_summary verdict.
#
# Since aether v0.613.0, run_summary returns the verdict (0 green / 1 failed)
# rather than exit()-ing, so that cleanup after a run -- arena.destroy,
# server_stop, sqlite.close -- is not skipped on exactly the failing runs where
# a leaked arena or an orphaned listener does the most damage. run_server_test
# below judges a spec purely by its process exit status, and main()'s return
# value IS that status.
#
# So a bare `spec.run_summary(fw)` as the last statement discards the verdict
# and A FAILING SUITE REPORTS GREEN. That is the worst failure mode a test
# harness has: silence that looks like success.
#
# This was already documented in ci.yml, in capitals, directly above the pin.
# A comment did not stop it happening anyway -- 83 specs were converted to the
# bare form in one commit and CI could not have told anyone. Hence a check
# rather than a note.
echo "=== Phase 0a: specs return their run_summary verdict ==="
bare_verdict=$(grep -rn --include='spec_*.ae' -E '^[[:space:]]*spec\.run_summary\(' \
"$ROOT/tests" "$ROOT/vg/test" 2>/dev/null || true)
if [ -n "$bare_verdict" ]; then
echo " FAIL: these specs drop the verdict, so a failing run would report green:"
echo "$bare_verdict" | sed 's/^/ /'
echo " use: return spec.run_summary(fw)"
FAIL=$((FAIL + 1))
else
echo " OK every spec returns its verdict"
fi
echo
echo "=== Phase 0: AeVG unit tests (pure Aether) ==="
for t in "${AEVG_TESTS[@]}"; do
src="vg/test/${t}.ae"
cfile="build/aevg_${t}.c"
bin="build/aevg_${t}"
if ! aetherc --lib "$ROOT" "$src" "$cfile" > "/tmp/ci_aevg_${t}.log" 2>&1; then
echo " FAIL $t (compile)"
tail -15 "/tmp/ci_aevg_${t}.log" | sed 's/^/ /'
FAIL=$((FAIL + 1))
continue
fi
# vg/module.ae declares the cairo text-metric externs; pure-Aether tests
# link no GTK backend, so a zero-returning stub resolves those symbols
# (tests importing vg never call them — test_text_metrics uses the real
# backend via AEVG_GTK_TESTS below).
if ! "$HOST_CC" "$cfile" vg/test/text_metrics_stub.c $(ae cflags) -o "$bin" >> "/tmp/ci_aevg_${t}.log" 2>&1; then
echo " FAIL $t (link)"
tail -15 "/tmp/ci_aevg_${t}.log" | sed 's/^/ /'
FAIL=$((FAIL + 1))
continue
fi
if "$bin" > "/tmp/ci_aevg_${t}_run.log" 2>&1; then
echo " OK $t"
else
echo " FAIL $t (run)"
tail -15 "/tmp/ci_aevg_${t}_run.log" | sed 's/^/ /'
FAIL=$((FAIL + 1))
fi
done
# GTK-backend-linked unit tests: those exercising the real cairo text
# metrics (aether_ui_text_measure etc.) need the GTK4 backend + gtk4 libs
# linked, unlike the pure-Aether tests above. Skipped when GTK is absent
# (e.g. build-only runners); pure metrics have no display dependency, so
# they run even under SKIP_RUNTIME as long as gtk4 dev libs are present.
if pkg-config --exists gtk4 2>/dev/null; then
for t in "${AEVG_GTK_TESTS[@]}"; do
src="vg/test/${t}.ae"; cfile="build/aevg_${t}.c"; bin="build/aevg_${t}"
if ! aetherc --lib "$ROOT" "$src" "$cfile" > "/tmp/ci_aevg_${t}.log" 2>&1; then
echo " FAIL $t (compile)"; tail -15 "/tmp/ci_aevg_${t}.log" | sed 's/^/ /'; FAIL=$((FAIL + 1)); continue
fi
# epoxy explicitly: gpuview (#92) calls GL from the GTK4 backend, and
# gtk4.pc exposes neither epoxy's headers nor its library.
if ! "$HOST_CC" $(pkg-config --cflags gtk4) $(pkg-config --cflags epoxy) "$cfile" \
backend/aether_ui_gtk4.c backend/aether_ui_system_extras.c backend/aether_ui_sni.c \
backend/aether_ui_test_server.c \
$(ae cflags) -pthread -lm $(pkg-config --libs gtk4) $(pkg-config --libs epoxy) -o "$bin" >> "/tmp/ci_aevg_${t}.log" 2>&1; then
echo " FAIL $t (link)"; tail -15 "/tmp/ci_aevg_${t}.log" | sed 's/^/ /'; FAIL=$((FAIL + 1)); continue
fi
runner=""
case "$AEVG_GTK_NEEDS_DISPLAY" in
*" $t "*)
if [ "$LAUNCH_PREFIX" = "SKIP_RUNTIME" ]; then
echo " SKIP $t (needs a display; none available and no xvfb-run)"
continue
fi
runner="$LAUNCH_PREFIX"
# Xvfb runs need the cairo renderer (GTK's NGL on llvmpipe
# churns memory), the same reason the GUI spec phases set it.
case "$runner" in *xvfb*) export GSK_RENDERER=cairo ;; esac
;;
esac
if $runner "$bin" > "/tmp/ci_aevg_${t}_run.log" 2>&1; then
echo " OK $t (gtk-linked)"
else
echo " FAIL $t (run)"; tail -15 "/tmp/ci_aevg_${t}_run.log" | sed 's/^/ /'; FAIL=$((FAIL + 1))
fi
unset GSK_RENDERER
done
else
echo " SKIP AEVG_GTK_TESTS (gtk4 dev libs absent)"
fi
# vg/chart (the Chartist port) unit tests: Chartist's own specs ported with
# their expected values, pure Aether like the AeVG tests above (they draw
# through vg only onto the recording backend, so the same text-metrics stub
# links them). They live beside the module in vg/chart/test.
CHART_TESTS=(test_chart_core test_chart_path test_chart_charts test_chart_render)
echo
echo "=== Phase 0: vg/chart unit tests (Chartist's specs, ported) ==="
for t in "${CHART_TESTS[@]}"; do
src="vg/chart/test/${t}.ae"; cfile="build/chart_${t}.c"; bin="build/chart_${t}"
if ! aetherc --lib "$ROOT" "$src" "$cfile" > "/tmp/ci_chart_${t}.log" 2>&1; then
echo " FAIL $t (compile)"; tail -15 "/tmp/ci_chart_${t}.log" | sed 's/^/ /'; FAIL=$((FAIL + 1)); continue
fi
if ! "$HOST_CC" "$cfile" vg/test/text_metrics_stub.c $(ae cflags) -o "$bin" >> "/tmp/ci_chart_${t}.log" 2>&1; then
echo " FAIL $t (link)"; tail -15 "/tmp/ci_chart_${t}.log" | sed 's/^/ /'; FAIL=$((FAIL + 1)); continue
fi
if "$bin" > "/tmp/ci_chart_${t}_run.log" 2>&1; then
echo " OK $t ($(tail -1 "/tmp/ci_chart_${t}_run.log" | sed 's/^[^:]*: //'))"
else
echo " FAIL $t (run)"; grep "FAIL" "/tmp/ci_chart_${t}_run.log" | head -15 | sed 's/^/ /'; FAIL=$((FAIL + 1))
fi
done
# App-level pure engine tests — game logic factored out of an app with NO UI
# or vg dependency, so it's unit-testable in isolation (the svg_tetris engine:
# collision, rotation, line-clear, ghost, scoring, game-over). Compiled with
# the app's own dir on the lib path so `import <engine>` resolves; links
# libaether only (no backend, no text-metrics stub — nothing imports vg).
ENGINE_TESTS=("svg_tetris:test_tetris_engine" "falling_blocks:test_fb_engine" "rubiks_cube:test_cube_engine" "font_picker:test_picker_engine" "maerkdown:test_mdown")
for spec in "${ENGINE_TESTS[@]}"; do
app="${spec%%:*}"; t="${spec##*:}"
src="apps/${app}/${t}.ae"; cfile="build/eng_${t}.c"; bin="build/eng_${t}"
if ! aetherc --lib "apps/${app}" "$src" "$cfile" > "/tmp/ci_eng_${t}.log" 2>&1; then
echo " FAIL $t (compile)"; tail -15 "/tmp/ci_eng_${t}.log" | sed 's/^/ /'; FAIL=$((FAIL + 1)); continue
fi
if ! "$HOST_CC" "$cfile" $(ae cflags) -o "$bin" >> "/tmp/ci_eng_${t}.log" 2>&1; then
echo " FAIL $t (link)"; tail -15 "/tmp/ci_eng_${t}.log" | sed 's/^/ /'; FAIL=$((FAIL + 1)); continue
fi
if "$bin" > "/tmp/ci_eng_${t}_run.log" 2>&1; then
echo " OK $t"
else
echo " FAIL $t (run)"; tail -15 "/tmp/ci_eng_${t}_run.log" | sed 's/^/ /'; FAIL=$((FAIL + 1))
fi
done
echo
echo "=== Phase 1: build all aether_ui examples (aeb fan-out) ==="
# Each example is a per-app .build.ae node under examples/<app>/; the root
# .all.ae scans + deps them, so this one command builds all 11 (cached,
# parallel). Binaries land at target/build/examples/<app>/bin/<app>; EX_BIN
# resolves that for the smoke/driver phases below.
EX_BIN() { echo "$ROOT/target/build/examples/$1/bin/$1"; }
if ( cd "$ROOT" && aeb .all.ae ) > /tmp/ci_build_all.log 2>&1; then
for ex in "${EXAMPLES[@]}"; do
if [ -x "$(EX_BIN "$ex")" ]; then echo " OK $ex"; else
echo " FAIL $ex (no binary)"; FAIL=$((FAIL + 1)); fi
done
else
echo " FAIL: aeb .all.ae fan-out build failed"
# The fan-out prints one progress line per app, so a plain tail is all
# progress and no diagnostic. A bare grep is not enough either: a linker
# error names the missing symbol on the lines AFTER the match, so print
# trailing context or the actual cause is lost (that is how an undefined
# symbol reached CI as four unattributed line numbers).
#
# And not make's own lines: under the parallel scheduler every node that
# fails costs one `make: *** [bldr.mk:N: label] Error 1`, printed BEFORE
# aeb's per-node "FAILED (see <log>)" dump, so a change that breaks every
# node (a compiler bump) filled the 60 lines with 60 make lines and the
# actual diagnostic never reached the CI log (#147).
grep -nEi -A6 "error|failed|undefined|cannot |no such file" /tmp/ci_build_all.log \
| grep -viE "^[0-9]+[:-] *build: |make: \*\*\*" | head -60 | sed 's/^/ /'
echo " --- last 60 lines ---"
tail -60 /tmp/ci_build_all.log | sed 's/^/ /'
FAIL=$((FAIL + 1))
fi
# Contrib-dependent apps are deliberately outside the .all.ae fan-out: they link
# archives (libaether_sqlite, libaether_avcodec) that a clean checkout does not
# have, so including them would make every fresh clone and every CI run red for
# a missing optional dependency. Build them only when the archive is actually
# present, and say plainly when they are skipped rather than passing silently.
CONTRIB_LIB_DIR="$(ae cflags --libs 2>/dev/null | tr ' ' '\n' | grep -E '^-L' | head -1 | sed 's/^-L//')"
LISMUSIC_BUILT=0
echo "=== Phase 1b: contrib apps (built only when their archives exist) ==="
contrib_app() {
app="$1"; archive="$2"
if [ -n "$CONTRIB_LIB_DIR" ] && [ -f "$CONTRIB_LIB_DIR/lib$archive.a" ]; then
if ( cd "$ROOT" && aeb "apps/$app/.build.contrib.ae" ) > "/tmp/ci_contrib_$app.log" 2>&1; then
echo " OK $app"
return 0
fi
echo " FAIL $app (lib$archive.a present but the build failed)"
tail -30 "/tmp/ci_contrib_$app.log" | sed 's/^/ /'
FAIL=$((FAIL + 1))
return 1
fi
echo " SKIP $app (lib$archive.a not installed; see roadmap.md for its manual build)"
return 1
}
contrib_app LisMusic aether_sqlite && LISMUSIC_BUILT=1
contrib_app video_frame aether_avcodec || true
# Phase 1.5: RUN the headless AeVG renderers (build was done by the .all.ae
# fan-out in Phase 1 — every vg app is a apps/<name>/ node now). The
# value here is RUNTIME coverage the build-only fan-out can't give: each writes
# a PNG, exercising the raster-blit + draw-region compose path. Linux/GTK only.
AEVG_BIN() { echo "$ROOT/target/build/apps/$1/bin/$1"; }
if [ "$PLATFORM" = "linux" ]; then
echo " --- AeVG headless renderers (run → PNG) ---"
run_png() { # $1=app $2=desc
local bin; bin="$(AEVG_BIN "$1")"
# HONOUR $LAUNCH_PREFIX like every other runtime phase. "Headless
# renderer" means no WINDOW -- GTK still needs a display connection to
# initialise, so on a DISPLAY-less box these died with
# "Gtk-WARNING: cannot open display" while every other phase was
# correctly wrapped in xvfb-run. Found by the first fresh-box run
# (asks/ci-sh-on-a-fresh-headless-box.md).
if [ "$LAUNCH_PREFIX" = "SKIP_RUNTIME" ]; then
echo " SKIP $1 ($2 — no display, no xvfb-run)"
return
fi
if [ -x "$bin" ] \
&& AEVG_OUT="/tmp/ci_$1.png" $LAUNCH_PREFIX "$bin" > "/tmp/ci_run_$1.log" 2>&1 \
&& [ -f "/tmp/ci_$1.png" ]; then
echo " OK $1 ($2)"
else
echo " FAIL $1"
tail -15 "/tmp/ci_run_$1.log" 2>/dev/null | sed 's/^/ /'
FAIL=$((FAIL + 1))
fi
}
run_png aevg_live_png "live raster + draw region composite"
run_png aevg_video_png "raw-RGBA clip in a live region"
run_png analog_clock_png "one-frame clock render"
fi
if [ "$FAIL" -gt 0 ]; then
echo
echo "=== CI result: $FAIL build failure(s) — skipping runtime phases ==="
exit 1
fi
if [ "$LAUNCH_PREFIX" = "SKIP_RUNTIME" ]; then
echo
echo "=== CI result: builds passed; runtime phases skipped (no display) ==="
exit 0
fi
echo
echo "=== Phase 1c: C-level widget suite (tests/test_widgets.c) ==="
# Nothing in this repo built this file, so it rotted unnoticed: a stale
# ../aether_ui_backend.h include (the header moved into backend/) and a 6-arg
# call to an 8-arg canvas_stroke ABI meant it had not compiled for a long
# while. Building it here is what keeps it honest. It links the same backend
# as any app, via build.sh, and runs headless.
if ./build.sh tests/test_widgets.c test_widgets > /tmp/ci_test_widgets_build.log 2>&1; then
# $LAUNCH_PREFIX, same as every other runtime phase. The suite sets
# AETHER_UI_HEADLESS itself, which keeps every modal from blocking, but
# GTK still needs a display to OPEN, so on Linux this goes through xvfb
# exactly like the smoke launches do.
if $LAUNCH_PREFIX ./build/test_widgets > /tmp/ci_test_widgets.log 2>&1; then
echo " OK $(tail -1 /tmp/ci_test_widgets.log)"
else
echo " FAIL test_widgets"
tail -20 /tmp/ci_test_widgets.log | sed 's/^/ /'
FAIL=$((FAIL + 1))
fi
else
echo " FAIL test_widgets (build)"
tail -20 /tmp/ci_test_widgets_build.log | sed 's/^/ /'
FAIL=$((FAIL + 1))
fi
echo
echo "=== Phase 1c3: headless UI-logic tests (tests/<name>/) ==="
# An .ae program that asserts against ui/module.ae and exits non-zero, with no
# window and no driver. Every other runtime phase needs a window: the driver
# specs serve their HTTP endpoint from a real one, so none of them can run on a
# display-less box, and none can run at all on a developer machine without
# opening one. A toolkit invariant that holds in pure Aether belongs here.
#
# Built by the .all.ae fan-out (it scans tests/**/.build.ae), so a compile
# failure has already been reported by Phase 1. This phase RUNS them.
UI_TESTS=(derived_views selection_survives tree_selection list_invariants platform_claims sort_keeps_model headless_canvas string_ownership undo_stack undo_group style_release ctxmenu_rebuild no_echo roundtrip keymap docs_snippets min_size state_batch background_for style_churn reorderable)
UT_BIN() { echo "$ROOT/target/build/tests/$1/bin/$1"; }
for ut in "${UI_TESTS[@]}"; do
bin="$(UT_BIN "$ut")"
if [ ! -x "$bin" ]; then
echo " FAIL $ut (not built)"
FAIL=$((FAIL + 1))
continue
fi
# Headless explicitly, like Phase 1c: no window is wanted, but GTK still
# needs a display to initialise, so $LAUNCH_PREFIX stays.
if AETHER_UI_HEADLESS=1 $LAUNCH_PREFIX "$bin" > "/tmp/ci_uitest_${ut}.log" 2>&1; then
echo " OK $ut ($(tail -1 "/tmp/ci_uitest_${ut}.log"))"
else
echo " FAIL $ut"
tail -20 "/tmp/ci_uitest_${ut}.log" | sed 's/^/ /'
FAIL=$((FAIL + 1))
fi
done
echo
echo "=== Phase 1d: Win32 backend cross-compile check ==="
# The Win32 backend is the one nobody here can run: there is no Windows leg in
# CI, so its sources were edited and shipped WITHOUT EVER BEING COMPILED. Eight
# commits touched aether_ui_win32.c in two days that way. A mingw-w64
# cross-compiler checks them for the price of an apt package and about a
# second, which is not a substitute for running the thing, but it is the
# difference between "mirrors the other backends" and "is known to build".
#
# -Wall -Werror. The gate deliberately ran at the default level while there
# was a backlog of pre-existing warnings, because failing a change for six
# warnings it did not introduce teaches people to ignore the gate. The backlog
# is zero now, so the ratchet closes: this is the one backend nobody here can
# run, and -Wall is the only reader it gets. It already earned its keep --
# -Wmisleading-indentation is what surfaced a gradient-stop path that clamped
# alpha and packed the other three components unclamped.
W32_CC=""
for cc in x86_64-w64-mingw32-gcc i686-w64-mingw32-gcc; do
command -v "$cc" > /dev/null 2>&1 && { W32_CC="$cc"; break; }
done
if [ -n "$W32_CC" ]; then
w32_fail=0
for src in backend/aether_ui_win32.c \
backend/aether_ui_test_server.c \
backend/aether_ui_system_extras.c; do
if "$W32_CC" -fsyntax-only -Wall -Werror -Ibackend "$ROOT/$src" \
> "/tmp/ci_w32_$(basename "$src").log" 2>&1; then
echo " OK $(basename "$src")"
else
echo " FAIL $(basename "$src")"
grep -E ": (error|warning):" "/tmp/ci_w32_$(basename "$src").log" | head -10 | sed 's/^/ /'
w32_fail=1
fi
done
# Syntax is not the whole story. A Windows API declared in a header whose
# IMPORT LIBRARY is missing compiles perfectly and fails at link, which is
# invisible to everyone who cannot build on Windows. Proven, not assumed:
# dropping -lole32 leaves the syntax check at 0 errors while the link
# reports __imp_CoCreateInstance and three more.
#
# The only thing between this and a real binary is the libaether runtime,
# which has no Windows build here; tests/win32/win32_runtime_test.c
# supplies it and says what to do when it stops being enough. That file is
# also the binary CI runs under Wine, so this link uses the same sources
# the runtime check does rather than a second stub that could drift.
if [ "$w32_fail" -eq 0 ]; then
if "$W32_CC" -o /tmp/ci_w32_link.exe \
"$ROOT/tests/win32/win32_runtime_test.c" \
"$ROOT/backend/aether_ui_win32.c" \
"$ROOT/backend/aether_ui_test_server.c" \
"$ROOT/backend/aether_ui_system_extras.c" \
-I"$ROOT/backend" \
-luser32 -lgdi32 -lgdiplus -lmsimg32 -lcomctl32 -lcomdlg32 \
-lshell32 -lole32 -loleaut32 -luuid -loleacc -ldwmapi \
-luxtheme -lws2_32 -lbcrypt > /tmp/ci_w32_link.log 2>&1; then
echo " OK links against the Windows import libraries"
else
echo " FAIL link"
grep -E "undefined reference|error:" /tmp/ci_w32_link.log \
| head -10 | sed 's/^/ /'
w32_fail=1
fi
fi
[ "$w32_fail" -eq 0 ] || FAIL=$((FAIL + 1))
else
# Say so rather than passing quietly: a skipped gate that looks like a
# green one is how the Win32 sources drifted in the first place.
echo " SKIP no mingw-w64 cross-compiler (install gcc-mingw-w64-x86-64 to enable)"
fi
echo
echo "=== Phase 1c2: backend ABI parity across all five backends ==="
# Compiling and linking a backend proves it builds; it proves nothing about
# what it OMITS. A missing entry point is invisible to every other phase,
# because nothing here calls it: the iOS lane links against a stub, and the
# Win32 lane only cross-compiles. Seven functions had drifted onto three
# backends and not the fourth before this existed, so an iOS app calling any
# of them would have failed at link with no earlier warning.
if python3 "$SCRIPT_DIR/tests/scripts/check_backend_parity.py"; then
echo " OK backend ABI parity"
else
echo " FAIL backend ABI parity"
FAIL=$((FAIL + 1))
fi
echo "=== Phase 1e: iOS/iPadOS (UIKit) backend compile+link check ==="
# The UIKit backend is, like Win32, one nobody here can RUN: there is no iOS leg
# in CI and no iOS build of libaether. But the iOS SDK ships with Xcode, so its
# sources can be COMPILED and LINKED against the real UIKit frameworks in a few
# seconds — the difference between "mirrors the other backends" and "is known to
# build". -Wall -Werror, the same ratchet Phase 1d holds the Win32 backend to.
# The shared driver/system sources are checked here too, since they must compile
# for iOS (BSD sockets + POSIX; no AppKit) just as they do for Windows.
IOS_SDK="$(xcrun --sdk iphonesimulator --show-sdk-path 2>/dev/null)"
if [ -n "$IOS_SDK" ] && [ -d "$IOS_SDK" ]; then
IOS_CLANG="$(xcrun --sdk iphonesimulator -f clang 2>/dev/null)"
IOS_TGT="arm64-apple-ios17.0-simulator"
ios_fail=0
for src in backend/aether_ui_uikit.m \
backend/aether_ui_test_server.c \
backend/aether_ui_system_extras.c; do
if "$IOS_CLANG" -fobjc-arc -fsyntax-only -Wall -Werror \
-target "$IOS_TGT" -isysroot "$IOS_SDK" -Ibackend "$ROOT/$src" \
> "/tmp/ci_ios_$(basename "$src").log" 2>&1; then
echo " OK $(basename "$src")"
else
echo " FAIL $(basename "$src")"
grep -E ": (error|warning):" "/tmp/ci_ios_$(basename "$src").log" \
| head -10 | sed 's/^/ /'
ios_fail=1
fi
done
# Syntax is not the whole story (Phase 1d's lesson): a framework whose symbols
# are missing compiles fine and fails at link. tests/ios/link_stub.c supplies
# the handful of libaether symbols the backend reads (there is no iOS build of
# libaether here) plus a main(), so the link proves every -framework resolves.
if [ "$ios_fail" -eq 0 ]; then
if "$IOS_CLANG" -fobjc-arc -target "$IOS_TGT" -isysroot "$IOS_SDK" -Ibackend \
"$ROOT/backend/aether_ui_uikit.m" \
"$ROOT/backend/aether_ui_test_server.c" \
"$ROOT/backend/aether_ui_system_extras.c" \
"$ROOT/tests/ios/link_stub.c" \
-framework UIKit -framework Foundation -framework QuartzCore \
-framework CoreGraphics -framework CoreText -framework ImageIO -framework UserNotifications \
-o /tmp/ci_ios_link > /tmp/ci_ios_link.log 2>&1; then
echo " OK links against the UIKit frameworks"
else
echo " FAIL link"
grep -iE "undefined|error:" /tmp/ci_ios_link.log | head -10 | sed 's/^/ /'
ios_fail=1
fi
fi
# Compiling and linking prove it builds; they prove nothing about the
# pixels. UIKit runs natively on macOS via Mac Catalyst, so drive the REAL
# canvas ABI (aether_ui_uikit.m's Core Graphics executor), read the pixels
# back with canvas_read_pixel and assert the colours — no simulator, device
# or iOS build of libaether. This is what turns "is known to build" into "is
# known to render". See tests/ios/render_probe.m. (macOS-host only.)
if [ "$ios_fail" -eq 0 ]; then
MAC_SDK="$(xcrun --sdk macosx --show-sdk-path 2>/dev/null)"
CAT_FW="$MAC_SDK/System/iOSSupport/System/Library/Frameworks"
MAC_CLANG="$(xcrun --sdk macosx -f clang 2>/dev/null)"
if [ -n "$MAC_SDK" ] && [ -d "$CAT_FW" ]; then
if "$MAC_CLANG" -fobjc-arc -target "$(uname -m)-apple-ios15.0-macabi" \
-isysroot "$MAC_SDK" -iframework "$CAT_FW" -Ibackend \
"$ROOT/tests/ios/render_probe.m" \
"$ROOT/backend/aether_ui_uikit.m" \
"$ROOT/backend/aether_ui_test_server.c" \
"$ROOT/backend/aether_ui_system_extras.c" \
-framework UIKit -framework Foundation -framework QuartzCore \
-framework CoreGraphics -framework CoreText -framework ImageIO -framework UserNotifications \
-o /tmp/ci_ios_render > /tmp/ci_ios_render.log 2>&1 \
&& AETHER_UI_RENDER_PROBE_PNG=/tmp/ci_ios_render.png \
/tmp/ci_ios_render >> /tmp/ci_ios_render.log 2>&1; then
echo " OK canvas renders (pixels checked via Mac Catalyst)"
else
echo " FAIL render probe"
grep -iE "FAIL|error:|undefined" /tmp/ci_ios_render.log \
| head -10 | sed 's/^/ /'
ios_fail=1
fi
else
echo " SKIP render probe (no Mac Catalyst SDK on this host)"
fi
fi
[ "$ios_fail" -eq 0 ] || FAIL=$((FAIL + 1))
else
# Say so rather than passing quietly, exactly as Phase 1d does for mingw.
echo " SKIP no iOS SDK (install Xcode + the iOS platform to enable)"
fi
echo
echo "=== Phase 1e2: iOS simulator run (listbox_demo through the driver) ==="
# Compiling, linking and a Catalyst render prove the backend builds and
# paints; they say nothing about an app RUNNING on iOS. `ae build
# --target=<arch>-ios-simulator --emit=staticlib` (aether 0.6xx, the
# prerequisite #22 named) builds the Aether runtime for the simulator, so an
# example can now be linked for real -- its portable C from aetherc, the
# UIKit backend, the driver -- wrapped in a bundle (tests/ios/Info.plist),
# installed in a booted simulator and driven over the same AetherUIDriver
# port every other backend answers: tests/listbox_demo's spec runs against
# it unchanged. Rows rendered by UIKit, a tap selecting one through the
# DSL's closure, selection surviving an update: that is #22's acceptance,
# on every push.
#
# The simulator shares the host's loopback, so the spec talks to 127.0.0.1
# as usual. Environment reaches the app through SIMCTL_CHILD_*.
IOS_SIM_UDID=""
ios_sim_pick() {
# An available iPhone, booted if one already is (a stale boot is fine to
# reuse), else the first listed. Prints the UDID.
xcrun simctl list devices available -j 2>/dev/null | jq -r '
[.devices | to_entries[]
| select(.key | contains("iOS"))
| .value[] | select(.name | startswith("iPhone"))]
| (map(select(.state == "Booted")) + .) | .[0].udid // empty'
}
# What the simulator knows when the app did not answer: is the process
# alive (a simulator app is a host process), what it logged, and whether it
# left a crash report -- the exception and the thread that raised it are in
# the report's first lines.
ios_sim_diagnose() {
echo " -- process:"
pgrep -fl AetherUIProbe 2>/dev/null | sed 's/^/ /' || echo " (not running)"
echo " -- app stdout/stderr:"
tail -20 /tmp/ci_ios_sim.app.log /tmp/ci_ios_sim.app.err 2>/dev/null | sed 's/^/ /'
echo " -- unified log (last 2 minutes, the app's process):"
xcrun simctl spawn "$IOS_SIM_UDID" log show --last 2m --style compact \
--predicate 'process == "AetherUIProbe"' 2>/dev/null | tail -40 | sed 's/^/ /'
echo " -- crash reports:"
local rep
rep="$(ls -t ~/Library/Logs/DiagnosticReports/AetherUIProbe* 2>/dev/null | head -1)"
if [ -n "$rep" ]; then
echo " $rep"
# .ips: a JSON header line, then the JSON report. The exception, the
# uncaught NSException's own message and backtrace if there was
# one (that is where an out-of-range index was raised), and the
# faulting thread's frames, each with its image.
python3 - "$rep" <<'PY' 2>&1 | sed 's/^/ /'
import json, sys
raw = open(sys.argv[1]).read().split('\n', 1)[1]
r = json.loads(raw)
imgs = r.get('usedImages', [])
def frame(f):
i = f.get('imageIndex', -1)
img = imgs[i].get('name', '?') if 0 <= i < len(imgs) else '?'
return '%-28s %s %s%s' % (img, f.get('symbol', '+%d' % f.get('imageOffset', 0)),
f.get('sourceFile', ''), (':%d' % f['sourceLine']) if 'sourceLine' in f else '')
print('exception:', json.dumps(r.get('exception')))
print('termination:', json.dumps(r.get('termination')))
for k in ('asi', 'ktriageinfo'):
if r.get(k): print(k + ':', json.dumps(r[k])[:600])
leb = r.get('lastExceptionBacktrace')
if leb:
print('last exception backtrace:')
for f in leb[:25]: print(' ', frame(f))
ft = r.get('faultingThread', 0)
threads = r.get('threads', [])
if ft < len(threads):
t = threads[ft]
print('faulting thread %d %s %s:' % (ft, t.get('name', ''), t.get('queue', '')))
for f in t.get('frames', [])[:25]: print(' ', frame(f))
PY
else
echo " (none)"
fi
}
ios_sim_cleanup() {
[ -n "$IOS_SIM_UDID" ] || return 0
xcrun simctl terminate "$IOS_SIM_UDID" dev.aether.ui.simprobe > /dev/null 2>&1 || true
xcrun simctl uninstall "$IOS_SIM_UDID" dev.aether.ui.simprobe > /dev/null 2>&1 || true
xcrun simctl shutdown "$IOS_SIM_UDID" > /dev/null 2>&1 || true
}
if [ "$PLATFORM" != "macos" ] || [ -z "$IOS_SDK" ] || [ ! -d "$IOS_SDK" ]; then
echo " SKIP needs a Mac with the iOS simulator SDK"
elif [ "$ios_fail" -ne 0 ]; then
echo " SKIP Phase 1e failed"
elif ! ae build --help 2>&1 | grep -q "ios-simulator"; then
echo " SKIP this ae has no iOS target (aether #1385)"
else
sim_fail=0
SIM_DIR="$ROOT/build/ios"
SIM_APP="$SIM_DIR/AetherUIProbe.app"
rm -rf "$SIM_DIR"; mkdir -p "$SIM_APP"
case "$(uname -m)" in
arm64) SIM_AE_TARGET="aarch64-ios-simulator"; SIM_CLANG_TGT="arm64-apple-ios17.0-simulator" ;;
*) SIM_AE_TARGET="x86_64-ios-simulator"; SIM_CLANG_TGT="x86_64-apple-ios17.0-simulator" ;;
esac
# 1. The runtime, compiled for the simulator (see tests/ios/runtime_seed.ae).
if ! AETHER_IOS_MIN=17.0 ae build --target="$SIM_AE_TARGET" --emit=staticlib \
"$ROOT/tests/ios/runtime_seed.ae" -o "$SIM_DIR/libaether_sim.a" \
> /tmp/ci_ios_sim_runtime.log 2>&1; then
echo " FAIL runtime archive for $SIM_AE_TARGET"
tail -15 /tmp/ci_ios_sim_runtime.log | sed 's/^/ /'
sim_fail=1
else
echo " OK runtime archive for $SIM_AE_TARGET"
fi
# 2-5 for each example: listbox_demo (#22's acceptance: rows rendered by
# UIKit, a tap selecting one, selection surviving an update), then
# frametick_demo (the frame clock on CADisplayLink, pinned so a silent
# fallback to the timer fails, and timer_once), vg_image_demo and
# vgpaint_demo (vg's live paint and the right/double-click hooks on UIKit),
# spacercanvas_demo (spacers take a stack's slack before a canvas does),
# pixelated_demo (image_rendering "pixelated": CoreGraphics nearest),
# submit_demo (on_submit: Return in a field), datagrid_demo (the
# virtualised grid, with a vslider: a rotated UISlider in a box),
# a11y_demo (roles, names, and a11y_announce through UIAccessibility),
# vg_chart_demo (vg/chart's charts on UIKit's vg scenes).
for SIM_EX in listbox_demo frametick_demo vg_image_demo vgpaint_demo spacercanvas_demo pixelated_demo submit_demo datagrid_demo a11y_demo vg_chart_demo scrollbar_demo secondary_demo typography_demo; do
[ "$sim_fail" -eq 0 ] || break
SIM_FRAME_SOURCE=""
if [ "$SIM_EX" = frametick_demo ]; then SIM_FRAME_SOURCE=cadisplaylink; fi
rm -f "$SIM_APP/AetherUIProbe"
# 2. The example's portable C, exactly as build.sh emits it for the host.
if [ "$sim_fail" -eq 0 ] && ! aetherc "$ROOT/examples/$SIM_EX/$SIM_EX.ae" \
"$SIM_DIR/$SIM_EX.c" > /tmp/ci_ios_sim_aetherc.log 2>&1; then
echo " FAIL aetherc $SIM_EX"
tail -15 /tmp/ci_ios_sim_aetherc.log | sed 's/^/ /'
sim_fail=1
fi
# 3. Link it for the simulator: the app, the UIKit backend, the driver, the
# runtime archive, the frameworks. The same clang line as Phase 1e's
# link check, with the real runtime where tests/ios/link_stub.c stood.
if [ "$sim_fail" -eq 0 ]; then
if "$IOS_CLANG" -fobjc-arc -target "$SIM_CLANG_TGT" -isysroot "$IOS_SDK" \
$(ae cflags | tr ' ' '\n' | awk 'p{print;p=0;next} /^-idirafter$/{print;p=1;next} /^-I/{print}' | tr '\n' ' ') -Ibackend \
"$SIM_DIR/$SIM_EX.c" \
"$ROOT/backend/aether_ui_uikit.m" \
"$ROOT/backend/aether_ui_test_server.c" \
"$ROOT/backend/aether_ui_system_extras.c" \
"$SIM_DIR/libaether_sim.a" \
-framework UIKit -framework Foundation -framework QuartzCore \
-framework CoreGraphics -framework CoreText -framework ImageIO \
-framework UserNotifications -lpthread -lm \
-o "$SIM_APP/AetherUIProbe" > /tmp/ci_ios_sim_link.log 2>&1; then
echo " OK $SIM_EX links against the simulator runtime"
else
echo " FAIL link $SIM_EX for the simulator"
grep -iE "undefined|error:" /tmp/ci_ios_sim_link.log | head -15 | sed 's/^/ /'
sim_fail=1
fi
fi
# 4. The bundle, and a simulator to put it in.
if [ "$sim_fail" -eq 0 ]; then
cp "$ROOT/tests/ios/Info.plist" "$SIM_APP/Info.plist"
[ -n "$IOS_SIM_UDID" ] || IOS_SIM_UDID="$(ios_sim_pick)"
if [ -z "$IOS_SIM_UDID" ]; then
echo " FAIL no available iPhone simulator on this host"
xcrun simctl list devices available 2>&1 | head -20 | sed 's/^/ /'
sim_fail=1
fi
fi
if [ "$sim_fail" -eq 0 ]; then
xcrun simctl boot "$IOS_SIM_UDID" > /tmp/ci_ios_sim_boot.log 2>&1 || true
if ! xcrun simctl bootstatus "$IOS_SIM_UDID" -b >> /tmp/ci_ios_sim_boot.log 2>&1; then
echo " FAIL simulator $IOS_SIM_UDID did not boot"
tail -10 /tmp/ci_ios_sim_boot.log | sed 's/^/ /'
sim_fail=1
elif ! xcrun simctl install "$IOS_SIM_UDID" "$SIM_APP" > /tmp/ci_ios_sim_install.log 2>&1; then
echo " FAIL install into the simulator"
tail -10 /tmp/ci_ios_sim_install.log | sed 's/^/ /'
sim_fail=1
else
echo " OK booted $IOS_SIM_UDID and installed the bundle"
fi
fi
# 5. Launch with the driver armed, then run the listbox spec against it as
# Phase 5f does on the desktop.
if [ "$sim_fail" -eq 0 ]; then
if curl -sf -o /dev/null "http://127.0.0.1:$PORT/widgets" 2>/dev/null; then
echo " FAIL port $PORT already answering (stray app?)"
sim_fail=1
elif ! SIMCTL_CHILD_AETHER_UI_TEST_PORT="$PORT" \
SIMCTL_CHILD_AETHER_UI_NO_ANIMATION=1 \
xcrun simctl launch --stdout=/tmp/ci_ios_sim.app.log --stderr=/tmp/ci_ios_sim.app.err \
"$IOS_SIM_UDID" dev.aether.ui.simprobe > /tmp/ci_ios_sim_launch.log 2>&1; then
echo " FAIL launch"
tail -10 /tmp/ci_ios_sim_launch.log | sed 's/^/ /'
sim_fail=1
else
up=0
for _ in $(seq 1 200); do
if curl -sf -o /dev/null "http://127.0.0.1:$PORT/widgets"; then up=1; break; fi
sleep 0.2
done
if [ "$up" -ne 1 ]; then
echo " FAIL the app's driver server never answered from the simulator"
ios_sim_diagnose
sim_fail=1