[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
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 = "<import_mode_directive>"
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<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) {
FRAMEWORKS("use_frameworks!"),
MODULAR_HEADERS("use_modular_headers!")
@@ -20,6 +20,6 @@ kotlin {
cocoapods {
noPodspec()
podfile = projectDir.resolve("ios-app/Podfile")
podfile = project.file("ios-app/Podfile")
}
}
@@ -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"))
}
}
@@ -20,6 +20,6 @@ kotlin {
cocoapods {
noPodspec()
podfile = projectDir.resolve("ios-app/Podfile")
podfile = project.file("ios-app/Podfile")
}
}
@@ -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"))
}
}
@@ -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
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 <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(
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<URI>()
fun url(url: String) {
specRepos.add(URI(url))
}
@Input
internal fun getAll(): Collection<URI> {
return specRepos
}
}
companion object {
private fun String.asModuleName() = this
.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.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<Project> {
private fun KotlinMultiplatformExtension.supportedTargets() = targets
@@ -186,7 +199,6 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
) {
val moduleNames = mutableSetOf<String>()
cocoapodsExtension.pods.all { pod ->
if (moduleNames.contains(pod.moduleName)) {
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(
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<Project> {
it.kotlinNativeTarget = target
it.cocoapodsExtension = cocoapodsExtension
it.dependsOn(podspecTaskProvider)
it.dependsOn(downloadPods)
it.onlyIf { isAvailableToProduceSynthetic }
}
}
@@ -415,6 +457,7 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
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<Project> {
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"
@@ -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<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`
* 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
)