android-studiogradle-kotlin-dslkotlin-multiplatform

How to speed up iOS app builds of Kotlin Multiplatform from Android Studio


I have a KMP project which contains an iOS app, Android app and a shared KMP module. When I try to build the iOS app from the Android Studio (in order to be able to debug it) the build succeed. ☺️

The problem is that the build always (when you change a code) take around 12 minutes 😅⏰. Which is way to long when you are debugging and often testing if given code works. The 90% percent of time spent on build is used to run these tasks:

> Task :kmpcorelib:linkDebugFrameworkIosArm64
> Task :kmpcorelib:linkDebugFrameworkIosX64
> Task :kmpcorelib:linkKMPCoreDebugFrameworkIosArm64
> Task :kmpcorelib:linkKMPCoreDebugFrameworkIosX64
> Task :kmpcorelib:linkKMPCoreReleaseFrameworkIosArm64
> Task :kmpcorelib:linkKMPCoreReleaseFrameworkIosX64
> Task :kmpcorelib:linkReleaseFrameworkIosArm64
> Task :kmpcorelib:linkReleaseFrameworkIosX64

My question is: Is there a way how to run the app on iOS simulator without running all these task?

I dont know why the studio needs to link the RELEASE version of the framework.

Also I dont understand why it has to run the iOSArm64 variants when its running the on the X64 simulator.

Whats the difference between running linkDebugFrameworkIosArm64 and linkKMPCoreDebugFrameworkIosArm64 isnt it duplication?

PS: I run the iOS buld via clicking the default configuration in Android studio, not some custom gradle script.

This is how the KMP build.gradle looks like:

plugins {
    id("com.android.library")
    kotlin("multiplatform")
    kotlin("native.cocoapods")
    kotlin("plugin.serialization") version "1.5.0"
    id("com.prof18.kmp.fatframework.cocoa") version "0.0.1"
    id("io.gitlab.arturbosch.detekt")
    id("maven-publish")
}
version = "1.3.11"
group = "com.betsys.kmpcorelib"
val podName = "KMPCore"
detekt {
    autoCorrect = true
    config = files("$rootDir/config/detekt.yml")
    baseline = file("$rootDir/config/baseline.xml")
    input = files("src/commonMain/kotlin")
    reports {
        html.enabled = true
        xml.enabled = true
        txt.enabled = true
    }
}

// If you want to build iOS app from Android Studio - you have to put this config to comment - it somehow breaks the iOS build
fatFrameworkCocoaConfig {
    fatFrameworkName = podName
    outputPath = "$rootDir/../cocoapods"
    versionName = "1.16.1"
}
val ktorVersion = "1.5.4"
val napierVersion = "1.4.1"
val koinVersion = "3.0.1"
val kotlinxVersion = "1.4.1"
val kotlinxDatetime = "0.1.1"
kotlin {
    // We cant use simple ios() target now, since it doesnt support Arm32 architecture
    // For that reason we have specify each target explicitly
    // Same for folders with iOS code. We havet to use duplicit iosX64Main etc. instead of one iosMain folder
    ios("ios") {
        binaries.framework(podName)
    }
    android {
        publishLibraryVariants("debug")
        publishLibraryVariantsGroupedByFlavor = true
    }
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-core:$ktorVersion")
                implementation("io.ktor:ktor-client-logging:$ktorVersion")
                implementation("io.ktor:ktor-client-mock:$ktorVersion")
                implementation("io.ktor:ktor-client-serialization:$ktorVersion")
                implementation("io.ktor:ktor-client-websockets:$ktorVersion")
                api("io.insert-koin:koin-core:$koinVersion")
                api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxVersion-native-mt")
                api("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDatetime")
                api("com.ionspin.kotlin:bignum:0.3.1-SNAPSHOT")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.0")
            }
        }
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test-common"))
                implementation(kotlin("test-annotations-common"))
                implementation( "io.mockk:mockk-common:1.11.0")
                implementation("com.ionspin.kotlin:bignum:0.3.1-SNAPSHOT")
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-android:$ktorVersion")
                implementation("com.google.android.material:material:1.3.0")
                implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinxVersion")
                implementation("com.jakewharton.timber:timber:4.7.1")
            }
        }
        val androidTest by getting {
            dependencies {
                implementation(kotlin("test-junit"))
                implementation("junit:junit:4.13.2")
                implementation("io.mockk:mockk:1.11.0")
            }
        }
        val iosMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-ios:$ktorVersion")
            }
        }
        val iosTest by getting
    }
}
android {
    compileSdkVersion(30)
    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
    defaultConfig {
        minSdkVersion(19)
        targetSdkVersion(30)
        versionCode = 1
        versionName = "1.0"
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
        }
    }
    packagingOptions {
        excludes.add("META-INF/*.kotlin_module")
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().all {
        kotlinOptions {
            jvmTarget = "1.8"
        }
    }
}
val packForXcode by tasks.creating(Sync::class) {
    val targetDir = File(buildDir, "xcode-frameworks")
    /// selecting the right configuration for the iOS
    /// framework depending on the environment
    /// variables set by Xcode build
    val mode = System.getenv("CONFIGURATION") ?: "DEBUG"
    val sdkName: String? = System.getenv("SDK_NAME")
    val isiOSDevice = sdkName.orEmpty().startsWith("iphoneos")
    val framework = kotlin.targets
        .getByName<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>(
            if (isiOSDevice) {
                "iosArm64"
            } else {
                "iosX64"
            }
        )
        .binaries.getFramework(mode)
    inputs.property("mode", mode)
    dependsOn(framework.linkTask)
    from({ framework.outputDirectory })
    into(targetDir)
    println("Build Folder => $targetDir")
    /// generate a helpful ./gradlew wrapper with embedded Java path
    doLast {
        val gradlew = File(targetDir, "gradlew")
        gradlew.writeText(
            "#!/bin/bash\n"
                    + "export 'JAVA_HOME=${System.getProperty("java.home")}'\n"
                    + "cd '${rootProject.rootDir}'\n"
                    + "./gradlew \$@\n"
        )
        gradlew.setExecutable(true)
    }
}
tasks.build.dependsOn("packForXCode")

Solution

  • The "default configuration" in Android Studio (e.g. the assemble or build gradle tasks), will build all "build" tasks, which includes the linking of iOS frameworks for both debug, release and for X64 and Arm64 architectures.

    Notice that packForXcode task you got there in your build.gradle.kts file? That's what you want to call when you want to build the KMM module for your iOS application (at least locally). Using that task will build only the architecture and configuration you need (e.g. X64 and DEBUG on your simulator), and you'll quickly see that it's quite a lot faster. It's supposed to be called in a Run script phase as part of the Build steps of your Xcode project.

    I believe this should have been added to your Xcode project automatically if you're using the Kotlin Multiplatform application template when creating a new project in Android Studio.

    You can read more about it here: Understand the KMM project structure | Kotlin Multiplatform Mobile Docs