Skip to content

Commit d8825d3

Browse files
authored
feat: allow passing KSP options via kt_ksp_plugin (#1478)
* feat: allow passing KSP options via kt_ksp_plugin * test: add an integration test with a KSP plugin * throw an error on duplicate option keys * exclude test lib from wildcard build analysis
1 parent d6e9b64 commit d8825d3

File tree

17 files changed

+389
-4
lines changed

17 files changed

+389
-4
lines changed

docs/kotlin.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ Define kotlin compiler options.
451451
<pre>
452452
load("@rules_kotlin//kotlin:core.bzl", "kt_ksp_plugin")
453453

454-
kt_ksp_plugin(<a href="#kt_ksp_plugin-name">name</a>, <a href="#kt_ksp_plugin-deps">deps</a>, <a href="#kt_ksp_plugin-generates_java">generates_java</a>, <a href="#kt_ksp_plugin-processor_class">processor_class</a>, <a href="#kt_ksp_plugin-target_embedded_compiler">target_embedded_compiler</a>)
454+
kt_ksp_plugin(<a href="#kt_ksp_plugin-name">name</a>, <a href="#kt_ksp_plugin-deps">deps</a>, <a href="#kt_ksp_plugin-generates_java">generates_java</a>, <a href="#kt_ksp_plugin-options">options</a>, <a href="#kt_ksp_plugin-processor_class">processor_class</a>, <a href="#kt_ksp_plugin-target_embedded_compiler">target_embedded_compiler</a>)
455455
</pre>
456456

457457
Define a KSP plugin for the Kotlin compiler to run. The plugin can then be referenced in the `plugins` attribute
@@ -485,6 +485,7 @@ kt_jvm_library(
485485
| <a id="kt_ksp_plugin-name"></a>name | A unique name for this target. | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required | |
486486
| <a id="kt_ksp_plugin-deps"></a>deps | The list of libraries to be added to the compiler's plugin classpath | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional | `[]` |
487487
| <a id="kt_ksp_plugin-generates_java"></a>generates_java | Runs Java compilation action for plugin generating Java output. | Boolean | optional | `False` |
488+
| <a id="kt_ksp_plugin-options"></a>options | Processor options passed to the KSP processor via SymbolProcessorEnvironment.options. Each entry is a key-value pair available to the processor at processing time. | <a href="https://bazel.build/rules/lib/core/dict">Dictionary: String -> String</a> | optional | `{}` |
488489
| <a id="kt_ksp_plugin-processor_class"></a>processor_class | The fully qualified class name that the Java compiler uses as an entry point to the annotation processor. | String | required | |
489490
| <a id="kt_ksp_plugin-target_embedded_compiler"></a>target_embedded_compiler | Plugin was compiled against the embeddable kotlin compiler. These plugins expect shaded kotlinc dependencies, and will fail when running against a non-embeddable compiler. | Boolean | optional | `False` |
490491

kotlin/internal/jvm/compile.bzl

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,8 @@ def _run_ksp_builder_actions(
442442
toolchains,
443443
srcs,
444444
compile_deps,
445-
transitive_runtime_jars):
445+
transitive_runtime_jars,
446+
ksp_options = {}):
446447
"""Runs KSP2 via a dedicated KSP2 worker.
447448
448449
The worker handles all staging, KSP2 execution, and output packaging internally.
@@ -503,6 +504,10 @@ def _run_ksp_builder_actions(
503504
if transitive_runtime_jars:
504505
args.add_all("--processor_classpath", transitive_runtime_jars)
505506

507+
# Pass KSP processor options as key=value pairs
508+
for key, value in ksp_options.items():
509+
args.add("--ksp_options", "%s=%s" % (key, value))
510+
506511
# Run KSP2 via dedicated worker (separate from kotlinc worker)
507512
# Single action: staging + KSP2 + packaging all happen in the worker
508513
ctx.actions.run(
@@ -736,6 +741,7 @@ def _kt_jvm_produce_output_jar_actions(
736741

737742
annotation_processors = _plugin_mappers.targets_to_annotation_processors(ctx.attr.plugins + ctx.attr.deps)
738743
ksp_annotation_processors = _plugin_mappers.targets_to_ksp_annotation_processors(ctx.attr.plugins + ctx.attr.deps)
744+
ksp_options = _plugin_mappers.targets_to_ksp_options(ctx.attr.plugins + ctx.attr.deps)
739745
transitive_runtime_jars = _plugin_mappers.targets_to_transitive_runtime_jars(ctx.attr.plugins + ctx.attr.deps)
740746
plugins = _new_plugins_from(ctx.attr.plugins + _exported_plugins(deps = ctx.attr.deps))
741747

@@ -759,6 +765,7 @@ def _kt_jvm_produce_output_jar_actions(
759765
deps_artifacts = deps_artifacts,
760766
annotation_processors = annotation_processors,
761767
ksp_annotation_processors = ksp_annotation_processors,
768+
ksp_options = ksp_options,
762769
transitive_runtime_jars = transitive_runtime_jars,
763770
plugins = plugins,
764771
compile_jar = compile_jar,
@@ -863,6 +870,7 @@ def _run_kt_java_builder_actions(
863870
deps_artifacts,
864871
annotation_processors,
865872
ksp_annotation_processors,
873+
ksp_options,
866874
transitive_runtime_jars,
867875
plugins,
868876
compile_jar,
@@ -910,6 +918,7 @@ def _run_kt_java_builder_actions(
910918
srcs = srcs,
911919
compile_deps = compile_deps,
912920
transitive_runtime_jars = transitive_runtime_jars,
921+
ksp_options = ksp_options,
913922
)
914923
ksp_generated_class_jar = ksp_outputs.ksp_generated_class_jar
915924
output_jars.append(ksp_generated_class_jar)

kotlin/internal/jvm/impl.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,5 +596,6 @@ def kt_ksp_plugin_impl(ctx):
596596
),
597597
],
598598
generates_java = ctx.attr.generates_java,
599+
options = ctx.attr.options,
599600
),
600601
]

kotlin/internal/jvm/jvm.bzl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,11 @@ kt_jvm_library(
678678
doc = """Runs Java compilation action for plugin generating Java output.""",
679679
default = False,
680680
),
681+
"options": attr.string_dict(
682+
doc = """Processor options passed to the KSP processor via SymbolProcessorEnvironment.options.
683+
Each entry is a key-value pair available to the processor at processing time.""",
684+
default = {},
685+
),
681686
"processor_class": attr.string(
682687
doc = " The fully qualified class name that the Java compiler uses as an entry point to the annotation processor.",
683688
mandatory = True,

kotlin/internal/jvm/plugins.bzl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ def _targets_to_ksp_annotation_processors(targets):
6060
def _targets_to_annotation_processors_java_plugin_info(targets):
6161
return [t[JavaPluginInfo] for t in targets if JavaPluginInfo in t]
6262

63+
def _targets_to_ksp_options(targets):
64+
options = {}
65+
for t in targets:
66+
if _KspPluginInfo in t:
67+
for key, value in t[_KspPluginInfo].options.items():
68+
if key in options:
69+
fail("Conflicting KSP option key '%s': defined in multiple plugins" % key)
70+
options[key] = value
71+
return options
72+
6373
def _targets_to_transitive_runtime_jars(targets):
6474
transitive = []
6575
for t in targets:
@@ -74,6 +84,7 @@ def _targets_to_transitive_runtime_jars(targets):
7484
mappers = struct(
7585
targets_to_annotation_processors = _targets_to_annotation_processors,
7686
targets_to_ksp_annotation_processors = _targets_to_ksp_annotation_processors,
87+
targets_to_ksp_options = _targets_to_ksp_options,
7788
targets_to_annotation_processors_java_plugin_info = _targets_to_annotation_processors_java_plugin_info,
7889
targets_to_transitive_runtime_jars = _targets_to_transitive_runtime_jars,
7990
kt_plugin_to_processor = _kt_plugin_to_processor,

src/main/kotlin/io/bazel/kotlin/builder/tasks/jvm/Ksp2Task.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,14 @@ class Ksp2Task : Work {
6565
API_VERSION("--api_version"),
6666
JVM_TARGET("--jvm_target"),
6767
JDK_HOME("--jdk_home"),
68+
KSP_OPTIONS("--ksp_options"),
6869
}
70+
71+
fun parseKspOptions(entries: List<String>): Map<String, String> =
72+
entries.associate { entry ->
73+
val eqIdx = entry.indexOf('=')
74+
if (eqIdx >= 0) entry.substring(0, eqIdx) to entry.substring(eqIdx + 1) else entry to ""
75+
}
6976
}
7077

7178
override fun invoke(
@@ -172,6 +179,8 @@ class Ksp2Task : Work {
172179
val processorUrls = processorClasspath.map { File(it).toURI().toURL() }.toTypedArray()
173180
val kspClassLoader = URLClassLoader(processorUrls, ClassLoader.getSystemClassLoader())
174181

182+
val processorOptions = parseKspOptions(argMap.optional(Ksp2Flags.KSP_OPTIONS) ?: emptyList())
183+
175184
// Load Ksp2Invoker via reflection (it's compiled against KSP2 classes)
176185
val invokerClass = kspClassLoader.loadClass("io.bazel.kotlin.ksp2.Ksp2Invoker")
177186
val invoker =
@@ -196,6 +205,7 @@ class Ksp2Task : Work {
196205
String::class.java, // languageVersion
197206
String::class.java, // apiVersion
198207
File::class.java, // jdkHome
208+
Map::class.java, // processorOptions
199209
Int::class.java, // logLevel
200210
)
201211

@@ -218,6 +228,7 @@ class Ksp2Task : Work {
218228
argMap.optionalSingle(Ksp2Flags.LANGUAGE_VERSION),
219229
argMap.optionalSingle(Ksp2Flags.API_VERSION),
220230
argMap.optionalSingle(Ksp2Flags.JDK_HOME)?.let { File(it) },
231+
processorOptions,
221232
1, // logLevel
222233
) as Int
223234

src/main/kotlin/io/bazel/kotlin/ksp2/Ksp2Invoker.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class Ksp2Invoker(
5555
languageVersion: String?,
5656
apiVersion: String?,
5757
jdkHome: File?,
58+
processorOptions: Map<String, String> = emptyMap(),
5859
logLevel: Int = 1,
5960
): Int {
6061
// Load processors via ServiceLoader from the provided classloader
@@ -81,6 +82,7 @@ class Ksp2Invoker(
8182
languageVersion?.let { this.languageVersion = it }
8283
apiVersion?.let { this.apiVersion = it }
8384
jdkHome?.let { this.jdkHome = it }
85+
this.processorOptions = processorOptions
8486
this.mapAnnotationArgumentsInJava = true
8587
}.build()
8688

src/main/starlark/core/plugin/providers.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ KtPluginConfiguration = provider(
3131
KspPluginInfo = provider(
3232
fields = {
3333
"generates_java": "Runs Java compilation action for this plugin",
34+
"options": "Dict of processor options (key-value strings) passed to KSP via environment.options",
3435
"plugins": "List of JavaPluginInfo providers for the plugins to run with KSP",
3536
},
3637
)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
load("//kotlin:core.bzl", "kt_ksp_plugin")
2+
load("//kotlin:jvm.bzl", "kt_jvm_library", "kt_jvm_test")
3+
4+
package(default_visibility = ["//visibility:private"])
5+
6+
# The processor itself
7+
kt_jvm_library(
8+
name = "options_recorder_processor",
9+
srcs = [
10+
"OptionsRecorderProcessor.kt",
11+
"OptionsRecorderProcessorProvider.kt",
12+
],
13+
resource_strip_prefix = "src/test/data/jvm/ksp/optionsrecorder/resources",
14+
resources = glob(["resources/META-INF/services/*"]),
15+
deps = [
16+
"//kotlin/compiler:symbol-processing-api",
17+
],
18+
)
19+
20+
# KSP plugin with the test options
21+
kt_ksp_plugin(
22+
name = "options_recorder_plugin",
23+
options = {
24+
"option_a": "value_a",
25+
"option_b": "value_b",
26+
},
27+
processor_class = "com.example.optionsrecorder.OptionsRecorderProcessorProvider",
28+
deps = [":options_recorder_processor"],
29+
)
30+
31+
# Library compiled with the plugin — KSP generates generated.RecordedOptions
32+
kt_jvm_library(
33+
name = "options_recorder_lib",
34+
srcs = ["Subject.kt"],
35+
plugins = [":options_recorder_plugin"],
36+
)
37+
38+
# Integration test: verifies the processor actually received the options
39+
kt_jvm_test(
40+
name = "OptionsRecorderTest",
41+
srcs = ["OptionsRecorderTest.kt"],
42+
test_class = "com.example.optionsrecorder.OptionsRecorderTest",
43+
visibility = ["//src/test:__subpackages__"],
44+
deps = [
45+
":options_recorder_lib",
46+
"@kotlin_rules_maven//:com_google_truth_truth",
47+
"@kotlin_rules_maven//:junit_junit",
48+
],
49+
)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.example.optionsrecorder
2+
3+
import com.google.devtools.ksp.processing.CodeGenerator
4+
import com.google.devtools.ksp.processing.Dependencies
5+
import com.google.devtools.ksp.processing.Resolver
6+
import com.google.devtools.ksp.processing.SymbolProcessor
7+
import com.google.devtools.ksp.symbol.KSAnnotated
8+
9+
/**
10+
* A minimal KSP processor that writes the options it received into a generated Kotlin file.
11+
* Used by integration tests to verify that options are passed through correctly.
12+
*/
13+
class OptionsRecorderProcessor(
14+
private val codeGenerator: CodeGenerator,
15+
private val options: Map<String, String>,
16+
) : SymbolProcessor {
17+
private var done = false
18+
19+
override fun process(resolver: Resolver): List<KSAnnotated> {
20+
if (done) return emptyList()
21+
done = true
22+
23+
val file = codeGenerator.createNewFile(
24+
dependencies = Dependencies(false),
25+
packageName = "generated",
26+
fileName = "RecordedOptions",
27+
)
28+
file.bufferedWriter().use { writer ->
29+
writer.write("package generated\n\n")
30+
writer.write("object RecordedOptions {\n")
31+
writer.write(" val options: Map<String, String> = mapOf(\n")
32+
options.forEach { (k, v) ->
33+
writer.write(" \"$k\" to \"$v\",\n")
34+
}
35+
writer.write(" )\n")
36+
writer.write("}\n")
37+
}
38+
return emptyList()
39+
}
40+
}

0 commit comments

Comments
 (0)