Skip to content

Commit 4a41138

Browse files
jonapoulGoooler
andauthored
Add Gradle Plugin (#479)
* Squashed/rebased * No task group * Make task registration non-public * Handle user-set source sets * Update android sdk dir * Convention extension * Task function * given -> transform * Shuffle androidHome * spotless * Update * More * Update tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/Utils.kt * Update tools/gradle-plugin/src/test/kotlin/io/github/composegears/valkyrie/gradle/ValkyrieGradlePluginTest.kt --------- Co-authored-by: Zongle Wang <[email protected]>
1 parent e1cc90f commit 4a41138

File tree

14 files changed

+1184
-4
lines changed

14 files changed

+1184
-4
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ needs.
8282

8383
- IntelliJ IDEA / Android Studio plugin
8484
- CLI tool
85-
- Gradle plugin and Web app (🚧 coming soon 🚧)
85+
- Gradle plugin
86+
- Web app (🚧 coming soon 🚧)
8687

8788
## IDEA Plugin
8889

components/test/coverage/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ plugins {
55
dependencies {
66
// include only necessary dependencies for the test coverage
77
kover(projects.tools.cli)
8+
kover(projects.tools.gradlePlugin)
89
kover(projects.components.generator.core)
910
kover(projects.components.generator.jvm.poetExtensions)
1011
kover(projects.components.generator.iconpack)

gradle/libs.versions.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[versions]
2+
agp = "8.13.0"
23
compose = "1.8.2"
34
intellij = "2.9.0"
45
jacoco = "0.8.13"
@@ -8,6 +9,7 @@ leviathan = "3.1.0-1.8.2"
89

910
[libraries]
1011
android-build-tools = "com.android.tools:sdk-common:31.13.0"
12+
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
1113
highlights = "dev.snipme:highlights:1.1.0"
1214
kotlinpoet = "com.squareup:kotlinpoet:2.2.0"
1315
kotlin-io = "org.jetbrains.kotlinx:kotlinx-io-core:0.8.0"
@@ -31,6 +33,9 @@ mockk = "io.mockk:mockk:1.14.6"
3133
ktlint = "com.pinterest.ktlint:ktlint-cli:1.7.1"
3234
composeRules = "io.nlopez.compose.rules:ktlint:0.4.27"
3335

36+
agp-api = { module = "com.android.tools.build:gradle-api", version.ref = "agp" }
37+
agp-full = { module = "com.android.tools.build:gradle", version.ref = "agp" }
38+
3439
# Dependencies for build-logic module
3540
kotlin-compose-compiler-plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" }
3641
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
public final class io/github/composegears/valkyrie/sdk/core/extensions/PathUtilsKt {
2-
public static final fun writeToKt (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZ)V
3-
public static synthetic fun writeToKt$default (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZILjava/lang/Object;)V
2+
public static final fun writeToKt (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZ)Ljava/nio/file/Path;
3+
public static synthetic fun writeToKt$default (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZILjava/lang/Object;)Ljava/nio/file/Path;
44
}
55

sdk/core/extensions/src/jvmMain/kotlin/io/github/composegears/valkyrie/sdk/core/extensions/PathUtils.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.composegears.valkyrie.sdk.core.extensions
22

33
import java.io.IOException
4+
import java.nio.file.Path
45
import kotlin.io.path.Path
56
import kotlin.io.path.createParentDirectories
67
import kotlin.io.path.deleteIfExists
@@ -27,7 +28,7 @@ private fun String.writeToFile(
2728
extension: String,
2829
deleteIfExists: Boolean,
2930
createParents: Boolean,
30-
) {
31+
): Path {
3132
val outputPath = Path(outputDir, "$nameWithoutExtension.$extension")
3233

3334
if (deleteIfExists) {
@@ -37,4 +38,5 @@ private fun String.writeToFile(
3738
outputPath.createParentDirectories()
3839
}
3940
outputPath.writeText(this)
41+
return outputPath
4042
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ includeBuild("build-logic")
5353

5454
include("tools:cli")
5555
include("tools:compose-app")
56+
include("tools:gradle-plugin")
5657
include("tools:idea-plugin")
5758

5859
include("components:generator:core")
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
public abstract class io/github/composegears/valkyrie/gradle/GenerateImageVectorsTask : org/gradle/api/DefaultTask {
2+
public fun <init> ()V
3+
public final fun execute ()V
4+
public abstract fun getAddTrailingComma ()Lorg/gradle/api/provider/Property;
5+
public abstract fun getDrawableFiles ()Lorg/gradle/api/file/ConfigurableFileCollection;
6+
public abstract fun getGeneratePreview ()Lorg/gradle/api/provider/Property;
7+
public abstract fun getIconPackName ()Lorg/gradle/api/provider/Property;
8+
public abstract fun getIndentSize ()Lorg/gradle/api/provider/Property;
9+
public abstract fun getNestedPackName ()Lorg/gradle/api/provider/Property;
10+
public abstract fun getOutputDirectory ()Lorg/gradle/api/file/DirectoryProperty;
11+
public abstract fun getOutputFormat ()Lorg/gradle/api/provider/Property;
12+
public abstract fun getPackageName ()Lorg/gradle/api/provider/Property;
13+
public abstract fun getPreviewAnnotationType ()Lorg/gradle/api/provider/Property;
14+
public abstract fun getSvgFiles ()Lorg/gradle/api/file/ConfigurableFileCollection;
15+
public abstract fun getUseComposeColors ()Lorg/gradle/api/provider/Property;
16+
public abstract fun getUseExplicitMode ()Lorg/gradle/api/provider/Property;
17+
public abstract fun getUseFlatPackage ()Lorg/gradle/api/provider/Property;
18+
}
19+
20+
public abstract interface class io/github/composegears/valkyrie/gradle/ValkyrieExtension {
21+
public abstract fun getAddTrailingComma ()Lorg/gradle/api/provider/Property;
22+
public abstract fun getGenerateAtSync ()Lorg/gradle/api/provider/Property;
23+
public abstract fun getGeneratePreview ()Lorg/gradle/api/provider/Property;
24+
public abstract fun getIconPackName ()Lorg/gradle/api/provider/Property;
25+
public abstract fun getIndentSize ()Lorg/gradle/api/provider/Property;
26+
public abstract fun getNestedPackName ()Lorg/gradle/api/provider/Property;
27+
public abstract fun getOutputDirectory ()Lorg/gradle/api/file/DirectoryProperty;
28+
public abstract fun getOutputFormat ()Lorg/gradle/api/provider/Property;
29+
public abstract fun getPackageName ()Lorg/gradle/api/provider/Property;
30+
public abstract fun getPreviewAnnotationType ()Lorg/gradle/api/provider/Property;
31+
public abstract fun getUseComposeColors ()Lorg/gradle/api/provider/Property;
32+
public abstract fun getUseExplicitMode ()Lorg/gradle/api/provider/Property;
33+
public abstract fun getUseFlatPackage ()Lorg/gradle/api/provider/Property;
34+
}
35+
36+
public final class io/github/composegears/valkyrie/gradle/ValkyrieGradlePlugin : org/gradle/api/Plugin {
37+
public fun <init> ()V
38+
public synthetic fun apply (Ljava/lang/Object;)V
39+
public fun apply (Lorg/gradle/api/Project;)V
40+
}
41+
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import java.nio.file.Paths
2+
import java.util.Properties
3+
import kotlin.io.path.exists
4+
5+
plugins {
6+
alias(libs.plugins.kotlin.jvm)
7+
alias(libs.plugins.valkyrie.kover)
8+
alias(libs.plugins.valkyrie.abi)
9+
alias(libs.plugins.buildConfig)
10+
`java-gradle-plugin`
11+
}
12+
13+
tasks.validatePlugins {
14+
// TODO: https://github.com/gradle/gradle/issues/22600
15+
enableStricterValidation = true
16+
}
17+
18+
gradlePlugin {
19+
vcsUrl = "https://github.com/ComposeGears/Valkyrie"
20+
website = "https://github.com/ComposeGears/Valkyrie"
21+
22+
plugins {
23+
create("valkyrie") {
24+
id = "io.github.composegears.valkyrie"
25+
displayName = name
26+
implementationClass = "io.github.composegears.valkyrie.gradle.ValkyrieGradlePlugin"
27+
description = "Generates Kotlin accessors for ImageVectors, based on input SVG files"
28+
tags.addAll("kotlin", "svg", "xml", "imagevector", "valkyrie")
29+
}
30+
}
31+
}
32+
33+
val sharedTestResourcesDir: File =
34+
project(projects.components.test.path)
35+
.layout
36+
.projectDirectory
37+
.dir("sharedTestResources/imagevector")
38+
.asFile
39+
40+
buildConfig.sourceSets.getByName("test") {
41+
packageName = "io.github.composegears.valkyrie.gradle"
42+
useKotlinOutput { topLevelConstants = true }
43+
44+
// So we can copy the shared test SVG/XML files into our test cases
45+
buildConfigField("RESOURCES_DIR_SVG", sharedTestResourcesDir.resolve("svg"))
46+
buildConfigField("RESOURCES_DIR_XML", sharedTestResourcesDir.resolve("xml"))
47+
buildConfigField<String?>("ANDROID_HOME", androidHome())
48+
buildConfigField("COMPOSE_UI", libs.compose.ui.get().toString())
49+
50+
// TODO: Set up tests to run for different gradle versions?
51+
buildConfigField("GRADLE_VERSION", GradleVersion.current().version)
52+
}
53+
54+
// Adapted from https://github.com/GradleUp/shadow/blob/1d7b0863fed3126bf376f11d563e9176de176cd3/build.gradle.kts#L63-L65
55+
// Allows gradle test cases to use the same classpath as the parent build - meaning we don't need to specify versions
56+
// when loading plugins into test projects.
57+
val testPluginClasspath by configurations.registering {
58+
isCanBeResolved = true
59+
}
60+
61+
tasks.pluginUnderTestMetadata {
62+
// Plugins used in tests could be resolved in classpath.
63+
pluginClasspath.from(testPluginClasspath)
64+
}
65+
66+
dependencies {
67+
compileOnly(libs.agp.api)
68+
compileOnly(libs.kotlin.gradle.plugin)
69+
70+
api(projects.sdk.core.extensions)
71+
api(projects.components.generator.iconpack)
72+
api(projects.components.generator.jvm.imagevector)
73+
api(projects.components.ir)
74+
api(projects.components.parser.unified)
75+
76+
testImplementation(libs.bundles.test)
77+
testRuntimeOnly(libs.junit.launcher)
78+
79+
testPluginClasspath(libs.agp.full)
80+
testPluginClasspath(libs.kotlin.gradle.plugin)
81+
}
82+
83+
fun androidHome(): String? {
84+
val androidSdkRoot = System.getenv("ANDROID_SDK_ROOT")
85+
if (!androidSdkRoot.isNullOrBlank() && Paths.get(androidSdkRoot).exists()) {
86+
logger.info("Using ANDROID_SDK_ROOT=$androidSdkRoot")
87+
return androidSdkRoot
88+
}
89+
90+
val androidHome = System.getenv("ANDROID_HOME")
91+
if (!androidHome.isNullOrBlank() && Paths.get(androidHome).exists()) {
92+
logger.info("Using ANDROID_HOME=$androidHome")
93+
return androidHome
94+
}
95+
96+
val localProps = rootProject.file("local.properties")
97+
if (localProps.exists()) {
98+
val properties = Properties()
99+
localProps.inputStream().use { properties.load(it) }
100+
val sdkHome = properties.getProperty("sdk.dir")?.takeIf { it.isNotBlank() }
101+
if (sdkHome != null && Paths.get(sdkHome).exists()) {
102+
logger.info("Using local.properties sdk.dir $sdkHome")
103+
return sdkHome
104+
}
105+
}
106+
107+
logger.warn("No Android SDK found - Android unit tests will be skipped")
108+
return null
109+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package io.github.composegears.valkyrie.gradle
2+
3+
import io.github.composegears.valkyrie.generator.jvm.imagevector.ImageVectorGenerator
4+
import io.github.composegears.valkyrie.generator.jvm.imagevector.ImageVectorGeneratorConfig
5+
import io.github.composegears.valkyrie.generator.jvm.imagevector.OutputFormat
6+
import io.github.composegears.valkyrie.generator.jvm.imagevector.PreviewAnnotationType
7+
import io.github.composegears.valkyrie.parser.unified.ParserType
8+
import io.github.composegears.valkyrie.parser.unified.SvgXmlParser
9+
import io.github.composegears.valkyrie.parser.unified.ext.toIOPath
10+
import io.github.composegears.valkyrie.sdk.core.extensions.writeToKt
11+
import kotlinx.io.files.Path
12+
import org.gradle.api.DefaultTask
13+
import org.gradle.api.GradleException
14+
import org.gradle.api.file.ConfigurableFileCollection
15+
import org.gradle.api.file.DirectoryProperty
16+
import org.gradle.api.provider.Property
17+
import org.gradle.api.tasks.CacheableTask
18+
import org.gradle.api.tasks.Input
19+
import org.gradle.api.tasks.InputFiles
20+
import org.gradle.api.tasks.Optional
21+
import org.gradle.api.tasks.OutputDirectory
22+
import org.gradle.api.tasks.PathSensitive
23+
import org.gradle.api.tasks.PathSensitivity.RELATIVE
24+
import org.gradle.api.tasks.TaskAction
25+
26+
@CacheableTask
27+
abstract class GenerateImageVectorsTask : DefaultTask() {
28+
@get:[PathSensitive(RELATIVE) InputFiles] abstract val svgFiles: ConfigurableFileCollection
29+
30+
@get:[PathSensitive(RELATIVE) InputFiles] abstract val drawableFiles: ConfigurableFileCollection
31+
32+
@get:Input abstract val packageName: Property<String>
33+
34+
@get:[Input Optional] abstract val iconPackName: Property<String>
35+
36+
@get:[Input Optional] abstract val nestedPackName: Property<String>
37+
38+
@get:Input abstract val outputFormat: Property<OutputFormat>
39+
40+
@get:Input abstract val useComposeColors: Property<Boolean>
41+
42+
@get:Input abstract val generatePreview: Property<Boolean>
43+
44+
@get:Input abstract val previewAnnotationType: Property<PreviewAnnotationType>
45+
46+
@get:Input abstract val useFlatPackage: Property<Boolean>
47+
48+
@get:Input abstract val useExplicitMode: Property<Boolean>
49+
50+
@get:Input abstract val addTrailingComma: Property<Boolean>
51+
52+
@get:Input abstract val indentSize: Property<Int>
53+
54+
@get:OutputDirectory abstract val outputDirectory: DirectoryProperty
55+
56+
@TaskAction
57+
fun execute() {
58+
val packageName = packageName.orNull
59+
?: throw GradleException("No package name configured for $this")
60+
61+
// e.g. "<project-root>/build/generated/sources/valkyrie/main"
62+
val outputDirectory = outputDirectory.get().asFile
63+
outputDirectory.deleteRecursively() // make sure nothing is left over from previous run
64+
outputDirectory.mkdirs()
65+
66+
val generatedFiles = arrayListOf<Path>()
67+
var fileIndex = 0
68+
69+
val useFlatPackage = useFlatPackage.get()
70+
val nestedPackName = nestedPackName.getOrElse("")
71+
val config = ImageVectorGeneratorConfig(
72+
packageName = packageName,
73+
iconPackPackage = packageName,
74+
packName = iconPackName.getOrElse(""),
75+
nestedPackName = nestedPackName,
76+
outputFormat = outputFormat.get(),
77+
useComposeColors = useComposeColors.get(),
78+
generatePreview = generatePreview.get(),
79+
previewAnnotationType = previewAnnotationType.get(),
80+
useFlatPackage = useFlatPackage,
81+
useExplicitMode = useExplicitMode.get(),
82+
addTrailingComma = addTrailingComma.get(),
83+
indentSize = indentSize.get(),
84+
)
85+
86+
(svgFiles + drawableFiles).files.forEach { file ->
87+
val parseOutput = SvgXmlParser.toIrImageVector(ParserType.Jvm, Path(file.absolutePath))
88+
val vectorSpecOutput = ImageVectorGenerator.convert(
89+
vector = parseOutput.irImageVector,
90+
iconName = parseOutput.iconName,
91+
config = config,
92+
)
93+
94+
val path = vectorSpecOutput.content.writeToKt(
95+
outputDir = when {
96+
useFlatPackage -> outputDirectory
97+
else -> outputDirectory.resolve(nestedPackName.lowercase())
98+
}.absolutePath,
99+
nameWithoutExtension = vectorSpecOutput.name,
100+
)
101+
generatedFiles.add(path.toIOPath())
102+
fileIndex++
103+
logger.info("File $fileIndex = $path")
104+
}
105+
106+
logger.lifecycle("Generated ${generatedFiles.size} ImageVectors in package $packageName")
107+
}
108+
109+
internal companion object {
110+
internal const val TASK_NAME = "generateImageVectors"
111+
}
112+
}

0 commit comments

Comments
 (0)