Dynamically select which Kotlin Multiplatform targets to build.
Status: alpha (pre-1.0). The core selector and the automatic minimal hierarchy template are implemented and exercised by the multi-module sample; further helper APIs (user-defined hierarchy groups, XCFramework) are roadmap items.
kmp-targets is a Gradle plugin for Kotlin Multiplatform projects that lets each developer (and each CI runner) choose which KMP targets to build, via a single Gradle property:
# local.properties, gradle.properties, or -P
KMP_TARGETS=jvm,iosArm64Targets you don't select are never registered with KGP, so their compile/link/KSP/publish tasks never run — cutting Gradle sync and build times when you only need a subset.
A real multi-module project is heterogeneous: a shared module builds every platform, a mobile
feature builds only Android + iOS, a tooling module builds only the JVM. kmp-targets separates two
facts:
- What a module can build — its supported set, declared per-module via the type-safe DSL.
- What you want to build now — the global
KMP_TARGETSselection.
The plugin registers selection ∩ supported for each module. A module never builds a target outside
its supported set, and the global selection narrows that further.
Apply KGP and the com.rsicarelli.kmptargets base id together, then declare the module's supported
set with the type-safe kmpTargets { supports { … } } DSL — the whole target vocabulary is in scope
as set algebra (+ / -), no imports, no strings:
// build.gradle.kts
plugins {
kotlin("multiplatform") version "2.3.21"
id("com.rsicarelli.kmptargets") version "<version>"
}
kmpTargets {
supports { mobile + web - iosX64 } // presets and leaves compose freely
}Every preset (all, mobile, apple, web, jvmFamily, …) and every leaf (jvm, iosArm64,
linuxX64, …) is available inside the block. Build-logic and consumers needing a raw value can use
the overload: supports(KmpTargetSet.mobile + KmpTargetSet.web).
Targets are explicit: a module that never calls supports registers nothing, just like plain
KGP where every target is declared by hand. supports registers eagerly — the moment it runs — so
call it before anything reads kotlin.targets, and calling it more than once unions (an
already-registered target can't be retracted). To build whatever the developer is currently
targeting, opt in explicitly with supports { all } — then the global KMP_TARGETS alone decides
what registers.
The selection itself is global (the KMP_TARGETS property, see below) — there is no per-module
selection block. A project-wide default for when KMP_TARGETS is unset can be set from build-logic
via the defaultSelection property; a global KMP_TARGETS always overrides it.
The plugin ships a sources jar, so every preset, leaf, and extension member carries KDoc you can
read on hover (F1 / Quick Documentation). But kmpTargets { … } resolves through the build-script
(plugin) classpath, and IntelliJ does not attach sources for build-script dependencies by
default — so out of the box you'll only see the bare signature. Turn it on once:
- Settings → Advanced Settings → Build Tools. Gradle → enable "Attach scripts dependencies sources" (also enable "Download sources").
- Reload the Gradle project (Gradle tool window → 🔄). A sync is required for the setting to take effect.
After the sync, hovering supports, mobile, apple, iosArm64, … shows their documentation.
The plugin ships just one extension (KmpTargetsExtension) and one set of public types
(KmpTargetSet, KmpTarget) — there are no bundled "preset" plugin ids. Teams that want
apply by id, no body ergonomics build their own conventions in build-logic/:
// build-logic/src/main/kotlin/my.mobile-module.gradle.kts
import com.rsicarelli.kmptargets.KmpTargetsExtension
import com.rsicarelli.kmptargets.model.KmpTargetSet
plugins {
kotlin("multiplatform")
id("com.rsicarelli.kmptargets")
}
extensions.configure<KmpTargetsExtension> {
supports(KmpTargetSet.mobile)
}// feature-mobile/build.gradle.kts
plugins { id("my.mobile-module") }This is how a team centralizes per-module shape vocabulary in their own naming, instead of taking a vocabulary baked into this plugin's artifact. The DSL stays the primitive; convention plugins are an optional layer you own.
The selection is global — one switch narrows the whole build. Set it via any of these sources (priority order, highest first):
-PKMP_TARGETS=...on the CLIORG_GRADLE_PROJECT_KMP_TARGETSenvironment variable- the root
gradle.properties(a subproject'sgradle.propertiesis not a source — Gradle only reads root-level project properties) local.properties(per-developer, gitignored)- a project-wide
defaultSelectionset from build-logic (overridden by all of the above) - Plugin default — every target the plugin currently knows about
KMP_TARGETS=android,iosArm64 # explicit list
KMP_TARGETS=appleMobile # preset (iosArm64 + iosSimulatorArm64)
KMP_TARGETS=appleMobile,-iosArm64 # preset minus a leaf
KMP_TARGETS=apple,+android # preset plus an addition
KMP_TARGETS=ANDROID, ios-arm64 # aliases + case-insensitiveAvailable presets:
| Preset | Expands to |
|---|---|
all |
every shipped target |
native |
every Kotlin/Native target (Apple + Linux + MinGW + Android Native) |
apple |
all Apple platforms: iOS + macOS + watchOS + tvOS |
appleMobile / appleDesktop |
all iOS / all macOS |
appleWatch / appleTv |
all watchOS / all tvOS |
linux |
linuxX64, linuxArm64 |
mingw (alias windows) |
mingwX64 |
androidNative |
the four androidNative* targets |
web |
js, wasmJs, wasmWasi |
jvmFamily |
androidTarget, jvm |
mobile |
androidTarget + all iOS |
Unknown tokens fail the build at configuration time with a "did you mean ...?" suggestion — silently dropping a misspelled target in CI is the worst failure mode, so the parser is strict. Bare Apple sub-family names (ios, macos, watchos, tvos) are rejected with a hint pointing at the relevant leaf or appleX preset.
Every target Kotlin Multiplatform (KGP 2.3.21) supports, except the deprecated linuxArm32Hfp:
| Family | Targets |
|---|---|
| JVM | androidTarget (alias android), jvm (alias desktop) |
| iOS | iosArm64, iosSimulatorArm64, iosX64 |
| macOS | macosArm64, macosX64 |
| watchOS | watchosArm64, watchosArm32, watchosX64, watchosSimulatorArm64, watchosDeviceArm64 |
| tvOS | tvosArm64, tvosX64, tvosSimulatorArm64 |
| Linux | linuxX64, linuxArm64 |
| MinGW | mingwX64 |
| Android Native | androidNativeArm32, androidNativeArm64, androidNativeX86, androidNativeX64 |
| Web | js, wasmJs, wasmWasi |
Every leaf also accepts a kebab-case alias (e.g. watchos-sim-arm64, linux-x64, android-native-arm64).
KGP's auto-applied applyDefaultHierarchyTemplate() builds the full source-set hierarchy for all
possible targets, so an iOS-only module still gets nativeMain and appleMain intermediate
source sets that are redundant with iosMain. Each redundant intermediate spawns ~8 wasteful Gradle
tasks; across dozens of modules this dominates sync time (see
the cost of default hierarchy templates).
Because kmp-targets already knows each module's active target set, it applies a minimal custom
hierarchy instead — collapsing every redundant single-child group:
| Active targets | Intermediate source sets |
|---|---|
| one iOS leaf | none (target attaches to commonMain) |
| iOS (≥2 leaves) | iosMain only — no appleMain, no nativeMain |
| iOS + macOS | appleMain over iosMain + macosMain — no nativeMain |
| iOS + Linux | nativeMain over iosMain + linuxMain — no appleMain |
It's on by default and applied automatically — no configuration needed. To opt out (and let KGP apply its own default), set the Gradle property globally or override per project (project wins):
# root gradle.properties — global default
kmptargets.hierarchyTemplate=false// any module's build.gradle.kts — per-project override
kmpTargets { hierarchyTemplate.set(false) }Precedence is project DSL > global property > built-in default (true). Opt out when a module
supplies its own applyHierarchyTemplate { … }, so the plugin stays out of the way.
The plugin is not yet published to the Gradle Plugin Portal or Maven Central. Track progress in the issues / releases. Locally:
task publish-local # publishes the plugin to mavenLocal
task sample # smoke test against the multi-module samples/hello-worldRequirements: mise (pins JDK 23) and Task (developer task runner).
mise install # provisions Temurin 23.0.2+7
task ci # build + test + sample
task dod # Definition of Done — fmt + build + test (run before committing)
task hooks:install # enable the version-controlled .githooks/ pre-commit gateSee CONTRIBUTING.md for the full development guide.
Apache-2.0. See LICENSE.