diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/KotlinNativeKlibCommonizer.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/KotlinNativeKlibCommonizer.kt index 905628b6055..8008f841c29 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/KotlinNativeKlibCommonizer.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/KotlinNativeKlibCommonizer.kt @@ -12,9 +12,9 @@ import org.jetbrains.kotlin.konan.target.KonanTarget import java.io.File import java.io.IOException import java.nio.charset.StandardCharsets -import java.nio.file.AtomicMoveNotSupportedException -import java.nio.file.Files -import java.nio.file.StandardCopyOption +import java.nio.file.* +import java.nio.file.attribute.* +import java.time.* import java.util.* internal fun runCommonizerInBulk( @@ -25,9 +25,11 @@ internal fun runCommonizerInBulk( kotlinVersion: String ): List { val commandLineArguments = mutableListOf() - val postActions = mutableListOf<() -> Unit>() - val result = targetGroups.map { targets -> + val successPostActions = mutableListOf<() -> Unit>() + val failurePostActions = mutableListOf<() -> Unit>() + + val destinationDirs = targetGroups.map { targets -> if (targets.size == 1) { // no need to commonize, just use the libraries from the distribution distributionDir.resolve(KONAN_DISTRIBUTION_KLIB_DIR) @@ -60,18 +62,27 @@ internal fun runCommonizerInBulk( commandLineArguments += "-targets" commandLineArguments += orderedTargets.joinToString(separator = ",") - postActions.add { renameDirectory(destinationTmpDir, destinationDir) } + successPostActions.add { renameDirectory(destinationTmpDir, destinationDir) } + failurePostActions.add { renameToTempAndDelete(destinationTmpDir) } } destinationDir } } - callCommonizerCLI(project, commandLineArguments) + // first of all remove directories with unused commonized libraries plus temporary directories with commonized libraries + // that accidentally were not cleaned up before + cleanUp(baseDestinationDir, excludedDirectories = destinationDirs) - postActions.forEach { it() } + try { + callCommonizerCLI(project, commandLineArguments) + successPostActions.forEach { it() } + } catch (e: Exception) { + failurePostActions.forEach { it() } + throw e + } - return result + return destinationDirs } private fun callCommonizerCLI(project: Project, commandLineArguments: List) { @@ -88,7 +99,7 @@ private fun renameDirectory(source: File, destination: File) { for (it in 0 until 3) { try { - safeDelete(destination) + renameToTempAndDelete(destination) Files.move(sourcePath, destinationPath, StandardCopyOption.ATOMIC_MOVE) return } catch (e: IOException) { @@ -104,21 +115,78 @@ private fun renameDirectory(source: File, destination: File) { throw IllegalStateException("Failed to rename $source to $destination").apply { suppressedExceptions.forEach(::addSuppressed) } } -private fun safeDelete(directory: File) { +private fun renameToTempAndDelete(directory: File) { if (!directory.exists()) return - // first, rename directory to some temp directory - val tempDir = createTempFile( - prefix = "tmp-" + directory.name, - suffix = ".old", - directory = directory.parentFile - ) - tempDir.delete() + val dirToRemove = if (directory.name.startsWith("tmp-")) { + // already temp directory, return as is + directory + } else { + // first, rename the directory to some temp directory + val tempDir = createTempFile( + prefix = "tmp-" + directory.name, + suffix = ".old", + directory = directory.parentFile + ) + tempDir.delete() - Files.move(directory.toPath(), tempDir.toPath(), StandardCopyOption.ATOMIC_MOVE) + Files.move(directory.toPath(), tempDir.toPath(), StandardCopyOption.ATOMIC_MOVE) - // only then delete it recursively - tempDir.deleteRecursively() + tempDir + } + + dirToRemove.deleteRecursively() +} + +private fun cleanUp(baseDirectory: File, excludedDirectories: List) { + fun File.getAttributes(): BasicFileAttributes? = + try { + Files.readAttributes(toPath(), BasicFileAttributes::class.java) + } catch (_: IOException) { + null + } + + fun FileTime.isSameOrAfter(targetInstant: Instant): Boolean { + val fileInstant = toInstant() + + if (fileInstant.atZone(ZoneOffset.UTC).toLocalDate().year <= 1970) { + // file time represents the epoch (or even a time point before it) + // such instant can't be used for reliable comparison + return false + } + + return fileInstant >= targetInstant + } + + val now = Instant.now() + val oneHourAgo = now.minus(Duration.ofHours(1)) + val oneMonthAgo = now.minus(Duration.ofDays(31)) + + val excludedPaths = excludedDirectories.map { it.absolutePath }.toSet() + + baseDirectory.listFiles() + ?.forEach { file -> + if (file.absolutePath in excludedPaths) return@forEach + + val attributes = file.getAttributes() ?: return@forEach + if (attributes.isDirectory) { + if (file.name.startsWith("tmp-")) { + // temp directories created more than 1 hour ago are stale and should be GCed + if (attributes.creationTime().isSameOrAfter(oneHourAgo)) return@forEach + } else { + // clean up other directories which were not accesses within the last month + if (attributes.lastAccessTime().isSameOrAfter(oneMonthAgo)) return@forEach + } + } /*else { + // clean up everything that is not a directory + }*/ + + try { + renameToTempAndDelete(file) + } catch (_: IOException) { + // do nothing + } + } } private val String.base64