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
1919import org.gradle.api.Project
2020import org.gradle.api.artifacts.VersionCatalog
2121import 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