KMT-2065 KMT-2064 KMT-2063 Xcode parity Swift Export

Enables SourceKit code insight for Swift code importing Kotlin modules exported via Swift Export using the .swiftmodule files produced during the regular build.
###  Changes
  - Gradle Sync: Extract Swift Export data model produced by DSL configuration (moduleName, flattenPackage) from KGP via reflection and persist in ProjectDataManager (same way ObjC export does it)
  - SourceKit Integration: Provide -I compiler flags to SourceKit for built .swiftmodule files, enabling code completion for imported Kotlin modules
  - Build Listener: Track Swift Export module paths after embed* gradle tasks completion to locate generated artifacts in build/SPMBuild/<target>/<config>/. KGP SwiftExport runner copies it to Xcode build folder from there during Xcode run, we hardcode the path relative to the module with SwiftExport DSL.

Merge-request: IJ-MR-187833
Merged-by: Elijah Semyonov <Elijah.Semyonov@jetbrains.com>

GitOrigin-RevId: 634c7e7a4bdda0bca3e5996dedefb7f2752baa17
This commit is contained in:
Elijah Semyonov
2026-02-03 10:45:34 +00:00
committed by intellij-monorepo-bot
parent a46fdb746f
commit de21bec1fe
6 changed files with 217 additions and 4 deletions
@@ -0,0 +1,44 @@
package org.jetbrains.kotlin.idea.projectModel
import java.io.Serializable
/**
* Model representing Swift Export configuration extracted from the Kotlin Gradle Plugin.
*
* This model captures the configuration from the `swiftExport {}` DSL block in build.gradle.kts
* and is serialized during Gradle sync for use by IDE features.
*
* ## KGP Reference
*
* `org.jetbrains.kotlin.gradle.plugin.mpp.apple.swiftexport.SwiftExportExtension`
*
* ## Usage in build.gradle.kts
*
* ```kotlin
* kotlin {
* swiftExport {
* moduleName = "MyModule"
* flattenPackage = "com.example.app"
* }
* }
* ```
*/
interface KotlinSwiftExportModel : Serializable {
/**
* The configured module name for Swift Export.
*
* KGP: `SwiftExportExtension.moduleName: Property<String>` (inherited from `SwiftExportedModuleMetadata`)
*
* If not set, the project name is used as the default module name.
*/
val moduleName: String?
/**
* The package to flatten in the exported Swift module.
*
* KGP: `SwiftExportExtension.flattenPackage: Property<String>` (inherited from `SwiftExportedModuleMetadata`)
*
* When set, the specified package prefix is collapsed in the generated Swift API.
*/
val flattenPackage: String?
}
@@ -7,6 +7,7 @@ import org.jetbrains.kotlin.idea.projectModel.KotlinDependencyId
import org.jetbrains.kotlin.idea.projectModel.KotlinGradlePluginVersionDependentApi
import org.jetbrains.kotlin.idea.projectModel.KotlinSourceSet
import org.jetbrains.kotlin.idea.projectModel.KotlinTarget
import org.jetbrains.kotlin.idea.projectModel.KotlinSwiftExportModel
import java.io.Serializable
interface KotlinMPPGradleModel : KotlinSourceSetContainer, Serializable {
@@ -27,6 +28,19 @@ interface KotlinMPPGradleModel : KotlinSourceSetContainer, Serializable {
val kotlinImportingDiagnostics: KotlinImportingDiagnosticsContainer
val kotlinGradlePluginVersion: KotlinGradlePluginVersion?
/**
* Swift Export configuration if enabled in the project, or null if not configured.
*
* This field is populated by [KotlinMPPGradleModelBuilder.buildSwiftExportModel] during Gradle sync.
*
* ## KGP Reference
* `org.jetbrains.kotlin.gradle.plugin.mpp.apple.swiftexport.SwiftExportExtension`
*
* @see KotlinSwiftExportModel
* @see org.jetbrains.kotlin.idea.gradleTooling.reflect.KotlinSwiftExportReflection
*/
val swiftExport: KotlinSwiftExportModel?
companion object {
const val NO_KOTLIN_NATIVE_HOME = ""
}
@@ -72,7 +72,7 @@ class KotlinMPPGradleModelBuilder : AbstractModelBuilderService() {
val coroutinesState = getCoroutinesState(project)
val kotlinNativeHome = KotlinNativeHomeEvaluator.getKotlinNativeHome(project) ?: NO_KOTLIN_NATIVE_HOME
val swiftExportModel = buildSwiftExportModel(kotlinExtensionReflection)
val model = KotlinMPPGradleModelImpl(
sourceSetsByName = filterOrphanSourceSets(importingContext),
@@ -84,7 +84,8 @@ class KotlinMPPGradleModelBuilder : AbstractModelBuilderService() {
kotlinNativeHome = kotlinNativeHome,
dependencyMap = importingContext.dependencyMapper.toDependencyMap(),
dependencies = dependenciesContainer,
kotlinGradlePluginVersion = importingContext.kotlinGradlePluginVersion
kotlinGradlePluginVersion = importingContext.kotlinGradlePluginVersion,
swiftExport = swiftExportModel
).apply {
kotlinImportingDiagnostics += collectDiagnostics(importingContext)
}
@@ -213,6 +214,26 @@ class KotlinMPPGradleModelBuilder : AbstractModelBuilderService() {
return extension.kotlinExtension.javaClass.getMethodOrNull("getTargets") != null && extension.targets.isNotEmpty()
}
/**
* Builds the [KotlinSwiftExportModelImpl] from the Swift Export DSL configuration.
*
* ## KGP Reference
* `org.jetbrains.kotlin.gradle.plugin.mpp.apple.swiftexport.SwiftExportExtension`
*
* The extension is registered via `addExtension("swiftExport", ...)` in `SetupSwiftExportDSL.kt`,
* so it's accessed via [KotlinExtensionReflection.swiftExport] which uses `getExtensions().findByName()`.
*
* @return The Swift Export model if the extension is configured, null otherwise
* @see org.jetbrains.kotlin.idea.gradleTooling.reflect.KotlinSwiftExportReflection
*/
private fun buildSwiftExportModel(extension: KotlinExtensionReflection): KotlinSwiftExportModelImpl? {
val swiftExportReflection = extension.swiftExport ?: return null
return KotlinSwiftExportModelImpl(
moduleName = swiftExportReflection.moduleName,
flattenPackage = swiftExportReflection.flattenPackage
)
}
companion object {
private val DEFAULT_IMPORTING_CHECKERS = listOf(OrphanSourceSetImportingChecker)
@@ -23,6 +23,7 @@ import org.jetbrains.kotlin.idea.projectModel.KotlinTargetJar
import org.jetbrains.kotlin.idea.projectModel.KotlinTaskProperties
import org.jetbrains.kotlin.idea.projectModel.KotlinTestRunTask
import org.jetbrains.kotlin.idea.projectModel.KotlinWasmCompilationExtensions
import org.jetbrains.kotlin.idea.projectModel.KotlinSwiftExportModel
import java.io.File
class KotlinAndroidSourceSetInfoImpl(
@@ -282,6 +283,28 @@ data class ExtraFeaturesImpl(
override val isHMPPEnabled: Boolean,
) : ExtraFeatures
/**
* Implementation of [KotlinSwiftExportModel] for serialization during Gradle sync.
*
* ## KGP Reference
*
* This class captures data from the Swift Export DSL in the Kotlin Gradle Plugin:
* - Extension: `org.jetbrains.kotlin.gradle.plugin.mpp.apple.swiftexport.SwiftExportExtension`
* - Source: `libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/swiftexport/SwiftExportExtension.kt`
*
* @see KotlinSwiftExportModel
* @see KotlinMPPGradleModelBuilder.buildSwiftExportModel
*/
data class KotlinSwiftExportModelImpl(
override val moduleName: String?,
override val flattenPackage: String?
) : KotlinSwiftExportModel {
constructor(swiftExport: KotlinSwiftExportModel) : this(
moduleName = swiftExport.moduleName,
flattenPackage = swiftExport.flattenPackage
)
}
data class KotlinMPPGradleModelImpl @OptIn(KotlinGradlePluginVersionDependentApi::class) constructor(
override val sourceSetsByName: Map<String, KotlinSourceSet>,
override val targets: Collection<KotlinTarget>,
@@ -290,7 +313,8 @@ data class KotlinMPPGradleModelImpl @OptIn(KotlinGradlePluginVersionDependentApi
override val dependencyMap: Map<KotlinDependencyId, KotlinDependency>,
override val dependencies: IdeaKotlinDependenciesContainer?,
override val kotlinImportingDiagnostics: KotlinImportingDiagnosticsContainer = mutableSetOf(),
override val kotlinGradlePluginVersion: KotlinGradlePluginVersion?
override val kotlinGradlePluginVersion: KotlinGradlePluginVersion?,
override val swiftExport: KotlinSwiftExportModel? = null
) : KotlinMPPGradleModel {
@OptIn(KotlinGradlePluginVersionDependentApi::class)
@@ -312,7 +336,8 @@ data class KotlinMPPGradleModelImpl @OptIn(KotlinGradlePluginVersionDependentApi
dependencyMap = mppModel.dependencyMap.map { it.key to it.value.deepCopy(cloningCache) }.toMap(),
dependencies = mppModel.dependencies,
kotlinImportingDiagnostics = mppModel.kotlinImportingDiagnostics.mapTo(mutableSetOf()) { it.deepCopy(cloningCache) },
kotlinGradlePluginVersion = mppModel.kotlinGradlePluginVersion?.reparse()
kotlinGradlePluginVersion = mppModel.kotlinGradlePluginVersion?.reparse(),
swiftExport = mppModel.swiftExport?.let { KotlinSwiftExportModelImpl(it) }
)
}
@@ -33,8 +33,39 @@ class KotlinExtensionReflection(
sourceSets.associateBy { it.name }
}
/**
* Returns the Swift Export extension if configured, or null if Swift Export is not enabled.
*
* ## KGP Reference
*
* The Swift Export extension is registered in the Kotlin Gradle Plugin via:
* - Setup: `SetUpSwiftExportAction` in `SetupSwiftExportDSL.kt`
* - Registration: `multiplatformExtension.addExtension("swiftExport", swiftExportExtension)`
* - Source: `libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/swiftexport/SetupSwiftExportDSL.kt`
*
* Because the extension is added via `addExtension()` (not as a direct property),
* we must access it via `getExtensions().findByName("swiftExport")` rather than `getSwiftExport()`.
*
* The extension name constant is defined in `SwiftExportDSLConstants.SWIFT_EXPORT_EXTENSION_NAME = "swiftExport"`.
*/
val swiftExport: KotlinSwiftExportReflection? by lazy {
val extensions = kotlinExtension.callReflectiveAnyGetter("getExtensions", logger) ?: return@lazy null
val swiftExportExtension = extensions.callReflective(
"findByName",
parameters(parameter<String>(SWIFT_EXPORT_EXTENSION_NAME)),
returnType<Any?>(),
logger
) ?: return@lazy null
KotlinSwiftExportReflection(swiftExportExtension)
}
companion object {
private val logger: ReflectionLogger = ReflectionLogger(KotlinExtensionReflection::class.java)
private const val KOTLIN_PLUGIN_WRAPPER_FILE_CLASS_NAME = "org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapperKt"
/**
* KGP: `SwiftExportDSLConstants.SWIFT_EXPORT_EXTENSION_NAME` in `SetupSwiftExportDSL.kt`
*/
private const val SWIFT_EXPORT_EXTENSION_NAME = "swiftExport"
}
}
@@ -0,0 +1,78 @@
package org.jetbrains.kotlin.idea.gradleTooling.reflect
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
/**
* Reflection wrapper for `SwiftExportExtension` from the Kotlin Gradle Plugin.
*
* This class accesses the Swift Export model configured by DSL via reflection.
*
* ## KGP reference
* `org.jetbrains.kotlin.gradle.plugin.mpp.apple.swiftexport.SwiftExportExtension`
*
* ## DSL example in build.gradle.kts
*
* ```kotlin
* kotlin {
* swiftExport {
* moduleName.set("MyModule")
* flattenPackage.set("com.example")
* }
* }
* ```
*
* @see KotlinExtensionReflection.swiftExport
*/
fun KotlinSwiftExportReflection(swiftExportExtension: Any): KotlinSwiftExportReflection = KotlinSwiftExportReflectionImpl(swiftExportExtension)
interface KotlinSwiftExportReflection {
/**
* The configured module name for Swift Export, or null if not configured.
*
* KGP: `SwiftExportExtension.moduleName: Property<String>`
*/
val moduleName: String?
/**
* The package to flatten in the exported Swift module, or null if not configured.
*
* KGP: `SwiftExportExtension.flattenPackage: Property<String>`
*/
val flattenPackage: String?
}
private class KotlinSwiftExportReflectionImpl(private val instance: Any) : KotlinSwiftExportReflection {
override val moduleName: String? by lazy {
getPropertyValue("getModuleName")
}
override val flattenPackage: String? by lazy {
getPropertyValue("getFlattenPackage")
}
/**
* Helper to get value from a Gradle `Property<String>` via reflection.
*
* The Swift Export extension uses Gradle's lazy properties (`Property<String>`),
* so we need to call `.orNull` to get the actual value.
*/
private fun getPropertyValue(getterName: String): String? {
return try {
val property = instance.callReflectiveAnyGetter(getterName, logger)
when (property) {
is Property<*> -> property.orNull as? String
is Provider<*> -> property.orNull as? String
else -> property as? String
}
}
catch (_: Exception) {
null
}
}
companion object {
private val logger = ReflectionLogger(KotlinSwiftExportReflection::class.java)
}
}