[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.
*/
@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
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.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() {
@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,
@@ -475,7 +55,6 @@ data class PodBuildSettingsProperties(
fun readSettingsFromReader(reader: Reader): PodBuildSettingsProperties {
with(Properties()) {
@Suppress("BlockingMethodInNonBlockingContext") // It's ok to do blocking call here
load(reader)
return PodBuildSettingsProperties(
readProperty(BUILD_DIR),
@@ -497,28 +76,3 @@ data class PodBuildSettingsProperties(
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.
*/
@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
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.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`
@@ -91,413 +63,6 @@ abstract class AbstractPodInstallTask : CocoapodsTask() {
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) {
@@ -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.
*/
@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
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.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency
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
get() = name.split("/")[0]
open class CocoapodsTask : DefaultTask() {
init {
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
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.Provider
import org.gradle.api.tasks.*
import org.gradle.api.tasks.wrapper.Wrapper
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
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.gradle.api.tasks.Input
import org.gradle.api.tasks.Nested
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency
import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs
import org.jetbrains.kotlin.gradle.utils.appendLine
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.
*/
@@ -7,224 +7,14 @@
package org.jetbrains.kotlin.gradle.tasks
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.Provider
import org.gradle.api.tasks.*
import org.gradle.api.tasks.wrapper.Wrapper
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
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.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs
import org.jetbrains.kotlin.gradle.utils.appendLine
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.
*
@@ -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.
*/
@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
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.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() {
@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.
*/
@@ -429,96 +75,3 @@ open class PodBuildTask : CocoapodsTask() {
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.
*/
@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
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.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
@@ -325,200 +114,3 @@ abstract class PodGenTask : CocoapodsTask() {
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.
*/
@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
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.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputDirectory
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.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() {
@@ -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.
*/
@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
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.CocoapodsExtension.CocoapodsDependency
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.SpecRepos
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.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() {
@@ -154,371 +81,3 @@ abstract class PodInstallTask : AbstractPodInstallTask() {
).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.
*/
@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
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.CocoapodsExtension.CocoapodsDependency
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.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() {
@@ -371,154 +58,3 @@ open class PodSetupBuildTask : CocoapodsTask() {
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
}
}
}
@@ -9,20 +9,19 @@ package org.jetbrains.kotlin.gradle.tasks
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.Provider
import org.gradle.api.tasks.*
import org.gradle.api.tasks.wrapper.Wrapper
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
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.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.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.utils.appendLine
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")
}
}
}
})
}
}