gradle kotlin-mp
Gradle Kotlin multi-platform.
Vocab
Target is a part of the build responsible for compiling, testing, and packaging a piece of software aimed at one of the supported platforms. - reference
Notes
Target: encompasses BOTH 1) What you are building for. And 2) How the code is built.
The term 'target' in Kotlin Multiplatform refers to both:
- The platform you are targeting (e.g., JVM, JS, iOS, etc.).
- The configuration and associated tasks (like compiling, testing, and packaging) required to build and test for that specific platform.
In short, 'target' encompasses:
- What you are building for (the platform).
- How the code is built for that platform (the build and test tasks).
For example:
kotlin {
jvm() // JVM target: Defines that the code will be built for the JVM and configures the necessary build steps.
js() // JS target: Similarly, defines and configures for JavaScript environments.
}
This means that 'target' encapsulates both the destination platform and the process of building for that platform.
Intermediate source sets in Kotlin Multiplatform projects are shared source sets that are used to share code between a subset of targets rather than all of them. These source sets act as a middle layer between the common code (shared across all targets) and platform-specific code, allowing for reuse across some, but not all, of the supported platforms.
Key points about intermediate source sets:
-
Sharing Code Across Some Targets: Intermediate source sets are designed to hold code that is shared between specific platforms. For example, you might have an
iosMain
source set that is shared between all iOS targets but not other platforms like Android or JVM. -
Customization for Specific Platforms: You can include platform-specific APIs or logic that apply to a subset of platforms (e.g., native platforms like iOS, macOS) while avoiding this code for others (like JVM or JS).
-
Layer Between Common and Platform-Specific Code:
- The common source set is shared across all targets.
- Intermediate source sets are placed between the common source set and specific platform source sets.
- Platform-specific source sets are at the bottom layer and contain code unique to that platform.
Example:
In a Kotlin Multiplatform project, you may define an intermediate source set for native targets (iOS and macOS) but exclude JVM and JS from sharing this code.
kotlin {
jvm()
ios()
macosX64()
sourceSets {
val commonMain by getting
val nativeMain by creating { // Intermediate source set shared between iOS and macOS
dependsOn(commonMain)
}
iosMain.get().dependsOn(nativeMain)
macosX64Main.get().dependsOn(nativeMain)
}
}
In this example:
commonMain
is shared across all targets.nativeMain
is an intermediate source set shared by bothiosMain
andmacosX64Main
, but not byjvmMain
.- Code placed in
nativeMain
can be used by both iOS and macOS, but not by JVM.
Why Use Intermediate Source Sets?
- Platform-Specific APIs: You can use APIs specific to a certain subset of platforms (like native iOS or macOS APIs) without polluting the code that gets compiled for other platforms like JVM or JS.
- Code Reuse: It reduces duplication by sharing common code between certain platforms.
- Flexibility: It gives you more granular control over code-sharing while respecting platform-specific constraints and APIs.
Example Structure:
In a project with ios
and android
targets, you might have these source sets:
commonMain
(shared across all platforms)iosMain
(specific to iOS)androidMain
(specific to Android)appleMain
(intermediate source set, shared by iOS and macOS targets)
Notes
Kotlin Multiplatform projects support hierarchical source set structures. This means you can arrange a hierarchy of intermediate source sets for sharing the common code among some, but don't have to share it across all, supported targets, (Target (Gradle Kotlin-MP))
Clarification on "not all"
The phrase "not all" in "sharing the common code among some, but not all, supported targets" means that when setting up a hierarchical source set structure in Kotlin Multiplatform projects, you don't necessarily need to share the common code across every single platform (or target) you're building for. Instead, you can choose to share the code among a subset of the supported targets.
What is 'intermediate source sets'?
Intermediate source sets in Kotlin Multiplatform projects are shared source sets that are used to share code between a subset of targets rather than all of them. These source sets act as a middle layer between the common code (shared across all targets) and platform-specific code, allowing for reuse across some, but not all, of the supported platforms.
Key points about intermediate source sets:
-
Sharing Code Across Some Targets: Intermediate source sets are designed to hold code that is shared between specific platforms. For example, you might have an
iosMain
source set that is shared between all iOS targets but not other platforms like Android or JVM. -
Customization for Specific Platforms: You can include platform-specific APIs or logic that apply to a subset of platforms (e.g., native platforms like iOS, macOS) while avoiding this code for others (like JVM or JS).
-
Layer Between Common and Platform-Specific Code:
- The common source set is shared across all targets.
- Intermediate source sets are placed between the common source set and specific platform source sets.
- Platform-specific source sets are at the bottom layer and contain code unique to that platform.
Example:
In a Kotlin Multiplatform project, you may define an intermediate source set for native targets (iOS and macOS) but exclude JVM and JS from sharing this code.
kotlin {
jvm()
ios()
macosX64()
sourceSets {
val commonMain by getting
val nativeMain by creating { // Intermediate source set shared between iOS and macOS
dependsOn(commonMain)
}
iosMain.get().dependsOn(nativeMain)
macosX64Main.get().dependsOn(nativeMain)
}
}
In this example:
commonMain
is shared across all targets.nativeMain
is an intermediate source set shared by bothiosMain
andmacosX64Main
, but not byjvmMain
.- Code placed in
nativeMain
can be used by both iOS and macOS, but not by JVM.
Why Use Intermediate Source Sets?
- Platform-Specific APIs: You can use APIs specific to a certain subset of platforms (like native iOS or macOS APIs) without polluting the code that gets compiled for other platforms like JVM or JS.
- Code Reuse: It reduces duplication by sharing common code between certain platforms.
- Flexibility: It gives you more granular control over code-sharing while respecting platform-specific constraints and APIs.
Example Structure:
In a project with ios
and android
targets, you might have these source sets:
commonMain
(shared across all platforms)iosMain
(specific to iOS)androidMain
(specific to Android)appleMain
(intermediate source set, shared by iOS and macOS targets)
Using intermediate source sets helps you to:
- Provide a specific API for some targets. For example, a library can add native-specific APIs in an intermediate source set for Kotlin/Native targets but not for Kotlin/JVM ones.
- Consume a specific API for some targets. For example, you can benefit from a rich API that the Kotlin Multiplatform library provides for some targets that form an intermediate source set.
- Use platform-dependent libraries in your project. For example, you can access iOS-specific dependencies from the intermediate iOS source set.
The Kotlin toolchain ensures that each source set has access only to the API that is available for all targets to which that source set compiles. This prevents cases like using a Windows-specific API and then compiling it to macOS, resulting in linkage errors or undefined behavior at runtime.
The recommended way to set up the source set hierarchy is to use the default hierarchy template. The template covers the most popular cases. If you have a more advanced project, you can configure it manually. This is a more low-level approach: it's more flexible but requires more effort and knowledge.
Default hierarchy template
The Kotlin Gradle plugin has a built-in default hierarchy template. It contains predefined intermediate source sets for some popular use cases. The plugin sets up those source sets automatically based on the targets specified in your project.
Consider the following example:
kotlin {
androidTarget()
iosArm64()
iosSimulatorArm64()
}
When you declare the targets androidTarget
, iosArm64
, and iosSimulatorArm64
in your code, the Kotlin Gradle plugin finds
suitable shared source sets from the template and creates them for you. The resulting hierarchy looks like this:
Colored source sets are actually created and present in the project, while gray ones from the default template are
ignored. The Kotlin Gradle plugin hasn't created the watchos
source set, for example, because there
are no watchOS targets in the project.
If you add a watchOS target, like watchosArm64
, the watchos
source set is created, and the code
from the apple
, native
, and common
source sets is compiled to watchosArm64
as well.
The Kotlin Gradle plugin provides both type-safe and static accessors for all of the source sets from the default hierarchy
template, so you can reference them without by getting
or by creating
constructs compared to the manual configuration.
If you try to access the source set without declaring the corresponding target first, you'll see a warning:
kotlin {
androidTarget()
iosArm64()
iosSimulatorArm64()
sourceSets {
iosMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:%coroutinesVersion%")
}
// Warning: accessing source set without declaring the target
linuxX64Main { }
}
}
kotlin {
androidTarget()
iosArm64()
iosSimulatorArm64()
sourceSets {
iosMain {
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:%coroutinesVersion%'
}
}
// Warning: accessing source set without declaring the target
linuxX64Main { }
}
}
In this example, the
apple
andnative
source sets compile only to theiosArm64
andiosSimulatorArm64
targets. Despite their names, they have access to the full iOS API. This can be counter-intuitive for source sets likenative
, as you might expect that only APIs available on all native targets are accessible in this source set. This behavior may change in the future.{style="note"}
Additional configuration
You might need to make adjustments to the default hierarchy template. If you have previously introduced intermediate sources
manually with dependsOn
calls, it cancels the use of the default
hierarchy template and leads to this warning:
The Default Kotlin Hierarchy Template was not applied to '<project-name>':
Explicit .dependsOn() edges were configured for the following source sets:
[<... names of the source sets with manually configured dependsOn-edges...>]
Consider removing dependsOn-calls or disabling the default template by adding
'kotlin.mpp.applyDefaultHierarchyTemplate=false'
to your gradle.properties
Learn more about hierarchy templates: https://kotl.in/hierarchy-template
To solve this issue, configure your project by doing one of the following:
Replacing a manual configuration
Case. All of your intermediate source sets are currently covered by the default hierarchy template.
Solution. Remove all manual dependsOn()
calls and source sets with by creating
constructions.
To check the list of all default source sets, see the full hierarchy template.
Creating additional source sets
Case. You want to add source sets that the default hierarchy template doesn't provide yet, for example, one between a macOS and a JVM target.
Solution:
-
Reapply the template by explicitly calling
applyDefaultHierarchyTemplate()
. -
Configure additional source sets manually using
dependsOn()
:kotlin { jvm() macosArm64() iosArm64() iosSimulatorArm64() // Apply the default hierarchy again. It'll create, for example, the iosMain source set: applyDefaultHierarchyTemplate() sourceSets { // Create an additional jvmAndMacos source set: val jvmAndMacos by creating { dependsOn(commonMain.get()) } macosArm64Main.get().dependsOn(jvmAndMacos) jvmMain.get().dependsOn(jvmAndMacos) } }
kotlin { jvm() macosArm64() iosArm64() iosSimulatorArm64() // Apply the default hierarchy again. It'll create, for example, the iosMain source set: applyDefaultHierarchyTemplate() sourceSets { // Create an additional jvmAndMacos source set: jvmAndMacos { dependsOn(commonMain.get()) } macosArm64Main { dependsOn(jvmAndMacos.get()) } jvmMain { dependsOn(jvmAndMacos.get()) } } }
Modifying source sets
Case. You already have the source sets with the exact same names as those generated by the template, but shared among
different sets of targets in your project. For example, a nativeMain
source set is shared only among the desktop-specific
targets: linuxX64
, mingwX64
, and macosX64
.
Solution. There's currently no way to modify the default dependsOn
relations among the template's source sets.
It's also important that the implementation and the meaning of source sets, for example,
nativeMain
, are the same in all projects.
However, you still can do one of the following:
- Find different source sets for your purposes, either in the default hierarchy template or ones that have been manually created.
- Opt out of the template completely by adding
kotlin.mpp.applyDefaultHierarchyTemplate=false
to yourgradle.properties
file and manually configure all source sets.
We're currently working on an API to create your own hierarchy templates. This will be useful for projects whose hierarchy configurations differ significantly from the default template.
This API is not ready yet, but if you're eager to try it, look into the
applyHierarchyTemplate {}
block and the declaration ofKotlinHierarchyTemplate.default
as an example. Keep in mind that this API is still in development. It might not be tested and can change in further releases.{style="tip"}
See the full hierarchy template {initial-collapse-state="collapsed" collapsible="true"}
When you declare the targets to which your project compiles, the plugin picks the shared source sets based on the specified targets from the template and creates them in your project.
This example only shows the production part of the project, omitting the
Main
suffix (for example, usingcommon
instead ofcommonMain
). However, everything is the same for*Test
sources as well.{style="tip"}
Manual configuration
You can manually introduce an intermediate source in the source set structure. It will hold the shared code for several targets.
For example, here’s what to do if you want to share code among native Linux,
Windows, and macOS targets (linuxX64
, mingwX64
, and macosX64
):
- Add the intermediate source set
desktopMain
, which holds the shared logic for these targets. - Specify the source set hierarchy using the
dependsOn
relation.
kotlin {
linuxX64()
mingwX64()
macosX64()
sourceSets {
val desktopMain by creating {
dependsOn(commonMain.get())
}
linuxX64Main.get().dependsOn(desktopMain)
mingwX64Main.get().dependsOn(desktopMain)
macosX64Main.get().dependsOn(desktopMain)
}
}
kotlin {
linuxX64()
mingwX64()
macosX64()
sourceSets {
desktopMain {
dependsOn(commonMain.get())
}
linuxX64Main {
dependsOn(desktopMain)
}
mingwX64Main {
dependsOn(desktopMain)
}
macosX64Main {
dependsOn(desktopMain)
}
}
}
The resulting hierarchical structure will look like this:
You can have a shared source set for the following combinations of targets:
- JVM or Android + JS + Native
- JVM or Android + Native
- JS + Native
- JVM or Android + JS
- Native
Kotlin doesn't currently support sharing a source set for these combinations:
- Several JVM targets
- JVM + Android targets
- Several JS targets
If you need to access platform-specific APIs from a shared native source set, IntelliJ IDEA will help you detect common declarations that you can use in the shared native code. For other cases, use the Kotlin mechanism of expected and actual declarations.
Children