diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/AppleFrameworkIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/AppleFrameworkIT.kt index e25ce2054c5..df2580c7066 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/AppleFrameworkIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/AppleFrameworkIT.kt @@ -78,6 +78,32 @@ class AppleFrameworkIT : BaseGradleIT() { } } + @Test + fun `check that macOS framework has symlinks`() { + with(Project("sharedAppleFramework")) { + val options: BuildOptions = defaultBuildOptions().copy( + customEnvironmentVariables = mapOf( + "CONFIGURATION" to "debug", + "SDK_NAME" to "macosx", + "ARCHS" to "x86_64", + "EXPANDED_CODE_SIGN_IDENTITY" to "-", + "TARGET_BUILD_DIR" to workingDir.absolutePath, + "FRAMEWORKS_FOLDER_PATH" to "${projectName}/build/xcode-derived" + ) + ) + build(":shared:embedAndSignAppleFrameworkForXcode", options = options) { + assertSuccessful() + assertTasksExecuted(":shared:assembleDebugAppleFrameworkForXcodeMacosX64") + // Verify symlinks in gradle build folder + assertFileExists("/shared/build/xcode-frameworks/debug/macosx/sdk.framework/Headers") + assertFileIsSymlink("/shared/build/xcode-frameworks/debug/macosx/sdk.framework/Headers") + // Verify symlinks in xcode derived folder + assertFileExists("/build/xcode-derived/sdk.framework/Headers") + assertFileIsSymlink("/build/xcode-derived/sdk.framework/Headers") + } + } + } + @Test fun `check embedAndSignAppleFrameworkForXcode fail`() { with(Project("sharedAppleFramework")) { diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt index 96bcf49e252..c0c29c6893b 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt @@ -31,6 +31,7 @@ import org.junit.Assert import org.junit.Before import org.junit.runner.RunWith import java.io.File +import java.nio.file.Files import java.util.regex.Pattern import kotlin.io.path.isDirectory import kotlin.test.* @@ -559,6 +560,11 @@ abstract class BaseGradleIT { return this } + fun CompiledProject.assertFileIsSymlink(path: String = ""): CompiledProject { + assertTrue(Files.isSymbolicLink(fileInWorkingDir(path).toPath()), "The file [$path] isn't a symlink.") + return this + } + fun CompiledProject.assertNoSuchFile(path: String = ""): CompiledProject { assertFalse(fileInWorkingDir(path).exists(), "The file [$path] exists.") return this diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/sharedAppleFramework/shared/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/sharedAppleFramework/shared/build.gradle.kts index b0fea6d2995..7a4badc2bd3 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/sharedAppleFramework/shared/build.gradle.kts +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/sharedAppleFramework/shared/build.gradle.kts @@ -6,10 +6,11 @@ plugins { kotlin { android() + val macosX64 = macosX64() val iosX64 = iosX64() val iosSimulatorArm64 = iosSimulatorArm64() val iosArm64 = iosArm64() - configure(listOf(iosX64, iosSimulatorArm64, iosArm64)) { + configure(listOf(macosX64, iosX64, iosSimulatorArm64, iosArm64)) { binaries { framework { baseName = "sdk" diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/sharedAppleFramework/shared/src/macosX64Main/kotlin/com/github/jetbrains/myapplication/Platform.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/sharedAppleFramework/shared/src/macosX64Main/kotlin/com/github/jetbrains/myapplication/Platform.kt new file mode 100644 index 00000000000..07d8e497173 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/sharedAppleFramework/shared/src/macosX64Main/kotlin/com/github/jetbrains/myapplication/Platform.kt @@ -0,0 +1,5 @@ +package com.github.jetbrains.myapplication + +actual class Platform actual constructor() { + actual val platform: String = "macOS" +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/AppleXcodeTasks.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/AppleXcodeTasks.kt index 75686751ea3..aa5cf11b1a2 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/AppleXcodeTasks.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/AppleXcodeTasks.kt @@ -6,10 +6,18 @@ package org.jetbrains.kotlin.gradle.plugin.mpp.apple import org.gradle.api.DefaultTask +import org.gradle.api.file.FileCollection import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.plugins.BasePlugin import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.IgnoreEmptyDirectories +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.SkipWhenEmpty +import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskProvider import org.jetbrains.kotlin.gradle.dsl.KotlinNativeBinaryContainer import org.jetbrains.kotlin.gradle.plugin.mpp.Framework @@ -17,7 +25,6 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType import org.jetbrains.kotlin.gradle.plugin.mpp.enabledOnCurrentHost import org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask import org.jetbrains.kotlin.gradle.tasks.locateOrRegisterTask -import org.jetbrains.kotlin.gradle.tasks.locateTask import org.jetbrains.kotlin.gradle.tasks.registerTask import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName import org.jetbrains.kotlin.konan.target.KonanTarget @@ -117,12 +124,12 @@ private fun Project.registerAssembleAppleFrameworkTask(framework: Framework): Ta }.also { it.configure { task -> task.from(framework) } } - else -> registerTask(frameworkTaskName) { task -> + else -> registerTask(frameworkTaskName) { task -> task.description = "Packs $frameworkBuildType ${frameworkTarget.name} framework for Xcode" task.isEnabled = frameworkBuildType == envBuildType task.dependsOn(framework.linkTaskName) - task.from(framework.outputDirectory) - task.into(appleFrameworkDir(envFrameworkSearchDir)) + task.files = files({ framework.outputDirectory.listFiles() }) + task.destDir = appleFrameworkDir(envFrameworkSearchDir) } } } @@ -160,7 +167,7 @@ internal fun Project.registerEmbedAndSignAppleFrameworkTask(framework: Framework return } - val embedAndSignTask = locateOrRegisterTask(frameworkTaskName) { task -> + val embedAndSignTask = locateOrRegisterTask(frameworkTaskName) { task -> task.group = BasePlugin.BUILD_GROUP task.description = "Embed and sign ${framework.namePrefix} framework as requested by Xcode's environment variables" task.inputs.apply { @@ -176,17 +183,14 @@ internal fun Project.registerEmbedAndSignAppleFrameworkTask(framework: Framework embedAndSignTask.configure { task -> task.dependsOn(assembleTask) - task.from(appleFrameworkDir(envFrameworkSearchDir)) { it.include(framework.outputFile.name + "/**") } - task.into(envEmbeddedFrameworksDir) - - if (envSign != null) { - task.doLast { - val binary = envEmbeddedFrameworksDir - .resolve(framework.outputFile.name) - .resolve(framework.outputFile.nameWithoutExtension) - exec { - it.commandLine("codesign", "--force", "--sign", envSign, "--", binary) - } + task.files = files(File(appleFrameworkDir(envFrameworkSearchDir), framework.outputFile.name)) + task.destDir = envEmbeddedFrameworksDir + if (envSign != null) task.doLast { + val binary = envEmbeddedFrameworksDir + .resolve(framework.outputFile.name) + .resolve(framework.outputFile.nameWithoutExtension) + exec { + it.commandLine("codesign", "--force", "--sign", envSign, "--", binary) } } } @@ -200,4 +204,33 @@ private val Framework.namePrefix: String ) private fun Project.appleFrameworkDir(frameworkSearchDir: File) = - buildDir.resolve("xcode-frameworks").resolve(frameworkSearchDir) \ No newline at end of file + buildDir.resolve("xcode-frameworks").resolve(frameworkSearchDir) + +/** + * macOS frameworks contain symlinks which are resolved/removed by the Gradle [Copy] task. + * To preserve these symlinks we are using the `cp` command instead. + * See https://youtrack.jetbrains.com/issue/KT-48594. + */ +private abstract class FrameworkCopy : DefaultTask() { + + @get:InputFiles + @get:SkipWhenEmpty + @get:IgnoreEmptyDirectories + @get:PathSensitive(PathSensitivity.ABSOLUTE) + abstract var files: FileCollection + + @get:OutputDirectory + abstract var destDir: File + + @TaskAction + fun copy() { + destDir.mkdirs() + files.forEach { file -> + File(destDir, file.name).let destFile@{ destFile -> + if (!destFile.exists()) return@destFile + project.exec { it.commandLine("rm", "-r", destFile.absolutePath) } + } + project.exec { it.commandLine("cp", "-R", file.absolutePath, destDir.absolutePath) } + } + } +}