· 7 min read Posted by Kevin Schildhorn
Beware of Build Time Bloat
Touchlab has worked with many teams trying to optimize their KMP build pipeline. One of the most common issues is easy to spot, easy to fix, and can have a huge impact on your build times. If your iOS build times are really slow, you may be building too many architectures.
KMP Config is a special skill
Kotlin Multiplatform is a powerful tool for sharing code across various platforms. While that has the potential to deliver massive efficiencies when building apps for both Android and iOS, few developers have a lot of experience with building apps for both platforms. Even fewer are experts with their various toolchains, and how KMP interacts with them.
Generally speaking, the folks setting up Gradle for KMP builds, at least initially, have Android experience. They usually aren’t familiar with Xcode build pipeline details. On the iOS side, those developers are generally unfamiliar with Gradle and with how KMP works.
KMP iOS builds are slow
The Kotlin compiler has been notably slow when building for iOS. Early on, the major focus was on correctness and stability, and less on build performance. Now that KMP is stable, there is considerable effort from JetBrains on optimizing the build toolchain. We will see significant improvement over the next few releases.
Still, though, it can be long. Often much longer than the equivalent Android build. That is unavoidable, as you do need to build for iOS to use KMP in iOS. However, you may be making this build process much worse than it needs to be.
The problem
What’s the problem? It’s simple. When building for Android or the JVM, your build choices are debug or release. Most Android developers are well aware of how to build the type that is needed at the time. For iOS builds, KMP or not, you need to build for the target architecture that you’ll be running on, as well as deciding on debug or release variants.
When building iOS apps, there are 3 main architectures you will likely encounter:
- Intel simulator (for Intel macs)
- Arm simulator (for newer macs with M[1-3] arm processors)
- The phone’s Arm architecture (this runs on real devices)
These are all entirely separate builds as far as the compiler is concerned. Building all three will take (roughly) three times as long as building one. In most situations, you only need one.
Are you running a build to test on your local simulator? You only need one. Are you running tests in CI? Again, you need one. Are you publishing an app build? Surprise. You need one.
Being specific with Gradle
Most iOS KMP config includes at least two architectures: a simulator to test with, and the phone architecture itself. The vast majority includes both simulators and the phone architectures.
For each framework output, the KMP Gradle plugin defines a task to build that specific architecture.
linkDebugFrameworkIosArm64 - Links a framework 'debugFramework' for a target 'iosArm64'.
linkDebugFrameworkIosSimulatorArm64 - Links a framework 'debugFramework' for a target 'iosSimulatorArm64'.
linkDebugFrameworkIosX64 - Links a framework 'debugFramework' for a target 'iosX64'.
linkReleaseFrameworkIosArm64 - Links a framework 'releaseFramework' for a target 'iosArm64'.
linkReleaseFrameworkIosSimulatorArm64 - Links a framework 'releaseFramework' for a target 'iosSimulatorArm64'.
linkReleaseFrameworkIosX64 - Links a framework 'releaseFramework' for a target 'iosX64'.
link___
If you want to run your local simulator to test a build, assuming you have an Apple ARM chip laptop, you only need linkDebugFrameworkIosSimulatorArm64
. To run tests for iOS on your machine, run iosSimulatorArm64Test
.
However, if you run something like ./gradlew build
locally or on CI, Gradle will essentially run “everything”. For Xcode frameworks, that means all three architectures, and both debug and release variants. In most cases, that means you’re building 5 more architectures than you need. And, worse still, release builds take longer than debug builds.
Custom Gradle Tasks
If you still want to run multiple tasks but don’t want to have to manually enter each one, consider creating a new Gradle task to do it automatically.
Let’s say we have a Multiplatform project and want to only build the Android code. We can register a task to only run the Android tasks we want so that we only run what we want to run.
Below we’re creating a task that cleans and builds only the debug Android code. This will help by not running tasks related to testing, other platforms, and release builds.
tasks.register("androidDebugBuild") {
description = "Assembles the debug variants of the shared and Android modules."
group = "Build"
dependsOn(":shared:assembleDebug", ":android:assembleDebug")
}
tasks.register("androidRebuild") {
description = "Cleans and builds the debug variants of the shared and Android modules."
group = "Build"
dependsOn(":shared:clean", ":shared:assembleDebug", ":android:clean", ":android:assembleDebug")
}
Look at logs
To see if you’re building extra architectures, run your Gradle task with info logging turned on (the -i
flag). In the log, look for “Tasks to be executed:“. In that list, look for tasks starting with link___
. When I run ./gradlew -i build
on my local sample, I’ll see a bunch of tasks I don’t want:
task ':allshared:linkDebugFrameworkIosArm64' ...
task ':allshared:linkDebugFrameworkIosX64' ...
task ':allshared:linkReleaseFrameworkIosArm64' ...
etc
The only one I do want, :allshared:linkDebugFrameworkIosSimulatorArm64
, is also there, but so are a bunch of other ones.
SPM and XCFrameworks
A special note about SPM and XCFramework builds. Most iOS teams are moving to SPM for their dependency integration. As a result, many teams are setting up SPM integration with KMP. We’ll have another post soon about why you probably don’t want, or need, to do this. If you are though, here’s some specific notes:
The KMP Gradle plugin includes a way to configure the output to be XCFrameworks rather than old-style regular “Xcode Frameworks”. It’s not magic. It basically just runs a tool provided by the Xcode toolchain to do this.
However, from Gradle, there aren’t XCFramework tasks specific to the architecture you need. In the tasks listing you’ll see assembleXCFramework
. While you do need to call that to build an XCFramework, it’ll create all six architectures (three architectures, debug and release for each).
assemble[module name]DebugXCFramework
. So, if your module was called allshared
, the task would be assembleAllsharedDebugXCFramework
. While still too much work, it’ll be three architectures instead of sixAs a bit of a hack, if you use KMMBridge to configure your SPM builds, you can use our local dev flow to limit your build to a single architecture. See SPM Local Dev Flow for details.
However, if your are building KMP frameworks directly from source (and you generally should be), use direct linking instead of SPM. Xcode will automatically build the correct architecture when asked. For those somewhat familiar with KMP, that is the embedAndSignAppleFrameworkForXcode
task that you put into an Xcode run script.
CI
For CI, rather than running a global Gradle task to build everything, be specific. Depending on the mac architecture you’re running CI on, run either iosSimulatorArm64Test
or iosX64Test
. Generally, the test tasks are smart enought to only build what’s needed, but avoid running things like build
.
What’s next?
As mentioned in the intro, this is usually the first thing Touchlab looks for when reviewing KMP build configurations. There’s a whole lot more that can be done. If you’re interested in investing in your KMP success, check out our DevEx Services.
Enhance your team’s experience and streamline KMP development with our DevEx Services.