diff --git a/build.gradle.kts b/build.gradle.kts index 43ba936dc5..2635729638 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,7 +20,7 @@ repositories { } plugins { - kotlin("jvm") version "1.9.0" + kotlin("jvm") version "1.9.23" id("io.github.gradle-nexus.publish-plugin") version "1.1.0" // Adding plugins used in multiple places to the classpath for centralized version control diff --git a/cmdline-parser-gen/build.gradle.kts b/cmdline-parser-gen/build.gradle.kts new file mode 100644 index 0000000000..65ac567607 --- /dev/null +++ b/cmdline-parser-gen/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + kotlin("jvm") +} + +dependencies { + implementation(kotlin("stdlib")) + implementation(project(":api")) +} diff --git a/cmdline-parser-gen/src/main/kotlin/CmdlineParserGenerator.kt b/cmdline-parser-gen/src/main/kotlin/CmdlineParserGenerator.kt new file mode 100644 index 0000000000..cb4d2b43c5 --- /dev/null +++ b/cmdline-parser-gen/src/main/kotlin/CmdlineParserGenerator.kt @@ -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 +) : SymbolProcessor { + override fun process(resolver: Resolver): List { + val annotationName = "com.google.devtools.ksp.processing.KSPArgParserGen" + val kspConfigBuilders = + resolver.getSymbolsWithAnnotation(annotationName) + + kspConfigBuilders.filterIsInstance().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): Pair<${configClass.simpleName.asString()}, List> {" + ) + os.appendLine(" val processorClasspath = mutableListOf()") + 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}))" + ) + } + "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("* ") + os.appendLine("\"\"\"") + } + } + return emptyList() + } +} + +class CmdlineParserGeneratorProvider : SymbolProcessorProvider { + override fun create( + environment: SymbolProcessorEnvironment + ): SymbolProcessor { + return CmdlineParserGenerator(environment.codeGenerator, environment.logger, environment.options) + } +} diff --git a/cmdline-parser-gen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/cmdline-parser-gen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 0000000000..244b31079a --- /dev/null +++ b/cmdline-parser-gen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +CmdlineParserGeneratorProvider diff --git a/common-deps/build.gradle.kts b/common-deps/build.gradle.kts index b6b457126c..492b9af066 100644 --- a/common-deps/build.gradle.kts +++ b/common-deps/build.gradle.kts @@ -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 { diff --git a/common-deps/src/main/kotlin/com/google/devtools/ksp/KSPConfig.kt b/common-deps/src/main/kotlin/com/google/devtools/ksp/KSPConfig.kt index 53f3968035..cc2ee40959 100644 --- a/common-deps/src/main/kotlin/com/google/devtools/ksp/KSPConfig.kt +++ b/common-deps/src/main/kotlin/com/google/devtools/ksp/KSPConfig.kt @@ -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, @@ -121,6 +123,7 @@ class KSPJvmConfig( allWarningsAsErrors, mapAnnotationArgumentsInJava, ) { + @KSPArgParserGen(name = "kspJvmArgParser") class Builder : KSPConfig.Builder(), Serializable { var javaSourceRoots: List = emptyList() lateinit var javaOutputDir: File @@ -222,6 +225,7 @@ class KSPNativeConfig( allWarningsAsErrors, mapAnnotationArgumentsInJava, ) { + @KSPArgParserGen(name = "kspNativeArgParser") class Builder : KSPConfig.Builder(), Serializable { lateinit var target: String @@ -314,6 +318,7 @@ class KSPJsConfig( allWarningsAsErrors, mapAnnotationArgumentsInJava, ) { + @KSPArgParserGen(name = "kspJsArgParser") class Builder : KSPConfig.Builder(), Serializable { lateinit var backend: String @@ -411,6 +416,7 @@ class KSPCommonConfig( allWarningsAsErrors, mapAnnotationArgumentsInJava, ) { + @KSPArgParserGen(name = "kspCommonArgParser") class Builder : KSPConfig.Builder(), Serializable { lateinit var targets: List @@ -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 parseList(arg: String, transform: (String) -> T): List { + if (arg.length > 0 && arg[0] == '-') + throw IllegalArgumentException("expecting a List but got $arg") + return arg.split(':').map { transform(it) } +} + +fun parseMap(arg: String, transform: (String) -> T): Map { + 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, i: Int): String { + if (i >= args.size || args[i].startsWith("-")) + throw IllegalArgumentException("Expecting an argument") + return args[i] +} diff --git a/common-deps/src/test/kotlin/com/google/devtools/ksp/CommandLineArgParserTest.kt b/common-deps/src/test/kotlin/com/google/devtools/ksp/CommandLineArgParserTest.kt new file mode 100644 index 0000000000..0b160eec66 --- /dev/null +++ b/common-deps/src/test/kotlin/com/google/devtools/ksp/CommandLineArgParserTest.kt @@ -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( + "-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" in helpMsg) + Assert.assertTrue("* " in helpMsg) + println(helpMsg) + } +} diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/cmdline/KSPCommonMain.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/cmdline/KSPCommonMain.kt new file mode 100644 index 0000000000..e23e23778e --- /dev/null +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/cmdline/KSPCommonMain.kt @@ -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) { + if ("-h" in args || "--help" in args) { + printHelpMsg(kspCommonArgParserHelp()) + } else { + runWithArgs(args, ::kspCommonArgParser) + } + } + } +} diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/cmdline/KSPJsMain.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/cmdline/KSPJsMain.kt new file mode 100644 index 0000000000..c4e6236b48 --- /dev/null +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/cmdline/KSPJsMain.kt @@ -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) { + if ("-h" in args || "--help" in args) { + printHelpMsg(kspJsArgParserHelp()) + } else { + runWithArgs(args, ::kspJsArgParser) + } + } + } +} diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/cmdline/KSPJvmMain.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/cmdline/KSPJvmMain.kt new file mode 100644 index 0000000000..c2560dfc7b --- /dev/null +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/cmdline/KSPJvmMain.kt @@ -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) { + 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, parse: (Array) -> Pair>) { + 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 + + KotlinSymbolProcessing(config, processorProviders, logger).execute() +} diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/cmdline/KSPNativeMain.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/cmdline/KSPNativeMain.kt new file mode 100644 index 0000000000..861837e4d0 --- /dev/null +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/cmdline/KSPNativeMain.kt @@ -0,0 +1,17 @@ +package com.google.devtools.ksp.cmdline + +import com.google.devtools.ksp.processing.kspNativeArgParser +import com.google.devtools.ksp.processing.kspNativeArgParserHelp + +class KSPNativeMain { + companion object { + @JvmStatic + fun main(args: Array) { + if ("-h" in args || "--help" in args) { + printHelpMsg(kspNativeArgParserHelp()) + } else { + runWithArgs(args, ::kspNativeArgParser) + } + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 8096e5a8ba..9d447b763b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,3 +19,4 @@ include("symbol-processing-cmdline") include("integration-tests") include("kotlin-analysis-api") include("symbol-processing-aa-embeddable") +include("cmdline-parser-gen")