From f586b6bd870e6541fcec2c2a01684e9191e72ce5 Mon Sep 17 00:00:00 2001 From: Andrey Yastrebov Date: Wed, 22 Jul 2026 17:14:05 +0200 Subject: [PATCH] [Gradle] Don't reinstall the K/N bundle over an existing installation The Kotlin/Native toolchain treats a bundle directory as installed only when it holds the 'provisioned.ok' marker, and only the toolchain itself ever wrote that marker. A distribution installed by 'NativeCompilerDownloader' -- the configuration-phase path, used whenever 'kotlin.native.distribution.downloadFromMaven' is off -- has no marker, so the toolchain declares it unprovisioned and unpacks the archive on top of it. That re-extraction is neither necessary nor safe. Extraction shells out to tar, which unlinks each existing file before recreating it, so every shipped file in the distribution briefly does not exist. A build reading the distribution at that moment gets a NoSuchFileException, which is what AndroidX has been hitting on files such as klib/cache/-gSTATIC-system/stdlib-per-file-cache/.../ir. They share one konan data dir between an outer build installing through 'NativeCompilerDownloader' and Gradle TestKit sub-builds installing through the toolchain, so the two paths meet routinely. Write the marker from 'NativeCompilerDownloader' as well, so that both installers agree on what a complete installation looks like and the toolchain leaves such a directory alone. The configuration-phase path had a second way to rewrite a live distribution. When its atomic rename failed because the directory was already there, it fell back to copying on top of it, which rewrites files another build may be reading. Skip that copy and, since this build then installed nothing, leave the marker to whoever did. ^KT-86251 --- .../native/NativeDownloadAndPlatformLibsIT.kt | 27 +++++++++++++++++++ .../native/NativeCompilerDownloader.kt | 20 +++++++++++--- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/NativeDownloadAndPlatformLibsIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/NativeDownloadAndPlatformLibsIT.kt index ecb0490f9903a..5b7fcf3a2f5d9 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/NativeDownloadAndPlatformLibsIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/NativeDownloadAndPlatformLibsIT.kt @@ -127,6 +127,33 @@ class NativeDownloadAndPlatformLibsIT : KGPBaseTest() { } } + @DisplayName("Toolchain leaves a distribution installed by the legacy downloader alone (KT-86251)") + @GradleTest + fun testToolchainDoesNotReinstallOverLegacyInstallation(gradleVersion: GradleVersion) { + platformLibrariesProject("linuxX64", gradleVersion = gradleVersion) { + val konanDataDir = workingDir.resolve(".konan") + + // this path has to write 'provisioned.ok' as well, that is the marker the toolchain looks for + build("tasks", buildOptions = defaultBuildOptions.copy(konanDataDir = konanDataDir)) { + assertOutputContains("Unpack Kotlin/Native compiler to ") + } + + // the toolchain has to leave that installation alone. Unpacking on top of it happens in place, so + // anything reading the distribution at the same time sees half-written files. + build( + "downloadKotlinNativeDistribution", + buildOptions = defaultBuildOptions.copy( + konanDataDir = konanDataDir, + freeArgs = listOf("-Pkotlin.native.toolchain.enabled=true"), + ), + ) { + assertTasksExecuted(":downloadKotlinNativeDistribution") + assertOutputDoesNotContain("Native bundle files will be overwritten") + assertOutputDoesNotContain("Moving Kotlin/Native bundle from") + } + } + } + @DisplayName("K/N distribution with platform libraries generation") @GradleTest fun testLibrariesGeneration(gradleVersion: GradleVersion) { diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/NativeCompilerDownloader.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/NativeCompilerDownloader.kt index 2f76a32f19f62..f3524744553b9 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/NativeCompilerDownloader.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/NativeCompilerDownloader.kt @@ -24,6 +24,7 @@ import org.jetbrains.kotlin.gradle.report.GradleBuildMetricsReporter import org.jetbrains.kotlin.gradle.targets.native.internal.NativeDistributionTypeProvider import org.jetbrains.kotlin.gradle.targets.native.internal.PlatformLibrariesGenerator import org.jetbrains.kotlin.gradle.targets.native.konanPropertiesBuildService +import org.jetbrains.kotlin.gradle.targets.native.toolchain.NativeVersionValueSource import org.jetbrains.kotlin.internal.compilerRunner.native.nativeCompilerClasspath import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.kotlin.konan.target.KonanTarget @@ -202,13 +203,24 @@ class NativeCompilerDownloader( it.into(tmpDir) } val compilerTmp = tmpDir.resolve(dependencyNameWithOsAndVersion) - if (!compilerTmp.renameTo(compilerDirectory)) { - project.copy { - it.from(compilerTmp) - it.into(compilerDirectory) + val installed = when { + compilerTmp.renameTo(compilerDirectory) -> true + !compilerDirectory.exists() -> { + project.copy { + it.from(compilerTmp) + it.into(compilerDirectory) + } + true } + // another build got there first, copying on top would rewrite files it is reading (KT-86251) + else -> false } logger.debug("Moved Kotlin/Native compiler from $tmpDir to $compilerDirectory") + if (installed) { + // without the marker a toolchain build sharing this konan data dir unpacks the archive over the + // distribution again (KT-86251). Keep it the last thing written. + compilerDirectory.resolve(NativeVersionValueSource.MARKER_FILE).createNewFile() + } } finally { tmpDir.deleteRecursively() }