[K/N][build] Download gtest like breakpad
diff --git a/kotlin-native/build-tools/build.gradle.kts b/kotlin-native/build-tools/build.gradle.kts
index 4d4c9d3..8d17416 100644
--- a/kotlin-native/build-tools/build.gradle.kts
+++ b/kotlin-native/build-tools/build.gradle.kts
@@ -77,10 +77,6 @@
             id = "compile-to-bitcode"
             implementationClass = "org.jetbrains.kotlin.bitcode.CompileToBitcodePlugin"
         }
-        create("runtimeTesting") {
-            id = "runtime-testing"
-            implementationClass = "org.jetbrains.kotlin.testing.native.RuntimeTestingPlugin"
-        }
         create("compilationDatabase") {
             id = "compilation-database"
             implementationClass = "org.jetbrains.kotlin.cpp.CompilationDatabasePlugin"
diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/bitcode/CompileToBitcodePlugin.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/bitcode/CompileToBitcodePlugin.kt
index d26ca3e..433ff7a9a 100644
--- a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/bitcode/CompileToBitcodePlugin.kt
+++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/bitcode/CompileToBitcodePlugin.kt
@@ -31,7 +31,6 @@
 import org.jetbrains.kotlin.konan.target.*
 import org.jetbrains.kotlin.nativeDistribution.nativeProtoDistribution
 import org.jetbrains.kotlin.resolveLlvmUtility
-import org.jetbrains.kotlin.testing.native.GoogleTestExtension
 import org.jetbrains.kotlin.utils.capitalized
 import java.io.File
 import java.time.Duration
@@ -145,6 +144,11 @@
         project.executorsClasspathConfiguration()
     }
 
+    /**
+     * GTest headers to be used in testFixtures and test source sets.
+     */
+    val googleTestHeaders: ConfigurableFileCollection = project.objects.fileCollection()
+
     private val allTestsTasks by lazy {
         val name = project.name.capitalized
         val platformManager = project.extensions.getByType<PlatformManager>()
@@ -314,9 +318,6 @@
         abstract class SourceSets @Inject constructor(private val module: Module, private val container: ExtensiblePolymorphicDomainObjectContainer<SourceSet>) : NamedDomainObjectContainer<SourceSet> by container {
             private val project by module::project
 
-            // googleTestExtension is only used if testFixtures or tests are used.
-            private val googleTestExtension by lazy { project.extensions.getByType<GoogleTestExtension>() }
-
             /**
              * Get `main` source set if it was configured.
              */
@@ -349,14 +350,14 @@
              */
             fun testFixtures(action: Action<in SourceSet>): SourceSet = create(TEST_FIXTURES_SOURCE_SET_NAME) {
                 this.inputFiles.include("**/*TestSupport.cpp", "**/*TestSupport.mm")
-                this.headersDirs.from(googleTestExtension.headersDirs)
+                this.headersDirs.from(module.owner.googleTestHeaders)
                 // TODO: Must generally depend on googletest module headers which must itself depend on sources being present.
-                dependencies.add(project.tasks.named("downloadGoogleTest"))
+                dependencies.add(project.tasks.named("unpackGoogletest"))
                 compileTask.configure {
                     this.group = VERIFICATION_BUILD_TASK_GROUP
 
                     // Without this explicit statement task dependency is not created even if it is requested in RuntimeTestingPlugin
-                    dependsOn(project.tasks.named("downloadGoogleTest"))
+                    dependsOn(project.tasks.named("unpackGoogletest"))
                 }
                 task.configure {
                     this.group = VERIFICATION_BUILD_TASK_GROUP
@@ -375,14 +376,14 @@
              */
             fun test(action: Action<in SourceSet>): SourceSet = create(TEST_SOURCE_SET_NAME) {
                 this.inputFiles.include("**/*Test.cpp", "**/*Test.mm")
-                this.headersDirs.from(googleTestExtension.headersDirs)
+                this.headersDirs.from(module.owner.googleTestHeaders)
                 // TODO: Must generally depend on googletest module headers which must itself depend on sources being present.
-                dependencies.add(project.tasks.named("downloadGoogleTest"))
+                dependencies.add(project.tasks.named("unpackGoogletest"))
                 compileTask.configure {
                     this.group = VERIFICATION_BUILD_TASK_GROUP
 
                     // Without this explicit statement task dependency is not created even if it is requested in RuntimeTestingPlugin
-                    dependsOn(project.tasks.named("downloadGoogleTest"))
+                    dependsOn(project.tasks.named("unpackGoogletest"))
                 }
                 task.configure {
                     this.group = VERIFICATION_BUILD_TASK_GROUP
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
deleted file mode 100644
index 725d705..0000000
--- a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/GitDownloadTask.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
- * that can be found in the LICENSE file.
- */
-
-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.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.net.URI
-import java.util.*
-import javax.inject.Inject
-
-/**
- * Clones the given revision of the given Git repository to the given directory.
- */
-@Suppress("UnstableApiUsage")
-abstract class GitDownloadTask @Inject constructor(
-        objects: ObjectFactory,
-        private val execOperations: ExecOperations,
-        private val fileOperations: FileOperations,
-) : DefaultTask() {
-    @get:Input
-    abstract val repository: Property<URI>
-
-    @get:Input
-    abstract val revision: Property<String>
-
-    @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")
-    @get:Input
-    val refresh: Property<Boolean> = objects.property<Boolean>().convention(false)
-
-    init {
-        /**
-         * The download task should be executed in the following cases:
-         *
-         *  - The output directory doesn't exist or is empty;
-         *  - Repository or revision was changed since the last execution;
-         *  - A user forced rerunning this tasks manually (see [GitDownloadTask.refresh]).
-         *
-         * In all other cases we consider the task UP-TO-DATE.
-         */
-        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 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.
-        outputDirectory.dir(".git").get().asFile.deleteRecursively()
-    }
-}
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
deleted file mode 100644
index 6dca23d..0000000
--- a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/RuntimeTestingPlugin.kt
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
- * that can be found in the LICENSE file.
- */
-
-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 java.net.URI
-import javax.inject.Inject
-
-open class RuntimeTestingPlugin : Plugin<Project> {
-    override fun apply(target: Project): Unit = with(target) {
-        val extension = extensions.create(GOOGLE_TEST_EXTENSION_NAME, GoogleTestExtension::class.java)
-        val downloadTask = registerDownloadTask(extension)
-
-        val googleTestRoot = extension.sourceDirectory
-
-        createBitcodeTasks(project.objects.directoryProperty().value(googleTestRoot), downloadTask)
-    }
-
-    private fun Project.registerDownloadTask(extension: GoogleTestExtension): TaskProvider<GitDownloadTask> =
-            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: DirectoryProperty,
-            downloadTask: TaskProvider<GitDownloadTask>
-    ) {
-        pluginManager.withPlugin("compile-to-bitcode") {
-            val bitcodeExtension = project.extensions.getByType<CompileToBitcodeExtension>()
-
-            bitcodeExtension.allTargets {
-                module("googletest") {
-                    sourceSets {
-                        testFixtures {
-                            inputFiles.from(googleTestRoot.dir("googletest/src"))
-                            // That's how googletest/CMakeLists.txt builds gtest library.
-                            inputFiles.include("gtest-all.cc")
-                            headersDirs.setFrom(
-                                    googleTestRoot.dir("googletest/include"),
-                                    googleTestRoot.dir("googletest")
-                            )
-                            // Fix Gradle Configuration Cache: support this task being configured before googletest sources are actually downloaded.
-                            compileTask.configure {
-                                inputFiles.setFrom(googleTestRoot.dir("googletest/src/gtest-all.cc"))
-                                dependsOn(downloadTask)
-                            }
-                        }
-                    }
-                    compilerArgs.set(listOf("-std=c++17", "-O2"))
-                    this.dependencies.add(downloadTask)
-                }
-
-                module("googlemock") {
-                    sourceSets {
-                        testFixtures {
-                            inputFiles.from(googleTestRoot.dir("googlemock/src"))
-                            // That's how googlemock/CMakeLists.txt builds gmock library.
-                            inputFiles.include("gmock-all.cc")
-                            headersDirs.setFrom(
-                                    googleTestRoot.dir("googlemock"),
-                                    googleTestRoot.dir("googlemock/include"),
-                                    googleTestRoot.dir("googletest/include"),
-                            )
-                            // Fix Gradle Configuration Cache: support this task being configured before googletest sources are actually downloaded.
-                            compileTask.configure {
-                                inputFiles.setFrom(googleTestRoot.dir("googlemock/src/gmock-all.cc"))
-                                dependsOn(downloadTask)
-                            }
-                        }
-                    }
-                    compilerArgs.set(listOf("-std=c++17", "-O2"))
-                    this.dependencies.add(downloadTask)
-                }
-            }
-        }
-    }
-
-    companion object {
-        internal const val GOOGLE_TEST_EXTENSION_NAME = "googletest"
-    }
-
-}
-
-/**
- * A project extension to configure from where we get the GoogleTest framework.
- */
-abstract class GoogleTestExtension @Inject constructor(
-        layout: ProjectLayout,
-        providers: ProviderFactory,
-        objects: ObjectFactory,
-) {
-
-    /**
-     * A repository to fetch GoogleTest from.
-     */
-    val repository: Property<String> = objects.property<String>().convention("https://github.com/google/googletest.git")
-
-    private var _revision: String? = null
-
-    /**
-     * A particular revision in the [repository] to be fetched. It can be a branch, a tag or a commit hash.
-     */
-    var revision: String
-        get() = _revision
-                ?: throw InvalidUserDataException(
-                        "No value provided for property '${RuntimeTestingPlugin.GOOGLE_TEST_EXTENSION_NAME}.revision'. " +
-                        "Please specify it in the buildscript."
-                )
-        set(value) { _revision = value }
-
-    /**
-     * Fetch the [revision] even if the [fetchDirectory] already contains it. Overwrite all changes manually made in the output directory.
-     */
-    val refresh: Property<Boolean> = objects.property<Boolean>().convention(false)
-
-    /**
-     * A directory to fetch the [revision] to.
-     */
-    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.
-     */
-    abstract val localSourceRoot: DirectoryProperty
-    val hasLocalSourceRoot: Provider<Boolean> = providers.provider { localSourceRoot.isPresent }
-
-    /**
-     * A getter for directory that contains the GTest sources.
-     * Returns a local source directory if it's specified (see [localSourceRoot]) or [fetchDirectory] otherwise.
-     */
-    val sourceDirectory: Provider<Directory> = localSourceRoot.orElse(fetchDirectory)
-
-    /**
-     * A file collection with header directories for GoogleTest and GoogleMock.
-     * Useful to configure compilation against GTest.
-     */
-    val headersDirs: FileCollection = layout.files(
-            sourceDirectory.map { it.dir("googletest/include")},
-            sourceDirectory.map { it.dir("googlemock/include")}
-    )
-}
-
diff --git a/kotlin-native/gradle.properties b/kotlin-native/gradle.properties
index 6828c85..136a787 100644
--- a/kotlin-native/gradle.properties
+++ b/kotlin-native/gradle.properties
@@ -20,12 +20,6 @@
 # A version of Xcode required to build the Kotlin/Native compiler.
 xcodeMajorVersion=26
 
-# A GTest revision used to test the runtime.
-# The latest release GTest (1.10.0) doesn't properly register skipped tests in an XML-report.
-# Therefore we use a fixed commit form the master branch where this problem is already fixed.
-# https://github.com/google/googletest/commit/07f4869221012b16b7f9ee685d94856e1fc9f361
-gtestRevision=07f4869221012b16b7f9ee685d94856e1fc9f361
-
 org.gradle.jvmargs='-Dfile.encoding=UTF-8'
 org.gradle.workers.max=4
 slackApiVersion=1.2.0
diff --git a/kotlin-native/runtime/build.gradle.kts b/kotlin-native/runtime/build.gradle.kts
index 6868431..1c8e4e2 100644
--- a/kotlin-native/runtime/build.gradle.kts
+++ b/kotlin-native/runtime/build.gradle.kts
@@ -18,19 +18,27 @@
 plugins {
     id("base")
     id("compile-to-bitcode")
-    id("runtime-testing")
 }
 
 repositories {
     githubTag("google", "breakpad")
+    githubCommit("google", "googletest")
 }
 
 val breakpad = configurations.dependencyScope("breakpad")
 val breakpadClasspath = configurations.resolvable("breakpadClasspath") {
     extendsFrom(breakpad.get())
 }
+val googletest = configurations.dependencyScope("googletest")
+val googletestClasspath = configurations.resolvable("googletestClasspath") {
+    extendsFrom(googletest.get())
+}
 dependencies {
     breakpad("google:breakpad:2024.02.16@zip")
+    // GTest 1.10.0 doesn't properly register skipped tests in an XML-report.
+    // Therefore we use a fixed commit form the master branch where this problem is already fixed.
+    // https://github.com/google/googletest/commit/07f4869221012b16b7f9ee685d94856e1fc9f361
+    googletest("google:googletest:07f4869221012b16b7f9ee685d94856e1fc9f361@zip")
 }
 
 if (HostManager.host == KonanTarget.MACOS_ARM64) {
@@ -60,15 +68,28 @@
     add(breakpadSources.name, unpackBreakpad)
 }
 
-googletest {
-    revision = project.property("gtestRevision") as String
-    refresh = project.hasProperty("refresh-gtest")
+val unpackGoogletest = tasks.register<Sync>("unpackGoogletest") {
+    from(googletestClasspath.map { zipTree(it.singleFile) })
+    eachFile {
+        relativePath = RelativePath(true, *relativePath.segments.drop(1).toTypedArray())
+    }
+    includeEmptyDirs = false
+    into(layout.buildDirectory.dir("googletest"))
+}
+
+val googleTestRoot = objects.directoryProperty().apply {
+    set(layout.dir(unpackGoogletest.map { (it.destinationDir) }))
 }
 
 val targetList = enabledTargets(extensions.getByType<PlatformManager>())
 
 // NOTE: the list of modules is duplicated in `RuntimeModule.kt`
 bitcode {
+    googleTestHeaders.from(
+            googleTestRoot.map { it.dir("googletest/include") },
+            googleTestRoot.map { it.dir("googlemock/include") }
+    )
+
     allTargets {
         val fixBrokenMacroExpansionInXcode15_3: List<String> = when (target) {
             KonanTarget.MACOS_ARM64, KonanTarget.MACOS_X64 -> hashMapOf(
@@ -378,6 +399,49 @@
             }
         }
 
+        module("googletest") {
+            sourceSets {
+                testFixtures {
+                    inputFiles.from(googleTestRoot.dir("googletest/src"))
+                    // That's how googletest/CMakeLists.txt builds gtest library.
+                    inputFiles.include("gtest-all.cc")
+                    headersDirs.setFrom(
+                            googleTestRoot.dir("googletest/include"),
+                            googleTestRoot.dir("googletest")
+                    )
+                    // Fix Gradle Configuration Cache: support this task being configured before googletest sources are actually downloaded.
+                    compileTask.configure {
+                        inputFiles.setFrom(googleTestRoot.dir("googletest/src/gtest-all.cc"))
+                        dependsOn(unpackGoogletest)
+                    }
+                }
+            }
+            compilerArgs.set(listOf("-std=c++17", "-O2"))
+            this.dependencies.add(unpackGoogletest)
+        }
+
+        module("googlemock") {
+            sourceSets {
+                testFixtures {
+                    inputFiles.from(googleTestRoot.dir("googlemock/src"))
+                    // That's how googlemock/CMakeLists.txt builds gmock library.
+                    inputFiles.include("gmock-all.cc")
+                    headersDirs.setFrom(
+                            googleTestRoot.dir("googlemock"),
+                            googleTestRoot.dir("googlemock/include"),
+                            googleTestRoot.dir("googletest/include"),
+                    )
+                    // Fix Gradle Configuration Cache: support this task being configured before googletest sources are actually downloaded.
+                    compileTask.configure {
+                        inputFiles.setFrom(googleTestRoot.dir("googlemock/src/gmock-all.cc"))
+                        dependsOn(unpackGoogletest)
+                    }
+                }
+            }
+            compilerArgs.set(listOf("-std=c++17", "-O2"))
+            this.dependencies.add(unpackGoogletest)
+        }
+
         module("test_support") {
             headersDirs.from(files("src/externalCallsChecker/common/cpp", "src/objcExport/cpp", "src/main/cpp"))
             sourceSets {