Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add command line parser and entry points #1885

Merged
merged 2 commits into from
May 13, 2024
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
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ repositories {
}

plugins {
kotlin("jvm") version "1.9.0"
kotlin("jvm") version "1.9.23"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any context on the compiler version update?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumped to be used with the latest KSP.

id("io.github.gradle-nexus.publish-plugin") version "1.1.0"

// Adding plugins used in multiple places to the classpath for centralized version control
Expand Down
8 changes: 8 additions & 0 deletions cmdline-parser-gen/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
plugins {
kotlin("jvm")
}

dependencies {
implementation(kotlin("stdlib"))
implementation(project(":api"))
}
132 changes: 132 additions & 0 deletions cmdline-parser-gen/src/main/kotlin/CmdlineParserGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*
import java.io.OutputStream

private fun OutputStream.appendText(str: String) {
this.write(str.toByteArray())
}

private fun OutputStream.appendLine(str: String = "") {
appendText(str + System.lineSeparator())
}

// modueName => -module-name
private fun String.camelToOptionName(): String = fold(StringBuilder()) { acc, c ->
acc.let {
val lower = c.lowercase()
acc.append(if (acc.isEmpty() || c.isUpperCase()) "-$lower" else lower)
}
}.toString()

class CmdlineParserGenerator(
val codeGenerator: CodeGenerator,
val logger: KSPLogger,
val options: Map<String, String>
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val annotationName = "com.google.devtools.ksp.processing.KSPArgParserGen"
val kspConfigBuilders =
resolver.getSymbolsWithAnnotation(annotationName)

kspConfigBuilders.filterIsInstance<KSClassDeclaration>().forEach { builderClass ->
val parserName = builderClass.annotations.single {
it.annotationType.resolve().declaration.qualifiedName?.asString() == annotationName
}.arguments.single().value as String
val configClass = builderClass.parentDeclaration as KSClassDeclaration
val builderName = "${configClass.simpleName.asString()}.${builderClass.simpleName.asString()}"
codeGenerator.createNewFile(
Dependencies(false, builderClass.containingFile!!),
builderClass.packageName.asString(),
parserName
).use { os ->
os.appendLine("package ${builderClass.packageName.asString()}")
os.appendLine()
os.appendLine(
"fun $parserName(args: Array<String>): Pair<${configClass.simpleName.asString()}, List<String>> {"
)
os.appendLine(" val processorClasspath = mutableListOf<String>()")
os.appendLine(" return Pair($builderName().apply {")
os.appendLine(" var i = 0")
os.appendLine(" while (i < args.size) {")
os.appendLine(" val arg = args[i++]")
os.appendLine(" when {")

builderClass.getAllProperties().filter { it.setter != null }.forEach { prop ->
val type = prop.type.resolve()
val typeName = type.declaration.simpleName.asString()
val propName = prop.simpleName.asString()
val optionName = propName.camelToOptionName()
val optionNameLen = optionName.length
when (typeName) {
"String", "Boolean", "File" -> {
os.appendLine(
" arg == \"$optionName\" -> " +
"$propName = parse$typeName(getArg(args, i++))"
)
os.appendLine(
" arg.startsWith(\"$optionName=\") -> " +
"$propName = parse$typeName(arg.substring(${optionNameLen + 1}))"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

substringAfter?

)
}
"List", "Map" -> {
val elementTypeName =
type.arguments.last().type!!.resolve().declaration.simpleName.asString()
os.appendLine(
" arg == \"$optionName\" -> " +
"$propName = parse$typeName(getArg(args, i++), ::parse$elementTypeName)"
)
os.appendLine(
" arg.startsWith(\"$optionName=\") -> " +
"$propName = parse$typeName(arg.substring(${optionNameLen + 1}), " +
"::parse$elementTypeName)"
)
}
else -> {
throw IllegalArgumentException("Unknown type of option `$propName: ${prop.type}`")
}
}
}

// Free args are processor classpath
os.appendLine(" else -> {")
os.appendLine(" processorClasspath.addAll(parseList(arg, ::parseString))")
os.appendLine(" }")
os.appendLine(" }")
os.appendLine(" }")
os.appendLine(" }.build(), processorClasspath)")
os.appendLine("}")
}

codeGenerator.createNewFile(
Dependencies(false, builderClass.containingFile!!),
builderClass.packageName.asString(),
parserName + "Help"
).use { os ->
os.appendLine("package ${builderClass.packageName.asString()}")
os.appendLine()
os.appendLine(
"fun ${parserName}Help(): String = \"\"\""
)
builderClass.getAllProperties().filter { it.setter != null }.forEach { prop ->
val type = prop.type.resolve()
val typeName = type.toString()
val propName = prop.simpleName.asString()
val optionName = propName.camelToOptionName()
val prefix = if (Modifier.LATEINIT in prop.modifiers) "*" else " "
os.appendLine("$prefix $optionName=$typeName")
}
os.appendLine("* <processor classpath>")
os.appendLine("\"\"\"")
}
}
return emptyList()
}
}

class CmdlineParserGeneratorProvider : SymbolProcessorProvider {
override fun create(
environment: SymbolProcessorEnvironment
): SymbolProcessor {
return CmdlineParserGenerator(environment.codeGenerator, environment.logger, environment.options)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CmdlineParserGeneratorProvider
4 changes: 4 additions & 0 deletions common-deps/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ plugins {
`maven-publish`
signing
id("org.jetbrains.dokka")
id("com.google.devtools.ksp") version "1.9.23-1.0.20"
}

dependencies {
compileOnly(project(":api"))
testImplementation("junit:junit:$junitVersion")

ksp(project(":cmdline-parser-gen"))
}

tasks {
Expand Down
52 changes: 52 additions & 0 deletions common-deps/src/main/kotlin/com/google/devtools/ksp/KSPConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.google.devtools.ksp.processing
import java.io.File
import java.io.Serializable

private annotation class KSPArgParserGen(val name: String)

abstract class KSPConfig(
val moduleName: String,
val sourceRoots: List<File>,
Expand Down Expand Up @@ -121,6 +123,7 @@ class KSPJvmConfig(
allWarningsAsErrors,
mapAnnotationArgumentsInJava,
) {
@KSPArgParserGen(name = "kspJvmArgParser")
class Builder : KSPConfig.Builder(), Serializable {
var javaSourceRoots: List<File> = emptyList()
lateinit var javaOutputDir: File
Expand Down Expand Up @@ -222,6 +225,7 @@ class KSPNativeConfig(
allWarningsAsErrors,
mapAnnotationArgumentsInJava,
) {
@KSPArgParserGen(name = "kspNativeArgParser")
class Builder : KSPConfig.Builder(), Serializable {
lateinit var target: String

Expand Down Expand Up @@ -314,6 +318,7 @@ class KSPJsConfig(
allWarningsAsErrors,
mapAnnotationArgumentsInJava,
) {
@KSPArgParserGen(name = "kspJsArgParser")
class Builder : KSPConfig.Builder(), Serializable {
lateinit var backend: String

Expand Down Expand Up @@ -411,6 +416,7 @@ class KSPCommonConfig(
allWarningsAsErrors,
mapAnnotationArgumentsInJava,
) {
@KSPArgParserGen(name = "kspCommonArgParser")
class Builder : KSPConfig.Builder(), Serializable {
lateinit var targets: List<Target>

Expand Down Expand Up @@ -446,3 +452,49 @@ class KSPCommonConfig(
}
}
}

fun parseString(arg: String): String {
if (arg.length > 0 && arg[0] == '-')
throw IllegalArgumentException("expecting a String arguemnt but got $arg")
return arg
}

fun parseBoolean(arg: String): Boolean {
if (arg.length > 0 && arg[0] == '-')
throw IllegalArgumentException("expecting a Boolean arguemnt but got $arg")
return arg.toBoolean()
}

fun parseFile(arg: String): File {
if (arg.length > 0 && arg[0] == '-')
throw IllegalArgumentException("expecting a File arguemnt but got $arg")
// FIXME: AA isn't happy relative paths for source roots.
return File(arg).absoluteFile
}

fun <T> parseList(arg: String, transform: (String) -> T): List<T> {
if (arg.length > 0 && arg[0] == '-')
throw IllegalArgumentException("expecting a List but got $arg")
return arg.split(':').map { transform(it) }
}

fun <T> parseMap(arg: String, transform: (String) -> T): Map<String, T> {
if (arg.length > 0 && arg[0] == '-')
throw IllegalArgumentException("expecting a Map but got $arg")
return arg.split(':').map {
val (k, v) = it.split('=')
k to transform(v)
}.toMap()
}

fun parseTarget(arg: String): Target {
if (arg.length > 0 && arg[0] == '-')
throw IllegalArgumentException("expecting a target but got $arg")
return Target(arg, emptyMap())
}

fun getArg(args: Array<String>, i: Int): String {
if (i >= args.size || args[i].startsWith("-"))
throw IllegalArgumentException("Expecting an argument")
return args[i]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.google.devtools.ksp

import com.google.devtools.ksp.processing.kspJvmArgParser
import com.google.devtools.ksp.processing.kspJvmArgParserHelp
import org.junit.Assert
import org.junit.Test
import java.io.File

class CommandLineArgParserTest {
@Test
fun testJvm() {
val args = arrayListOf<String>(
"-module-name=MyModule",
"-source-roots", "/path/to/A:/path/to/B",
"/path/to/processorA.jar",
"-kotlin-output-dir=/path/to/output/kotlin",
"-java-output-dir=/path/to/output/java",
"-class-output-dir=/path/to/output/class",
"-resource-output-dir=/path/to/output/resource",
"-language-version=2.0",
"-api-version=2.0",
"-jvm-target", "21",
"-project-base-dir", "/path/to/base",
"-output-base-dir", "/path/to/output",
"-caches-dir", "/path/to/caches",
"/path/to/processorB.jar:rel/to/processorC.jar",
).toTypedArray()
val (config, classpath) = kspJvmArgParser(args)
Assert.assertEquals(
listOf("/path/to/A", "/path/to/B").map(::File),
config.sourceRoots
)
Assert.assertEquals(
"MyModule",
config.moduleName
)
Assert.assertEquals(
listOf("/path/to/processorA.jar", "/path/to/processorB.jar", "rel/to/processorC.jar"),
classpath
)
}

@Test
fun testJvmHelp() {
val helpMsg = kspJvmArgParserHelp()
Assert.assertTrue("* -java-output-dir=File" in helpMsg)
Assert.assertTrue(" -libraries=List<File>" in helpMsg)
Assert.assertTrue("* <processor classpath>" in helpMsg)
println(helpMsg)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.google.devtools.ksp.cmdline

import com.google.devtools.ksp.processing.kspCommonArgParser
import com.google.devtools.ksp.processing.kspCommonArgParserHelp

class KSPCommonMain {
companion object {
@JvmStatic
fun main(args: Array<String>) {
if ("-h" in args || "--help" in args) {
printHelpMsg(kspCommonArgParserHelp())
} else {
runWithArgs(args, ::kspCommonArgParser)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.google.devtools.ksp.cmdline

import com.google.devtools.ksp.processing.kspJsArgParser
import com.google.devtools.ksp.processing.kspJsArgParserHelp

class KSPJsMain {
companion object {
@JvmStatic
fun main(args: Array<String>) {
if ("-h" in args || "--help" in args) {
printHelpMsg(kspJsArgParserHelp())
} else {
runWithArgs(args, ::kspJsArgParser)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.google.devtools.ksp.cmdline

import com.google.devtools.ksp.impl.KotlinSymbolProcessing
import com.google.devtools.ksp.processing.KSPConfig
import com.google.devtools.ksp.processing.KspGradleLogger
import com.google.devtools.ksp.processing.SymbolProcessorProvider
import com.google.devtools.ksp.processing.kspJvmArgParser
import com.google.devtools.ksp.processing.kspJvmArgParserHelp
import java.io.File
import java.net.URLClassLoader
import java.util.ServiceLoader

class KSPJvmMain {
companion object {
@JvmStatic
fun main(args: Array<String>) {
if ("-h" in args || "--help" in args) {
printHelpMsg(kspJvmArgParserHelp())
} else {
runWithArgs(args, ::kspJvmArgParser)
}
}
}
}

internal fun printHelpMsg(optionsList: String) {
println("Available options:")
println(optionsList)
println("where:")
println(" * is required")
println(" List is colon separated. E.g., arg1:arg2:arg3")
println(" Map is in the form key1=value1:key2=value2")
}

internal fun runWithArgs(args: Array<String>, parse: (Array<String>) -> Pair<KSPConfig, List<String>>) {
val logger = KspGradleLogger(KspGradleLogger.LOGGING_LEVEL_WARN)
val (config, classpath) = parse(args)
val processorClassloader = URLClassLoader(classpath.map { File(it).toURI().toURL() }.toTypedArray())

val processorProviders = ServiceLoader.load(
processorClassloader.loadClass("com.google.devtools.ksp.processing.SymbolProcessorProvider"),
processorClassloader
).toList() as List<SymbolProcessorProvider>

KotlinSymbolProcessing(config, processorProviders, logger).execute()
}
Loading
Loading