From cd9d74b029cf4f885865075ec53b0e1c27709de6 Mon Sep 17 00:00:00 2001 From: Viacheslav Kormushkin Date: Thu, 7 Oct 2021 08:14:57 +0000 Subject: [PATCH] Generate podspec for XCFramework suitable for publishing #KT-47812 #KT-47633 #KT-43815 #KT-42630 #KT-42105 #KT-48808 Merge-request: KT-MR-4483 --- .../kotlin/gradle/native/CocoaPodsIT.kt | 112 ++++++++++++++--- .../plugin/mpp/apple/XCFrameworkTask.kt | 58 +++++---- .../native/cocoapods/CocoapodsExtension.kt | 60 ++++----- .../native/cocoapods/KotlinCocoapodsPlugin.kt | 118 +++++++++++++++++- .../targets/native/tasks/CocoapodsTasks.kt | 114 +++++++++++------ .../targets/native/tasks/FatFrameworkTask.kt | 5 +- 6 files changed, 354 insertions(+), 113 deletions(-) diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsIT.kt index 0fd3276c7a6..fc5c9d4bbbb 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsIT.kt @@ -162,7 +162,7 @@ class CocoaPodsIT : BaseGradleIT() { fun testPodspecMultiple() = doTestPodspec( cocoapodsMultipleKtPods, mapOf("kotlin-library" to null, "second-library" to null), - mapOf("kotlin-library" to kotlinLibraryPodspecContent(), "second-library" to secondLibraryPodspecContent()), + mapOf("kotlin-library" to kotlinLibraryPodspecContent(), "second-library" to secondLibraryPodspecContent("second_library")), ) @Test @@ -324,7 +324,7 @@ class CocoaPodsIT : BaseGradleIT() { }.also { writeText(it) } } hooks.addHook { - assertContains("Cocoapods Integration requires version of this project to be specified.") + assertContains("Cocoapods Integration requires pod version to be specified.") } project.build(POD_IMPORT_TASK_NAME, "-Pkotlin.native.cocoapods.generate.wrapper=true") { @@ -863,8 +863,8 @@ class CocoaPodsIT : BaseGradleIT() { val framework = fileInWorkingDir("build/cocoapods/framework/cocoapods.framework/cocoapods") with(runProcess(listOf("file", framework.absolutePath), projectDir)) { assertTrue(isSuccessful) - assertTrue(output.contains("\\(for architecture armv7\\):\\s+current ar archive random librar".toRegex())) - assertTrue(output.contains("\\(for architecture arm64\\):\\s+current ar archive random library".toRegex())) + assertTrue(output.contains("\\(for architecture armv7\\):\\s+current ar archive".toRegex())) + assertTrue(output.contains("\\(for architecture arm64\\):\\s+current ar archive".toRegex())) } } @@ -925,6 +925,60 @@ class CocoaPodsIT : BaseGradleIT() { project.testWithWrapper(":compileIosMainKotlinMetadata") } + @Test + fun testPodPublishing() { + hooks.addHook { + assertTasksExecuted(":podPublishReleaseXCFramework") + assertTasksExecuted(":podPublishDebugXCFramework") + assertFileExists("build/cocoapods/publish/release/cocoapods.xcframework") + assertFileExists("build/cocoapods/publish/debug/cocoapods.xcframework") + assertFileExists("build/cocoapods/publish/release/cocoapods.podspec") + assertFileExists("build/cocoapods/publish/debug/cocoapods.podspec") + val actualPodspecContentWithoutBlankLines = fileInWorkingDir("build/cocoapods/publish/release/cocoapods.podspec").readText() + .lineSequence() + .filter { it.isNotBlank() } + .joinToString("\n") + + assertEquals(publishPodspecContent, actualPodspecContentWithoutBlankLines) + } + + project.testWithWrapper(":podPublishXCFramework") + } + + + @Test + fun testPodPublishingWithCustomProperties() { + + with(project.gradleBuildScript()) { + appendToCocoapodsBlock("name = \"CustomPod\"") + appendToCocoapodsBlock("version = \"2.0\"") + appendToCocoapodsBlock("publishDir = projectDir.resolve(\"CustomPublishDir\")") + appendToCocoapodsBlock("license = \"'MIT'\"") + appendToCocoapodsBlock("authors = \"{ 'Kotlin Dev' => 'kotlin.dev@jetbrains.com' }\"") + appendToCocoapodsBlock("extraSpecAttributes[\"social_media_url\"] = \"'https://twitter.com/kotlin'\"") + appendToCocoapodsBlock("extraSpecAttributes[\"vendored_frameworks\"] = \"'CustomFramework.xcframework'\"") + appendToCocoapodsBlock("extraSpecAttributes[\"libraries\"] = \"'xml'\"") + addPod(defaultPodName) + } + + hooks.addHook { + assertTasksExecuted(":podPublishReleaseXCFramework") + assertTasksExecuted(":podPublishDebugXCFramework") + assertFileExists("CustomPublishDir/release/cocoapods.xcframework") + assertFileExists("CustomPublishDir/debug/cocoapods.xcframework") + assertFileExists("CustomPublishDir/release/CustomPod.podspec") + assertFileExists("CustomPublishDir/debug/CustomPod.podspec") + val actualPodspecContentWithoutBlankLines = fileInWorkingDir("CustomPublishDir/release/CustomPod.podspec").readText() + .lineSequence() + .filter { it.isNotBlank() } + .joinToString("\n") + + assertEquals(publishPodspecCustomContent, actualPodspecContentWithoutBlankLines) + } + + project.testWithWrapper(":podPublishXCFramework") + } + // paths private fun CompiledProject.url() = externalSources().resolve("url") @@ -1411,18 +1465,17 @@ class CocoaPodsIT : BaseGradleIT() { 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.source = { :http=> ''} spec.authors = '' spec.license = '' spec.summary = 'CocoaPods test library' - spec.vendored_frameworks = "build/cocoapods/framework/${frameworkName ?: "kotlin_library"}.framework" - spec.libraries = "c++" - spec.module_name = "#{spec.name}_umbrella" + spec.vendored_frameworks = 'build/cocoapods/framework/${frameworkName ?: "kotlin_library"}.framework' + spec.libraries = 'c++' spec.dependency 'pod_dependency', '1.0' spec.dependency 'subspec_dependency/Core', '1.0' spec.pod_target_xcconfig = { 'KOTLIN_PROJECT_PATH' => ':kotlin-library', - 'PRODUCT_MODULE_NAME' => 'kotlin_library', + 'PRODUCT_MODULE_NAME' => '${frameworkName ?: "kotlin_library"}', } spec.script_phases = [ { @@ -1451,16 +1504,15 @@ class CocoaPodsIT : BaseGradleIT() { 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.source = { :http=> ''} spec.authors = '' spec.license = '' spec.summary = 'CocoaPods test library' - spec.vendored_frameworks = "build/cocoapods/framework/${frameworkName ?: "second_library"}.framework" - spec.libraries = "c++" - spec.module_name = "#{spec.name}_umbrella" + spec.vendored_frameworks = 'build/cocoapods/framework/${frameworkName ?: "second_library"}.framework' + spec.libraries = 'c++' spec.pod_target_xcconfig = { 'KOTLIN_PROJECT_PATH' => ':second-library', - 'PRODUCT_MODULE_NAME' => 'second_library', + 'PRODUCT_MODULE_NAME' => '${frameworkName ?: "kotlin_library"}', } spec.script_phases = [ { @@ -1484,6 +1536,38 @@ class CocoaPodsIT : BaseGradleIT() { end """.trimIndent() + private val publishPodspecContent = """ + Pod::Spec.new do |spec| + spec.name = 'cocoapods' + spec.version = '1.0' + spec.homepage = 'https://github.com/JetBrains/kotlin' + spec.source = { :http=> ''} + spec.authors = '' + spec.license = '' + spec.summary = 'CocoaPods test library' + spec.vendored_frameworks = 'cocoapods.xcframework' + spec.libraries = 'c++' + spec.ios.deployment_target = '13.5' + end + """.trimIndent() + + private val publishPodspecCustomContent = """ + Pod::Spec.new do |spec| + spec.name = 'CustomPod' + spec.version = '2.0' + spec.homepage = 'https://github.com/JetBrains/kotlin' + spec.source = { :http=> ''} + spec.authors = { 'Kotlin Dev' => 'kotlin.dev@jetbrains.com' } + spec.license = 'MIT' + spec.summary = 'CocoaPods test library' + spec.ios.deployment_target = '13.5' + spec.dependency 'AFNetworking' + spec.social_media_url = 'https://twitter.com/kotlin' + spec.vendored_frameworks = 'CustomFramework.xcframework' + spec.libraries = 'xml' + end + """.trimIndent() + companion object { @BeforeClass @JvmStatic diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/XCFrameworkTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/XCFrameworkTask.kt index eb074bcbf50..efdd1566311 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/XCFrameworkTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/XCFrameworkTask.kt @@ -9,6 +9,7 @@ import org.gradle.api.DefaultTask import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.file.FileTree +import org.gradle.api.provider.Provider import org.gradle.api.tasks.* import org.jetbrains.kotlin.gradle.plugin.cocoapods.asValidFrameworkName import org.jetbrains.kotlin.gradle.plugin.mpp.Framework @@ -110,7 +111,7 @@ private fun Project.registerAssembleXCFrameworkTask( "XCFramework" ) return registerTask(taskName) { task -> - task.baseName = xcFrameworkName + task.baseName = provider { xcFrameworkName } task.buildType = buildType } } @@ -130,7 +131,7 @@ private fun Project.registerAssembleFatForXCFrameworkTask( ) return registerTask(taskName) { task -> - task.destinationDir = fatFrameworkDir(xcFrameworkName, buildType).resolve(appleTarget.targetName) + task.destinationDir = XCFrameworkTask.fatFrameworkDir(project, xcFrameworkName, buildType, appleTarget) task.baseName = xcFrameworkName.asValidFrameworkName() task.onlyIf { task.frameworks.size > 1 @@ -138,23 +139,16 @@ private fun Project.registerAssembleFatForXCFrameworkTask( } } -private fun Project.fatFrameworkDir( - xcFrameworkName: String, - buildType: NativeBuildType -) = buildDir - .resolve("fat-framework") - .resolve(buildType.getName()) - .resolve(xcFrameworkName) - abstract class XCFrameworkTask : DefaultTask() { /** * A base name for the XCFramework. */ @Input - var baseName: String = project.name + var baseName: Provider = project.provider { project.name } - private val xcFrameworkName: String - get() = baseName.asValidFrameworkName() + @get:Internal + internal val xcFrameworkName: Provider + get() = baseName.map { it.asValidFrameworkName() } /** * A build type of the XCFramework. @@ -189,11 +183,11 @@ abstract class XCFrameworkTask : DefaultTask() { */ @get:Internal // We take it into account as an input in the buildType and baseName properties. protected val fatFrameworksDir: File - get() = project.fatFrameworkDir(xcFrameworkName, buildType) + get() = fatFrameworkDir(project, xcFrameworkName.get(), buildType) @get:OutputDirectory protected val outputXCFrameworkFile: File - get() = outputDir.resolve(buildType.getName()).resolve("$xcFrameworkName.xcframework") + get() = outputDir.resolve(buildType.getName()).resolve("${xcFrameworkName.get()}.xcframework") /** * Adds the specified frameworks in this XCFramework. @@ -214,10 +208,13 @@ abstract class XCFrameworkTask : DefaultTask() { val group = frameworks.filter { it.konanTarget in appleTarget.targets } when { group.size == 1 -> { - group.first().outputFile + XCFrameworkFile(group.first().outputFile, group.first().isStatic) } group.size > 1 -> { - fatFrameworksDir.resolve(appleTarget.targetName).resolve("$xcFrameworkName.framework") + XCFrameworkFile( + fatFrameworksDir.resolve(appleTarget.targetName).resolve("${xcFrameworkName.get()}.framework"), + group.all { it.isStatic } + ) } else -> null } @@ -225,20 +222,37 @@ abstract class XCFrameworkTask : DefaultTask() { createXCFramework(frameworksForXCFramework, outputXCFrameworkFile, buildType) } - private fun createXCFramework(frameworks: List, output: File, buildType: NativeBuildType) { + private data class XCFrameworkFile (val file: File, val isStatic: Boolean) + + private fun createXCFramework(frameworkFiles: List, output: File, buildType: NativeBuildType) { if (output.exists()) output.deleteRecursively() val cmdArgs = mutableListOf("xcodebuild", "-create-xcframework") - frameworks.forEach { framework -> + frameworkFiles.forEach { frameworkFile -> cmdArgs.add("-framework") - cmdArgs.add(framework.path) - if (buildType == NativeBuildType.DEBUG) { + cmdArgs.add(frameworkFile.file.path) + if (buildType == NativeBuildType.DEBUG && frameworkFile.isStatic.not()) { cmdArgs.add("-debug-symbols") - cmdArgs.add(framework.path + ".dSYM") + cmdArgs.add(frameworkFile.file.path + ".dSYM") } } cmdArgs.add("-output") cmdArgs.add(output.path) project.exec { it.commandLine(cmdArgs) } } + + internal companion object { + fun fatFrameworkDir( + project: Project, + xcFrameworkName: String, + buildType: NativeBuildType, + appleTarget: AppleTarget? = null + ) = project.buildDir + .resolve("fat-framework") + .resolve(buildType.getName()) + .resolve(xcFrameworkName) + .resolveIfNotNull(appleTarget?.targetName) + + private fun File.resolveIfNotNull(relative: String?): File = if (relative == null) this else this.resolve(relative) + } } \ No newline at end of file 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 256d5279147..1e2412b325a 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 @@ -11,6 +11,7 @@ import org.gradle.api.Action import org.gradle.api.Named import org.gradle.api.NamedDomainObjectSet import org.gradle.api.Project +import org.gradle.api.plugins.ExtensionAware import org.gradle.api.tasks.* import org.gradle.util.ConfigureUtil import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension @@ -18,38 +19,26 @@ import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.Cocoapods import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType import org.jetbrains.kotlin.gradle.plugin.mpp.Framework import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBinary -import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework import java.io.File import java.net.URI open class CocoapodsExtension(private val project: Project) { - @get:Input - val version: String - get() { - require(project.version != Project.DEFAULT_VERSION) { """ - Cocoapods Integration requires version of this project to be specified. - Please, add line 'version = ""' to project's build file. - For more details, please, see https://guides.cocoapods.org/syntax/podspec.html#version - """.trimIndent() - } - return project.version.toString() - } + /** + * Configure version of the pod + */ + var version: String? = null /** * Configure authors of the pod built from this project. */ - @Optional - @Input var authors: String? = null /** * Configure existing file `Podfile`. */ - @Optional - @InputFile var podfile: File? = null - @get:Input internal var needPodspec: Boolean = true /** @@ -66,30 +55,38 @@ open class CocoapodsExtension(private val project: Project) { useLibraries = true } - @get:Input internal var useLibraries: Boolean = false + /** + * Configure name of the pod built from this project. + */ + var name: String = project.name.asValidFrameworkName() + /** * Configure license of the pod built from this project. */ - @Optional - @Input var license: String? = null /** * Configure description of the pod built from this project. */ - @Optional - @Input var summary: String? = null /** * Configure homepage of the pod built from this project. */ - @Optional - @Input var homepage: String? = null + /** + * Configure location of the pod built from this project. + */ + var source: String? = null + + /** + * Configure other podspec attributes + */ + var extraSpecAttributes: MutableMap = mutableMapOf() + /** * Configure framework of the pod built from this project. */ @@ -102,16 +99,12 @@ open class CocoapodsExtension(private val project: Project) { configure.execute(this) } - @Nested val ios: PodspecPlatformSettings = PodspecPlatformSettings("ios") - @Nested val osx: PodspecPlatformSettings = PodspecPlatformSettings("osx") - @Nested val tvos: PodspecPlatformSettings = PodspecPlatformSettings("tvos") - @Nested val watchos: PodspecPlatformSettings = PodspecPlatformSettings("watchos") /** @@ -133,27 +126,26 @@ open class CocoapodsExtension(private val project: Project) { /** * Configure custom Xcode Configurations to Native Build Types mapping */ - @Input val xcodeConfigurationToNativeBuildType: MutableMap = mutableMapOf( "Debug" to NativeBuildType.DEBUG, "Release" to NativeBuildType.RELEASE ) - @get:Nested + /** + * Configure output directory for pod publishing + */ + var publishDir: File = CocoapodsBuildDirs(project).publish + internal val specRepos = SpecRepos() private val _pods = project.container(CocoapodsDependency::class.java) - // For some reason Gradle doesn't consume the @Nested annotation on NamedDomainObjectContainer. - @get:Nested val podsAsTaskInput: List get() = _pods.toList() /** * Returns a list of pod dependencies. */ - // Already taken into account as a task input in the [podsAsTaskInput] property. - @get:Internal val pods: NamedDomainObjectSet get() = _pods 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 510332f3507..489cf3767e3 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 @@ -6,6 +6,7 @@ @file:Suppress("PackageDirectoryMismatch") // Old package for compatibility package org.jetbrains.kotlin.gradle.plugin.cocoapods +import org.gradle.api.DefaultTask import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.provider.Provider @@ -18,10 +19,13 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation import org.jetbrains.kotlin.gradle.plugin.addExtension import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.* +import org.jetbrains.kotlin.gradle.plugin.mpp.Framework import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable import org.jetbrains.kotlin.gradle.plugin.mpp.apple.AppleSdk +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.AppleTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFrameworkTask import org.jetbrains.kotlin.gradle.plugin.whenEvaluated import org.jetbrains.kotlin.gradle.targets.native.tasks.* import org.jetbrains.kotlin.gradle.tasks.* @@ -57,6 +61,8 @@ internal class CocoapodsBuildDirs(val project: Project) { val externalSources: File get() = root.resolve("externalSources") + val publish: File = root.resolve("publish") + fun externalSources(fileName: String) = externalSources.resolve(fileName) fun fatFramework(buildType: NativeBuildType) = @@ -359,8 +365,12 @@ open class KotlinCocoapodsPlugin : Plugin { it.group = TASK_GROUP it.description = "Generates a podspec file for CocoaPods import" it.needPodspec = project.provider { cocoapodsExtension.needPodspec } + it.publishing.set(false) it.pods.set(cocoapodsExtension.pods) - it.version = project.provider { cocoapodsExtension.version } + it.version.set(cocoapodsExtension.version ?: project.version.toString()) + it.specName.set(cocoapodsExtension.name) + it.extraSpecAttributes.set(cocoapodsExtension.extraSpecAttributes) + it.outputDir.set(project.projectDir) it.homepage.set(cocoapodsExtension.homepage) it.license.set(cocoapodsExtension.license) it.authors.set(cocoapodsExtension.authors) @@ -435,7 +445,7 @@ open class KotlinCocoapodsPlugin : Plugin { project.tasks.register(family.toPodGenTaskName, PodGenTask::class.java) { it.description = "Сreates a synthetic Xcode project to retrieve CocoaPods dependencies" - it.podspec = podspecTaskProvider.map { task -> task.outputFileProvider.get() } + it.podspec = podspecTaskProvider.map { task -> task.outputFile } it.useLibraries = project.provider { cocoapodsExtension.useLibraries } it.specRepos = project.provider { cocoapodsExtension.specRepos } it.family = family @@ -550,6 +560,109 @@ open class KotlinCocoapodsPlugin : Plugin { } } + private fun registerPodXCFrameworkTask( + project: Project, + cocoapodsExtension: CocoapodsExtension, + buildType: NativeBuildType + ): TaskProvider = + with(project) { + registerTask("podPublish${buildType.name.toLowerCase().capitalize()}XCFramework") { task -> + multiplatformExtension.supportedTargets().all { target -> + target.binaries.matching { it.buildType == buildType }.withType(Framework::class.java) { framework -> + task.from(framework) + } + } + task.outputDir = cocoapodsExtension.publishDir + task.buildType = buildType + task.baseName = project.provider { cocoapodsExtension.frameworkNameInternal } + task.description = "Produces ${buildType.name.toLowerCase().capitalize()} XCFramework for all requested targets" + task.group = TASK_GROUP + } + } + + private fun registerPodspecPublishTask( + project: Project, + cocoapodsExtension: CocoapodsExtension, + xcFrameworkTask: TaskProvider, + buildType: NativeBuildType + ): TaskProvider = + with(project) { + val task = tasks.register("podSpec${buildType.name.toLowerCase().capitalize()}", PodspecTask::class.java) { task -> + task.description = "Generates podspec for ${buildType.name.toLowerCase().capitalize()} XCFramework publishing" + task.outputDir.set(xcFrameworkTask.map { it.outputDir.resolve(it.buildType.getName()) }) + task.needPodspec = provider { true } + task.publishing.set(true) + task.pods.set(cocoapodsExtension.pods) + task.specName.set(cocoapodsExtension.name) + task.version.set(cocoapodsExtension.version ?: version.toString()) + task.extraSpecAttributes.set(cocoapodsExtension.extraSpecAttributes) + task.homepage.set(cocoapodsExtension.homepage) + task.license.set(cocoapodsExtension.license) + task.authors.set(cocoapodsExtension.authors) + task.summary.set(cocoapodsExtension.summary) + task.source.set(cocoapodsExtension.source) + task.frameworkName = provider { cocoapodsExtension.frameworkNameInternal } + task.ios = provider { cocoapodsExtension.ios } + task.osx = provider { cocoapodsExtension.osx } + task.tvos = provider { cocoapodsExtension.tvos } + task.watchos = provider { cocoapodsExtension.watchos } + val generateWrapper = project.findProperty(GENERATE_WRAPPER_PROPERTY)?.toString()?.toBoolean() ?: false + if (generateWrapper) { + task.dependsOn(":wrapper") + } + } + xcFrameworkTask.dependsOn(task) + return task + } + + private fun registerPodPublishFatFrameworkTasks( + project: Project, + xcFrameworkTask: TaskProvider, + buildType: NativeBuildType + ) = + with(project) { + multiplatformExtension.supportedTargets().all { target -> + target.binaries.matching { it.buildType == buildType }.withType(Framework::class.java) { framework -> + + val appleTarget = AppleTarget.values().firstOrNull { it.targets.contains(target.konanTarget) } ?: return@withType + val fatFrameworkTaskName = + "pod${buildType.name.toLowerCase().capitalize()}${appleTarget.targetName.capitalize()}FatFramework" + val fatFrameworkTask = if (fatFrameworkTaskName in tasks.names) { + tasks.named(fatFrameworkTaskName, FatFrameworkTask::class.java) + } else { + tasks.register(fatFrameworkTaskName, FatFrameworkTask::class.java) { fatTask -> + fatTask.baseName = framework.baseName + fatTask.destinationDir = XCFrameworkTask.fatFrameworkDir(this, fatTask.fatFrameworkName, buildType, appleTarget) + fatTask.onlyIf { + fatTask.frameworks.size > 1 + } + } + } + + fatFrameworkTask.configure { + it.from(framework) + } + + xcFrameworkTask.dependsOn(fatFrameworkTask) + } + } + } + + private fun registerPodPublishTasks(project: Project, cocoapodsExtension: CocoapodsExtension) { + + val xcFrameworkTasks = NativeBuildType.values().map { buildType -> + val xcFrameworkTask = registerPodXCFrameworkTask(project, cocoapodsExtension, buildType) + registerPodPublishFatFrameworkTasks(project, xcFrameworkTask, buildType) + registerPodspecPublishTask(project, cocoapodsExtension, xcFrameworkTask, buildType) + xcFrameworkTask + } + + project.registerTask("podPublishXCFramework", DefaultTask::class.java) { task -> + task.description = "Produces Release and Debug XCFrameworks with respective podspecs" + task.dependsOn(xcFrameworkTasks) + task.group = TASK_GROUP + } + } override fun apply(project: Project): Unit = with(project) { @@ -568,6 +681,7 @@ open class KotlinCocoapodsPlugin : Plugin { registerPodSetupBuildTasks(project, kotlinExtension, cocoapodsExtension) registerPodBuildTasks(project, kotlinExtension, cocoapodsExtension) registerPodImportTask(project, kotlinExtension) + registerPodPublishTasks(project, cocoapodsExtension) if (HostManager.hostIsMac && !isAvailableToProduceSynthetic) { logger.quiet( 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 5226cb08ebf..2537f146358 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 @@ -20,7 +20,6 @@ import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.COCOAPODS_EXTENSION_NAME import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.GENERATE_WRAPPER_PROPERTY import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.SYNC_TASK_NAME -import org.jetbrains.kotlin.gradle.plugin.cocoapods.asValidFrameworkName import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs import java.io.File @@ -31,11 +30,14 @@ import java.io.File open class PodspecTask : DefaultTask() { @get:Input - internal val specName = project.name.asValidFrameworkName() + internal val specName = project.objects.property(String::class.java) + + @get:Internal + internal val outputDir = project.objects.property(File::class.java) @get:OutputFile - internal val outputFileProvider: Provider - get() = project.provider { project.file("$specName.podspec") } + val outputFile: File + get() = outputDir.get().resolve("${specName.get()}.podspec") @get:Input internal lateinit var needPodspec: Provider @@ -44,7 +46,14 @@ open class PodspecTask : DefaultTask() { val pods = project.objects.listProperty(CocoapodsDependency::class.java) @get:Input - internal lateinit var version: Provider + internal val version = project.objects.property(String::class.java) + + @get:Input + internal val publishing = project.objects.property(Boolean::class.java) + + @get:Input + @get:Optional + internal val source = project.objects.property(String::class.java) @get:Input @get:Optional @@ -62,6 +71,10 @@ open class PodspecTask : DefaultTask() { @get:Optional internal val summary = project.objects.property(String::class.java) + @get:Input + @get:Optional + internal val extraSpecAttributes = project.objects.mapProperty(String::class.java, String::class.java) + @get:Input internal lateinit var frameworkName: Provider @@ -84,11 +97,14 @@ open class PodspecTask : DefaultTask() { @TaskAction fun generate() { - val frameworkDir = project.cocoapodsBuildDirs.framework.relativeTo(outputFileProvider.get().parentFile).path - val dependencies = pods.get().map { pod -> - val versionSuffix = if (pod.version != null) ", '${pod.version}'" else "" - "| spec.dependency '${pod.name}'$versionSuffix" - }.joinToString(separator = "\n") + check(version.get() != Project.DEFAULT_VERSION) { + """ + Cocoapods Integration requires pod version to be specified. + Please specify pod version by adding 'version = ""' to the cocoapods block. + Alternatively, specify the version for the entire project explicitly. + Pod version format has to conform podspec syntax requirements: https://guides.cocoapods.org/syntax/podspec.html#version + """.trimIndent() + } val gradleWrapper = (project.rootProject.tasks.getByName("wrapper") as? Wrapper)?.scriptFile require(gradleWrapper != null && gradleWrapper.exists()) { @@ -101,42 +117,37 @@ open class PodspecTask : DefaultTask() { """.trimIndent() } - val gradleCommand = "\$REPO_ROOT/${gradleWrapper.toRelativeString(project.projectDir)}" - val deploymentTargets = run { listOf(ios, osx, tvos, watchos).map { it.get() }.filter { it.deploymentTarget != null }.joinToString("\n") { - "| spec.${it.name}.deployment_target = '${it.deploymentTarget}'" + if (extraSpecAttributes.get().containsKey("${it.name}.deployment_target")) "" else "| spec.${it.name}.deployment_target = '${it.deploymentTarget}'" } } - with(outputFileProvider.get()) { - writeText( - """ - |Pod::Spec.new do |spec| - | spec.name = '$specName' - | spec.version = '${version.get()}' - | spec.homepage = '${homepage.getOrEmpty()}' - | spec.source = { :git => "Not Published", :tag => "Cocoapods/#{spec.name}/#{spec.version}" } - | spec.authors = '${authors.getOrEmpty()}' - | spec.license = '${license.getOrEmpty()}' - | spec.summary = '${summary.getOrEmpty()}' - | - | spec.vendored_frameworks = "$frameworkDir/${frameworkName.get()}.framework" - | spec.libraries = "c++" - | spec.module_name = "#{spec.name}_umbrella" - | - $deploymentTargets - | - $dependencies - | + val dependencies = pods.get().map { pod -> + val versionSuffix = if (pod.version != null) ", '${pod.version}'" else "" + "| spec.dependency '${pod.name}'$versionSuffix" + }.joinToString(separator = "\n") + + val frameworkDir = project.cocoapodsBuildDirs.framework.relativeTo(outputFile.parentFile).path + val vendoredFramework = if (publishing.get()) "${frameworkName.get()}.xcframework" else "$frameworkDir/${frameworkName.get()}.framework" + val vendoredFrameworks = if (extraSpecAttributes.get().containsKey("vendored_frameworks")) "" else "| spec.vendored_frameworks = '$vendoredFramework'" + + val libraries = if (extraSpecAttributes.get().containsKey("libraries")) "" else "| spec.libraries = 'c++'" + + val xcConfig = if (publishing.get() || extraSpecAttributes.get().containsKey("pod_target_xcconfig")) "" else + """ | | spec.pod_target_xcconfig = { | 'KOTLIN_PROJECT_PATH' => '${project.path}', - | 'PRODUCT_MODULE_NAME' => '$specName', + | 'PRODUCT_MODULE_NAME' => '${frameworkName.get()}', | } - | + """.trimMargin() + + val gradleCommand = "\$REPO_ROOT/${gradleWrapper.toRelativeString(project.projectDir)}" + val scriptPhase = if (publishing.get() || extraSpecAttributes.get().containsKey("script_phases")) "" else + """ | | spec.script_phases = [ | { - | :name => 'Build $specName', + | :name => 'Build ${specName.get()}', | :execution_position => :before_compile, | :shell_path => '/bin/sh', | :script => <<-SCRIPT @@ -153,17 +164,39 @@ open class PodspecTask : DefaultTask() { | SCRIPT | } | ] + """.trimMargin() + + val customSpec = extraSpecAttributes.get().map { "| spec.${it.key} = ${it.value}" }.joinToString("\n") + + with(outputFile) { + writeText( + """ + |Pod::Spec.new do |spec| + | spec.name = '${specName.get()}' + | spec.version = '${version.get()}' + | spec.homepage = ${homepage.getOrEmpty().surroundWithSingleQuotesIfNeeded()} + | spec.source = ${source.getOrElse("{ :http=> ''}")} + | spec.authors = ${authors.getOrEmpty().surroundWithSingleQuotesIfNeeded()} + | spec.license = ${license.getOrEmpty().surroundWithSingleQuotesIfNeeded()} + | spec.summary = '${summary.getOrEmpty()}' + $vendoredFrameworks + $libraries + $deploymentTargets + $dependencies + $xcConfig + $scriptPhase + $customSpec |end """.trimMargin() ) - if (hasPodfileOwnOrParent(project)) { + if (hasPodfileOwnOrParent(project) && publishing.get().not()) { logger.quiet( """ Generated a podspec file at: ${absolutePath}. To include it in your Xcode project, check that the following dependency snippet exists in your Podfile: - pod '$specName', :path => '${parentFile.absolutePath}' + pod '${specName.get()}', :path => '${parentFile.absolutePath}' """.trimIndent() ) @@ -172,7 +205,10 @@ open class PodspecTask : DefaultTask() { } } - fun Provider.getOrEmpty() = getOrElse("") + private fun Provider.getOrEmpty(): String = getOrElse("") + + private fun String.surroundWithSingleQuotesIfNeeded(): String = + if (startsWith("{") || startsWith("<<-") || startsWith("'")) this else "'$this'" companion object { private val KotlinMultiplatformExtension?.cocoapodsExtensionOrNull: CocoapodsExtension? diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/FatFrameworkTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/FatFrameworkTask.kt index c8a54f2e384..0e86d99f375 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/FatFrameworkTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/FatFrameworkTask.kt @@ -82,7 +82,7 @@ open class FatFrameworkTask : DefaultTask() { var baseName: String = project.name /** - * A parent directory for the fat framework.. + * A parent directory for the fat framework. */ @OutputDirectory var destinationDir: File = project.buildDir.resolve("fat-framework") @@ -98,7 +98,8 @@ open class FatFrameworkTask : DefaultTask() { private val archToFramework: MutableMap = mutableMapOf() - private val fatFrameworkName: String + @get:Internal + val fatFrameworkName: String get() = baseName.asValidFrameworkName() private val fatFramework: IosFrameworkFiles