Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
38 changes: 38 additions & 0 deletions .claude/skills/android-emulator/SKILL.md
Original file line number Diff line number Diff line change
@@ -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 > <scratchpad>/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 <apk>` — 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 > <scratchpad>/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 <cx> <cy>`.
3. Text entry: `adb shell input text 'foo%sbar'` (`%s` = space); keys via `adb shell input keyevent <code>` (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.
4 changes: 2 additions & 2 deletions .envrc
Original file line number Diff line number Diff line change
@@ -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
15 changes: 14 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
});
}
38 changes: 15 additions & 23 deletions monero-sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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}"))
Expand Down
2 changes: 1 addition & 1 deletion monero-sys/monero-depends
7 changes: 5 additions & 2 deletions monero-sys/src/bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -700,12 +700,15 @@ namespace monero_rust_log
#include <vector>
#include <string>

// The following is a hack to ensure the linker includes the pair destructor in the binary
using String = std::string;
using StringMap = std::map<String, String>;
using StringVec = std::vector<String>;

static std::pair<StringMap, StringVec> _monero_sys_pair_instantiation;
#ifdef __ANDROID__
using MoneroSysAccountTagsPair = std::pair<StringMap, StringVec>;
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
{
Expand Down
85 changes: 81 additions & 4 deletions nix/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <cmd>` 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 `<target>/debug/monero-depends/<triple>`, 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_<target>`/`CARGO_TARGET_<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-<arch>-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
Expand Down
38 changes: 38 additions & 0 deletions nix/macos-isolated-build.sh
Original file line number Diff line number Diff line change
@@ -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 "$@"
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Loading
Loading