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.
239 lines
9.7 KiB
239 lines
9.7 KiB
/* |
|
* 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. |
|
*/ |
|
|
|
package org.jetbrains.compose.resources.ios |
|
|
|
import org.gradle.api.Project |
|
import org.gradle.api.file.Directory |
|
import org.gradle.api.provider.Provider |
|
import org.gradle.api.tasks.Input |
|
import org.gradle.api.tasks.TaskContainer |
|
import org.gradle.api.tasks.TaskAction |
|
import org.jetbrains.compose.desktop.application.internal.ComposeProperties |
|
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 |
|
import org.jetbrains.compose.experimental.uikit.tasks.AbstractComposeIosTask |
|
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 |
|
|
|
private val incompatiblePlugins = listOf( |
|
"dev.icerock.mobile.multiplatform-resources", |
|
"io.github.skeptick.libres", |
|
) |
|
|
|
internal fun Project.configureSyncTask(mppExt: KotlinMultiplatformExtension) { |
|
fun reportSyncIsDisabled(reason: String) { |
|
logger.info("Compose Multiplatform resource management for iOS is disabled: $reason") |
|
} |
|
|
|
if (!ComposeProperties.syncResources(providers).get()) { |
|
reportSyncIsDisabled("'${ComposeProperties.SYNC_RESOURCES_PROPERTY}' value is 'false'") |
|
return |
|
} |
|
|
|
for (incompatiblePluginId in incompatiblePlugins) { |
|
if (project.plugins.hasPlugin(incompatiblePluginId)) { |
|
reportSyncIsDisabled("resource management is not compatible with '$incompatiblePluginId'") |
|
return |
|
} |
|
} |
|
|
|
with(SyncIosResourcesContext(project, mppExt)) { |
|
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()) { |
|
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; |
|
| * Alternative action: turn off Compose Multiplatform's resources management for iOS by adding '${ComposeProperties.SYNC_RESOURCES_PROPERTY}=false' to your gradle.properties; |
|
""".trimMargin()) |
|
} |
|
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() |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* 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." |
|
) |
|
} |
|
} |
|
} |
|
|
|
private fun SyncIosResourcesContext.configureSyncResourcesTasks() { |
|
val lazyTasksDependencies = LazyTasksDependencyConfigurator(project.tasks) |
|
configureEachIosFramework { framework -> |
|
val frameworkClassifier = framework.namePrefix.uppercaseFirstChar() |
|
val syncResourcesTaskName = "sync${frameworkClassifier}ComposeResourcesForIos" |
|
val checkSyncResourcesTaskName = "checkCanSync${frameworkClassifier}ComposeResourcesForIos" |
|
val checkNoSandboxTask = framework.project.tasks.registerOrConfigure<CheckCanAccessComposeResourcesDirectory>(checkSyncResourcesTaskName) {} |
|
val syncTask = framework.project.tasks.registerOrConfigure<SyncComposeResourcesForIosTask>(syncResourcesTaskName) { |
|
dependsOn(checkNoSandboxTask) |
|
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) |
|
} |
|
} |
|
}
|
|
|