You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
240 lines
9.7 KiB
240 lines
9.7 KiB
10 months ago
|
/*
|
||
|
* Copyright 2020-2023 JetBrains s.r.o. and respective authors and developers.
|
||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
|
||
|
*/
|
||
|
|
||
6 months ago
|
package org.jetbrains.compose.resources.ios
|
||
10 months ago
|
|
||
|
import org.gradle.api.Project
|
||
|
import org.gradle.api.file.Directory
|
||
|
import org.gradle.api.provider.Provider
|
||
7 months ago
|
import org.gradle.api.tasks.Input
|
||
10 months ago
|
import org.gradle.api.tasks.TaskContainer
|
||
7 months ago
|
import org.gradle.api.tasks.TaskAction
|
||
6 months ago
|
import org.jetbrains.compose.desktop.application.internal.ComposeProperties
|
||
10 months ago
|
import org.jetbrains.compose.experimental.uikit.internal.utils.asIosNativeTargetOrNull
|
||
|
import org.jetbrains.compose.experimental.uikit.internal.utils.cocoapodsExt
|
||
|
import org.jetbrains.compose.experimental.uikit.internal.utils.withCocoapodsPlugin
|
||
7 months ago
|
import org.jetbrains.compose.experimental.uikit.tasks.AbstractComposeIosTask
|
||
10 months ago
|
import org.jetbrains.compose.internal.utils.joinLowerCamelCase
|
||
|
import org.jetbrains.compose.internal.utils.new
|
||
|
import org.jetbrains.compose.internal.utils.registerOrConfigure
|
||
|
import org.jetbrains.compose.internal.utils.uppercaseFirstChar
|
||
|
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
|
||
|
import org.jetbrains.kotlin.gradle.plugin.mpp.Framework
|
||
|
import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType
|
||
|
import java.io.File
|
||
|
|
||
10 months ago
|
private val incompatiblePlugins = listOf(
|
||
|
"dev.icerock.mobile.multiplatform-resources",
|
||
|
"io.github.skeptick.libres",
|
||
|
)
|
||
|
|
||
10 months ago
|
internal fun Project.configureSyncTask(mppExt: KotlinMultiplatformExtension) {
|
||
10 months ago
|
fun reportSyncIsDisabled(reason: String) {
|
||
|
logger.info("Compose Multiplatform resource management for iOS is disabled: $reason")
|
||
|
}
|
||
|
|
||
6 months ago
|
if (!ComposeProperties.syncResources(providers).get()) {
|
||
|
reportSyncIsDisabled("'${ComposeProperties.SYNC_RESOURCES_PROPERTY}' value is 'false'")
|
||
10 months ago
|
return
|
||
|
}
|
||
|
|
||
|
for (incompatiblePluginId in incompatiblePlugins) {
|
||
|
if (project.plugins.hasPlugin(incompatiblePluginId)) {
|
||
|
reportSyncIsDisabled("resource management is not compatible with '$incompatiblePluginId'")
|
||
|
return
|
||
|
}
|
||
|
}
|
||
10 months ago
|
|
||
6 months ago
|
with(SyncIosResourcesContext(project, mppExt)) {
|
||
10 months ago
|
configureSyncResourcesTasks()
|
||
|
configureCocoapodsResourcesAttribute()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private class SyncIosResourcesContext(
|
||
|
val project: Project,
|
||
|
val mppExt: KotlinMultiplatformExtension
|
||
|
) {
|
||
|
fun syncDirFor(framework: Framework): Provider<Directory> {
|
||
|
val providers = framework.project.providers
|
||
|
return if (framework.isCocoapodsFramework) {
|
||
|
project.layout.buildDirectory.dir("compose/ios/${framework.baseName}/compose-resources/")
|
||
|
} else {
|
||
|
providers.environmentVariable("BUILT_PRODUCTS_DIR")
|
||
|
.zip(providers.environmentVariable("CONTENTS_FOLDER_PATH")) { builtProductsDir, contentsFolderPath ->
|
||
|
File(builtProductsDir)
|
||
|
.resolve(contentsFolderPath)
|
||
|
.resolve("compose-resources")
|
||
|
.canonicalPath
|
||
|
}.flatMap {
|
||
|
framework.project.objects.directoryProperty().apply { set(File(it)) }
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
fun configureEachIosFramework(fn: (Framework) -> Unit) {
|
||
|
mppExt.targets.all { target ->
|
||
|
target.asIosNativeTargetOrNull()?.let { iosTarget ->
|
||
|
iosTarget.binaries.withType(Framework::class.java).configureEach { framework ->
|
||
|
fn(framework)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private const val RESOURCES_SPEC_ATTR = "resources"
|
||
|
private fun SyncIosResourcesContext.configureCocoapodsResourcesAttribute() {
|
||
|
project.withCocoapodsPlugin {
|
||
|
project.gradle.taskGraph.whenReady {
|
||
|
val cocoapodsExt = mppExt.cocoapodsExt
|
||
|
val specAttributes = cocoapodsExt.extraSpecAttributes
|
||
|
val resourcesSpec = specAttributes[RESOURCES_SPEC_ATTR]
|
||
|
if (!resourcesSpec.isNullOrBlank()) {
|
||
9 months ago
|
error("""
|
||
|
|Kotlin.cocoapods.extraSpecAttributes["resources"] is not compatible with Compose Multiplatform's resources management for iOS.
|
||
|
| * Recommended action: remove extraSpecAttributes["resources"] from '${project.buildFile}' and run '${project.path}:podInstall' once;
|
||
6 months ago
|
| * Alternative action: turn off Compose Multiplatform's resources management for iOS by adding '${ComposeProperties.SYNC_RESOURCES_PROPERTY}=false' to your gradle.properties;
|
||
9 months ago
|
""".trimMargin())
|
||
10 months ago
|
}
|
||
|
cocoapodsExt.framework {
|
||
|
val syncDir = syncDirFor(this).get().asFile
|
||
|
specAttributes[RESOURCES_SPEC_ATTR] = "['${syncDir.relativeTo(project.projectDir).path}']"
|
||
|
project.tasks.named("podInstall").configure {
|
||
|
it.doFirst {
|
||
|
syncDir.mkdirs()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
7 months ago
|
/**
|
||
|
* Since Xcode 15, there is a new default setting: `ENABLE_USER_SCRIPT_SANDBOXING = YES`
|
||
|
* It's set in project.pbxproj
|
||
|
*
|
||
|
* SyncComposeResourcesForIosTask fails to work with it right now.
|
||
|
*
|
||
|
* Gradle attempts to create an output folder for SyncComposeResourcesForIosTask on our behalf,
|
||
|
* so we can't handle an exception when it occurs. Therefore, we make SyncComposeResourcesForIosTask
|
||
|
* depend on CheckCanAccessComposeResourcesDirectory, where we check ENABLE_USER_SCRIPT_SANDBOXING.
|
||
|
*/
|
||
|
internal abstract class CheckCanAccessComposeResourcesDirectory : AbstractComposeIosTask() {
|
||
|
|
||
|
@get:Input
|
||
|
val enabled = providers.environmentVariable("ENABLE_USER_SCRIPT_SANDBOXING")
|
||
|
.orElse("NOT_DEFINED")
|
||
|
.map { it == "YES" }
|
||
|
|
||
|
private val errorMessage = """
|
||
|
|Failed to sync compose resources!
|
||
|
|Please make sure ENABLE_USER_SCRIPT_SANDBOXING is set to 'NO' in 'project.pbxproj'
|
||
|
|""".trimMargin()
|
||
|
|
||
|
@TaskAction
|
||
|
fun run() {
|
||
|
if (enabled.get()) {
|
||
|
logger.error(errorMessage)
|
||
|
throw IllegalStateException(
|
||
|
"Sandbox environment detected (ENABLE_USER_SCRIPT_SANDBOXING = YES). It's not supported so far."
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
10 months ago
|
private fun SyncIosResourcesContext.configureSyncResourcesTasks() {
|
||
|
val lazyTasksDependencies = LazyTasksDependencyConfigurator(project.tasks)
|
||
|
configureEachIosFramework { framework ->
|
||
|
val frameworkClassifier = framework.namePrefix.uppercaseFirstChar()
|
||
|
val syncResourcesTaskName = "sync${frameworkClassifier}ComposeResourcesForIos"
|
||
7 months ago
|
val checkSyncResourcesTaskName = "checkCanSync${frameworkClassifier}ComposeResourcesForIos"
|
||
|
val checkNoSandboxTask = framework.project.tasks.registerOrConfigure<CheckCanAccessComposeResourcesDirectory>(checkSyncResourcesTaskName) {}
|
||
10 months ago
|
val syncTask = framework.project.tasks.registerOrConfigure<SyncComposeResourcesForIosTask>(syncResourcesTaskName) {
|
||
7 months ago
|
dependsOn(checkNoSandboxTask)
|
||
10 months ago
|
outputDir.set(syncDirFor(framework))
|
||
|
iosTargets.add(iosTargetResourcesProvider(framework))
|
||
|
}
|
||
|
with (lazyTasksDependencies) {
|
||
|
if (framework.isCocoapodsFramework) {
|
||
|
"syncFramework".lazyDependsOn(syncTask.name)
|
||
|
} else {
|
||
|
"embedAndSign${frameworkClassifier}AppleFrameworkForXcode".lazyDependsOn(syncTask.name)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private val Framework.isCocoapodsFramework: Boolean
|
||
|
get() = name.startsWith("pod")
|
||
|
|
||
|
private val Framework.namePrefix: String
|
||
|
get() = extractPrefixFromBinaryName(
|
||
|
name,
|
||
|
buildType,
|
||
|
outputKind.taskNameClassifier
|
||
|
)
|
||
|
|
||
|
private fun extractPrefixFromBinaryName(name: String, buildType: NativeBuildType, outputKindClassifier: String): String {
|
||
|
val suffix = joinLowerCamelCase(buildType.getName(), outputKindClassifier)
|
||
|
return if (name == suffix)
|
||
|
""
|
||
|
else
|
||
|
name.substringBeforeLast(suffix.uppercaseFirstChar())
|
||
|
}
|
||
|
|
||
|
private fun iosTargetResourcesProvider(framework: Framework): Provider<IosTargetResources> {
|
||
|
val kotlinTarget = framework.target
|
||
|
val project = framework.project
|
||
|
return project.provider {
|
||
|
val resourceDirs = framework.compilation.allKotlinSourceSets
|
||
|
.flatMap { sourceSet ->
|
||
|
sourceSet.resources.srcDirs.map { it.canonicalPath }
|
||
|
}
|
||
|
project.objects.new<IosTargetResources>().apply {
|
||
|
name.set(kotlinTarget.name)
|
||
|
konanTarget.set(kotlinTarget.konanTarget.name)
|
||
|
dirs.set(resourceDirs)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Ensures, that a dependency between tasks is set up,
|
||
|
* when a dependent task (fromTask) is created, while avoiding eager configuration.
|
||
|
*/
|
||
|
private class LazyTasksDependencyConfigurator(private val tasks: TaskContainer) {
|
||
|
private val existingDependencies = HashSet<Pair<String, String>>()
|
||
|
private val requestedDependencies = HashMap<String, MutableSet<String>>()
|
||
|
|
||
|
init {
|
||
|
tasks.configureEach { fromTask ->
|
||
|
val onTasks = requestedDependencies.remove(fromTask.name) ?: return@configureEach
|
||
|
for (onTaskName in onTasks) {
|
||
|
val dependency = fromTask.name to onTaskName
|
||
|
if (existingDependencies.add(dependency)) {
|
||
|
fromTask.dependsOn(onTaskName)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fun String.lazyDependsOn(dependencyTask: String) {
|
||
|
val dependingTask = this
|
||
|
val dependency = dependingTask to dependencyTask
|
||
|
if (dependency in existingDependencies) return
|
||
|
|
||
|
if (dependingTask in tasks.names) {
|
||
|
tasks.named(dependingTask).configure { it.dependsOn(dependencyTask) }
|
||
|
existingDependencies.add(dependency)
|
||
|
} else {
|
||
|
requestedDependencies
|
||
|
.getOrPut(dependingTask) { HashSet() }
|
||
|
.add(dependencyTask)
|
||
|
}
|
||
|
}
|
||
|
}
|