Improve build logic for Kotlin (#1520)

- Enforce Kotlin version via resolution rule (replaces BOM)
  - fail if kotlin-stdlib/kotlin-reflect exceed target version
- Replace kotlin-stdlib-jdk8 with kotlin-stdlib (jdk7/8 are now shims)
- Port pkl-core annotation processor to Java (with Codex)
- removes kotlin-stdlib from its compile classpath for better dependency
hygiene (Java module)
- Downgrade clikt for Kotlin 2.2 compatibility
- Upgrade kotlinx-serialization

---------

Co-authored-by: Daniel Chao <dan.chao@apple.com>
This commit is contained in:
odenix
2026-04-15 17:02:42 +01:00
committed by GitHub
parent 2e0b4a3a97
commit 04a9cc90d2
24 changed files with 558 additions and 456 deletions
@@ -0,0 +1,272 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* 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.
*/
package org.pkl.core.generator;
import com.oracle.truffle.api.dsl.GeneratedBy;
import com.palantir.javapoet.ClassName;
import com.palantir.javapoet.JavaFile;
import com.palantir.javapoet.MethodSpec;
import com.palantir.javapoet.TypeSpec;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
/**
* Generates a subclass of {@code org.pkl.core.stdlib.registry.ExternalMemberRegistry} for each
* stdlib module and a factory to instantiate them. Generated classes are written to {@code
* generated/truffle/org/pkl/core/stdlib/registry}.
*
* <p>Inputs:
*
* <ul>
* <li>Generated Truffle node classes for stdlib members. These classes are located in subpackages
* of {@code org.pkl.core.stdlib} and identified via their {@code @GeneratedBy} annotations.
* <li>{@code @PklName} annotations on handwritten node classes from which Truffle node classes
* are generated.
* </ul>
*/
public final class MemberRegistryGenerator extends AbstractProcessor {
private static final String TRUFFLE_NODE_CLASS_SUFFIX = "NodeGen";
private static final String TRUFFLE_NODE_FACTORY_SUFFIX = "NodesFactory";
private static final String STDLIB_PACKAGE_NAME = "org.pkl.core.stdlib";
private static final String REGISTRY_PACKAGE_NAME = STDLIB_PACKAGE_NAME + ".registry";
private static final String MODULE_PACKAGE_NAME = "org.pkl.core.module";
private static final ClassName EXTERNAL_MEMBER_REGISTRY_CLASS_NAME =
ClassName.get(REGISTRY_PACKAGE_NAME, "ExternalMemberRegistry");
private static final ClassName EMPTY_MEMBER_REGISTRY_CLASS_NAME =
ClassName.get(REGISTRY_PACKAGE_NAME, "EmptyMemberRegistry");
private static final ClassName MEMBER_REGISTRY_FACTORY_CLASS_NAME =
ClassName.get(REGISTRY_PACKAGE_NAME, "MemberRegistryFactory");
private static final ClassName MODULE_KEY_CLASS_NAME =
ClassName.get(MODULE_PACKAGE_NAME, "ModuleKey");
private static final ClassName MODULE_KEYS_CLASS_NAME =
ClassName.get(MODULE_PACKAGE_NAME, "ModuleKeys");
@Override
public Set<String> getSupportedAnnotationTypes() {
return Set.of(GeneratedBy.class.getName());
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_17;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (annotations.isEmpty()) {
return true;
}
var nodeClassesByPackage = collectNodeClasses(roundEnv);
generateRegistryClasses(nodeClassesByPackage);
generateRegistryFactoryClass(nodeClassesByPackage.keySet());
return true;
}
private Map<PackageElement, List<TypeElement>> collectNodeClasses(RoundEnvironment roundEnv) {
var nodeClasses = new ArrayList<TypeElement>();
for (var element : roundEnv.getElementsAnnotatedWith(GeneratedBy.class)) {
if (!(element instanceof TypeElement typeElement)) {
continue;
}
if (!typeElement.getQualifiedName().toString().startsWith(STDLIB_PACKAGE_NAME)) {
continue;
}
if (!typeElement.getSimpleName().toString().endsWith(TRUFFLE_NODE_CLASS_SUFFIX)) {
continue;
}
nodeClasses.add(typeElement);
}
nodeClasses.sort(
Comparator.comparing(
(TypeElement element) -> {
var enclosingElement = element.getEnclosingElement();
return enclosingElement.getKind() == ElementKind.PACKAGE
? ""
: enclosingElement.getSimpleName().toString();
})
.thenComparing(element -> element.getSimpleName().toString()));
var result = new LinkedHashMap<PackageElement, List<TypeElement>>();
for (var nodeClass : nodeClasses) {
var pkg = processingEnv.getElementUtils().getPackageOf(nodeClass);
result.computeIfAbsent(pkg, ignored -> new ArrayList<>()).add(nodeClass);
}
return result;
}
private void generateRegistryClasses(
Map<PackageElement, List<TypeElement>> nodeClassesByPackage) {
for (var entry : nodeClassesByPackage.entrySet()) {
generateRegistryClass(entry.getKey(), entry.getValue());
}
}
private void generateRegistryClass(PackageElement pkg, List<TypeElement> nodeClasses) {
var pklModuleName = getAnnotatedPklName(pkg);
if (pklModuleName == null) {
pklModuleName = pkg.getSimpleName().toString();
}
var pklModuleNameCapitalized = capitalize(pklModuleName);
var registryClassName =
ClassName.get(REGISTRY_PACKAGE_NAME, pklModuleNameCapitalized + "MemberRegistry");
TypeSpec.Builder registryClass =
TypeSpec.classBuilder(registryClassName)
.addJavadoc("Generated by {@link $L}.\n", getClass().getName())
.addModifiers(Modifier.FINAL)
.superclass(EXTERNAL_MEMBER_REGISTRY_CLASS_NAME);
var constructor = MethodSpec.constructorBuilder();
for (var nodeClass : nodeClasses) {
var enclosingClass = nodeClass.getEnclosingElement();
var pklClassName = getAnnotatedPklName(enclosingClass);
if (pklClassName == null) {
pklClassName =
stripSuffix(enclosingClass.getSimpleName().toString(), TRUFFLE_NODE_FACTORY_SUFFIX);
}
var pklMemberName = getAnnotatedPklName(nodeClass);
if (pklMemberName == null) {
pklMemberName =
stripSuffix(nodeClass.getSimpleName().toString(), TRUFFLE_NODE_CLASS_SUFFIX);
}
String pklMemberNameQualified;
if (pklClassName.equals(pklModuleNameCapitalized)) {
pklMemberNameQualified = "pkl." + pklModuleName + "#" + pklMemberName;
} else {
pklMemberNameQualified = "pkl." + pklModuleName + "#" + pklClassName + "." + pklMemberName;
}
registryClass.addOriginatingElement(nodeClass);
constructor.addStatement("register($S, $T::create)", pklMemberNameQualified, nodeClass);
}
registryClass.addMethod(constructor.build());
writeJavaFile(REGISTRY_PACKAGE_NAME, registryClass.build());
}
private void generateRegistryFactoryClass(Collection<PackageElement> packages) {
var registryFactoryClass =
TypeSpec.classBuilder(MEMBER_REGISTRY_FACTORY_CLASS_NAME)
.addJavadoc("Generated by {@link $L}.\n", getClass().getName())
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
registryFactoryClass.addMethod(
MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build());
MethodSpec.Builder getMethod =
MethodSpec.methodBuilder("get")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(MODULE_KEY_CLASS_NAME, "moduleKey")
.returns(EXTERNAL_MEMBER_REGISTRY_CLASS_NAME)
.beginControlFlow("if (!$T.isStdLibModule(moduleKey))", MODULE_KEYS_CLASS_NAME)
.addStatement("return $T.INSTANCE", EMPTY_MEMBER_REGISTRY_CLASS_NAME)
.endControlFlow()
.beginControlFlow("switch (moduleKey.getUri().getSchemeSpecificPart())");
for (var pkg : packages) {
var pklModuleName = getAnnotatedPklName(pkg);
if (pklModuleName == null) {
pklModuleName = pkg.getSimpleName().toString();
}
var pklModuleNameCapitalized = capitalize(pklModuleName);
var registryClassName =
ClassName.get(REGISTRY_PACKAGE_NAME, pklModuleNameCapitalized + "MemberRegistry");
registryFactoryClass.addOriginatingElement(pkg);
getMethod.addCode("case $S:\n", pklModuleName);
getMethod.addStatement(" return new $T()", registryClassName);
}
getMethod
.addCode("default:\n")
.addStatement(" return $T.INSTANCE", EMPTY_MEMBER_REGISTRY_CLASS_NAME)
.endControlFlow();
registryFactoryClass.addMethod(getMethod.build());
writeJavaFile(REGISTRY_PACKAGE_NAME, registryFactoryClass.build());
}
private String getAnnotatedPklName(Element element) {
for (var annotation : element.getAnnotationMirrors()) {
var annotationName = annotation.getAnnotationType().asElement().getSimpleName().toString();
if (annotationName.equals("PklName")) {
return firstAnnotationValue(annotation).getValue().toString();
}
if (annotationName.equals("GeneratedBy")) {
var value = firstAnnotationValue(annotation).getValue();
if (value instanceof TypeMirror typeMirror) {
var generatedByElement = processingEnv.getTypeUtils().asElement(typeMirror);
if (generatedByElement != null) {
return getAnnotatedPklName(generatedByElement);
}
}
}
}
return null;
}
private static AnnotationValue firstAnnotationValue(AnnotationMirror annotation) {
return annotation.getElementValues().values().iterator().next();
}
private void writeJavaFile(String packageName, TypeSpec typeSpec) {
try {
JavaFile.builder(packageName, typeSpec).build().writeTo(processingEnv.getFiler());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static String stripSuffix(String value, String suffix) {
return value.endsWith(suffix) ? value.substring(0, value.length() - suffix.length()) : value;
}
private static String capitalize(String value) {
if (value.isEmpty()) {
return value;
}
return Character.toUpperCase(value.charAt(0)) + value.substring(1);
}
}
@@ -1,191 +0,0 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* 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.
*/
package org.pkl.core.generator
import com.oracle.truffle.api.dsl.GeneratedBy
import com.palantir.javapoet.ClassName
import com.palantir.javapoet.JavaFile
import com.palantir.javapoet.MethodSpec
import com.palantir.javapoet.TypeSpec
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion
import javax.lang.model.element.*
import javax.lang.model.type.TypeMirror
/**
* Generates a subclass of `org.pkl.core.stdlib.registry.ExternalMemberRegistry` for each stdlib
* module and a factory to instantiate them. Generated classes are written to
* `generated/truffle/org/pkl/core/stdlib/registry`.
*
* Inputs:
* - Generated Truffle node classes for stdlib members. These classes are located in subpackages of
* `org.pkl.core.stdlib` and identified via their `@GeneratedBy` annotations.
* - `@PklName` annotations on handwritten node classes from which Truffle node classes are
* generated.
*/
class MemberRegistryGenerator : AbstractProcessor() {
private val truffleNodeClassSuffix = "NodeGen"
private val truffleNodeFactorySuffix = "NodesFactory"
private val stdLibPackageName: String = "org.pkl.core.stdlib"
private val registryPackageName: String = "$stdLibPackageName.registry"
private val modulePackageName: String = "org.pkl.core.module"
private val externalMemberRegistryClassName: ClassName =
ClassName.get(registryPackageName, "ExternalMemberRegistry")
private val emptyMemberRegistryClassName: ClassName =
ClassName.get(registryPackageName, "EmptyMemberRegistry")
private val memberRegistryFactoryClassName: ClassName =
ClassName.get(registryPackageName, "MemberRegistryFactory")
private val moduleKeyClassName: ClassName = ClassName.get(modulePackageName, "ModuleKey")
private val moduleKeysClassName: ClassName = ClassName.get(modulePackageName, "ModuleKeys")
override fun getSupportedAnnotationTypes(): Set<String> = setOf(GeneratedBy::class.java.name)
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.RELEASE_17
override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
if (annotations.isEmpty()) return true
val nodeClassesByPackage = collectNodeClasses(roundEnv)
generateRegistryClasses(nodeClassesByPackage)
generateRegistryFactoryClass(nodeClassesByPackage.keys)
return true
}
private fun collectNodeClasses(roundEnv: RoundEnvironment) =
roundEnv
.getElementsAnnotatedWith(GeneratedBy::class.java)
.asSequence()
.filterIsInstance<TypeElement>()
.filter { it.qualifiedName.toString().startsWith(stdLibPackageName) }
.filter { it.simpleName.toString().endsWith(truffleNodeClassSuffix) }
.sortedWith(
compareBy(
{
if (it.enclosingElement.kind == ElementKind.PACKAGE) ""
else it.enclosingElement.simpleName.toString()
},
{ it.simpleName.toString() },
)
)
.groupBy { processingEnv.elementUtils.getPackageOf(it) }
private fun generateRegistryClasses(
nodeClassesByPackage: Map<PackageElement, List<TypeElement>>
) {
for ((pkg, nodeClasses) in nodeClassesByPackage) {
generateRegistryClass(pkg, nodeClasses)
}
}
private fun generateRegistryClass(pkg: PackageElement, nodeClasses: List<TypeElement>) {
val pklModuleName = getAnnotatedPklName(pkg) ?: pkg.simpleName.toString()
val pklModuleNameCapitalized = pklModuleName.capitalize()
val registryClassName =
ClassName.get(registryPackageName, "${pklModuleNameCapitalized}MemberRegistry")
val registryClass =
TypeSpec.classBuilder(registryClassName)
.addJavadoc("Generated by {@link ${this::class.qualifiedName}}.")
.addModifiers(Modifier.FINAL)
.superclass(externalMemberRegistryClassName)
val registryClassConstructor = MethodSpec.constructorBuilder()
for (nodeClass in nodeClasses) {
val enclosingClass = nodeClass.enclosingElement
val pklClassName =
getAnnotatedPklName(enclosingClass)
?: enclosingClass.simpleName.toString().removeSuffix(truffleNodeFactorySuffix)
val pklMemberName =
getAnnotatedPklName(nodeClass)
?: nodeClass.simpleName.toString().removeSuffix(truffleNodeClassSuffix)
val pklMemberNameQualified =
when (pklClassName) {
// By convention, the top-level class containing node classes
// for *module* members is named `<SimpleModuleName>Nodes`.
// Example: `BaseNodes` for pkl.base
pklModuleNameCapitalized -> "pkl.$pklModuleName#$pklMemberName"
else -> "pkl.$pklModuleName#$pklClassName.$pklMemberName"
}
registryClass.addOriginatingElement(nodeClass)
registryClassConstructor.addStatement(
"register(\$S, \$T::create)",
pklMemberNameQualified,
nodeClass,
)
}
registryClass.addMethod(registryClassConstructor.build())
val javaFile = JavaFile.builder(registryPackageName, registryClass.build()).build()
javaFile.writeTo(processingEnv.filer)
}
private fun generateRegistryFactoryClass(packages: Collection<PackageElement>) {
val registryFactoryClass =
TypeSpec.classBuilder(memberRegistryFactoryClassName)
.addJavadoc("Generated by {@link ${this::class.qualifiedName}}.")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
val registryFactoryConstructor = MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE)
registryFactoryClass.addMethod(registryFactoryConstructor.build())
val registryFactoryGetMethod =
MethodSpec.methodBuilder("get")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(moduleKeyClassName, "moduleKey")
.returns(externalMemberRegistryClassName)
.beginControlFlow("if (!\$T.isStdLibModule(moduleKey))", moduleKeysClassName)
.addStatement("return \$T.INSTANCE", emptyMemberRegistryClassName)
.endControlFlow()
.beginControlFlow("switch (moduleKey.getUri().getSchemeSpecificPart())")
for (pkg in packages) {
val pklModuleName = getAnnotatedPklName(pkg) ?: pkg.simpleName.toString()
val pklModuleNameCapitalized = pklModuleName.capitalize()
val registryClassName =
ClassName.get(registryPackageName, "${pklModuleNameCapitalized}MemberRegistry")
// declare dependency on package-info.java (for `@PklName`)
registryFactoryClass.addOriginatingElement(pkg)
registryFactoryGetMethod
.addCode("case \$S:\n", pklModuleName)
.addStatement(" return new \$T()", registryClassName)
}
registryFactoryGetMethod
.addCode("default:\n")
.addStatement(" return \$T.INSTANCE", emptyMemberRegistryClassName)
.endControlFlow()
registryFactoryClass.addMethod(registryFactoryGetMethod.build())
val javaFile = JavaFile.builder(registryPackageName, registryFactoryClass.build()).build()
javaFile.writeTo(processingEnv.filer)
}
private fun getAnnotatedPklName(element: Element): String? {
for (annotation in element.annotationMirrors) {
when (annotation.annotationType.asElement().simpleName.toString()) {
"PklName" -> return annotation.elementValues.values.iterator().next().value.toString()
"GeneratedBy" -> {
val annotationValue = annotation.elementValues.values.first().value as TypeMirror
return getAnnotatedPklName(processingEnv.typeUtils.asElement(annotationValue))
}
}
}
return null
}
private fun String.capitalize(): String = replaceFirstChar { it.titlecaseChar() }
}