From b13b6d86c70ebd42f28485216ebd72e9235f4d40 Mon Sep 17 00:00:00 2001 From: Yaroslav Chernyshev Date: Wed, 25 Mar 2020 11:20:01 +0300 Subject: [PATCH] [Gradle, Native] Advanced CocoaPods integration. --- .../cocoapods/KotlinCocoaPodsModelResolver.kt | 24 ++ .../KotlinCocoaPodsModelResolver.kt.191 | 8 + ...plugins.gradle.tooling.ModelBuilderService | 1 + ...ins.gradle.tooling.ModelBuilderService.191 | 0 .../src/KotlinCocoaPodsModelBuilder.kt | 38 +++ .../src/KotlinCocoaPodsModelBuilder.kt.191 | 0 idea/resources/META-INF/gradle-java.xml | 107 +++---- .../native/cocoapods/CocoapodsExtension.kt | 17 +- .../native/cocoapods/KotlinCocoapodsPlugin.kt | 206 ++++++++++--- .../native/tasks/AdvancedCocoapodsTasks.kt | 279 ++++++++++++++++++ .../targets/native/tasks/CocoapodsTasks.kt | 121 ++++---- 11 files changed, 646 insertions(+), 155 deletions(-) create mode 100644 idea/idea-gradle/src/org/jetbrains/kotlin/idea/cocoapods/KotlinCocoaPodsModelResolver.kt create mode 100644 idea/idea-gradle/src/org/jetbrains/kotlin/idea/cocoapods/KotlinCocoaPodsModelResolver.kt.191 create mode 100644 idea/kotlin-gradle-tooling/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService.191 create mode 100644 idea/kotlin-gradle-tooling/src/KotlinCocoaPodsModelBuilder.kt create mode 100644 idea/kotlin-gradle-tooling/src/KotlinCocoaPodsModelBuilder.kt.191 create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/AdvancedCocoapodsTasks.kt diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/cocoapods/KotlinCocoaPodsModelResolver.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/cocoapods/KotlinCocoaPodsModelResolver.kt new file mode 100644 index 00000000000..7853746a00b --- /dev/null +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/cocoapods/KotlinCocoaPodsModelResolver.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.cocoapods + +import org.jetbrains.kotlin.gradle.EnablePodImportTask +import org.jetbrains.plugins.gradle.model.ClassSetProjectImportModelProvider +import org.jetbrains.plugins.gradle.model.ProjectImportModelProvider +import org.jetbrains.plugins.gradle.service.project.AbstractProjectResolverExtension + +class KotlinCocoaPodsModelResolver : AbstractProjectResolverExtension() { + + override fun getProjectsLoadedModelProvider(): ProjectImportModelProvider { + return ClassSetProjectImportModelProvider( + setOf(EnablePodImportTask::class.java) + ) + } + + override fun requiresTaskRunning(): Boolean = true +} + + diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/cocoapods/KotlinCocoaPodsModelResolver.kt.191 b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/cocoapods/KotlinCocoaPodsModelResolver.kt.191 new file mode 100644 index 00000000000..d165935db5b --- /dev/null +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/cocoapods/KotlinCocoaPodsModelResolver.kt.191 @@ -0,0 +1,8 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.cocoapods + +class KotlinCocoaPodsModelResolver \ No newline at end of file diff --git a/idea/kotlin-gradle-tooling/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService b/idea/kotlin-gradle-tooling/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService index b918a8c27ab..d35ac4b4278 100644 --- a/idea/kotlin-gradle-tooling/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService +++ b/idea/kotlin-gradle-tooling/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService @@ -1,2 +1,3 @@ org.jetbrains.kotlin.gradle.KotlinGradleModelBuilder org.jetbrains.kotlin.gradle.KotlinMPPGradleModelBuilder +org.jetbrains.kotlin.gradle.KotlinCocoaPodsModelBuilder diff --git a/idea/kotlin-gradle-tooling/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService.191 b/idea/kotlin-gradle-tooling/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService.191 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/idea/kotlin-gradle-tooling/src/KotlinCocoaPodsModelBuilder.kt b/idea/kotlin-gradle-tooling/src/KotlinCocoaPodsModelBuilder.kt new file mode 100644 index 00000000000..d33c732ce20 --- /dev/null +++ b/idea/kotlin-gradle-tooling/src/KotlinCocoaPodsModelBuilder.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.gradle + +import org.gradle.api.Project +import org.jetbrains.plugins.gradle.tooling.AbstractModelBuilderService +import org.jetbrains.plugins.gradle.tooling.ErrorMessageBuilder +import org.jetbrains.plugins.gradle.tooling.ModelBuilderContext + +private const val POD_IMPORT_TASK_NAME = "podImport" + +interface EnablePodImportTask + +class KotlinCocoaPodsModelBuilder : AbstractModelBuilderService() { + override fun canBuild(modelName: String?): Boolean { + return EnablePodImportTask::class.java.name == modelName + } + + override fun buildAll(modelName: String, project: Project, context: ModelBuilderContext): Any? { + val startParameter = project.gradle.startParameter + val taskNames = startParameter.taskNames + + if (project.tasks.findByPath(POD_IMPORT_TASK_NAME) != null && POD_IMPORT_TASK_NAME !in taskNames) { + taskNames.add(POD_IMPORT_TASK_NAME) + startParameter.setTaskNames(taskNames) + } + return null + } + + override fun getErrorMessageBuilder(project: Project, e: Exception): ErrorMessageBuilder { + return ErrorMessageBuilder.create( + project, e, "EnablePodImportTask error" + ).withDescription("Unable to create $POD_IMPORT_TASK_NAME task.") + } +} \ No newline at end of file diff --git a/idea/kotlin-gradle-tooling/src/KotlinCocoaPodsModelBuilder.kt.191 b/idea/kotlin-gradle-tooling/src/KotlinCocoaPodsModelBuilder.kt.191 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/idea/resources/META-INF/gradle-java.xml b/idea/resources/META-INF/gradle-java.xml index 8f920688c46..e48bcd17ea8 100644 --- a/idea/resources/META-INF/gradle-java.xml +++ b/idea/resources/META-INF/gradle-java.xml @@ -1,60 +1,65 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - + + + + - + - - - - - - - + + + + + + + - - - + + + - - + + - - - - - - + + + + + + diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/CocoapodsExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/CocoapodsExtension.kt index 0bd73bb2bf1..cfe63fd8ea2 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/CocoapodsExtension.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/CocoapodsExtension.kt @@ -9,16 +9,25 @@ package org.jetbrains.kotlin.gradle.plugin.cocoapods import org.gradle.api.Named import org.gradle.api.NamedDomainObjectSet import org.gradle.api.Project -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.Internal -import org.gradle.api.tasks.Nested -import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.* +import java.io.File open class CocoapodsExtension(private val project: Project) { @get:Input 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. */ diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/KotlinCocoapodsPlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/KotlinCocoapodsPlugin.kt index 7e200046921..67a1708ea4b 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/KotlinCocoapodsPlugin.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/KotlinCocoapodsPlugin.kt @@ -16,12 +16,20 @@ 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.tasks.* +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.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) @@ -42,6 +50,12 @@ internal class CocoapodsBuildDirs(val project: Project) { internal fun String.asValidFrameworkName() = replace('-', '_') +internal val KotlinNativeTarget.toBuildSetupTaskName: String + get() = lowerCamelCaseName(KotlinCocoapodsPlugin.POD_SETUP_BUILD_TASK_NAME, disambiguationClassifier) + +internal val KotlinNativeTarget.toBuildDependenciesTaskName: String + get() = lowerCamelCaseName(KotlinCocoapodsPlugin.POD_BUILD_DEPENDENCIES_TASK_NAME, disambiguationClassifier) + open class KotlinCocoapodsPlugin : Plugin { private fun KotlinMultiplatformExtension.supportedTargets() = targets @@ -70,8 +84,9 @@ open class KotlinCocoapodsPlugin : Plugin { } } - private fun Project.createSyncFrameworkTask(originalDirectory: File, buildingTask: Task) = - tasks.create(SYNC_TASK_NAME, Sync::class.java) { + private fun Project.createSyncFrameworkTask(originalDirectory: File, buildingTask: Task): Sync? { + + return tasks.create(SYNC_TASK_NAME, Sync::class.java) { it.group = TASK_GROUP it.description = "Copies a framework for given platform and build type into the CocoaPods build directory" @@ -79,6 +94,7 @@ open class KotlinCocoapodsPlugin : Plugin { it.from(originalDirectory) it.destinationDir = cocoapodsBuildDirs.framework } + } private fun createSyncForFatFramework( project: Project, @@ -153,32 +169,14 @@ open class KotlinCocoapodsPlugin : Plugin { } } - private fun createPodspecGenerationTask( - project: Project, - cocoapodsExtension: CocoapodsExtension - ) { - val dummyFrameworkTask = project.tasks.create("generateDummyFramework", DummyFrameworkTask::class.java) { - it.settings = cocoapodsExtension - } - - project.tasks.create("podspec", PodspecTask::class.java) { - it.group = TASK_GROUP - it.description = "Generates a podspec file for CocoaPods import" - it.settings = cocoapodsExtension - it.dependsOn(dummyFrameworkTask) - val generateWrapper = project.findProperty(GENERATE_WRAPPER_PROPERTY)?.toString()?.toBoolean() ?: false - if (generateWrapper) { - it.dependsOn(":wrapper") - } - } - } - private fun createInterops( project: Project, kotlinExtension: KotlinMultiplatformExtension, cocoapodsExtension: CocoapodsExtension ) { val moduleNames = mutableSetOf() + + cocoapodsExtension.pods.all { pod -> if (moduleNames.contains(pod.moduleName)) { return@all @@ -199,10 +197,19 @@ open class KotlinCocoapodsPlugin : Plugin { target.compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME).cinterops.create(pod.moduleName) { interop -> val interopTask = project.tasks.getByPath(interop.interopProcessingTaskName) + interopTask.dependsOn(defTask) + interop.defFile = defTask.outputFile interop.packageName = "cocoapods.${pod.moduleName}" + + if (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) + } + project.findProperty(CFLAGS_PROPERTY)?.toString()?.let { args -> // Xcode quotes around paths with spaces. // Here and below we need to split such paths taking this into account. @@ -215,23 +222,30 @@ open class KotlinCocoapodsPlugin : Plugin { interop.compilerOpts.addAll(args.splitQuotedArgs().map { "-F$it" }) } - // Show a human-readable error messages if the interop is created - // but there are no parameters set by Xcode or manually by user (KT-31062). interopTask.doFirst { _ -> - val hasCompilerOpts = interop.compilerOpts.isNotEmpty() - val hasHeaderSearchPath = interop.includeDirs.let { - !it.headerFilterDirs.isEmpty || !it.allHeadersDirs.isEmpty - } + // 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) { + val podBuildTaskProvider = project.tasks.named(target.toBuildDependenciesTaskName, PodBuildTask::class.java) + val buildSettings = + podBuildTaskProvider.get().buildSettingsFileProvider.get() + ?.inputStream() + ?.use { + PodBuildSettingsProperties.readSettingsFromStream(it) + } - check(hasCompilerOpts || hasHeaderSearchPath) { - """ - |Cannot perform cinterop processing for module ${pod.moduleName}: cannot determine headers location. - | - |Probably the build is executed from command line. - |Note that a Kotlin/Native module using CocoaPods dependencies can be built only from Xcode. - | - |See details at https://kotlinlang.org/docs/reference/native/cocoapods.html#interoperability. - """.trimMargin() + 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 -> + interop.compilerOpts.addAll(args.splitQuotedArgs().map { "-I$it" }) + } + buildSettings?.frameworkPaths?.let { args -> + interop.compilerOpts.addAll(args.splitQuotedArgs().map { "-F$it" }) + } } } } @@ -239,6 +253,109 @@ open class KotlinCocoapodsPlugin : Plugin { } } + private fun registerDummyFrameworkTask( + project: Project, + cocoapodsExtension: CocoapodsExtension + ) { + project.tasks.register(DUMMY_FRAMEWORK_TASK_NAME, DummyFrameworkTask::class.java) { + it.settings = cocoapodsExtension + } + } + + private fun registerPodspecTask( + project: Project, + cocoapodsExtension: CocoapodsExtension + ) { + val dummyFrameworkTaskProvider = project.tasks.named(DUMMY_FRAMEWORK_TASK_NAME) + + project.tasks.register(POD_SPEC_TASK_NAME, PodspecTask::class.java) { + it.group = TASK_GROUP + it.description = "Generates a podspec file for CocoaPods import" + it.cocoapodsExtension = cocoapodsExtension + it.dependsOn(dummyFrameworkTaskProvider) + val generateWrapper = project.findProperty(GENERATE_WRAPPER_PROPERTY)?.toString()?.toBoolean() ?: false + if (generateWrapper) { + it.dependsOn(":wrapper") + } + } + } + + private fun registerPodInstallTask( + 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) + } + } + + private fun registerPodSetupBuildTasks( + project: Project, + kotlinExtension: KotlinMultiplatformExtension, + 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) { + 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) + } + } + } + + private fun registerPodBuildTasks( + project: Project, + kotlinExtension: KotlinMultiplatformExtension, + cocoapodsExtension: CocoapodsExtension + ) { + + kotlinExtension.supportedTargets().all { target -> + + val podSetupBuildTaskProvider = project.tasks.named(target.toBuildSetupTaskName, 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.dependsOn(podSetupBuildTaskProvider) + } + } + } + + private fun registerPodImportTask( + project: Project, + kotlinExtension: KotlinMultiplatformExtension + ) { + + 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" + + kotlinExtension.supportedTargets().all { target -> + target.compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME).cinterops.all { interop -> + val interopTaskProvider = project.tasks.named(interop.interopProcessingTaskName) + it.dependsOn(interopTaskProvider) + } + } + + } + } + override fun apply(project: Project): Unit = with(project) { pluginManager.withPlugin("kotlin-multiplatform") { @@ -247,8 +364,13 @@ open class KotlinCocoapodsPlugin : Plugin { kotlinExtension.addExtension(EXTENSION_NAME, cocoapodsExtension) createDefaultFrameworks(kotlinExtension, cocoapodsExtension) + registerDummyFrameworkTask(project, cocoapodsExtension) createSyncTask(project, kotlinExtension) - createPodspecGenerationTask(project, cocoapodsExtension) + registerPodspecTask(project, cocoapodsExtension) + registerPodInstallTask(project, cocoapodsExtension) + registerPodSetupBuildTasks(project, kotlinExtension, cocoapodsExtension) + registerPodBuildTasks(project, kotlinExtension, cocoapodsExtension) + registerPodImportTask(project, kotlinExtension) createInterops(project, kotlinExtension, cocoapodsExtension) } } @@ -257,6 +379,12 @@ open class KotlinCocoapodsPlugin : Plugin { const val 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_SETUP_BUILD_TASK_NAME = "podSetupBuild" + const val POD_BUILD_DEPENDENCIES_TASK_NAME = "podBuildDependencies" + const val POD_IMPORT_TASK_NAME = "podImport" // We don't move these properties in PropertiesProvider because // they are not intended to be overridden in local.properties. diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/AdvancedCocoapodsTasks.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/AdvancedCocoapodsTasks.kt new file mode 100644 index 00000000000..32881ae9cfe --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/AdvancedCocoapodsTasks.kt @@ -0,0 +1,279 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.gradle.targets.native.tasks + +import org.gradle.api.DefaultTask +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.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.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 + + @TaskAction + fun commonInvoke() { + podfileCheck(cocoapodsExtension) + specificInvoke() + } + + 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}." + } + } + +} + +/** + * 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 = project.provider { cocoapodsExtension.podfile } + + @get:OutputDirectory + @get:Optional + internal val podsXcodeProjDirProvider: Provider = project.provider { + cocoapodsExtension.podfile + ?.parentFile + ?.resolve("Pods") + ?.resolve("Pods.xcodeproj") + } + + 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" } + + 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." + } + } +} + +open class PodSetupBuildTask : AdvancedCocoapodsTask() { + @get:InputDirectory + @get:Optional + internal lateinit var podsXcodeProjDirProvider: Provider + + @Internal + lateinit var kotlinNativeTarget: KotlinNativeTarget + + @get:OutputFile + @get:Optional + internal val buildSettingsFileProvider: Provider = + project.provider { + project + .cocoapodsBuildDirs + .root + .resolve("buildSettings") + .resolve(kotlinNativeTarget.toBuildSettingsFileName) + } + + override fun specificInvoke() { + val podsXcodeProjDir = podsXcodeProjDirProvider.get() + + val buildSettingsReceivingCommand = listOf( + "xcodebuild", "-showBuildSettings", + "-project", podsXcodeProjDir!!.name, + "-scheme", cocoapodsExtension.frameworkName, + "-sdk", kotlinNativeTarget.toValidSDK + ) + + val buildSettingsProcess = ProcessBuilder(buildSettingsReceivingCommand) + .apply { + directory(podsXcodeProjDir!!.parentFile) + }.start() + + val buildSettingsRetCode = buildSettingsProcess.waitFor() + check(buildSettingsRetCode == 0) { + "Unable to run '${buildSettingsReceivingCommand.joinToString(" ")}' return code $buildSettingsRetCode" + } + + + 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" + } +} + +/** + * The task compiles external cocoa pods sources. + */ +open class PodBuildTask : AdvancedCocoapodsTask() { + @get:InputDirectory + @get:Optional + internal lateinit var podsXcodeProjDirProvider: Provider + + @get:InputFile + @get:Optional + internal lateinit var buildSettingsFileProvider: Provider + + @Internal + lateinit var kotlinNativeTarget: KotlinNativeTarget + + @get:OutputFile + @get:Optional + internal val buildDirHashFileProvider: Provider = + project.provider { + project + .cocoapodsBuildDirs + .root + .resolve("buildDirHashSums") + .resolve(kotlinNativeTarget.toBuildDirHashSumFileName) + } + + override fun specificInvoke() { + val podBuildSettings = PodBuildSettingsProperties.readSettingsFromStream( + FileInputStream(buildSettingsFileProvider.get()) + ) + + val podsXcodeProjDir = podsXcodeProjDirProvider.get() + + cocoapodsExtension.pods.all { + + val podXcodeBuildCommand = listOf( + "xcodebuild", + "-project", podsXcodeProjDir!!.name, + "-scheme", it.moduleName, + "-sdk", kotlinNativeTarget.toValidSDK, + "-configuration", podBuildSettings.configuration + ) + + val podBuildProcess = ProcessBuilder(podXcodeBuildCommand) + .apply { + directory(podsXcodeProjDir!!.parentFile) + inheritIO() + }.start() + + val podBuildRetCode = podBuildProcess.waitFor() + check(podBuildRetCode == 0) { + "Unable to run '${podXcodeBuildCommand.joinToString(" ")}' return code $podBuildRetCode" + } + } + 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}") + } + } + } +} + +internal data class PodBuildSettingsProperties( + internal val buildDir: String, + internal val configuration: String, + internal val cflags: String? = null, + internal val headerPaths: String? = null, + internal val frameworkPaths: String? = null +) { + + fun writeSettings(buildSettingsFile: File) { + buildSettingsFile.parentFile.mkdirs() + buildSettingsFile.createNewFile() + + check(buildSettingsFile.exists()) { "Unable to create file ${buildSettingsFile.path}!" } + + with(Properties()) { + setProperty(BUILD_DIR, buildDir) + setProperty(CONFIGURATION, configuration) + cflags?.let { setProperty(OTHER_CFLAGS, it) } + headerPaths?.let { setProperty(HEADER_SEARCH_PATHS, it) } + frameworkPaths?.let { setProperty(FRAMEWORK_SEARCH_PATHS, it) } + buildSettingsFile.outputStream().use { + store(it, null) + } + } + } + + companion object { + const val BUILD_DIR: String = "BUILD_DIR" + const val CONFIGURATION: String = "CONFIGURATION" + const val OTHER_CFLAGS: String = "OTHER_CFLAGS" + const val HEADER_SEARCH_PATHS: String = "HEADER_SEARCH_PATHS" + const val FRAMEWORK_SEARCH_PATHS: String = "FRAMEWORK_SEARCH_PATHS" + + fun readSettingsFromStream(inputStream: InputStream): PodBuildSettingsProperties { + with(Properties()) { + load(inputStream) + return PodBuildSettingsProperties( + getProperty(BUILD_DIR), + getProperty(CONFIGURATION), + getProperty(OTHER_CFLAGS), + getProperty(HEADER_SEARCH_PATHS), + getProperty(FRAMEWORK_SEARCH_PATHS) + ) + } + } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/CocoapodsTasks.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/CocoapodsTasks.kt index 9c60400d498..afe42c8e3b8 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/CocoapodsTasks.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/CocoapodsTasks.kt @@ -28,20 +28,16 @@ open class PodspecTask : DefaultTask() { private val specName = project.name.asValidFrameworkName() - @OutputFile - val outputFile: File = project.projectDir.resolve("$specName.podspec") - - @Input - val frameworkNameProvider: Provider = project.provider { settings.frameworkName } + @get:OutputFile + internal val outputFileProvider: Provider = project.provider { project.file("$specName.podspec") } @get:Nested - internal lateinit var settings: CocoapodsExtension + internal lateinit var cocoapodsExtension: CocoapodsExtension - // TODO: Handle Framework name customization - rename the framework during sync process. @TaskAction fun generate() { - val frameworkDir = project.cocoapodsBuildDirs.framework.relativeTo(outputFile.parentFile).path - val dependencies = settings.pods.map { pod -> + 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 "" "| spec.dependency '${pod.name}'$versionSuffix" }.joinToString(separator = "\n") @@ -61,67 +57,70 @@ open class PodspecTask : DefaultTask() { val syncTask = "${project.path}:$SYNC_TASK_NAME" - outputFile.writeText( - """ - |Pod::Spec.new do |spec| - | spec.name = '$specName' - | spec.version = '${settings.version}' - | spec.homepage = '${settings.homepage.orEmpty()}' - | spec.source = { :git => "Not Published", :tag => "Cocoapods/#{spec.name}/#{spec.version}" } - | spec.authors = '${settings.authors.orEmpty()}' - | spec.license = '${settings.license.orEmpty()}' - | spec.summary = '${settings.summary.orEmpty()}' - | - | spec.static_framework = true - | spec.vendored_frameworks = "$frameworkDir/${frameworkNameProvider.get()}.framework" - | spec.libraries = "c++" - | spec.module_name = "#{spec.name}_umbrella" - | - $dependencies - | - | spec.pod_target_xcconfig = { - | 'KOTLIN_TARGET[sdk=iphonesimulator*]' => 'ios_x64', - | 'KOTLIN_TARGET[sdk=iphoneos*]' => '$KOTLIN_TARGET_FOR_IOS_DEVICE', - | 'KOTLIN_TARGET[sdk=watchsimulator*]' => 'watchos_x86', - | 'KOTLIN_TARGET[sdk=watchos*]' => '$KOTLIN_TARGET_FOR_WATCHOS_DEVICE', - | 'KOTLIN_TARGET[sdk=appletvsimulator*]' => 'tvos_x64', - | 'KOTLIN_TARGET[sdk=appletvos*]' => 'tvos_arm64', - | 'KOTLIN_TARGET[sdk=macosx*]' => 'macos_x64' - | } - | - | spec.script_phases = [ - | { - | :name => 'Build $specName', - | :execution_position => :before_compile, - | :shell_path => '/bin/sh', - | :script => <<-SCRIPT - | set -ev - | REPO_ROOT="${'$'}PODS_TARGET_SRCROOT" - | "$gradleCommand" -p "${'$'}REPO_ROOT" $syncTask \ - | -P${KotlinCocoapodsPlugin.TARGET_PROPERTY}=${'$'}KOTLIN_TARGET \ - | -P${KotlinCocoapodsPlugin.CONFIGURATION_PROPERTY}=${'$'}CONFIGURATION \ - | -P${KotlinCocoapodsPlugin.CFLAGS_PROPERTY}="${'$'}OTHER_CFLAGS" \ - | -P${KotlinCocoapodsPlugin.HEADER_PATHS_PROPERTY}="${'$'}HEADER_SEARCH_PATHS" \ - | -P${KotlinCocoapodsPlugin.FRAMEWORK_PATHS_PROPERTY}="${'$'}FRAMEWORK_SEARCH_PATHS" - | SCRIPT - | } - | ] - |end + with(outputFileProvider.get()) { + writeText( + """ + |Pod::Spec.new do |spec| + | spec.name = '$specName' + | spec.version = '${cocoapodsExtension.version}' + | spec.homepage = '${cocoapodsExtension.homepage.orEmpty()}' + | spec.source = { :git => "Not Published", :tag => "Cocoapods/#{spec.name}/#{spec.version}" } + | spec.authors = '${cocoapodsExtension.authors.orEmpty()}' + | spec.license = '${cocoapodsExtension.license.orEmpty()}' + | spec.summary = '${cocoapodsExtension.summary.orEmpty()}' + | + | spec.static_framework = true + | spec.vendored_frameworks = "$frameworkDir/${cocoapodsExtension.frameworkName}.framework" + | spec.libraries = "c++" + | spec.module_name = "#{spec.name}_umbrella" + | + $dependencies + | + | spec.pod_target_xcconfig = { + | 'KOTLIN_TARGET[sdk=iphonesimulator*]' => 'ios_x64', + | 'KOTLIN_TARGET[sdk=iphoneos*]' => '$KOTLIN_TARGET_FOR_IOS_DEVICE', + | 'KOTLIN_TARGET[sdk=watchsimulator*]' => 'watchos_x86', + | 'KOTLIN_TARGET[sdk=watchos*]' => '$KOTLIN_TARGET_FOR_WATCHOS_DEVICE', + | 'KOTLIN_TARGET[sdk=appletvsimulator*]' => 'tvos_x64', + | 'KOTLIN_TARGET[sdk=appletvos*]' => 'tvos_arm64', + | 'KOTLIN_TARGET[sdk=macosx*]' => 'macos_x64' + | } + | + | spec.script_phases = [ + | { + | :name => 'Build $specName', + | :execution_position => :before_compile, + | :shell_path => '/bin/sh', + | :script => <<-SCRIPT + | set -ev + | REPO_ROOT="${'$'}PODS_TARGET_SRCROOT/../" + | "$gradleCommand" -p "${'$'}REPO_ROOT" $syncTask \ + | -P${KotlinCocoapodsPlugin.TARGET_PROPERTY}=${'$'}KOTLIN_TARGET \ + | -P${KotlinCocoapodsPlugin.CONFIGURATION_PROPERTY}=${'$'}CONFIGURATION \ + | -P${KotlinCocoapodsPlugin.CFLAGS_PROPERTY}="${'$'}OTHER_CFLAGS" \ + | -P${KotlinCocoapodsPlugin.HEADER_PATHS_PROPERTY}="${'$'}HEADER_SEARCH_PATHS" \ + | -P${KotlinCocoapodsPlugin.FRAMEWORK_PATHS_PROPERTY}="${'$'}FRAMEWORK_SEARCH_PATHS" + | SCRIPT + | } + | ] + |end """.trimMargin() - ) + ) - logger.quiet( - """ - Generated a podspec file at: ${outputFile.absolutePath}. + logger.quiet( + """ + Generated a podspec file at: ${absolutePath}. To include it in your Xcode project, add the following dependency snippet in your Podfile: - pod '$specName', :path => '${outputFile.parentFile.absolutePath}' + pod '$specName', :path => '${parentFile.absolutePath}' """.trimIndent() - ) + ) + } } } + /** * Creates a dummy framework in the target directory. *