From 6ee48862057fdcec6a593aa7a5db11c636cd2b2e Mon Sep 17 00:00:00 2001 From: Omar Ismail Date: Fri, 3 Jul 2026 10:29:15 +0000 Subject: [PATCH 1/2] Fix configuration cache invalidation in klib cross-compilation check KotlinNativeCompilation.crossCompilationSupported resolves the crossCompilationMetadata JSONs exported by project dependencies and probes their existence. Capturing it in onlyIf makes the configuration cache evaluate it at store time, before the exporting tasks have run. When the build subsequently creates these files, the next build discards the cache entry with: "configuration cache cannot be reused because the file system entry ... has been created". To fix this, we mirror KGP's own compileKotlin task behavior: * Keep the target-level condition (cross-compilation property enabled and no cinterops) in a stored provider * Declare the metadata JSONs as task inputs so that the exporting tasks run first. * Read and parse these metadata files in onlyIf at execution time. TESTED: Update the regression test (testNativeConfigurationCacheReuse) which could not catch this bug previously. The old test used a host-supported target that never took the cross-compilation target. The updated test targets iosArm64 to force the cross-compilation path, and builds the iosArm64 klib and asserts configuration cache reuse. --- .../google/devtools/ksp/gradle/KspAATask.kt | 128 +++++++++++++++--- .../ksp/test/primary/KMPImplementedIT.kt | 34 +++-- 2 files changed, 132 insertions(+), 30 deletions(-) diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspAATask.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspAATask.kt index 6280e72bce..6c0238396b 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspAATask.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspAATask.kt @@ -36,8 +36,10 @@ import org.gradle.api.JavaVersion import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.attributes.Attribute +import org.gradle.api.attributes.Usage import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileCollection import org.gradle.api.logging.LogLevel import org.gradle.api.provider.ListProperty import org.gradle.api.provider.MapProperty @@ -103,6 +105,13 @@ abstract class KspAATask @Inject constructor( @get:Nested abstract val commandLineArgumentProviders: ListProperty + // Metadata JSONs from dependencies indicating cross-compilation support. + // Declared as input so Gradle runs the exporting tasks before this task executes. + @get:InputFiles + @get:PathSensitive(PathSensitivity.NONE) + @get:Optional + abstract val crossCompilationMetadata: ConfigurableFileCollection + @get:ServiceReference abstract val isolatedClassLoaderCacheBuildService: Property @@ -445,13 +454,7 @@ abstract class KspAATask @Inject constructor( it.name == konanTargetName } if (!isHostSupported) { - val isKlibCrossCompilationEnabled = getKlibCrossCompilationSupport( - project, - kotlinCompilation - ) - kspAATask.onlyIf { - isKlibCrossCompilationEnabled.get() - } + configureCrossCompilationSkipCondition(project, kotlinCompilation, kspAATask) } } @@ -468,27 +471,110 @@ abstract class KspAATask @Inject constructor( return kspTaskProvider } + private const val ENABLE_KLIBS_CROSSCOMPILATION_PROPERTY = "kotlin.native.enableKlibsCrossCompilation" + private const val CROSS_COMPILATION_SHARED_DATA_KEY = "crossCompilationMetadata" + private val KOTLIN_PROJECT_SHARED_DATA_ATTRIBUTE = + Attribute.of("org.jetbrains.kotlin.project-shared-data", String::class.java) + /** - * Returns a Provider indicating whether klib cross-compilation is enabled. - * For Kotlin 2.3.20-Beta2+, uses KotlinNativeCompilation.crossCompilationSupported. - * For earlier versions, falls back to checking the gradle property kotlin.native.enableKlibsCrossCompilation. + * Configures [kspAATask] to skip when cross-compilation is not possible on the current host, + * mirroring how KGP skips compile tasks (see KGP's KotlinCreateNativeCompileTasksSideEffect). + * + * To remain compatible with the Configuration Cache, we split KGP's check: + * 1. Target-level check (evaluated during configuration): Checks host compatibility, + * cross-compilation properties, and cinterops. + * 2. Dependency-level check (evaluated during execution): Reads shared metadata JSONs + * from project dependencies (declared as task inputs to ensure correct task ordering). + * + * TODO(https://youtrack.jetbrains.com/issue/KT-87411): Replace with the public KGP API once available. */ - private fun getKlibCrossCompilationSupport( + private fun configureCrossCompilationSkipCondition( project: Project, - kotlinCompilation: KotlinNativeCompilation - ): Provider { + kotlinCompilation: KotlinNativeCompilation, + kspAATask: KspAATask, + ) { val kotlinVersion = project.getKotlinPluginVersion() - val isSupportCrossCompilationPropertyAvailable = + val isCrossCompilationMetadataAvailable = KotlinToolingVersion(kotlinVersion) >= KotlinToolingVersion("2.3.20-Beta2") - return if (isSupportCrossCompilationPropertyAvailable) { - kotlinCompilation.crossCompilationSupported - } else { - // Fallback for Kotlin versions before 2.3.20-Beta2 - project.providers.gradleProperty( - "kotlin.native.enableKlibsCrossCompilation" - ).orElse("false").map { it.toBoolean() } + val isKlibCrossCompilationEnabled = getKlibCrossCompilationEnabledProvider(project) + + if (!isCrossCompilationMetadataAvailable) { + kspAATask.onlyIf { isKlibCrossCompilationEnabled.get() } + return + } + + val target = kotlinCompilation.target + val hasCinterops = project.objects.property(Boolean::class.java).convention(false) + target.compilations.configureEach { compilation -> + compilation.cinterops.configureEach { + hasCinterops.set(true) + } } + + val isTargetCrossCompilationPossible = isKlibCrossCompilationEnabled.zip(hasCinterops) { klibCrossCompilationEnabled, targetHasCinterops -> + HostManager.hostOrNull != null && + klibCrossCompilationEnabled && + !targetHasCinterops + } + kspAATask.onlyIf("Cross compilation should be supported on host") { + isTargetCrossCompilationPossible.get() + } + + val metadataFiles = getCrossCompilationMetadataFiles(project, kotlinCompilation) + kspAATask.crossCompilationMetadata.from(metadataFiles) + kspAATask.onlyIf("Cross compilation should be possible with project dependencies") { task -> + (task as KspAATask).crossCompilationMetadata.files.all(::isCrossCompilationSupportedBy) + } + } + + private fun getKlibCrossCompilationEnabledProvider(project: Project): Provider { + val gradlePropertiesSetting = project.providers.gradleProperty(ENABLE_KLIBS_CROSSCOMPILATION_PROPERTY) + .map { it.toBoolean() } + .orElse(true) + + val subprojectPropertiesSetting = project.providers + .fileContents(project.layout.projectDirectory.file("gradle.properties")) + .asText + .map { text -> + val props = Properties().apply { load(java.io.StringReader(text)) } + props.getProperty(ENABLE_KLIBS_CROSSCOMPILATION_PROPERTY)?.toBoolean() + } + + return subprojectPropertiesSetting.orElse(gradlePropertiesSetting) + } + + private fun getCrossCompilationMetadataFiles( + project: Project, + kotlinCompilation: KotlinNativeCompilation + ): FileCollection { + val configurationProvider = project.configurations.named(kotlinCompilation.compileDependencyConfigurationName) + val sharedDataUsage = project.objects.named(Usage::class.java, "kotlin-project-shared-data") + val filesProvider = configurationProvider.map { configuration -> + configuration.incoming.artifactView { view -> + view.isLenient = true + view.attributes.attribute(Usage.USAGE_ATTRIBUTE, sharedDataUsage) + view.attributes.attribute(KOTLIN_PROJECT_SHARED_DATA_ATTRIBUTE, CROSS_COMPILATION_SHARED_DATA_KEY) + view.attributes.attribute( + Attribute.of("artifactType", String::class.java), + "kotlin-project-shared-data-$CROSS_COMPILATION_SHARED_DATA_KEY" + ) + }.files + } + return project.files(filesProvider) + } + + /** + * Parses the cross-compilation metadata JSON exported by KGP. + * Missing or malformed files default to supported (matching KGP's parser). + */ + private fun isCrossCompilationSupportedBy(metadataFile: File): Boolean { + val content = runCatching { metadataFile.readText() }.getOrNull() ?: return true + return runCatching { + val parser = groovy.json.JsonSlurper() + val map = parser.parseText(content) as Map<*, *> + map["crossCompilationSupported"] as? Boolean + }.getOrNull() ?: true } } } diff --git a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/primary/KMPImplementedIT.kt b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/primary/KMPImplementedIT.kt index 0a31825e3d..056837c330 100644 --- a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/primary/KMPImplementedIT.kt +++ b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/primary/KMPImplementedIT.kt @@ -288,21 +288,37 @@ class AnnoOnProperty { @Test fun testNativeConfigurationCacheReuse() { - Assume.assumeFalse(System.getProperty("os.name").startsWith("Windows", ignoreCase = true)) + Assume.assumeTrue(System.getProperty("os.name").startsWith("Linux", ignoreCase = true)) val gradleRunner = GradleRunner.create().withProjectDir(project.root) - // Warmup build to download and extract Kotlin Native compiler - gradleRunner.withArguments( - "--no-configuration-cache", - ":workload-linuxX64:assemble" - ).build() + // Add a klib-only iosArm64 target to workload-linuxX64 and to its project dependency + // (:annotations), so that the skip condition has cross-compilation metadata of a + // project dependency to consult. + val buildScript = File(project.root, "workload-linuxX64/build.gradle.kts") + buildScript.writeText( + buildScript.readText().replace( + "linuxX64() {", + "iosArm64() {\n }\n linuxX64() {" + ) + ) + buildScript.appendText("\ndependencies {\n add(\"kspIosArm64\", project(\":test-processor\"))\n}\n") + val annotationsBuildScript = File(project.root, "annotations/build.gradle.kts") + annotationsBuildScript.writeText( + annotationsBuildScript.readText().replace( + "linuxX64() {", + "iosArm64() {\n }\n linuxX64() {" + ) + ) + project.appendProperty( + "org.gradle.configuration-cache.inputs.unsafe.ignore.file-system-checks=~/.konan/**" + ) - // First run: calculates and stores configuration cache val result1 = gradleRunner.withArguments( "--configuration-cache", "--configuration-cache-problems=fail", - ":workload-linuxX64:assemble" + ":workload-linuxX64:compileKotlinIosArm64" ).build() + Assert.assertEquals(TaskOutcome.SUCCESS, result1.task(":workload-linuxX64:kspKotlinIosArm64")?.outcome) Assert.assertTrue( "Expected 'Configuration cache entry stored.' but output was:\n${result1.output}", result1.output.contains("Configuration cache entry stored.") @@ -312,7 +328,7 @@ class AnnoOnProperty { val result2 = gradleRunner.withArguments( "--configuration-cache", "--configuration-cache-problems=fail", - ":workload-linuxX64:assemble" + ":workload-linuxX64:compileKotlinIosArm64" ).build() Assert.assertTrue( "Expected 'Reusing configuration cache.' but output was:\n${result2.output}", From 60facc3c29a5d0b2aaf4df3b69d728ac5c4aff4e Mon Sep 17 00:00:00 2001 From: Omar Ismail Date: Mon, 6 Jul 2026 12:02:54 +0100 Subject: [PATCH 2/2] Update KMPImplementedIT.kt Test on mac as well to make sure we dont break compiling on same platform --- .../com/google/devtools/ksp/test/primary/KMPImplementedIT.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/primary/KMPImplementedIT.kt b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/primary/KMPImplementedIT.kt index 056837c330..fc4d2b6466 100644 --- a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/primary/KMPImplementedIT.kt +++ b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/primary/KMPImplementedIT.kt @@ -288,7 +288,7 @@ class AnnoOnProperty { @Test fun testNativeConfigurationCacheReuse() { - Assume.assumeTrue(System.getProperty("os.name").startsWith("Linux", ignoreCase = true)) + Assume.assumeFalse(System.getProperty("os.name").startsWith("Windows", ignoreCase = true)) val gradleRunner = GradleRunner.create().withProjectDir(project.root) // Add a klib-only iosArm64 target to workload-linuxX64 and to its project dependency