Browse Source

Simplify resource management for iOS (#3340)

* Simplify resource management for iOS

Introduces new a new task 'sync<FRAMEWORK_CLASSIFIER>ComposeIosResources',
which collects resources from all source sets, included in iOS targets.

With this change:
* CocoaPods integration does not require any configuration or calling 'pod install' after changing resources.
    * Important: existing projects need to remove 'extraSpecAttributes["resources"] = ...' from build scripts, and rerun `./gradlew podInstall` once!
* Without CocoaPods, the resource directory should be added to XCode build phases once.

Resolves #3073
Resolves #3113
Resolves #3066
ivan.matkov/skiko-dsl v1.5.0-dev1104
Alexey Tsvetkov 1 year ago committed by GitHub
parent
commit
16114b2f0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      components/gradle.properties
  2. 6
      components/resources/demo/iosApp/Podfile
  3. 56
      components/resources/demo/iosApp/iosApp.xcodeproj/project.pbxproj
  4. 44
      components/resources/demo/shared/build.gradle.kts
  5. 7
      components/resources/demo/shared/src/iosMain/kotlin/main.ios.kt
  6. 6
      components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt
  7. 4
      examples/imageviewer/shared/build.gradle.kts
  8. 12
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt
  9. 24
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt
  10. 2
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/tasks/AbstractConfigureDesktopPreviewTask.kt
  11. 2
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractComposeDesktopTask.kt
  12. 21
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/internal/checkExperimentalTargets.kt
  13. 56
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/IosTargetResources.kt
  14. 178
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/configureSyncIosResources.kt
  15. 38
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/determineIosKonanTargetsFromEnv.kt
  16. 18
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/IosGradleProperties.kt
  17. 26
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/cocoapodsDslHelpers.kt
  18. 22
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/kotlinNativeTargetUtils.kt
  19. 4
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/AbstractComposeIosTask.kt
  20. 100
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/SyncComposeResourcesForIosTask.kt
  21. 17
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/providerUtils.kt
  22. 12
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/taskUtils.kt

2
components/gradle.properties

@ -5,7 +5,7 @@ kotlin.code.style=official
# __KOTLIN_COMPOSE_VERSION__
kotlin.version=1.8.20
# __LATEST_COMPOSE_RELEASE_VERSION__
compose.version=1.4.1
compose.version=0.0.0-dev1101
agp.version=7.3.1
org.jetbrains.compose.experimental.jscanvas.enabled=true
org.jetbrains.compose.experimental.macos.enabled=true

6
components/resources/demo/iosApp/Podfile

@ -1,6 +0,0 @@
target 'iosApp' do
use_frameworks!
platform :ios, '14.1'
pod 'shared', :path => '../shared'
project 'iosApp.xcodeproj'
end

56
components/resources/demo/iosApp/iosApp.xcodeproj/project.pbxproj

@ -8,16 +8,12 @@
/* Begin PBXBuildFile section */
2152FB042600AC8F00CF470E /* iosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iosApp.swift */; };
C1FC908188C4E8695729CB06 /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DE96E47030356CE6AD9794A /* Pods_iosApp.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
1EB65E27D2C0F884D0A1A133 /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = "<group>"; };
2152FB032600AC8F00CF470E /* iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosApp.swift; sourceTree = "<group>"; };
3D7A606AB0AD7636269BD9D0 /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = "<group>"; };
7555FF7B242A565900829871 /* ResourcesDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ResourcesDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8DE96E47030356CE6AD9794A /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AB3632DC29227652001CCB65 /* TeamId.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = TeamId.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -26,7 +22,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C1FC908188C4E8695729CB06 /* Pods_iosApp.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -72,7 +67,6 @@
B62309C7396AD7BF607A63B2 /* Frameworks */ = {
isa = PBXGroup;
children = (
8DE96E47030356CE6AD9794A /* Pods_iosApp.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -80,8 +74,6 @@
E1DAFBE8E1CFC0878361EF0E /* Pods */ = {
isa = PBXGroup;
children = (
1EB65E27D2C0F884D0A1A133 /* Pods-iosApp.debug.xcconfig */,
3D7A606AB0AD7636269BD9D0 /* Pods-iosApp.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@ -93,11 +85,10 @@
isa = PBXNativeTarget;
buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */;
buildPhases = (
E8D673591E7196AEA2EA10E2 /* [CP] Check Pods Manifest.lock */,
05FBB7462A65505400D51BB4 /* Compile Kotlin */,
7555FF77242A565900829871 /* Sources */,
7555FF79242A565900829871 /* Resources */,
9964867F0862B4D9FB6ABFC7 /* Frameworks */,
A51DDDB74597C98E89765935 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@ -152,24 +143,7 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
A51DDDB74597C98E89765935 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n";
showEnvVarsInLog = 0;
};
E8D673591E7196AEA2EA10E2 /* [CP] Check Pods Manifest.lock */ = {
05FBB7462A65505400D51BB4 /* Compile Kotlin */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -177,19 +151,15 @@
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
name = "Compile Kotlin";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-iosApp-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
shellScript = "cd \"$SRCROOT/..\"\n../.././gradlew :resources:demo:shared:embedAndSignAppleFrameworkForXcode\n";
};
/* End PBXShellScriptBuildPhase section */
@ -325,7 +295,6 @@
};
7555FFA6242A565B00829871 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 1EB65E27D2C0F884D0A1A133 /* Pods-iosApp.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
@ -333,14 +302,20 @@
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "${TEAM_ID}";
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)";
INFOPLIST_FILE = iosApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
OTHER_LDFLAGS = (
"$(inherited)",
"-framework",
shared,
);
PRODUCT_BUNDLE_IDENTIFIER = "org.jetbrains.ResourcesDemo${TEAM_ID}";
PRODUCT_NAME = "ResourcesDemo";
PRODUCT_NAME = ResourcesDemo;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@ -349,7 +324,6 @@
};
7555FFA7242A565B00829871 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 3D7A606AB0AD7636269BD9D0 /* Pods-iosApp.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
@ -357,14 +331,20 @@
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "${TEAM_ID}";
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n";
INFOPLIST_FILE = iosApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
OTHER_LDFLAGS = (
"$(inherited)",
"-framework",
shared,
);
PRODUCT_BUNDLE_IDENTIFIER = "org.jetbrains.ResourcesDemo${TEAM_ID}";
PRODUCT_NAME = "ResourcesDemo";
PRODUCT_NAME = ResourcesDemo;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";

44
components/resources/demo/shared/build.gradle.kts

@ -1,6 +1,5 @@
plugins {
kotlin("multiplatform")
kotlin("native.cocoapods")
id("com.android.library")
id("org.jetbrains.compose")
}
@ -10,8 +9,16 @@ version = "1.0-SNAPSHOT"
kotlin {
android()
jvm("desktop")
ios()
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "shared"
isStatic = true
}
}
js(IR) {
browser()
binaries.executable()
@ -31,18 +38,6 @@ kotlin {
}
}
cocoapods {
summary = "Shared code for the sample"
homepage = "https://github.com/JetBrains/compose-jb"
ios.deploymentTarget = "14.1"
podfile = project.file("../iosApp/Podfile")
framework {
baseName = "shared"
isStatic = true
}
extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']"
}
sourceSets {
val commonMain by getting {
dependencies {
@ -53,13 +48,28 @@ kotlin {
implementation(project(":resources:library"))
}
}
val iosMain by getting
val iosTest by getting
val iosMain by creating {
dependsOn(commonMain)
}
val iosTest by creating {
}
val iosX64Main by getting {
dependsOn(iosMain)
}
val iosArm64Main by getting {
dependsOn(iosMain)
}
val iosSimulatorArm64Main by getting {
dependsOn(iosMain)
}
val iosX64Test by getting {
dependsOn(iosMain)
}
val iosArm64Test by getting {
dependsOn(iosMain)
}
val iosSimulatorArm64Test by getting {
dependsOn(iosTest)
dependsOn(iosMain)
}
val desktopMain by getting {
dependencies {

7
components/resources/demo/shared/src/iosMain/kotlin/main.ios.kt

@ -8,12 +8,11 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Application
import androidx.compose.ui.window.ComposeUIViewController
import org.jetbrains.compose.resources.demo.shared.UseResources
import platform.UIKit.UIViewController
fun MainViewController(): UIViewController =
Application("Resources demo") {
fun MainViewController() =
ComposeUIViewController {
Column {
Box(
modifier = Modifier

6
components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt

@ -18,8 +18,10 @@ actual fun resource(path: String): Resource = UIKitResourceImpl(path)
@ExperimentalResourceApi
private class UIKitResourceImpl(path: String) : AbstractResourceImpl(path) {
override suspend fun readBytes(): ByteArray {
val absolutePath = NSBundle.mainBundle.resourcePath + "/" + path
val contentsAtPath: NSData? = NSFileManager.defaultManager().contentsAtPath(absolutePath)
val fileManager = NSFileManager.defaultManager()
// todo: support fallback path at bundle root?
val composeResourcesPath = NSBundle.mainBundle.resourcePath + "/compose-resources/" + path
val contentsAtPath: NSData? = fileManager.contentsAtPath(composeResourcesPath)
if (contentsAtPath != null) {
val byteArray = ByteArray(contentsAtPath.length.toInt())
byteArray.usePinned {

4
examples/imageviewer/shared/build.gradle.kts

@ -26,8 +26,8 @@ kotlin {
baseName = "shared"
isStatic = true
}
extraSpecAttributes["resources"] =
"['src/commonMain/resources/**', 'src/iosMain/resources/**']"
extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']"
}
sourceSets {

12
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt

@ -10,9 +10,6 @@ package org.jetbrains.compose
import groovy.lang.Closure
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.ComponentMetadataContext
import org.gradle.api.artifacts.ComponentMetadataRule
import org.gradle.api.artifacts.dsl.ComponentModuleMetadataHandler
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.artifacts.dsl.RepositoryHandler
import org.gradle.api.artifacts.repositories.MavenArtifactRepository
@ -24,6 +21,9 @@ import org.jetbrains.compose.desktop.preview.internal.initializePreview
import org.jetbrains.compose.experimental.dsl.ExperimentalExtension
import org.jetbrains.compose.experimental.internal.configureExperimentalTargetsFlagsCheck
import org.jetbrains.compose.experimental.internal.configureExperimental
import org.jetbrains.compose.experimental.uikit.internal.resources.configureSyncTask
import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID
import org.jetbrains.compose.internal.mppExt
import org.jetbrains.compose.internal.utils.currentTarget
import org.jetbrains.compose.web.WebExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
@ -53,7 +53,11 @@ class ComposePlugin : Plugin<Project> {
project.afterEvaluate {
configureDesktop(project, desktopExtension)
project.configureExperimental(composeExtension, experimentalExtension)
project.configureExperimentalTargetsFlagsCheck()
project.plugins.withId(KOTLIN_MPP_PLUGIN_ID) {
val mppExt = project.mppExt
project.configureExperimentalTargetsFlagsCheck(mppExt)
project.configureSyncTask(mppExt)
}
project.tasks.withType(KotlinCompile::class.java).configureEach {
it.kotlinOptions.apply {

24
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt

@ -7,6 +7,8 @@ package org.jetbrains.compose.desktop.application.internal
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory
import org.jetbrains.compose.internal.utils.findProperty
import org.jetbrains.compose.internal.utils.toBooleanProvider
internal object ComposeProperties {
internal const val VERBOSE = "compose.desktop.verbose"
@ -20,13 +22,13 @@ internal object ComposeProperties {
internal const val MAC_NOTARIZATION_ASC_PROVIDER = "compose.desktop.mac.notarization.ascProvider"
fun isVerbose(providers: ProviderFactory): Provider<Boolean> =
providers.findProperty(VERBOSE).toBoolean()
providers.findProperty(VERBOSE).toBooleanProvider(false)
fun preserveWorkingDir(providers: ProviderFactory): Provider<Boolean> =
providers.findProperty(PRESERVE_WD).toBoolean()
providers.findProperty(PRESERVE_WD).toBooleanProvider(false)
fun macSign(providers: ProviderFactory): Provider<Boolean> =
providers.findProperty(MAC_SIGN).toBoolean()
providers.findProperty(MAC_SIGN).toBooleanProvider(false)
fun macSignIdentity(providers: ProviderFactory): Provider<String?> =
providers.findProperty(MAC_SIGN_ID)
@ -45,20 +47,4 @@ internal object ComposeProperties {
fun macNotarizationAscProvider(providers: ProviderFactory): Provider<String?> =
providers.findProperty(MAC_NOTARIZATION_ASC_PROVIDER)
private fun ProviderFactory.findProperty(prop: String): Provider<String?> =
provider {
gradleProperty(prop).forUseAtConfigurationTimeSafe().orNull
}
private fun Provider<String?>.forUseAtConfigurationTimeSafe(): Provider<String?> =
try {
forUseAtConfigurationTime()
} catch (e: NoSuchMethodError) {
// todo: remove once we drop support for Gradle 6.4
this
}
private fun Provider<String?>.toBoolean(): Provider<Boolean> =
orElse("false").map { "true" == it }
}

2
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/tasks/AbstractConfigureDesktopPreviewTask.kt

@ -28,10 +28,12 @@ abstract class AbstractConfigureDesktopPreviewTask : AbstractComposeDesktopTask(
@get:Optional
internal val jvmArgs: ListProperty<String> = objects.listProperty(String::class.java)
@get:Optional
@get:Input
internal val previewTarget: Provider<String> =
project.providers.gradleProperty("compose.desktop.preview.target")
@get:Optional
@get:Input
internal val idePort: Provider<String> =
project.providers.gradleProperty("compose.desktop.preview.ide.port")

2
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractComposeDesktopTask.kt

@ -7,7 +7,6 @@ package org.jetbrains.compose.desktop.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.file.Directory
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
@ -18,7 +17,6 @@ import org.gradle.api.tasks.LocalState
import org.gradle.process.ExecOperations
import org.jetbrains.compose.desktop.application.internal.ComposeProperties
import org.jetbrains.compose.desktop.application.internal.ExternalToolRunner
import org.jetbrains.compose.internal.utils.clearDirs
import org.jetbrains.compose.internal.utils.notNullProperty
import javax.inject.Inject

21
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/internal/checkExperimentalTargets.kt

@ -6,15 +6,12 @@
package org.jetbrains.compose.experimental.internal
import org.gradle.api.Project
import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID
import org.jetbrains.compose.internal.mppExt
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
internal fun Project.configureExperimentalTargetsFlagsCheck() {
plugins.withId(KOTLIN_MPP_PLUGIN_ID) {
internal fun Project.configureExperimentalTargetsFlagsCheck(mppExt: KotlinMultiplatformExtension) {
gradle.taskGraph.whenReady {
checkExperimentalTargetsWithSkikoIsEnabled()
}
checkExperimentalTargetsWithSkikoIsEnabled(project, mppExt)
}
}
@ -38,9 +35,11 @@ private sealed interface CheckResult {
class Fail(val target: TargetType) : CheckResult
}
private fun Project.checkExperimentalTargetsWithSkikoIsEnabled() {
val mppExt = project.mppExt
val failedResults = mppExt.targets.map { checkTarget(it) }
private fun checkExperimentalTargetsWithSkikoIsEnabled(
project: Project,
mppExt: KotlinMultiplatformExtension,
) {
val failedResults = mppExt.targets.map { checkTarget(project, it) }
.filterIsInstance<CheckResult.Fail>()
.distinctBy { it.target }
@ -59,7 +58,7 @@ private fun Project.checkExperimentalTargetsWithSkikoIsEnabled() {
}
}
private fun Project.checkTarget(target: KotlinTarget): CheckResult {
private fun checkTarget(project: Project, target: KotlinTarget): CheckResult {
val presetName = target.preset?.name ?: return CheckResult.Success
val targetType = EXPERIMENTAL_TARGETS.firstOrNull {
@ -70,7 +69,7 @@ private fun Project.checkTarget(target: KotlinTarget): CheckResult {
compilation.compileDependencyConfigurationName
}
configurations.forEach { configuration ->
project.configurations.forEach { configuration ->
if (configuration.isCanBeResolved && configuration.name in targetConfigurationNames) {
val containsSkikoArtifact = configuration.resolvedConfiguration.resolvedArtifacts.any {
it.id.displayName.contains(SKIKO_ARTIFACT_PREFIX)

56
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/IosTargetResources.kt

@ -0,0 +1,56 @@
/*
* 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.experimental.uikit.internal.resources
import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.Input
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.Serializable
internal abstract class IosTargetResources : Serializable {
@get:Input
abstract val name: Property<String>
@get:Input
abstract val konanTarget: Property<String>
@get:Input
abstract val dirs: SetProperty<String>
@Suppress("unused") // used by Gradle Configuration Cache
fun readObject(input: ObjectInputStream) {
name.set(input.readUTF())
konanTarget.set(input.readUTF())
dirs.set(input.readUTFStrings())
}
@Suppress("unused") // used by Gradle Configuration Cache
fun writeObject(output: ObjectOutputStream) {
output.writeUTF(name.get())
output.writeUTF(konanTarget.get())
output.writeUTFStrings(dirs.get())
}
private fun ObjectOutputStream.writeUTFStrings(collection: Collection<String>) {
writeInt(collection.size)
collection.forEach { writeUTF(it) }
}
private fun ObjectInputStream.readUTFStrings(): Set<String> {
val size = readInt()
return LinkedHashSet<String>(size).apply {
repeat(size) {
add(readUTF())
}
}
}
companion object {
private const val serialVersionUID: Long = 0
}
}

178
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/configureSyncIosResources.kt

@ -0,0 +1,178 @@
/*
* 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.experimental.uikit.internal.resources
import org.gradle.api.Project
import org.gradle.api.file.Directory
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.TaskContainer
import org.jetbrains.compose.experimental.uikit.internal.utils.IosGradleProperties
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.SyncComposeResourcesForIosTask
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
internal fun Project.configureSyncTask(mppExt: KotlinMultiplatformExtension) {
if (!IosGradleProperties.syncResources(providers).get()) 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()) {
project.logger.warn("Warning: kotlin.cocoapods.extraSpecAttributes[\"resources\"] is ignored by Compose Multiplatform's resource synchronization for iOS")
}
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()
}
}
}
}
}
}
private fun SyncIosResourcesContext.configureSyncResourcesTasks() {
val lazyTasksDependencies = LazyTasksDependencyConfigurator(project.tasks)
configureEachIosFramework { framework ->
val frameworkClassifier = framework.namePrefix.uppercaseFirstChar()
val syncResourcesTaskName = "sync${frameworkClassifier}ComposeResourcesForIos"
val syncTask = framework.project.tasks.registerOrConfigure<SyncComposeResourcesForIosTask>(syncResourcesTaskName) {
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)
}
}
}

38
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/determineIosKonanTargetsFromEnv.kt

@ -0,0 +1,38 @@
/*
* 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.experimental.uikit.internal.resources
import org.jetbrains.kotlin.konan.target.KonanTarget
// based on AppleSdk.kt from Kotlin Gradle Plugin
// See https://github.com/JetBrains/kotlin/blob/142421da5b966049b4eab44ce6856eb172cf122a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/AppleSdk.kt
internal fun determineIosKonanTargetsFromEnv(platform: String, archs: List<String>): List<KonanTarget> {
val targets: MutableSet<KonanTarget> = mutableSetOf()
when {
platform.startsWith("iphoneos") -> {
targets.addAll(archs.map { arch ->
when (arch) {
"arm64", "arm64e" -> KonanTarget.IOS_ARM64
"armv7", "armv7s" -> KonanTarget.IOS_ARM32
else -> error("Unknown iOS device arch: '$arch'")
}
})
}
platform.startsWith("iphonesimulator") -> {
targets.addAll(archs.map { arch ->
when (arch) {
"arm64", "arm64e" -> KonanTarget.IOS_SIMULATOR_ARM64
"x86_64" -> KonanTarget.IOS_X64
else -> error("Unknown iOS simulator arch: '$arch'")
}
})
}
else -> error("Unknown iOS platform: '$platform'")
}
return targets.toList()
}

18
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/IosGradleProperties.kt

@ -0,0 +1,18 @@
/*
* 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.experimental.uikit.internal.utils
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory
import org.jetbrains.compose.internal.utils.findProperty
import org.jetbrains.compose.internal.utils.toBooleanProvider
internal object IosGradleProperties {
const val SYNC_RESOURCES_PROPERTY = "org.jetbrains.compose.ios.resources.sync"
fun syncResources(providers: ProviderFactory): Provider<Boolean> =
providers.findProperty(SYNC_RESOURCES_PROPERTY).toBooleanProvider(true)
}

26
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/cocoapodsDslHelpers.kt

@ -0,0 +1,26 @@
/*
* 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.experimental.uikit.internal.utils
import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionAware
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension
private const val COCOAPODS_PLUGIN_ID = "org.jetbrains.kotlin.native.cocoapods"
internal fun Project.withCocoapodsPlugin(fn: () -> Unit) {
project.plugins.withId(COCOAPODS_PLUGIN_ID) {
fn()
}
}
internal val KotlinMultiplatformExtension.cocoapodsExt: CocoapodsExtension
get() {
val extensionAware = (this as? ExtensionAware) ?: error("KotlinMultiplatformExtension is not ExtensionAware")
val extName = "cocoapods"
val ext = extensionAware.extensions.findByName(extName) ?: error("KotlinMultiplatformExtension does not contain '$extName' extension")
return ext as CocoapodsExtension
}

22
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/kotlinNativeTargetUtils.kt

@ -0,0 +1,22 @@
/*
* 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.experimental.uikit.internal.utils
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.konan.target.KonanTarget
internal fun KotlinNativeTarget.isIosSimulatorTarget(): Boolean =
konanTarget === KonanTarget.IOS_X64 || konanTarget === KonanTarget.IOS_SIMULATOR_ARM64
internal fun KotlinNativeTarget.isIosDeviceTarget(): Boolean =
konanTarget === KonanTarget.IOS_ARM64 || konanTarget === KonanTarget.IOS_ARM32
internal fun KotlinNativeTarget.isIosTarget(): Boolean =
isIosSimulatorTarget() || isIosDeviceTarget()
internal fun KotlinTarget.asIosNativeTargetOrNull(): KotlinNativeTarget? =
(this as? KotlinNativeTarget)?.takeIf { it.isIosTarget() }

4
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/AbstractComposeIosTask.kt

@ -8,6 +8,7 @@ package org.jetbrains.compose.experimental.uikit.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.file.Directory
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.ProjectLayout
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
@ -33,6 +34,9 @@ abstract class AbstractComposeIosTask : DefaultTask() {
@get:Inject
protected abstract val fileOperations: FileSystemOperations
@get:Inject
protected abstract val layout: ProjectLayout
@get:LocalState
protected val logsDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/logs/$name")

100
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/SyncComposeResourcesForIosTask.kt

@ -0,0 +1,100 @@
/*
* 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.experimental.uikit.tasks
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileCollection
import org.gradle.api.provider.Provider
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.*
import org.jetbrains.compose.experimental.uikit.internal.resources.determineIosKonanTargetsFromEnv
import org.jetbrains.compose.experimental.uikit.internal.resources.IosTargetResources
import org.jetbrains.compose.internal.utils.clearDirs
import java.io.File
import kotlin.io.path.Path
import kotlin.io.path.pathString
import kotlin.io.path.relativeTo
abstract class SyncComposeResourcesForIosTask : AbstractComposeIosTask() {
private fun missingTargetEnvAttributeError(attribute: String): Provider<Nothing> =
providers.provider {
error(
"Could not infer iOS target $attribute. Make sure to build " +
"via XCode (directly or via Kotlin Multiplatform Mobile plugin for Android Studio)")
}
@get:Input
val xcodeTargetPlatform: Provider<String> =
providers.gradleProperty("compose.ios.resources.platform")
.orElse(providers.environmentVariable("PLATFORM_NAME"))
.orElse(missingTargetEnvAttributeError("platform"))
@get:Input
val xcodeTargetArchs: Provider<List<String>> =
providers.gradleProperty("compose.ios.resources.archs")
.orElse(providers.environmentVariable("ARCHS"))
.orElse(missingTargetEnvAttributeError("architectures"))
.map { it.split(",", " ").filter { it.isNotBlank() } }
@get:Input
internal val iosTargets: SetProperty<IosTargetResources> = objects.setProperty(IosTargetResources::class.java)
@get:PathSensitive(PathSensitivity.ABSOLUTE)
@get:InputFiles
val resourceFiles: Provider<FileCollection> = xcodeTargetPlatform.zip(xcodeTargetArchs, ::Pair)
.map { (xcodeTargetPlatform, xcodeTargetArchs) ->
val allResources = objects.fileCollection()
val activeKonanTargets = determineIosKonanTargetsFromEnv(xcodeTargetPlatform, xcodeTargetArchs)
.mapTo(HashSet()) { it.name }
val dirsToInclude = iosTargets.get()
.filter { it.konanTarget.get() in activeKonanTargets }
.flatMapTo(HashSet()) { it.dirs.get() }
for (dirPath in dirsToInclude) {
val fileTree = objects.fileTree().apply {
setDir(layout.projectDirectory.dir(dirPath))
include("**/*")
}
allResources.from(fileTree)
}
allResources
}
@get:OutputDirectory
val outputDir: DirectoryProperty = objects.directoryProperty()
@TaskAction
fun run() {
val outputDir = outputDir.get().asFile
fileOperations.clearDirs(outputDir)
val allResourceDirs = iosTargets.get().flatMapTo(HashSet()) { it.dirs.get().map { Path(it).toAbsolutePath() } }
fun copyFileToOutputDir(file: File) {
for (dir in allResourceDirs) {
val path = file.toPath().toAbsolutePath()
if (path.startsWith(dir)) {
val targetFile = outputDir.resolve(path.relativeTo(dir).pathString)
file.copyTo(targetFile, overwrite = true)
return
}
}
error(
buildString {
appendLine("Resource file '$file' does not belong to a known resource directory:")
allResourceDirs.forEach {
appendLine("* $it")
}
}
)
}
val resourceFiles = resourceFiles.get().files
for (file in resourceFiles) {
copyFileToOutputDir(file)
}
logger.info("Synced Compose resource files. Copied ${resourceFiles.size} files to $outputDir")
}
}

17
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/providerUtils.kt

@ -9,6 +9,7 @@ import org.gradle.api.Task
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory
internal inline fun <reified T> ObjectFactory.new(vararg params: Any): T =
newInstance(T::class.java, *params)
@ -28,3 +29,19 @@ internal inline fun <reified T> Provider<T>.toProperty(objects: ObjectFactory):
internal inline fun <reified T> Task.provider(noinline fn: () -> T): Provider<T> =
project.provider(fn)
internal fun ProviderFactory.findProperty(prop: String): Provider<String?> =
provider {
gradleProperty(prop).forUseAtConfigurationTimeSafe().orNull
}
private fun Provider<String?>.forUseAtConfigurationTimeSafe(): Provider<String?> =
try {
forUseAtConfigurationTime()
} catch (e: NoSuchMethodError) {
// todo: remove once we drop support for Gradle 6.4
this
}
internal fun Provider<String?>.toBooleanProvider(defaultValue: Boolean): Provider<Boolean> =
orElse(defaultValue.toString()).map { "true" == it }

12
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/taskUtils.kt

@ -7,6 +7,7 @@ package org.jetbrains.compose.internal.utils
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
internal fun <T : Task> TaskProvider<T>.dependsOn(vararg dependencies: Any) {
@ -20,3 +21,14 @@ internal inline fun <reified T : Task> Project.registerTask(
tasks.register(name, T::class.java) { task ->
task.fn()
}
@Suppress("UNCHECKED_CAST")
inline fun <reified T : Task> TaskContainer.registerOrConfigure(
taskName: String,
crossinline configureFn: T.() -> Unit
): TaskProvider<T> = when (taskName) {
in names -> named(taskName) as TaskProvider<T>
else -> register(taskName, T::class.java) as TaskProvider<T>
}.apply {
configure { it.configureFn() }
}
Loading…
Cancel
Save