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
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+ * 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