ci: build the core library and the app for the iOS simulator, run a UI smoke test

Adds the ios-sim workflow, triggered only by pushes to f/ci-ios-sim and by workflow_dispatch.

The core job builds lib:simplex-chat with cabal using the flake's iOS settings (swift JSON, client library, static roll-up archive, threaded RTS, CommonCrypto), packages it as the Hydra zip, and patches it for the simulator with scripts/ios/prepare.sh and mac2ios. The app job builds "SimpleX (iOS)" for the arm64 simulator against it and drives onboarding, the chat list and the search field with AXe, saving screenshots and accessibility trees.

This first run tests the whole pipeline, mainly whether the core library builds and links.
This commit is contained in:
spacedandy-agents-simplex committed 2026-10-13 18:36:06 +00:00
1 parent e7678b634c
commit bcef840e99
4 files changed
+366

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
name: ios-sim
on:
push:
branches:
- f/ci-ios-sim
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ios-sim-${{ github.ref }}
cancel-in-progress: true
env:
GHC_VER: 9.6.3
CABAL_VER: 3.10.2.0
CORE_PKG: pkg-ios-aarch64-swift-json.zip
HOMEBREW_NO_AUTO_UPDATE: 1
HOMEBREW_NO_INSTALL_CLEANUP: 1
jobs:
core:
name: core library for the iOS simulator
runs-on: macos-latest
timeout-minutes: 240
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Show runner
run: |
sw_vers
sysctl -n hw.ncpu hw.memsize
df -h /
xcodebuild -version
- name: Restore core library
id: core-cache
uses: actions/cache/restore@v4
with:
path: ~/Downloads/${{ env.CORE_PKG }}
key: ios-sim-core-${{ hashFiles('src/**', 'cabal.project', 'simplex-chat.cabal', 'scripts/ci/ios-sim/build-core.sh') }}
- name: Setup Haskell
if: steps.core-cache.outputs.cache-hit != 'true'
uses: simplex-chat/setup-haskell-action@v2
with:
ghc-version: ${{ env.GHC_VER }}
cabal-version: ${{ env.CABAL_VER }}
- name: Restore cabal store
id: cabal-cache
if: steps.core-cache.outputs.cache-hit != 'true'
uses: actions/cache/restore@v4
with:
path: |
~/.cabal/store
dist-newstyle
key: ios-sim-ghc${{ env.GHC_VER }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }}-${{ github.run_id }}
restore-keys: |
ios-sim-ghc${{ env.GHC_VER }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }}-
ios-sim-ghc${{ env.GHC_VER }}-
macos-latest-ghc${{ env.GHC_VER }}-
- name: Install libraries
if: steps.core-cache.outputs.cache-hit != 'true'
run: brew install openssl@3 libffi gmp
- name: Build core library
id: build
if: steps.core-cache.outputs.cache-hit != 'true'
run: scripts/ci/ios-sim/build-core.sh ~/Downloads/${{ env.CORE_PKG }}
- name: Save cabal store
if: ${{ !cancelled() && steps.core-cache.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
with:
path: |
~/.cabal/store
dist-newstyle
key: ${{ steps.cabal-cache.outputs.cache-primary-key }}
- name: Save core library
if: steps.core-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: ~/Downloads/${{ env.CORE_PKG }}
key: ${{ steps.core-cache.outputs.cache-primary-key }}
- name: Patch core library for the simulator
run: scripts/ci/ios-sim/prepare-libs.sh
- name: Upload core library
uses: actions/upload-artifact@v4
with:
name: ios-core-aarch64
path: ~/Downloads/${{ env.CORE_PKG }}
compression-level: 0
retention-days: 3
if-no-files-found: error
app:
name: app in the iOS simulator
needs: core
runs-on: macos-latest
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Show runner
run: |
sw_vers
df -h /
xcodebuild -version
xcrun simctl list runtimes
- name: Download core library
uses: actions/download-artifact@v4
with:
name: ios-core-aarch64
path: ~/Downloads
- name: Prepare libraries
run: |
scripts/ci/ios-sim/prepare-libs.sh
scripts/ios/update-pbxproj.sh
- name: Build app
run: |
xcodebuild build -quiet \
-project apps/ios/SimpleX.xcodeproj \
-scheme "SimpleX (iOS)" \
-configuration Debug \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath "$RUNNER_TEMP/DerivedData" \
-onlyUsePackageVersionsFromResolvedFile \
-skipPackagePluginValidation \
-skipMacroValidation \
ARCHS=arm64 \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY=- \
DEVELOPMENT_TEAM= \
PROVISIONING_PROFILE_SPECIFIER=
- name: Install AXe
run: brew install cameroncooke/axe/axe
- name: Run smoke test
run: scripts/ci/ios-sim/smoke-test.sh "$RUNNER_TEMP/DerivedData/Build/Products/Debug-iphonesimulator/SimpleX.app" "$RUNNER_TEMP/smoke-test"
- name: Upload smoke test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: ios-sim-smoke-test
path: ${{ runner.temp }}/smoke-test
retention-days: 3
if-no-files-found: warn
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
# Builds the iOS core library package with cabal, as flake.nix builds aarch64-darwin-ios:lib:simplex-chat.
# Usage: scripts/ci/ios-sim/build-core.sh <output zip>
# The zip has the same files as the Hydra package that scripts/ios/prepare.sh expects.
set -euo pipefail
mkdir -p "$(dirname "$1")"
out="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
ghc_ver=$(ghc --numeric-version)
version=$(sed -n 's/^version: *//p' simplex-chat.cabal)
openssl_dir=$(brew --prefix openssl@3)
ffi_dir=$(brew --prefix libffi)
gmp_dir=$(brew --prefix gmp)
ghc --version
cabal --version
echo "rts extra-libraries: $(ghc-pkg field rts extra-libraries --simple-output)"
echo "ghc-bignum extra-libraries: $(ghc-pkg field ghc-bignum extra-libraries --simple-output)"
find "$(ghc --print-libdir)" -name 'lib*ffi*.a' -o -name 'libgmp*.a' -o -name 'libHSrts*_thr.a'
du -sh ~/.cabal/store dist-newstyle || true
cat > cabal.project.local <<EOF
ignore-project: False
package simplex-chat
flags: +swift +client_library
static: True
shared: False
ghc-options: -threaded -DIOS
package simplexmq
flags: +swift +client_library +commoncrypto
extra-include-dirs: $openssl_dir/include
extra-lib-dirs: $openssl_dir/lib
package direct-sqlcipher
flags: +commoncrypto
package entropy
flags: +DoNotGetEntropy
EOF
cabal build -j lib:simplex-chat
build_dir=dist-newstyle/build/aarch64-osx/ghc-$ghc_ver/simplex-chat-$version/build
ls -la "$build_dir"/libHSsimplex-chat-*
ghc_lib=$(ls "$build_dir"/libHSsimplex-chat-*-ghc"$ghc_ver".a)
echo "Selected symbols defined in $ghc_lib:"
{ nm -gU "$ghc_lib" 2>/dev/null || true; } \
| awk '$NF ~ /^_(ffi_call|__gmpz_init|hs_init_with_rtsopts|chat_migrate_init_key|EVP_CIPHER_CTX_new|sqlite3_key)$/ {print " " $NF}' \
| sort -u
pkg=$(mktemp -d)
cp "$build_dir"/libHSsimplex-chat-*.a "$ffi_dir"/lib/libffi.a "$gmp_dir"/lib/libgmp.a "$gmp_dir"/lib/libgmpxx.a "$pkg"
chmod u+w "$pkg"/*.a
for f in "$pkg"/libHSsimplex-chat-*.a; do
strip -x "$f"
done
ls -la "$pkg"
rm -f "$out"
(cd "$pkg" && zip -9 "$out" *.a)
ls -la "$out"
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# Builds mac2ios, runs scripts/ios/prepare.sh on ~/Downloads/pkg-ios-aarch64-swift-json.zip,
# and fails unless every object in apps/ios/Libraries/sim is marked for the iOS simulator.
set -euo pipefail
mac2ios_rev=4dcb77d5ea896d749381806dfab5358851b08951
tools=$(mktemp -d)
for f in mac2ios.c ar.h macho.h; do
curl --proto '=https' --tlsv1.2 -sSfL -o "$tools/$f" "https://raw.githubusercontent.com/zw3rk/mobile-core-tools/$mac2ios_rev/$f"
done
cc -O2 -o "$tools/mac2ios" "$tools/mac2ios.c"
PATH="$tools:$PATH" scripts/ios/prepare.sh
for f in apps/ios/Libraries/sim/*.a; do
echo "$f: $(du -h "$f" | cut -f1), $(lipo -info "$f")"
platforms=$({ otool -l "$f" || true; } | awk '$1 == "platform" {print $2}' | sort | uniq -c)
echo "${platforms:- no LC_BUILD_VERSION found}"
if echo "$platforms" | awk 'NF {print $2}' | grep -vE '^(7|IOSSIMULATOR)$' > /dev/null; then
echo "error: $f has objects for a platform other than the iOS simulator"
exit 1
fi
done
+114
View File
@@ -0,0 +1,114 @@
#!/bin/bash
# Installs the app in a new iPhone simulator and drives it with AXe: onboarding, chat list, typing in the search field.
# Usage: scripts/ci/ios-sim/smoke-test.sh <path to SimpleX.app> <output dir>
# Each step saves a screenshot and an accessibility tree to the output dir and prints the tree to the log.
set -euo pipefail
app=$1
out=$2
bundle_id=chat.simplex.app
search_text="ci smoke"
mkdir -p "$out"
axe --version
runtime=com.apple.CoreSimulator.SimRuntime.iOS-$(xcrun --sdk iphonesimulator --show-sdk-version | tr . -)
udid=$(xcrun simctl create ci-iphone com.apple.CoreSimulator.SimDeviceType.iPhone-17 "$runtime")
echo "simulator $udid, runtime $runtime"
xcrun simctl boot "$udid"
xcrun simctl bootstatus "$udid" -b
open -a Simulator --args -CurrentDeviceUDID "$udid"
xcrun simctl spawn "$udid" log stream --level debug --style compact --predicate 'process == "SimpleX"' > "$out/app.log" 2>&1 &
log_pid=$!
step=0
snap() {
step=$((step + 1))
local f
f=$(printf '%s/%02d-%s' "$out" "$step" "$1")
xcrun simctl io "$udid" screenshot "$f.png" > /dev/null
axe describe-ui --udid "$udid" > "$f.json"
echo "::group::accessibility tree: $1"
cat "$f.json"
echo "::endgroup::"
}
wait_for() {
local deadline=$((SECONDS + $2))
local tree="$out/wait.json"
until axe describe-ui --udid "$udid" > "$tree" && grep -qF "$1" "$tree"; do
if [ "$SECONDS" -ge "$deadline" ]; then
echo "error: \"$1\" not shown after $2 s"
return 1
fi
sleep 2
done
}
tap_label() {
axe tap --label "$1" --wait-timeout "$2" --post-delay 2 --udid "$udid"
}
finish() {
local status=$?
snap final || true
kill "$log_pid" || true
echo "::group::core start in the app log"
grep -E 'chatMigrateInit|startChat' "$out/app.log" || true
echo "::endgroup::"
echo "::group::app log, last 300 lines"
tail -n 300 "$out/app.log"
echo "::endgroup::"
find ~/Library/Logs/DiagnosticReports -name 'SimpleX*' -newer "$app" 2>/dev/null | while read -r report; do
echo "::group::crash report $report"
head -n 200 "$report"
echo "::endgroup::"
done || true
exit "$status"
}
trap finish EXIT
xcrun simctl install "$udid" "$app"
xcrun simctl launch "$udid" "$bundle_id"
wait_for "Get started" 180
snap first-screen
tap_label "Get started" 10
wait_for "Create profile" 30
tap_label "Enter profile name..." 10 || echo "warning: name field not found by label, typing into the focused field"
axe type 'citester' --udid "$udid"
snap profile-name
tap_label "Create profile" 10
wait_for "Continue" 60
snap your-network
tap_label "Continue" 10
wait_for "Accept" 30
snap network-commitments
tap_label "Accept" 10
sleep 5
snap after-onboarding
tap_label "Allow" 20 || echo "no notifications alert to allow"
wait_for "Search or paste SimpleX link" 60
snap chat-list
tap_label "Search or paste SimpleX link" 10 || axe tap --value "Search or paste SimpleX link" --post-delay 2 --udid "$udid"
axe type "$search_text" --udid "$udid"
sleep 2
snap search-typed
tree=$(ls "$out"/*-search-typed.json)
if ! grep -qF "$search_text" "$tree"; then
echo "error: \"$search_text\" is not in the accessibility tree after typing"
exit 1
fi
if ! grep -qF "Search in Directory" "$tree"; then
echo "error: no \"Search in Directory\" row after typing"
exit 1
fi
echo "smoke test passed: the chat list search field shows \"$search_text\""