diff --git a/docs/incident-tauri-android-init-tcb.md b/docs/incident-tauri-android-init-tcb.md new file mode 100644 index 0000000..e27f306 --- /dev/null +++ b/docs/incident-tauri-android-init-tcb.md @@ -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 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` + `fetch_add`, no mutex, no thread, no includes beyond ``. | ❌ **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`)? diff --git a/scripts/Dockerfile.android-builder b/scripts/Dockerfile.android-builder index 647c9f9..fd07b46 100644 --- a/scripts/Dockerfile.android-builder +++ b/scripts/Dockerfile.android-builder @@ -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 diff --git a/scripts/build-tauri-android.sh b/scripts/build-tauri-android.sh new file mode 100755 index 0000000..178081b --- /dev/null +++ b/scripts/build-tauri-android.sh @@ -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 diff --git a/scripts/mint-tmux.sh b/scripts/mint-tmux.sh new file mode 100755 index 0000000..644d7bd --- /dev/null +++ b/scripts/mint-tmux.sh @@ -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 # start a new tmux window +# mint-tmux.sh send # send keys to a window +# mint-tmux.sh kill # close a window +# mint-tmux.sh list # list windows +# mint-tmux.sh tail # 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 </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 diff --git a/scripts/prep-linux-mint.sh b/scripts/prep-linux-mint.sh new file mode 100755 index 0000000..06d8b7a --- /dev/null +++ b/scripts/prep-linux-mint.sh @@ -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 <&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."