diff --git a/settings.gradle.kts b/settings.gradle.kts index 14521e78..5f3dad06 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,4 +13,4 @@ dependencyResolutionManagement { } } -include("vgo-core", "vgo", "vgo-plugin") +include("vgo-core", "vgo", "vgo-plugin", "vgo-cli") diff --git a/vgo-cli/build.gradle.kts b/vgo-cli/build.gradle.kts new file mode 100644 index 00000000..c2764d1f --- /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 r8 on the jar application." + group = "build" + + inputs.file("build/libs/debug/vgo-cli.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", + "optimize.pro", + "build/libs/debug/vgo-cli.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/optimize.pro b/vgo-cli/optimize.pro similarity index 80% rename from optimize.pro rename to vgo-cli/optimize.pro index 2e1661c8..c7ae2b86 100644 --- a/optimize.pro +++ b/vgo-cli/optimize.pro @@ -5,6 +5,6 @@ -mergeinterfacesaggressively -verbose --keep class com.jzbrooks.vgo.Application { +-keep class com.jzbrooks.vgo.cli.MainKt { public static void main(java.lang.String[]); } \ No newline at end of file diff --git a/vgo/src/main/kotlin/com/jzbrooks/vgo/ArgReader.kt b/vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/ArgReader.kt similarity index 82% rename from vgo/src/main/kotlin/com/jzbrooks/vgo/ArgReader.kt rename to vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/ArgReader.kt index 29d005fc..182b9277 100644 --- a/vgo/src/main/kotlin/com/jzbrooks/vgo/ArgReader.kt +++ b/vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/ArgReader.kt @@ -1,4 +1,14 @@ -package com.jzbrooks.vgo +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 @@ -41,7 +51,7 @@ class ArgReader(private val args: MutableList) { } fun readArguments(): List { - val arguments = mutableListOf() + val arguments = kotlin.collections.mutableListOf() while (hasArguments) { arguments.add(readArgument()) } 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..92020724 --- /dev/null +++ b/vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/main.kt @@ -0,0 +1,39 @@ +package com.jzbrooks.vgo.cli + +import com.jzbrooks.vgo.Application +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)) } }