Enable Gradle configuration cache (#1646)

Enable the configuration cache feature in Gradle, and adjust various
pieces of build logic that aren't configuration cache compatible.
This commit is contained in:
Daniel Chao
2026-06-05 11:05:19 -07:00
committed by GitHub
parent 74eae0388e
commit 87ec8ee730
20 changed files with 296 additions and 177 deletions
+23 -20
View File
@@ -288,7 +288,12 @@ open class BuildInfo(private val project: Project) {
classpath = template.classpath classpath = template.classpath
testClassesDirs = template.testClassesDirs testClassesDirs = template.testClassesDirs
jvmArgs.addAll(template.jvmArgs) jvmArgs.addAll(template.jvmArgs)
jvmArgumentProviders.addAll(template.jvmArgumentProviders) // jvmArgumentProviders are NOT copied: providers added by plugins (e.g.
// java-gradle-plugin's GradleJvmCommandLineArgumentProvider) hold a direct
// reference to the task they were registered on. Copying them to derived tasks
// causes those tasks to capture a foreign task reference, which the
// configuration cache cannot serialize. Each derived task receives its own
// providers via withType<Test>().configureEach in the subproject.
forkEvery = template.forkEvery forkEvery = template.forkEvery
maxParallelForks = template.maxParallelForks maxParallelForks = template.maxParallelForks
minHeapSize = template.minHeapSize minHeapSize = template.minHeapSize
@@ -372,35 +377,33 @@ open class BuildInfo(private val project: Project) {
org.gradle.internal.os.OperatingSystem.current() org.gradle.internal.os.OperatingSystem.current()
} }
// could be `commitId: Provider<String> = project.provider { ... }` private val computedCommitId: Provider<String> =
val commitId: String by lazy {
// allow -DcommitId=abc123 for build environments that don't have git.
System.getProperty("commitId").let { if (it != null) return@lazy it }
// only run command once per build invocation // only run command once per build invocation
if (project.path == project.rootProject.path) { if (project.path == project.rootProject.path) {
val process = project.providers
ProcessBuilder() .exec { commandLine("git", "rev-parse", "--short", "HEAD") }
.command("git", "rev-parse", "--short", "HEAD") .standardOutput
.directory(project.rootDir) .asText
.start() .map { it.trim() }
process.waitFor().also { exitCode ->
if (exitCode == -1) throw RuntimeException(process.errorStream.reader().readText())
}
process.inputStream.reader().readText().trim()
} else { } else {
project.rootProject.extensions.getByType(BuildInfo::class.java).commitId project.rootProject.extensions.getByType(BuildInfo::class.java).commitId
} }
}
val commitish: String by lazy { if (isReleaseBuild) project.version.toString() else commitId } val commitId: Provider<String> =
// allow -DcommitId=abc123 for build environments that don't have git.
System.getProperty("commitId")?.let { project.providers.provider { it } } ?: computedCommitId
val pklVersion: String by lazy { val commitish: Provider<String> =
if (isReleaseBuild) project.providers.provider { project.version.toString() } else commitId
val pklVersion: Provider<String> =
if (isReleaseBuild) { if (isReleaseBuild) {
project.version.toString() project.providers.provider { project.version.toString() }
} else { } else {
project.version.toString().replace("-SNAPSHOT", "-dev+$commitId") project.providers
.provider { project.version.toString() }
.zip(commitId) { version, id -> version.replace("-SNAPSHOT", "-dev+$id") }
} }
}
val pklVersionNonUnique: String by lazy { val pklVersionNonUnique: String by lazy {
if (isReleaseBuild) { if (isReleaseBuild) {
+22 -11
View File
@@ -20,9 +20,13 @@ import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.io.path.createDirectories import kotlin.io.path.createDirectories
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.internal.file.FileOperations import org.gradle.api.internal.file.FileOperations
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations import org.gradle.process.ExecOperations
@@ -32,25 +36,32 @@ constructor(
private val fileOperations: FileOperations, private val fileOperations: FileOperations,
private val execOperations: ExecOperations, private val execOperations: ExecOperations,
) : DefaultTask() { ) : DefaultTask() {
@get:Input abstract val graalVm: Property<BuildInfo.GraalVm> @get:Input abstract val homeDir: Property<String>
@get:InputFile abstract val downloadFile: RegularFileProperty
@get:Input abstract val version: Property<String>
@get:Input abstract val graalVmJdkVersion: Property<String>
@get:Internal abstract val installDir: DirectoryProperty
init { init {
@Suppress("LeakingThis") onlyIf("GraalVM not installed") { !graalVm.get().installDir.exists() } @Suppress("LeakingThis") onlyIf("GraalVM not installed") { !installDir.get().asFile.exists() }
} }
@TaskAction @TaskAction
@Suppress("unused") @Suppress("unused")
fun run() { fun run() {
// minimize chance of corruption by extract-to-random-dir-and-flip-symlink val distroDir = Paths.get(homeDir.get(), UUID.randomUUID().toString())
val distroDir = Paths.get(graalVm.get().homeDir, UUID.randomUUID().toString())
try { try {
distroDir.createDirectories() distroDir.createDirectories()
println("Extracting ${graalVm.get().downloadFile} into $distroDir") println("Extracting ${downloadFile.get().asFile} into $distroDir")
// faster and more reliable than Gradle's `copy { from tarTree() }` // faster and more reliable than Gradle's `copy { from tarTree() }`
execOperations.exec { execOperations.exec {
workingDir = distroDir.toFile() workingDir = distroDir.toFile()
executable = "tar" executable = "tar"
args("--strip-components=1", "-xzf", graalVm.get().downloadFile) args("--strip-components=1", "-xzf", downloadFile.get().asFile)
} }
val os = org.gradle.internal.os.OperatingSystem.current() val os = org.gradle.internal.os.OperatingSystem.current()
@@ -59,8 +70,8 @@ constructor(
println("Installing native-image into $distroDir") println("Installing native-image into $distroDir")
val gvmVersionMajor = val gvmVersionMajor =
requireNotNull(graalVm.get().version.split(".").first().toIntOrNull()) { requireNotNull(version.get().split(".").first().toIntOrNull()) {
"Invalid GraalVM JDK version: ${graalVm.get().graalVmJdkVersion}" "Invalid GraalVM JDK version: ${graalVmJdkVersion.get()}"
} }
if (gvmVersionMajor < 24) { if (gvmVersionMajor < 24) {
execOperations.exec { execOperations.exec {
@@ -70,11 +81,11 @@ constructor(
} }
} }
println("Creating symlink ${graalVm.get().installDir} for $distroDir") println("Creating symlink ${installDir.get().asFile} for $distroDir")
val tempLink = Paths.get(graalVm.get().homeDir, UUID.randomUUID().toString()) val tempLink = Paths.get(homeDir.get(), UUID.randomUUID().toString())
Files.createSymbolicLink(tempLink, distroDir) Files.createSymbolicLink(tempLink, distroDir)
try { try {
Files.move(tempLink, graalVm.get().installDir.toPath(), StandardCopyOption.ATOMIC_MOVE) Files.move(tempLink, installDir.get().asFile.toPath(), StandardCopyOption.ATOMIC_MOVE)
} catch (e: Exception) { } catch (e: Exception) {
try { try {
fileOperations.delete(tempLink.toFile()) fileOperations.delete(tempLink.toFile())
+24 -33
View File
@@ -16,18 +16,18 @@
import javax.inject.Inject import javax.inject.Inject
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.ProjectLayout
import org.gradle.api.provider.ListProperty import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.services.BuildService import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters import org.gradle.api.services.BuildServiceParameters
import org.gradle.api.services.ServiceReference
import org.gradle.api.tasks.ClasspathNormalizer import org.gradle.api.tasks.ClasspathNormalizer
import org.gradle.api.tasks.Input import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.registerIfAbsent
import org.gradle.kotlin.dsl.withNormalizer import org.gradle.kotlin.dsl.withNormalizer
import org.gradle.process.ExecOperations import org.gradle.process.ExecOperations
@@ -49,25 +49,31 @@ abstract class NativeImageBuild : DefaultTask() {
@get:InputFiles abstract val classpath: ConfigurableFileCollection @get:InputFiles abstract val classpath: ConfigurableFileCollection
private val outputDir = project.layout.buildDirectory.dir("executable") /** Path to the `native-image` binary (e.g. `<graalVmBaseDir>/bin/native-image`). */
@get:Input abstract val nativeImageExecutable: Property<String>
@get:OutputFile val outputFile = outputDir.flatMap { it.file(imageName) } @get:Input abstract val graalSdkLibraryName: Property<String>
@get:Input abstract val releaseBuild: Property<Boolean>
@get:Input abstract val nativeArch: Property<Boolean>
/** Divisor applied to `availableProcessors` to throttle native-image CPU usage. */
@get:Input abstract val processorDivisor: Property<Int>
@get:Inject protected abstract val execOperations: ExecOperations @get:Inject protected abstract val execOperations: ExecOperations
private val graalVm: Provider<BuildInfo.GraalVm> = arch.map { a -> @get:Inject protected abstract val layout: ProjectLayout
when (a) {
Architecture.AMD64 -> buildInfo.graalVmAmd64
Architecture.AARCH64 -> buildInfo.graalVmAarch64
}
}
private val buildInfo: BuildInfo = project.extensions.getByType(BuildInfo::class.java) private val outputDir
get() = layout.buildDirectory.dir("executable")
private val nativeImageCommandName = @get:OutputFile
if (buildInfo.os.isWindows) "native-image.cmd" else "native-image" val outputFile
get() = outputDir.flatMap { it.file(imageName) }
private val nativeImageExecutable = graalVm.map { "${it.baseDir}/bin/$nativeImageCommandName" } @get:ServiceReference("nativeImageBuildService")
abstract val buildService: Property<NativeImageBuildService>
private val extraArgsFromProperties by lazy { private val extraArgsFromProperties by lazy {
System.getProperties() System.getProperties()
@@ -75,19 +81,7 @@ abstract class NativeImageBuild : DefaultTask() {
.map { "${it.key}=${it.value}".substring("pkl.native".length) } .map { "${it.key}=${it.value}".substring("pkl.native".length) }
} }
private val buildService =
project.gradle.sharedServices.registerIfAbsent(
"nativeImageBuildService",
NativeImageBuildService::class,
) {
maxParallelUsages.set(1)
}
init { init {
// ensure native-image builds run in serial (prevent `gw buildNative` from consuming all host
// CPU resources).
usesService(buildService)
group = "build" group = "build"
inputs inputs
@@ -104,8 +98,7 @@ abstract class NativeImageBuild : DefaultTask() {
@Suppress("unused") @Suppress("unused")
protected fun run() { protected fun run() {
execOperations.exec { execOperations.exec {
val exclusions = val exclusions = listOf(graalSdkLibraryName.get())
listOf(buildInfo.libs.findLibrary("graalSdk").get()).map { it.get().module.name }
executable = nativeImageExecutable.get() executable = nativeImageExecutable.get()
workingDir(outputDir) workingDir(outputDir)
@@ -140,10 +133,10 @@ abstract class NativeImageBuild : DefaultTask() {
add("-H:-ParseRuntimeOptions") add("-H:-ParseRuntimeOptions")
// quick build mode: 40% faster compilation, 20% smaller (but presumably also slower) // quick build mode: 40% faster compilation, 20% smaller (but presumably also slower)
// executable // executable
if (!buildInfo.isReleaseBuild) { if (!releaseBuild.get()) {
add("-Ob") add("-Ob")
} }
if (buildInfo.isNativeArch) { if (nativeArch.get()) {
add("-march=native") add("-march=native")
} else { } else {
add("-march=compatibility") add("-march=compatibility")
@@ -155,9 +148,7 @@ abstract class NativeImageBuild : DefaultTask() {
} }
add(pathInput.asPath) add(pathInput.asPath)
// make sure dev machine stays responsive (15% slowdown on my laptop) // make sure dev machine stays responsive (15% slowdown on my laptop)
val processors = val processors = Runtime.getRuntime().availableProcessors() / processorDivisor.get()
Runtime.getRuntime().availableProcessors() /
if (buildInfo.os.isMacOsX && !buildInfo.isCiBuild) 4 else 1
add("-J-XX:ActiveProcessorCount=${processors}") add("-J-XX:ActiveProcessorCount=${processors}")
// Pass through all `HOMEBREW_` prefixed environment variables to allow build with shimmed // Pass through all `HOMEBREW_` prefixed environment variables to allow build with shimmed
// tools. // tools.
@@ -15,8 +15,10 @@
*/ */
import com.diffplug.spotless.FormatterFunc import com.diffplug.spotless.FormatterFunc
import com.diffplug.spotless.FormatterStep import com.diffplug.spotless.FormatterStep
import java.io.File
import java.io.Serial import java.io.Serial
import java.io.Serializable import java.io.Serializable
import java.lang.reflect.Method
import java.net.URLClassLoader import java.net.URLClassLoader
import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.Configuration
@@ -26,31 +28,45 @@ class PklFormatterStep(@Transient private val configuration: Configuration) : Se
} }
fun create(): FormatterStep { fun create(): FormatterStep {
val files = configuration.files.toList()
return FormatterStep.createLazy( return FormatterStep.createLazy(
"pkl", "pkl",
{ PklFormatterStep(configuration) }, { PklFormatterState(files) },
{ PklFormatterFunc(configuration) }, { PklFormatterFunc(it.files) },
) )
} }
} }
class PklFormatterFunc(@Transient private val configuration: Configuration) : data class PklFormatterState(val files: List<File>) : Serializable {
FormatterFunc, Serializable { companion object {
@Serial private const val serialVersionUID: Long = 1L
}
}
class PklFormatterFunc(private val files: List<File>) : FormatterFunc, Serializable {
companion object { companion object {
@Serial private const val serialVersionUID: Long = 1L @Serial private const val serialVersionUID: Long = 1L
} }
private val classLoader by lazy { @delegate:Transient
val urls = configuration.files.map { it.toURI().toURL() } private val classLoader: URLClassLoader by lazy {
val urls = files.map { it.toURI().toURL() }
// Use the platform classloader as parent to isolate from Gradle's classloader // Use the platform classloader as parent to isolate from Gradle's classloader
URLClassLoader(urls.toTypedArray(), ClassLoader.getPlatformClassLoader()) URLClassLoader(urls.toTypedArray(), ClassLoader.getPlatformClassLoader())
} }
private val formatterClass by lazy { classLoader.loadClass("org.pkl.formatter.Formatter") } @delegate:Transient
private val formatterClass: Class<*> by lazy {
classLoader.loadClass("org.pkl.formatter.Formatter")
}
private val formatMethod by lazy { formatterClass.getMethod("format", String::class.java) } @delegate:Transient
private val formatMethod: Method by lazy {
formatterClass.getMethod("format", String::class.java)
}
private val formatterInstance by lazy { formatterClass.getConstructor().newInstance() } @delegate:Transient
private val formatterInstance: Any by lazy { formatterClass.getConstructor().newInstance() }
override fun apply(input: String): String { override fun apply(input: String): String {
return formatMethod(formatterInstance, input) as String return formatMethod(formatterInstance, input) as String
+1 -1
View File
@@ -46,7 +46,7 @@ fun Project.configurePklPomMetadata() {
connection.set("scm:git:git://github.com/apple/pkl.git") connection.set("scm:git:git://github.com/apple/pkl.git")
developerConnection.set("scm:git:ssh://github.com/apple/pkl.git") developerConnection.set("scm:git:ssh://github.com/apple/pkl.git")
val buildInfo = extensions.getByType<BuildInfo>() val buildInfo = extensions.getByType<BuildInfo>()
url.set("https://github.com/apple/pkl/tree/${buildInfo.commitish}") url.set(buildInfo.commitish.map { "https://github.com/apple/pkl/tree/$it" })
} }
issueManagement { issueManagement {
system.set("GitHub Issues") system.set("GitHub Issues")
@@ -15,10 +15,12 @@
*/ */
import org.gradle.api.GradleException import org.gradle.api.GradleException
import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.Configuration
import org.gradle.api.file.ArchiveOperations
import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.tasks.bundling.Jar import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.testing.Test import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.* import org.gradle.kotlin.dsl.*
import org.gradle.kotlin.dsl.support.serviceOf
plugins { plugins {
`java-library` `java-library`
@@ -150,17 +152,19 @@ tasks.check { dependsOn(testFatJar) }
val validateFatJar by tasks.registering { val validateFatJar by tasks.registering {
val outputFile = layout.buildDirectory.file("validateFatJar/result.txt") val outputFile = layout.buildDirectory.file("validateFatJar/result.txt")
inputs.files(tasks.shadowJar) val shadowJarFile = tasks.shadowJar.flatMap { it.archiveFile }
val archiveOps = serviceOf<ArchiveOperations>()
inputs.file(shadowJarFile)
inputs.property("nonRelocations", nonRelocations) inputs.property("nonRelocations", nonRelocations)
outputs.file(outputFile) outputs.file(outputFile)
val nonRelocations = nonRelocations
doLast { doLast {
val unshadowedFiles = mutableListOf<String>() val unshadowedFiles = mutableListOf<String>()
zipTree(tasks.shadowJar.get().outputs.files.singleFile).visit { archiveOps.zipTree(shadowJarFile.get().asFile).visit {
val fileDetails = this val path = relativePath.pathString
val path = fileDetails.relativePath.pathString
if ( if (
!(fileDetails.isDirectory || !(isDirectory ||
path.startsWith("org/pkl/") || path.startsWith("org/pkl/") ||
path.startsWith("META-INF/") || path.startsWith("META-INF/") ||
nonRelocations.any { path.startsWith(it) }) nonRelocations.any { path.startsWith(it) })
@@ -28,11 +28,13 @@ val downloadGraalVmAmd64 by
tasks.registering(Download::class) { configureDownloadGraalVm(buildInfo.graalVmAmd64) } tasks.registering(Download::class) { configureDownloadGraalVm(buildInfo.graalVmAmd64) }
fun Download.configureDownloadGraalVm(graalvm: BuildInfo.GraalVm) { fun Download.configureDownloadGraalVm(graalvm: BuildInfo.GraalVm) {
onlyIf { !graalvm.installDir.exists() } val installDir = graalvm.installDir
doLast { println("Downloaded GraalVm to ${graalvm.downloadFile}") } val downloadFile = graalvm.downloadFile
onlyIf { !installDir.exists() }
doLast { println("Downloaded GraalVm to $downloadFile") }
src(graalvm.downloadUrl) src(graalvm.downloadUrl)
dest(graalvm.downloadFile) dest(downloadFile)
overwrite(false) overwrite(false)
tempAndMove(true) tempAndMove(true)
} }
@@ -50,7 +52,8 @@ val verifyGraalVmAmd64 by
} }
fun Verify.configureVerifyGraalVm(graalvm: BuildInfo.GraalVm) { fun Verify.configureVerifyGraalVm(graalvm: BuildInfo.GraalVm) {
onlyIf { !graalvm.installDir.exists() } val installDir = graalvm.installDir
onlyIf { !installDir.exists() }
src(graalvm.downloadFile) src(graalvm.downloadFile)
checksum( checksum(
@@ -59,16 +62,26 @@ fun Verify.configureVerifyGraalVm(graalvm: BuildInfo.GraalVm) {
algorithm("SHA-256") algorithm("SHA-256")
} }
// incorrect diagnostic
@Suppress("UnusedReceiverParameter")
fun InstallGraalVm.configureInstallGraalVm(graalVm: BuildInfo.GraalVm) {
homeDir = graalVm.homeDir
downloadFile = graalVm.downloadFile
version = graalVm.version
graalVmJdkVersion = graalVm.graalVmJdkVersion
installDir = graalVm.installDir
}
@Suppress("unused") @Suppress("unused")
val installGraalVmAarch64 by val installGraalVmAarch64 by
tasks.registering(InstallGraalVm::class) { tasks.registering(InstallGraalVm::class) {
dependsOn(verifyGraalVmAarch64) dependsOn(verifyGraalVmAarch64)
graalVm = buildInfo.graalVmAarch64 configureInstallGraalVm(buildInfo.graalVmAarch64)
} }
@Suppress("unused") @Suppress("unused")
val installGraalVmAmd64 by val installGraalVmAmd64 by
tasks.registering(InstallGraalVm::class) { tasks.registering(InstallGraalVm::class) {
dependsOn(verifyGraalVmAmd64) dependsOn(verifyGraalVmAmd64)
graalVm = buildInfo.graalVmAmd64 configureInstallGraalVm(buildInfo.graalVmAmd64)
} }
@@ -68,7 +68,7 @@ val validateHtml by
// write a basic result file s.t. gradle can consider task up-to-date // write a basic result file s.t. gradle can consider task up-to-date
// writing a result file in case validation fails is not easily possible with JavaExec, but also // writing a result file in case validation fails is not easily possible with JavaExec, but also
// not strictly necessary // not strictly necessary
doFirst { project.delete(resultFile) } doFirst { resultFile.get().asFile.delete() }
doLast { resultFile.get().asFile.writeText("Success.") } doLast { resultFile.get().asFile.writeText("Success.") }
} }
@@ -47,8 +47,10 @@ fun Task.setupTestStartJavaExecutable(launcher: Provider<JavaLauncher>? = null)
val outputFile = layout.buildDirectory.file("testStartJavaExecutable/$name") val outputFile = layout.buildDirectory.file("testStartJavaExecutable/$name")
outputs.file(outputFile) outputs.file(outputFile)
val executableFile = javaExecutable.flatMap { it.outJar }
val pklVersion = buildInfo.pklVersionNonUnique
val execOutput = providers.exec { val execOutput = providers.exec {
val executablePath = javaExecutable.get().outputs.files.singleFile val executablePath = executableFile.get().asFile
if (launcher?.isPresent == true) { if (launcher?.isPresent == true) {
commandLine( commandLine(
launcher.get().executablePath.asFile.absolutePath, launcher.get().executablePath.asFile.absolutePath,
@@ -63,9 +65,9 @@ fun Task.setupTestStartJavaExecutable(launcher: Provider<JavaLauncher>? = null)
doLast { doLast {
val outputText = execOutput.standardOutput.asText.get() val outputText = execOutput.standardOutput.asText.get()
if (!outputText.contains(buildInfo.pklVersionNonUnique)) { if (!outputText.contains(pklVersion)) {
throw GradleException( throw GradleException(
"Expected version output to contain current version (${buildInfo.pklVersionNonUnique}), but got '$outputText'" "Expected version output to contain current version ($pklVersion), but got '$outputText'"
) )
} }
outputFile.get().asFile.toPath().apply { outputFile.get().asFile.toPath().apply {
@@ -67,13 +67,35 @@ dependencies {
stagedWindowsAmd64Executable(executableFile("windows-amd64.exe")) stagedWindowsAmd64Executable(executableFile("windows-amd64.exe"))
} }
val nativeImageBuildService =
gradle.sharedServices.registerIfAbsent(
"nativeImageBuildService",
NativeImageBuildService::class,
) {
maxParallelUsages = 1
}
private val nativeImageCommandName =
if (buildInfo.os.isWindows) "native-image.cmd" else "native-image"
private fun NativeImageBuild.configure() {
graalSdkLibraryName = buildInfo.libs.findLibrary("graalSdk").get().map { it.module.name }
releaseBuild = buildInfo.isReleaseBuild
nativeArch = buildInfo.isNativeArch
processorDivisor = if (buildInfo.os.isMacOsX && !buildInfo.isCiBuild) 4 else 1
buildService = nativeImageBuildService
usesService(nativeImageBuildService)
}
private fun NativeImageBuild.amd64() { private fun NativeImageBuild.amd64() {
arch = Architecture.AMD64 arch = Architecture.AMD64
nativeImageExecutable = "${buildInfo.graalVmAmd64.baseDir}/bin/$nativeImageCommandName"
dependsOn(":installGraalVmAmd64") dependsOn(":installGraalVmAmd64")
} }
private fun NativeImageBuild.aarch64() { private fun NativeImageBuild.aarch64() {
arch = Architecture.AARCH64 arch = Architecture.AARCH64
nativeImageExecutable = "${buildInfo.graalVmAarch64.baseDir}/bin/$nativeImageCommandName"
dependsOn(":installGraalVmAarch64") dependsOn(":installGraalVmAarch64")
} }
@@ -91,6 +113,7 @@ val macExecutableAmd64 by
mainClass = executableSpec.mainClass mainClass = executableSpec.mainClass
amd64() amd64()
setClasspath() setClasspath()
configure()
} }
val macExecutableAarch64 by val macExecutableAarch64 by
@@ -99,6 +122,7 @@ val macExecutableAarch64 by
mainClass = executableSpec.mainClass mainClass = executableSpec.mainClass
aarch64() aarch64()
setClasspath() setClasspath()
configure()
} }
val linuxExecutableAmd64 by val linuxExecutableAmd64 by
@@ -107,6 +131,7 @@ val linuxExecutableAmd64 by
mainClass = executableSpec.mainClass mainClass = executableSpec.mainClass
amd64() amd64()
setClasspath() setClasspath()
configure()
} }
val linuxExecutableAarch64 by val linuxExecutableAarch64 by
@@ -115,6 +140,7 @@ val linuxExecutableAarch64 by
mainClass = executableSpec.mainClass mainClass = executableSpec.mainClass
aarch64() aarch64()
setClasspath() setClasspath()
configure()
// Ensure compatibility for kernels with page size set to 4k, 16k and 64k // Ensure compatibility for kernels with page size set to 4k, 16k and 64k
// (e.g. Raspberry Pi 5, Asahi Linux) // (e.g. Raspberry Pi 5, Asahi Linux)
extraNativeImageArgs.add("-H:PageSize=65536") extraNativeImageArgs.add("-H:PageSize=65536")
@@ -126,6 +152,7 @@ val alpineExecutableAmd64 by
mainClass = executableSpec.mainClass mainClass = executableSpec.mainClass
amd64() amd64()
setClasspath() setClasspath()
configure()
extraNativeImageArgs.addAll(listOf("--static", "--libc=musl")) extraNativeImageArgs.addAll(listOf("--static", "--libc=musl"))
} }
@@ -135,6 +162,7 @@ val windowsExecutableAmd64 by
mainClass = executableSpec.mainClass mainClass = executableSpec.mainClass
amd64() amd64()
setClasspath() setClasspath()
configure()
} }
val assembleNative by tasks.existing val assembleNative by tasks.existing
@@ -143,18 +171,18 @@ val testStartNativeExecutable by tasks.registering {
dependsOn(assembleNative) dependsOn(assembleNative)
// dummy file for up-to-date checking // dummy file for up-to-date checking
val outputFile = project.layout.buildDirectory.file("testStartNativeExecutable/output.txt") val outputFile = layout.buildDirectory.file("testStartNativeExecutable/output.txt")
outputs.file(outputFile) outputs.file(outputFile)
val execOutput = providers.exec { val nativeExecutableFile = assembleNative.map { it.outputs.files.singleFile }
commandLine(assembleNative.get().outputs.files.singleFile, "--version") val execOutput = providers.exec { commandLine(nativeExecutableFile.get(), "--version") }
} val pklVersion = buildInfo.pklVersionNonUnique
doLast { doLast {
val outputText = execOutput.standardOutput.asText.get() val outputText = execOutput.standardOutput.asText.get()
if (!outputText.contains(buildInfo.pklVersionNonUnique)) { if (!outputText.contains(pklVersion)) {
throw GradleException( throw GradleException(
"Expected version output to contain current version (${buildInfo.pklVersionNonUnique}), but got '$outputText'" "Expected version output to contain current version ($pklVersion), but got '$outputText'"
) )
} }
outputFile.get().asFile.toPath().apply { outputFile.get().asFile.toPath().apply {
@@ -171,10 +199,10 @@ val requiredGlibcVersion: Version = Version.parse("2.17")
val checkGlibc by tasks.registering { val checkGlibc by tasks.registering {
enabled = buildInfo.os.isLinux && !buildInfo.musl enabled = buildInfo.os.isLinux && !buildInfo.musl
dependsOn(assembleNative) dependsOn(assembleNative)
val nativeExecutableFile = assembleNative.map { it.outputs.files.singleFile }
val requiredGlibcVersion = requiredGlibcVersion
val exec = providers.exec { commandLine("objdump", "-T", nativeExecutableFile.get()) }
doLast { doLast {
val exec = providers.exec {
commandLine("objdump", "-T", assembleNative.get().outputs.files.singleFile)
}
val output = exec.standardOutput.asText.get() val output = exec.standardOutput.asText.get()
val minimumGlibcVersion = val minimumGlibcVersion =
output output
+39 -19
View File
@@ -53,27 +53,47 @@ idea {
} }
} }
val clean by tasks.existing { delete(layout.buildDirectory) } val clean by tasks.existing {
val buildDirectory = layout.buildDirectory.map { it.asFile }
doLast { buildDirectory.get().delete() }
}
val printVersion by tasks.registering { doFirst { println(buildInfo.pklVersion) } } val printVersion by tasks.registering {
val pklVersion = buildInfo.pklVersion
doFirst { println(pklVersion.get()) }
}
val message = val printInfo by tasks.registering {
""" val arch = buildInfo.arch
==== val pklVersion = buildInfo.pklVersion
Gradle version : ${gradle.gradleVersion} val pklVersionNonUnique = buildInfo.pklVersionNonUnique
Java version : ${System.getProperty("java.version")} val commitId = buildInfo.commitId
isParallel : ${gradle.startParameter.isParallelProjectExecutionEnabled} val gradleVerison = gradle.gradleVersion
maxWorkerCount : ${gradle.startParameter.maxWorkerCount} val javaVersion = System.getProperty("java.version")
Architecture : ${buildInfo.arch} val isParallel = gradle.startParameter.isParallelProjectExecutionEnabled
val maxWorkerCount = gradle.startParameter.maxWorkerCount
val projectVersion = project.version
doFirst {
val message =
"""
====
Gradle version : $gradleVerison
Java version : $javaVersion
isParallel : $isParallel
maxWorkerCount : $maxWorkerCount
Architecture : $arch
Project Version : ${project.version} Project Version : $projectVersion
Pkl Version : ${buildInfo.pklVersion} Pkl Version : ${pklVersion.get()}
Pkl Non-Unique Version : ${buildInfo.pklVersionNonUnique} Pkl Non-Unique Version : $pklVersionNonUnique
Git Commit ID : ${buildInfo.commitId} Git Commit ID : ${commitId.get()}
==== ====
""" """
.trimIndent()
val formattedMessage = val formattedMessage =
message.replace("\n====", "\n" + "=".repeat(message.lines().maxByOrNull { it.length }!!.length)) message.replace("====", "=".repeat(message.lines().maxByOrNull { it.length }!!.length))
logger.info(formattedMessage) println(formattedMessage)
}
}
+2
View File
@@ -15,6 +15,8 @@ org.gradle.jvmargs= \
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.caching=true org.gradle.caching=true
org.gradle.configuration-cache=true
kotlin.stdlib.default.dependency=false kotlin.stdlib.default.dependency=false
kotlin.daemon.jvmargs=-XX:+UseParallelGC kotlin.daemon.jvmargs=-XX:+UseParallelGC
#org.gradle.workers.max=1 #org.gradle.workers.max=1
+12 -4
View File
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import java.io.ByteArrayOutputStream
import java.io.OutputStream import java.io.OutputStream
import org.gradle.kotlin.dsl.support.serviceOf import org.gradle.kotlin.dsl.support.serviceOf
@@ -118,9 +117,15 @@ private fun setupJavaExecutableRun(
null -> "java" null -> "java"
else -> launcher.get().executablePath.asFile.absolutePath else -> launcher.get().executablePath.asFile.absolutePath
} }
standardOutput = OutputStream.nullOutputStream()
args("-jar", tasks.javaExecutable.get().outputs.files.singleFile.toString(), *args) doFirst { standardOutput = OutputStream.nullOutputStream() }
val javaExecutableFile = tasks.javaExecutable.map { it.outputs.files.singleFile }
argumentProviders.add(
CommandLineArgumentProvider {
listOf("-jar", javaExecutableFile.get().absolutePath) + args.toList()
}
)
doFirst { outputFile.get().asFile.delete() } doFirst { outputFile.get().asFile.delete() }
@@ -133,7 +138,10 @@ val evalTestFlags = arrayOf("eval", "-x", "1 + 1", "pkl:base")
fun Exec.useRootDirAndSuppressOutput() { fun Exec.useRootDirAndSuppressOutput() {
workingDir = rootProject.layout.projectDirectory.asFile workingDir = rootProject.layout.projectDirectory.asFile
standardOutput = ByteArrayOutputStream() // we only care that this exec doesn't fail doFirst {
// we only care that this exec doesn't fail
standardOutput = OutputStream.nullOutputStream()
}
} }
// 0.28 Preparing for JDK21 toolchains revealed that `testStartJavaExecutable` may pass, even though // 0.28 Preparing for JDK21 toolchains revealed that `testStartJavaExecutable` may pass, even though
+20 -25
View File
@@ -70,17 +70,26 @@ for (packageDir in file("src/main/files/packages").listFiles()!!) {
into(destinationDir) into(destinationDir)
val shasumFile = destinationDir.map { it.file("${packageDir.name}.json.sha256") } val shasumFile = destinationDir.map { it.file("${packageDir.name}.json.sha256") }
outputs.file(shasumFile) outputs.file(shasumFile)
filter { line -> val isWindows = buildInfo.os.isWindows
line.replaceFirst("\$computedChecksum", archiveFile.get().asFile.computeChecksum())
}
doLast { doLast {
val outputFile = destinationDir.get().asFile.resolve("${packageDir.name}.json") val outputFile = destinationDir.get().asFile.resolve("${packageDir.name}.json")
if (buildInfo.os.isWindows) { fun sha256Hex(file: File): String {
val contents = outputFile.readText() val hash = MessageDigest.getInstance("SHA-256").digest(file.readBytes())
// workaround for https://github.com/gradle/gradle/issues/1151 return buildString(hash.size * 2) {
outputFile.writeText(contents.replace("\r\n", "\n")) for (b in hash) {
append("0123456789abcdef"[b.toInt() shr 4 and 0xF])
append("0123456789abcdef"[b.toInt() and 0xF])
}
}
} }
shasumFile.get().asFile.writeText(outputFile.computeChecksum()) var contents =
outputFile.readText().replace("\$computedChecksum", sha256Hex(archiveFile.get().asFile))
if (isWindows) {
// workaround for https://github.com/gradle/gradle/issues/1151
contents = contents.replace("\r\n", "\n")
}
outputFile.writeText(contents)
shasumFile.get().asFile.writeText(sha256Hex(outputFile))
} }
} }
@@ -111,6 +120,9 @@ val generateKeys by
"CN=localhost", "CN=localhost",
) )
workingDir(keystoreDir) workingDir(keystoreDir)
// capture keystoreFile inside closure so we don't reference `this$0`; which breaks Gradle
// configuration cache
val keystoreFile = keystoreFile
doFirst { doFirst {
workingDir.mkdirs() workingDir.mkdirs()
keystoreFile.get().asFile.delete() keystoreFile.get().asFile.delete()
@@ -143,20 +155,3 @@ val exportCerts by
outputFile.get().asFile.delete() outputFile.get().asFile.delete()
} }
} }
fun toHex(hash: ByteArray): String {
val hexDigitTable =
charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
return buildString(hash.size * 2) {
for (b in hash) {
append(hexDigitTable[b.toInt() shr 4 and 0xF])
append(hexDigitTable[b.toInt() and 0xF])
}
}
}
fun File.computeChecksum(): String {
val md = MessageDigest.getInstance("SHA-256")
val hash = md.digest(readBytes())
return toHex(hash)
}
+2 -1
View File
@@ -33,10 +33,11 @@ val generateTestConfigClasses by
classpath = pklCodegenJava classpath = pklCodegenJava
mainClass.set("org.pkl.codegen.java.Main") mainClass.set("org.pkl.codegen.java.Main")
val codegenSources = fileTree("src/test/resources/codegenPkl")
argumentProviders.add( argumentProviders.add(
CommandLineArgumentProvider { CommandLineArgumentProvider {
listOf("--output-dir", outputDir.get().asFile.path, "--generate-javadoc") + listOf("--output-dir", outputDir.get().asFile.path, "--generate-javadoc") +
fileTree("src/test/resources/codegenPkl").map { it.path } codegenSources.map { it.path }
} }
) )
} }
@@ -57,10 +57,11 @@ val generateTestConfigClasses by
classpath = pklCodegenKotlin classpath = pklCodegenKotlin
mainClass.set("org.pkl.codegen.kotlin.Main") mainClass.set("org.pkl.codegen.kotlin.Main")
val codegenSources = fileTree("src/test/resources/codegenPkl")
argumentProviders.add( argumentProviders.add(
CommandLineArgumentProvider { CommandLineArgumentProvider {
listOf("--output-dir", outputDir.get().asFile.absolutePath) + listOf("--output-dir", outputDir.get().asFile.absolutePath) +
fileTree("src/test/resources/codegenPkl").map { it.absolutePath } codegenSources.map { it.absolutePath }
} }
) )
} }
+17 -13
View File
@@ -98,20 +98,22 @@ tasks.processResources {
inputs.property("version", buildInfo.pklVersion) inputs.property("version", buildInfo.pklVersion)
inputs.property("commitId", buildInfo.commitId) inputs.property("commitId", buildInfo.commitId)
filesMatching("org/pkl/core/Release.properties") { val pklVersion = buildInfo.pklVersion
val stdlibModules = val commitId = buildInfo.commitId
fileTree("$rootDir/stdlib") { val stdlibModules =
include("*.pkl") fileTree("$rootDir/stdlib") {
exclude("doc-package-info.pkl") include("*.pkl")
} exclude("doc-package-info.pkl")
.map { "pkl:" + it.nameWithoutExtension } }
.sortedBy { it.lowercase() } .map { "pkl:" + it.nameWithoutExtension }
.sortedBy { it.lowercase() }
filesMatching("org/pkl/core/Release.properties") {
filter<ReplaceTokens>( filter<ReplaceTokens>(
"tokens" to "tokens" to
mapOf( mapOf(
"version" to buildInfo.pklVersion, "version" to pklVersion.get(),
"commitId" to buildInfo.commitId, "commitId" to commitId.get(),
"stdlibModules" to stdlibModules.joinToString(","), "stdlibModules" to stdlibModules.joinToString(","),
) )
) )
@@ -140,13 +142,15 @@ val externalReaderFixture by tasks.registering {
group = "build" group = "build"
dependsOn(tasks.named("compileExternalReaderFixtureJava")) dependsOn(tasks.named("compileExternalReaderFixtureJava"))
inputs.files(externalReaderFixtureSourceSet.map { it.output }) inputs.files(externalReaderFixtureSourceSet.map { it.output })
val fileName = if (buildInfo.os.isWindows) "externalreader.bat" else "externalreader" val isWindows = buildInfo.os.isWindows
val fileName = if (isWindows) "externalreader.bat" else "externalreader"
val outputFile = layout.buildDirectory.file("fixtures/$fileName") val outputFile = layout.buildDirectory.file("fixtures/$fileName")
outputs.file(outputFile) outputs.file(outputFile)
val runtimeClasspath = externalReaderFixtureSourceSet.map { it.runtimeClasspath }
doLast { doLast {
val classpath = externalReaderFixtureSourceSet.get().runtimeClasspath.asPath val classpath = runtimeClasspath.get().asPath
val scriptContent = val scriptContent =
if (buildInfo.os.isWindows) { if (isWindows) {
""" """
@echo off @echo off
java -cp $classpath org.pkl.core.externalreaderfixture.Main java -cp $classpath org.pkl.core.externalreaderfixture.Main
+4 -3
View File
@@ -68,15 +68,16 @@ sourceSets { main { java { srcDir("src/main/java") } } }
val prepareHistoricalDistributions by tasks.registering { val prepareHistoricalDistributions by tasks.registering {
val outputDir = layout.buildDirectory.dir("pklHistoricalDistributions") val outputDir = layout.buildDirectory.dir("pklHistoricalDistributions")
inputs.files(pklHistoricalDistributions.files) inputs.files(pklHistoricalDistributions)
outputs.dir(outputDir) outputs.dir(outputDir)
val isWindows = buildInfo.os.isWindows
doLast { doLast {
val distributionDir = outputDir.get().asFile.toPath().also(Files::createDirectories) val distributionDir = outputDir.get().asFile.toPath().also(Files::createDirectories)
for (file in pklHistoricalDistributions.files) { for (file in inputs.files) {
val target = distributionDir.resolve(file.name) val target = distributionDir.resolve(file.name)
// Create normal files on Windows, symlink on macOS/linux (need admin privileges to create // Create normal files on Windows, symlink on macOS/linux (need admin privileges to create
// symlinks on Windows) // symlinks on Windows)
if (buildInfo.os.isWindows) { if (isWindows) {
if (!Files.isRegularFile(target, LinkOption.NOFOLLOW_LINKS)) { if (!Files.isRegularFile(target, LinkOption.NOFOLLOW_LINKS)) {
if (Files.exists(target)) { if (Files.exists(target)) {
Files.delete(target) Files.delete(target)
+28 -10
View File
@@ -13,6 +13,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import org.gradle.api.file.ArchiveOperations
import org.gradle.kotlin.dsl.support.serviceOf
plugins { plugins {
id("pklAllProjects") id("pklAllProjects")
id("pklJavaLibrary") id("pklJavaLibrary")
@@ -80,13 +83,14 @@ val externalReaderJar by
archiveVersion = "" archiveVersion = ""
// Package all dependencies into the jar (shadow plugin lite). // Package all dependencies into the jar (shadow plugin lite).
val archiveOps = serviceOf<ArchiveOperations>()
from( from(
externalReader.runtimeClasspath.elements.map { locations -> externalReader.runtimeClasspath.elements.map { locations ->
locations.mapNotNull { location -> locations.mapNotNull { location ->
val f = location.asFile val f = location.asFile
when { when {
f.isDirectory -> f f.isDirectory -> f
f.isFile -> zipTree(f) f.isFile -> archiveOps.zipTree(f)
else -> null else -> null
} }
} }
@@ -96,18 +100,32 @@ val externalReaderJar by
manifest { attributes("Main-Class" to "org.pkl.gradle.test.extreader.Main") } manifest { attributes("Main-Class" to "org.pkl.gradle.test.extreader.Main") }
} }
tasks.test { // Named class avoids the anonymous inner-class `this$0` field that Gradle's configuration
// cache cannot serialize when a SAM lambda is created inside a lambda-with-receiver.
class ExternalReaderArgProvider(
private val jarFile: Provider<RegularFile>,
private val javaExecutable: Provider<String>,
) : CommandLineArgumentProvider {
override fun asArguments() =
listOf(
"-DpklGradle.externalReaderJar=${jarFile.get().asFile.absolutePath}",
"-DpklGradle.javaExecutable=${javaExecutable.get()}",
)
}
val externalReaderJarFile = externalReaderJar.flatMap { it.archiveFile }
val javaExecutablePath =
javaToolchains.launcherFor(java.toolchain).map { it.executablePath.asFile.absolutePath }
// Apply to all Test tasks (not just `test`) so that testJdk* tasks also receive the
// external-reader system properties without relying on jvmArgumentProviders being copied
// across tasks (which breaks the configuration cache via stale task references).
tasks.withType<Test>().configureEach {
dependsOn(externalReaderJar) dependsOn(externalReaderJar)
// Currently the only way to inject system properties from lazy values in Gradle // Currently the only way to inject system properties from lazy values in Gradle
// is via `jvmArgumentProviders`. // is via `jvmArgumentProviders`.
jvmArgumentProviders += CommandLineArgumentProvider { jvmArgumentProviders += ExternalReaderArgProvider(externalReaderJarFile, javaExecutablePath)
listOf(
"-DpklGradle.externalReaderJar=" +
externalReaderJar.get().archiveFile.get().asFile.absolutePath,
"-DpklGradle.javaExecutable=" +
javaToolchains.launcherFor(java.toolchain).get().executablePath.asFile.absolutePath,
)
}
} }
publishing { publishing {
+4 -3
View File
@@ -81,13 +81,14 @@ private fun Exec.configureTestStartFatJar(launcher: Provider<JavaLauncher>) {
inputs.files(tasks.shadowJar) inputs.files(tasks.shadowJar)
executable = launcher.get().executablePath.asFile.absolutePath executable = launcher.get().executablePath.asFile.absolutePath
standardOutput = OutputStream.nullOutputStream() doFirst { standardOutput = OutputStream.nullOutputStream() }
val shadowJarFile = tasks.shadowJar.flatMap { it.archiveFile }
argumentProviders.add( argumentProviders.add(
CommandLineArgumentProvider { CommandLineArgumentProvider {
buildList { buildList {
add("-cp") add("-cp")
add(tasks.shadowJar.get().outputs.files.singleFile.absolutePath) add(shadowJarFile.get().asFile.absolutePath)
add("org.pkl.cli.Main") add("org.pkl.cli.Main")
add("eval") add("eval")
add("-x") add("-x")
@@ -163,7 +164,7 @@ publishing {
connection.set("scm:git:git://github.com/apple/pkl.git") connection.set("scm:git:git://github.com/apple/pkl.git")
developerConnection.set("scm:git:ssh://github.com/apple/pkl.git") developerConnection.set("scm:git:ssh://github.com/apple/pkl.git")
val buildInfo = project.extensions.getByType<BuildInfo>() val buildInfo = project.extensions.getByType<BuildInfo>()
url.set("https://github.com/apple/pkl/tree/${buildInfo.commitish}") url.set(buildInfo.commitish.map { "https://github.com/apple/pkl/tree/$it" })
} }
issueManagement { issueManagement {
system.set("GitHub Issues") system.set("GitHub Issues")