Skip to content

Commit 8b29630

Browse files
committed
feat: graalvm jdk21 toolchain
- feat: support for jvm21+ toolchain - feat: support for gradle toolchains - feat: pass `-PnativeArch=native` to build with `-march=native` - test: multi-jdk testing support - test: support for `jvm-test-suite` plugin - test: add tasks to run `jpkl eval` on multiple jdks - test: make jdk exec tests respect multi-jdk flags and ranges - fix: remove mrjar classes at >jvm17 from fatjars - fix: use jdk21 to run the tests (needed for `Unsafe.ensureInitialized`) - fix: truffle svm dependency is required after graalvm `24.0.0` - fix: warnings for gvm flag usage, renamed truffle svm macro - fix: build with `--add-modules=jdk.unsupported` where needed - fix: don't use `gu` tool for modern graalvm versions - fix: catch `Throwable` instead of deprecated-for-removal `ThreadDeath` - chore: buildinfo changes for JVM targets, toolchains - chore: enforce testing at exactly jdk21 - chore: enforce build tooling at jdk21+ - chore: bump graalvm/truffle libs → `24.1.2` - chore: toolchains for `buildSrc` Signed-off-by: Sam Gammon <sam@elide.dev>
1 parent 8cfd235 commit 8b29630

34 files changed

+710
-115
lines changed

.java-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
17.0
1+
21

DEVELOPMENT.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
== Setup
99

10-
. (mandatory) Install a JDK (any version between 17 - 21).
10+
. (mandatory) Install a JDK (JDK 21+ required).
1111
. (recommended) Install {uri-intellij}[IntelliJ IDEA] +
1212
To import the project into IntelliJ, go to File->Open and select the project's root directory.
1313
If the project is opened but not imported, look for a popup in the lower right corner
@@ -17,7 +17,7 @@ _gng_ enables to run Gradle commands with `gw` (instead of `./gradlew`) from any
1717
. (recommended) Set up Git ignore-revs +
1818
`git config blame.ignoreRevsFile .git-blame-ignore-revs`
1919
. (recommended) Install {uri-jenv}[jenv] and plugins +
20-
_jenv_ use specific JDK versions in certain subdirectories. _Pkl_ comes with a `.java-version` file specifying JDK 17. +
20+
_jenv_ use specific JDK versions in certain subdirectories. _Pkl_ comes with a `.java-version` file specifying JDK 21. +
2121
Enable _jenv_ plugins for better handling by `gradle`:
2222
+
2323
[source,shell]

bench/gradle.lockfile

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ net.sf.jopt-simple:jopt-simple:5.0.4=jmh,jmhCompileClasspath,jmhImplementationDe
77
org.apache.commons:commons-math3:3.6.1=jmh,jmhCompileClasspath,jmhImplementationDependenciesMetadata,jmhRuntimeClasspath
88
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata
99
org.assertj:assertj-core:3.27.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
10-
org.graalvm.compiler:compiler:23.0.6=graal
11-
org.graalvm.sdk:graal-sdk:23.0.6=graal,jmh,jmhRuntimeClasspath,truffle
12-
org.graalvm.truffle:truffle-api:23.0.6=graal,jmh,jmhRuntimeClasspath,truffle
10+
org.graalvm.compiler:compiler:24.1.2=graal
11+
org.graalvm.polyglot:polyglot:24.1.2=jmh,jmhRuntimeClasspath,truffle
12+
org.graalvm.sdk:collections:24.1.2=graal,jmh,jmhRuntimeClasspath,truffle
13+
org.graalvm.sdk:graal-sdk:24.1.2=jmh,jmhRuntimeClasspath
14+
org.graalvm.sdk:nativeimage:24.1.2=jmh,jmhRuntimeClasspath,truffle
15+
org.graalvm.sdk:word:24.1.2=graal,jmh,jmhRuntimeClasspath,truffle
16+
org.graalvm.truffle:truffle-api:24.1.2=jmh,jmhRuntimeClasspath,truffle
17+
org.graalvm.truffle:truffle-compiler:24.1.2=graal
1318
org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
1419
org.jetbrains.kotlin:kotlin-build-common:2.0.21=kotlinBuildToolsApiClasspath
1520
org.jetbrains.kotlin:kotlin-build-tools-api:2.0.21=kotlinBuildToolsApiClasspath

buildSrc/build.gradle.kts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@
1515
*/
1616
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
1717

18-
plugins { `kotlin-dsl` }
18+
plugins {
19+
`kotlin-dsl`
20+
`jvm-toolchains`
21+
}
22+
23+
// Keep this in sync with the constants in `BuildInfo.kt` (those are not addressable here).
24+
val toolchainVersion = 21
1925

2026
dependencies {
2127
implementation(libs.downloadTaskPlugin)
@@ -29,8 +35,16 @@ dependencies {
2935
}
3036

3137
java {
32-
sourceCompatibility = JavaVersion.VERSION_17
33-
targetCompatibility = JavaVersion.VERSION_17
38+
sourceCompatibility = JavaVersion.toVersion(toolchainVersion)
39+
targetCompatibility = JavaVersion.toVersion(toolchainVersion)
40+
41+
toolchain {
42+
languageVersion = JavaLanguageVersion.of(toolchainVersion)
43+
vendor = JvmVendorSpec.ADOPTIUM
44+
}
3445
}
3546

36-
kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_17 } }
47+
kotlin {
48+
jvmToolchain(toolchainVersion)
49+
compilerOptions { jvmTarget = JvmTarget.fromTarget(toolchainVersion.toString()) }
50+
}

buildSrc/settings.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
2+
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,6 +24,8 @@ pluginManagement {
2424
}
2525
}
2626

27+
plugins { id("org.gradle.toolchains.foojay-resolver-convention") }
28+
2729
// makes ~/.gradle/init.gradle unnecessary and ~/.gradle/gradle.properties optional
2830
dependencyResolutionManagement {
2931
// use same version catalog as main build

buildSrc/src/main/kotlin/BuildInfo.kt

Lines changed: 266 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
2+
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,11 +19,56 @@ import java.io.File
1919
import org.gradle.api.Project
2020
import org.gradle.api.artifacts.VersionCatalog
2121
import org.gradle.api.artifacts.VersionCatalogsExtension
22-
import org.gradle.kotlin.dsl.getByType
22+
import org.gradle.api.attributes.Category
23+
import org.gradle.api.plugins.JvmTestSuitePlugin
24+
import org.gradle.api.plugins.jvm.JvmTestSuite
25+
import org.gradle.api.provider.Provider
26+
import org.gradle.api.tasks.TaskProvider
27+
import org.gradle.api.tasks.testing.Test
28+
import org.gradle.internal.extensions.stdlib.capitalized
29+
import org.gradle.jvm.toolchain.*
30+
import org.gradle.kotlin.dsl.*
31+
import org.gradle.kotlin.dsl.support.serviceOf
32+
import org.gradle.process.CommandLineArgumentProvider
33+
import org.gradle.testing.base.TestingExtension
34+
35+
/**
36+
* JVM bytecode target; this is pinned at a reasonable version, because downstream JVM projects
37+
* which consume Pkl will need a minimum Bytecode level at or above this one.
38+
*
39+
* Kotlin and Java need matching bytecode targets, so this is expressed as a build setting and
40+
* constant default. To override, pass `-DpklJdkToolchain=X` to the Gradle command line, where X is
41+
* a major Java version.
42+
*/
43+
const val PKL_JVM_TARGET_DEFAULT_MAXIMUM = 17
44+
45+
/**
46+
* The Pkl build requires JDK 21+ to build, because JDK 17 is no longer within the default set of
47+
* supported JDKs for GraalVM. This is a build-time requirement, not a runtime requirement.
48+
*/
49+
const val PKL_JDK_VERSION_MIN = 21
50+
51+
/**
52+
* The JDK minimum is set to match the bytecode minimum, to guarantee that fat JARs work against the
53+
* earliest supported bytecode target.
54+
*/
55+
const val PKL_TEST_JDK_MINIMUM = PKL_JVM_TARGET_DEFAULT_MAXIMUM
56+
57+
/**
58+
* Maximum JDK version which Pkl is tested with; this should be bumped when new JDK stable releases
59+
* are issued. At the time of this writing, JDK 23 is the latest available release.
60+
*/
61+
const val PKL_TEST_JDK_MAXIMUM = 23
62+
63+
/**
64+
* Test the full suite of JDKs between [PKL_TEST_JDK_MINIMUM] and [PKL_TEST_JDK_MAXIMUM]; if this is
65+
* set to `false` (or overridden on the command line), only LTS releases are tested by default.
66+
*/
67+
const val PKL_TEST_ALL_JDKS = false
2368

2469
// `buildInfo` in main build scripts
2570
// `project.extensions.getByType<BuildInfo>()` in precompiled script plugins
26-
open class BuildInfo(project: Project) {
71+
open class BuildInfo(private val project: Project) {
2772
inner class GraalVm(val arch: String) {
2873
val homeDir: String by lazy {
2974
System.getenv("GRAALVM_HOME") ?: "${System.getProperty("user.home")}/.graalvm"
@@ -80,6 +125,220 @@ open class BuildInfo(project: Project) {
80125

81126
val isReleaseBuild: Boolean by lazy { java.lang.Boolean.getBoolean("releaseBuild") }
82127

128+
val isNativeArch: Boolean by lazy { java.lang.Boolean.getBoolean("nativeArch") }
129+
130+
val jvmTarget: Int by lazy {
131+
System.getProperty("pklJvmTarget")?.toInt() ?: PKL_JVM_TARGET_DEFAULT_MAXIMUM
132+
}
133+
134+
// JPMS exports for Truffle; needed on some versions of Java, and transitively within some JARs.
135+
private val jpmsExports =
136+
arrayOf(
137+
"org.graalvm.truffle/com.oracle.truffle.api.exception=ALL-UNNAMED",
138+
"org.graalvm.truffle/com.oracle.truffle.api=ALL-UNNAMED",
139+
"org.graalvm.truffle/com.oracle.truffle.api.nodes=ALL-UNNAMED",
140+
"org.graalvm.truffle/com.oracle.truffle.api.source=ALL-UNNAMED",
141+
)
142+
143+
// Extra JPMS modules forced onto the module path via `--add-modules` in some cases.
144+
private val jpmsAddModules = arrayOf("jdk.unsupported")
145+
146+
// Formats `jpmsExports` for use in JAR manifest attributes.
147+
val jpmsExportsForJarManifest: String by lazy {
148+
jpmsExports.joinToString(" ") { it.substringBefore("=") }
149+
}
150+
151+
// Formats `jpmsExports` for use on the command line with `--add-exports`.
152+
val jpmsExportsForAddExportsFlags: Collection<String> by lazy {
153+
jpmsExports.map { "--add-exports=$it" }
154+
}
155+
156+
// Formats `jpmsAddModules` for use on the command line with `--add-modules`.
157+
val jpmsAddModulesFlags: Collection<String> by lazy { jpmsAddModules.map { "--add-modules=$it" } }
158+
159+
// JVM properties to set during testing.
160+
val testProperties =
161+
mapOf<String, Any>(
162+
// @TODO: this should be removed once pkl supports JPMS as a true Java Module.
163+
"polyglotimpl.DisableClassPathIsolation" to true
164+
)
165+
166+
val jdkVendor: JvmVendorSpec = JvmVendorSpec.ADOPTIUM
167+
168+
val jdkToolchainVersion: JavaLanguageVersion by lazy {
169+
JavaLanguageVersion.of(System.getProperty("pklJdkToolchain")?.toInt() ?: PKL_JDK_VERSION_MIN)
170+
}
171+
172+
val jdkTestFloor: JavaLanguageVersion by lazy { JavaLanguageVersion.of(PKL_TEST_JDK_MINIMUM) }
173+
174+
val jdkTestCeiling: JavaLanguageVersion by lazy { JavaLanguageVersion.of(PKL_TEST_JDK_MAXIMUM) }
175+
176+
val testAllJdks: Boolean by lazy {
177+
// By default, Pkl is tested against LTS JDK releases within the bounds of `PKL_TEST_JDK_TARGET`
178+
// and `PKL_TEST_JDK_MAXIMUM`. To test against the full suite of JDK versions, past and present,
179+
// set `-DpklTestAllJdks=true` on the Gradle command line. This results in non-LTS releases, old
180+
// releases, and "experimental releases" (newer than the toolchain version) being included in
181+
// the default `check` suite.
182+
System.getProperty("pklTestAllJdks")?.toBoolean() ?: PKL_TEST_ALL_JDKS
183+
}
184+
185+
val testExperimentalJdks: Boolean by lazy {
186+
System.getProperty("pklTestFutureJdks")?.toBoolean() ?: false
187+
}
188+
189+
val testJdkVendors: Sequence<JvmVendorSpec> by lazy {
190+
// By default, only OpenJDK is tested during multi-JDK testing. Flip `-DpklTestAllVendors=true`
191+
// to additionally test against a suite of JDK vendors, including Azul, Oracle, and GraalVM.
192+
when (System.getProperty("pklTestAllVendors")?.toBoolean()) {
193+
true -> sequenceOf(JvmVendorSpec.ADOPTIUM, JvmVendorSpec.GRAAL_VM, JvmVendorSpec.ORACLE)
194+
else -> sequenceOf(JvmVendorSpec.ADOPTIUM)
195+
}
196+
}
197+
198+
// Assembles a collection of JDK versions which tests can be run against, considering ancillary
199+
// parameters like `testAllJdks` and `testExperimentalJdks`.
200+
val jdkTestRange: Collection<JavaLanguageVersion> by lazy {
201+
JavaVersionRange.inclusive(jdkTestFloor, jdkTestCeiling).filter { version ->
202+
// unless we are instructed to test all JDKs, tests only include LTS releases and
203+
// versions above the toolchain version.
204+
testAllJdks || (JavaVersionRange.isLTS(version) || version >= jdkToolchainVersion)
205+
}
206+
}
207+
208+
private fun JavaToolchainSpec.pklJdkToolchain() {
209+
languageVersion.set(jdkToolchainVersion)
210+
vendor.set(jdkVendor)
211+
}
212+
213+
private fun labelForVendor(vendor: JvmVendorSpec): String =
214+
when (vendor) {
215+
JvmVendorSpec.AZUL -> "Zulu"
216+
JvmVendorSpec.GRAAL_VM -> "GraalVm"
217+
JvmVendorSpec.ORACLE -> "Oracle"
218+
JvmVendorSpec.ADOPTIUM -> "Adoptium"
219+
else -> error("Unrecognized JDK vendor: $vendor")
220+
}
221+
222+
private fun testNamer(baseName: () -> String): (JavaLanguageVersion, JvmVendorSpec?) -> String =
223+
{ jdkTarget, vendor ->
224+
val targetToken =
225+
when (vendor) {
226+
null -> "Jdk${jdkTarget.asInt()}"
227+
else -> "Jdk${jdkTarget.asInt()}${labelForVendor(vendor).capitalized()}"
228+
}
229+
if (jdkTarget > jdkToolchainVersion) {
230+
// test targets above the toolchain target are considered "experimental".
231+
"${baseName()}${targetToken}Experimental"
232+
} else {
233+
"${baseName()}${targetToken}"
234+
}
235+
}
236+
237+
@Suppress("UnstableApiUsage")
238+
fun multiJdkTestingWith(
239+
templateTask: TaskProvider<out Test>,
240+
configurator: MultiJdkTestConfigurator = {},
241+
): Iterable<Provider<out Any>> =
242+
with(project) {
243+
// force the `jvm-test-suite` plugin to apply first
244+
project.pluginManager.apply(JvmTestSuitePlugin::class.java)
245+
246+
val isMultiVendor = testJdkVendors.count() > 1
247+
val baseNameProvider = { templateTask.get().name }
248+
val namer = testNamer(baseNameProvider)
249+
val applyConfig: MultiJdkTestConfigurator = { (version, jdk) ->
250+
// 1) copy configurations from the template task
251+
dependsOn(templateTask)
252+
templateTask.get().let { template ->
253+
classpath = template.classpath
254+
testClassesDirs = template.testClassesDirs
255+
jvmArgs.addAll(template.jvmArgs)
256+
jvmArgumentProviders.addAll(template.jvmArgumentProviders)
257+
forkEvery = template.forkEvery
258+
maxParallelForks = template.maxParallelForks
259+
minHeapSize = template.minHeapSize
260+
maxHeapSize = template.maxHeapSize
261+
exclude(template.excludes)
262+
template.systemProperties.forEach { prop -> systemProperty(prop.key, prop.value) }
263+
}
264+
265+
// 2) assign launcher
266+
javaLauncher = jdk
267+
268+
// 3) dispatch the user's configurator
269+
configurator(version to jdk)
270+
}
271+
272+
serviceOf<JavaToolchainService>().let { toolchains ->
273+
jdkTestRange
274+
.flatMap { targetVersion ->
275+
// multiply out by jdk vendor
276+
testJdkVendors.map { vendor -> (targetVersion to vendor) }
277+
}
278+
.filter { (jdkTarget, vendor) ->
279+
// only include experimental tasks in the return suite if the flag is set. if the task
280+
// is withheld from the returned list, it will not be executed by default with `gradle
281+
// check`.
282+
testExperimentalJdks ||
283+
(!namer(jdkTarget, vendor.takeIf { isMultiVendor }).contains("Experimental"))
284+
}
285+
.map { (jdkTarget, vendor) ->
286+
if (jdkToolchainVersion == jdkTarget)
287+
tasks.register(namer(jdkTarget, vendor)) {
288+
// alias to `test`
289+
dependsOn(templateTask)
290+
group = Category.VERIFICATION
291+
description =
292+
"Alias for regular '${baseNameProvider()}' task, on JDK ${jdkTarget.asInt()}"
293+
}
294+
else
295+
the<TestingExtension>().suites.register(
296+
namer(jdkTarget, vendor.takeIf { isMultiVendor }),
297+
JvmTestSuite::class,
298+
) {
299+
targets.all {
300+
testTask.configure {
301+
group = Category.VERIFICATION
302+
description = "Run tests against JDK ${jdkTarget.asInt()}"
303+
applyConfig(jdkTarget to toolchains.launcherFor { languageVersion = jdkTarget })
304+
305+
// fix: on jdk17, we must force the polyglot module on to the modulepath
306+
if (jdkTarget.asInt() == 17)
307+
jvmArgumentProviders.add(
308+
CommandLineArgumentProvider {
309+
buildList { listOf("--add-modules=org.graalvm.polyglot") }
310+
}
311+
)
312+
}
313+
}
314+
}
315+
}
316+
.toList()
317+
}
318+
}
319+
320+
val javaCompiler: Provider<JavaCompiler> by lazy {
321+
project.serviceOf<JavaToolchainService>().let { toolchainService ->
322+
toolchainService.compilerFor { pklJdkToolchain() }
323+
}
324+
}
325+
326+
val javaTestLauncher: Provider<JavaLauncher> by lazy {
327+
project.serviceOf<JavaToolchainService>().let { toolchainService ->
328+
toolchainService.launcherFor { pklJdkToolchain() }
329+
}
330+
}
331+
332+
val multiJdkTesting: Boolean by lazy {
333+
// By default, Pkl is tested against a full range of JDK versions, past and present, within the
334+
// supported bounds of `PKL_TEST_JDK_TARGET` and `PKL_TEST_JDK_MAXIMUM`. To opt-out of this
335+
// behavior, set `-DpklMultiJdkTesting=false` on the Gradle command line.
336+
//
337+
// In CI, this defaults to `true` to catch potential cross-JDK compat regressions or other bugs.
338+
// In local dev, this defaults to `false` to speed up the build and reduce contributor load.
339+
System.getProperty("pklMultiJdkTesting")?.toBoolean() ?: isCiBuild
340+
}
341+
83342
val hasMuslToolchain: Boolean by lazy {
84343
// see "install musl" in .circleci/jobs/BuildNativeJob.pkl
85344
File(System.getProperty("user.home"), "staticdeps/bin/x86_64-linux-musl-gcc").exists()
@@ -136,3 +395,7 @@ open class BuildInfo(project: Project) {
136395
}
137396
}
138397
}
398+
399+
// Shape of a function which is applied to configure multi-JDK testing.
400+
private typealias MultiJdkTestConfigurator =
401+
Test.(Pair<JavaLanguageVersion, Provider<JavaLauncher>>) -> Unit

0 commit comments

Comments
 (0)