diff --git a/build-logic/src/main/kotlin/BuildInfo.kt b/build-logic/src/main/kotlin/BuildInfo.kt index 766f9a39a..6cfd0ac72 100644 --- a/build-logic/src/main/kotlin/BuildInfo.kt +++ b/build-logic/src/main/kotlin/BuildInfo.kt @@ -288,7 +288,12 @@ open class BuildInfo(private val project: Project) { classpath = template.classpath testClassesDirs = template.testClassesDirs 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().configureEach in the subproject. forkEvery = template.forkEvery maxParallelForks = template.maxParallelForks minHeapSize = template.minHeapSize @@ -372,35 +377,33 @@ open class BuildInfo(private val project: Project) { org.gradle.internal.os.OperatingSystem.current() } - // could be `commitId: Provider = project.provider { ... }` - 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 } + private val computedCommitId: Provider = // only run command once per build invocation if (project.path == project.rootProject.path) { - val process = - ProcessBuilder() - .command("git", "rev-parse", "--short", "HEAD") - .directory(project.rootDir) - .start() - process.waitFor().also { exitCode -> - if (exitCode == -1) throw RuntimeException(process.errorStream.reader().readText()) - } - process.inputStream.reader().readText().trim() + project.providers + .exec { commandLine("git", "rev-parse", "--short", "HEAD") } + .standardOutput + .asText + .map { it.trim() } } else { project.rootProject.extensions.getByType(BuildInfo::class.java).commitId } - } - val commitish: String by lazy { if (isReleaseBuild) project.version.toString() else commitId } + val commitId: Provider = + // 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 = + if (isReleaseBuild) project.providers.provider { project.version.toString() } else commitId + + val pklVersion: Provider = if (isReleaseBuild) { - project.version.toString() + project.providers.provider { project.version.toString() } } 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 { if (isReleaseBuild) { diff --git a/build-logic/src/main/kotlin/InstallGraalVm.kt b/build-logic/src/main/kotlin/InstallGraalVm.kt index d7a9dbc63..188a16360 100644 --- a/build-logic/src/main/kotlin/InstallGraalVm.kt +++ b/build-logic/src/main/kotlin/InstallGraalVm.kt @@ -20,9 +20,13 @@ import java.util.* import javax.inject.Inject import kotlin.io.path.createDirectories 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.provider.Property 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.process.ExecOperations @@ -32,25 +36,32 @@ constructor( private val fileOperations: FileOperations, private val execOperations: ExecOperations, ) : DefaultTask() { - @get:Input abstract val graalVm: Property + @get:Input abstract val homeDir: Property + + @get:InputFile abstract val downloadFile: RegularFileProperty + + @get:Input abstract val version: Property + + @get:Input abstract val graalVmJdkVersion: Property + + @get:Internal abstract val installDir: DirectoryProperty init { - @Suppress("LeakingThis") onlyIf("GraalVM not installed") { !graalVm.get().installDir.exists() } + @Suppress("LeakingThis") onlyIf("GraalVM not installed") { !installDir.get().asFile.exists() } } @TaskAction @Suppress("unused") fun run() { - // minimize chance of corruption by extract-to-random-dir-and-flip-symlink - val distroDir = Paths.get(graalVm.get().homeDir, UUID.randomUUID().toString()) + val distroDir = Paths.get(homeDir.get(), UUID.randomUUID().toString()) try { 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() }` execOperations.exec { workingDir = distroDir.toFile() 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() @@ -59,8 +70,8 @@ constructor( println("Installing native-image into $distroDir") val gvmVersionMajor = - requireNotNull(graalVm.get().version.split(".").first().toIntOrNull()) { - "Invalid GraalVM JDK version: ${graalVm.get().graalVmJdkVersion}" + requireNotNull(version.get().split(".").first().toIntOrNull()) { + "Invalid GraalVM JDK version: ${graalVmJdkVersion.get()}" } if (gvmVersionMajor < 24) { execOperations.exec { @@ -70,11 +81,11 @@ constructor( } } - println("Creating symlink ${graalVm.get().installDir} for $distroDir") - val tempLink = Paths.get(graalVm.get().homeDir, UUID.randomUUID().toString()) + println("Creating symlink ${installDir.get().asFile} for $distroDir") + val tempLink = Paths.get(homeDir.get(), UUID.randomUUID().toString()) Files.createSymbolicLink(tempLink, distroDir) try { - Files.move(tempLink, graalVm.get().installDir.toPath(), StandardCopyOption.ATOMIC_MOVE) + Files.move(tempLink, installDir.get().asFile.toPath(), StandardCopyOption.ATOMIC_MOVE) } catch (e: Exception) { try { fileOperations.delete(tempLink.toFile()) diff --git a/build-logic/src/main/kotlin/NativeImageBuild.kt b/build-logic/src/main/kotlin/NativeImageBuild.kt index f9a2be005..d6827b79b 100644 --- a/build-logic/src/main/kotlin/NativeImageBuild.kt +++ b/build-logic/src/main/kotlin/NativeImageBuild.kt @@ -16,18 +16,18 @@ import javax.inject.Inject import org.gradle.api.DefaultTask import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.ProjectLayout import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property -import org.gradle.api.provider.Provider import org.gradle.api.services.BuildService import org.gradle.api.services.BuildServiceParameters +import org.gradle.api.services.ServiceReference import org.gradle.api.tasks.ClasspathNormalizer import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction -import org.gradle.kotlin.dsl.registerIfAbsent import org.gradle.kotlin.dsl.withNormalizer import org.gradle.process.ExecOperations @@ -49,25 +49,31 @@ abstract class NativeImageBuild : DefaultTask() { @get:InputFiles abstract val classpath: ConfigurableFileCollection - private val outputDir = project.layout.buildDirectory.dir("executable") + /** Path to the `native-image` binary (e.g. `/bin/native-image`). */ + @get:Input abstract val nativeImageExecutable: Property - @get:OutputFile val outputFile = outputDir.flatMap { it.file(imageName) } + @get:Input abstract val graalSdkLibraryName: Property + + @get:Input abstract val releaseBuild: Property + + @get:Input abstract val nativeArch: Property + + /** Divisor applied to `availableProcessors` to throttle native-image CPU usage. */ + @get:Input abstract val processorDivisor: Property @get:Inject protected abstract val execOperations: ExecOperations - private val graalVm: Provider = arch.map { a -> - when (a) { - Architecture.AMD64 -> buildInfo.graalVmAmd64 - Architecture.AARCH64 -> buildInfo.graalVmAarch64 - } - } + @get:Inject protected abstract val layout: ProjectLayout - private val buildInfo: BuildInfo = project.extensions.getByType(BuildInfo::class.java) + private val outputDir + get() = layout.buildDirectory.dir("executable") - private val nativeImageCommandName = - if (buildInfo.os.isWindows) "native-image.cmd" else "native-image" + @get:OutputFile + val outputFile + get() = outputDir.flatMap { it.file(imageName) } - private val nativeImageExecutable = graalVm.map { "${it.baseDir}/bin/$nativeImageCommandName" } + @get:ServiceReference("nativeImageBuildService") + abstract val buildService: Property private val extraArgsFromProperties by lazy { System.getProperties() @@ -75,19 +81,7 @@ abstract class NativeImageBuild : DefaultTask() { .map { "${it.key}=${it.value}".substring("pkl.native".length) } } - private val buildService = - project.gradle.sharedServices.registerIfAbsent( - "nativeImageBuildService", - NativeImageBuildService::class, - ) { - maxParallelUsages.set(1) - } - init { - // ensure native-image builds run in serial (prevent `gw buildNative` from consuming all host - // CPU resources). - usesService(buildService) - group = "build" inputs @@ -104,8 +98,7 @@ abstract class NativeImageBuild : DefaultTask() { @Suppress("unused") protected fun run() { execOperations.exec { - val exclusions = - listOf(buildInfo.libs.findLibrary("graalSdk").get()).map { it.get().module.name } + val exclusions = listOf(graalSdkLibraryName.get()) executable = nativeImageExecutable.get() workingDir(outputDir) @@ -140,10 +133,10 @@ abstract class NativeImageBuild : DefaultTask() { add("-H:-ParseRuntimeOptions") // quick build mode: 40% faster compilation, 20% smaller (but presumably also slower) // executable - if (!buildInfo.isReleaseBuild) { + if (!releaseBuild.get()) { add("-Ob") } - if (buildInfo.isNativeArch) { + if (nativeArch.get()) { add("-march=native") } else { add("-march=compatibility") @@ -155,9 +148,7 @@ abstract class NativeImageBuild : DefaultTask() { } add(pathInput.asPath) // make sure dev machine stays responsive (15% slowdown on my laptop) - val processors = - Runtime.getRuntime().availableProcessors() / - if (buildInfo.os.isMacOsX && !buildInfo.isCiBuild) 4 else 1 + val processors = Runtime.getRuntime().availableProcessors() / processorDivisor.get() add("-J-XX:ActiveProcessorCount=${processors}") // Pass through all `HOMEBREW_` prefixed environment variables to allow build with shimmed // tools. diff --git a/build-logic/src/main/kotlin/PklFormatterSpotless.kt b/build-logic/src/main/kotlin/PklFormatterSpotless.kt index 737366069..d726c504d 100644 --- a/build-logic/src/main/kotlin/PklFormatterSpotless.kt +++ b/build-logic/src/main/kotlin/PklFormatterSpotless.kt @@ -15,8 +15,10 @@ */ import com.diffplug.spotless.FormatterFunc import com.diffplug.spotless.FormatterStep +import java.io.File import java.io.Serial import java.io.Serializable +import java.lang.reflect.Method import java.net.URLClassLoader import org.gradle.api.artifacts.Configuration @@ -26,31 +28,45 @@ class PklFormatterStep(@Transient private val configuration: Configuration) : Se } fun create(): FormatterStep { + val files = configuration.files.toList() return FormatterStep.createLazy( "pkl", - { PklFormatterStep(configuration) }, - { PklFormatterFunc(configuration) }, + { PklFormatterState(files) }, + { PklFormatterFunc(it.files) }, ) } } -class PklFormatterFunc(@Transient private val configuration: Configuration) : - FormatterFunc, Serializable { +data class PklFormatterState(val files: List) : Serializable { + companion object { + @Serial private const val serialVersionUID: Long = 1L + } +} + +class PklFormatterFunc(private val files: List) : FormatterFunc, Serializable { companion object { @Serial private const val serialVersionUID: Long = 1L } - private val classLoader by lazy { - val urls = configuration.files.map { it.toURI().toURL() } + @delegate:Transient + 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 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 { return formatMethod(formatterInstance, input) as String diff --git a/build-logic/src/main/kotlin/PklPublishing.kt b/build-logic/src/main/kotlin/PklPublishing.kt index 07ea6f32a..8f9181001 100644 --- a/build-logic/src/main/kotlin/PklPublishing.kt +++ b/build-logic/src/main/kotlin/PklPublishing.kt @@ -46,7 +46,7 @@ fun Project.configurePklPomMetadata() { connection.set("scm:git:git://github.com/apple/pkl.git") developerConnection.set("scm:git:ssh://github.com/apple/pkl.git") val buildInfo = extensions.getByType() - url.set("https://github.com/apple/pkl/tree/${buildInfo.commitish}") + url.set(buildInfo.commitish.map { "https://github.com/apple/pkl/tree/$it" }) } issueManagement { system.set("GitHub Issues") diff --git a/build-logic/src/main/kotlin/pklFatJar.gradle.kts b/build-logic/src/main/kotlin/pklFatJar.gradle.kts index 907349188..acb56c769 100644 --- a/build-logic/src/main/kotlin/pklFatJar.gradle.kts +++ b/build-logic/src/main/kotlin/pklFatJar.gradle.kts @@ -15,10 +15,12 @@ */ import org.gradle.api.GradleException import org.gradle.api.artifacts.Configuration +import org.gradle.api.file.ArchiveOperations import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.tasks.bundling.Jar import org.gradle.api.tasks.testing.Test import org.gradle.kotlin.dsl.* +import org.gradle.kotlin.dsl.support.serviceOf plugins { `java-library` @@ -150,17 +152,19 @@ tasks.check { dependsOn(testFatJar) } val validateFatJar by tasks.registering { val outputFile = layout.buildDirectory.file("validateFatJar/result.txt") - inputs.files(tasks.shadowJar) + val shadowJarFile = tasks.shadowJar.flatMap { it.archiveFile } + val archiveOps = serviceOf() + inputs.file(shadowJarFile) inputs.property("nonRelocations", nonRelocations) outputs.file(outputFile) + val nonRelocations = nonRelocations doLast { val unshadowedFiles = mutableListOf() - zipTree(tasks.shadowJar.get().outputs.files.singleFile).visit { - val fileDetails = this - val path = fileDetails.relativePath.pathString + archiveOps.zipTree(shadowJarFile.get().asFile).visit { + val path = relativePath.pathString if ( - !(fileDetails.isDirectory || + !(isDirectory || path.startsWith("org/pkl/") || path.startsWith("META-INF/") || nonRelocations.any { path.startsWith(it) }) diff --git a/build-logic/src/main/kotlin/pklGraalVm.gradle.kts b/build-logic/src/main/kotlin/pklGraalVm.gradle.kts index 95351bebc..eb68c3889 100644 --- a/build-logic/src/main/kotlin/pklGraalVm.gradle.kts +++ b/build-logic/src/main/kotlin/pklGraalVm.gradle.kts @@ -28,11 +28,13 @@ val downloadGraalVmAmd64 by tasks.registering(Download::class) { configureDownloadGraalVm(buildInfo.graalVmAmd64) } fun Download.configureDownloadGraalVm(graalvm: BuildInfo.GraalVm) { - onlyIf { !graalvm.installDir.exists() } - doLast { println("Downloaded GraalVm to ${graalvm.downloadFile}") } + val installDir = graalvm.installDir + val downloadFile = graalvm.downloadFile + onlyIf { !installDir.exists() } + doLast { println("Downloaded GraalVm to $downloadFile") } src(graalvm.downloadUrl) - dest(graalvm.downloadFile) + dest(downloadFile) overwrite(false) tempAndMove(true) } @@ -50,7 +52,8 @@ val verifyGraalVmAmd64 by } fun Verify.configureVerifyGraalVm(graalvm: BuildInfo.GraalVm) { - onlyIf { !graalvm.installDir.exists() } + val installDir = graalvm.installDir + onlyIf { !installDir.exists() } src(graalvm.downloadFile) checksum( @@ -59,16 +62,26 @@ fun Verify.configureVerifyGraalVm(graalvm: BuildInfo.GraalVm) { 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") val installGraalVmAarch64 by tasks.registering(InstallGraalVm::class) { dependsOn(verifyGraalVmAarch64) - graalVm = buildInfo.graalVmAarch64 + configureInstallGraalVm(buildInfo.graalVmAarch64) } @Suppress("unused") val installGraalVmAmd64 by tasks.registering(InstallGraalVm::class) { dependsOn(verifyGraalVmAmd64) - graalVm = buildInfo.graalVmAmd64 + configureInstallGraalVm(buildInfo.graalVmAmd64) } diff --git a/build-logic/src/main/kotlin/pklHtmlValidator.gradle.kts b/build-logic/src/main/kotlin/pklHtmlValidator.gradle.kts index 69c9ed800..59014de3c 100644 --- a/build-logic/src/main/kotlin/pklHtmlValidator.gradle.kts +++ b/build-logic/src/main/kotlin/pklHtmlValidator.gradle.kts @@ -68,7 +68,7 @@ val validateHtml by // 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 // not strictly necessary - doFirst { project.delete(resultFile) } + doFirst { resultFile.get().asFile.delete() } doLast { resultFile.get().asFile.writeText("Success.") } } diff --git a/build-logic/src/main/kotlin/pklJavaExecutable.gradle.kts b/build-logic/src/main/kotlin/pklJavaExecutable.gradle.kts index d05bb1850..729bd6442 100644 --- a/build-logic/src/main/kotlin/pklJavaExecutable.gradle.kts +++ b/build-logic/src/main/kotlin/pklJavaExecutable.gradle.kts @@ -47,8 +47,10 @@ fun Task.setupTestStartJavaExecutable(launcher: Provider? = null) val outputFile = layout.buildDirectory.file("testStartJavaExecutable/$name") outputs.file(outputFile) + val executableFile = javaExecutable.flatMap { it.outJar } + val pklVersion = buildInfo.pklVersionNonUnique val execOutput = providers.exec { - val executablePath = javaExecutable.get().outputs.files.singleFile + val executablePath = executableFile.get().asFile if (launcher?.isPresent == true) { commandLine( launcher.get().executablePath.asFile.absolutePath, @@ -63,9 +65,9 @@ fun Task.setupTestStartJavaExecutable(launcher: Provider? = null) doLast { val outputText = execOutput.standardOutput.asText.get() - if (!outputText.contains(buildInfo.pklVersionNonUnique)) { + if (!outputText.contains(pklVersion)) { 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 { diff --git a/build-logic/src/main/kotlin/pklNativeExecutable.gradle.kts b/build-logic/src/main/kotlin/pklNativeExecutable.gradle.kts index 563c89640..3d8c08e59 100644 --- a/build-logic/src/main/kotlin/pklNativeExecutable.gradle.kts +++ b/build-logic/src/main/kotlin/pklNativeExecutable.gradle.kts @@ -67,13 +67,35 @@ dependencies { 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() { arch = Architecture.AMD64 + nativeImageExecutable = "${buildInfo.graalVmAmd64.baseDir}/bin/$nativeImageCommandName" dependsOn(":installGraalVmAmd64") } private fun NativeImageBuild.aarch64() { arch = Architecture.AARCH64 + nativeImageExecutable = "${buildInfo.graalVmAarch64.baseDir}/bin/$nativeImageCommandName" dependsOn(":installGraalVmAarch64") } @@ -91,6 +113,7 @@ val macExecutableAmd64 by mainClass = executableSpec.mainClass amd64() setClasspath() + configure() } val macExecutableAarch64 by @@ -99,6 +122,7 @@ val macExecutableAarch64 by mainClass = executableSpec.mainClass aarch64() setClasspath() + configure() } val linuxExecutableAmd64 by @@ -107,6 +131,7 @@ val linuxExecutableAmd64 by mainClass = executableSpec.mainClass amd64() setClasspath() + configure() } val linuxExecutableAarch64 by @@ -115,6 +140,7 @@ val linuxExecutableAarch64 by mainClass = executableSpec.mainClass aarch64() setClasspath() + configure() // Ensure compatibility for kernels with page size set to 4k, 16k and 64k // (e.g. Raspberry Pi 5, Asahi Linux) extraNativeImageArgs.add("-H:PageSize=65536") @@ -126,6 +152,7 @@ val alpineExecutableAmd64 by mainClass = executableSpec.mainClass amd64() setClasspath() + configure() extraNativeImageArgs.addAll(listOf("--static", "--libc=musl")) } @@ -135,6 +162,7 @@ val windowsExecutableAmd64 by mainClass = executableSpec.mainClass amd64() setClasspath() + configure() } val assembleNative by tasks.existing @@ -143,18 +171,18 @@ val testStartNativeExecutable by tasks.registering { dependsOn(assembleNative) // 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) - val execOutput = providers.exec { - commandLine(assembleNative.get().outputs.files.singleFile, "--version") - } + val nativeExecutableFile = assembleNative.map { it.outputs.files.singleFile } + val execOutput = providers.exec { commandLine(nativeExecutableFile.get(), "--version") } + val pklVersion = buildInfo.pklVersionNonUnique doLast { val outputText = execOutput.standardOutput.asText.get() - if (!outputText.contains(buildInfo.pklVersionNonUnique)) { + if (!outputText.contains(pklVersion)) { 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 { @@ -171,10 +199,10 @@ val requiredGlibcVersion: Version = Version.parse("2.17") val checkGlibc by tasks.registering { enabled = buildInfo.os.isLinux && !buildInfo.musl dependsOn(assembleNative) + val nativeExecutableFile = assembleNative.map { it.outputs.files.singleFile } + val requiredGlibcVersion = requiredGlibcVersion + val exec = providers.exec { commandLine("objdump", "-T", nativeExecutableFile.get()) } doLast { - val exec = providers.exec { - commandLine("objdump", "-T", assembleNative.get().outputs.files.singleFile) - } val output = exec.standardOutput.asText.get() val minimumGlibcVersion = output diff --git a/build.gradle.kts b/build.gradle.kts index 1270141a3..e60d6c757 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 = - """ -==== -Gradle version : ${gradle.gradleVersion} -Java version : ${System.getProperty("java.version")} -isParallel : ${gradle.startParameter.isParallelProjectExecutionEnabled} -maxWorkerCount : ${gradle.startParameter.maxWorkerCount} -Architecture : ${buildInfo.arch} +val printInfo by tasks.registering { + val arch = buildInfo.arch + val pklVersion = buildInfo.pklVersion + val pklVersionNonUnique = buildInfo.pklVersionNonUnique + val commitId = buildInfo.commitId + val gradleVerison = gradle.gradleVersion + val javaVersion = System.getProperty("java.version") + 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} -Pkl Version : ${buildInfo.pklVersion} -Pkl Non-Unique Version : ${buildInfo.pklVersionNonUnique} -Git Commit ID : ${buildInfo.commitId} -==== -""" + Project Version : $projectVersion + Pkl Version : ${pklVersion.get()} + Pkl Non-Unique Version : $pklVersionNonUnique + Git Commit ID : ${commitId.get()} + ==== + """ + .trimIndent() -val formattedMessage = - message.replace("\n====", "\n" + "=".repeat(message.lines().maxByOrNull { it.length }!!.length)) + val formattedMessage = + message.replace("====", "=".repeat(message.lines().maxByOrNull { it.length }!!.length)) -logger.info(formattedMessage) + println(formattedMessage) + } +} diff --git a/gradle.properties b/gradle.properties index 08b8cd48a..d7bde8e88 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,6 +15,8 @@ org.gradle.jvmargs= \ org.gradle.parallel=true org.gradle.caching=true +org.gradle.configuration-cache=true + kotlin.stdlib.default.dependency=false kotlin.daemon.jvmargs=-XX:+UseParallelGC #org.gradle.workers.max=1 diff --git a/pkl-cli/pkl-cli.gradle.kts b/pkl-cli/pkl-cli.gradle.kts index f25a54f8d..707ce1c27 100644 --- a/pkl-cli/pkl-cli.gradle.kts +++ b/pkl-cli/pkl-cli.gradle.kts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import java.io.ByteArrayOutputStream import java.io.OutputStream import org.gradle.kotlin.dsl.support.serviceOf @@ -118,9 +117,15 @@ private fun setupJavaExecutableRun( null -> "java" 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() } @@ -133,7 +138,10 @@ val evalTestFlags = arrayOf("eval", "-x", "1 + 1", "pkl:base") fun Exec.useRootDirAndSuppressOutput() { 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 diff --git a/pkl-commons-test/pkl-commons-test.gradle.kts b/pkl-commons-test/pkl-commons-test.gradle.kts index f6f1dda9d..3601a7281 100644 --- a/pkl-commons-test/pkl-commons-test.gradle.kts +++ b/pkl-commons-test/pkl-commons-test.gradle.kts @@ -70,17 +70,26 @@ for (packageDir in file("src/main/files/packages").listFiles()!!) { into(destinationDir) val shasumFile = destinationDir.map { it.file("${packageDir.name}.json.sha256") } outputs.file(shasumFile) - filter { line -> - line.replaceFirst("\$computedChecksum", archiveFile.get().asFile.computeChecksum()) - } + val isWindows = buildInfo.os.isWindows doLast { val outputFile = destinationDir.get().asFile.resolve("${packageDir.name}.json") - if (buildInfo.os.isWindows) { - val contents = outputFile.readText() - // workaround for https://github.com/gradle/gradle/issues/1151 - outputFile.writeText(contents.replace("\r\n", "\n")) + fun sha256Hex(file: File): String { + val hash = MessageDigest.getInstance("SHA-256").digest(file.readBytes()) + return buildString(hash.size * 2) { + 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", ) workingDir(keystoreDir) + // capture keystoreFile inside closure so we don't reference `this$0`; which breaks Gradle + // configuration cache + val keystoreFile = keystoreFile doFirst { workingDir.mkdirs() keystoreFile.get().asFile.delete() @@ -143,20 +155,3 @@ val exportCerts by 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) -} diff --git a/pkl-config-java/pkl-config-java.gradle.kts b/pkl-config-java/pkl-config-java.gradle.kts index 24c2eaef7..7cf0caf51 100644 --- a/pkl-config-java/pkl-config-java.gradle.kts +++ b/pkl-config-java/pkl-config-java.gradle.kts @@ -33,10 +33,11 @@ val generateTestConfigClasses by classpath = pklCodegenJava mainClass.set("org.pkl.codegen.java.Main") + val codegenSources = fileTree("src/test/resources/codegenPkl") argumentProviders.add( CommandLineArgumentProvider { listOf("--output-dir", outputDir.get().asFile.path, "--generate-javadoc") + - fileTree("src/test/resources/codegenPkl").map { it.path } + codegenSources.map { it.path } } ) } diff --git a/pkl-config-kotlin/pkl-config-kotlin.gradle.kts b/pkl-config-kotlin/pkl-config-kotlin.gradle.kts index b9c4c8762..07f0c7809 100644 --- a/pkl-config-kotlin/pkl-config-kotlin.gradle.kts +++ b/pkl-config-kotlin/pkl-config-kotlin.gradle.kts @@ -57,10 +57,11 @@ val generateTestConfigClasses by classpath = pklCodegenKotlin mainClass.set("org.pkl.codegen.kotlin.Main") + val codegenSources = fileTree("src/test/resources/codegenPkl") argumentProviders.add( CommandLineArgumentProvider { listOf("--output-dir", outputDir.get().asFile.absolutePath) + - fileTree("src/test/resources/codegenPkl").map { it.absolutePath } + codegenSources.map { it.absolutePath } } ) } diff --git a/pkl-core/pkl-core.gradle.kts b/pkl-core/pkl-core.gradle.kts index d715386dd..ee360c870 100644 --- a/pkl-core/pkl-core.gradle.kts +++ b/pkl-core/pkl-core.gradle.kts @@ -98,20 +98,22 @@ tasks.processResources { inputs.property("version", buildInfo.pklVersion) inputs.property("commitId", buildInfo.commitId) - filesMatching("org/pkl/core/Release.properties") { - val stdlibModules = - fileTree("$rootDir/stdlib") { - include("*.pkl") - exclude("doc-package-info.pkl") - } - .map { "pkl:" + it.nameWithoutExtension } - .sortedBy { it.lowercase() } + val pklVersion = buildInfo.pklVersion + val commitId = buildInfo.commitId + val stdlibModules = + fileTree("$rootDir/stdlib") { + include("*.pkl") + exclude("doc-package-info.pkl") + } + .map { "pkl:" + it.nameWithoutExtension } + .sortedBy { it.lowercase() } + filesMatching("org/pkl/core/Release.properties") { filter( "tokens" to mapOf( - "version" to buildInfo.pklVersion, - "commitId" to buildInfo.commitId, + "version" to pklVersion.get(), + "commitId" to commitId.get(), "stdlibModules" to stdlibModules.joinToString(","), ) ) @@ -140,13 +142,15 @@ val externalReaderFixture by tasks.registering { group = "build" dependsOn(tasks.named("compileExternalReaderFixtureJava")) 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") outputs.file(outputFile) + val runtimeClasspath = externalReaderFixtureSourceSet.map { it.runtimeClasspath } doLast { - val classpath = externalReaderFixtureSourceSet.get().runtimeClasspath.asPath + val classpath = runtimeClasspath.get().asPath val scriptContent = - if (buildInfo.os.isWindows) { + if (isWindows) { """ @echo off java -cp $classpath org.pkl.core.externalreaderfixture.Main diff --git a/pkl-executor/pkl-executor.gradle.kts b/pkl-executor/pkl-executor.gradle.kts index 573112699..618956e89 100644 --- a/pkl-executor/pkl-executor.gradle.kts +++ b/pkl-executor/pkl-executor.gradle.kts @@ -68,15 +68,16 @@ sourceSets { main { java { srcDir("src/main/java") } } } val prepareHistoricalDistributions by tasks.registering { val outputDir = layout.buildDirectory.dir("pklHistoricalDistributions") - inputs.files(pklHistoricalDistributions.files) + inputs.files(pklHistoricalDistributions) outputs.dir(outputDir) + val isWindows = buildInfo.os.isWindows doLast { 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) // Create normal files on Windows, symlink on macOS/linux (need admin privileges to create // symlinks on Windows) - if (buildInfo.os.isWindows) { + if (isWindows) { if (!Files.isRegularFile(target, LinkOption.NOFOLLOW_LINKS)) { if (Files.exists(target)) { Files.delete(target) diff --git a/pkl-gradle/pkl-gradle.gradle.kts b/pkl-gradle/pkl-gradle.gradle.kts index 449d85a78..f6d660b9b 100644 --- a/pkl-gradle/pkl-gradle.gradle.kts +++ b/pkl-gradle/pkl-gradle.gradle.kts @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import org.gradle.api.file.ArchiveOperations +import org.gradle.kotlin.dsl.support.serviceOf + plugins { id("pklAllProjects") id("pklJavaLibrary") @@ -80,13 +83,14 @@ val externalReaderJar by archiveVersion = "" // Package all dependencies into the jar (shadow plugin lite). + val archiveOps = serviceOf() from( externalReader.runtimeClasspath.elements.map { locations -> locations.mapNotNull { location -> val f = location.asFile when { f.isDirectory -> f - f.isFile -> zipTree(f) + f.isFile -> archiveOps.zipTree(f) else -> null } } @@ -96,18 +100,32 @@ val externalReaderJar by 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, + private val javaExecutable: Provider, +) : 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().configureEach { dependsOn(externalReaderJar) // Currently the only way to inject system properties from lazy values in Gradle // is via `jvmArgumentProviders`. - jvmArgumentProviders += CommandLineArgumentProvider { - listOf( - "-DpklGradle.externalReaderJar=" + - externalReaderJar.get().archiveFile.get().asFile.absolutePath, - "-DpklGradle.javaExecutable=" + - javaToolchains.launcherFor(java.toolchain).get().executablePath.asFile.absolutePath, - ) - } + jvmArgumentProviders += ExternalReaderArgProvider(externalReaderJarFile, javaExecutablePath) } publishing { diff --git a/pkl-tools/pkl-tools.gradle.kts b/pkl-tools/pkl-tools.gradle.kts index 61fd0c0b0..bcfbc028f 100644 --- a/pkl-tools/pkl-tools.gradle.kts +++ b/pkl-tools/pkl-tools.gradle.kts @@ -81,13 +81,14 @@ private fun Exec.configureTestStartFatJar(launcher: Provider) { inputs.files(tasks.shadowJar) executable = launcher.get().executablePath.asFile.absolutePath - standardOutput = OutputStream.nullOutputStream() + doFirst { standardOutput = OutputStream.nullOutputStream() } + val shadowJarFile = tasks.shadowJar.flatMap { it.archiveFile } argumentProviders.add( CommandLineArgumentProvider { buildList { add("-cp") - add(tasks.shadowJar.get().outputs.files.singleFile.absolutePath) + add(shadowJarFile.get().asFile.absolutePath) add("org.pkl.cli.Main") add("eval") add("-x") @@ -163,7 +164,7 @@ publishing { connection.set("scm:git:git://github.com/apple/pkl.git") developerConnection.set("scm:git:ssh://github.com/apple/pkl.git") val buildInfo = project.extensions.getByType() - url.set("https://github.com/apple/pkl/tree/${buildInfo.commitish}") + url.set(buildInfo.commitish.map { "https://github.com/apple/pkl/tree/$it" }) } issueManagement { system.set("GitHub Issues")