[Gradle, Cocoapods] implement DSL to support Cocoapods Dependencies from Git or Web

Issue #KT-41367 Fixed
This commit is contained in:
Andrey Lozhkin
2020-07-15 13:30:22 +03:00
committed by Andrey.Lozhkin
parent b02f0f0a25
commit fcf5060907
14 changed files with 1090 additions and 129 deletions
@@ -6,17 +6,25 @@
package org.jetbrains.kotlin.gradle package org.jetbrains.kotlin.gradle
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin 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_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_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_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.gradle.util.modify
import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.kotlin.konan.target.HostManager
import org.junit.Assume.assumeFalse import org.junit.Assume.assumeFalse
import org.junit.Assume.assumeTrue import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.fail import kotlin.test.fail
class CocoaPodsIT : BaseGradleIT() { class CocoaPodsIT : BaseGradleIT() {
@@ -24,14 +32,44 @@ class CocoaPodsIT : BaseGradleIT() {
override val defaultGradleVersion: GradleVersionRequired override val defaultGradleVersion: GradleVersionRequired
get() = GradleVersionRequired.FOR_MPP_SUPPORT get() = GradleVersionRequired.FOR_MPP_SUPPORT
// We use Kotlin DSL. Earlier Gradle versions fail at accessors codegen. // 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 = "<import_mode_directive>" val PODFILE_IMPORT_DIRECTIVE_PLACEHOLDER = "<import_mode_directive>"
val cocoapodsSingleKtPod = "new-mpp-cocoapods-single" private val cocoapodsSingleKtPod = "new-mpp-cocoapods-single"
val cocoapodsMultipleKtPods = "new-mpp-cocoapods-multiple" 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 @Test
fun testPodspecSingle() = doTestPodspec( fun testPodspecSingle() = doTestPodspec(
@@ -79,31 +117,20 @@ class CocoaPodsIT : BaseGradleIT() {
) )
@Test @Test
fun testPodImportUseFrameworksSingle() = doTestPodImport( fun testPodImportUseFrameworksSingle() = doTestImportSingle(ImportMode.FRAMEWORKS)
cocoapodsSingleKtPod,
"ios-app",
ImportMode.FRAMEWORKS,
mapOf("kotlin-library" to null)
)
@Test @Test
fun testPodImportUseModularHeadersSingle() = fun testPodImportUseModularHeadersSingle() = doTestImportSingle(ImportMode.MODULAR_HEADERS)
doTestPodImport(
cocoapodsSingleKtPod,
"ios-app",
ImportMode.MODULAR_HEADERS,
mapOf("kotlin-library" to null)
)
@Test @Test
fun testPodspecMupltiple() = doTestPodspec( fun testPodspecMultiple() = doTestPodspec(
cocoapodsMultipleKtPods, cocoapodsMultipleKtPods,
mapOf("kotlin-library" to null, "second-library" to null), mapOf("kotlin-library" to null, "second-library" to null),
mapOf("kotlin-library" to kotlinLibraryPodspecContent(), "second-library" to secondLibraryPodspecContent()), mapOf("kotlin-library" to kotlinLibraryPodspecContent(), "second-library" to secondLibraryPodspecContent()),
) )
@Test @Test
fun testPodspecCustomFrameworkNameMupltiple() = doTestPodspec( fun testPodspecCustomFrameworkNameMultiple() = doTestPodspec(
cocoapodsMultipleKtPods, cocoapodsMultipleKtPods,
mapOf("kotlin-library" to "FirstMultiplatformLibrary", "second-library" to "SecondMultiplatformLibrary"), mapOf("kotlin-library" to "FirstMultiplatformLibrary", "second-library" to "SecondMultiplatformLibrary"),
mapOf( mapOf(
@@ -113,7 +140,7 @@ class CocoaPodsIT : BaseGradleIT() {
) )
@Test @Test
fun testXcodeUseFrameworksMupltiple() = doTestXcode( fun testXcodeUseFrameworksMultiple() = doTestXcode(
cocoapodsMultipleKtPods, cocoapodsMultipleKtPods,
ImportMode.FRAMEWORKS, ImportMode.FRAMEWORKS,
"ios-app", "ios-app",
@@ -121,7 +148,7 @@ class CocoaPodsIT : BaseGradleIT() {
) )
@Test @Test
fun testXcodeUseFrameworksWithCustomFrameworkNameMupltiple() = doTestXcode( fun testXcodeUseFrameworksWithCustomFrameworkNameMultiple() = doTestXcode(
cocoapodsMultipleKtPods, cocoapodsMultipleKtPods,
ImportMode.FRAMEWORKS, ImportMode.FRAMEWORKS,
"ios-app", "ios-app",
@@ -129,7 +156,7 @@ class CocoaPodsIT : BaseGradleIT() {
) )
@Test @Test
fun testXcodeUseModularHeadersMupltiple() = doTestXcode( fun testXcodeUseModularHeadersMultiple() = doTestXcode(
cocoapodsMultipleKtPods, cocoapodsMultipleKtPods,
ImportMode.MODULAR_HEADERS, ImportMode.MODULAR_HEADERS,
"ios-app", "ios-app",
@@ -137,7 +164,7 @@ class CocoaPodsIT : BaseGradleIT() {
) )
@Test @Test
fun testXcodeUseModularHeadersWithCustomFrameworkNameMupltiple() = doTestXcode( fun testXcodeUseModularHeadersWithCustomFrameworkNameMultiple() = doTestXcode(
cocoapodsMultipleKtPods, cocoapodsMultipleKtPods,
ImportMode.MODULAR_HEADERS, ImportMode.MODULAR_HEADERS,
"ios-app", "ios-app",
@@ -145,47 +172,528 @@ class CocoaPodsIT : BaseGradleIT() {
) )
@Test @Test
fun testPodImportUseFrameworksMupltiple() = doTestPodImport( fun testPodImportUseFrameworksMultiple() = doTestImportMultiple(ImportMode.FRAMEWORKS)
cocoapodsMultipleKtPods,
"ios-app",
ImportMode.FRAMEWORKS,
mapOf("kotlin-library" to null, "second-library" to null)
)
@Test @Test
fun testPodImportUseModularHeadersMupltiple() = fun testPodImportUseModularHeadersMultiple() = doTestImportMultiple(ImportMode.MODULAR_HEADERS)
doTestPodImport(
cocoapodsMultipleKtPods, @Test
"ios-app", fun testSpecReposDownloads() {
ImportMode.MODULAR_HEADERS, val podName = "example"
mapOf("kotlin-library" to null, "second-library" to null) 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 @Test
fun testPodImportCustomFrameworkName() = doTestPodImport( fun testSpecReposUTD() {
cocoapodsSingleKtPod, with(project.gradleBuildScript()) {
"ios-app", addPod("AFNetworking")
ImportMode.FRAMEWORKS, }
mapOf("kotlin-library" to "foobaz") 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 @Test
fun testSkippingTasksOnOtherHosts() { fun testGroovyDownloadAndImport() {
assumeFalse(HostManager.hostIsMac) 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( // other tests
":podInstall",
":kotlin-library:podGenIOS", @Test
":kotlin-library:podSetupBuildIOS", fun testDownloadUrlTestSupportDashInNames() {
":kotlin-library:podBuildDependenciesIOS", val fileExtension = "tar.gz"
":kotlin-library:podImport" 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. // paths
assertSuccessful()
assertTasksSkipped(*syntheticTasks) 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<CompiledProject.() -> 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<String>, 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<String> = listOf(),
vararg args: String
) {
hooks.addHook {
podImportAsserts()
}
testImport(repos, *args)
}
private fun Project.testImport(
repos: List<String> = listOf(),
vararg args: String
) {
for (repo in repos) {
assumeTrue(isRepoAvailable(repo))
}
testSynthetic(podImportTaskName, *args)
}
private fun Project.testDownload(
repos: List<String>,
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<String, String?>,
) {
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) { private enum class ImportMode(val directive: String) {
FRAMEWORKS("use_frameworks!"), FRAMEWORKS("use_frameworks!"),
MODULAR_HEADERS("use_modular_headers!") MODULAR_HEADERS("use_modular_headers!")
@@ -20,6 +20,6 @@ kotlin {
cocoapods { cocoapods {
noPodspec() noPodspec()
podfile = projectDir.resolve("ios-app/Podfile") podfile = project.file("ios-app/Podfile")
} }
} }
@@ -21,7 +21,7 @@ kotlin {
cocoapods { cocoapods {
summary = "CocoaPods test library" summary = "CocoaPods test library"
homepage = "https://github.com/JetBrains/kotlin" homepage = "https://github.com/JetBrains/kotlin"
pod("pod_dependency", "1.0", projectDir.resolve("../pod_dependency/pod_dependency.podspec")) pod("pod_dependency", "1.0", project.file("../pod_dependency"))
pod("subspec_dependency/Core", "1.0", projectDir.resolve("../subspec_dependency/subspec_dependency.podspec")) pod("subspec_dependency/Core", "1.0", project.file("../subspec_dependency"))
} }
} }
@@ -20,6 +20,6 @@ kotlin {
cocoapods { cocoapods {
noPodspec() noPodspec()
podfile = projectDir.resolve("ios-app/Podfile") podfile = project.file("ios-app/Podfile")
} }
} }
@@ -21,7 +21,8 @@ kotlin {
cocoapods { cocoapods {
summary = "CocoaPods test library" summary = "CocoaPods test library"
homepage = "https://github.com/JetBrains/kotlin" homepage = "https://github.com/JetBrains/kotlin"
pod("pod_dependency", "1.0", projectDir.resolve("../pod_dependency/pod_dependency.podspec")) // test warnIfDeprecatedPodspecPathIsUsed depends on deprecated api usage
pod("subspec_dependency/Core", "1.0", projectDir.resolve("../subspec_dependency/subspec_dependency.podspec")) pod("pod_dependency", "1.0", project.file("../pod_dependency/pod_dependency.podspec"))
pod("subspec_dependency/Core", "1.0", project.file("../subspec_dependency"))
} }
} }
@@ -0,0 +1,25 @@
plugins {
id("org.jetbrains.kotlin.multiplatform").version("<pluginMarkerVersion>")
id("org.jetbrains.kotlin.native.cocoapods").version("<pluginMarkerVersion>")
}
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"
}
}
@@ -0,0 +1,11 @@
pluginManagement {
repositories {
mavenLocal()
jcenter()
gradlePluginPortal()
}
}
enableFeaturePreview('GRADLE_METADATA')
rootProject.name = "cocoapods"
@@ -0,0 +1,5 @@
package testProject.`new-mpp-cocoapods-template-groovy`.src.iosMain.kotlin
fun foo() {
println("hi!")
}
@@ -0,0 +1,25 @@
plugins {
id("org.jetbrains.kotlin.multiplatform").version("<pluginMarkerVersion>")
id("org.jetbrains.kotlin.native.cocoapods").version("<pluginMarkerVersion>")
}
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"
}
}
@@ -0,0 +1,11 @@
pluginManagement {
repositories {
mavenLocal()
jcenter()
gradlePluginPortal()
}
}
enableFeaturePreview('GRADLE_METADATA')
rootProject.name = "cocoapods"
@@ -0,0 +1,5 @@
package testProject.`new-mpp-cocoapods-template`.src.iosMain.kotlin
fun foo() {
println("hi!")
}
@@ -6,11 +6,15 @@
@file:Suppress("PackageDirectoryMismatch") // Old package for compatibility @file:Suppress("PackageDirectoryMismatch") // Old package for compatibility
package org.jetbrains.kotlin.gradle.plugin.cocoapods package org.jetbrains.kotlin.gradle.plugin.cocoapods
import groovy.lang.Closure
import org.gradle.api.Named import org.gradle.api.Named
import org.gradle.api.NamedDomainObjectSet import org.gradle.api.NamedDomainObjectSet
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.tasks.* 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.io.File
import java.net.URI
open class CocoapodsExtension(private val project: Project) { open class CocoapodsExtension(private val project: Project) {
@get:Input @get:Input
@@ -80,6 +84,9 @@ open class CocoapodsExtension(private val project: Project) {
@Input @Input
var frameworkName: String = project.name.asValidFrameworkName() var frameworkName: String = project.name.asValidFrameworkName()
@get:Nested
internal val specRepos = SpecRepos()
private val _pods = project.container(CocoapodsDependency::class.java) private val _pods = project.container(CocoapodsDependency::class.java)
// For some reason Gradle doesn't consume the @Nested annotation on NamedDomainObjectContainer. // 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. * Add a CocoaPods dependency to the pod built from this project.
*/ */
@JvmOverloads @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" } 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 <a href="https://guides.cocoapods.org/making/private-cocoapods.html">cocoapods documentation</a>
* 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 <a href="https://guides.cocoapods.org/making/private-cocoapods.html">cocoapods documentation</a>
* for additional information.
* Default sources (cdn.cocoapods.org) implicitly included.
*/
fun specRepos(configure: Closure<*>) = specRepos {
ConfigureUtil.configure(configure, this)
} }
data class CocoapodsDependency( data class CocoapodsDependency(
private val name: String, private val name: String,
@get:Optional @get:Input val version: String?, @get:Input var moduleName: String,
@get:Optional @get:InputFile val podspec: File?, @get:Optional @get:Input var version: String? = null,
@get:Input val moduleName: String @get:Optional @get:Nested var source: PodLocation? = null
) : Named { ) : Named {
@Input @Input
override fun getName(): String = name 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( data class PodspecPlatformSettings(
@@ -123,6 +251,20 @@ open class CocoapodsExtension(private val project: Project) {
override fun getName(): String = name override fun getName(): String = name
} }
class SpecRepos {
@get:Internal
internal val specRepos = mutableSetOf<URI>()
fun url(url: String) {
specRepos.add(URI(url))
}
@Input
internal fun getAll(): Collection<URI> {
return specRepos
}
}
companion object { companion object {
private fun String.asModuleName() = this private fun String.asModuleName() = this
.split("/")[0] // Pick the module name from a subspec name. .split("/")[0] // Pick the module name from a subspec name.
@@ -15,10 +15,13 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.addExtension 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.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.plugin.whenEvaluated import org.jetbrains.kotlin.gradle.plugin.whenEvaluated
import org.jetbrains.kotlin.gradle.targets.native.tasks.* import org.jetbrains.kotlin.gradle.targets.native.tasks.*
import org.jetbrains.kotlin.gradle.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.asValidTaskName
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.kotlin.konan.target.HostManager
@@ -41,11 +44,16 @@ internal class CocoapodsBuildDirs(val project: Project) {
val buildSettings: File val buildSettings: File
get() = root.resolve("buildSettings") get() = root.resolve("buildSettings")
private val synthetic: File val synthetic: File
get() = root.resolve("synthetic") get() = root.resolve("synthetic")
fun synthetic(kotlinNativeTarget: KotlinNativeTarget) = synthetic.resolve(kotlinNativeTarget.name) 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) = fun fatFramework(buildType: String) =
root.resolve("fat-frameworks/${buildType.toLowerCase()}") root.resolve("fat-frameworks/${buildType.toLowerCase()}")
} }
@@ -61,13 +69,18 @@ private val KotlinNativeTarget.toSetupBuildTaskName: String
disambiguationClassifier disambiguationClassifier
) )
private val KotlinNativeTarget.toBuildDependenciesTaskName: String private val KotlinNativeTarget.toBuildDependenciesTaskName: String
get() = lowerCamelCaseName( get() = lowerCamelCaseName(
KotlinCocoapodsPlugin.POD_BUILD_DEPENDENCIES_TASK_NAME, KotlinCocoapodsPlugin.POD_BUILD_DEPENDENCIES_TASK_NAME,
disambiguationClassifier disambiguationClassifier
) )
private val CocoapodsDependency.toPodDownloadTaskName: String
get() = lowerCamelCaseName(
KotlinCocoapodsPlugin.POD_DOWNLOAD_TASK_NAME,
name
)
open class KotlinCocoapodsPlugin : Plugin<Project> { open class KotlinCocoapodsPlugin : Plugin<Project> {
private fun KotlinMultiplatformExtension.supportedTargets() = targets private fun KotlinMultiplatformExtension.supportedTargets() = targets
@@ -186,7 +199,6 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
) { ) {
val moduleNames = mutableSetOf<String>() val moduleNames = mutableSetOf<String>()
cocoapodsExtension.pods.all { pod -> cocoapodsExtension.pods.all { pod ->
if (moduleNames.contains(pod.moduleName)) { if (moduleNames.contains(pod.moduleName)) {
return@all return@all
@@ -317,12 +329,41 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
} }
} }
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( private fun registerPodGenTask(
project: Project, kotlinExtension: KotlinMultiplatformExtension, cocoapodsExtension: CocoapodsExtension project: Project, kotlinExtension: KotlinMultiplatformExtension, cocoapodsExtension: CocoapodsExtension
) { ) {
val podspecTaskProvider = project.tasks.named(POD_SPEC_TASK_NAME, PodspecTask::class.java) 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 -> kotlinExtension.supportedTargets().all { target ->
project.tasks.register(target.toPodGenTaskName, PodGenTask::class.java) { project.tasks.register(target.toPodGenTaskName, PodGenTask::class.java) {
it.description = "Сreates a synthetic Xcode project to retrieve CocoaPods dependencies" it.description = "Сreates a synthetic Xcode project to retrieve CocoaPods dependencies"
@@ -330,6 +371,7 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
it.kotlinNativeTarget = target it.kotlinNativeTarget = target
it.cocoapodsExtension = cocoapodsExtension it.cocoapodsExtension = cocoapodsExtension
it.dependsOn(podspecTaskProvider) it.dependsOn(podspecTaskProvider)
it.dependsOn(downloadPods)
it.onlyIf { isAvailableToProduceSynthetic } it.onlyIf { isAvailableToProduceSynthetic }
} }
} }
@@ -415,6 +457,7 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
createSyncTask(project, kotlinExtension) createSyncTask(project, kotlinExtension)
registerPodspecTask(project, cocoapodsExtension) registerPodspecTask(project, cocoapodsExtension)
registerPodDownloadTask(project, cocoapodsExtension)
registerPodGenTask(project, kotlinExtension, cocoapodsExtension) registerPodGenTask(project, kotlinExtension, cocoapodsExtension)
registerPodInstallTask(project, cocoapodsExtension) registerPodInstallTask(project, cocoapodsExtension)
registerPodSetupBuildTasks(project, kotlinExtension, cocoapodsExtension) registerPodSetupBuildTasks(project, kotlinExtension, cocoapodsExtension)
@@ -443,6 +486,7 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
const val POD_SPEC_TASK_NAME = "podspec" const val POD_SPEC_TASK_NAME = "podspec"
const val DUMMY_FRAMEWORK_TASK_NAME = "generateDummyFramework" const val DUMMY_FRAMEWORK_TASK_NAME = "generateDummyFramework"
const val POD_INSTALL_TASK_NAME = "podInstall" const val POD_INSTALL_TASK_NAME = "podInstall"
const val POD_DOWNLOAD_TASK_NAME = "podDownload"
const val POD_GEN_TASK_NAME = "podGen" const val POD_GEN_TASK_NAME = "podGen"
const val POD_SETUP_BUILD_TASK_NAME = "podSetupBuild" const val POD_SETUP_BUILD_TASK_NAME = "podSetupBuild"
const val POD_BUILD_DEPENDENCIES_TASK_NAME = "podBuildDependencies" const val POD_BUILD_DEPENDENCIES_TASK_NAME = "podBuildDependencies"
@@ -6,9 +6,14 @@
package org.jetbrains.kotlin.gradle.targets.native.tasks package org.jetbrains.kotlin.gradle.targets.native.tasks
import org.gradle.api.DefaultTask 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.provider.Provider
import org.gradle.api.tasks.* import org.gradle.api.tasks.*
import org.gradle.api.tasks.Optional 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.CocoapodsExtension
import org.jetbrains.kotlin.gradle.plugin.cocoapods.asValidFrameworkName import org.jetbrains.kotlin.gradle.plugin.cocoapods.asValidFrameworkName
import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs 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.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStream import java.io.InputStream
import java.net.URI
import java.util.* import java.util.*
import kotlin.concurrent.thread
internal val KotlinNativeTarget.toBuildSettingsFileName: String internal val KotlinNativeTarget.toBuildSettingsFileName: String
get() = "build-settings-$disambiguationClassifier.properties" get() = "build-settings-$disambiguationClassifier.properties"
@@ -91,6 +98,7 @@ open class PodInstallTask : DefaultTask() {
} }
} }
abstract class CocoapodsWithSyntheticTask : DefaultTask() { abstract class CocoapodsWithSyntheticTask : DefaultTask() {
init { init {
onlyIf { onlyIf {
@@ -102,6 +110,237 @@ abstract class CocoapodsWithSyntheticTask : DefaultTask() {
internal lateinit var cocoapodsExtension: CocoapodsExtension internal lateinit var cocoapodsExtension: CocoapodsExtension
} }
abstract class PodDownloadTask : CocoapodsWithSyntheticTask() {
@get:Input
internal lateinit var podName: Provider<String>
}
open class PodDownloadUrlTask : PodDownloadTask() {
@get:Nested
internal lateinit var podSource: Provider<Url>
@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<Git>
@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<String>,
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` * The task takes the path to the .podspec file and calls `pod gen`
* to create synthetic xcode project and workspace. * to create synthetic xcode project and workspace.
@@ -126,13 +365,17 @@ open class PodGenTask : CocoapodsWithSyntheticTask() {
@TaskAction @TaskAction
fun generate() { fun generate() {
val syntheticDir = project.cocoapodsBuildDirs.synthetic(kotlinNativeTarget).apply { mkdirs() } 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( val podGenProcessArgs = listOfNotNull(
"pod", "gen", "pod", "gen",
"--platforms=${kotlinNativeTarget.platformLiteral}", "--platforms=${kotlinNativeTarget.platformLiteral}",
"--gen-directory=${syntheticDir.absolutePath}", "--gen-directory=${syntheticDir.absolutePath}",
localPodspecPaths.takeIf { it.isNotEmpty() }?.joinToString(separator = ",")?.let { "--local-sources=$it" }, localPodspecPaths.takeIf { it.isNotEmpty() }?.joinToString(separator = ",")?.let { "--local-sources=$it" },
sources.takeIf { it.isNotEmpty() }?.joinToString(separator = ",")?.let { "--sources=$it" },
podspecProvider.get().absolutePath podspecProvider.get().absolutePath
) )