From 6e4d2389322eb0532308ff803419bca8e804b4c1 Mon Sep 17 00:00:00 2001 From: jaoleal Date: Thu, 7 May 2026 19:25:04 -0300 Subject: [PATCH 1/3] feat: minimal android support This commit presents the minimal work needed for this crate to be able to be compiled for android, this is minimal to avoid bloating too much the build process, which would make it hard to review and maintain. The rest of the work will be done on nix side, which provides better ergonomics for such job. --- libbitcoinkernel-sys/CHANGELOG.md | 1 + libbitcoinkernel-sys/build.rs | 130 +++++++++++++++++++++++++++--- 2 files changed, 121 insertions(+), 10 deletions(-) diff --git a/libbitcoinkernel-sys/CHANGELOG.md b/libbitcoinkernel-sys/CHANGELOG.md index 0a12c19e..433ad2e6 100644 --- a/libbitcoinkernel-sys/CHANGELOG.md +++ b/libbitcoinkernel-sys/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### 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 56488652..2fe2d0f0 100644 --- a/libbitcoinkernel-sys/build.rs +++ b/libbitcoinkernel-sys/build.rs @@ -4,6 +4,35 @@ use std::path::Path; use std::path::PathBuf; 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"), + t if t.contains("i686") => Some("x86"), + _ => 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 + } +} + +/// NDK root from `ANDROID_NDK_HOME`, `ANDROID_NDK_ROOT`, or `NDK_HOME`. +fn android_ndk_home() -> Option { + env::var("ANDROID_NDK_HOME") + .or_else(|_| env::var("ANDROID_NDK_ROOT")) + .or_else(|_| env::var("NDK_HOME")) + .ok() +} + fn main() { let bitcoin_dir = Path::new("bitcoin"); let out_dir = env::var("OUT_DIR").unwrap(); @@ -17,7 +46,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") @@ -40,7 +70,42 @@ 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 = + android_ndk_home().expect("Android target detected but ANDROID_NDK_HOME is not set"); + let toolchain_file = format!("{ndk}/build/cmake/android.toolchain.cmake"); + assert!( + Path::new(&toolchain_file).exists(), + "Android NDK toolchain file not found at {toolchain_file}. \ + Check that ANDROID_NDK_HOME points to a valid NDK installation" + ); + 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 = env::var("ANDROID_API_LEVEL").unwrap_or_else(|_| "24".to_string()); + + 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"); @@ -81,13 +146,45 @@ fn main() { let header = include_path.join("bitcoinkernel.h"); #[allow(deprecated)] - let bindings = bindgen::Builder::default() + let mut builder = bindgen::Builder::default() .header(header.to_str().unwrap()) .clang_arg("-DBITCOINKERNEL_STATIC") .rust_target(bindgen::RustTarget::Stable_1_71) - .rust_edition(RustEdition::Edition2021) - .generate() - .expect("Unable to generate bindings"); + .rust_edition(RustEdition::Edition2021); + + // When cross-compiling for Android, bindgen's host libclang does not + // know the NDK sysroot or target triple. Pass them explicitly so + // system headers like stddef.h are found. + if target_os == "android" { + if let Some(ndk) = android_ndk_home() { + let host_tag = if cfg!(target_os = "macos") { + "darwin-x86_64" + } else { + "linux-x86_64" + }; + let prebuilt = format!("{ndk}/toolchains/llvm/prebuilt/{host_tag}"); + let sysroot = format!("{prebuilt}/sysroot"); + builder = builder + .clang_arg(format!("--target={target}")) + .clang_arg(format!("--sysroot={sysroot}")); + + // The NDK's clang resource directory contains compiler builtins + // (stddef.h, stdarg.h, etc.) that the host libclang may lack for + // this target. Add it as a system include path. + let clang_include = Path::new(&prebuilt).join("lib").join("clang"); + if let Ok(entries) = std::fs::read_dir(&clang_include) { + for entry in entries.flatten() { + let include = entry.path().join("include"); + if include.is_dir() { + builder = builder.clang_arg(format!("-isystem{}", include.display())); + break; + } + } + } + } + } + + let bindings = builder.generate().expect("Unable to generate bindings"); let out_path = PathBuf::from( env::var("OUT_DIR").expect("OUT_DIR was not defined by the cargo environment!"), @@ -97,14 +194,27 @@ fn main() { .expect("Couldn't write bindings!"); 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"); - } - - if compiler.is_like_clang() { + } 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). + if let Some(ndk) = android_ndk_home() { + 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 { From 179271f59e5f60abfc0f1613f8980184109df793 Mon Sep 17 00:00:00 2001 From: jaoleal Date: Thu, 7 May 2026 19:58:32 -0300 Subject: [PATCH 2/3] feat(nix): add android cross-compilation packages This commit presents the rest of the work to provide android builds, the packaging and compiling is done trough nix for simplicity --- flake.nix | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/flake.nix b/flake.nix index c3853bdd..22d283e3 100644 --- a/flake.nix +++ b/flake.nix @@ -77,6 +77,81 @@ pkgs.gcc.cc.lib ]; }; + packages = + # Android build infrastructure (unfree NDK + SDK). + let + androidPkgs = import nixpkgs { + inherit system; + config.android_sdk.accept_license = true; + config.allowUnfree = true; + }; + androidComposition = androidPkgs.androidenv.composeAndroidPackages { + platformVersions = [ "34" ]; + ndkVersions = [ "27.2.12479018" ]; + includeNDK = true; + }; + androidSdk = androidComposition.androidsdk; + androidNdk = "${androidSdk}/libexec/android-sdk/ndk/27.2.12479018"; + + 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 = "sha256-ks0nMEGGXKrHnfv4Fku+vhQ7gx76ruv6Ij4fKZR3l78="; + }).rust-std + ]; + rustPlatform = androidPkgs.makeRustPlatform { + cargo = rustTargetToolchain; + rustc = rustTargetToolchain; + }; + in + rustPlatform.buildRustPackage { + pname = "libbitcoinkernel-${rustTarget}"; + version = "0.2.0"; + src = ./.; + cargoLock.lockFile = ./Cargo-minimal.lock; + postPatch = '' + cp ${./Cargo-minimal.lock} Cargo.lock + ''; + nativeBuildInputs = [ + androidPkgs.cmake + androidPkgs.boost.dev + androidSdk + ]; + + LIBCLANG_PATH = "${androidPkgs.llvmPackages.clang-unwrapped.lib}/lib/"; + 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"; + }; } ); } From 30f44a8755260895bbf9f4a2f9fe867dea2382c9 Mon Sep 17 00:00:00 2001 From: jaoleal Date: Thu, 7 May 2026 19:58:36 -0300 Subject: [PATCH 3/3] ci: add Android cross-compilation CI job --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ CHANGELOG.md | 1 + README.md | 16 ++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5be4adfe..68bac373 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -211,6 +211,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/CHANGELOG.md b/CHANGELOG.md index fad0ca1c..ceaf19c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### 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..8d20d25c 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,22 @@ 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 drying out the support +needed on 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/`. + ## MSRV (Minimum Supported Rust Version) The minimum supported Rust version is 1.71. Users on rustc older than