Compare commits
2 Commits
feat/andro
...
android-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75bc72a884 | ||
|
|
6aa52accef |
@@ -625,3 +625,123 @@ curl -s http://relay-host:9090/metrics | grep wzp_relay_active_sessions
|
||||
# Check federation probe health
|
||||
curl -s http://relay-host:9090/metrics | grep wzp_probe_up
|
||||
```
|
||||
|
||||
## Build Pipelines
|
||||
|
||||
All production artifacts (Android APK, Linux x86_64 binaries, Windows `.exe`) are built on **SepehrHomeserverdk** using Docker, not on developer workstations. The pipelines are fire-and-forget: a local script invokes a `tmux` session on the remote, the build runs in a Docker container, and the artifact is uploaded to `paste.dk.manko.yoga` (rustypaste) with a notification sent to `ntfy.sh/wzp` on start and completion.
|
||||
|
||||
### Docker images
|
||||
|
||||
Two long-lived images live on the remote:
|
||||
|
||||
| Image | Used by | Base | Key contents |
|
||||
|---|---|---|---|
|
||||
| `wzp-android-builder` | Android APK (Tauri mobile + legacy Kotlin), Linux x86_64 relay/CLI | Debian bookworm | Rust stable with Android targets, cargo-ndk, NDK 26.1, Android SDK (API 34 + 35 + 36), JDK 17, Gradle 8.5, Node.js 20, cmake, ninja, tauri-cli 2.x |
|
||||
| `wzp-windows-builder` | Windows x86_64 `.exe` | Debian bookworm | Rust stable with `x86_64-pc-windows-msvc` target, cargo-xwin (with pre-warmed MSVC CRT + Windows SDK cache), Node.js 20, cmake, ninja, clang, lld, nasm |
|
||||
|
||||
Both images are rebuilt rarely — once the base toolchain is stable, rebuilds are only needed to pick up new dependencies or security patches.
|
||||
|
||||
**Rebuilding an image** (fire-and-forget, ~10 min on a warm base):
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
./scripts/build-windows-docker.sh --image-build
|
||||
|
||||
# Android (upload and rebuild handled by the Android build script itself — see
|
||||
# its --image-build flag or equivalent)
|
||||
```
|
||||
|
||||
The `--image-build` flag uploads the local Dockerfile to the remote, kicks off `docker build` under `nohup`, and returns immediately. Monitor with:
|
||||
|
||||
```bash
|
||||
ssh SepehrHomeserverdk 'tail -f /tmp/wzp-windows-image-build.log'
|
||||
```
|
||||
|
||||
### Pipeline: Android APK (Tauri Mobile)
|
||||
|
||||
```bash
|
||||
./scripts/build-tauri-android.sh # Full: pull + build + upload + notify
|
||||
./scripts/build-tauri-android.sh --no-pull # Skip git fetch
|
||||
./scripts/build-tauri-android.sh --clean # Force-clean Rust target
|
||||
```
|
||||
|
||||
- **Branch**: `android-rewrite`
|
||||
- **Image**: `wzp-android-builder`
|
||||
- **Build command**: `cargo tauri android build --release`
|
||||
- **Output**: `wzp-release.apk` → uploaded to rustypaste
|
||||
- **Notifications**: start + completion to `ntfy.sh/wzp`
|
||||
- **Remote artifact path**: `/mnt/storage/manBuilder/data/cache-android/target/…/release/app-release.apk`
|
||||
|
||||
### Pipeline: Linux x86_64 (relay + CLI + bench + web)
|
||||
|
||||
```bash
|
||||
./scripts/build-linux-docker.sh # Fire-and-forget
|
||||
./scripts/build-linux-docker.sh --no-pull # Skip git fetch
|
||||
./scripts/build-linux-docker.sh --clean # Force-clean target
|
||||
./scripts/build-linux-docker.sh --install # Wait for completion and download locally
|
||||
```
|
||||
|
||||
- **Branch**: `feat/android-voip-client` (script default — override by editing the script or passing an env var)
|
||||
- **Image**: `wzp-android-builder` (shared, not a separate Linux-only image)
|
||||
- **Targets built**: `wzp-relay`, `wzp-client`, `wzp-client-audio` (with `--features audio`), `wzp-web`, `wzp-bench`
|
||||
- **Output**: `wzp-linux-x86_64.tar.gz` with all five binaries → uploaded to rustypaste
|
||||
- **Local landing dir** (with `--install`): `target/linux-x86_64/`
|
||||
|
||||
### Pipeline: Windows x86_64 (`wzp-desktop.exe`)
|
||||
|
||||
```bash
|
||||
./scripts/build-windows-docker.sh # Full: pull + build + download locally
|
||||
./scripts/build-windows-docker.sh --no-pull # Skip git fetch
|
||||
./scripts/build-windows-docker.sh --rust # Force-clean target-windows cache
|
||||
./scripts/build-windows-docker.sh --image-build # Rebuild the Docker image (fire-and-forget)
|
||||
```
|
||||
|
||||
- **Branch**: `feat/desktop-audio-rewrite`
|
||||
- **Image**: `wzp-windows-builder`
|
||||
- **Build command**: `cargo xwin build --release --target x86_64-pc-windows-msvc --bin wzp-desktop`
|
||||
- **Output**: `wzp-desktop.exe` (~16 MB) → downloaded to `target/windows-exe/wzp-desktop.exe`, also uploaded to rustypaste
|
||||
- **Target cache volume**: `target-windows` (separate from the Android target cache to avoid triple cross-contamination)
|
||||
- **Shared cache volumes**: `cargo-registry`, `cargo-git` (shared with Android — both pipelines pull the same crates)
|
||||
|
||||
**A/B-preserving workflow** for testing audio backends: rename the prior `.exe` before re-running the build, so both coexist:
|
||||
|
||||
```bash
|
||||
# Preserve prior build as the noAEC baseline
|
||||
mv target/windows-exe/wzp-desktop.exe target/windows-exe/wzp-desktop-noAEC.exe
|
||||
./scripts/build-windows-docker.sh
|
||||
ls -la target/windows-exe/
|
||||
# wzp-desktop-noAEC.exe (previous build)
|
||||
# wzp-desktop.exe (new build)
|
||||
```
|
||||
|
||||
### Alternative pipeline: Windows via Hetzner Cloud VPS
|
||||
|
||||
For situations where Docker image rebuilds would be disruptive, or for one-shot debug builds on a clean machine:
|
||||
|
||||
```bash
|
||||
./scripts/build-windows-cloud.sh # Full: create VM → build → download → destroy
|
||||
./scripts/build-windows-cloud.sh --prepare # Create VM + install deps, don't build
|
||||
./scripts/build-windows-cloud.sh --build # Build on existing VM
|
||||
./scripts/build-windows-cloud.sh --transfer # Download .exe from existing VM
|
||||
./scripts/build-windows-cloud.sh --destroy # Delete the VM
|
||||
WZP_KEEP_VM=1 ./scripts/build-windows-cloud.sh # Don't auto-destroy after successful build
|
||||
```
|
||||
|
||||
- **Provider**: Hetzner Cloud
|
||||
- **Default server type**: `cx33` (8 GB RAM, 8 vCPU — `cx23` with 4 GB OOMs on the tauri+rustls cross-compile)
|
||||
- **Image**: `ubuntu-24.04`
|
||||
- **SSH key**: must be named `wz` in Hetzner and loaded in the local ssh-agent
|
||||
- **Reminder**: set `WZP_KEEP_VM=1` for multi-build sessions, then **remember to `--destroy` at end of day** so the VM isn't left running overnight. This is tracked in the auto-memory as `feedback_keep_windows_builder_vm.md`.
|
||||
|
||||
### Notifications
|
||||
|
||||
All pipelines post to `https://ntfy.sh/wzp`. Subscribe from your phone via the [ntfy.sh app](https://ntfy.sh/) to get push notifications on build start/success/failure. Messages include the short git hash and the rustypaste URL on success:
|
||||
|
||||
```
|
||||
WZP Windows build OK [03a80a3] (16M)
|
||||
https://paste.dk.manko.yoga/<uuid>/wzp-desktop.exe
|
||||
```
|
||||
|
||||
### Rustypaste credentials
|
||||
|
||||
Build pipelines read `rusty_address` and `rusty_auth_token` from the `.env` file at `/mnt/storage/manBuilder/.env` on SepehrHomeserverdk. Local scripts that upload directly (`build-windows-cloud.sh` when run in `--transfer` mode) read from `~/.wzp/rustypaste.env` with the same variable names. Both files must be kept in sync manually if rotated.
|
||||
|
||||
@@ -872,3 +872,71 @@ warzonePhone/
|
||||
| wzp-relay | 40 + 4 integration | Room ACL, session mgmt, metrics, probes, mesh, trunking |
|
||||
| wzp-client | 30 + 2 integration | Encoder/decoder, quality adapter, silence, drift, sweep |
|
||||
| wzp-web | 2 | Metrics |
|
||||
|
||||
## Audio Backend Architecture (Platform Matrix)
|
||||
|
||||
WarzonePhone's audio I/O goes through one of four backends depending on the target platform and feature flags. All backends expose the same public API (`AudioCapture::start() → AudioCapture { ring(), stop() }`) via conditional re-exports in `crates/wzp-client/src/lib.rs`, so the `CallEngine` above the audio layer doesn't know or care which backend is running.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ CallEngine (platform-agnostic) │
|
||||
│ reads PCM from AudioCapture::ring() │
|
||||
│ writes PCM to AudioPlayback::ring() │
|
||||
└────────────────────┬────────────────────────┘
|
||||
│
|
||||
┌─────────────────────┼─────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌───────────────┐ ┌────────────────┐ ┌───────────────┐
|
||||
│ audio_io │ │ audio_vpio │ │ audio_wasapi │
|
||||
│ (CPAL) │ │ (Core Audio │ │ (Windows │
|
||||
│ │ │ VoiceProc IO) │ │ IAudioClient2│
|
||||
│ All platforms │ │ macOS only │ │ Windows │
|
||||
│ (baseline) │ │ feature=vpio │ │ feature= │
|
||||
│ │ │ │ │ windows-aec │
|
||||
└───────────────┘ └────────────────┘ └───────────────┘
|
||||
│
|
||||
▼ on Android only
|
||||
┌───────────────┐
|
||||
│ wzp-native │
|
||||
│ (Oboe bridge │
|
||||
│ via dlopen) │
|
||||
│ │
|
||||
│ Android only │
|
||||
│ libloading │
|
||||
└───────────────┘
|
||||
```
|
||||
|
||||
### Backend selection matrix
|
||||
|
||||
| Platform | Capture | Playback | OS AEC | Feature flags |
|
||||
|---|---|---|---|---|
|
||||
| macOS | VoiceProcessingIO (native Core Audio) | CPAL | **Yes** — Apple's hardware-accelerated AEC (same AEC as FaceTime, iMessage audio, Voice Memos) | `audio`, `vpio` |
|
||||
| Windows (AEC build) | Direct WASAPI with `AudioCategory_Communications` | CPAL | **Yes** — Windows routes the capture stream through the driver's communications APO chain (AEC + NS + AGC), driver-dependent quality | `audio`, `windows-aec` |
|
||||
| Windows (baseline) | CPAL (WASAPI shared mode) | CPAL | No | `audio` |
|
||||
| Linux | CPAL (ALSA / PulseAudio) | CPAL | No | `audio` |
|
||||
| Android (Tauri Mobile) | Oboe via `wzp-native` cdylib, `Usage::VoiceCommunication` + `MODE_IN_COMMUNICATION` | Same Oboe stream | Depends on device (some Android devices apply AEC to the voice-communication stream, most do not) | none (`wzp-client` compiled with `default-features = false`) |
|
||||
|
||||
### Why `wzp-native` is a standalone cdylib
|
||||
|
||||
On Android, the audio backend lives in a separate cdylib crate (`crates/wzp-native`) that `wzp-desktop`'s lib crate loads at runtime via `libloading`. It is **not** linked as a regular Rust dep.
|
||||
|
||||
This is deliberate. rust-lang/rust#104707 documents that a crate with `crate-type = ["cdylib", "staticlib"]` leaks non-exported symbols from the staticlib into the cdylib. On Android, that caused Bionic's private `__init_tcb` / `pthread_create` symbols to be bound LOCALLY inside our `.so` instead of resolved dynamically against `libc.so` at `dlopen` time — which crashed the app at launch as soon as `tao` tried to `std::thread::spawn()` from the JNI `onCreate` callback.
|
||||
|
||||
Keeping `wzp-native` in its own cdylib and loading it via `libloading` means:
|
||||
|
||||
1. The app's own `.so` has `crate-type = ["cdylib", "rlib"]` only — no `staticlib`, no symbol leak.
|
||||
2. `libwzp_native.so` is loaded via `System.loadLibrary` from the JVM side (or `dlopen` from Rust), which triggers the normal Bionic resolver and binds all private symbols against `libc.so` at load time.
|
||||
3. The C/C++ Oboe bridge is fully isolated inside `libwzp_native.so`'s symbol space — no chance of its archives leaking into `wzp-desktop`'s `.so`.
|
||||
|
||||
See `docs/BRANCH-android-rewrite.md` for the full incident postmortem and `docs/incident-tauri-android-init-tcb.md` for the debug log.
|
||||
|
||||
### Vendored `audiopus_sys` for libopus / clang-cl cross-compile
|
||||
|
||||
The workspace root carries a vendored copy of `audiopus_sys` at `vendor/audiopus_sys/` with a patched `opus/CMakeLists.txt`. This is needed because libopus 1.3.1 gates its per-file `-msse4.1` / `-mssse3` `COMPILE_FLAGS` behind `if(NOT MSVC)`, and under `clang-cl` (used by `cargo-xwin` for Windows cross-compiles) CMake sets `MSVC=1` unconditionally — so the SIMD source files compile without the required target feature and fail to link the intrinsic `always_inline` functions.
|
||||
|
||||
The patch introduces an `MSVC_CL` variable that is true only for real `cl.exe` (distinguished via `CMAKE_C_COMPILER_ID STREQUAL "MSVC"`), and flips the eight `if(NOT MSVC)` SIMD guards to `if(NOT MSVC_CL)` so clang-cl gets the GCC-style per-file flags. Wired in via `[patch.crates-io] audiopus_sys = { path = "vendor/audiopus_sys" }` at the workspace root.
|
||||
|
||||
This does not affect macOS or Linux builds — on those platforms `MSVC=0` everywhere so the patched logic behaves identically to upstream.
|
||||
|
||||
Upstream tracking: xiph/opus#256, xiph/opus PR #257 (both stale).
|
||||
|
||||
139
docs/BRANCH-android-rewrite.md
Normal file
139
docs/BRANCH-android-rewrite.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Branch: `android-rewrite`
|
||||
|
||||
Pivot away from the legacy Kotlin + JNI Android client to a pure-Rust **Tauri 2.x Mobile** app that shares the same frontend and backend code as the desktop client.
|
||||
|
||||
## Why this branch exists
|
||||
|
||||
The Kotlin + JNI stack was a crash factory. Every failure mode we hit was at the Kotlin ↔ Rust boundary, and each fix uncovered the next layer of the onion:
|
||||
|
||||
| Symptom | Root cause | Fix |
|
||||
|---|---|---|
|
||||
| App crashed on launch before `onCreate` returned | `__init_tcb` / `pthread_create` bionic private symbols leaking out of `libwzp_android.so` because the Rust crate used `crate-type = ["cdylib", "staticlib"]`. rust-lang/rust#104707 documents that staticlib alongside cdylib leaks non-exported symbols from the staticlib into the cdylib, and Bionic's private internal pthread symbols got bound LOCALLY inside our `.so` instead of resolved against `libc.so` at `dlopen` time | Dropped `staticlib` from the crate-type list. `crate-type = ["cdylib", "rlib"]` only. |
|
||||
| Stack overflow on `place_call` | `Dispatchers.IO` threads have a ~512 KB stack, too small for the Rust signal-connect path that does TLS handshake + quinn setup inside one closure | Launched JNI calls from a dedicated `java.lang.Thread` with an explicit 8 MB stack |
|
||||
| `ring` / `libcrypto` TLS reuse crash on second call | tokio runtime got dropped between calls, but `ring` keeps a TLS-stored SSL context that is invalidated when the runtime thread is reused by a new runtime — `ring` sees stale context and segfaults | Single long-lived tokio runtime for the entire signal client lifetime; split `start()` into an inline `connect+register` path and a `run()` path on a separate thread to avoid the `thread::spawn` closure's stack overflow |
|
||||
| Null dereference on register with fresh install | Identity seed file empty when it existed-but-was-blank, Rust side deref'd the zero-length slice | Generate seed if empty on register |
|
||||
|
||||
Every fix kept the app limping along but the fundamental design problem remained: **state management was split across a Kotlin ViewModel and a Rust engine, with a hand-rolled JNI bridge in between that had to be perfect to not crash**. The working desktop Tauri client (with the same Rust backend) had none of these problems because it spoke to the Rust code via in-process `invoke()` from a WebView, not JNI.
|
||||
|
||||
So: rewrite the Android app as a **Tauri 2.x Mobile app**, reusing the entire desktop codebase verbatim (`main.ts`, `style.css`, `index.html`, `main.rs`, `engine.rs` — everything). Tauri Mobile added Android support in v2, it's production-ready, and it eliminates the JNI boundary entirely.
|
||||
|
||||
The incident postmortem lives at [`docs/incident-tauri-android-init-tcb.md`](incident-tauri-android-init-tcb.md).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Tauri 2.x Mobile │
|
||||
│ │
|
||||
│ Android WebView ────────── HTML/JS/CSS │ ← Shared with desktop
|
||||
│ │ (main.ts) │
|
||||
│ │ │
|
||||
│ invoke() ─────────────── Rust Commands │ ← Shared with desktop
|
||||
│ (main.rs) │
|
||||
│ │ │
|
||||
│ ┌───────────────┼────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ SignalMgr CallEngine Identity │ ← Shared crates
|
||||
│ (signal_hub) (wzp-client) (wzp-crypto)│
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ QUIC to relay Oboe audio (Android) │
|
||||
│ via wzp-native cdylib │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**What is reused from desktop verbatim** (zero rewrite):
|
||||
|
||||
- `desktop/src/main.ts` — entire frontend
|
||||
- `desktop/src/style.css` — all styling
|
||||
- `desktop/src/identicon.ts` — identicon rendering
|
||||
- `desktop/index.html` — HTML structure
|
||||
- `desktop/src-tauri/src/main.rs` — all Tauri commands (`connect`, `disconnect`, `register_signal`, `place_call`, …)
|
||||
- `desktop/src-tauri/src/engine.rs` — `CallEngine` wrapper
|
||||
|
||||
**What is Android-specific**:
|
||||
|
||||
- `desktop/src-tauri/src/android_audio.rs` — JVM-side audio routing (`AudioManager.setSpeakerphoneOn` for earpiece/speaker toggle). Runs from Tauri's existing JNI context — no hand-rolled bridge, Tauri owns the JVM hookup.
|
||||
- `desktop/src-tauri/src/wzp_native.rs` — runtime `dlopen` of `libwzp_native.so`, a standalone cdylib crate (`crates/wzp-native`) that owns all C++ (Oboe bridge). Kept in its own crate so its C/C++ static archives never get statically linked into `wzp-desktop`'s `.so`, which would re-trigger the `__init_tcb` / pthread leak.
|
||||
- `crates/wzp-native/` — the standalone C++/Oboe bridge cdylib. Loaded via `libloading` at runtime from `wzp_native.rs`. Provides capture + playout streams using Oboe's `Usage::VoiceCommunication` + `MODE_IN_COMMUNICATION` combo.
|
||||
- Android-specific target dependencies in `desktop/src-tauri/Cargo.toml` (`jni`, `ndk-context`, `libloading`) — no CPAL, no VPIO.
|
||||
|
||||
## Key architectural decisions
|
||||
|
||||
### 1. `wzp-native` as a standalone cdylib loaded via `libloading`
|
||||
|
||||
The alternative — linking `wzp-native` as a regular Rust dep with C++ static archives — would cause the same `__init_tcb` crash that killed the Kotlin version. By making `wzp-native` its own cdylib and `dlopen`-ing it at runtime, Bionic's `libc.so` resolves every symbol at load time the way it's supposed to, and no private TCB symbols leak.
|
||||
|
||||
### 2. `crate-type = ["cdylib", "rlib"]` only (no `staticlib`)
|
||||
|
||||
Same reason. The `rlib` output is needed so the `wzp-desktop` binary target can link against the library; `cdylib` is needed for Android's `System.loadLibrary`; `staticlib` would reintroduce the symbol-leak bug.
|
||||
|
||||
### 3. Oboe audio config
|
||||
|
||||
`Usage::VoiceCommunication` + Java-side `MODE_IN_COMMUNICATION`. **Never** call `setAudioApi(AAudio)` explicitly — on some devices (Nothing Phone in particular) it causes Oboe to open the wrong stream type and audio goes silent. Let Oboe pick the audio API automatically. This is documented in the auto-memory `project_tauri_android_audio.md`.
|
||||
|
||||
### 4. Speaker/earpiece toggle uses `tokio::task::spawn_blocking`
|
||||
|
||||
Oboe's `stop()` + `start()` cycle is synchronous and can block for 50–200 ms. Calling it on the tokio executor stalls every other async task (including the QUIC datagram loop), dropping audio packets. Wrapping the toggle in `spawn_blocking` isolates it to a dedicated thread pool. Fixed in commit `76a4c53`.
|
||||
|
||||
## Build pipeline
|
||||
|
||||
Docker on SepehrHomeserverdk, same pattern as the Android legacy pipeline and the Windows pipeline:
|
||||
|
||||
```
|
||||
./scripts/build-tauri-android.sh # Full: pull + build + ntfy + rustypaste
|
||||
./scripts/build-tauri-android.sh --pull # Explicit git pull (default)
|
||||
./scripts/build-tauri-android.sh --clean # Blow away the Rust target cache
|
||||
```
|
||||
|
||||
**Image**: `wzp-android-builder` (shared with the legacy Kotlin pipeline). The Dockerfile was extended to install Node.js 20 LTS, Android API level 36, build-tools 35.0.0, tauri-cli 2.x, and all four Android Rust targets on top of the legacy NDK 26.1 + cargo-ndk + Gradle setup. Both pipelines coexist in the same image.
|
||||
|
||||
**Output**: `wzp-release.apk` uploaded to rustypaste, URL delivered via `ntfy.sh/wzp`.
|
||||
|
||||
## Known quirks (Tauri Mobile specific)
|
||||
|
||||
1. **tauri-cli `android init` writes absolute paths** into `gradle.properties` for the NDK path. Those paths are local to wherever `android init` was run, so they break any cross-machine build unless overridden with `ANDROID_NDK_HOME` at build time. The build script exports `ANDROID_NDK_HOME` explicitly to work around this.
|
||||
|
||||
2. **API 36 vs API 34 coexistence**: the legacy Kotlin pipeline targets API 34, Tauri Mobile 2.x wants compileSdk 36. The shared Docker image installs both SDK levels so neither pipeline needs to reinstall.
|
||||
|
||||
3. **Identity seed lives in Android-specific app data dir**: `/data/data/com.wzp.phone/files/.wzp/identity` instead of `$HOME/.wzp/identity`. The shared `load_or_create_seed()` function in `desktop/src-tauri/src/lib.rs` uses Tauri's `app_data_dir()` which resolves correctly on both Android and desktop — no per-platform code needed.
|
||||
|
||||
4. **Direct calls on macOS previously hit an identity mismatch bug** — the `CallEngine` was using `$HOME/.wzp/identity` directly while `register_signal` used Tauri's `app_data_dir()`. Fixed by routing both through `load_or_create_seed()` (commit `2fd9465`). This was important for cross-platform consistency.
|
||||
|
||||
## Current state (snapshot)
|
||||
|
||||
What works:
|
||||
|
||||
- Tauri Mobile scaffold builds and runs on Android
|
||||
- Signal hub connect + register works
|
||||
- Room mode (SFU group calls) works with Oboe audio
|
||||
- Direct 1:1 calls work with full parity to desktop
|
||||
- Speaker/earpiece toggle works without stalling the audio pipeline
|
||||
- Call history, recent contacts, deregister UI all present (inherited from desktop)
|
||||
|
||||
What remains (task list refs in parens):
|
||||
|
||||
- Background service for keeping signal alive when app is backgrounded (#19)
|
||||
- Proper permission requests (microphone, notifications) on first launch (#19)
|
||||
- Incoming call notification while backgrounded (#19)
|
||||
- App icon + splash screen (#19)
|
||||
|
||||
## Testing
|
||||
|
||||
- **Build**: `./scripts/build-tauri-android.sh` — verify the APK lands on rustypaste and installs on device.
|
||||
- **Smoke test**: Install → open app → Register → Place call → Receive call. No crashes, audio flows both ways.
|
||||
- **Speaker toggle**: During a call, toggle speaker/earpiece several times in rapid succession. Audio should never stop, and the toggle should respond within ~200 ms.
|
||||
- **Stress test**: Call for 10+ minutes continuous. No memory growth, no packet loss beyond what's attributable to the network.
|
||||
|
||||
## Files of interest
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `desktop/src-tauri/src/lib.rs` | Shared Tauri commands (desktop + Android) |
|
||||
| `desktop/src-tauri/src/android_audio.rs` | JVM-side speaker/earpiece routing |
|
||||
| `desktop/src-tauri/src/wzp_native.rs` | Runtime dlopen of libwzp_native.so |
|
||||
| `crates/wzp-native/` | Standalone C++/Oboe cdylib, loaded at runtime |
|
||||
| `scripts/build-tauri-android.sh` | Remote Docker build pipeline |
|
||||
| `scripts/Dockerfile.android-builder` | Shared Android Docker image (legacy + Tauri) |
|
||||
| `docs/incident-tauri-android-init-tcb.md` | Postmortem of the Kotlin+JNI crash cascade |
|
||||
@@ -457,3 +457,52 @@ Auto mode uses three tiers (Good, Degraded, Catastrophic). It does not use the S
|
||||
When you select a specific profile (not Auto), adaptive switching is disabled. The encoder stays at the selected profile regardless of network conditions. This is useful when you know your network quality and want consistent encoding, or when you want to force a specific bitrate.
|
||||
|
||||
Note: The decoder always accepts all codecs. A manual quality selection only affects what you send, not what you receive.
|
||||
|
||||
## Direct 1:1 Calling (Desktop + Android)
|
||||
|
||||
In addition to room-mode group calls, you can place direct calls to a specific peer by fingerprint. Direct calls bypass room state entirely — the relay is used purely as a signaling gateway and for media relay. There is no need for the callee to join a room beforehand; they just need to be registered with the same signal hub.
|
||||
|
||||
### UI elements in the direct-call panel
|
||||
|
||||
- **Place call field** — paste a fingerprint (the long hex string you see under your own identity) and click Call. The callee sees a ringing UI.
|
||||
- **Recent contacts row** — a horizontal strip of chips showing your most recently called/receiving peers. Click a chip to re-dial. Aliases are shown if the peer has one, otherwise a short fingerprint prefix.
|
||||
- **Call history list** — every direct call you've placed, received, or missed, with direction indicator (↗ Outgoing, ↙ Incoming, ✗ Missed), the peer's alias (if known) or fingerprint prefix, and a timestamp. Click an entry to re-dial.
|
||||
- **Deregister button** — drops your signal-hub registration without quitting the app. Useful when switching identities (e.g. testing with two accounts on one machine) or when you want to explicitly appear offline to peers.
|
||||
- **Clear history button** — wipes the call history store. Does not affect current calls.
|
||||
|
||||
### Live updates
|
||||
|
||||
The call history updates in real time across all views via Tauri events (`history-changed`). Placing, answering, or missing a call immediately refreshes the history list and the recent contacts row — no manual refresh needed.
|
||||
|
||||
### Default room
|
||||
|
||||
On first launch, the room name in the room-mode panel defaults to `general` (changed from the prior `android` default so the desktop and Android clients don't silently talk past each other). You can still change it to any room name, and the last-used room is remembered across launches.
|
||||
|
||||
### Random alias
|
||||
|
||||
New installations derive a human-friendly alias from your identity seed — something like `silent-forest-41` or `bold-river-07`. It's deterministic, so reinstalling without changing your seed gives you the same alias. The alias is shown alongside your fingerprint in the header and is what peers see in their call history when they receive your call.
|
||||
|
||||
You can override the alias in Settings → Identity if you want a specific name.
|
||||
|
||||
## Windows AEC Variants
|
||||
|
||||
The Windows desktop build ships in two variants for echo cancellation, depending on which backend you want to exercise. Both are `wzp-desktop.exe` binaries — only the internal audio backend differs.
|
||||
|
||||
| Build | File | Capture backend | AEC | When to use |
|
||||
|---|---|---|---|---|
|
||||
| **noAEC baseline** | `wzp-desktop-noAEC.exe` | CPAL (WASAPI shared mode) | None | Headphone-only use, or for A/B comparison against the AEC build |
|
||||
| **Communications AEC** | `wzp-desktop.exe` | Direct WASAPI with `AudioCategory_Communications` | **Yes** — Windows routes the capture stream through the driver's communications APO chain (AEC + noise suppression + automatic gain control) | Any speaker-mode call, laptop built-in speakers, anywhere echo is audible |
|
||||
|
||||
**Quality caveat**: the communications AEC operates at the OS level and its algorithm depends on the audio driver's installed APO chain. On modern consumer laptops with Intel Smart Sound, Dolby, recent Realtek, or Windows 11 Voice Clarity, the quality is excellent (effectively matching what Teams/Zoom deliver). On generic class-compliant USB microphones or older drivers, the communications APO may not be present at all — in that case the build behaves identically to the noAEC baseline.
|
||||
|
||||
If you hear echo on the AEC build, try these in order before escalating:
|
||||
|
||||
1. **Check which capture device is selected as "Default Device - Communications"** in Windows Sound Settings → Recording tab. Right-click any device to set it. The AEC build opens the device marked as `eCommunications`, not `eConsole`, so changing the default-communications device changes what we capture from.
|
||||
2. **Verify the driver exposes a communications APO**. Sound Settings → Recording → your mic → Properties → Advanced → look for an "Enhancements" or "Signal Enhancements" tab. If it's absent, the driver has no APOs and the AEC build effectively has no AEC.
|
||||
3. **Try the classic Voice Capture DSP build** when it ships (tracked as task #26). That uses Microsoft's bundled software AEC (`CLSID_CWMAudioAEC`) which works on every Windows machine regardless of driver.
|
||||
|
||||
### Installing the Windows builds
|
||||
|
||||
1. Windows 10: install the [WebView2 Runtime Evergreen Bootstrapper](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) first. Windows 11 has it pre-installed.
|
||||
2. Copy `wzp-desktop.exe` (or `wzp-desktop-noAEC.exe`) to any directory and double-click. No installer needed.
|
||||
3. First launch creates the config + identity store at `%APPDATA%\com.wzp.phone\`.
|
||||
|
||||
431
docs/incident-tauri-android-init-tcb.md
Normal file
431
docs/incident-tauri-android-init-tcb.md
Normal file
@@ -0,0 +1,431 @@
|
||||
# Incident report — Tauri Android `__init_tcb+4` SIGSEGV
|
||||
|
||||
**Status:** Blocked. Reproducible crash with a known trigger at the cc::Build /
|
||||
rustc-link-lib layer that we cannot yet explain. Writing this report to hand
|
||||
off for external help.
|
||||
|
||||
**Project:** WarzonePhone (Rust + Tauri 2.x Mobile) Android rewrite
|
||||
**Branch:** `feat/desktop-audio-rewrite`
|
||||
**Target phone:** Pixel 6 (`oriole`), Android 16 (`BP3A.250905.014`), arm64-v8a
|
||||
**Date range of investigation:** 2026-04-09 (one working session, ~27 builds)
|
||||
|
||||
---
|
||||
|
||||
## One-paragraph summary
|
||||
|
||||
We're porting the existing CPAL-backed desktop Tauri app (`desktop/src-tauri`)
|
||||
to Tauri Mobile Android so the same Rust + Tauri + WebView codebase runs on
|
||||
both platforms. The Android `.apk` launches, renders the home screen, and
|
||||
registers on a relay for signal-only builds (no audio backend). The moment
|
||||
we add **any** `cc::Build::new().cpp(true).cpp_link_stdlib("c++_shared")`
|
||||
call to `build.rs` — even with a 6-line cpp file that just returns 42 and is
|
||||
never called from Rust — the built `.so` crashes at launch inside
|
||||
`__init_tcb(bionic_tcb*, pthread_internal_t*)+4` via `pthread_create` via
|
||||
`std::thread::spawn` via `tao::ndk_glue::create` via
|
||||
`Java_com_wzp_desktop_WryActivity_create`, before our Rust entry point has
|
||||
a chance to run. The exact same NDK, exact same Rust toolchain, exact same
|
||||
Docker image is used by the legacy `wzp-android` crate (via `cargo-ndk`)
|
||||
which compiles Oboe and runs fine on the same phone.
|
||||
|
||||
---
|
||||
|
||||
## Environment
|
||||
|
||||
**Docker build image:** `wzp-android-builder` (Dockerfile at
|
||||
`scripts/Dockerfile.android-builder`)
|
||||
|
||||
- Base: `debian:bookworm`
|
||||
- JDK 17
|
||||
- Android SDK:
|
||||
- cmdline-tools latest
|
||||
- `platforms;android-34`, `platforms;android-36`
|
||||
- `build-tools;34.0.0`, `build-tools;35.0.0`
|
||||
- `ndk;26.1.10909125` (last stable before scudo/MTE crash on NDK r27+)
|
||||
- `platform-tools`
|
||||
- Node.js 20 LTS
|
||||
- Rust stable `1.94.1 (e408947bf 2026-03-25)`
|
||||
- Rust android targets: `aarch64-linux-android`, `armv7-linux-androideabi`,
|
||||
`i686-linux-android`, `x86_64-linux-android`
|
||||
- `cargo-ndk` + `cargo tauri-cli 2.10.1` (latest 2.x)
|
||||
|
||||
**Host:** Docker on `SepehrHomeserverdk` (remote build server).
|
||||
|
||||
**Phone:** Pixel 6, Android 16, kernel 6.1.134-android14-11, on the same LAN
|
||||
as the build machine and a local `wzp-relay` binary.
|
||||
|
||||
**Tauri crate:** `desktop/src-tauri/` in the workspace at the root of the
|
||||
repo. Depends on `tauri = "2"`, `tauri-plugin-shell = "2"`, `tokio`, `rustls`,
|
||||
`wzp-proto`, `wzp-codec`, `wzp-fec`, `wzp-crypto`, `wzp-transport`, and (on
|
||||
non-Android only) `wzp-client` with `features = ["audio", "vpio"]`. The
|
||||
crate's `[lib]` section is:
|
||||
|
||||
```toml
|
||||
[lib]
|
||||
name = "wzp_desktop_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
```
|
||||
|
||||
The crate produces `libwzp_desktop_lib.so` which is `System.loadLibrary`'d by
|
||||
Tauri's generated `WryActivity.onCreate` via JNI.
|
||||
|
||||
---
|
||||
|
||||
## The crash
|
||||
|
||||
Every failing build produces the same stack at launch, same pc offsets:
|
||||
|
||||
```
|
||||
signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x00000072XXXXXX00f (write)
|
||||
|
||||
#00 pc 000000000130cc74 libwzp_desktop_lib.so (__init_tcb(bionic_tcb*, pthread_internal_t*)+4)
|
||||
#01 pc 0000000001331cf0 libwzp_desktop_lib.so (pthread_create+360)
|
||||
#02 pc 00000000012bee04 libwzp_desktop_lib.so (std::sys::thread::unix::Thread::new::h87be8e9feeaaaf84+184)
|
||||
#03 pc 0000000000e37f5c libwzp_desktop_lib.so (std::thread::lifecycle::spawn_unchecked::h941f828f9a95150d+1504)
|
||||
#04 pc 0000000000e461e8 libwzp_desktop_lib.so (std::thread::builder::Builder::spawn_unchecked::hec5f087680cb0248+112)
|
||||
#05 pc 0000000000e441c8 libwzp_desktop_lib.so (std::thread::functions::spawn::ha3d3fbf2d9fe53e3+108)
|
||||
#06 pc ... libwzp_desktop_lib.so (tao::platform_impl::platform::ndk_glue::create::h254c68662718841a+1792)
|
||||
#07 pc ... libwzp_desktop_lib.so (Java_com_wzp_desktop_WryActivity_create+76)
|
||||
```
|
||||
|
||||
The offsets are **byte-identical across every failing build**, even when the
|
||||
cpp content changes drastically (cf. `cpp_smoke.cpp` at 6 lines, 20 lines,
|
||||
200+ Oboe source files). We believe this is because cargo caches the Rust
|
||||
compilation unit and only the build-script artifacts differ, and the final
|
||||
link produces the same layout.
|
||||
|
||||
`__init_tcb` is defined locally inside our `.so` with C++ mangling:
|
||||
|
||||
```
|
||||
_Z10__init_tcbP10bionic_tcbP18pthread_internal_t
|
||||
```
|
||||
|
||||
It originates from bionic's `pthread_create.cpp`, which got pulled in
|
||||
statically from the NDK's `sysroot/usr/lib/aarch64-linux-android/libc.a`.
|
||||
Both failing and known-good (legacy `wzp_android.so`) builds contain this
|
||||
same static symbol — the presence of the symbol is not the problem.
|
||||
|
||||
Fault address `0x72XXXXXX00f` with code `SEGV_ACCERR` (access permission
|
||||
error, write). Aligned to `+4` inside `__init_tcb`, which is typically a
|
||||
store into the passed-in `bionic_tcb*`. The pointer is either NULL-ish or
|
||||
pointing into read-only memory.
|
||||
|
||||
---
|
||||
|
||||
## Bisection (the important part)
|
||||
|
||||
We started from a known-good commit (`5309938`) where the Tauri Android app
|
||||
launches, registers on a relay, and behaves identically to the desktop app
|
||||
modulo audio. Then we added features **one variable at a time**:
|
||||
|
||||
| Step | Commit | Change vs previous | Result |
|
||||
|---|---|---|---|
|
||||
| Baseline | `5309938` | — | ✅ launches, renders home, registers on relay |
|
||||
| **A** | `f96d7ce` | Add `cc = "1"` build-dep + compile trivial `cpp/hello.c` via `cc::Build` (C, not C++). Static lib never linked in. | ✅ |
|
||||
| **B** | `ae4f366` | Add `wzp-client` Android dep with `default-features = false` (no CPAL, no VPIO). No new imports. | ✅ |
|
||||
| **C** | `19fd3dd` | Un-cfg-gate `mod engine;` in `lib.rs` so `engine.rs` compiles on Android. `CallEngine::start()` has an Android stub returning an error. | ✅ |
|
||||
| **D** | `a852cad` | Compile `cpp/getauxval_fix.c` (legacy wzp-android shim). Still pure C. | ✅ |
|
||||
| **E** | `4250f1b` | **Compile full Oboe C++ bridge** (200+ source files from `google/oboe@1.8.1`). `cc::Build::new().cpp(true).std("c++17").cpp_link_stdlib(Some("c++_shared"))` + `-llog` + `-lOpenSLES` link directives. Nothing called from Rust yet — the `extern "C"` bridge functions are exported but never referenced from the Rust side. | ❌ **crash** |
|
||||
| E.4 | `aa240c6` | **Only change:** replace the entire Oboe compile with ONE tiny `cpp_smoke.cpp` file: `extern "C" int wzp_cpp_smoke(void) { std::lock_guard<std::mutex> lk(m); std::thread t([](){...}); t.join(); return g.load(); }`. Still `cpp(true) + cpp_link_stdlib("c++_shared")`. Drop `-llog`/`-lOpenSLES`. | ❌ **same crash, same offsets** |
|
||||
| E.2 | `0224ce6` | Shrink `cpp_smoke.cpp` further: just `std::atomic<int>` + `fetch_add`, no mutex, no thread, no includes beyond `<atomic>`. | ❌ **same crash, same offsets** |
|
||||
| E.1 | `0d74366` | **Absolute minimum:** `cpp_smoke.cpp` = `extern "C" int wzp_cpp_hello(void){return 42;}`. NO `#include`. NO STL. Just a function. Still compiled with `cpp(true) + cpp_link_stdlib("c++_shared")`. | ❌ **same crash, same offsets** |
|
||||
|
||||
### Additional confirming observations
|
||||
|
||||
1. **The cpp code is dead-stripped.** `llvm-nm -a libwzp_desktop_lib.so` shows
|
||||
zero matches for `wzp_cpp_hello`, `wzp_cpp_smoke`, or any Oboe symbol in
|
||||
builds E through E.1. The static archive (`libwzp_cpp_smoke.a` /
|
||||
`liboboe_bridge.a`) exists on disk under
|
||||
`target/aarch64-linux-android/debug/build/wzp-desktop-*/out/`, but because
|
||||
nothing in Rust ever references the exported C function, the final linker
|
||||
drops it.
|
||||
|
||||
2. **`build.rs` link directives are the real delta.** `cc::Build::new()
|
||||
.cpp(true).cpp_link_stdlib(Some("c++_shared"))` emits a
|
||||
`cargo:rustc-link-lib=c++_shared` directive that adds a `NEEDED` entry for
|
||||
`libc++_shared.so` to the final `.so`'s dynamic table. `readelf -d` on
|
||||
the crashing `.so` shows:
|
||||
|
||||
```
|
||||
NEEDED Shared library: [libc++_shared.so]
|
||||
NEEDED Shared library: [liblog.so] (only in full Oboe build)
|
||||
NEEDED Shared library: [libOpenSLES.so] (only in full Oboe build)
|
||||
```
|
||||
|
||||
The working baseline `.so` has no `NEEDED` entries beyond libc/liblog.
|
||||
|
||||
3. **Linker version doesn't matter.** We tried forcing
|
||||
`aarch64-linux-android26-clang` as the linker (API 26 has proper dynamic
|
||||
bindings to libc.so's runtime `pthread_create`/`__init_tcb`) via three
|
||||
different mechanisms:
|
||||
- `CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER` env var in `docker run`
|
||||
- `.cargo/config.toml` workspace-level linker override
|
||||
- **Binary replacement inside the image**: `mv
|
||||
aarch64-linux-android24-clang .orig` and replace with a shell script
|
||||
that `exec`s `aarch64-linux-android26-clang`. Verified by calling
|
||||
`--version` which prints `Target: aarch64-unknown-linux-android26`.
|
||||
|
||||
All three made no difference. The `__init_tcb` symbol is pulled statically
|
||||
from the **same** `libc.a` regardless of which clang wrapper is used — the
|
||||
NDK ships ONE `libc.a` at
|
||||
`sysroot/usr/lib/aarch64-linux-android/libc.a` shared across all API
|
||||
levels. Only the per-API `libc.so` symlinks change (and we're linked
|
||||
statically, not dynamically, against libc).
|
||||
|
||||
4. **Legacy `wzp-android` crate works on the same phone, same image.** Run
|
||||
in the exact same Docker container, the legacy Kotlin app's JNI library
|
||||
(`crates/wzp-android` built via `cargo ndk`) compiles a subset of the
|
||||
same Oboe code, produces a `.so` that has the same static
|
||||
`_Z10__init_tcbP...` + `pthread_create` + `pthread_create.cpp` symbols,
|
||||
and launches cleanly on the Pixel 6. Key differences between the two
|
||||
build paths:
|
||||
|
||||
| | `wzp-android` (works) | `wzp-desktop` Tauri (crashes) |
|
||||
|---|---|---|
|
||||
| Build driver | `cargo ndk -t arm64-v8a build --release -p wzp-android` | `cargo tauri android build --debug --target aarch64 --apk` |
|
||||
| Profile | release | debug (release crashes identically) |
|
||||
| Linker | `aarch64-linux-android26-clang` (via `.cargo/config.toml` which cargo-ndk honors) | `aarch64-linux-android24-clang` (tauri-cli hardcodes and ignores config; the shim redirect makes no difference) |
|
||||
| crate-type | `["cdylib", "rlib"]` | `["staticlib", "cdylib", "rlib"]` |
|
||||
| JNI entrypoint | direct Kotlin `System.loadLibrary` + our own `native fun` declarations; first `pthread_create` runs later from the tokio runtime inside a command | `WryActivity.onCreate` via Tauri's generated Java glue; first `pthread_create` runs **inside the JNI call** via `tao::ndk_glue::create` |
|
||||
| Other heavy deps | tokio, wzp-{proto,codec,fec,crypto,transport} | tokio, tauri, tauri-runtime-wry, tao, wry, webview2-com, soup3, webkit2gtk (all platform-specific ones cfg-gated out of android), and also all of the above |
|
||||
| Binary size | `libwzp_android.so` ≈ 14 MB (release) | `libwzp_desktop_lib.so` ≈ 160 MB (debug), 16 MB (release) |
|
||||
|
||||
5. **The crash happens in the JNI-callback thread during `onCreate`.** Frame
|
||||
#06 `tao::platform_impl::platform::ndk_glue::create+1792` is tao's Android
|
||||
event-loop bootstrap, which Tauri calls from inside
|
||||
`Java_com_wzp_desktop_WryActivity_create` in response to the Java-side
|
||||
activity lifecycle. This means the thread spawn is happening while the
|
||||
Java VM still holds the native onCreate call, before `onCreate` has
|
||||
returned to the Android runtime. Legacy `wzp-android` never spawns a
|
||||
thread from an onCreate JNI call — it spawns threads only from
|
||||
`nativeSignalConnect`/similar commands invoked later from Kotlin button
|
||||
clicks, after the activity is fully initialised.
|
||||
|
||||
---
|
||||
|
||||
## Current suspect
|
||||
|
||||
One of the two items below, probably (2):
|
||||
|
||||
1. **The `.cpp(true)` mode in cc-rs changes something invisible in the link
|
||||
pipeline** (for example, emitting a different `-x` flag to clang, or
|
||||
changing linker driver selection). We have not yet verified this by
|
||||
diffing the actual rustc linker invocation between a working and a
|
||||
crashing build with `--verbose` + `-Clink-arg=-Wl,-t`.
|
||||
|
||||
2. **Adding `libc++_shared.so` as a NEEDED entry causes Android's dynamic
|
||||
linker to load libc++_shared.so before our `.so`'s init runs, and
|
||||
something in libc++_shared's `.init_array` interacts badly with
|
||||
tao::ndk_glue's `pthread_create` call from inside the JNI onCreate
|
||||
window**. The legacy crate doesn't hit this because (a) it has no
|
||||
NEEDED libc++_shared when built without Oboe, and (b) even when it does
|
||||
build Oboe, its thread spawns happen outside the onCreate JNI call so
|
||||
whatever libc state is wrong at that moment is already stabilised.
|
||||
|
||||
We have not yet confirmed (2) with the obvious A/B test: keep `cpp_smoke.cpp`
|
||||
but drop `.cpp_link_stdlib(Some("c++_shared"))` (and drop any manual
|
||||
`cargo:rustc-link-lib=c++_shared`) so the NEEDED entry disappears but the
|
||||
rest of the pipeline stays identical. That's the next experiment we were
|
||||
going to run, but the user reasonably asked for this report first.
|
||||
|
||||
---
|
||||
|
||||
## What we've ruled out
|
||||
|
||||
- **NDK API level** — forcing API-26 linker via three independent mechanisms
|
||||
made zero difference.
|
||||
- **Build profile** — release (`0x6b8000` offset, 21 MB unsigned APK) and
|
||||
debug (same 193 MB APK, same crash offsets) both crash identically.
|
||||
- **Oboe specifically** — replacing the Oboe compile with 6 lines of C++
|
||||
that does nothing still reproduces the crash.
|
||||
- **cpp code being executed at runtime** — dead-stripped, not in the final
|
||||
`.so` at all per `nm -a`.
|
||||
- **minSdk in build.gradle** — bumped from 24 to 26, no effect.
|
||||
- **libdl.a stub issue** — ruled out via logcat (`libdl.a is a stub --- use
|
||||
libdl.so instead` was only surfacing from our own `dlsym` shim that we
|
||||
subsequently deleted).
|
||||
- **`pthread_create` interposition via `-Wl,--wrap=pthread_create`** — tried
|
||||
and reverted; the wrap target still resolved to the broken static stub.
|
||||
- **Keystore / signing** — debug signing with persistent `~/.android/
|
||||
debug.keystore` works fine; no signature mismatch issues.
|
||||
|
||||
---
|
||||
|
||||
## The files involved
|
||||
|
||||
### `desktop/src-tauri/build.rs` (current state, E.1)
|
||||
|
||||
```rust
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
// Embedded git hash
|
||||
let git_hash = Command::new("git")
|
||||
.args(["rev-parse", "--short", "HEAD"])
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|o| o.status.success())
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_else(|| "unknown".into());
|
||||
println!("cargo:rustc-env=WZP_GIT_HASH={git_hash}");
|
||||
println!("cargo:rerun-if-changed=../../.git/HEAD");
|
||||
println!("cargo:rerun-if-changed=../../.git/refs/heads");
|
||||
|
||||
let target = std::env::var("TARGET").unwrap_or_default();
|
||||
if target.contains("android") {
|
||||
// Step A: plain C sanity file
|
||||
println!("cargo:rerun-if-changed=cpp/hello.c");
|
||||
cc::Build::new().file("cpp/hello.c").compile("wzp_hello");
|
||||
|
||||
// Step D: legacy getauxval shim
|
||||
println!("cargo:rerun-if-changed=cpp/getauxval_fix.c");
|
||||
cc::Build::new().file("cpp/getauxval_fix.c").compile("getauxval_fix");
|
||||
|
||||
// Step E.1: minimal C++ smoke — THIS STEP BRINGS BACK THE CRASH
|
||||
println!("cargo:rerun-if-changed=cpp/cpp_smoke.cpp");
|
||||
cc::Build::new()
|
||||
.cpp(true)
|
||||
.std("c++17")
|
||||
.cpp_link_stdlib(Some("c++_shared"))
|
||||
.file("cpp/cpp_smoke.cpp")
|
||||
.compile("wzp_cpp_smoke");
|
||||
|
||||
// Copy libc++_shared.so into gen/android jniLibs so the runtime
|
||||
// linker can find it when the NEEDED entry fires.
|
||||
if let Ok(ndk) = std::env::var("ANDROID_NDK_HOME").or_else(|_| std::env::var("NDK_HOME")) {
|
||||
let triple = "aarch64-linux-android";
|
||||
let abi = "arm64-v8a";
|
||||
let lib_dir = format!(
|
||||
"{ndk}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/{triple}"
|
||||
);
|
||||
println!("cargo:rustc-link-search=native={lib_dir}");
|
||||
let shared_so = format!("{lib_dir}/libc++_shared.so");
|
||||
if std::path::Path::new(&shared_so).exists() {
|
||||
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
|
||||
let jni_dir = format!("{manifest}/gen/android/app/src/main/jniLibs/{abi}");
|
||||
if std::fs::create_dir_all(&jni_dir).is_ok() {
|
||||
let _ = std::fs::copy(&shared_so, format!("{jni_dir}/libc++_shared.so"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tauri_build::build()
|
||||
}
|
||||
```
|
||||
|
||||
### `desktop/src-tauri/cpp/cpp_smoke.cpp` (E.1)
|
||||
|
||||
```cpp
|
||||
extern "C" int wzp_cpp_hello(void) {
|
||||
return 42;
|
||||
}
|
||||
```
|
||||
|
||||
### `desktop/src-tauri/Cargo.toml` (relevant excerpts)
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "wzp-desktop"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "wzp_desktop_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "wzp-desktop"
|
||||
path = "src/main.rs"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
cc = "1"
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-shell = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
anyhow = "1"
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
||||
|
||||
wzp-proto = { path = "../../crates/wzp-proto" }
|
||||
wzp-codec = { path = "../../crates/wzp-codec" }
|
||||
wzp-fec = { path = "../../crates/wzp-fec" }
|
||||
wzp-crypto = { path = "../../crates/wzp-crypto" }
|
||||
wzp-transport = { path = "../../crates/wzp-transport" }
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
wzp-client = { path = "../../crates/wzp-client", features = ["audio", "vpio"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
wzp-client = { path = "../../crates/wzp-client", default-features = false }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reproduction
|
||||
|
||||
A fresh clone on a Linux x86_64 host with:
|
||||
|
||||
```bash
|
||||
git clone ssh://git@git.manko.yoga:222/manawenuz/wz-phone.git
|
||||
cd wz-phone
|
||||
git checkout feat/desktop-audio-rewrite
|
||||
git reset --hard 0d74366 # <-- step E.1, smallest crashing commit
|
||||
|
||||
# Need: Android NDK r26.1.10909125, JDK 17, Node 20, Rust stable, cargo tauri 2.x
|
||||
scripts/prep-linux-mint.sh # installs all the above into /opt/android-sdk etc.
|
||||
|
||||
cd desktop
|
||||
npm install
|
||||
cd src-tauri
|
||||
cargo tauri android build --debug --target aarch64 --apk
|
||||
adb install -r gen/android/app/build/outputs/apk/universal/debug/app-universal-debug.apk
|
||||
adb logcat -c && adb shell am start -n com.wzp.desktop/.MainActivity
|
||||
adb logcat | grep -E "F DEBUG|__init_tcb|pthread_create"
|
||||
```
|
||||
|
||||
Expected result: SIGSEGV at `__init_tcb+4` within ~500 ms of launch.
|
||||
|
||||
Reverting `cpp/cpp_smoke.cpp` + the `cc::Build` call for it in `build.rs`
|
||||
(one git command: `git revert 0d74366 aa240c6 0224ce6 a852cad`) restores a
|
||||
working build. Keeping the C sanity compile (`hello.c`, `getauxval_fix.c`)
|
||||
is fine — only the `.cpp(true) + .cpp_link_stdlib("c++_shared")` combination
|
||||
triggers the regression.
|
||||
|
||||
---
|
||||
|
||||
## What we'd like help with
|
||||
|
||||
1. **Is our suspect #2 actually the mechanism?** Is there a known issue
|
||||
where a Tauri/tao android cdylib crashes on load when it has a
|
||||
`libc++_shared.so` NEEDED entry and tries to spawn a thread from inside
|
||||
an onCreate JNI call?
|
||||
|
||||
2. **What's the correct way to link Oboe (or any C++ Android audio
|
||||
library) into a `cargo tauri android build` cdylib** without hitting
|
||||
this? Is there a known-good combination of cc-rs flags / linker
|
||||
arguments / cargo config?
|
||||
|
||||
3. **Is there a way to force `cargo tauri` to use the same linker setup
|
||||
as `cargo ndk`**, which reliably produces working Oboe-linked .so
|
||||
files from the exact same workspace? We've tried env var override,
|
||||
`.cargo/config.toml`, and image-level binary replacement — cargo
|
||||
tauri ignores all three and keeps using
|
||||
`aarch64-linux-android24-clang`.
|
||||
|
||||
4. **Is there a way to defer `tao::ndk_glue::create`'s thread spawn to
|
||||
after `onCreate` returns** so that whatever bionic state `__init_tcb`
|
||||
depends on is ready?
|
||||
|
||||
5. **Lastly** — is there a fundamentally different approach we should
|
||||
take (e.g., use the `oboe` Rust crate from crates.io instead of a
|
||||
hand-rolled C++ bridge, use Android's AAudio directly via the `ndk`
|
||||
crate's aaudio bindings, or even abandon the C++ audio path and
|
||||
implement mic/speaker via JNI into Java `AudioRecord`/`AudioTrack`)?
|
||||
@@ -1,11 +1,16 @@
|
||||
# =============================================================================
|
||||
# WZ Phone — Android build environment (Debian 12 / Bookworm)
|
||||
#
|
||||
# Matches the bare-metal build-android.sh environment:
|
||||
# Supports both:
|
||||
# 1. Legacy Kotlin+JNI Android app (via cargo-ndk + gradle)
|
||||
# 2. Tauri 2.x Mobile Android app (via tauri-cli + Node/npm)
|
||||
#
|
||||
# Toolchain:
|
||||
# - Debian 12 (cmake 3.25, no Android cross-compilation bugs)
|
||||
# - JDK 17 (Gradle 8.5 + AGP 8.2.0 compatible)
|
||||
# - NDK 26.1 (last stable before scudo/MTE crash on NDK 27+)
|
||||
# - Rust stable with aarch64-linux-android target + cargo-ndk
|
||||
# - Node.js 20 LTS (for Tauri frontend build)
|
||||
# - Rust stable with all 4 Android targets + cargo-ndk + tauri-cli 2.x
|
||||
#
|
||||
# Build: docker build -t wzp-android-builder -f Dockerfile.android-builder .
|
||||
# =============================================================================
|
||||
@@ -13,6 +18,11 @@ FROM debian:bookworm
|
||||
|
||||
ARG NDK_VERSION=26.1.10909125
|
||||
ARG ANDROID_API=34
|
||||
# Tauri 2.x mobile targets compileSdk 36 + build-tools 35 by default. Install
|
||||
# both 34 (legacy Kotlin app) and 35/36 (Tauri mobile) so the same image works
|
||||
# for both pipelines.
|
||||
ARG ANDROID_API_TAURI=36
|
||||
ARG BUILD_TOOLS_TAURI=35.0.0
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
ANDROID_HOME=/opt/android-sdk \
|
||||
@@ -35,8 +45,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
openjdk-17-jdk-headless \
|
||||
ca-certificates \
|
||||
libasound2-dev \
|
||||
file \
|
||||
xz-utils \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ── Node.js 20 LTS (required by Tauri for frontend build) ────────────────────
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& node --version \
|
||||
&& npm --version
|
||||
|
||||
# ── Android SDK + NDK 26.1 ──────────────────────────────────────────────────
|
||||
RUN mkdir -p $ANDROID_HOME/cmdline-tools \
|
||||
&& cd /tmp \
|
||||
@@ -49,10 +68,36 @@ RUN yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/nu
|
||||
&& $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install \
|
||||
"platforms;android-${ANDROID_API}" \
|
||||
"build-tools;${ANDROID_API}.0.0" \
|
||||
"platforms;android-${ANDROID_API_TAURI}" \
|
||||
"build-tools;${BUILD_TOOLS_TAURI}" \
|
||||
"ndk;${NDK_VERSION}" \
|
||||
"platform-tools" \
|
||||
2>&1 | grep -v '^\[' > /dev/null
|
||||
|
||||
# Work around the API-24 libc.a stub in the NDK. Any C++ static lib we
|
||||
# link into libwzp_desktop_lib.so (e.g. the Oboe audio bridge) pulls in
|
||||
# bionic's static pthread_create from API-24 libc.a via libc++_shared,
|
||||
# and that pthread_create crashes at __init_tcb+4 when called from a
|
||||
# .so loaded via dlopen (the static stub expects libc init state that
|
||||
# only exists for main executables). API-26 has the proper runtime
|
||||
# bindings. Tauri-cli hard-codes aarch64-linux-android24-clang as the
|
||||
# linker and ignores .cargo/config.toml overrides, so the only sure
|
||||
# fix is to replace the NDK's ${abi}24-clang binary itself with a
|
||||
# shim that exec()s the ${abi}26-clang equivalent. Applies to all four
|
||||
# ABIs × {clang, clang++}. The legacy wzp-android crate works without
|
||||
# this because cargo-ndk honours a crate-level linker override; the
|
||||
# shim is the minimal targeted fix for the cargo-tauri build path.
|
||||
# Added as Option 3 for the incremental Step E regression (commit 4250f1b).
|
||||
RUN set -eux; \
|
||||
BIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin; \
|
||||
for abi in aarch64-linux-android armv7a-linux-androideabi i686-linux-android x86_64-linux-android; do \
|
||||
for suffix in clang clang++; do \
|
||||
mv "$BIN/${abi}24-${suffix}" "$BIN/${abi}24-${suffix}.orig"; \
|
||||
printf '#!/bin/sh\nexec "%s/%s26-%s" "$@"\n' "$BIN" "$abi" "$suffix" > "$BIN/${abi}24-${suffix}"; \
|
||||
chmod +x "$BIN/${abi}24-${suffix}"; \
|
||||
done; \
|
||||
done
|
||||
|
||||
# Make SDK world-readable so builder user can access it
|
||||
RUN chmod -R a+rX $ANDROID_HOME
|
||||
|
||||
@@ -64,12 +109,22 @@ USER builder
|
||||
WORKDIR /home/builder
|
||||
|
||||
# ── Rust toolchain ───────────────────────────────────────────────────────────
|
||||
# Install all 4 Android targets (Tauri Mobile builds for all ABIs by default;
|
||||
# cargo-ndk legacy path only needs arm64-v8a — both workflows supported).
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||
| sh -s -- -y --default-toolchain stable \
|
||||
&& . $HOME/.cargo/env \
|
||||
&& rustup target add aarch64-linux-android \
|
||||
&& cargo install cargo-ndk
|
||||
&& rustup target add \
|
||||
aarch64-linux-android \
|
||||
armv7-linux-androideabi \
|
||||
i686-linux-android \
|
||||
x86_64-linux-android \
|
||||
&& cargo install cargo-ndk \
|
||||
&& cargo install tauri-cli --version "^2.0" --locked
|
||||
|
||||
ENV PATH="/home/builder/.cargo/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$JAVA_HOME/bin:$PATH"
|
||||
|
||||
# NDK_HOME is the env var tauri-cli checks (in addition to ANDROID_NDK_HOME)
|
||||
ENV NDK_HOME=$ANDROID_NDK_HOME
|
||||
|
||||
WORKDIR /build/source
|
||||
|
||||
253
scripts/build-tauri-android.sh
Executable file
253
scripts/build-tauri-android.sh
Executable file
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# WZ Phone — Tauri 2.x Mobile Android APK build
|
||||
#
|
||||
# Builds the desktop/ Tauri app as an Android APK via cargo-tauri inside the
|
||||
# wzp-android-builder Docker image on SepehrHomeserverdk. Uploads the APK to
|
||||
# rustypaste, fires ntfy.sh/wzp notifications at start + finish, and SCPs the
|
||||
# APK back locally.
|
||||
#
|
||||
# Same pattern as build-and-notify.sh but for the Tauri mobile pipeline:
|
||||
# - Source: desktop/src-tauri/ (not android/)
|
||||
# - Build: cargo tauri android build (not gradlew assembleDebug)
|
||||
# - Output: desktop/src-tauri/gen/android/.../*.apk
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/build-tauri-android.sh # full pipeline (debug)
|
||||
# ./scripts/build-tauri-android.sh --release # release APK
|
||||
# ./scripts/build-tauri-android.sh --no-pull # skip git fetch
|
||||
# ./scripts/build-tauri-android.sh --rust # force-clean rust target
|
||||
# ./scripts/build-tauri-android.sh --init # also run `cargo tauri android init`
|
||||
#
|
||||
# Environment:
|
||||
# WZP_BRANCH Branch to build (default: feat/desktop-audio-rewrite)
|
||||
# =============================================================================
|
||||
|
||||
REMOTE_HOST="SepehrHomeserverdk"
|
||||
BASE_DIR="/mnt/storage/manBuilder"
|
||||
NTFY_TOPIC="https://ntfy.sh/wzp"
|
||||
LOCAL_OUTPUT="target/tauri-android-apk"
|
||||
BRANCH="${WZP_BRANCH:-feat/desktop-audio-rewrite}"
|
||||
SSH_OPTS="-o ConnectTimeout=15 -o ServerAliveInterval=15 -o ServerAliveCountMax=4 -o LogLevel=ERROR"
|
||||
|
||||
REBUILD_RUST=0
|
||||
DO_PULL=1
|
||||
DO_INIT=0
|
||||
BUILD_RELEASE=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--rust) REBUILD_RUST=1 ;;
|
||||
--pull) DO_PULL=1 ;;
|
||||
--no-pull) DO_PULL=0 ;;
|
||||
--init) DO_INIT=1 ;;
|
||||
--release) BUILD_RELEASE=1 ;;
|
||||
-h|--help)
|
||||
sed -n '3,30p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
log() { echo -e "\033[1;36m>>> $*\033[0m"; }
|
||||
ssh_cmd() { ssh -A $SSH_OPTS "$REMOTE_HOST" "$@"; }
|
||||
|
||||
notify_local() { curl -s -d "$1" "$NTFY_TOPIC" > /dev/null 2>&1 || true; }
|
||||
|
||||
mkdir -p "$LOCAL_OUTPUT"
|
||||
|
||||
log "Uploading remote build script..."
|
||||
ssh_cmd "cat > /tmp/wzp-tauri-build.sh" <<'REMOTE_SCRIPT'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_DIR="/mnt/storage/manBuilder"
|
||||
NTFY_TOPIC="https://ntfy.sh/wzp"
|
||||
BRANCH="${1:-feat/desktop-audio-rewrite}"
|
||||
DO_PULL="${2:-1}"
|
||||
REBUILD_RUST="${3:-0}"
|
||||
DO_INIT="${4:-0}"
|
||||
BUILD_RELEASE="${5:-0}"
|
||||
|
||||
LOG_FILE=/tmp/wzp-tauri-build.log
|
||||
GIT_HASH="unknown" # populated after fetch
|
||||
ENV_FILE="$BASE_DIR/.env"
|
||||
|
||||
notify() { curl -s -d "$1" "$NTFY_TOPIC" > /dev/null 2>&1 || true; }
|
||||
|
||||
# Upload a file to rustypaste; print URL on stdout (or empty on failure).
|
||||
upload_to_rustypaste() {
|
||||
local file="$1"
|
||||
[ ! -f "$ENV_FILE" ] && { echo ""; return; }
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
if [ -n "${rusty_address:-}" ] && [ -n "${rusty_auth_token:-}" ]; then
|
||||
curl -s -F "file=@$file" -H "Authorization: $rusty_auth_token" "$rusty_address" || echo ""
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# On failure: upload the build log to rustypaste, then notify with hash + url.
|
||||
on_error() {
|
||||
local line="$1"
|
||||
local log_url
|
||||
log_url=$(upload_to_rustypaste "$LOG_FILE" || echo "")
|
||||
if [ -n "$log_url" ]; then
|
||||
notify "WZP Tauri Android build FAILED [$GIT_HASH] (line $line)
|
||||
log: $log_url"
|
||||
else
|
||||
notify "WZP Tauri Android build FAILED [$GIT_HASH] (line $line) — log upload failed, see $LOG_FILE on remote"
|
||||
fi
|
||||
}
|
||||
trap 'on_error $LINENO' ERR
|
||||
|
||||
exec > >(tee "$LOG_FILE") 2>&1
|
||||
|
||||
if [ "$DO_PULL" = "1" ]; then
|
||||
echo ">>> git fetch + reset $BRANCH"
|
||||
cd "$BASE_DIR/data/source"
|
||||
git reset --hard HEAD 2>/dev/null || true
|
||||
# NOTE: deliberately do NOT run `git clean -fd` here. It would wipe the
|
||||
# tauri-generated `desktop/src-tauri/gen/android/` scaffold (gradlew,
|
||||
# settings.gradle, etc.) which is expensive to recreate and breaks
|
||||
# subsequent builds with "gradlew not found".
|
||||
git gc --prune=now 2>/dev/null || true
|
||||
git fetch origin "$BRANCH" 2>&1 | tail -3
|
||||
git checkout "$BRANCH" 2>/dev/null || git checkout -b "$BRANCH" "origin/$BRANCH"
|
||||
git reset --hard "origin/$BRANCH"
|
||||
git submodule update --init || true
|
||||
fi
|
||||
|
||||
GIT_HASH=$(cd "$BASE_DIR/data/source" && git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||
GIT_MSG=$(cd "$BASE_DIR/data/source" && git log -1 --pretty=%s 2>/dev/null | head -c 60 || echo "?")
|
||||
notify "WZP Tauri Android build STARTED [$GIT_HASH] — $GIT_MSG"
|
||||
|
||||
# Fix perms so uid 1000 can write
|
||||
find "$BASE_DIR/data/source" "$BASE_DIR/data/cache" \
|
||||
! -user 1000 -o ! -group 1000 2>/dev/null | \
|
||||
xargs -r chown 1000:1000 2>/dev/null || true
|
||||
|
||||
# Optionally clean rust target for android triples
|
||||
if [ "$REBUILD_RUST" = "1" ]; then
|
||||
echo ">>> Cleaning Rust android target dirs..."
|
||||
rm -rf "$BASE_DIR/data/cache/target/aarch64-linux-android" \
|
||||
"$BASE_DIR/data/cache/target/armv7-linux-androideabi" \
|
||||
"$BASE_DIR/data/cache/target/i686-linux-android" \
|
||||
"$BASE_DIR/data/cache/target/x86_64-linux-android"
|
||||
fi
|
||||
|
||||
# Profile flag
|
||||
PROFILE_FLAG="--debug"
|
||||
[ "$BUILD_RELEASE" = "1" ] && PROFILE_FLAG=""
|
||||
|
||||
# Persist ~/.android (where the auto-generated debug.keystore lives) so every
|
||||
# build is signed with the SAME key. Without this, every fresh container gets
|
||||
# a new debug keystore and `adb install -r` fails with INSTALL_FAILED_UPDATE_
|
||||
# INCOMPATIBLE because the signature changed.
|
||||
mkdir -p "$BASE_DIR/data/cache/android-home"
|
||||
chown 1000:1000 "$BASE_DIR/data/cache/android-home" 2>/dev/null || true
|
||||
|
||||
docker run --rm \
|
||||
--user 1000:1000 \
|
||||
-e DO_INIT="$DO_INIT" \
|
||||
-e PROFILE_FLAG="$PROFILE_FLAG" \
|
||||
-v "$BASE_DIR/data/source:/build/source" \
|
||||
-v "$BASE_DIR/data/cache/cargo-registry:/home/builder/.cargo/registry" \
|
||||
-v "$BASE_DIR/data/cache/cargo-git:/home/builder/.cargo/git" \
|
||||
-v "$BASE_DIR/data/cache/target:/build/source/target" \
|
||||
-v "$BASE_DIR/data/cache/gradle:/home/builder/.gradle" \
|
||||
-v "$BASE_DIR/data/cache/android-home:/home/builder/.android" \
|
||||
wzp-android-builder \
|
||||
bash -c '
|
||||
set -euo pipefail
|
||||
cd /build/source/desktop
|
||||
|
||||
echo ">>> npm install"
|
||||
npm install --silent 2>&1 | tail -5 || npm install 2>&1 | tail -20
|
||||
|
||||
cd src-tauri
|
||||
|
||||
# Run init if forced, OR if the gradle wrapper is missing. Just checking
|
||||
# for `gen/android` is not enough — Tauri creates a few subdirectories
|
||||
# during build (app/, buildSrc/, .gradle/) that survive a partial wipe and
|
||||
# would make a naive `[ ! -d gen/android ]` check return false even though
|
||||
# the build wrapper itself is gone.
|
||||
if [ "${DO_INIT}" = "1" ] || [ ! -x gen/android/gradlew ]; then
|
||||
echo ">>> cargo tauri android init"
|
||||
cargo tauri android init 2>&1 | tail -20
|
||||
fi
|
||||
|
||||
# ─── wzp-native standalone cdylib (built with cargo-ndk, not cargo-tauri) ──
|
||||
# Produces libwzp_native.so which wzp-desktop dlopens at runtime via
|
||||
# libloading. Split exists because cargo-tauri`s linker wiring pulls
|
||||
# bionic private symbols into any cdylib with cc::Build C++, causing
|
||||
# __init_tcb+4 SIGSEGV. cargo-ndk uses the same linker path as the
|
||||
# legacy wzp-android crate which works.
|
||||
echo ">>> cargo ndk build -p wzp-native --release"
|
||||
JNI_ABI_DIR=gen/android/app/src/main/jniLibs/arm64-v8a
|
||||
mkdir -p "$JNI_ABI_DIR"
|
||||
(
|
||||
cd /build/source
|
||||
cargo ndk -t arm64-v8a -o desktop/src-tauri/gen/android/app/src/main/jniLibs \
|
||||
build --release -p wzp-native 2>&1 | tail -10
|
||||
)
|
||||
if [ -f "$JNI_ABI_DIR/libwzp_native.so" ]; then
|
||||
ls -lh "$JNI_ABI_DIR/libwzp_native.so"
|
||||
else
|
||||
echo ">>> WARNING: libwzp_native.so not produced"
|
||||
fi
|
||||
|
||||
echo ">>> cargo tauri android build ${PROFILE_FLAG} --target aarch64 --apk"
|
||||
cargo tauri android build ${PROFILE_FLAG} --target aarch64 --apk
|
||||
|
||||
echo ""
|
||||
echo ">>> Build artifacts:"
|
||||
find gen/android -name "*.apk" -exec ls -lh {} \; 2>/dev/null
|
||||
'
|
||||
|
||||
# Locate the produced APK
|
||||
APK=$(find "$BASE_DIR/data/source/desktop/src-tauri/gen/android" -name "*.apk" -type f 2>/dev/null | head -1)
|
||||
if [ -z "$APK" ] || [ ! -f "$APK" ]; then
|
||||
LOG_URL=$(upload_to_rustypaste "$LOG_FILE" || echo "")
|
||||
if [ -n "$LOG_URL" ]; then
|
||||
notify "WZP Tauri Android build [$GIT_HASH]: no APK produced
|
||||
log: $LOG_URL"
|
||||
else
|
||||
notify "WZP Tauri Android build [$GIT_HASH]: no APK produced — log upload failed"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
APK_SIZE=$(du -h "$APK" | cut -f1)
|
||||
|
||||
RUSTY_URL=$(upload_to_rustypaste "$APK" || echo "")
|
||||
if [ -n "$RUSTY_URL" ]; then
|
||||
notify "WZP Tauri Android build OK [$GIT_HASH] ($APK_SIZE)
|
||||
$RUSTY_URL"
|
||||
else
|
||||
notify "WZP Tauri Android build OK [$GIT_HASH] ($APK_SIZE) — rustypaste upload skipped"
|
||||
fi
|
||||
|
||||
# Print path so the local script can grab it
|
||||
echo "APK_REMOTE_PATH=$APK"
|
||||
REMOTE_SCRIPT
|
||||
|
||||
ssh_cmd "chmod +x /tmp/wzp-tauri-build.sh"
|
||||
|
||||
notify_local "WZP Tauri Android build dispatched (branch=$BRANCH, release=$BUILD_RELEASE)"
|
||||
log "Triggering remote build (branch=$BRANCH)..."
|
||||
|
||||
# Run; capture full output, last line is APK_REMOTE_PATH=...
|
||||
REMOTE_OUTPUT=$(ssh_cmd "/tmp/wzp-tauri-build.sh '$BRANCH' '$DO_PULL' '$REBUILD_RUST' '$DO_INIT' '$BUILD_RELEASE'" || true)
|
||||
echo "$REMOTE_OUTPUT" | tail -60
|
||||
|
||||
APK_REMOTE=$(echo "$REMOTE_OUTPUT" | grep '^APK_REMOTE_PATH=' | tail -1 | cut -d= -f2-)
|
||||
if [ -n "$APK_REMOTE" ]; then
|
||||
log "Downloading APK to $LOCAL_OUTPUT/wzp-tauri.apk..."
|
||||
scp $SSH_OPTS "$REMOTE_HOST:$APK_REMOTE" "$LOCAL_OUTPUT/wzp-tauri.apk"
|
||||
echo " $LOCAL_OUTPUT/wzp-tauri.apk ($(du -h "$LOCAL_OUTPUT/wzp-tauri.apk" | cut -f1))"
|
||||
else
|
||||
log "No APK produced — see ntfy / remote log /tmp/wzp-tauri-build.log"
|
||||
exit 1
|
||||
fi
|
||||
72
scripts/mint-tmux.sh
Executable file
72
scripts/mint-tmux.sh
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# mint-tmux.sh — run a command inside a persistent tmux session on the
|
||||
# Linux Mint build box so the user can attach and watch/interact at any time.
|
||||
#
|
||||
# Usage:
|
||||
# mint-tmux.sh run <window-name> <command...> # start a new tmux window
|
||||
# mint-tmux.sh send <window-name> <text...> # send keys to a window
|
||||
# mint-tmux.sh kill <window-name> # close a window
|
||||
# mint-tmux.sh list # list windows
|
||||
# mint-tmux.sh tail <window-name> # dump last 200 lines
|
||||
#
|
||||
# Session name is always "wzp". Attach manually with:
|
||||
# ssh -t root@172.16.81.192 tmux attach -t wzp
|
||||
#
|
||||
# If the wzp session doesn't exist yet, it's created automatically.
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
HOST="root@172.16.81.192"
|
||||
SESSION="wzp"
|
||||
SSH_OPTS="-o ConnectTimeout=10 -o LogLevel=ERROR"
|
||||
|
||||
ensure_session() {
|
||||
ssh $SSH_OPTS "$HOST" "
|
||||
tmux has-session -t $SESSION 2>/dev/null || tmux new-session -d -s $SESSION -n home 'bash -l'
|
||||
"
|
||||
}
|
||||
|
||||
cmd="${1:-list}"
|
||||
shift || true
|
||||
|
||||
case "$cmd" in
|
||||
run)
|
||||
WIN="${1:?window name required}"; shift
|
||||
ensure_session
|
||||
# Use a heredoc so multi-arg commands don't need escaping
|
||||
CMD="$*"
|
||||
ssh $SSH_OPTS "$HOST" bash -s <<REMOTE
|
||||
if tmux list-windows -t $SESSION -F '#W' 2>/dev/null | grep -qx '$WIN'; then
|
||||
tmux kill-window -t $SESSION:$WIN 2>/dev/null || true
|
||||
fi
|
||||
tmux new-window -t $SESSION -n '$WIN' "bash -l -c '$CMD; echo; echo --- window $WIN exited with code \\\$?; exec bash -l'"
|
||||
REMOTE
|
||||
echo "Started '$WIN' in tmux session $SESSION on $HOST"
|
||||
echo "Attach: ssh -t $HOST tmux attach -t $SESSION"
|
||||
;;
|
||||
send)
|
||||
WIN="${1:?window name required}"; shift
|
||||
TEXT="$*"
|
||||
ssh $SSH_OPTS "$HOST" "tmux send-keys -t $SESSION:$WIN '$TEXT' C-m"
|
||||
;;
|
||||
kill)
|
||||
WIN="${1:?window name required}"
|
||||
ssh $SSH_OPTS "$HOST" "tmux kill-window -t $SESSION:$WIN 2>/dev/null || true"
|
||||
;;
|
||||
list)
|
||||
ensure_session
|
||||
ssh $SSH_OPTS "$HOST" "tmux list-windows -t $SESSION"
|
||||
;;
|
||||
tail)
|
||||
WIN="${1:?window name required}"
|
||||
ssh $SSH_OPTS "$HOST" "tmux capture-pane -p -t $SESSION:$WIN -S -200 || echo 'no such window'"
|
||||
;;
|
||||
attach)
|
||||
exec ssh -t $SSH_OPTS "$HOST" tmux attach -t $SESSION
|
||||
;;
|
||||
*)
|
||||
sed -n '3,20p' "$0"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
167
scripts/prep-linux-mint.sh
Executable file
167
scripts/prep-linux-mint.sh
Executable file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Prepare a Linux Mint / Debian / Ubuntu x86_64 host as a full WarzonePhone
|
||||
# Android build environment. Installs everything the docker wzp-android-builder
|
||||
# image has, but directly on the host — so we can iterate locally without
|
||||
# docker layer caching, see real linker output, run gdbserver, etc.
|
||||
#
|
||||
# Target host: root@172.16.81.192 (Linux Mint on the LAN)
|
||||
#
|
||||
# Usage (from the macOS workstation):
|
||||
# scp scripts/prep-linux-mint.sh root@172.16.81.192:/tmp/
|
||||
# ssh root@172.16.81.192 'nohup bash /tmp/prep-linux-mint.sh > /var/log/wzp-prep.log 2>&1 &'
|
||||
#
|
||||
# The script is idempotent: safe to re-run if a step fails. Each stage tests
|
||||
# for its target before doing work. Progress + completion is pinged to
|
||||
# ntfy.sh/wzp so we can track it from the phone.
|
||||
#
|
||||
# On success the host has:
|
||||
# - JDK 17
|
||||
# - Android SDK (cmdline-tools + platforms 34/36, build-tools 34/35, NDK 26.1)
|
||||
# - Node.js 20 LTS + npm
|
||||
# - Rust stable + aarch64/armv7/i686/x86_64 android targets
|
||||
# - cargo-ndk + cargo tauri-cli 2.x
|
||||
# - /opt/wzp/warzonePhone (cloned workspace checkout on feat/desktop-audio-rewrite)
|
||||
#
|
||||
# Everything lives under /opt/android-sdk and /opt/wzp so nothing leaks into $HOME.
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
NTFY_TOPIC="https://ntfy.sh/wzp"
|
||||
NDK_VERSION="26.1.10909125"
|
||||
ANDROID_API=34
|
||||
ANDROID_API_TAURI=36
|
||||
BUILD_TOOLS_TAURI="35.0.0"
|
||||
ANDROID_HOME=/opt/android-sdk
|
||||
WZP_DIR=/opt/wzp
|
||||
GIT_REPO="ssh://git@git.manko.yoga:222/manawenuz/wz-phone.git"
|
||||
GIT_BRANCH="feat/desktop-audio-rewrite"
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
export ANDROID_HOME ANDROID_NDK_HOME="$ANDROID_HOME/ndk/$NDK_VERSION"
|
||||
export NDK_HOME="$ANDROID_NDK_HOME"
|
||||
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:/root/.cargo/bin:$PATH"
|
||||
|
||||
notify() { curl -s -d "$1" "$NTFY_TOPIC" > /dev/null 2>&1 || true; }
|
||||
log() { echo -e "\n\033[1;36m[prep-linux-mint]\033[0m $*"; }
|
||||
die() { notify "wzp prep-linux-mint FAILED: $1"; echo "FATAL: $1" >&2; exit 1; }
|
||||
|
||||
trap 'die "line $LINENO"' ERR
|
||||
|
||||
notify "wzp prep-linux-mint STARTED on $(hostname) ($(whoami))"
|
||||
|
||||
# ─── 1. Base packages ────────────────────────────────────────────────────────
|
||||
log "Installing base packages..."
|
||||
apt-get update -qq
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
cmake \
|
||||
curl \
|
||||
file \
|
||||
git \
|
||||
libasound2-dev \
|
||||
libc6-dev \
|
||||
libssl-dev \
|
||||
openjdk-17-jdk-headless \
|
||||
pkg-config \
|
||||
unzip \
|
||||
wget \
|
||||
xz-utils \
|
||||
zip
|
||||
|
||||
# ─── 2. Android SDK + NDK ────────────────────────────────────────────────────
|
||||
if [ ! -x "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" ]; then
|
||||
log "Installing Android cmdline-tools..."
|
||||
mkdir -p "$ANDROID_HOME/cmdline-tools"
|
||||
cd /tmp
|
||||
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O cmdtools.zip
|
||||
unzip -qo cmdtools.zip -d "$ANDROID_HOME/cmdline-tools"
|
||||
mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
|
||||
rm cmdtools.zip
|
||||
else
|
||||
log "cmdline-tools already installed"
|
||||
fi
|
||||
|
||||
if [ ! -d "$ANDROID_HOME/ndk/$NDK_VERSION" ] || \
|
||||
[ ! -d "$ANDROID_HOME/platforms/android-$ANDROID_API" ] || \
|
||||
[ ! -d "$ANDROID_HOME/platforms/android-$ANDROID_API_TAURI" ]; then
|
||||
log "Installing Android platforms + NDK $NDK_VERSION..."
|
||||
yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses > /dev/null 2>&1 || true
|
||||
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --install \
|
||||
"platforms;android-$ANDROID_API" \
|
||||
"build-tools;$ANDROID_API.0.0" \
|
||||
"platforms;android-$ANDROID_API_TAURI" \
|
||||
"build-tools;$BUILD_TOOLS_TAURI" \
|
||||
"ndk;$NDK_VERSION" \
|
||||
"platform-tools" 2>&1 | grep -v '^\[' || true
|
||||
else
|
||||
log "Android SDK components already installed"
|
||||
fi
|
||||
|
||||
# ─── 3. Node.js 20 LTS ───────────────────────────────────────────────────────
|
||||
if ! command -v node >/dev/null 2>&1 || ! node --version | grep -q "^v20"; then
|
||||
log "Installing Node.js 20 LTS..."
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||
apt-get install -y --no-install-recommends nodejs
|
||||
else
|
||||
log "Node.js already at $(node --version)"
|
||||
fi
|
||||
|
||||
# ─── 4. Rust + Android targets ───────────────────────────────────────────────
|
||||
if ! command -v rustup >/dev/null 2>&1; then
|
||||
log "Installing rustup..."
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||
fi
|
||||
. /root/.cargo/env
|
||||
|
||||
log "Ensuring Rust android targets + cargo-ndk + cargo-tauri..."
|
||||
rustup target add \
|
||||
aarch64-linux-android \
|
||||
armv7-linux-androideabi \
|
||||
i686-linux-android \
|
||||
x86_64-linux-android
|
||||
command -v cargo-ndk >/dev/null 2>&1 || cargo install cargo-ndk
|
||||
command -v cargo-tauri >/dev/null 2>&1 || cargo install tauri-cli --version "^2.0" --locked
|
||||
|
||||
# ─── 5. Clone the workspace ──────────────────────────────────────────────────
|
||||
mkdir -p "$WZP_DIR"
|
||||
cd "$WZP_DIR"
|
||||
if [ -d warzonePhone/.git ]; then
|
||||
log "Pulling latest on $GIT_BRANCH..."
|
||||
cd warzonePhone
|
||||
git fetch origin || true
|
||||
git checkout "$GIT_BRANCH" 2>/dev/null || git checkout -b "$GIT_BRANCH" "origin/$GIT_BRANCH"
|
||||
git reset --hard "origin/$GIT_BRANCH" || true
|
||||
else
|
||||
log "Cloning warzonePhone from $GIT_REPO..."
|
||||
# The public repo URL needs ssh keys; if unavailable, skip and let the user sort it later
|
||||
if git clone --branch "$GIT_BRANCH" "$GIT_REPO" warzonePhone 2>/dev/null; then
|
||||
log " cloned ok"
|
||||
else
|
||||
log " clone failed (no SSH keys for $GIT_REPO — skipping, user will rsync)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ─── 6. Persistent env for the user ──────────────────────────────────────────
|
||||
cat > /etc/profile.d/wzp-android.sh <<ENVEOF
|
||||
export ANDROID_HOME=$ANDROID_HOME
|
||||
export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/$NDK_VERSION
|
||||
export NDK_HOME=\$ANDROID_NDK_HOME
|
||||
export PATH=\$ANDROID_HOME/cmdline-tools/latest/bin:\$ANDROID_HOME/platform-tools:/root/.cargo/bin:\$PATH
|
||||
ENVEOF
|
||||
chmod 644 /etc/profile.d/wzp-android.sh
|
||||
|
||||
# ─── 7. Sanity summary ───────────────────────────────────────────────────────
|
||||
log "Sanity checks:"
|
||||
echo " java: $(java -version 2>&1 | head -1)"
|
||||
echo " node: $(node --version)"
|
||||
echo " npm: $(npm --version)"
|
||||
echo " rustc: $(rustc --version)"
|
||||
echo " cargo-ndk: $(cargo ndk --version 2>&1 | head -1)"
|
||||
echo " cargo-tauri:$(cargo tauri --version 2>&1 | head -1)"
|
||||
echo " NDK dir: $ANDROID_NDK_HOME"
|
||||
echo " WZP dir: $WZP_DIR/warzonePhone"
|
||||
|
||||
notify "wzp prep-linux-mint DONE on $(hostname) — ready at /opt/wzp/warzonePhone"
|
||||
log "All done."
|
||||
Reference in New Issue
Block a user