Slash KMP Build Times
Kotlin Multiplatform: Build Only What You Need for iOS in Kotlin Multiplatform
Stop building unnecessary iOS architectures in Kotlin Multiplatform and drastically speed up your development workflow.
Introduction
Kotlin Multiplatform (KMP) offers fantastic code-sharing capabilities between Android and iOS. However, iOS build times can often be significantly longer than their Android counterparts. A common reason for this, and one that’s surprisingly easy to fix, is building more iOS architectures than you actually need. This article, inspired by Touchlab’s excellent blog post Beware of Build Time Bloat, will show you how to identify this issue and create a custom Gradle task to target only the specific iOS architecture you require, dramatically speeding up your development workflow.
The Problem: Building for Every Architecture
When building for Android, you typically deal with debug
debug and release
variants. iOS development introduces another dimension: architectures
. You’ll commonly encounter:
- Intel simulator (x86_64): For older Intel-based Macs.
- Arm simulator (arm64): For newer Macs with Apple silicon (M1, M2, M3 chips).
- Device Arm (arm64): For physical iOS devices.
The Kotlin compiler treats each of these as entirely separate builds. Building all three takes roughly three times as long as building just one. The problem is that, in most development scenarios, you only need one.
- Local Simulator Testing: You need either the Intel or Arm simulator architecture, depending on your Mac.
- CI Testing: You typically run tests on a single simulator architecture.
- App Store Releases: You’ll eventually need the device architecture, but even then, often just one build at a time.
Running ./gradlew build
(or similar broad tasks) in a typical KMP project often builds all iOS architectures and variants (debug and release), most of which are unnecessary for your immediate task. This leads to significant wasted build time.
Identifying Unnecessary Builds
The first step is to confirm whether you’re building more than you need. Run your usual Gradle build command with the -i (info) flag for more verbose logging:
./gradlew -i build
Look for a section in the output that says “Tasks to be executed:”. Within that list, search for tasks starting with link…Framework.
You’ll likely see tasks like:
linkDebugFrameworkIosArm64
linkDebugFrameworkIosSimulatorArm64
linkDebugFrameworkIosX64
linkReleaseFrameworkIosArm64
linkReleaseFrameworkIosSimulatorArm64
linkReleaseFrameworkIosX64
If you’re developing on an Apple silicon Mac and only need to run on the simulator, you only need linkDebugFrameworkIosSimulatorArm64
(or the linkRelease… variant if you’re doing a release build). All the others are wasted effort.
The Solution: A Custom Gradle Task
The best solution is to create a custom Gradle task that targets only the specific compilation task you need. Let’s say you have a multi-module KMP project with modules named module1, module2, module3, module4, and module5, and you want to build only the compileKotlinIosSimulatorArm64
task for these modules (the most common scenario for local development on an Apple silicon Mac).
Add the following to your root build.gradle.kts
(or build.gradle) file:
tasks.register("compileKotlinIosSimulatorArm64ForModules") {
description = "Compiles Kotlin code for iOS Simulator (Arm64) for specified modules."
group = "Build"
val modules = listOf("module1", "module2", "module3", "module4", "module5")
modules.forEach { moduleName ->
dependsOn(":$moduleName:compileKotlinIosSimulatorArm64")
}
}
Explanation:
- tasks.register(…): This registers a new Gradle task named
compileKotlinIosSimulatorArm64ForModules.
- description and group: These properties make the task easier to find and understand when listing available Gradle tasks (./gradlew tasks).
- modules list: This list defines the specific modules you want to target. Easily add or remove modules as needed.
- dependsOn(…): This is the core of the solution. It tells Gradle that our custom task depends on the compileKotlinIosSimulatorArm64 task of each specified module. The :moduleName: syntax is how you reference tasks in other modules.
- forEach loop: The loop iterates through the modules list, adding the dependency for each module.
How to Use
- Add the code: Paste the code snippet into your root build.gradle.kts file.
- Run the task:
./gradlew compileKotlinIosSimulatorArm64ForModules
This command will now only execute the compileKotlinIosSimulatorArm64 task for the specified modules. It will skip all other iOS architectures and variants, and it will also skip Android tasks, resulting in significantly faster build times.
Adapting for Other Scenarios
You can easily adapt this task for other situations:
- Intel Simulator: Change compileKotlinIosSimulatorArm64 to compileKotlinIosX64.
- Physical Device: You’ll eventually need to build for the device architecture (compileKotlinIosArm64), but you can create a separate task for that.
- CI: Create tasks specific to your CI environment (e.g., compileKotlinIosX64ForCI if your CI runs on Intel machines).
- Different Modules: Modify the modules list.
- link tasks: you can create tasks that depend on the link… tasks directly, if you really need to link. But, it’s generally better to let Xcode handle the linking during its build phase. The compilation tasks are the most time-consuming.
Conclusion
By being specific about which iOS architectures you build, you can drastically reduce build times in your Kotlin Multiplatform projects. Creating custom Gradle tasks, as demonstrated in this article, is a simple yet powerful way to achieve this optimization. This will lead to a more efficient development workflow, faster iteration cycles, and happier developers! Don’t build everything; build only what you need.
Did you find this article helpful? Let me know on Medium by giving it a clap! 👏 The more claps, the more I know to create content you love.
If you have any queries related to Android or KMP, I’m always happy to help you. You can reach me on LinkedIn.
Happy Learning🚀 Happy Coding📱