diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ec073dcc..0de86304 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Dec 09 14:59:52 CET 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https://services.gradle.org/distributions/gradle-7.3.3-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/BuildFlutterDebugAppTask.kt b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/BuildFlutterDebugAppTask.kt deleted file mode 100644 index 1dcefa65..00000000 --- a/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/BuildFlutterDebugAppTask.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* Copyright (c) 2021 - 2022 Buijs Software - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ -package dev.buijs.klutter.tasks - -import dev.buijs.klutter.kore.KlutterTask -import java.io.File - -/** - * Task to build debug .apk for Android and Runner.app for IOS. - */ -open class BuildAndroidAndIosWithFlutterTask( - private val pathToFlutterApp: File, - private val pathToTestFolder: File, -) : KlutterTask { - override fun run() { - buildAndroid(pathToTestFolder, pathToFlutterApp) - buildIos(pathToTestFolder, pathToFlutterApp) - } -} - -/** - * Task to build debug .apk Android artifact. - */ -open class BuildAndroidWithFlutterTask( - private val pathToTestFolder: File, - private val pathToFlutterApp: File, -) : KlutterTask { - override fun run() = buildAndroid(pathToTestFolder, pathToFlutterApp) -} - -/** - * Task to build Runner.app IOS artifact. - */ -open class BuildIosWithFlutterTask( - private val pathToTestFolder: File, - private val pathToFlutterApp: File, -) : KlutterTask { - override fun run() = buildIos(pathToTestFolder, pathToFlutterApp) -} - -private fun buildIos( - pathToTestFolder: File, - pathToToFlutterApp: File, -) = IosArtifactBuildTask( - pathToFlutterApp = pathToToFlutterApp, - pathToOutput = pathToTestFolder.resolve("src/test/resources"), -).run() - -private fun buildAndroid( - pathToTestFolder: File, - pathToToFlutterApp: File, -) = AndroidArtifactBuildTask( - pathToFlutterApp = pathToToFlutterApp, - pathToOutput = pathToTestFolder.resolve("src/test/resources"), -).run() \ No newline at end of file diff --git a/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/ArtifactBuildTask.kt b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/BuildTasks.kt similarity index 56% rename from lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/ArtifactBuildTask.kt rename to lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/BuildTasks.kt index 6a9f363d..d1031b2f 100644 --- a/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/ArtifactBuildTask.kt +++ b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/BuildTasks.kt @@ -23,6 +23,7 @@ package dev.buijs.klutter.tasks import dev.buijs.klutter.kore.KlutterException import dev.buijs.klutter.kore.KlutterTask +import dev.buijs.klutter.kore.project.Project import java.io.BufferedOutputStream import java.io.File import java.io.FileOutputStream @@ -30,9 +31,96 @@ import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream /** + * Task to build a Klutter plugin project. * + * Executes the following steps: + * - clean platform module + * - build platform module + * - create XCFramework + * - klutterCopyAarFile + * - klutterCopyFramework + * + */ +class BuildKlutterPluginProjectTask( + private val project: Project, + private val executor: CliExecutor = CliExecutor(), +) : KlutterTask { + + override fun run() { + executor.execute( + command = """./gradlew clean build assemblePlatformReleaseXCFramework -p platform""", + runFrom = project.root.folder, + ) + + executor.execute( + command = """./gradlew klutterCopyAarFile""", + runFrom = project.root.folder, + timeout = 30 + ) + + executor.execute( + command = """./gradlew klutterCopyFramework""", + runFrom = project.root.folder, + timeout = 30 + ) + + } + +} + +/** + * Task to build debug .apk for Android and Runner.app for IOS. */ -sealed class ArtifactBuildTask( +class BuildAndroidAndIosWithFlutterTask( + private val pathToFlutterApp: File, + private val pathToTestFolder: File, +) : KlutterTask { + override fun run() { + buildAndroid(pathToTestFolder, pathToFlutterApp) + buildIos(pathToTestFolder, pathToFlutterApp) + } +} + +/** + * Task to build debug .apk Android artifact. + */ +class BuildAndroidWithFlutterTask( + private val pathToTestFolder: File, + private val pathToFlutterApp: File, +) : KlutterTask { + override fun run() = buildAndroid(pathToTestFolder, pathToFlutterApp) +} + +/** + * Task to build Runner.app IOS artifact. + */ +class BuildIosWithFlutterTask( + private val pathToTestFolder: File, + private val pathToFlutterApp: File, +) : KlutterTask { + override fun run() = buildIos(pathToTestFolder, pathToFlutterApp) +} + +private fun buildIos( + pathToTestFolder: File, + pathToToFlutterApp: File, +) = IosArtifactBuildTask( + pathToFlutterApp = pathToToFlutterApp, + pathToOutput = pathToTestFolder.resolve("src/test/resources"), +).run() + +private fun buildAndroid( + pathToTestFolder: File, + pathToToFlutterApp: File, +) = AndroidArtifactBuildTask( + pathToFlutterApp = pathToToFlutterApp, + pathToOutput = pathToTestFolder.resolve("src/test/resources"), +).run() + +/** + * + */ +private sealed class ArtifactBuildTask( /** * Path to the Flutter frontend folder. @@ -46,14 +134,14 @@ sealed class ArtifactBuildTask( ) : KlutterTask { - internal fun pathToFlutterApp(): File { + fun pathToFlutterApp(): File { if (!pathToFlutterApp.exists()) { throw KlutterException("Missing directory: $pathToFlutterApp.") } return pathToFlutterApp } - internal fun pathToOutputFolder(): File { + fun pathToOutputFolder(): File { if (!pathToOutput.exists()) { throw KlutterException("Missing output directory: $pathToOutput.") } @@ -62,16 +150,20 @@ sealed class ArtifactBuildTask( } /** - * + * Task to build debug app with flutter. */ -class AndroidArtifactBuildTask( +private class AndroidArtifactBuildTask( pathToFlutterApp: File, pathToOutput: File, + private val executor: CliExecutor = CliExecutor(), ): ArtifactBuildTask(pathToFlutterApp, pathToOutput) { override fun run() { // Build the artifact using Flutter. - """flutter build apk --debug""".execute(pathToFlutterApp()) + executor.execute( + command = """flutter build apk --debug""", + runFrom = pathToFlutterApp(), + ) // Check if artifact exists and fail if not. val artifact = pathToFlutterApp().resolve("build/app/outputs/flutter-apk/app-debug.apk").also { @@ -88,16 +180,20 @@ class AndroidArtifactBuildTask( } /** - * + * Task to build Runner.app with Flutter. */ -class IosArtifactBuildTask( +private class IosArtifactBuildTask( pathToFlutterApp: File, pathToOutput: File, + private val executor: CliExecutor = CliExecutor(), ): ArtifactBuildTask(pathToFlutterApp, pathToOutput) { override fun run() { // Build the artifact using Flutter. - """flutter build ios --no-codesign --debug""".execute(pathToFlutterApp()) + executor.execute( + command = """flutter build ios --no-codesign --debug""", + runFrom = pathToFlutterApp(), + ) // Check if artifact exists and fail if not. val artifact = pathToFlutterApp().resolve("build/ios/iphonesimulator/Runner.app").also { diff --git a/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/CliExecutor.kt b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/CliExecutor.kt new file mode 100644 index 00000000..8704d7ff --- /dev/null +++ b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/CliExecutor.kt @@ -0,0 +1,71 @@ +package dev.buijs.klutter.tasks + +import dev.buijs.klutter.kore.KlutterException +import java.io.File +import java.util.concurrent.TimeUnit + +/** + * Execute a CLI command. + * + */ +open class CliExecutor { + + /** + * Execute a CLI command. + */ + fun execute( + /** + * Folder from where to execute this command. + */ + runFrom: File, + + /** + * Maximum time in seconds to wait for the command to be executed. + */ + timeout: Long? = null, + + /** + * The command to be executed. + */ + command: String, + ): String = command.execute( + runFrom = runFrom, + timeout = timeout, + ) + + open fun String.execute( + /** + * Folder from where to execute this command. + */ + runFrom: File, + + /** + * Maximum time in seconds to wait for the command to be executed. + */ + timeout: Long? = null, + ): String { + + val process = ProcessBuilder() + .command(split(" ")) + .directory(runFrom) + .start() + + if(timeout == null) { + process.waitFor() + } else { + process.waitFor(timeout, TimeUnit.SECONDS) + } + + if(process.exitValue() != 0) { + throw KlutterException( + "Failed to execute command: \n${ + process.errorStream.reader().readText() + }" + ) + } + + return process.inputStream.readBytes().decodeToString() + + } + +} \ No newline at end of file diff --git a/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/CopyTasks.kt b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/CopyTasks.kt new file mode 100644 index 00000000..aa050c9f --- /dev/null +++ b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/CopyTasks.kt @@ -0,0 +1,104 @@ +/* Copyright (c) 2021 - 2022 Buijs Software + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package dev.buijs.klutter.tasks + +import dev.buijs.klutter.kore.KlutterTask +import dev.buijs.klutter.kore.shared.verifyExists +import java.io.File + +/** + * Copy the platform release aar file build in the platform module + * to the plugin android/Klutter folder. + */ +class CopyAndroidAarFileKlutterTask( + private val isApplication: Boolean, + private val pathToRoot: File, + private val pluginName: String? = null, +): KlutterTask { + + private val pathToTarget: String = + if(isApplication) "app/backend" else "" + + private val pathToSource: String = + if(isApplication) "lib" else "platform" + + override fun run() { + val name = when { + isApplication -> { + "lib" + } + pluginName != null -> { + pluginName + } + else -> { + "platform" + } + } + + val pathToAarFile = pathToRoot + .resolve(pathToSource) + .resolve("build/outputs/aar/$name-release.aar") + .verifyExists() + + val target = pathToRoot + .resolve(pathToTarget) + .resolve("android/klutter") + .verifyExists() + + pathToAarFile.renameTo(target.resolve("platform.aar")) + } + + +} + +/** + * Copy the 'Platform.xcframework' build in the platform module + * to the plugin ios/Klutter folder. + */ +class CopyIosFrameworkKlutterTask( + isApplication: Boolean, + private val pathToRoot: File, +): KlutterTask { + + private val pathToTarget: String = + if(isApplication) "app/backend" else "" + + private val pathToSource: String = + if(isApplication) "lib" else "platform" + + override fun run() { + val target = pathToRoot + .resolve(pathToTarget) + .resolve("ios/Klutter") + .verifyExists() + .resolve("Platform.xcframework") + .also { if(it.exists()) it.deleteRecursively() } + + val pathToIosFramework = pathToRoot + .resolve(pathToSource) + .resolve("build/XCFrameworks/release/Platform.xcframework") + .verifyExists() + + pathToIosFramework.copyRecursively(target) + } + +} \ No newline at end of file diff --git a/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/Executable.kt b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/Executable.kt deleted file mode 100644 index af9ccf33..00000000 --- a/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/Executable.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.buijs.klutter.tasks - -import dev.buijs.klutter.kore.KlutterException -import java.io.File -import java.util.concurrent.TimeUnit - -internal fun String.execute(runFrom: File): String { - val process = ProcessBuilder() - .command(split(" ")) - .directory(runFrom) - .start() - - process.waitFor(60, TimeUnit.SECONDS) - - if(process.exitValue() != 0) { - throw KlutterException( - "Failed to execute command: \n${ - process.errorStream.reader().readText() - }" - ) - } - - return process.inputStream.readBytes().decodeToString() -} \ No newline at end of file diff --git a/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/GenerateAdaptersForApplicationTask.kt b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/GenerateAdaptersForApplicationTask.kt new file mode 100644 index 00000000..7cc1d2fd --- /dev/null +++ b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/GenerateAdaptersForApplicationTask.kt @@ -0,0 +1,96 @@ +/* Copyright (c) 2021 - 2022 Buijs Software + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package dev.buijs.klutter.tasks + +import dev.buijs.klutter.kore.KlutterTask +import dev.buijs.klutter.kore.project.* +import dev.buijs.klutter.kore.shared.excludeArm64 +import dev.buijs.klutter.kore.shared.maybeCreate +import dev.buijs.klutter.kore.shared.toChannelName +import dev.buijs.klutter.kore.shared.write +import dev.buijs.klutter.kore.templates.* + +/** + * Task to generate the boilerplate code required to let Kotlin Multiplatform and Flutter communicate. + */ +internal class GenerateAdaptersForApplicationTask( + private val android: Android, + private val ios: IOS, + private val root: Root, + private val platform: Platform, +) : KlutterTask{ + + private val methodChannelName = root + .toPubspec() + .toChannelName() + + override fun run() { + platform.collect().let { + it.flutter(root) + it.android(android) + it.ios(ios) + } + } + + private fun AdapterData.flutter(root: Root) { + root.pathToLib.maybeCreate().write( + KomposeFlutterAdapter( + pluginClassName = root.pluginClassName, + methodChannelName = methodChannelName, + messages = messages, + enumerations = enumerations, + ) + ) + } + + private fun AdapterData.ios(ios: IOS) { + ios.podspec().excludeArm64("dependency'Flutter'") + ios.pathToPlugin.maybeCreate().write( + KomposeIosAdapter( + pluginClassName = ios.pluginClassName, + methodChannelName = methodChannelName, + controllers = controllers, + ) + ) + ios.pathToClasses.resolve("SwiftKomposeAppState.swift").write( + IosAdapterState(controllers) + ) + } + + private fun AdapterData.android(android: Android) { + android.pathToPlugin.maybeCreate().write( + KomposeAndroidAdapter( + pluginClassName = android.pluginClassName, + pluginPackageName = android.pluginPackageName, + methodChannelName = methodChannelName, + ) + ) + + android.pathToPluginPackage.resolve("KomposeAppState.kt").write( + AndroidAdapterState( + pluginPackageName = android.pluginPackageName, + fullyQualifiedControllers = controllers + ) + ) + } + +} \ No newline at end of file diff --git a/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/GenerateAdaptersForPluginTask.kt b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/GenerateAdaptersForPluginTask.kt new file mode 100644 index 00000000..1aa78e0a --- /dev/null +++ b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/GenerateAdaptersForPluginTask.kt @@ -0,0 +1,88 @@ +/* Copyright (c) 2021 - 2022 Buijs Software + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package dev.buijs.klutter.tasks + +import dev.buijs.klutter.kore.KlutterTask +import dev.buijs.klutter.kore.project.* +import dev.buijs.klutter.kore.shared.excludeArm64 +import dev.buijs.klutter.kore.shared.maybeCreate +import dev.buijs.klutter.kore.shared.toChannelName +import dev.buijs.klutter.kore.shared.write +import dev.buijs.klutter.kore.templates.AndroidAdapter +import dev.buijs.klutter.kore.templates.FlutterAdapter +import dev.buijs.klutter.kore.templates.IosAdapter + +/** + * Task to generate the boilerplate code required to let Kotlin Multiplatform and Flutter communicate. + */ +internal class GenerateAdaptersForPluginTask( + private val android: Android, + private val ios: IOS, + private val root: Root, + private val platform: Platform, +) : KlutterTask{ + + private val methodChannelName = root.toPubspec().toChannelName() + + override fun run() { + platform.collect().let { + it.flutter(root) + it.android(android) + it.ios(ios) + } + } + + private fun AdapterData.flutter(root: Root){ + root.pathToLib.maybeCreate().write( + FlutterAdapter( + pluginClassName = root.pluginClassName, + methodChannelName = methodChannelName, + methods = methods, + messages = messages, + enumerations = enumerations, + ) + ) + } + + private fun AdapterData.ios(ios: IOS){ + ios.podspec().excludeArm64("dependency'Flutter'") + ios.pathToPlugin.maybeCreate().write( + IosAdapter( + pluginClassName = ios.pluginClassName, + methodChannelName = methodChannelName, + methods = methods, + ) + ) + } + + private fun AdapterData.android(android: Android){ + android.pathToPlugin.maybeCreate().write( + AndroidAdapter( + pluginClassName = android.pluginClassName, + pluginPackageName = android.pluginPackageName, + methodChannelName = methodChannelName, + methods = methods, + ) + ) + } + +} \ No newline at end of file diff --git a/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/AdapterGeneratorTask.kt b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/GenerateAdaptersTask.kt similarity index 63% rename from lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/AdapterGeneratorTask.kt rename to lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/GenerateAdaptersTask.kt index b2faaa15..dfe339c6 100644 --- a/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/AdapterGeneratorTask.kt +++ b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/GenerateAdaptersTask.kt @@ -25,111 +25,31 @@ import dev.buijs.klutter.kore.KlutterException import dev.buijs.klutter.kore.KlutterTask import dev.buijs.klutter.kore.project.* import dev.buijs.klutter.kore.shared.* -import dev.buijs.klutter.kore.templates.* import java.io.File /** * Task to generate the boilerplate code required to let Kotlin Multiplatform and Flutter communicate. */ -open class AdapterGeneratorTask( - private val android: Android, - private val ios: IOS, - private val root: Root, - private val platform: Platform, -) : KlutterTask{ - - companion object { - fun from(project: Project) = AdapterGeneratorTask( - ios = project.ios, - root = project.root, - android = project.android, - platform = project.platform, - ) - } - - private val methodChannelName = root.toPubspec().toChannelName() +class GenerateAdaptersTask( + private val project: Project, + private val isApplication: Boolean, +) : KlutterTask { override fun run() { - platform.collect().let { - it.flutter(root) - it.android(android) - it.ios(ios) - } - } - - private fun AdapterData.flutter(root: Root){ - if(controllers.isNotEmpty()) { - root.pathToLib.maybeCreate().write( - KomposeFlutterAdapter( - pluginClassName = root.pluginClassName, - methodChannelName = methodChannelName, - messages = messages, - enumerations = enumerations, - ) - ) - } else { - root.pathToLib.maybeCreate().write( - FlutterAdapter( - pluginClassName = root.pluginClassName, - methodChannelName = methodChannelName, - methods = methods, - messages = messages, - enumerations = enumerations, - ) - ) - } - } - - private fun AdapterData.ios(ios: IOS){ - ios.podspec().excludeArm64("dependency'Flutter'") - if(controllers.isNotEmpty()) { - ios.pathToPlugin.maybeCreate().write( - KomposeIosAdapter( - pluginClassName = ios.pluginClassName, - methodChannelName = methodChannelName, - controllers = controllers, - ) - ) - ios.pathToClasses.resolve("SwiftKomposeAppState.swift").write( - IosAdapterState(controllers) - ) - - } else { - ios.pathToPlugin.maybeCreate().write( - IosAdapter( - pluginClassName = ios.pluginClassName, - methodChannelName = methodChannelName, - methods = methods, - ) - ) - } - } - - private fun AdapterData.android(android: Android){ - if(controllers.isNotEmpty()) { - android.pathToPlugin.maybeCreate().write( - KomposeAndroidAdapter( - pluginClassName = android.pluginClassName, - pluginPackageName = android.pluginPackageName, - methodChannelName = methodChannelName, - ) - ) - - android.pathToPluginPackage.resolve("KomposeAppState.kt").write( - AndroidAdapterState( - pluginPackageName = android.pluginPackageName, - fullyQualifiedControllers = controllers - ) - ) + if(isApplication) { + GenerateAdaptersForApplicationTask( + ios = project.ios, + root = project.root, + android = project.android, + platform = project.platform, + ).run() } else { - android.pathToPlugin.maybeCreate().write( - AndroidAdapter( - pluginClassName = android.pluginClassName, - pluginPackageName = android.pluginPackageName, - methodChannelName = methodChannelName, - methods = methods, - ) - ) + GenerateAdaptersForPluginTask( + ios = project.ios, + root = project.root, + android = project.android, + platform = project.platform, + ).run() } } diff --git a/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/GenerateKlutterPluginProjectTask.kt b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/GenerateKlutterPluginProjectTask.kt deleted file mode 100644 index dea40f36..00000000 --- a/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/GenerateKlutterPluginProjectTask.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* Copyright (c) 2021 - 2022 Buijs Software - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ -package dev.buijs.klutter.tasks - -import dev.buijs.klutter.kore.KlutterTask -import dev.buijs.klutter.kore.shared.verifyExists -import java.io.File - -private const val klutterPubVersion = "0.2.0" - -/** - * Task to generate a klutter plugin project. - */ -class GenerateKlutterPluginProjectTask( - /** - * Path to the folder where to create the new project. - */ - private val pathToRoot: String, - - /** - * Name of the application. - */ - private val appName: String, - - /** - * Name of the application organisation. - */ - private val groupName: String, - -) : KlutterTask { - - override fun run() { - File(pathToRoot).also { root -> - root.verifyExists() - root.createApp() - } - } - - private fun File.createApp() { - "flutter create $appName --org $groupName --template=plugin --platforms=android,ios".execute(this) - - val pluginFolder = resolve(appName) - val exampleFolder = resolve(appName).resolve("example") - val pubspecs = listOf( - pluginFolder.resolve("pubspec.yaml"), - exampleFolder.resolve("pubspec.yaml"), - ) - - for(pubspec in pubspecs) { - val lines = pubspec.readLines() - val updated = mutableListOf() - for(line in lines) { - updated.add(line) - if (line.trim().startsWith("dependencies:")) { - updated.add(" klutter: ^$klutterPubVersion") - } - } - pubspec.writeText(updated.joinToString("\n")) - } - - "flutter pub get".execute(pluginFolder) - "flutter pub get".execute(exampleFolder) - "flutter pub run klutter:producer init".execute(pluginFolder) - "flutter pub run klutter:consumer init".execute(exampleFolder) - "flutter pub run klutter:consumer add=$appName".execute(exampleFolder) - - resolve("gradlew.bat").let { gradlew -> - gradlew.setExecutable(true) - gradlew.setReadable(true) - gradlew.setWritable(true) - } - - resolve("gradlew").let { gradlew -> - gradlew.setExecutable(true) - gradlew.setReadable(true) - gradlew.setWritable(true) - } - } - -} \ No newline at end of file diff --git a/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/GeneratePluginProjectTask.kt b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/GeneratePluginProjectTask.kt new file mode 100644 index 00000000..458cff44 --- /dev/null +++ b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/GeneratePluginProjectTask.kt @@ -0,0 +1,139 @@ +/* Copyright (c) 2021 - 2022 Buijs Software + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package dev.buijs.klutter.tasks + +import dev.buijs.klutter.kore.KlutterTask +import dev.buijs.klutter.kore.shared.verifyExists +import java.io.File + + +private const val klutterPubVersion = "0.2.0" + +/** + * Task to generate a klutter plugin project. + */ +class GeneratePluginProjectTask( + /** + * Path to the folder where to create the new project. + */ + private val pathToRoot: String, + + /** + * Name of the plugin. + */ + private val pluginName: String, + + /** + * Name of the plugin organisation. + */ + private val groupName: String, + + /** + * Utility to execute the flutter commands. + * + * Can be swapped for testing purposes. + */ + private val executor: CliExecutor = CliExecutor(), + + ) : KlutterTask { + + override fun run() { + File(pathToRoot).also { root -> + root.verifyExists() + root.createApp() + } + } + + private fun File.createApp() { + "flutter create $pluginName --org $groupName --template=plugin --platforms=android,ios".execute(this) + + val pluginFolder = resolve(pluginName) + val exampleFolder = resolve(pluginName).resolve("example") + val pubspecs = listOf( + pluginFolder.resolve("pubspec.yaml"), + exampleFolder.resolve("pubspec.yaml"), + ) + + for(pubspec in pubspecs) { + val updated = mutableListOf().also { + pubspec.verifyExists().readLines().forEach { line -> + it.add(line) + if (line.trim().startsWith("dependencies:")) { + it.add(" klutter: ^$klutterPubVersion") + } + } + } + pubspec.writeText(updated.joinToString("\n")) + } + + "flutter pub get".execute(pluginFolder) + "flutter pub get".execute(exampleFolder) + "flutter pub run klutter:producer init".execute(pluginFolder) + "flutter pub run klutter:consumer init".execute(exampleFolder) + "flutter pub run klutter:producer install=library".execute(pluginFolder) + + pluginFolder.resolve("android/local.properties") + .copyTo(pluginFolder.resolve("local.properties")) + + // You should test, but we're going to do that with Spock/JUnit + // in the platform module, not with Dart in the root/test folder. + pluginFolder.resolve("test").deleteRecursively() + + // Change the README content to explain Klutter plugin development. + pluginFolder.resolve("README.md").let { readme -> + + // Seems redundant to delete and then recreate the README.md file. + // However, the project is created by invoking the Flutter version + // installed by the end-user. Future versions of Flutter might not + // Create a README.md file but a readme.md, PLEASE_README.md or not + // a readme file at all! + // + // In that case this code won't try to delete a file that does not + // exist and only create a new file. We do NOT want the task to fail + // because of a README.md file now, do we? :-) + if(readme.exists()) readme.delete() + readme.createNewFile() + readme.writeText(""" + # $pluginName + |A new Klutter plugin project. + |Klutter is a framework which interconnects Flutter and Kotlin Multiplatform. + | + |## Getting Started + |This project is a starting point for a Klutter + |[plug-in package](https://github.com/buijs-dev/klutter), + |a specialized package that includes platform-specific implementation code for + |Android and/or iOS. + | + |This platform-specific code is written in Kotlin programming language by using + |Kotlin Multiplatform. + """.trimMargin()) + } + + } + + /** + * Execute a CLI command in the given folder. + */ + private fun String.execute(runFrom: File) = + executor.execute(runFrom = runFrom, command = this) + +} \ No newline at end of file diff --git a/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/ExcludeArchsPlatformPodspecTask.kt b/lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/VisitorTasks.kt similarity index 100% rename from lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/ExcludeArchsPlatformPodspecTask.kt rename to lib/klutter-tasks/src/main/kotlin/dev/buijs/klutter/tasks/VisitorTasks.kt diff --git a/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/CopyAndroidAarFileKlutterTaskSpec.groovy b/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/CopyAndroidAarFileKlutterTaskSpec.groovy new file mode 100644 index 00000000..6f88b356 --- /dev/null +++ b/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/CopyAndroidAarFileKlutterTaskSpec.groovy @@ -0,0 +1,151 @@ +/* Copyright (c) 2021 - 2022 Buijs Software + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package dev.buijs.klutter.tasks + +import dev.buijs.klutter.kore.KlutterException +import spock.lang.Specification +import java.nio.file.Files + +class CopyAndroidAarFileKlutterTaskSpec extends Specification { + + def "For application copies lib-release.aar from lib to app/android/klutter folder"() { + given: + def root = Files.createTempDirectory("") + .toAbsolutePath() + .toFile() + + def target = new File("${root.absolutePath}/app/backend/android/klutter") + target.mkdirs() + + def buildFolder = new File("${root.absolutePath}/lib/build/outputs/aar") + buildFolder.mkdirs() + + def aarFile = new File("${buildFolder.absolutePath}/lib-release.aar") + aarFile.createNewFile() + aarFile.write("TC1") + + when: + new CopyAndroidAarFileKlutterTask(true, root, null).run() + + then: + with(target.absolutePath) { + new File("${it}/platform.aar").exists() + new File("${it}/platform.aar").text == "TC1" + } + } + + def "For plugin copies platform-release.aar from platform to root/android/klutter folder"() { + given: + def root = Files.createTempDirectory("") + .toAbsolutePath() + .toFile() + + def target = new File("${root.absolutePath}/android/klutter") + target.mkdirs() + + def buildFolder = new File("${root.absolutePath}/platform/build/outputs/aar") + buildFolder.mkdirs() + + def aarFile = new File("${buildFolder.absolutePath}/platform-release.aar") + aarFile.createNewFile() + aarFile.write("TC2") + + when: + new CopyAndroidAarFileKlutterTask(false, root, null).run() + + then: + with(target.absolutePath) { + new File("${it}/platform.aar").exists() + new File("${it}/platform.aar").text == "TC2" + } + } + + def "For plugin copies plugin_name-release.aar from platform to root/android/klutter folder"() { + given: + def pluginName = "my_plugin" + def root = Files.createTempDirectory("") + .toAbsolutePath() + .toFile() + + def target = new File("${root.absolutePath}/android/klutter") + target.mkdirs() + + def buildFolder = new File("${root.absolutePath}/platform/build/outputs/aar") + buildFolder.mkdirs() + + def aarFile = new File("${buildFolder.absolutePath}/$pluginName-release.aar") + aarFile.createNewFile() + aarFile.write("TC2") + + when: + new CopyAndroidAarFileKlutterTask(false, root, pluginName).run() + + then: + with(target.absolutePath) { + new File("${it}/platform.aar").exists() + new File("${it}/platform.aar").text == "TC2" + } + } + + def "An exception is thrown when the aar file does not exist"() { + given: + def pluginName = "my_plugin" + def root = Files.createTempDirectory("") + .toAbsolutePath() + .toFile() + + def target = new File("${root.absolutePath}/android/klutter") + target.mkdirs() + + when: + new CopyAndroidAarFileKlutterTask(false, root, pluginName).run() + + then: + KlutterException e = thrown() + e.message.startsWith("Path does not exist:") + e.message.endsWith("my_plugin-release.aar") + } + + def "An exception is thrown when the root/android/klutter folder does not exist"() { + given: + def pluginName = "my_plugin" + def root = Files.createTempDirectory("") + .toAbsolutePath() + .toFile() + + def buildFolder = new File("${root.absolutePath}/platform/build/outputs/aar") + buildFolder.mkdirs() + + def aarFile = new File("${buildFolder.absolutePath}/$pluginName-release.aar") + aarFile.createNewFile() + aarFile.write("TC4") + + when: + new CopyAndroidAarFileKlutterTask(false, root, pluginName).run() + + then: + KlutterException e = thrown() + e.message.startsWith("Path does not exist:") + e.message.endsWith("/android/klutter") + } + +} \ No newline at end of file diff --git a/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/CopyIosFrameworkKlutterTaskSpec.groovy b/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/CopyIosFrameworkKlutterTaskSpec.groovy new file mode 100644 index 00000000..84d6fbf1 --- /dev/null +++ b/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/CopyIosFrameworkKlutterTaskSpec.groovy @@ -0,0 +1,125 @@ +/* Copyright (c) 2021 - 2022 Buijs Software + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package dev.buijs.klutter.tasks + +import dev.buijs.klutter.kore.KlutterException +import spock.lang.Specification + +import java.nio.file.Files + +class CopyIosFrameworkKlutterTaskSpec extends Specification { + + def "For application copies Platform.xcframework from lib to app folder"() { + given: + def root = Files.createTempDirectory("") + .toAbsolutePath() + .toFile() + + // root/app/backend/ios/Klutter exists + // root/app/backend/ios/Klutter/Platform.xcframework does not exist + def target = new File("${root.absolutePath}/app/backend/ios/Klutter") + target.mkdirs() + + // root/lib/build/XCFrameworks/release/Platform.xcframework exists + def xcframework = new File("${root.absolutePath}/lib/build/XCFrameworks/release/Platform.xcframework") + xcframework.mkdirs() + + def plist = new File("${xcframework.absolutePath}/Info.plist") + plist.createNewFile() + plist.write("TC1") + + when: + new CopyIosFrameworkKlutterTask(true, root).run() + + then: + with(target.absolutePath) { + new File("${it}/Platform.xcframework").exists() + new File("${it}/Platform.xcframework/Info.plist").exists() + new File("${it}/Platform.xcframework/Info.plist").text == "TC1" + } + + } + + def "For plugin copies Platform.xcframework from platform to root/ios folder"() { + given: + def root = Files.createTempDirectory("") + .toAbsolutePath() + .toFile() + + // root/app/backend/ios/Klutter exists + // root/app/backend/ios/Klutter/Platform.xcframework does not exist + def target = new File("${root.absolutePath}/ios/Klutter") + target.mkdirs() + + // root/platform/build/XCFrameworks/release/Platform.xcframework exists + def xcframework = new File("${root.absolutePath}/platform/build/XCFrameworks/release/Platform.xcframework") + xcframework.mkdirs() + + def plist = new File("${xcframework.absolutePath}/Info.plist") + plist.createNewFile() + plist.write("TC2") + + when: + new CopyIosFrameworkKlutterTask(false, root).run() + + then: + with(target.absolutePath) { + new File("${it}/Platform.xcframework").exists() + new File("${it}/Platform.xcframework/Info.plist").exists() + new File("${it}/Platform.xcframework/Info.plist").text == "TC2" + } + + } + + def "If ios/Klutter folder does not exist then an exception is thrown"() { + given: + def root = Files.createTempDirectory("") + .toAbsolutePath() + .toFile() + + when: + new CopyIosFrameworkKlutterTask(false, root).run() + + then: + KlutterException e = thrown() + e.message.startsWith("Path does not exist:") + e.message.endsWith("/ios/Klutter") + } + + def "If ios/Klutter folder does not exist then an exception is thrown"() { + given: + def root = Files.createTempDirectory("") + .toAbsolutePath() + .toFile() + + new File("${root.absolutePath}/app/backend/ios/Klutter").mkdirs() + + when: + new CopyIosFrameworkKlutterTask(true, root).run() + + then: + KlutterException e = thrown() + e.message.startsWith("Path does not exist:") + e.message.endsWith("Platform.xcframework") + } + +} \ No newline at end of file diff --git a/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/ExcludeArchsPlatformPodspecTaskSpec.groovy b/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/ExcludeArchsPlatformPodspecTaskSpec.groovy new file mode 100644 index 00000000..56c12c28 --- /dev/null +++ b/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/ExcludeArchsPlatformPodspecTaskSpec.groovy @@ -0,0 +1,47 @@ +package dev.buijs.klutter.tasks + + +import dev.buijs.klutter.kore.project.ProjectKt +import dev.buijs.klutter.kore.test.TestPlugin +import spock.lang.Specification + +class ExcludeArchsPlatformPodspecTaskSpec extends Specification { + + def "Verify excluded lines are not duplicated"(){ + + given: + def plugin = new TestPlugin() + plugin.platformPodSpec.write(podfile) + def project = ProjectKt.plugin(plugin.root) + + when: + new ExcludeArchsPlatformPodspecTask(project).run() + + then: + project.platform.podspec().text.replaceAll(" ", "") == expected.replaceAll(" ", "") + + where: + podfile = """ + Pod::Spec.new do |spec| + + spec.ios.deployment_target = '14.1' + spec.pod_target_xcconfig = { + 'KOTLIN_PROJECT_PATH' => ':klutter:ridiculous_plugin', + 'PRODUCT_MODULE_NAME' => 'ridiculous_plugin', + } + """ + expected = """ + Pod::Spec.new do |spec| + + spec.ios.deployment_target = '14.1' + spec.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } + spec.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } + spec.pod_target_xcconfig = { + 'KOTLIN_PROJECT_PATH' => ':klutter:ridiculous_plugin', + 'PRODUCT_MODULE_NAME' => 'ridiculous_plugin', + } + """ + + } + +} diff --git a/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/AdapterGeneratorTaskSpec.groovy b/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/GenerateAdaptersForPluginTaskSpec.groovy similarity index 59% rename from lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/AdapterGeneratorTaskSpec.groovy rename to lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/GenerateAdaptersForPluginTaskSpec.groovy index 848a9bed..9dde87c7 100644 --- a/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/AdapterGeneratorTaskSpec.groovy +++ b/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/GenerateAdaptersForPluginTaskSpec.groovy @@ -22,70 +22,13 @@ package dev.buijs.klutter.tasks import dev.buijs.klutter.kore.KlutterException -import dev.buijs.klutter.kore.project.ProjectKt import dev.buijs.klutter.kore.shared.DartEnum import dev.buijs.klutter.kore.shared.DartField import dev.buijs.klutter.kore.shared.DartMessage -import dev.buijs.klutter.kore.test.TestPlugin -import dev.buijs.klutter.kore.test.TestResource import dev.buijs.klutter.kore.test.TestUtil -import spock.lang.Shared import spock.lang.Specification -class AdapterGeneratorTaskSpec extends Specification { - - @Shared - def resources = new TestResource() - - def "Verify adapters are generated correctly for a plugin project"() { - - given: - def sut = new TestPlugin() - - and: - resources.copyAll([ - "platform_source_code" : sut.platformSourceFile, - "android_app_manifest" : sut.manifest, - "build_gradle_plugin" : sut.platformBuildGradle, - "settings_gradle_plugin": sut.rootSettingsGradle, - "plugin_pubspec" : sut.pubspecYaml, - "plugin_ios_podspec" : sut.iosPodspec, - ]) - - when: - with(ProjectKt.plugin(sut.root, sut.pluginName)) { project -> - new AdapterGeneratorTask( - project.android, - project.ios, - project.root, - project.platform, - ).run() - } - - then: "Verify lib/plugin.dart file is created" - with(new File("${sut.libFolder}/${sut.pluginName}.dart")) { - it.exists() - TestUtil.verify(it.text, resources.load("flutter_plugin_library")) - } - - and: "Verify SuperAwesomePlugin.kt file is generated in android folder" - with(new File("${sut.androidSrcMain}/kotlin/foo/bar/${sut.pluginName}/${sut.pluginClassName}.kt")) { - it.exists() - TestUtil.verify(it.text, resources.load("android_plugin_class")) - } - - and: "Verify SwiftSuperAwesomePlugin.kt file is generated in ios folder" - with(new File("${sut.iosClasses.absolutePath}/Swift${sut.pluginClassName}.swift")) { - it.exists() - TestUtil.verify(it.text, resources.load("ios_swift_plugin")) - } - - and: "Verify ios podspec has excluded SDK" - with(new File("${sut.ios.absolutePath}/${sut.pluginName}.podspec")) { - it.exists() - TestUtil.verify(it.text, resources.load("plugin_ios_podspec_excluded")) - } - } +class GenerateAdaptersForPluginTaskSpec extends Specification { def "Validate throws exception if customDataType list is not empty after validating"() { given: @@ -98,7 +41,7 @@ class AdapterGeneratorTaskSpec extends Specification { ) when: - AdapterGeneratorTaskKt.validate([message1, message2], []) + GenerateAdaptersTaskKt.validate([message1, message2], []) then: KlutterException e = thrown() @@ -142,7 +85,7 @@ class AdapterGeneratorTaskSpec extends Specification { def enum1 = new DartEnum("GruMood", ["BAD, GOOD, CARING, HATING, CONQUER_THE_WORLD"], []) then: - AdapterGeneratorTaskKt.validate([message1, message2, message3], [enum1]) + GenerateAdaptersTaskKt.validate([message1, message2, message3], [enum1]) } } \ No newline at end of file diff --git a/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/GenerateAdaptersTaskSpec.groovy b/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/GenerateAdaptersTaskSpec.groovy new file mode 100644 index 00000000..9a9ba2ba --- /dev/null +++ b/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/GenerateAdaptersTaskSpec.groovy @@ -0,0 +1,91 @@ +/* Copyright (c) 2021 - 2022 Buijs Software + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package dev.buijs.klutter.tasks + +import dev.buijs.klutter.kore.KlutterException +import dev.buijs.klutter.kore.shared.DartEnum +import dev.buijs.klutter.kore.shared.DartField +import dev.buijs.klutter.kore.shared.DartMessage +import dev.buijs.klutter.kore.test.TestUtil +import spock.lang.Specification + +class GenerateAdaptersTaskSpec extends Specification { + + def "Validate throws exception if customDataType list is not empty after validating"() { + given: + def message1 = new DartMessage("Bob", + [new DartField("FartCannon", "", false, false, true)] + ) + + def message2 = new DartMessage("Dave", + [new DartField("String", "", false, false, false)] + ) + + when: + GenerateAdaptersTaskKt.validate([message1, message2], []) + + then: + KlutterException e = thrown() + TestUtil.verify(e.message, + """Processing annotation '@KlutterResponse' failed, caused by: + + Could not resolve the following classes: + + - 'FartCannon' + + + Verify if all KlutterResponse annotated classes comply with the following rules: + + 1. Must be an open class + 2. Fields must be immutable + 3. Constructor only (no body) + 4. No inheritance + 5. Any field type should comply with the same rules + + If this looks like a bug please file an issue at: https://github.com/buijs-dev/klutter/issues + """ + + ) + + } + + def "Validate no exception is thrown if customDataType list not empty after validating"() { + when: + def message1 = new DartMessage("Bob", + [new DartField("FartCannon", "", false, false, true)] + ) + + def message2 = new DartMessage("Dave", + [new DartField("String", "", false, false, false)] + ) + + def message3 = new DartMessage("FartCannon", + [new DartField("GruMood", "intensity", false, false, true)] + ) + + def enum1 = new DartEnum("GruMood", ["BAD, GOOD, CARING, HATING, CONQUER_THE_WORLD"], []) + + then: + GenerateAdaptersTaskKt.validate([message1, message2, message3], [enum1]) + + } +} \ No newline at end of file diff --git a/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/GeneratePluginProjectTaskSpec.groovy b/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/GeneratePluginProjectTaskSpec.groovy new file mode 100644 index 00000000..bb273755 --- /dev/null +++ b/lib/klutter-tasks/src/test/groovy/dev/buijs/klutter/tasks/GeneratePluginProjectTaskSpec.groovy @@ -0,0 +1,295 @@ +/* Copyright (c) 2021 - 2022 Buijs Software + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package dev.buijs.klutter.tasks + +import dev.buijs.klutter.kore.test.TestUtil +import spock.lang.Shared +import spock.lang.Specification + +import java.nio.file.Files + +class GeneratePluginProjectTaskSpec extends Specification { + + @Shared + def executor = new Exeggutor() + + @Shared + def pluginName = "my_awesome_plugin" + + @Shared + def groupName = "com.example.awesomeness" + + @Shared + def root = Files.createTempDirectory("").toFile() + + @Shared + def pathToRoot = root.absolutePath + + @Shared + def plugin = new File("${pathToRoot}/$pluginName") + + @Shared + def pathToPlugin = plugin.absolutePath + + @Shared + def example = new File("${pathToPlugin}/example") + + @Shared + def pathToExample = example.absolutePath + + @Shared + def sut = new GeneratePluginProjectTask(pathToRoot, pluginName, groupName, executor) + + @Shared + def createFlutterPlugin = "flutter create my_awesome_plugin --org com.example.awesomeness --template=plugin --platforms=android,ios" + + @Shared + def flutterPubGet = "flutter pub get" + + @Shared + def klutterProducerInit = "flutter pub run klutter:producer init" + + @Shared + def klutterConsumerInit = "flutter pub run klutter:consumer init" + + @Shared + def klutterProducerInstallLibrary = "flutter pub run klutter:producer install=library" + + def setupSpec() { + plugin.mkdirs() + example.mkdirs() + } + + def "Verify a new project is created"(){ + given: + def pubspecInRoot = new File("${pathToPlugin}/pubspec.yaml") + pubspecInRoot.createNewFile() + pubspecInRoot.write(rootPubspecYaml) + + def pubspecInExample = new File("${pathToExample}/pubspec.yaml") + pubspecInExample.createNewFile() + pubspecInExample.write(examplePubspecYaml) + + new File("${pathToPlugin}/android").mkdirs() + def localProperties = new File("${pathToPlugin}/android/local.properties") + localProperties.createNewFile() + localProperties.write("hello=true") + + and: + executor.putExpectation(pathToRoot, createFlutterPlugin) + executor.putExpectation(pathToPlugin, flutterPubGet) + executor.putExpectation(pathToExample, flutterPubGet) + executor.putExpectation(pathToPlugin, klutterProducerInit) + executor.putExpectation(pathToExample, klutterConsumerInit) + executor.putExpectation(pathToPlugin, klutterProducerInstallLibrary) + + when: + sut.run() + + then: "Klutter is added as dependency to pubspec.yaml" + TestUtil.verify(pubspecInRoot.text, rootPubspecYamlWithKlutter) + TestUtil.verify(pubspecInExample.text, examplePubspecYamlWithKlutter) + + and: "local.properties is copied to root" + with(new File("$pathToPlugin/local.properties")) { + it.exists() + it.text.contains("hello=true") + } + + and: "test folder is deleted" + !new File("$pathToPlugin/test").exists() + + and: "a new README.md is created" + with(new File("$pathToPlugin/README.md")) { + it.exists() + TestUtil.verify(it.text, readme) + } + } + + @Shared + def readme = """ + # my_awesome_plugin + A new Klutter plugin project. + Klutter is a framework which interconnects Flutter and Kotlin Multiplatform. + + ## Getting Started + This project is a starting point for a Klutter + [plug-in package](https://github.com/buijs-dev/klutter), + a specialized package that includes platform-specific implementation code for + Android and/or iOS. + + This platform-specific code is written in Kotlin programming language by using + Kotlin Multiplatform. + """ + + @Shared + def rootPubspecYaml = """ + name: my_awesome_plugin + description: A new Flutter plugin project. + version: 0.0.1 + homepage: + + environment: + sdk: ">=2.17.5 <3.0.0" + flutter: ">=2.5.0" + + dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + + dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + """ + + @Shared + def rootPubspecYamlWithKlutter = """ + name: my_awesome_plugin + description: A new Flutter plugin project. + version: 0.0.1 + homepage: + + environment: + sdk: ">=2.17.5 <3.0.0" + flutter: ">=2.5.0" + + dependencies: + klutter: ^0.2.0 + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + + dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + """ + + @Shared + def examplePubspecYaml = """ + name: my_awesome_plugin_example + description: Demonstrates how to use the my_plugin plugin. + + publish_to: 'none' # Remove this line if you wish to publish to pub.dev + + environment: + sdk: ">=2.17.5 <3.0.0" + + dependencies: + flutter: + sdk: flutter + + my_plugin: + + path: ../ + + cupertino_icons: ^1.0.2 + + dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + + # For information on the generic Dart part of this file, see the + # following page: https://dart.dev/tools/pub/pubspec + + # The following section is specific to Flutter packages. + flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + """ + + @Shared + def examplePubspecYamlWithKlutter = """ + name: my_awesome_plugin_example + description: Demonstrates how to use the my_plugin plugin. + + publish_to: 'none' # Remove this line if you wish to publish to pub.dev + + environment: + sdk: ">=2.17.5 <3.0.0" + + dependencies: + klutter: ^0.2.0 + flutter: + sdk: flutter + + my_plugin: + + path: ../ + + cupertino_icons: ^1.0.2 + + dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + + # For information on the generic Dart part of this file, see the + # following page: https://dart.dev/tools/pub/pubspec + + # The following section is specific to Flutter packages. + flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + """ + + private static class Exeggutor extends CliExecutor { + + /** + * Map of expected CLI executions. + * + * Key: String absolutepath where command is to be executed. + * Value: List commands to be executed. + */ + private def expectations = new HashMap>() + + @Override + String execute(String command, File runFrom, Long timeout) { + if(expectations.containsKey(runFrom.absolutePath)) { + if(expectations[runFrom.absolutePath].contains(command)) { + return "" + } + } + + throw new RuntimeException("CLI execution failure: $command - $runFrom.absolutePath") + } + + def putExpectation(String runFrom, String command) { + if(!expectations.containsKey(runFrom)) { + expectations.put(runFrom, [command]) + } else { + expectations.get(runFrom).add(command) + } + } + + } + +}