diff --git a/.github/workflows/release-swift-kotlin.yml b/.github/workflows/release-swift-kotlin.yml
index 3e4981765..8ce9b11b4 100644
--- a/.github/workflows/release-swift-kotlin.yml
+++ b/.github/workflows/release-swift-kotlin.yml
@@ -20,7 +20,7 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Get new version
id: version
@@ -29,6 +29,8 @@ jobs:
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
build-swift:
+ # Swift depends on the host macOS/Xcode configuration and intentionally does
+ # not run inside Nix.
runs-on: macos-latest
needs: pre-release-checks
permissions:
@@ -36,7 +38,7 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.pre-release-checks.outputs.commit_sha }} # to ensure all builds are consistent
@@ -57,14 +59,14 @@ jobs:
zip -r WalletKit.xcframework.zip swift/WalletKit.xcframework
- name: Checkout swift repo
- uses: actions/checkout@v6
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: worldcoin/walletkit-swift
token: ${{ secrets.WALLETKIT_BOT_TOKEN }}
path: target-repo
- name: Create draft release in walletkit-swift & upload binaries
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
id: release
with:
repository: worldcoin/walletkit-swift
@@ -126,11 +128,9 @@ jobs:
name: Prepare Kotlin
runs-on: arc-public-8xlarge-amd64-runner
needs: [pre-release-checks]
- env:
- CARGO_HOME: /home/runner/_work/_cargo
- RUSTUP_HOME: /home/runner/_work/_rustup
permissions:
- contents: write # to upload artifacts
+ contents: read
+ actions: write # to save Rust cache
# Each target is built separately to run in parallel. Running sequentially takes vastly more time.
strategy:
@@ -143,31 +143,36 @@ jobs:
- target: i686-linux-android
steps:
- - name: Add Cargo to PATH
- run: echo "$CARGO_HOME/bin" >> $GITHUB_PATH
-
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.pre-release-checks.outputs.commit_sha }} # to ensure all builds are consistent
- - name: Set up Rust
- uses: dtolnay/rust-toolchain@master
- with:
- toolchain: 1.92.0
- targets: ${{ matrix.settings.target }}
- components: rustfmt
+ - name: Install Nix
+ uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- - name: Install Cross
- run: |
- cargo install cross --git https://github.com/cross-rs/cross --locked
+ # TODO: magic-nix-cache is best-effort; builds still work without it.
+ - name: Enable Nix cache
+ uses: DeterminateSystems/magic-nix-cache-action@565684385bcd71bad329742eefe8d12f2e765b39 # v13
+
+ - name: Cache Rust build artifacts
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target/release
+ target/${{ matrix.settings.target }}
+ key: ${{ runner.os }}-cargo-android-${{ matrix.settings.target }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml', 'flake.lock', 'flake.nix', 'nix/**/*.nix') }}
+ restore-keys: |
+ ${{ runner.os }}-cargo-android-${{ matrix.settings.target }}-
+ ${{ runner.os }}-cargo-android-
- name: Build for target
- run: |
- CROSS_NO_WARNINGS=0 cross build -p walletkit --target ${{ matrix.settings.target }} --release --locked --features "$WALLETKIT_CARGO_FEATURES"
+ run: ./nix/build-android.sh --target ${{ matrix.settings.target }}
- name: Upload artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: android-${{ matrix.settings.target }}
path: ./target/${{ matrix.settings.target }}/release/libwalletkit.so
@@ -183,40 +188,30 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.pre-release-checks.outputs.commit_sha }} # to ensure all builds are consistent
- - name: Set up Rust
- uses: dtolnay/rust-toolchain@master
- with:
- toolchain: 1.92.0
- targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android,i686-linux-android
- components: rustfmt
+ - name: Install Nix
+ uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
+
+ # TODO: magic-nix-cache is best-effort; builds still work without it.
+ - name: Enable Nix cache
+ uses: DeterminateSystems/magic-nix-cache-action@565684385bcd71bad329742eefe8d12f2e765b39 # v13
- name: Setup Java
- uses: actions/setup-java@v3
+ uses: actions/setup-java@17f84c3641ba7b8f6deff6309fc4c864478f5d62 # v3.14.1
with:
distribution: temurin
java-version: 17
- name: Download Artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
- path: .
+ path: artifacts
- - name: Move artifacts
- run: |
- mkdir -p kotlin/walletkit/src/main/jniLibs && cd kotlin/walletkit/src/main/jniLibs
- mkdir armeabi-v7a arm64-v8a x86 x86_64
- mv /home/runner/work/walletkit/walletkit/android-armv7-linux-androideabi/libwalletkit.so ./armeabi-v7a/libwalletkit.so
- mv /home/runner/work/walletkit/walletkit/android-aarch64-linux-android/libwalletkit.so ./arm64-v8a/libwalletkit.so
- mv /home/runner/work/walletkit/walletkit/android-i686-linux-android/libwalletkit.so ./x86/libwalletkit.so
- mv /home/runner/work/walletkit/walletkit/android-x86_64-linux-android/libwalletkit.so ./x86_64/libwalletkit.so
-
- - name: Generate bindings
- working-directory: kotlin
- run: cargo run -p uniffi-bindgen generate ./walletkit/src/main/jniLibs/arm64-v8a/libwalletkit.so --library --language kotlin --no-format --out-dir walletkit/src/main/java
+ - name: Assemble jniLibs & generate bindings
+ run: nix develop . --command ./kotlin/build_kotlin.sh --artifacts-dir artifacts
- name: Publish
working-directory: kotlin
diff --git a/Cross.toml b/Cross.toml
deleted file mode 100644
index d65322e09..000000000
--- a/Cross.toml
+++ /dev/null
@@ -1,24 +0,0 @@
-[build.env]
-passthrough = [
- "CARGO_NET_GIT_FETCH_WITH_CLI=true", # to force cargo to use git cli with config --global in homedir
- "HOME", # set $HOME to shared dir where we put .gitconfig
- "RUSTUP_HOME", # override rustup home to prevent host permission issues
- "CARGO_HOME", # override cargo home to prevent host permission issues
-]
-
-# max-page-size=16384:
-# Android 15 (API 35) introduces support for 16KB page sizes to improve performance on devices with larger RAM.
-# Apps with native libraries MUST be compiled with 16KB ELF alignment or they will crash on startup
-# on devices configured with 16KB page sizes. This flag ensures proper alignment for both 32-bit and 64-bit targets.
-# Reference: https://developer.android.com/guide/practices/page-sizes
-[target.aarch64-linux-android]
-rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]
-
-[target.armv7-linux-androideabi]
-rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]
-
-[target.x86_64-linux-android]
-rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]
-
-[target.i686-linux-android]
-rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]
diff --git a/README.md b/README.md
index 886ea5554..a9e86ca09 100644
--- a/README.md
+++ b/README.md
@@ -40,13 +40,9 @@ Replace `VERSION` with the desired WalletKit version.
### Prerequisites
-1. **Docker Desktop**: Required for cross-compilation
- - The build uses [`cross`](https://github.com/cross-rs/cross) which runs builds in Docker containers with all necessary toolchains
- - Install via Homebrew:
- ```bash
- brew install --cask docker
- ```
- - Launch Docker Desktop and ensure it's running before building
+1. **Nix**: The Android cross-compilation toolchain (Rust, NDK, linkers) is provided
+ by the Nix devshell — see [`nix/README.md`](nix/README.md). Without Nix, you
+ must provision the equivalent dependencies manually.
2. **Android SDK + NDK**: Required for Gradle Android tasks
- Install via Android Studio > Settings > Android SDK (ensure the NDK is installed)
@@ -57,7 +53,7 @@ Replace `VERSION` with the desired WalletKit version.
To test local changes before publishing a release, use the build script to compile the Rust library, generate UniFFI bindings, and publish a SNAPSHOT to Maven Local:
```bash
-./kotlin/local_kotlin.sh 0.3.1
+nix develop .#android --command ./kotlin/local_kotlin.sh 0.3.1
```
Example with custom Rust locations:
@@ -65,7 +61,7 @@ Example with custom Rust locations:
RUSTUP_HOME=~/.rustup CARGO_HOME=~/.cargo ./kotlin/local_kotlin.sh 0.1.0-SNAPSHOT
```
-> **Note**: The script can be run from any working directory (it resolves its own location). It sets `RUSTUP_HOME` and `CARGO_HOME` to `/tmp` by default to avoid Docker permission issues when using `cross`. You can override them by exporting your own values.
+> **Note**: The script resolves its own location, but does not provision or enter the build environment. Run it from the Nix `android` devshell or with the required dependencies configured manually.
This will:
1. Build the Rust library for all Android architectures (arm64-v8a, armeabi-v7a, x86_64, x86)
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 000000000..cfb7a8b68
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,82 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1777268161,
+ "narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs",
+ "rust-overlay": "rust-overlay"
+ }
+ },
+ "rust-overlay": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1777605393,
+ "narHash": "sha256-Hjp0VOOHgHcTrX23iVvnfAudPcuCmfkfpQNFwv2v/ks=",
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "rev": "ff88db34cfa486fc4964a6991cab1678d82eee8c",
+ "type": "github"
+ },
+ "original": {
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "type": "github"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 000000000..49b3ac9d0
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,51 @@
+{
+ description = "WalletKit development shells";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ flake-utils.url = "github:numtide/flake-utils";
+ rust-overlay.url = "github:oxalica/rust-overlay";
+ rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
+ };
+
+ outputs = { nixpkgs, flake-utils, rust-overlay, ... }:
+ flake-utils.lib.eachSystem [
+ "x86_64-linux"
+ "aarch64-linux"
+ "aarch64-darwin"
+ "x86_64-darwin"
+ ] (system:
+ let
+ overlays = [
+ (import rust-overlay)
+ (final: prev: { nargo = final.callPackage ./nix/nargo.nix { }; })
+ ];
+ pkgs = import nixpkgs {
+ inherit system overlays;
+ config = {
+ allowUnfree = true;
+ android_sdk.accept_license = true;
+ };
+ };
+
+ rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
+ in
+ {
+ devShells = {
+ default = pkgs.mkShell {
+ packages = [
+ rustToolchain
+ pkgs.curl
+ pkgs.git
+ pkgs.nargo
+ ];
+ };
+
+ wasm = import ./nix/wasm.nix { inherit pkgs; };
+ } // pkgs.lib.optionalAttrs (!(pkgs.stdenv.isLinux && pkgs.stdenv.isAarch64)) {
+ # The Android NDK has no aarch64-linux prebuilt toolchain
+ # (on aarch64 Docker hosts, run the container as linux/amd64).
+ android = import ./nix/android.nix { inherit pkgs; };
+ };
+ });
+}
diff --git a/kotlin/build_kotlin.sh b/kotlin/build_kotlin.sh
index ffa2057c7..f13b322a0 100755
--- a/kotlin/build_kotlin.sh
+++ b/kotlin/build_kotlin.sh
@@ -1,42 +1,103 @@
-#!/bin/bash
-set -e
+#!/usr/bin/env bash
+set -euo pipefail
-## This script is only used for local builds. For production releases, the code is in the CI workflow.
+# Build Android native libraries and assemble the Kotlin JNI/UniFFI bindings.
+# Usage: ./kotlin/build_kotlin.sh [--artifacts-dir
]
+# See nix/README.md for build environment setup.
-echo "Building WalletKit Android SDK..."
+cd "$(dirname "${BASH_SOURCE[0]}")/.."
-CARGO_FEATURES="compress-zkeys,embed-zkeys,v3"
+readonly TARGETS=(
+ "aarch64-linux-android"
+ "armv7-linux-androideabi"
+ "x86_64-linux-android"
+ "i686-linux-android"
+)
-# Create jniLibs directories
-mkdir -p ./walletkit/src/main/jniLibs/{arm64-v8a,armeabi-v7a,x86_64,x86}
+abi_for_target() {
+ case "$1" in
+ aarch64-linux-android) echo "arm64-v8a" ;;
+ armv7-linux-androideabi) echo "armeabi-v7a" ;;
+ x86_64-linux-android) echo "x86_64" ;;
+ i686-linux-android) echo "x86" ;;
+ esac
+}
-# Build for all Android architectures
-echo "Building for aarch64-linux-android..."
-cross build -p walletkit --release --target=aarch64-linux-android --features "$CARGO_FEATURES"
+usage() {
+ cat <]
-echo "Building for armv7-linux-androideabi..."
-cross build -p walletkit --release --target=armv7-linux-androideabi --features "$CARGO_FEATURES"
+Options:
+ --artifacts-dir Skip the cargo builds and take prebuilt libraries
+ from /android-/libwalletkit.so
+ (the layout produced by the CI build matrix).
-echo "Building for x86_64-linux-android..."
-cross build -p walletkit --release --target=x86_64-linux-android --features "$CARGO_FEATURES"
+EOF
+}
-echo "Building for i686-linux-android..."
-cross build -p walletkit --release --target=i686-linux-android --features "$CARGO_FEATURES"
+ARTIFACTS_DIR=""
-# Move .so files to jniLibs
-echo "Moving native libraries..."
-mv ../target/aarch64-linux-android/release/libwalletkit.so ./walletkit/src/main/jniLibs/arm64-v8a/libwalletkit.so
-mv ../target/armv7-linux-androideabi/release/libwalletkit.so ./walletkit/src/main/jniLibs/armeabi-v7a/libwalletkit.so
-mv ../target/x86_64-linux-android/release/libwalletkit.so ./walletkit/src/main/jniLibs/x86_64/libwalletkit.so
-mv ../target/i686-linux-android/release/libwalletkit.so ./walletkit/src/main/jniLibs/x86/libwalletkit.so
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --artifacts-dir)
+ if [[ $# -lt 2 ]]; then
+ echo "error: --artifacts-dir requires a value" >&2
+ usage >&2
+ exit 1
+ fi
+ ARTIFACTS_DIR="$2"
+ shift 2
+ ;;
+ --help | -h)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "error: unknown argument: $1" >&2
+ usage >&2
+ exit 1
+ ;;
+ esac
+done
+
+if [[ -z "${ARTIFACTS_DIR}" ]]; then
+ if [[ -z "${CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER:-}" ]]; then
+ echo "error: Android cross-compilation environment not configured." >&2
+ echo "Run inside the Nix devshell: nix develop .#android --command $0" >&2
+ echo "or via Docker without Nix: nix/docker.sh develop .#android --command $0" >&2
+ exit 1
+ fi
+
+ # Defaults to the release feature set; CI overrides via WALLETKIT_CARGO_FEATURES.
+ CARGO_FEATURES="${WALLETKIT_CARGO_FEATURES:-compress-zkeys,embed-zkeys,v3}"
+
+ echo "Building WalletKit Android SDK..."
+ for target in "${TARGETS[@]}"; do
+ echo "Building for ${target}..."
+ cargo build -p walletkit --release --locked --target "${target}" --features "$CARGO_FEATURES"
+ done
+fi
+
+# Copy .so files into the jniLibs layout
+echo "Copying native libraries..."
+for target in "${TARGETS[@]}"; do
+ abi="$(abi_for_target "${target}")"
+ if [[ -n "${ARTIFACTS_DIR}" ]]; then
+ src="${ARTIFACTS_DIR}/android-${target}/libwalletkit.so"
+ else
+ src="target/${target}/release/libwalletkit.so"
+ fi
+ mkdir -p "kotlin/walletkit/src/main/jniLibs/${abi}"
+ cp "${src}" "kotlin/walletkit/src/main/jniLibs/${abi}/libwalletkit.so"
+done
# Generate Kotlin bindings
echo "Generating Kotlin bindings..."
-cargo run -p uniffi-bindgen generate \
- ./walletkit/src/main/jniLibs/arm64-v8a/libwalletkit.so \
+cargo run -p uniffi-bindgen --locked -- generate \
+ kotlin/walletkit/src/main/jniLibs/arm64-v8a/libwalletkit.so \
--library \
--language kotlin \
--no-format \
- --out-dir walletkit/src/main/java
+ --out-dir kotlin/walletkit/src/main/java
echo "✅ Build complete!"
diff --git a/kotlin/local_kotlin.sh b/kotlin/local_kotlin.sh
index a119a2b29..e4b9f3a67 100755
--- a/kotlin/local_kotlin.sh
+++ b/kotlin/local_kotlin.sh
@@ -1,11 +1,11 @@
#!/bin/bash
set -e
-echo "Building WalletKit Android SDK for local development..."
+# Build the Kotlin bindings and publish them to Maven Local.
+# Usage: ./kotlin/local_kotlin.sh
+# See nix/README.md for build environment setup.
-# Set rustup and cargo home to /tmp to prevent Docker permission issues
-export RUSTUP_HOME="${RUSTUP_HOME:-/tmp/.rustup}"
-export CARGO_HOME="${CARGO_HOME:-/tmp/.cargo}"
+echo "Building WalletKit Android SDK for local development..."
# Version is required
if [ -z "$1" ]; then
@@ -18,7 +18,6 @@ fi
VERSION="$1"
echo "Using version: $VERSION"
-# Build using kotlin/build.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
diff --git a/kotlin/walletkit/src/main/jniLibs/.gitignore b/kotlin/walletkit/src/main/jniLibs/.gitignore
deleted file mode 100644
index d6b7ef32c..000000000
--- a/kotlin/walletkit/src/main/jniLibs/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*
-!.gitignore
diff --git a/nix/README.md b/nix/README.md
new file mode 100644
index 000000000..6d4b1433a
--- /dev/null
+++ b/nix/README.md
@@ -0,0 +1,77 @@
+# Nix development environments
+
+This directory contains the Nix devshells and helper scripts used to
+cross-compile WalletKit for Android and wasm, both locally and in CI.
+Swift/iOS builds intentionally continue to use their existing host Xcode setup.
+
+The flake provides **devshells, not packages**: the shells pin the Rust
+toolchain (from `rust-toolchain.toml`), the Android NDK, nargo and every
+env var cargo needs for cross-compilation — the builds themselves are
+ordinary `cargo build` invocations run inside a shell.
+
+## Shells
+
+| Shell | Purpose | Systems |
+|---|---|---|
+| `default` | Host builds, uniffi-bindgen, nargo | all |
+| `android` | Cross-compile the 4 Android targets (NDK, linkers, API 23) | linux/darwin x86_64, darwin aarch64 |
+| `wasm` | `wasm32-unknown-unknown` with a wasm-safe clang | all |
+
+```bash
+nix develop .#android # enter a shell
+nix develop .#android --command cargo build ... # or run one command
+```
+
+Convenience wrappers (they enter the right shell for you):
+
+```bash
+nix/build-android.sh --target aarch64-linux-android
+nix/build-wasm.sh
+nix develop .#android --command ./kotlin/build_kotlin.sh # full Android jniLibs + bindings
+nix develop .#android --command ./kotlin/local_kotlin.sh 0.3.1 # publish to Maven Local
+```
+
+## No Nix installed? Use Docker
+
+`nix/docker.sh` proxies Nix commands into a `nixos/nix` container — the only
+host dependency is Docker. Its arguments intentionally match the native Nix CLI:
+
+```bash
+nix/docker.sh develop .#android --command ./kotlin/build_kotlin.sh
+nix/docker.sh develop .#wasm --command cargo build -p walletkit --release --locked --target wasm32-unknown-unknown
+nix/docker.sh develop .#default # interactive shell
+nix/docker.sh flake show # other Nix commands work too
+```
+
+Native Nix is recommended when available. The Docker wrapper always uses a
+`linux/amd64` container because the Android NDK does not ship an aarch64-linux
+host toolchain. Consequently, it runs under emulation on ARM hosts such as Apple
+Silicon and is slower than `nix develop` directly.
+
+Notes:
+
+- The Nix store is kept in the `walletkit-nix-store-amd64` Docker volume, so
+ toolchains download only on the first run. Remove it with `docker volume rm
+ walletkit-nix-store-amd64` to reclaim the space. Cargo uses the corresponding
+ `walletkit-cargo-home-amd64` volume.
+- On Apple Silicon, enable Rosetta emulation in Docker Desktop settings (on by
+ default in recent versions).
+- The flake does not provide a Swift shell. Swift builds depend on the host
+ macOS/Xcode configuration; see [`swift/README.md`](../swift/README.md).
+- On Linux hosts, files created by the container (e.g. `target/`) are
+ root-owned.
+
+## Building without Nix at all
+
+Possible, if you bring the dependencies yourself:
+
+- **Host / wasm**: `rustup` picks the toolchain and targets up from
+ `rust-toolchain.toml`, and `.cargo/config.toml` carries the target
+ rustflags. wasm needs clang ≥ 18 exported as `CC_wasm32_unknown_unknown`
+ (see `nix/wasm.nix`).
+- **Android**: you need NDK r27 and the `CC_*`/`AR_*`/
+ `CARGO_TARGET_*_LINKER` env vars pointing into it — exactly what
+ `nix/android.nix` sets. There is no script for the manual setup; the
+ Nix shell (or its Docker wrapper) is the supported path.
+- **nargo**: install the version matching the `provekit_*` crates in
+ `Cargo.lock` (see `nix/nargo.nix`).
diff --git a/nix/android.nix b/nix/android.nix
new file mode 100644
index 000000000..d04127e55
--- /dev/null
+++ b/nix/android.nix
@@ -0,0 +1,76 @@
+{ pkgs }:
+let
+ androidApiLevel = "23";
+ androidPackages = pkgs.androidenv.composeAndroidPackages {
+ platformVersions = [ androidApiLevel ];
+ includeNDK = true;
+ ndkVersion = "27.2.12479018";
+ };
+ androidNdkPackage = androidPackages.ndk-bundle;
+ # These paths reach into the internal layout of the nixpkgs androidenv derivation
+ # (there is no stable API for them). Pinned via flake.lock, but expect them to be
+ # the first thing to break on a `nix flake update`.
+ androidNdkHome = "${androidNdkPackage}/libexec/android-sdk/ndk-bundle";
+ androidToolchainPlatform = if pkgs.stdenv.isDarwin then "darwin-x86_64" else "linux-x86_64";
+ androidToolchainBin = "${androidNdkHome}/toolchains/llvm/prebuilt/${androidToolchainPlatform}/bin";
+
+ androidClang = "${androidToolchainBin}/aarch64-linux-android${androidApiLevel}-clang";
+ androidClangxx = "${androidToolchainBin}/aarch64-linux-android${androidApiLevel}-clang++";
+ androidI686Clang = "${androidToolchainBin}/i686-linux-android${androidApiLevel}-clang";
+ androidI686Clangxx = "${androidToolchainBin}/i686-linux-android${androidApiLevel}-clang++";
+ androidArmv7Clang = "${androidToolchainBin}/armv7a-linux-androideabi${androidApiLevel}-clang";
+ androidArmv7Clangxx = "${androidToolchainBin}/armv7a-linux-androideabi${androidApiLevel}-clang++";
+ androidX8664Clang = "${androidToolchainBin}/x86_64-linux-android${androidApiLevel}-clang";
+ androidX8664Clangxx = "${androidToolchainBin}/x86_64-linux-android${androidApiLevel}-clang++";
+
+ rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml;
+in
+pkgs.mkShell {
+ packages = [
+ rustToolchain
+ androidNdkPackage
+ pkgs.curl
+ pkgs.git
+ pkgs.jdk17
+ pkgs.nargo
+ ];
+
+ ANDROID_API_LEVEL = androidApiLevel;
+ ANDROID_NDK_HOME = androidNdkHome;
+ ANDROID_NDK_ROOT = androidNdkHome;
+ NDK_HOME = androidNdkHome;
+
+ # Target rustflags live in .cargo/config.toml — a CARGO_TARGET_*_RUSTFLAGS
+ # env var here would replace them, not merge.
+ CC_aarch64_linux_android = androidClang;
+ CXX_aarch64_linux_android = androidClangxx;
+ AR_aarch64_linux_android = "${androidToolchainBin}/llvm-ar";
+ RANLIB_aarch64_linux_android = "${androidToolchainBin}/llvm-ranlib";
+ CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER = androidClang;
+
+ CC_i686_linux_android = androidI686Clang;
+ CXX_i686_linux_android = androidI686Clangxx;
+ AR_i686_linux_android = "${androidToolchainBin}/llvm-ar";
+ RANLIB_i686_linux_android = "${androidToolchainBin}/llvm-ranlib";
+ CARGO_TARGET_I686_LINUX_ANDROID_LINKER = androidI686Clang;
+
+ CC_armv7_linux_androideabi = androidArmv7Clang;
+ CXX_armv7_linux_androideabi = androidArmv7Clangxx;
+ AR_armv7_linux_androideabi = "${androidToolchainBin}/llvm-ar";
+ RANLIB_armv7_linux_androideabi = "${androidToolchainBin}/llvm-ranlib";
+ CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER = androidArmv7Clang;
+
+ CC_x86_64_linux_android = androidX8664Clang;
+ CXX_x86_64_linux_android = androidX8664Clangxx;
+ AR_x86_64_linux_android = "${androidToolchainBin}/llvm-ar";
+ RANLIB_x86_64_linux_android = "${androidToolchainBin}/llvm-ranlib";
+ CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER = androidX8664Clang;
+
+ shellHook = ''
+ echo "WalletKit Android dev shell"
+ echo " targets: aarch64-linux-android, armv7-linux-androideabi, i686-linux-android, x86_64-linux-android (API $ANDROID_API_LEVEL)"
+ echo " ndk: $ANDROID_NDK_HOME"
+ echo ""
+ echo "Build with: cargo build -p walletkit --release --target aarch64-linux-android --features compress-zkeys,embed-zkeys,v3"
+ '';
+}
diff --git a/nix/build-android.sh b/nix/build-android.sh
new file mode 100755
index 000000000..4bb027a79
--- /dev/null
+++ b/nix/build-android.sh
@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+cd "$(dirname "${BASH_SOURCE[0]}")/.."
+
+readonly TARGETS=(
+ "aarch64-linux-android"
+ "armv7-linux-androideabi"
+ "x86_64-linux-android"
+ "i686-linux-android"
+)
+
+usage() {
+ cat <
+
+Targets:
+$(printf ' %s\n' "${TARGETS[@]}")
+
+EOF
+}
+
+TARGET=""
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --target | -t)
+ if [[ $# -lt 2 ]]; then
+ echo "error: --target requires a value" >&2
+ usage >&2
+ exit 1
+ fi
+ TARGET="$2"
+ shift 2
+ ;;
+ --help | -h)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "error: unknown argument: $1" >&2
+ usage >&2
+ exit 1
+ ;;
+ esac
+done
+
+if [[ -z "${TARGET}" ]]; then
+ echo "error: --target is required" >&2
+ usage >&2
+ exit 1
+fi
+
+valid_target=false
+for candidate in "${TARGETS[@]}"; do
+ if [[ "${TARGET}" == "${candidate}" ]]; then
+ valid_target=true
+ break
+ fi
+done
+
+if [[ "${valid_target}" != true ]]; then
+ echo "error: unsupported target: ${TARGET}" >&2
+ usage >&2
+ exit 1
+fi
+
+# Defaults to the release feature set; CI overrides via WALLETKIT_CARGO_FEATURES.
+exec nix develop .#android --command cargo build \
+ -p walletkit \
+ --release \
+ --locked \
+ --target "${TARGET}" \
+ --features "${WALLETKIT_CARGO_FEATURES:-compress-zkeys,embed-zkeys,v3}"
diff --git a/nix/build-wasm.sh b/nix/build-wasm.sh
new file mode 100755
index 000000000..2a00ab7e3
--- /dev/null
+++ b/nix/build-wasm.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+cd "$(dirname "${BASH_SOURCE[0]}")/.."
+
+exec nix develop .#wasm --command cargo build \
+ -p walletkit \
+ --release \
+ --locked \
+ --target wasm32-unknown-unknown \
+ "$@"
diff --git a/nix/docker.sh b/nix/docker.sh
new file mode 100755
index 000000000..02545e810
--- /dev/null
+++ b/nix/docker.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Run Nix commands in a Linux/amd64 Docker container for hosts without Nix.
+# This mirrors the Nix CLI; only the container boundary is added.
+#
+# Usage:
+# nix/docker.sh develop .#android
+# nix/docker.sh develop .#android --command ./kotlin/build_kotlin.sh
+# nix/docker.sh develop .#wasm --command cargo build --release --target wasm32-unknown-unknown
+# nix/docker.sh flake show
+#
+# Swift builds are not provided by the flake because they depend on macOS and
+# the host Xcode configuration; use the existing Swift build flow instead.
+
+cd "$(dirname "${BASH_SOURCE[0]}")/.."
+
+TTY_FLAGS=(-i)
+if [[ -t 0 && -t 1 ]]; then
+ TTY_FLAGS=(-it)
+fi
+
+# Always use linux/amd64 because the Android NDK has no aarch64-linux host
+# toolchain. This is emulated on ARM hosts. Keep the Nix store and Cargo home
+# architecture-specific so they cannot be contaminated by native ARM binaries.
+# Nix's syscall filter cannot be installed under Rosetta emulation; Docker still
+# provides the outer container sandbox.
+#
+# Note: the container runs as root, so on Linux hosts files created in target/
+# will be root-owned.
+exec docker run --rm "${TTY_FLAGS[@]}" --platform linux/amd64 \
+ --volume "$PWD:/src" \
+ --volume walletkit-nix-store-amd64:/nix \
+ --volume walletkit-cargo-home-amd64:/root/.cargo \
+ --workdir /src \
+ nixos/nix:2.34.8 \
+ nix --extra-experimental-features 'nix-command flakes' \
+ --option filter-syscalls false \
+ "$@"
diff --git a/nix/nargo.nix b/nix/nargo.nix
new file mode 100644
index 000000000..c07c15260
--- /dev/null
+++ b/nix/nargo.nix
@@ -0,0 +1,56 @@
+# Noir is not packaged in nixpkgs, so we repackage the official prebuilt
+# release binaries. The version must match what the provekit crates expect
+# (see the provekit_* entries in Cargo.lock, versioned `-alpha.N`).
+{ stdenv, lib, fetchurl, autoPatchelfHook }:
+
+let
+ artifacts = {
+ aarch64-darwin = {
+ target = "aarch64-apple-darwin";
+ hash = "sha256-XAYTKQvnKPyzFKSAV4pg4KEc0ZLSSdCN7US+58XuoUI=";
+ };
+ x86_64-darwin = {
+ target = "x86_64-apple-darwin";
+ hash = "sha256-q/qd+hpYsQoQBt0uq/2xnrpWl99com4XB+qhsK36j/c=";
+ };
+ x86_64-linux = {
+ target = "x86_64-unknown-linux-gnu";
+ hash = "sha256-iaAU/TBYoFnRv6+ZzZiR65SKCT7DExSpzwRzSI9Ud5U=";
+ };
+ aarch64-linux = {
+ target = "aarch64-unknown-linux-gnu";
+ hash = "sha256-v25Eqc0dqMnERdIcaYtAZ4o9bveda3txjCSdZp0HCWY=";
+ };
+ };
+ artifact =
+ artifacts.${stdenv.hostPlatform.system}
+ or (throw "nargo: no prebuilt binary for ${stdenv.hostPlatform.system}");
+in stdenv.mkDerivation rec {
+ pname = "nargo";
+ version = "1.0.0-beta.11";
+
+ src = fetchurl {
+ url = "https://github.com/noir-lang/noir/releases/download/v${version}/nargo-${artifact.target}.tar.gz";
+ hash = artifact.hash;
+ };
+
+ sourceRoot = ".";
+
+ nativeBuildInputs = lib.optionals stdenv.isLinux [ autoPatchelfHook ];
+ buildInputs = lib.optionals stdenv.isLinux [ stdenv.cc.cc.lib ];
+
+ installPhase = ''
+ runHook preInstall
+ install -Dm755 nargo $out/bin/nargo
+ runHook postInstall
+ '';
+
+ meta = {
+ description = "Noir compiler and package manager";
+ homepage = "https://noir-lang.org";
+ license = with lib.licenses; [ mit asl20 ];
+ sourceProvenance = [ lib.sourceTypes.binaryNativeCode ];
+ platforms = lib.attrNames artifacts;
+ mainProgram = "nargo";
+ };
+}
diff --git a/nix/wasm.nix b/nix/wasm.nix
new file mode 100644
index 000000000..e5d911b88
--- /dev/null
+++ b/nix/wasm.nix
@@ -0,0 +1,27 @@
+{ pkgs }:
+let
+ rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml;
+ llvm = pkgs.llvmPackages;
+in
+pkgs.mkShell {
+ packages = [
+ rustToolchain
+ llvm.clang-unwrapped
+ llvm.bintools-unwrapped
+ pkgs.curl
+ pkgs.git
+ pkgs.nargo
+ ];
+
+ # Use unwrapped clang: cc-wrapper injects host hardening flags that are invalid for wasm.
+ CC_wasm32_unknown_unknown = "${llvm.clang-unwrapped}/bin/clang";
+ AR_wasm32_unknown_unknown = "${llvm.bintools-unwrapped}/bin/llvm-ar";
+
+ shellHook = ''
+ echo "WalletKit wasm dev shell"
+ echo " target: wasm32-unknown-unknown"
+ echo " clang: $CC_wasm32_unknown_unknown"
+ echo ""
+ echo "Build with: cargo build -p walletkit --target wasm32-unknown-unknown"
+ '';
+}