diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..1e1089bf9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,104 @@ +# Valkyrie — AI Agent Guide + +## Project Overview + +Valkyrie converts SVG/XML icons to Compose `ImageVector` Kotlin code. It ships as four tools sharing the same core +pipeline: **IDEA/Android Studio plugin**, **CLI**, **Gradle plugin**, and a **WASM web app** (WIP). + +## Architecture: Data Flow + +``` +SVG / XML file + │ + ▼ +components:parser (jvm/svg, jvm/xml, kmp/xml, kmp/svg) + │ produces ──▶ sdk:ir:core (IrImageVector, IrVectorNode, IrPathNode, …) + ▼ +components:parser:unified (SvgXmlParser — single entry point for both formats) + │ + ▼ +components:generator (kmp/imagevector, jvm/imagevector, iconpack) + │ produces ──▶ Kotlin source string + ▼ +tools:idea-plugin / tools:cli / tools:gradle-plugin +``` + +- **`sdk/ir/core`** — Intermediate Representation (IR). All domain types live here (`IrImageVector`, `IrVectorNode`, + `IrFill`, `IrStroke`, etc.). Parsers produce IR; generators consume IR. +- **`components/parser/unified`** — `SvgXmlParser` is the unified entry point used by all tools. +- **`components/generator/kmp/imagevector`** — KMP-compatible generator (used by web/CLI/Gradle); JVM variant ( + `jvm/imagevector`) uses KotlinPoet. +- **`sdk/shared`** — `ValkyrieMode` enum (`Simple` | `IconPack`) controls generation style across all tools. +- **`build-logic/`** — Convention plugins (`valkyrie.jvm`, `valkyrie.kmp`, `valkyrie.abi`, `valkyrie.compose`, + `valkyrie.kover`) applied via `alias(libs.plugins.valkyrie.*)`. + +## Module Taxonomy + +| Prefix | Purpose | +|----------------|---------------------------------------------------------------------| +| `sdk/*` | Reusable building blocks (IR, Compose UI, IntelliJ PSI, utils) | +| `components/*` | Core pipeline: parsers & generators; have public ABI tracked | +| `tools/*` | End-user deliverables: idea-plugin, cli, gradle-plugin, compose-app | + +## Developer Workflows + +```bash +# Verify before committing +./gradlew test +./gradlew spotlessCheck +./gradlew checkLegacyAbi # ABI compatibility check for components/* + +# Fix formatting +./gradlew spotlessApply + +# Update ABI snapshots after intentional API changes +./gradlew updateLegacyAbi + +# IDEA plugin +./gradlew buildPlugin # produces tools/idea-plugin/build/distributions/ +./gradlew runIde # launches sandbox IDE + +# Web / WASM +./gradlew tools:compose-app:wasmJsBrowserDevelopmentRun + +# Coverage +./gradlew components:test:coverage:koverLog +./gradlew components:test:coverage:koverHtmlReport +``` + +**Java 21+ is required** (`settings.gradle.kts` enforces this at configuration time). + +## Code Style + +- **ktlint** via Spotless on all `src/**/*.kt`. Run `./gradlew spotlessApply` before pushing. +- Compose rules enforced: Material2 disallowed (`compose_disallow_material2 = true`), preview naming required (suffix + strategy), lambda-param-event-trailing disabled. +- KotlinGradle files also linted. + +## Convention Plugins (build-logic) + +Every module picks exactly one of: + +- `alias(libs.plugins.valkyrie.jvm)` — JVM-only library +- `alias(libs.plugins.valkyrie.kmp)` — Kotlin Multiplatform (JVM + wasmJs targets) +- `alias(libs.plugins.valkyrie.compose)` — adds Compose compiler + +Plus optional: `valkyrie.abi` (ABI tracking), `valkyrie.kover` (coverage). + +## Key Files + +- `sdk/ir/core/src/commonMain/.../IrImageVector.kt` — central domain model +- `components/parser/unified/src/commonMain/.../SvgXmlParser.kt` — unified parser entry point +- `components/generator/kmp/imagevector/src/commonMain/.../ImageVectorGenerator.kt` — KMP generator +- `sdk/shared/src/commonMain/.../ValkyrieMode.kt` — Simple vs IconPack mode +- `gradle/libs.versions.toml` — main version catalog; `gradle/cli.versions.toml`, `gradle/gradle.versions.toml`, + `gradle/plugin.versions.toml` for tool-specific versions +- `tools/idea-plugin/CHANGELOG.md` — updated via `./gradlew tools:idea-plugin:patchChangelog` + +## IDEA Plugin Specifics + +- Targets IntelliJ IDEA 2025.3.3 (`sinceBuild = "253.31033.145"`, `untilBuild` is unbounded). +- Bundled Kotlin/Coroutines/Compose are excluded from the plugin ZIP (classpath clash workaround). +- Signing credentials read from env vars: `CERTIFICATE_CHAIN`, `PRIVATE_KEY`, `PRIVATE_KEY_PASSWORD`. +- Publish token: `PUBLISH_TOKEN`. + diff --git a/README.md b/README.md index d55f875e1..c4d0ea024 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ needs. - [`changelog` command](#changelog-command) - [Build](#build-cli) - 🐘 [Gradle plugin](#gradle-plugin) + - [Compatibility](#compatibility) - [Plugin configuration](#plugin-configuration) - [Samples](#gradle-plugin-samples) - [Basic conversion](#basic-conversion) @@ -368,6 +369,13 @@ Run `./gradlew buildCLI` to build minified version of CLI tool. Artifact will be The Gradle plugin automates the conversion of SVG/XML files to Compose ImageVector format during the build process. It's ideal for projects that need to version control icon sources and generate type-safe Kotlin code automatically. +### Compatibility + +| Valkyrie plugin version | Min AGP | Min Gradle | +|-------------------------|---------|-------------------------| +| 0.4.0 | 9.0.0 | 9.0.0 (should be lower) | +| 0.5.0 | 9.2.0 | 9.4.1 | + ### Common scenarios - **Team collaboration**: Keep SVG/XML sources in version control and let the build system generate Kotlin code for diff --git a/gradle/gradle.versions.toml b/gradle/gradle.versions.toml index 641384735..4899e465f 100644 --- a/gradle/gradle.versions.toml +++ b/gradle/gradle.versions.toml @@ -1,7 +1,7 @@ [versions] gradle-plugin-version = "0.4.0" -agp = "8.13.2" +agp = "9.2.0" [libraries] agp-api = { module = "com.android.tools.build:gradle-api", version.ref = "agp" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fafd960ee..d5b9ce814 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ ktor = "3.3.1" [libraries] androidx-collection = { module = "androidx.collection:collection", version.ref = "collection" } -android-build-tools = "com.android.tools:sdk-common:31.13.2" +android-build-tools = "com.android.tools:sdk-common:32.2.0" compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose" } compose-material3 = "org.jetbrains.compose.material3:material3:1.10.0-alpha05" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index dbc3ce4a0..c61a118f7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 0262dcbd5..739907dfd 100755 --- a/gradlew +++ b/gradlew @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/tools/gradle-plugin/CHANGELOG.md b/tools/gradle-plugin/CHANGELOG.md index a56edb294..a0c55f6ef 100644 --- a/tools/gradle-plugin/CHANGELOG.md +++ b/tools/gradle-plugin/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +### Added + +- Add support for AGP 9.0+ built-in Kotlin — source-set enumeration and generated source wiring now work + without the external `kotlin("android")` plugin via the stable `CommonExtension` API + +### Removed + +- Drop support for AGP 8.x. The minimum supported AGP version is now **9.2.0**. + ## 0.4.0 - 2026-02-26 ### Added diff --git a/tools/gradle-plugin/build.gradle.kts b/tools/gradle-plugin/build.gradle.kts index b11b3bfed..9309af119 100644 --- a/tools/gradle-plugin/build.gradle.kts +++ b/tools/gradle-plugin/build.gradle.kts @@ -56,7 +56,7 @@ configurations.named(API_ELEMENTS_CONFIGURATION_NAME) { attributes.attribute( // TODO: https://github.com/gradle/gradle/issues/24608 GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, - objects.named("9.0.0"), + objects.named("9.4.1"), ) } diff --git a/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/IconPackExtension.kt b/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/IconPackExtension.kt index bba8485da..6c08f493a 100644 --- a/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/IconPackExtension.kt +++ b/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/IconPackExtension.kt @@ -10,7 +10,6 @@ import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.Nested import org.gradle.api.tasks.Optional -import org.gradle.declarative.dsl.model.annotations.Configuring abstract class IconPackExtension @Inject constructor( private val objects: ObjectFactory, @@ -62,7 +61,6 @@ abstract class IconPackExtension @Inject constructor( .convention(emptyList()) @Suppress("unused") - @Configuring fun nested(action: NestedPack.() -> Unit) { val config = objects.newInstance().apply(action) nestedPacks.add(config) diff --git a/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/ValkyrieExtension.kt b/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/ValkyrieExtension.kt index 57a6ca381..56d82f202 100644 --- a/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/ValkyrieExtension.kt +++ b/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/ValkyrieExtension.kt @@ -9,7 +9,6 @@ import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property import org.gradle.api.tasks.Nested import org.gradle.api.tasks.Optional -import org.gradle.declarative.dsl.model.annotations.Configuring abstract class ValkyrieExtension @Inject constructor(private val objects: ObjectFactory) { /** @@ -81,7 +80,6 @@ abstract class ValkyrieExtension @Inject constructor(private val objects: Object * Configures code style options for generated code */ @Suppress("unused") - @Configuring fun codeStyle(action: CodeStyleConfigExtension.() -> Unit) = action.invoke(codeStyle) /** @@ -101,14 +99,12 @@ abstract class ValkyrieExtension @Inject constructor(private val objects: Object * Configures ImageVector generation options */ @Suppress("unused") - @Configuring fun imageVector(action: ImageVectorConfigExtension.() -> Unit) = action.invoke(imageVector) /** * Configures Icon Pack options */ @Suppress("unused") - @Configuring fun iconPack(action: IconPackExtension.() -> Unit) { val spec = iconPack.getOrElse(objects.newInstance()).apply(action) iconPack.set(spec) diff --git a/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/ValkyrieGradlePlugin.kt b/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/ValkyrieGradlePlugin.kt index 8e99326ac..98e0f9999 100644 --- a/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/ValkyrieGradlePlugin.kt +++ b/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/ValkyrieGradlePlugin.kt @@ -1,6 +1,9 @@ package io.github.composegears.valkyrie.gradle +import com.android.build.api.dsl.CommonExtension +import com.android.build.api.variant.AndroidComponentsExtension import io.github.composegears.valkyrie.gradle.dsl.create +import io.github.composegears.valkyrie.gradle.dsl.findByType import io.github.composegears.valkyrie.gradle.dsl.getByType import io.github.composegears.valkyrie.gradle.dsl.withType import io.github.composegears.valkyrie.gradle.internal.DEFAULT_GENERATED_SOURCES_DIR @@ -9,9 +12,9 @@ import io.github.composegears.valkyrie.gradle.internal.common.ExtensionValidator import io.github.composegears.valkyrie.gradle.internal.common.PackageNameProvider.packageNameOrThrow import io.github.composegears.valkyrie.gradle.internal.registerTask import io.github.composegears.valkyrie.gradle.internal.task.GenerateImageVectorsTask +import io.github.composegears.valkyrie.parser.unified.ext.capitalized import org.gradle.api.Plugin import org.gradle.api.Project -import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetContainer @@ -34,14 +37,29 @@ class ValkyrieGradlePlugin : Plugin { registerTasks(extension) } - pluginManager.withPlugin("org.jetbrains.kotlin.android") { - pluginManager.withPlugin("com.android.base") { - registerTasks(extension) - } + // AGP 9.0+ has built-in Kotlin — kotlin("android") is no longer applied. + // Use CommonExtension.sourceSets (stable AGP API) instead of KotlinAndroidProjectExtension + // so that source-set enumeration works regardless of whether the external Kotlin plugin is present. + // Only one Android plugin can be applied per module, so each callback fires at most once. + listOf( + "com.android.application", + "com.android.library", + "com.android.test", + "com.android.dynamic-feature", + ).forEach { id -> + pluginManager.withPlugin(id) { registerAndroidTasks(extension) } } val codegenTasks = tasks.withType() + // AGP's ExtractAnnotations tasks scan all source directories contributing to a variant, including + // the generated ones registered via addStaticSourceDirectory. Because addStaticSourceDirectory + // doesn't declare a producer-task relationship, Gradle's strict dependency validation (used with + // --configuration-cache) raises an "implicit dependency" error. Declaring an explicit dependsOn + // here ensures the files are generated before any extractAnnotations task reads them. + tasks.matching { it.name.startsWith("extract") && it.name.endsWith("Annotations") } + .configureEach { it.dependsOn(codegenTasks) } + // Run generation immediately if we're syncing Intellij/Android Studio - helps to speed up dev cycle afterEvaluate { ExtensionValidator.validate(extension) @@ -53,7 +71,7 @@ class ValkyrieGradlePlugin : Plugin { } // Run generation before any kind of kotlin source processing - tasks.withType(AbstractKotlinCompile::class.java).configureEach { compileTask -> + tasks.withType>().configureEach { compileTask -> compileTask.dependsOn(codegenTasks) } @@ -68,4 +86,65 @@ class ValkyrieGradlePlugin : Plugin { registerTask(project, extension, sourceSet) } } + + /** + * Registers generation tasks for all Android source sets using the stable [CommonExtension] API. + * + * In AGP 9.0 with built-in Kotlin (no external kotlin("android") plugin): + * - Phase 1: enumerate source sets via [CommonExtension.sourceSets] and register one + * [GenerateImageVectorsTask] per source set with its output directory set via convention. + * - Phase 2: use [AndroidComponentsExtension.onVariants] to wire each variant's generated + * source directory via [com.android.build.api.variant.SourceDirectories.addStaticSourceDirectory]. + * We deliberately use addStaticSourceDirectory (not addGeneratedSourceDirectory) because + * addGeneratedSourceDirectory overrides the task's outputDirectory property, which would + * redirect generated files away from the user-visible path + * (build/generated/sources/valkyrie//kotlin). + * The compile → gen task dependency is declared separately via AbstractKotlinCompile.dependsOn. + */ + @Suppress("UNCHECKED_CAST") + private fun Project.registerAndroidTasks(extension: ValkyrieExtension) { + val androidExt = extensions.findByType() ?: return + val androidComponents = extensions.findByType>() ?: return + + // Phase 1: register one task per source set (no srcDir — wired in phase 2) + androidExt.sourceSets.configureEach { sourceSet -> + registerTask( + project = this, + extension = extension, + sourceSetName = sourceSet.name, + addGeneratedSrcDir = { /* handled via addStaticSourceDirectory in phase 2 */ }, + ) + } + + // Phase 2: wire the generated output directories into each variant's Kotlin compilation. + // We use addStaticSourceDirectory (not addGeneratedSourceDirectory) so that AGP does NOT + // override the task's outputDirectory convention — the files must land at the path our tests + // and users expect (build/generated/sources/valkyrie//kotlin). + // The compile → gen task dependency is already declared globally via + // AbstractKotlinCompile.dependsOn(codegenTasks) above. + androidComponents.onVariants { variant -> + // Source sets that contribute to this variant: + // "main" + buildType (e.g. "debug") + flavors (e.g. "free") + combined (e.g. "freeDebug") + val contributingSourceSets = buildList { + add("main") + variant.buildType?.let { add(it) } + variant.productFlavors.forEach { (_, flavorName) -> add(flavorName) } + if (variant.productFlavors.isNotEmpty()) add(variant.name) + } + + contributingSourceSets + .filter { sourceSetName -> + // Only wire tasks that were actually registered (safe guard) + "${TASK_NAME}${sourceSetName.capitalized()}" in tasks.names + } + .forEach { sourceSetName -> + val genDir = extension.outputDirectory + .get() + .dir("$sourceSetName/kotlin") + .asFile + .absolutePath + variant.sources.kotlin?.addStaticSourceDirectory(genDir) + } + } + } } diff --git a/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/dsl/ExtensionContainerExtensions.kt b/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/dsl/ExtensionContainerExtensions.kt index 7544dcb01..a9ee4445f 100644 --- a/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/dsl/ExtensionContainerExtensions.kt +++ b/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/dsl/ExtensionContainerExtensions.kt @@ -10,4 +10,6 @@ internal inline fun ExtensionContainer.create( internal inline fun ExtensionContainer.getByType(): T = getByType(typeOf()) +internal inline fun ExtensionContainer.findByType(): T? = findByType(T::class.java) + internal inline fun typeOf(): TypeOf = object : TypeOf() {} diff --git a/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/dsl/TaskCollectionExtensions.kt b/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/dsl/TaskCollectionExtensions.kt index a7311116b..2e8e1a198 100644 --- a/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/dsl/TaskCollectionExtensions.kt +++ b/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/dsl/TaskCollectionExtensions.kt @@ -3,4 +3,7 @@ package io.github.composegears.valkyrie.gradle.dsl import org.gradle.api.Task import org.gradle.api.tasks.TaskCollection -internal inline fun TaskCollection.withType(): TaskCollection = withType(T::class.java) +@Suppress("UNCHECKED_CAST") +internal inline fun TaskCollection<*>.withType(): TaskCollection { + return (this as TaskCollection).withType(T::class.java) +} diff --git a/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/TaskRegistry.kt b/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/TaskRegistry.kt index b0d9090e5..381441153 100644 --- a/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/TaskRegistry.kt +++ b/tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/TaskRegistry.kt @@ -5,23 +5,30 @@ import io.github.composegears.valkyrie.gradle.dsl.conventionCompat import io.github.composegears.valkyrie.gradle.internal.task.GenerateImageVectorsTask import io.github.composegears.valkyrie.parser.unified.ext.capitalized import org.gradle.api.Project -import org.gradle.api.file.Directory import org.gradle.api.file.FileCollection import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +/** + * Core task registration used by both Kotlin (JVM/KMP) and Android (AGP 9.0 built-in Kotlin) paths. + * + * @param sourceSetName name of the source set (e.g. "main", "debug", "commonMain") + * @param addGeneratedSrcDir callback that wires the generated output directory into the + * appropriate compilation so the Kotlin compiler picks it up. + */ internal fun registerTask( project: Project, extension: ValkyrieExtension, - sourceSet: KotlinSourceSet, + sourceSetName: String, + addGeneratedSrcDir: (Provider<*>) -> Unit, ) { - val taskName = "${TASK_NAME}${sourceSet.name.capitalized()}" - val sourceSetName = sourceSet.name + val taskName = "${TASK_NAME}${sourceSetName.capitalized()}" project.tasks.register(taskName, GenerateImageVectorsTask::class.java) { task -> task.description = "Converts SVG & Drawable files into ImageVector Kotlin accessor properties" val resourceDirName = extension.resourceDirectoryName - val iconFiles = sourceSet.findIconFiles(resourceDirName) + val iconFiles = project.findIconFiles(sourceSetName, resourceDirName) task.iconFiles.conventionCompat(iconFiles) task.onlyIf("Needs at least one input file or iconPack") { !iconFiles.isEmpty || @@ -31,9 +38,9 @@ internal fun registerTask( task.packageName.convention(extension.packageName) val outputRoot = extension.outputDirectory - val perSourceSetDir = outputRoot.map { it.dir("${sourceSet.name}/kotlin") } + val perSourceSetDir = outputRoot.map { it.dir("$sourceSetName/kotlin") } task.outputDirectory.convention(perSourceSetDir) - sourceSet.kotlin.srcDir(perSourceSetDir) + addGeneratedSrcDir(perSourceSetDir) task.outputFormat.convention(extension.imageVector.outputFormat) task.useComposeColors.convention(extension.imageVector.useComposeColors) @@ -44,27 +51,35 @@ internal fun registerTask( task.usePathDataString.convention(extension.imageVector.usePathDataString) task.autoMirror.convention(extension.autoMirror) - task.sourceSet.convention(sourceSet.name) + task.sourceSet.convention(sourceSetName) task.iconPack.convention(extension.iconPack) } } -private fun KotlinSourceSet.root(): Directory = with(project) { - // kotlin.srcDirs returns a set like ["src/main/kotlin", "src/main/java"] - we want the "src/main" directory. - val src = provider { - kotlin.srcDirs - .firstOrNull() - ?.resolve("..") - ?: error("No srcDir found for source set $name") - } - return layout.dir(src).get() +/** Convenience overload for Kotlin (JVM / KMP) source sets. */ +internal fun registerTask( + project: Project, + extension: ValkyrieExtension, + sourceSet: KotlinSourceSet, +) { + registerTask( + project = project, + extension = extension, + sourceSetName = sourceSet.name, + addGeneratedSrcDir = { dir -> sourceSet.kotlin.srcDir(dir) }, + ) } -private fun KotlinSourceSet.findIconFiles(resourceDirectoryName: Property): FileCollection { - return root() - .dir(resourceDirectoryName.get()) - .asFileTree - .filter { - it.extension == "svg" || it.extension == "xml" - } +/** + * Locates icon files for [sourceSetName] using the standard src// convention. + * This is reliable across JVM, KMP, and AGP 9.0 built-in Kotlin where kotlin.srcDirs may be empty + * at configuration time. + */ +private fun Project.findIconFiles(sourceSetName: String, resourceDirectoryName: Property): FileCollection { + val resourceDirProvider = resourceDirectoryName.map { dirName -> + layout.projectDirectory.dir("src/$sourceSetName").dir(dirName) + } + return files( + resourceDirProvider.map { dir -> dir.asFileTree.filter { it.extension == "svg" || it.extension == "xml" } }, + ) } diff --git a/tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/ValkyrieGradlePluginTest.kt b/tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/ValkyrieGradlePluginTest.kt index b404a85ab..98184f6cb 100644 --- a/tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/ValkyrieGradlePluginTest.kt +++ b/tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/ValkyrieGradlePluginTest.kt @@ -35,7 +35,6 @@ class ValkyrieGradlePluginTest : CommonGradleTest() { root.resolve("build.gradle.kts").writeText( """ plugins { - kotlin("android") id("com.android.library") id("io.github.composegears.valkyrie") } @@ -165,28 +164,26 @@ class ValkyrieGradlePluginTest : CommonGradleTest() { @Test fun `Generate from SVGs in KMP project`() { + // AGP 9.0: com.android.library is incompatible with kotlin("multiplatform"). + // Use com.android.kotlin.multiplatform.library + androidLibrary { } instead. root.resolve("build.gradle.kts").writeText( """ plugins { kotlin("multiplatform") - id("com.android.library") + id("com.android.kotlin.multiplatform.library") id("io.github.composegears.valkyrie") } - android { - namespace = "x.y.z" - compileSdk = 36 - - flavorDimensions += "test" - productFlavors { - create("free") { dimension = "test" } - create("paid") { dimension = "test" } + kotlin { + androidLibrary { + namespace = "x.y.z" + compileSdk = 36 } + jvm() } - kotlin { - androidTarget() - jvm() + valkyrie { + packageName = "x.y.z" } """.trimIndent(), ) @@ -197,8 +194,6 @@ class ValkyrieGradlePluginTest : CommonGradleTest() { val result = runTask(root, TASK_NAME) // no SVGs/drawables under these source sets, so the tasks are skipped (but still registered) - assertThat(result).taskHadResult(":generateValkyrieImageVectorAndroidFree", SKIPPED) - assertThat(result).taskHadResult(":generateValkyrieImageVectorAndroidPaidDebug", SKIPPED) assertThat(result).taskHadResult(":generateValkyrieImageVectorCommonMain", SKIPPED) assertThat(result).taskHadResult(":generateValkyrieImageVectorJvmTest", SKIPPED) @@ -212,7 +207,6 @@ class ValkyrieGradlePluginTest : CommonGradleTest() { root.resolve("build.gradle.kts").writeText( """ plugins { - kotlin("android") id("com.android.library") id("io.github.composegears.valkyrie") } @@ -244,7 +238,6 @@ class ValkyrieGradlePluginTest : CommonGradleTest() { root.resolve("build.gradle.kts").writeText( """ plugins { - kotlin("android") id("com.android.library") id("io.github.composegears.valkyrie") } @@ -274,7 +267,6 @@ class ValkyrieGradlePluginTest : CommonGradleTest() { root.resolve("build.gradle.kts").writeText( """ plugins { - kotlin("android") id("com.android.library") id("io.github.composegears.valkyrie") } @@ -396,7 +388,6 @@ class ValkyrieGradlePluginTest : CommonGradleTest() { root.resolve("build.gradle.kts").writeText( """ plugins { - kotlin("android") id("com.android.library") id("io.github.composegears.valkyrie") } @@ -412,11 +403,6 @@ class ValkyrieGradlePluginTest : CommonGradleTest() { } } - kotlin { - // to match that used in validation.yml for CI - jvmToolchain(21) - } - dependencies { implementation("$COMPOSE_UI") } diff --git a/tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/common/CommonGradleTest.kt b/tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/common/CommonGradleTest.kt index b6b1c42c2..3c17caddd 100644 --- a/tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/common/CommonGradleTest.kt +++ b/tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/common/CommonGradleTest.kt @@ -47,7 +47,6 @@ open class CommonGradleTest { task, "--configuration-cache", "--stacktrace", - "-Pandroid.useAndroidX=true", // needed for android builds to work, unused otherwise ) protected fun Path.writeSettingsFile() = resolve("settings.gradle.kts").writeText(