diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87b1b0f6..00209cd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -218,6 +218,26 @@ jobs: - name: Build and test run: cargo test -vv + + android-cross-compile: + name: Cross-Compile for Android (${{ matrix.package }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + package: + - android-aarch64 + - android-armv7 + - android-x86_64 + steps: + - uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v31 + + - name: Build + run: nix build .#${{ matrix.package }} -L + fuzz-corpus: name: Verify Fuzz Corpus runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 995e1377..cdd53e24 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target Cargo.lock coverage_report +result diff --git a/CHANGELOG.md b/CHANGELOG.md index c71b2b36..8406eca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.2.1] 2026-05-20 ### Added +- Added Nix package outputs for Android with bundled NDK r27, Rust toolchains, Boost, and cmake. - Added `BlockTreeEntry::ancestor` to look up an ancestor block at a given height. Returns `None` if the height is out of range. This operation is O(log N). - Added `Transaction::locktime()` to retrieve a transaction's `nLockTime` value as a `u32`. - Added `TxIn::sequence()` to retrieve an input's `nSequence` value as a `u32`. diff --git a/README.md b/README.md index a62c2315..319606fd 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,24 @@ dependencies. Once setup, run: cargo b ``` +### Android Cross-Compilation + +Android cross-compilation requires [Nix](https://nixos.org/). + +Nix package outputs bundle the exact NDK version, Rust toolchains with Android +targets, Boost, and cmake in a single reproducible build reduncing the support +needed from the rust-bitcoinkernel side. + +```bash +nix build .#android-aarch64 +nix build .#android-armv7 +nix build .#android-x86_64 +``` + +The resulting libraries are placed in `result/lib/`. + +Output targets Android API 24+ (Nougat) minimum. + ## MSRV (Minimum Supported Rust Version) The minimum supported Rust version is 1.71. Users on rustc older than diff --git a/flake.nix b/flake.nix index acc4c365..557bc5cb 100644 --- a/flake.nix +++ b/flake.nix @@ -25,9 +25,10 @@ }; rustVersion = "1.71.0"; + rustToolchainSha256 = "sha256-ks0nMEGGXKrHnfv4Fku+vhQ7gx76ruv6Ij4fKZR3l78="; rustToolchain = fenix.packages.${system}.fromToolchainName { name = rustVersion; - sha256 = "sha256-ks0nMEGGXKrHnfv4Fku+vhQ7gx76ruv6Ij4fKZR3l78="; + sha256 = rustToolchainSha256; }; rustBuildToolchain = fenix.packages.${system}.combine [ rustToolchain.rustc @@ -85,6 +86,85 @@ pkgs.gcc.cc.lib ]; }; + + packages = + # Android build infrastructure (unfree NDK + SDK). + let + ndkVersion = "27.2.12479018"; + lockfile = ./Cargo-minimal.lock; + androidPkgs = import nixpkgs { + inherit system; + config.android_sdk.accept_license = true; + config.allowUnfree = true; + }; + androidComposition = androidPkgs.androidenv.composeAndroidPackages { + # platformVersions is the SDK tooling version, not the minimum API level. + # The NDK target floor is set via ANDROID_API_LEVEL in build.rs (default 24). + platformVersions = [ "34" ]; + ndkVersions = [ ndkVersion ]; + includeNDK = true; + }; + androidSdk = androidComposition.androidsdk; + androidNdk = "${androidSdk}/libexec/android-sdk/ndk/${ndkVersion}"; + + mkAndroidPackage = + rustTarget: + let + rustTargetToolchain = fenix.packages.${system}.combine [ + rustToolchain.rustc + rustToolchain.cargo + rustToolchain.rust-src + rustToolchain.rust-std + (fenix.packages.${system}.targets.${rustTarget}.fromToolchainName { + name = rustVersion; + sha256 = rustToolchainSha256; + }).rust-std + ]; + rustPlatform = androidPkgs.makeRustPlatform { + cargo = rustTargetToolchain; + rustc = rustTargetToolchain; + }; + in + rustPlatform.buildRustPackage { + pname = "libbitcoinkernel-${rustTarget}"; + version = "0.2.0"; + src = ./.; + cargoLock.lockFile = lockfile; + postPatch = '' + cp ${lockfile} Cargo.lock + ''; + nativeBuildInputs = [ + androidPkgs.cmake + androidPkgs.boost.dev + androidSdk + ]; + + ANDROID_HOME = "${androidSdk}/libexec/android-sdk"; + ANDROID_NDK_HOME = androidNdk; + ANDROID_NDK_ROOT = androidNdk; + CMAKE_PREFIX_PATH = "${androidPkgs.boost.dev}"; + + # cargoBuildHook hardcodes the host --target at + # derivation time, so we bypass it for cross builds. + dontCargoBuild = true; + doCheck = false; + buildPhase = '' + cargo build -p libbitcoinkernel-sys --target ${rustTarget} --offline --release + ''; + installPhase = '' + mkdir -p $out/lib $out/include + find target/${rustTarget}/release -path "*/out/install/lib/*.a" \ + -exec cp {} $out/lib/ \; + find target/${rustTarget}/release -path "*/out/install/include/*" \ + -exec cp {} $out/include/ \; + ''; + }; + in + { + android-aarch64 = mkAndroidPackage "aarch64-linux-android"; + android-armv7 = mkAndroidPackage "armv7-linux-androideabi"; + android-x86_64 = mkAndroidPackage "x86_64-linux-android"; + }; } ); } diff --git a/libbitcoinkernel-sys/CHANGELOG.md b/libbitcoinkernel-sys/CHANGELOG.md index 7366bad9..268ad76e 100644 --- a/libbitcoinkernel-sys/CHANGELOG.md +++ b/libbitcoinkernel-sys/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.3.0] - 2026-05-20 ### Added +- Added build support for android - New `btck_ConsensusParams` opaque type for holding consensus parameters - New `btck_chain_parameters_get_consensus_params` for extracting consensus params from `btck_ChainParameters` (lifetime-bound to the chain parameters object) - New `btck_block_check` for context-free block validation (size limits, coinbase structure, sigop limits, with optional POW and merkle-root checks via `btck_BlockCheckFlags`) diff --git a/libbitcoinkernel-sys/build.rs b/libbitcoinkernel-sys/build.rs index 61f505a1..cee4d804 100644 --- a/libbitcoinkernel-sys/build.rs +++ b/libbitcoinkernel-sys/build.rs @@ -2,6 +2,26 @@ use std::env; use std::path::Path; use std::process::Command; +/// Rust target triple -> NDK ABI name (`arm64-v8a`, `armeabi-v7a`, …). +fn android_abi(target: &str) -> Option<&'static str> { + match target { + t if t.contains("aarch64") => Some("arm64-v8a"), + t if t.contains("armv7") => Some("armeabi-v7a"), + t if t.contains("x86_64") => Some("x86_64"), + _ => None, + } +} + +/// Rust target triple -> NDK sysroot lib directory triple. +/// armv7 differs: Rust says `armv7-linux-androideabi`, NDK says `arm-linux-androideabi`. +fn android_sysroot_triple(target: &str) -> &str { + if target.starts_with("armv7") { + "arm-linux-androideabi" + } else { + target + } +} + fn main() { let bitcoin_dir = Path::new("bitcoin"); let out_dir = env::var("OUT_DIR").unwrap(); @@ -15,7 +35,8 @@ fn main() { let build_config = "RelWithDebInfo"; - Command::new("cmake") + let mut cmake_configure = Command::new("cmake"); + cmake_configure .arg("-B") .arg(&build_dir) .arg("-S") @@ -38,7 +59,38 @@ fn main() { .arg("-DBUILD_SHARED_LIBS=OFF") .arg("-DCMAKE_INSTALL_LIBDIR=lib") .arg("-DENABLE_IPC=OFF") - .arg(format!("-DCMAKE_INSTALL_PREFIX={}", install_dir.display())) + .arg(format!("-DCMAKE_INSTALL_PREFIX={}", install_dir.display())); + + let target = env::var("TARGET").unwrap(); + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); + + if target_os == "android" { + let ndk = env::var("ANDROID_NDK_HOME") + .expect("Android target detected but ANDROID_NDK_HOME is not set"); + let toolchain_file = format!("{ndk}/build/cmake/android.toolchain.cmake"); + + let abi = + android_abi(&target).unwrap_or_else(|| panic!("unsupported Android target: {target}")); + + // API level 24+ is required because Bitcoin Core uses getifaddrs + // which was introduced in Android API 24 (Nougat). + let api_level = "24"; + + cmake_configure + .arg(format!("-DCMAKE_TOOLCHAIN_FILE={toolchain_file}")) + .arg(format!("-DANDROID_ABI={abi}")) + .arg(format!("-DANDROID_PLATFORM=android-{api_level}")) + .arg("-DCMAKE_SYSTEM_NAME=Android") + .arg(format!("-DCMAKE_ANDROID_ARCH_ABI={abi}")) + .arg(format!("-DCMAKE_SYSTEM_VERSION={api_level}")) + .arg(format!("-DCMAKE_ANDROID_NDK={ndk}")) + // The Android NDK toolchain sets CMAKE_FIND_ROOT_PATH_MODE_PACKAGE + // to ONLY, which prevents cmake from finding host packages via + // CMAKE_PREFIX_PATH. Override it so Boost headers can be located. + .arg("-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH"); + } + + cmake_configure .status() .expect("cmake should be installed and available in PATH"); @@ -70,19 +122,34 @@ fn main() { } else { install_dir.join("lib") }; + println!("cargo:rustc-link-search=native={}", lib_dir.display()); println!("cargo:rustc-link-lib=static=bitcoinkernel"); let compiler = cc::Build::new().get_compiler(); - let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); if target_os == "windows" { println!("cargo:rustc-link-lib=bcrypt"); println!("cargo:rustc-link-lib=shell32"); - } + } else if target_os == "android" { + // Android NDK ships libc++_static.a and libc++abi.a in the + // per-architecture sysroot directory (not the API-level subdirectory). + let ndk = env::var("ANDROID_NDK_HOME").expect("We called ANDROID_NDK_HOME before."); - if compiler.is_like_clang() { + let ndk_triple = android_sysroot_triple(&target); + + let host_tag = if cfg!(target_os = "macos") { + "darwin-x86_64" + } else { + "linux-x86_64" + }; + let ndk_lib_dir = + format!("{ndk}/toolchains/llvm/prebuilt/{host_tag}/sysroot/usr/lib/{ndk_triple}"); + println!("cargo:rustc-link-search=native={ndk_lib_dir}"); + println!("cargo:rustc-link-lib=static=c++_static"); + println!("cargo:rustc-link-lib=static=c++abi"); + } else if compiler.is_like_clang() { if target_os == "macos" { println!("cargo:rustc-link-lib=dylib=c++"); } else {