[Gradle] Split CocoapodsTasks and AdvancedCocoapodsTasks into separate files (2/2)

This commit is contained in:
Artem Daugel-Dauge
2023-03-30 14:54:54 +02:00
committed by Space Team
parent fc68873df7
commit 7aeca2fda0
11 changed files with 24 additions and 4277 deletions
@@ -3,434 +3,14 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
@file:Suppress("LeakingThis") // All tasks should be inherited only by Gradle @file:Suppress("PackageDirectoryMismatch") // Old package for compatibility
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.file.FileCollection
import org.gradle.api.file.FileTree
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
import org.gradle.api.tasks.Optional
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs
import org.jetbrains.kotlin.gradle.plugin.cocoapods.platformLiteral
import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingCocoapodsMessage
import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingSpecReposMessage
import org.jetbrains.kotlin.gradle.utils.runCommand
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.HostManager
import java.io.File import java.io.File
import java.io.IOException
import java.io.Reader import java.io.Reader
import java.util.* import java.util.*
val CocoapodsDependency.schemeName: String
get() = name.split("/")[0]
open class CocoapodsTask : DefaultTask() {
init {
onlyIf {
HostManager.hostIsMac
}
}
}
/**
* The task takes the path to the Podfile and calls `pod install`
* to obtain sources or artifacts for the declared dependencies.
* This task is a part of CocoaPods integration infrastructure.
*/
abstract class AbstractPodInstallTask : CocoapodsTask() {
init {
onlyIf { podfile.isPresent }
}
@get:Optional
@get:InputFile
abstract val podfile: Property<File?>
@get:Internal
protected val workingDir: Provider<File> = podfile.map { file: File? ->
requireNotNull(file) { "Task outputs shouldn't be queried if it's skipped" }.parentFile
}
@get:OutputDirectory
internal val podsDir: Provider<File> = workingDir.map { it.resolve("Pods") }
@get:Internal
internal val podsXcodeProjDirProvider: Provider<File> = podsDir.map { it.resolve("Pods.xcodeproj") }
@TaskAction
open fun doPodInstall() {
val podInstallCommand = listOf("pod", "install")
runCommand(podInstallCommand,
logger,
errorHandler = ::handleError,
exceptionHandler = { e: IOException ->
CocoapodsErrorHandlingUtil.handle(e, podInstallCommand)
},
processConfiguration = {
directory(workingDir.get())
})
with(podsXcodeProjDirProvider.get()) {
check(exists() && isDirectory) {
"The directory 'Pods/Pods.xcodeproj' was not created as a result of the `pod install` call."
}
}
}
abstract fun handleError(retCode: Int, error: String, process: Process): String?
}
abstract class PodInstallTask : AbstractPodInstallTask() {
@get:Optional
@get:InputFile
abstract val podspec: Property<File?>
@get:Input
abstract val frameworkName: Property<String>
@get:Nested
abstract val specRepos: Property<SpecRepos>
@get:Nested
abstract val pods: ListProperty<CocoapodsDependency>
@get:InputDirectory
abstract val dummyFramework: Property<File>
private val framework = project.provider { project.cocoapodsBuildDirs.framework.resolve("${frameworkName.get()}.framework") }
private val tmpFramework = dummyFramework.map { dummy -> dummy.parentFile.resolve("tmp.framework").also { it.deleteOnExit() } }
override fun doPodInstall() {
// We always need to execute 'pod install' with the dummy framework because the one left from a previous build
// may have a wrong linkage type. So we temporarily swap them, run 'pod install' and then swap them back
framework.rename(tmpFramework)
dummyFramework.rename(framework)
super.doPodInstall()
framework.rename(dummyFramework)
tmpFramework.rename(framework)
}
private fun Provider<File>.rename(dest: Provider<File>) = get().rename(dest.get())
private fun File.rename(dest: File) {
if (!exists()) {
mkdirs()
}
check(renameTo(dest)) { "Can't rename '${this}' to '${dest}'" }
}
override fun handleError(retCode: Int, error: String, process: Process): String? {
val specReposMessages = MissingSpecReposMessage(specRepos.get()).missingMessage
val cocoapodsMessages = pods.get().map { MissingCocoapodsMessage(it).missingMessage }
return listOfNotNull(
"'pod install' command failed with code $retCode.",
"Error message:",
error.lines().filter { it.isNotBlank() }.joinToString("\n"),
"""
| Please, check that podfile contains following lines in header:
| $specReposMessages
|
""".trimMargin(),
"""
| Please, check that each target depended on ${frameworkName.get()} contains following dependencies:
| ${cocoapodsMessages.joinToString("\n")}
|
""".trimMargin()
).joinToString("\n")
}
}
abstract class PodInstallSyntheticTask : AbstractPodInstallTask() {
@get:Input
abstract val family: Property<Family>
@get:Input
abstract val podName: Property<String>
@get:OutputDirectory
internal val syntheticXcodeProject: Provider<File> = workingDir.map { it.resolve("synthetic.xcodeproj") }
override fun doPodInstall() {
val projResource = "/cocoapods/project.pbxproj"
val projDestination = syntheticXcodeProject.get().resolve("project.pbxproj")
syntheticXcodeProject.get().mkdirs()
projDestination.outputStream().use { file ->
javaClass.getResourceAsStream(projResource)!!.use { resource ->
resource.copyTo(file)
}
}
super.doPodInstall()
}
override fun handleError(retCode: Int, error: String, process: Process): String? {
var message = """
|'pod install' command on the synthetic project failed with return code: $retCode
|
| Error: ${error.lines().filter { it.contains("[!]") }.joinToString("\n")}
|
""".trimMargin()
if (
error.contains("deployment target") ||
error.contains("no platform was specified") ||
error.contains(Regex("The platform of the target .+ is not compatible with `${podName.get()}"))
) {
message += """
|
| Possible reason: ${family.get().platformLiteral} deployment target is not configured
| Configure deployment_target for ALL targets as follows:
| cocoapods {
| ...
| ${family.get().platformLiteral}.deploymentTarget = "..."
| ...
| }
|
""".trimMargin()
return message
} else if (
error.contains("Unable to add a source with url") ||
error.contains("Couldn't determine repo name for URL") ||
error.contains("Unable to find a specification")
) {
message += """
|
| Possible reason: spec repos are not configured correctly.
| Ensure that spec repos are correctly configured for all private pod dependencies:
| cocoapods {
| specRepos {
| url("<private spec repo url>")
| }
| }
|
""".trimMargin()
return message
} else {
return null
}
}
}
/**
* The task generates a synthetic project with all cocoapods dependencies
*/
abstract class PodGenTask : CocoapodsTask() {
init {
onlyIf {
pods.get().isNotEmpty()
}
}
@get:InputFile
internal abstract val podspec: Property<File>
@get:Input
internal abstract val podName: Property<String>
@get:Input
internal abstract val useLibraries: Property<Boolean>
@get:Input
internal abstract val family: Property<Family>
@get:Nested
internal abstract val platformSettings: Property<PodspecPlatformSettings>
@get:Nested
internal abstract val specRepos: Property<SpecRepos>
@get:Nested
internal abstract val pods: ListProperty<CocoapodsDependency>
@get:OutputFile
val podfile: Provider<File> = family.map { project.cocoapodsBuildDirs.synthetic(it).resolve("Podfile") }
@TaskAction
fun generate() {
val specRepos = specRepos.get().getAll()
val podfile = this.podfile.get()
podfile.createNewFile()
val podfileContent = getPodfileContent(specRepos, family.get().platformLiteral)
podfile.writeText(podfileContent)
}
private fun getPodfileContent(specRepos: Collection<String>, xcodeTarget: String) =
buildString {
specRepos.forEach {
appendLine("source '$it'")
}
appendLine("target '$xcodeTarget' do")
if (useLibraries.get().not()) {
appendLine("\tuse_frameworks!")
}
val settings = platformSettings.get()
val deploymentTarget = settings.deploymentTarget
if (deploymentTarget != null) {
appendLine("\tplatform :${settings.name}, '$deploymentTarget'")
} else {
appendLine("\tplatform :${settings.name}")
}
pods.get().mapNotNull {
buildString {
append("pod '${it.name}'")
val version = it.version
val source = it.source
if (source != null) {
append(", ${source.getPodSourcePath()}")
} else if (version != null) {
append(", '$version'")
}
}
}.forEach { appendLine("\t$it") }
appendLine("end\n")
//disable signing for all synthetic pods KT-54314
append(
"""
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = ""
config.build_settings['CODE_SIGNING_REQUIRED'] = "NO"
config.build_settings['CODE_SIGNING_ALLOWED'] = "NO"
end
end
end
""".trimIndent()
)
appendLine()
}
}
open class PodSetupBuildTask : CocoapodsTask() {
@get:Input
lateinit var frameworkName: Provider<String>
@get:Input
internal lateinit var sdk: Provider<String>
@get:Nested
lateinit var pod: Provider<CocoapodsDependency>
@get:OutputFile
val buildSettingsFile: Provider<File> = project.provider {
project.cocoapodsBuildDirs
.buildSettings
.resolve(getBuildSettingFileName(pod.get(), sdk.get()))
}
@get:Internal
internal lateinit var podsXcodeProjDir: Provider<File>
@TaskAction
fun setupBuild() {
val podsXcodeProjDir = podsXcodeProjDir.get()
val buildSettingsReceivingCommand = listOf(
"xcodebuild", "-showBuildSettings",
"-project", podsXcodeProjDir.name,
"-scheme", pod.get().schemeName,
"-sdk", sdk.get()
)
val outputText = runCommand(buildSettingsReceivingCommand, project.logger) { directory(podsXcodeProjDir.parentFile) }
val buildSettingsProperties = PodBuildSettingsProperties.readSettingsFromReader(outputText.reader())
buildSettingsFile.get().let { bsf ->
buildSettingsProperties.writeSettings(bsf)
}
}
}
private fun getBuildSettingFileName(pod: CocoapodsDependency, sdk: String): String =
"build-settings-$sdk-${pod.schemeName}.properties"
/**
* The task compiles external cocoa pods sources.
*/
open class PodBuildTask : CocoapodsTask() {
@get:PathSensitive(PathSensitivity.ABSOLUTE)
@get:InputFile
lateinit var buildSettingsFile: Provider<File>
internal set
@get:Nested
internal lateinit var pod: Provider<CocoapodsDependency>
@get:PathSensitive(PathSensitivity.ABSOLUTE)
@get:IgnoreEmptyDirectories
@get:InputFiles
internal val srcDir: FileTree
get() = project.fileTree(
buildSettingsFile.map { PodBuildSettingsProperties.readSettingsFromReader(it.reader()).podsTargetSrcRoot }
)
@get:Internal
internal var buildDir: Provider<File> = project.provider {
project.file(PodBuildSettingsProperties.readSettingsFromReader(buildSettingsFile.get().reader()).buildDir)
}
@get:Input
internal lateinit var sdk: Provider<String>
@Suppress("unused") // declares an ouptut
@get:OutputFiles
internal val buildResult: Provider<FileCollection> = project.provider {
project.fileTree(buildDir.get()) {
it.include("**/${pod.get().schemeName}.*/")
it.include("**/${pod.get().schemeName}/")
}
}
@get:Internal
internal lateinit var podsXcodeProjDir: Provider<File>
@TaskAction
fun buildDependencies() {
val podBuildSettings = PodBuildSettingsProperties.readSettingsFromReader(buildSettingsFile.get().reader())
val podsXcodeProjDir = podsXcodeProjDir.get()
val podXcodeBuildCommand = listOf(
"xcodebuild",
"-project", podsXcodeProjDir.name,
"-scheme", pod.get().schemeName,
"-sdk", sdk.get(),
"-configuration", podBuildSettings.configuration
)
runCommand(podXcodeBuildCommand, project.logger) { directory(podsXcodeProjDir.parentFile) }
}
}
data class PodBuildSettingsProperties( data class PodBuildSettingsProperties(
internal val buildDir: String, internal val buildDir: String,
internal val configuration: String, internal val configuration: String,
@@ -475,7 +55,6 @@ data class PodBuildSettingsProperties(
fun readSettingsFromReader(reader: Reader): PodBuildSettingsProperties { fun readSettingsFromReader(reader: Reader): PodBuildSettingsProperties {
with(Properties()) { with(Properties()) {
@Suppress("BlockingMethodInNonBlockingContext") // It's ok to do blocking call here
load(reader) load(reader)
return PodBuildSettingsProperties( return PodBuildSettingsProperties(
readProperty(BUILD_DIR), readProperty(BUILD_DIR),
@@ -497,28 +76,3 @@ data class PodBuildSettingsProperties(
getProperty(propertyName) getProperty(propertyName)
} }
} }
private object CocoapodsErrorHandlingUtil {
fun handle(e: IOException, command: List<String>) {
if (e.message?.contains("No such file or directory") == true) {
val message = """
|'${command.take(2).joinToString(" ")}' command failed with an exception:
| ${e.message}
|
| Full command: ${command.joinToString(" ")}
|
| Possible reason: CocoaPods is not installed
| Please check that CocoaPods v1.10 or above is installed.
|
| To check CocoaPods version type 'pod --version' in the terminal
|
| To install CocoaPods execute 'sudo gem install cocoapods'
|
""".trimMargin()
throw IllegalStateException(message)
} else {
throw e
}
}
}
@@ -3,44 +3,16 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
@file:Suppress("LeakingThis") // All tasks should be inherited only by Gradle @file:Suppress("LeakingThis", "PackageDirectoryMismatch") // All tasks should be inherited only by Gradle, Old package for compatibility
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.file.FileCollection
import org.gradle.api.file.FileTree
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
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.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs
import org.jetbrains.kotlin.gradle.plugin.cocoapods.platformLiteral
import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingCocoapodsMessage
import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingSpecReposMessage
import org.jetbrains.kotlin.gradle.utils.runCommand import org.jetbrains.kotlin.gradle.utils.runCommand
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.HostManager
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.Reader
import java.util.*
val CocoapodsDependency.schemeName: String
get() = name.split("/")[0]
open class CocoapodsTask : DefaultTask() {
init {
onlyIf {
HostManager.hostIsMac
}
}
}
/** /**
* The task takes the path to the Podfile and calls `pod install` * The task takes the path to the Podfile and calls `pod install`
@@ -91,413 +63,6 @@ abstract class AbstractPodInstallTask : CocoapodsTask() {
abstract fun handleError(retCode: Int, error: String, process: Process): String? abstract fun handleError(retCode: Int, error: String, process: Process): String?
} }
abstract class PodInstallTask : AbstractPodInstallTask() {
@get:Optional
@get:InputFile
abstract val podspec: Property<File?>
@get:Input
abstract val frameworkName: Property<String>
@get:Nested
abstract val specRepos: Property<SpecRepos>
@get:Nested
abstract val pods: ListProperty<CocoapodsDependency>
@get:InputDirectory
abstract val dummyFramework: Property<File>
private val framework = project.provider { project.cocoapodsBuildDirs.framework.resolve("${frameworkName.get()}.framework") }
private val tmpFramework = dummyFramework.map { dummy -> dummy.parentFile.resolve("tmp.framework").also { it.deleteOnExit() } }
override fun doPodInstall() {
// We always need to execute 'pod install' with the dummy framework because the one left from a previous build
// may have a wrong linkage type. So we temporarily swap them, run 'pod install' and then swap them back
framework.rename(tmpFramework)
dummyFramework.rename(framework)
super.doPodInstall()
framework.rename(dummyFramework)
tmpFramework.rename(framework)
}
private fun Provider<File>.rename(dest: Provider<File>) = get().rename(dest.get())
private fun File.rename(dest: File) {
if (!exists()) {
mkdirs()
}
check(renameTo(dest)) { "Can't rename '${this}' to '${dest}'" }
}
override fun handleError(retCode: Int, error: String, process: Process): String? {
val specReposMessages = MissingSpecReposMessage(specRepos.get()).missingMessage
val cocoapodsMessages = pods.get().map { MissingCocoapodsMessage(it).missingMessage }
return listOfNotNull(
"'pod install' command failed with code $retCode.",
"Error message:",
error.lines().filter { it.isNotBlank() }.joinToString("\n"),
"""
| Please, check that podfile contains following lines in header:
| $specReposMessages
|
""".trimMargin(),
"""
| Please, check that each target depended on ${frameworkName.get()} contains following dependencies:
| ${cocoapodsMessages.joinToString("\n")}
|
""".trimMargin()
).joinToString("\n")
}
}
abstract class PodInstallSyntheticTask : AbstractPodInstallTask() {
@get:Input
abstract val family: Property<Family>
@get:Input
abstract val podName: Property<String>
@get:OutputDirectory
internal val syntheticXcodeProject: Provider<File> = workingDir.map { it.resolve("synthetic.xcodeproj") }
override fun doPodInstall() {
val projResource = "/cocoapods/project.pbxproj"
val projDestination = syntheticXcodeProject.get().resolve("project.pbxproj")
syntheticXcodeProject.get().mkdirs()
projDestination.outputStream().use { file ->
javaClass.getResourceAsStream(projResource)!!.use { resource ->
resource.copyTo(file)
}
}
super.doPodInstall()
}
override fun handleError(retCode: Int, error: String, process: Process): String? {
var message = """
|'pod install' command on the synthetic project failed with return code: $retCode
|
| Error: ${error.lines().filter { it.contains("[!]") }.joinToString("\n")}
|
""".trimMargin()
if (
error.contains("deployment target") ||
error.contains("no platform was specified") ||
error.contains(Regex("The platform of the target .+ is not compatible with `${podName.get()}"))
) {
message += """
|
| Possible reason: ${family.get().platformLiteral} deployment target is not configured
| Configure deployment_target for ALL targets as follows:
| cocoapods {
| ...
| ${family.get().platformLiteral}.deploymentTarget = "..."
| ...
| }
|
""".trimMargin()
return message
} else if (
error.contains("Unable to add a source with url") ||
error.contains("Couldn't determine repo name for URL") ||
error.contains("Unable to find a specification")
) {
message += """
|
| Possible reason: spec repos are not configured correctly.
| Ensure that spec repos are correctly configured for all private pod dependencies:
| cocoapods {
| specRepos {
| url("<private spec repo url>")
| }
| }
|
""".trimMargin()
return message
} else {
return null
}
}
}
/**
* The task generates a synthetic project with all cocoapods dependencies
*/
abstract class PodGenTask : CocoapodsTask() {
init {
onlyIf {
pods.get().isNotEmpty()
}
}
@get:InputFile
internal abstract val podspec: Property<File>
@get:Input
internal abstract val podName: Property<String>
@get:Input
internal abstract val useLibraries: Property<Boolean>
@get:Input
internal abstract val family: Property<Family>
@get:Nested
internal abstract val platformSettings: Property<PodspecPlatformSettings>
@get:Nested
internal abstract val specRepos: Property<SpecRepos>
@get:Nested
internal abstract val pods: ListProperty<CocoapodsDependency>
@get:OutputFile
val podfile: Provider<File> = family.map { project.cocoapodsBuildDirs.synthetic(it).resolve("Podfile") }
@TaskAction
fun generate() {
val specRepos = specRepos.get().getAll()
val podfile = this.podfile.get()
podfile.createNewFile()
val podfileContent = getPodfileContent(specRepos, family.get().platformLiteral)
podfile.writeText(podfileContent)
}
private fun getPodfileContent(specRepos: Collection<String>, xcodeTarget: String) =
buildString {
specRepos.forEach {
appendLine("source '$it'")
}
appendLine("target '$xcodeTarget' do")
if (useLibraries.get().not()) {
appendLine("\tuse_frameworks!")
}
val settings = platformSettings.get()
val deploymentTarget = settings.deploymentTarget
if (deploymentTarget != null) {
appendLine("\tplatform :${settings.name}, '$deploymentTarget'")
} else {
appendLine("\tplatform :${settings.name}")
}
pods.get().mapNotNull {
buildString {
append("pod '${it.name}'")
val version = it.version
val source = it.source
if (source != null) {
append(", ${source.getPodSourcePath()}")
} else if (version != null) {
append(", '$version'")
}
}
}.forEach { appendLine("\t$it") }
appendLine("end\n")
//disable signing for all synthetic pods KT-54314
append(
"""
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = ""
config.build_settings['CODE_SIGNING_REQUIRED'] = "NO"
config.build_settings['CODE_SIGNING_ALLOWED'] = "NO"
end
end
end
""".trimIndent()
)
appendLine()
}
}
open class PodSetupBuildTask : CocoapodsTask() {
@get:Input
lateinit var frameworkName: Provider<String>
@get:Input
internal lateinit var sdk: Provider<String>
@get:Nested
lateinit var pod: Provider<CocoapodsDependency>
@get:OutputFile
val buildSettingsFile: Provider<File> = project.provider {
project.cocoapodsBuildDirs
.buildSettings
.resolve(getBuildSettingFileName(pod.get(), sdk.get()))
}
@get:Internal
internal lateinit var podsXcodeProjDir: Provider<File>
@TaskAction
fun setupBuild() {
val podsXcodeProjDir = podsXcodeProjDir.get()
val buildSettingsReceivingCommand = listOf(
"xcodebuild", "-showBuildSettings",
"-project", podsXcodeProjDir.name,
"-scheme", pod.get().schemeName,
"-sdk", sdk.get()
)
val outputText = runCommand(buildSettingsReceivingCommand, project.logger) { directory(podsXcodeProjDir.parentFile) }
val buildSettingsProperties = PodBuildSettingsProperties.readSettingsFromReader(outputText.reader())
buildSettingsFile.get().let { bsf ->
buildSettingsProperties.writeSettings(bsf)
}
}
}
private fun getBuildSettingFileName(pod: CocoapodsDependency, sdk: String): String =
"build-settings-$sdk-${pod.schemeName}.properties"
/**
* The task compiles external cocoa pods sources.
*/
open class PodBuildTask : CocoapodsTask() {
@get:PathSensitive(PathSensitivity.ABSOLUTE)
@get:InputFile
lateinit var buildSettingsFile: Provider<File>
internal set
@get:Nested
internal lateinit var pod: Provider<CocoapodsDependency>
@get:PathSensitive(PathSensitivity.ABSOLUTE)
@get:IgnoreEmptyDirectories
@get:InputFiles
internal val srcDir: FileTree
get() = project.fileTree(
buildSettingsFile.map { PodBuildSettingsProperties.readSettingsFromReader(it.reader()).podsTargetSrcRoot }
)
@get:Internal
internal var buildDir: Provider<File> = project.provider {
project.file(PodBuildSettingsProperties.readSettingsFromReader(buildSettingsFile.get().reader()).buildDir)
}
@get:Input
internal lateinit var sdk: Provider<String>
@Suppress("unused") // declares an ouptut
@get:OutputFiles
internal val buildResult: Provider<FileCollection> = project.provider {
project.fileTree(buildDir.get()) {
it.include("**/${pod.get().schemeName}.*/")
it.include("**/${pod.get().schemeName}/")
}
}
@get:Internal
internal lateinit var podsXcodeProjDir: Provider<File>
@TaskAction
fun buildDependencies() {
val podBuildSettings = PodBuildSettingsProperties.readSettingsFromReader(buildSettingsFile.get().reader())
val podsXcodeProjDir = podsXcodeProjDir.get()
val podXcodeBuildCommand = listOf(
"xcodebuild",
"-project", podsXcodeProjDir.name,
"-scheme", pod.get().schemeName,
"-sdk", sdk.get(),
"-configuration", podBuildSettings.configuration
)
runCommand(podXcodeBuildCommand, project.logger) { directory(podsXcodeProjDir.parentFile) }
}
}
data class PodBuildSettingsProperties(
internal val buildDir: String,
internal val configuration: String,
val configurationBuildDir: String,
internal val podsTargetSrcRoot: String,
internal val cflags: String? = null,
internal val headerPaths: String? = null,
internal val publicHeadersFolderPath: String? = null,
internal val frameworkPaths: String? = null
) {
fun writeSettings(
buildSettingsFile: File
) {
buildSettingsFile.parentFile.mkdirs()
buildSettingsFile.delete()
buildSettingsFile.createNewFile()
check(buildSettingsFile.exists()) { "Unable to create file ${buildSettingsFile.path}!" }
with(buildSettingsFile) {
appendText("$BUILD_DIR=$buildDir\n")
appendText("$CONFIGURATION=$configuration\n")
appendText("$CONFIGURATION_BUILD_DIR=$configurationBuildDir\n")
appendText("$PODS_TARGET_SRCROOT=$podsTargetSrcRoot\n")
cflags?.let { appendText("$OTHER_CFLAGS=$it\n") }
headerPaths?.let { appendText("$HEADER_SEARCH_PATHS=$it\n") }
publicHeadersFolderPath?.let { appendText("$PUBLIC_HEADERS_FOLDER_PATH=$it\n") }
frameworkPaths?.let { appendText("$FRAMEWORK_SEARCH_PATHS=$it") }
}
}
companion object {
const val BUILD_DIR = "BUILD_DIR"
const val CONFIGURATION = "CONFIGURATION"
const val CONFIGURATION_BUILD_DIR = "CONFIGURATION_BUILD_DIR"
const val PODS_TARGET_SRCROOT = "PODS_TARGET_SRCROOT"
const val OTHER_CFLAGS = "OTHER_CFLAGS"
const val HEADER_SEARCH_PATHS = "HEADER_SEARCH_PATHS"
const val PUBLIC_HEADERS_FOLDER_PATH = "PUBLIC_HEADERS_FOLDER_PATH"
const val FRAMEWORK_SEARCH_PATHS = "FRAMEWORK_SEARCH_PATHS"
fun readSettingsFromReader(reader: Reader): PodBuildSettingsProperties {
with(Properties()) {
@Suppress("BlockingMethodInNonBlockingContext") // It's ok to do blocking call here
load(reader)
return PodBuildSettingsProperties(
readProperty(BUILD_DIR),
readProperty(CONFIGURATION),
readProperty(CONFIGURATION_BUILD_DIR),
readProperty(PODS_TARGET_SRCROOT),
readNullableProperty(OTHER_CFLAGS),
readNullableProperty(HEADER_SEARCH_PATHS),
readNullableProperty(PUBLIC_HEADERS_FOLDER_PATH),
readNullableProperty(FRAMEWORK_SEARCH_PATHS)
)
}
}
private fun Properties.readProperty(propertyName: String) =
readNullableProperty(propertyName) ?: error("$propertyName property is absent")
private fun Properties.readNullableProperty(propertyName: String) =
getProperty(propertyName)
}
}
private object CocoapodsErrorHandlingUtil { private object CocoapodsErrorHandlingUtil {
fun handle(e: IOException, command: List<String>) { fun handle(e: IOException, command: List<String>) {
if (e.message?.contains("No such file or directory") == true) { if (e.message?.contains("No such file or directory") == true) {
@@ -3,36 +3,17 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
@file:Suppress("LeakingThis") // All tasks should be inherited only by Gradle @file:Suppress("LeakingThis", "PackageDirectoryMismatch") // All tasks should be inherited only by Gradle, Old package for compatibility
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.file.FileCollection import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency
import org.gradle.api.file.FileTree
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
import org.gradle.api.tasks.Optional
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs
import org.jetbrains.kotlin.gradle.plugin.cocoapods.platformLiteral
import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingCocoapodsMessage
import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingSpecReposMessage
import org.jetbrains.kotlin.gradle.utils.runCommand
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.kotlin.konan.target.HostManager
import java.io.File
import java.io.IOException
import java.io.Reader
import java.util.*
val CocoapodsDependency.schemeName: String val CocoapodsDependency.schemeName: String
get() = name.split("/")[0] get() = name.split("/")[0]
open class CocoapodsTask : DefaultTask() { open class CocoapodsTask : DefaultTask() {
init { init {
onlyIf { onlyIf {
@@ -40,485 +21,3 @@ open class CocoapodsTask : DefaultTask() {
} }
} }
} }
/**
* The task takes the path to the Podfile and calls `pod install`
* to obtain sources or artifacts for the declared dependencies.
* This task is a part of CocoaPods integration infrastructure.
*/
abstract class AbstractPodInstallTask : CocoapodsTask() {
init {
onlyIf { podfile.isPresent }
}
@get:Optional
@get:InputFile
abstract val podfile: Property<File?>
@get:Internal
protected val workingDir: Provider<File> = podfile.map { file: File? ->
requireNotNull(file) { "Task outputs shouldn't be queried if it's skipped" }.parentFile
}
@get:OutputDirectory
internal val podsDir: Provider<File> = workingDir.map { it.resolve("Pods") }
@get:Internal
internal val podsXcodeProjDirProvider: Provider<File> = podsDir.map { it.resolve("Pods.xcodeproj") }
@TaskAction
open fun doPodInstall() {
val podInstallCommand = listOf("pod", "install")
runCommand(podInstallCommand,
logger,
errorHandler = ::handleError,
exceptionHandler = { e: IOException ->
CocoapodsErrorHandlingUtil.handle(e, podInstallCommand)
},
processConfiguration = {
directory(workingDir.get())
})
with(podsXcodeProjDirProvider.get()) {
check(exists() && isDirectory) {
"The directory 'Pods/Pods.xcodeproj' was not created as a result of the `pod install` call."
}
}
}
abstract fun handleError(retCode: Int, error: String, process: Process): String?
}
abstract class PodInstallTask : AbstractPodInstallTask() {
@get:Optional
@get:InputFile
abstract val podspec: Property<File?>
@get:Input
abstract val frameworkName: Property<String>
@get:Nested
abstract val specRepos: Property<SpecRepos>
@get:Nested
abstract val pods: ListProperty<CocoapodsDependency>
@get:InputDirectory
abstract val dummyFramework: Property<File>
private val framework = project.provider { project.cocoapodsBuildDirs.framework.resolve("${frameworkName.get()}.framework") }
private val tmpFramework = dummyFramework.map { dummy -> dummy.parentFile.resolve("tmp.framework").also { it.deleteOnExit() } }
override fun doPodInstall() {
// We always need to execute 'pod install' with the dummy framework because the one left from a previous build
// may have a wrong linkage type. So we temporarily swap them, run 'pod install' and then swap them back
framework.rename(tmpFramework)
dummyFramework.rename(framework)
super.doPodInstall()
framework.rename(dummyFramework)
tmpFramework.rename(framework)
}
private fun Provider<File>.rename(dest: Provider<File>) = get().rename(dest.get())
private fun File.rename(dest: File) {
if (!exists()) {
mkdirs()
}
check(renameTo(dest)) { "Can't rename '${this}' to '${dest}'" }
}
override fun handleError(retCode: Int, error: String, process: Process): String? {
val specReposMessages = MissingSpecReposMessage(specRepos.get()).missingMessage
val cocoapodsMessages = pods.get().map { MissingCocoapodsMessage(it).missingMessage }
return listOfNotNull(
"'pod install' command failed with code $retCode.",
"Error message:",
error.lines().filter { it.isNotBlank() }.joinToString("\n"),
"""
| Please, check that podfile contains following lines in header:
| $specReposMessages
|
""".trimMargin(),
"""
| Please, check that each target depended on ${frameworkName.get()} contains following dependencies:
| ${cocoapodsMessages.joinToString("\n")}
|
""".trimMargin()
).joinToString("\n")
}
}
abstract class PodInstallSyntheticTask : AbstractPodInstallTask() {
@get:Input
abstract val family: Property<Family>
@get:Input
abstract val podName: Property<String>
@get:OutputDirectory
internal val syntheticXcodeProject: Provider<File> = workingDir.map { it.resolve("synthetic.xcodeproj") }
override fun doPodInstall() {
val projResource = "/cocoapods/project.pbxproj"
val projDestination = syntheticXcodeProject.get().resolve("project.pbxproj")
syntheticXcodeProject.get().mkdirs()
projDestination.outputStream().use { file ->
javaClass.getResourceAsStream(projResource)!!.use { resource ->
resource.copyTo(file)
}
}
super.doPodInstall()
}
override fun handleError(retCode: Int, error: String, process: Process): String? {
var message = """
|'pod install' command on the synthetic project failed with return code: $retCode
|
| Error: ${error.lines().filter { it.contains("[!]") }.joinToString("\n")}
|
""".trimMargin()
if (
error.contains("deployment target") ||
error.contains("no platform was specified") ||
error.contains(Regex("The platform of the target .+ is not compatible with `${podName.get()}"))
) {
message += """
|
| Possible reason: ${family.get().platformLiteral} deployment target is not configured
| Configure deployment_target for ALL targets as follows:
| cocoapods {
| ...
| ${family.get().platformLiteral}.deploymentTarget = "..."
| ...
| }
|
""".trimMargin()
return message
} else if (
error.contains("Unable to add a source with url") ||
error.contains("Couldn't determine repo name for URL") ||
error.contains("Unable to find a specification")
) {
message += """
|
| Possible reason: spec repos are not configured correctly.
| Ensure that spec repos are correctly configured for all private pod dependencies:
| cocoapods {
| specRepos {
| url("<private spec repo url>")
| }
| }
|
""".trimMargin()
return message
} else {
return null
}
}
}
/**
* The task generates a synthetic project with all cocoapods dependencies
*/
abstract class PodGenTask : CocoapodsTask() {
init {
onlyIf {
pods.get().isNotEmpty()
}
}
@get:InputFile
internal abstract val podspec: Property<File>
@get:Input
internal abstract val podName: Property<String>
@get:Input
internal abstract val useLibraries: Property<Boolean>
@get:Input
internal abstract val family: Property<Family>
@get:Nested
internal abstract val platformSettings: Property<PodspecPlatformSettings>
@get:Nested
internal abstract val specRepos: Property<SpecRepos>
@get:Nested
internal abstract val pods: ListProperty<CocoapodsDependency>
@get:OutputFile
val podfile: Provider<File> = family.map { project.cocoapodsBuildDirs.synthetic(it).resolve("Podfile") }
@TaskAction
fun generate() {
val specRepos = specRepos.get().getAll()
val podfile = this.podfile.get()
podfile.createNewFile()
val podfileContent = getPodfileContent(specRepos, family.get().platformLiteral)
podfile.writeText(podfileContent)
}
private fun getPodfileContent(specRepos: Collection<String>, xcodeTarget: String) =
buildString {
specRepos.forEach {
appendLine("source '$it'")
}
appendLine("target '$xcodeTarget' do")
if (useLibraries.get().not()) {
appendLine("\tuse_frameworks!")
}
val settings = platformSettings.get()
val deploymentTarget = settings.deploymentTarget
if (deploymentTarget != null) {
appendLine("\tplatform :${settings.name}, '$deploymentTarget'")
} else {
appendLine("\tplatform :${settings.name}")
}
pods.get().mapNotNull {
buildString {
append("pod '${it.name}'")
val version = it.version
val source = it.source
if (source != null) {
append(", ${source.getPodSourcePath()}")
} else if (version != null) {
append(", '$version'")
}
}
}.forEach { appendLine("\t$it") }
appendLine("end\n")
//disable signing for all synthetic pods KT-54314
append(
"""
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = ""
config.build_settings['CODE_SIGNING_REQUIRED'] = "NO"
config.build_settings['CODE_SIGNING_ALLOWED'] = "NO"
end
end
end
""".trimIndent()
)
appendLine()
}
}
open class PodSetupBuildTask : CocoapodsTask() {
@get:Input
lateinit var frameworkName: Provider<String>
@get:Input
internal lateinit var sdk: Provider<String>
@get:Nested
lateinit var pod: Provider<CocoapodsDependency>
@get:OutputFile
val buildSettingsFile: Provider<File> = project.provider {
project.cocoapodsBuildDirs
.buildSettings
.resolve(getBuildSettingFileName(pod.get(), sdk.get()))
}
@get:Internal
internal lateinit var podsXcodeProjDir: Provider<File>
@TaskAction
fun setupBuild() {
val podsXcodeProjDir = podsXcodeProjDir.get()
val buildSettingsReceivingCommand = listOf(
"xcodebuild", "-showBuildSettings",
"-project", podsXcodeProjDir.name,
"-scheme", pod.get().schemeName,
"-sdk", sdk.get()
)
val outputText = runCommand(buildSettingsReceivingCommand, project.logger) { directory(podsXcodeProjDir.parentFile) }
val buildSettingsProperties = PodBuildSettingsProperties.readSettingsFromReader(outputText.reader())
buildSettingsFile.get().let { bsf ->
buildSettingsProperties.writeSettings(bsf)
}
}
}
private fun getBuildSettingFileName(pod: CocoapodsDependency, sdk: String): String =
"build-settings-$sdk-${pod.schemeName}.properties"
/**
* The task compiles external cocoa pods sources.
*/
open class PodBuildTask : CocoapodsTask() {
@get:PathSensitive(PathSensitivity.ABSOLUTE)
@get:InputFile
lateinit var buildSettingsFile: Provider<File>
internal set
@get:Nested
internal lateinit var pod: Provider<CocoapodsDependency>
@get:PathSensitive(PathSensitivity.ABSOLUTE)
@get:IgnoreEmptyDirectories
@get:InputFiles
internal val srcDir: FileTree
get() = project.fileTree(
buildSettingsFile.map { PodBuildSettingsProperties.readSettingsFromReader(it.reader()).podsTargetSrcRoot }
)
@get:Internal
internal var buildDir: Provider<File> = project.provider {
project.file(PodBuildSettingsProperties.readSettingsFromReader(buildSettingsFile.get().reader()).buildDir)
}
@get:Input
internal lateinit var sdk: Provider<String>
@Suppress("unused") // declares an ouptut
@get:OutputFiles
internal val buildResult: Provider<FileCollection> = project.provider {
project.fileTree(buildDir.get()) {
it.include("**/${pod.get().schemeName}.*/")
it.include("**/${pod.get().schemeName}/")
}
}
@get:Internal
internal lateinit var podsXcodeProjDir: Provider<File>
@TaskAction
fun buildDependencies() {
val podBuildSettings = PodBuildSettingsProperties.readSettingsFromReader(buildSettingsFile.get().reader())
val podsXcodeProjDir = podsXcodeProjDir.get()
val podXcodeBuildCommand = listOf(
"xcodebuild",
"-project", podsXcodeProjDir.name,
"-scheme", pod.get().schemeName,
"-sdk", sdk.get(),
"-configuration", podBuildSettings.configuration
)
runCommand(podXcodeBuildCommand, project.logger) { directory(podsXcodeProjDir.parentFile) }
}
}
data class PodBuildSettingsProperties(
internal val buildDir: String,
internal val configuration: String,
val configurationBuildDir: String,
internal val podsTargetSrcRoot: String,
internal val cflags: String? = null,
internal val headerPaths: String? = null,
internal val publicHeadersFolderPath: String? = null,
internal val frameworkPaths: String? = null
) {
fun writeSettings(
buildSettingsFile: File
) {
buildSettingsFile.parentFile.mkdirs()
buildSettingsFile.delete()
buildSettingsFile.createNewFile()
check(buildSettingsFile.exists()) { "Unable to create file ${buildSettingsFile.path}!" }
with(buildSettingsFile) {
appendText("$BUILD_DIR=$buildDir\n")
appendText("$CONFIGURATION=$configuration\n")
appendText("$CONFIGURATION_BUILD_DIR=$configurationBuildDir\n")
appendText("$PODS_TARGET_SRCROOT=$podsTargetSrcRoot\n")
cflags?.let { appendText("$OTHER_CFLAGS=$it\n") }
headerPaths?.let { appendText("$HEADER_SEARCH_PATHS=$it\n") }
publicHeadersFolderPath?.let { appendText("$PUBLIC_HEADERS_FOLDER_PATH=$it\n") }
frameworkPaths?.let { appendText("$FRAMEWORK_SEARCH_PATHS=$it") }
}
}
companion object {
const val BUILD_DIR = "BUILD_DIR"
const val CONFIGURATION = "CONFIGURATION"
const val CONFIGURATION_BUILD_DIR = "CONFIGURATION_BUILD_DIR"
const val PODS_TARGET_SRCROOT = "PODS_TARGET_SRCROOT"
const val OTHER_CFLAGS = "OTHER_CFLAGS"
const val HEADER_SEARCH_PATHS = "HEADER_SEARCH_PATHS"
const val PUBLIC_HEADERS_FOLDER_PATH = "PUBLIC_HEADERS_FOLDER_PATH"
const val FRAMEWORK_SEARCH_PATHS = "FRAMEWORK_SEARCH_PATHS"
fun readSettingsFromReader(reader: Reader): PodBuildSettingsProperties {
with(Properties()) {
@Suppress("BlockingMethodInNonBlockingContext") // It's ok to do blocking call here
load(reader)
return PodBuildSettingsProperties(
readProperty(BUILD_DIR),
readProperty(CONFIGURATION),
readProperty(CONFIGURATION_BUILD_DIR),
readProperty(PODS_TARGET_SRCROOT),
readNullableProperty(OTHER_CFLAGS),
readNullableProperty(HEADER_SEARCH_PATHS),
readNullableProperty(PUBLIC_HEADERS_FOLDER_PATH),
readNullableProperty(FRAMEWORK_SEARCH_PATHS)
)
}
}
private fun Properties.readProperty(propertyName: String) =
readNullableProperty(propertyName) ?: error("$propertyName property is absent")
private fun Properties.readNullableProperty(propertyName: String) =
getProperty(propertyName)
}
}
private object CocoapodsErrorHandlingUtil {
fun handle(e: IOException, command: List<String>) {
if (e.message?.contains("No such file or directory") == true) {
val message = """
|'${command.take(2).joinToString(" ")}' command failed with an exception:
| ${e.message}
|
| Full command: ${command.joinToString(" ")}
|
| Possible reason: CocoaPods is not installed
| Please check that CocoaPods v1.10 or above is installed.
|
| To check CocoaPods version type 'pod --version' in the terminal
|
| To install CocoaPods execute 'sudo gem install cocoapods'
|
""".trimMargin()
throw IllegalStateException(message)
} else {
throw e
}
}
}
@@ -7,310 +7,16 @@
package org.jetbrains.kotlin.gradle.tasks package org.jetbrains.kotlin.gradle.tasks
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionAware
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider import org.gradle.api.tasks.Input
import org.gradle.api.tasks.* import org.gradle.api.tasks.Nested
import org.gradle.api.tasks.wrapper.Wrapper import org.gradle.api.tasks.OutputFile
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.gradle.api.tasks.TaskAction
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.COCOAPODS_EXTENSION_NAME
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.GENERATE_WRAPPER_PROPERTY
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.SYNC_TASK_NAME
import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs
import org.jetbrains.kotlin.gradle.utils.appendLine import org.jetbrains.kotlin.gradle.utils.appendLine
import java.io.File import java.io.File
/**
* The task generates a podspec file which allows a user to
* integrate a Kotlin/Native framework into a CocoaPods project.
*/
open class PodspecTask : DefaultTask() {
@get:Input
internal val specName = project.objects.property(String::class.java)
@get:Internal
internal val outputDir = project.objects.property(File::class.java)
@get:OutputFile
val outputFile: File
get() = outputDir.get().resolve("${specName.get()}.podspec")
@get:Input
internal lateinit var needPodspec: Provider<Boolean>
@get:Nested
val pods = project.objects.listProperty(CocoapodsDependency::class.java)
@get:Input
internal val version = project.objects.property(String::class.java)
@get:Input
internal val publishing = project.objects.property(Boolean::class.java)
@get:Input
@get:Optional
internal val source = project.objects.property(String::class.java)
@get:Input
@get:Optional
internal val homepage = project.objects.property(String::class.java)
@get:Input
@get:Optional
internal val license = project.objects.property(String::class.java)
@get:Input
@get:Optional
internal val authors = project.objects.property(String::class.java)
@get:Input
@get:Optional
internal val summary = project.objects.property(String::class.java)
@get:Input
@get:Optional
internal val extraSpecAttributes = project.objects.mapProperty(String::class.java, String::class.java)
@get:Input
internal lateinit var frameworkName: Provider<String>
@get:Nested
internal lateinit var ios: Provider<PodspecPlatformSettings>
@get:Nested
internal lateinit var osx: Provider<PodspecPlatformSettings>
@get:Nested
internal lateinit var tvos: Provider<PodspecPlatformSettings>
@get:Nested
internal lateinit var watchos: Provider<PodspecPlatformSettings>
init {
onlyIf { needPodspec.get() }
}
@TaskAction
fun generate() {
check(version.get() != Project.DEFAULT_VERSION) {
"""
Cocoapods Integration requires pod version to be specified.
Please specify pod version by adding 'version = "<version>"' to the cocoapods block.
Alternatively, specify the version for the entire project explicitly.
Pod version format has to conform podspec syntax requirements: https://guides.cocoapods.org/syntax/podspec.html#version
""".trimIndent()
}
val gradleWrapper = (project.rootProject.tasks.getByName("wrapper") as? Wrapper)?.scriptFile
require(gradleWrapper != null && gradleWrapper.exists()) {
"""
The Gradle wrapper is required to run the build from Xcode.
Please run the same command with `-P$GENERATE_WRAPPER_PROPERTY=true` or run the `:wrapper` task to generate the wrapper manually.
See details about the wrapper at https://docs.gradle.org/current/userguide/gradle_wrapper.html
""".trimIndent()
}
val deploymentTargets = run {
listOf(ios, osx, tvos, watchos).map { it.get() }.filter { it.deploymentTarget != null }.joinToString("\n") {
if (extraSpecAttributes.get().containsKey("${it.name}.deployment_target")) "" else "| spec.${it.name}.deployment_target = '${it.deploymentTarget}'"
}
}
val dependencies = pods.get().map { pod ->
val versionSuffix = if (pod.version != null) ", '${pod.version}'" else ""
"| spec.dependency '${pod.name}'$versionSuffix"
}.joinToString(separator = "\n")
val frameworkDir = project.cocoapodsBuildDirs.framework.relativeTo(outputFile.parentFile)
val vendoredFramework = if (publishing.get()) "${frameworkName.get()}.xcframework" else frameworkDir.resolve("${frameworkName.get()}.framework").invariantSeparatorsPath
val vendoredFrameworks = if (extraSpecAttributes.get().containsKey("vendored_frameworks")) "" else "| spec.vendored_frameworks = '$vendoredFramework'"
val libraries = if (extraSpecAttributes.get().containsKey("libraries")) "" else "| spec.libraries = 'c++'"
val xcConfig = if (publishing.get() || extraSpecAttributes.get().containsKey("pod_target_xcconfig")) "" else
""" |
| spec.pod_target_xcconfig = {
| 'KOTLIN_PROJECT_PATH' => '${if (project.depth != 0) project.path else ""}',
| 'PRODUCT_MODULE_NAME' => '${frameworkName.get()}',
| }
""".trimMargin()
val gradleCommand = "\$REPO_ROOT/${gradleWrapper.relativeTo(project.projectDir).invariantSeparatorsPath}"
val scriptPhase = if (publishing.get() || extraSpecAttributes.get().containsKey("script_phases")) "" else
""" |
| spec.script_phases = [
| {
| :name => 'Build ${specName.get()}',
| :execution_position => :before_compile,
| :shell_path => '/bin/sh',
| :script => <<-SCRIPT
| if [ "YES" = "${'$'}OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
| echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
| exit 0
| fi
| set -ev
| REPO_ROOT="${'$'}PODS_TARGET_SRCROOT"
| "$gradleCommand" -p "${'$'}REPO_ROOT" ${'$'}KOTLIN_PROJECT_PATH:$SYNC_TASK_NAME \
| -P${KotlinCocoapodsPlugin.PLATFORM_PROPERTY}=${'$'}PLATFORM_NAME \
| -P${KotlinCocoapodsPlugin.ARCHS_PROPERTY}="${'$'}ARCHS" \
| -P${KotlinCocoapodsPlugin.CONFIGURATION_PROPERTY}="${'$'}CONFIGURATION"
| SCRIPT
| }
| ]
""".trimMargin()
val customSpec = extraSpecAttributes.get().map { "| spec.${it.key} = ${it.value}" }.joinToString("\n")
with(outputFile) {
writeText(
"""
|Pod::Spec.new do |spec|
| spec.name = '${specName.get()}'
| spec.version = '${version.get()}'
| spec.homepage = ${homepage.getOrEmpty().surroundWithSingleQuotesIfNeeded()}
| spec.source = ${source.getOrElse("{ :http=> ''}")}
| spec.authors = ${authors.getOrEmpty().surroundWithSingleQuotesIfNeeded()}
| spec.license = ${license.getOrEmpty().surroundWithSingleQuotesIfNeeded()}
| spec.summary = '${summary.getOrEmpty()}'
$vendoredFrameworks
$libraries
$deploymentTargets
$dependencies
$xcConfig
$scriptPhase
$customSpec
|end
""".trimMargin()
)
if (hasPodfileOwnOrParent(project) && publishing.get().not()) {
logger.quiet(
"""
Generated a podspec file at: ${absolutePath}.
To include it in your Xcode project, check that the following dependency snippet exists in your Podfile:
pod '${specName.get()}', :path => '${parentFile.absolutePath}'
""".trimIndent()
)
}
}
}
private fun Provider<String>.getOrEmpty(): String = getOrElse("")
private fun String.surroundWithSingleQuotesIfNeeded(): String =
if (startsWith("{") || startsWith("<<-") || startsWith("'")) this else "'$this'"
companion object {
private val KotlinMultiplatformExtension?.cocoapodsExtensionOrNull: CocoapodsExtension?
get() = (this as? ExtensionAware)?.extensions?.findByName(COCOAPODS_EXTENSION_NAME) as? CocoapodsExtension
private fun hasPodfileOwnOrParent(project: Project): Boolean =
if (project.rootProject == project) project.multiplatformExtensionOrNull?.cocoapodsExtensionOrNull?.podfile != null
else project.multiplatformExtensionOrNull?.cocoapodsExtensionOrNull?.podfile != null
|| (project.parent?.let { hasPodfileOwnOrParent(it) } ?: false)
}
}
/**
* Creates a dummy framework in the target directory.
*
* We represent a Kotlin/Native module to CocoaPods as a vendored framework.
* CocoaPods needs access to such frameworks during installation process to obtain
* their type (static or dynamic) and configure the Xcode project accordingly.
* But we cannot build the real framework before installation because it may
* depend on CocoaPods libraries which are not downloaded and built at this stage.
* So we create a dummy static framework to allow CocoaPods install our pod correctly
* and then replace it with the real one during a real build process.
*/
abstract class DummyFrameworkTask : DefaultTask() {
@get:Input
abstract val frameworkName: Property<String>
@get:Input
abstract val useStaticFramework: Property<Boolean>
@get:OutputDirectory
val outputFramework: Provider<File> = project.provider { project.cocoapodsBuildDirs.dummyFramework }
private val dummyFrameworkResource: String
get() {
val staticOrDynamic = if (!useStaticFramework.get()) "dynamic" else "static"
return "/cocoapods/$staticOrDynamic/dummy.framework/"
}
private fun copyResource(from: String, to: File) {
to.parentFile.mkdirs()
to.outputStream().use { file ->
javaClass.getResourceAsStream(from)!!.use { resource ->
resource.copyTo(file)
}
}
}
private fun copyTextResource(from: String, to: File, transform: (String) -> String = { it }) {
to.parentFile.mkdirs()
to.printWriter().use { file ->
javaClass.getResourceAsStream(from)!!.use {
it.reader().forEachLine { str ->
file.println(transform(str))
}
}
}
}
private fun copyFrameworkFile(relativeFrom: String, relativeTo: String = relativeFrom) =
copyResource(
"$dummyFrameworkResource$relativeFrom",
outputFramework.get().resolve(relativeTo)
)
private fun copyFrameworkTextFile(
relativeFrom: String,
relativeTo: String = relativeFrom,
transform: (String) -> String = { it }
) = copyTextResource(
"$dummyFrameworkResource$relativeFrom",
outputFramework.get().resolve(relativeTo),
transform
)
@TaskAction
fun create() {
// Reset the destination directory
with(outputFramework.get()) {
deleteRecursively()
mkdirs()
}
// Copy files for the dummy framework.
copyFrameworkFile("Info.plist")
copyFrameworkFile("dummy", frameworkName.get())
copyFrameworkFile("Headers/placeholder.h")
copyFrameworkTextFile("Modules/module.modulemap") {
if (it == "framework module dummy {") {
it.replace("dummy", frameworkName.get())
} else {
it
}
}
}
}
/** /**
* Generates a def-file for the given CocoaPods dependency. * Generates a def-file for the given CocoaPods dependency.
*/ */
@@ -7,224 +7,14 @@
package org.jetbrains.kotlin.gradle.tasks package org.jetbrains.kotlin.gradle.tasks
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionAware
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider import org.gradle.api.provider.Provider
import org.gradle.api.tasks.* import org.gradle.api.tasks.Input
import org.gradle.api.tasks.wrapper.Wrapper import org.gradle.api.tasks.OutputDirectory
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.gradle.api.tasks.TaskAction
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.COCOAPODS_EXTENSION_NAME
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.GENERATE_WRAPPER_PROPERTY
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.SYNC_TASK_NAME
import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs
import org.jetbrains.kotlin.gradle.utils.appendLine
import java.io.File import java.io.File
/**
* The task generates a podspec file which allows a user to
* integrate a Kotlin/Native framework into a CocoaPods project.
*/
open class PodspecTask : DefaultTask() {
@get:Input
internal val specName = project.objects.property(String::class.java)
@get:Internal
internal val outputDir = project.objects.property(File::class.java)
@get:OutputFile
val outputFile: File
get() = outputDir.get().resolve("${specName.get()}.podspec")
@get:Input
internal lateinit var needPodspec: Provider<Boolean>
@get:Nested
val pods = project.objects.listProperty(CocoapodsDependency::class.java)
@get:Input
internal val version = project.objects.property(String::class.java)
@get:Input
internal val publishing = project.objects.property(Boolean::class.java)
@get:Input
@get:Optional
internal val source = project.objects.property(String::class.java)
@get:Input
@get:Optional
internal val homepage = project.objects.property(String::class.java)
@get:Input
@get:Optional
internal val license = project.objects.property(String::class.java)
@get:Input
@get:Optional
internal val authors = project.objects.property(String::class.java)
@get:Input
@get:Optional
internal val summary = project.objects.property(String::class.java)
@get:Input
@get:Optional
internal val extraSpecAttributes = project.objects.mapProperty(String::class.java, String::class.java)
@get:Input
internal lateinit var frameworkName: Provider<String>
@get:Nested
internal lateinit var ios: Provider<PodspecPlatformSettings>
@get:Nested
internal lateinit var osx: Provider<PodspecPlatformSettings>
@get:Nested
internal lateinit var tvos: Provider<PodspecPlatformSettings>
@get:Nested
internal lateinit var watchos: Provider<PodspecPlatformSettings>
init {
onlyIf { needPodspec.get() }
}
@TaskAction
fun generate() {
check(version.get() != Project.DEFAULT_VERSION) {
"""
Cocoapods Integration requires pod version to be specified.
Please specify pod version by adding 'version = "<version>"' to the cocoapods block.
Alternatively, specify the version for the entire project explicitly.
Pod version format has to conform podspec syntax requirements: https://guides.cocoapods.org/syntax/podspec.html#version
""".trimIndent()
}
val gradleWrapper = (project.rootProject.tasks.getByName("wrapper") as? Wrapper)?.scriptFile
require(gradleWrapper != null && gradleWrapper.exists()) {
"""
The Gradle wrapper is required to run the build from Xcode.
Please run the same command with `-P$GENERATE_WRAPPER_PROPERTY=true` or run the `:wrapper` task to generate the wrapper manually.
See details about the wrapper at https://docs.gradle.org/current/userguide/gradle_wrapper.html
""".trimIndent()
}
val deploymentTargets = run {
listOf(ios, osx, tvos, watchos).map { it.get() }.filter { it.deploymentTarget != null }.joinToString("\n") {
if (extraSpecAttributes.get().containsKey("${it.name}.deployment_target")) "" else "| spec.${it.name}.deployment_target = '${it.deploymentTarget}'"
}
}
val dependencies = pods.get().map { pod ->
val versionSuffix = if (pod.version != null) ", '${pod.version}'" else ""
"| spec.dependency '${pod.name}'$versionSuffix"
}.joinToString(separator = "\n")
val frameworkDir = project.cocoapodsBuildDirs.framework.relativeTo(outputFile.parentFile)
val vendoredFramework = if (publishing.get()) "${frameworkName.get()}.xcframework" else frameworkDir.resolve("${frameworkName.get()}.framework").invariantSeparatorsPath
val vendoredFrameworks = if (extraSpecAttributes.get().containsKey("vendored_frameworks")) "" else "| spec.vendored_frameworks = '$vendoredFramework'"
val libraries = if (extraSpecAttributes.get().containsKey("libraries")) "" else "| spec.libraries = 'c++'"
val xcConfig = if (publishing.get() || extraSpecAttributes.get().containsKey("pod_target_xcconfig")) "" else
""" |
| spec.pod_target_xcconfig = {
| 'KOTLIN_PROJECT_PATH' => '${if (project.depth != 0) project.path else ""}',
| 'PRODUCT_MODULE_NAME' => '${frameworkName.get()}',
| }
""".trimMargin()
val gradleCommand = "\$REPO_ROOT/${gradleWrapper.relativeTo(project.projectDir).invariantSeparatorsPath}"
val scriptPhase = if (publishing.get() || extraSpecAttributes.get().containsKey("script_phases")) "" else
""" |
| spec.script_phases = [
| {
| :name => 'Build ${specName.get()}',
| :execution_position => :before_compile,
| :shell_path => '/bin/sh',
| :script => <<-SCRIPT
| if [ "YES" = "${'$'}OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
| echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
| exit 0
| fi
| set -ev
| REPO_ROOT="${'$'}PODS_TARGET_SRCROOT"
| "$gradleCommand" -p "${'$'}REPO_ROOT" ${'$'}KOTLIN_PROJECT_PATH:$SYNC_TASK_NAME \
| -P${KotlinCocoapodsPlugin.PLATFORM_PROPERTY}=${'$'}PLATFORM_NAME \
| -P${KotlinCocoapodsPlugin.ARCHS_PROPERTY}="${'$'}ARCHS" \
| -P${KotlinCocoapodsPlugin.CONFIGURATION_PROPERTY}="${'$'}CONFIGURATION"
| SCRIPT
| }
| ]
""".trimMargin()
val customSpec = extraSpecAttributes.get().map { "| spec.${it.key} = ${it.value}" }.joinToString("\n")
with(outputFile) {
writeText(
"""
|Pod::Spec.new do |spec|
| spec.name = '${specName.get()}'
| spec.version = '${version.get()}'
| spec.homepage = ${homepage.getOrEmpty().surroundWithSingleQuotesIfNeeded()}
| spec.source = ${source.getOrElse("{ :http=> ''}")}
| spec.authors = ${authors.getOrEmpty().surroundWithSingleQuotesIfNeeded()}
| spec.license = ${license.getOrEmpty().surroundWithSingleQuotesIfNeeded()}
| spec.summary = '${summary.getOrEmpty()}'
$vendoredFrameworks
$libraries
$deploymentTargets
$dependencies
$xcConfig
$scriptPhase
$customSpec
|end
""".trimMargin()
)
if (hasPodfileOwnOrParent(project) && publishing.get().not()) {
logger.quiet(
"""
Generated a podspec file at: ${absolutePath}.
To include it in your Xcode project, check that the following dependency snippet exists in your Podfile:
pod '${specName.get()}', :path => '${parentFile.absolutePath}'
""".trimIndent()
)
}
}
}
private fun Provider<String>.getOrEmpty(): String = getOrElse("")
private fun String.surroundWithSingleQuotesIfNeeded(): String =
if (startsWith("{") || startsWith("<<-") || startsWith("'")) this else "'$this'"
companion object {
private val KotlinMultiplatformExtension?.cocoapodsExtensionOrNull: CocoapodsExtension?
get() = (this as? ExtensionAware)?.extensions?.findByName(COCOAPODS_EXTENSION_NAME) as? CocoapodsExtension
private fun hasPodfileOwnOrParent(project: Project): Boolean =
if (project.rootProject == project) project.multiplatformExtensionOrNull?.cocoapodsExtensionOrNull?.podfile != null
else project.multiplatformExtensionOrNull?.cocoapodsExtensionOrNull?.podfile != null
|| (project.parent?.let { hasPodfileOwnOrParent(it) } ?: false)
}
}
/** /**
* Creates a dummy framework in the target directory. * Creates a dummy framework in the target directory.
* *
@@ -310,45 +100,3 @@ abstract class DummyFrameworkTask : DefaultTask() {
} }
} }
} }
/**
* Generates a def-file for the given CocoaPods dependency.
*/
abstract class DefFileTask : DefaultTask() {
@get:Nested
abstract val pod: Property<CocoapodsDependency>
@get:Input
abstract val useLibraries: Property<Boolean>
@get:OutputFile
val outputFile: File
get() = project.cocoapodsBuildDirs.defs.resolve("${pod.get().moduleName}.def")
@TaskAction
fun generate() {
outputFile.parentFile.mkdirs()
outputFile.writeText(buildString {
appendLine("language = Objective-C")
with(pod.get()) {
when {
headers != null -> appendLine("headers = $headers")
useLibraries.get() -> logger.warn(
"""
w: Pod '$moduleName' should have 'headers' property specified when using 'useLibraries()'.
Otherwise code from this pod won't be accessible from Kotlin.
""".trimIndent()
)
else -> {
appendLine("modules = $moduleName")
// Linker opt with framework name is added so produced cinterop klib would have this flag inside its manifest
// This way error will be more obvious when someone will try to depend on a library with this cinterop
appendLine("linkerOpts = -framework $moduleName")
}
}
}
})
}
}
@@ -3,374 +3,20 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
@file:Suppress("LeakingThis") // All tasks should be inherited only by Gradle @file:Suppress("LeakingThis", "PackageDirectoryMismatch") // All tasks should be inherited only by Gradle, Old package for compatibility
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.file.FileCollection import org.gradle.api.file.FileCollection
import org.gradle.api.file.FileTree import org.gradle.api.file.FileTree
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
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.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.* import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.* import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs
import org.jetbrains.kotlin.gradle.plugin.cocoapods.platformLiteral
import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingCocoapodsMessage
import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingSpecReposMessage
import org.jetbrains.kotlin.gradle.utils.runCommand import org.jetbrains.kotlin.gradle.utils.runCommand
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.HostManager
import java.io.File import java.io.File
import java.io.IOException
import java.io.Reader
import java.util.* import java.util.*
val CocoapodsDependency.schemeName: String
get() = name.split("/")[0]
open class CocoapodsTask : DefaultTask() {
init {
onlyIf {
HostManager.hostIsMac
}
}
}
/**
* The task takes the path to the Podfile and calls `pod install`
* to obtain sources or artifacts for the declared dependencies.
* This task is a part of CocoaPods integration infrastructure.
*/
abstract class AbstractPodInstallTask : CocoapodsTask() {
init {
onlyIf { podfile.isPresent }
}
@get:Optional
@get:InputFile
abstract val podfile: Property<File?>
@get:Internal
protected val workingDir: Provider<File> = podfile.map { file: File? ->
requireNotNull(file) { "Task outputs shouldn't be queried if it's skipped" }.parentFile
}
@get:OutputDirectory
internal val podsDir: Provider<File> = workingDir.map { it.resolve("Pods") }
@get:Internal
internal val podsXcodeProjDirProvider: Provider<File> = podsDir.map { it.resolve("Pods.xcodeproj") }
@TaskAction
open fun doPodInstall() {
val podInstallCommand = listOf("pod", "install")
runCommand(podInstallCommand,
logger,
errorHandler = ::handleError,
exceptionHandler = { e: IOException ->
CocoapodsErrorHandlingUtil.handle(e, podInstallCommand)
},
processConfiguration = {
directory(workingDir.get())
})
with(podsXcodeProjDirProvider.get()) {
check(exists() && isDirectory) {
"The directory 'Pods/Pods.xcodeproj' was not created as a result of the `pod install` call."
}
}
}
abstract fun handleError(retCode: Int, error: String, process: Process): String?
}
abstract class PodInstallTask : AbstractPodInstallTask() {
@get:Optional
@get:InputFile
abstract val podspec: Property<File?>
@get:Input
abstract val frameworkName: Property<String>
@get:Nested
abstract val specRepos: Property<SpecRepos>
@get:Nested
abstract val pods: ListProperty<CocoapodsDependency>
@get:InputDirectory
abstract val dummyFramework: Property<File>
private val framework = project.provider { project.cocoapodsBuildDirs.framework.resolve("${frameworkName.get()}.framework") }
private val tmpFramework = dummyFramework.map { dummy -> dummy.parentFile.resolve("tmp.framework").also { it.deleteOnExit() } }
override fun doPodInstall() {
// We always need to execute 'pod install' with the dummy framework because the one left from a previous build
// may have a wrong linkage type. So we temporarily swap them, run 'pod install' and then swap them back
framework.rename(tmpFramework)
dummyFramework.rename(framework)
super.doPodInstall()
framework.rename(dummyFramework)
tmpFramework.rename(framework)
}
private fun Provider<File>.rename(dest: Provider<File>) = get().rename(dest.get())
private fun File.rename(dest: File) {
if (!exists()) {
mkdirs()
}
check(renameTo(dest)) { "Can't rename '${this}' to '${dest}'" }
}
override fun handleError(retCode: Int, error: String, process: Process): String? {
val specReposMessages = MissingSpecReposMessage(specRepos.get()).missingMessage
val cocoapodsMessages = pods.get().map { MissingCocoapodsMessage(it).missingMessage }
return listOfNotNull(
"'pod install' command failed with code $retCode.",
"Error message:",
error.lines().filter { it.isNotBlank() }.joinToString("\n"),
"""
| Please, check that podfile contains following lines in header:
| $specReposMessages
|
""".trimMargin(),
"""
| Please, check that each target depended on ${frameworkName.get()} contains following dependencies:
| ${cocoapodsMessages.joinToString("\n")}
|
""".trimMargin()
).joinToString("\n")
}
}
abstract class PodInstallSyntheticTask : AbstractPodInstallTask() {
@get:Input
abstract val family: Property<Family>
@get:Input
abstract val podName: Property<String>
@get:OutputDirectory
internal val syntheticXcodeProject: Provider<File> = workingDir.map { it.resolve("synthetic.xcodeproj") }
override fun doPodInstall() {
val projResource = "/cocoapods/project.pbxproj"
val projDestination = syntheticXcodeProject.get().resolve("project.pbxproj")
syntheticXcodeProject.get().mkdirs()
projDestination.outputStream().use { file ->
javaClass.getResourceAsStream(projResource)!!.use { resource ->
resource.copyTo(file)
}
}
super.doPodInstall()
}
override fun handleError(retCode: Int, error: String, process: Process): String? {
var message = """
|'pod install' command on the synthetic project failed with return code: $retCode
|
| Error: ${error.lines().filter { it.contains("[!]") }.joinToString("\n")}
|
""".trimMargin()
if (
error.contains("deployment target") ||
error.contains("no platform was specified") ||
error.contains(Regex("The platform of the target .+ is not compatible with `${podName.get()}"))
) {
message += """
|
| Possible reason: ${family.get().platformLiteral} deployment target is not configured
| Configure deployment_target for ALL targets as follows:
| cocoapods {
| ...
| ${family.get().platformLiteral}.deploymentTarget = "..."
| ...
| }
|
""".trimMargin()
return message
} else if (
error.contains("Unable to add a source with url") ||
error.contains("Couldn't determine repo name for URL") ||
error.contains("Unable to find a specification")
) {
message += """
|
| Possible reason: spec repos are not configured correctly.
| Ensure that spec repos are correctly configured for all private pod dependencies:
| cocoapods {
| specRepos {
| url("<private spec repo url>")
| }
| }
|
""".trimMargin()
return message
} else {
return null
}
}
}
/**
* The task generates a synthetic project with all cocoapods dependencies
*/
abstract class PodGenTask : CocoapodsTask() {
init {
onlyIf {
pods.get().isNotEmpty()
}
}
@get:InputFile
internal abstract val podspec: Property<File>
@get:Input
internal abstract val podName: Property<String>
@get:Input
internal abstract val useLibraries: Property<Boolean>
@get:Input
internal abstract val family: Property<Family>
@get:Nested
internal abstract val platformSettings: Property<PodspecPlatformSettings>
@get:Nested
internal abstract val specRepos: Property<SpecRepos>
@get:Nested
internal abstract val pods: ListProperty<CocoapodsDependency>
@get:OutputFile
val podfile: Provider<File> = family.map { project.cocoapodsBuildDirs.synthetic(it).resolve("Podfile") }
@TaskAction
fun generate() {
val specRepos = specRepos.get().getAll()
val podfile = this.podfile.get()
podfile.createNewFile()
val podfileContent = getPodfileContent(specRepos, family.get().platformLiteral)
podfile.writeText(podfileContent)
}
private fun getPodfileContent(specRepos: Collection<String>, xcodeTarget: String) =
buildString {
specRepos.forEach {
appendLine("source '$it'")
}
appendLine("target '$xcodeTarget' do")
if (useLibraries.get().not()) {
appendLine("\tuse_frameworks!")
}
val settings = platformSettings.get()
val deploymentTarget = settings.deploymentTarget
if (deploymentTarget != null) {
appendLine("\tplatform :${settings.name}, '$deploymentTarget'")
} else {
appendLine("\tplatform :${settings.name}")
}
pods.get().mapNotNull {
buildString {
append("pod '${it.name}'")
val version = it.version
val source = it.source
if (source != null) {
append(", ${source.getPodSourcePath()}")
} else if (version != null) {
append(", '$version'")
}
}
}.forEach { appendLine("\t$it") }
appendLine("end\n")
//disable signing for all synthetic pods KT-54314
append(
"""
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = ""
config.build_settings['CODE_SIGNING_REQUIRED'] = "NO"
config.build_settings['CODE_SIGNING_ALLOWED'] = "NO"
end
end
end
""".trimIndent()
)
appendLine()
}
}
open class PodSetupBuildTask : CocoapodsTask() {
@get:Input
lateinit var frameworkName: Provider<String>
@get:Input
internal lateinit var sdk: Provider<String>
@get:Nested
lateinit var pod: Provider<CocoapodsDependency>
@get:OutputFile
val buildSettingsFile: Provider<File> = project.provider {
project.cocoapodsBuildDirs
.buildSettings
.resolve(getBuildSettingFileName(pod.get(), sdk.get()))
}
@get:Internal
internal lateinit var podsXcodeProjDir: Provider<File>
@TaskAction
fun setupBuild() {
val podsXcodeProjDir = podsXcodeProjDir.get()
val buildSettingsReceivingCommand = listOf(
"xcodebuild", "-showBuildSettings",
"-project", podsXcodeProjDir.name,
"-scheme", pod.get().schemeName,
"-sdk", sdk.get()
)
val outputText = runCommand(buildSettingsReceivingCommand, project.logger) { directory(podsXcodeProjDir.parentFile) }
val buildSettingsProperties = PodBuildSettingsProperties.readSettingsFromReader(outputText.reader())
buildSettingsFile.get().let { bsf ->
buildSettingsProperties.writeSettings(bsf)
}
}
}
private fun getBuildSettingFileName(pod: CocoapodsDependency, sdk: String): String =
"build-settings-$sdk-${pod.schemeName}.properties"
/** /**
* The task compiles external cocoa pods sources. * The task compiles external cocoa pods sources.
*/ */
@@ -429,96 +75,3 @@ open class PodBuildTask : CocoapodsTask() {
runCommand(podXcodeBuildCommand, project.logger) { directory(podsXcodeProjDir.parentFile) } runCommand(podXcodeBuildCommand, project.logger) { directory(podsXcodeProjDir.parentFile) }
} }
} }
data class PodBuildSettingsProperties(
internal val buildDir: String,
internal val configuration: String,
val configurationBuildDir: String,
internal val podsTargetSrcRoot: String,
internal val cflags: String? = null,
internal val headerPaths: String? = null,
internal val publicHeadersFolderPath: String? = null,
internal val frameworkPaths: String? = null
) {
fun writeSettings(
buildSettingsFile: File
) {
buildSettingsFile.parentFile.mkdirs()
buildSettingsFile.delete()
buildSettingsFile.createNewFile()
check(buildSettingsFile.exists()) { "Unable to create file ${buildSettingsFile.path}!" }
with(buildSettingsFile) {
appendText("$BUILD_DIR=$buildDir\n")
appendText("$CONFIGURATION=$configuration\n")
appendText("$CONFIGURATION_BUILD_DIR=$configurationBuildDir\n")
appendText("$PODS_TARGET_SRCROOT=$podsTargetSrcRoot\n")
cflags?.let { appendText("$OTHER_CFLAGS=$it\n") }
headerPaths?.let { appendText("$HEADER_SEARCH_PATHS=$it\n") }
publicHeadersFolderPath?.let { appendText("$PUBLIC_HEADERS_FOLDER_PATH=$it\n") }
frameworkPaths?.let { appendText("$FRAMEWORK_SEARCH_PATHS=$it") }
}
}
companion object {
const val BUILD_DIR = "BUILD_DIR"
const val CONFIGURATION = "CONFIGURATION"
const val CONFIGURATION_BUILD_DIR = "CONFIGURATION_BUILD_DIR"
const val PODS_TARGET_SRCROOT = "PODS_TARGET_SRCROOT"
const val OTHER_CFLAGS = "OTHER_CFLAGS"
const val HEADER_SEARCH_PATHS = "HEADER_SEARCH_PATHS"
const val PUBLIC_HEADERS_FOLDER_PATH = "PUBLIC_HEADERS_FOLDER_PATH"
const val FRAMEWORK_SEARCH_PATHS = "FRAMEWORK_SEARCH_PATHS"
fun readSettingsFromReader(reader: Reader): PodBuildSettingsProperties {
with(Properties()) {
@Suppress("BlockingMethodInNonBlockingContext") // It's ok to do blocking call here
load(reader)
return PodBuildSettingsProperties(
readProperty(BUILD_DIR),
readProperty(CONFIGURATION),
readProperty(CONFIGURATION_BUILD_DIR),
readProperty(PODS_TARGET_SRCROOT),
readNullableProperty(OTHER_CFLAGS),
readNullableProperty(HEADER_SEARCH_PATHS),
readNullableProperty(PUBLIC_HEADERS_FOLDER_PATH),
readNullableProperty(FRAMEWORK_SEARCH_PATHS)
)
}
}
private fun Properties.readProperty(propertyName: String) =
readNullableProperty(propertyName) ?: error("$propertyName property is absent")
private fun Properties.readNullableProperty(propertyName: String) =
getProperty(propertyName)
}
}
private object CocoapodsErrorHandlingUtil {
fun handle(e: IOException, command: List<String>) {
if (e.message?.contains("No such file or directory") == true) {
val message = """
|'${command.take(2).joinToString(" ")}' command failed with an exception:
| ${e.message}
|
| Full command: ${command.joinToString(" ")}
|
| Possible reason: CocoaPods is not installed
| Please check that CocoaPods v1.10 or above is installed.
|
| To check CocoaPods version type 'pod --version' in the terminal
|
| To install CocoaPods execute 'sudo gem install cocoapods'
|
""".trimMargin()
throw IllegalStateException(message)
} else {
throw e
}
}
}
@@ -3,230 +3,19 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
@file:Suppress("LeakingThis") // All tasks should be inherited only by Gradle @file:Suppress("LeakingThis", "PackageDirectoryMismatch") // All tasks should be inherited only by Gradle, Old package for compatibility
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.file.FileCollection
import org.gradle.api.file.FileTree
import org.gradle.api.provider.ListProperty import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
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.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.* import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs
import org.jetbrains.kotlin.gradle.plugin.cocoapods.platformLiteral import org.jetbrains.kotlin.gradle.plugin.cocoapods.platformLiteral
import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingCocoapodsMessage
import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingSpecReposMessage
import org.jetbrains.kotlin.gradle.utils.runCommand
import org.jetbrains.kotlin.konan.target.Family import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.HostManager
import java.io.File import java.io.File
import java.io.IOException
import java.io.Reader
import java.util.*
val CocoapodsDependency.schemeName: String
get() = name.split("/")[0]
open class CocoapodsTask : DefaultTask() {
init {
onlyIf {
HostManager.hostIsMac
}
}
}
/**
* The task takes the path to the Podfile and calls `pod install`
* to obtain sources or artifacts for the declared dependencies.
* This task is a part of CocoaPods integration infrastructure.
*/
abstract class AbstractPodInstallTask : CocoapodsTask() {
init {
onlyIf { podfile.isPresent }
}
@get:Optional
@get:InputFile
abstract val podfile: Property<File?>
@get:Internal
protected val workingDir: Provider<File> = podfile.map { file: File? ->
requireNotNull(file) { "Task outputs shouldn't be queried if it's skipped" }.parentFile
}
@get:OutputDirectory
internal val podsDir: Provider<File> = workingDir.map { it.resolve("Pods") }
@get:Internal
internal val podsXcodeProjDirProvider: Provider<File> = podsDir.map { it.resolve("Pods.xcodeproj") }
@TaskAction
open fun doPodInstall() {
val podInstallCommand = listOf("pod", "install")
runCommand(podInstallCommand,
logger,
errorHandler = ::handleError,
exceptionHandler = { e: IOException ->
CocoapodsErrorHandlingUtil.handle(e, podInstallCommand)
},
processConfiguration = {
directory(workingDir.get())
})
with(podsXcodeProjDirProvider.get()) {
check(exists() && isDirectory) {
"The directory 'Pods/Pods.xcodeproj' was not created as a result of the `pod install` call."
}
}
}
abstract fun handleError(retCode: Int, error: String, process: Process): String?
}
abstract class PodInstallTask : AbstractPodInstallTask() {
@get:Optional
@get:InputFile
abstract val podspec: Property<File?>
@get:Input
abstract val frameworkName: Property<String>
@get:Nested
abstract val specRepos: Property<SpecRepos>
@get:Nested
abstract val pods: ListProperty<CocoapodsDependency>
@get:InputDirectory
abstract val dummyFramework: Property<File>
private val framework = project.provider { project.cocoapodsBuildDirs.framework.resolve("${frameworkName.get()}.framework") }
private val tmpFramework = dummyFramework.map { dummy -> dummy.parentFile.resolve("tmp.framework").also { it.deleteOnExit() } }
override fun doPodInstall() {
// We always need to execute 'pod install' with the dummy framework because the one left from a previous build
// may have a wrong linkage type. So we temporarily swap them, run 'pod install' and then swap them back
framework.rename(tmpFramework)
dummyFramework.rename(framework)
super.doPodInstall()
framework.rename(dummyFramework)
tmpFramework.rename(framework)
}
private fun Provider<File>.rename(dest: Provider<File>) = get().rename(dest.get())
private fun File.rename(dest: File) {
if (!exists()) {
mkdirs()
}
check(renameTo(dest)) { "Can't rename '${this}' to '${dest}'" }
}
override fun handleError(retCode: Int, error: String, process: Process): String? {
val specReposMessages = MissingSpecReposMessage(specRepos.get()).missingMessage
val cocoapodsMessages = pods.get().map { MissingCocoapodsMessage(it).missingMessage }
return listOfNotNull(
"'pod install' command failed with code $retCode.",
"Error message:",
error.lines().filter { it.isNotBlank() }.joinToString("\n"),
"""
| Please, check that podfile contains following lines in header:
| $specReposMessages
|
""".trimMargin(),
"""
| Please, check that each target depended on ${frameworkName.get()} contains following dependencies:
| ${cocoapodsMessages.joinToString("\n")}
|
""".trimMargin()
).joinToString("\n")
}
}
abstract class PodInstallSyntheticTask : AbstractPodInstallTask() {
@get:Input
abstract val family: Property<Family>
@get:Input
abstract val podName: Property<String>
@get:OutputDirectory
internal val syntheticXcodeProject: Provider<File> = workingDir.map { it.resolve("synthetic.xcodeproj") }
override fun doPodInstall() {
val projResource = "/cocoapods/project.pbxproj"
val projDestination = syntheticXcodeProject.get().resolve("project.pbxproj")
syntheticXcodeProject.get().mkdirs()
projDestination.outputStream().use { file ->
javaClass.getResourceAsStream(projResource)!!.use { resource ->
resource.copyTo(file)
}
}
super.doPodInstall()
}
override fun handleError(retCode: Int, error: String, process: Process): String? {
var message = """
|'pod install' command on the synthetic project failed with return code: $retCode
|
| Error: ${error.lines().filter { it.contains("[!]") }.joinToString("\n")}
|
""".trimMargin()
if (
error.contains("deployment target") ||
error.contains("no platform was specified") ||
error.contains(Regex("The platform of the target .+ is not compatible with `${podName.get()}"))
) {
message += """
|
| Possible reason: ${family.get().platformLiteral} deployment target is not configured
| Configure deployment_target for ALL targets as follows:
| cocoapods {
| ...
| ${family.get().platformLiteral}.deploymentTarget = "..."
| ...
| }
|
""".trimMargin()
return message
} else if (
error.contains("Unable to add a source with url") ||
error.contains("Couldn't determine repo name for URL") ||
error.contains("Unable to find a specification")
) {
message += """
|
| Possible reason: spec repos are not configured correctly.
| Ensure that spec repos are correctly configured for all private pod dependencies:
| cocoapods {
| specRepos {
| url("<private spec repo url>")
| }
| }
|
""".trimMargin()
return message
} else {
return null
}
}
}
/** /**
* The task generates a synthetic project with all cocoapods dependencies * The task generates a synthetic project with all cocoapods dependencies
@@ -325,200 +114,3 @@ abstract class PodGenTask : CocoapodsTask() {
appendLine() appendLine()
} }
} }
open class PodSetupBuildTask : CocoapodsTask() {
@get:Input
lateinit var frameworkName: Provider<String>
@get:Input
internal lateinit var sdk: Provider<String>
@get:Nested
lateinit var pod: Provider<CocoapodsDependency>
@get:OutputFile
val buildSettingsFile: Provider<File> = project.provider {
project.cocoapodsBuildDirs
.buildSettings
.resolve(getBuildSettingFileName(pod.get(), sdk.get()))
}
@get:Internal
internal lateinit var podsXcodeProjDir: Provider<File>
@TaskAction
fun setupBuild() {
val podsXcodeProjDir = podsXcodeProjDir.get()
val buildSettingsReceivingCommand = listOf(
"xcodebuild", "-showBuildSettings",
"-project", podsXcodeProjDir.name,
"-scheme", pod.get().schemeName,
"-sdk", sdk.get()
)
val outputText = runCommand(buildSettingsReceivingCommand, project.logger) { directory(podsXcodeProjDir.parentFile) }
val buildSettingsProperties = PodBuildSettingsProperties.readSettingsFromReader(outputText.reader())
buildSettingsFile.get().let { bsf ->
buildSettingsProperties.writeSettings(bsf)
}
}
}
private fun getBuildSettingFileName(pod: CocoapodsDependency, sdk: String): String =
"build-settings-$sdk-${pod.schemeName}.properties"
/**
* The task compiles external cocoa pods sources.
*/
open class PodBuildTask : CocoapodsTask() {
@get:PathSensitive(PathSensitivity.ABSOLUTE)
@get:InputFile
lateinit var buildSettingsFile: Provider<File>
internal set
@get:Nested
internal lateinit var pod: Provider<CocoapodsDependency>
@get:PathSensitive(PathSensitivity.ABSOLUTE)
@get:IgnoreEmptyDirectories
@get:InputFiles
internal val srcDir: FileTree
get() = project.fileTree(
buildSettingsFile.map { PodBuildSettingsProperties.readSettingsFromReader(it.reader()).podsTargetSrcRoot }
)
@get:Internal
internal var buildDir: Provider<File> = project.provider {
project.file(PodBuildSettingsProperties.readSettingsFromReader(buildSettingsFile.get().reader()).buildDir)
}
@get:Input
internal lateinit var sdk: Provider<String>
@Suppress("unused") // declares an ouptut
@get:OutputFiles
internal val buildResult: Provider<FileCollection> = project.provider {
project.fileTree(buildDir.get()) {
it.include("**/${pod.get().schemeName}.*/")
it.include("**/${pod.get().schemeName}/")
}
}
@get:Internal
internal lateinit var podsXcodeProjDir: Provider<File>
@TaskAction
fun buildDependencies() {
val podBuildSettings = PodBuildSettingsProperties.readSettingsFromReader(buildSettingsFile.get().reader())
val podsXcodeProjDir = podsXcodeProjDir.get()
val podXcodeBuildCommand = listOf(
"xcodebuild",
"-project", podsXcodeProjDir.name,
"-scheme", pod.get().schemeName,
"-sdk", sdk.get(),
"-configuration", podBuildSettings.configuration
)
runCommand(podXcodeBuildCommand, project.logger) { directory(podsXcodeProjDir.parentFile) }
}
}
data class PodBuildSettingsProperties(
internal val buildDir: String,
internal val configuration: String,
val configurationBuildDir: String,
internal val podsTargetSrcRoot: String,
internal val cflags: String? = null,
internal val headerPaths: String? = null,
internal val publicHeadersFolderPath: String? = null,
internal val frameworkPaths: String? = null
) {
fun writeSettings(
buildSettingsFile: File
) {
buildSettingsFile.parentFile.mkdirs()
buildSettingsFile.delete()
buildSettingsFile.createNewFile()
check(buildSettingsFile.exists()) { "Unable to create file ${buildSettingsFile.path}!" }
with(buildSettingsFile) {
appendText("$BUILD_DIR=$buildDir\n")
appendText("$CONFIGURATION=$configuration\n")
appendText("$CONFIGURATION_BUILD_DIR=$configurationBuildDir\n")
appendText("$PODS_TARGET_SRCROOT=$podsTargetSrcRoot\n")
cflags?.let { appendText("$OTHER_CFLAGS=$it\n") }
headerPaths?.let { appendText("$HEADER_SEARCH_PATHS=$it\n") }
publicHeadersFolderPath?.let { appendText("$PUBLIC_HEADERS_FOLDER_PATH=$it\n") }
frameworkPaths?.let { appendText("$FRAMEWORK_SEARCH_PATHS=$it") }
}
}
companion object {
const val BUILD_DIR = "BUILD_DIR"
const val CONFIGURATION = "CONFIGURATION"
const val CONFIGURATION_BUILD_DIR = "CONFIGURATION_BUILD_DIR"
const val PODS_TARGET_SRCROOT = "PODS_TARGET_SRCROOT"
const val OTHER_CFLAGS = "OTHER_CFLAGS"
const val HEADER_SEARCH_PATHS = "HEADER_SEARCH_PATHS"
const val PUBLIC_HEADERS_FOLDER_PATH = "PUBLIC_HEADERS_FOLDER_PATH"
const val FRAMEWORK_SEARCH_PATHS = "FRAMEWORK_SEARCH_PATHS"
fun readSettingsFromReader(reader: Reader): PodBuildSettingsProperties {
with(Properties()) {
@Suppress("BlockingMethodInNonBlockingContext") // It's ok to do blocking call here
load(reader)
return PodBuildSettingsProperties(
readProperty(BUILD_DIR),
readProperty(CONFIGURATION),
readProperty(CONFIGURATION_BUILD_DIR),
readProperty(PODS_TARGET_SRCROOT),
readNullableProperty(OTHER_CFLAGS),
readNullableProperty(HEADER_SEARCH_PATHS),
readNullableProperty(PUBLIC_HEADERS_FOLDER_PATH),
readNullableProperty(FRAMEWORK_SEARCH_PATHS)
)
}
}
private fun Properties.readProperty(propertyName: String) =
readNullableProperty(propertyName) ?: error("$propertyName property is absent")
private fun Properties.readNullableProperty(propertyName: String) =
getProperty(propertyName)
}
}
private object CocoapodsErrorHandlingUtil {
fun handle(e: IOException, command: List<String>) {
if (e.message?.contains("No such file or directory") == true) {
val message = """
|'${command.take(2).joinToString(" ")}' command failed with an exception:
| ${e.message}
|
| Full command: ${command.joinToString(" ")}
|
| Possible reason: CocoaPods is not installed
| Please check that CocoaPods v1.10 or above is installed.
|
| To check CocoaPods version type 'pod --version' in the terminal
|
| To install CocoaPods execute 'sudo gem install cocoapods'
|
""".trimMargin()
throw IllegalStateException(message)
} else {
throw e
}
}
}
@@ -3,157 +3,17 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
@file:Suppress("LeakingThis") // All tasks should be inherited only by Gradle @file:Suppress("LeakingThis", "PackageDirectoryMismatch") // All tasks should be inherited only by Gradle, Old package for compatibility
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.file.FileCollection
import org.gradle.api.file.FileTree
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider import org.gradle.api.provider.Provider
import org.gradle.api.tasks.* import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputDirectory
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs
import org.jetbrains.kotlin.gradle.plugin.cocoapods.platformLiteral import org.jetbrains.kotlin.gradle.plugin.cocoapods.platformLiteral
import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingCocoapodsMessage
import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingSpecReposMessage
import org.jetbrains.kotlin.gradle.utils.runCommand
import org.jetbrains.kotlin.konan.target.Family import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.HostManager
import java.io.File import java.io.File
import java.io.IOException
import java.io.Reader
import java.util.*
val CocoapodsDependency.schemeName: String
get() = name.split("/")[0]
open class CocoapodsTask : DefaultTask() {
init {
onlyIf {
HostManager.hostIsMac
}
}
}
/**
* The task takes the path to the Podfile and calls `pod install`
* to obtain sources or artifacts for the declared dependencies.
* This task is a part of CocoaPods integration infrastructure.
*/
abstract class AbstractPodInstallTask : CocoapodsTask() {
init {
onlyIf { podfile.isPresent }
}
@get:Optional
@get:InputFile
abstract val podfile: Property<File?>
@get:Internal
protected val workingDir: Provider<File> = podfile.map { file: File? ->
requireNotNull(file) { "Task outputs shouldn't be queried if it's skipped" }.parentFile
}
@get:OutputDirectory
internal val podsDir: Provider<File> = workingDir.map { it.resolve("Pods") }
@get:Internal
internal val podsXcodeProjDirProvider: Provider<File> = podsDir.map { it.resolve("Pods.xcodeproj") }
@TaskAction
open fun doPodInstall() {
val podInstallCommand = listOf("pod", "install")
runCommand(podInstallCommand,
logger,
errorHandler = ::handleError,
exceptionHandler = { e: IOException ->
CocoapodsErrorHandlingUtil.handle(e, podInstallCommand)
},
processConfiguration = {
directory(workingDir.get())
})
with(podsXcodeProjDirProvider.get()) {
check(exists() && isDirectory) {
"The directory 'Pods/Pods.xcodeproj' was not created as a result of the `pod install` call."
}
}
}
abstract fun handleError(retCode: Int, error: String, process: Process): String?
}
abstract class PodInstallTask : AbstractPodInstallTask() {
@get:Optional
@get:InputFile
abstract val podspec: Property<File?>
@get:Input
abstract val frameworkName: Property<String>
@get:Nested
abstract val specRepos: Property<SpecRepos>
@get:Nested
abstract val pods: ListProperty<CocoapodsDependency>
@get:InputDirectory
abstract val dummyFramework: Property<File>
private val framework = project.provider { project.cocoapodsBuildDirs.framework.resolve("${frameworkName.get()}.framework") }
private val tmpFramework = dummyFramework.map { dummy -> dummy.parentFile.resolve("tmp.framework").also { it.deleteOnExit() } }
override fun doPodInstall() {
// We always need to execute 'pod install' with the dummy framework because the one left from a previous build
// may have a wrong linkage type. So we temporarily swap them, run 'pod install' and then swap them back
framework.rename(tmpFramework)
dummyFramework.rename(framework)
super.doPodInstall()
framework.rename(dummyFramework)
tmpFramework.rename(framework)
}
private fun Provider<File>.rename(dest: Provider<File>) = get().rename(dest.get())
private fun File.rename(dest: File) {
if (!exists()) {
mkdirs()
}
check(renameTo(dest)) { "Can't rename '${this}' to '${dest}'" }
}
override fun handleError(retCode: Int, error: String, process: Process): String? {
val specReposMessages = MissingSpecReposMessage(specRepos.get()).missingMessage
val cocoapodsMessages = pods.get().map { MissingCocoapodsMessage(it).missingMessage }
return listOfNotNull(
"'pod install' command failed with code $retCode.",
"Error message:",
error.lines().filter { it.isNotBlank() }.joinToString("\n"),
"""
| Please, check that podfile contains following lines in header:
| $specReposMessages
|
""".trimMargin(),
"""
| Please, check that each target depended on ${frameworkName.get()} contains following dependencies:
| ${cocoapodsMessages.joinToString("\n")}
|
""".trimMargin()
).joinToString("\n")
}
}
abstract class PodInstallSyntheticTask : AbstractPodInstallTask() { abstract class PodInstallSyntheticTask : AbstractPodInstallTask() {
@@ -227,298 +87,3 @@ abstract class PodInstallSyntheticTask : AbstractPodInstallTask() {
} }
} }
} }
/**
* The task generates a synthetic project with all cocoapods dependencies
*/
abstract class PodGenTask : CocoapodsTask() {
init {
onlyIf {
pods.get().isNotEmpty()
}
}
@get:InputFile
internal abstract val podspec: Property<File>
@get:Input
internal abstract val podName: Property<String>
@get:Input
internal abstract val useLibraries: Property<Boolean>
@get:Input
internal abstract val family: Property<Family>
@get:Nested
internal abstract val platformSettings: Property<PodspecPlatformSettings>
@get:Nested
internal abstract val specRepos: Property<SpecRepos>
@get:Nested
internal abstract val pods: ListProperty<CocoapodsDependency>
@get:OutputFile
val podfile: Provider<File> = family.map { project.cocoapodsBuildDirs.synthetic(it).resolve("Podfile") }
@TaskAction
fun generate() {
val specRepos = specRepos.get().getAll()
val podfile = this.podfile.get()
podfile.createNewFile()
val podfileContent = getPodfileContent(specRepos, family.get().platformLiteral)
podfile.writeText(podfileContent)
}
private fun getPodfileContent(specRepos: Collection<String>, xcodeTarget: String) =
buildString {
specRepos.forEach {
appendLine("source '$it'")
}
appendLine("target '$xcodeTarget' do")
if (useLibraries.get().not()) {
appendLine("\tuse_frameworks!")
}
val settings = platformSettings.get()
val deploymentTarget = settings.deploymentTarget
if (deploymentTarget != null) {
appendLine("\tplatform :${settings.name}, '$deploymentTarget'")
} else {
appendLine("\tplatform :${settings.name}")
}
pods.get().mapNotNull {
buildString {
append("pod '${it.name}'")
val version = it.version
val source = it.source
if (source != null) {
append(", ${source.getPodSourcePath()}")
} else if (version != null) {
append(", '$version'")
}
}
}.forEach { appendLine("\t$it") }
appendLine("end\n")
//disable signing for all synthetic pods KT-54314
append(
"""
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = ""
config.build_settings['CODE_SIGNING_REQUIRED'] = "NO"
config.build_settings['CODE_SIGNING_ALLOWED'] = "NO"
end
end
end
""".trimIndent()
)
appendLine()
}
}
open class PodSetupBuildTask : CocoapodsTask() {
@get:Input
lateinit var frameworkName: Provider<String>
@get:Input
internal lateinit var sdk: Provider<String>
@get:Nested
lateinit var pod: Provider<CocoapodsDependency>
@get:OutputFile
val buildSettingsFile: Provider<File> = project.provider {
project.cocoapodsBuildDirs
.buildSettings
.resolve(getBuildSettingFileName(pod.get(), sdk.get()))
}
@get:Internal
internal lateinit var podsXcodeProjDir: Provider<File>
@TaskAction
fun setupBuild() {
val podsXcodeProjDir = podsXcodeProjDir.get()
val buildSettingsReceivingCommand = listOf(
"xcodebuild", "-showBuildSettings",
"-project", podsXcodeProjDir.name,
"-scheme", pod.get().schemeName,
"-sdk", sdk.get()
)
val outputText = runCommand(buildSettingsReceivingCommand, project.logger) { directory(podsXcodeProjDir.parentFile) }
val buildSettingsProperties = PodBuildSettingsProperties.readSettingsFromReader(outputText.reader())
buildSettingsFile.get().let { bsf ->
buildSettingsProperties.writeSettings(bsf)
}
}
}
private fun getBuildSettingFileName(pod: CocoapodsDependency, sdk: String): String =
"build-settings-$sdk-${pod.schemeName}.properties"
/**
* The task compiles external cocoa pods sources.
*/
open class PodBuildTask : CocoapodsTask() {
@get:PathSensitive(PathSensitivity.ABSOLUTE)
@get:InputFile
lateinit var buildSettingsFile: Provider<File>
internal set
@get:Nested
internal lateinit var pod: Provider<CocoapodsDependency>
@get:PathSensitive(PathSensitivity.ABSOLUTE)
@get:IgnoreEmptyDirectories
@get:InputFiles
internal val srcDir: FileTree
get() = project.fileTree(
buildSettingsFile.map { PodBuildSettingsProperties.readSettingsFromReader(it.reader()).podsTargetSrcRoot }
)
@get:Internal
internal var buildDir: Provider<File> = project.provider {
project.file(PodBuildSettingsProperties.readSettingsFromReader(buildSettingsFile.get().reader()).buildDir)
}
@get:Input
internal lateinit var sdk: Provider<String>
@Suppress("unused") // declares an ouptut
@get:OutputFiles
internal val buildResult: Provider<FileCollection> = project.provider {
project.fileTree(buildDir.get()) {
it.include("**/${pod.get().schemeName}.*/")
it.include("**/${pod.get().schemeName}/")
}
}
@get:Internal
internal lateinit var podsXcodeProjDir: Provider<File>
@TaskAction
fun buildDependencies() {
val podBuildSettings = PodBuildSettingsProperties.readSettingsFromReader(buildSettingsFile.get().reader())
val podsXcodeProjDir = podsXcodeProjDir.get()
val podXcodeBuildCommand = listOf(
"xcodebuild",
"-project", podsXcodeProjDir.name,
"-scheme", pod.get().schemeName,
"-sdk", sdk.get(),
"-configuration", podBuildSettings.configuration
)
runCommand(podXcodeBuildCommand, project.logger) { directory(podsXcodeProjDir.parentFile) }
}
}
data class PodBuildSettingsProperties(
internal val buildDir: String,
internal val configuration: String,
val configurationBuildDir: String,
internal val podsTargetSrcRoot: String,
internal val cflags: String? = null,
internal val headerPaths: String? = null,
internal val publicHeadersFolderPath: String? = null,
internal val frameworkPaths: String? = null
) {
fun writeSettings(
buildSettingsFile: File
) {
buildSettingsFile.parentFile.mkdirs()
buildSettingsFile.delete()
buildSettingsFile.createNewFile()
check(buildSettingsFile.exists()) { "Unable to create file ${buildSettingsFile.path}!" }
with(buildSettingsFile) {
appendText("$BUILD_DIR=$buildDir\n")
appendText("$CONFIGURATION=$configuration\n")
appendText("$CONFIGURATION_BUILD_DIR=$configurationBuildDir\n")
appendText("$PODS_TARGET_SRCROOT=$podsTargetSrcRoot\n")
cflags?.let { appendText("$OTHER_CFLAGS=$it\n") }
headerPaths?.let { appendText("$HEADER_SEARCH_PATHS=$it\n") }
publicHeadersFolderPath?.let { appendText("$PUBLIC_HEADERS_FOLDER_PATH=$it\n") }
frameworkPaths?.let { appendText("$FRAMEWORK_SEARCH_PATHS=$it") }
}
}
companion object {
const val BUILD_DIR = "BUILD_DIR"
const val CONFIGURATION = "CONFIGURATION"
const val CONFIGURATION_BUILD_DIR = "CONFIGURATION_BUILD_DIR"
const val PODS_TARGET_SRCROOT = "PODS_TARGET_SRCROOT"
const val OTHER_CFLAGS = "OTHER_CFLAGS"
const val HEADER_SEARCH_PATHS = "HEADER_SEARCH_PATHS"
const val PUBLIC_HEADERS_FOLDER_PATH = "PUBLIC_HEADERS_FOLDER_PATH"
const val FRAMEWORK_SEARCH_PATHS = "FRAMEWORK_SEARCH_PATHS"
fun readSettingsFromReader(reader: Reader): PodBuildSettingsProperties {
with(Properties()) {
@Suppress("BlockingMethodInNonBlockingContext") // It's ok to do blocking call here
load(reader)
return PodBuildSettingsProperties(
readProperty(BUILD_DIR),
readProperty(CONFIGURATION),
readProperty(CONFIGURATION_BUILD_DIR),
readProperty(PODS_TARGET_SRCROOT),
readNullableProperty(OTHER_CFLAGS),
readNullableProperty(HEADER_SEARCH_PATHS),
readNullableProperty(PUBLIC_HEADERS_FOLDER_PATH),
readNullableProperty(FRAMEWORK_SEARCH_PATHS)
)
}
}
private fun Properties.readProperty(propertyName: String) =
readNullableProperty(propertyName) ?: error("$propertyName property is absent")
private fun Properties.readNullableProperty(propertyName: String) =
getProperty(propertyName)
}
}
private object CocoapodsErrorHandlingUtil {
fun handle(e: IOException, command: List<String>) {
if (e.message?.contains("No such file or directory") == true) {
val message = """
|'${command.take(2).joinToString(" ")}' command failed with an exception:
| ${e.message}
|
| Full command: ${command.joinToString(" ")}
|
| Possible reason: CocoaPods is not installed
| Please check that CocoaPods v1.10 or above is installed.
|
| To check CocoaPods version type 'pod --version' in the terminal
|
| To install CocoaPods execute 'sudo gem install cocoapods'
|
""".trimMargin()
throw IllegalStateException(message)
} else {
throw e
}
}
}
@@ -3,93 +3,20 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
@file:Suppress("LeakingThis") // All tasks should be inherited only by Gradle @file:Suppress("LeakingThis", "PackageDirectoryMismatch") // All tasks should be inherited only by Gradle, Old package for compatibility
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.file.FileCollection
import org.gradle.api.file.FileTree
import org.gradle.api.provider.ListProperty import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
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.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.* import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.SpecRepos
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs
import org.jetbrains.kotlin.gradle.plugin.cocoapods.platformLiteral
import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingCocoapodsMessage import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingCocoapodsMessage
import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingSpecReposMessage import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingSpecReposMessage
import org.jetbrains.kotlin.gradle.utils.runCommand
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.HostManager
import java.io.File import java.io.File
import java.io.IOException
import java.io.Reader
import java.util.*
val CocoapodsDependency.schemeName: String
get() = name.split("/")[0]
open class CocoapodsTask : DefaultTask() {
init {
onlyIf {
HostManager.hostIsMac
}
}
}
/**
* The task takes the path to the Podfile and calls `pod install`
* to obtain sources or artifacts for the declared dependencies.
* This task is a part of CocoaPods integration infrastructure.
*/
abstract class AbstractPodInstallTask : CocoapodsTask() {
init {
onlyIf { podfile.isPresent }
}
@get:Optional
@get:InputFile
abstract val podfile: Property<File?>
@get:Internal
protected val workingDir: Provider<File> = podfile.map { file: File? ->
requireNotNull(file) { "Task outputs shouldn't be queried if it's skipped" }.parentFile
}
@get:OutputDirectory
internal val podsDir: Provider<File> = workingDir.map { it.resolve("Pods") }
@get:Internal
internal val podsXcodeProjDirProvider: Provider<File> = podsDir.map { it.resolve("Pods.xcodeproj") }
@TaskAction
open fun doPodInstall() {
val podInstallCommand = listOf("pod", "install")
runCommand(podInstallCommand,
logger,
errorHandler = ::handleError,
exceptionHandler = { e: IOException ->
CocoapodsErrorHandlingUtil.handle(e, podInstallCommand)
},
processConfiguration = {
directory(workingDir.get())
})
with(podsXcodeProjDirProvider.get()) {
check(exists() && isDirectory) {
"The directory 'Pods/Pods.xcodeproj' was not created as a result of the `pod install` call."
}
}
}
abstract fun handleError(retCode: Int, error: String, process: Process): String?
}
abstract class PodInstallTask : AbstractPodInstallTask() { abstract class PodInstallTask : AbstractPodInstallTask() {
@@ -154,371 +81,3 @@ abstract class PodInstallTask : AbstractPodInstallTask() {
).joinToString("\n") ).joinToString("\n")
} }
} }
abstract class PodInstallSyntheticTask : AbstractPodInstallTask() {
@get:Input
abstract val family: Property<Family>
@get:Input
abstract val podName: Property<String>
@get:OutputDirectory
internal val syntheticXcodeProject: Provider<File> = workingDir.map { it.resolve("synthetic.xcodeproj") }
override fun doPodInstall() {
val projResource = "/cocoapods/project.pbxproj"
val projDestination = syntheticXcodeProject.get().resolve("project.pbxproj")
syntheticXcodeProject.get().mkdirs()
projDestination.outputStream().use { file ->
javaClass.getResourceAsStream(projResource)!!.use { resource ->
resource.copyTo(file)
}
}
super.doPodInstall()
}
override fun handleError(retCode: Int, error: String, process: Process): String? {
var message = """
|'pod install' command on the synthetic project failed with return code: $retCode
|
| Error: ${error.lines().filter { it.contains("[!]") }.joinToString("\n")}
|
""".trimMargin()
if (
error.contains("deployment target") ||
error.contains("no platform was specified") ||
error.contains(Regex("The platform of the target .+ is not compatible with `${podName.get()}"))
) {
message += """
|
| Possible reason: ${family.get().platformLiteral} deployment target is not configured
| Configure deployment_target for ALL targets as follows:
| cocoapods {
| ...
| ${family.get().platformLiteral}.deploymentTarget = "..."
| ...
| }
|
""".trimMargin()
return message
} else if (
error.contains("Unable to add a source with url") ||
error.contains("Couldn't determine repo name for URL") ||
error.contains("Unable to find a specification")
) {
message += """
|
| Possible reason: spec repos are not configured correctly.
| Ensure that spec repos are correctly configured for all private pod dependencies:
| cocoapods {
| specRepos {
| url("<private spec repo url>")
| }
| }
|
""".trimMargin()
return message
} else {
return null
}
}
}
/**
* The task generates a synthetic project with all cocoapods dependencies
*/
abstract class PodGenTask : CocoapodsTask() {
init {
onlyIf {
pods.get().isNotEmpty()
}
}
@get:InputFile
internal abstract val podspec: Property<File>
@get:Input
internal abstract val podName: Property<String>
@get:Input
internal abstract val useLibraries: Property<Boolean>
@get:Input
internal abstract val family: Property<Family>
@get:Nested
internal abstract val platformSettings: Property<PodspecPlatformSettings>
@get:Nested
internal abstract val specRepos: Property<SpecRepos>
@get:Nested
internal abstract val pods: ListProperty<CocoapodsDependency>
@get:OutputFile
val podfile: Provider<File> = family.map { project.cocoapodsBuildDirs.synthetic(it).resolve("Podfile") }
@TaskAction
fun generate() {
val specRepos = specRepos.get().getAll()
val podfile = this.podfile.get()
podfile.createNewFile()
val podfileContent = getPodfileContent(specRepos, family.get().platformLiteral)
podfile.writeText(podfileContent)
}
private fun getPodfileContent(specRepos: Collection<String>, xcodeTarget: String) =
buildString {
specRepos.forEach {
appendLine("source '$it'")
}
appendLine("target '$xcodeTarget' do")
if (useLibraries.get().not()) {
appendLine("\tuse_frameworks!")
}
val settings = platformSettings.get()
val deploymentTarget = settings.deploymentTarget
if (deploymentTarget != null) {
appendLine("\tplatform :${settings.name}, '$deploymentTarget'")
} else {
appendLine("\tplatform :${settings.name}")
}
pods.get().mapNotNull {
buildString {
append("pod '${it.name}'")
val version = it.version
val source = it.source
if (source != null) {
append(", ${source.getPodSourcePath()}")
} else if (version != null) {
append(", '$version'")
}
}
}.forEach { appendLine("\t$it") }
appendLine("end\n")
//disable signing for all synthetic pods KT-54314
append(
"""
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = ""
config.build_settings['CODE_SIGNING_REQUIRED'] = "NO"
config.build_settings['CODE_SIGNING_ALLOWED'] = "NO"
end
end
end
""".trimIndent()
)
appendLine()
}
}
open class PodSetupBuildTask : CocoapodsTask() {
@get:Input
lateinit var frameworkName: Provider<String>
@get:Input
internal lateinit var sdk: Provider<String>
@get:Nested
lateinit var pod: Provider<CocoapodsDependency>
@get:OutputFile
val buildSettingsFile: Provider<File> = project.provider {
project.cocoapodsBuildDirs
.buildSettings
.resolve(getBuildSettingFileName(pod.get(), sdk.get()))
}
@get:Internal
internal lateinit var podsXcodeProjDir: Provider<File>
@TaskAction
fun setupBuild() {
val podsXcodeProjDir = podsXcodeProjDir.get()
val buildSettingsReceivingCommand = listOf(
"xcodebuild", "-showBuildSettings",
"-project", podsXcodeProjDir.name,
"-scheme", pod.get().schemeName,
"-sdk", sdk.get()
)
val outputText = runCommand(buildSettingsReceivingCommand, project.logger) { directory(podsXcodeProjDir.parentFile) }
val buildSettingsProperties = PodBuildSettingsProperties.readSettingsFromReader(outputText.reader())
buildSettingsFile.get().let { bsf ->
buildSettingsProperties.writeSettings(bsf)
}
}
}
private fun getBuildSettingFileName(pod: CocoapodsDependency, sdk: String): String =
"build-settings-$sdk-${pod.schemeName}.properties"
/**
* The task compiles external cocoa pods sources.
*/
open class PodBuildTask : CocoapodsTask() {
@get:PathSensitive(PathSensitivity.ABSOLUTE)
@get:InputFile
lateinit var buildSettingsFile: Provider<File>
internal set
@get:Nested
internal lateinit var pod: Provider<CocoapodsDependency>
@get:PathSensitive(PathSensitivity.ABSOLUTE)
@get:IgnoreEmptyDirectories
@get:InputFiles
internal val srcDir: FileTree
get() = project.fileTree(
buildSettingsFile.map { PodBuildSettingsProperties.readSettingsFromReader(it.reader()).podsTargetSrcRoot }
)
@get:Internal
internal var buildDir: Provider<File> = project.provider {
project.file(PodBuildSettingsProperties.readSettingsFromReader(buildSettingsFile.get().reader()).buildDir)
}
@get:Input
internal lateinit var sdk: Provider<String>
@Suppress("unused") // declares an ouptut
@get:OutputFiles
internal val buildResult: Provider<FileCollection> = project.provider {
project.fileTree(buildDir.get()) {
it.include("**/${pod.get().schemeName}.*/")
it.include("**/${pod.get().schemeName}/")
}
}
@get:Internal
internal lateinit var podsXcodeProjDir: Provider<File>
@TaskAction
fun buildDependencies() {
val podBuildSettings = PodBuildSettingsProperties.readSettingsFromReader(buildSettingsFile.get().reader())
val podsXcodeProjDir = podsXcodeProjDir.get()
val podXcodeBuildCommand = listOf(
"xcodebuild",
"-project", podsXcodeProjDir.name,
"-scheme", pod.get().schemeName,
"-sdk", sdk.get(),
"-configuration", podBuildSettings.configuration
)
runCommand(podXcodeBuildCommand, project.logger) { directory(podsXcodeProjDir.parentFile) }
}
}
data class PodBuildSettingsProperties(
internal val buildDir: String,
internal val configuration: String,
val configurationBuildDir: String,
internal val podsTargetSrcRoot: String,
internal val cflags: String? = null,
internal val headerPaths: String? = null,
internal val publicHeadersFolderPath: String? = null,
internal val frameworkPaths: String? = null
) {
fun writeSettings(
buildSettingsFile: File
) {
buildSettingsFile.parentFile.mkdirs()
buildSettingsFile.delete()
buildSettingsFile.createNewFile()
check(buildSettingsFile.exists()) { "Unable to create file ${buildSettingsFile.path}!" }
with(buildSettingsFile) {
appendText("$BUILD_DIR=$buildDir\n")
appendText("$CONFIGURATION=$configuration\n")
appendText("$CONFIGURATION_BUILD_DIR=$configurationBuildDir\n")
appendText("$PODS_TARGET_SRCROOT=$podsTargetSrcRoot\n")
cflags?.let { appendText("$OTHER_CFLAGS=$it\n") }
headerPaths?.let { appendText("$HEADER_SEARCH_PATHS=$it\n") }
publicHeadersFolderPath?.let { appendText("$PUBLIC_HEADERS_FOLDER_PATH=$it\n") }
frameworkPaths?.let { appendText("$FRAMEWORK_SEARCH_PATHS=$it") }
}
}
companion object {
const val BUILD_DIR = "BUILD_DIR"
const val CONFIGURATION = "CONFIGURATION"
const val CONFIGURATION_BUILD_DIR = "CONFIGURATION_BUILD_DIR"
const val PODS_TARGET_SRCROOT = "PODS_TARGET_SRCROOT"
const val OTHER_CFLAGS = "OTHER_CFLAGS"
const val HEADER_SEARCH_PATHS = "HEADER_SEARCH_PATHS"
const val PUBLIC_HEADERS_FOLDER_PATH = "PUBLIC_HEADERS_FOLDER_PATH"
const val FRAMEWORK_SEARCH_PATHS = "FRAMEWORK_SEARCH_PATHS"
fun readSettingsFromReader(reader: Reader): PodBuildSettingsProperties {
with(Properties()) {
@Suppress("BlockingMethodInNonBlockingContext") // It's ok to do blocking call here
load(reader)
return PodBuildSettingsProperties(
readProperty(BUILD_DIR),
readProperty(CONFIGURATION),
readProperty(CONFIGURATION_BUILD_DIR),
readProperty(PODS_TARGET_SRCROOT),
readNullableProperty(OTHER_CFLAGS),
readNullableProperty(HEADER_SEARCH_PATHS),
readNullableProperty(PUBLIC_HEADERS_FOLDER_PATH),
readNullableProperty(FRAMEWORK_SEARCH_PATHS)
)
}
}
private fun Properties.readProperty(propertyName: String) =
readNullableProperty(propertyName) ?: error("$propertyName property is absent")
private fun Properties.readNullableProperty(propertyName: String) =
getProperty(propertyName)
}
}
private object CocoapodsErrorHandlingUtil {
fun handle(e: IOException, command: List<String>) {
if (e.message?.contains("No such file or directory") == true) {
val message = """
|'${command.take(2).joinToString(" ")}' command failed with an exception:
| ${e.message}
|
| Full command: ${command.joinToString(" ")}
|
| Possible reason: CocoaPods is not installed
| Please check that CocoaPods v1.10 or above is installed.
|
| To check CocoaPods version type 'pod --version' in the terminal
|
| To install CocoaPods execute 'sudo gem install cocoapods'
|
""".trimMargin()
throw IllegalStateException(message)
} else {
throw e
}
}
}
@@ -3,329 +3,16 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
@file:Suppress("LeakingThis") // All tasks should be inherited only by Gradle @file:Suppress("LeakingThis", "PackageDirectoryMismatch") // All tasks should be inherited only by Gradle, Old package for compatibility
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.file.FileCollection
import org.gradle.api.file.FileTree
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
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.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs
import org.jetbrains.kotlin.gradle.plugin.cocoapods.platformLiteral
import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingCocoapodsMessage
import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingSpecReposMessage
import org.jetbrains.kotlin.gradle.utils.runCommand import org.jetbrains.kotlin.gradle.utils.runCommand
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.HostManager
import java.io.File import java.io.File
import java.io.IOException
import java.io.Reader
import java.util.*
val CocoapodsDependency.schemeName: String
get() = name.split("/")[0]
open class CocoapodsTask : DefaultTask() {
init {
onlyIf {
HostManager.hostIsMac
}
}
}
/**
* The task takes the path to the Podfile and calls `pod install`
* to obtain sources or artifacts for the declared dependencies.
* This task is a part of CocoaPods integration infrastructure.
*/
abstract class AbstractPodInstallTask : CocoapodsTask() {
init {
onlyIf { podfile.isPresent }
}
@get:Optional
@get:InputFile
abstract val podfile: Property<File?>
@get:Internal
protected val workingDir: Provider<File> = podfile.map { file: File? ->
requireNotNull(file) { "Task outputs shouldn't be queried if it's skipped" }.parentFile
}
@get:OutputDirectory
internal val podsDir: Provider<File> = workingDir.map { it.resolve("Pods") }
@get:Internal
internal val podsXcodeProjDirProvider: Provider<File> = podsDir.map { it.resolve("Pods.xcodeproj") }
@TaskAction
open fun doPodInstall() {
val podInstallCommand = listOf("pod", "install")
runCommand(podInstallCommand,
logger,
errorHandler = ::handleError,
exceptionHandler = { e: IOException ->
CocoapodsErrorHandlingUtil.handle(e, podInstallCommand)
},
processConfiguration = {
directory(workingDir.get())
})
with(podsXcodeProjDirProvider.get()) {
check(exists() && isDirectory) {
"The directory 'Pods/Pods.xcodeproj' was not created as a result of the `pod install` call."
}
}
}
abstract fun handleError(retCode: Int, error: String, process: Process): String?
}
abstract class PodInstallTask : AbstractPodInstallTask() {
@get:Optional
@get:InputFile
abstract val podspec: Property<File?>
@get:Input
abstract val frameworkName: Property<String>
@get:Nested
abstract val specRepos: Property<SpecRepos>
@get:Nested
abstract val pods: ListProperty<CocoapodsDependency>
@get:InputDirectory
abstract val dummyFramework: Property<File>
private val framework = project.provider { project.cocoapodsBuildDirs.framework.resolve("${frameworkName.get()}.framework") }
private val tmpFramework = dummyFramework.map { dummy -> dummy.parentFile.resolve("tmp.framework").also { it.deleteOnExit() } }
override fun doPodInstall() {
// We always need to execute 'pod install' with the dummy framework because the one left from a previous build
// may have a wrong linkage type. So we temporarily swap them, run 'pod install' and then swap them back
framework.rename(tmpFramework)
dummyFramework.rename(framework)
super.doPodInstall()
framework.rename(dummyFramework)
tmpFramework.rename(framework)
}
private fun Provider<File>.rename(dest: Provider<File>) = get().rename(dest.get())
private fun File.rename(dest: File) {
if (!exists()) {
mkdirs()
}
check(renameTo(dest)) { "Can't rename '${this}' to '${dest}'" }
}
override fun handleError(retCode: Int, error: String, process: Process): String? {
val specReposMessages = MissingSpecReposMessage(specRepos.get()).missingMessage
val cocoapodsMessages = pods.get().map { MissingCocoapodsMessage(it).missingMessage }
return listOfNotNull(
"'pod install' command failed with code $retCode.",
"Error message:",
error.lines().filter { it.isNotBlank() }.joinToString("\n"),
"""
| Please, check that podfile contains following lines in header:
| $specReposMessages
|
""".trimMargin(),
"""
| Please, check that each target depended on ${frameworkName.get()} contains following dependencies:
| ${cocoapodsMessages.joinToString("\n")}
|
""".trimMargin()
).joinToString("\n")
}
}
abstract class PodInstallSyntheticTask : AbstractPodInstallTask() {
@get:Input
abstract val family: Property<Family>
@get:Input
abstract val podName: Property<String>
@get:OutputDirectory
internal val syntheticXcodeProject: Provider<File> = workingDir.map { it.resolve("synthetic.xcodeproj") }
override fun doPodInstall() {
val projResource = "/cocoapods/project.pbxproj"
val projDestination = syntheticXcodeProject.get().resolve("project.pbxproj")
syntheticXcodeProject.get().mkdirs()
projDestination.outputStream().use { file ->
javaClass.getResourceAsStream(projResource)!!.use { resource ->
resource.copyTo(file)
}
}
super.doPodInstall()
}
override fun handleError(retCode: Int, error: String, process: Process): String? {
var message = """
|'pod install' command on the synthetic project failed with return code: $retCode
|
| Error: ${error.lines().filter { it.contains("[!]") }.joinToString("\n")}
|
""".trimMargin()
if (
error.contains("deployment target") ||
error.contains("no platform was specified") ||
error.contains(Regex("The platform of the target .+ is not compatible with `${podName.get()}"))
) {
message += """
|
| Possible reason: ${family.get().platformLiteral} deployment target is not configured
| Configure deployment_target for ALL targets as follows:
| cocoapods {
| ...
| ${family.get().platformLiteral}.deploymentTarget = "..."
| ...
| }
|
""".trimMargin()
return message
} else if (
error.contains("Unable to add a source with url") ||
error.contains("Couldn't determine repo name for URL") ||
error.contains("Unable to find a specification")
) {
message += """
|
| Possible reason: spec repos are not configured correctly.
| Ensure that spec repos are correctly configured for all private pod dependencies:
| cocoapods {
| specRepos {
| url("<private spec repo url>")
| }
| }
|
""".trimMargin()
return message
} else {
return null
}
}
}
/**
* The task generates a synthetic project with all cocoapods dependencies
*/
abstract class PodGenTask : CocoapodsTask() {
init {
onlyIf {
pods.get().isNotEmpty()
}
}
@get:InputFile
internal abstract val podspec: Property<File>
@get:Input
internal abstract val podName: Property<String>
@get:Input
internal abstract val useLibraries: Property<Boolean>
@get:Input
internal abstract val family: Property<Family>
@get:Nested
internal abstract val platformSettings: Property<PodspecPlatformSettings>
@get:Nested
internal abstract val specRepos: Property<SpecRepos>
@get:Nested
internal abstract val pods: ListProperty<CocoapodsDependency>
@get:OutputFile
val podfile: Provider<File> = family.map { project.cocoapodsBuildDirs.synthetic(it).resolve("Podfile") }
@TaskAction
fun generate() {
val specRepos = specRepos.get().getAll()
val podfile = this.podfile.get()
podfile.createNewFile()
val podfileContent = getPodfileContent(specRepos, family.get().platformLiteral)
podfile.writeText(podfileContent)
}
private fun getPodfileContent(specRepos: Collection<String>, xcodeTarget: String) =
buildString {
specRepos.forEach {
appendLine("source '$it'")
}
appendLine("target '$xcodeTarget' do")
if (useLibraries.get().not()) {
appendLine("\tuse_frameworks!")
}
val settings = platformSettings.get()
val deploymentTarget = settings.deploymentTarget
if (deploymentTarget != null) {
appendLine("\tplatform :${settings.name}, '$deploymentTarget'")
} else {
appendLine("\tplatform :${settings.name}")
}
pods.get().mapNotNull {
buildString {
append("pod '${it.name}'")
val version = it.version
val source = it.source
if (source != null) {
append(", ${source.getPodSourcePath()}")
} else if (version != null) {
append(", '$version'")
}
}
}.forEach { appendLine("\t$it") }
appendLine("end\n")
//disable signing for all synthetic pods KT-54314
append(
"""
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = ""
config.build_settings['CODE_SIGNING_REQUIRED'] = "NO"
config.build_settings['CODE_SIGNING_ALLOWED'] = "NO"
end
end
end
""".trimIndent()
)
appendLine()
}
}
open class PodSetupBuildTask : CocoapodsTask() { open class PodSetupBuildTask : CocoapodsTask() {
@@ -371,154 +58,3 @@ open class PodSetupBuildTask : CocoapodsTask() {
private fun getBuildSettingFileName(pod: CocoapodsDependency, sdk: String): String = private fun getBuildSettingFileName(pod: CocoapodsDependency, sdk: String): String =
"build-settings-$sdk-${pod.schemeName}.properties" "build-settings-$sdk-${pod.schemeName}.properties"
/**
* The task compiles external cocoa pods sources.
*/
open class PodBuildTask : CocoapodsTask() {
@get:PathSensitive(PathSensitivity.ABSOLUTE)
@get:InputFile
lateinit var buildSettingsFile: Provider<File>
internal set
@get:Nested
internal lateinit var pod: Provider<CocoapodsDependency>
@get:PathSensitive(PathSensitivity.ABSOLUTE)
@get:IgnoreEmptyDirectories
@get:InputFiles
internal val srcDir: FileTree
get() = project.fileTree(
buildSettingsFile.map { PodBuildSettingsProperties.readSettingsFromReader(it.reader()).podsTargetSrcRoot }
)
@get:Internal
internal var buildDir: Provider<File> = project.provider {
project.file(PodBuildSettingsProperties.readSettingsFromReader(buildSettingsFile.get().reader()).buildDir)
}
@get:Input
internal lateinit var sdk: Provider<String>
@Suppress("unused") // declares an ouptut
@get:OutputFiles
internal val buildResult: Provider<FileCollection> = project.provider {
project.fileTree(buildDir.get()) {
it.include("**/${pod.get().schemeName}.*/")
it.include("**/${pod.get().schemeName}/")
}
}
@get:Internal
internal lateinit var podsXcodeProjDir: Provider<File>
@TaskAction
fun buildDependencies() {
val podBuildSettings = PodBuildSettingsProperties.readSettingsFromReader(buildSettingsFile.get().reader())
val podsXcodeProjDir = podsXcodeProjDir.get()
val podXcodeBuildCommand = listOf(
"xcodebuild",
"-project", podsXcodeProjDir.name,
"-scheme", pod.get().schemeName,
"-sdk", sdk.get(),
"-configuration", podBuildSettings.configuration
)
runCommand(podXcodeBuildCommand, project.logger) { directory(podsXcodeProjDir.parentFile) }
}
}
data class PodBuildSettingsProperties(
internal val buildDir: String,
internal val configuration: String,
val configurationBuildDir: String,
internal val podsTargetSrcRoot: String,
internal val cflags: String? = null,
internal val headerPaths: String? = null,
internal val publicHeadersFolderPath: String? = null,
internal val frameworkPaths: String? = null
) {
fun writeSettings(
buildSettingsFile: File
) {
buildSettingsFile.parentFile.mkdirs()
buildSettingsFile.delete()
buildSettingsFile.createNewFile()
check(buildSettingsFile.exists()) { "Unable to create file ${buildSettingsFile.path}!" }
with(buildSettingsFile) {
appendText("$BUILD_DIR=$buildDir\n")
appendText("$CONFIGURATION=$configuration\n")
appendText("$CONFIGURATION_BUILD_DIR=$configurationBuildDir\n")
appendText("$PODS_TARGET_SRCROOT=$podsTargetSrcRoot\n")
cflags?.let { appendText("$OTHER_CFLAGS=$it\n") }
headerPaths?.let { appendText("$HEADER_SEARCH_PATHS=$it\n") }
publicHeadersFolderPath?.let { appendText("$PUBLIC_HEADERS_FOLDER_PATH=$it\n") }
frameworkPaths?.let { appendText("$FRAMEWORK_SEARCH_PATHS=$it") }
}
}
companion object {
const val BUILD_DIR = "BUILD_DIR"
const val CONFIGURATION = "CONFIGURATION"
const val CONFIGURATION_BUILD_DIR = "CONFIGURATION_BUILD_DIR"
const val PODS_TARGET_SRCROOT = "PODS_TARGET_SRCROOT"
const val OTHER_CFLAGS = "OTHER_CFLAGS"
const val HEADER_SEARCH_PATHS = "HEADER_SEARCH_PATHS"
const val PUBLIC_HEADERS_FOLDER_PATH = "PUBLIC_HEADERS_FOLDER_PATH"
const val FRAMEWORK_SEARCH_PATHS = "FRAMEWORK_SEARCH_PATHS"
fun readSettingsFromReader(reader: Reader): PodBuildSettingsProperties {
with(Properties()) {
@Suppress("BlockingMethodInNonBlockingContext") // It's ok to do blocking call here
load(reader)
return PodBuildSettingsProperties(
readProperty(BUILD_DIR),
readProperty(CONFIGURATION),
readProperty(CONFIGURATION_BUILD_DIR),
readProperty(PODS_TARGET_SRCROOT),
readNullableProperty(OTHER_CFLAGS),
readNullableProperty(HEADER_SEARCH_PATHS),
readNullableProperty(PUBLIC_HEADERS_FOLDER_PATH),
readNullableProperty(FRAMEWORK_SEARCH_PATHS)
)
}
}
private fun Properties.readProperty(propertyName: String) =
readNullableProperty(propertyName) ?: error("$propertyName property is absent")
private fun Properties.readNullableProperty(propertyName: String) =
getProperty(propertyName)
}
}
private object CocoapodsErrorHandlingUtil {
fun handle(e: IOException, command: List<String>) {
if (e.message?.contains("No such file or directory") == true) {
val message = """
|'${command.take(2).joinToString(" ")}' command failed with an exception:
| ${e.message}
|
| Full command: ${command.joinToString(" ")}
|
| Possible reason: CocoaPods is not installed
| Please check that CocoaPods v1.10 or above is installed.
|
| To check CocoaPods version type 'pod --version' in the terminal
|
| To install CocoaPods execute 'sudo gem install cocoapods'
|
""".trimMargin()
throw IllegalStateException(message)
} else {
throw e
}
}
}
@@ -9,20 +9,19 @@ package org.jetbrains.kotlin.gradle.tasks
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionAware import org.gradle.api.plugins.ExtensionAware
import org.gradle.api.provider.Property
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.wrapper.Wrapper import org.gradle.api.tasks.wrapper.Wrapper
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.* import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.PodspecPlatformSettings
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.COCOAPODS_EXTENSION_NAME import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.COCOAPODS_EXTENSION_NAME
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.GENERATE_WRAPPER_PROPERTY import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.GENERATE_WRAPPER_PROPERTY
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.SYNC_TASK_NAME import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.SYNC_TASK_NAME
import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs
import org.jetbrains.kotlin.gradle.utils.appendLine
import java.io.File import java.io.File
/** /**
@@ -223,132 +222,3 @@ open class PodspecTask : DefaultTask() {
} }
} }
/**
* Creates a dummy framework in the target directory.
*
* We represent a Kotlin/Native module to CocoaPods as a vendored framework.
* CocoaPods needs access to such frameworks during installation process to obtain
* their type (static or dynamic) and configure the Xcode project accordingly.
* But we cannot build the real framework before installation because it may
* depend on CocoaPods libraries which are not downloaded and built at this stage.
* So we create a dummy static framework to allow CocoaPods install our pod correctly
* and then replace it with the real one during a real build process.
*/
abstract class DummyFrameworkTask : DefaultTask() {
@get:Input
abstract val frameworkName: Property<String>
@get:Input
abstract val useStaticFramework: Property<Boolean>
@get:OutputDirectory
val outputFramework: Provider<File> = project.provider { project.cocoapodsBuildDirs.dummyFramework }
private val dummyFrameworkResource: String
get() {
val staticOrDynamic = if (!useStaticFramework.get()) "dynamic" else "static"
return "/cocoapods/$staticOrDynamic/dummy.framework/"
}
private fun copyResource(from: String, to: File) {
to.parentFile.mkdirs()
to.outputStream().use { file ->
javaClass.getResourceAsStream(from)!!.use { resource ->
resource.copyTo(file)
}
}
}
private fun copyTextResource(from: String, to: File, transform: (String) -> String = { it }) {
to.parentFile.mkdirs()
to.printWriter().use { file ->
javaClass.getResourceAsStream(from)!!.use {
it.reader().forEachLine { str ->
file.println(transform(str))
}
}
}
}
private fun copyFrameworkFile(relativeFrom: String, relativeTo: String = relativeFrom) =
copyResource(
"$dummyFrameworkResource$relativeFrom",
outputFramework.get().resolve(relativeTo)
)
private fun copyFrameworkTextFile(
relativeFrom: String,
relativeTo: String = relativeFrom,
transform: (String) -> String = { it }
) = copyTextResource(
"$dummyFrameworkResource$relativeFrom",
outputFramework.get().resolve(relativeTo),
transform
)
@TaskAction
fun create() {
// Reset the destination directory
with(outputFramework.get()) {
deleteRecursively()
mkdirs()
}
// Copy files for the dummy framework.
copyFrameworkFile("Info.plist")
copyFrameworkFile("dummy", frameworkName.get())
copyFrameworkFile("Headers/placeholder.h")
copyFrameworkTextFile("Modules/module.modulemap") {
if (it == "framework module dummy {") {
it.replace("dummy", frameworkName.get())
} else {
it
}
}
}
}
/**
* Generates a def-file for the given CocoaPods dependency.
*/
abstract class DefFileTask : DefaultTask() {
@get:Nested
abstract val pod: Property<CocoapodsDependency>
@get:Input
abstract val useLibraries: Property<Boolean>
@get:OutputFile
val outputFile: File
get() = project.cocoapodsBuildDirs.defs.resolve("${pod.get().moduleName}.def")
@TaskAction
fun generate() {
outputFile.parentFile.mkdirs()
outputFile.writeText(buildString {
appendLine("language = Objective-C")
with(pod.get()) {
when {
headers != null -> appendLine("headers = $headers")
useLibraries.get() -> logger.warn(
"""
w: Pod '$moduleName' should have 'headers' property specified when using 'useLibraries()'.
Otherwise code from this pod won't be accessible from Kotlin.
""".trimIndent()
)
else -> {
appendLine("modules = $moduleName")
// Linker opt with framework name is added so produced cinterop klib would have this flag inside its manifest
// This way error will be more obvious when someone will try to depend on a library with this cinterop
appendLine("linkerOpts = -framework $moduleName")
}
}
}
})
}
}