diff --git a/cli/src/main/kotlin/com/mitteloupe/cag/cli/Main.kt b/cli/src/main/kotlin/com/mitteloupe/cag/cli/Main.kt index 1e80c86..1a57d5b 100644 --- a/cli/src/main/kotlin/com/mitteloupe/cag/cli/Main.kt +++ b/cli/src/main/kotlin/com/mitteloupe/cag/cli/Main.kt @@ -99,6 +99,7 @@ fun main(arguments: Array) { projectName = request.projectName, packageName = request.packageName, overrideMinimumAndroidSdk = null, + overrideAndroidGradlePluginVersion = null, enableCompose = request.enableCompose, enableKtlint = request.enableKtlint, enableDetekt = request.enableDetekt, diff --git a/core/src/main/kotlin/com/mitteloupe/cag/core/Generator.kt b/core/src/main/kotlin/com/mitteloupe/cag/core/Generator.kt index 1d1fcb2..727c36b 100644 --- a/core/src/main/kotlin/com/mitteloupe/cag/core/Generator.kt +++ b/core/src/main/kotlin/com/mitteloupe/cag/core/Generator.kt @@ -26,7 +26,6 @@ import com.mitteloupe.cag.core.generation.architecture.CoroutineModuleContentGen import com.mitteloupe.cag.core.generation.versioncatalog.DependencyConfiguration import com.mitteloupe.cag.core.generation.versioncatalog.LibraryConstants import com.mitteloupe.cag.core.generation.versioncatalog.PluginConstants -import com.mitteloupe.cag.core.generation.versioncatalog.SectionEntryRequirement import com.mitteloupe.cag.core.generation.versioncatalog.VersionCatalogConstants import com.mitteloupe.cag.core.generation.versioncatalog.VersionCatalogUpdater import com.mitteloupe.cag.core.generation.withoutSpaces @@ -392,12 +391,13 @@ class Generator( throw GenerationException("Package name is missing.") } - println("Comparing ${request.destinationRootDirectory.name} to ${projectName.withoutSpaces()}") + val sanitizedProjectName = projectName.withoutSpaces() + println("Comparing ${request.destinationRootDirectory.name} to $sanitizedProjectName") val projectRoot = - if (request.destinationRootDirectory.name == projectName.withoutSpaces()) { + if (request.destinationRootDirectory.name.matches("$sanitizedProjectName\\d*".toRegex())) { request.destinationRootDirectory } else { - File(request.destinationRootDirectory, projectName.withoutSpaces()) + File(request.destinationRootDirectory, sanitizedProjectName) } if (projectRoot != request.destinationRootDirectory && projectRoot.exists()) { @@ -434,24 +434,21 @@ class Generator( add(PluginConstants.DETEKT) } } - val overrideVersions = - if (request.overrideMinimumAndroidSdk == null) { - VersionCatalogConstants.ANDROID_VERSIONS - } else { - VersionCatalogConstants.ANDROID_VERSIONS.map { androidVersion -> - if (androidVersion.key == VersionCatalogConstants.MIN_SDK_VERSION.key) { - SectionEntryRequirement.VersionRequirement( - key = androidVersion.key, - version = request.overrideMinimumAndroidSdk.toString() - ) - } else { - androidVersion - } - } + val versionOverrides = + listOf( + VersionCatalogConstants.MIN_SDK_VERSION to request.overrideMinimumAndroidSdk?.toString(), + VersionCatalogConstants.ANDROID_GRADLE_PLUGIN_VERSION to request.overrideAndroidGradlePluginVersion + ).mapNotNull { override -> override.second?.let { override.first to it } } + .toMap() + val androidVersions = + VersionCatalogConstants.ANDROID_VERSIONS.map { version -> + versionOverrides[version]?.let { versionOverride -> + version.copy(version = versionOverride) + } ?: version } val dependencyConfiguration = DependencyConfiguration( - versions = overrideVersions, + versions = androidVersions, libraries = libraries, plugins = plugins ) diff --git a/core/src/main/kotlin/com/mitteloupe/cag/core/content/AndroidResourceBuilders.kt b/core/src/main/kotlin/com/mitteloupe/cag/core/content/AndroidResourceBuilders.kt index 24d086b..2f00c90 100644 --- a/core/src/main/kotlin/com/mitteloupe/cag/core/content/AndroidResourceBuilders.kt +++ b/core/src/main/kotlin/com/mitteloupe/cag/core/content/AndroidResourceBuilders.kt @@ -30,11 +30,11 @@ fun buildAndroidManifest(appName: String): String = """.trimIndent() -fun buildStringsXml(packageName: String): String = +fun buildStringsXml(appName: String): String = """ - ${packageName.split('.').last().capitalized} + $appName """.trimIndent() diff --git a/core/src/main/kotlin/com/mitteloupe/cag/core/generation/AppModuleContentGenerator.kt b/core/src/main/kotlin/com/mitteloupe/cag/core/generation/AppModuleContentGenerator.kt index 9e48ad9..64b9832 100644 --- a/core/src/main/kotlin/com/mitteloupe/cag/core/generation/AppModuleContentGenerator.kt +++ b/core/src/main/kotlin/com/mitteloupe/cag/core/generation/AppModuleContentGenerator.kt @@ -64,14 +64,15 @@ class AppModuleContentGenerator( fileCreator.createDirectoryIfNotExists(basePackageDir) + val sanitizedAppName = appName.withoutSpaces() val mainActivityFile = File(basePackageDir, "MainActivity.kt") - val mainActivityContent = + fileCreator.createFileIfNotExists(mainActivityFile) { buildMainActivityKotlinFile( - appName = appName, + appName = sanitizedAppName, projectNamespace = projectNamespace, enableCompose = enableCompose ) - fileCreator.createFileIfNotExists(mainActivityFile) { mainActivityContent } + } val applicationFile = File(basePackageDir, "Application.kt") val applicationContent = buildApplicationKotlinFile(projectNamespace) @@ -91,24 +92,25 @@ class AppModuleContentGenerator( packageName: String, enableCompose: Boolean ) { + val sanitizedAppName = appName.withoutSpaces() val manifestFile = File(appModuleDirectory, "src/main/AndroidManifest.xml") - fileCreator.createOrUpdateFile(manifestFile) { buildAndroidManifest(appName) } + fileCreator.createOrUpdateFile(manifestFile) { buildAndroidManifest(sanitizedAppName) } val valuesDirectory = File(appModuleDirectory, "src/main/res/values") fileCreator.createDirectoryIfNotExists(valuesDirectory) val stringsFile = File(valuesDirectory, "strings.xml") - fileCreator.createFileIfNotExists(stringsFile) { buildStringsXml(packageName) } + fileCreator.createFileIfNotExists(stringsFile) { buildStringsXml(appName) } val xmlDirectory = File(appModuleDirectory, "src/main/res/xml") fileCreator.createDirectoryIfNotExists(xmlDirectory) if (enableCompose) { val themeFile = File(valuesDirectory, "themes.xml") - fileCreator.createFileIfNotExists(themeFile) { buildThemesXml(appName) } + fileCreator.createFileIfNotExists(themeFile) { buildThemesXml(sanitizedAppName) } val uiDirectory = File(appModuleDirectory, "src/main/java/${packageName.replace('.', '/')}/ui/theme") fileCreator.createDirectoryIfNotExists(uiDirectory) val themeKtFile = File(uiDirectory, "Theme.kt") - fileCreator.createFileIfNotExists(themeKtFile) { buildThemeKt(appName = appName, packageName = packageName) } + fileCreator.createFileIfNotExists(themeKtFile) { buildThemeKt(appName = sanitizedAppName, packageName = packageName) } val colorsKtFile = File(uiDirectory, "Color.kt") fileCreator.createFileIfNotExists(colorsKtFile) { buildColorsKt(packageName) } diff --git a/core/src/main/kotlin/com/mitteloupe/cag/core/generation/GradleWrapperCreator.kt b/core/src/main/kotlin/com/mitteloupe/cag/core/generation/GradleWrapperCreator.kt index 19d50f8..b4d4860 100644 --- a/core/src/main/kotlin/com/mitteloupe/cag/core/generation/GradleWrapperCreator.kt +++ b/core/src/main/kotlin/com/mitteloupe/cag/core/generation/GradleWrapperCreator.kt @@ -11,16 +11,13 @@ class GradleWrapperCreator(private val fileCreator: FileCreator) { fileCreator.createDirectoryIfNotExists(gradleWrapperDirectory) val gradleWrapperPropertiesFile = File(gradleWrapperDirectory, "gradle-wrapper.properties") - val gradleWrapperPropertiesContent = buildGradleWrapperPropertiesFile() - - fileCreator.createFileIfNotExists(gradleWrapperPropertiesFile) { gradleWrapperPropertiesContent } + fileCreator.createOrUpdateFile(gradleWrapperPropertiesFile) { buildGradleWrapperPropertiesFile() } val gradleWrapperJarFile = File(gradleWrapperDirectory, "gradle-wrapper.jar") fileCreator.createBinaryFileIfNotExists(gradleWrapperJarFile) { getGradleWrapperResourceAsBytes() } val gradlewFile = File(projectRoot, "gradlew") - val gradlewContent = getResourceAsString("gradlew") - fileCreator.createFileIfNotExists(gradlewFile) { gradlewContent } + fileCreator.createFileIfNotExists(gradlewFile) { getResourceAsString("gradlew") } if (!gradlewFile.setExecutable(true)) { throw GenerationException("Failed to make gradlew executable") } diff --git a/core/src/main/kotlin/com/mitteloupe/cag/core/generation/SettingsFileUpdater.kt b/core/src/main/kotlin/com/mitteloupe/cag/core/generation/SettingsFileUpdater.kt index f50ab1e..e0039a7 100644 --- a/core/src/main/kotlin/com/mitteloupe/cag/core/generation/SettingsFileUpdater.kt +++ b/core/src/main/kotlin/com/mitteloupe/cag/core/generation/SettingsFileUpdater.kt @@ -162,8 +162,10 @@ class SettingsFileUpdater(private val fileCreator: FileCreator) { featureNames: List ) { val settingsFile = File(projectRoot, "settings.gradle.kts") - val content = buildSettingsGradleScript(projectName, featureNames) - runCatching { fileCreator.createOrUpdateFile(settingsFile) { content } } - .onFailure { throw GenerationException("Failed to create settings.gradle.kts: ${it.message}") } + runCatching { + fileCreator.createOrUpdateFile(settingsFile) { + buildSettingsGradleScript(projectName.withoutSpaces(), featureNames) + } + }.onFailure { throw GenerationException("Failed to create settings.gradle.kts: ${it.message}") } } } diff --git a/core/src/main/kotlin/com/mitteloupe/cag/core/generation/versioncatalog/VersionCatalogUpdater.kt b/core/src/main/kotlin/com/mitteloupe/cag/core/generation/versioncatalog/VersionCatalogUpdater.kt index f22055c..1ec7fb5 100644 --- a/core/src/main/kotlin/com/mitteloupe/cag/core/generation/versioncatalog/VersionCatalogUpdater.kt +++ b/core/src/main/kotlin/com/mitteloupe/cag/core/generation/versioncatalog/VersionCatalogUpdater.kt @@ -169,7 +169,9 @@ class VersionCatalogUpdater( insertPositionIfMissing = CatalogInsertPosition.Start, requirements = (dependencyConfiguration.versions + pluginRequirements.versions + libraryRequirements.versions) - .distinct() + .associateBy { it.key } + .values + .toList() ), SectionTransaction( insertPositionIfMissing = CatalogInsertPosition.End, @@ -221,8 +223,10 @@ class VersionCatalogUpdater( } val versionRequirements = - (dependencyConfiguration.versions + pluginRequirements.versions + libraryRequirements.versions) - .distinct() + (pluginRequirements.versions + libraryRequirements.versions + dependencyConfiguration.versions) + .associateBy { it.key } + .values + .toList() updateVersionCatalogIfPresent( projectRootDir = projectRootDir, diff --git a/core/src/main/kotlin/com/mitteloupe/cag/core/request/GenerateProjectTemplateRequest.kt b/core/src/main/kotlin/com/mitteloupe/cag/core/request/GenerateProjectTemplateRequest.kt index 103ac79..49e3286 100644 --- a/core/src/main/kotlin/com/mitteloupe/cag/core/request/GenerateProjectTemplateRequest.kt +++ b/core/src/main/kotlin/com/mitteloupe/cag/core/request/GenerateProjectTemplateRequest.kt @@ -7,6 +7,7 @@ data class GenerateProjectTemplateRequest( val projectName: String, val packageName: String, val overrideMinimumAndroidSdk: Int?, + val overrideAndroidGradlePluginVersion: String?, val enableCompose: Boolean, val enableKtlint: Boolean, val enableDetekt: Boolean, diff --git a/core/src/test/kotlin/com/mitteloupe/cag/core/GeneratorTest.kt b/core/src/test/kotlin/com/mitteloupe/cag/core/GeneratorTest.kt index 5c4e74b..e943143 100644 --- a/core/src/test/kotlin/com/mitteloupe/cag/core/GeneratorTest.kt +++ b/core/src/test/kotlin/com/mitteloupe/cag/core/GeneratorTest.kt @@ -527,12 +527,12 @@ class GeneratorTest { val expectedContent = """ [versions] + kotlin = "2.2.10" + ksp = "2.2.10-2.0.2" + androidGradlePlugin = "8.12.2" compileSdk = "36" minSdk = "24" targetSdk = "36" - androidGradlePlugin = "8.12.2" - kotlin = "2.2.10" - ksp = "2.2.10-2.0.2" [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/core/src/test/kotlin/com/mitteloupe/cag/core/generation/GradleWrapperCreatorTest.kt b/core/src/test/kotlin/com/mitteloupe/cag/core/generation/GradleWrapperCreatorTest.kt index 8dfa3f8..194dbe3 100644 --- a/core/src/test/kotlin/com/mitteloupe/cag/core/generation/GradleWrapperCreatorTest.kt +++ b/core/src/test/kotlin/com/mitteloupe/cag/core/generation/GradleWrapperCreatorTest.kt @@ -89,7 +89,7 @@ class GradleWrapperCreatorTest { } @Test - fun `Given existing gradle-wrapper-properties when writeGradleWrapperFiles then does not overwrite existing file`() { + fun `Given existing gradle-wrapper-properties when writeGradleWrapperFiles then overwrites existing file`() { // Given val projectRoot = createTempDirectory(prefix = "projectRoot").toFile() val gradleWrapperDirectory = File(projectRoot, "gradle/wrapper") @@ -98,12 +98,21 @@ class GradleWrapperCreatorTest { val gradleWrapperPropertiesFile = File(gradleWrapperDirectory, "gradle-wrapper.properties") val initialContent = "initial content" gradleWrapperPropertiesFile.writeText(initialContent) + val expectedContent = + """ + distributionBase=GRADLE_USER_HOME + distributionPath=wrapper/dists + distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip + zipStoreBase=GRADLE_USER_HOME + zipStorePath=wrapper/dists + """.trimIndent() // When classUnderTest.writeGradleWrapperFiles(projectRoot) + val actualContent = gradleWrapperPropertiesFile.readText() // Then - assertEquals("Existing gradle wrapper properties should not be overwritten", initialContent, gradleWrapperPropertiesFile.readText()) + assertEquals("Existing gradle wrapper properties should be overwritten", expectedContent, actualContent) } @Test diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 516f497..155edd9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ ktlint = "12.1.1" shadow = "8.1.1" mockk = "1.13.11" junit4 = "4.13.2" -androidStudio = "2025.1.4.2" +androidStudio = "2025.1.4.7" hamcrest = "3.0" [libraries] diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index 21c8e66..c87de99 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -18,6 +18,9 @@ repositories { intellijPlatform { pluginVerification { ides { + ide(IntelliJPlatformType.AndroidStudio, "2024.3.1.13") + ide(IntelliJPlatformType.AndroidStudio, "2025.1.2.11") + ide(IntelliJPlatformType.AndroidStudio, "2025.1.3.7") ide(IntelliJPlatformType.AndroidStudio, libs.versions.androidStudio.get()) } } diff --git a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateUseCaseDialog.kt b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateUseCaseDialog.kt index d2845f8..0cf613d 100644 --- a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateUseCaseDialog.kt +++ b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateUseCaseDialog.kt @@ -9,7 +9,7 @@ import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.ValidationInfo import com.intellij.ui.dsl.builder.bindText import com.intellij.ui.dsl.builder.panel -import com.intellij.ui.dsl.builder.whenItemChangedFromUi +import com.intellij.ui.dsl.builder.whenItemSelectedFromUi import com.intellij.util.ui.UIUtil import com.mitteloupe.cag.cleanarchitecturegenerator.form.PredicateDocumentFilter import com.mitteloupe.cag.cleanarchitecturegenerator.validation.SymbolValidator @@ -91,7 +91,7 @@ class CreateUseCaseDialog( row(CleanArchitectureGeneratorBundle.message("dialog.usecase.input.type.label")) { @Suppress("UnstableApiUsage") comboBox(inputDataTypeModel) - .whenItemChangedFromUi { _inputDataType = it } + .whenItemSelectedFromUi { _inputDataType = it } .applyToComponent { inputDataTypeComboBox = this isEditable = true @@ -108,7 +108,7 @@ class CreateUseCaseDialog( row(CleanArchitectureGeneratorBundle.message("dialog.usecase.output.type.label")) { @Suppress("UnstableApiUsage") comboBox(outputDataTypeModel) - .whenItemChangedFromUi { _outputDataType = it } + .whenItemSelectedFromUi { _outputDataType = it } .applyToComponent { outputDataTypeComboBox = this isEditable = true diff --git a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/projectwizard/CleanArchitectureWizardTemplateProvider.kt b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/projectwizard/CleanArchitectureWizardTemplateProvider.kt index a40d52f..57ea59d 100644 --- a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/projectwizard/CleanArchitectureWizardTemplateProvider.kt +++ b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/projectwizard/CleanArchitectureWizardTemplateProvider.kt @@ -15,6 +15,7 @@ import com.android.tools.idea.wizard.template.WizardUiContext import com.android.tools.idea.wizard.template.booleanParameter import com.android.tools.idea.wizard.template.impl.activities.common.MIN_API import com.android.tools.idea.wizard.template.template +import com.intellij.openapi.application.ApplicationInfo import com.mitteloupe.cag.cleanarchitecturegenerator.CleanArchitectureGeneratorBundle import com.mitteloupe.cag.cleanarchitecturegenerator.IdeBridge import com.mitteloupe.cag.cleanarchitecturegenerator.filesystem.GeneratorProvider @@ -23,7 +24,8 @@ import com.mitteloupe.cag.core.GenerationException import com.mitteloupe.cag.core.request.GenerateProjectTemplateRequest import com.mitteloupe.cag.git.Git import java.io.File -import java.lang.reflect.Field + +private val MEERKAT_PREFIX = "^(?:.* )?2024\\.3\\..*$".toRegex() class CleanArchitectureWizardTemplateProvider : WizardTemplateProvider() { private val ideBridge = IdeBridge() @@ -105,7 +107,7 @@ class CleanArchitectureWizardTemplateProvider : WizardTemplateProvider() { val projectRootDirectory = moduleData.rootDir.parentFile createProject( projectRootDirectory, - data, + moduleData, enableCompose, enableKtlint, enableDetekt, @@ -139,17 +141,29 @@ class CleanArchitectureWizardTemplateProvider : WizardTemplateProvider() { ) { val selectedMinSdk: Int? = try { - data.apis.minApi.apiLevel + val apis = data.apis + val getMinApi = apis.javaClass.methods.first { it.name == "getMinApi" } + val minApi = getMinApi(apis) + minApi.apiLevelCompat } catch (_: Exception) { null } + val applicationInfo = ApplicationInfo.getInstance() + val overrideAndroidGradlePluginVersion = + if (applicationInfo.fullVersion.matches(MEERKAT_PREFIX)) { + "8.9.0" + } else { + null + } + val request = GenerateProjectTemplateRequest( destinationRootDirectory = projectRootDirectory, - projectName = readProjectName(projectRootDirectory.name), + projectName = readProjectName(projectRootDirectory) ?: projectRootDirectory.name, packageName = data.packageName, overrideMinimumAndroidSdk = selectedMinSdk, + overrideAndroidGradlePluginVersion = overrideAndroidGradlePluginVersion, enableCompose = enableCompose.value, enableKtlint = enableKtlint.value, enableDetekt = enableDetekt.value, @@ -172,28 +186,30 @@ class CleanArchitectureWizardTemplateProvider : WizardTemplateProvider() { } } - private fun RecipeExecutor.readProjectName(fallbackProjectName: String): String { - val projectNameFromContext = - try { - val findReferencesExecutor = this as? FindReferencesRecipeExecutor - if (findReferencesExecutor != null) { - val contextField: Field = FindReferencesRecipeExecutor::class.java.getDeclaredField("context") - contextField.isAccessible = true - val context = contextField.get(findReferencesExecutor) - val projectField = context?.javaClass?.getDeclaredField("project") - projectField?.isAccessible = true - val project = projectField?.get(context) - val nameField = project?.javaClass?.getDeclaredField("name") - nameField?.isAccessible = true - nameField?.get(project) as? String - } else { - null - } - } catch (_: Exception) { - null - } + private fun readProjectName(rootDir: File): String? { + val gradleFile = + File(rootDir, "settings.gradle.kts").takeIf { it.exists() } + ?: File(rootDir, "settings.gradle").takeIf { it.exists() } + + if (gradleFile?.exists() == true) { + val gradleContents = gradleFile.readText() + val projectNameRegex = Regex("""rootProject\.name\s*=\s*["'](.+?)["']""") + val match = projectNameRegex.find(gradleContents) + return match?.groups?.get(1)?.value + } - return projectNameFromContext?.takeIf { it.isNotEmpty() } - ?: fallbackProjectName + return null } + + private val Any.apiLevelCompat: Int + get() = + runCatching { + val field = this::class.java.getDeclaredField("apiLevel") + field.isAccessible = true + field.getInt(this) + }.getOrElse { + val fallback = this::class.java.getDeclaredField("api") + fallback.isAccessible = true + fallback.getInt(this) + } } diff --git a/plugin/src/main/resources/META-INF/plugin.xml b/plugin/src/main/resources/META-INF/plugin.xml index d0f7873..e5eee8a 100644 --- a/plugin/src/main/resources/META-INF/plugin.xml +++ b/plugin/src/main/resources/META-INF/plugin.xml @@ -109,4 +109,6 @@ + +