[Gradle, CocoaPods] Improved CocoaPods Integration features with tests

This commit is contained in:
Yaroslav Chernyshev
2020-06-19 17:56:02 +03:00
parent 130987fa1e
commit c638043aee
59 changed files with 1535 additions and 338 deletions
@@ -11,6 +11,11 @@ import org.jetbrains.plugins.gradle.model.ProjectImportModelProvider
import org.jetbrains.plugins.gradle.service.project.AbstractProjectResolverExtension
class KotlinCocoaPodsModelResolver : AbstractProjectResolverExtension() {
override fun requiresTaskRunning(): Boolean = true
override fun getToolingExtensionsClasses(): Set<Class<out Any>> {
return setOf(EnablePodImportTask::class.java)
}
override fun getProjectsLoadedModelProvider(): ProjectImportModelProvider {
return ClassSetProjectImportModelProvider(
@@ -18,7 +23,6 @@ class KotlinCocoaPodsModelResolver : AbstractProjectResolverExtension() {
)
}
override fun requiresTaskRunning(): Boolean = true
}
@@ -21,7 +21,8 @@ class KotlinCocoaPodsModelBuilder : AbstractModelBuilderService() {
override fun buildAll(modelName: String, project: Project, context: ModelBuilderContext): Any? {
val startParameter = project.gradle.startParameter
val taskNames = startParameter.taskNames
val taskNames = mutableListOf<String>()
taskNames.addAll(startParameter.taskNames)
if (project.tasks.findByPath(POD_IMPORT_TASK_NAME) != null && POD_IMPORT_TASK_NAME !in taskNames) {
taskNames.add(POD_IMPORT_TASK_NAME)
+1
View File
@@ -15,6 +15,7 @@
<projectResolve implementation="org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModelResolver" order="first"/>
<projectResolve implementation="org.jetbrains.kotlin.idea.cocoapods.KotlinCocoaPodsModelResolver" order="first"/>
<projectResolve implementation="org.jetbrains.kotlin.idea.commonizer.KotlinCommonizerModelResolver" order="first"/>
<projectResolve implementation="org.jetbrains.kotlin.idea.cocoapods.KotlinCocoaPodsModelResolver" order="last"/>
<projectResolve implementation="org.jetbrains.kotlin.idea.configuration.KotlinGradleProjectResolverExtension" order="first"/>
<projectResolve implementation="org.jetbrains.kotlin.idea.configuration.KotlinGradleCoroutineDebugProjectResolver" order="last"/>
<projectResolve implementation="org.jetbrains.kotlin.kapt.idea.KaptProjectResolverExtension" order="last"/>
@@ -11,6 +11,7 @@
<pluginDescriptions implementation="org.jetbrains.kotlin.idea.configuration.KotlinGradlePluginDescription"/>
<projectResolve implementation="org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModelResolver" order="first"/>
<projectResolve implementation="org.jetbrains.kotlin.idea.commonizer.KotlinCommonizerModelResolver" order="first"/>
<projectResolve implementation="org.jetbrains.kotlin.idea.cocoapods.KotlinCocoaPodsModelResolver" order="last"/>
<projectResolve implementation="org.jetbrains.kotlin.idea.configuration.KotlinGradleProjectResolverExtension" order="first"/>
<projectResolve implementation="org.jetbrains.kotlin.idea.configuration.KotlinGradleCoroutineDebugProjectResolver" order="last"/>
<projectResolve implementation="org.jetbrains.kotlin.kapt.idea.KaptProjectResolverExtension" order="last"/>
@@ -479,6 +479,12 @@ abstract class BaseGradleIT {
}
}
fun CompiledProject.assertTasksExecutedByPrefix(taskPrefixes: Iterable<String>) {
for (prefix in taskPrefixes) {
assertContainsRegex("(Executing actions for task|Executing task) '$prefix\\w*'".toRegex())
}
}
fun CompiledProject.assertTasksExecuted(vararg tasks: String) {
assertTasksExecuted(tasks.toList())
}
@@ -521,6 +527,18 @@ abstract class BaseGradleIT {
}
}
fun CompiledProject.assertTasksRegisteredByPrefix(taskPrefixes: Iterable<String>) {
for (prefix in taskPrefixes) {
assertContainsRegex("'Register task $prefix\\w*'".toRegex())
}
}
fun CompiledProject.assertTasksNotRegisteredByPrefix(taskPrefixes: Iterable<String>) {
for (prefix in taskPrefixes) {
assertNotContains("'Register task $prefix\\w*'".toRegex())
}
}
fun CompiledProject.assertTasksNotRealized(vararg tasks: String) {
for (task in tasks) {
assertNotContains("'Realize task $task'")
@@ -538,6 +556,12 @@ abstract class BaseGradleIT {
}
}
fun CompiledProject.assertTasksSkippedByPrefix(taskPrefixes: Iterable<String>) {
for (prefix in taskPrefixes) {
assertContainsRegex("Skipping task '$prefix\\w*'".toRegex())
}
}
fun CompiledProject.getOutputForTask(taskName: String): String {
@Language("RegExp")
val taskOutputRegex = """
@@ -5,6 +5,10 @@
package org.jetbrains.kotlin.gradle
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_BUILD_DEPENDENCIES_TASK_NAME
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_GEN_TASK_NAME
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_SETUP_BUILD_TASK_NAME
import org.jetbrains.kotlin.gradle.util.modify
import org.jetbrains.kotlin.konan.target.HostManager
import org.junit.Assume.assumeTrue
@@ -19,124 +23,241 @@ class CocoaPodsIT : BaseGradleIT() {
override val defaultGradleVersion: GradleVersionRequired
get() = GradleVersionRequired.FOR_MPP_SUPPORT
// We use Kotlin DSL. Earlier Gradle versions fail at accessors codegen.
val gradleVersion = GradleVersionRequired.None
val gradleVersion = GradleVersionRequired.FOR_MPP_SUPPORT
val PODFILE_IMPORT_DIRECTIVE_PLACEHOLDER = "<import_mode_directive>"
@Test
fun testPodspec() = doTestPodspec()
val cocoapodsSingleKtPod = "new-mpp-cocoapods-single"
val cocoapodsMultipleKtPods = "new-mpp-cocoapods-multiple"
@Test
fun testPodspecCustomFrameworkName() = doTestPodspec("MultiPlatformLibrary")
fun testPodspecSingle() = doTestPodspec(
cocoapodsSingleKtPod,
mapOf("kotlin-library" to null),
mapOf("kotlin-library" to kotlinLibraryPodspecContent())
)
private fun doTestPodspec(frameworkName: String? = null) {
@Test
fun testPodspecCustomFrameworkNameSingle() = doTestPodspec(
cocoapodsSingleKtPod,
mapOf("kotlin-library" to "MultiplatformLibrary"),
mapOf("kotlin-library" to kotlinLibraryPodspecContent("MultiplatformLibrary"))
)
@Test
fun testXcodeUseFrameworksSingle() = doTestXcode(
cocoapodsSingleKtPod,
ImportMode.FRAMEWORKS,
"ios-app", mapOf("kotlin-library" to null)
)
@Test
fun testXcodeUseFrameworksWithCustomFrameworkNameSingle() = doTestXcode(
cocoapodsSingleKtPod,
ImportMode.FRAMEWORKS,
"ios-app",
mapOf("kotlin-library" to "MultiplatformLibrary")
)
@Test
fun testXcodeUseModularHeadersSingle() = doTestXcode(
cocoapodsSingleKtPod,
ImportMode.MODULAR_HEADERS,
"ios-app",
mapOf("kotlin-library" to null)
)
@Test
fun testXcodeUseModularHeadersWithCustomFrameworkNameSingle() = doTestXcode(
cocoapodsSingleKtPod,
ImportMode.MODULAR_HEADERS,
"ios-app",
mapOf("kotlin-library" to "MultiplatformLibrary")
)
@Test
fun testPodImportUseFrameworksSingle() = doTestPodImport(
cocoapodsSingleKtPod,
"ios-app",
ImportMode.FRAMEWORKS,
listOf("kotlin-library")
)
@Test
fun testPodImportUseModularHeadersSingle() =
doTestPodImport(
cocoapodsSingleKtPod,
"ios-app",
ImportMode.MODULAR_HEADERS,
listOf("kotlin-library")
)
@Test
fun testPodspecMupltiple() = doTestPodspec(
cocoapodsMultipleKtPods,
mapOf("kotlin-library" to null, "second-library" to null),
mapOf("kotlin-library" to kotlinLibraryPodspecContent(), "second-library" to secondLibraryPodspecContent()),
)
@Test
fun testPodspecCustomFrameworkNameMupltiple() = doTestPodspec(
cocoapodsMultipleKtPods,
mapOf("kotlin-library" to "FirstMultiplatformLibrary", "second-library" to "SecondMultiplatformLibrary"),
mapOf(
"kotlin-library" to kotlinLibraryPodspecContent("FirstMultiplatformLibrary"),
"second-library" to secondLibraryPodspecContent("SecondMultiplatformLibrary")
)
)
@Test
fun testXcodeUseFrameworksMupltiple() = doTestXcode(
cocoapodsMultipleKtPods,
ImportMode.FRAMEWORKS,
"ios-app",
mapOf("kotlin-library" to null, "second-library" to null)
)
@Test
fun testXcodeUseFrameworksWithCustomFrameworkNameMupltiple() = doTestXcode(
cocoapodsMultipleKtPods,
ImportMode.FRAMEWORKS,
"ios-app",
mapOf("kotlin-library" to "FirstMultiplatformLibrary", "second-library" to "SecondMultiplatformLibrary")
)
@Test
fun testXcodeUseModularHeadersMupltiple() = doTestXcode(
cocoapodsMultipleKtPods,
ImportMode.MODULAR_HEADERS,
"ios-app",
mapOf("kotlin-library" to null, "second-library" to null)
)
@Test
fun testXcodeUseModularHeadersWithCustomFrameworkNameMupltiple() = doTestXcode(
cocoapodsMultipleKtPods,
ImportMode.MODULAR_HEADERS,
"ios-app",
mapOf("kotlin-library" to "FirstMultiplatformLibrary", "second-library" to "SecondMultiplatformLibrary")
)
@Test
fun testPodImportUseFrameworksMupltiple() = doTestPodImport(
cocoapodsMultipleKtPods,
"ios-app",
ImportMode.FRAMEWORKS,
listOf("kotlin-library", "second-library")
)
@Test
fun testPodImportUseModularHeadersMupltiple() =
doTestPodImport(
cocoapodsMultipleKtPods,
"ios-app",
ImportMode.MODULAR_HEADERS,
listOf("kotlin-library", "second-library")
)
private fun doTestPodspec(
projectName: String,
subprojectsToFrameworkNamesMap: Map<String, String?>,
subprojectsToPodspecContentMap: Map<String, String?>
) {
assumeTrue(HostManager.hostIsMac)
val gradleProject = transformProjectWithPluginsDsl("new-mpp-cocoapods", gradleVersion)
val gradleProject = transformProjectWithPluginsDsl(projectName, gradleVersion)
// Check that the podspec task fails if there is no Gradle wrapper in the project.
gradleProject.build(":kotlin-library:podspec") {
assertFailed()
assertContains("The Gradle wrapper is required to run the build from Xcode.")
assertContains(
"Please run the same command with `-Pkotlin.native.cocoapods.generate.wrapper=true` " +
"or run the `:wrapper` task to generate the wrapper manually."
)
gradleProject.build(":podspec") {
assertSuccessful()
assertTasksSkipped(":podspec")
assertNoSuchFile("cocoapods.podspec")
}
// Check that we can generate the wrapper along with the podspec if the corresponding property specified
gradleProject.build(":kotlin-library:podspec", "-Pkotlin.native.cocoapods.generate.wrapper=true") {
for ((subproject, frameworkName) in subprojectsToFrameworkNamesMap) {
frameworkName?.also {
gradleProject.gradleBuildScript(subproject).appendText(
"""
|kotlin {
| cocoapods {
| frameworkName = "$frameworkName"
| }
|}
""".trimMargin()
)
}
// Check that we can generate the wrapper along with the podspec if the corresponding property specified
gradleProject.build(":$subproject:podspec", "-Pkotlin.native.cocoapods.generate.wrapper=true") {
assertSuccessful()
assertTasksExecuted(":$subproject:podspec")
// Check that the podspec file is correctly generated.
val podspecFileName = "$subproject/${subproject.validFrameworkName}.podspec"
assertFileExists(podspecFileName)
val actualPodspecContentWithoutBlankLines = fileInWorkingDir(podspecFileName).readText()
.lineSequence()
.filter { it.isNotBlank() }
.joinToString("\n")
assertEquals(subprojectsToPodspecContentMap[subproject], actualPodspecContentWithoutBlankLines)
}
}
}
private fun doTestPodImport(
projectName: String,
iosAppLocation: String,
mode: ImportMode,
subprojects: List<String>
) {
assumeTrue(HostManager.hostIsMac)
assumeTrue(KotlinCocoapodsPlugin.isAvailableToProduceSynthetic())
val gradleProject = transformProjectWithPluginsDsl(projectName, gradleVersion)
with(gradleProject) {
preparePodfile(iosAppLocation, mode)
podImportAsserts()
subprojects.forEach { podImportAsserts(it) }
}
}
private fun BaseGradleIT.Project.podImportAsserts(subproject: String? = null) {
val buildScriptText = gradleBuildScript(subproject).readText()
val taskPrefix = subproject?.let { ":$it" } ?: ""
val podImport = "podImport"
val podspec = "podspec"
val podInstall = "podInstall"
build("$taskPrefix:$podImport", "-Pkotlin.native.cocoapods.generate.wrapper=true") {
assertSuccessful()
assertTasksExecuted(":kotlin-library:podspec", ":wrapper")
assertFileExists("gradlew")
// Check that the podspec file is correctly generated.
val podspecFileName = "kotlin-library/kotlin_library.podspec"
if ("noPodspec()" in buildScriptText) {
assertTasksSkipped("$taskPrefix:$podspec")
}
if ("podfile" in buildScriptText) {
assertTasksExecuted("$taskPrefix:$podInstall")
} else {
assertTasksSkipped("$taskPrefix:$podInstall")
}
assertTasksRegisteredByPrefix(listOf("$taskPrefix:$POD_GEN_TASK_NAME"))
if (buildScriptText.matches("pod\\(.*\\)".toRegex())) {
assertTasksExecutedByPrefix(listOf("$taskPrefix:$POD_GEN_TASK_NAME"))
}
frameworkName?.let {
fileInWorkingDir(podspecFileName).modify {
it.replace("build/cocoapods/framework/kotlin_library.framework", "build/cocoapods/framework/$frameworkName.framework")
with(listOf(POD_SETUP_BUILD_TASK_NAME, POD_BUILD_DEPENDENCIES_TASK_NAME).map { "$taskPrefix:$it" }) {
if (buildScriptText.matches("pod\\(.*\\)".toRegex())) {
assertTasksRegisteredByPrefix(this)
assertTasksExecutedByPrefix(this)
}
}
val expectedPodspecContent = """
Pod::Spec.new do |spec|
spec.name = 'kotlin_library'
spec.version = '1.0'
spec.homepage = 'https://github.com/JetBrains/kotlin'
spec.source = { :git => "Not Published", :tag => "Cocoapods/#{spec.name}/#{spec.version}" }
spec.authors = ''
spec.license = ''
spec.summary = 'CocoaPods test library'
spec.static_framework = true
spec.vendored_frameworks = "build/cocoapods/framework/${frameworkName ?: "kotlin_library"}.framework"
spec.libraries = "c++"
spec.module_name = "#{spec.name}_umbrella"
spec.dependency 'pod_dependency', '1.0'
spec.dependency 'subspec_dependency/Core', '1.0'
spec.pod_target_xcconfig = {
'KOTLIN_TARGET[sdk=iphonesimulator*]' => 'ios_x64',
'KOTLIN_TARGET[sdk=iphoneos*]' => 'ios_arm',
'KOTLIN_TARGET[sdk=watchsimulator*]' => 'watchos_x86',
'KOTLIN_TARGET[sdk=watchos*]' => 'watchos_arm',
'KOTLIN_TARGET[sdk=appletvsimulator*]' => 'tvos_x64',
'KOTLIN_TARGET[sdk=appletvos*]' => 'tvos_arm64',
'KOTLIN_TARGET[sdk=macosx*]' => 'macos_x64'
}
spec.script_phases = [
{
:name => 'Build kotlin_library',
:execution_position => :before_compile,
:shell_path => '/bin/sh',
:script => <<-SCRIPT
set -ev
REPO_ROOT="${'$'}PODS_TARGET_SRCROOT"
"${'$'}REPO_ROOT/../gradlew" -p "${'$'}REPO_ROOT" :kotlin-library:syncFramework \
-Pkotlin.native.cocoapods.target=${'$'}KOTLIN_TARGET \
-Pkotlin.native.cocoapods.configuration=${'$'}CONFIGURATION \
-Pkotlin.native.cocoapods.cflags="${'$'}OTHER_CFLAGS" \
-Pkotlin.native.cocoapods.paths.headers="${'$'}HEADER_SEARCH_PATHS" \
-Pkotlin.native.cocoapods.paths.frameworks="${'$'}FRAMEWORK_SEARCH_PATHS"
SCRIPT
}
]
end
""".trimIndent()
assertFileExists(podspecFileName)
assertEquals(expectedPodspecContent, fileInWorkingDir(podspecFileName).readText())
}
}
@Test
fun testInterop() {
assumeTrue(HostManager.hostIsMac)
val gradleProject = transformProjectWithPluginsDsl("new-mpp-cocoapods", gradleVersion)
with(gradleProject) {
// Check that a project with CocoaPods interop fails to be built from command line.
build(":kotlin-library:build") {
assertFailed()
assertContains("Cannot perform cinterop processing for module pod_dependency: cannot determine headers location.")
}
// Check that a project without CocoaPods interop can be built from command line.
gradleBuildScript("kotlin-library").modify {
it.replace("""pod("pod_dependency", "1.0")""", "").replace("""pod("subspec_dependency/Core", "1.0")""", "")
}
projectDir.resolve("kotlin-library/src/iosMain/kotlin/A.kt").modify {
it.replace("import cocoapods.pod_dependency.*", "").replace("println(foo())", "")
.replace("import cocoapods.subspec_dependency.*", "").replace("println(baz())", "")
}
build(":kotlin-library:linkReleaseFrameworkIOS") {
assertSuccessful()
}
}
}
private enum class ImportMode(val directive: String) {
FRAMEWORKS("use_frameworks!"),
@@ -177,40 +298,47 @@ class CocoaPodsIT : BaseGradleIT() {
CommandResult(process.exitValue(), stdOut, stdErr).block()
}
private fun doTestXcode(mode: ImportMode, isCustomFrameworkName: Boolean) {
private fun doTestXcode(
projectName: String,
mode: ImportMode,
iosAppLocation: String,
subprojectsToFrameworkNamesMap: Map<String, String?>
) {
assumeTrue(HostManager.hostIsMac)
val gradleProject = transformProjectWithPluginsDsl("new-mpp-cocoapods", gradleVersion)
val gradleProject = transformProjectWithPluginsDsl(projectName, gradleVersion)
with(gradleProject) {
setupWorkingDir()
// Add property with custom framework name
if (isCustomFrameworkName) {
gradleBuildScript("kotlin-library").appendText(
"""
kotlin {
cocoapods {
frameworkName = "MultiPlatformLibrary"
for ((subproject, frameworkName) in subprojectsToFrameworkNamesMap) {
// Add property with custom framework name
frameworkName?.also { name ->
gradleBuildScript(subproject).appendText(
"""
kotlin {
cocoapods {
frameworkName = "$name"
}
}
""".trimIndent()
)
// Change swift sources import
val iosAppDir = projectDir.resolve(iosAppLocation)
iosAppDir.resolve("ios-app/ViewController.swift").modify {
it.replace("import ${subproject.validFrameworkName}", "import $name")
}
}
""".trimIndent()
)
// Change swift sources import
val iosAppDir = projectDir.resolve("ios-app")
iosAppDir.resolve("ios-app/ViewController.swift").modify {
it.replace("import kotlin_library", "import MultiPlatformLibrary")
// Generate podspec.
gradleProject.build(":$subproject:podspec", "-Pkotlin.native.cocoapods.generate.wrapper=true") {
assertSuccessful()
}
}
// Generate podspec.
gradleProject.build(":kotlin-library:podspec", "-Pkotlin.native.cocoapods.generate.wrapper=true") {
assertSuccessful()
}
val iosAppDir = projectDir.resolve("ios-app")
val iosAppDir = projectDir.resolve(iosAppLocation)
// Set import mode for Podfile.
iosAppDir.resolve("Podfile").modify {
@@ -218,17 +346,8 @@ class CocoaPodsIT : BaseGradleIT() {
}
// Install pods.
runCommand(iosAppDir, "pod", "install") {
assertEquals(
0, exitCode, """
|Exit code mismatch for `pod install`.
|stdout:
|$stdOut
|
|stderr:
|$stdErr
""".trimMargin()
)
gradleProject.build(":podInstall", "-Pkotlin.native.cocoapods.generate.wrapper=true") {
assertSuccessful()
}
// Run Xcode build.
@@ -237,8 +356,8 @@ class CocoaPodsIT : BaseGradleIT() {
"-sdk", "iphonesimulator",
"-arch", "arm64",
"-configuration", "Release",
"-workspace", "ios-app.xcworkspace",
"-scheme", "ios-app",
"-workspace", "${iosAppDir.name}.xcworkspace",
"-scheme", iosAppDir.name,
inheritIO = true // Xcode doesn't finish the process if the PIPE redirect is used.
) {
assertEquals(
@@ -255,13 +374,102 @@ class CocoaPodsIT : BaseGradleIT() {
}
}
@Test
fun testXcodeUseFrameworks() = doTestXcode(ImportMode.FRAMEWORKS, false)
private fun Project.preparePodfile(iosAppLocation: String, mode: ImportMode) {
val iosAppDir = projectDir.resolve(iosAppLocation)
@Test
fun testXcodeUseModularHeaders() = doTestXcode(ImportMode.MODULAR_HEADERS, false)
// Set import mode for Podfile.
iosAppDir.resolve("Podfile").modify {
it.replace(PODFILE_IMPORT_DIRECTIVE_PLACEHOLDER, mode.directive)
}
}
@Test
fun testXcodeWithCustomFrameworkName() = doTestXcode(ImportMode.FRAMEWORKS, true)
private val String.validFrameworkName: String
get() = replace('-', '_')
private fun kotlinLibraryPodspecContent(frameworkName: String? = null) = """
Pod::Spec.new do |spec|
spec.name = 'kotlin_library'
spec.version = '1.0'
spec.homepage = 'https://github.com/JetBrains/kotlin'
spec.source = { :git => "Not Published", :tag => "Cocoapods/#{spec.name}/#{spec.version}" }
spec.authors = ''
spec.license = ''
spec.summary = 'CocoaPods test library'
spec.static_framework = true
spec.vendored_frameworks = "build/cocoapods/framework/${frameworkName ?: "kotlin_library"}.framework"
spec.libraries = "c++"
spec.module_name = "#{spec.name}_umbrella"
spec.dependency 'pod_dependency', '1.0'
spec.dependency 'subspec_dependency/Core', '1.0'
spec.pod_target_xcconfig = {
'KOTLIN_TARGET[sdk=iphonesimulator*]' => 'ios_x64',
'KOTLIN_TARGET[sdk=iphoneos*]' => 'ios_arm',
'KOTLIN_TARGET[sdk=watchsimulator*]' => 'watchos_x86',
'KOTLIN_TARGET[sdk=watchos*]' => 'watchos_arm',
'KOTLIN_TARGET[sdk=appletvsimulator*]' => 'tvos_x64',
'KOTLIN_TARGET[sdk=appletvos*]' => 'tvos_arm64',
'KOTLIN_TARGET[sdk=macosx*]' => 'macos_x64'
}
spec.script_phases = [
{
:name => 'Build kotlin_library',
:execution_position => :before_compile,
:shell_path => '/bin/sh',
:script => <<-SCRIPT
set -ev
REPO_ROOT="${'$'}PODS_TARGET_SRCROOT"
"${'$'}REPO_ROOT/../gradlew" -p "${'$'}REPO_ROOT" :kotlin-library:syncFramework \
-Pkotlin.native.cocoapods.target=${'$'}KOTLIN_TARGET \
-Pkotlin.native.cocoapods.configuration=${'$'}CONFIGURATION \
-Pkotlin.native.cocoapods.cflags="${'$'}OTHER_CFLAGS" \
-Pkotlin.native.cocoapods.paths.headers="${'$'}HEADER_SEARCH_PATHS" \
-Pkotlin.native.cocoapods.paths.frameworks="${'$'}FRAMEWORK_SEARCH_PATHS"
SCRIPT
}
]
end
""".trimIndent()
private fun secondLibraryPodspecContent(frameworkName: String? = null) = """
Pod::Spec.new do |spec|
spec.name = 'second_library'
spec.version = '1.0'
spec.homepage = 'https://github.com/JetBrains/kotlin'
spec.source = { :git => "Not Published", :tag => "Cocoapods/#{spec.name}/#{spec.version}" }
spec.authors = ''
spec.license = ''
spec.summary = 'CocoaPods test library'
spec.static_framework = true
spec.vendored_frameworks = "build/cocoapods/framework/${frameworkName ?: "second_library"}.framework"
spec.libraries = "c++"
spec.module_name = "#{spec.name}_umbrella"
spec.pod_target_xcconfig = {
'KOTLIN_TARGET[sdk=iphonesimulator*]' => 'ios_x64',
'KOTLIN_TARGET[sdk=iphoneos*]' => 'ios_arm',
'KOTLIN_TARGET[sdk=watchsimulator*]' => 'watchos_x86',
'KOTLIN_TARGET[sdk=watchos*]' => 'watchos_arm',
'KOTLIN_TARGET[sdk=appletvsimulator*]' => 'tvos_x64',
'KOTLIN_TARGET[sdk=appletvos*]' => 'tvos_arm64',
'KOTLIN_TARGET[sdk=macosx*]' => 'macos_x64'
}
spec.script_phases = [
{
:name => 'Build second_library',
:execution_position => :before_compile,
:shell_path => '/bin/sh',
:script => <<-SCRIPT
set -ev
REPO_ROOT="${'$'}PODS_TARGET_SRCROOT"
"${'$'}REPO_ROOT/../gradlew" -p "${'$'}REPO_ROOT" :second-library:syncFramework \
-Pkotlin.native.cocoapods.target=${'$'}KOTLIN_TARGET \
-Pkotlin.native.cocoapods.configuration=${'$'}CONFIGURATION \
-Pkotlin.native.cocoapods.cflags="${'$'}OTHER_CFLAGS" \
-Pkotlin.native.cocoapods.paths.headers="${'$'}HEADER_SEARCH_PATHS" \
-Pkotlin.native.cocoapods.paths.frameworks="${'$'}FRAMEWORK_SEARCH_PATHS"
SCRIPT
}
]
end
""".trimIndent()
}
@@ -0,0 +1,9 @@
ios-app/Pods
Podfile.lock
kotlin-library/kotlin_library.podspec
second-library/second_library.podspec
ios-app/ios-app.xcodeproj/xcuserdata
ios-app/ios-app.xcworkspace/xcuserdata
ios-app/ios-app.xcodeproj/project.xcworkspace/xcuserdata
ios-app/ios-app.xcworkspace/contents.xcworkspacedata
ios-app/ios-app.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -19,9 +19,7 @@ kotlin {
iosX64("iOS")
cocoapods {
summary = "CocoaPods test library"
homepage = "https://github.com/JetBrains/kotlin"
pod("pod_dependency", "1.0")
pod("subspec_dependency/Core", "1.0")
noPodspec()
podfile = projectDir.resolve("ios-app/Podfile")
}
}
@@ -0,0 +1,10 @@
<import_mode_directive>
platform :ios, '9.0'
target 'ios-app' do
pod 'pod_dependency', :path => '../pod_dependency'
pod 'subspec_dependency', :path => '../subspec_dependency'
pod 'kotlin_library', :path => '../kotlin-library'
pod 'second_library', :path => '../second-library'
end
@@ -0,0 +1,13 @@
import UIKit
import kotlin_library
import second_library
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
AKt.bar()
AKt.bazz()
BKt.secondBar()
// Do any additional setup after loading the view.
}
}
@@ -0,0 +1,27 @@
plugins {
id("org.jetbrains.kotlin.multiplatform")
id("org.jetbrains.kotlin.native.cocoapods")
}
group = "com.example"
version = "1.0"
repositories {
mavenLocal()
jcenter()
maven { setUrl("https://dl.bintray.com/kotlin/kotlinx.html/") }
}
group = "org.jetbrains.kotlin.sample.native"
version = "1.0"
kotlin {
iosX64("iOS")
cocoapods {
summary = "CocoaPods test library"
homepage = "https://github.com/JetBrains/kotlin"
pod("pod_dependency", "1.0", projectDir.resolve("../pod_dependency/pod_dependency.podspec"))
pod("subspec_dependency/Core", "1.0", projectDir.resolve("../subspec_dependency/subspec_dependency.podspec"))
}
}
@@ -0,0 +1,2 @@
kotlin.native.disableCompilerDaemon=true
kotlin.native.cacheKind=none
@@ -0,0 +1,25 @@
plugins {
id("org.jetbrains.kotlin.multiplatform")
id("org.jetbrains.kotlin.native.cocoapods")
}
group = "com.example"
version = "1.0"
repositories {
mavenLocal()
jcenter()
maven { setUrl("https://dl.bintray.com/kotlin/kotlinx.html/") }
}
group = "org.jetbrains.kotlin.sample.native"
version = "1.0"
kotlin {
iosX64("iOS")
cocoapods {
summary = "CocoaPods test library"
homepage = "https://github.com/JetBrains/kotlin"
}
}
@@ -0,0 +1,2 @@
kotlin.native.disableCompilerDaemon=true
kotlin.native.cacheKind=none
@@ -0,0 +1,14 @@
pluginManagement {
repositories {
mavenLocal()
jcenter()
gradlePluginPortal()
}
}
enableFeaturePreview('GRADLE_METADATA')
// We move the actual library in a subproject to test accessing gradle wrapper from Xcode.
rootProject.name = "cocoapods"
include 'kotlin-library'
include 'second-library'
@@ -0,0 +1,25 @@
plugins {
id("org.jetbrains.kotlin.multiplatform").version("<pluginMarkerVersion>")
id("org.jetbrains.kotlin.native.cocoapods").version("<pluginMarkerVersion>")
}
group = "com.example"
version = "1.0"
repositories {
mavenLocal()
jcenter()
maven { setUrl("https://dl.bintray.com/kotlin/kotlinx.html/") }
}
group = "org.jetbrains.kotlin.sample.native"
version = "1.0"
kotlin {
iosX64("iOS")
cocoapods {
noPodspec()
podfile = projectDir.resolve("ios-app/Podfile")
}
}
@@ -0,0 +1,341 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
2C4835762268863D00C928E6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4835752268863D00C928E6 /* AppDelegate.swift */; };
2C4835782268863D00C928E6 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4835772268863D00C928E6 /* ViewController.swift */; };
2C48357B2268863D00C928E6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2C4835792268863D00C928E6 /* Main.storyboard */; };
2C48357D2268863E00C928E6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2C48357C2268863E00C928E6 /* Assets.xcassets */; };
2C4835802268863E00C928E6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2C48357E2268863E00C928E6 /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
2C4835722268863D00C928E6 /* ios-app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ios-app.app"; sourceTree = BUILT_PRODUCTS_DIR; };
2C4835752268863D00C928E6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
2C4835772268863D00C928E6 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
2C48357A2268863D00C928E6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
2C48357C2268863E00C928E6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
2C48357F2268863E00C928E6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
2C4835812268863E00C928E6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
2C48356F2268863D00C928E6 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
2C4835692268863D00C928E6 = {
isa = PBXGroup;
children = (
2C4835742268863D00C928E6 /* ios-app */,
2C4835732268863D00C928E6 /* Products */,
);
sourceTree = "<group>";
};
2C4835732268863D00C928E6 /* Products */ = {
isa = PBXGroup;
children = (
2C4835722268863D00C928E6 /* ios-app.app */,
);
name = Products;
sourceTree = "<group>";
};
2C4835742268863D00C928E6 /* ios-app */ = {
isa = PBXGroup;
children = (
2C4835752268863D00C928E6 /* AppDelegate.swift */,
2C4835772268863D00C928E6 /* ViewController.swift */,
2C4835792268863D00C928E6 /* Main.storyboard */,
2C48357C2268863E00C928E6 /* Assets.xcassets */,
2C48357E2268863E00C928E6 /* LaunchScreen.storyboard */,
2C4835812268863E00C928E6 /* Info.plist */,
);
path = "ios-app";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
2C4835712268863D00C928E6 /* ios-app */ = {
isa = PBXNativeTarget;
buildConfigurationList = 2C4835842268863E00C928E6 /* Build configuration list for PBXNativeTarget "ios-app" */;
buildPhases = (
2C48356E2268863D00C928E6 /* Sources */,
2C48356F2268863D00C928E6 /* Frameworks */,
2C4835702268863D00C928E6 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = "ios-app";
productName = "ios-app";
productReference = 2C4835722268863D00C928E6 /* ios-app.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
2C48356A2268863D00C928E6 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1020;
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = jetbrains;
TargetAttributes = {
2C4835712268863D00C928E6 = {
CreatedOnToolsVersion = 10.2;
};
};
};
buildConfigurationList = 2C48356D2268863D00C928E6 /* Build configuration list for PBXProject "ios-app" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 2C4835692268863D00C928E6;
productRefGroup = 2C4835732268863D00C928E6 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
2C4835712268863D00C928E6 /* ios-app */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
2C4835702268863D00C928E6 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2C4835802268863E00C928E6 /* LaunchScreen.storyboard in Resources */,
2C48357D2268863E00C928E6 /* Assets.xcassets in Resources */,
2C48357B2268863D00C928E6 /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
2C48356E2268863D00C928E6 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2C4835782268863D00C928E6 /* ViewController.swift in Sources */,
2C4835762268863D00C928E6 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
2C4835792268863D00C928E6 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
2C48357A2268863D00C928E6 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
2C48357E2268863E00C928E6 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
2C48357F2268863E00C928E6 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
2C4835822268863E00C928E6 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
2C4835832268863E00C928E6 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
2C4835852268863E00C928E6 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = "ios-app/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.jetbrains.konan.ios-app";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
2C4835862268863E00C928E6 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = "ios-app/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.jetbrains.konan.ios-app";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
2C48356D2268863D00C928E6 /* Build configuration list for PBXProject "ios-app" */ = {
isa = XCConfigurationList;
buildConfigurations = (
2C4835822268863E00C928E6 /* Debug */,
2C4835832268863E00C928E6 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
2C4835842268863E00C928E6 /* Build configuration list for PBXNativeTarget "ios-app" */ = {
isa = XCConfigurationList;
buildConfigurations = (
2C4835852268863E00C928E6 /* Debug */,
2C4835862268863E00C928E6 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 2C48356A2268863D00C928E6 /* Project object */;
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,34 @@
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}
@@ -0,0 +1,98 @@
{
"images": [
{
"idiom": "iphone",
"size": "20x20",
"scale": "2x"
},
{
"idiom": "iphone",
"size": "20x20",
"scale": "3x"
},
{
"idiom": "iphone",
"size": "29x29",
"scale": "2x"
},
{
"idiom": "iphone",
"size": "29x29",
"scale": "3x"
},
{
"idiom": "iphone",
"size": "40x40",
"scale": "2x"
},
{
"idiom": "iphone",
"size": "40x40",
"scale": "3x"
},
{
"idiom": "iphone",
"size": "60x60",
"scale": "2x"
},
{
"idiom": "iphone",
"size": "60x60",
"scale": "3x"
},
{
"idiom": "ipad",
"size": "20x20",
"scale": "1x"
},
{
"idiom": "ipad",
"size": "20x20",
"scale": "2x"
},
{
"idiom": "ipad",
"size": "29x29",
"scale": "1x"
},
{
"idiom": "ipad",
"size": "29x29",
"scale": "2x"
},
{
"idiom": "ipad",
"size": "40x40",
"scale": "1x"
},
{
"idiom": "ipad",
"size": "40x40",
"scale": "2x"
},
{
"idiom": "ipad",
"size": "76x76",
"scale": "1x"
},
{
"idiom": "ipad",
"size": "76x76",
"scale": "2x"
},
{
"idiom": "ipad",
"size": "83.5x83.5",
"scale": "2x"
},
{
"idiom": "ios-marketing",
"size": "1024x1024",
"scale": "1x"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
@@ -0,0 +1,27 @@
plugins {
id("org.jetbrains.kotlin.multiplatform")
id("org.jetbrains.kotlin.native.cocoapods")
}
group = "com.example"
version = "1.0"
repositories {
mavenLocal()
jcenter()
maven { setUrl("https://dl.bintray.com/kotlin/kotlinx.html/") }
}
group = "org.jetbrains.kotlin.sample.native"
version = "1.0"
kotlin {
iosX64("iOS")
cocoapods {
summary = "CocoaPods test library"
homepage = "https://github.com/JetBrains/kotlin"
pod("pod_dependency", "1.0", projectDir.resolve("../pod_dependency/pod_dependency.podspec"))
pod("subspec_dependency/Core", "1.0", projectDir.resolve("../subspec_dependency/subspec_dependency.podspec"))
}
}
@@ -0,0 +1,10 @@
import cocoapods.pod_dependency.*
import cocoapods.subspec_dependency.*
fun bar() {
println(foo())
}
fun bazz() {
println(baz())
}
@@ -0,0 +1,11 @@
Pod::Spec.new do |spec|
spec.name = 'pod_dependency'
spec.version = '1.0'
spec.homepage = 'foo'
spec.source = { :git => "Not Published", :tag => "Cocoapods/#{spec.name}/#{spec.version}" }
spec.authors = ''
spec.license = ''
spec.summary = 'foo'
spec.source_files = 'src/*'
spec.public_header_files = 'src/*.h'
end
@@ -0,0 +1,3 @@
#import <Foundation/Foundation.h>
NSString* foo(void);
@@ -0,0 +1,5 @@
#include "foo.h"
NSString* foo() {
return @"Foo";
}
@@ -0,0 +1,3 @@
#import <Foundation/Foundation.h>
NSString* baz(void);
@@ -0,0 +1,5 @@
#include "baz.h"
NSString* baz() {
return @"Baz";
}
@@ -0,0 +1,15 @@
Pod::Spec.new do |spec|
spec.name = 'subspec_dependency'
spec.version = '1.0'
spec.homepage = 'baz'
spec.source = { :git => "Not Published", :tag => "Cocoapods/#{spec.name}/#{spec.version}" }
spec.authors = ''
spec.license = ''
spec.summary = 'baz'
spec.default_subspec = 'Core'
spec.subspec 'Core' do |core|
core.source_files = "src/*"
core.public_header_files = "src/*.h"
end
end
@@ -17,17 +17,6 @@ open class CocoapodsExtension(private val project: Project) {
val version: String
get() = project.version.toString()
@Optional
@InputFile
var podfile: File? = null
/**
* Configure path to the Podfile.
*/
fun podfile(path: String) {
podfile = project.file(path)
}
/**
* Configure authors of the pod built from this project.
*/
@@ -35,6 +24,23 @@ open class CocoapodsExtension(private val project: Project) {
@Input
var authors: String? = null
/**
* Configure existing file `Podfile`.
*/
@Optional
@InputFile
var podfile: File? = null
@get:Input
internal var needPodspec: Boolean = true
/**
* Setup plugin not to produce podspec file for cocoapods section
*/
fun noPodspec() {
needPodspec = false
}
/**
* Configure license of the pod built from this project.
*/
@@ -56,6 +62,18 @@ open class CocoapodsExtension(private val project: Project) {
@Input
var homepage: String? = null
@Nested
val ios: PodspecPlatformSettings = PodspecPlatformSettings("ios")
@Nested
val osx: PodspecPlatformSettings = PodspecPlatformSettings("osx")
@Nested
val tvos: PodspecPlatformSettings = PodspecPlatformSettings("tvos")
@Nested
val watchos: PodspecPlatformSettings = PodspecPlatformSettings("watchos")
/**
* Configure framework name of the pod built from this project.
*/
@@ -81,17 +99,27 @@ open class CocoapodsExtension(private val project: Project) {
* Add a CocoaPods dependency to the pod built from this project.
*/
@JvmOverloads
fun pod(name: String, version: String? = null, moduleName: String = name.split("/")[0]) {
fun pod(name: String, version: String? = null, podspec: File? = null, moduleName: String = name.split("/")[0]) {
check(_pods.findByName(name) == null) { "Project already has a CocoaPods dependency with name $name" }
_pods.add(CocoapodsDependency(name, version, moduleName))
_pods.add(CocoapodsDependency(name, version, podspec, moduleName))
}
data class CocoapodsDependency(
private val name: String,
@get:Optional @get:Input val version: String?,
@get:Optional @get:InputFile val podspec: File?,
@get:Input val moduleName: String
) : Named {
@Input
override fun getName(): String = name
}
data class PodspecPlatformSettings(
private val name: String,
@get:Optional @get:Input var deploymentTarget: String? = null
) : Named {
@Input
override fun getName(): String = name
}
}
@@ -12,23 +12,23 @@ import org.gradle.api.Task
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Sync
import org.gradle.api.tasks.TaskProvider
import org.jetbrains.kotlin.gradle.tasks.registerTask
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.addExtension
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.plugin.whenEvaluated
import org.jetbrains.kotlin.gradle.targets.native.tasks.PodBuildSettingsProperties
import org.jetbrains.kotlin.gradle.targets.native.tasks.PodBuildTask
import org.jetbrains.kotlin.gradle.targets.native.tasks.PodInstallTask
import org.jetbrains.kotlin.gradle.targets.native.tasks.PodSetupBuildTask
import org.jetbrains.kotlin.gradle.tasks.*
import org.jetbrains.kotlin.gradle.targets.native.tasks.*
import org.jetbrains.kotlin.gradle.tasks.DefFileTask
import org.jetbrains.kotlin.gradle.tasks.DummyFrameworkTask
import org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask
import org.jetbrains.kotlin.gradle.tasks.PodspecTask
import org.jetbrains.kotlin.gradle.utils.asValidTaskName
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.konan.target.KonanTarget
import java.io.File
import java.io.FileInputStream
internal val Project.cocoapodsBuildDirs: CocoapodsBuildDirs
get() = CocoapodsBuildDirs(this)
@@ -43,17 +43,35 @@ internal class CocoapodsBuildDirs(val project: Project) {
val defs: File
get() = root.resolve("defs")
val buildSettings: File
get() = root.resolve("buildSettings")
private val synthetic: File
get() = root.resolve("synthetic")
fun synthetic(kotlinNativeTarget: KotlinNativeTarget) = synthetic.resolve(kotlinNativeTarget.name)
fun fatFramework(buildType: String) =
root.resolve("fat-frameworks/${buildType.toLowerCase()}")
}
internal fun String.asValidFrameworkName() = replace('-', '_')
internal val KotlinNativeTarget.toBuildSetupTaskName: String
get() = lowerCamelCaseName(KotlinCocoapodsPlugin.POD_SETUP_BUILD_TASK_NAME, disambiguationClassifier)
private val KotlinNativeTarget.toPodGenTaskName: String
get() = lowerCamelCaseName(KotlinCocoapodsPlugin.POD_GEN_TASK_NAME, disambiguationClassifier)
internal val KotlinNativeTarget.toBuildDependenciesTaskName: String
get() = lowerCamelCaseName(KotlinCocoapodsPlugin.POD_BUILD_DEPENDENCIES_TASK_NAME, disambiguationClassifier)
private val KotlinNativeTarget.toSetupBuildTaskName: String
get() = lowerCamelCaseName(
KotlinCocoapodsPlugin.POD_SETUP_BUILD_TASK_NAME,
disambiguationClassifier
)
private val KotlinNativeTarget.toBuildDependenciesTaskName: String
get() = lowerCamelCaseName(
KotlinCocoapodsPlugin.POD_BUILD_DEPENDENCIES_TASK_NAME,
disambiguationClassifier
)
open class KotlinCocoapodsPlugin : Plugin<Project> {
@@ -181,7 +199,8 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
moduleNames.add(pod.moduleName)
val defTask = project.registerTask<DefFileTask>(
lowerCamelCaseName("generateDef", pod.moduleName).asValidTaskName()) {
lowerCamelCaseName("generateDef", pod.moduleName).asValidTaskName()
) {
it.pod = pod
it.description = "Generates a def file for CocoaPods dependencies with module ${pod.moduleName}"
// This task is an implementation detail so we don't add it in any group
@@ -199,7 +218,11 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
interop.packageName = "cocoapods.${pod.moduleName}"
if (project.findProperty(TARGET_PROPERTY) == null && project.findProperty(CONFIGURATION_PROPERTY) == null) {
if (//TODO ychernyshev improve first check
isAvailableToProduceSynthetic()
&& project.findProperty(TARGET_PROPERTY) == null
&& project.findProperty(CONFIGURATION_PROPERTY) == null
) {
val podBuildTaskProvider = project.tasks.named(target.toBuildDependenciesTaskName, PodBuildTask::class.java)
interopTask.inputs.file(podBuildTaskProvider.get().buildSettingsFileProvider)
interopTask.dependsOn(podBuildTaskProvider)
@@ -221,24 +244,28 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
// Since we cannot expand the configuration phase of interop tasks
// receiving the required environment variables happens on execution phase.
// TODO This needs to be fixed to improve UP-TO-DATE checks.
if (project.findProperty(TARGET_PROPERTY) == null && project.findProperty(CONFIGURATION_PROPERTY) == null) {
if (// TODO ychernyshev improve first check
isAvailableToProduceSynthetic()
&& project.findProperty(TARGET_PROPERTY) == null
&& project.findProperty(CONFIGURATION_PROPERTY) == null
) {
val podBuildTaskProvider = project.tasks.named(target.toBuildDependenciesTaskName, PodBuildTask::class.java)
val buildSettings =
podBuildTaskProvider.get().buildSettingsFileProvider.get()
?.inputStream()
?.use {
.inputStream()
.use {
PodBuildSettingsProperties.readSettingsFromStream(it)
}
buildSettings?.cflags?.let { args ->
buildSettings.cflags?.let { args ->
// Xcode quotes around paths with spaces.
// Here and below we need to split such paths taking this into account.
interop.compilerOpts.addAll(args.splitQuotedArgs())
}
buildSettings?.headerPaths?.let { args ->
buildSettings.headerPaths?.let { args ->
interop.compilerOpts.addAll(args.splitQuotedArgs().map { "-I$it" })
}
buildSettings?.frameworkPaths?.let { args ->
buildSettings.frameworkPaths?.let { args ->
interop.compilerOpts.addAll(args.splitQuotedArgs().map { "-F$it" })
}
}
@@ -253,7 +280,7 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
cocoapodsExtension: CocoapodsExtension
) {
project.tasks.register(DUMMY_FRAMEWORK_TASK_NAME, DummyFrameworkTask::class.java) {
it.settings = cocoapodsExtension
it.cocoapodsExtension = cocoapodsExtension
}
}
@@ -279,14 +306,34 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
project: Project,
cocoapodsExtension: CocoapodsExtension
) {
val podspecTaskProvider = project.tasks.named(POD_SPEC_TASK_NAME, PodspecTask::class.java)
project.tasks.register(POD_INSTALL_TASK_NAME, PodInstallTask::class.java) {
it.group = TASK_GROUP
it.description = "Invokes `pod install` call within Podfile location directory"
it.cocoapodsExtension = cocoapodsExtension
it.dependsOn(podspecTaskProvider)
//TODO avoid subproject task management here
project.allprojects.map { it.tasks.named(POD_SPEC_TASK_NAME, PodspecTask::class.java) }
.forEach { podspecTaskProvider ->
podspecTaskProvider.get().takeIf { task -> task.cocoapodsExtension.needPodspec }
?.also { task -> it.inputs.file(task.outputFileProvider) }
it.dependsOn(podspecTaskProvider)
}
}
}
private fun registerPodGenTask(
project: Project, kotlinExtension: KotlinMultiplatformExtension, cocoapodsExtension: CocoapodsExtension
) {
val podspecTaskProvider = project.tasks.named(POD_SPEC_TASK_NAME, PodspecTask::class.java)
kotlinExtension.supportedTargets().all { target ->
project.tasks.register(target.toPodGenTaskName, PodGenTask::class.java) {
it.description = "Сreates a synthetic Xcode project to retrieve CocoaPods dependencies"
it.podspecProvider = podspecTaskProvider.get().outputFileProvider
it.kotlinNativeTarget = target
it.cocoapodsExtension = cocoapodsExtension
it.dependsOn(podspecTaskProvider)
}
}
}
@@ -296,16 +343,16 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
cocoapodsExtension: CocoapodsExtension
) {
val podInstallTaskProvider = project.tasks.named(POD_INSTALL_TASK_NAME, PodInstallTask::class.java)
kotlinExtension.supportedTargets().all { target ->
project.tasks.register(target.toBuildSetupTaskName, PodSetupBuildTask::class.java) {
project.tasks.register(target.toSetupBuildTaskName, PodSetupBuildTask::class.java) {
it.group = TASK_GROUP
it.kotlinNativeTarget = target
it.description = "Collect environment variables from .xcworkspace file"
it.cocoapodsExtension = cocoapodsExtension
it.podsXcodeProjDirProvider = podInstallTaskProvider.get().podsXcodeProjDirProvider
it.dependsOn(podInstallTaskProvider)
it.kotlinNativeTarget = target
val podGenTaskProvider = project.tasks.named(target.toPodGenTaskName, PodGenTask::class.java)
it.podsXcodeProjDirProvider = podGenTaskProvider.get().podsXcodeProjDirProvider
it.dependsOn(podGenTaskProvider)
}
}
}
@@ -318,15 +365,15 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
kotlinExtension.supportedTargets().all { target ->
val podSetupBuildTaskProvider = project.tasks.named(target.toBuildSetupTaskName, PodSetupBuildTask::class.java)
val podSetupBuildTaskProvider = project.tasks.named(target.toSetupBuildTaskName, PodSetupBuildTask::class.java)
project.tasks.register(target.toBuildDependenciesTaskName, PodBuildTask::class.java) {
it.group = TASK_GROUP
it.description = "Calls `xcodebuild` on xcworkspace for the pod scheme"
it.kotlinNativeTarget = target
it.cocoapodsExtension = cocoapodsExtension
it.podsXcodeProjDirProvider = (podSetupBuildTaskProvider.get()).podsXcodeProjDirProvider
it.buildSettingsFileProvider = (podSetupBuildTaskProvider.get()).buildSettingsFileProvider
it.podsXcodeProjDirProvider = podSetupBuildTaskProvider.get().podsXcodeProjDirProvider
it.buildSettingsFileProvider = podSetupBuildTaskProvider.get().buildSettingsFileProvider
it.dependsOn(podSetupBuildTaskProvider)
}
}
@@ -337,9 +384,11 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
kotlinExtension: KotlinMultiplatformExtension
) {
val podInstallTaskProvider = project.tasks.named(POD_INSTALL_TASK_NAME, PodInstallTask::class.java)
project.tasks.register(POD_IMPORT_TASK_NAME) {
it.group = TASK_GROUP
it.description = "Called on Gradle sync, depends on Cinterop tasks for every used pod"
it.dependsOn(podInstallTaskProvider)
kotlinExtension.supportedTargets().all { target ->
target.compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME).cinterops.all { interop ->
@@ -347,7 +396,6 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
it.dependsOn(interopTaskProvider)
}
}
}
}
@@ -356,27 +404,40 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
pluginManager.withPlugin("kotlin-multiplatform") {
val kotlinExtension = project.multiplatformExtension
val cocoapodsExtension = CocoapodsExtension(this)
kotlinExtension.addExtension(EXTENSION_NAME, cocoapodsExtension)
kotlinExtension.addExtension(COCOAPODS_EXTENSION_NAME, cocoapodsExtension)
createDefaultFrameworks(kotlinExtension, cocoapodsExtension)
registerDummyFrameworkTask(project, cocoapodsExtension)
createSyncTask(project, kotlinExtension)
registerPodspecTask(project, cocoapodsExtension)
registerPodInstallTask(project, cocoapodsExtension)
registerPodSetupBuildTasks(project, kotlinExtension, cocoapodsExtension)
registerPodBuildTasks(project, kotlinExtension, cocoapodsExtension)
registerPodImportTask(project, kotlinExtension)
if (isAvailableToProduceSynthetic()) {
registerPodGenTask(project, kotlinExtension, cocoapodsExtension)
registerPodInstallTask(project, cocoapodsExtension)
registerPodSetupBuildTasks(project, kotlinExtension, cocoapodsExtension)
registerPodBuildTasks(project, kotlinExtension, cocoapodsExtension)
registerPodImportTask(project, kotlinExtension)
} else {
logger.quiet(
"""
To take advantage of the new functionality for Cocoapods Integration like synchronizing with the Xcode project
and supporting dependencies on pods, please install the `cocoapods-generate` plugin for CocoaPods
by calling `gem install cocoapods-generate` in terminal.
More details are available by https://github.com/square/cocoapods-generate
""".trimIndent()
)
}
createInterops(project, kotlinExtension, cocoapodsExtension)
}
}
companion object {
const val EXTENSION_NAME = "cocoapods"
const val COCOAPODS_EXTENSION_NAME = "cocoapods"
const val TASK_GROUP = "CocoaPods"
const val SYNC_TASK_NAME = "syncFramework"
const val POD_SPEC_TASK_NAME = "podspec"
const val DUMMY_FRAMEWORK_TASK_NAME = "generateDummyFramework"
const val POD_INSTALL_TASK_NAME = "podInstall"
const val POD_GEN_TASK_NAME = "podGen"
const val POD_SETUP_BUILD_TASK_NAME = "podSetupBuild"
const val POD_BUILD_DEPENDENCIES_TASK_NAME = "podBuildDependencies"
const val POD_IMPORT_TASK_NAME = "podImport"
@@ -396,5 +457,14 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
// so we should generate a fat framework with arm32 and arm64 binaries.
const val KOTLIN_TARGET_FOR_IOS_DEVICE = "ios_arm"
const val KOTLIN_TARGET_FOR_WATCHOS_DEVICE = "watchos_arm"
fun isAvailableToProduceSynthetic(): Boolean {
val gemListProcess = ProcessBuilder("gem", "list").start()
val gemListRetCode = gemListProcess.waitFor()
val gemListOutput = gemListProcess.inputStream.use {
it.reader().readText()
}
return gemListRetCode == 0 && gemListOutput.contains("cocoapods-generate")
}
}
}
@@ -12,150 +12,229 @@ import org.gradle.api.tasks.Optional
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension
import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.targets.native.tasks.PodBuildTask.Companion.toValidSDK
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.KonanTarget
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.math.BigInteger
import java.security.MessageDigest
import java.util.*
abstract class AdvancedCocoapodsTask : DefaultTask() {
@get:Nested
lateinit var cocoapodsExtension: CocoapodsExtension
internal val KotlinNativeTarget.toBuildSettingsFileName: String
get() = "build-settings-$disambiguationClassifier.properties"
@TaskAction
fun commonInvoke() {
podfileCheck(cocoapodsExtension)
specificInvoke()
internal val KotlinNativeTarget.toValidSDK: String
get() = when (konanTarget) {
KonanTarget.IOS_X64 -> "iphonesimulator"
KonanTarget.IOS_ARM32, KonanTarget.IOS_ARM64 -> "iphoneos"
KonanTarget.WATCHOS_X86, KonanTarget.WATCHOS_X64 -> "watchsimulator"
KonanTarget.WATCHOS_ARM32, KonanTarget.WATCHOS_ARM64 -> "watchos"
KonanTarget.TVOS_X64 -> "appletvsimulator"
KonanTarget.TVOS_ARM64 -> "appletvos"
KonanTarget.MACOS_X64 -> "macosx"
else -> throw IllegalArgumentException("Bad target ${konanTarget.name}.")
}
abstract fun specificInvoke()
private fun podfileCheck(cocoapodsExtension: CocoapodsExtension) {
check(cocoapodsExtension.podfile != null) {
"Execution of task '$name' requires the path to the Podfile. Specify it in the file ${project.buildFile.absolutePath}."
}
internal val KotlinNativeTarget.platformLiteral: String
get() = when (konanTarget.family) {
Family.OSX -> "macos"
Family.IOS -> "ios"
Family.TVOS -> "tvos"
Family.WATCHOS -> "watchos"
else -> throw IllegalArgumentException("Unsupported native target '${konanTarget.name}'")
}
}
/**
* 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.
*/
open class PodInstallTask : AdvancedCocoapodsTask() {
@get:InputFile
@get:Optional
internal val podfileProvider: Provider<File?> = project.provider { cocoapodsExtension.podfile }
@get:OutputDirectory
@get:Optional
internal val podsXcodeProjDirProvider: Provider<File?> = project.provider {
cocoapodsExtension.podfile
?.parentFile
?.resolve("Pods")
?.resolve("Pods.xcodeproj")
open class PodInstallTask : DefaultTask() {
init {
onlyIf { cocoapodsExtension?.podfile != null }
}
override fun specificInvoke() {
val podfileDir = podfileProvider.get()!!.parentFile
val podInstallProcess = ProcessBuilder("pod", "install").apply {
directory(podfileDir)
inheritIO()
}.start()
val podInstallRetCode = podInstallProcess.waitFor()
check(podInstallRetCode == 0) { "Unable to run 'pod install', return code $podInstallRetCode" }
@get:Optional
@get:Nested
internal var cocoapodsExtension: CocoapodsExtension? = null
val podsXcprojFile = podsXcodeProjDirProvider.get()
check(podsXcprojFile != null && podsXcprojFile.exists() && podsXcprojFile.isDirectory) {
"The directory 'Pods/Pods.xcodeproj' was not created as a result of the `pod install` call."
@get:Optional
@get:OutputDirectory
internal val podsXcodeProjDirProvider: Provider<File>?
get() = cocoapodsExtension?.podfile?.let {
project.provider { it.parentFile.resolve("Pods").resolve("Pods.xcodeproj") }
}
@TaskAction
fun doPodInstall() {
cocoapodsExtension?.podfile?.parentFile?.also { podfileDir ->
val podInstallProcess = ProcessBuilder("pod", "install").apply {
directory(podfileDir)
}.start()
val podInstallRetCode = podInstallProcess.waitFor()
val podInstallOutput = podInstallProcess.inputStream.use { it.reader().readText() }
check(podInstallRetCode == 0) {
listOf(
"Executing of 'pod install' failed with code $podInstallRetCode.",
"Error message:",
podInstallOutput
).joinToString("\n")
}
with(podsXcodeProjDirProvider) {
check(this != null && get().exists() && get().isDirectory) {
"The directory 'Pods/Pods.xcodeproj' was not created as a result of the `pod install` call."
}
}
}
}
}
open class PodSetupBuildTask : AdvancedCocoapodsTask() {
abstract class CocoapodsWithSyntheticTask : DefaultTask() {
init {
onlyIf {
cocoapodsExtension.pods.isNotEmpty()
}
}
@get:Nested
internal lateinit var cocoapodsExtension: CocoapodsExtension
}
/**
* The task takes the path to the .podspec file and calls `pod gen`
* to create synthetic xcode project and workspace.
*/
open class PodGenTask : CocoapodsWithSyntheticTask() {
@get:InputFile
internal lateinit var podspecProvider: Provider<File>
@Internal
lateinit var kotlinNativeTarget: KotlinNativeTarget
@get:OutputDirectory
internal val podsXcodeProjDirProvider: Provider<File>
get() = project.provider {
project.cocoapodsBuildDirs.synthetic(kotlinNativeTarget)
.resolve(podspecProvider.get().nameWithoutExtension)
.resolve("Pods")
.resolve("Pods.xcodeproj")
}
@TaskAction
fun generate() {
val podspecDir = podspecProvider.get().parentFile
val localPodspecPaths = cocoapodsExtension.pods.mapNotNull { it.podspec?.parentFile?.absolutePath }
val podGenProcessArgs = listOfNotNull(
"pod", "gen",
"--platforms=${kotlinNativeTarget.platformLiteral}",
"--gen-directory=${project.cocoapodsBuildDirs.synthetic(kotlinNativeTarget).absolutePath}",
localPodspecPaths.takeIf { it.isNotEmpty() }?.joinToString(separator = ",")?.let { "--local-sources=$it" },
podspecProvider.get().name
)
val podGenProcess = ProcessBuilder(podGenProcessArgs).apply {
directory(podspecDir)
}.start()
val podGenRetCode = podGenProcess.waitFor()
val outputText = podGenProcess.inputStream.use { it.reader().readText() }
check(podGenRetCode == 0) {
listOfNotNull(
"Executing of '${podGenProcessArgs.joinToString(" ")}' failed with code $podGenRetCode and message:",
outputText,
outputText.takeIf {
it.contains("deployment target")
|| it.contains("requested platforms: [\"${kotlinNativeTarget.platformLiteral}\"]")
}?.let {
"""
Tip: try to configure deployment_target for ALL targets as follows:
cocoapods {
...
${kotlinNativeTarget.konanTarget.family.name.toLowerCase()}.deploymentTarget = "..."
...
}
""".trimIndent()
}
).joinToString("\n")
}
val podsXcprojFile = podsXcodeProjDirProvider.get()
check(podsXcprojFile.exists() && podsXcprojFile.isDirectory) {
"The directory '${podsXcprojFile.path}' was not created as a result of the `pod gen` call."
}
}
}
open class PodSetupBuildTask : CocoapodsWithSyntheticTask() {
@get:InputDirectory
@get:Optional
internal lateinit var podsXcodeProjDirProvider: Provider<File?>
internal lateinit var podsXcodeProjDirProvider: Provider<File>
@Internal
lateinit var kotlinNativeTarget: KotlinNativeTarget
@get:OutputFile
@get:Optional
internal val buildSettingsFileProvider: Provider<File?> =
project.provider {
project
.cocoapodsBuildDirs
.root
.resolve("buildSettings")
.resolve(kotlinNativeTarget.toBuildSettingsFileName)
}
internal val buildSettingsFileProvider: Provider<File> = project.provider {
project.cocoapodsBuildDirs
.buildSettings
.resolve(kotlinNativeTarget.toBuildSettingsFileName)
}
override fun specificInvoke() {
@TaskAction
fun setupBuild() {
val podsXcodeProjDir = podsXcodeProjDirProvider.get()
val buildSettingsReceivingCommand = listOf(
"xcodebuild", "-showBuildSettings",
"-project", podsXcodeProjDir!!.name,
"-project", podsXcodeProjDir.name,
"-scheme", cocoapodsExtension.frameworkName,
"-sdk", kotlinNativeTarget.toValidSDK
)
val buildSettingsProcess = ProcessBuilder(buildSettingsReceivingCommand)
.apply {
directory(podsXcodeProjDir!!.parentFile)
directory(podsXcodeProjDir.parentFile)
}.start()
val buildSettingsRetCode = buildSettingsProcess.waitFor()
check(buildSettingsRetCode == 0) {
"Unable to run '${buildSettingsReceivingCommand.joinToString(" ")}' return code $buildSettingsRetCode"
listOf(
"Executing of '${buildSettingsReceivingCommand.joinToString(" ")}' failed with code $buildSettingsRetCode and message:",
buildSettingsProcess.errorStream.use { it.reader().readText() }
).joinToString("\n")
}
val stdOut = buildSettingsProcess.inputStream
val buildSettingsProperties = PodBuildSettingsProperties.readSettingsFromStream(stdOut)
buildSettingsProperties.writeSettings(buildSettingsFileProvider.get()!!)
}
companion object {
private val KotlinNativeTarget.toBuildSettingsFileName: String
get() = "build-settings-$disambiguationClassifier.properties"
buildSettingsFileProvider.get().let { buildSettingsProperties.writeSettings(it) }
}
}
/**
* The task compiles external cocoa pods sources.
*/
open class PodBuildTask : AdvancedCocoapodsTask() {
open class PodBuildTask : CocoapodsWithSyntheticTask() {
@get:InputDirectory
@get:Optional
internal lateinit var podsXcodeProjDirProvider: Provider<File?>
internal lateinit var podsXcodeProjDirProvider: Provider<File>
@get:InputFile
@get:Optional
internal lateinit var buildSettingsFileProvider: Provider<File?>
internal lateinit var buildSettingsFileProvider: Provider<File>
@Internal
lateinit var kotlinNativeTarget: KotlinNativeTarget
@get:OutputFile
@get:Optional
internal val buildDirHashFileProvider: Provider<File?> =
project.provider {
project
.cocoapodsBuildDirs
.root
.resolve("buildDirHashSums")
.resolve(kotlinNativeTarget.toBuildDirHashSumFileName)
}
@get:OutputDirectory
internal var buildDirProvider: Provider<File>? = null
override fun specificInvoke() {
@TaskAction
fun buildDependencies() {
val podBuildSettings = PodBuildSettingsProperties.readSettingsFromStream(
FileInputStream(buildSettingsFileProvider.get())
)
@@ -166,7 +245,7 @@ open class PodBuildTask : AdvancedCocoapodsTask() {
val podXcodeBuildCommand = listOf(
"xcodebuild",
"-project", podsXcodeProjDir!!.name,
"-project", podsXcodeProjDir.name,
"-scheme", it.moduleName,
"-sdk", kotlinNativeTarget.toValidSDK,
"-configuration", podBuildSettings.configuration
@@ -174,59 +253,19 @@ open class PodBuildTask : AdvancedCocoapodsTask() {
val podBuildProcess = ProcessBuilder(podXcodeBuildCommand)
.apply {
directory(podsXcodeProjDir!!.parentFile)
directory(podsXcodeProjDir.parentFile)
inheritIO()
}.start()
val podBuildRetCode = podBuildProcess.waitFor()
check(podBuildRetCode == 0) {
"Unable to run '${podXcodeBuildCommand.joinToString(" ")}' return code $podBuildRetCode"
listOf(
"Executing of '${podXcodeBuildCommand.joinToString(" ")}' failed with code $podBuildRetCode and message:",
podBuildProcess.errorStream.use { it.reader().readText() }
).joinToString("\n")
}
}
val buildDirHashSumFile = buildDirHashFileProvider.get()!!
buildDirHashSumFile!!.parentFile.mkdirs()
buildDirHashSumFile!!.createNewFile()
check(buildDirHashSumFile!!.exists()) {
"Unable to create file ${buildDirHashSumFile.toRelativeString(project.projectDir)}!"
}
buildDirHashSumFile.writeText(getFileChecksumStr(project.file(podBuildSettings.buildDir)))
}
companion object {
private fun getFileChecksumStr(file: File): String {
val digest = MessageDigest.getInstance("SHA-1")
file.walkTopDown().forEach {
val byteArray = ByteArray(1024)
var bytesCount = 0
if (it.isFile) {
val inputStream = it.inputStream()
while (inputStream.read(byteArray).also { bt -> bytesCount = bt } != -1) {
digest.update(byteArray, 0, bytesCount)
}
}
}
val buildDirHashSumStr = BigInteger(1, digest.digest()).toString(16).padStart(32, '0')
return buildDirHashSumStr
}
internal val KotlinNativeTarget.toBuildDirHashSumFileName: String
get() = "build-dir-hash-$disambiguationClassifier.txt"
internal val KotlinNativeTarget.toValidSDK: String
get() {
return when (konanTarget) {
KonanTarget.IOS_X64 -> "iphonesimulator"
KonanTarget.IOS_ARM32, KonanTarget.IOS_ARM64 -> "iphoneos"
KonanTarget.WATCHOS_X86, KonanTarget.WATCHOS_X64 -> "watchsimulator"
KonanTarget.WATCHOS_ARM32, KonanTarget.WATCHOS_ARM64 -> "watchos"
KonanTarget.TVOS_X64 -> "appletvsimulator"
KonanTarget.TVOS_ARM64 -> "appletvos"
KonanTarget.MACOS_X64 -> "macos"
else -> throw Error("Bad target ${konanTarget.name}")
}
}
buildDirProvider = project.provider { project.file(podBuildSettings.buildDir) }
}
}
@@ -7,11 +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.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.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.KOTLIN_TARGET_FOR_IOS_DEVICE
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.KOTLIN_TARGET_FOR_WATCHOS_DEVICE
@@ -29,13 +34,19 @@ open class PodspecTask : DefaultTask() {
private val specName = project.name.asValidFrameworkName()
@get:OutputFile
internal val outputFileProvider: Provider<File> = project.provider { project.file("$specName.podspec") }
internal val outputFileProvider: Provider<File>
get() = project.provider { project.file("$specName.podspec") }
@get:Nested
internal lateinit var cocoapodsExtension: CocoapodsExtension
init {
onlyIf { cocoapodsExtension.needPodspec }
}
@TaskAction
fun generate() {
val frameworkDir = project.cocoapodsBuildDirs.framework.relativeTo(outputFileProvider.get().parentFile).path
val dependencies = cocoapodsExtension.pods.map { pod ->
val versionSuffix = if (pod.version != null) ", '${pod.version}'" else ""
@@ -56,6 +67,13 @@ open class PodspecTask : DefaultTask() {
val gradleCommand = "\$REPO_ROOT/${gradleWrapper!!.toRelativeString(project.projectDir)}"
val syncTask = "${project.path}:$SYNC_TASK_NAME"
val deploymentTargets = run {
with(cocoapodsExtension) {
listOf(ios, osx, tvos, watchos).filter { it.deploymentTarget != null }.joinToString("\n") {
"| spec.${it.name}.deployment_target = '${it.deploymentTarget}'"
}
}
}
with(outputFileProvider.get()) {
writeText(
@@ -74,6 +92,8 @@ open class PodspecTask : DefaultTask() {
| spec.libraries = "c++"
| spec.module_name = "#{spec.name}_umbrella"
|
$deploymentTargets
|
$dependencies
|
| spec.pod_target_xcconfig = {
@@ -93,7 +113,7 @@ open class PodspecTask : DefaultTask() {
| :shell_path => '/bin/sh',
| :script => <<-SCRIPT
| set -ev
| REPO_ROOT="${'$'}PODS_TARGET_SRCROOT/"
| REPO_ROOT="${'$'}PODS_TARGET_SRCROOT"
| "$gradleCommand" -p "${'$'}REPO_ROOT" $syncTask \
| -P${KotlinCocoapodsPlugin.TARGET_PROPERTY}=${'$'}KOTLIN_TARGET \
| -P${KotlinCocoapodsPlugin.CONFIGURATION_PROPERTY}=${'$'}CONFIGURATION \
@@ -107,17 +127,30 @@ open class PodspecTask : DefaultTask() {
""".trimMargin()
)
logger.quiet(
"""
Generated a podspec file at: ${absolutePath}.
To include it in your Xcode project, add the following dependency snippet in your Podfile:
if (hasPodfileOwnOrParent(project)) {
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', :path => '${parentFile.absolutePath}'
pod '$specName', :path => '${parentFile.absolutePath}'
""".trimIndent()
)
)
}
}
}
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)
}
}
@@ -137,10 +170,10 @@ open class DummyFrameworkTask : DefaultTask() {
val destinationDir = project.cocoapodsBuildDirs.framework
@Input
val frameworkNameProvider: Provider<String> = project.provider { settings.frameworkName }
val frameworkNameProvider: Provider<String> = project.provider { cocoapodsExtension.frameworkName }
@get:Nested
internal lateinit var settings: CocoapodsExtension
internal lateinit var cocoapodsExtension: CocoapodsExtension
private val frameworkDir: File
get() = destinationDir.resolve("${frameworkNameProvider.get()}.framework")
@@ -157,8 +190,10 @@ open class DummyFrameworkTask : DefaultTask() {
private fun copyTextResource(from: String, to: File, transform: (String) -> String = { it }) {
to.parentFile.mkdirs()
to.printWriter().use { file ->
javaClass.getResourceAsStream(from).reader().forEachLine {
file.println(transform(it))
javaClass.getResourceAsStream(from).use {
it.reader().forEachLine { str ->
file.println(transform(str))
}
}
}
}