Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .config/nextest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# 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.

# 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. 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"
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
7 changes: 5 additions & 2 deletions .github/actions/setup-rust/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 }}
Expand Down
57 changes: 34 additions & 23 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand Down
84 changes: 20 additions & 64 deletions .github/workflows/mobile.yml
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -46,25 +47,13 @@ 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.
# 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
steps:
- uses: actions/checkout@v5
Expand Down Expand Up @@ -99,10 +88,12 @@ jobs:
TINYCLIENT_TELEGRAM_API_HASH: ${{ secrets.TINYCLIENT_TELEGRAM_API_HASH }}
# Output is the layout AGP packages automatically: jniLibs/<abi>/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 --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
Expand All @@ -117,19 +108,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
Expand All @@ -140,11 +118,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
Expand All @@ -162,50 +135,33 @@ 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
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
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

- name: XcodeGen
run: brew install xcodegen
Expand Down
8 changes: 4 additions & 4 deletions docs/mobile-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down
Loading
Loading