From fcf506090707b69d2aaee8a3b3fb18baacde4b0a Mon Sep 17 00:00:00 2001 From: Andrey Lozhkin Date: Wed, 15 Jul 2020 13:30:22 +0300 Subject: [PATCH] [Gradle, Cocoapods] implement DSL to support Cocoapods Dependencies from Git or Web Issue #KT-41367 Fixed --- .../jetbrains/kotlin/gradle/CocoaPodsIT.kt | 675 +++++++++++++++--- .../build.gradle.kts | 2 +- .../kotlin-library/build.gradle.kts | 4 +- .../new-mpp-cocoapods-single/build.gradle.kts | 2 +- .../kotlin-library/build.gradle.kts | 5 +- .../build.gradle | 25 + .../settings.gradle | 11 + .../src/iosMain/kotlin/A.kt | 5 + .../build.gradle.kts | 25 + .../settings.gradle | 11 + .../src/iosMain/kotlin/A.kt | 5 + .../native/cocoapods/CocoapodsExtension.kt | 152 +++- .../native/cocoapods/KotlinCocoapodsPlugin.kt | 52 +- .../native/tasks/AdvancedCocoapodsTasks.kt | 245 ++++++- 14 files changed, 1090 insertions(+), 129 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template-groovy/build.gradle create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template-groovy/settings.gradle create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template-groovy/src/iosMain/kotlin/A.kt create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template/build.gradle.kts create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template/settings.gradle create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template/src/iosMain/kotlin/A.kt diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/CocoaPodsIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/CocoaPodsIT.kt index d98121f12cd..0d70a29b4ca 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/CocoaPodsIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/CocoaPodsIT.kt @@ -6,17 +6,25 @@ package org.jetbrains.kotlin.gradle import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin +import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.DUMMY_FRAMEWORK_TASK_NAME import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_BUILD_DEPENDENCIES_TASK_NAME +import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_DOWNLOAD_TASK_NAME import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_GEN_TASK_NAME +import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_IMPORT_TASK_NAME +import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_INSTALL_TASK_NAME import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_SETUP_BUILD_TASK_NAME +import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_SPEC_TASK_NAME import org.jetbrains.kotlin.gradle.util.modify import org.jetbrains.kotlin.konan.target.HostManager import org.junit.Assume.assumeFalse import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.Ignore import org.junit.Test import java.io.File import java.util.concurrent.TimeUnit import kotlin.test.assertEquals +import kotlin.test.assertTrue import kotlin.test.fail class CocoaPodsIT : BaseGradleIT() { @@ -24,14 +32,44 @@ class CocoaPodsIT : BaseGradleIT() { override val defaultGradleVersion: GradleVersionRequired get() = GradleVersionRequired.FOR_MPP_SUPPORT - // We use Kotlin DSL. Earlier Gradle versions fail at accessors codegen. - val gradleVersion = GradleVersionRequired.FOR_MPP_SUPPORT + private val gradleVersion = GradleVersionRequired.FOR_MPP_SUPPORT val PODFILE_IMPORT_DIRECTIVE_PLACEHOLDER = "" - val cocoapodsSingleKtPod = "new-mpp-cocoapods-single" - val cocoapodsMultipleKtPods = "new-mpp-cocoapods-multiple" + private val cocoapodsSingleKtPod = "new-mpp-cocoapods-single" + private val cocoapodsMultipleKtPods = "new-mpp-cocoapods-multiple" + private val templateProjectName = "new-mpp-cocoapods-template" + private val groovyTemplateProjectName = "new-mpp-cocoapods-template-groovy" + + private val dummyTaskName = ":$DUMMY_FRAMEWORK_TASK_NAME" + private val podspecTaskName = ":$POD_SPEC_TASK_NAME" + private val podDownloadTaskName = ":$POD_DOWNLOAD_TASK_NAME" + private val podGenTaskName = ":$POD_GEN_TASK_NAME" + private val podBuildTaskName = ":$POD_BUILD_DEPENDENCIES_TASK_NAME" + private val podSetupBuildTaskName = ":$POD_SETUP_BUILD_TASK_NAME" + private val podImportTaskName = ":$POD_IMPORT_TASK_NAME" + private val podInstallTaskName = ":$POD_INSTALL_TASK_NAME" + private val cinteropTaskName = ":cinterop" + + private val defaultPodRepo = "https://github.com/AFNetworking/AFNetworking" + private val defaultPodName = "AFNetworking" + private val defaultTarget = "IOS" + + private val defaultPodDownloadTaskName = podDownloadTaskName + defaultPodName + private val defaultPodGenTaskName = podGenTaskName + defaultTarget + private val defaultBuildTaskName = podBuildTaskName + defaultTarget + private val defaultSetupBuildTaskName = podSetupBuildTaskName + defaultTarget + private val defaultCinteropTaskName = cinteropTaskName + defaultPodName + defaultTarget + + private lateinit var hooks: CustomHooks + private lateinit var project: BaseGradleIT.Project + + @Before + fun configure() { + hooks = CustomHooks() + project = getProjectByName(templateProjectName) + } @Test fun testPodspecSingle() = doTestPodspec( @@ -79,31 +117,20 @@ class CocoaPodsIT : BaseGradleIT() { ) @Test - fun testPodImportUseFrameworksSingle() = doTestPodImport( - cocoapodsSingleKtPod, - "ios-app", - ImportMode.FRAMEWORKS, - mapOf("kotlin-library" to null) - ) + fun testPodImportUseFrameworksSingle() = doTestImportSingle(ImportMode.FRAMEWORKS) @Test - fun testPodImportUseModularHeadersSingle() = - doTestPodImport( - cocoapodsSingleKtPod, - "ios-app", - ImportMode.MODULAR_HEADERS, - mapOf("kotlin-library" to null) - ) + fun testPodImportUseModularHeadersSingle() = doTestImportSingle(ImportMode.MODULAR_HEADERS) @Test - fun testPodspecMupltiple() = doTestPodspec( + fun testPodspecMultiple() = doTestPodspec( cocoapodsMultipleKtPods, mapOf("kotlin-library" to null, "second-library" to null), mapOf("kotlin-library" to kotlinLibraryPodspecContent(), "second-library" to secondLibraryPodspecContent()), ) @Test - fun testPodspecCustomFrameworkNameMupltiple() = doTestPodspec( + fun testPodspecCustomFrameworkNameMultiple() = doTestPodspec( cocoapodsMultipleKtPods, mapOf("kotlin-library" to "FirstMultiplatformLibrary", "second-library" to "SecondMultiplatformLibrary"), mapOf( @@ -113,7 +140,7 @@ class CocoaPodsIT : BaseGradleIT() { ) @Test - fun testXcodeUseFrameworksMupltiple() = doTestXcode( + fun testXcodeUseFrameworksMultiple() = doTestXcode( cocoapodsMultipleKtPods, ImportMode.FRAMEWORKS, "ios-app", @@ -121,7 +148,7 @@ class CocoaPodsIT : BaseGradleIT() { ) @Test - fun testXcodeUseFrameworksWithCustomFrameworkNameMupltiple() = doTestXcode( + fun testXcodeUseFrameworksWithCustomFrameworkNameMultiple() = doTestXcode( cocoapodsMultipleKtPods, ImportMode.FRAMEWORKS, "ios-app", @@ -129,7 +156,7 @@ class CocoaPodsIT : BaseGradleIT() { ) @Test - fun testXcodeUseModularHeadersMupltiple() = doTestXcode( + fun testXcodeUseModularHeadersMultiple() = doTestXcode( cocoapodsMultipleKtPods, ImportMode.MODULAR_HEADERS, "ios-app", @@ -137,7 +164,7 @@ class CocoaPodsIT : BaseGradleIT() { ) @Test - fun testXcodeUseModularHeadersWithCustomFrameworkNameMupltiple() = doTestXcode( + fun testXcodeUseModularHeadersWithCustomFrameworkNameMultiple() = doTestXcode( cocoapodsMultipleKtPods, ImportMode.MODULAR_HEADERS, "ios-app", @@ -145,47 +172,528 @@ class CocoaPodsIT : BaseGradleIT() { ) @Test - fun testPodImportUseFrameworksMupltiple() = doTestPodImport( - cocoapodsMultipleKtPods, - "ios-app", - ImportMode.FRAMEWORKS, - mapOf("kotlin-library" to null, "second-library" to null) - ) + fun testPodImportUseFrameworksMultiple() = doTestImportMultiple(ImportMode.FRAMEWORKS) @Test - fun testPodImportUseModularHeadersMupltiple() = - doTestPodImport( - cocoapodsMultipleKtPods, - "ios-app", - ImportMode.MODULAR_HEADERS, - mapOf("kotlin-library" to null, "second-library" to null) + fun testPodImportUseModularHeadersMultiple() = doTestImportMultiple(ImportMode.MODULAR_HEADERS) + + @Test + fun testSpecReposDownloads() { + val podName = "example" + val podRepo = "https://github.com/alozhkin/spec_repo_example" + with(project.gradleBuildScript()) { + addPod(podName) + addSpecRepo(podRepo) + } + project.testDownload(listOf(podRepo)) + } + + @Test + fun testPodDownloadGitNoTagNorCommit() { + doTestGit() + } + + @Test + fun testPodDownloadGitTag() { + doTestGit(tag = "4.0.0") + } + + @Test + fun testPodDownloadGitCommit() { + doTestGit(commit = "9c07ac0a5645abb58850253eeb109ed0dca515c1") + } + + @Test + fun testPodDownloadGitBranch() { + doTestGit(branch = "2974") + } + + @Test + fun testPodDownloadGitBranchAndCommit() { + val branch = "2974" + val commit = "21637dd6164c0641e414bdaf3885af6f1ef15aee" + with(project.gradleBuildScript()) { + addPod(defaultPodName, produceGitBlock(defaultPodRepo, branchName = branch, commitName = commit)) + } + hooks.addHook { + checkGitRepo(commitName = commit) + } + project.testDownload(listOf(defaultPodRepo)) + } + + // tag priority is bigger than branch priority + @Test + fun testPodDownloadGitBranchAndTag() { + val branch = "2974" + val tag = "4.0.0" + with(project.gradleBuildScript()) { + addPod(defaultPodName, produceGitBlock(defaultPodRepo, branchName = branch, tagName = tag)) + } + hooks.addHook { + checkGitRepo(tagName = tag) + } + project.testDownload(listOf(defaultPodRepo)) + } + + @Test + fun testPodDownloadUrlZip() = doTestPodDownloadUrl("zip") + + @Test + fun testPodDownloadUrlTar() = doTestPodDownloadUrl("tar") + + @Test + fun testPodDownloadUrlGZ() = doTestPodDownloadUrl("tar.gz") + + @Test + fun testPodDownloadUrlBZ2() = doTestPodDownloadUrl("tar.bz2") + + @Test + fun testPodDownloadUrlJar() = doTestPodDownloadUrl("jar") + + @Test + fun testPodDownloadUrlWrongName() = doTestPodDownloadUrl(fileExtension = "zip", archiveName = "wrongName") + + @Test + fun testDownloadAndImport() { + val tag = "4.0.0" + with(project.gradleBuildScript()) { + addPod(defaultPodName, produceGitBlock(defaultPodRepo, tagName = tag)) + } + hooks.addHook { + checkGitRepo(tagName = tag) + } + project.testImportWithAsserts(listOf(defaultPodRepo)) + } + + @Test + fun warnIfDeprecatedPodspecPathIsUsed() { + project = getProjectByName(cocoapodsSingleKtPod) + hooks.addHook { + assertContains("Please use directory with podspec file, not podspec file itself") + } + project.test(":kotlin-library:podDownload") + } + + @Ignore + @Test + fun testSubspecWithModuleName() { + with(project.gradleBuildScript()) { + addPod("AFNetworking/Reachability", "moduleName = \"CustomName\"") + addPod("AFNetworking/Security") + } + hooks.addHook { + assertTasksExecuted(defaultCinteropTaskName, cinteropTaskName + "AFNetworking/Security" + defaultTarget) + } + project.testImportWithAsserts() + } + + + // up-to-date tests + + @Test + fun testDummyUTD() { + hooks.addHook { + assertTasksExecuted(dummyTaskName) + } + project.testWithWrapper(dummyTaskName) + + hooks.rewriteHooks { + assertTasksUpToDate(dummyTaskName) + } + project.testWithWrapper(dummyTaskName) + } + + @Test + fun testPodDownloadUTDWithoutPods() { + hooks.addHook { + assertTasksUpToDate(podDownloadTaskName) + } + project.testWithWrapper(podDownloadTaskName) + } + + @Test + fun basicUTDTest() { + val tasks = listOf( + podspecTaskName, + defaultPodDownloadTaskName, + defaultPodGenTaskName, + defaultSetupBuildTaskName, + defaultBuildTaskName, + defaultCinteropTaskName ) + with(project.gradleBuildScript()) { + addPod(defaultPodName, produceGitBlock(defaultPodRepo)) + } + hooks.addHook { + assertTasksExecuted(tasks) + } + project.testImportWithAsserts(listOf(defaultPodRepo)) + + hooks.rewriteHooks { + assertTasksUpToDate(tasks) + } + project.testImport(listOf(defaultPodRepo)) + } @Test - fun testPodImportCustomFrameworkName() = doTestPodImport( - cocoapodsSingleKtPod, - "ios-app", - ImportMode.FRAMEWORKS, - mapOf("kotlin-library" to "foobaz") - ) + fun testSpecReposUTD() { + with(project.gradleBuildScript()) { + addPod("AFNetworking") + } + hooks.addHook { + assertTasksExecuted(defaultPodGenTaskName) + } + project.testSynthetic(defaultPodGenTaskName) + with(project.gradleBuildScript()) { + addSpecRepo("https://github.com/alozhkin/spec_repo_example.git") + } + project.testSynthetic(defaultPodGenTaskName) + hooks.rewriteHooks { + assertTasksUpToDate(defaultPodGenTaskName) + } + project.testSynthetic(defaultPodGenTaskName) + } + + + // groovy tests @Test - fun testSkippingTasksOnOtherHosts() { - assumeFalse(HostManager.hostIsMac) + fun testGroovyDownloadAndImport() { + val project = getProjectByName(groovyTemplateProjectName) + val tag = "4.0.0" + with(project.gradleBuildScript()) { + addPod(defaultPodName, produceGitBlock(defaultPodRepo, tagName = tag)) + } + hooks.addHook { + checkGitRepo(tagName = tag) + } + project.testImportWithAsserts(listOf(defaultPodRepo)) + } - with(transformProjectWithPluginsDsl(cocoapodsSingleKtPod, gradleVersion)) { - val syntheticTasks = arrayOf( - ":podInstall", - ":kotlin-library:podGenIOS", - ":kotlin-library:podSetupBuildIOS", - ":kotlin-library:podBuildDependenciesIOS", - ":kotlin-library:podImport" + + // other tests + + @Test + fun testDownloadUrlTestSupportDashInNames() { + val fileExtension = "tar.gz" + val podName = "Pod-with-dashes" + val repoPath = "https://github.com/alozhkin/Pod-with-dashes/raw/master" + val flatten = true + val repo = "$repoPath/$podName.$fileExtension" + with(project.gradleBuildScript()) { + addPod( + podName, + """source = url("$repo", $flatten) + |moduleName = "Pod_with_dashes" + """.trimMargin() ) + } + hooks.addHook { + assertTrue(url().resolve(podName).exists()) + } + project.testImportWithAsserts(listOf(repo)) + } - build(*syntheticTasks, "-Pkotlin.native.cocoapods.generate.wrapper=true") { - // Check that tasks working with synthetic projects are skipped on non-mac hosts. - assertSuccessful() - assertTasksSkipped(*syntheticTasks) + + // paths + + private fun CompiledProject.url() = externalSources().resolve("url") + + private fun CompiledProject.git() = externalSources().resolve("git") + + private fun CompiledProject.externalSources() = + fileInWorkingDir("build").resolve("cocoapods").resolve("externalSources") + + + // test configuration phase + + private class CustomHooks() { + private val hooks = mutableSetOf Unit>() + + fun addHook(hook: CompiledProject.() -> Unit) { + hooks.add(hook) + } + + fun rewriteHooks(hook: CompiledProject.() -> Unit) { + hooks.clear() + hooks.add(hook) + } + + fun trigger(project: CompiledProject) { + hooks.forEach { function -> + project.function() + } + } + } + + private fun doTestImportSingle(importMode: ImportMode) { + val project = getProjectByName(cocoapodsSingleKtPod) + val subprojects = listOf("kotlin-library") + doTestPodImport(project, subprojects, importMode) + } + + private fun doTestImportMultiple(importMode: ImportMode) { + val project = getProjectByName(cocoapodsMultipleKtPods) + val subprojects = listOf("kotlin-library", "second-library") + doTestPodImport(project, subprojects, importMode) + } + + private fun doTestPodImport(project: BaseGradleIT.Project, subprojects: List, importMode: ImportMode) { + with(project) { + preparePodfile("ios-app", importMode) + } + project.testImportWithAsserts() + subprojects.forEach { + hooks.rewriteHooks { + podImportAsserts(it) + } + project.testSynthetic(":$it:podImport") + } + } + + private fun doTestGit( + repo: String = defaultPodRepo, + pod: String = defaultPodName, + branch: String? = null, + commit: String? = null, + tag: String? = null + ) { + with(project.gradleBuildScript()) { + addPod(pod, produceGitBlock(repo, branch, commit, tag)) + } + hooks.addHook { + checkGitRepo(branch, commit, tag, pod) + } + project.testDownload(listOf(repo)) + } + + fun doTestPodDownloadUrl( + fileExtension: String, + podName: String = "podspecWithFilesExample", + repoPath: String = "https://github.com/alozhkin/podspecWithFilesExample/raw/master", + archiveName: String = podName, + flatten: Boolean = false + ) { + val repo = "$repoPath/$archiveName.$fileExtension" + with(project.gradleBuildScript()) { + addPod(podName, "source = url(\"$repo\", $flatten)") + } + hooks.addHook { + assertTrue(url().resolve(podName).exists()) + } + project.testImportWithAsserts(listOf(repo)) + } + + private fun Project.testImportWithAsserts( + repos: List = listOf(), + vararg args: String + ) { + hooks.addHook { + podImportAsserts() + } + testImport(repos, *args) + } + + private fun Project.testImport( + repos: List = listOf(), + vararg args: String + ) { + for (repo in repos) { + assumeTrue(isRepoAvailable(repo)) + } + testSynthetic(podImportTaskName, *args) + } + + private fun Project.testDownload( + repos: List, + vararg args: String + ) { + for (repo in repos) { + assumeTrue(isRepoAvailable(repo)) + } + test(podDownloadTaskName, *args) + } + + private fun Project.testSynthetic( + taskName: String, + vararg args: String + ) { + assumeTrue(KotlinCocoapodsPlugin.isAvailableToProduceSynthetic) + testWithWrapper(taskName, *args) + } + + private fun Project.testWithWrapper( + taskName: String, + vararg args: String + ) { + test(taskName, "-Pkotlin.native.cocoapods.generate.wrapper=true", *args) + } + + private fun Project.test( + taskName: String, + vararg args: String + ) { + // check that test executable + assumeTrue(HostManager.hostIsMac) + build(taskName, *args) { + //base checks + assertSuccessful() + hooks.trigger(this) + } + } + + private fun getProjectByName(projectName: String) = transformProjectWithPluginsDsl(projectName, gradleVersion) + + + // build script configuration phase + + private fun File.addPod(podName: String, configuration: String? = null) { + val pod = "pod(\"$podName\")" + val podBlock = configuration?.wrap(pod) ?: pod + appendToCocoapodsBlock(podBlock) + } + + private fun File.addSpecRepo(specRepo: String) = appendToCocoapodsBlock("url(\"$specRepo\")".wrap("specRepos")) + + private fun File.appendToKotlinBlock(str: String) = appendLine(str.wrap("kotlin")) + + private fun File.appendToCocoapodsBlock(str: String) = appendToKotlinBlock(str.wrap("cocoapods")) + + private fun String.wrap(s: String): String = """ + |$s { + | $this + |} + """.trimMargin() + + private fun File.appendLine(s: String) = appendText("\n$s") + + private fun produceGitBlock( + repo: String = defaultPodRepo, + branchName: String? = null, + commitName: String? = null, + tagName: String? = null + ): String { + val branch = if (branchName != null) "branch = \"$branchName\"" else "" + val commit = if (commitName != null) "commit = \"$commitName\"" else "" + val tag = if (tagName != null) "tag = \"$tagName\"" else "" + return """source = git("$repo") { + | $branch + | $commit + | $tag + |} + """.trimMargin() + } + + + // proposition phase + + private fun CompiledProject.checkGitRepo( + branchName: String? = null, + commitName: String? = null, + tagName: String? = null, + aPodDownloadName: String = defaultPodName + ) { + val gitDir = git().resolve(aPodDownloadName) + val podspecFile = gitDir.resolve("$aPodDownloadName.podspec") + assertTrue(podspecFile.exists()) + if (tagName != null) { + checkTag(gitDir, tagName) + } + checkPresentCommits(gitDir, commitName) + if (branchName != null) { + checkBranch(gitDir, branchName) + } + } + + private fun checkTag(gitDir: File, tagName: String) { + runCommand( + gitDir, + "git", "name-rev", + "--tags", + "--name-only", + "HEAD" + ) { + val (retCode, out, errorMessage) = this + assertEquals(0, retCode, errorMessage) + assertTrue(out.contains(tagName), errorMessage) + } + } + + private fun checkPresentCommits(gitDir: File, commitName: String?) { + runCommand( + gitDir, + "git", "log", "--pretty=oneline" + ) { + val (retCode, out, _) = this + assertEquals(0, retCode) + // get rid of '\n' at the end + assertEquals(1, out.trimEnd().lines().size) + if (commitName != null) { + assertEquals(commitName, out.substringBefore(" ")) + } + } + } + + private fun checkBranch(gitDir: File, branchName: String) { + runCommand( + gitDir, + "git", "show-branch" + ) { + val (retCode, out, errorMessage) = this + assertEquals(0, retCode) + // get rid of '\n' at the end + assertEquals(1, out.trimEnd().lines().size, errorMessage) + assertEquals("[$branchName]", out.substringBefore(" "), errorMessage) + } + } + + private fun isRepoAvailable(repo: String): Boolean { + var responseCode = 0 + runCommand( + File("/"), + "curl", + "-s", + "-o", + "/dev/null", + "-w", + "%{http_code}", + "-L", + repo, + "--retry", "2" + ) { + val (retCode, out, errorMessage) = this + assertEquals(0, retCode, errorMessage) + responseCode = out.toInt() + } + return responseCode == 200 + } + + private fun CompiledProject.podImportAsserts(projectName: String? = null) { + + val buildScriptText = project.gradleBuildScript(projectName).readText() + val taskPrefix = projectName?.let { ":$it" } ?: "" + val podspec = "podspec" + val podInstall = "podInstall" + assertSuccessful() + + if ("noPodspec()" in buildScriptText) { + assertTasksSkipped("$taskPrefix:$podspec") + } + + if ("podfile" in buildScriptText) { + assertTasksExecuted("$taskPrefix:$podInstall") + } else { + assertTasksSkipped("$taskPrefix:$podInstall") + } + assertTasksRegisteredByPrefix(listOf("$taskPrefix:$POD_GEN_TASK_NAME")) + if (buildScriptText.matches("pod\\(.*\\)".toRegex())) { + assertTasksExecutedByPrefix(listOf("$taskPrefix:$POD_GEN_TASK_NAME")) + } + + with(listOf(POD_SETUP_BUILD_TASK_NAME, POD_BUILD_DEPENDENCIES_TASK_NAME).map { "$taskPrefix:$it" }) { + if (buildScriptText.matches("pod\\(.*\\)".toRegex())) { + assertTasksRegisteredByPrefix(this) + assertTasksExecutedByPrefix(this) } } } @@ -249,65 +757,6 @@ class CocoaPodsIT : BaseGradleIT() { } } - private fun doTestPodImport( - projectName: String, - iosAppLocation: String, - mode: ImportMode, - subprojectsToFrameworkNamesMap: Map, - ) { - assumeTrue(HostManager.hostIsMac) - assumeTrue(KotlinCocoapodsPlugin.isAvailableToProduceSynthetic) - - val subprojects = subprojectsToFrameworkNamesMap.keys - val gradleProject = transformProjectWithPluginsDsl(projectName, gradleVersion) - - subprojectsToFrameworkNamesMap.forEach { subproject, frameworkName -> - frameworkName?.let { - gradleProject.useCustomFrameworkName(subproject, it, iosAppLocation) - } - } - - with(gradleProject) { - preparePodfile(iosAppLocation, mode) - podImportAsserts() - subprojects.forEach { podImportAsserts(it) } - } - } - - private fun BaseGradleIT.Project.podImportAsserts(subproject: String? = null) { - val buildScriptText = gradleBuildScript(subproject).readText() - val taskPrefix = subproject?.let { ":$it" } ?: "" - val podImport = "podImport" - val podspec = "podspec" - val podInstall = "podInstall" - - build("$taskPrefix:$podImport", "-Pkotlin.native.cocoapods.generate.wrapper=true") { - assertSuccessful() - - if ("noPodspec()" in buildScriptText) { - assertTasksSkipped("$taskPrefix:$podspec") - } - - if ("podfile" in buildScriptText) { - assertTasksExecuted("$taskPrefix:$podInstall") - } else { - assertTasksSkipped("$taskPrefix:$podInstall") - } - assertTasksRegisteredByPrefix(listOf("$taskPrefix:$POD_GEN_TASK_NAME")) - if (buildScriptText.matches("pod\\(.*\\)".toRegex())) { - assertTasksExecutedByPrefix(listOf("$taskPrefix:$POD_GEN_TASK_NAME")) - } - - with(listOf(POD_SETUP_BUILD_TASK_NAME, POD_BUILD_DEPENDENCIES_TASK_NAME).map { "$taskPrefix:$it" }) { - if (buildScriptText.matches("pod\\(.*\\)".toRegex())) { - assertTasksRegisteredByPrefix(this) - assertTasksExecutedByPrefix(this) - } - } - } - } - - private enum class ImportMode(val directive: String) { FRAMEWORKS("use_frameworks!"), MODULAR_HEADERS("use_modular_headers!") diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-multiple/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-multiple/build.gradle.kts index e4a80c9f0ae..5e309cd9a38 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-multiple/build.gradle.kts +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-multiple/build.gradle.kts @@ -20,6 +20,6 @@ kotlin { cocoapods { noPodspec() - podfile = projectDir.resolve("ios-app/Podfile") + podfile = project.file("ios-app/Podfile") } } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-multiple/kotlin-library/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-multiple/kotlin-library/build.gradle.kts index cd7e5bc51cd..e209f948a73 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-multiple/kotlin-library/build.gradle.kts +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-multiple/kotlin-library/build.gradle.kts @@ -21,7 +21,7 @@ kotlin { cocoapods { summary = "CocoaPods test library" homepage = "https://github.com/JetBrains/kotlin" - pod("pod_dependency", "1.0", projectDir.resolve("../pod_dependency/pod_dependency.podspec")) - pod("subspec_dependency/Core", "1.0", projectDir.resolve("../subspec_dependency/subspec_dependency.podspec")) + pod("pod_dependency", "1.0", project.file("../pod_dependency")) + pod("subspec_dependency/Core", "1.0", project.file("../subspec_dependency")) } } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-single/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-single/build.gradle.kts index e4a80c9f0ae..5e309cd9a38 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-single/build.gradle.kts +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-single/build.gradle.kts @@ -20,6 +20,6 @@ kotlin { cocoapods { noPodspec() - podfile = projectDir.resolve("ios-app/Podfile") + podfile = project.file("ios-app/Podfile") } } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-single/kotlin-library/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-single/kotlin-library/build.gradle.kts index cd7e5bc51cd..4fed4336c55 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-single/kotlin-library/build.gradle.kts +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-single/kotlin-library/build.gradle.kts @@ -21,7 +21,8 @@ kotlin { cocoapods { summary = "CocoaPods test library" homepage = "https://github.com/JetBrains/kotlin" - pod("pod_dependency", "1.0", projectDir.resolve("../pod_dependency/pod_dependency.podspec")) - pod("subspec_dependency/Core", "1.0", projectDir.resolve("../subspec_dependency/subspec_dependency.podspec")) + // test warnIfDeprecatedPodspecPathIsUsed depends on deprecated api usage + pod("pod_dependency", "1.0", project.file("../pod_dependency/pod_dependency.podspec")) + pod("subspec_dependency/Core", "1.0", project.file("../subspec_dependency")) } } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template-groovy/build.gradle b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template-groovy/build.gradle new file mode 100644 index 00000000000..03b532d54c1 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template-groovy/build.gradle @@ -0,0 +1,25 @@ +plugins { + id("org.jetbrains.kotlin.multiplatform").version("") + id("org.jetbrains.kotlin.native.cocoapods").version("") +} + +group = "com.example" +version = "1.0" + +repositories { + mavenLocal() + jcenter() + maven { setUrl("https://dl.bintray.com/kotlin/kotlinx.html/") } +} + +group = "org.jetbrains.kotlin.sample.native" +version = "1.0" + +kotlin { + iosX64("iOS") + cocoapods { + homepage = "https://github.com/JetBrains/kotlin" + summary = "CocoaPods test library" + ios.deploymentTarget = "13.5" + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template-groovy/settings.gradle b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template-groovy/settings.gradle new file mode 100644 index 00000000000..4080de9fec6 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template-groovy/settings.gradle @@ -0,0 +1,11 @@ +pluginManagement { + repositories { + mavenLocal() + jcenter() + gradlePluginPortal() + } +} + +enableFeaturePreview('GRADLE_METADATA') + +rootProject.name = "cocoapods" \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template-groovy/src/iosMain/kotlin/A.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template-groovy/src/iosMain/kotlin/A.kt new file mode 100644 index 00000000000..9ceb42831c5 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template-groovy/src/iosMain/kotlin/A.kt @@ -0,0 +1,5 @@ +package testProject.`new-mpp-cocoapods-template-groovy`.src.iosMain.kotlin + +fun foo() { + println("hi!") +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template/build.gradle.kts new file mode 100644 index 00000000000..c87938aca45 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id("org.jetbrains.kotlin.multiplatform").version("") + id("org.jetbrains.kotlin.native.cocoapods").version("") +} + +group = "com.example" +version = "1.0" + +repositories { + mavenLocal() + jcenter() + maven { setUrl("https://dl.bintray.com/kotlin/kotlinx.html/") } +} + +group = "org.jetbrains.kotlin.sample.native" +version = "1.0" + +kotlin { + iosX64("iOS") + cocoapods { + homepage = "https://github.com/JetBrains/kotlin" + summary = "CocoaPods test library" + ios.deploymentTarget = "13.5" + } +} diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template/settings.gradle b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template/settings.gradle new file mode 100644 index 00000000000..4080de9fec6 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template/settings.gradle @@ -0,0 +1,11 @@ +pluginManagement { + repositories { + mavenLocal() + jcenter() + gradlePluginPortal() + } +} + +enableFeaturePreview('GRADLE_METADATA') + +rootProject.name = "cocoapods" \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template/src/iosMain/kotlin/A.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template/src/iosMain/kotlin/A.kt new file mode 100644 index 00000000000..bb4f2e4422f --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-cocoapods-template/src/iosMain/kotlin/A.kt @@ -0,0 +1,5 @@ +package testProject.`new-mpp-cocoapods-template`.src.iosMain.kotlin + +fun foo() { + println("hi!") +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/CocoapodsExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/CocoapodsExtension.kt index 2c4e6a581cb..9b260ffb4a3 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/CocoapodsExtension.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/CocoapodsExtension.kt @@ -6,11 +6,15 @@ @file:Suppress("PackageDirectoryMismatch") // Old package for compatibility package org.jetbrains.kotlin.gradle.plugin.cocoapods +import groovy.lang.Closure import org.gradle.api.Named import org.gradle.api.NamedDomainObjectSet import org.gradle.api.Project import org.gradle.api.tasks.* +import org.gradle.util.ConfigureUtil +import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.* import java.io.File +import java.net.URI open class CocoapodsExtension(private val project: Project) { @get:Input @@ -80,6 +84,9 @@ open class CocoapodsExtension(private val project: Project) { @Input var frameworkName: String = project.name.asValidFrameworkName() + @get:Nested + internal val specRepos = SpecRepos() + private val _pods = project.container(CocoapodsDependency::class.java) // For some reason Gradle doesn't consume the @Nested annotation on NamedDomainObjectContainer. @@ -99,19 +106,140 @@ open class CocoapodsExtension(private val project: Project) { * Add a CocoaPods dependency to the pod built from this project. */ @JvmOverloads - fun pod(name: String, version: String? = null, podspec: File? = null, moduleName: String = name.asModuleName()) { + fun pod(name: String, version: String? = null, path: File? = null, moduleName: String = name.asModuleName()) { + // Empty string will lead to an attempt to create two podDownload tasks. + // One is original podDownload and second is podDownload + pod.name + var podSource = path + if (path != null && !path.isDirectory) { + project.logger.warn("Please use directory with podspec file, not podspec file itself") + podSource = path.parentFile + } + require(name.isNotEmpty()) { "Please provide not empty pod name to avoid ambiguity" } + addToPods(CocoapodsDependency(name, moduleName, version, podSource?.let { Path(it) })) + } + + + /** + * Add a CocoaPods dependency to the pod built from this project. + */ + fun pod(name: String, configure: CocoapodsDependency.() -> Unit) { + // Empty string will lead to an attempt to create two podDownload tasks. + // One is original podDownload and second is podDownload + pod.name + require(name.isNotEmpty()) { "Please provide not empty pod name to avoid ambiguity" } + val dependency = CocoapodsDependency(name, name.split("/")[0]) + dependency.configure() + addToPods(dependency) + } + + /** + * Add a CocoaPods dependency to the pod built from this project. + */ + fun pod(name: String, configure: Closure<*>) = pod(name) { + ConfigureUtil.configure(configure, this) + } + + private fun addToPods(dependency: CocoapodsDependency) { + val name = dependency.name check(_pods.findByName(name) == null) { "Project already has a CocoaPods dependency with name $name" } - _pods.add(CocoapodsDependency(name, version, podspec, moduleName)) + _pods.add(dependency) + } + + /** + * Add spec repositories (note that spec repository is different from usual git repository). + * Please refer to cocoapods documentation + * for additional information. + * Default sources (cdn.cocoapods.org) implicitly included. + */ + fun specRepos(configure: SpecRepos.() -> Unit) = specRepos.configure() + + /** + * Add spec repositories (note that spec repository is different from usual git repository). + * Please refer to cocoapods documentation + * for additional information. + * Default sources (cdn.cocoapods.org) implicitly included. + */ + fun specRepos(configure: Closure<*>) = specRepos { + ConfigureUtil.configure(configure, this) } data class CocoapodsDependency( private val name: String, - @get:Optional @get:Input val version: String?, - @get:Optional @get:InputFile val podspec: File?, - @get:Input val moduleName: String + @get:Input var moduleName: String, + @get:Optional @get:Input var version: String? = null, + @get:Optional @get:Nested var source: PodLocation? = null ) : Named { @Input override fun getName(): String = name + + /** + * Url to archived (tar, jar, zip) pod folder, that should contain the podspec file and all sources required by it. + * + * Archive name should match pod name. + * + * @param url url to tar, jar or zip archive. + * @param flatten does archive contains subdirectory that needs to be expanded + */ + @JvmOverloads + fun url(url: String, flatten: Boolean = false): PodLocation = Url(URI(url), flatten) + + /** + * Path to local pod + */ + fun path(podspecDirectory: String): PodLocation = Path(File(podspecDirectory)) + + /** + * Path to local pod + */ + fun path(podspecDirectory: File): PodLocation = Path(podspecDirectory) + + /** + * Configure pod from git repository. The podspec file is expected to be in the repository root. + */ + @JvmOverloads + fun git(url: String, configure: (Git.() -> Unit)? = null): PodLocation { + val git = Git(URI(url)) + if (configure != null) { + git.configure() + } + return git + } + + /** + * Configure pod from git repository. The podspec file is expected to be in the repository root. + */ + fun git(url: String, configure: Closure<*>) = git(url) { + ConfigureUtil.configure(configure, this) + } + + sealed class PodLocation { + internal abstract fun getLocalPath(project: Project, podName: String): String + + data class Url( + @get:Input val url: URI, + @get:Input var flatten: Boolean + ) : PodLocation() { + override fun getLocalPath(project: Project, podName: String): String { + return project.cocoapodsBuildDirs.externalSources("url").resolve(podName).absolutePath + } + } + + data class Path(@get:InputDirectory val dir: File) : PodLocation() { + override fun getLocalPath(project: Project, podName: String): String { + return dir.absolutePath + } + } + + data class Git( + @get:Input val url: URI, + @get:Input @get:Optional var branch: String? = null, + @get:Input @get:Optional var tag: String? = null, + @get:Input @get:Optional var commit: String? = null + ) : PodLocation() { + override fun getLocalPath(project: Project, podName: String): String { + return project.cocoapodsBuildDirs.externalSources("git").resolve(podName).absolutePath + } + } + } } data class PodspecPlatformSettings( @@ -123,6 +251,20 @@ open class CocoapodsExtension(private val project: Project) { override fun getName(): String = name } + class SpecRepos { + @get:Internal + internal val specRepos = mutableSetOf() + + fun url(url: String) { + specRepos.add(URI(url)) + } + + @Input + internal fun getAll(): Collection { + return specRepos + } + } + companion object { private fun String.asModuleName() = this .split("/")[0] // Pick the module name from a subspec name. diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/KotlinCocoapodsPlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/KotlinCocoapodsPlugin.kt index 1a70917956e..8f7d602b9fa 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/KotlinCocoapodsPlugin.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/KotlinCocoapodsPlugin.kt @@ -15,10 +15,13 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation import org.jetbrains.kotlin.gradle.plugin.addExtension +import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency +import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.* import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.plugin.whenEvaluated import org.jetbrains.kotlin.gradle.targets.native.tasks.* import org.jetbrains.kotlin.gradle.tasks.* +import org.jetbrains.kotlin.gradle.tasks.registerTask import org.jetbrains.kotlin.gradle.utils.asValidTaskName import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName import org.jetbrains.kotlin.konan.target.HostManager @@ -41,11 +44,16 @@ internal class CocoapodsBuildDirs(val project: Project) { val buildSettings: File get() = root.resolve("buildSettings") - private val synthetic: File + val synthetic: File get() = root.resolve("synthetic") fun synthetic(kotlinNativeTarget: KotlinNativeTarget) = synthetic.resolve(kotlinNativeTarget.name) + val externalSources: File + get() = root.resolve("externalSources") + + fun externalSources(fileName: String) = externalSources.resolve(fileName) + fun fatFramework(buildType: String) = root.resolve("fat-frameworks/${buildType.toLowerCase()}") } @@ -61,13 +69,18 @@ private val KotlinNativeTarget.toSetupBuildTaskName: String disambiguationClassifier ) - private val KotlinNativeTarget.toBuildDependenciesTaskName: String get() = lowerCamelCaseName( KotlinCocoapodsPlugin.POD_BUILD_DEPENDENCIES_TASK_NAME, disambiguationClassifier ) +private val CocoapodsDependency.toPodDownloadTaskName: String + get() = lowerCamelCaseName( + KotlinCocoapodsPlugin.POD_DOWNLOAD_TASK_NAME, + name + ) + open class KotlinCocoapodsPlugin : Plugin { private fun KotlinMultiplatformExtension.supportedTargets() = targets @@ -186,7 +199,6 @@ open class KotlinCocoapodsPlugin : Plugin { ) { val moduleNames = mutableSetOf() - cocoapodsExtension.pods.all { pod -> if (moduleNames.contains(pod.moduleName)) { return@all @@ -317,12 +329,41 @@ open class KotlinCocoapodsPlugin : Plugin { } } + private fun registerPodDownloadTask( + project: Project, + cocoapodsExtension: CocoapodsExtension + ) { + val downloadAllTask = project.tasks.register(POD_DOWNLOAD_TASK_NAME) { + it.group = TASK_GROUP + it.description = "Downloads CocoaPods dependencies from external sources" + } + + cocoapodsExtension.pods.all { pod -> + val podSource = pod.source + val downloadPodTask = when (podSource) { + is Git -> project.tasks.register(pod.toPodDownloadTaskName, PodDownloadGitTask::class.java) { + it.cocoapodsExtension = cocoapodsExtension + it.podName = project.provider { pod.name } + it.podSource = project.provider { podSource as Git } + } + is Url -> project.tasks.register(pod.toPodDownloadTaskName, PodDownloadUrlTask::class.java) { + it.cocoapodsExtension = cocoapodsExtension + it.podName = project.provider { pod.name } + it.podSource = project.provider { podSource as Url } + } + else -> return@all + } + + downloadAllTask.dependsOn(downloadPodTask) + } + } + private fun registerPodGenTask( project: Project, kotlinExtension: KotlinMultiplatformExtension, cocoapodsExtension: CocoapodsExtension ) { val podspecTaskProvider = project.tasks.named(POD_SPEC_TASK_NAME, PodspecTask::class.java) - + val downloadPods = project.tasks.named(POD_DOWNLOAD_TASK_NAME) kotlinExtension.supportedTargets().all { target -> project.tasks.register(target.toPodGenTaskName, PodGenTask::class.java) { it.description = "Сreates a synthetic Xcode project to retrieve CocoaPods dependencies" @@ -330,6 +371,7 @@ open class KotlinCocoapodsPlugin : Plugin { it.kotlinNativeTarget = target it.cocoapodsExtension = cocoapodsExtension it.dependsOn(podspecTaskProvider) + it.dependsOn(downloadPods) it.onlyIf { isAvailableToProduceSynthetic } } } @@ -415,6 +457,7 @@ open class KotlinCocoapodsPlugin : Plugin { createSyncTask(project, kotlinExtension) registerPodspecTask(project, cocoapodsExtension) + registerPodDownloadTask(project, cocoapodsExtension) registerPodGenTask(project, kotlinExtension, cocoapodsExtension) registerPodInstallTask(project, cocoapodsExtension) registerPodSetupBuildTasks(project, kotlinExtension, cocoapodsExtension) @@ -443,6 +486,7 @@ open class KotlinCocoapodsPlugin : Plugin { const val POD_SPEC_TASK_NAME = "podspec" const val DUMMY_FRAMEWORK_TASK_NAME = "generateDummyFramework" const val POD_INSTALL_TASK_NAME = "podInstall" + const val POD_DOWNLOAD_TASK_NAME = "podDownload" const val POD_GEN_TASK_NAME = "podGen" const val POD_SETUP_BUILD_TASK_NAME = "podSetupBuild" const val POD_BUILD_DEPENDENCIES_TASK_NAME = "podBuildDependencies" diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/AdvancedCocoapodsTasks.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/AdvancedCocoapodsTasks.kt index 7082bd9792b..dd7822c1217 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/AdvancedCocoapodsTasks.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/AdvancedCocoapodsTasks.kt @@ -6,9 +6,14 @@ package org.jetbrains.kotlin.gradle.targets.native.tasks import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.repositories.ArtifactRepository +import org.gradle.api.file.FileTree +import org.gradle.api.file.RelativePath import org.gradle.api.provider.Provider import org.gradle.api.tasks.* import org.gradle.api.tasks.Optional +import org.jetbrains.kotlin.gradle.plugin.cocoapods.* +import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.* import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension import org.jetbrains.kotlin.gradle.plugin.cocoapods.asValidFrameworkName import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs @@ -18,7 +23,9 @@ import org.jetbrains.kotlin.konan.target.KonanTarget import java.io.File import java.io.FileInputStream import java.io.InputStream +import java.net.URI import java.util.* +import kotlin.concurrent.thread internal val KotlinNativeTarget.toBuildSettingsFileName: String get() = "build-settings-$disambiguationClassifier.properties" @@ -91,6 +98,7 @@ open class PodInstallTask : DefaultTask() { } } + abstract class CocoapodsWithSyntheticTask : DefaultTask() { init { onlyIf { @@ -102,6 +110,237 @@ abstract class CocoapodsWithSyntheticTask : DefaultTask() { internal lateinit var cocoapodsExtension: CocoapodsExtension } +abstract class PodDownloadTask : CocoapodsWithSyntheticTask() { + @get:Input + internal lateinit var podName: Provider +} + + +open class PodDownloadUrlTask : PodDownloadTask() { + + @get:Nested + internal lateinit var podSource: Provider + + @get:Internal + internal val urlDir = project.cocoapodsBuildDirs.externalSources("url") + + + @get:OutputDirectory + internal val podSourceDir = project.provider { + urlDir.resolve(podName.get()) + } + + @get:Internal + internal val permittedFileExtensions = listOf("zip", "tar", "tgz", "tbz", "txz", "gzip", "tar.gz", "tar.bz2", "tar.xz", "jar") + + @TaskAction + fun download() { + val podLocation = podSource.get() + val url = podLocation.url.toString() + val repoUrl = url.substringBeforeLast("/") + val fileName = url.substringAfterLast("/") + val fileNameWithoutExtension = fileName.substringBefore(".") + val extension = fileName.substringAfter(".") + require(permittedFileExtensions.contains(extension)) { "Unknown file extension" } + + val repo = setupRepo(repoUrl) + val dependency = createDependency(fileNameWithoutExtension, extension) + val configuration = project.configurations.detachedConfiguration(dependency) + val artifact = configuration.singleFile + copyArtifactToUrlDir(artifact, extension, podLocation.flatten) + project.repositories.remove(repo) + } + + private fun setupRepo(repoUrl: String): ArtifactRepository { + return project.repositories.ivy { repo -> + repo.setUrl(repoUrl) + repo.patternLayout { + it.artifact("[artifact].[ext]") + } + repo.metadataSources { + it.artifact() + } + } + } + + private fun createDependency(fileNameWithoutExtension: String, extension: String) = project.dependencies.create( + mapOf( + "name" to fileNameWithoutExtension, + "version" to "1.0", + "ext" to extension + ) + ) + + private fun copyArtifactToUrlDir(artifact: File, extension: String, flatten: Boolean) { + val archiveTree = archiveTree(artifact.absolutePath, extension) + project.copy { + val destinationDir = podSourceDir.get() + it.into(destinationDir) + it.from(archiveTree) + if (extension == "jar") { + it.exclude("META-INF/") + } + if (!flatten) { + it.eachFile { file -> + file.relativePath = RelativePath(true, *file.relativePath.segments.drop(1).toTypedArray()) + } + it.includeEmptyDirs = false + } + } + } + + private fun archiveTree(path: String, extension: String): FileTree { + return if (extension == "zip" || extension == "jar") { + project.zipTree(path) + } else { + project.tarTree(path) + } + } +} + + +open class PodDownloadGitTask : PodDownloadTask() { + + @get:Nested + internal lateinit var podSource: Provider + + @get:Internal + internal val gitDir = project.cocoapodsBuildDirs.externalSources("git") + + @get:OutputDirectory + internal val repo = project.provider { + gitDir.resolve(podName.get()) + } + + @TaskAction + fun download() { + repo.get().deleteRecursively() + val git = podSource.get() + val branch = git.tag ?: git.branch + val commit = git.commit + val url = git.url + try { + when { + commit != null -> { + retrieveCommit(url, commit) + } + branch != null -> { + cloneShallow(url, branch) + } + else -> { + cloneHead(git) + } + } + } catch (e: IllegalStateException) { + fallback(git) + } + } + + private fun retrieveCommit(url: URI, commit: String) { + val initCommand = listOf( + "git", + "init" + ) + val repo = repo.get() + repo.mkdir() + val configProcess: ProcessBuilder.() -> Unit = { directory(repo) } + runCommand(initCommand, configProcess) + + val fetchCommand = listOf( + "git", + "fetch", + "--depth", "1", + "$url", + commit + ) + runCommand(fetchCommand, configProcess) + + val checkoutCommand = listOf( + "git", + "checkout", + "FETCH_HEAD" + ) + runCommand(checkoutCommand, configProcess) + } + + private fun cloneShallow(url: URI, branch: String) { + val shallowCloneCommand = listOf( + "git", + "clone", + "$url", + podName.get(), + "--branch", branch, + "--depth", "1" + ) + val configProcess: ProcessBuilder.() -> Unit = { directory(gitDir) } + runCommand(shallowCloneCommand, configProcess) + } + + private fun cloneHead(podspecLocation: Git) { + val cloneHeadCommand = listOf( + "git", + "clone", + "${podspecLocation.url}", + podName.get(), + "--depth", "1" + ) + val configProcess: ProcessBuilder.() -> Unit = { directory(gitDir) } + runCommand(cloneHeadCommand, configProcess) + } + + private fun fallback(podspecLocation: Git) { + // removing any traces of other commands + gitDir.resolve(podName.get()).deleteRecursively() + val cloneAllCommand = listOf( + "git", + "clone", + "${podspecLocation.url}", + podName.get() + ) + val configProcess: ProcessBuilder.() -> Unit = { directory(gitDir) } + runCommand(cloneAllCommand, configProcess) + } +} + +private fun runCommand( + command: List, + processConfiguration: ((ProcessBuilder.() -> Unit))? = null, + errorHandler: ((retCode: Int, process: Process) -> Unit)? = null +): String { + val process = ProcessBuilder(command) + .apply { + if (processConfiguration != null) { + this.processConfiguration() + } + }.start() + + var inputText = "" + var errorText = "" + + val inputThread = thread { + inputText = process.inputStream.use { + it.reader().readText() + } + } + + val errorThread = thread { + errorText = process.errorStream.use { + it.reader().readText() + } + } + + inputThread.join() + errorThread.join() + + val retCode = process.waitFor() + check(retCode == 0) { + errorHandler?.invoke(retCode, process) + ?: "Executing of '${command.joinToString(" ")}' failed with code $retCode and message: $errorText" + } + + return inputText +} + /** * The task takes the path to the .podspec file and calls `pod gen` * to create synthetic xcode project and workspace. @@ -126,13 +365,17 @@ open class PodGenTask : CocoapodsWithSyntheticTask() { @TaskAction fun generate() { val syntheticDir = project.cocoapodsBuildDirs.synthetic(kotlinNativeTarget).apply { mkdirs() } - val localPodspecPaths = cocoapodsExtension.pods.mapNotNull { it.podspec?.parentFile?.absolutePath } + val localPodspecPaths = cocoapodsExtension.pods.mapNotNull { it.source?.getLocalPath(project, it.name) } + + val sources = cocoapodsExtension.specRepos.getAll().toMutableList() + sources += URI("https://cdn.cocoapods.org") val podGenProcessArgs = listOfNotNull( "pod", "gen", "--platforms=${kotlinNativeTarget.platformLiteral}", "--gen-directory=${syntheticDir.absolutePath}", localPodspecPaths.takeIf { it.isNotEmpty() }?.joinToString(separator = ",")?.let { "--local-sources=$it" }, + sources.takeIf { it.isNotEmpty() }?.joinToString(separator = ",")?.let { "--sources=$it" }, podspecProvider.get().absolutePath )