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 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"; + }; } ); } 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 {