Why this happens
When you build a Kotlin Multiplatform (KMP) app from Xcode, the iOS framework build runs the Kotlin/Native compiler through a Gradle task. If a dependency in commonMain fails to compile for the native target, Gradle surfaces it in Xcode as:
Failed to build cache for /Users/<you>/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.ui/ui-uikitx64/1.7.3/.../ui.klib.
The line that matters is further down in the log:
Details: Internal error in body lowering: java.lang.StackOverflowError: null
A StackOverflowError inside the Kotlin compiler’s body lowering phase means the compiler blew its stack while processing a .klib, not that your Mac ran out of memory. In this exact scenario it is triggered by a version mismatch in the Compose Multiplatform stack: the project pins composeMultiplatform = "1.7.3" (which ships ui-uikitx64 1.7.3) while using Kotlin 2.4.0 and the Compose compiler plugin from that same Kotlin line. The 1.7.3 UIKit .klib was not built for that compiler, and the body-lowering pass recurses until the stack overflows.
Running macOS inside Docker (the docurr/macos setup) makes this worse in two ways: the Gradle cache under ~/.gradle/caches can hold stale .klib artifacts from earlier attempts, and the container’s default stack size for spawned compiler processes is smaller than a native macOS shell. So the same project may build on a colleague’s real Mac and fail in your container.
How to fix it
1. Move Compose UI libraries out of commonMain
The accepted fix is to stop compiling the heavy Compose UI libraries for the iOS target and keep only the Compose runtime in commonMain. In shared/build.gradle.kts, change the sourceSets block:
sourceSets {
androidMain.dependencies {
implementation(libs.compose.uiToolingPreview)
implementation(libs.compose.foundation)
implementation(libs.compose.material3)
implementation(libs.compose.ui)
implementation(libs.compose.components.resources)
implementation(libs.compose.webview.multiplatform)
implementation(libs.androidx.lifecycle.viewmodelCompose)
implementation(libs.androidx.lifecycle.runtimeCompose)
}
commonMain.dependencies {
implementation(libs.compose.runtime)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
}
Then split the UI code itself. Empty out shared/src/commonMain/kotlin/app/mercury/App.kt and add an Android-only composable in shared/src/androidMain/kotlin/app/mercury/AndroidApp.kt:
package app.mercury
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.multiplatform.webview.web.WebView
import com.multiplatform.webview.web.rememberWebViewState
@Composable
fun AndroidApp() {
MaterialTheme {
val webViewState = rememberWebViewState("https://your-site.example/")
WebView(
state = webViewState,
modifier = Modifier.fillMaxSize()
)
}
}
Point MainActivity at AndroidApp() and implement the iOS side in iosApp/iosApp/ContentView.swift with SwiftUI’s WebView instead of the Compose one. The iOS target now only needs compose-runtime, which compiles cleanly for iosArm64/iosSimulatorArm64.
2. Align the Compose Multiplatform version with your Kotlin version
If you want to keep shared Compose UI (the normal KMP setup), the real root cause is the version pair. You had composeMultiplatform = "1.7.3" commented out next to composeMultiplatform = "1.11.1" in your version catalog — use the newer one:
[versions]
composeMultiplatform = "1.11.1"
kotlin = "2.4.0"
Compose Multiplatform 1.11.x is built against the Kotlin 2.x compiler line and its ui-uikitx64 .klib no longer trips the body-lowering crash. After changing the version:
- Stop the Gradle daemon:
./gradlew --stop. - Clear the stale artifact so Gradle re-fetches the correct
.klib:
bash
rm -rf ~/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.ui - Rebuild the iOS framework from the command line first, so you see the real Gradle error instead of Xcode’s condensed one:
bash
./gradlew :shared:linkReleaseFrameworkIosSimulatorArm64 - Only then build from Xcode.
3. Give the Kotlin/Native compiler more stack in the container
If you must keep the Docker-based macOS environment, raise the stack size for Gradle-spawned processes in gradle.properties:
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -Xss4m
kotlin.native.daemon.jvmargs=-Xss8m
This does not fix a version mismatch, but it stops the compiler from overflowing on legitimate deep recursion in large shared composables. Combine it with fix 2, not instead of it.
How to prevent it
- Keep
composeMultiplatformandkotlinversions on compatible lines; check the Compose Multiplatform release notes before bumping either. - Pin one version in
libs.versions.tomland delete commented-out alternates so a stale pin can’t sneak back in. - Run
./gradlew :shared:linkDebugFrameworkIosSimulatorArm64from a terminal before building in Xcode — Gradle’s error output is far more readable than Xcode’s build log. - Wipe
~/.gradle/caches/modules-2after any Compose Multiplatform version change. - If you share UI across platforms, keep
commonMaindependencies minimal and platform-specific ones inandroidMain/iosMain.
FAQ
Why does the error mention a path under ~/.gradle/caches?
That is where Gradle stores downloaded .klib artifacts. The message names the file the Kotlin/Native compiler choked on, not the location of the bug — the bug is the version mismatch between that artifact and your compiler.
Is this caused by running macOS in Docker?
No, but the container makes it more likely to surface. A smaller default stack and a shared Gradle cache mean the same bad version pair that might limp through on a native Mac fails hard in the container.
Can I keep one shared Compose UI for both platforms?
Yes — that is the point of Compose Multiplatform. Use a Compose Multiplatform release that matches your Kotlin version (1.11.x with Kotlin 2.4.0), clear the module cache, and rebuild. Only fall back to per-platform UIs if you are stuck on an old Compose version.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
