diff --git a/.idea/dictionaries/Artem_Daugel_Dauge.xml b/.idea/dictionaries/Artem_Daugel_Dauge.xml new file mode 100644 index 00000000000..0c7f9c49110 --- /dev/null +++ b/.idea/dictionaries/Artem_Daugel_Dauge.xml @@ -0,0 +1,8 @@ + + + + vendored + xcframework + + + \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-api/api/kotlin-gradle-plugin-api.api b/libraries/tools/kotlin-gradle-plugin-api/api/kotlin-gradle-plugin-api.api index 910fb30b06f..7cf00b3bb4b 100644 --- a/libraries/tools/kotlin-gradle-plugin-api/api/kotlin-gradle-plugin-api.api +++ b/libraries/tools/kotlin-gradle-plugin-api/api/kotlin-gradle-plugin-api.api @@ -143,6 +143,7 @@ public abstract interface class org/jetbrains/kotlin/gradle/dsl/KaptJavacOption public abstract interface class org/jetbrains/kotlin/gradle/dsl/KotlinArtifact : org/gradle/api/Named, org/gradle/api/plugins/ExtensionAware { public abstract fun getArtifactName ()Ljava/lang/String; public abstract fun getModules ()Ljava/util/Set; + public abstract fun getOutDir ()Ljava/lang/String; public abstract fun getTaskName ()Ljava/lang/String; public abstract fun registerAssembleTask (Lorg/gradle/api/Project;)V } diff --git a/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinArtifact.kt b/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinArtifact.kt index 2afa9201807..e8ee60fa203 100644 --- a/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinArtifact.kt +++ b/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinArtifact.kt @@ -15,6 +15,7 @@ interface KotlinArtifact : Named, ExtensionAware { val artifactName: String val modules: Set val taskName: String + val outDir: String fun registerAssembleTask(project: Project) } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/NativeLibraryDslWithCocoapodsIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/NativeLibraryDslWithCocoapodsIT.kt new file mode 100644 index 00000000000..840c51ecb62 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/NativeLibraryDslWithCocoapodsIT.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2010-2022 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.native + +import org.jetbrains.kotlin.gradle.BaseGradleIT +import org.jetbrains.kotlin.gradle.GradleVersionRequired +import org.jetbrains.kotlin.gradle.transformProjectWithPluginsDsl +import org.jetbrains.kotlin.konan.target.HostManager +import org.junit.Assume +import org.junit.BeforeClass +import kotlin.test.Test +import kotlin.test.assertEquals + +class NativeLibraryDslWithCocoapodsIT : BaseGradleIT() { + + companion object { + @BeforeClass + @JvmStatic + fun assumeMacHost() { + Assume.assumeTrue(HostManager.hostIsMac) + } + } + + override val defaultGradleVersion = GradleVersionRequired.FOR_MPP_SUPPORT + + @Test + fun `check registered gradle tasks`() { + project { + build(":shared:tasks") { + assertSuccessful() + assertTasksRegistered( + ":shared:generateMylibPodspec", + ":shared:generateMyslibPodspec", + ":shared:generateMyframePodspec", + ":shared:generateMyfatframePodspec", + ":shared:generateSharedPodspec", + ":lib:generateGrooframePodspec", + ) + assertTasksNotRegistered( + ":shared:generateMyfatframewithoutpodspecPodspec", + ) + } + } + } + + @Test + fun `generate podspec when assembling lib`() { + project { + build(":shared:assembleMylibSharedLibraryLinuxX64") { + assertSuccessful() + assertTasksExecuted(":shared:generateMylibPodspec") + assertFilesContentEqual("podspecs/mylib.podspec", "/shared/build/out/dynamic/mylib.podspec") + } + } + } + + @Test + fun `generate podspec when assembling shared lib`() { + project { + build(":shared:assembleMyslibSharedLibraryLinuxX64") { + assertSuccessful() + assertTasksExecuted(":shared:generateMyslibPodspec") + assertFilesContentEqual("podspecs/myslib.podspec", "/shared/build/out/dynamic/myslib.podspec") + } + } + } + + @Test + fun `generate podspec when assembling framework`() { + project { + build(":shared:assembleMyframeFrameworkIosArm64") { + assertSuccessful() + assertTasksExecuted(":shared:generateMyframePodspec") + assertFilesContentEqual("podspecs/myframe.podspec", "/shared/build/out/framework/myframe.podspec") + } + } + } + + @Test + fun `generate podspec when assembling fat framework`() { + project { + build(":shared:assembleMyfatframeFatFramework") { + assertSuccessful() + assertTasksExecuted(":shared:generateMyfatframePodspec") + assertFilesContentEqual("podspecs/myfatframe.podspec", "/shared/build/out/fatframework/myfatframe.podspec") + } + } + } + + @Test + fun `generate podspec when assembling xcframework`() { + project { + build(":shared:assembleSharedXCFramework") { + assertSuccessful() + assertTasksExecuted(":shared:generateSharedPodspec") + assertFilesContentEqual("podspecs/shared.podspec", "/shared/build/out/xcframework/shared.podspec") + } + } + } + + @Test + fun `generate podspec when assembling framework from groovy`() { + project { + build(":lib:assembleGrooframeFrameworkIosArm64") { + assertSuccessful() + assertTasksExecuted(":lib:generateGrooframePodspec") + assertFilesContentEqual("podspecs/grooframe.podspec", "/lib/build/out/framework/grooframe.podspec") + } + } + } + + private fun project(block: Project.() -> Unit) { + transformProjectWithPluginsDsl("new-kn-library-dsl-cocoapods").block() + } + + private fun CompiledProject.assertFilesContentEqual(expected: String, actual: String) { + assertFileExists(expected) + assertFileExists(actual) + assertEquals(fileInWorkingDir(expected).readText(), fileInWorkingDir(actual).readText()) + } + +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/build.gradle.kts new file mode 100644 index 00000000000..f556befce79 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + kotlin("multiplatform").apply(false) +} + +allprojects { + repositories { + mavenLocal() + mavenCentral() + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/lib/build.gradle b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/lib/build.gradle new file mode 100644 index 00000000000..54fdd39c2bb --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/lib/build.gradle @@ -0,0 +1,31 @@ +plugins { + id 'org.jetbrains.kotlin.multiplatform' + id 'org.jetbrains.kotlin.native.cocoapods' +} + +version '0.2' + +kotlin { + linuxX64() + iosX64() + iosArm64() + iosSimulatorArm64() +} + +kotlinArtifacts { + it.native.Framework("grooframe") { + modes(DEBUG, RELEASE) + target = iosArm64 + addModule(project(":shared")) + + withPodspec { + attribute('license', 'Apache-2.0') + attribute('homepage', 'https://example.com/lib') + attribute('ios.deployment_target', '"10.0"') + attribute('swift_versions', '[\'4\', \'5\']') + + rawStatement(' # This is raw statement that is appended \'as is\' to the podspec') + rawStatement(' spec.frameworks = \'CFNetwork\'') + } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/lib/src/commonMain/kotlin/org/my/sam/Platform.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/lib/src/commonMain/kotlin/org/my/sam/Platform.kt new file mode 100644 index 00000000000..b43f95488da --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/lib/src/commonMain/kotlin/org/my/sam/Platform.kt @@ -0,0 +1,5 @@ +package org.my.sam + +class Greeting { + fun greeting() = "Hello, lib!" +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/grooframe.podspec b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/grooframe.podspec new file mode 100644 index 00000000000..787505d3129 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/grooframe.podspec @@ -0,0 +1,13 @@ +Pod::Spec.new do |spec| + spec.name = 'grooframe' + spec.version = '0.2' + spec.vendored_frameworks = 'grooframe.framework' + + spec.license = 'Apache-2.0' + spec.homepage = 'https://example.com/lib' + spec.ios.deployment_target = "10.0" + spec.swift_versions = ['4', '5'] + + # This is raw statement that is appended 'as is' to the podspec + spec.frameworks = 'CFNetwork' +end diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/myfatframe.podspec b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/myfatframe.podspec new file mode 100644 index 00000000000..678d09b25ab --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/myfatframe.podspec @@ -0,0 +1,6 @@ +Pod::Spec.new do |spec| + spec.version = '0.1' + spec.vendored_frameworks = 'myfatframe.framework' + + spec.name = 'custom-podspec-name' +end diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/myframe.podspec b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/myframe.podspec new file mode 100644 index 00000000000..aff1b60587d --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/myframe.podspec @@ -0,0 +1,7 @@ +Pod::Spec.new do |spec| + spec.name = 'myframe' + spec.version = '0.1' + spec.vendored_frameworks = 'myframe.framework' + + spec.prefix_header_file = false +end diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/mylib.podspec b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/mylib.podspec new file mode 100644 index 00000000000..214577f4c93 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/mylib.podspec @@ -0,0 +1,16 @@ +Pod::Spec.new do |spec| + spec.name = 'mylib' + spec.version = '0.1' + spec.vendored_library = 'mylib.dylib' + + spec.description = <<-DESC + Computes the meaning of life. + Features: + 1. Is self aware + ... + 42. Likes candies + DESC + spec.static_framework = true + spec.requires_arc = 'true' + spec.authors = 'Tony O\'Connor' +end diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/myslib.podspec b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/myslib.podspec new file mode 100644 index 00000000000..cc94d27b99c --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/myslib.podspec @@ -0,0 +1,5 @@ +Pod::Spec.new do |spec| + spec.name = 'myslib' + spec.version = '0.1' + spec.vendored_library = 'myslib.dylib' +end diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/shared.podspec b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/shared.podspec new file mode 100644 index 00000000000..c11b9fa1eb5 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/podspecs/shared.podspec @@ -0,0 +1,16 @@ +Pod::Spec.new do |spec| + spec.name = 'shared' + spec.vendored_frameworks = 'shared.xcframework' + + spec.version = '5.6.2' + spec.license = 'MIT' + spec.homepage = 'https://github.com/Alpaca/Alpaca' + spec.source = { :git => 'https://github.com/Alpaca/Alpaca.git', :tag => spec.version } + spec.ios.deployment_target = '10.0' + spec.osx.deployment_target = '10.12' + spec.watchos.deployment_target = '3.0' + spec.swift_versions = ['4', '5'] + + # This is raw statement that is appended 'as is' to the podspec + spec.frameworks = 'CFNetwork' +end diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/settings.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/settings.gradle.kts new file mode 100644 index 00000000000..beb0025f5d5 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/settings.gradle.kts @@ -0,0 +1,2 @@ +include(":shared") +include(":lib") \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/shared/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/shared/build.gradle.kts new file mode 100644 index 00000000000..e76f6fc925f --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/shared/build.gradle.kts @@ -0,0 +1,96 @@ +import org.jetbrains.kotlin.gradle.plugin.cocoapods.withPodspec + +plugins { + kotlin("multiplatform") + kotlin("native.cocoapods") +} + +version = "0.1" + +kotlin { + linuxX64() + iosX64() + iosArm64() + iosSimulatorArm64() +} + +kotlinArtifacts { + Native.Library("mylib") { + target = linuxX64 + + withPodspec { + attribute( + "description", """ |<<-DESC + | Computes the meaning of life. + | Features: + | 1. Is self aware + | ... + | 42. Likes candies + | DESC + """.trimMargin() + ) + + attribute("static_framework", "true") + attribute("requires_arc", "'true'") + attribute("authors", "Tony O'Connor") + } + } + Native.Library("myslib") { + target = linuxX64 + isStatic = false + modes(DEBUG) + addModule(project(":lib")) + + withPodspec {} // intentionally empty + } + Native.Framework("myframe") { + modes(DEBUG, RELEASE) + target = iosArm64 + isStatic = false + embedBitcode = EmbedBitcodeMode.MARKER + + withPodspec { + attribute("prefix_header_file", "false") + } + } + Native.Framework("myframewihtoutpodspec") { + modes(DEBUG, RELEASE) + target = iosArm64 + isStatic = false + embedBitcode = EmbedBitcodeMode.MARKER + } + Native.FatFramework("myfatframe") { + targets(iosX64, iosSimulatorArm64) + embedBitcode = EmbedBitcodeMode.DISABLE + toolOptions { + suppressWarnings.set(true) + } + + withPodspec { + attribute("name", "custom-podspec-name") + } + } + Native.XCFramework { + targets(iosX64, iosArm64, iosSimulatorArm64) + setModules( + project(":shared"), + project(":lib") + ) + + withPodspec { + attribute("version", "5.6.2") + attribute("license", "MIT") + attribute("homepage", "https://github.com/Alpaca/Alpaca") + attribute("source", "{ :git => 'https://github.com/Alpaca/Alpaca.git', :tag => spec.version }") + + attribute("ios.deployment_target", "10.0") + attribute("osx.deployment_target", "10.12") + attribute("watchos.deployment_target", "3.0") + + attribute("swift_versions", "['4', '5']") + + rawStatement(" # This is raw statement that is appended 'as is' to the podspec") + rawStatement(" spec.frameworks = 'CFNetwork'") + } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/shared/src/commonMain/kotlin/org/sample/application/Platform.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/shared/src/commonMain/kotlin/org/sample/application/Platform.kt new file mode 100644 index 00000000000..da2586c36ac --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl-cocoapods/shared/src/commonMain/kotlin/org/sample/application/Platform.kt @@ -0,0 +1,5 @@ +package org.sample.application + +class Greeting { + fun greeting() = "Hello, new DSL!" +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl/shared/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl/shared/build.gradle.kts index 3a4bdb35369..1d4a465af06 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl/shared/build.gradle.kts +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-kn-library-dsl/shared/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.plugin.cocoapods.withPodspec + plugins { kotlin("multiplatform") } @@ -49,4 +51,4 @@ kotlinArtifacts { project(":lib") ) } -} \ No newline at end of file +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/KotlinArtifactsPodspecExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/KotlinArtifactsPodspecExtension.kt new file mode 100644 index 00000000000..cf267e95c05 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/KotlinArtifactsPodspecExtension.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2010-2022 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.cocoapods + +import org.gradle.api.* +import org.gradle.api.plugins.ExtensionAware +import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin +import org.jetbrains.kotlin.gradle.plugin.findExtension +import javax.inject.Inject + +abstract class KotlinArtifactsPodspecExtension @Inject constructor(private val project: Project) { + + private val attrs = mutableMapOf() + private val statements = mutableListOf() + + internal val attributes: Map + get() = attrs + + internal val rawStatements: List + get() = statements + + fun attribute(key: String, value: String) { + attrs[key] = value + } + + fun rawStatement(statement: String) { + statements.add(statement) + } +} + +val ExtensionAware.kotlinArtifactsPodspecExtension: KotlinArtifactsPodspecExtension? + get() = findExtension(KotlinCocoapodsPlugin.ARTIFACTS_PODSPEC_EXTENSION_NAME) \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/KotlinCocoapodsPlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/KotlinCocoapodsPlugin.kt index fb0638e6cec..39756f31be9 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/KotlinCocoapodsPlugin.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/KotlinCocoapodsPlugin.kt @@ -9,11 +9,11 @@ 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.plugins.ExtensionAware import org.gradle.api.provider.Provider import org.gradle.api.tasks.TaskProvider import org.jetbrains.kotlin.daemon.common.trimQuotes -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension +import org.jetbrains.kotlin.gradle.dsl.* import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation import org.jetbrains.kotlin.gradle.plugin.addExtension import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency @@ -25,15 +25,20 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.apple.AppleTarget import org.jetbrains.kotlin.gradle.plugin.mpp.apple.FrameworkCopy import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFrameworkTask import org.jetbrains.kotlin.gradle.plugin.whenEvaluated +import org.jetbrains.kotlin.gradle.targets.native.cocoapods.KotlinArtifactsPodspecExtension +import org.jetbrains.kotlin.gradle.targets.native.cocoapods.kotlinArtifactsPodspecExtension import org.jetbrains.kotlin.gradle.targets.native.tasks.* +import org.jetbrains.kotlin.gradle.targets.native.tasks.artifact.kotlinArtifactsExtension import org.jetbrains.kotlin.gradle.tasks.* import org.jetbrains.kotlin.gradle.utils.asValidTaskName import org.jetbrains.kotlin.gradle.utils.filesProvider import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName +import org.jetbrains.kotlin.gradle.utils.newInstance import org.jetbrains.kotlin.konan.target.Family import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.kotlin.konan.target.KonanTarget import org.jetbrains.kotlin.konan.target.KonanTarget.* +import org.jetbrains.kotlin.utils.addToStdlib.cast import java.io.File internal val Project.cocoapodsBuildDirs: CocoapodsBuildDirs @@ -149,7 +154,7 @@ open class KotlinCocoapodsPlugin : Plugin { private fun KotlinMultiplatformExtension.targetsForPlatform(requestedPlatform: KonanTarget) = supportedTargets().matching { it.konanTarget == requestedPlatform } - private fun createDefaultFrameworks(kotlinExtension: KotlinMultiplatformExtension, cocoapodsExtension: CocoapodsExtension) { + private fun createDefaultFrameworks(kotlinExtension: KotlinMultiplatformExtension) { kotlinExtension.supportedTargets().all { target -> target.binaries.framework(POD_FRAMEWORK_PREFIX) { baseName = project.name.asValidFrameworkName() @@ -216,7 +221,7 @@ open class KotlinCocoapodsPlugin : Plugin { check(targets.size == 1) { "The project has more than one target for the requested platform: `${requestedPlatform.visibleName}`" } val frameworkLinkTask = targets.single().binaries.getFramework(POD_FRAMEWORK_PREFIX, requestedBuildType).linkTaskProvider - project.createCopyFrameworkTask(frameworkLinkTask.flatMap { it.destinationDirectory.map { it.asFile } }, frameworkLinkTask) + project.createCopyFrameworkTask(frameworkLinkTask.flatMap { it.destinationDirectory.map { dir -> dir.asFile } }, frameworkLinkTask) } private fun createSyncTask( @@ -387,6 +392,58 @@ open class KotlinCocoapodsPlugin : Plugin { } } + private fun registerPodspecTask( + project: Project, + artifact: KotlinNativeArtifact, + podspecExtension: KotlinArtifactsPodspecExtension, + cocoapodsExtension: CocoapodsExtension, + ) { + val artifactName = artifact.artifactName + val assembleTask = project.tasks.named(artifact.taskName) + val podspecTaskName = lowerCamelCaseName("generate", artifactName, "podspec") + + val artifactType = when (artifact) { + is KotlinNativeLibrary -> GenerateArtifactPodspecTask.ArtifactType.Library + is KotlinNativeFramework -> GenerateArtifactPodspecTask.ArtifactType.Framework + is KotlinNativeFatFramework -> GenerateArtifactPodspecTask.ArtifactType.FatFramework + is KotlinNativeXCFramework -> GenerateArtifactPodspecTask.ArtifactType.XCFramework + else -> error("Podspec can only be generated for Library, Framework, FatFramework or XCFramework") + } + + val podspecTask = project.tasks.register(podspecTaskName, GenerateArtifactPodspecTask::class.java) { task -> + task.group = TASK_GROUP + task.description = "Generates a podspec file for '$artifactName' artifact" + task.specName.set(artifactName) + task.specVersion.set(project.version.takeIf { it != Project.DEFAULT_VERSION }.toString()) + task.destinationDir.set(project.buildDir.resolve(artifact.outDir)) + task.attributes.set(podspecExtension.attributes) + task.rawStatements.set(podspecExtension.rawStatements) + task.dependencies.set(cocoapodsExtension.pods) + task.artifactType.set(artifactType) + } + + assembleTask.dependsOn(podspecTask) + } + + private fun injectPodspecExtensionToArtifacts( + project: Project, + artifactsExtension: KotlinArtifactsExtension, + cocoapodsExtension: CocoapodsExtension, + ) { + artifactsExtension.artifactConfigs.withType(KotlinNativeArtifactConfig::class.java) { artifactConfig -> + val podspecExtension = project.objects.newInstance(project) + artifactConfig.addExtension(ARTIFACTS_PODSPEC_EXTENSION_NAME, podspecExtension) + } + + artifactsExtension.artifacts.withType(KotlinNativeArtifact::class.java) { artifact -> + val podspecExtension = artifact.kotlinArtifactsPodspecExtension + + if (podspecExtension != null) { + registerPodspecTask(project, artifact, podspecExtension, cocoapodsExtension) + } + } + } + private fun registerPodInstallTask( project: Project, cocoapodsExtension: CocoapodsExtension @@ -688,11 +745,15 @@ open class KotlinCocoapodsPlugin : Plugin { pluginManager.withPlugin("kotlin-multiplatform") { val kotlinExtension = project.multiplatformExtension + val kotlinArtifactsExtension = project.kotlinArtifactsExtension val cocoapodsExtension = project.objects.newInstance(CocoapodsExtension::class.java, this) + kotlinExtension.addExtension(COCOAPODS_EXTENSION_NAME, cocoapodsExtension) - createDefaultFrameworks(kotlinExtension, cocoapodsExtension) + + createDefaultFrameworks(kotlinExtension) registerDummyFrameworkTask(project, cocoapodsExtension) createSyncTask(project, kotlinExtension, cocoapodsExtension) + injectPodspecExtensionToArtifacts(project, kotlinArtifactsExtension, cocoapodsExtension) registerPodspecTask(project, cocoapodsExtension) registerPodGenTask(project, kotlinExtension, cocoapodsExtension) @@ -726,6 +787,7 @@ open class KotlinCocoapodsPlugin : Plugin { const val POD_SETUP_BUILD_TASK_NAME = "podSetupBuild" const val POD_BUILD_TASK_NAME = "podBuild" const val POD_IMPORT_TASK_NAME = "podImport" + const val ARTIFACTS_PODSPEC_EXTENSION_NAME = "withPodspec" // We don't move these properties in PropertiesProvider because // they are not intended to be overridden in local.properties. @@ -741,3 +803,16 @@ open class KotlinCocoapodsPlugin : Plugin { const val GENERATE_WRAPPER_PROPERTY = "kotlin.native.cocoapods.generate.wrapper" } } + +/** + * Extends a KotlinArtifact with a corresponding Podspec + * + * Only needed in *.kts build files. In Groovy you can use the same syntax but without explicit extension import + */ +fun KotlinNativeArtifactConfig.withPodspec(configure: KotlinArtifactsPodspecExtension.() -> Unit) { + val extension = cast().kotlinArtifactsPodspecExtension + + checkNotNull(extension) { "CocoaPods plugin should be applied before using `${KotlinCocoapodsPlugin.ARTIFACTS_PODSPEC_EXTENSION_NAME}` extension" } + + extension.configure() +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/GenerateArtifactPodspecTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/GenerateArtifactPodspecTask.kt new file mode 100644 index 00000000000..2abbf8e2de5 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/GenerateArtifactPodspecTask.kt @@ -0,0 +1,171 @@ +/* + * Copyright 2010-2022 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.file.DirectoryProperty +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.* +import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency +import org.jetbrains.kotlin.gradle.targets.native.tasks.GenerateArtifactPodspecTask.ArtifactType.* +import org.jetbrains.kotlin.gradle.utils.appendLine + +abstract class GenerateArtifactPodspecTask : DefaultTask() { + + enum class ArtifactType { + Library, Framework, FatFramework, XCFramework + } + + @get:Input + abstract val specName: Property + + @get:Input + abstract val specVersion: Property + + @get:OutputDirectory + abstract val destinationDir: DirectoryProperty + + @get:Input + abstract val attributes: MapProperty + + @get:Input + abstract val rawStatements: ListProperty + + @get:Nested + abstract val dependencies: ListProperty + + @get:Input + abstract val artifactType: Property + + @get:OutputFile + @Suppress("LeakingThis") // should be inherited only by gradle machinery + val outputFile: Provider = specName.flatMap { specName -> + destinationDir.file("$specName.podspec") + } + + @TaskAction + fun generate() { + outputFile.get().asFile.writeText(buildString { + + append("Pod::Spec.new do |spec|") + + appendAttributes(buildAdditionalAttrs()) + appendAttributes(attributes.get()) + appendDependencies() + appendRawStatements() + + appendLine("end") + + }) + } + + private fun Appendable.appendAttributes(attributes: Map) { + if (attributes.isNotEmpty()) { + appendLine() + } + + for ((key, value) in attributes) { + append(" spec.") + append(key) + + repeat(podspecValueIndent - key.length) { append(' ') } + append(" = ") + + append(value.wrapInSingleQuotesIfNeeded()) + + appendLine() + } + } + + private fun Appendable.appendDependencies() { + if (dependencies.get().isNotEmpty()) { + appendLine() + } + + for (dependency in dependencies.get()) { + + append(" spec.dependency ") + append(dependency.name.wrapInSingleQuotes()) + + val version = dependency.version + if (version != null) { + append(", ") + append(version.wrapInSingleQuotes()) + } + appendLine() + } + } + + private fun Appendable.appendRawStatements() { + if (rawStatements.get().isNotEmpty()) { + appendLine() + } + + for (statement in rawStatements.get()) { + appendLine(statement) + } + } + + private fun buildAdditionalAttrs(): Map { + val artifactTypeValue: ArtifactType = artifactType.get() + + return mutableMapOf().apply { + if (!attributes.get().containsKey(specNameKey)) { + put(specNameKey, specName.get()) + } + + if (!attributes.get().containsKey(specVersionKey)) { + specVersion.get()?.let { put(specVersionKey, it) } + } + + if (vendoredKeys.none { attributes.get().containsKey(it) }) { + val vendoredKey = when (artifactTypeValue) { + Library -> vendoredLibrary + Framework, FatFramework, XCFramework -> vendoredFrameworks + } + + val vendoredValue = specName.get() + "." + when (artifactTypeValue) { + Library -> "dylib" + Framework, FatFramework -> "framework" + XCFramework -> "xcframework" + } + + put(vendoredKey, vendoredValue) + } + } + } + + private fun String.wrapInSingleQuotesIfNeeded(): String { + return when { + startsWith('{') || + startsWith('[') || + startsWith("<<-") || + startsWith('\'') || + startsWith('"') || + equals("true") || + equals("false") -> this + + else -> wrapInSingleQuotes() + } + } + + private fun String.wrapInSingleQuotes() = "'" + replace("'", "\\'") + "'" + + companion object { + private const val specNameKey = "name" + private const val specVersionKey = "version" + private const val vendoredLibrary = "vendored_library" + private const val vendoredLibraries = "vendored_libraries" + private const val vendoredFrameworks = "vendored_frameworks" + private const val podspecValueIndent = 24 + + private val vendoredKeys = listOf(vendoredLibrary, vendoredLibraries, vendoredFrameworks) + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/artifact/KotlinNativeFatFramework.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/artifact/KotlinNativeFatFramework.kt index 3d91011e1fb..3e80bcbac6c 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/artifact/KotlinNativeFatFramework.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/artifact/KotlinNativeFatFramework.kt @@ -78,6 +78,8 @@ class KotlinNativeFatFrameworkImpl( ) : KotlinNativeFatFramework, ExtensionAware by extensions { override fun getName() = lowerCamelCaseName(artifactName, "FatFramework") override val taskName = lowerCamelCaseName("assemble", name) + override val outDir + get() = "out/fatframework" override fun registerAssembleTask(project: Project) { val parentTask = project.registerTask(taskName) { @@ -91,7 +93,7 @@ class KotlinNativeFatFrameworkImpl( lowerCamelCaseName("assemble", artifactName, buildType.visibleName, "FatFramework") ) { it.baseName = artifactName - it.destinationDir = project.buildDir.resolve("out/fatframework/${buildType.getName()}") + it.destinationDir = project.buildDir.resolve("$outDir/${buildType.getName()}") } parentTask.dependsOn(fatTask) diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/artifact/KotlinNativeFramework.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/artifact/KotlinNativeFramework.kt index 661992e55f9..d4790bee1ca 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/artifact/KotlinNativeFramework.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/artifact/KotlinNativeFramework.kt @@ -72,6 +72,7 @@ class KotlinNativeFrameworkImpl( private val kind = NativeOutputKind.FRAMEWORK override fun getName() = lowerCamelCaseName(artifactName, kind.taskNameClassifier, target.presetName) override val taskName = lowerCamelCaseName("assemble", name) + override val outDir = "out/${kind.visibleName}" override fun registerAssembleTask(project: Project) { val resultTask = project.registerTask(taskName) { task -> @@ -105,11 +106,11 @@ internal fun KotlinNativeArtifact.registerLinkFrameworkTask( librariesConfigurationName: String, exportConfigurationName: String, embedBitcode: BitcodeEmbeddingMode?, - outDirName: String = "out", + outDirName: String = outDir, taskNameSuffix: String = "" ): TaskProvider { val kind = NativeOutputKind.FRAMEWORK - val destinationDir = project.buildDir.resolve("$outDirName/${kind.visibleName}/${target.visibleName}/${buildType.visibleName}") + val destinationDir = project.buildDir.resolve("$outDirName/${target.visibleName}/${buildType.visibleName}") val resultTask = project.registerTask( lowerCamelCaseName("assemble", name, buildType.visibleName, kind.taskNameClassifier, target.presetName, taskNameSuffix), listOf(target, kind.compilerOutputKind) diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/artifact/KotlinNativeLibrary.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/artifact/KotlinNativeLibrary.kt index 766df6f6999..2e88a63061e 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/artifact/KotlinNativeLibrary.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/artifact/KotlinNativeLibrary.kt @@ -67,6 +67,7 @@ class KotlinNativeLibraryImpl( private val kind = if (isStatic) NativeOutputKind.STATIC else NativeOutputKind.DYNAMIC override fun getName() = lowerCamelCaseName(artifactName, kind.taskNameClassifier, "Library", target.presetName) override val taskName = lowerCamelCaseName("assemble", name) + override val outDir = "out/${kind.visibleName}" override fun registerAssembleTask(project: Project) { val resultTask = project.registerTask(taskName) { task -> @@ -84,6 +85,7 @@ class KotlinNativeLibraryImpl( listOf(target, kind.compilerOutputKind) ) { task -> task.description = "Assemble ${kind.description} '$artifactName' for a target '${target.name}'." + task.destinationDir.set(project.buildDir.resolve("$outDir/${target.visibleName}/${buildType.visibleName}")) task.enabled = target.enabledOnCurrentHost task.baseName.set(artifactName) task.optimized.set(buildType.optimized) diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/artifact/KotlinNativeXCFramework.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/artifact/KotlinNativeXCFramework.kt index 2c9492aba8b..e848b4bea6d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/artifact/KotlinNativeXCFramework.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/artifact/KotlinNativeXCFramework.kt @@ -78,6 +78,8 @@ class KotlinNativeXCFrameworkImpl( ) : KotlinNativeXCFramework, ExtensionAware by extensions { override fun getName() = lowerCamelCaseName(artifactName, "XCFramework") override val taskName = lowerCamelCaseName("assemble", name) + override val outDir: String + get() = "out/xcframework" override fun registerAssembleTask(project: Project) { val parentTask = project.registerTask(taskName) { @@ -120,7 +122,7 @@ class KotlinNativeXCFrameworkImpl( } holder.task.configure { it.fromFrameworkDescriptors(frameworkDescriptors) - it.outputDir = project.buildDir.resolve("out/xcframework") + it.outputDir = project.buildDir.resolve(outDir) } } }