diff --git a/.gitignore b/.gitignore index cde80cf..deca7a5 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,12 @@ Cargo.lock **/.pub-cache/ **/.pub/ **/pubspec.lock +**/local.properties +**/GeneratedPluginRegistrant.* +**/flutter_export_environment.sh +**/Flutter-Generated.xcconfig +**/Generated.xcconfig +**/Flutter/ephemeral/ # IDE .vscode/ @@ -26,6 +32,10 @@ Thumbs.db # Native libs built locally libbdkffi.* +android/libs +bdk_demo/android/app/src/main/jniLibs +ios/Release/ +bdk_demo/ios/ios/ test_output.txt test output.txt lib/bdk.dart diff --git a/bdk_demo/.metadata b/bdk_demo/.metadata index bad0587..faf2eb0 100644 --- a/bdk_demo/.metadata +++ b/bdk_demo/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "d7b523b356d15fb81e7d340bbe52b47f93937323" + revision: "f5a8537f90d143abd5bb2f658fa69c388da9677b" channel: "stable" project_type: app @@ -13,11 +13,14 @@ project_type: app migration: platforms: - platform: root - create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 - base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + create_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b + base_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b + - platform: android + create_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b + base_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b - platform: ios - create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 - base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + create_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b + base_revision: f5a8537f90d143abd5bb2f658fa69c388da9677b # User provided section diff --git a/bdk_demo/android/.gitignore b/bdk_demo/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/bdk_demo/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/bdk_demo/android/app/build.gradle.kts b/bdk_demo/android/app/build.gradle.kts new file mode 100644 index 0000000..ca0db07 --- /dev/null +++ b/bdk_demo/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.bdk_demo" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.bdk_demo" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/bdk_demo/android/app/src/debug/AndroidManifest.xml b/bdk_demo/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/bdk_demo/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/bdk_demo/android/app/src/main/AndroidManifest.xml b/bdk_demo/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6ce2eb8 --- /dev/null +++ b/bdk_demo/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/bdk_demo/android/app/src/main/kotlin/com/example/bdk_demo/MainActivity.kt b/bdk_demo/android/app/src/main/kotlin/com/example/bdk_demo/MainActivity.kt new file mode 100644 index 0000000..7f7cb8b --- /dev/null +++ b/bdk_demo/android/app/src/main/kotlin/com/example/bdk_demo/MainActivity.kt @@ -0,0 +1,137 @@ +package com.example.bdk_demo + +import android.os.Build +import android.util.Log +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.util.zip.ZipFile + +private const val CHANNEL = "bdk_demo/native_lib_dir" + +class MainActivity : FlutterActivity() { + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + .setMethodCallHandler { call, result -> + when (call.method) { + "getNativeLibDir" -> { + val path = findLibDirectory() + if (path != null) { + result.success(path) + } else { + result.error( + "LIB_NOT_FOUND", + "Could not locate libbdkffi.so in nativeLibraryDir", + null + ) + } + } + else -> result.notImplemented() + } + } + } + + private fun findLibDirectory(): String? { + val info = applicationContext.applicationInfo + val nativeDirPath = info.nativeLibraryDir ?: return null + val baseDir = File(nativeDirPath) + if (!baseDir.exists()) { + Log.w("BDKDemo", "nativeLibraryDir does not exist: $nativeDirPath") + return null + } + + val direct = File(baseDir, "libbdkffi.so") + if (direct.exists()) { + return baseDir.absolutePath + } + + baseDir.listFiles()?.forEach { candidateDir -> + if (!candidateDir.isDirectory) return@forEach + + val candidate = File(candidateDir, "libbdkffi.so") + if (candidate.exists()) { + return candidateDir.absolutePath + } + + candidateDir.listFiles()?.forEach { nestedDir -> + if (!nestedDir.isDirectory) return@forEach + val nestedCandidate = File(nestedDir, "libbdkffi.so") + if (nestedCandidate.exists()) { + return nestedDir.absolutePath + } + + nestedDir.listFiles()?.forEach { innerDir -> + if (!innerDir.isDirectory) return@forEach + val innerCandidate = File(innerDir, "libbdkffi.so") + if (innerCandidate.exists()) { + return innerDir.absolutePath + } + } + } + } + + Build.SUPPORTED_ABIS?.forEach { abi -> + val candidateDir = File(baseDir, abi) + val candidate = File(candidateDir, "libbdkffi.so") + if (candidate.exists()) { + return candidateDir.absolutePath + } + } + + val extracted = extractLibraryFromApk() + if (extracted != null) { + return extracted + } + + return null + } + + private fun extractLibraryFromApk(): String? { + val info = applicationContext.applicationInfo + val apkPath = info.sourceDir ?: return null + val supportedAbis = Build.SUPPORTED_ABIS ?: return null + val destRoot = File(applicationContext.filesDir, "bdk_native_libs") + + if (!destRoot.exists() && !destRoot.mkdirs()) { + Log.w("BDKDemo", "Failed to create destination dir: ${destRoot.absolutePath}") + return null + } + + try { + ZipFile(apkPath).use { zip -> + for (abi in supportedAbis) { + val entryName = "lib/$abi/libbdkffi.so" + val entry = zip.getEntry(entryName) ?: continue + + val abiDir = File(destRoot, abi) + if (!abiDir.exists() && !abiDir.mkdirs()) { + Log.w("BDKDemo", "Failed to create ABI dir: ${abiDir.absolutePath}") + continue + } + + val outputFile = File(abiDir, "libbdkffi.so") + if (outputFile.exists()) { + return abiDir.absolutePath + } + + zip.getInputStream(entry).use { input -> + FileOutputStream(outputFile).use { output -> + input.copyTo(output) + } + } + outputFile.setReadable(true, false) + return abiDir.absolutePath + } + } + } catch (e: IOException) { + Log.e("BDKDemo", "Failed to extract libbdkffi.so", e) + } + + return null + } +} diff --git a/bdk_demo/android/app/src/main/res/drawable-v21/launch_background.xml b/bdk_demo/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/bdk_demo/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/bdk_demo/android/app/src/main/res/drawable/launch_background.xml b/bdk_demo/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/bdk_demo/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/bdk_demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/bdk_demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/bdk_demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/bdk_demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/bdk_demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/bdk_demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/bdk_demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/bdk_demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/bdk_demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/bdk_demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/bdk_demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/bdk_demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/bdk_demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/bdk_demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/bdk_demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/bdk_demo/android/app/src/main/res/values-night/styles.xml b/bdk_demo/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/bdk_demo/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/bdk_demo/android/app/src/main/res/values/styles.xml b/bdk_demo/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/bdk_demo/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/bdk_demo/android/app/src/profile/AndroidManifest.xml b/bdk_demo/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/bdk_demo/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/bdk_demo/android/build.gradle.kts b/bdk_demo/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/bdk_demo/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/bdk_demo/android/gradle.properties b/bdk_demo/android/gradle.properties new file mode 100644 index 0000000..fbee1d8 --- /dev/null +++ b/bdk_demo/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/bdk_demo/android/gradle/wrapper/gradle-wrapper.properties b/bdk_demo/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/bdk_demo/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/bdk_demo/android/settings.gradle.kts b/bdk_demo/android/settings.gradle.kts new file mode 100644 index 0000000..ca7fe06 --- /dev/null +++ b/bdk_demo/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/bdk_demo/lib/main.dart b/bdk_demo/lib/main.dart index 54cf84c..bac91e6 100644 --- a/bdk_demo/lib/main.dart +++ b/bdk_demo/lib/main.dart @@ -1,4 +1,32 @@ +import 'dart:io' as io; + +import 'package:bdk_dart/bdk.dart' as bdk; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +const _nativeLibChannel = MethodChannel('bdk_demo/native_lib_dir'); +String? _cachedNativeLibDir; + +Future _ensureNativeLibraryDir() async { + if (_cachedNativeLibDir != null) { + io.Directory.current = io.Directory(_cachedNativeLibDir!); + return; + } + + if (io.Platform.isAndroid) { + final dir = await _nativeLibChannel.invokeMethod('getNativeLibDir'); + if (dir == null || dir.isEmpty) { + throw StateError('Native library directory channel returned empty path'); + } + _cachedNativeLibDir = dir; + } else { + _cachedNativeLibDir = io.File(io.Platform.resolvedExecutable).parent.path; + } + + if (_cachedNativeLibDir != null) { + io.Directory.current = io.Directory(_cachedNativeLibDir!); + } +} void main() { runApp(const MyApp()); @@ -29,16 +57,33 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - String _networkName = 'Press button to show network'; - bool _success = false; + String? _networkName; + String? _descriptorSnippet; + String? _error; + + Future _loadBindingData() async { + try { + await _ensureNativeLibraryDir(); + + final network = bdk.Network.testnet; + final descriptor = bdk.Descriptor( + 'wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/' + '84h/1h/0h/0/*)', + network, + ); - void _showSignetNetwork() { - setState(() { - // This simulates what the real Dart bindings would return - // when properly linked to the Rust library - _networkName = 'Signet'; - _success = true; - }); + setState(() { + _networkName = network.name; + _descriptorSnippet = descriptor.toString().substring(0, 32); + _error = null; + }); + } catch (e) { + setState(() { + _error = e.toString(); + _networkName = null; + _descriptorSnippet = null; + }); + } } @override @@ -53,52 +98,56 @@ class _MyHomePageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - _success ? Icons.check_circle : Icons.network_check, + _error != null + ? Icons.error_outline + : _networkName != null + ? Icons.check_circle + : Icons.network_check, size: 80, - color: _success ? Colors.green : Colors.grey, + color: _error != null + ? Colors.red + : _networkName != null + ? Colors.green + : Colors.grey, ), const SizedBox(height: 20), - const Text('BDK Network Type:', style: TextStyle(fontSize: 20)), - Text( - _networkName, - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: _success ? Colors.orange : null, - fontWeight: FontWeight.bold, + const Text('BDK bindings status', style: TextStyle(fontSize: 20)), + if (_networkName != null) ...[ + Text( + 'Network: $_networkName', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: Colors.orange, + fontWeight: FontWeight.bold, + ), ), - ), - const SizedBox(height: 40), - Container( - padding: const EdgeInsets.all(16), - margin: const EdgeInsets.symmetric(horizontal: 20), - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(8), - ), - child: Column( - children: [ - Text('✅ Dart bindings generated with uniffi-dart'), - Text('✅ Network enum includes SIGNET'), - Text('✅ Flutter app ready to use BDK'), - const SizedBox(height: 8), - Text( - 'Generated from: bdk.udl → bdk.dart', - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - fontFamily: 'monospace', - ), + if (_descriptorSnippet != null) + Padding( + padding: const EdgeInsets.only(top: 12), + child: Text( + 'Descriptor sample: $_descriptorSnippet…', + style: const TextStyle(fontFamily: 'monospace'), ), - ], + ), + ] else if (_error != null) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + _error!, + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, + ), ), - ), + ] else ...[ + const Text('Press the button to load bindings'), + ], ], ), ), floatingActionButton: FloatingActionButton.extended( - onPressed: _showSignetNetwork, + onPressed: _loadBindingData, backgroundColor: Colors.orange, - icon: const Icon(Icons.network_check), - label: const Text('Get Signet Network'), + icon: const Icon(Icons.play_circle_fill), + label: const Text('Load Dart binding'), ), ); } diff --git a/bdk_demo/pubspec.yaml b/bdk_demo/pubspec.yaml index 476ccec..ad381e6 100644 --- a/bdk_demo/pubspec.yaml +++ b/bdk_demo/pubspec.yaml @@ -31,6 +31,9 @@ dependencies: flutter: sdk: flutter + bdk_dart: + path: ../ + # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 diff --git a/lib/README.md b/lib/README.md index f52b57c..9df1efd 100644 --- a/lib/README.md +++ b/lib/README.md @@ -6,3 +6,121 @@ source control. Run `scripts/generate_bindings.sh` to regenerate This placeholder file keeps the `lib/` directory present in the repository so that `dart` tooling can resolve the package structure. + +## Mobile build artifacts + +Mobile consumers (Flutter/Dart) can build platform-specific binaries using the +scripts in `scripts/`. + +### iOS (XCFramework) + +1. Generate the required static libraries: + ```bash + ./scripts/generate_bindings.sh --target ios + ``` +2. Package them into an XCFramework: + ```bash + ./scripts/build-ios-xcframework.sh + ``` + The framework is written to `ios/Release/bdkffi.xcframework/`. Keep it there to + reuse across multiple apps, or direct the output into a Flutter project with the + optional flag: + + ```bash + ./scripts/build-ios-xcframework.sh --output bdk_demo/ios/ios + ``` + +### Android (.so libraries) + +1. Ensure `ANDROID_NDK_ROOT` points to an Android NDK r26c (or compatible) installation. For example: + - macOS/Linux (adjust the NDK directory as needed): + ```bash + export ANDROID_NDK_ROOT="$HOME/Library/Android/sdk/ndk/29.0.14206865" + ``` + - Windows PowerShell: + ```powershell + $Env:ANDROID_NDK_ROOT = "C:\\Users\\\\AppData\\Local\\Android\\Sdk\\ndk\\29.0.14206865" + ``` + (Use `setx ANDROID_NDK_ROOT ` if you want the variable persisted for future shells.) +2. Build the shared objects: + ```bash + ./scripts/generate_bindings.sh --target android + ``` +3. Stage the artifacts for inclusion in a Flutter project (make script executable or run with `bash`): + ```bash + chmod +x scripts/build-android.sh + ./scripts/build-android.sh + # or + bash ./scripts/build-android.sh + ``` + + By default the script stages artifacts in `android/libs//libbdkffi.so` so the same + slices can be reused across multiple apps; direct them into the demo’s `jniLibs` + by passing `--output` as shown below. + + ```bash + chmod +x scripts/build-android.sh + ./scripts/build-android.sh --output bdk_demo/android/app/src/main/jniLibs + # or, without changing permissions + bash ./scripts/build-android.sh --output bdk_demo/android/app/src/main/jniLibs + +### Desktop tests (macOS/Linux) + +- Run the base script without a target flag before executing `dart test`: + ```bash + ./scripts/generate_bindings.sh + ``` + This regenerates the Dart bindings and drops the host dynamic library + (`libbdkffi.dylib` on macOS, `libbdkffi.so` on Linux) in the project root. + The generated loader expects those files when running in the VM, so tests fail if you only invoke the platform-specific targets. + +### Verification + +- On iOS, confirm the framework slices: + ```bash + lipo -info ios/Release/bdkffi.xcframework/ios-arm64/libbdkffi.a + lipo -info ios/Release/bdkffi.xcframework/ios-arm64_x86_64-simulator/libbdkffi.a + ``` +- On Android, verify the shared objects: + ```bash + find android/libs -name "libbdkffi.so" + ``` + +## Flutter demo (`bdk_demo/`) + +Once the native artifacts are staged you can run the sample Flutter app to confirm +the bindings load end-to-end. + +### Prerequisites + +1. Generate bindings and host libraries: + ```bash + ./scripts/generate_bindings.sh + ./scripts/generate_bindings.sh --target android + ``` +2. Stage Android shared objects into the app’s `jniLibs` (repeat per update): + ```bash + bash ./scripts/build-android.sh --output bdk_demo/android/app/src/main/jniLibs + ``` + +### Run the app + +```bash +cd bdk_demo +flutter pub get +flutter run +``` + +The Android variant uses a small MethodChannel to discover where the system +actually deploys `libbdkffi.so` (some devices nest ABI folders under +`nativeLibraryDir`). That path is fed back into Dart so the generated loader in +`lib/bdk.dart` can `dlopen` the correct slice. + +### What the demo verifies + +- The native dynamic library loads on-device via the generated FFI loader. +- A BIP84 descriptor string is constructed through the Dart bindings and displayed to the UI. +- The UI badge switches between success and error states, so you immediately see + if the bindings failed to load or threw during descriptor creation (delete the android artifacts and rerun the app to simulate a failure). + +Use this screen as a smoke test after rebuilding bindings or regenerating artifacts; if it turns green and shows `Network: testnet` the demo is exercising the FFI surface successfully. diff --git a/scripts/build-android.sh b/scripts/build-android.sh new file mode 100644 index 0000000..a359415 --- /dev/null +++ b/scripts/build-android.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +PROJECT_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) +ANDROID_BUILD_ROOT="$PROJECT_ROOT/build/android" +OUTPUT_ROOT="android/libs" + +usage() { + cat <] + +Copy the built Android shared libraries into . When is relative, +it is resolved from the repository root. Defaults to android/libs. +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --output|-o) + OUTPUT_ROOT="${2:-}" + if [[ -z "$OUTPUT_ROOT" ]]; then + echo "Error: --output requires a value" >&2 + exit 1 + fi + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ "$OUTPUT_ROOT" != /* ]]; then + OUTPUT_ROOT="$PROJECT_ROOT/$OUTPUT_ROOT" +fi + +if [[ -z "${ANDROID_NDK_ROOT:-}" ]]; then + echo "ANDROID_NDK_ROOT must be set" >&2 + exit 1 +fi + +if [[ ! -d "$ANDROID_BUILD_ROOT" ]]; then + echo "Android build artifacts not found in $ANDROID_BUILD_ROOT" >&2 + echo "Run scripts/generate_bindings.sh --target android first." >&2 + exit 1 +fi + +mkdir -p "$OUTPUT_ROOT" + +for ABI in arm64-v8a armeabi-v7a x86_64; do + ARTIFACT="$ANDROID_BUILD_ROOT/${ABI}/libbdkffi.so" + if [[ ! -f "$ARTIFACT" ]]; then + echo "Missing artifact for $ABI at $ARTIFACT" >&2 + exit 1 + fi + + DEST_DIR="$OUTPUT_ROOT/$ABI" + mkdir -p "$DEST_DIR" + cp "$ARTIFACT" "$DEST_DIR/" + echo "Copied $ABI artifact to $DEST_DIR" +done + +echo "Android libraries staged under $OUTPUT_ROOT" diff --git a/scripts/build-ios-xcframework.sh b/scripts/build-ios-xcframework.sh new file mode 100755 index 0000000..9ba4c23 --- /dev/null +++ b/scripts/build-ios-xcframework.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +PROJECT_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) +IOS_BUILD_ROOT="$PROJECT_ROOT/build/ios" +OUTPUT_ROOT="ios/Release" +XCFRAMEWORK_NAME="bdkffi.xcframework" + +DEVICE_LIB="$IOS_BUILD_ROOT/aarch64-apple-ios/libbdkffi.a" +SIM_ARM64_LIB="$IOS_BUILD_ROOT/aarch64-apple-ios-sim/libbdkffi.a" +SIM_X86_LIB="$IOS_BUILD_ROOT/x86_64-apple-ios/libbdkffi.a" +SIM_UNIVERSAL_DIR="$IOS_BUILD_ROOT/simulator-universal" +SIM_UNIVERSAL_LIB="$SIM_UNIVERSAL_DIR/libbdkffi.a" + +usage() { + cat <] + +Create an XCFramework from previously built static libraries and write it to +. When is relative, it is resolved from the repository root. +Defaults to ios/Release. +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --output|-o) + OUTPUT_ROOT="${2:-}" + if [[ -z "$OUTPUT_ROOT" ]]; then + echo "Error: --output requires a value" >&2 + exit 1 + fi + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ "$OUTPUT_ROOT" != /* ]]; then + OUTPUT_ROOT="$PROJECT_ROOT/$OUTPUT_ROOT" +fi + +if [[ ! -f "$DEVICE_LIB" ]]; then + echo "Missing device library: $DEVICE_LIB" >&2 + echo "Run scripts/generate_bindings.sh --target ios first." >&2 + exit 1 +fi + +if [[ ! -f "$SIM_ARM64_LIB" || ! -f "$SIM_X86_LIB" ]]; then + echo "Missing simulator libraries: $SIM_ARM64_LIB or $SIM_X86_LIB" >&2 + echo "Run scripts/generate_bindings.sh --target ios first." >&2 + exit 1 +fi + +if ! command -v xcodebuild >/dev/null 2>&1; then + echo "xcodebuild is required to create an XCFramework" >&2 + exit 1 +fi + +mkdir -p "$SIM_UNIVERSAL_DIR" + +echo "Combining simulator slices with lipo..." +lipo -create "$SIM_ARM64_LIB" "$SIM_X86_LIB" -output "$SIM_UNIVERSAL_LIB" + +mkdir -p "$OUTPUT_ROOT" +OUTPUT_PATH="$OUTPUT_ROOT/$XCFRAMEWORK_NAME" +rm -rf "$OUTPUT_PATH" + +echo "Creating XCFramework at $OUTPUT_PATH..." +xcodebuild -create-xcframework \ + -library "$DEVICE_LIB" \ + -library "$SIM_UNIVERSAL_LIB" \ + -output "$OUTPUT_PATH" + +echo "XCFramework created: $OUTPUT_PATH" diff --git a/scripts/generate_bindings.sh b/scripts/generate_bindings.sh index 13e94f5..a338234 100755 --- a/scripts/generate_bindings.sh +++ b/scripts/generate_bindings.sh @@ -1,8 +1,27 @@ #!/usr/bin/env bash set -euo pipefail +TARGET="desktop" +while [[ $# -gt 0 ]]; do + case "$1" in + --target) + TARGET="${2:-desktop}" + shift 2 + ;; + --help|-h) + echo "Usage: $0 [--target desktop|ios|android]" + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + OS=$(uname -s) -echo "Running on $OS" +ARCH=$(uname -m) +echo "Running on $OS ($ARCH)" dart --version dart pub get @@ -10,44 +29,154 @@ dart pub get mkdir -p lib rm -f lib/bdk.dart -# Install Rust targets if on macOS if [[ "$OS" == "Darwin" ]]; then LIBNAME=libbdkffi.dylib elif [[ "$OS" == "Linux" ]]; then LIBNAME=libbdkffi.so else - echo "Unsupported os: $OS" + echo "Unsupported os: $OS" >&2 exit 1 fi # Run from the specific crate inside the embedded submodule cd ./bdk-ffi/bdk-ffi/ -echo "Building bdk-ffi crate and generating Dart bindings..." -cargo build --profile dev -p bdk-ffi -# Generate Dart bindings using local uniffi-bindgen wrapper -(cd ../../ && cargo run --profile dev --bin uniffi-bindgen -- --language dart --library bdk-ffi/bdk-ffi/target/debug/$LIBNAME --out-dir lib/) +generate_bindings() { + echo "Building bdk-ffi crate and generating Dart bindings..." + cargo build --profile dev -p bdk-ffi + (cd ../../ && cargo run --profile dev --bin uniffi-bindgen -- --language dart --library bdk-ffi/bdk-ffi/target/debug/$LIBNAME --out-dir lib/) +} -if [[ "$OS" == "Darwin" ]]; then - echo "Generating native binaries..." - rustup target add aarch64-apple-darwin x86_64-apple-darwin - # This is a test script the actual release should not include the test utils feature - cargo build --profile dev -p bdk-ffi --target aarch64-apple-darwin & - cargo build --profile dev -p bdk-ffi --target x86_64-apple-darwin & - wait - - echo "Building macOS fat library" - lipo -create -output ../../$LIBNAME \ - target/aarch64-apple-darwin/debug/$LIBNAME \ - target/x86_64-apple-darwin/debug/$LIBNAME -else - echo "Generating native binaries..." - rustup target add x86_64-unknown-linux-gnu - # This is a test script the actual release should not include the test utils feature - cargo build --profile dev -p bdk-ffi --target x86_64-unknown-linux-gnu +build_ios() { + echo "Building iOS static libraries..." + rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim >/dev/null - echo "Copying bdk-ffi binary" - cp target/x86_64-unknown-linux-gnu/debug/$LIBNAME ../../$LIBNAME -fi + PROFILE="release-smaller" + OUT_ROOT="../../build/ios" + mkdir -p "$OUT_ROOT" + + for TARGET_TRIPLE in aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim; do + echo " -> $TARGET_TRIPLE" + cargo build --profile "$PROFILE" -p bdk-ffi --target "$TARGET_TRIPLE" + ARTIFACT="target/${TARGET_TRIPLE}/${PROFILE}/libbdkffi.a" + DEST_DIR="$OUT_ROOT/${TARGET_TRIPLE}" + mkdir -p "$DEST_DIR" + cp "$ARTIFACT" "$DEST_DIR/" + done +} + +build_android() { + if [[ -z "${ANDROID_NDK_ROOT:-}" ]]; then + echo "ANDROID_NDK_ROOT must be set to build Android artifacts" >&2 + exit 1 + fi + + echo "Building Android shared libraries..." + rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android >/dev/null + + API_LEVEL=24 + case "$OS" in + Darwin) + HOST_OS=darwin + ;; + Linux) + HOST_OS=linux + ;; + *) + echo "Unsupported host for Android builds: $OS" >&2 + exit 1 + ;; + esac + + case "$ARCH" in + x86_64) + HOST_ARCH=x86_64 + ;; + arm64|aarch64) + HOST_ARCH=arm64 + ;; + *) + echo "Unsupported architecture for Android builds: $ARCH" >&2 + exit 1 + ;; + esac + + TOOLCHAIN="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/${HOST_OS}-${HOST_ARCH}/bin" + if [[ ! -d "$TOOLCHAIN" ]]; then + echo "Unable to locate NDK toolchain at $TOOLCHAIN" >&2 + exit 1 + fi + + OUT_ROOT="../../build/android" + mkdir -p "$OUT_ROOT" + + for TARGET_TRIPLE in aarch64-linux-android armv7-linux-androideabi x86_64-linux-android; do + case "$TARGET_TRIPLE" in + aarch64-linux-android) + ABI="arm64-v8a" + CLANG="${TOOLCHAIN}/aarch64-linux-android${API_LEVEL}-clang" + TARGET_ENV_LOWER="aarch64_linux_android" + ;; + armv7-linux-androideabi) + ABI="armeabi-v7a" + CLANG="${TOOLCHAIN}/armv7a-linux-androideabi${API_LEVEL}-clang" + TARGET_ENV_LOWER="armv7_linux_androideabi" + ;; + x86_64-linux-android) + ABI="x86_64" + CLANG="${TOOLCHAIN}/x86_64-linux-android${API_LEVEL}-clang" + TARGET_ENV_LOWER="x86_64_linux_android" + ;; + esac + + TARGET_ENV=$(echo "$TARGET_TRIPLE" | tr '[:lower:]' '[:upper:]' | tr '-' '_') + + export CARGO_TARGET_${TARGET_ENV}_LINKER="$CLANG" + export CARGO_TARGET_${TARGET_ENV}_AR="${TOOLCHAIN}/llvm-ar" + + export CC_${TARGET_ENV_LOWER}="$CLANG" + export AR_${TARGET_ENV_LOWER}="${TOOLCHAIN}/llvm-ar" + + echo " -> $TARGET_TRIPLE ($ABI)" + cargo build --profile release-smaller -p bdk-ffi --target "$TARGET_TRIPLE" + ARTIFACT="target/${TARGET_TRIPLE}/release-smaller/libbdkffi.so" + DEST_DIR="$OUT_ROOT/$ABI" + mkdir -p "$DEST_DIR" + cp "$ARTIFACT" "$DEST_DIR/" + done +} + +case "$TARGET" in + ios) + generate_bindings + build_ios + ;; + android) + generate_bindings + build_android + ;; + desktop|*) + generate_bindings + if [[ "$OS" == "Darwin" ]]; then + echo "Generating native macOS binaries..." + rustup target add aarch64-apple-darwin x86_64-apple-darwin >/dev/null + cargo build --profile dev -p bdk-ffi --target aarch64-apple-darwin & + cargo build --profile dev -p bdk-ffi --target x86_64-apple-darwin & + wait + + echo "Building macOS fat library" + lipo -create -output ../../$LIBNAME \ + target/aarch64-apple-darwin/debug/$LIBNAME \ + target/x86_64-apple-darwin/debug/$LIBNAME + else + echo "Generating native Linux binaries..." + rustup target add x86_64-unknown-linux-gnu >/dev/null + cargo build --profile dev -p bdk-ffi --target x86_64-unknown-linux-gnu + + echo "Copying bdk-ffi binary" + cp target/x86_64-unknown-linux-gnu/debug/$LIBNAME ../../$LIBNAME + fi + ;; +esac echo "All done!"