[Gradle] Fix invalidation framework after sync while preserving 'generateDummyFramework' task semantic

^KT-49430 Verification Pending
This commit is contained in:
Artem Daugel-Dauge
2023-06-23 22:05:25 +02:00
committed by Space Team
parent f96b5bae48
commit caaade1f4f
12 changed files with 278 additions and 89 deletions
@@ -17,10 +17,8 @@ import org.jetbrains.kotlin.gradle.util.assertProcessRunResult
import org.jetbrains.kotlin.gradle.util.replaceText import org.jetbrains.kotlin.gradle.util.replaceText
import org.jetbrains.kotlin.gradle.util.runProcess import org.jetbrains.kotlin.gradle.util.runProcess
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.condition.OS import org.junit.jupiter.api.condition.OS
import java.io.File
import java.util.zip.ZipFile import java.util.zip.ZipFile
import kotlin.io.path.* import kotlin.io.path.*
import kotlin.test.* import kotlin.test.*
@@ -121,11 +119,19 @@ class CocoaPodsIT : KGPBaseTest() {
} }
} }
@DisplayName("UTD after linking framework") @DisplayName("UTD after syncing framework")
@GradleTest @GradleTest
fun testImportUTDAfterLinkingFramework(gradleVersion: GradleVersion) { fun testImportUTDAfterSyncingFramework(gradleVersion: GradleVersion) {
nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { val buildOptions = defaultBuildOptions.copy(
val linkTaskName = ":linkPodDebugFrameworkIOS" nativeOptions = defaultBuildOptions.nativeOptions.copy(
cocoapodsPlatform = "iphonesimulator",
cocoapodsArchs = "x86_64",
cocoapodsConfiguration = "Debug"
)
)
nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion, buildOptions = buildOptions) {
val syncTaskName = ":syncFramework"
buildGradleKts.addCocoapodsBlock( buildGradleKts.addCocoapodsBlock(
""" """
@@ -142,12 +148,13 @@ class CocoaPodsIT : KGPBaseTest() {
assertTasksExecuted(podInstallTaskName) assertTasksExecuted(podInstallTaskName)
} }
buildWithCocoapodsWrapper(linkTaskName) { buildWithCocoapodsWrapper(syncTaskName) {
assertTasksExecuted(linkTaskName) assertTasksExecuted(syncTaskName)
} }
buildWithCocoapodsWrapper(podImportTaskName) { buildWithCocoapodsWrapper(podImportTaskName) {
assertTasksUpToDate(dummyTaskName) assertTasksExecuted(dummyTaskName)
assertOutputContains("Skipping dummy-framework generation because a dynamic framework is already present")
assertTasksUpToDate(podInstallTaskName) assertTasksUpToDate(podInstallTaskName)
} }
} }
@@ -156,7 +163,17 @@ class CocoaPodsIT : KGPBaseTest() {
@DisplayName("Changing framework type and checks UTD") @DisplayName("Changing framework type and checks UTD")
@GradleTest @GradleTest
fun testChangeFrameworkTypeUTD(gradleVersion: GradleVersion) { 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( buildGradleKts.addCocoapodsBlock(
""" """
framework { framework {
@@ -177,9 +194,14 @@ class CocoaPodsIT : KGPBaseTest() {
assertTasksUpToDate(podInstallTaskName) assertTasksUpToDate(podInstallTaskName)
} }
buildWithCocoapodsWrapper(syncTaskName) {
assertTasksExecuted(syncTaskName)
}
buildGradleKts.addFrameworkBlock("isStatic = true") buildGradleKts.addFrameworkBlock("isStatic = true")
buildWithCocoapodsWrapper(podImportTaskName) { buildWithCocoapodsWrapper(podImportTaskName) {
assertTasksExecuted(dummyTaskName) assertTasksExecuted(dummyTaskName)
assertOutputContains("Regenerating dummy-framework because present framework has different linkage")
assertTasksExecuted(podInstallTaskName) assertTasksExecuted(podInstallTaskName)
} }
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.gradle.native package org.jetbrains.kotlin.gradle.native
import org.gradle.util.GradleVersion 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.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_INSTALL_TASK_NAME
import org.jetbrains.kotlin.gradle.testbase.* import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.gradle.util.assertProcessRunResult import org.jetbrains.kotlin.gradle.util.assertProcessRunResult
@@ -46,6 +47,19 @@ class CocoaPodsXcodeIT : KGPBaseTest() {
mapOf("kotlin-library" to null) 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") @DisplayName("Checks xcodebuild for ios-app with a single framework with custom name")
@GradleTest @GradleTest
fun testXcodeUseFrameworksWithCustomFrameworkNameSingle(gradleVersion: GradleVersion) = doTestXcode( fun testXcodeUseFrameworksWithCustomFrameworkNameSingle(gradleVersion: GradleVersion) = doTestXcode(
@@ -66,7 +80,6 @@ class CocoaPodsXcodeIT : KGPBaseTest() {
mapOf("kotlin-library" to null) mapOf("kotlin-library" to null)
) )
@DisplayName("Checks xcodebuild for ios-app using modular headers with a single framework with custom name") @DisplayName("Checks xcodebuild for ios-app using modular headers with a single framework with custom name")
@GradleTest @GradleTest
fun testXcodeUseModularHeadersWithCustomFrameworkNameSingle(gradleVersion: GradleVersion) = doTestXcode( fun testXcodeUseModularHeadersWithCustomFrameworkNameSingle(gradleVersion: GradleVersion) = doTestXcode(
@@ -77,6 +90,21 @@ class CocoaPodsXcodeIT : KGPBaseTest() {
mapOf("kotlin-library" to "MultiplatformLibrary") 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") @DisplayName("Checks xcodebuild for ios-app with kotlin library from root project")
@GradleTest @GradleTest
fun testXcodeBuildsWithKotlinLibraryFromRootProject(gradleVersion: GradleVersion) { fun testXcodeBuildsWithKotlinLibraryFromRootProject(gradleVersion: GradleVersion) {
@@ -163,6 +191,7 @@ class CocoaPodsXcodeIT : KGPBaseTest() {
iosAppLocation: String?, iosAppLocation: String?,
subprojectsToFrameworkNamesMap: Map<String, String?>, subprojectsToFrameworkNamesMap: Map<String, String?>,
arch: String = "x86_64", arch: String = "x86_64",
podInstall: (taskPrefix: String, iosAppPath: Path) -> Unit = ::gradlePodInstall,
) { ) {
gradleProperties gradleProperties
@@ -189,32 +218,46 @@ class CocoaPodsXcodeIT : KGPBaseTest() {
// Generate podspec. // Generate podspec.
build("$taskPrefix:podspec", buildOptions = buildOptions) build("$taskPrefix:podspec", buildOptions = buildOptions)
iosAppLocation?.also { if (iosAppLocation != null) {
// Set import mode for Podfile. // Set import mode for Podfile.
preparePodfile(it, mode) preparePodfile(iosAppLocation, mode)
// Install pods.
build("$taskPrefix:$POD_INSTALL_TASK_NAME", buildOptions = buildOptions)
projectPath.resolve(it).apply { val iosAppPath = projectPath.resolve(iosAppLocation)
// Run Xcode build.
val xcodebuildResult = runProcess( // Install pods.
cmd = listOf( podInstall(taskPrefix, iosAppPath)
"xcodebuild",
"-sdk", "iphonesimulator", // Run Xcode build.
"-configuration", "Release", val xcodebuildResult = runProcess(
"-workspace", "$name.xcworkspace", cmd = listOf(
"-scheme", name, "xcodebuild",
"-arch", arch "-sdk", "iphonesimulator",
), "-configuration", "Release",
environmentVariables = environmentVariables.environmentalVariables, "-workspace", "${iosAppPath.name}.xcworkspace",
workingDir = this.toFile(), "-scheme", iosAppPath.name,
) "-arch", arch
assertProcessRunResult(xcodebuildResult) { ),
assertEquals(0, exitCode, "Exit code mismatch for `xcodebuild`.") 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(),
)
}
@@ -10,6 +10,7 @@ import org.gradle.api.Action
import org.gradle.api.Named import org.gradle.api.Named
import org.gradle.api.NamedDomainObjectSet import org.gradle.api.NamedDomainObjectSet
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.* import org.gradle.api.tasks.*
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.* import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.*
@@ -118,8 +119,8 @@ abstract class CocoapodsExtension @Inject constructor(private val project: Proje
anyFramework anyFramework
} }
internal val podFrameworkName = anyPodFramework.map { it.baseName.asValidFrameworkName() } internal val podFrameworkName: Provider<String> = anyPodFramework.map { it.baseName.asValidFrameworkName() }
internal val podFrameworkIsStatic = anyPodFramework.map { it.isStatic } internal val podFrameworkIsStatic: Provider<Boolean> = anyPodFramework.map { it.isStatic }
/** /**
* Configure custom Xcode Configurations to Native Build Types mapping * Configure custom Xcode Configurations to Native Build Types mapping
@@ -60,8 +60,6 @@ internal class CocoapodsBuildDirs(private val layout: ProjectLayout) {
get() = layout.buildDirectory.dir("cocoapods") get() = layout.buildDirectory.dir("cocoapods")
val framework: Provider<Directory> val framework: Provider<Directory>
get() = dir("framework") get() = dir("framework")
val dummyFramework: Provider<Directory>
get() = dir("dummy.framework")
val defs: Provider<Directory> val defs: Provider<Directory>
get() = dir("defs") get() = dir("defs")
val publish: Provider<Directory> val publish: Provider<Directory>
@@ -394,8 +392,9 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
cocoapodsExtension: CocoapodsExtension cocoapodsExtension: CocoapodsExtension
) { ) {
project.registerTask<DummyFrameworkTask>(DUMMY_FRAMEWORK_TASK_NAME) { task -> project.registerTask<DummyFrameworkTask>(DUMMY_FRAMEWORK_TASK_NAME) { task ->
task.frameworkName.set(cocoapodsExtension.podFrameworkName) task.frameworkName.convention(cocoapodsExtension.podFrameworkName)
task.useStaticFramework.set(cocoapodsExtension.podFrameworkIsStatic) 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<Project> {
cocoapodsExtension: CocoapodsExtension cocoapodsExtension: CocoapodsExtension
) { ) {
val podspecTaskProvider = project.tasks.named<PodspecTask>(POD_SPEC_TASK_NAME) val podspecTaskProvider = project.tasks.named<PodspecTask>(POD_SPEC_TASK_NAME)
val dummyFrameworkTaskProvider = project.tasks.named<DummyFrameworkTask>(DUMMY_FRAMEWORK_TASK_NAME) val dummyFrameworkTaskProvider = project.tasks.named(DUMMY_FRAMEWORK_TASK_NAME)
project.registerTask<PodInstallTask>(POD_INSTALL_TASK_NAME) { task -> project.registerTask<PodInstallTask>(POD_INSTALL_TASK_NAME) { task ->
task.group = TASK_GROUP task.group = TASK_GROUP
task.description = "Invokes `pod install` call within Podfile location directory" task.description = "Invokes `pod install` call within Podfile location directory"
task.podfile.set(project.provider { cocoapodsExtension.podfile }) task.podfile.set(project.provider { cocoapodsExtension.podfile })
task.podspec.set(podspecTaskProvider.map { it.outputFile }) task.podspec.set(podspecTaskProvider.map { it.outputFile })
task.useStaticFramework.set(cocoapodsExtension.podFrameworkIsStatic)
task.frameworkName.set(cocoapodsExtension.podFrameworkName) task.frameworkName.set(cocoapodsExtension.podFrameworkName)
task.specRepos.set(project.provider { cocoapodsExtension.specRepos }) task.specRepos.set(project.provider { cocoapodsExtension.specRepos })
task.pods.set(cocoapodsExtension.pods) task.pods.set(cocoapodsExtension.pods)
task.dummyFramework.set(dummyFrameworkTaskProvider.map { it.outputFramework.get() })
task.dependsOn(podspecTaskProvider) task.dependsOn(podspecTaskProvider)
task.dependsOn(dummyFrameworkTaskProvider)
} }
} }
@@ -7,17 +7,16 @@
package org.jetbrains.kotlin.gradle.tasks package org.jetbrains.kotlin.gradle.tasks
import org.gradle.api.DefaultTask 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.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskAction
import org.gradle.work.DisableCachingByDefault import org.gradle.work.DisableCachingByDefault
import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs import org.jetbrains.kotlin.gradle.utils.MachO
import org.jetbrains.kotlin.gradle.utils.mapToFile import org.jetbrains.kotlin.gradle.utils.getFile
import java.io.File import java.io.File
import javax.inject.Inject
/** /**
* Creates a dummy framework in the target directory. * 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. * and then replace it with the real one during a real build process.
*/ */
@DisableCachingByDefault @DisableCachingByDefault
abstract class DummyFrameworkTask @Inject constructor(projectLayout: ProjectLayout) : DefaultTask() { abstract class DummyFrameworkTask : DefaultTask() {
@get:Input @get:Input
abstract val frameworkName: Property<String> abstract val frameworkName: Property<String>
@@ -40,13 +39,18 @@ abstract class DummyFrameworkTask @Inject constructor(projectLayout: ProjectLayo
abstract val useStaticFramework: Property<Boolean> abstract val useStaticFramework: Property<Boolean>
@get:OutputDirectory @get:OutputDirectory
val outputFramework: Provider<File> = 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 private val dummyFrameworkResource: String
get() { get() = "/cocoapods/$linkageName/dummy.framework/"
val staticOrDynamic = if (!useStaticFramework.get()) "dynamic" else "static"
return "/cocoapods/$staticOrDynamic/dummy.framework/"
}
private fun copyResource(from: String, to: File) { private fun copyResource(from: String, to: File) {
to.parentFile.mkdirs() to.parentFile.mkdirs()
@@ -71,23 +75,22 @@ abstract class DummyFrameworkTask @Inject constructor(projectLayout: ProjectLayo
private fun copyFrameworkFile(relativeFrom: String, relativeTo: String = relativeFrom) = private fun copyFrameworkFile(relativeFrom: String, relativeTo: String = relativeFrom) =
copyResource( copyResource(
"$dummyFrameworkResource$relativeFrom", "$dummyFrameworkResource$relativeFrom",
outputFramework.get().resolve(relativeTo) outputFramework.getFile().resolve(relativeTo)
) )
private fun copyFrameworkTextFile( private fun copyFrameworkTextFile(
relativeFrom: String, relativeFrom: String,
relativeTo: String = relativeFrom, relativeTo: String = relativeFrom,
transform: (String) -> String = { it } transform: (String) -> String = { it },
) = copyTextResource( ) = copyTextResource(
"$dummyFrameworkResource$relativeFrom", "$dummyFrameworkResource$relativeFrom",
outputFramework.get().resolve(relativeTo), outputFramework.getFile().resolve(relativeTo),
transform transform
) )
@TaskAction private fun copyFramework() {
fun create() {
// Reset the destination directory // Reset the destination directory
with(outputFramework.get()) { with(outputFramework.getFile()) {
deleteRecursively() deleteRecursively()
mkdirs() 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()
}
}
}
} }
@@ -7,20 +7,16 @@
package org.jetbrains.kotlin.gradle.targets.native.tasks 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.ListProperty
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.* import org.gradle.api.tasks.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency 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.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.MissingCocoapodsMessage
import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingSpecReposMessage import org.jetbrains.kotlin.gradle.targets.native.cocoapods.MissingSpecReposMessage
import java.io.File import java.io.File
import javax.inject.Inject
abstract class PodInstallTask @Inject constructor(projectLayout: ProjectLayout) : AbstractPodInstallTask() { abstract class PodInstallTask : AbstractPodInstallTask() {
@get:Optional @get:Optional
@get:InputFile @get:InputFile
@@ -29,38 +25,15 @@ abstract class PodInstallTask @Inject constructor(projectLayout: ProjectLayout)
@get:Input @get:Input
abstract val frameworkName: Property<String> abstract val frameworkName: Property<String>
@get:Input
abstract val useStaticFramework: Property<Boolean>
@get:Nested @get:Nested
abstract val specRepos: Property<SpecRepos> abstract val specRepos: Property<SpecRepos>
@get:Nested @get:Nested
abstract val pods: ListProperty<CocoapodsDependency> abstract val pods: ListProperty<CocoapodsDependency>
@get:InputDirectory
abstract val dummyFramework: Property<File>
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<File>.rename(dest: Provider<File>) = 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? { override fun handleError(retCode: Int, error: String, process: Process): String? {
val specReposMessages = MissingSpecReposMessage(specRepos.get()).missingMessage val specReposMessages = MissingSpecReposMessage(specRepos.get()).missingMessage
val cocoapodsMessages = pods.get().map { MissingCocoapodsMessage(it).missingMessage } val cocoapodsMessages = pods.get().map { MissingCocoapodsMessage(it).missingMessage }
@@ -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
}
}
}
@@ -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)
}
}
}
}