From b9380cd1ce3a2c7ae337531c455c425fac38302c Mon Sep 17 00:00:00 2001 From: Corbin McNeely-Smith <58151731+restingbull@users.noreply.github.com> Date: Sun, 10 Mar 2024 15:21:00 -0400 Subject: [PATCH] [kt_compiler_plugin] Add kt_plugin_cfg for reusable and composable options (#1105) * Fix non-bzlmod, format * Centralize plugin providers, add plugin option provider * Add rules testing * First pass, tests fail * [kt_compiler_plugin] Add kt_plugin_cfg for reusuable and composable options * Example of using ksp without leveraging kapt architecture * Remove MODULE.bazel.lock --- MODULE.bazel | 5 + examples/ksp/BUILD | 72 +++- examples/ksp/MODULE.bazel | 6 + kotlin/core.bzl | 2 + kotlin/internal/compiler_plugins.bzl | 21 - kotlin/internal/defs.bzl | 27 +- kotlin/internal/jvm/compile.bzl | 139 +++++-- kotlin/internal/jvm/impl.bzl | 68 +++- kotlin/internal/jvm/jvm.bzl | 41 +- .../builder/tasks/jvm/CompilationTask.kt | 6 +- src/main/starlark/core/BUILD.bazel | 3 +- src/main/starlark/core/options/BUILD.bazel | 15 +- src/main/starlark/core/plugin/BUILD.bazel | 18 + .../starlark/core/plugin/BUILD.release.bazel | 7 + src/main/starlark/core/plugin/providers.bzl | 34 ++ .../BUILD.com_github_jetbrains_kotlin.bazel | 7 + .../starlark/core/repositories/versions.bzl | 7 + src/test/starlark/case.bzl | 34 ++ src/test/starlark/core/plugin/BUILD.bazel | 5 + src/test/starlark/core/plugin/subjects.bzl | 37 ++ src/test/starlark/core/plugin/test.bzl | 382 ++++++++++++++++++ src/test/starlark/truth.bzl | 41 ++ 22 files changed, 875 insertions(+), 102 deletions(-) create mode 100644 examples/ksp/MODULE.bazel delete mode 100644 kotlin/internal/compiler_plugins.bzl create mode 100644 src/main/starlark/core/plugin/BUILD.bazel create mode 100644 src/main/starlark/core/plugin/BUILD.release.bazel create mode 100644 src/main/starlark/core/plugin/providers.bzl create mode 100644 src/test/starlark/case.bzl create mode 100644 src/test/starlark/core/plugin/BUILD.bazel create mode 100644 src/test/starlark/core/plugin/subjects.bzl create mode 100644 src/test/starlark/core/plugin/test.bzl create mode 100644 src/test/starlark/truth.bzl diff --git a/MODULE.bazel b/MODULE.bazel index 9cfc80f12..0e492cc4d 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -27,6 +27,7 @@ rules_kotlin_bootstrap_extensions = use_extension( "//src/main/starlark/core/repositories:bzlmod_bootstrap.bzl", "rules_kotlin_bootstrap_extensions", ) + use_repo( rules_kotlin_bootstrap_extensions, "kt_java_stub_template", @@ -41,6 +42,7 @@ register_toolchains("//kotlin/internal:default_toolchain") # TODO(bencodes) We should be able to remove this once rules_android has rolled out official Bzlmod support remote_android_extensions = use_extension("@bazel_tools//tools/android:android_extensions.bzl", "remote_android_tools_extensions") + use_repo(remote_android_extensions, "android_gmaven_r8", "android_tools") # Development dependencies @@ -84,8 +86,11 @@ maven.install( "https://repo1.maven.org/maven2", ], ) + use_repo(maven, "kotlin_rules_maven") bazel_dep(name = "rules_pkg", version = "0.7.0") bazel_dep(name = "stardoc", version = "0.5.6", repo_name = "io_bazel_stardoc") bazel_dep(name = "rules_proto", version = "5.3.0-21.7") + +bazel_dep(name = "rules_testing", version = "0.5.0", dev_dependency = True) diff --git a/examples/ksp/BUILD b/examples/ksp/BUILD index 0c4a5acdf..6722c96f2 100644 --- a/examples/ksp/BUILD +++ b/examples/ksp/BUILD @@ -14,7 +14,7 @@ load("@rules_java//java:defs.bzl", "java_binary", "java_plugin") # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -load("@rules_kotlin//kotlin:core.bzl", "define_kt_toolchain", "kt_ksp_plugin") +load("@rules_kotlin//kotlin:core.bzl", "define_kt_toolchain", "kt_compiler_plugin", "kt_ksp_plugin", "kt_plugin_cfg") load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") package(default_visibility = ["//visibility:public"]) @@ -81,3 +81,73 @@ build_test( "//:coffee_app_deploy.jar", ], ) + +kt_compiler_plugin( + name = "ksp", + compile_phase = True, + id = "com.google.devtools.ksp.symbol-processing", + options = { + "apclasspath": "{classpath}", + # projectBaseDir shouldn't matter because incremental is disabled + "projectBaseDir": "{temp}", + # Disable incremental mode + "incremental": "false", + # Directory where class files are written to. Files written to this directory are class + # files being written directly from the annotation processor, not Kotlinc + "classOutputDir": "{generatedClasses}", + # Directory where generated Java sources files are written to + "javaOutputDir": "{generatedSources}", + # Directory where generated Kotlin sources files are written to + "kotlinOutputDir": "{generatedSources}", + # Directory where META-INF data is written to. This might not be the most ideal place to + # write this. Maybe just directly to the classes directory? + "resourceOutputDir": "{generatedSources}", + # TODO(bencodes) Not sure what this directory is yet. + "kspOutputDir": "{temp}", + # Directory to write KSP caches. Shouldn't matter because incremental is disabled + "cachesDir": "{temp}", + # Include in compilation as an example. This should be processed in the stubs phase. + "withCompilation": "true", + # Set returnOkOnError to false because we want to fail the build if there are any errors + "returnOkOnError": "false", + "allWarningsAsErrors": "false", + }, + deps = [ + "@rules_kotlin//kotlin/compiler:symbol-processing-api", + "@rules_kotlin//kotlin/compiler:symbol-processing-cmdline", + ], +) + +kt_plugin_cfg( + name = "ksp_moshi", + options = { + }, + plugin = ":ksp", + deps = [ + "@maven//:com_squareup_moshi_moshi", + "@maven//:com_squareup_moshi_moshi_kotlin", + "@maven//:com_squareup_moshi_moshi_kotlin_codegen", + ], +) + +kt_jvm_library( + name = "raw_ksp_coffee_app_lib", + srcs = ["CoffeeAppModel.kt"], + plugins = [ + "//:ksp", + "//:ksp_moshi", + ], + deps = [ + "@maven//:com_google_auto_service_auto_service_annotations", + "@maven//:com_google_auto_value_auto_value_annotations", + "@maven//:com_squareup_moshi_moshi", + "@maven//:com_squareup_moshi_moshi_kotlin", + ], +) + +build_test( + name = "raw_ksp_lib_test", + targets = [ + "//:raw_ksp_coffee_app_lib", + ], +) diff --git a/examples/ksp/MODULE.bazel b/examples/ksp/MODULE.bazel new file mode 100644 index 000000000..00bb18361 --- /dev/null +++ b/examples/ksp/MODULE.bazel @@ -0,0 +1,6 @@ +############################################################################### +# Bazel now uses Bzlmod by default to manage external dependencies. +# Please consider migrating your external dependencies from WORKSPACE to MODULE.bazel. +# +# For more details, please check https://github.com/bazelbuild/bazel/issues/18958 +############################################################################### diff --git a/kotlin/core.bzl b/kotlin/core.bzl index a43a3eb3b..3b9dd46d8 100644 --- a/kotlin/core.bzl +++ b/kotlin/core.bzl @@ -12,6 +12,7 @@ load( "//kotlin/internal/jvm:jvm.bzl", _kt_compiler_plugin = "kt_compiler_plugin", _kt_ksp_plugin = "kt_ksp_plugin", + _kt_plugin_cfg = "kt_plugin_cfg", ) define_kt_toolchain = _define_kt_toolchain @@ -20,3 +21,4 @@ kt_javac_options = _kt_javac_options kt_kotlinc_options = _kt_kotlinc_options kt_compiler_plugin = _kt_compiler_plugin kt_ksp_plugin = _kt_ksp_plugin +kt_plugin_cfg = _kt_plugin_cfg diff --git a/kotlin/internal/compiler_plugins.bzl b/kotlin/internal/compiler_plugins.bzl deleted file mode 100644 index 4306a0d40..000000000 --- a/kotlin/internal/compiler_plugins.bzl +++ /dev/null @@ -1,21 +0,0 @@ -load( - "//kotlin/internal:defs.bzl", - _KtCompilerPluginInfo = "KtCompilerPluginInfo", -) - -def plugins_to_classpaths(providers_list): - flattened_files = [] - for providers in providers_list: - if _KtCompilerPluginInfo in providers: - provider = providers[_KtCompilerPluginInfo] - for e in provider.classpath: - flattened_files.append(e) - return flattened_files - -def plugins_to_options(providers_list): - kt_compiler_plugin_providers = [providers[_KtCompilerPluginInfo] for providers in providers_list if _KtCompilerPluginInfo in providers] - flattened_options = [] - for provider in kt_compiler_plugin_providers: - for option in provider.options: - flattened_options.append("%s:%s" % (option.id, option.value)) - return flattened_options diff --git a/kotlin/internal/defs.bzl b/kotlin/internal/defs.bzl index 498ce0645..7981fe438 100644 --- a/kotlin/internal/defs.bzl +++ b/kotlin/internal/defs.bzl @@ -11,6 +11,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License.# +load( + "//src/main/starlark/core/plugin:providers.bzl", + _KspPluginInfo = "KspPluginInfo", + _KtCompilerPluginInfo = "KtCompilerPluginInfo", + _KtCompilerPluginOption = "KtCompilerPluginOption", + _KtPluginConfiguration = "KtPluginConfiguration", +) # The Kotlin Toolchain type. TOOLCHAIN_TYPE = "%s" % Label("//kotlin/internal:kt_toolchain_type") @@ -50,18 +57,10 @@ KtJsInfo = provider( }, ) -KtCompilerPluginInfo = provider( - fields = { - "plugin_jars": "List of plugin jars.", - "classpath": "The kotlin compiler plugin classpath.", - "stubs": "Run this plugin during kapt stub generation.", - "compile": "Run this plugin during koltinc compilation.", - "options": "List of plugin options, represented as structs with an id and a value field, to be passed to the compiler", - }, -) +KtCompilerPluginInfo = _KtCompilerPluginInfo -KspPluginInfo = provider( - fields = { - "plugins": "List of JavaPLuginInfo providers for the plugins to run with KSP", - }, -) +KspPluginInfo = _KspPluginInfo + +KtCompilerPluginOption = _KtCompilerPluginOption + +KtPluginConfiguration = _KtPluginConfiguration diff --git a/kotlin/internal/jvm/compile.bzl b/kotlin/internal/jvm/compile.bzl index 5b1df20fb..c32ed245d 100644 --- a/kotlin/internal/jvm/compile.bzl +++ b/kotlin/internal/jvm/compile.bzl @@ -1,9 +1,3 @@ -load( - "@bazel_tools//tools/jdk:toolchain_utils.bzl", - "find_java_runtime_toolchain", - "find_java_toolchain", -) - # Copyright 2018 The Bazel Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,11 +11,21 @@ load( # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -load("@rules_java//java:defs.bzl", "JavaInfo", "java_common") +load( + "@bazel_tools//tools/jdk:toolchain_utils.bzl", + "find_java_runtime_toolchain", + "find_java_toolchain", +) +load( + "@rules_java//java:defs.bzl", + "JavaInfo", + "java_common", +) load( "//kotlin/internal:defs.bzl", _KtCompilerPluginInfo = "KtCompilerPluginInfo", _KtJvmInfo = "KtJvmInfo", + _KtPluginConfiguration = "KtPluginConfiguration", _TOOLCHAIN_TYPE = "TOOLCHAIN_TYPE", ) load( @@ -178,9 +182,83 @@ def _adjust_resources_path(path, resource_strip_prefix): else: return _adjust_resources_path_by_default_prefixes(path) -def _format_compile_plugin_options(options): - """Format options into id:value for cmd line.""" - return ["%s:%s" % (o.id, o.value) for o in options] +def _format_compile_plugin_options(o): + """Format compiler option into id:value for cmd line.""" + return [ + "%s:%s" % (o.id, o.value), + ] + +def _new_plugins_from(targets): + """Returns a struct containing the plugin metadata for the given targets. + + Args: + targets: A list of targets. + Returns: + A struct containing the plugins for the given targets in the format: + { + stubs_phase = { + classpath = depset, + options= List[KtCompilerPluginOption], + ), + compile = { + classpath = depset, + options = List[KtCompilerPluginOption], + }, + } + """ + + all_plugins = {} + plugins_without_phase = [] + for t in targets: + if _KtCompilerPluginInfo not in t: + continue + plugin = t[_KtCompilerPluginInfo] + if not (plugin.stubs or plugin.compile): + plugins_without_phase.append("%s: %s" % (t.label, plugin.id)) + if plugin.id in all_plugins: + # This need a more robust error messaging. + fail("has multiple plugins with the same id: %s." % plugin.id) + all_plugins[plugin.id] = plugin + + if plugins_without_phase: + fail("has plugin without a phase defined: %s" % cfgs_without_plugin) + + all_plugin_cfgs = {} + cfgs_without_plugin = [] + for t in targets: + if _KtPluginConfiguration not in t: + continue + cfg = t[_KtPluginConfiguration] + if cfg.id not in all_plugins: + cfgs_without_plugin.append("%s: %s" % (t.label, cfg.id)) + all_plugin_cfgs[cfg.id] = cfg + + if cfgs_without_plugin: + fail("has plugin configurations without corresponding plugins: %s" % cfgs_without_plugin) + + return struct( + stubs_phase = _new_plugin_from(all_plugin_cfgs, [p for p in all_plugins.values() if p.stubs]), + compile_phase = _new_plugin_from(all_plugin_cfgs, [p for p in all_plugins.values() if p.compile]), + ) + +def _new_plugin_from(all_cfgs, plugins_for_phase): + classpath = [] + data = [] + options = [] + for p in plugins_for_phase: + classpath.append(p.classpath) + options.extend(p.options) + if p.id in all_cfgs: + cfg = all_cfgs[p.id] + classpath.append(cfg.classpath) + data.append(cfg.data) + options.extend(cfg.options) + + return struct( + classpath = depset(transitive = classpath), + data = depset(transitive = data), + options = options, + ) # INTERNAL ACTIONS ##################################################################################################### def _fold_jars_action(ctx, rule_kind, toolchains, output_jar, input_jars, action_type = ""): @@ -425,49 +503,28 @@ def _run_kt_builder_action( uniquify = True, ) - compiler_plugins = [ - p[_KtCompilerPluginInfo] - for p in plugins - if _KtCompilerPluginInfo in p and p[_KtCompilerPluginInfo] - ] - - stubs_compiler_plugins = [ - kcp - for kcp in compiler_plugins - if kcp.stubs - ] - - compiler_compiler_plugins = [ - ccp - for ccp in compiler_plugins - if ccp.compile - ] - - if compiler_plugins and not (stubs_compiler_plugins or compiler_compiler_plugins): - fail("plugins but no phase plugins: %s" % compiler_plugins) - args.add_all( "--stubs_plugin_classpath", - depset(transitive = [p.classpath for p in stubs_compiler_plugins]), + plugins.stubs_phase.classpath, omit_if_empty = True, ) args.add_all( "--stubs_plugin_options", - [p.options for p in stubs_compiler_plugins], + plugins.stubs_phase.options, map_each = _format_compile_plugin_options, omit_if_empty = True, ) args.add_all( "--compiler_plugin_classpath", - depset(transitive = [p.classpath for p in compiler_compiler_plugins]), + plugins.compile_phase.classpath, omit_if_empty = True, ) args.add_all( "--compiler_plugin_options", - [p.options for p in compiler_compiler_plugins], + plugins.compile_phase.options, map_each = _format_compile_plugin_options, omit_if_empty = True, ) @@ -493,7 +550,13 @@ def _run_kt_builder_action( mnemonic = mnemonic, inputs = depset( srcs.all_srcs + srcs.src_jars + generated_src_jars, - transitive = [compile_deps.compile_jars, transitive_runtime_jars, deps_artifacts] + [p.classpath for p in compiler_plugins], + transitive = [ + compile_deps.compile_jars, + transitive_runtime_jars, + deps_artifacts, + plugins.stubs_phase.classpath, + plugins.compile_phase.classpath, + ], ), tools = tools, input_manifests = input_manifests, @@ -533,10 +596,12 @@ def kt_jvm_produce_jar_actions(ctx, rule_kind): deps = ctx.attr.deps, runtime_deps = ctx.attr.runtime_deps, ) + annotation_processors = _plugin_mappers.targets_to_annotation_processors(ctx.attr.plugins + ctx.attr.deps) ksp_annotation_processors = _plugin_mappers.targets_to_ksp_annotation_processors(ctx.attr.plugins + ctx.attr.deps) transitive_runtime_jars = _plugin_mappers.targets_to_transitive_runtime_jars(ctx.attr.plugins + ctx.attr.deps) - plugins = ctx.attr.plugins + _exported_plugins(deps = ctx.attr.deps) + plugins = _new_plugins_from(ctx.attr.plugins + _exported_plugins(deps = ctx.attr.deps)) + deps_artifacts = _deps_artifacts(toolchains, ctx.attr.deps + associates.targets) generated_src_jars = [] diff --git a/kotlin/internal/jvm/impl.bzl b/kotlin/internal/jvm/impl.bzl index 4c6569829..393d44950 100644 --- a/kotlin/internal/jvm/impl.bzl +++ b/kotlin/internal/jvm/impl.bzl @@ -1,12 +1,3 @@ -load("@rules_java//java:defs.bzl", "JavaInfo", "JavaPluginInfo", "java_common") -load( - "//kotlin/internal:defs.bzl", - _KspPluginInfo = "KspPluginInfo", - _KtCompilerPluginInfo = "KtCompilerPluginInfo", - _KtJvmInfo = "KtJvmInfo", - _TOOLCHAIN_TYPE = "TOOLCHAIN_TYPE", -) - # Copyright 2018 The Bazel Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +11,17 @@ load( # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +load("@rules_java//java:defs.bzl", "JavaInfo", "JavaPluginInfo", "java_common") +load( + "//kotlin/internal:defs.bzl", + "KtCompilerPluginOption", + "KtPluginConfiguration", + _KspPluginInfo = "KspPluginInfo", + _KtCompilerPluginInfo = "KtCompilerPluginInfo", + _KtJvmInfo = "KtJvmInfo", + _TOOLCHAIN_TYPE = "TOOLCHAIN_TYPE", +) load( "//kotlin/internal/jvm:compile.bzl", "export_only_providers", @@ -383,16 +385,39 @@ def _deshade_embedded_kotlinc_jars(target, ctx, jars, deps): ], ) -def kt_compiler_plugin_impl(ctx): - plugin_id = ctx.attr.id +def _resolve_plugin_options(id, string_list_dict, expand_location): + """ + Resolves plugin options from a string dict to a dict of strings. + + Args: + id: the plugin id + string_list_dict: a dict of list[string]. + Returns: + a dict of strings + """ options = [] - for (k, v) in ctx.attr.options.items(): - if "=" in k: - fail("kt_compiler_plugin options keys cannot contain the = symbol") - options.append(struct(id = plugin_id, value = "%s=%s" % (k, v))) + for (k, vs) in string_list_dict.items(): + for v in vs: + if "=" in k: + fail("kotlin compiler option keys cannot contain the = symbol") + value = k + "=" + expand_location(v) if v else k + options.append(KtCompilerPluginOption(id = id, value = value)) + return options + +# This is naive reference implementation for resolving configurations. +# A more complicated plugin will need to provide its own implementation. +def _resolve_plugin_cfg(info, options, deps, expand_location): + ji = java_common.merge([dep[JavaInfo] for dep in deps if JavaInfo in dep]) + classpath = depset(ji.runtime_output_jars, transitive = [ji.transitive_runtime_jars]) + return KtPluginConfiguration( + id = info.id, + options = _resolve_plugin_options(info.id, options, expand_location), + classpath = classpath, + data = depset(), + ) - if not (ctx.attr.compile_phase or ctx.attr.stubs_phase): - fail("Plugin must execute during in one or more phases: stubs_phase, compile_phase") +def kt_compiler_plugin_impl(ctx): + plugin_id = ctx.attr.id deps = ctx.attr.deps info = None @@ -411,16 +436,25 @@ def kt_compiler_plugin_impl(ctx): classpath = depset(info.runtime_output_jars, transitive = [info.transitive_runtime_jars]) + # TODO(1035): Migrate kt_compiler_plugin.options to string_list_dict + options = _resolve_plugin_options(plugin_id, {k: [v] for (k, v) in ctx.attr.options.items()}, ctx.expand_location) + return [ DefaultInfo(files = classpath), _KtCompilerPluginInfo( + id = plugin_id, classpath = classpath, options = options, stubs = ctx.attr.stubs_phase, compile = ctx.attr.compile_phase, + resolve_cfg = _resolve_plugin_cfg, ), ] +def kt_plugin_cfg_impl(ctx): + plugin = ctx.attr.plugin[_KtCompilerPluginInfo] + return plugin.resolve_cfg(plugin, ctx.attr.options, ctx.attr.deps, ctx.expand_location) + def kt_ksp_plugin_impl(ctx): info = java_common.merge([dep[JavaInfo] for dep in ctx.attr.deps]) classpath = depset(info.runtime_output_jars, transitive = [info.transitive_runtime_jars]) diff --git a/kotlin/internal/jvm/jvm.bzl b/kotlin/internal/jvm/jvm.bzl index bef495e43..d7099037c 100644 --- a/kotlin/internal/jvm/jvm.bzl +++ b/kotlin/internal/jvm/jvm.bzl @@ -95,6 +95,7 @@ kt_jvm_binary( load("@rules_java//java:defs.bzl", "JavaInfo") load( "//kotlin/internal:defs.bzl", + "KtPluginConfiguration", _JAVA_RUNTIME_TOOLCHAIN_TYPE = "JAVA_RUNTIME_TOOLCHAIN_TYPE", _JAVA_TOOLCHAIN_TYPE = "JAVA_TOOLCHAIN_TYPE", _KspPluginInfo = "KspPluginInfo", @@ -109,6 +110,7 @@ load( ) load( "//kotlin/internal/jvm:impl.bzl", + "kt_plugin_cfg_impl", _kt_compiler_deps_aspect_impl = "kt_compiler_deps_aspect_impl", _kt_compiler_plugin_impl = "kt_compiler_plugin_impl", _kt_jvm_binary_impl = "kt_jvm_binary_impl", @@ -172,6 +174,7 @@ _common_attr = utils.add_dicts( [Attributes common to all build rules](https://docs.bazel.build/versions/master/be/common-definitions.html#common-attributes).""", providers = [ [JavaInfo], + [_KtJvmInfo], ], allow_files = False, ), @@ -211,6 +214,12 @@ _common_attr = utils.add_dicts( "plugins": attr.label_list( default = [], cfg = "exec", + providers = [ + [JavaPluginInfo], + [_KtJvmInfo], + [KtPluginConfiguration], + [_KtCompilerPluginInfo], + ], ), "module_name": attr.string( doc = """The name of the module, if not provided the module name is derived from the label. --e.g., @@ -253,7 +262,7 @@ Compiler plugins listed here will be treated as if they were added in the plugin of any targets that directly depend on this target. Unlike `java_plugin`s exported_plugins, this is not transitive""", default = [], - providers = [_KtCompilerPluginInfo], + providers = [[_KtCompilerPluginInfo], [KtPluginConfiguration]], ), "neverlink": attr.bool( doc = """If true only use this library for compilation and not at runtime.""", @@ -452,7 +461,7 @@ Compiler plugins listed here will be treated as if they were added in the plugin attribute of any targets that directly depend on this target. Unlike java_plugins' exported_plugins, this is not transitive""", default = [], - providers = [_KtCompilerPluginInfo], + providers = [[_KtCompilerPluginInfo], [KtPluginConfiguration]], ), "neverlink": attr.bool( doc = """If true only use this library for compilation and not at runtime.""", @@ -532,6 +541,7 @@ Supports the following template values: - `{generatedClasses}`: directory for generated class output - `{temp}`: temporary directory, discarded between invocations - `{generatedSources}`: directory for generated source output +- `{classpath}` : replaced with a list of jars separated by the filesystem appropriate separator. """, default = {}, ), @@ -601,3 +611,30 @@ kt_jvm_library( implementation = _kt_ksp_plugin_impl, provides = [_KspPluginInfo], ) + +kt_plugin_cfg = rule( + implementation = kt_plugin_cfg_impl, + doc = """ + Configurations for kt_compiler_plugin, ksp_plugin, and java_plugin. + + This allows setting options and dependencies independently from the initial plugin definition. + """, + attrs = { + "plugin": attr.label( + doc = "The plugin to associate with this configuration", + providers = [_KtCompilerPluginInfo], + mandatory = True, + ), + "options": attr.string_list_dict( + doc = "A dictionary of flag to values to be used as plugin configuration options.", + ), + "deps": attr.label_list( + doc = "Dependencies for this configuration.", + providers = [ + [_KspPluginInfo], + [JavaInfo], + [JavaPluginInfo], + ], + ), + }, +) diff --git a/src/main/kotlin/io/bazel/kotlin/builder/tasks/jvm/CompilationTask.kt b/src/main/kotlin/io/bazel/kotlin/builder/tasks/jvm/CompilationTask.kt index 9f8dbf30a..a5b0ff212 100644 --- a/src/main/kotlin/io/bazel/kotlin/builder/tasks/jvm/CompilationTask.kt +++ b/src/main/kotlin/io/bazel/kotlin/builder/tasks/jvm/CompilationTask.kt @@ -97,13 +97,15 @@ internal fun JvmCompilationTask.plugins( xFlag("plugin", it) } - val dirTokens = mapOf( + val optionTokens = mapOf( "{generatedClasses}" to directories.generatedClasses, "{stubs}" to directories.stubs, + "{temp}" to directories.temp, "{generatedSources}" to directories.generatedSources, + "{classpath}" to classpath.joinToString(File.pathSeparator) ) options.forEach { opt -> - val formatted = dirTokens.entries.fold(opt) { formatting, (token, value) -> + val formatted = optionTokens.entries.fold(opt) { formatting, (token, value) -> formatting.replace(token, value) } flag("-P", "plugin:$formatted") diff --git a/src/main/starlark/core/BUILD.bazel b/src/main/starlark/core/BUILD.bazel index 7cb934ba0..ce8674dab 100644 --- a/src/main/starlark/core/BUILD.bazel +++ b/src/main/starlark/core/BUILD.bazel @@ -11,6 +11,7 @@ release_archive( }, deps = [ "//src/main/starlark/core/options:pkg", + "//src/main/starlark/core/plugin:pkg", "//src/main/starlark/core/repositories:pkg", ], ) @@ -20,7 +21,7 @@ bzl_library( srcs = glob(["*.bzl"]), visibility = ["//:__subpackages__"], deps = [ - "//src/main/starlark/core/options", + "//src/main/starlark/core/plugin", "//src/main/starlark/core/repositories", ], ) diff --git a/src/main/starlark/core/options/BUILD.bazel b/src/main/starlark/core/options/BUILD.bazel index 6512aeb1b..17fa67159 100644 --- a/src/main/starlark/core/options/BUILD.bazel +++ b/src/main/starlark/core/options/BUILD.bazel @@ -11,10 +11,11 @@ release_archive( }, ) -bzl_library( - name = "options", - srcs = glob(["*.bzl"]) + [ - "@com_github_jetbrains_kotlin//:capabilities.bzl", - ], - visibility = ["//:__subpackages__"], -) +#bzl_library( +# name = "options", +# srcs = glob(["*.bzl"]), +# visibility = ["//:__subpackages__"], +# deps = [ +# "@com_github_jetbrains_kotlin//:capabilities", +# ], +#) diff --git a/src/main/starlark/core/plugin/BUILD.bazel b/src/main/starlark/core/plugin/BUILD.bazel new file mode 100644 index 000000000..3a04b6ed1 --- /dev/null +++ b/src/main/starlark/core/plugin/BUILD.bazel @@ -0,0 +1,18 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("//src/main/starlark/release:packager.bzl", "release_archive") + +release_archive( + name = "pkg", + srcs = glob( + ["*.bzl"], + ), + src_map = { + "BUILD.release.bazel": "BUILD.bazel", + }, +) + +bzl_library( + name = "plugin", + srcs = glob(["*.bzl"]), + visibility = ["//:__subpackages__"], +) diff --git a/src/main/starlark/core/plugin/BUILD.release.bazel b/src/main/starlark/core/plugin/BUILD.release.bazel new file mode 100644 index 000000000..79697e76b --- /dev/null +++ b/src/main/starlark/core/plugin/BUILD.release.bazel @@ -0,0 +1,7 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +bzl_library( + name = "plugin", + srcs = glob(["*.bzl"]), + visibility = ["//:__subpackages__"], +) diff --git a/src/main/starlark/core/plugin/providers.bzl b/src/main/starlark/core/plugin/providers.bzl new file mode 100644 index 000000000..93c610bd7 --- /dev/null +++ b/src/main/starlark/core/plugin/providers.bzl @@ -0,0 +1,34 @@ +KtCompilerPluginOption = provider( + fields = { + "id": "The id of the option.", + "value": "The value of the option.", + }, +) + +KtCompilerPluginInfo = provider( + fields = { + "id": "The id of the plugin.", + "plugin_jars": "List of plugin jars.", + "classpath": "The kotlin compiler plugin classpath.", + "stubs": "Run this plugin during kapt stub generation.", + "compile": "Run this plugin during koltinc compilation.", + "options": "List of plugin options, represented as KtCompilerPluginOption, to be passed to the compiler", + "resolve_cfg": "A Callable[[KtCompilerPluginInfo, Dict[str,str], List[Target], KtPluginConfiguration]" + + " that resolves an associated plugin configuration.", + }, +) + +KtPluginConfiguration = provider( + fields = { + "id": "The id of the compiler plugin associated with this configuration.", + "options": "List of plugin options, represented KtCompilerPluginOption", + "classpath": "Depset of jars to add to the classpath when running the plugin.", + "data": "Depset of files to pass to the plugin as data.", + }, +) + +KspPluginInfo = provider( + fields = { + "plugins": "List of JavaPluginInfo providers for the plugins to run with KSP", + }, +) diff --git a/src/main/starlark/core/repositories/BUILD.com_github_jetbrains_kotlin.bazel b/src/main/starlark/core/repositories/BUILD.com_github_jetbrains_kotlin.bazel index 782b6e857..d343f854d 100644 --- a/src/main/starlark/core/repositories/BUILD.com_github_jetbrains_kotlin.bazel +++ b/src/main/starlark/core/repositories/BUILD.com_github_jetbrains_kotlin.bazel @@ -13,8 +13,15 @@ # limitations under the License. package(default_visibility = ["//visibility:public"]) +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + # Kotlin home filegroup containing everything that is needed. filegroup( name = "home", srcs = glob(["**"]), ) + +bzl_library( + name = "capabilities", + srcs = ["capabilities.bzl"], +) diff --git a/src/main/starlark/core/repositories/versions.bzl b/src/main/starlark/core/repositories/versions.bzl index 383546f18..b9c7bc657 100644 --- a/src/main/starlark/core/repositories/versions.bzl +++ b/src/main/starlark/core/repositories/versions.bzl @@ -125,5 +125,12 @@ versions = struct( ], sha256 = None, ), + RULES_TESTING = version( + version = "0.5.0", + url_templates = [ + "https://github.com/bazelbuild/rules_testing/releases/download/v{version}/rules_testing-v{version}.tar.gz", + ], + sha256 = "b84ed8546f1969d700ead4546de9f7637e0f058d835e47e865dcbb13c4210aed", + ), use_repository = _use_repository, ) diff --git a/src/test/starlark/case.bzl b/src/test/starlark/case.bzl new file mode 100644 index 000000000..38b1932f2 --- /dev/null +++ b/src/test/starlark/case.bzl @@ -0,0 +1,34 @@ +load("@rules_testing//lib:util.bzl", "util") +load("@rules_testing//lib:test_suite.bzl", "test_suite") + +def _prepend(rule, name, **kwargs): + util.helper_target( + rule, + name = name, + **kwargs + ) + return ":" + name + +def case(namespace): + return struct( + name = namespace, + have = lambda rule, name, **kwargs: _prepend(rule, namespace + "_" + name, **kwargs), + artifact = lambda name, **kwargs: util.empty_file( + name = namespace + "_" + name, + **kwargs + ), + got = lambda rule, name, **kwargs: _prepend(rule, namespace + "_" + name, **kwargs), + ref = lambda name: ":" + namespace + "_" + name, + ) + +def suite(name, *tests): + test_targets = [] + for test in tests: + test_name = str(test).split(" ")[1] + test_targets.append(":" + test_name) + test(case(test_name)) + + native.test_suite( + name = name, + tests = test_targets, + ) diff --git a/src/test/starlark/core/plugin/BUILD.bazel b/src/test/starlark/core/plugin/BUILD.bazel new file mode 100644 index 000000000..9a78d311f --- /dev/null +++ b/src/test/starlark/core/plugin/BUILD.bazel @@ -0,0 +1,5 @@ +load(":test.bzl", "test_suite") + +test_suite( + name = "plugin", +) diff --git a/src/test/starlark/core/plugin/subjects.bzl b/src/test/starlark/core/plugin/subjects.bzl new file mode 100644 index 000000000..06541b3b5 --- /dev/null +++ b/src/test/starlark/core/plugin/subjects.bzl @@ -0,0 +1,37 @@ +load("@rules_testing//lib:truth.bzl", "subjects") + +def plugin_option_subject_factory(value, meta): + return subjects.struct( + value, + meta = meta.derive("option"), + attrs = { + "id": subjects.str, + "value": subjects.str, + }, + ) + +def plugin_subject_factory(value, meta): + return subjects.struct( + value, + meta = meta, + attrs = { + "id": subjects.str, + "plugin_jars": subjects.collection, + "classpath": subjects.collection, + "stubs": subjects.bool, + "compile": subjects.bool, + "options": subjects.collection, + }, + ) + +def plugin_configuration_subject_factory(value, meta): + return subjects.struct( + value, + meta = meta, + attrs = { + "id": subjects.str, + "options": subjects.collection, + "classpath": subjects.collection, + "data": subjects.collection, + }, + ) diff --git a/src/test/starlark/core/plugin/test.bzl b/src/test/starlark/core/plugin/test.bzl new file mode 100644 index 000000000..9716afcc6 --- /dev/null +++ b/src/test/starlark/core/plugin/test.bzl @@ -0,0 +1,382 @@ +load("//src/main/starlark/core/plugin:providers.bzl", "KspPluginInfo", "KtCompilerPluginInfo", "KtPluginConfiguration") +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:util.bzl", "util") +load("//kotlin:core.bzl", "kt_compiler_plugin", "kt_plugin_cfg") +load("//kotlin:jvm.bzl", "kt_jvm_import", "kt_jvm_library") +load("@rules_testing//lib:truth.bzl", "subjects") +load("//src/test/starlark:case.bzl", "suite") +load("//src/test/starlark:truth.bzl", "fail_messages_in", "flags_and_values_of") +load(":subjects.bzl", "plugin_configuration_subject_factory") + +def _provider_test_impl(env, target): + want_options = env.ctx.attr.want_options + got_target = env.expect.that_target(target) + got_target.has_provider(KtPluginConfiguration) + got_provider = got_target.provider(KtPluginConfiguration, plugin_configuration_subject_factory) + got_provider.options().transform(desc = "option.value", map_each = lambda o: o.value).contains_at_least(want_options) + got_provider.id().equals(env.ctx.attr.want_plugin[KtCompilerPluginInfo].id) + +def _action_test_impl(env, target): + action = env.expect.that_target(target).action_named(env.ctx.attr.on_action_mnemonic) + action.inputs().contains_at_least([f.short_path for f in env.ctx.files.want_inputs]) + flags_and_values_of(action).contains_at_least(env.ctx.attr.want_flags.items()) + +def _expect_failure(env, target): + fail_messages_in(env.expect.that_target(target)).contains_at_least(env.ctx.attr.want_failures) + +def plugin_for(test, name, deps = [], id = None, **kwargs): + plugin_jar = test.artifact( + name = name + ".jar", + ) + + deps = deps + [ + test.have( + kt_jvm_import, + name = name + "_plugin_jar", + jars = [ + plugin_jar, + ], + ), + ] + + plugin = test.have( + kt_compiler_plugin, + id = id if id else "plugin." + name, + name = name, + deps = deps, + **kwargs + ) + return (plugin, plugin_jar) + +def _test_kt_plugin_cfg(test): + plugin = test.have( + kt_compiler_plugin, + name = "plugin", + id = "test.stub", + options = { + "annotation": "plugin.StubForTesting", + }, + deps = [ + test.have( + kt_jvm_library, + name = "plugin_dep", + srcs = [ + test.artifact( + name = "plugin.kt", + ), + ], + ), + ], + ) + cfg_dep = test.have( + kt_jvm_library, + name = "cfg_dep", + srcs = [ + test.artifact( + name = "dependency.kt", + ), + ], + ) + + cfg = test.got( + kt_plugin_cfg, + name = "got", + plugin = plugin, + options = { + "extra": "annotation", + }, + deps = [ + cfg_dep, + ], + ) + + analysis_test( + name = test.name, + impl = _provider_test_impl, + target = cfg, + attr_values = { + "want_plugin": plugin, + "want_deps": [cfg_dep], + "want_options": [ + "extra=annotation", + ], + }, + attrs = { + "want_plugin": attr.label(providers = [KtCompilerPluginInfo]), + "want_options": attr.string_list(), + "want_deps": attr.label_list(providers = [JavaInfo]), + }, + ) + +def _test_compile_configuration(test): + plugin_jar = test.artifact( + name = "plugin.jar", + ) + + plugin = test.have( + kt_compiler_plugin, + name = "plugin", + id = "test.stub", + options = { + "annotation": "plugin.StubForTesting", + }, + deps = [ + test.have( + kt_jvm_import, + name = "plugin_jar", + jars = [ + plugin_jar, + ], + ), + ], + ) + + dep_jar = test.artifact( + name = "dep.jar", + ) + + cfg = test.have( + kt_plugin_cfg, + name = "cfg", + plugin = plugin, + options = { + "-Dop": "koo", + }, + deps = [ + test.have( + kt_jvm_import, + name = "dep_jar", + jars = [ + dep_jar, + ], + ), + ], + ) + + got = test.got( + kt_jvm_library, + name = "got_library", + srcs = [ + test.artifact( + name = "got_library.kt", + ), + ], + plugins = [ + plugin, + cfg, + ], + ) + + analysis_test( + name = test.name, + impl = _action_test_impl, + target = got, + attr_values = { + "on_action_mnemonic": "KotlinCompile", + "want_flags": { + "--compiler_plugin_options": ["test.stub:annotation=plugin.StubForTesting", "test.stub:-Dop=koo"], + "--stubs_plugin_options": ["test.stub:annotation=plugin.StubForTesting", "test.stub:-Dop=koo"], + }, + "want_inputs": [ + plugin_jar, + dep_jar, + ], + }, + attrs = { + "on_action_mnemonic": attr.string(), + "want_flags": attr.string_list_dict(), + "want_inputs": attr.label_list(providers = [DefaultInfo], allow_files = True), + }, + ) + +def _test_compile_configuration_single_phase(test): + stub, stub_jar = plugin_for( + test, + name = "stub", + id = "plugin.stub", + compile_phase = False, + stubs_phase = True, + ) + + compile, compile_jar = plugin_for( + test, + name = "compile", + id = "plugin.compile", + stubs_phase = False, + compile_phase = True, + ) + + stub_cfg = test.have( + kt_plugin_cfg, + name = "stub_cfg", + plugin = stub, + options = { + "-Dop": "stub_only", + }, + ) + + compile_cfg = test.have( + kt_plugin_cfg, + name = "compile_cfg", + plugin = compile, + options = { + "-Dop": "compile_only", + }, + ) + + got = test.got( + kt_jvm_library, + name = "got_library", + srcs = [ + test.artifact( + name = "got_library.kt", + ), + ], + plugins = [ + stub, + compile, + compile_cfg, + stub_cfg, + ], + ) + + analysis_test( + name = test.name, + impl = _action_test_impl, + target = got, + attr_values = { + "on_action_mnemonic": "KotlinCompile", + "want_flags": { + "--compiler_plugin_options": ["plugin.compile:-Dop=compile_only"], + "--stubs_plugin_options": ["plugin.stub:-Dop=stub_only"], + }, + "want_inputs": [ + stub_jar, + compile_jar, + ], + }, + attrs = { + "on_action_mnemonic": attr.string(), + "want_flags": attr.string_list_dict(), + "want_inputs": attr.label_list(providers = [DefaultInfo], allow_files = True), + }, + ) + +def _test_library_multiple_plugins_with_same_id(test): + got = test.got( + kt_jvm_library, + name = "got_library", + srcs = [ + test.artifact( + name = "got_library.kt", + ), + ], + plugins = [ + test.have( + kt_compiler_plugin, + name = "one", + id = "test.stub", + options = { + "annotation": "plugin.StubForTesting", + }, + deps = [ + test.have( + kt_jvm_import, + name = "one_plugin_jar", + jars = [ + test.artifact( + name = "one_plugin.jar", + ), + ], + ), + ], + ), + test.have( + kt_compiler_plugin, + name = "two", + id = "test.stub", + options = { + "annotation": "plugin.StubForTesting", + }, + deps = [ + test.have( + kt_jvm_import, + name = "two_plugin_jar", + jars = [ + test.artifact( + name = "two_plugin.jar", + ), + ], + ), + ], + ), + ], + ) + + analysis_test( + name = test.name, + impl = _expect_failure, + expect_failure = True, + target = got, + attr_values = { + "want_failures": [ + "has multiple plugins with the same id: test.stub.", + ], + }, + attrs = { + "want_failures": attr.string_list(), + }, + ) + +def _test_cfg_without_plugin(test): + adee, _ = plugin_for( + test, + name = "Adee", + id = "adee.see", + ) + adee_cfg = test.have( + kt_plugin_cfg, + name = "adee_cfg", + plugin = adee, + options = { + "-Dop": "compile_only", + }, + ) + + got = test.got( + kt_jvm_library, + name = "got_library", + srcs = [ + test.artifact( + name = "got_library.kt", + ), + ], + plugins = [ + adee_cfg, + ], + ) + + analysis_test( + name = test.name, + impl = _expect_failure, + expect_failure = True, + target = got, + attr_values = { + "want_failures": [ + "has plugin configurations without corresponding plugins: [\"%s: adee.see\"]" % Label(adee_cfg), + ], + }, + attrs = { + "want_failures": attr.string_list(), + }, + ) + +def test_suite(name): + suite( + name, + _test_kt_plugin_cfg, + _test_compile_configuration, + _test_library_multiple_plugins_with_same_id, + _test_compile_configuration_single_phase, + _test_cfg_without_plugin, + ) diff --git a/src/test/starlark/truth.bzl b/src/test/starlark/truth.bzl new file mode 100644 index 000000000..42cf81eb7 --- /dev/null +++ b/src/test/starlark/truth.bzl @@ -0,0 +1,41 @@ +""" +Collection of utility functions for the action subject +""" + +def fail_messages_in(target_subject): + return target_subject.failures().transform( + desc = "failure.message", + map_each = lambda f: f.partition("Error in fail:")[2].strip() if "Error in fail:" in f else f, + ) + +def flags_and_values_of(action_subject): + return action_subject.argv().transform(desc = "parsed()", loop = _action_subject_parse_flags) + +def _action_subject_parse_flags(argv): + parsed_flags = {} + + # argv might be none for e.g. builtin actions + if argv == None: + return parsed_flags + last_flag = None + for arg in argv: + value = None + if arg == "--": + # skip the rest of the arguments, this is standard end of the flags. + break + if arg.startswith("-"): + if "=" in arg: + last_flag, value = arg.split("=", 1) + else: + last_flag = arg + elif last_flag: + # have a flag, therefore this is probably an associated argument + value = arg + else: + # skip non-flag arguments + continue + + # only set the value if it exists + if value: + parsed_flags.setdefault(last_flag, []).append(value) + return parsed_flags.items()