diff --git a/android/bin/main/com/rees46/android/AndroidPlugin.kt b/android/bin/main/com/rees46/android/AndroidPlugin.kt new file mode 100644 index 0000000..515b14a --- /dev/null +++ b/android/bin/main/com/rees46/android/AndroidPlugin.kt @@ -0,0 +1,36 @@ +package com.rees46.android + +import com.android.build.gradle.BaseExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +class AndroidPlugin : Plugin { + override fun apply(target: Project) { + target.pluginManager.apply("org.jetbrains.kotlin.multiplatform") + target.pluginManager.apply("com.android.library") + + target.extensions.configure { + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_22) + } + } + } + + target.extensions.configure { + compileSdkVersion(35) + defaultConfig { + minSdk = 24 + targetSdk = 34 + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_22 + targetCompatibility = JavaVersion.VERSION_22 + } + } + } +} diff --git a/android/src/main/kotlin/com/rees46/android/AndroidPlugin.kt b/android/src/main/kotlin/com/rees46/android/AndroidPlugin.kt index 94b4255..515b14a 100644 --- a/android/src/main/kotlin/com/rees46/android/AndroidPlugin.kt +++ b/android/src/main/kotlin/com/rees46/android/AndroidPlugin.kt @@ -5,6 +5,7 @@ import org.gradle.api.JavaVersion import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension class AndroidPlugin : Plugin { @@ -13,7 +14,11 @@ class AndroidPlugin : Plugin { target.pluginManager.apply("com.android.library") target.extensions.configure { - androidTarget() + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_22) + } + } } target.extensions.configure { diff --git a/build.gradle.kts b/build.gradle.kts index 6841330..edd69f7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } dependencies { - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.21") implementation("com.android.tools.build:gradle:8.9.0") } diff --git a/compose/bin/main/com/rees46/compose/ComposePlugin.kt b/compose/bin/main/com/rees46/compose/ComposePlugin.kt new file mode 100644 index 0000000..a6bb62e --- /dev/null +++ b/compose/bin/main/com/rees46/compose/ComposePlugin.kt @@ -0,0 +1,41 @@ +package com.rees46.compose + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +const val composeVersion = "1.7.0" +const val androidxLifecycleVersion = "2.8.4" + +class ComposePlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply("org.jetbrains.kotlin.multiplatform") + pluginManager.apply("org.jetbrains.compose") + pluginManager.apply("org.jetbrains.kotlin.plugin.compose") + pluginManager.apply("com.rees46.ios") + pluginManager.apply("com.rees46.android") + + val composeDependencies = listOf( + "org.jetbrains.compose.runtime:runtime:$composeVersion", + "org.jetbrains.compose.foundation:foundation:$composeVersion", + "org.jetbrains.compose.material:material:$composeVersion", + "org.jetbrains.compose.ui:ui:$composeVersion", + "org.jetbrains.compose.components:components-resources:$composeVersion", + "org.jetbrains.compose.components:components-ui-tooling-preview:$composeVersion" + ) + + extensions.getByType().apply { + sourceSets.getByName("commonMain").dependencies { + composeDependencies.forEach { implementation(it) } + } + sourceSets.getByName("androidMain").dependencies { + implementation("androidx.lifecycle:lifecycle-viewmodel:$androidxLifecycleVersion") + implementation("androidx.lifecycle:lifecycle-runtime-compose:$androidxLifecycleVersion") + implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:${androidxLifecycleVersion}") + } + } + } + } +} diff --git a/generator/bin/main/com/rees46/generator/ModuleGeneratorPlugin.kt b/generator/bin/main/com/rees46/generator/ModuleGeneratorPlugin.kt new file mode 100644 index 0000000..7128483 --- /dev/null +++ b/generator/bin/main/com/rees46/generator/ModuleGeneratorPlugin.kt @@ -0,0 +1,16 @@ +package com.rees46.generator + +import com.rees46.generator.tasks.compose.GenerateComposeModuleTask +import com.rees46.generator.tasks.kmp.GenerateKmpModuleTask +import org.gradle.api.Plugin +import org.gradle.api.Project + +const val kmpTask = "generateKmpModule" +const val composeTask = "generateComposeModule" + +class ModuleGeneratorPlugin : Plugin { + override fun apply(project: Project) { + project.tasks.register(kmpTask, GenerateKmpModuleTask::class.java) + project.tasks.register(composeTask, GenerateComposeModuleTask::class.java) + } +} diff --git a/generator/bin/main/com/rees46/generator/compose/ComposeModuleGenerator.kt b/generator/bin/main/com/rees46/generator/compose/ComposeModuleGenerator.kt new file mode 100644 index 0000000..f1ecbac --- /dev/null +++ b/generator/bin/main/com/rees46/generator/compose/ComposeModuleGenerator.kt @@ -0,0 +1,13 @@ +package com.rees46.generator.compose + +import com.rees46.generator.core.ModuleGenerator +import org.gradle.api.Project + +class ComposeModuleGenerator( + project: Project, + moduleName: String, + organization: String +) : ModuleGenerator(project, moduleName, organization) { + override val gradleTemplatePath = "templates/compose/compose.build.gradle.kts.template" + override val kotlinTemplatePath = "templates/compose/ComposeBaseFile.kt.template" +} diff --git a/generator/bin/main/com/rees46/generator/core/ModuleGenerator.kt b/generator/bin/main/com/rees46/generator/core/ModuleGenerator.kt new file mode 100644 index 0000000..d630db6 --- /dev/null +++ b/generator/bin/main/com/rees46/generator/core/ModuleGenerator.kt @@ -0,0 +1,152 @@ +package com.rees46.generator.core + +import org.gradle.api.Project +import org.gradle.api.logging.Logger +import java.io.File +import java.io.IOException + +abstract class ModuleGenerator( + protected val project: Project, + protected val modulePath: String, + protected val organization: String +) { + protected abstract val gradleTemplatePath: String + protected abstract val kotlinTemplatePath: String + + protected val logger: Logger = project.logger + private val pathComponents: List + private val moduleName: String + private val parentDirs: List + + init { + require(modulePath.isNotBlank()) { "Module path cannot be blank" } + require(organization.isNotBlank()) { "Organization cannot be blank" } + + pathComponents = modulePath.split(":").filter { it.isNotBlank() } + require(pathComponents.isNotEmpty()) { "Module path cannot be empty" } + require(pathComponents.all { it.isNotBlank() }) { "Module path components cannot be blank" } + + moduleName = pathComponents.last() + parentDirs = pathComponents.dropLast(1) + } + + fun generate() { + try { + val moduleDir = resolveModuleDir() + validateModuleDoesNotExist(moduleDir) + + createDirectoryStructure(moduleDir) + generateGradleFile(moduleDir) + generateKotlinFiles(moduleDir) + addGitignore(moduleDir) + addToSettingsGradle() + + logger.lifecycle("Created module '$modulePath' at ${moduleDir.relativeTo(project.rootDir)}") + } catch (e: Exception) { + logger.error("Failed to generate module '$modulePath'", e) + throw e + } + } + + private fun resolveModuleDir(): File { + return parentDirs.fold(project.rootDir) { dir, name -> + File(dir, name).also { + if (!it.exists() && !it.mkdirs()) { + throw IOException("Failed to create directory: ${it.absolutePath}") + } + } + }.resolve(moduleName) + } + + private fun validateModuleDoesNotExist(moduleDir: File) { + if (moduleDir.exists()) { + throw IllegalStateException( + "Module '$modulePath' already exists at ${moduleDir.relativeTo(project.rootDir)}" + ) + } + } + + protected open fun createDirectoryStructure(moduleDir: File) { + val kotlinPath = "src/commonMain/kotlin/${organization.replace(".", "/")}/$moduleName" + File(moduleDir, kotlinPath).mkdirs().also { success -> + if (!success) { + throw IOException("Failed to create directory structure in ${moduleDir.absolutePath}") + } + } + } + + protected open fun generateGradleFile(moduleDir: File) { + try { + val template = readResource(gradleTemplatePath) + .replace("{{PACKAGE}}", "$organization.$moduleName") + .replace("{{BASENAME}}", "${moduleName}Kit") + + File(moduleDir, "build.gradle.kts").writeText(template) + } catch (e: Exception) { + throw IllegalStateException("Failed to generate Gradle file", e) + } + } + + protected open fun generateKotlinFiles(moduleDir: File) { + try { + val template = readResource(kotlinTemplatePath) + .replace("{{PACKAGE}}", "$organization.$moduleName") + .replace("{{MODULENAME}}", moduleName) + + val kotlinPath = "src/commonMain/kotlin/${organization.replace(".", "/")}/$moduleName" + File(moduleDir, "$kotlinPath/Greeting.kt").writeText(template) + } catch (e: Exception) { + throw IllegalStateException("Failed to generate Kotlin files", e) + } + } + + private fun addGitignore(moduleDir: File) { + try { + val gitignoreContent = """ + /build + """.trimIndent() + + File(moduleDir, ".gitignore").writeText(gitignoreContent) + logger.debug("Added .gitignore to ${moduleDir.relativeTo(project.rootDir)}") + } catch (e: Exception) { + logger.warn("Failed to add .gitignore file", e) + } + } + + private fun addToSettingsGradle() { + val settingsFile = project.rootDir.resolve("settings.gradle.kts") + if (!settingsFile.exists()) { + logger.warn("settings.gradle.kts not found in project root") + return + } + + val includePath = ":" + modulePath.replace(":", ":") + val includeStatement = "include(\"$includePath\")" + + try { + val content = settingsFile.readText() + val pattern = Regex("""include\s*\(["']$includePath["']\)""") + if (pattern.containsMatchIn(content)) { + logger.lifecycle("Module '$includePath' is already included in settings.gradle.kts") + return + } + + val newContent = if (content.isNotBlank() && !content.endsWith("\n")) { + "$content\n$includeStatement\n" + } else { + "$content$includeStatement\n" + } + + settingsFile.writeText(newContent) + logger.lifecycle("Added to settings.gradle.kts: $includeStatement") + } catch (e: Exception) { + throw IllegalStateException("Failed to update settings.gradle.kts", e) + } + } + + protected fun readResource(path: String): String { + return javaClass.classLoader.getResourceAsStream(path)?.use { stream -> + stream.bufferedReader().use { it.readText() } + } ?: throw IllegalStateException("Resource '$path' not found. Make sure the template exists.") + } +} diff --git a/generator/bin/main/com/rees46/generator/kmp/KmpModuleGenerator.kt b/generator/bin/main/com/rees46/generator/kmp/KmpModuleGenerator.kt new file mode 100644 index 0000000..fde0465 --- /dev/null +++ b/generator/bin/main/com/rees46/generator/kmp/KmpModuleGenerator.kt @@ -0,0 +1,13 @@ +package com.rees46.generator.kmp + +import com.rees46.generator.core.ModuleGenerator +import org.gradle.api.Project + +class KmpModuleGenerator( + project: Project, + moduleName: String, + organization: String +) : ModuleGenerator(project, moduleName, organization) { + override val gradleTemplatePath = "templates/kmp/kmp.build.gradle.kts.template" + override val kotlinTemplatePath = "templates/kmp/KmpBaseFile.kt.template" +} diff --git a/generator/bin/main/com/rees46/generator/tasks/BaseGeneratorTask.kt b/generator/bin/main/com/rees46/generator/tasks/BaseGeneratorTask.kt new file mode 100644 index 0000000..4c30ae8 --- /dev/null +++ b/generator/bin/main/com/rees46/generator/tasks/BaseGeneratorTask.kt @@ -0,0 +1,17 @@ +package com.rees46.generator.tasks + +import org.gradle.api.DefaultTask +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.options.Option + +abstract class BaseGeneratorTask : DefaultTask() { + + @get:Input + @get:Option(option = "moduleName", description = "Name of the module to generate") + abstract val moduleName: Property + + @get:Input + @get:Option(option = "organization", description = "Organization (package prefix)") + abstract val organization: Property +} diff --git a/generator/bin/main/com/rees46/generator/tasks/compose/GenerateComposeModuleTask.kt b/generator/bin/main/com/rees46/generator/tasks/compose/GenerateComposeModuleTask.kt new file mode 100644 index 0000000..836904b --- /dev/null +++ b/generator/bin/main/com/rees46/generator/tasks/compose/GenerateComposeModuleTask.kt @@ -0,0 +1,22 @@ +package com.rees46.generator.tasks.compose + +import com.rees46.generator.compose.ComposeModuleGenerator +import com.rees46.generator.tasks.BaseGeneratorTask +import org.gradle.api.tasks.TaskAction + +abstract class GenerateComposeModuleTask : BaseGeneratorTask() { + + init { + group = "generator" + description = "Generates a new Compose module" + } + + @TaskAction + fun generate() { + ComposeModuleGenerator( + project = project, + moduleName = moduleName.get(), + organization = organization.get() + ).generate() + } +} diff --git a/generator/bin/main/com/rees46/generator/tasks/kmp/GenerateKmpModuleTask.kt b/generator/bin/main/com/rees46/generator/tasks/kmp/GenerateKmpModuleTask.kt new file mode 100644 index 0000000..818b59e --- /dev/null +++ b/generator/bin/main/com/rees46/generator/tasks/kmp/GenerateKmpModuleTask.kt @@ -0,0 +1,22 @@ +package com.rees46.generator.tasks.kmp + +import com.rees46.generator.kmp.KmpModuleGenerator +import com.rees46.generator.tasks.BaseGeneratorTask +import org.gradle.api.tasks.TaskAction + +abstract class GenerateKmpModuleTask : BaseGeneratorTask() { + + init { + group = "generator" + description = "Generates a new KMP module" + } + + @TaskAction + fun generate() { + KmpModuleGenerator( + project = project, + moduleName = moduleName.get(), + organization = organization.get() + ).generate() + } +} diff --git a/generator/bin/main/templates/compose/ComposeBaseFile.kt.template b/generator/bin/main/templates/compose/ComposeBaseFile.kt.template new file mode 100644 index 0000000..7b250b4 --- /dev/null +++ b/generator/bin/main/templates/compose/ComposeBaseFile.kt.template @@ -0,0 +1,9 @@ +package {{PACKAGE}} + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable + +@Composable +fun Greeting() { + Text(text = "Hello compose module!") +} diff --git a/generator/bin/main/templates/compose/compose.build.gradle.kts.template b/generator/bin/main/templates/compose/compose.build.gradle.kts.template new file mode 100644 index 0000000..c5fadf4 --- /dev/null +++ b/generator/bin/main/templates/compose/compose.build.gradle.kts.template @@ -0,0 +1,22 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + id("com.rees46.compose") version "1.0.0" +} + +kotlin { + sourceSets { + commonMain { + dependencies { + + } + } + } +} + +iosConfig { + baseName.set("{{BASENAME}}") +} + +android { + namespace = "{{PACKAGE}}" +} diff --git a/generator/bin/main/templates/kmp/KmpBaseFile.kt.template b/generator/bin/main/templates/kmp/KmpBaseFile.kt.template new file mode 100644 index 0000000..fda9d36 --- /dev/null +++ b/generator/bin/main/templates/kmp/KmpBaseFile.kt.template @@ -0,0 +1,5 @@ +package {{PACKAGE}} + +class Greeting() { + fun hello() = "Hello from {{MODULENAME}}!" +} diff --git a/generator/bin/main/templates/kmp/kmp.build.gradle.kts.template b/generator/bin/main/templates/kmp/kmp.build.gradle.kts.template new file mode 100644 index 0000000..4352c46 --- /dev/null +++ b/generator/bin/main/templates/kmp/kmp.build.gradle.kts.template @@ -0,0 +1,21 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + id("com.rees46.multiplatform") version "1.0.0" +} + +kotlin { + sourceSets { + commonMain { + dependencies { + } + } + } +} + +iosConfig { + baseName.set("{{BASENAME}}") +} + +android { + namespace = "{{PACKAGE}}" +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a0fe67d..5252a66 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] agp = "8.9.0" -kotlin = "2.1.0" +kotlin = "2.1.21" compose = "1.7.0" compose-material3 = "1.1.2" androidx-activityCompose = "1.8.0" diff --git a/ios/bin/main/com/rees46/ios/IosPlugin.kt b/ios/bin/main/com/rees46/ios/IosPlugin.kt new file mode 100644 index 0000000..424dce3 --- /dev/null +++ b/ios/bin/main/com/rees46/ios/IosPlugin.kt @@ -0,0 +1,45 @@ +package com.rees46.ios + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.create +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework + +class IosPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + val extension = extensions.create("iosConfig") + pluginManager.apply("org.jetbrains.kotlin.multiplatform") + + afterEvaluate { + val xcFrameworkName = extension.baseName.get() + val xcFramework = XCFramework(xcFrameworkName) + + extensions.configure { + iosX64().apply { + binaries.framework { + baseName = xcFrameworkName + xcFramework.add(this) + } + } + + iosArm64().apply { + binaries.framework { + baseName = xcFrameworkName + xcFramework.add(this) + } + } + + iosSimulatorArm64().apply { + binaries.framework { + baseName = xcFrameworkName + xcFramework.add(this) + } + } + } + } + } + } +} diff --git a/ios/bin/main/com/rees46/ios/IosPluginExtension.kt b/ios/bin/main/com/rees46/ios/IosPluginExtension.kt new file mode 100644 index 0000000..4422dc0 --- /dev/null +++ b/ios/bin/main/com/rees46/ios/IosPluginExtension.kt @@ -0,0 +1,9 @@ +package com.rees46.ios + +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import javax.inject.Inject + +abstract class IosPluginExtension @Inject constructor(objects: ObjectFactory) { + val baseName: Property = objects.property(String::class.java) +} diff --git a/multiplatform/bin/main/com/rees46/multiplatform/MultiplatformPlugin.kt b/multiplatform/bin/main/com/rees46/multiplatform/MultiplatformPlugin.kt new file mode 100644 index 0000000..35d9f81 --- /dev/null +++ b/multiplatform/bin/main/com/rees46/multiplatform/MultiplatformPlugin.kt @@ -0,0 +1,21 @@ +package com.rees46.multiplatform + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +class MultiplatformPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply("com.rees46.ios") + pluginManager.apply("com.rees46.android") + + extensions.getByType().apply { + sourceSets.getByName("commonMain").dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib:2.1.21") + } + } + } + } +} diff --git a/multiplatform/src/main/kotlin/com/rees46/multiplatform/MultiplatformPlugin.kt b/multiplatform/src/main/kotlin/com/rees46/multiplatform/MultiplatformPlugin.kt index f3255cd..35d9f81 100644 --- a/multiplatform/src/main/kotlin/com/rees46/multiplatform/MultiplatformPlugin.kt +++ b/multiplatform/src/main/kotlin/com/rees46/multiplatform/MultiplatformPlugin.kt @@ -13,7 +13,7 @@ class MultiplatformPlugin : Plugin { extensions.getByType().apply { sourceSets.getByName("commonMain").dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib:2.1.0") + implementation("org.jetbrains.kotlin:kotlin-stdlib:2.1.21") } } }