From d220d528ded19cc4b88c97d28704ac37aa965e9e Mon Sep 17 00:00:00 2001 From: Justin Brooks Date: Thu, 3 Oct 2024 22:22:36 -0400 Subject: [PATCH] Start decoupling AGP --- settings.gradle.kts | 1 + vgo-cli/build.gradle.kts | 106 ++++++++++++++++++ .../kotlin/com/jzbrooks/vgo/cli/ArgReader.kt | 84 ++++++++++++++ .../main/kotlin/com/jzbrooks/vgo/cli/main.kt | 38 +++++++ .../vgo/core/optimization/MergePaths.kt | 3 +- vgo/build.gradle.kts | 90 +-------------- .../kotlin/com/jzbrooks/vgo/Application.kt | 62 +++++----- .../jzbrooks/vgo => test/kotlin}/ArgReader.kt | 2 - 8 files changed, 258 insertions(+), 128 deletions(-) create mode 100644 vgo-cli/build.gradle.kts create mode 100644 vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/ArgReader.kt create mode 100644 vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/main.kt rename vgo/src/{main/kotlin/com/jzbrooks/vgo => test/kotlin}/ArgReader.kt (98%) diff --git a/settings.gradle.kts b/settings.gradle.kts index 14521e78..70cdefe4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,3 +14,4 @@ dependencyResolutionManagement { } include("vgo-core", "vgo", "vgo-plugin") +include("vgo-cli") diff --git a/vgo-cli/build.gradle.kts b/vgo-cli/build.gradle.kts new file mode 100644 index 00000000..24d38e90 --- /dev/null +++ b/vgo-cli/build.gradle.kts @@ -0,0 +1,106 @@ +plugins { + id("org.jetbrains.kotlin.jvm") + id("com.vanniktech.maven.publish") +} + +tasks { + jar { + dependsOn(configurations.runtimeClasspath) + manifest { + attributes["Main-Class"] = "com.jzbrooks.vgo.cli.MainKt" + attributes["Bundle-Version"] = project.properties["VERSION_NAME"] + } + + val sourceClasses = sourceSets.main.get().output.classesDirs + inputs.files(sourceClasses) + destinationDirectory.set(layout.buildDirectory.dir("libs/debug")) + + doFirst { + from(files(sourceClasses)) + from(configurations.runtimeClasspath.get().asFileTree.files.map(::zipTree)) + + exclude( + "**/*.kotlin_metadata", + "**/*.kotlin_module", + "**/*.kotlin_builtins", + "**/module-info.class", + "META-INF/maven/**", + "META-INF/*.version", + "META-INF/LICENSE*", + "META-INF/LGPL2.1", + "META-INF/DEPENDENCIES", + "META-INF/AL2.0", + "META-INF/BCKEY.DSA", + "META-INF/BC2048KE.DSA", + "META-INF/BCKEY.SF", + "META-INF/BC2048KE.SF", + "**/NOTICE*", + "javax/activation/**", + "xsd/catalog.xml", + ) + } + } + + + val optimize by registering(JavaExec::class) { + description = "Runs proguard on the jar application." + group = "build" + + inputs.file("build/libs/debug/vgo.jar") + outputs.file("build/libs/vgo.jar") + + val javaHome = System.getProperty("java.home") + + classpath(r8) + mainClass = "com.android.tools.r8.R8" + + args( + "--release", + "--classfile", + "--lib", + javaHome, + "--output", + "build/libs/vgo.jar", + "--pg-conf", + "$rootDir/optimize.pro", + "build/libs/debug/vgo.jar", + ) + + dependsOn(getByName("jar")) + } + + val binaryFileProp = layout.buildDirectory.file("libs/vgo") + val binary by registering { + description = "Prepends shell script in the jar to improve CLI" + group = "build" + + dependsOn(optimize) + + inputs.file("build/libs/vgo.jar") + outputs.file(binaryFileProp) + + doLast { + val binaryFile = binaryFileProp.get().asFile + binaryFile.parentFile.mkdirs() + binaryFile.delete() + binaryFile.appendText("#!/bin/sh\n\nexec java \$JAVA_OPTS -jar \$0 \"\$@\"\n\n") + file("build/libs/vgo.jar").inputStream().use { binaryFile.appendBytes(it.readBytes()) } + binaryFile.setExecutable(true, false) + } + } +} + +val r8: Configuration by configurations.creating + +dependencies { + implementation(project(":vgo")) + + implementation("com.android.tools:sdk-common:31.7.0") + + r8("com.android.tools:r8:8.5.35") + + testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.28.1") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.0") + testImplementation("org.junit.jupiter:junit-jupiter-params") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") +} diff --git a/vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/ArgReader.kt b/vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/ArgReader.kt new file mode 100644 index 00000000..182b9277 --- /dev/null +++ b/vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/ArgReader.kt @@ -0,0 +1,84 @@ +package com.jzbrooks.vgo.cli + +import kotlin.collections.any +import kotlin.collections.first +import kotlin.collections.firstOrNull +import kotlin.collections.getOrElse +import kotlin.collections.indexOfFirst +import kotlin.collections.isNotEmpty +import kotlin.collections.map +import kotlin.text.isNotBlank +import kotlin.text.split + +class ArgReader(private val args: MutableList) { + private val hasArguments + get() = args.isNotEmpty() + + fun readFlag(name: String): Boolean { + require(name.isNotBlank()) + + val names = name.split('|') + if (names.size > 1) { + return names.any(::readFlag) + } + + val index = args.indexOfFirst { isOptionArgument(name, it) } + if (index == -1) return false + + args.removeAt(index) + return true + } + + fun readOption(name: String): String? { + require(name.isNotBlank()) + + val names = name.split('|') + if (names.size > 1) { + return names.map(::readOption).firstOrNull { it != null } + } + + val index = args.indexOfFirst { isOptionArgument(name, it) } + if (index == -1) return null + + val value = + args.getOrElse(index + 1) { + throw IllegalStateException("Missing value after ${if (name.length == 1) "-" else "--"}$name") + } + + args.removeAt(index) + args.removeAt(index) + return value + } + + fun readArguments(): List { + val arguments = kotlin.collections.mutableListOf() + while (hasArguments) { + arguments.add(readArgument()) + } + return arguments + } + + private fun readArgument(): String { + val value = args.first() + + check(!isOption(value)) { "Unexpected option $value" } + + args.removeAt(0) + return value + } + + companion object { + private fun isOption(name: String) = name.length >= 2 && name[0] == '-' + + private fun isOptionArgument( + name: String, + argument: String, + ): Boolean { + return if (name.length == 1) { + "-$name" == argument + } else { + "--$name" == argument + } + } + } +} diff --git a/vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/main.kt b/vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/main.kt new file mode 100644 index 00000000..50b5b848 --- /dev/null +++ b/vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/main.kt @@ -0,0 +1,38 @@ +import com.jzbrooks.vgo.Application +import com.jzbrooks.vgo.cli.ArgReader +import kotlin.system.exitProcess + +fun main(args: Array) { + val argReader = ArgReader(args.toMutableList()) + + val printHelp = argReader.readFlag("help|h") + val printVersion = argReader.readFlag("version|v") + val printStats = argReader.readFlag("stats|s") + val indent = argReader.readOption("indent")?.toIntOrNull() + + val outputs = run { + val outputPaths = mutableListOf() + var output = argReader.readOption("output|o") + while (output != null) { + outputPaths.add(output) + output = argReader.readOption("output|o") + } + outputPaths.toList() + } + + var format = argReader.readOption("format") + + var inputs = argReader.readArguments() + + val options = Application.Options( + printHelp = printHelp, + printVersion = printVersion, + printStats = printStats, + indent = indent, + output = outputs, + format = format, + input = inputs + ) + + exitProcess(Application(options).run()) +} \ No newline at end of file diff --git a/vgo-core/src/main/kotlin/com/jzbrooks/vgo/core/optimization/MergePaths.kt b/vgo-core/src/main/kotlin/com/jzbrooks/vgo/core/optimization/MergePaths.kt index 999dab10..4c2d1d44 100644 --- a/vgo-core/src/main/kotlin/com/jzbrooks/vgo/core/optimization/MergePaths.kt +++ b/vgo-core/src/main/kotlin/com/jzbrooks/vgo/core/optimization/MergePaths.kt @@ -7,6 +7,7 @@ import com.jzbrooks.vgo.core.graphic.Extra import com.jzbrooks.vgo.core.graphic.Graphic import com.jzbrooks.vgo.core.graphic.Group import com.jzbrooks.vgo.core.graphic.Path +import com.jzbrooks.vgo.core.util.math.computeAbsoluteCoordinates /** * Merges multiple paths into a single path where possible @@ -64,7 +65,7 @@ class MergePaths : BottomUpOptimization { // // There might be a reasonable way to deduce that situation more // specifically, which could enable merging of some even odd paths. - if (!haveSameAttributes(current, previous) || current.fillRule == Path.FillRule.EVEN_ODD) { + if (!haveSameAttributes(current, previous)) { mergedPaths.add(current) } else { previous.commands += current.commands diff --git a/vgo/build.gradle.kts b/vgo/build.gradle.kts index a291569c..df9433fc 100644 --- a/vgo/build.gradle.kts +++ b/vgo/build.gradle.kts @@ -11,18 +11,16 @@ plugins { kotlin.sourceSets.getByName("main").kotlin.srcDir("src/generated/kotlin") -val r8: Configuration by configurations.creating - dependencies { implementation(project(":vgo-core")) - implementation("com.android.tools:sdk-common:31.7.0") + + compileOnly("com.android.tools:sdk-common:31.7.0") testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.28.1") testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.1") testImplementation("org.junit.jupiter:junit-jupiter-params") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") - r8("com.android.tools:r8:8.5.35") } tasks { @@ -30,43 +28,6 @@ tasks { dependsOn("generateConstants") } - jar { - dependsOn(configurations.runtimeClasspath) - manifest { - attributes["Main-Class"] = "com.jzbrooks.vgo.Application" - attributes["Bundle-Version"] = project.properties["VERSION_NAME"] - } - - val sourceClasses = sourceSets.main.get().output.classesDirs - inputs.files(sourceClasses) - destinationDirectory.set(layout.buildDirectory.dir("libs/debug")) - - doFirst { - from(files(sourceClasses)) - from(configurations.runtimeClasspath.get().asFileTree.files.map(::zipTree)) - - exclude( - "**/*.kotlin_metadata", - "**/*.kotlin_module", - "**/*.kotlin_builtins", - "**/module-info.class", - "META-INF/maven/**", - "META-INF/*.version", - "META-INF/LICENSE*", - "META-INF/LGPL2.1", - "META-INF/DEPENDENCIES", - "META-INF/AL2.0", - "META-INF/BCKEY.DSA", - "META-INF/BC2048KE.DSA", - "META-INF/BCKEY.SF", - "META-INF/BC2048KE.SF", - "**/NOTICE*", - "javax/activation/**", - "xsd/catalog.xml", - ) - } - } - val generateConstants by registering { finalizedBy("compileKotlin") @@ -115,53 +76,6 @@ tasks { mustRunAfter(generateConstants) } - val optimize by registering(JavaExec::class) { - description = "Runs proguard on the jar application." - group = "build" - - inputs.file("build/libs/debug/vgo.jar") - outputs.file("build/libs/vgo.jar") - - val javaHome = System.getProperty("java.home") - - classpath(r8) - mainClass = "com.android.tools.r8.R8" - - args( - "--release", - "--classfile", - "--lib", - javaHome, - "--output", - "build/libs/vgo.jar", - "--pg-conf", - "$rootDir/optimize.pro", - "build/libs/debug/vgo.jar", - ) - - dependsOn(getByName("jar")) - } - - val binaryFileProp = layout.buildDirectory.file("libs/vgo") - val binary by registering { - description = "Prepends shell script in the jar to improve CLI" - group = "build" - - dependsOn(optimize) - - inputs.file("build/libs/vgo.jar") - outputs.file(binaryFileProp) - - doLast { - val binaryFile = binaryFileProp.get().asFile - binaryFile.parentFile.mkdirs() - binaryFile.delete() - binaryFile.appendText("#!/bin/sh\n\nexec java \$JAVA_OPTS -jar \$0 \"\$@\"\n\n") - file("build/libs/vgo.jar").inputStream().use { binaryFile.appendBytes(it.readBytes()) } - binaryFile.setExecutable(true, false) - } - } - val updateBaselineOptimizations by registering(Copy::class) { description = "Updates baseline assets with the latest integration test outputs." group = "Build Setup" diff --git a/vgo/src/main/kotlin/com/jzbrooks/vgo/Application.kt b/vgo/src/main/kotlin/com/jzbrooks/vgo/Application.kt index 4ffc8cb8..d5a131ac 100644 --- a/vgo/src/main/kotlin/com/jzbrooks/vgo/Application.kt +++ b/vgo/src/main/kotlin/com/jzbrooks/vgo/Application.kt @@ -19,51 +19,32 @@ import java.io.File import javax.xml.parsers.DocumentBuilderFactory import kotlin.math.absoluteValue import kotlin.math.roundToInt -import kotlin.system.exitProcess -class Application { - private var printStats = false +class Application(private val options: Options) { private var printFileNames = false private var totalBytesBefore = 0.0 private var totalBytesAfter = 0.0 - private var outputFormat: String? = null - fun run(args: Array): Int { - val argReader = ArgReader(args.toMutableList()) + fun run(): Int { - if (argReader.readFlag("help|h")) { + if (options.printHelp) { println(HELP_MESSAGE) return 0 } - if (argReader.readFlag("version|v")) { + if (options.printVersion) { println(BuildConstants.VERSION_NAME) return 0 } val writerOptions = mutableSetOf() - argReader.readOption("indent")?.toIntOrNull()?.let { indentColumns -> + options.indent?.let { indentColumns -> writerOptions.add(Writer.Option.Indent(indentColumns)) } - printStats = argReader.readFlag("stats|s") - - val outputs = - run { - val outputPaths = mutableListOf() - var output = argReader.readOption("output|o") - while (output != null) { - outputPaths.add(output) - output = argReader.readOption("output|o") - } - outputPaths.toList() - } - - outputFormat = argReader.readOption("format") - - var inputs = argReader.readArguments() + var inputs = options.input if (inputs.isEmpty()) { - require(outputs.isEmpty()) + require(options.output.isEmpty()) var path = readlnOrNull() val standardInPaths = mutableListOf() @@ -76,8 +57,8 @@ class Application { } val inputOutputMap = - if (outputs.isNotEmpty()) { - inputs.zip(outputs) { a, b -> + if (options.output.isNotEmpty()) { + inputs.zip(options.output) { a, b -> Pair(File(a), File(b)) } } else { @@ -88,7 +69,7 @@ class Application { val files = inputOutputMap.count { (input, _) -> input.isFile } val containsDirectory = inputOutputMap.any { (input, _) -> input.isDirectory } - printFileNames = printStats && (files > 1 || containsDirectory) + printFileNames = options.printStats && (files > 1 || containsDirectory) return handleFiles(inputOutputMap, writerOptions) } @@ -151,7 +132,7 @@ class Application { var graphic = when { rootNodes.any { it.nodeName == "svg" || input.extension == "svg" } -> { - if (outputFormat == "vd") { + if (this.options.format == "vd") { ByteArrayOutputStream().use { pipeOrigin -> val errors = Svg2Vector.parseSvgToXml(input.toPath(), pipeOrigin) if (errors != "") { @@ -187,7 +168,7 @@ class Application { else -> if (input == output) return else null } - if (graphic is VectorDrawable && outputFormat == "svg") { + if (graphic is VectorDrawable && this.options.format == "svg") { graphic = graphic.toSvg() } @@ -220,7 +201,7 @@ class Application { inputStream.copyTo(outputStream) } - if (printStats) { + if (this.options.printStats) { val sizeAfter = outputStream.channel.size() val percentSaved = ((sizeBefore - sizeAfter) / sizeBefore.toDouble()) * 100 totalBytesBefore += sizeBefore @@ -249,12 +230,12 @@ class Application { handleFile(file, File(output, file.name), options) } - if (printStats) { + if (this.options.printStats) { val message = "| Total bytes saved: ${(totalBytesBefore - totalBytesAfter).roundToInt()} |" val border = "-".repeat(message.length) println( """ - + $border $message $border @@ -290,6 +271,16 @@ class Application { private val Map.Entry.isDirectoryPair get() = key.isDirectory && (value.isDirectory || !value.exists()) + data class Options( + val printHelp: Boolean = false, + val printVersion: Boolean = false, + val printStats: Boolean = false, + val output: List = emptyList(), + val input: List = emptyList(), + val indent: Int? = null, + val format: String? = null, + ) + companion object { private const val HELP_MESSAGE = """ > vgo [options] [file/directory] @@ -302,8 +293,5 @@ Options: --indent value write files with value columns of indentation --format value output format (svg, vd, etc) """ - - @JvmStatic - fun main(args: Array): Unit = exitProcess(Application().run(args)) } } diff --git a/vgo/src/main/kotlin/com/jzbrooks/vgo/ArgReader.kt b/vgo/src/test/kotlin/ArgReader.kt similarity index 98% rename from vgo/src/main/kotlin/com/jzbrooks/vgo/ArgReader.kt rename to vgo/src/test/kotlin/ArgReader.kt index 29d005fc..2a669463 100644 --- a/vgo/src/main/kotlin/com/jzbrooks/vgo/ArgReader.kt +++ b/vgo/src/test/kotlin/ArgReader.kt @@ -1,5 +1,3 @@ -package com.jzbrooks.vgo - class ArgReader(private val args: MutableList) { private val hasArguments get() = args.isNotEmpty()