[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.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)
}
@@ -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<String, String?>,
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(),
)
}
@@ -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<String> = anyPodFramework.map { it.baseName.asValidFrameworkName() }
internal val podFrameworkIsStatic: Provider<Boolean> = anyPodFramework.map { it.isStatic }
/**
* 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")
val framework: Provider<Directory>
get() = dir("framework")
val dummyFramework: Provider<Directory>
get() = dir("dummy.framework")
val defs: Provider<Directory>
get() = dir("defs")
val publish: Provider<Directory>
@@ -394,8 +392,9 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
cocoapodsExtension: CocoapodsExtension
) {
project.registerTask<DummyFrameworkTask>(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<Project> {
cocoapodsExtension: CocoapodsExtension
) {
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 ->
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)
}
}
@@ -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<String>
@@ -40,13 +39,18 @@ abstract class DummyFrameworkTask @Inject constructor(projectLayout: ProjectLayo
abstract val useStaticFramework: Property<Boolean>
@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
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()
}
}
}
}
@@ -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<String>
@get:Input
abstract val useStaticFramework: Property<Boolean>
@get:Nested
abstract val specRepos: Property<SpecRepos>
@get:Nested
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? {
val specReposMessages = MissingSpecReposMessage(specRepos.get()).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)
}
}
}
}