gradle-kmp

v2026.09.24

Gradle build system for Kotlin Multiplatform projects. Covers settings.gradle.kts, version catalogs (libs.versions.toml), KMP plugin configuration, source set hierarchy, target binaries (JAR, AAR, XCFramework, JS bundle), publishing (Maven Central, GitHub Packages), CI presets (build matrix, caching, parallel execution), composite builds, and dependency management. USE WHEN: user mentions "Gradle KMP", "settings.gradle.kts", "version catalog", "libs.versions.toml", "XCFramework Gradle", "publishToMavenCentral", "Gradle composite build", "Gradle build cache", "configuration cache", "Gradle CI" DO NOT USE FOR: Cross-compiling Rust crates - use `build-tools/rust-cross-compile` DO NOT USE FOR: Reproducible builds spec - use `infrastructure/reproducible-builds` DO NOT USE FOR: KMP source code patterns - use `mobile/kotlin-multiplatform`

GitHub
Install command
npx skhub add claude-dev-suite/gradle-kmp
Markdown
SKILL.md

Gradle for Kotlin Multiplatform

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: gradle-kmp or gradle.

settings.gradle.kts

pluginManagement {
    repositories {
        google { content { includeGroupByRegex("com\\.android.*"); includeGroupByRegex("androidx.*") } }
        mavenCentral()
        gradlePluginPortal()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
        // For UniFFI KMP fork
        maven("https://maven.ubique.ch/snapshots")
    }
}

plugins {
    id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
    id("com.gradle.develocity") version "3.18.1"          // optional: build scan + cache
}

develocity {
    buildScan {
        termsOfUseUrl = "https://gradle.com/terms-of-service"
        termsOfUseAgree = "yes"
        publishing.onlyIf { System.getenv("CI") != null }
    }
}

rootProject.name = "BHODL"
include(":shared")
include(":apps:android")
include(":apps:desktop")

Version Catalog (gradle/libs.versions.toml)

Single source of truth for dependency versions. Replaces ad-hoc ext blocks.

[versions]
kotlin = "2.2.0"
agp = "8.7.0"
compose-multiplatform = "1.8.0"
ktor = "3.0.0"
coroutines = "1.10.0"
serialization = "1.7.3"
sqldelight = "2.0.2"
koin = "4.0.0"

[libraries]
kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqldelight" }
sqldelight-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelight-native = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }

[bundles]
ktor-common = ["ktor-core", "ktor-content-negotiation", "ktor-serialization"]

[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
compose = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }

Use:

plugins {
    alias(libs.plugins.kotlin.multiplatform)
}

dependencies {
    implementation(libs.coroutines.core)
    implementation(libs.bundles.ktor.common)
}

gradle.properties

# JVM
org.gradle.jvmargs=-Xmx4g -XX:+UseG1GC -XX:MaxMetaspaceSize=1g

# Performance
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configureondemand=true
org.gradle.configuration-cache=true                # Gradle 8+ stable
org.gradle.unsafe.configuration-cache-problems=warn

# Kotlin
kotlin.code.style=official
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.mpp.enableCInteropCommonization=true
kotlin.native.cacheKind=static                      # Faster Native build
kotlin.native.ignoreDisabledTargets=true            # Skip iOS targets on Linux

# Android
android.useAndroidX=true
android.nonTransitiveRClass=true
android.nonFinalResIds=true

# Compose
org.jetbrains.compose.experimental.uikit.enabled=true

Source Set Hierarchy

KMP 1.9+ has a default template — most projects don't need custom intermediate sets.

kotlin {
    androidTarget()
    jvm("desktop")
    listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { /* ... */ }

    sourceSets {
        commonMain.dependencies {
            implementation(libs.coroutines.core)
        }
        androidMain.dependencies {
            implementation(libs.ktor.okhttp)
        }
        iosMain.dependencies {
            implementation(libs.ktor.darwin)
        }
        commonTest.dependencies {
            implementation(kotlin("test"))
            implementation(libs.coroutines.test)
        }
    }
}

For custom intermediate set (e.g., shared between Android + Desktop):

sourceSets {
    val jvmCommonMain by creating {
        dependsOn(commonMain.get())
    }
    androidMain.get().dependsOn(jvmCommonMain)
    getByName("desktopMain").dependsOn(jvmCommonMain)
}

XCFramework Output

import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework

kotlin {
    val xcf = XCFramework("Shared")
    listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { target ->
        target.binaries.framework {
            baseName = "Shared"
            isStatic = true
            xcf.add(this)
            export(libs.coroutines.core.get())          // expose to Swift consumers
        }
    }
}

Build:

./gradlew :shared:assembleSharedXCFramework
# Output: shared/build/XCFrameworks/release/Shared.xcframework

CocoaPods Plugin

plugins {
    kotlin("native.cocoapods") version "2.2.0"
}

kotlin {
    cocoapods {
        version = "1.0.0"
        summary = "Shared KMP module"
        homepage = "https://github.com/example/bhodl"
        ios.deploymentTarget = "16.0"
        framework {
            baseName = "Shared"
            isStatic = true
        }
        // Optionally consume CocoaPods deps from Kotlin
        pod("FirebaseAuth") { version = "11.0.0" }
    }
}
./gradlew :shared:podPublishXCFramework
cd apps/ios && pod install

Embed-and-Sign for Xcode

Auto-build framework when Xcode builds:

# In Xcode build phase:
cd "$SRCROOT/.."
./gradlew :shared:embedAndSignAppleFrameworkForXcode

The embedAndSignAppleFrameworkForXcode task is auto-registered by KMP plugin.

Kotlin Compiler Options

kotlin {
    targets.all {
        compilations.all {
            compilerOptions.configure {
                freeCompilerArgs.addAll(
                    "-Xexpect-actual-classes",
                    "-Xcontext-parameters",
                    "-opt-in=kotlin.RequiresOptIn",
                    "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
                    "-Xjsr305=strict",
                )
            }
        }
    }
    androidTarget {
        compilerOptions {
            jvmTarget.set(JvmTarget.JVM_17)
        }
    }
    jvm("desktop") {
        compilerOptions {
            jvmTarget.set(JvmTarget.JVM_17)
        }
    }
}

Maven Publishing

plugins {
    `maven-publish`
    signing
}

group = "com.bhodl"
version = "1.0.0"

publishing {
    repositories {
        maven {
            name = "GitHubPackages"
            url = uri("https://maven.pkg.github.com/bhodl/shared")
            credentials {
                username = providers.gradleProperty("gpr.user").orNull
                    ?: System.getenv("GITHUB_ACTOR")
                password = providers.gradleProperty("gpr.token").orNull
                    ?: System.getenv("GITHUB_TOKEN")
            }
        }
        maven {
            name = "MavenCentral"
            url = uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/")
            credentials {
                username = providers.gradleProperty("ossrhUsername").orNull
                password = providers.gradleProperty("ossrhPassword").orNull
            }
        }
    }
    publications.withType<MavenPublication> {
        pom {
            name.set("Shared")
            description.set("Shared KMP module")
            url.set("https://github.com/bhodl/shared")
            licenses {
                license {
                    name.set("MIT")
                    url.set("https://opensource.org/licenses/MIT")
                }
            }
            developers {
                developer { id.set("bhodl"); name.set("BHODL Team") }
            }
            scm {
                url.set("https://github.com/bhodl/shared")
            }
        }
    }
}

signing {
    val signingKey = providers.environmentVariable("SIGNING_KEY").orNull
    val signingPassword = providers.environmentVariable("SIGNING_PASSWORD").orNull
    if (signingKey != null) {
        useInMemoryPgpKeys(signingKey, signingPassword)
        sign(publishing.publications)
    }
}

For modern Maven Central → use vanniktech/gradle-maven-publish-plugin:

plugins {
    id("com.vanniktech.maven.publish") version "0.30.0"
}

mavenPublishing {
    publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
    signAllPublications()
    coordinates("com.bhodl", "shared", "1.0.0")
    pom { /* ... */ }
}

Build Cache

Local cache

# gradle.properties
org.gradle.caching=true

Stored in ~/.gradle/caches/build-cache-1.

Remote cache (CI / team)

// settings.gradle.kts
buildCache {
    local { enabled = true }
    remote<HttpBuildCache> {
        url = uri("https://cache.bhodl.dev/cache/")
        push = System.getenv("CI") != null               // only CI pushes
        credentials {
            username = providers.gradleProperty("buildCacheUser").orNull
            password = providers.gradleProperty("buildCachePass").orNull
        }
    }
}

Or use Gradle Develocity (formerly Gradle Enterprise) — best-in-class with build scans.

Configuration Cache (Gradle 8+)

# gradle.properties
org.gradle.configuration-cache=true
org.gradle.unsafe.configuration-cache-problems=warn

Caches build configuration → faster subsequent builds (skip configuration phase).

If a plugin doesn't support it, set =warn and gradually fix incompatible code (no Project.afterEvaluate, no Task.project, etc.).

Composite Builds

For monorepos splitting independent modules:

// settings.gradle.kts
includeBuild("../bhodl-core") {
    dependencySubstitution {
        substitute(module("com.bhodl:core")).using(project(":"))
    }
}

Build of root project automatically builds and uses local bhodl-core. No publish needed.

Common Gradle Tasks

# Build everything
./gradlew build

# Just one target
./gradlew :shared:assembleDebug                  # Android library debug
./gradlew :shared:linkReleaseFrameworkIosArm64   # iOS device framework
./gradlew :shared:assembleSharedXCFramework      # All iOS arch into XCFramework
./gradlew :shared:desktopJar
./gradlew :apps:android:assembleRelease
./gradlew :apps:android:bundleRelease            # AAB for Play Store
./gradlew :apps:android:installDebug             # Install on connected device

# Tests
./gradlew test                                   # JVM tests
./gradlew :shared:jvmTest
./gradlew :shared:iosSimulatorArm64Test
./gradlew :shared:desktopTest

# Quality
./gradlew :shared:detekt
./gradlew :shared:ktlintCheck
./gradlew :shared:lintDebug                      # Android lint

# Publishing
./gradlew :shared:publishToMavenLocal
./gradlew :shared:publishAllPublicationsToMavenCentralRepository

# Cleanup
./gradlew clean
./gradlew --refresh-dependencies                 # force re-resolve

# Diagnostics
./gradlew :shared:dependencies
./gradlew :shared:dependencyInsight --dependency kotlinx-coroutines-core
./gradlew :shared:tasks --all
./gradlew help --task assembleDebug              # docs for a task

# Performance
./gradlew --scan                                 # build scan to scans.gradle.com
./gradlew --profile                              # local HTML profile in build/reports/profile/
./gradlew --build-cache                          # force cache use

CI: GitHub Actions

# .github/workflows/build.yml
name: Build

on:
  push:
    branches: [main]
  pull_request:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-14]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with: { java-version: '17', distribution: 'temurin' }

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v4
        with:
          cache-read-only: ${{ github.ref != 'refs/heads/main' }}

      - name: Build (Linux: skip iOS targets)
        if: runner.os == 'Linux'
        run: ./gradlew assembleDebug -Pkotlin.native.ignoreDisabledTargets=true

      - name: Build full (macOS: includes iOS)
        if: runner.os == 'macOS'
        run: ./gradlew assemble

      - name: Test
        run: ./gradlew test

      - name: Upload reports
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: reports-${{ matrix.os }}
          path: |
            **/build/reports/
            **/build/test-results/

Caching tips for CI

  • Use gradle/actions/setup-gradle@v4 — handles Gradle home + dependency cache automatically
  • cache-read-only on PRs to avoid polluting cache from forks
  • For Konan (Native), cache ~/.konan separately

CI: GitLab CI Snippet

build:
  image: eclipse-temurin:17-jdk
  cache:
    paths:
      - .gradle/
      - ~/.gradle/caches/
  script:
    - ./gradlew assembleDebug --build-cache --parallel
    - ./gradlew test
  artifacts:
    when: on_failure
    paths:
      - "**/build/reports/"

Anti-Patterns

Anti-patternWhy it's badCorrect approach
ext { } for versions in root build.gradle.ktsHard to refactor, no IDE supportUse version catalog
Hardcoded versions in build.gradle.ktsDrift across moduleslibs.versions.toml single source
apply plugin: 'X' (Groovy syntax)Oldplugins { id("X") } block
compileOptions.sourceCompatibility = JavaVersion.VERSION_8Old JVM targetAt least 17 for KMP
useFakeStrictMode() workaroundsHides bugsFix incompatibilities
Duplicate config across subprojectsDriftUse convention plugin in buildSrc/ or build-logic/
Disabling configuration cacheSlowFix compat issues progressively
gradle.properties per-moduleConfusingSingle root gradle.properties
Push to remote build cache from PR forksPollutionSet push = System.getenv("CI") != null and gate by branch
Heavy afterEvaluate blocksConfg cache breaksUse lazy properties (Provider API)

Convention Plugins (build-logic/)

For monorepos with many modules sharing config:

build-logic/
├── settings.gradle.kts
├── convention/
│   ├── build.gradle.kts
│   └── src/main/kotlin/
│       ├── bhodl.kotlin-multiplatform.gradle.kts
│       ├── bhodl.android-application.gradle.kts
│       └── bhodl.android-library.gradle.kts
// bhodl.kotlin-multiplatform.gradle.kts
plugins {
    kotlin("multiplatform")
}

kotlin {
    targets.all {
        compilations.all {
            compilerOptions.configure {
                freeCompilerArgs.addAll("-Xexpect-actual-classes", "-Xcontext-parameters")
            }
        }
    }
}

// In a module
plugins {
    id("bhodl.kotlin-multiplatform")
}

Troubleshooting

SymptomCauseFix
Could not find method ... for argumentsPlugin version mismatchAlign all Kotlin/AGP/Compose plugin versions
Could not determine the dependencies of task ...Configuration cache incompatibilityDisable plugin temporarily or fix lazy access
Slow Native compilation (iOS)No cachekotlin.native.cacheKind=static
:apps:android:lintDebug failsLint baseline outdatedUpdate with ./gradlew updateLintBaseline
OOM during buildToo many parallel workersReduce org.gradle.parallel.threads or increase Xmx
Cannot create variant 'metadataApiElements'Multiple KMP plugin applicationsApply once per module, use convention plugin
INSTALL_FAILED_NO_MATCHING_ABIS on emulatorWrong APK ABIBuild matching emulator (x86_64 → build x86_64)
iOS framework "module not found"Wrong target builtBuild for iosArm64 (device) AND iosSimulatorArm64 (M-series sim)

When NOT to Use This Skill

ScenarioUse Instead
Cross-compiling Rust crates for mobilebuild-tools/rust-cross-compile
Reproducible build specinfrastructure/reproducible-builds
KMP code patternsmobile/kotlin-multiplatform
Compose Multiplatform UI patternsfrontend-frameworks/compose-multiplatform
Gradle for plain Spring BootGeneric Gradle skill
Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

skills/build-tools/gradle-kmp

Default branch

main

Latest commit

9496306

Tree SHA

fe4e2f1