Skip to content

Commit de99b34

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 de99b34

34 files changed

+712
-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: 268 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,58 @@ 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+
* See `settings.gradle.kts`, where this is enforced.
50+
*/
51+
const val PKL_JDK_VERSION_MIN = 21
52+
53+
/**
54+
* The JDK minimum is set to match the bytecode minimum, to guarantee that fat JARs work against the
55+
* earliest supported bytecode target.
56+
*/
57+
const val PKL_TEST_JDK_MINIMUM = PKL_JVM_TARGET_DEFAULT_MAXIMUM
58+
59+
/**
60+
* Maximum JDK version which Pkl is tested with; this should be bumped when new JDK stable releases
61+
* are issued. At the time of this writing, JDK 23 is the latest available release.
62+
*/
63+
const val PKL_TEST_JDK_MAXIMUM = 23
64+
65+
/**
66+
* Test the full suite of JDKs between [PKL_TEST_JDK_MINIMUM] and [PKL_TEST_JDK_MAXIMUM]; if this is
67+
* set to `false` (or overridden on the command line), only LTS releases are tested by default.
68+
*/
69+
const val PKL_TEST_ALL_JDKS = false
2370

2471
// `buildInfo` in main build scripts
2572
// `project.extensions.getByType<BuildInfo>()` in precompiled script plugins
26-
open class BuildInfo(project: Project) {
73+
open class BuildInfo(private val project: Project) {
2774
inner class GraalVm(val arch: String) {
2875
val homeDir: String by lazy {
2976
System.getenv("GRAALVM_HOME") ?: "${System.getProperty("user.home")}/.graalvm"
@@ -80,6 +127,220 @@ open class BuildInfo(project: Project) {
80127

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

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

0 commit comments

Comments
 (0)