diff --git a/components/gradle.properties b/components/gradle.properties index 5a9519dfb0..19184d3993 100644 --- a/components/gradle.properties +++ b/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 diff --git a/components/resources/demo/iosApp/Podfile b/components/resources/demo/iosApp/Podfile deleted file mode 100644 index b0020795ae..0000000000 --- a/components/resources/demo/iosApp/Podfile +++ /dev/null @@ -1,6 +0,0 @@ -target 'iosApp' do - use_frameworks! - platform :ios, '14.1' - pod 'shared', :path => '../shared' - project 'iosApp.xcodeproj' -end \ No newline at end of file diff --git a/components/resources/demo/iosApp/iosApp.xcodeproj/project.pbxproj b/components/resources/demo/iosApp/iosApp.xcodeproj/project.pbxproj index 595f09262d..f7d234b66d 100644 --- a/components/resources/demo/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/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 = ""; }; 2152FB032600AC8F00CF470E /* iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosApp.swift; sourceTree = ""; }; - 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 = ""; }; 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 = ""; }; - 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 = ""; }; /* 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 = ""; @@ -80,8 +74,6 @@ E1DAFBE8E1CFC0878361EF0E /* Pods */ = { isa = PBXGroup; children = ( - 1EB65E27D2C0F884D0A1A133 /* Pods-iosApp.debug.xcconfig */, - 3D7A606AB0AD7636269BD9D0 /* Pods-iosApp.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -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"; diff --git a/components/resources/demo/shared/build.gradle.kts b/components/resources/demo/shared/build.gradle.kts index b7bd4275aa..306e748cba 100644 --- a/components/resources/demo/shared/build.gradle.kts +++ b/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() - iosSimulatorArm64() + 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 { diff --git a/components/resources/demo/shared/src/iosMain/kotlin/main.ios.kt b/components/resources/demo/shared/src/iosMain/kotlin/main.ios.kt index 8614d4176a..b3f0107a7d 100644 --- a/components/resources/demo/shared/src/iosMain/kotlin/main.ios.kt +++ b/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 diff --git a/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt index 60b950775e..9d9be82c9a 100644 --- a/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt +++ b/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 { diff --git a/examples/imageviewer/shared/build.gradle.kts b/examples/imageviewer/shared/build.gradle.kts index daf1577632..49db30760d 100755 --- a/examples/imageviewer/shared/build.gradle.kts +++ b/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 { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt index 87b13b4e66..a831a9e8a7 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt +++ b/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.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 { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt index b032f42f2f..977ed4a32e 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt +++ b/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 = - providers.findProperty(VERBOSE).toBoolean() + providers.findProperty(VERBOSE).toBooleanProvider(false) fun preserveWorkingDir(providers: ProviderFactory): Provider = - providers.findProperty(PRESERVE_WD).toBoolean() + providers.findProperty(PRESERVE_WD).toBooleanProvider(false) fun macSign(providers: ProviderFactory): Provider = - providers.findProperty(MAC_SIGN).toBoolean() + providers.findProperty(MAC_SIGN).toBooleanProvider(false) fun macSignIdentity(providers: ProviderFactory): Provider = providers.findProperty(MAC_SIGN_ID) @@ -45,20 +47,4 @@ internal object ComposeProperties { fun macNotarizationAscProvider(providers: ProviderFactory): Provider = providers.findProperty(MAC_NOTARIZATION_ASC_PROVIDER) - - private fun ProviderFactory.findProperty(prop: String): Provider = - provider { - gradleProperty(prop).forUseAtConfigurationTimeSafe().orNull - } - - private fun Provider.forUseAtConfigurationTimeSafe(): Provider = - try { - forUseAtConfigurationTime() - } catch (e: NoSuchMethodError) { - // todo: remove once we drop support for Gradle 6.4 - this - } - - private fun Provider.toBoolean(): Provider = - orElse("false").map { "true" == it } } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/tasks/AbstractConfigureDesktopPreviewTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/tasks/AbstractConfigureDesktopPreviewTask.kt index b3257dc74f..673e7b3a2d 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/tasks/AbstractConfigureDesktopPreviewTask.kt +++ b/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 = objects.listProperty(String::class.java) + @get:Optional @get:Input internal val previewTarget: Provider = project.providers.gradleProperty("compose.desktop.preview.target") + @get:Optional @get:Input internal val idePort: Provider = project.providers.gradleProperty("compose.desktop.preview.ide.port") diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractComposeDesktopTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractComposeDesktopTask.kt index 857f2325a8..63110a417d 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractComposeDesktopTask.kt +++ b/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 diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/internal/checkExperimentalTargets.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/internal/checkExperimentalTargets.kt index db2ceec3f8..532d77ec13 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/internal/checkExperimentalTargets.kt +++ b/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) { - gradle.taskGraph.whenReady { - checkExperimentalTargetsWithSkikoIsEnabled() - } +internal fun Project.configureExperimentalTargetsFlagsCheck(mppExt: KotlinMultiplatformExtension) { + gradle.taskGraph.whenReady { + 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() .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) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/IosTargetResources.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/IosTargetResources.kt new file mode 100644 index 0000000000..7259a2a85d --- /dev/null +++ b/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 + + @get:Input + abstract val konanTarget: Property + + @get:Input + abstract val dirs: SetProperty + + @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) { + writeInt(collection.size) + collection.forEach { writeUTF(it) } + } + + private fun ObjectInputStream.readUTFStrings(): Set { + val size = readInt() + return LinkedHashSet(size).apply { + repeat(size) { + add(readUTF()) + } + } + } + + companion object { + private const val serialVersionUID: Long = 0 + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/configureSyncIosResources.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/configureSyncIosResources.kt new file mode 100644 index 0000000000..7e86148dd5 --- /dev/null +++ b/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 { + 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(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 { + 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().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>() + private val requestedDependencies = HashMap>() + + 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) + } + } +} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/determineIosKonanTargetsFromEnv.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/determineIosKonanTargetsFromEnv.kt new file mode 100644 index 0000000000..c1df81b773 --- /dev/null +++ b/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): List { + val targets: MutableSet = 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() +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/IosGradleProperties.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/IosGradleProperties.kt new file mode 100644 index 0000000000..bfacedbf39 --- /dev/null +++ b/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 = + providers.findProperty(SYNC_RESOURCES_PROPERTY).toBooleanProvider(true) +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/cocoapodsDslHelpers.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/cocoapodsDslHelpers.kt new file mode 100644 index 0000000000..34815b8fae --- /dev/null +++ b/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 + } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/kotlinNativeTargetUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/kotlinNativeTargetUtils.kt new file mode 100644 index 0000000000..a6811a749c --- /dev/null +++ b/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() } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/AbstractComposeIosTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/AbstractComposeIosTask.kt index 94ac162184..ea6c6b827f 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/AbstractComposeIosTask.kt +++ b/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 = project.layout.buildDirectory.dir("compose/logs/$name") diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/SyncComposeResourcesForIosTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/SyncComposeResourcesForIosTask.kt new file mode 100644 index 0000000000..01797f9f7a --- /dev/null +++ b/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 = + 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 = + providers.gradleProperty("compose.ios.resources.platform") + .orElse(providers.environmentVariable("PLATFORM_NAME")) + .orElse(missingTargetEnvAttributeError("platform")) + + @get:Input + val xcodeTargetArchs: Provider> = + 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 = objects.setProperty(IosTargetResources::class.java) + + @get:PathSensitive(PathSensitivity.ABSOLUTE) + @get:InputFiles + val resourceFiles: Provider = 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") + } +} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/providerUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/providerUtils.kt index 3ce83c064d..955b49dd14 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/providerUtils.kt +++ b/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 ObjectFactory.new(vararg params: Any): T = newInstance(T::class.java, *params) @@ -27,4 +28,20 @@ internal inline fun Provider.toProperty(objects: ObjectFactory): objects.property(T::class.java).value(this) internal inline fun Task.provider(noinline fn: () -> T): Provider = - project.provider(fn) \ No newline at end of file + project.provider(fn) + +internal fun ProviderFactory.findProperty(prop: String): Provider = + provider { + gradleProperty(prop).forUseAtConfigurationTimeSafe().orNull + } + +private fun Provider.forUseAtConfigurationTimeSafe(): Provider = + try { + forUseAtConfigurationTime() + } catch (e: NoSuchMethodError) { + // todo: remove once we drop support for Gradle 6.4 + this + } + +internal fun Provider.toBooleanProvider(defaultValue: Boolean): Provider = + orElse(defaultValue.toString()).map { "true" == it } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/taskUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/taskUtils.kt index a9a02977e0..a4087f13ec 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/taskUtils.kt +++ b/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 TaskProvider.dependsOn(vararg dependencies: Any) { @@ -20,3 +21,14 @@ internal inline fun Project.registerTask( tasks.register(name, T::class.java) { task -> task.fn() } + +@Suppress("UNCHECKED_CAST") +inline fun TaskContainer.registerOrConfigure( + taskName: String, + crossinline configureFn: T.() -> Unit +): TaskProvider = when (taskName) { + in names -> named(taskName) as TaskProvider + else -> register(taskName, T::class.java) as TaskProvider +}.apply { + configure { it.configureFn() } +} \ No newline at end of file