This repository has no description
1import com.vanniktech.maven.publish.SonatypeHost
2import org.jetbrains.compose.ExperimentalComposeLibrary
3import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree
4
5mavenPublishing {
6 publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
7 coordinates("com.performancecoachlab.posedetection", "posedetection-compose", "4.15.0")
8
9 pom {
10 name.set("Pose Detection")
11 description.set("real time body tracking for compose multiplatform mobile apps")
12 inceptionYear.set("2025")
13 url.set("https://tangled.sh/@nateholland.bsky.social/PoseDetection")
14 licenses {
15 license {
16 name.set("The Apache License, Version 2.0")
17 url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
18 distribution.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
19 }
20 }
21 developers {
22 developer {
23 id.set("nateholland")
24 name.set("Nate")
25 url.set("https://tangled.sh/@nateholland.bsky.social")
26 }
27 }
28 scm {
29 url.set("https://tangled.sh/@nateholland.bsky.social/PoseDetection")
30 connection.set("scm:git:git://tangled.sh/@nateholland.bsky.social/PoseDetection.git")
31 developerConnection.set("scm:git:ssh://git@tangled.sh:nateholland.bsky.social/PoseDetection")
32 }
33 }
34 // Only sign when explicitly enabled (CI / Maven Central). Local
35 // `publishToMavenLocal` runs for consumers like the kima app and
36 // doesn't need GPG signatures.
37 if (project.findProperty("signingEnabled") == "true") {
38 signAllPublications()
39 }
40}
41plugins {
42 alias(libs.plugins.multiplatform)
43 alias(libs.plugins.compose.compiler)
44 alias(libs.plugins.compose)
45 alias(libs.plugins.android.library)
46 id("com.vanniktech.maven.publish") version "0.31.0"
47}
48
49kotlin {
50 jvmToolchain(11)
51 androidTarget {
52 //https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html
53 instrumentedTestVariant.sourceSetTree.set(KotlinSourceSetTree.test)
54 }
55
56 listOf(
57 iosX64(),
58 iosArm64(),
59 iosSimulatorArm64()
60 ).forEach { target ->
61 target.binaries.framework {
62 baseName = "ComposeApp"
63 isStatic = true
64 }
65 // MLKit pose detection is embedded via cinterop `staticLibraries` for
66 // iosArm64 only — the library's klib ships the MLKit binaries so
67 // downstream consumers get MLKit without running cocoapods themselves.
68 // Sim targets fall back to Apple Vision (upstream MLKit has no
69 // arm64-simulator slice).
70 if (target.name == "iosArm64") {
71 target.compilations.getByName("main").cinterops.create("mlkitAccurate") {
72 defFile(layout.buildDirectory.file("mlkit-archives/mlkitAccurate.def").get().asFile)
73 packageName("cocoapods.MLKitPoseDetectionAccurate")
74 }
75 }
76 }
77
78 sourceSets {
79 commonMain.dependencies {
80 implementation(compose.runtime)
81 implementation(compose.foundation)
82 implementation(compose.material3)
83 implementation(compose.components.resources)
84 implementation(compose.components.uiToolingPreview)
85 api("co.touchlab:kermit:2.0.4")
86 }
87
88 commonTest.dependencies {
89 implementation(kotlin("test"))
90 @OptIn(ExperimentalComposeLibrary::class)
91 implementation(compose.uiTest)
92 }
93
94 androidMain.dependencies {
95 implementation(compose.uiTooling)
96 implementation(libs.androidx.activityCompose)
97 implementation(libs.androidx.camera.core)
98 implementation(libs.androidx.camera.camera2)
99 implementation(libs.androidx.camera.lifecycle)
100 implementation(libs.androidx.camera.video)
101 implementation(libs.androidx.camera.view)
102 implementation(libs.androidx.camera.extensions)
103 implementation(libs.androidx.camera.compose)
104 implementation(libs.pose.detection)
105 implementation(libs.pose.detection.common)
106 implementation(libs.androidx.media3.common.ktx)
107 implementation(libs.litert)
108 implementation(libs.litert.support)
109 implementation(libs.litert.metadata)
110 implementation(libs.litert.gpu)
111 }
112
113 }
114}
115
116android {
117 namespace = "com.performancecoachlab.posedetection"
118 compileSdk = 36
119 defaultConfig {
120 minSdk = 21
121 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
122 }
123 buildFeatures {
124 mlModelBinding = true
125 }
126}
127
128//https://developer.android.com/develop/ui/compose/testing#setup
129dependencies {
130 androidTestImplementation(libs.androidx.uitest.junit4)
131 debugImplementation(libs.androidx.uitest.testManifest)
132}
133
134// ============================================================================
135// MLKit iOS integration
136// ----------------------------------------------------------------------------
137// The library's iosArm64 klib ships MLKit statically-embedded via cinterop
138// `staticLibraries`. Downstream consumers get MLKit symbols without needing
139// cocoapods — standard Maven/Gradle KMP dependency resolution is enough.
140//
141// The `syncMlkitBinaries` task runs tools/sync-mlkit.sh, which fetches MLKit
142// pods into build/mlkit-staging and extracts per-target static archives into
143// build/mlkit-archives. `generateMlkitDefFile` then writes a cinterop .def
144// referencing those archives with absolute paths resolved at configuration
145// time. `cinteropMlkitAccurateIosArm64` is wired to depend on both.
146// ============================================================================
147
148val mlkitStagingDir = layout.buildDirectory.dir("mlkit-staging")
149val mlkitArchivesDir = layout.buildDirectory.dir("mlkit-archives")
150val mlkitDefFile = layout.buildDirectory.file("mlkit-archives/mlkitAccurate.def")
151val mlkitRedirectSrc =
152 project.file("src/nativeInterop/mlkitRedirect/MLKitResourceRedirect.m")
153val mlkitRedirectDir = layout.buildDirectory.dir("mlkit-redirect")
154val mlkitRedirectLib =
155 layout.buildDirectory.file("mlkit-redirect/ios_arm64/libMLKitRedirect.a")
156val mlkitComposeResDir = layout.buildDirectory.dir("mlkit-compose-resources")
157
158// Compile our NSBundle swizzle for iosArm64 and archive into libMLKitRedirect.a.
159// The cinterop .def includes this static library alongside MLKit's, so the
160// swizzle gets linked into the consumer's KMP framework automatically.
161val compileMlkitRedirect = tasks.register<Exec>("compileMlkitRedirect") {
162 group = "mlkit"
163 description = "Compile NSBundle MLKit redirect swizzle to static archive."
164 inputs.file(mlkitRedirectSrc)
165 outputs.file(mlkitRedirectLib)
166 val out = mlkitRedirectLib.get().asFile
167 val obj = out.resolveSibling("MLKitResourceRedirect.o")
168 out.parentFile.mkdirs()
169 commandLine(
170 "sh", "-c",
171 "xcrun --sdk iphoneos clang " +
172 "-c -fobjc-arc -fmodules " +
173 "-arch arm64 -mios-version-min=16.2 " +
174 "${mlkitRedirectSrc.absolutePath} -o ${obj.absolutePath} && " +
175 "xcrun ar rcs ${out.absolutePath} ${obj.absolutePath}"
176 )
177}
178
179// Copy MLKit resource bundles (.tflite / .binarypb) from the synced pods into
180// the library's iosMain Compose Multiplatform resources, so they're packaged
181// into the consumer app bundle automatically. At runtime, Kotlin extracts them
182// to NSCachesDirectory and the NSBundle swizzle redirects MLKit's lookups.
183val stageMlkitResources = tasks.register<Copy>("stageMlkitResources") {
184 group = "mlkit"
185 description = "Stage MLKit resource bundles as Compose MP resources."
186 dependsOn("syncMlkitBinaries")
187 val pods = mlkitStagingDir.get().asFile.resolve("Pods")
188 from(pods.resolve("MLKitPoseDetectionAccurate/Resources/MLKitPoseDetectionAccurateResources")) {
189 into("files/mlkit/MLKitPoseDetectionAccurateResources")
190 }
191 from(pods.resolve("MLKitPoseDetectionCommon/Resources/MLKitPoseDetectionCommonResources")) {
192 into("files/mlkit/MLKitPoseDetectionCommonResources")
193 }
194 from(pods.resolve("MLKitXenoCommon/Frameworks/MLKitXenoCommon.framework/MLKitXenoResources.bundle")) {
195 into("files/mlkit/MLKitXenoResources")
196 }
197 into(mlkitComposeResDir)
198}
199
200// Wire the staged MLKit resources into the Compose Multiplatform resource set
201// for the ios source tree. Compose packages these into the consumer's app
202// bundle at `compose-resources/<pkg>.generated.resources/files/mlkit/...`.
203compose.resources {
204 customDirectory(
205 sourceSetName = "iosMain",
206 directoryProvider = mlkitComposeResDir,
207 )
208}
209
210// Compose's `generateResourceAccessors*` tasks read from the resource dir, so
211// make them wait for the stage task (which populates the dir).
212tasks.matching { it.name.startsWith("generateResourceAccessors") }
213 .configureEach { dependsOn(stageMlkitResources) }
214tasks.matching { it.name.startsWith("generateExpectResourceCollectors") }
215 .configureEach { dependsOn(stageMlkitResources) }
216tasks.matching { it.name.startsWith("convertXmlValueResources") }
217 .configureEach { dependsOn(stageMlkitResources) }
218tasks.matching { it.name.startsWith("copyNonXmlValueResources") }
219 .configureEach { dependsOn(stageMlkitResources) }
220tasks.matching { it.name.startsWith("prepareComposeResources") }
221 .configureEach { dependsOn(stageMlkitResources) }
222
223val syncMlkitBinaries = tasks.register<Exec>("syncMlkitBinaries") {
224 group = "mlkit"
225 description = "Fetch MLKit pods + extract static archives for each Kotlin iOS target."
226 inputs.file("tools/sync-mlkit.sh")
227 outputs.dir(mlkitArchivesDir)
228 executable = project.file("tools/sync-mlkit.sh").absolutePath
229 environment("MLKIT_STAGING_DIR", mlkitStagingDir.get().asFile.absolutePath)
230 environment("MLKIT_ARCHIVES_DIR", mlkitArchivesDir.get().asFile.absolutePath)
231 // Only iosArm64 (iPhone device) is supported upstream; sim targets fall
232 // back to Vision via the MlKitPose expect/actual stub.
233 environment("MLKIT_TARGETS", "ios_arm64")
234}
235
236val generateMlkitDefFile = tasks.register("generateMlkitDefFile") {
237 group = "mlkit"
238 description = "Generate the MLKit cinterop .def with absolute archive paths."
239 dependsOn(syncMlkitBinaries, compileMlkitRedirect)
240 outputs.file(mlkitDefFile)
241 // Capture as locals so the closure doesn't hold project-level refs —
242 // configuration-cache-safe.
243 val pods = mlkitStagingDir.get().asFile.resolve("Pods").absolutePath
244 val archives = mlkitArchivesDir.get().asFile.absolutePath
245 val redirectArchives = mlkitRedirectDir.get().asFile.absolutePath
246 val defFile = mlkitDefFile.get().asFile
247 doLast {
248 val defContent = buildString {
249 appendLine("language = Objective-C")
250 appendLine("modules = MLKitPoseDetectionAccurate MLKitPoseDetectionCommon MLKitVision")
251 appendLine("package = cocoapods.MLKitPoseDetectionAccurate")
252 appendLine(
253 "compilerOpts = -fmodules " +
254 "-F$pods/MLKitPoseDetectionAccurate/Frameworks " +
255 "-F$pods/MLKitPoseDetectionCommon/Frameworks " +
256 "-F$pods/MLKitVision/Frameworks " +
257 "-F$pods/MLKitCommon/Frameworks " +
258 "-F$pods/MLImage/Frameworks " +
259 "-F$pods/MLKitXenoCommon/Frameworks"
260 )
261 appendLine(
262 "staticLibraries = " +
263 "libMLKitPoseDetectionAccurate.a libMLKitPoseDetectionCommon.a " +
264 "libMLKitVision.a libMLKitCommon.a libMLImage.a libMLKitXenoCommon.a " +
265 "libGTMSessionFetcher.a libGoogleDataTransport.a libGoogleToolboxForMac.a " +
266 "libGoogleUtilities.a libFBLPromises.a libnanopb.a libMLKitRedirect.a"
267 )
268 appendLine("libraryPaths.ios_arm64 = $archives/ios_arm64 $redirectArchives/ios_arm64")
269 appendLine(
270 "linkerOpts = -ObjC -lc++ -lsqlite3 -lz " +
271 "-framework Accelerate -framework CoreML"
272 )
273 // Expose the swizzle control function to Kotlin.
274 appendLine("---")
275 appendLine("void mlkit_set_resource_dir(const char *path);")
276 }
277 defFile.writeText(defContent)
278 }
279}
280
281tasks.matching { it.name.startsWith("cinteropMlkitAccurate") }.configureEach {
282 dependsOn(generateMlkitDefFile)
283 // Cinterop's up-to-date check only watches its .def file — a content
284 // change in the static archives (e.g. libMLKitRedirect.a after an .m
285 // edit) won't invalidate the produced klib. Declare the archive dirs
286 // as explicit inputs so any rebuilt .a forces cinterop + downstream
287 // klib + publish to re-run.
288 inputs.dir(mlkitArchivesDir)
289 inputs.dir(mlkitRedirectDir)
290}