From 24526947ade74bc7711c8d4219b03b9c45098476 Mon Sep 17 00:00:00 2001 From: dima-avdeev-jb <99798741+dima-avdeev-jb@users.noreply.github.com> Date: Fri, 4 Mar 2022 09:42:51 +0300 Subject: [PATCH] Gradle deploy to iOS simulator (#1899) --- .../falling-balls-mpp/build.gradle.kts | 18 ++- .../examples/falling-balls-mpp/project.yml | 29 ----- .../examples/minesweeper/build.gradle.kts | 18 ++- experimental/examples/minesweeper/project.yml | 29 ----- gradle-plugins/build.gradle.kts | 4 +- .../src/main/kotlin/BuildProperties.kt | 1 + gradle-plugins/compose/build.gradle.kts | 5 + .../desktop/application/internal/osUtils.kt | 15 +++ .../dsl/ExperimentalUiKitApplication.kt | 18 ++- .../compose/experimental/dsl/IOSDevices.kt | 59 +++++++++ .../dsl/IosDeployConfigurations.kt | 33 +++++ .../uikit/internal/SimctlListData.kt | 77 ++++++++++++ .../uikit/internal/SimctlUtils.kt | 25 ++++ .../configureExperimentalUikitApplication.kt | 4 +- .../uikit/internal/configureIosDeployTasks.kt | 84 +++++++++++++ .../internal/configureUseXcodeGenTask.kt | 57 +++++++++ .../uikit/internal/registerSimulatorTasks.kt | 119 ++++++++++++++++++ .../uikit/tasks/AbstractComposeIosTask.kt | 49 ++++++++ .../compose/internal/requiredDslProperty.kt | 28 +++++ 19 files changed, 608 insertions(+), 64 deletions(-) delete mode 100644 experimental/examples/falling-balls-mpp/project.yml delete mode 100644 experimental/examples/minesweeper/project.yml create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/dsl/IOSDevices.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/dsl/IosDeployConfigurations.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/SimctlListData.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/SimctlUtils.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/configureIosDeployTasks.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/configureUseXcodeGenTask.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/registerSimulatorTasks.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/AbstractComposeIosTask.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/requiredDslProperty.kt diff --git a/experimental/examples/falling-balls-mpp/build.gradle.kts b/experimental/examples/falling-balls-mpp/build.gradle.kts index 5ae444ce7b..79f559f8a5 100644 --- a/experimental/examples/falling-balls-mpp/build.gradle.kts +++ b/experimental/examples/falling-balls-mpp/build.gradle.kts @@ -3,6 +3,7 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension +import org.jetbrains.compose.experimental.dsl.IOSDevices buildscript { repositories { @@ -150,7 +151,22 @@ compose.desktop { compose.experimental { web.application {} - uikit.application {} + uikit.application { + bundleIdPrefix = "org.jetbrains" + projectName = "FallingBalls" + deployConfigurations { + simulator("IPhone8") { + //Usage: ./gradlew iosDeployIPhone8 + device = IOSDevices.IPHONE_8 + buildConfiguration = "Debug" // or "Release" + } + simulator("IPad") { + //Usage: ./gradlew iosDeployIPad + device = IOSDevices.IPAD_MINI_6th_Gen + buildConfiguration = "Debug" + } + } + } } tasks.withType { diff --git a/experimental/examples/falling-balls-mpp/project.yml b/experimental/examples/falling-balls-mpp/project.yml deleted file mode 100644 index d5964bda80..0000000000 --- a/experimental/examples/falling-balls-mpp/project.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: ComposeFallingBalls -options: - bundleIdPrefix: org.jetbrains -settings: - DEVELOPMENT_TEAM: N462MKSJ7M - CODE_SIGN_IDENTITY: "iPhone Developer" - CODE_SIGN_STYLE: Automatic - MARKETING_VERSION: "1.0" - CURRENT_PROJECT_VERSION: "4" - SDKROOT: iphoneos -targets: - ComposeFallingBalls: - type: application - platform: iOS - deploymentTarget: "12.0" - prebuildScripts: - - script: cd "$SRCROOT" && ./gradlew -i -p . packComposeUikitApplicationForXCode - name: GradleCompile - info: - path: plists/Ios/Info.plist - properties: - UILaunchStoryboardName: "" - sources: - - "src/" - settings: - LIBRARY_SEARCH_PATHS: "$(inherited)" - ENABLE_BITCODE: "YES" - ONLY_ACTIVE_ARCH: "NO" - VALID_ARCHS: "arm64" diff --git a/experimental/examples/minesweeper/build.gradle.kts b/experimental/examples/minesweeper/build.gradle.kts index f099d97472..0901912322 100644 --- a/experimental/examples/minesweeper/build.gradle.kts +++ b/experimental/examples/minesweeper/build.gradle.kts @@ -3,6 +3,7 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension +import org.jetbrains.compose.experimental.dsl.IOSDevices buildscript { repositories { @@ -151,7 +152,22 @@ compose.desktop { compose.experimental { web.application {} - uikit.application {} + uikit.application { + bundleIdPrefix = "org.jetbrains" + projectName = "ComposeMinesweeper" + deployConfigurations { + simulator("IPhone8") { + //Usage: ./gradlew iosDeployIPhone8 + device = IOSDevices.IPHONE_8 + buildConfiguration = "Debug" // or "Release" + } + simulator("IPad) { + //Usage: ./gradlew iosDeployIPad + device = IOSDevices.IPAD_MINI_6th_Gen + buildConfiguration = "Debug" + } + } + } } tasks.withType { diff --git a/experimental/examples/minesweeper/project.yml b/experimental/examples/minesweeper/project.yml deleted file mode 100644 index 4961fee57c..0000000000 --- a/experimental/examples/minesweeper/project.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: ComposeMinesweeper -options: - bundleIdPrefix: org.jetbrains -settings: - DEVELOPMENT_TEAM: N462MKSJ7M - CODE_SIGN_IDENTITY: "iPhone Developer" - CODE_SIGN_STYLE: Automatic - MARKETING_VERSION: "1.0" - CURRENT_PROJECT_VERSION: "4" - SDKROOT: iphoneos -targets: - ComposeMinesweeper: - type: application - platform: iOS - deploymentTarget: "12.0" - prebuildScripts: - - script: cd "$SRCROOT" && ./gradlew -i -p . packComposeUikitApplicationForXCode - name: GradleCompile - info: - path: plists/Ios/Info.plist - properties: - UILaunchStoryboardName: "" - sources: - - "src/" - settings: - LIBRARY_SEARCH_PATHS: "$(inherited)" - ENABLE_BITCODE: "YES" - ONLY_ACTIVE_ARCH: "NO" - VALID_ARCHS: "arm64" \ No newline at end of file diff --git a/gradle-plugins/build.gradle.kts b/gradle-plugins/build.gradle.kts index 3b35fa20ac..af120f9968 100644 --- a/gradle-plugins/build.gradle.kts +++ b/gradle-plugins/build.gradle.kts @@ -1,7 +1,9 @@ import com.gradle.publish.PluginBundleExtension plugins { - kotlin("jvm") version "1.5.10" apply false + val kotlinVersion = "1.5.10" + kotlin("jvm") version kotlinVersion apply false + kotlin("plugin.serialization") version kotlinVersion apply false id("com.gradle.plugin-publish") version "0.17.0" apply false } diff --git a/gradle-plugins/buildSrc/src/main/kotlin/BuildProperties.kt b/gradle-plugins/buildSrc/src/main/kotlin/BuildProperties.kt index 99e32f97c0..e89ddecdde 100644 --- a/gradle-plugins/buildSrc/src/main/kotlin/BuildProperties.kt +++ b/gradle-plugins/buildSrc/src/main/kotlin/BuildProperties.kt @@ -11,6 +11,7 @@ object BuildProperties { const val group = "org.jetbrains.compose" const val website = "https://www.jetbrains.com/lp/compose/" const val vcs = "https://github.com/JetBrains/compose-jb" + const val serializationVersion = "1.2.1" fun composeVersion(project: Project): String = System.getenv("COMPOSE_GRADLE_PLUGIN_COMPOSE_VERSION") ?: project.findProperty("compose.version") as String diff --git a/gradle-plugins/compose/build.gradle.kts b/gradle-plugins/compose/build.gradle.kts index 0a680a239f..307167211f 100644 --- a/gradle-plugins/compose/build.gradle.kts +++ b/gradle-plugins/compose/build.gradle.kts @@ -4,6 +4,7 @@ import java.util.zip.ZipFile plugins { kotlin("jvm") + kotlin("plugin.serialization") id("com.gradle.plugin-publish") id("java-gradle-plugin") id("maven-publish") @@ -66,6 +67,10 @@ dependencies { // include relocated download task to avoid potential runtime conflicts embedded("de.undercouch:gradle-download-task:4.1.1") + + embedded("org.jetbrains.kotlinx:kotlinx-serialization-json:${BuildProperties.serializationVersion}") + embedded("org.jetbrains.kotlinx:kotlinx-serialization-core:${BuildProperties.serializationVersion}") + embedded("org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:${BuildProperties.serializationVersion}") embedded(project(":preview-rpc")) } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/osUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/osUtils.kt index 9a77d2c672..733e3db8b4 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/osUtils.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/osUtils.kt @@ -68,6 +68,21 @@ internal object MacUtils { val xcrun: File by lazy { File("/usr/bin/xcrun").checkExistingFile() } + + val make: File by lazy { + File("/usr/bin/make").checkExistingFile() + } + + val open: File by lazy { + File("/usr/bin/open").checkExistingFile() + } + +} + +internal object UnixUtils { + val git: File by lazy { + File("/usr/bin/git").checkExistingFile() + } } internal fun jvmToolFile(toolName: String, javaHome: Provider): File { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/dsl/ExperimentalUiKitApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/dsl/ExperimentalUiKitApplication.kt index 4b0f0f6877..c1ec3f4dac 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/dsl/ExperimentalUiKitApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/dsl/ExperimentalUiKitApplication.kt @@ -5,7 +5,21 @@ package org.jetbrains.compose.experimental.dsl +import org.gradle.api.Action +import org.gradle.api.model.ObjectFactory +import org.jetbrains.compose.internal.requiredDslProperty import javax.inject.Inject -abstract class ExperimentalUiKitApplication @Inject constructor(val name: String) { -} \ No newline at end of file +@Suppress("unused") +abstract class ExperimentalUiKitApplication @Inject constructor( + val name: String, + val objects: ObjectFactory +) { + var bundleIdPrefix: String by requiredDslProperty("require property [bundleIdPrefix] in uikit.application { ...") + var projectName: String by requiredDslProperty("require property [projectName] in uikit.application { ...") + + val deployConfigurations: IosDeployConfigurations = objects.newInstance(IosDeployConfigurations::class.java) + fun deployConfigurations(fn: Action) { + fn.execute(deployConfigurations) + } +} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/dsl/IOSDevices.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/dsl/IOSDevices.kt new file mode 100644 index 0000000000..15c01b5cb6 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/dsl/IOSDevices.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2020-2022 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.dsl + +/** + * iOS device type + * xcrun simctl list devices + */ +@Suppress("unused") +public enum class IOSDevices(val id: String) { + IPHONE_6S("com.apple.CoreSimulator.SimDeviceType.iPhone-6s"), + IPHONE_6S_PLUS("com.apple.CoreSimulator.SimDeviceType.iPhone-6s-Plus"), + IPHONE_SE("com.apple.CoreSimulator.SimDeviceType.iPhone-SE"), + IPHONE_7("com.apple.CoreSimulator.SimDeviceType.iPhone-7"), + IPHONE_7_PLUS("com.apple.CoreSimulator.SimDeviceType.iPhone-7-Plus"), + IPHONE_8("com.apple.CoreSimulator.SimDeviceType.iPhone-8"), + IPHONE_8_PLUS("com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus"), + IPHONE_X("com.apple.CoreSimulator.SimDeviceType.iPhone-X"), + IPHONE_XS("com.apple.CoreSimulator.SimDeviceType.iPhone-XS"), + IPHONE_XS_MAX("com.apple.CoreSimulator.SimDeviceType.iPhone-XS-Max"), + IPHONE_XR("com.apple.CoreSimulator.SimDeviceType.iPhone-XR"), + IPHONE_11("com.apple.CoreSimulator.SimDeviceType.iPhone-11"), + IPHONE_11_PRO("com.apple.CoreSimulator.SimDeviceType.iPhone-11-Pro"), + IPHONE_11_PRO_MAX("com.apple.CoreSimulator.SimDeviceType.iPhone-11-Pro-Max"), + IPHONE_SE_2nd_Gen("com.apple.CoreSimulator.SimDeviceType.iPhone-SE--2nd-generation-"), + IPHONE_12_MINI("com.apple.CoreSimulator.SimDeviceType.iPhone-12-mini"), + IPHONE_12("com.apple.CoreSimulator.SimDeviceType.iPhone-12"), + IPHONE_12_PRO("com.apple.CoreSimulator.SimDeviceType.iPhone-12-Pro"), + IPHONE_12_PRO_MAX("com.apple.CoreSimulator.SimDeviceType.iPhone-12-Pro-Max"), + IPHONE_13_PRO("com.apple.CoreSimulator.SimDeviceType.iPhone-13-Pro"), + IPHONE_13_PRO_MAX("com.apple.CoreSimulator.SimDeviceType.iPhone-13-Pro-Max"), + IPHONE_13_MINI("com.apple.CoreSimulator.SimDeviceType.iPhone-13-mini"), + IPHONE_13("com.apple.CoreSimulator.SimDeviceType.iPhone-13"), + IPOD_TOUCH_7th_Gen("com.apple.CoreSimulator.SimDeviceType.iPod-touch--7th-generation-"), + IPAD_MINI_4("com.apple.CoreSimulator.SimDeviceType.iPad-mini-4"), + IPAD_AIR_2("com.apple.CoreSimulator.SimDeviceType.iPad-Air-2"), + IPAD_PRO_9_7_INCH("com.apple.CoreSimulator.SimDeviceType.iPad-Pro--9-7-inch-"), + IPAD_PRO("com.apple.CoreSimulator.SimDeviceType.iPad-Pro"), + IPAD_5th_Gen("com.apple.CoreSimulator.SimDeviceType.iPad--5th-generation-"), + IPAD_PRO_12_9_INCH_2nd_Gen("com.apple.CoreSimulator.SimDeviceType.iPad-Pro--12-9-inch---2nd-generation-"), + IPAD_PRO_10_5_INCH("com.apple.CoreSimulator.SimDeviceType.iPad-Pro--10-5-inch-"), + IPAD_6th_Gen("com.apple.CoreSimulator.SimDeviceType.iPad--6th-generation-"), + IPAD_7th_Gen("com.apple.CoreSimulator.SimDeviceType.iPad--7th-generation-"), + IPAD_PRO_11_INCH("com.apple.CoreSimulator.SimDeviceType.iPad-Pro--11-inch-"), + IPAD_PRO_12_9_INCH_3rd_Gen("com.apple.CoreSimulator.SimDeviceType.iPad-Pro--12-9-inch---3rd-generation-"), + IPAD_PRO_11_INCH_2nd_Gen("com.apple.CoreSimulator.SimDeviceType.iPad-Pro--11-inch---2nd-generation-"), + IPAD_PRO_12_9_INCH_4th_Gen("com.apple.CoreSimulator.SimDeviceType.iPad-Pro--12-9-inch---4th-generation-"), + IPAD_MINI_5th_Gen("com.apple.CoreSimulator.SimDeviceType.iPad-mini--5th-generation-"), + IPAD_AIR_3th_Gen("com.apple.CoreSimulator.SimDeviceType.iPad-Air--3rd-generation-"), + IPAD_8th_Gen("com.apple.CoreSimulator.SimDeviceType.iPad--8th-generation-"), + IPAD_9th_Gen("com.apple.CoreSimulator.SimDeviceType.iPad-9th-generation"), + IPAD_AIR_4th_Gen("com.apple.CoreSimulator.SimDeviceType.iPad-Air--4th-generation-"), + IPAD_PRO_11_INCH_3rd_Gen("com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-3rd-generation"), + IPAD_12_9_INCH_5th_Gen("com.apple.CoreSimulator.SimDeviceType.iPad-Pro-12-9-inch-5th-generation"), + IPAD_MINI_6th_Gen("com.apple.CoreSimulator.SimDeviceType.iPad-mini-6th-generation"), +} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/dsl/IosDeployConfigurations.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/dsl/IosDeployConfigurations.kt new file mode 100644 index 0000000000..98c443a092 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/dsl/IosDeployConfigurations.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2020-2022 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.dsl + +import org.gradle.api.Action +import org.gradle.api.model.ObjectFactory +import javax.inject.Inject + +open class IosDeployConfigurations @Inject constructor( + val objects: ObjectFactory +) { + internal val deployTargets: MutableList = mutableListOf() + public fun simulator(id: String, configureSimulator: Action) { + val currentSimulator = objects.newInstance(DeployTarget.Simulator::class.java) + configureSimulator.execute(currentSimulator) + deployTargets.add(DeployTargetWithId(id, currentSimulator)) + } +} + +sealed interface DeployTarget { + open class Simulator : DeployTarget { + var device: IOSDevices = IOSDevices.IPHONE_8 + var buildConfiguration: String = "Debug" + } +} + +internal class DeployTargetWithId( + val id: String, + val deploy: DeployTarget +) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/SimctlListData.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/SimctlListData.kt new file mode 100644 index 0000000000..b999f9c60d --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/SimctlListData.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2020-2022 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. + */ + +@file:Suppress("unused") + +package org.jetbrains.compose.experimental.uikit.internal + +import kotlinx.serialization.Serializable + +@Serializable +internal class SimctlListData( + val devicetypes: List, + val runtimes: List, + val devices: Map>, + val pairs: Map, +) + +@Serializable +internal class DeviceTypeData( + val name: String, + val minRuntimeVersion: Long, + val bundlePath: String, + val maxRuntimeVersion: Long, + val identifier: String, + val productFamily: String +) + +@Serializable +internal class RuntimeData( + val name: String, + val bundlePath: String, + val buildversion: String, + val runtimeRoot: String, + val identifier: String, + val version: String, + val isAvailable: Boolean, + val supportedDeviceTypes: List +) + +@Serializable +internal class SupportedDeviceTypeData( + val bundlePath: String, + val name: String, + val identifier: String, + val productFamily: String +) + +@Serializable +internal class DeviceData( + val name: String, + val availabilityError: String? = null, + val dataPath: String, + val dataPathSize: Long, + val logPath: String, + val udid: String, + val isAvailable: Boolean, + val deviceTypeIdentifier: String, + val state: String, +) + +internal val DeviceData.booted: Boolean + get() = state == "Booted" + +@Serializable +internal class WatchAndPhonePairData( + val watch: DeviceInPairData, + val phone: DeviceInPairData +) + +@Serializable +internal class DeviceInPairData( + val name: String, + val udid: String, + val state: String, +) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/SimctlUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/SimctlUtils.kt new file mode 100644 index 0000000000..558239faf3 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/SimctlUtils.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2020-2022 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 + +import kotlinx.serialization.json.Json +import org.jetbrains.compose.desktop.application.internal.MacUtils +import org.jetbrains.compose.experimental.uikit.tasks.AbstractComposeIosTask + +val json = Json { + ignoreUnknownKeys = true +} + +internal fun AbstractComposeIosTask.getSimctlListData(): SimctlListData { + lateinit var simctlResult: SimctlListData + runExternalTool( + MacUtils.xcrun, listOf("simctl", "list", "--json"), + processStdout = { stdout -> + simctlResult = json.decodeFromString(SimctlListData.serializer(), stdout) + } + ) + return simctlResult +} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/configureExperimentalUikitApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/configureExperimentalUikitApplication.kt index 7d08836177..0c09b6fcb7 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/configureExperimentalUikitApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/configureExperimentalUikitApplication.kt @@ -47,4 +47,6 @@ internal fun Project.configureExperimentalUikitApplication( packTask.destinationDir.set(targetBuildDir) packTask.executablePath.set(executablePath) } -} \ No newline at end of file + + configureIosDeployTasks(application) +} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/configureIosDeployTasks.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/configureIosDeployTasks.kt new file mode 100644 index 0000000000..3abf5b889b --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/configureIosDeployTasks.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2020-2022 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 + +import org.gradle.api.* +import org.gradle.api.tasks.TaskContainer +import org.jetbrains.compose.desktop.application.internal.MacUtils +import org.jetbrains.compose.desktop.application.internal.UnixUtils +import org.jetbrains.compose.experimental.dsl.DeployTarget +import org.jetbrains.compose.experimental.dsl.ExperimentalUiKitApplication +import org.jetbrains.compose.experimental.uikit.tasks.AbstractComposeIosTask + +const val XCODE_GEN_GIT = "https://github.com/yonaskolb/XcodeGen.git" +const val XCODE_GEN_TAG = "2.26.0" +const val TASK_INSTALL_XCODE_GEN_NAME = "iosInstallXcodeGen" +const val TASK_USE_XCODE_GEN_NAME = "iosUseXCodeGen" +const val SDK_PREFIFX_SIMULATOR = "iphonesimulator" +const val SDK_PREFIX_IPHONEOS = "iphoneos" + +internal fun Project.configureIosDeployTasks(application: ExperimentalUiKitApplication) { + val projectName = application.projectName + val bundleIdPrefix = application.bundleIdPrefix + val xcodeGenSrc = rootProject.buildDir.resolve("xcodegen-$XCODE_GEN_TAG-src") + val xcodeGenExecutable = xcodeGenSrc.resolve(".build/apple/Products/Release/xcodegen") + val buildIosDir = buildDir.resolve("ios") + + tasks.composeIosTask(TASK_INSTALL_XCODE_GEN_NAME) { + onlyIf { !xcodeGenExecutable.exists() } + doLast { + xcodeGenSrc.deleteRecursively() + runExternalTool( + UnixUtils.git, + listOf( + "clone", + "--depth", "1", + "--branch", XCODE_GEN_TAG, + XCODE_GEN_GIT, + xcodeGenSrc.absolutePath + ) + ) + runExternalTool( + MacUtils.make, + listOf("build"), + workingDir = xcodeGenSrc + ) + } + } + + configureUseXcodeGenTask( + buildIosDir = buildIosDir, + projectName = projectName, + bundleIdPrefix = bundleIdPrefix, + xcodeGenExecutable = xcodeGenExecutable + ) + + application.deployConfigurations.deployTargets.forEach { target -> + val id = target.id // .replaceFirstChar { it.uppercase() } // todo upperCase first char? ./gradlew iosDeployId + when (target.deploy) { + is DeployTarget.Simulator -> { + registerSimulatorTasks( + id = id, + deploy = target.deploy, + buildIosDir = buildIosDir, + projectName = projectName, + bundleIdPrefix = bundleIdPrefix + ) + } + } + } +} + +inline fun TaskContainer.composeIosTask( + name: String, + args: List = emptyList(), + noinline configureFn: T.() -> Unit = {} +) = register(name, T::class.java, *args.toTypedArray()).apply { + configure { + it.group = "Compose iOS" + it.configureFn() + } +} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/configureUseXcodeGenTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/configureUseXcodeGenTask.kt new file mode 100644 index 0000000000..2e23234d2c --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/configureUseXcodeGenTask.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2020-2022 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 + +import org.gradle.api.Project +import org.jetbrains.compose.experimental.uikit.tasks.AbstractComposeIosTask +import java.io.File + +internal fun Project.configureUseXcodeGenTask( + buildIosDir: File, + projectName: String, + bundleIdPrefix: String, + xcodeGenExecutable: File +) { + tasks.composeIosTask(TASK_USE_XCODE_GEN_NAME) { + dependsOn(TASK_INSTALL_XCODE_GEN_NAME) + doLast { + buildIosDir.mkdirs() + buildIosDir.resolve("project.yml").writeText( + """ + name: $projectName + options: + bundleIdPrefix: $bundleIdPrefix + settings: + CODE_SIGN_IDENTITY: "iPhone Developer" + CODE_SIGN_STYLE: Automatic + MARKETING_VERSION: "1.0" + CURRENT_PROJECT_VERSION: "4" + SDKROOT: iphoneos + targets: + $projectName: + type: application + platform: iOS + deploymentTarget: "12.0" + prebuildScripts: + - script: cd "${rootDir.absolutePath}" && ./gradlew -i -p . packComposeUikitApplicationForXCode + name: GradleCompile + info: + path: plists/Ios/Info.plist + properties: + UILaunchStoryboardName: "" + sources: + - "../../src/" + settings: + LIBRARY_SEARCH_PATHS: "${'$'}(inherited)" + ENABLE_BITCODE: "YES" + ONLY_ACTIVE_ARCH: "NO" + VALID_ARCHS: "arm64" + """.trimIndent() + ) + runExternalTool(xcodeGenExecutable, emptyList(), workingDir = buildIosDir) + } + } +} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/registerSimulatorTasks.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/registerSimulatorTasks.kt new file mode 100644 index 0000000000..88084439bb --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/registerSimulatorTasks.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2020-2022 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 + +import org.gradle.api.* +import org.jetbrains.compose.desktop.application.internal.Arch +import org.jetbrains.compose.desktop.application.internal.MacUtils +import org.jetbrains.compose.desktop.application.internal.currentArch +import org.jetbrains.compose.experimental.dsl.DeployTarget +import org.jetbrains.compose.experimental.uikit.tasks.AbstractComposeIosTask +import java.io.File + + +fun Project.registerSimulatorTasks( + id: String, + deploy: DeployTarget.Simulator, + buildIosDir: File, + projectName: String, + bundleIdPrefix: String +) { + val xcodeProjectDir = buildIosDir.resolve("$projectName.xcodeproj") + val deviceName = "device-$id" + + val taskCreateSimulator = tasks.composeIosTask("iosSimulatorCreate$id") { + onlyIf { getSimctlListData().devices.map { it.value }.flatten().none { it.name == deviceName } } + doFirst { + val availableRuntimes = getSimctlListData().runtimes.filter { runtime -> + runtime.supportedDeviceTypes.any { it.identifier == deploy.device.id } + } + val runtime = availableRuntimes.firstOrNull() ?: error("device not found is runtimes") + runExternalTool( + MacUtils.xcrun, + listOf("simctl", "create", deviceName, deploy.device.id, runtime.identifier) + ) + } + } + + val taskBootSimulator = tasks.composeIosTask("iosSimulatorBoot$id") { + onlyIf { + getSimctlListData().devices.map { it.value }.flatten().any { it.name == deviceName && it.booted.not() } + } + dependsOn(taskCreateSimulator) + doLast { + val device = getSimctlListData().devices.map { it.value }.flatten().firstOrNull { it.name == deviceName } + ?: error("device '$deviceName' not found") + + runExternalTool( + MacUtils.xcrun, + listOf("simctl", "boot", device.udid) + ) + runExternalTool( + MacUtils.open, + listOf( + "-a", "Simulator", + "--args", "-CurrentDeviceUDID", device.udid + ) + ) + } + } + + val simulatorArch = when (currentArch) { + Arch.X64 -> "x86_64" + Arch.Arm64 -> "arm64" + } + val iosCompiledAppDir = xcodeProjectDir.resolve("build/Build/Products/Debug-iphonesimulator/$projectName.app") + val taskBuild = tasks.composeIosTask("iosSimulatorBuild$id") { + dependsOn(TASK_USE_XCODE_GEN_NAME) + doLast { + val sdk = SDK_PREFIFX_SIMULATOR + getSimctlListData().runtimes.first().version // xcrun xcodebuild -showsdks + val scheme = projectName // xcrun xcodebuild -list -project . + repeat(2) { + // todo repeat(2) is workaround of error (domain=NSPOSIXErrorDomain, code=22) + // The bundle identifier of the application could not be determined + // Ensure that the application's Info.plist contains a value for CFBundleIdentifier. + runExternalTool( + MacUtils.xcrun, + listOf( + "xcodebuild", + "-scheme", scheme, + "-project", ".", + "-configuration", deploy.buildConfiguration, + "-derivedDataPath", "build", + "-arch", simulatorArch, + "-sdk", sdk + ), + workingDir = xcodeProjectDir + ) + } + } + } + + val installIosSimulator = tasks.composeIosTask("iosSimulatorInstall$id") { + dependsOn(taskBuild, taskBootSimulator) + doLast { + val device = getSimctlListData().devices.map { it.value }.flatten() + .firstOrNull { it.name == deviceName && it.booted } ?: error("device $deviceName not booted") + runExternalTool( + MacUtils.xcrun, + listOf("simctl", "install", device.udid, iosCompiledAppDir.absolutePath) + ) + } + } + + tasks.composeIosTask("iosDeploy$id") { + dependsOn(installIosSimulator) + doFirst { + val device = getSimctlListData().devices.map { it.value }.flatten() + .firstOrNull { it.name == deviceName && it.booted } ?: error("device $deviceName not booted") + val bundleIdentifier = "$bundleIdPrefix.$projectName" + runExternalTool( + MacUtils.xcrun, + listOf("simctl", "launch", "--console", device.udid, bundleIdentifier) + ) + } + } +} 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 new file mode 100644 index 0000000000..921b053930 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/AbstractComposeIosTask.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2020-2022 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.DefaultTask +import org.gradle.api.file.Directory +import org.gradle.api.internal.file.FileOperations +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory +import org.gradle.api.tasks.Internal +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.desktop.application.internal.notNullProperty +import javax.inject.Inject + +abstract class AbstractComposeIosTask : DefaultTask() { + @get:Inject + protected abstract val objects: ObjectFactory + + @get:Inject + protected abstract val providers: ProviderFactory + + @get:Inject + protected abstract val execOperations: ExecOperations + + @get:Inject + protected abstract val fileOperations: FileOperations + + @get:LocalState + protected val logsDir: Provider = project.layout.buildDirectory.dir("compose/logs/$name") + + @get:Internal + val verbose: Property = objects.notNullProperty().apply { + set(providers.provider { + logger.isDebugEnabled || ComposeProperties.isVerbose(providers).get() + }) + } + + @get:Internal + internal val runExternalTool: ExternalToolRunner + get() = ExternalToolRunner(verbose, logsDir, execOperations) +} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/requiredDslProperty.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/requiredDslProperty.kt new file mode 100644 index 0000000000..56dc0e801b --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/requiredDslProperty.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2022 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.internal + +import kotlin.reflect.KProperty + +internal fun requiredDslProperty(missingMessage: String) = RequiredPropertyDelegate(missingMessage) + +class RequiredPropertyDelegate(val missingMessage: String) { + var realValue: T? = null + operator fun setValue( + ref: Any, + property: KProperty<*>, + newValue: T + ) { + realValue = newValue + } + + operator fun getValue( + ref: Any, + property: KProperty<*> + ): T { + return realValue ?: error(missingMessage) + } +}