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 4233f96f739..859694c029f 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 @@ -17,10 +17,8 @@ import org.jetbrains.kotlin.gradle.util.assertProcessRunResult import org.jetbrains.kotlin.gradle.util.replaceText import org.jetbrains.kotlin.gradle.util.runProcess import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.condition.OS -import java.io.File import java.util.zip.ZipFile import kotlin.io.path.* import kotlin.test.* @@ -121,11 +119,19 @@ class CocoaPodsIT : KGPBaseTest() { } } - @DisplayName("UTD after linking framework") + @DisplayName("UTD after syncing framework") @GradleTest - fun testImportUTDAfterLinkingFramework(gradleVersion: GradleVersion) { - nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { - val linkTaskName = ":linkPodDebugFrameworkIOS" + fun testImportUTDAfterSyncingFramework(gradleVersion: GradleVersion) { + val buildOptions = defaultBuildOptions.copy( + nativeOptions = defaultBuildOptions.nativeOptions.copy( + cocoapodsPlatform = "iphonesimulator", + cocoapodsArchs = "x86_64", + cocoapodsConfiguration = "Debug" + ) + ) + + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion, buildOptions = buildOptions) { + val syncTaskName = ":syncFramework" buildGradleKts.addCocoapodsBlock( """ @@ -142,12 +148,13 @@ class CocoaPodsIT : KGPBaseTest() { assertTasksExecuted(podInstallTaskName) } - buildWithCocoapodsWrapper(linkTaskName) { - assertTasksExecuted(linkTaskName) + buildWithCocoapodsWrapper(syncTaskName) { + assertTasksExecuted(syncTaskName) } buildWithCocoapodsWrapper(podImportTaskName) { - assertTasksUpToDate(dummyTaskName) + assertTasksExecuted(dummyTaskName) + assertOutputContains("Skipping dummy-framework generation because a dynamic framework is already present") assertTasksUpToDate(podInstallTaskName) } } @@ -156,7 +163,17 @@ class CocoaPodsIT : KGPBaseTest() { @DisplayName("Changing framework type and checks UTD") @GradleTest fun testChangeFrameworkTypeUTD(gradleVersion: GradleVersion) { - nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + val buildOptions = defaultBuildOptions.copy( + nativeOptions = defaultBuildOptions.nativeOptions.copy( + cocoapodsPlatform = "iphonesimulator", + cocoapodsArchs = "x86_64", + cocoapodsConfiguration = "Debug" + ) + ) + + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion, buildOptions = buildOptions) { + val syncTaskName = ":syncFramework" + buildGradleKts.addCocoapodsBlock( """ framework { @@ -177,9 +194,14 @@ class CocoaPodsIT : KGPBaseTest() { assertTasksUpToDate(podInstallTaskName) } + buildWithCocoapodsWrapper(syncTaskName) { + assertTasksExecuted(syncTaskName) + } + buildGradleKts.addFrameworkBlock("isStatic = true") buildWithCocoapodsWrapper(podImportTaskName) { assertTasksExecuted(dummyTaskName) + assertOutputContains("Regenerating dummy-framework because present framework has different linkage") assertTasksExecuted(podInstallTaskName) } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsXcodeIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsXcodeIT.kt index 3f237418d7a..97e2e398635 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsXcodeIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsXcodeIT.kt @@ -6,6 +6,7 @@ package org.jetbrains.kotlin.gradle.native import org.gradle.util.GradleVersion +import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.DUMMY_FRAMEWORK_TASK_NAME import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_INSTALL_TASK_NAME import org.jetbrains.kotlin.gradle.testbase.* import org.jetbrains.kotlin.gradle.util.assertProcessRunResult @@ -46,6 +47,19 @@ class CocoaPodsXcodeIT : KGPBaseTest() { mapOf("kotlin-library" to null) ) + @DisplayName("Checks xcodebuild for ios-app with a single framework with manual pod install") + @GradleTest + fun testXcodeUseFrameworksSingleWithManualPodInstall(gradleVersion: GradleVersion) { + nativeProject(cocoapodsSingleKtPod, gradleVersion, environmentVariables = environmentVariables) { + doTestXcode( + mode = ImportMode.FRAMEWORKS, + iosAppLocation = "ios-app", + subprojectsToFrameworkNamesMap = mapOf("kotlin-library" to null), + podInstall = ::manualPodInstall + ) + } + } + @DisplayName("Checks xcodebuild for ios-app with a single framework with custom name") @GradleTest fun testXcodeUseFrameworksWithCustomFrameworkNameSingle(gradleVersion: GradleVersion) = doTestXcode( @@ -66,7 +80,6 @@ class CocoaPodsXcodeIT : KGPBaseTest() { mapOf("kotlin-library" to null) ) - @DisplayName("Checks xcodebuild for ios-app using modular headers with a single framework with custom name") @GradleTest fun testXcodeUseModularHeadersWithCustomFrameworkNameSingle(gradleVersion: GradleVersion) = doTestXcode( @@ -77,6 +90,21 @@ class CocoaPodsXcodeIT : KGPBaseTest() { mapOf("kotlin-library" to "MultiplatformLibrary") ) + @DisplayName("Checks xcodebuild for ios-app with manual pod install and static framework") + @GradleTest + fun testXcodeWithManualPodInstallForStaticFramework(gradleVersion: GradleVersion) { + nativeProject(cocoapodsSingleKtPod, gradleVersion, environmentVariables = environmentVariables) { + buildGradleKts.addFrameworkBlock("isStatic = true") + + doTestXcode( + mode = ImportMode.MODULAR_HEADERS, + iosAppLocation = "ios-app", + subprojectsToFrameworkNamesMap = mapOf("kotlin-library" to "MultiplatformLibrary"), + podInstall = ::manualPodInstall + ) + } + } + @DisplayName("Checks xcodebuild for ios-app with kotlin library from root project") @GradleTest fun testXcodeBuildsWithKotlinLibraryFromRootProject(gradleVersion: GradleVersion) { @@ -163,6 +191,7 @@ class CocoaPodsXcodeIT : KGPBaseTest() { iosAppLocation: String?, subprojectsToFrameworkNamesMap: Map, arch: String = "x86_64", + podInstall: (taskPrefix: String, iosAppPath: Path) -> Unit = ::gradlePodInstall, ) { gradleProperties @@ -189,32 +218,46 @@ class CocoaPodsXcodeIT : KGPBaseTest() { // Generate podspec. build("$taskPrefix:podspec", buildOptions = buildOptions) - iosAppLocation?.also { + if (iosAppLocation != null) { // Set import mode for Podfile. - preparePodfile(it, mode) - // Install pods. - build("$taskPrefix:$POD_INSTALL_TASK_NAME", buildOptions = buildOptions) + preparePodfile(iosAppLocation, mode) - projectPath.resolve(it).apply { - // Run Xcode build. - val xcodebuildResult = runProcess( - cmd = listOf( - "xcodebuild", - "-sdk", "iphonesimulator", - "-configuration", "Release", - "-workspace", "$name.xcworkspace", - "-scheme", name, - "-arch", arch - ), - environmentVariables = environmentVariables.environmentalVariables, - workingDir = this.toFile(), - ) - assertProcessRunResult(xcodebuildResult) { - assertEquals(0, exitCode, "Exit code mismatch for `xcodebuild`.") - } + val iosAppPath = projectPath.resolve(iosAppLocation) + + // Install pods. + podInstall(taskPrefix, iosAppPath) + + // Run Xcode build. + val xcodebuildResult = runProcess( + cmd = listOf( + "xcodebuild", + "-sdk", "iphonesimulator", + "-configuration", "Release", + "-workspace", "${iosAppPath.name}.xcworkspace", + "-scheme", iosAppPath.name, + "-arch", arch + ), + environmentVariables = environmentVariables.environmentalVariables, + workingDir = iosAppPath.toFile(), + ) + assertProcessRunResult(xcodebuildResult) { + assertEquals(0, exitCode, "Exit code mismatch for `xcodebuild`.") } } } - } +} + +private fun TestProject.gradlePodInstall(taskPrefix: String, @Suppress("UNUSED_PARAMETER") iosAppPath: Path) { + build("$taskPrefix:$POD_INSTALL_TASK_NAME", buildOptions = buildOptions) +} + +private fun TestProject.manualPodInstall(taskPrefix: String, iosAppPath: Path) { + build("$taskPrefix:$DUMMY_FRAMEWORK_TASK_NAME", buildOptions = buildOptions) + + runProcess( + cmd = listOf("pod", "install"), + environmentVariables = environmentVariables.environmentalVariables, + workingDir = iosAppPath.toFile(), + ) } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/CocoapodsExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/CocoapodsExtension.kt index 6b5fe2770cd..a61bc46a50b 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/CocoapodsExtension.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/CocoapodsExtension.kt @@ -10,6 +10,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.provider.Provider import org.gradle.api.tasks.* import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.* @@ -118,8 +119,8 @@ abstract class CocoapodsExtension @Inject constructor(private val project: Proje anyFramework } - internal val podFrameworkName = anyPodFramework.map { it.baseName.asValidFrameworkName() } - internal val podFrameworkIsStatic = anyPodFramework.map { it.isStatic } + internal val podFrameworkName: Provider = anyPodFramework.map { it.baseName.asValidFrameworkName() } + internal val podFrameworkIsStatic: Provider = anyPodFramework.map { it.isStatic } /** * Configure custom Xcode Configurations to Native Build Types mapping 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 65379e97337..5fbe7826d92 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 @@ -60,8 +60,6 @@ internal class CocoapodsBuildDirs(private val layout: ProjectLayout) { get() = layout.buildDirectory.dir("cocoapods") val framework: Provider get() = dir("framework") - val dummyFramework: Provider - get() = dir("dummy.framework") val defs: Provider get() = dir("defs") val publish: Provider @@ -394,8 +392,9 @@ open class KotlinCocoapodsPlugin : Plugin { cocoapodsExtension: CocoapodsExtension ) { project.registerTask(DUMMY_FRAMEWORK_TASK_NAME) { task -> - task.frameworkName.set(cocoapodsExtension.podFrameworkName) - task.useStaticFramework.set(cocoapodsExtension.podFrameworkIsStatic) + task.frameworkName.convention(cocoapodsExtension.podFrameworkName) + task.useStaticFramework.convention(cocoapodsExtension.podFrameworkIsStatic) + task.outputFramework.convention(project.layout.cocoapodsBuildDirs.framework.map { it.dir(task.frameworkName.get() + ".framework") }) } } @@ -478,17 +477,18 @@ open class KotlinCocoapodsPlugin : Plugin { cocoapodsExtension: CocoapodsExtension ) { val podspecTaskProvider = project.tasks.named(POD_SPEC_TASK_NAME) - val dummyFrameworkTaskProvider = project.tasks.named(DUMMY_FRAMEWORK_TASK_NAME) + val dummyFrameworkTaskProvider = project.tasks.named(DUMMY_FRAMEWORK_TASK_NAME) project.registerTask(POD_INSTALL_TASK_NAME) { task -> task.group = TASK_GROUP task.description = "Invokes `pod install` call within Podfile location directory" task.podfile.set(project.provider { cocoapodsExtension.podfile }) task.podspec.set(podspecTaskProvider.map { it.outputFile }) + task.useStaticFramework.set(cocoapodsExtension.podFrameworkIsStatic) task.frameworkName.set(cocoapodsExtension.podFrameworkName) task.specRepos.set(project.provider { cocoapodsExtension.specRepos }) task.pods.set(cocoapodsExtension.pods) - task.dummyFramework.set(dummyFrameworkTaskProvider.map { it.outputFramework.get() }) task.dependsOn(podspecTaskProvider) + task.dependsOn(dummyFrameworkTaskProvider) } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/DummyFrameworkTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/DummyFrameworkTask.kt index ab8595b0aab..bfdf3cea2ac 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/DummyFrameworkTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/DummyFrameworkTask.kt @@ -7,17 +7,16 @@ package org.jetbrains.kotlin.gradle.tasks import org.gradle.api.DefaultTask -import org.gradle.api.file.ProjectLayout +import org.gradle.api.file.DirectoryProperty import org.gradle.api.provider.Property -import org.gradle.api.provider.Provider import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction import org.gradle.work.DisableCachingByDefault -import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs -import org.jetbrains.kotlin.gradle.utils.mapToFile +import org.jetbrains.kotlin.gradle.utils.MachO +import org.jetbrains.kotlin.gradle.utils.getFile import java.io.File -import javax.inject.Inject /** * Creates a dummy framework in the target directory. @@ -31,7 +30,7 @@ import javax.inject.Inject * and then replace it with the real one during a real build process. */ @DisableCachingByDefault -abstract class DummyFrameworkTask @Inject constructor(projectLayout: ProjectLayout) : DefaultTask() { +abstract class DummyFrameworkTask : DefaultTask() { @get:Input abstract val frameworkName: Property @@ -40,13 +39,18 @@ abstract class DummyFrameworkTask @Inject constructor(projectLayout: ProjectLayo abstract val useStaticFramework: Property @get:OutputDirectory - val outputFramework: Provider = projectLayout.cocoapodsBuildDirs.dummyFramework.mapToFile() + abstract val outputFramework: DirectoryProperty + + @get:Internal + @Deprecated("Use outputFramework", replaceWith = ReplaceWith("outputFramework.get().asFile")) + val destinationDir: File + get() = outputFramework.getFile() + + private val linkageName: String + get() = if (useStaticFramework.get()) "static" else "dynamic" private val dummyFrameworkResource: String - get() { - val staticOrDynamic = if (!useStaticFramework.get()) "dynamic" else "static" - return "/cocoapods/$staticOrDynamic/dummy.framework/" - } + get() = "/cocoapods/$linkageName/dummy.framework/" private fun copyResource(from: String, to: File) { to.parentFile.mkdirs() @@ -71,23 +75,22 @@ abstract class DummyFrameworkTask @Inject constructor(projectLayout: ProjectLayo private fun copyFrameworkFile(relativeFrom: String, relativeTo: String = relativeFrom) = copyResource( "$dummyFrameworkResource$relativeFrom", - outputFramework.get().resolve(relativeTo) + outputFramework.getFile().resolve(relativeTo) ) private fun copyFrameworkTextFile( relativeFrom: String, relativeTo: String = relativeFrom, - transform: (String) -> String = { it } + transform: (String) -> String = { it }, ) = copyTextResource( "$dummyFrameworkResource$relativeFrom", - outputFramework.get().resolve(relativeTo), + outputFramework.getFile().resolve(relativeTo), transform ) - @TaskAction - fun create() { + private fun copyFramework() { // Reset the destination directory - with(outputFramework.get()) { + with(outputFramework.getFile()) { deleteRecursively() mkdirs() } @@ -104,4 +107,25 @@ abstract class DummyFrameworkTask @Inject constructor(projectLayout: ProjectLayo } } } + + + @TaskAction + fun create() { + val framework = outputFramework.getFile() + val binary = framework.resolve(frameworkName.get()) + + return when { + !binary.exists() -> { + logger.info("Generating dummy-framework because the framework is missing") + copyFramework() + } + MachO.isDylib(binary, logger) == !useStaticFramework.get() -> { + logger.info("Skipping dummy-framework generation because a $linkageName framework is already present") + } + else -> { + logger.info("Regenerating dummy-framework because present framework has different linkage") + copyFramework() + } + } + } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallTask.kt index 62e6604456d..9e351cc5371 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallTask.kt @@ -7,20 +7,16 @@ package org.jetbrains.kotlin.gradle.targets.native.tasks -import org.gradle.api.file.ProjectLayout import org.gradle.api.provider.ListProperty 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.plugin.cocoapods.CocoapodsExtension.SpecRepos -import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingCocoapodsMessage import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingSpecReposMessage import java.io.File -import javax.inject.Inject -abstract class PodInstallTask @Inject constructor(projectLayout: ProjectLayout) : AbstractPodInstallTask() { +abstract class PodInstallTask : AbstractPodInstallTask() { @get:Optional @get:InputFile @@ -29,38 +25,15 @@ abstract class PodInstallTask @Inject constructor(projectLayout: ProjectLayout) @get:Input abstract val frameworkName: Property + @get:Input + abstract val useStaticFramework: Property + @get:Nested abstract val specRepos: Property @get:Nested abstract val pods: ListProperty - @get:InputDirectory - abstract val dummyFramework: Property - - private val framework = projectLayout.cocoapodsBuildDirs.framework.map { it.file("${frameworkName.get()}.framework").asFile } - private val tmpFramework = dummyFramework.map { dummy -> dummy.parentFile.resolve("tmp.framework").also { it.deleteOnExit() } } - - override fun doPodInstall() { - // We always need to execute 'pod install' with the dummy framework because the one left from a previous build - // may have a wrong linkage type. So we temporarily swap them, run 'pod install' and then swap them back - framework.rename(tmpFramework) - dummyFramework.rename(framework) - super.doPodInstall() - framework.rename(dummyFramework) - tmpFramework.rename(framework) - } - - private fun Provider.rename(dest: Provider) = get().rename(dest.get()) - - private fun File.rename(dest: File) { - if (!exists()) { - mkdirs() - } - - check(renameTo(dest)) { "Can't rename '${this}' to '${dest}'" } - } - override fun handleError(retCode: Int, error: String, process: Process): String? { val specReposMessages = MissingSpecReposMessage(specRepos.get()).missingMessage val cocoapodsMessages = pods.get().map { MissingCocoapodsMessage(it).missingMessage } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/MachO.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/MachO.kt new file mode 100644 index 00000000000..50ac8b70b85 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/MachO.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2010-2023 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.utils + +import org.gradle.api.logging.Logger +import org.jetbrains.kotlin.resolve.constants.fromUIntToLong +import java.io.File +import java.io.IOException +import java.io.RandomAccessFile + +// https://github.com/aidansteele/osx-abi-macho-file-format-reference +@Suppress("SpellCheckingInspection") +internal object MachO { + + private const val FAT_MAGIC = 0xcafebabeL + private const val FAT_MAGIC_64 = 0xcafebabfL + + private const val MH_CIGAM = 0xcefaedfeL + private const val MH_CIGAM_64 = 0xcffaedfeL + + private const val MH_BILYD = 0x06000000L // MH_DYLIB = 0x6 converted for JVM + + private const val FILE_TYPE_OFFSET = 12L + private const val FAT_FIRST_MACHO_OFFSET_OFFSET = 16L + + /** + * Checks if the [file] is a Mach-O dynamic shared library + * or a Mach-O fat binary containing a number of dynamic libraries + */ + fun isDylib(file: File, logger: Logger): Boolean { + try { + RandomAccessFile(file, "r").use { raf -> + val magic = raf.readInt().fromUIntToLong() + + val fileTypeOffset = when (magic) { + // all supported platforms are little-endian and JVM is big-endian, so we don't check MH_MAGIC[_64] + MH_CIGAM, MH_CIGAM_64 -> FILE_TYPE_OFFSET + FAT_MAGIC, FAT_MAGIC_64 -> { + raf.seek(FAT_FIRST_MACHO_OFFSET_OFFSET) + val firstMachoOffset = raf.readInt().fromUIntToLong() // guaranteed to be big-endian by the spec + firstMachoOffset + FILE_TYPE_OFFSET + } + else -> return false // not Mach-O or fat + } + + raf.seek(fileTypeOffset) + val fileType = raf.readInt().toLong() + + return fileType == MH_BILYD + } + } catch (e: IOException) { + logger.info("IOException while cheking if '$file' is a dylib", e) + return false + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/MachOTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/MachOTest.kt new file mode 100644 index 00000000000..9288f40071e --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/MachOTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2010-2023 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. + */ + +@file:Suppress("FunctionName") + +package org.jetbrains.kotlin.gradle.unitTests + +import org.jetbrains.kotlin.gradle.util.buildProject +import org.jetbrains.kotlin.gradle.utils.MachO +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import java.io.InputStream +import kotlin.test.assertEquals + +class MachOTest { + + @get:Rule + val temporaryFolder = TemporaryFolder() + + @Test + fun `dynamic dummy`() { + assertDylib(true, "/cocoapods/dynamic/dummy.framework/dummy") + } + + @Test + fun `static dummy`() { + assertDylib(false, "/cocoapods/static/dummy.framework/dummy") + } + + @Test + fun `dynamic fat`() { + assertDylib(true, "/machoBinaries/dynamicFat") + } + + @Test + fun `static fat`() { + assertDylib(false, "/machoBinaries/staticFat") + } + + @Test + fun `dynamic lib`() { + assertDylib(true, "/machoBinaries/dynamicLib") + } + + @Test + fun `static lib`() { + assertDylib(false, "/machoBinaries/staticLib") + } + + private fun assertDylib(expected: Boolean, resource: String) { + val tmp = temporaryFolder.newFile() + tmp.writeResource(resource) + return assertEquals(expected, MachO.isDylib(tmp, buildProject().logger)) + } + + private fun File.writeResource(resource: String) { + outputStream().use { out -> + MachOTest::class.java.getResourceAsStream(resource)!!.use { input: InputStream -> + input.copyTo(out) + } + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/resources/machoBinaries/dynamicFat b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/resources/machoBinaries/dynamicFat new file mode 100755 index 00000000000..f85e1ec7c49 Binary files /dev/null and b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/resources/machoBinaries/dynamicFat differ diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/resources/machoBinaries/dynamicLib b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/resources/machoBinaries/dynamicLib new file mode 100755 index 00000000000..301c06973a6 Binary files /dev/null and b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/resources/machoBinaries/dynamicLib differ diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/resources/machoBinaries/staticFat b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/resources/machoBinaries/staticFat new file mode 100755 index 00000000000..97afba5ae5d Binary files /dev/null and b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/resources/machoBinaries/staticFat differ diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/resources/machoBinaries/staticLib b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/resources/machoBinaries/staticLib new file mode 100644 index 00000000000..1dd1bd88248 Binary files /dev/null and b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/resources/machoBinaries/staticLib differ