Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ needs.

- IntelliJ IDEA / Android Studio plugin
- CLI tool
- Gradle plugin and Web app (🚧 coming soon 🚧)
- Gradle plugin
- Web app (🚧 coming soon 🚧)

## IDEA Plugin

Expand Down
1 change: 1 addition & 0 deletions components/test/coverage/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins {
dependencies {
// include only necessary dependencies for the test coverage
kover(projects.tools.cli)
kover(projects.tools.gradlePlugin)
kover(projects.components.generator.core)
kover(projects.components.generator.jvm.poetExtensions)
kover(projects.components.generator.iconpack)
Expand Down
5 changes: 5 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[versions]
agp = "8.13.0"
compose = "1.8.2"
intellij = "2.9.0"
jacoco = "0.8.13"
Expand All @@ -8,6 +9,7 @@ leviathan = "3.1.0-1.8.2"

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

agp-api = { module = "com.android.tools.build:gradle-api", version.ref = "agp" }
agp-full = { module = "com.android.tools.build:gradle", version.ref = "agp" }

# Dependencies for build-logic module
kotlin-compose-compiler-plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" }
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
Expand Down
4 changes: 2 additions & 2 deletions sdk/core/extensions/api/extensions.api
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
public final class io/github/composegears/valkyrie/sdk/core/extensions/PathUtilsKt {
public static final fun writeToKt (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZ)V
public static synthetic fun writeToKt$default (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZILjava/lang/Object;)V
public static final fun writeToKt (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZ)Ljava/nio/file/Path;
public static synthetic fun writeToKt$default (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZILjava/lang/Object;)Ljava/nio/file/Path;
}

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.composegears.valkyrie.sdk.core.extensions

import java.io.IOException
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.createParentDirectories
import kotlin.io.path.deleteIfExists
Expand All @@ -27,7 +28,7 @@ private fun String.writeToFile(
extension: String,
deleteIfExists: Boolean,
createParents: Boolean,
) {
): Path {
val outputPath = Path(outputDir, "$nameWithoutExtension.$extension")

if (deleteIfExists) {
Expand All @@ -37,4 +38,5 @@ private fun String.writeToFile(
outputPath.createParentDirectories()
}
outputPath.writeText(this)
return outputPath
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ includeBuild("build-logic")

include("tools:cli")
include("tools:compose-app")
include("tools:gradle-plugin")
include("tools:idea-plugin")

include("components:generator:core")
Expand Down
41 changes: 41 additions & 0 deletions tools/gradle-plugin/api/gradle-plugin.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
public abstract class io/github/composegears/valkyrie/gradle/GenerateImageVectorsTask : org/gradle/api/DefaultTask {
public fun <init> ()V
public final fun execute ()V
public abstract fun getAddTrailingComma ()Lorg/gradle/api/provider/Property;
public abstract fun getDrawableFiles ()Lorg/gradle/api/file/ConfigurableFileCollection;
public abstract fun getGeneratePreview ()Lorg/gradle/api/provider/Property;
public abstract fun getIconPackName ()Lorg/gradle/api/provider/Property;
public abstract fun getIndentSize ()Lorg/gradle/api/provider/Property;
public abstract fun getNestedPackName ()Lorg/gradle/api/provider/Property;
public abstract fun getOutputDirectory ()Lorg/gradle/api/file/DirectoryProperty;
public abstract fun getOutputFormat ()Lorg/gradle/api/provider/Property;
public abstract fun getPackageName ()Lorg/gradle/api/provider/Property;
public abstract fun getPreviewAnnotationType ()Lorg/gradle/api/provider/Property;
public abstract fun getSvgFiles ()Lorg/gradle/api/file/ConfigurableFileCollection;
public abstract fun getUseComposeColors ()Lorg/gradle/api/provider/Property;
public abstract fun getUseExplicitMode ()Lorg/gradle/api/provider/Property;
public abstract fun getUseFlatPackage ()Lorg/gradle/api/provider/Property;
}

public abstract interface class io/github/composegears/valkyrie/gradle/ValkyrieExtension {
public abstract fun getAddTrailingComma ()Lorg/gradle/api/provider/Property;
public abstract fun getGenerateAtSync ()Lorg/gradle/api/provider/Property;
public abstract fun getGeneratePreview ()Lorg/gradle/api/provider/Property;
public abstract fun getIconPackName ()Lorg/gradle/api/provider/Property;
public abstract fun getIndentSize ()Lorg/gradle/api/provider/Property;
public abstract fun getNestedPackName ()Lorg/gradle/api/provider/Property;
public abstract fun getOutputDirectory ()Lorg/gradle/api/file/DirectoryProperty;
public abstract fun getOutputFormat ()Lorg/gradle/api/provider/Property;
public abstract fun getPackageName ()Lorg/gradle/api/provider/Property;
public abstract fun getPreviewAnnotationType ()Lorg/gradle/api/provider/Property;
public abstract fun getUseComposeColors ()Lorg/gradle/api/provider/Property;
public abstract fun getUseExplicitMode ()Lorg/gradle/api/provider/Property;
public abstract fun getUseFlatPackage ()Lorg/gradle/api/provider/Property;
}

public final class io/github/composegears/valkyrie/gradle/ValkyrieGradlePlugin : org/gradle/api/Plugin {
public fun <init> ()V
public synthetic fun apply (Ljava/lang/Object;)V
public fun apply (Lorg/gradle/api/Project;)V
}

109 changes: 109 additions & 0 deletions tools/gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import java.nio.file.Paths
import java.util.Properties
import kotlin.io.path.exists

plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.valkyrie.kover)
alias(libs.plugins.valkyrie.abi)
alias(libs.plugins.buildConfig)
`java-gradle-plugin`
}

tasks.validatePlugins {
// TODO: https://github.com/gradle/gradle/issues/22600
enableStricterValidation = true
}

gradlePlugin {
vcsUrl = "https://github.com/ComposeGears/Valkyrie"
website = "https://github.com/ComposeGears/Valkyrie"

plugins {
create("valkyrie") {
id = "io.github.composegears.valkyrie"
displayName = name
implementationClass = "io.github.composegears.valkyrie.gradle.ValkyrieGradlePlugin"
description = "Generates Kotlin accessors for ImageVectors, based on input SVG files"
tags.addAll("kotlin", "svg", "xml", "imagevector", "valkyrie")
}
}
}

val sharedTestResourcesDir: File =
project(projects.components.test.path)
.layout
.projectDirectory
.dir("sharedTestResources/imagevector")
.asFile

buildConfig.sourceSets.getByName("test") {
packageName = "io.github.composegears.valkyrie.gradle"
useKotlinOutput { topLevelConstants = true }

// So we can copy the shared test SVG/XML files into our test cases
buildConfigField("RESOURCES_DIR_SVG", sharedTestResourcesDir.resolve("svg"))
buildConfigField("RESOURCES_DIR_XML", sharedTestResourcesDir.resolve("xml"))
buildConfigField<String?>("ANDROID_HOME", androidHome())
buildConfigField("COMPOSE_UI", libs.compose.ui.get().toString())

// TODO: Set up tests to run for different gradle versions?
buildConfigField("GRADLE_VERSION", GradleVersion.current().version)
}

// Adapted from https://github.com/GradleUp/shadow/blob/1d7b0863fed3126bf376f11d563e9176de176cd3/build.gradle.kts#L63-L65
// Allows gradle test cases to use the same classpath as the parent build - meaning we don't need to specify versions
// when loading plugins into test projects.
val testPluginClasspath by configurations.registering {
isCanBeResolved = true
}

tasks.pluginUnderTestMetadata {
// Plugins used in tests could be resolved in classpath.
pluginClasspath.from(testPluginClasspath)
}

dependencies {
compileOnly(libs.agp.api)
compileOnly(libs.kotlin.gradle.plugin)

api(projects.sdk.core.extensions)
api(projects.components.generator.iconpack)
api(projects.components.generator.jvm.imagevector)
api(projects.components.ir)
api(projects.components.parser.unified)

testImplementation(libs.bundles.test)
testRuntimeOnly(libs.junit.launcher)

testPluginClasspath(libs.agp.full)
testPluginClasspath(libs.kotlin.gradle.plugin)
}

fun androidHome(): String? {
val androidSdkRoot = System.getenv("ANDROID_SDK_ROOT")
if (!androidSdkRoot.isNullOrBlank() && Paths.get(androidSdkRoot).exists()) {
logger.info("Using ANDROID_SDK_ROOT=$androidSdkRoot")
return androidSdkRoot
}

val androidHome = System.getenv("ANDROID_HOME")
if (!androidHome.isNullOrBlank() && Paths.get(androidHome).exists()) {
logger.info("Using ANDROID_HOME=$androidHome")
return androidHome
}

val localProps = rootProject.file("local.properties")
if (localProps.exists()) {
val properties = Properties()
localProps.inputStream().use { properties.load(it) }
val sdkHome = properties.getProperty("sdk.dir")?.takeIf { it.isNotBlank() }
if (sdkHome != null && Paths.get(sdkHome).exists()) {
logger.info("Using local.properties sdk.dir $sdkHome")
return sdkHome
}
}

logger.warn("No Android SDK found - Android unit tests will be skipped")
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package io.github.composegears.valkyrie.gradle

import io.github.composegears.valkyrie.generator.jvm.imagevector.ImageVectorGenerator
import io.github.composegears.valkyrie.generator.jvm.imagevector.ImageVectorGeneratorConfig
import io.github.composegears.valkyrie.generator.jvm.imagevector.OutputFormat
import io.github.composegears.valkyrie.generator.jvm.imagevector.PreviewAnnotationType
import io.github.composegears.valkyrie.parser.unified.ParserType
import io.github.composegears.valkyrie.parser.unified.SvgXmlParser
import io.github.composegears.valkyrie.parser.unified.ext.toIOPath
import io.github.composegears.valkyrie.sdk.core.extensions.writeToKt
import kotlinx.io.files.Path
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity.RELATIVE
import org.gradle.api.tasks.TaskAction

@CacheableTask
abstract class GenerateImageVectorsTask : DefaultTask() {
@get:[PathSensitive(RELATIVE) InputFiles] abstract val svgFiles: ConfigurableFileCollection

@get:[PathSensitive(RELATIVE) InputFiles] abstract val drawableFiles: ConfigurableFileCollection

@get:Input abstract val packageName: Property<String>

@get:[Input Optional] abstract val iconPackName: Property<String>

@get:[Input Optional] abstract val nestedPackName: Property<String>

@get:Input abstract val outputFormat: Property<OutputFormat>

@get:Input abstract val useComposeColors: Property<Boolean>

@get:Input abstract val generatePreview: Property<Boolean>

@get:Input abstract val previewAnnotationType: Property<PreviewAnnotationType>

@get:Input abstract val useFlatPackage: Property<Boolean>

@get:Input abstract val useExplicitMode: Property<Boolean>

@get:Input abstract val addTrailingComma: Property<Boolean>

@get:Input abstract val indentSize: Property<Int>

@get:OutputDirectory abstract val outputDirectory: DirectoryProperty

@TaskAction
fun execute() {
val packageName = packageName.orNull
?: throw GradleException("No package name configured for $this")

// e.g. "<project-root>/build/generated/sources/valkyrie/main"
val outputDirectory = outputDirectory.get().asFile
outputDirectory.deleteRecursively() // make sure nothing is left over from previous run
outputDirectory.mkdirs()

val generatedFiles = arrayListOf<Path>()
var fileIndex = 0

val useFlatPackage = useFlatPackage.get()
val nestedPackName = nestedPackName.getOrElse("")
val config = ImageVectorGeneratorConfig(
packageName = packageName,
iconPackPackage = packageName,
packName = iconPackName.getOrElse(""),
nestedPackName = nestedPackName,
outputFormat = outputFormat.get(),
useComposeColors = useComposeColors.get(),
generatePreview = generatePreview.get(),
previewAnnotationType = previewAnnotationType.get(),
useFlatPackage = useFlatPackage,
useExplicitMode = useExplicitMode.get(),
addTrailingComma = addTrailingComma.get(),
indentSize = indentSize.get(),
)

(svgFiles + drawableFiles).files.forEach { file ->
val parseOutput = SvgXmlParser.toIrImageVector(ParserType.Jvm, Path(file.absolutePath))
val vectorSpecOutput = ImageVectorGenerator.convert(
vector = parseOutput.irImageVector,
iconName = parseOutput.iconName,
config = config,
)

val path = vectorSpecOutput.content.writeToKt(
outputDir = when {
useFlatPackage -> outputDirectory
else -> outputDirectory.resolve(nestedPackName.lowercase())
}.absolutePath,
nameWithoutExtension = vectorSpecOutput.name,
)
generatedFiles.add(path.toIOPath())
fileIndex++
logger.info("File $fileIndex = $path")
}

logger.lifecycle("Generated ${generatedFiles.size} ImageVectors in package $packageName")
}

internal companion object {
internal const val TASK_NAME = "generateImageVectors"
}
}
Loading