feat(android): Tauri 2.x mobile build infrastructure
Adds infrastructure for building the Tauri 2.x Android app (the pivot away from the Kotlin+JNI approach whose stack overflow / libcrypto TLS crash / thread lifecycle hell is documented in the incident report): - scripts/Dockerfile.android-builder: extended to support both the legacy Kotlin+JNI pipeline (cargo-ndk + Gradle) and the new Tauri mobile pipeline (tauri-cli + Node/npm). Adds Node.js 20 LTS, API level 36 + build-tools 35.0.0, and additional apt packages. - scripts/build-tauri-android.sh: fire-and-forget remote build via Docker on SepehrHomeserverdk, with ntfy.sh notifications and rustypaste upload of the resulting APK. Mirrors the pattern of build-tauri-android-docker.sh but targets the new Tauri pipeline. - docs/incident-tauri-android-init-tcb.md: postmortem of the Kotlin+JNI crash cascade that drove the Tauri mobile rewrite decision. Covers the __init_tcb / pthread_create bionic private symbol leak, the staticlib + cdylib crate-type interaction, the Dispatchers.IO 512 KB thread stack overflow, and the tokio runtime / libcrypto TLS race. - scripts/mint-tmux.sh, scripts/prep-linux-mint.sh: general dev infrastructure (tmux + Linux Mint workstation prep scripts). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
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)
|
# 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)
|
# - Debian 12 (cmake 3.25, no Android cross-compilation bugs)
|
||||||
# - JDK 17 (Gradle 8.5 + AGP 8.2.0 compatible)
|
# - JDK 17 (Gradle 8.5 + AGP 8.2.0 compatible)
|
||||||
# - NDK 26.1 (last stable before scudo/MTE crash on NDK 27+)
|
# - 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 .
|
# 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 NDK_VERSION=26.1.10909125
|
||||||
ARG ANDROID_API=34
|
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 \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
ANDROID_HOME=/opt/android-sdk \
|
ANDROID_HOME=/opt/android-sdk \
|
||||||
@@ -35,8 +45,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
openjdk-17-jdk-headless \
|
openjdk-17-jdk-headless \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
|
file \
|
||||||
|
xz-utils \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& 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 ──────────────────────────────────────────────────
|
# ── Android SDK + NDK 26.1 ──────────────────────────────────────────────────
|
||||||
RUN mkdir -p $ANDROID_HOME/cmdline-tools \
|
RUN mkdir -p $ANDROID_HOME/cmdline-tools \
|
||||||
&& cd /tmp \
|
&& 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 \
|
&& $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install \
|
||||||
"platforms;android-${ANDROID_API}" \
|
"platforms;android-${ANDROID_API}" \
|
||||||
"build-tools;${ANDROID_API}.0.0" \
|
"build-tools;${ANDROID_API}.0.0" \
|
||||||
|
"platforms;android-${ANDROID_API_TAURI}" \
|
||||||
|
"build-tools;${BUILD_TOOLS_TAURI}" \
|
||||||
"ndk;${NDK_VERSION}" \
|
"ndk;${NDK_VERSION}" \
|
||||||
"platform-tools" \
|
"platform-tools" \
|
||||||
2>&1 | grep -v '^\[' > /dev/null
|
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
|
# Make SDK world-readable so builder user can access it
|
||||||
RUN chmod -R a+rX $ANDROID_HOME
|
RUN chmod -R a+rX $ANDROID_HOME
|
||||||
|
|
||||||
@@ -64,12 +109,22 @@ USER builder
|
|||||||
WORKDIR /home/builder
|
WORKDIR /home/builder
|
||||||
|
|
||||||
# ── Rust toolchain ───────────────────────────────────────────────────────────
|
# ── 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 \
|
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||||
| sh -s -- -y --default-toolchain stable \
|
| sh -s -- -y --default-toolchain stable \
|
||||||
&& . $HOME/.cargo/env \
|
&& . $HOME/.cargo/env \
|
||||||
&& rustup target add aarch64-linux-android \
|
&& rustup target add \
|
||||||
&& cargo install cargo-ndk
|
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"
|
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
|
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