From bcef840e99a5cbf628579124d6aea3bbf4979419 Mon Sep 17 00:00:00 2001 From: spacedandy-agents-simplex <342056592+spacedandy-agents-simplex@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:36:06 +0000 Subject: [PATCH] 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. --- .github/workflows/ios-sim.yml | 161 +++++++++++++++++++++++++++++ scripts/ci/ios-sim/build-core.sh | 66 ++++++++++++ scripts/ci/ios-sim/prepare-libs.sh | 25 +++++ scripts/ci/ios-sim/smoke-test.sh | 114 ++++++++++++++++++++ 4 files changed, 366 insertions(+) create mode 100644 .github/workflows/ios-sim.yml create mode 100755 scripts/ci/ios-sim/build-core.sh create mode 100755 scripts/ci/ios-sim/prepare-libs.sh create mode 100755 scripts/ci/ios-sim/smoke-test.sh diff --git a/.github/workflows/ios-sim.yml b/.github/workflows/ios-sim.yml new file mode 100644 index 0000000000..7db793a452 --- /dev/null +++ b/.github/workflows/ios-sim.yml @@ -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 diff --git a/scripts/ci/ios-sim/build-core.sh b/scripts/ci/ios-sim/build-core.sh new file mode 100755 index 0000000000..c34a9306f9 --- /dev/null +++ b/scripts/ci/ios-sim/build-core.sh @@ -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 +# 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 </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" diff --git a/scripts/ci/ios-sim/prepare-libs.sh b/scripts/ci/ios-sim/prepare-libs.sh new file mode 100755 index 0000000000..78287b5ccf --- /dev/null +++ b/scripts/ci/ios-sim/prepare-libs.sh @@ -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 diff --git a/scripts/ci/ios-sim/smoke-test.sh b/scripts/ci/ios-sim/smoke-test.sh new file mode 100755 index 0000000000..2ab51756d9 --- /dev/null +++ b/scripts/ci/ios-sim/smoke-test.sh @@ -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 +# 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\""