diff --git a/.cargo/config.toml b/.cargo/config.toml index 8cc5779db4..7c775886f7 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -32,17 +32,6 @@ rustflags = [ "link-arg=-static-libstdc++", ] -[target.aarch64-linux-android] -linker = "/home/me/Android/Sdk/ndk/27.3.13750724/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" -ar = "/home/me/Android/Sdk/ndk/27.3.13750724/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar" - -rustflags = [ - "-Lnative=/home/me/Android/Sdk/ndk/27.3.13750724/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/18/lib/linux", - "-lstatic=clang_rt.builtins-aarch64-android", - # "-Lnative=/home/me/Android/Sdk/ndk/27.3.13750724/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android", - # "-lstatic=c++abi", -] - # Increase the default thread stack size to 40 MiB. # Our callstack can get pretty big (especially in debug mode). [env] diff --git a/.claude/skills/android-emulator/SKILL.md b/.claude/skills/android-emulator/SKILL.md new file mode 100644 index 0000000000..83a17bc8ac --- /dev/null +++ b/.claude/skills/android-emulator/SKILL.md @@ -0,0 +1,38 @@ +--- +name: android-emulator +description: Drive the Android emulator for realistic end-to-end tests of the app's Android features and UI (install APK, puppeteer the UI, read backend logs). Use whenever a change needs verification on Android. +--- + +# Android emulator testing + +Use the emulator for realistic end-to-end tests of Android features and UI — install the current APK, drive the app like a user would, and verify behaviour through the screen and the backend logs. + +## Delegate to a subagent + +Emulator interaction is far too context-heavy for the main agent (screenshots, XML dumps, log greps). ALWAYS delegate the routine interaction loop — navigation, coordinate gathering via uiautomator, tapping, log greps — to a subagent (Agent tool, opus model, low reasoning effort). Give it a concrete goal and the procedure below; have it return only a compact report (what it did, what it observed, verbatim error lines if any). The main agent never runs uiautomator/logcat itself. + +Exception: visual judgement stays with the main agent. For any decision based on how the UI actually looks (what is broken, how something renders), have the subagent save a screenshot (`adb exec-out screencap -p > /screen.png`) and return the path, then Read the PNG yourself — and do this at least once per session regardless. Don't take the subagent's verbal description on faith for anything load-bearing. + +## Environment + +- SDK root: `~/Android/Sdk` on Linux, `~/Library/Android/sdk` on macOS — below `$SDK` means that root +- adb: `$SDK/platform-tools/adb`, emulator binary: `$SDK/emulator/emulator` +- AVD: `eigen` (shows up as `emulator-5554`), screen 1080x2400 +- Start if not running: `$SDK/emulator/emulator -avd eigen &` then `adb wait-for-device` +- App: package `net.unstoppableswap.gui`, activity `.MainActivity` +- APK: `src-tauri/gen/android/app/build/outputs/apk/universal/debug/app-universal-debug.apk` +- Install: `adb install -r ` — launch: `adb shell am start -n net.unstoppableswap.gui/.MainActivity` +- Fresh app state: `adb shell pm clear net.unstoppableswap.gui` + +## Interaction procedure + +1. ALWAYS look at the screen visually first: `adb exec-out screencap -p > /screen.png` and Read the image. Never interact blind — confirm what state the app is actually in before every action sequence, and after any action whose effect you aren't sure about. +2. To tap something, do NOT estimate pixel coordinates from the screenshot. Get exact coordinates from the view structure: run `adb shell uiautomator dump` then `adb exec-out cat /sdcard/window_dump.xml`. The Tauri WebView exposes its full accessibility tree, so every element appears with its text/content-desc and exact `bounds="[x1,y1][x2,y2]"`. Grep the XML for the element's text, compute the center of its bounds, and tap it: `adb shell input tap `. +3. Text entry: `adb shell input text 'foo%sbar'` (`%s` = space); keys via `adb shell input keyevent ` (4 = back, 66 = enter). +4. Re-dump after every UI transition — bounds go stale. + +## Verification + +- Backend (Rust) logs: `adb logcat -d | grep RustStdoutStderr` (tracing JSON) +- Native crashes: `adb logcat -d -b crash` +- A test passes only if both the screen shows the expected state AND the logs contain no new errors/crashes. diff --git a/.envrc b/.envrc index badc8aacf9..2dbe595aa4 100644 --- a/.envrc +++ b/.envrc @@ -1,5 +1,5 @@ -if [ "$(uname -s)" = "Linux" ] && [ "$(uname -m)" = "x86_64" ]; then +if { [ "$(uname -s)" = "Linux" ] && [ "$(uname -m)" = "x86_64" ]; } || [ "$(uname -s)" = "Darwin" ]; then use nix else - echo "direnv: skipping Nix shell; supported only on x86_64 Linux." >&2 + echo "direnv: skipping Nix shell; supported only on x86_64 Linux or macOS." >&2 fi diff --git a/flake.nix b/flake.nix index 525769a2db..603a2aa9ec 100644 --- a/flake.nix +++ b/flake.nix @@ -8,11 +8,24 @@ outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: - let pkgs = import nixpkgs { inherit system; }; + let + pkgs = import nixpkgs { inherit system; }; + pkgsAndroid = import nixpkgs { + inherit system; + config = { + allowUnfree = true; + android_sdk.accept_license = true; + }; + }; in { devShells.default = import ./shell.nix { inherit pkgs; nvidiaVersion = null; }; + devShells.android = import ./shell.nix { + pkgs = pkgsAndroid; + nvidiaVersion = null; + withAndroid = true; + }; }); } diff --git a/monero-sys/build.rs b/monero-sys/build.rs index 4295642bd0..04042e189f 100644 --- a/monero-sys/build.rs +++ b/monero-sys/build.rs @@ -311,20 +311,20 @@ fn main() { // Add search paths for clang runtime libraries on macOS (not iOS) if target.contains("apple-darwin") { - // Dynamically detect Homebrew installation prefix (works on both Apple Silicon and Intel Macs) - let brew_prefix = std::process::Command::new("brew") + if let Some(brew_prefix) = std::process::Command::new("brew") .arg("--prefix") .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(|| "/opt/homebrew".into()); - - // add homebrew search paths using dynamic prefix - println!("cargo:rustc-link-search=native={brew_prefix}/lib",); - println!("cargo:rustc-link-search=native={brew_prefix}/opt/unbound/lib",); - println!("cargo:rustc-link-search=native={brew_prefix}/opt/expat/lib",); - println!("cargo:rustc-link-search=native={brew_prefix}/Cellar/protobuf@21/21.12_1/lib/",); + .filter(|s| !s.is_empty()) + { + println!("cargo:rustc-link-search=native={brew_prefix}/lib",); + println!("cargo:rustc-link-search=native={brew_prefix}/opt/unbound/lib",); + println!("cargo:rustc-link-search=native={brew_prefix}/opt/expat/lib",); + println!("cargo:rustc-link-search=native={brew_prefix}/Cellar/protobuf@21/21.12_1/lib/",); + } // Add search paths for clang runtime libraries let resource_dir = std::process::Command::new("clang") @@ -384,26 +384,15 @@ fn main() { // Link libsodium statically println!("cargo:rustc-link-lib=static=sodium"); - // Link OpenSSL statically (on android we use openssl-sys's vendored version instead) - #[cfg(not(target_os = "android"))] - { - println!("cargo:rustc-link-lib=static=ssl"); // This is OpenSSL (libsll) - println!("cargo:rustc-link-lib=static=crypto"); // This is OpenSSLs crypto library (libcrypto) - } + // Link OpenSSL statically + println!("cargo:rustc-link-lib=static=ssl"); // This is OpenSSL (libsll) + println!("cargo:rustc-link-lib=static=crypto"); // This is OpenSSLs crypto library (libcrypto) // Link unbound statically println!("cargo:rustc-link-lib=static=unbound"); println!("cargo:rustc-link-lib=static=expat"); // Expat is required by unbound // println!("cargo:rustc-link-lib=static=nghttp2"); // println!("cargo:rustc-link-lib=static=event"); - // Android - #[cfg(target_os = "android")] - { - println!( - "cargo:rustc-link-search=/home/me/Android/Sdk/ndk/27.3.13750724/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/" - ); - // println!("cargo:rustc-link-lib=static=c++_static"); - } // Link protobuf statically // println!("cargo:rustc-link-lib=static=protobuf"); @@ -493,6 +482,9 @@ fn compile_dependencies( cmd.arg("-i"); let path = std::env::var("PATH").unwrap_or_default(); cmd.arg(format!("PATH={path}")); + if let Ok(aclocal_path) = std::env::var("ACLOCAL_PATH") { + cmd.arg(format!("ACLOCAL_PATH={aclocal_path}")); + } } cmd.arg("make") .arg(format!("HOST={target}")) diff --git a/monero-sys/monero-depends b/monero-sys/monero-depends index 51e08f30f3..6db2297651 160000 --- a/monero-sys/monero-depends +++ b/monero-sys/monero-depends @@ -1 +1 @@ -Subproject commit 51e08f30f3b39a65adecdcabff97bfb2ed183b6b +Subproject commit 6db2297651797a5ddfce3102e8f51c9a2baa19cb diff --git a/monero-sys/src/bridge.h b/monero-sys/src/bridge.h index 09c629c335..069beb9f0a 100644 --- a/monero-sys/src/bridge.h +++ b/monero-sys/src/bridge.h @@ -700,12 +700,15 @@ namespace monero_rust_log #include #include -// The following is a hack to ensure the linker includes the pair destructor in the binary using String = std::string; using StringMap = std::map; using StringVec = std::vector; -static std::pair _monero_sys_pair_instantiation; +#ifdef __ANDROID__ +using MoneroSysAccountTagsPair = std::pair; +void monero_sys_account_tags_pair_dtor(MoneroSysAccountTagsPair* p) asm("_ZNSt6__ndk14pairINS_3mapINS_12basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEES7_NS_4lessIS7_EENS5_INS0_IKS7_S7_EEEEEENS_6vectorIS7_NS5_IS7_EEEEED1Ev"); +__attribute__((weak)) void monero_sys_account_tags_pair_dtor(MoneroSysAccountTagsPair* p) { p->~MoneroSysAccountTagsPair(); } +#endif namespace Monero { diff --git a/nix/README.md b/nix/README.md index a4df1852e2..de7ca34a81 100644 --- a/nix/README.md +++ b/nix/README.md @@ -5,18 +5,95 @@ run eigenwallet (monero-sys, the Tauri GUI, and the `just` recipes) on non-NixOS This file documents the *why* behind `shell.nix` and `flake.nix`; the code itself is kept comment-free. +`shell.nix` branches on the host platform: **x86_64 Linux** gets the full nixpkgs toolchain +(the rest of this document), **macOS** gets a leaner shell described under +[macOS](#macos-apple-silicon--intel), and every other platform gets a no-op stub. + ## Usage -- `nix-shell` — impure shell; auto-detects the host NVIDIA driver for GPU rendering. -- `direnv` — `.envrc` runs `use nix` on x86_64 Linux, so the shell loads +- `nix-shell` — impure shell; on Linux auto-detects the host NVIDIA driver for GPU rendering. +- `direnv` — `.envrc` runs `use nix` on x86_64 Linux and on macOS, so the shell loads automatically on `cd`. On other systems it intentionally does nothing. -- `nix develop` — pure flake shell; uses mesa software rendering (pure eval can't read - `/proc`). For GPU acceleration use `nix-shell`, or `nix develop --impure` with an +- `nix develop` — pure flake shell. On Linux it uses mesa software rendering (pure eval can't + read `/proc`); for GPU acceleration use `nix-shell`, or `nix develop --impure` with an explicit `nvidiaVersion` (e.g. `"580.159.03"`). Inputs are pinned: `flake.lock` for the flake path, and an explicit rev + `sha256` on each `fetchTarball` in `shell.nix` for the `nix-shell` path. Bump both together. +## macOS (Apple Silicon & Intel) + +On macOS the dev shell is deliberately different from the Linux one: nix provides **only the +auxiliary build tooling** (cmake, autotools, pkg-config, node, `just`, `sqlx-cli`, +`cargo-tauri`, …) and the **system Xcode toolchain** (clang + the macOS SDK, located via +`xcrun`) does every bit of C/C++ compilation and linking. This mirrors CI on the `macos-15` +runners and is what keeps the build reproducible: + +- `monero-depends` hardcodes the system `xcrun` clang + SDK in `builders/darwin.mk` and builds + every native library Monero needs (boost, openssl, unbound, expat, libsodium, zeromq, …) as + static archives. **Nothing comes from Homebrew** — verified by building with `brew` + unreachable; `unbound`/`expat` (which Homebrew also ships) come straight from monero-depends. +- The only other native dependencies are system frameworks: `native-tls` → `Security`, + Tauri's webview → `WebKit`. No nix C libraries are linked. + +Mixing nixpkgs' own clang/`apple-sdk` with the xcrun-built static libs would only create +sysroot/ABI mismatches, so the shell uses `mkShellNoCC` and then repairs the handful of things +the nixpkgs darwin stdenv does that get in the way: + +- Unsets `SDKROOT`/`DEVELOPER_DIR` (the stdenv points them at nix's pinned `apple-sdk`, and + even `/usr/bin/xcrun` honours those env vars) so every step sees one consistent *system* SDK. +- Shims `xcrun` → `/usr/bin/xcrun` (nixpkgs ships an ancient xcbuild reimplementation). +- Shims `make` → `/usr/bin/make`. nixpkgs' gnumake 4.x rejects a `build_darwin_CC: = …` typo + in `monero-depends/builders/darwin.mk` ("empty variable name"); the OS-pinned GNU make 3.81 + — frozen by Apple, and what CI/Homebrew also use — tolerates it as a harmless no-op rule. +- Sets `CC`/`CXX` to `/usr/bin/clang(++)` — the driver auto-injects `-isysroot`, whereas the + bare toolchain binary would fail to find the C++ standard headers — and pins + `MACOSX_DEPLOYMENT_TARGET=11.0` to match `build.rs` / `tauri.conf.json`. +- `monero-sys/build.rs` forwards `ACLOCAL_PATH` through the `env -i` it wraps the depends + build in: some depends packages (hidapi) run autoreconf, and under nix the libtool/automake + m4 macros live in separate store paths that `aclocal` only finds via that variable. It also + emits the `/opt/homebrew` link search paths only when `brew` is actually reachable, so the + nix flow can never silently pick up a host-installed library. + +### Prerequisites + +Three things, all of which a developer already has once Nix is installed: **Nix**, the +**Xcode Command Line Tools** (for `xcrun`, clang and the SDK), and **rustup** (the Rust +toolchain is taken from `~/.cargo/bin`; `rust-toolchain.toml` pins the exact version). + +### Reproducible / isolated builds + +`nix/macos-isolated-build.sh ` runs a build inside the pinned dev shell with Homebrew, +MacPorts and nvm scrubbed from `PATH` and a dedicated `../nixbuild/target` target dir (the +target dir must end in `target`: `monero-sys/build.rs` resolves the shared monero-depends +location by walking up from `OUT_DIR` to a dir named `target`/`target-check`). It +proves both that a "nix-only" mac can build the project and that nothing silently links a +host-installed library: `brew` is made unreachable, which — together with the guard in +`monero-sys/build.rs` — means no Homebrew path is ever passed to the linker. Audit the +resulting binaries with `otool -L` (dynamic deps) and `otool -l` (rpaths); there must be zero +`/opt/homebrew` references. + +The first run rebuilds all of monero-depends from source (~30 min, dominated by boost). The +build is cached in `/debug/monero-depends/`, keyed so it is not rebuilt on +subsequent runs. + +## Android + +`nix develop .#android` extends the platform shell (Linux or macOS) with everything the +Android cross-build needs: the SDK (platform 36 + build-tools), NDK r28c and JDK 21 from +nixpkgs' `androidenv`, plus the `CC_`/`CARGO_TARGET__*` environment cargo and +cc-rs need to drive the NDK clang for `aarch64-linux-android` and `x86_64-linux-android`. +The flake shell is required (plain `nix-shell` can't accept the SDK license); build with +`cargo tauri android build --debug [--target aarch64]`. + +Two NDKs are involved, deliberately the same release (r28c): the nix-provided one compiles +and links everything cargo drives (the cxx bridge, `ring`, the final rustc link), while +monero-depends downloads its own copy — the linux or darwin zip depending on the build host — +and generates a standalone toolchain from it for the depends packages and the monero cmake +build. `clang_rt.builtins--android` is linked statically because rustc links with the +NDK clang driver but its own runtime expects the compiler builtins that libgcc/compiler-rt +normally inject. + ## GPU rendering (Tauri webview) WebKitGTK dispatches OpenGL/EGL through nix's libglvnd, which ships no vendor ICD. On a diff --git a/nix/macos-isolated-build.sh b/nix/macos-isolated-build.sh new file mode 100755 index 0000000000..e4256a934e --- /dev/null +++ b/nix/macos-isolated-build.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +self="$repo_root/nix/macos-isolated-build.sh" +cd "$repo_root" + +export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$(cd "$repo_root/.." && pwd)/nixbuild/target}" + +if [ -z "${IN_NIX_SHELL:-}" ]; then + exec nix-shell "$repo_root/shell.nix" --run "$(printf '%q ' "$self" "$@")" +fi + +clean="" +IFS=':' read -ra _parts <<<"$PATH" +for _p in "${_parts[@]}"; do + case "$_p" in + /opt/homebrew/* | /usr/local/Homebrew/* | /usr/local/bin | /usr/local/sbin | /opt/local/* | */.nvm/*) + continue + ;; + esac + clean="${clean:+$clean:}$_p" +done +export PATH="$clean" + +unset LIBRARY_PATH DYLD_LIBRARY_PATH DYLD_FALLBACK_LIBRARY_PATH \ + CPATH C_INCLUDE_PATH CPLUS_INCLUDE_PATH CMAKE_PREFIX_PATH OPENSSL_DIR 2>/dev/null || true + +if command -v brew >/dev/null 2>&1; then + echo "FATAL: brew is still reachable at $(command -v brew); isolation broken." >&2 + exit 1 +fi + +echo "[isolated] CARGO_TARGET_DIR=$CARGO_TARGET_DIR" +echo "[isolated] cc=$(command -v cc) clang=$(command -v clang) xcrun=$(command -v xcrun)" +echo "[isolated] cmake=$(command -v cmake) make=$(command -v make) node=$(command -v node)" +echo "[isolated] running: $*" +exec "$@" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 5426985195..4253c1c296 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -2,4 +2,4 @@ # also update this in the readme, changelog, and github actions channel = "1.90.0" components = ["clippy", "rust-analyzer"] -targets = ["armv7-unknown-linux-gnueabihf"] +targets = ["armv7-unknown-linux-gnueabihf", "aarch64-linux-android", "x86_64-linux-android"] diff --git a/shell.nix b/shell.nix index 2a8cb5ed19..62f762993f 100644 --- a/shell.nix +++ b/shell.nix @@ -5,6 +5,7 @@ config.allowUnfreePredicate = p: builtins.match "nvidia.*" (builtins.parseDrvName p.name).name != null; } +, withAndroid ? false , nvidiaVersion ? let # nix can't readFile /proc directly (NixOS/nix#3539); copy it out impurely first @@ -21,6 +22,46 @@ let supportedSystem = pkgs.stdenv.hostPlatform.system == "x86_64-linux"; + + androidComposition = pkgs.androidenv.composeAndroidPackages { + platformVersions = [ "36" ]; + buildToolsVersions = [ "35.0.0" "36.0.0" ]; + includeNDK = true; + ndkVersion = "28.2.13676358"; + }; + androidHome = "${androidComposition.androidsdk}/libexec/android-sdk"; + ndkPrebuilt = if pkgs.stdenv.hostPlatform.isDarwin then "darwin-x86_64" else "linux-x86_64"; + + androidShellHook = if !withAndroid then "" else '' + export ANDROID_HOME="${androidHome}" + export ANDROID_SDK_ROOT="$ANDROID_HOME" + export NDK_HOME="$ANDROID_HOME/ndk-bundle" + export ANDROID_NDK_HOME="$NDK_HOME" + export ANDROID_NDK_ROOT="$NDK_HOME" + export JAVA_HOME="${pkgs.jdk21.home}" + + _ndk_bin="$NDK_HOME/toolchains/llvm/prebuilt/${ndkPrebuilt}/bin" + _clang_rt="$(echo "$NDK_HOME"/toolchains/llvm/prebuilt/${ndkPrebuilt}/lib/clang/*/lib/linux)" + export CC_aarch64_linux_android="$_ndk_bin/aarch64-linux-android24-clang" + export CXX_aarch64_linux_android="$_ndk_bin/aarch64-linux-android24-clang++" + export AR_aarch64_linux_android="$_ndk_bin/llvm-ar" + export RANLIB_aarch64_linux_android="$_ndk_bin/llvm-ranlib" + export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$CC_aarch64_linux_android" + export CARGO_TARGET_AARCH64_LINUX_ANDROID_RUSTFLAGS="-L native=$_clang_rt -l static=clang_rt.builtins-aarch64-android -C link-arg=-landroid -C link-arg=-llog" + export CC_x86_64_linux_android="$_ndk_bin/x86_64-linux-android24-clang" + export CXX_x86_64_linux_android="$_ndk_bin/x86_64-linux-android24-clang++" + export AR_x86_64_linux_android="$_ndk_bin/llvm-ar" + export RANLIB_x86_64_linux_android="$_ndk_bin/llvm-ranlib" + export CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="$CC_x86_64_linux_android" + export CARGO_TARGET_X86_64_LINUX_ANDROID_RUSTFLAGS="-L native=$_clang_rt -l static=clang_rt.builtins-x86_64-android -C link-arg=-landroid -C link-arg=-llog" + unset _ndk_bin _clang_rt + ''; + + androidPackages = pkgs.lib.optionals withAndroid (with pkgs; [ + androidComposition.androidsdk + jdk21 + unzip + ]); in if supportedSystem then let @@ -98,7 +139,7 @@ pkgs.mkShell { dprint sqlx-cli cargo-tauri - ]) ++ [ moneroDependsToolchain ]; + ]) ++ [ moneroDependsToolchain ] ++ androidPackages; buildInputs = tauriLinkLibs; @@ -116,6 +157,7 @@ pkgs.mkShell { fi ${gpuShellHook} + ${androidShellHook} if ! command -v docker >/dev/null 2>&1 && command -v podman >/dev/null 2>&1; then _docker_shim="$HOME/.cache/eigenwallet-docker-shim" mkdir -p -m 700 "$_docker_shim" @@ -138,9 +180,63 @@ pkgs.mkShell { export XDG_DATA_DIRS="${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS" ''; } +else if pkgs.stdenv.hostPlatform.isDarwin then +let + darwinTools = with pkgs; [ + cmake + autoconf + automake + libtool + pkg-config + ccache + gperf + lbzip2 + curl + git + python3 + nodejs_22 + just + typeshare + dprint + sqlx-cli + cargo-tauri + ]; +in +pkgs.mkShellNoCC { + nativeBuildInputs = darwinTools ++ androidPackages; + + MACOSX_DEPLOYMENT_TARGET = "11.0"; + + shellHook = '' + unset SDKROOT DEVELOPER_DIR + export MACOSX_DEPLOYMENT_TARGET=11.0 + + apple_shim="$HOME/.cache/eigenwallet-apple-shim" + mkdir -p "$apple_shim" + ln -sf /usr/bin/xcrun "$apple_shim/xcrun" + ln -sf /usr/bin/make "$apple_shim/make" + export PATH="$apple_shim:$PATH" + + export PATH="$PATH:/usr/bin:/usr/sbin:/bin" + + export CC=/usr/bin/clang + export CXX=/usr/bin/clang++ + + ${androidShellHook} + + export PATH="$HOME/.cargo/bin:$PATH" + + export COREPACK_HOME="$HOME/.cache/corepack" + export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 + corepack_bin="$HOME/.cache/corepack/bin" + mkdir -p "$corepack_bin" + ${pkgs.nodejs_22}/bin/corepack enable --install-directory "$corepack_bin" + export PATH="$corepack_bin:$PATH" + ''; +} else pkgs.mkShell { shellHook = '' - echo "Skipping eigenwallet Nix dev shell; supported only on x86_64 Linux." + echo "Skipping eigenwallet Nix dev shell; supported only on x86_64 Linux or macOS." ''; } diff --git a/src-gui/src/renderer/components/App.tsx b/src-gui/src/renderer/components/App.tsx index ad59a961c3..bff935283e 100644 --- a/src-gui/src/renderer/components/App.tsx +++ b/src-gui/src/renderer/components/App.tsx @@ -76,8 +76,9 @@ function InnerContent() { return ( { - // Check for updates when component mounts + if (/android|iphone|ipad/i.test(navigator.userAgent)) return; + check() .then((updateResponse) => { console.log("updateResponse", updateResponse); diff --git a/src-gui/src/renderer/components/navigation/Navigation.tsx b/src-gui/src/renderer/components/navigation/Navigation.tsx index bae3a98214..a24d5ca70e 100644 --- a/src-gui/src/renderer/components/navigation/Navigation.tsx +++ b/src-gui/src/renderer/components/navigation/Navigation.tsx @@ -1,33 +1,64 @@ -import { Box, Drawer } from "@mui/material"; +import { Box, Drawer, Fab, Theme, useMediaQuery } from "@mui/material"; +import MenuIcon from "@mui/icons-material/Menu"; +import { useState } from "react"; import NavigationFooter from "./NavigationFooter"; import NavigationHeader from "./NavigationHeader"; export const drawerWidth = "240px"; export default function Navigation() { + const isMobile = useMediaQuery((theme: Theme) => + theme.breakpoints.down("sm"), + ); + const [open, setOpen] = useState(false); + return ( - - + {isMobile && ( + setOpen(true)} + sx={{ + position: "fixed", + top: 8, + left: 8, + zIndex: (theme) => theme.zIndex.drawer - 1, + bgcolor: "background.paper", + color: "text.primary", + "&:hover": { + bgcolor: "background.paper", + }, + }} + > + + + )} + setOpen(false)} sx={{ - overflow: "auto", - display: "flex", - flexDirection: "column", - justifyContent: "space-between", - height: "100%", + width: drawerWidth, + flexShrink: 0, + "& .MuiDrawer-paper": { + width: drawerWidth, + }, }} > - - - - + isMobile && setOpen(false)} + sx={{ + overflow: "auto", + display: "flex", + flexDirection: "column", + justifyContent: "space-between", + height: "100%", + }} + > + + + + + ); } diff --git a/src-gui/src/renderer/components/pages/monero/SetRestoreHeightModal.tsx b/src-gui/src/renderer/components/pages/monero/SetRestoreHeightModal.tsx index 7ebc3a27e1..f29e7f05f1 100644 --- a/src-gui/src/renderer/components/pages/monero/SetRestoreHeightModal.tsx +++ b/src-gui/src/renderer/components/pages/monero/SetRestoreHeightModal.tsx @@ -82,7 +82,7 @@ export default function SetRestoreHeightModal({ return ( Set Restore Height - + ({ "&.notistack-MuiContent": { maxWidth: "50vw", + "@media (max-width:599.95px)": { + maxWidth: "calc(100vw - 16px)", + }, }, })); diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0b92aeaed3..ef6007cdb4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -7,7 +7,7 @@ description = "GUI for XMR<>BTC Atomic Swaps written in Rust" [lib] name = "unstoppableswap_gui_rs_lib" -crate-type = ["rlib", "staticlib", "cdylib"] +crate-type = ["rlib", "cdylib"] [build-dependencies] tauri-build = { version = "2.*", features = ["config-json5"] } diff --git a/src-tauri/gen/android/app/build.gradle.kts b/src-tauri/gen/android/app/build.gradle.kts index 99f7c6561c..97a3d3ddb7 100644 --- a/src-tauri/gen/android/app/build.gradle.kts +++ b/src-tauri/gen/android/app/build.gradle.kts @@ -15,6 +15,7 @@ val tauriProperties = Properties().apply { android { compileSdk = 36 + ndkVersion = "28.2.13676358" namespace = "net.unstoppableswap.gui" defaultConfig { manifestPlaceholders["usesCleartextTraffic"] = "false" @@ -28,13 +29,8 @@ android { getByName("debug") { manifestPlaceholders["usesCleartextTraffic"] = "true" isDebuggable = true - isJniDebuggable = true + isJniDebuggable = false isMinifyEnabled = false - packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so") - jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so") - jniLibs.keepDebugSymbols.add("*/x86/*.so") - jniLibs.keepDebugSymbols.add("*/x86_64/*.so") - } } getByName("release") { isMinifyEnabled = true diff --git a/src-tauri/gen/android/app/src/main/java/net/unstoppableswap/gui/MainActivity.kt b/src-tauri/gen/android/app/src/main/java/net/unstoppableswap/gui/MainActivity.kt index 88a61d6c9c..34eb1583a0 100644 --- a/src-tauri/gen/android/app/src/main/java/net/unstoppableswap/gui/MainActivity.kt +++ b/src-tauri/gen/android/app/src/main/java/net/unstoppableswap/gui/MainActivity.kt @@ -2,10 +2,17 @@ package net.unstoppableswap.gui import android.os.Bundle import androidx.activity.enableEdgeToEdge +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat class MainActivity : TauriActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content)) { view, insets -> + val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime()) + view.setPadding(bars.left, bars.top, bars.right, bars.bottom) + WindowInsetsCompat.CONSUMED + } } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5bd5397df5..f51745d0b8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -8,6 +8,8 @@ mod commands; use commands::*; +const THREAD_STACK_SIZE: usize = 40 * 1024 * 1024; + /// Represents the shared Tauri state. It is accessed by Tauri commands struct State { pub context: Arc, @@ -45,6 +47,16 @@ impl State { /// Initializes the Tauri state /// Sets the window title fn setup(app: &mut tauri::App) -> Result<(), Box> { + #[cfg(mobile)] + { + let data_dir = app.path().app_data_dir()?; + unsafe { + std::env::set_var("HOME", &data_dir); + std::env::set_var("XDG_DATA_HOME", data_dir.join("data")); + std::env::set_var("XDG_CONFIG_HOME", data_dir.join("config")); + } + } + // Set the window title to include the product name and version #[cfg(desktop)] { @@ -77,6 +89,18 @@ fn setup(app: &mut tauri::App) -> Result<(), Box> { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .thread_stack_size(THREAD_STACK_SIZE) + .build() + .expect("failed to build tokio runtime"); + tauri::async_runtime::set(runtime.handle().clone()); + std::mem::forget(runtime); + + rustls::crypto::ring::default_provider() + .install_default() + .expect("failed to install default rustls provider"); + let mut builder = tauri::Builder::default(); #[cfg(desktop)] @@ -89,10 +113,10 @@ pub fn run() { })); builder = builder.plugin(tauri_plugin_cli::init()); + builder = builder.plugin(tauri_plugin_updater::Builder::new().build()); } builder - .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_store::Builder::new().build()) .plugin(tauri_plugin_clipboard_manager::init()) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ab0c8207ac..e8958d2773 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,9 +2,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - rustls::crypto::ring::default_provider() - .install_default() - .expect("failed to install default rustls provider"); - unstoppableswap_gui_rs_lib::run() } diff --git a/swap/src/cli/transport.rs b/swap/src/cli/transport.rs index 6d24da3c7c..169eb1e2bb 100644 --- a/swap/src/cli/transport.rs +++ b/swap/src/cli/transport.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use std::time::Duration; use crate::network::transport::authenticate_and_multiplex; -use anyhow::Result; +use anyhow::{Context, Result}; use arti_client::TorClient; use libp2p::core::muxing::StreamMuxerBox; use libp2p::core::transport::{Boxed, OptionalTransport}; @@ -47,6 +47,20 @@ fn new_tor_dial_limiter() -> (TorDialLimiter, TorDialPriorityTracker) { (dial_limiter, priority_tracker) } +fn new_dns_transport( + inner: tcp::tokio::Transport, +) -> std::io::Result> { + if cfg!(target_os = "android") { + return Ok(dns::tokio::Transport::custom( + inner, + dns::ResolverConfig::cloudflare(), + dns::ResolverOpts::default(), + )); + } + + dns::tokio::Transport::system(inner) +} + /// Creates the libp2p transport for the swap CLI. /// /// The CLI's transport needs the following capabilities: @@ -74,7 +88,8 @@ pub fn new( // delegates to its inner transport, so we give it a Tor-or-TCP+DNS chain so // that ws connections are routed over Tor when available. let ws_inner_tcp = tcp::tokio::Transport::new(tcp::Config::new().nodelay(true)); - let ws_inner_tcp_dns = dns::tokio::Transport::system(ws_inner_tcp)?; + let ws_inner_tcp_dns = new_dns_transport(ws_inner_tcp) + .context("Failed to create DNS transport for websocket transport")?; let ws_inner_tor: OptionalTransport = match &maybe_tor_client { Some(client) => { let mut transport = @@ -93,7 +108,8 @@ pub fn new( // Build the plain Tor-or-TCP+DNS transport for non-websocket addresses. let tcp = tcp::tokio::Transport::new(tcp::Config::new().nodelay(true)); - let tcp_with_dns = dns::tokio::Transport::system(tcp)?; + let tcp_with_dns = + new_dns_transport(tcp).context("Failed to create DNS transport for TCP transport")?; let maybe_tor_transport: OptionalTransport = match maybe_tor_client { Some(client) => { let mut transport = TorTransport::from_client(client, AddressConversion::IpAndDns);