1 group 'com.ondeinference.onde_inference'
2 version '1.0'
3
4 buildscript {
5 repositories {
6 google()
7 mavenCentral()
8 }
9 dependencies {
10 classpath 'com.android.tools.build:gradle:8.7.3'
11 }
12 }
13
14 rootProject.allprojects {
15 repositories {
16 google()
17 mavenCentral()
18 }
19 }
20
21 apply plugin: 'com.android.library'
22
23 // ── ABIs to cross-compile ────────────────────────────────────────────────────
24 // Override from the consuming app to shorten local iteration, e.g.
25 // flutter run -d <device> --android-project-arg=onde.androidAbis=arm64-v8a
26 def ondeAndroidAbis = (project.findProperty('onde.androidAbis') ?: 'arm64-v8a,armeabi-v7a,x86_64')
27 .split(',')
28 .collect { it.trim() }
29 .findAll { !it.isEmpty() }
30
31 // Lowest API level the Rust code is linked against. Must stay <= the consuming
32 // app's minSdk, and matches defaultConfig.minSdkVersion below.
33 def ondeAndroidApiLevel = 21
34
35 android {
36 compileSdkVersion 35
37 namespace 'com.ondeinference.onde_inference'
38
39 defaultConfig {
40 minSdkVersion ondeAndroidApiLevel
41 }
42
43 compileOptions {
44 sourceCompatibility JavaVersion.VERSION_17
45 targetCompatibility JavaVersion.VERSION_17
46 }
47
48 // cargo-ndk drops libonde_inference_dart.so into <abi>/ subdirectories here.
49 // AGP packages anything under a jniLibs source dir, which an IMPORTED CMake
50 // library would *not* have been.
51 sourceSets {
52 main {
53 jniLibs.srcDirs += ["${layout.buildDirectory.get().asFile}/rustJniLibs"]
54 }
55 }
56 }
57
58 // ── NDK location ─────────────────────────────────────────────────────────────
59 // `android.ndkDirectory` is deliberately not used: this is a library module
60 // with no ndkVersion of its own, so AGP resolves its own default version and
61 // throws "NDK is not installed" even when the app module has just installed a
62 // perfectly good NDK. Resolve it ourselves instead.
63 def resolveNdkDir = {
64 def fromEnv = System.getenv('ANDROID_NDK_HOME') ?: System.getenv('ANDROID_NDK_ROOT')
65 if (fromEnv && new File(fromEnv).isDirectory()) {
66 return new File(fromEnv)
67 }
68
69 def sdkRoot = System.getenv('ANDROID_HOME') ?: System.getenv('ANDROID_SDK_ROOT')
70 if (sdkRoot) {
71 def ndkRoot = new File(sdkRoot, 'ndk')
72 def installed = ndkRoot.isDirectory() ? ndkRoot.listFiles({ it.isDirectory() } as FileFilter) : null
73 if (installed) {
74 // Newest installed version wins. The Rust output is a standalone
75 // .so, so it need not match the NDK the Flutter engine was built
76 // with: any recent one links fine.
77 //
78 // Directory names are dotted versions like "26.1.10909125".
79 // Compare them segment by segment as integers rather than leaning
80 // on Groovy's implicit list ordering, so "9.x" cannot outrank
81 // "26.x" and a missing trailing segment counts as zero.
82 def versionOf = { File dir ->
83 dir.name.split('\\.').collect { it.isInteger() ? it.toInteger() : 0 }
84 }
85 def newest = installed.toList().max { a, b ->
86 def va = versionOf(a)
87 def vb = versionOf(b)
88 for (int i = 0; i < Math.max(va.size(), vb.size()); i++) {
89 def cmp = (va[i] ?: 0) <=> (vb[i] ?: 0)
90 if (cmp != 0) {
91 return cmp
92 }
93 }
94 return 0
95 }
96 if (newest) {
97 return newest
98 }
99 }
100 }
101
102 throw new GradleException(
103 'Could not locate an Android NDK. Install one via Android Studio\'s SDK ' +
104 'Manager, or set ANDROID_NDK_HOME to an NDK directory.')
105 }
106
107 // ── Rust cross-compilation ───────────────────────────────────────────────────
108 // Always built with --release: a debug-profile inference engine is too slow to
109 // be worth running, so there is no point wiring this per build type.
110 tasks.register('cargoBuildAndroid', Exec) {
111 description = "Cross-compiles the onde_inference_dart crate for ${ondeAndroidAbis.join(', ')}"
112 workingDir file('../rust')
113
114 def outDir = file("${layout.buildDirectory.get().asFile}/rustJniLibs")
115
116 // Gradle launched from Android Studio does not necessarily inherit the
117 // shell PATH, so fall back to the standard rustup install location.
118 def cargoBin = 'cargo'
119 def rustupCargo = new File("${System.getProperty('user.home')}/.cargo/bin/cargo")
120 if (rustupCargo.exists()) {
121 cargoBin = rustupCargo.absolutePath
122 }
123
124 def cargoArgs = [cargoBin, 'ndk']
125 ondeAndroidAbis.each { cargoArgs += ['-t', it] }
126 cargoArgs += ['-P', ondeAndroidApiLevel.toString(), '-o', outDir.absolutePath, 'build', '--release']
127 commandLine cargoArgs
128
129 doFirst {
130 outDir.mkdirs()
131 // cargo-ndk locates the toolchain via ANDROID_NDK_HOME.
132 environment 'ANDROID_NDK_HOME', resolveNdkDir().absolutePath
133 }
134 }
135
136 // preBuild on its own only orders the cargo run early in the build. What
137 // actually matters is that the .so files exist before AGP merges native libs,
138 // and those merge tasks are created late by the variant API, so match them
139 // after evaluation rather than assuming preBuild covers it.
140 afterEvaluate {
141 tasks.matching { it.name == 'preBuild' }.configureEach {
142 dependsOn 'cargoBuildAndroid'
143 }
144 tasks.matching { it.name ==~ /merge.*(NativeLibs|JniLibFolders)/ }.configureEach {
145 dependsOn 'cargoBuildAndroid'
146 }
147 }