From c5d5fb42d479042abd20d426cf09786cd25328ea Mon Sep 17 00:00:00 2001 From: Inaki Villar Date: Thu, 19 Feb 2026 12:19:07 +0100 Subject: [PATCH] Add Android KMP library mode for Android lib convention --- cli/build.gradle.kts | 1 + .../cli/GenerateVersionsYaml.kt | 1 + .../github/cdsap/projectgenerator/cli/Main.kt | 36 ++++++++++++++----- .../cli/GenerateProjectsCliTest.kt | 26 ++++++++++++++ .../android/CompositeBuildPluginAndroidLib.kt | 10 +++++- .../generator/rootproject/BuildGradle.kt | 10 +++++- .../generator/toml/AndroidToml.kt | 1 + .../cdsap/projectgenerator/model/Versions.kt | 1 + .../writer/ConventionPluginWriterTest.kt | 22 ++++++++++++ .../writer/ProjectWriterTest.kt | 25 +++++++++++++ 10 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 cli/src/test/kotlin/io/github/cdsap/projectgenerator/cli/GenerateProjectsCliTest.kt diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index 4a1da563..2baf9d64 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(libs.jackson.dataformat.yaml) testImplementation(platform(libs.junit.bom)) testImplementation(libs.junit.jupiter) + testRuntimeOnly(libs.junit.platform.launcher) } tasks.test { diff --git a/cli/src/main/kotlin/io/github/cdsap/projectgenerator/cli/GenerateVersionsYaml.kt b/cli/src/main/kotlin/io/github/cdsap/projectgenerator/cli/GenerateVersionsYaml.kt index 061a1d9b..e3e8ffc2 100644 --- a/cli/src/main/kotlin/io/github/cdsap/projectgenerator/cli/GenerateVersionsYaml.kt +++ b/cli/src/main/kotlin/io/github/cdsap/projectgenerator/cli/GenerateVersionsYaml.kt @@ -32,6 +32,7 @@ class GenerateVersionsYaml { | work: ${versions.android.work} | room: ${versions.android.room} | roomDatabase: ${versions.android.roomDatabase} + | kotlinMultiplatformLibrary: ${versions.android.kotlinMultiplatformLibrary} | hilt: ${versions.android.hilt} | hiltAandroidx: ${versions.android.hiltAandroidx} | metro: ${versions.android.metro} diff --git a/cli/src/main/kotlin/io/github/cdsap/projectgenerator/cli/Main.kt b/cli/src/main/kotlin/io/github/cdsap/projectgenerator/cli/Main.kt index 55967664..28b56ca9 100644 --- a/cli/src/main/kotlin/io/github/cdsap/projectgenerator/cli/Main.kt +++ b/cli/src/main/kotlin/io/github/cdsap/projectgenerator/cli/Main.kt @@ -7,6 +7,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.readValue import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.UsageError import com.github.ajalt.clikt.core.main import com.github.ajalt.clikt.core.subcommands import com.github.ajalt.clikt.parameters.options.* @@ -59,13 +60,23 @@ class GenerateProjects : CliktCommand(name = "generate-project") { private val develocityUrl by option() private val agp9 by option().flag(default = false) private val roomDatabase by option("--room-database").flag(default = false) + private val kotlinMultiplatformLibrary by option("--android-kotlin-multiplatform-library").flag(default = false) override fun run() { val typeOfProjectRequested = TypeProjectRequested.valueOf(type.uppercase()) val shape = Shape.valueOf(shape.uppercase()) val dependencyInjection = DependencyInjection.valueOf(di.uppercase()) - val versions = getVersions(versionsFile, develocityUrl, agp9, roomDatabase).copy(di = dependencyInjection) + if (typeOfProjectRequested != TypeProjectRequested.ANDROID && kotlinMultiplatformLibrary) { + throw UsageError("--android-kotlin-multiplatform-library is only available when --type android.") + } + val versions = getVersions( + fileVersions = versionsFile, + develocityUrl = develocityUrl, + agp9 = agp9, + roomDatabase = roomDatabase, + kotlinMultiplatformLibrary = kotlinMultiplatformLibrary + ).copy(di = dependencyInjection) val develocityEnabled = getDevelocityEnabled(develocity, develocityUrl) ProjectGenerator( modules, @@ -104,7 +115,13 @@ class GenerateProjects : CliktCommand(name = "generate-project") { } } - private fun getVersions(fileVersions: File?, develocityUrl: String?, agp9: Boolean, roomDatabase: Boolean): Versions { + private fun getVersions( + fileVersions: File?, + develocityUrl: String?, + agp9: Boolean, + roomDatabase: Boolean, + kotlinMultiplatformLibrary: Boolean + ): Versions { val versions = if (fileVersions != null) { parseYaml(fileVersions) } else { @@ -117,15 +134,18 @@ class GenerateProjects : CliktCommand(name = "generate-project") { Versions() } } - val withRoomDatabase = if (roomDatabase) { - versions.copy(android = versions.android.copy(roomDatabase = true)) - } else { - versions + var androidConfig = versions.android + if (roomDatabase) { + androidConfig = androidConfig.copy(roomDatabase = true) + } + if (kotlinMultiplatformLibrary) { + androidConfig = androidConfig.copy(kotlinMultiplatformLibrary = true) } + val withAndroidFlags = versions.copy(android = androidConfig) return if (develocityUrl != null) { - withRoomDatabase.copy(project = withRoomDatabase.project.copy(develocityUrl = develocityUrl)) + withAndroidFlags.copy(project = withAndroidFlags.project.copy(develocityUrl = develocityUrl)) } else { - withRoomDatabase + withAndroidFlags } } diff --git a/cli/src/test/kotlin/io/github/cdsap/projectgenerator/cli/GenerateProjectsCliTest.kt b/cli/src/test/kotlin/io/github/cdsap/projectgenerator/cli/GenerateProjectsCliTest.kt new file mode 100644 index 00000000..3b094c37 --- /dev/null +++ b/cli/src/test/kotlin/io/github/cdsap/projectgenerator/cli/GenerateProjectsCliTest.kt @@ -0,0 +1,26 @@ +package io.github.cdsap.projectgenerator.cli + +import com.github.ajalt.clikt.core.UsageError +import com.github.ajalt.clikt.core.parse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class GenerateProjectsCliTest { + + @Test + fun `android kotlin multiplatform library flag is rejected for jvm type`() { + val error = assertThrows { + GenerateProjects().parse( + listOf( + "--modules", "6", + "--type", "jvm", + "--android-kotlin-multiplatform-library" + ) + ) + } + assertTrue( + error.message?.contains("--android-kotlin-multiplatform-library is only available when --type android.") == true + ) + } +} diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/plugins/android/CompositeBuildPluginAndroidLib.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/plugins/android/CompositeBuildPluginAndroidLib.kt index b04c60d9..ec82d861 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/plugins/android/CompositeBuildPluginAndroidLib.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/plugins/android/CompositeBuildPluginAndroidLib.kt @@ -21,7 +21,7 @@ class CompositeBuildPluginAndroidLib { | override fun apply(target: Project) { | with(target) { | with(pluginManager) { - | apply("com.android.library") + | apply("${androidLibraryPluginId(versions)}") | ${provideKgpBasedOnAgp(versions)} | ${provideKotlinProcessor(versions,di)} | ${applyDiPlugin(di)} @@ -85,4 +85,12 @@ class CompositeBuildPluginAndroidLib { DependencyInjection.NONE -> """""" } } + + private fun androidLibraryPluginId(versions: Versions): String { + return if (versions.android.kotlinMultiplatformLibrary) { + "com.android.kotlin.multiplatform.library" + } else { + "com.android.library" + } + } } diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/rootproject/BuildGradle.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/rootproject/BuildGradle.kt index 571722ca..9a0d849d 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/rootproject/BuildGradle.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/rootproject/BuildGradle.kt @@ -12,7 +12,7 @@ class BuildGradle { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.android.application) apply false - alias(libs.plugins.android.library) apply false + ${androidLibraryRootPlugin(versions)} ${provideKotlinProcessor(versions)} ${diPlugins(di)} ${additionalBuildGradlePlugins(versions)} @@ -48,4 +48,12 @@ class BuildGradle { return additionalPlugins } + private fun androidLibraryRootPlugin(versions: Versions): String { + return if (versions.android.kotlinMultiplatformLibrary) { + "alias(libs.plugins.android.kotlin.multiplatform.library) apply false" + } else { + "alias(libs.plugins.android.library) apply false" + } + } + } diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/toml/AndroidToml.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/toml/AndroidToml.kt index 08f13fc7..c96ca7f4 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/toml/AndroidToml.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/toml/AndroidToml.kt @@ -91,6 +91,7 @@ class AndroidToml { [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } + android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } ${hiltPlugin(version.di)} ${metroPlugin(version.di)} kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/model/Versions.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/model/Versions.kt index 62dd971a..b421e72b 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/model/Versions.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/model/Versions.kt @@ -76,6 +76,7 @@ data class Android( val work: String = "2.11.1", val room: String = "2.8.4", val roomDatabase: Boolean = false, + val kotlinMultiplatformLibrary: Boolean = false, val hilt: String = "2.59.1", val hiltAandroidx: String = "1.3.0", val metro: String = "0.10.4", diff --git a/project-generator/src/test/kotlin/io/github/cdsap/projectgenerator/writer/ConventionPluginWriterTest.kt b/project-generator/src/test/kotlin/io/github/cdsap/projectgenerator/writer/ConventionPluginWriterTest.kt index 83f17bea..fcccd7ca 100644 --- a/project-generator/src/test/kotlin/io/github/cdsap/projectgenerator/writer/ConventionPluginWriterTest.kt +++ b/project-generator/src/test/kotlin/io/github/cdsap/projectgenerator/writer/ConventionPluginWriterTest.kt @@ -5,6 +5,7 @@ import io.github.cdsap.projectgenerator.generator.includedbuild.CompositeBuildSe import io.github.cdsap.projectgenerator.generator.plugins.android.CompositeBuildPluginAndroidApp import io.github.cdsap.projectgenerator.generator.plugins.android.CompositeBuildPluginAndroidLib import io.github.cdsap.projectgenerator.generator.plugins.jvm.CompositeBuildJvmLib +import io.github.cdsap.projectgenerator.model.Android import io.github.cdsap.projectgenerator.model.LanguageAttributes import io.github.cdsap.projectgenerator.model.TypeProjectRequested import io.github.cdsap.projectgenerator.model.Versions @@ -104,6 +105,27 @@ class ConventionPluginWriterTest { assertEquals(0, tempDir.listFiles()?.size ?: 0, "Temp directory should be empty") } + @Test + fun `write should use android kotlin multiplatform library plugin when enabled`() { + val projectName = "androidKmpLib" + val language = LanguageAttributes(projectName = "${tempDir.path}/$projectName", extension = "gradle.kts") + val versions = Versions(android = Android(kotlinMultiplatformLibrary = true)) + val writer = ConventionPluginWriter( + languages = listOf(language), + versions = versions, + requested = TypeProjectRequested.ANDROID + ) + + writer.write() + + val libPluginFile = File("${language.projectName}/build-logic/convention/src/main/kotlin/com/logic/CompositeBuildPluginAndroidLib.kt") + assertTrue(libPluginFile.exists(), "Lib plugin missing for ${language.projectName}") + assertTrue( + libPluginFile.readText().contains("apply(\"com.android.kotlin.multiplatform.library\")"), + "Expected KMP Android library plugin application in lib convention plugin" + ) + } + private fun assertAndroidConventionFilesExist(projectBasePath: String, versions: Versions) { val conventionSrcDir = File("$projectBasePath/build-logic/convention/src/main/kotlin/com/logic") assertTrue(conventionSrcDir.exists() && conventionSrcDir.isDirectory, "Convention src dir missing for $projectBasePath") diff --git a/project-generator/src/test/kotlin/io/github/cdsap/projectgenerator/writer/ProjectWriterTest.kt b/project-generator/src/test/kotlin/io/github/cdsap/projectgenerator/writer/ProjectWriterTest.kt index 3bba6a94..2c68a345 100644 --- a/project-generator/src/test/kotlin/io/github/cdsap/projectgenerator/writer/ProjectWriterTest.kt +++ b/project-generator/src/test/kotlin/io/github/cdsap/projectgenerator/writer/ProjectWriterTest.kt @@ -99,4 +99,29 @@ class ProjectWriterTest { assertTrue(content.contains("Room.databaseBuilder")) assertTrue(content.contains("by viewModels { viewModelFactory }")) } + + @Test + fun `uses android kotlin multiplatform library root plugin alias when enabled`() { + val node = ProjectGraph("module_1_1", 1, emptyList(), TypeProject.ANDROID_LIB, 10) + val language = LanguageAttributes("gradle.kts", "${tempDir}/project_kts_kmp") + val versions = Versions(android = Android(kotlinMultiplatformLibrary = true)) + val projectWriter = ProjectWriter( + listOf(node), + listOf(language), + versions, + TypeProjectRequested.ANDROID, + TypeOfStringResources.NORMAL, + false, + GradleWrapper(Gradle.GRADLE_9_3_0), + false, + "kmp_android_lib_alias" + ) + + projectWriter.write() + + val rootBuild = File("${language.projectName}/build.gradle.kts") + assertTrue(rootBuild.exists(), "Expected root build.gradle.kts to exist") + val content = rootBuild.readText() + assertTrue(content.contains("alias(libs.plugins.android.kotlin.multiplatform.library) apply false")) + } }