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)
|
||||
#
|
||||
# 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