Skip to content

Add initial support for Kotlin Native toolchain and klib compilation #1351

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

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
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
10 changes: 10 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ bazel_dep(name = "rules_shell", version = "0.4.1")

bazel_dep(name = "buildifier_prebuilt", version = "8.0.3", dev_dependency = True)

bazel_dep(name = "aspect_bazel_lib", version = "2.19.4")
Copy link
Author

@smocherla-brex smocherla-brex Jul 13, 2025

Choose a reason for hiding this comment

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

I added this to leverage copy_to_directory to expose the Kotlin native compiler distribution as a directory in the toolchain here (as it's massive and has a ton of files)

Copy link
Collaborator

@restingbull restingbull Jul 15, 2025

Choose a reason for hiding this comment

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

Please do not use copy_to_directory it's basically antagonistic to RBE.

copy_to_directory uses uses a tree artifact (via declare_directory which is, under the hood, a tarball.) It doesn't save time or number of files. Why? Because bazel only works with files, rather than directories. The intent of that feature is handle actions where the outputs are unknown before execution. copy_to_directory does know the inputs (all the files in the glob), and then applies filters etc to the them via an action... Which means we take a well known set of inputs, execute an action, place them in a tarball, and then unpack them everytime we need them (give or take -- there are some optimizations under the hood.)

For RBE, this is ugly: first, transfer all the files into a remote action; second, tar all the outputs and place them in CAS. Given that the files have not changed, this bloats the CAS size, increases transfer costs, adds additional actions... for no good reason, other than laziness on the rule writers part. After all, copy_to_directory could be written in starlark to do the filtering... or the rules could handle the filtering during globbing, or...

You get the idea.


rules_java_toolchains = use_extension("@rules_java//java:extensions.bzl", "toolchains")
use_repo(rules_java_toolchains, "remote_java_tools")

Expand All @@ -28,6 +30,14 @@ use_repo(
"com_github_google_ksp",
"com_github_jetbrains_kotlin",
"com_github_jetbrains_kotlin_git",
"com_github_jetbrains_kotlin_native_linux_x86_64",
"com_github_jetbrains_kotlin_native_linux_x86_64_git",
"com_github_jetbrains_kotlin_native_macos_aarch64",
"com_github_jetbrains_kotlin_native_macos_aarch64_git",
"com_github_jetbrains_kotlin_native_macos_x86_64",
"com_github_jetbrains_kotlin_native_macos_x86_64_git",
"com_github_jetbrains_kotlin_native_windows_x86_64",
"com_github_jetbrains_kotlin_native_windows_x86_64_git",
"com_github_pinterest_ktlint",
"kotlin_build_tools_impl",
"kotlinx_serialization_core_jvm",
Expand Down
35 changes: 25 additions & 10 deletions kotlin/compiler/compiler.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.

load("@com_github_jetbrains_kotlin//:artifacts.bzl", "KOTLINC_ARTIFACTS")
load("@com_github_jetbrains_kotlin//:artifacts.bzl", "KOTLINC_ARTIFACTS", "KOTLIN_NATIVE_ARTIFACTS")
load("//kotlin:jvm.bzl", "kt_jvm_import")
load("//kotlin/internal:defs.bzl", _KT_COMPILER_REPO = "KT_COMPILER_REPO")
load("//kotlin/internal:defs.bzl", _KT_COMPILER_REPO = "KT_COMPILER_REPO", _KT_NATIVE_COMPILER_REPO_PREFIX = "KT_NATIVE_COMPILER_REPO_PREFIX")

KOTLIN_STDLIBS = [
"//kotlin/compiler:annotations",
Expand All @@ -25,18 +25,18 @@ KOTLIN_STDLIBS = [
"//kotlin/compiler:trove4j",
]

def _import_artifacts(artifacts, rule_kind):
_import_labels(artifacts.plugin, rule_kind)
_import_labels(artifacts.runtime, rule_kind)
_import_labels(artifacts.compile, rule_kind, neverlink = 1)
def _import_artifacts(artifacts, rule_kind, compiler_repo = _KT_COMPILER_REPO):
_import_labels(artifacts.plugin, rule_kind, compiler_repo)
_import_labels(artifacts.runtime, rule_kind, compiler_repo)
_import_labels(artifacts.compile, rule_kind, compiler_repo, neverlink = 1)

def _import_labels(labels, rule_kind, **rule_args):
def _import_labels(labels, rule_kind, compiler_repo, **rule_args):
for (label, file) in labels.items():
if not file.endswith(".jar"):
native.filegroup(
name = label,
srcs = [
"@%s//:%s" % (_KT_COMPILER_REPO, label),
"@%s//:%s" % (compiler_repo, label),
],
)
return
Expand All @@ -46,10 +46,10 @@ def _import_labels(labels, rule_kind, **rule_args):
args = dict(rule_args.items())
args["visibility"] = ["//visibility:public"]
args["name"] = label
args["jars"] = ["@%s//:%s" % (_KT_COMPILER_REPO, label)]
args["jars"] = ["@%s//:%s" % (compiler_repo, label)]
sources = label + "-sources"
if sources in labels:
args["srcjar"] = "@%s//:%s" % (_KT_COMPILER_REPO, sources)
args["srcjar"] = "@%s//:%s" % (compiler_repo, sources)
rule_kind(**args)

def kt_configure_compiler():
Expand All @@ -63,3 +63,18 @@ def kt_configure_compiler():

_import_artifacts(KOTLINC_ARTIFACTS.jvm, kt_jvm_import)
_import_artifacts(KOTLINC_ARTIFACTS.core, kt_jvm_import)
_import_artifacts(KOTLIN_NATIVE_ARTIFACTS.linux_x86_64, kt_jvm_import, compiler_repo = _KT_NATIVE_COMPILER_REPO_PREFIX + "_linux_x86_64")
_import_artifacts(KOTLIN_NATIVE_ARTIFACTS.macos_x86_64, kt_jvm_import, compiler_repo = _KT_NATIVE_COMPILER_REPO_PREFIX + "_macos_x86_64")
_import_artifacts(KOTLIN_NATIVE_ARTIFACTS.macos_aarch64, kt_jvm_import, compiler_repo = _KT_NATIVE_COMPILER_REPO_PREFIX + "_macos_aarch64")
_import_artifacts(KOTLIN_NATIVE_ARTIFACTS.windows_x86_64, kt_jvm_import, compiler_repo = _KT_NATIVE_COMPILER_REPO_PREFIX + "_windows_x86_64")

# a convenience alias for kotlin-native to be referenced in other places
native.alias(
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is the usecase for this? It's far preferable to use specific toolchains.

actual = select({
"@bazel_tools//src/conditions:linux_x86_64": "//kotlin/compiler:kotlin-native-linux-x86_64",
"@bazel_tools//src/conditions:darwin": "//kotlin/compiler:kotlin-native-macos-x86_64",
"@bazel_tools//src/conditions:windows": "//kotlin/compiler:kotlin-native-windows_x86_64",
"@bazel_tools//src/conditions:darwin_arm64": "//kotlin/compiler:kotlin-native-macos_aarch64",
}),
name = "kotlin-native",
)
6 changes: 6 additions & 0 deletions kotlin/internal/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ load(
_JAVA_RUNTIME_TOOLCHAIN_TYPE = "JAVA_RUNTIME_TOOLCHAIN_TYPE",
_JAVA_TOOLCHAIN_TYPE = "JAVA_TOOLCHAIN_TYPE",
_KtJvmInfo = "KtJvmInfo",
_KtKlibInfo = "KtKlibInfo",
)
load(
"//src/main/starlark/core/plugin:providers.bzl",
Expand All @@ -35,6 +36,9 @@ JAVA_RUNTIME_TOOLCHAIN_TYPE = _JAVA_RUNTIME_TOOLCHAIN_TYPE
# The name of the Kotlin compiler workspace.
KT_COMPILER_REPO = "com_github_jetbrains_kotlin"

# The preifx of the Kotlin native compiler workspace name (will be suffixed with the platform)
KT_NATIVE_COMPILER_REPO_PREFIX = "com_github_jetbrains_kotlin_native"

# The name of the KSP compiler plugin workspace
KSP_COMPILER_PLUGIN_REPO = "com_github_google_ksp"

Expand All @@ -47,3 +51,5 @@ KspPluginInfo = _KspPluginInfo
KtCompilerPluginOption = _KtCompilerPluginOption

KtPluginConfiguration = _KtPluginConfiguration

KtKlibInfo = _KtKlibInfo
Empty file.
78 changes: 78 additions & 0 deletions kotlin/internal/klib/klib.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
load("//kotlin/internal:defs.bzl", _KtKlibInfo = "KtKlibInfo", _TOOLCHAIN_TYPE = "TOOLCHAIN_TYPE")
load("//kotlin/internal/utils:utils.bzl", "utils")

def _kt_klib_library(ctx):
module_name = utils.derive_module_name(ctx)
builder_args = utils.init_args(ctx, "kt_klib_library", module_name)

klib = ctx.actions.declare_file("{}.klib".format(ctx.label.name))
outputs = [klib]

toolchains = ctx.toolchains[_TOOLCHAIN_TYPE]
deps_klibs = []
for dep in ctx.attr.deps:
deps_klibs.append(dep[_KtKlibInfo].klibs)
libraries = depset(transitive = deps_klibs)
builder_args.add_all("--sources", ctx.files.srcs)
builder_inputs, _, input_manifests = ctx.resolve_command(tools = [toolchains.kotlinbuilder, toolchains.konan_home])

builder_args.add("--strict_kotlin_deps", "off")
builder_args.add("--reduced_classpath_mode", "off")
builder_args.add("--output_klib", klib.path)

deps_klibs = []
for dep in ctx.attr.deps:
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is duplicated from line #13?

deps_klibs.append(dep[_KtKlibInfo].klibs)
libraries = depset(transitive = deps_klibs)
builder_args.add_all("--klibs", libraries, omit_if_empty = False)

# This will be a directory we need to propagate to the compiler
konan_home = toolchains.konan_home[DefaultInfo].files.to_list()[0]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Rule of thumb: if you unpack a depset with to_list, you are doing it wrong.

Let's step back and ask the question: does starlark need to know about konan home?

Or can we derive it in the action? I suspect we could just pass the files into a well known place and work from there.

if not konan_home.is_directory:
fail("konan home must be a directory!")

ctx.actions.run(
mnemonic = "KotlinKlibCompile",
inputs = depset(builder_inputs + ctx.files.srcs, transitive = [libraries]),
outputs = outputs,
executable = toolchains.kotlinbuilder.files_to_run.executable,
tools = [
toolchains.kotlinbuilder.files_to_run,
toolchains.konan_home[DefaultInfo].files_to_run,
],
execution_requirements = {"supports-workers": "1"},
arguments = [ctx.actions.args().add_all(toolchains.builder_args), builder_args],
progress_message = "Compiling Kotlin to Klib %%{label} { kt: %d }" % len(ctx.files.srcs),
input_manifests = input_manifests,
env = {
"REPOSITORY_NAME": utils.builder_workspace_name(ctx),
"KONAN_HOME": konan_home.path,
},
)

return [
DefaultInfo(files = depset(outputs)),
_KtKlibInfo(
klibs = depset(outputs),
Copy link
Collaborator

Choose a reason for hiding this comment

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

You'll want to include the transitive outputs here.

),
]

kt_klib_library = rule(
Copy link
Author

@smocherla-brex smocherla-brex Jul 13, 2025

Choose a reason for hiding this comment

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

This is the new kt_klib_library rule that produces klibs using the kotlin/native toolchain. Potentially we could just have it as action and have an attr on the existing rules to trigger klib compilation on them as well (I'm not sure if that's better - let me know)

Copy link
Collaborator

Choose a reason for hiding this comment

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

kt_library fits the naming convention: kt_js_library, kt_jvm_library.

kotlin seems to be moving towards using klib as the core intermediate format (js and native), so I suspect java may follow at some point.

implementation = _kt_klib_library,
doc = """
This rule is intended to leverage the new Kotlin IR backend to allow for compiling platform-independent Kotlin code
to be shared between Kotlin code for different platforms (JS/JVM/WASM etc.). It produces a klib file as the output.
""",
attrs = {
"srcs": attr.label_list(
doc = "A list of source files to be compiled to klib",
allow_files = [".kt"],
),
"deps": attr.label_list(
doc = "A list of other kt_klib_library targets that this library depends on for compilation",
providers = [_KtKlibInfo],
),
},
toolchains = [_TOOLCHAIN_TYPE],
provides = [_KtKlibInfo],
)
12 changes: 12 additions & 0 deletions kotlin/internal/toolchains.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ load("@rules_java//java:defs.bzl", "JavaInfo", "java_common")
load(
"//kotlin/internal:defs.bzl",
_KT_COMPILER_REPO = "KT_COMPILER_REPO",
_KT_NATIVE_COMPILER_REPO_PREFIX = "KT_NATIVE_COMPILER_REPO_PREFIX",
_TOOLCHAIN_TYPE = "TOOLCHAIN_TYPE",
)
load(
Expand Down Expand Up @@ -79,6 +80,7 @@ def _kotlin_toolchain_impl(ctx):
],
jdeps_merger = ctx.attr.jdeps_merger,
kotlin_home = ctx.attr.kotlin_home,
konan_home = ctx.attr.konan_home,
jvm_stdlibs = java_common.merge(compile_time_providers + runtime_providers),
jvm_emit_jdeps = ctx.attr._jvm_emit_jdeps[BuildSettingInfo].value,
execution_requirements = {
Expand Down Expand Up @@ -115,6 +117,10 @@ _kt_toolchain = rule(
default = Label("@" + _KT_COMPILER_REPO + "//:home"),
allow_files = True,
),
"konan_home": attr.label(
doc = "the filegroup defining the konan/kotlin-native home",
allow_files = True,
),
"kotlinbuilder": attr.label(
doc = "the kotlin builder executable",
default = Label("//src/main/kotlin:build"),
Expand Down Expand Up @@ -379,6 +385,12 @@ def define_kt_toolchain(
jvm_runtime = jvm_runtime if jvm_runtime != None else [
Label("//kotlin/compiler:kotlin-stdlib"),
],
konan_home = select({
Copy link
Collaborator

Choose a reason for hiding this comment

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

Each of these should be a separate toolchain with different platform constraints.

If nothing else, it will make debugging toolchain issues sane.

Copy link
Author

Choose a reason for hiding this comment

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

Makes sense to put it into its own toolchain.

"@bazel_tools//src/conditions:linux_x86_64": Label("@" + _KT_NATIVE_COMPILER_REPO_PREFIX + "_linux_x86_64//:konan_home"),
"@bazel_tools//src/conditions:darwin_arm64": Label("@" + _KT_NATIVE_COMPILER_REPO_PREFIX + "_macos_aarch64//:konan_home"),
"@bazel_tools//src/conditions:darwin_x86_64": Label("@" + _KT_NATIVE_COMPILER_REPO_PREFIX + "_macos_x86_64//:konan_home"),
"@bazel_tools//src/conditions:windows": Label("@" + _KT_NATIVE_COMPILER_REPO_PREFIX + "_windows_x86_64//:konan_home"),
}),
)
native.toolchain(
name = name,
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/io/bazel/kotlin/builder/cmd/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ kt_bootstrap_binary(
"//kotlin/compiler:jvm-abi-gen",
"//kotlin/compiler:kotlin-annotation-processing",
"//kotlin/compiler:kotlin-compiler",
"//kotlin/compiler:kotlin-native",
"//kotlin/compiler:kotlin-reflect",
"//kotlin/compiler:symbol-processing-api",
"//kotlin/compiler:symbol-processing-cmdline",
Expand All @@ -38,6 +39,7 @@ kt_bootstrap_binary(
"-D@com_github_jetbrains_kotlin...build-tools-impl=$(rlocationpath @kotlin_build_tools_impl//jar)",
"-D@com_github_jetbrains_kotlin...jvm-abi-gen=$(rlocationpath //kotlin/compiler:jvm-abi-gen)",
"-D@com_github_jetbrains_kotlin...kotlin-compiler=$(rlocationpath //kotlin/compiler:kotlin-compiler)",
"-D@com_github_jetbrains_kotlin_native...kotlin-native=$(rlocationpath //kotlin/compiler:kotlin-native)",
"-D@com_github_jetbrains_kotlin...kapt=$(rlocationpath //kotlin/compiler:kotlin-annotation-processing)",
"-D@rules_kotlin...jdeps-gen=$(rlocationpath //src/main/kotlin:jdeps-gen)",
"-D@rules_kotlin...skip-code-gen=$(rlocationpath //src/main/kotlin:skip-code-gen)",
Expand Down
41 changes: 41 additions & 0 deletions src/main/kotlin/io/bazel/kotlin/builder/tasks/KotlinBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package io.bazel.kotlin.builder.tasks

import io.bazel.kotlin.builder.tasks.jvm.KotlinJvmTaskExecutor
import io.bazel.kotlin.builder.tasks.klib.KotlinKlibTaskExecutor
import io.bazel.kotlin.builder.toolchain.CompilationStatusException
import io.bazel.kotlin.builder.toolchain.CompilationTaskContext
import io.bazel.kotlin.builder.utils.ArgMap
Expand All @@ -26,6 +27,7 @@ import io.bazel.kotlin.builder.utils.partitionJvmSources
import io.bazel.kotlin.builder.utils.resolveNewDirectories
import io.bazel.kotlin.model.CompilationTaskInfo
import io.bazel.kotlin.model.JvmCompilationTask
import io.bazel.kotlin.model.KlibCompilationTask
import io.bazel.kotlin.model.Platform
import io.bazel.kotlin.model.RuleKind
import io.bazel.worker.WorkerContext
Expand All @@ -43,6 +45,7 @@ class KotlinBuilder
@Inject
internal constructor(
private val jvmTaskExecutor: KotlinJvmTaskExecutor,
private val klibTaskExecutor: KotlinKlibTaskExecutor,
) {
companion object {
@JvmStatic
Expand Down Expand Up @@ -89,6 +92,8 @@ class KotlinBuilder
INSTRUMENT_COVERAGE("--instrument_coverage"),
KSP_GENERATED_JAVA_SRCJAR("--ksp_generated_java_srcjar"),
BUILD_TOOLS_API("--build_tools_api"),
KLIBS("--klibs"),
OUTPUT_KLIB("--output_klib"),
}
}

Expand All @@ -105,6 +110,7 @@ class KotlinBuilder
Platform.JVM,
Platform.ANDROID,
-> executeJvmTask(compileContext, taskContext.directory, argMap)
Platform.KLIB -> executeKlibTask(compileContext, taskContext.directory, argMap)
Platform.UNRECOGNIZED -> throw IllegalStateException(
"unrecognized platform: ${compileContext.info}",
)
Expand Down Expand Up @@ -189,6 +195,41 @@ class KotlinBuilder
jvmTaskExecutor.execute(context, task)
}

private fun executeKlibTask(
context: CompilationTaskContext,
workingDir: Path,
argMap: ArgMap,
) {
val task = buildKlibTask(context.info, workingDir, argMap)
context.whenTracing { printProto("klib common task input", task) }
buildKlibTask(context.info, workingDir, argMap)
klibTaskExecutor.execute(context, task)
}

private fun buildKlibTask(
info: CompilationTaskInfo,
workingDir: Path,
argMap: ArgMap,
): KlibCompilationTask =
with(KlibCompilationTask.newBuilder()) {
this.info = info
with(directoriesBuilder) {
temp = workingDir.toString()
}

with(inputsBuilder) {
argMap.optional(KotlinBuilderFlags.KLIBS)?.let {
addAllLibraries(it)
}
addAllKotlinSources(argMap.mandatory(KotlinBuilderFlags.SOURCES))
}

with(outputsBuilder) {
this.setKlib(argMap.mandatorySingle(KotlinBuilderFlags.OUTPUT_KLIB))
}
build()
}

private fun buildJvmTask(
info: CompilationTaskInfo,
workingDir: Path,
Expand Down
Loading