diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/cpp/RunGTest.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/cpp/RunGTest.kt index d327c58b7b4..b0a5c19a03c 100644 --- a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/cpp/RunGTest.kt +++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/cpp/RunGTest.kt @@ -146,6 +146,9 @@ abstract class RunGTest : DefaultTask() { @get:Input abstract val executionTimeout: Property + @get:Input + val platformManagerExtension: PlatformManager = project.extensions.getByType() + @TaskAction fun run() { val workQueue = workerExecutor.noIsolation() @@ -157,7 +160,7 @@ abstract class RunGTest : DefaultTask() { reportFileUnprocessed.set(this@RunGTest.reportFileUnprocessed) filter.set(this@RunGTest.filter) tsanSuppressionsFile.set(this@RunGTest.tsanSuppressionsFile) - platformManager.set(project.extensions.getByType()) + platformManager.set(platformManagerExtension) targetName.set(this@RunGTest.target.get().name) executionTimeout.set(this@RunGTest.executionTimeout) } diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/GitDownloadTask.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/GitDownloadTask.kt index 75d89fbfbf0..dc2e6fd90f8 100644 --- a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/GitDownloadTask.kt +++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/GitDownloadTask.kt @@ -6,16 +6,20 @@ package org.jetbrains.kotlin.testing.native import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.internal.file.FileOperations +import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property -import org.gradle.api.provider.Provider import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.options.Option +import org.gradle.kotlin.dsl.property +import org.gradle.process.ExecOperations import org.gradle.process.ExecResult import org.gradle.process.ExecSpec import org.jetbrains.kotlin.isNotEmpty -import java.io.File -import java.net.URL +import java.net.URI import java.util.* import javax.inject.Inject @@ -23,112 +27,27 @@ import javax.inject.Inject * Clones the given revision of the given Git repository to the given directory. */ @Suppress("UnstableApiUsage") -open class GitDownloadTask @Inject constructor( - private val repositoryProvider: Provider, - private val revisionProvider: Provider, - private val outputDirectoryProvider: Provider +abstract class GitDownloadTask @Inject constructor( + objects: ObjectFactory, + private val execOperations: ExecOperations, + private val fileOperations: FileOperations, ) : DefaultTask() { + @get:Input + abstract val repository: Property - private val repository: URL - get() = repositoryProvider.get() + @get:Input + abstract val revision: Property - private val revision: String - get() = revisionProvider.get() - - private val outputDirectory: File - get() = outputDirectoryProvider.get() + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty @Option(option = "refresh", description = "Fetch and checkout the revision even if the output directory already contains it. " + "All changes in the output directory will be overwritten") - @Input - val refresh: Property = project.objects.property(Boolean::class.java).apply { - set(false) - } - - private val upToDateChecker = UpToDateChecker() + @get:Input + val refresh: Property = objects.property().convention(false) init { - outputs.upToDateWhen { upToDateChecker.isUpToDate() } - } - - private fun git( - vararg args: String, - ignoreExitValue: Boolean = false, - execConfiguration: ExecSpec.() -> Unit = {} - ): ExecResult = - project.exec { - executable = "git" - args(*args) - isIgnoreExitValue = ignoreExitValue - execConfiguration() - } - - private fun tryCloneBranch(): Boolean { - val execResult = git( - "clone", repository.toString(), - outputDirectory.absolutePath, - "--depth", "1", - "--branch", revision, - ignoreExitValue = true - ) - return execResult.exitValue == 0 - } - - private fun fetchByHash() { - git("init", outputDirectory.absolutePath) - git("fetch", repository.toString(), "--depth", "1", revision) { - workingDir(outputDirectory) - } - git("reset", "--hard", revision) { - workingDir(outputDirectory) - } - } - - @TaskAction - fun clone() { - // Gradle ignores outputs.upToDateWhen { ... } and reruns a task if the classpath of the task was changed - // So we have to perform the up-to-date check manually one more time in the task action. - if (upToDateChecker.isUpToDate()) { - logger.info("Skip cloning to avoid rewriting possible debug changes in ${outputDirectory.absolutePath}.") - return - } - - project.delete { - delete(outputDirectory) - } - - if (!tryCloneBranch()) { - logger.info("Cannot use the revision '$revision' to clone the repository. Trying to use init && fetch instead.") - fetchByHash() - } - - // Store info about used revision for the manual up-to-date check. - upToDateChecker.storeRevisionInfo() - - // Delete the .git directory of the cloned repo to avoid adding it to IDEA's VCS roots. - outputDirectory.resolve(".git").deleteRecursively() - } - - - /** - * This class performs manual UP-TO-DATE checking. - * - * We want to be able to edit downloaded sources for debug purposes. Thus this task should not rewrite changes in - * the output directory. - * - * Gradle does allow us to provide a custom logic to determine if task outputs are UP-TO-DATE or not (see - * Task.outputs.upToDateWhen). But Gradle still reruns a task if its classpath was changed. This may lead - * to rewriting our manual debug changes in downloaded sources if there are some changes in the - * `build-tools` project. - * - * So we have to manually check up-to-dateness in upToDateWhen and as a first step of the task action. - */ - private inner class UpToDateChecker() { - - private val revisionInfoFile: File - get() = outputDirectory.resolve(".revision") - /** * The download task should be executed in the following cases: * @@ -138,38 +57,58 @@ open class GitDownloadTask @Inject constructor( * * In all other cases we consider the task UP-TO-DATE. */ - fun isUpToDate(): Boolean { - return !refresh.get() && - outputDirectory.let { it.exists() && project.fileTree(it).isNotEmpty } && - noRevisionChanges() - } - - private fun noRevisionChanges(): Boolean = - revisionInfoFile.exists() && loadRevisionInfo() == RevisionInfo(repository, revision) - - fun storeRevisionInfo() { - val properties = Properties() - properties["repository"] = repository.toString() - properties["revision"] = revision - revisionInfoFile.bufferedWriter().use { - properties.store(it, null) - } - } - - private fun loadRevisionInfo(): RevisionInfo? { - return try { - val properties = Properties() - revisionInfoFile.bufferedReader().use { - properties.load(it) - } - RevisionInfo(properties.getProperty("repository"), properties.getProperty("revision")) - } catch (_ : Exception) { - null + outputs.upToDateWhen { + val upToDate = !refresh.get() && outputDirectory.asFileTree.isNotEmpty + if (upToDate) { + logger.info("Skip cloning to avoid rewriting possible debug changes in ${outputDirectory.get().asFile.absolutePath}.") } + upToDate } } - private data class RevisionInfo(val repository: String, val revision: String) { - constructor(repository: URL, revision: String): this(repository.toString(), revision) + private fun git( + vararg args: String, + ignoreExitValue: Boolean = false, + execConfiguration: ExecSpec.() -> Unit = {} + ): ExecResult = + execOperations.exec { + executable = "git" + args(*args) + isIgnoreExitValue = ignoreExitValue + execConfiguration() + } + + private fun tryCloneBranch(): Boolean { + val execResult = git( + "clone", repository.get().toString(), + outputDirectory.get().asFile.absolutePath, + "--depth", "1", + "--branch", revision.get(), + ignoreExitValue = true + ) + return execResult.exitValue == 0 + } + + private fun fetchByHash() { + git("init", outputDirectory.get().asFile.absolutePath) + git("fetch", repository.get().toString(), "--depth", "1", revision.get()) { + workingDir(outputDirectory) + } + git("reset", "--hard", revision.get()) { + workingDir(outputDirectory) + } + } + + @TaskAction + fun clone() { + fileOperations.delete(outputDirectory) + + if (!tryCloneBranch()) { + logger.info("Cannot use the revision '${revision.get()}' to clone the repository. Trying to use init && fetch instead.") + fetchByHash() + } + + // Delete the .git directory of the cloned repo to avoid adding it to IDEA's VCS roots. + fileOperations.delete(outputDirectory.dir(".git")) } } diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/RuntimeTestingPlugin.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/RuntimeTestingPlugin.kt index b29a143a1d4..0be68b89bb9 100644 --- a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/RuntimeTestingPlugin.kt +++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/RuntimeTestingPlugin.kt @@ -8,46 +8,47 @@ package org.jetbrains.kotlin.testing.native import org.gradle.api.InvalidUserDataException import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.FileCollection +import org.gradle.api.file.ProjectLayout +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory import org.gradle.api.tasks.TaskProvider import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.property import org.jetbrains.kotlin.bitcode.CompileToBitcodeExtension -import org.jetbrains.kotlin.resolve -import java.io.File -import java.net.URL +import java.net.URI import javax.inject.Inject @Suppress("UnstableApiUsage") open class RuntimeTestingPlugin : Plugin { override fun apply(target: Project): Unit = with(target) { - val extension = extensions.create(GOOGLE_TEST_EXTENSION_NAME, GoogleTestExtension::class.java, target) + val extension = extensions.create(GOOGLE_TEST_EXTENSION_NAME, GoogleTestExtension::class.java) val downloadTask = registerDownloadTask(extension) - val googleTestRoot = project.provider { extension.sourceDirectory } + val googleTestRoot = extension.sourceDirectory - createBitcodeTasks(googleTestRoot, listOf(downloadTask)) + createBitcodeTasks(project.objects.directoryProperty().value(googleTestRoot), listOf(downloadTask)) } - private fun Project.registerDownloadTask(extension: GoogleTestExtension): TaskProvider { - val task = tasks.register( - "downloadGoogleTest", - GitDownloadTask::class.java, - provider { URL(extension.repository) }, - provider { extension.revision }, - provider { extension.fetchDirectory } - ) - task.configure { - refresh.set(provider { extension.refresh }) - onlyIf { extension.localSourceRoot == null } - description = "Retrieves GoogleTest from the given repository" - group = "Google Test" - } - return task - } + private fun Project.registerDownloadTask(extension: GoogleTestExtension): TaskProvider = + tasks.register("downloadGoogleTest", GitDownloadTask::class.java) { + description = "Retrieves GoogleTest from the given repository" + group = "Google Test" + onlyIf { + !extension.hasLocalSourceRoot.get() + } + repository.set(extension.repository.map { URI.create(it) }) + revision.set(extension.revision) + outputDirectory.set(extension.sourceDirectory) + refresh.set(extension.refresh) + } private fun Project.createBitcodeTasks( - googleTestRoot: Provider, + googleTestRoot: DirectoryProperty, dependencies: Iterable> ) { pluginManager.withPlugin("compile-to-bitcode") { @@ -57,12 +58,12 @@ open class RuntimeTestingPlugin : Plugin { module("googletest") { sourceSets { testFixtures { - inputFiles.from(googleTestRoot.resolve("googletest/src")) + inputFiles.from(googleTestRoot.dir("googletest/src")) // That's how googletest/CMakeLists.txt builds gtest library. inputFiles.include("gtest-all.cc") headersDirs.setFrom( - googleTestRoot.resolve("googletest/include"), - googleTestRoot.resolve("googletest") + googleTestRoot.dir("googletest/include"), + googleTestRoot.dir("googletest") ) } } @@ -73,13 +74,13 @@ open class RuntimeTestingPlugin : Plugin { module("googlemock") { sourceSets { testFixtures { - inputFiles.from(googleTestRoot.resolve("googlemock/src")) + inputFiles.from(googleTestRoot.dir("googlemock/src")) // That's how googlemock/CMakeLists.txt builds gmock library. inputFiles.include("gmock-all.cc") headersDirs.setFrom( - googleTestRoot.resolve("googlemock"), - googleTestRoot.resolve("googlemock/include"), - googleTestRoot.resolve("googletest/include"), + googleTestRoot.dir("googlemock"), + googleTestRoot.dir("googlemock/include"), + googleTestRoot.dir("googletest/include"), ) } } @@ -99,12 +100,16 @@ open class RuntimeTestingPlugin : Plugin { /** * A project extension to configure from where we get the GoogleTest framework. */ -open class GoogleTestExtension @Inject constructor(private val project: Project) { +abstract class GoogleTestExtension @Inject constructor( + layout: ProjectLayout, + providers: ProviderFactory, + objects: ObjectFactory, +) { /** * A repository to fetch GoogleTest from. */ - var repository: String = "https://github.com/google/googletest.git" + val repository: Property = objects.property().convention("https://github.com/google/googletest.git") private var _revision: String? = null @@ -122,43 +127,32 @@ open class GoogleTestExtension @Inject constructor(private val project: Project) /** * Fetch the [revision] even if the [fetchDirectory] already contains it. Overwrite all changes manually made in the output directory. */ - var refresh: Boolean = false + val refresh: Property = objects.property().convention(false) /** * A directory to fetch the [revision] to. */ - var fetchDirectory: File = project.file("googletest") - - internal var localSourceRoot: File? = null + private val fetchDirectory: Directory = layout.projectDirectory.dir("googletest") /** - * Use a local [directory] with GoogleTest instead of the fetched one. If set, the download task will not be executed. + * Use a local directory with GoogleTest instead of the fetched one. If set, the download task will not be executed. */ - fun useLocalSources(directory: File) { - localSourceRoot = directory - } - - /** - * Use a local [directory] with GoogleTest instead of the fetched one. If set, the download task will not be executed. - */ - fun useLocalSources(directory: String) { - localSourceRoot = project.file(directory) - } + abstract val localSourceRoot: DirectoryProperty + val hasLocalSourceRoot: Provider = providers.provider { localSourceRoot.isPresent } /** * A getter for directory that contains the GTest sources. - * Returns a local source directory if it's specified (see [useLocalSources]) or [fetchDirectory] otherwise. + * Returns a local source directory if it's specified (see [localSourceRoot]) or [fetchDirectory] otherwise. */ - val sourceDirectory: File - get() = localSourceRoot ?: fetchDirectory + val sourceDirectory: Provider = localSourceRoot.orElse(fetchDirectory) /** * A file collection with header directories for GoogleTest and GoogleMock. * Useful to configure compilation against GTest. */ - val headersDirs: FileCollection = project.files( - project.provider { sourceDirectory.resolve("googletest/include") }, - project.provider { sourceDirectory.resolve("googlemock/include") } + val headersDirs: FileCollection = layout.files( + sourceDirectory.map { it.dir("googletest/include")}, + sourceDirectory.map { it.dir("googlemock/include")} ) }