From ea93ee84c7a13305296fb9eb5eecc6a93a556a5e Mon Sep 17 00:00:00 2001 From: Justin Scroggins Date: Mon, 2 Nov 2026 19:08:55 -0500 Subject: [PATCH 1/3] Speed up CI: nextest, main-only release builds, one job per phone - Tests run under cargo-nextest (all binaries side by side) with a ci profile: no fail-fast, a retry that reports flaky tests, a hang timeout, and extra tries for the known flaky Telegram comments test. Doc tests run separately. - macOS tests the workspace once with the gui feature instead of twice. - macOS and Windows release builds run only on main; pull requests package Linux x86_64. Main also packages Linux aarch64 (tinyclient-linux-aarch64), without a cache. - The mobile host check moves into the Linux test job and shares its cache. - Mobile: one job per phone. iOS builds only the device slice as a staticlib; Android only the cdylib. Pull requests use the new ci-mobile profile (no LTO, 16 codegen units) and no cache; main and tags keep release. Co-Authored-By: Claude Opus 5.5 --- .config/nextest.toml | 19 ++++++ .github/actions/setup-rust/action.yml | 7 ++- .github/workflows/ci.yml | 57 ++++++++++------- .github/workflows/mobile.yml | 90 ++++++++------------------- Cargo.toml | 8 +++ docs/mobile-build.md | 8 +-- scripts/mobile/build-ios.sh | 43 ++++++++++--- 7 files changed, 130 insertions(+), 102 deletions(-) create mode 100644 .config/nextest.toml diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 00000000..3fdd14ee --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,19 @@ +# cargo-nextest runs each test in its own process and every test binary at once, where +# `cargo test` ran the binaries one after another. CI selects the `ci` profile through +# NEXTEST_PROFILE; a local `cargo nextest run` keeps nextest's defaults. + +[profile.ci] +# Run everything, so one failure does not hide the next. +fail-fast = false +# A test that fails and then passes is reported as FLAKY in the summary, not dropped. +retries = 1 +# A few call and MLS tests legitimately take a minute or more. Ten minutes is a hang. +slow-timeout = { period = "60s", terminate-after = 10 } +failure-output = "immediate-final" +success-output = "never" + +# Known to fail now and then on a loaded runner and pass on the next run. More tries here +# keep it from failing the build while it is looked into. +[[profile.ci.overrides]] +filter = 'binary_id(chatd::telegram) & test(=a_channel_posts_comments_count_rides_the_post)' +retries = 3 diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml index 7e823f2e..66e4c61e 100644 --- a/.github/actions/setup-rust/action.yml +++ b/.github/actions/setup-rust/action.yml @@ -3,7 +3,7 @@ description: Toolchain, protoc, system libraries, linker, and the cargo cache sh inputs: cache-key: - description: test or release. One cache per profile, shared by every job that builds it. + description: test or release. One cache per profile, shared by every job that builds it. none builds without a cache. required: true runs: @@ -37,8 +37,11 @@ runs: # each save is about a gigabyte, the repository holds 10 GB, and pull-request copies were # evicting the cache before the next build could use it. shared-key drops the job name so # CI and the release workflow restore the same entry. The v1 prefix starts clean of the - # caches from before the linker flags and the line-table profile. + # caches from before the linker flags and the line-table profile. The key also carries the + # runner's OS and CPU, so each platform keeps its own entry; a job off the critical path can + # pass none and leave the room to the ones that are on it. - uses: swatinem/rust-cache@v2 + if: inputs.cache-key != 'none' with: prefix-key: v1-rust shared-key: ${{ inputs.cache-key }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed503f11..fd2fbbc3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,8 @@ name: CI # Pull requests, and pushes to main. A branch that also has a pull request used to run this # twice. Tests and the release build run together: each used to wait for the other, and the -# release programs are what a later tag packages instead of compiling again. +# release programs are what a later tag packages instead of compiling again. A pull request +# packages Linux x86_64 only; main builds every release program, Linux aarch64 included. on: push: branches: [main] @@ -25,6 +26,8 @@ permissions: env: CARGO_TERM_COLOR: always + # .config/nextest.toml: no fail-fast, a retry that reports flaky tests, and a hang timeout. + NEXTEST_PROFILE: ci jobs: test: @@ -33,40 +36,48 @@ jobs: fail-fast: false matrix: os: [ubuntu-22.04, macos-latest, windows-latest] + include: + - os: macos-latest + features: --features tinyclient/gui runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v5 - uses: ./.github/actions/setup-rust with: cache-key: test - - name: Test - run: cargo test --workspace + - uses: taiki-e/install-action@nextest + # nextest runs the test binaries side by side, where `cargo test` ran them one at a time. # The window's own tests (prefs, search, shortcuts against the menus, the controls) build - # only with the gui feature. - - name: Test the window - if: runner.os == 'macOS' - run: cargo test -p tinyclient --features gui + # only with the gui feature, so macOS turns it on for the one pass over the workspace. + - name: Test + run: cargo nextest run --workspace ${{ matrix.features }} + # nextest does not run doc tests. + - name: Doc tests + run: cargo test --workspace --doc ${{ matrix.features }} + # The cfg(target_os = "ios"|"android") arms are not type-checked on this host; this proves + # the mobile feature does not disturb the desktop arms. It shares this job's cache. + - name: Window with the mobile feature enabled + if: runner.os == 'Linux' + run: cargo check -p tinyclient --features "gui mobile" package: name: package (${{ matrix.artifact }}) strategy: fail-fast: false matrix: - include: - # Ubuntu 22.04 is the oldest glibc the release packages promise. Building here is - # what lets a tag reuse these binaries. - - os: ubuntu-22.04 - artifact: linux-x86_64 - - os: macos-latest - artifact: macos - - os: windows-latest - artifact: windows - runs-on: ${{ matrix.os }} + # A pull request packages Linux x86_64, which proves the release build and the packaging. + # The others build only on main, which is what a tag reuses. On a pull request the macOS + # build took a second macOS runner beside the tests and queued behind the phone jobs. + artifact: ${{ fromJSON(github.event_name == 'push' && '["linux-x86_64","linux-aarch64","macos","windows"]' || '["linux-x86_64"]') }} + # Ubuntu 22.04 is the oldest glibc the release packages promise. + runs-on: ${{ fromJSON('{"linux-x86_64":"ubuntu-22.04","linux-aarch64":"ubuntu-22.04-arm","macos":"macos-latest","windows":"windows-latest"}')[matrix.artifact] }} steps: - uses: actions/checkout@v5 + # Linux aarch64 builds without a cache. It runs only on main, beside the longer macOS + # tests, and its entry would be another few gigabytes in a cache already at its 10 GB cap. - uses: ./.github/actions/setup-rust with: - cache-key: release + cache-key: ${{ matrix.artifact == 'linux-aarch64' && 'none' || 'release' }} - name: Build release shell: bash env: @@ -87,17 +98,17 @@ jobs: # The release's packaging, so a change that breaks it shows here rather than at the next tag. - name: Package .deb, .rpm, AppImage and .tar.gz - if: matrix.artifact == 'linux-x86_64' + if: startsWith(matrix.artifact, 'linux-') run: | eval "$(packaging/linux/tools.sh)" packaging/linux.sh --no-build ci desktop-file-validate packaging/linux/dev.tinyclient.app.desktop - dpkg-deb --info dist/tinyclient-ci-linux-x86_64.deb - dpkg-deb --contents dist/tinyclient-ci-linux-x86_64.deb + dpkg-deb --info "dist/tinyclient-ci-linux-$(uname -m).deb" + dpkg-deb --contents "dist/tinyclient-ci-linux-$(uname -m).deb" - uses: actions/upload-artifact@v6 - if: matrix.artifact == 'linux-x86_64' + if: startsWith(matrix.artifact, 'linux-') with: - name: tinyclient-linux-x86_64 + name: tinyclient-${{ matrix.artifact }} path: dist/* - name: Pack binaries diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index 5409a0a0..af5671db 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -1,8 +1,9 @@ name: Mobile -# The desktop workflow (ci.yml) already builds and tests the host. This one proves the two -# things it cannot: that the crate still compiles for the phones, and that the native shells -# package. Nothing here is signed or uploaded to a store. +# The desktop workflow (ci.yml) already builds and tests the host, and checks the window with +# the mobile feature on. This one proves the two things it cannot: that the crate still compiles +# for the phones, and that the native shells package. One job per phone, android-app and +# ios-app. Nothing here is signed or uploaded to a store. # # Versions are the ones checked against vendor docs on 2026-10-31 # (research_notes/mobile/build-ci-release.md, rust-core-native-shells.md): @@ -46,26 +47,16 @@ env: GRADLE_VERSION: "9.6.0" jobs: - # The cheap one: does `mobile` break the desktop build? This is the check that catches a - # feature-graph mistake before either expensive job runs. - host-check: - name: host check (gui + mobile) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: ./.github/actions/setup-rust - with: - cache-key: mobile-host - - name: Window - run: cargo check -p tinyclient --features gui - - name: Window with the mobile feature enabled - # The cfg(target_os = "ios"|"android") arms are not type-checked on this host; this - # proves the feature does not disturb the desktop arms. - run: cargo check -p tinyclient --features "gui mobile" - - android-libs: - name: android libs + # Each phone builds its Rust library and packages the app in one job. They used to be two jobs + # apiece, and the app job waited in a second runner queue and then downloaded the library. + # A pull request builds the libraries with the ci-mobile profile (no LTO, Cargo.toml), from + # scratch: main's cache holds the release profile, which a pull request cannot reuse. Main and + # tags build release, which is what ships, and main saves that cache. + android-app: + name: android (libs + assembleDebug) runs-on: ubuntu-latest + env: + PROFILE: ${{ github.event_name == 'pull_request' && 'ci-mobile' || 'release' }} steps: - uses: actions/checkout@v5 @@ -74,6 +65,7 @@ jobs: targets: aarch64-linux-android,x86_64-linux-android - uses: swatinem/rust-cache@v2 + if: github.event_name != 'pull_request' with: prefix-key: v1-rust shared-key: mobile-android @@ -99,10 +91,12 @@ jobs: TINYCLIENT_TELEGRAM_API_HASH: ${{ secrets.TINYCLIENT_TELEGRAM_API_HASH }} # Output is the layout AGP packages automatically: jniLibs//libtinyclient.so. # `--platform 29` matches minSdk: below it the NDK has no libnativewindow to link. + # The app loads only the shared library, so `--crate-type cdylib` builds that one instead + # of every type in [lib] crate-type. run: | cargo ndk -t arm64-v8a -t x86_64 --platform 29 \ -o mobile/android/app/src/main/jniLibs \ - build --release -p tinyclient --lib --features mobile + rustc --profile "$PROFILE" -p tinyclient --lib --features mobile --crate-type cdylib - name: 16 KB page check # Play blocks updates without 16 KB support from 2027-02-01, and a misaligned ELF @@ -117,19 +111,6 @@ jobs: exit 1 fi - - uses: actions/upload-artifact@v6 - with: - name: android-jniLibs - path: mobile/android/app/src/main/jniLibs - if-no-files-found: error - - android-app: - name: android app (assembleDebug) - needs: android-libs - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-java@v4 with: distribution: temurin @@ -140,11 +121,6 @@ jobs: with: gradle-version: ${{ env.GRADLE_VERSION }} - - uses: actions/download-artifact@v6 - with: - name: android-jniLibs - path: mobile/android/app/src/main/jniLibs - - name: assembleDebug working-directory: mobile/android run: gradle :app:assembleDebug --console=plain @@ -162,50 +138,36 @@ jobs: path: mobile/android/app/build/outputs/apk/debug/app-debug.apk if-no-files-found: error - ios-libs: - name: ios libs (xcframework) + ios-app: + name: ios (libs + unsigned archive) # Xcode is macOS-only. macos-26 carries Xcode 26.6 / iOS 26.5 SDK, which satisfies the # current mandate; move to the xcode-27 preview image before April 2027. runs-on: macos-26 + env: + PROFILE: ${{ github.event_name == 'pull_request' && 'ci-mobile' || 'release' }} steps: - uses: actions/checkout@v5 + # The archive below is for a device, so the simulator slices are not built here. - uses: dtolnay/rust-toolchain@stable with: - targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios + targets: aarch64-apple-ios - uses: swatinem/rust-cache@v2 + if: github.event_name != 'pull_request' with: prefix-key: v1-rust shared-key: mobile-ios save-if: ${{ github.ref == 'refs/heads/main' }} - - name: Build device + simulator and make the xcframework + - name: Build the device library and make the xcframework env: # chatd runs in the app, and its build script refuses a release without these. TINYCLIENT_TELEGRAM_API_ID: ${{ secrets.TINYCLIENT_TELEGRAM_API_ID }} TINYCLIENT_TELEGRAM_API_HASH: ${{ secrets.TINYCLIENT_TELEGRAM_API_HASH }} run: | chmod +x scripts/mobile/build-ios.sh - scripts/mobile/build-ios.sh - - - uses: actions/upload-artifact@v6 - with: - name: TinyCore.xcframework - path: mobile/ios/TinyCore.xcframework - if-no-files-found: error - - ios-app: - name: ios app (unsigned archive) - needs: ios-libs - runs-on: macos-26 - steps: - - uses: actions/checkout@v5 - - - uses: actions/download-artifact@v6 - with: - name: TinyCore.xcframework - path: mobile/ios/TinyCore.xcframework + scripts/mobile/build-ios.sh --device-only --profile "$PROFILE" - name: XcodeGen run: brew install xcodegen diff --git a/Cargo.toml b/Cargo.toml index 295bbc11..a78e7870 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,14 @@ opt-level = 2 lto = "thin" codegen-units = 1 +# The phone libraries a pull request builds (.github/workflows/mobile.yml). They prove the crate +# compiles and links for the phones and are never shipped, so they skip LTO and spread codegen +# across the runner's cores. Pushes to main build the release profile. +[profile.ci-mobile] +inherits = "release" +lto = false +codegen-units = 16 + # `wgpu::InstanceFlags::default()` (what gpui_ce_wgpu asks for) turns on the Vulkan debug-utils and # validation layers when wgpu-types is built with debug_assertions. On the Android emulator's # Vulkan driver that segfaults in `vk_common_SetDebugUtilsObjectNameEXT` on the first labelled diff --git a/docs/mobile-build.md b/docs/mobile-build.md index 03314487..ccf47534 100644 --- a/docs/mobile-build.md +++ b/docs/mobile-build.md @@ -20,9 +20,9 @@ parts were not. Nothing here was built for a phone. | `mobile/ios/` | XcodeGen project, `Info.plist`, Swift `AppDelegate`/`SceneDelegate`, bridging header | | `scripts/mobile/build-android.ps1` | Windows: NDK check → `cargo ndk` → `gradlew assembleDebug` | | `scripts/mobile/build-android.sh` | Linux/macOS: the same | -| `scripts/mobile/build-ios.sh` | macOS: Rust device + simulator → `lipo` → `xcodebuild -create-xcframework` | +| `scripts/mobile/build-ios.sh` | macOS: Rust device + simulator → `lipo` → `xcodebuild -create-xcframework` (`--device-only` for CI's archive) | | `scripts/mobile/seed-demo.ps1` | `chatd --seed-demo` in a scratch `TINYCLIENT_DIR`, prints the env | -| `.github/workflows/mobile.yml` | `host-check`, `android-libs`, `android-app`, `ios-libs`, `ios-app` | +| `.github/workflows/mobile.yml` | `android-app`, `ios-app`: each builds its library and packages the app (the host check is in `ci.yml`) | `src/main.rs` is unchanged: the desktop binary keeps its own module tree and its own entry point. @@ -124,8 +124,8 @@ xcodebuild -project Tiny.xcodeproj -scheme Tiny \ CODE_SIGNING_ALLOWED=NO build ``` -`build-ios.sh` runs three `cargo build --lib --features mobile` (device, arm64 sim, x86_64 sim), -`lipo`s the two simulator slices into one, and calls `xcodebuild -create-xcframework` into +`build-ios.sh` builds the static library three times with `cargo rustc --lib --features mobile +--crate-type staticlib` (device, arm64 sim, x86_64 sim), `lipo`s the two simulator slices into one, and calls `xcodebuild -create-xcframework` into `mobile/ios/TinyCore.xcframework`. Two simulator slices cannot be passed to `-create-xcframework` separately: it refuses two libraries with the same platform+variant. diff --git a/scripts/mobile/build-ios.sh b/scripts/mobile/build-ios.sh index 4cf44c2a..2fa654b1 100755 --- a/scripts/mobile/build-ios.sh +++ b/scripts/mobile/build-ios.sh @@ -6,13 +6,15 @@ # scripts/mobile/build-ios.sh --debug # scripts/mobile/build-ios.sh --debug --demo # proof build: made-up chats from launch (`demo`) # scripts/mobile/build-ios.sh --debug --sim-only # Apple-silicon simulator slice only (fast loop) +# scripts/mobile/build-ios.sh --device-only # device slice only, for a device archive (CI) +# scripts/mobile/build-ios.sh --profile ci-mobile # any Cargo profile; output is target// # # Nothing here needs UniFFI: the shell calls one C symbol (`tinyclient_ios_main`). See # docs/mobile-build.md. This script does NOT build the .app — that is xcodebuild's job, and the # exact command is printed at the end. # # macOS only: lipo and xcodebuild ship with Xcode. On Windows there is no equivalent; use the -# `ios-libs` / `ios-app` jobs in .github/workflows/mobile.yml. +# `ios-app` job in .github/workflows/mobile.yml. set -euo pipefail root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" @@ -24,14 +26,22 @@ profile_dir="release" extra_args=(--release) features="mobile" sim_only=0 +device_only=0 while [ $# -gt 0 ]; do case "$1" in --debug) profile="debug"; profile_dir="debug"; extra_args=() ;; --release) profile="release"; profile_dir="release"; extra_args=(--release) ;; + --profile) + [ $# -ge 2 ] || { echo "build-ios: --profile needs a name" >&2; exit 2; } + profile="$2"; extra_args=(--profile "$2") + # Cargo writes the dev profile to target//debug and every other one to its own name. + if [ "$2" = "dev" ]; then profile_dir="debug"; else profile_dir="$2"; fi + shift ;; --demo) features="mobile demo" ;; --sim-only) sim_only=1 ;; - -h|--help) sed -n '2,15p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; + --device-only) device_only=1 ;; + -h|--help) sed -n '2,17p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "build-ios: unknown argument: $1 (try --help)" >&2; exit 2 ;; esac shift @@ -43,7 +53,8 @@ fail() { } # The made-up chatd shows invented data and never reaches the real core: never in a release. -[ "$features" = "mobile" ] || [ "$profile" = "debug" ] || fail "--demo is for debug proof builds only, never a release." +[ "$features" = "mobile" ] || [ "$profile" = "debug" ] || [ "$profile" = "dev" ] || fail "--demo is for debug proof builds only, never a release." +[ "$sim_only" = 0 ] || [ "$device_only" = 0 ] || fail "--sim-only and --device-only leave nothing to build together; pick one." note() { printf 'build-ios: %s\n' "$1" @@ -51,7 +62,7 @@ note() { # --- host and tools -------------------------------------------------------- -[ "$(uname -s)" = "Darwin" ] || fail "this script needs macOS: lipo and xcodebuild ship with Xcode, and the iOS SDK is macOS-only. On Windows use the ios-libs job in .github/workflows/mobile.yml (a macos-26 runner)." +[ "$(uname -s)" = "Darwin" ] || fail "this script needs macOS: lipo and xcodebuild ship with Xcode, and the iOS SDK is macOS-only. On Windows use the ios-app job in .github/workflows/mobile.yml (a macos-26 runner)." if ! command -v cargo >/dev/null 2>&1; then if [ -x "$HOME/.cargo/bin/cargo" ]; then @@ -71,6 +82,8 @@ command -v lipo >/dev/null 2>&1 || fail "lipo is not on PATH. It ships with Xcod # platform+variant, so both simulator architectures must be lipo'd into one first. targets=(aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios) [ "$sim_only" = 1 ] && targets=(aarch64-apple-ios-sim) +# A device archive (`-destination 'generic/platform=iOS'`) links only the device slice. +[ "$device_only" = 1 ] && targets=(aarch64-apple-ios) installed="$(rustup target list --installed)" for triple in "${targets[@]}"; do if ! printf '%s\n' "$installed" | grep -qx "$triple"; then @@ -81,15 +94,17 @@ done # --- Build ----------------------------------------------------------------- -# The app's floor (project.yml). Unset, rustc links the library's cdylib for iOS 10, where -# the C dependencies built against the current SDK find no ___chkstk_darwin. +# The app's floor (project.yml). Unset, the C dependencies and any dylib rustc links target +# iOS 10, where code built against the current SDK finds no ___chkstk_darwin. export IPHONEOS_DEPLOYMENT_TARGET="${IPHONEOS_DEPLOYMENT_TARGET:-16.0}" # `--lib` because the package also has the desktop binary; `mobile` is the entry point + gui. +# Xcode links only the static library, so `cargo rustc --crate-type staticlib` builds that one +# instead of every type in [lib] crate-type. Under release LTO each type is a separate link. build() { local triple="$1" - note "cargo build $profile --target $triple" - (cd "$root" && cargo build "${extra_args[@]}" -p tinyclient --lib --features "$features" --target "$triple") + note "cargo rustc $profile --target $triple (staticlib)" + (cd "$root" && cargo rustc "${extra_args[@]}" -p tinyclient --lib --features "$features" --target "$triple" --crate-type staticlib) } for triple in "${targets[@]}"; do @@ -104,11 +119,21 @@ if [ "$sim_only" = 1 ]; then fi device_lib="$root/target/aarch64-apple-ios/$profile_dir/libtinyclient.a" + +if [ "$device_only" = 1 ]; then + [ -f "$device_lib" ] || fail "expected $device_lib after the staticlib build." + rm -rf "$xcframework" + note "xcodebuild -create-xcframework (device slice only) -> $xcframework" + xcodebuild -create-xcframework -library "$device_lib" -output "$xcframework" + note "done: $(du -sh "$xcframework" | cut -f1) $xcframework" + exit 0 +fi + sim_arm_lib="$root/target/aarch64-apple-ios-sim/$profile_dir/libtinyclient.a" sim_intel_lib="$root/target/x86_64-apple-ios/$profile_dir/libtinyclient.a" for lib in "$device_lib" "$sim_arm_lib" "$sim_intel_lib"; do - [ -f "$lib" ] || fail "expected $lib; does [lib] crate-type include staticlib?" + [ -f "$lib" ] || fail "expected $lib after the staticlib build." done mkdir -p "$root/target/universal-ios-sim" From ef2e6b46d7f27450b1fe1673f6c003782c081604 Mon Sep 17 00:00:00 2001 From: Justin Scroggins Date: Mon, 2 Nov 2026 20:14:29 -0500 Subject: [PATCH 2/3] CI: run Windows voice tests one at a time, mobile back on release - native-tls on Windows imports every test certificate's key into one CryptoAPI container, so parallel mediad::voice processes broke each other's TLS handshake. They share a one-thread test group on Windows. - a_media_drop_finds_a_dead_socket_at_once fails on Linux whenever it runs without other chatd tests in its process (plain cargo test too), so the ci profile leaves it out until the test stands alone. - Mobile pull requests build release again and restore main's cache. The uncached ci-mobile profile took Android from about 11 to 19.7 minutes. Co-Authored-By: Claude Opus 5.5 --- .config/nextest.toml | 16 ++++++++++++++++ .github/workflows/mobile.yml | 16 +++++----------- Cargo.toml | 8 -------- scripts/mobile/build-ios.sh | 2 +- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/.config/nextest.toml b/.config/nextest.toml index 3fdd14ee..92cb616c 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -2,9 +2,25 @@ # `cargo test` ran the binaries one after another. CI selects the `ci` profile through # NEXTEST_PROFILE; a local `cargo nextest run` keeps nextest's defaults. +# mediad's voice tests run a TLS server with a certificate each test process makes. On Windows, +# native-tls imports every such key into the one CryptoAPI key container it names ("schannel"), +# so two processes at once replace each other's key and the handshake fails. One at a time there. +[test-groups] +windows-tls = { max-threads = 1 } + +[[profile.default.overrides]] +platform = 'cfg(windows)' +filter = 'binary_id(mediad::voice)' +test-group = 'windows-tls' + [profile.ci] # Run everything, so one failure does not hide the next. fail-fast = false +# a_media_drop_finds_a_dead_socket_at_once passes on Linux only when other chatd tests have run +# in the same process before it. Alone, it times out every time, under plain `cargo test` as +# well (`cargo test -p chatd --lib a_media_drop_finds`). nextest gives each test its own process, +# so it is left out here until the test stands on its own. +default-filter = 'not (binary_id(chatd) & test(=adapters::tiny::live::tests::a_media_drop_finds_a_dead_socket_at_once))' # A test that fails and then passes is reported as FLAKY in the summary, not dropped. retries = 1 # A few call and MLS tests legitimately take a minute or more. Ten minutes is a hang. diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index af5671db..6cfb5962 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -49,14 +49,12 @@ env: jobs: # Each phone builds its Rust library and packages the app in one job. They used to be two jobs # apiece, and the app job waited in a second runner queue and then downloaded the library. - # A pull request builds the libraries with the ci-mobile profile (no LTO, Cargo.toml), from - # scratch: main's cache holds the release profile, which a pull request cannot reuse. Main and - # tags build release, which is what ships, and main saves that cache. + # Every build is release, so a pull request reuses the dependencies in main's cache. A cheaper + # profile was tried for pull requests: with nothing cached for it, it was slower (Android 19.7 + # minutes against about 11). android-app: name: android (libs + assembleDebug) runs-on: ubuntu-latest - env: - PROFILE: ${{ github.event_name == 'pull_request' && 'ci-mobile' || 'release' }} steps: - uses: actions/checkout@v5 @@ -65,7 +63,6 @@ jobs: targets: aarch64-linux-android,x86_64-linux-android - uses: swatinem/rust-cache@v2 - if: github.event_name != 'pull_request' with: prefix-key: v1-rust shared-key: mobile-android @@ -96,7 +93,7 @@ jobs: run: | cargo ndk -t arm64-v8a -t x86_64 --platform 29 \ -o mobile/android/app/src/main/jniLibs \ - rustc --profile "$PROFILE" -p tinyclient --lib --features mobile --crate-type cdylib + rustc --release -p tinyclient --lib --features mobile --crate-type cdylib - name: 16 KB page check # Play blocks updates without 16 KB support from 2027-02-01, and a misaligned ELF @@ -143,8 +140,6 @@ jobs: # Xcode is macOS-only. macos-26 carries Xcode 26.6 / iOS 26.5 SDK, which satisfies the # current mandate; move to the xcode-27 preview image before April 2027. runs-on: macos-26 - env: - PROFILE: ${{ github.event_name == 'pull_request' && 'ci-mobile' || 'release' }} steps: - uses: actions/checkout@v5 @@ -154,7 +149,6 @@ jobs: targets: aarch64-apple-ios - uses: swatinem/rust-cache@v2 - if: github.event_name != 'pull_request' with: prefix-key: v1-rust shared-key: mobile-ios @@ -167,7 +161,7 @@ jobs: TINYCLIENT_TELEGRAM_API_HASH: ${{ secrets.TINYCLIENT_TELEGRAM_API_HASH }} run: | chmod +x scripts/mobile/build-ios.sh - scripts/mobile/build-ios.sh --device-only --profile "$PROFILE" + scripts/mobile/build-ios.sh --device-only - name: XcodeGen run: brew install xcodegen diff --git a/Cargo.toml b/Cargo.toml index a78e7870..295bbc11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,14 +36,6 @@ opt-level = 2 lto = "thin" codegen-units = 1 -# The phone libraries a pull request builds (.github/workflows/mobile.yml). They prove the crate -# compiles and links for the phones and are never shipped, so they skip LTO and spread codegen -# across the runner's cores. Pushes to main build the release profile. -[profile.ci-mobile] -inherits = "release" -lto = false -codegen-units = 16 - # `wgpu::InstanceFlags::default()` (what gpui_ce_wgpu asks for) turns on the Vulkan debug-utils and # validation layers when wgpu-types is built with debug_assertions. On the Android emulator's # Vulkan driver that segfaults in `vk_common_SetDebugUtilsObjectNameEXT` on the first labelled diff --git a/scripts/mobile/build-ios.sh b/scripts/mobile/build-ios.sh index 2fa654b1..499d3ef3 100755 --- a/scripts/mobile/build-ios.sh +++ b/scripts/mobile/build-ios.sh @@ -7,7 +7,7 @@ # scripts/mobile/build-ios.sh --debug --demo # proof build: made-up chats from launch (`demo`) # scripts/mobile/build-ios.sh --debug --sim-only # Apple-silicon simulator slice only (fast loop) # scripts/mobile/build-ios.sh --device-only # device slice only, for a device archive (CI) -# scripts/mobile/build-ios.sh --profile ci-mobile # any Cargo profile; output is target// +# scripts/mobile/build-ios.sh --profile NAME # any Cargo profile; output is target// # # Nothing here needs UniFFI: the shell calls one C symbol (`tinyclient_ios_main`). See # docs/mobile-build.md. This script does NOT build the .app — that is xcodebuild's job, and the From 31162e324d3617bd3ab8f6871705440adb3dc274 Mon Sep 17 00:00:00 2001 From: Justin Scroggins Date: Mon, 2 Nov 2026 21:04:16 -0500 Subject: [PATCH 3/3] CI: wait before retrying a failed test A Windows runner stalled for a couple of minutes (a 20 MB download test took 131 seconds instead of 5), and the Discord tests retried straight away failed again inside the stall. Two retries, 15 and 30 seconds later. Co-Authored-By: Claude Opus 5.5 --- .config/nextest.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.config/nextest.toml b/.config/nextest.toml index 92cb616c..82279be5 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -21,8 +21,10 @@ fail-fast = false # well (`cargo test -p chatd --lib a_media_drop_finds`). nextest gives each test its own process, # so it is left out here until the test stands on its own. default-filter = 'not (binary_id(chatd) & test(=adapters::tiny::live::tests::a_media_drop_finds_a_dead_socket_at_once))' -# A test that fails and then passes is reported as FLAKY in the summary, not dropped. -retries = 1 +# A test that fails and then passes is reported as FLAKY in the summary, not dropped. The +# retries wait first: a Windows runner sometimes stalls for a minute or two (a 20 MB download +# test went from 5 to 131 seconds), and every test retried straight away failed again inside it. +retries = { backoff = "exponential", count = 2, delay = "15s" } # A few call and MLS tests legitimately take a minute or more. Ten minutes is a hang. slow-timeout = { period = "60s", terminate-after = 10 } failure-output = "immediate-final"