Add sourceBundle task to download and prepare source bundles (#1902)

* Add tasks to generate source jars for java executables

* native source bundles

* source bundles in separate plugin

* Wire up sourceBundle task

* Produce bundles on a per-variant basis (Java fat jar, each native variant)
* Introduce `pklNativeSourceBundle` and `pklJvmSourceBundle` plugins for handling fat jar vs. GraalVM bits
* Download compiled classes list, and exclude Java sources for executable stuff specifically
* Include native sources when appropriate (GraalVM, JDK, zlib, musl, libpkl)

---------

Co-authored-by: Jen Basch <jbasch@apple.com>
This commit is contained in:
Daniel ChaoandJen Basch authored and GitHub committed 2026-10-14 10:03:45 -07:00
1 parent f4ad0da658
commit d062312df9
17 files changed
+777 -23

No files matched your search

@@ -0,0 +1,111 @@
/*
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.io.File
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
open class BuildSourceBundle : DefaultTask() {
/** Sources jars that are included in full. */
@get:InputFiles val inputJars: ConfigurableFileCollection = project.objects.fileCollection()
/**
* Sources jars that are only included in part, if [compiledClasses] is set.
*
* Only `.java` files are dropped, if their class is not in [compiledClasses]. Everything else is
* kept: other languages (e.g. Kotlin) can't be matched by name, because the classes that they
* compile to don't necessarily relate to the file name or package (e.g. with `@file:JvmName` or
* `@file:JvmPackageName`), and licenses and other metadata must be retained.
*
* If [compiledClasses] is not set, these jars are included in full.
*/
@get:InputFiles
val filteredInputJars: ConfigurableFileCollection = project.objects.fileCollection()
/** Fully qualified names of top-level classes, one per line. */
@get:[InputFile Optional]
val compiledClasses: RegularFileProperty = project.objects.fileProperty()
@get:OutputFile val outputZip: RegularFileProperty = project.objects.fileProperty()
@TaskAction
@Suppress("unused")
fun merge() {
val zipFile = outputZip.asFile.get()
if (!zipFile.getParentFile().exists()) zipFile.getParentFile().mkdirs()
val classes =
if (compiledClasses.isPresent) {
compiledClasses
.get()
.asFile
.readLines()
.map { it.trim() }
.filterTo(hashSetOf()) { it.isNotEmpty() }
} else null
val packages = classes?.mapTo(hashSetOf()) { it.substringBeforeLast('.', "").replace('.', '/') }
var dropped = 0
ZipOutputStream(zipFile.outputStream()).use { zip ->
fun copyJar(jar: File, keep: (String) -> Boolean) {
val jarName = jar.toPath().fileName.toString().removeSuffix(".jar")
project.zipTree(jar).visit {
if (isDirectory) return@visit
val path = relativePath.pathString
if (!keep(path)) {
dropped++
return@visit
}
zip.putNextEntry(ZipEntry("$jarName/$path"))
copyTo(zip)
zip.closeEntry()
}
}
for (jar in inputJars) copyJar(jar) { true }
for (jar in filteredInputJars) {
copyJar(jar) { path ->
classes == null || packages == null || isEvident(path, classes, packages)
}
}
}
if (classes != null) {
logger.lifecycle("Dropped $dropped source files that are not in ${compiledClasses.get()}")
}
}
private fun isEvident(path: String, classes: Set<String>, packages: Set<String>): Boolean {
// sources of multi-release jars have the same classes as the unversioned ones
val sourcePath = path.replace(multiReleasePrefix, "")
val packagePath = sourcePath.substringBeforeLast('/', "")
val fileName = sourcePath.substringAfterLast('/')
return when (sourcePath.substringAfterLast('.', "")) {
"java" ->
when (fileName) {
"package-info.java" -> packagePath in packages
"module-info.java" -> true
else -> sourcePath.removeSuffix(".java").replace('/', '.') in classes
}
else -> true
}
}
private val multiReleasePrefix = Regex("^META-INF/versions/\\d+/")
}
@@ -0,0 +1,109 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
/**
* Collects the JDK, SubstrateVM, and Graal Java sources for every class that has methods compiled
* into a native image.
*
* The classes are read from [compiledClasses], and their sources are looked up in the OpenJDK and
* Graal repositories' tagged source archives. These archives contain the sources of every platform
* and architecture, so this task does not need to run on the platform that it collects sources for.
*/
abstract class NativeImageSourcesJar : DefaultTask() {
/**
* Fully qualified top-level names of the classes compiled into the image, one per line.
*
* This is the list that native-image reports (via `-H:+PrintUniverse`) when building the image.
*/
@get:InputFile abstract val compiledClasses: RegularFileProperty
/** The gzipped tarball of the OpenJDK repository (e.g. `jdk25u`) at the JDK's release tag. */
@get:InputFiles abstract val openJdkTarball: ConfigurableFileCollection
/** The gzipped tarball of the `oracle/graal` repository at the GraalVM's release tag. */
@get:InputFiles abstract val graalTarball: ConfigurableFileCollection
/**
* The platform-specific source roots to include from OpenJDK; any of `share`, `unix`, `linux`,
* `macosx`, and `windows`.
*/
@get:Input abstract val openJdkPlatforms: SetProperty<String>
@get:OutputFile abstract val outputJar: RegularFileProperty
@TaskAction
@Suppress("unused")
fun collect() {
val wanted =
compiledClasses
.get()
.asFile
.readLines()
.filter { it.isNotBlank() }
.mapTo(hashSetOf()) { it.trim().replace('.', '/') + ".java" }
if (wanted.isEmpty()) {
throw GradleException("No compiled classes found in ${compiledClasses.get()}")
}
val platforms = openJdkPlatforms.get()
// <repo>-<tag>/src/<module>/<platform>/classes/<package path>.java
val openJdkSource = Regex("""^[^/]+/src/[^/]+/([^/]+)/classes/(.+\.java)$""")
// <repo>-<tag>/<suite>/src/<project>/src/<package path>.java
//
// Truffle sources are intentionally omitted; they are already published as sources jars.
val graalSource = Regex("""^[^/]+/(?:substratevm|compiler|sdk)/src/[^/]+/src/(.+\.java)$""")
val out = outputJar.get().asFile
out.parentFile.mkdirs()
val written = hashSetOf<String>()
ZipOutputStream(out.outputStream().buffered()).use { zip ->
fun collectFrom(tarball: java.io.File, sourcePathOf: (String) -> String?) {
project.tarTree(project.resources.gzip(tarball)).visit {
if (isDirectory) return@visit
val sourcePath = sourcePathOf(relativePath.pathString) ?: return@visit
if (sourcePath !in wanted || !written.add(sourcePath)) return@visit
zip.putNextEntry(ZipEntry(sourcePath))
copyTo(zip)
zip.closeEntry()
}
}
collectFrom(openJdkTarball.singleFile) { path ->
openJdkSource
.matchEntire(path)
?.takeIf { it.groupValues[1] in platforms }
?.let { it.groupValues[2] }
}
collectFrom(graalTarball.singleFile) { path ->
graalSource.matchEntire(path)?.groupValues?.get(1)
}
}
logger.lifecycle(
"Collected sources of ${written.size} of ${wanted.size} compiled classes into $out"
)
}
}
@@ -0,0 +1,43 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.provider.Property
/**
* The inputs that source bundles of a project are made of.
*
* This is configured by the `pklSourceBundle` plugin; the plugins that register the actual source
* bundles (`pklJvmSourceBundle` and `pklNativeSourceBundle`) consume it.
*/
abstract class SourceBundleSpec {
/** Sources jars of the project itself, and of the projects that it depends on. */
abstract val firstPartyJars: ConfigurableFileCollection
/** Sources jars of third-party dependencies, as resolved from the runtime classpath. */
abstract val dependencyJars: ConfigurableFileCollection
/**
* Additional sources to include in source bundles of native targets (e.g. C sources of the
* project itself).
*/
abstract val extraNativeSources: ConfigurableFileCollection
/**
* Whether musl and zlib are statically linked into the project's native executable for musl
* targets, in which case their sources are included in the source bundle of those targets.
*/
abstract val staticMuslLibc: Property<Boolean>
}
@@ -24,6 +24,7 @@ plugins {
`java-library`
`maven-publish`
id("com.gradleup.shadow")
id("pklJvmSourceBundle")
}
// make fat Jar available to other subprojects
@@ -31,10 +32,6 @@ val fatJarConfiguration: Configuration = configurations.create("fatJar")
val fatJarPublication: MavenPublication = publishing.publications.create<MavenPublication>("fatJar")
// ideally we'd configure this automatically based on project dependencies
val firstPartySourcesJarsConfiguration: Configuration =
configurations.create("firstPartySourcesJars")
val relocations =
mapOf(
// pkl-core dependencies
@@ -182,17 +179,13 @@ val validateFatJar =
tasks.check { dependsOn(validateFatJar) }
val resolveSourcesJars =
tasks.register<ResolveSourcesJars>("resolveSourcesJars") {
configuration.set(configurations.runtimeClasspath)
outputDir.set(layout.buildDirectory.dir("resolveSourcesJars"))
}
val fatSourcesJar =
tasks.register<MergeSourcesJars>("fatSourcesJar") {
plugins.withId("pklJavaLibrary") { inputJars.from(tasks.named("sourcesJar")) }
inputJars.from(firstPartySourcesJarsConfiguration)
inputJars.from(resolveSourcesJars.map { fileTree(it.outputDir) })
inputJars.from(configurations.named("firstPartySourcesJars"))
inputJars.from(
tasks.named<ResolveSourcesJars>("resolveSourcesJars").map { fileTree(it.outputDir) }
)
mergedBinaryJars.from(tasks.shadowJar)
relocatedPackages.set(relocations)
@@ -21,6 +21,7 @@ plugins {
id("pklJavaLibrary")
// id("pklPublishLibrary")
id("com.gradleup.shadow")
id("pklJvmSourceBundle")
}
val executableSpec = project.extensions.create("executable", ExecutableSpec::class.java)
@@ -0,0 +1,35 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Source bundle for the JVM variant of a project: its fat JAR, or its executable JAR.
//
// These contain the complete sources of all dependencies, because fat JARs contain the complete
// classes of all dependencies.
plugins { id("pklSourceBundle") }
val sourceBundleSpec = extensions.getByType<SourceBundleSpec>()
val sourceBundleJava =
tasks.register<BuildSourceBundle>("sourceBundleJava") {
group = "build"
description = "Builds the source bundle of ${project.name}'s JVM variant."
inputJars.from(sourceBundleSpec.firstPartyJars, sourceBundleSpec.dependencyJars)
outputZip =
layout.buildDirectory.file(
"sourceBundle/${project.name}-${project.version}-java-sourcebundle.zip"
)
}
tasks.named("sourceBundle") { dependsOn(sourceBundleJava) }
@@ -22,6 +22,7 @@ plugins {
id("pklGraalVm")
id("pklJavaLibrary")
id("pklNativeLifecycle")
id("pklNativeSourceBundle")
id("pklPublishLibrary")
id("com.gradleup.shadow")
}
@@ -30,6 +31,9 @@ plugins {
val executableSpec = project.extensions.getByType<ExecutableSpec>()
val buildInfo = project.extensions.getByType<BuildInfo>()
// musl executables are linked with `--static`, so musl and zlib become part of the executable
extensions.getByType<SourceBundleSpec>().staticMuslLibc = true
val stagedMacAarch64Executable: Configuration = configurations.create("stagedMacAarch64Executable")
val stagedLinuxAmd64Executable: Configuration = configurations.create("stagedLinuxAmd64Executable")
val stagedLinuxAarch64Executable: Configuration =
@@ -0,0 +1,322 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import javax.inject.Inject
import org.gradle.accessors.dm.LibrariesForLibs
import org.gradle.api.tasks.options.Option
import org.gradle.process.ExecOperations
// Source bundles for the native variants of a project, one for each `Target`.
//
// Each bundle consists of the sources of everything that is compiled into the native image or
// library of that target:
// * the project's own sources, and those of its first-party dependencies;
// * the sources of third-party dependencies that have classes in the image;
// * the Java sources of the JDK, SubstrateVM, and Graal classes that are in the image;
// * the C sources of the JDK and SubstrateVM native libraries that are linked into the image;
// * for statically linked musl executables, the sources of musl and zlib.
//
// Which classes are in the image is read from the lists in `build/compiled-classes` (download them
// with the `downloadCompiledClasses` task). native-image produces these lists, and CI uploads them
// as `compiled-classes-<project>-*` artifacts. This way, the source bundle of any target can be
// built on any machine.
//
// Sources of the JDK, SubstrateVM, and Graal come from the OpenJDK and Graal repositories at the
// tags matching the GraalVM that is used to build, because GraalVM doesn't ship all of the Java
// sources (or any of the C sources).
plugins { id("pklSourceBundle") }
val libs = the<LibrariesForLibs>()
val sourceBundleSpec = extensions.getByType<SourceBundleSpec>()
val openJdkSourcesTarball: Configuration = configurations.create("openJdkSourcesTarball")
val graalSourcesTarball: Configuration = configurations.create("graalSourcesTarball")
val muslSourcesTarball: Configuration = configurations.create("muslSourcesTarball")
val zlibSourcesTarball: Configuration = configurations.create("zlibSourcesTarball")
// CI builds musl and zlib from source for statically linked executables; use the same versions.
fun muslToolchainVersion(name: String): String {
val script = rootProject.file(".github/scripts/install_musl.sh").readText()
return Regex("${name}_VERSION=\"([^\"]+)\"").find(script)?.groupValues?.get(1)
?: throw GradleException("Could not find ${name}_VERSION in .github/scripts/install_musl.sh")
}
// The "GitHub tag archives" and "musl releases" repositories are declared in settings.gradle.kts;
// declaring repositories in a project would stop it from using the ones declared in settings.
dependencies {
val jdkVersion = libs.versions.graalVmJdkVersion.get()
openJdkSourcesTarball("openjdk:jdk${jdkVersion.substringBefore('.')}u:jdk-$jdkVersion-ga")
graalSourcesTarball("oracle:graal:vm-${libs.versions.graalVm.get()}")
muslSourcesTarball("musl:musl:${muslToolchainVersion("MUSL")}")
zlibSourcesTarball("madler:zlib:v${muslToolchainVersion("ZLIB")}")
}
// native-image's class path can differ from the runtime class path (e.g. `nativeImageClasspath` in
// `pklNativeExecutable`), so resolve the sources of what's actually compiled in.
val resolveNativeSourcesJars =
tasks.register<ResolveSourcesJars>("resolveNativeSourcesJars") {
configuration.set(
provider {
configurations.findByName("nativeImageClasspath")
?: configurations.getByName("runtimeClasspath")
}
)
outputDir.set(layout.buildDirectory.dir("resolveNativeSourcesJars"))
}
// The JDK native libraries that native-image links into the image.
val openJdkNativeLibraries = listOf("java", "net", "nio", "zip", "extnet", "management_ext")
// Platform-specific directories in OpenJDK's `src/<module>/<platform>/`.
val Target.OS.openJdkPlatforms: List<String>
get() = buildList {
add("share")
if (!isWindows) add("unix")
add(
when {
isMacOS -> "macosx"
isWindows -> "windows"
else -> "linux"
}
)
}
// The `substratevm/src/com.oracle.svm.native.*` projects that apply to the OS.
val Target.OS.svmNativeProjects: List<String>
get() = buildList {
add("libchelper")
when {
isMacOS -> {
add("darwin")
add("jvm.posix")
}
isLinux -> {
add("libcontainer")
add("jvm.posix")
}
isWindows -> add("jvm.windows")
}
}
val sourceBundleTasks =
Target.entries.map { target ->
val variant = target.name
val outputDir = layout.buildDirectory.dir("tmp/nativeImageSources/${target.targetName}")
val compiledClassesFile =
layout.buildDirectory.file("compiled-classes/${target.targetName}.txt")
val nativeImageSourcesJar =
tasks.register<NativeImageSourcesJar>("nativeImageSourcesJar$variant") {
compiledClasses = compiledClassesFile
openJdkTarball.from(openJdkSourcesTarball)
graalTarball.from(graalSourcesTarball)
openJdkPlatforms.addAll(target.os.openJdkPlatforms)
outputJar = outputDir.map { it.file("native-image-sources.jar") }
}
val openJdkNativeSourcesJar =
tasks.register<Zip>("openJdkNativeSourcesJar$variant") {
archiveFileName = "openjdk-native-sources.jar"
destinationDirectory = outputDir
includeEmptyDirs = false
from(provider { tarTree(resources.gzip(openJdkSourcesTarball.singleFile)) }) {
for (platform in target.os.openJdkPlatforms) {
include("*/src/*/$platform/native/include/**")
for (library in openJdkNativeLibraries) {
include("*/src/*/$platform/native/lib$library/**")
}
}
// drop the `<repo>-<tag>/` root directory
eachFile {
relativePath = RelativePath(true, *relativePath.segments.drop(1).toTypedArray())
}
}
}
val graalNativeSourcesJar =
tasks.register<Zip>("graalNativeSourcesJar$variant") {
archiveFileName = "graal-native-sources.jar"
destinationDirectory = outputDir
includeEmptyDirs = false
from(provider { tarTree(resources.gzip(graalSourcesTarball.singleFile)) }) {
for (svmProject in target.os.svmNativeProjects) {
include("*/substratevm/src/com.oracle.svm.native.$svmProject/**")
}
eachFile {
relativePath = RelativePath(true, *relativePath.segments.drop(1).toTypedArray())
}
}
}
// Executables for musl targets are statically linked, so musl and zlib are part of them.
val staticLibcSources =
if (target.musl) {
val muslArch =
when (target.arch) {
Target.Arch.AMD64 -> "x86_64"
Target.Arch.AARCH64 -> "aarch64"
}
val muslSourcesJar =
tasks.register<Zip>("muslSourcesJar$variant") {
archiveFileName = "musl-sources.jar"
destinationDirectory = outputDir
includeEmptyDirs = false
from(provider { tarTree(resources.gzip(muslSourcesTarball.singleFile)) }) {
// skip the other architectures
include("*/src/**", "*/include/**", "*/crt/**", "*/ldso/**", "*/compat/**")
include("*/arch/generic/**", "*/arch/$muslArch/**")
include("*/COPYRIGHT", "*/VERSION", "*/WHATSNEW")
eachFile {
relativePath = RelativePath(true, *relativePath.segments.drop(1).toTypedArray())
}
}
}
val zlibSourcesJar =
tasks.register<Zip>("zlibSourcesJar$variant") {
archiveFileName = "zlib-sources.jar"
destinationDirectory = outputDir
includeEmptyDirs = false
from(provider { tarTree(resources.gzip(zlibSourcesTarball.singleFile)) }) {
exclude("*/.github/**")
eachFile {
relativePath = RelativePath(true, *relativePath.segments.drop(1).toTypedArray())
}
}
}
sourceBundleSpec.staticMuslLibc.map { isStatic ->
if (isStatic) files(muslSourcesJar, zlibSourcesJar) else files()
}
} else {
provider { files() }
}
tasks.register<BuildSourceBundle>("sourceBundle$variant") {
group = "build"
description = "Builds the source bundle of ${project.name} for ${target.targetName}."
inputJars.from(
sourceBundleSpec.firstPartyJars,
sourceBundleSpec.extraNativeSources,
nativeImageSourcesJar,
openJdkNativeSourcesJar,
graalNativeSourcesJar,
staticLibcSources,
)
// Only include sources of dependency classes that are compiled into the image.
filteredInputJars.from(resolveNativeSourcesJars.map { fileTree(it.outputDir) })
compiledClasses = compiledClassesFile
outputZip =
layout.buildDirectory.file(
"sourceBundle/${project.name}-${project.version}-${target.targetName}-sourcebundle.zip"
)
}
}
tasks.named("sourceBundle") { dependsOn(sourceBundleTasks) }
/**
* Downloads the lists of classes compiled into native images, which CI uploads as
* `compiled-classes-<project>-<os>-<arch>` artifacts, into `build/compiled-classes`.
*
* Requires the GitHub CLI (`gh`) to be installed and authenticated.
*/
abstract class DownloadCompiledClasses : DefaultTask() {
@get:Option(option = "run-id", description = "ID of the GitHub Actions run to download from.")
@get:Input
abstract val runId: Property<String>
@get:Option(option = "repo", description = "GitHub repository that the run belongs to.")
@get:Input
abstract val repo: Property<String>
/** The project whose artifacts to download (named `compiled-classes-<project>-*`). */
@get:Input abstract val projectName: Property<String>
@get:OutputDirectory abstract val destinationDir: DirectoryProperty
@get:Inject protected abstract val execOperations: ExecOperations
init {
group = "build"
description = "Downloads the compiled classes lists that CI produced into the build directory."
repo.convention("apple/pkl")
// always re-download; this task is meant to be run manually
outputs.upToDateWhen { false }
}
@TaskAction
@Suppress("unused")
fun download() {
if (!runId.isPresent) {
throw GradleException("Specify the run to download from, e.g. `--run-id=<id>`")
}
val name = projectName.get()
val downloadDir = temporaryDir.resolve("download")
downloadDir.deleteRecursively()
downloadDir.mkdirs()
execOperations.exec {
commandLine(
"gh",
"run",
"download",
runId.get(),
"--repo",
repo.get(),
"--pattern",
"compiled-classes-$name-*",
"--dir",
downloadDir.absolutePath,
)
}
// Files are named `<project>-<version>-<target name>.txt`; keep only the target name.
val fileName = Regex("^${Regex.escape(name)}-\\d+(?:\\.\\d+)*(?:-SNAPSHOT)?-(.+\\.txt)$")
val downloaded =
downloadDir
.walkTopDown()
.filter { it.isFile }
.associate { file ->
val target =
fileName.matchEntire(file.name)?.groupValues?.get(1)
?: throw GradleException("Unexpected file in downloaded artifacts: $file")
target to file
}
if (downloaded.isEmpty()) {
throw GradleException("Run ${runId.get()} has no `compiled-classes-$name-*` artifacts")
}
val destination = destinationDir.get().asFile
destination.mkdirs()
// remove lists of targets that are no longer built
destination.listFiles { f -> f.extension == "txt" }?.forEach { it.delete() }
for ((target, file) in downloaded) {
file.copyTo(destination.resolve(target))
}
logger.lifecycle("Downloaded ${downloaded.keys.sorted()} into $destination")
}
}
tasks.register<DownloadCompiledClasses>("downloadCompiledClasses") {
projectName = project.name
destinationDir = layout.buildDirectory.dir("compiled-classes")
}
@@ -0,0 +1,49 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins { id("pklAllProjects") }
// Sources jars of the first-party projects that this project depends on.
// Ideally we'd configure this automatically based on project dependencies.
val firstPartySourcesJars: Configuration = configurations.create("firstPartySourcesJars")
// Sources jars of third-party dependencies. Project dependencies are not covered by this; they
// are added to `firstPartySourcesJars` instead.
val resolveSourcesJars =
tasks.register<ResolveSourcesJars>("resolveSourcesJars") {
configuration.set(configurations.named("runtimeClasspath"))
outputDir.set(layout.buildDirectory.dir("resolveSourcesJars"))
}
val sourceBundleSpec = extensions.create<SourceBundleSpec>("sourceBundleSpec")
sourceBundleSpec.staticMuslLibc.convention(false)
plugins.withId("pklJavaLibrary") { sourceBundleSpec.firstPartyJars.from(tasks.named("sourcesJar")) }
plugins.withId("pklKotlinLibrary") {
sourceBundleSpec.firstPartyJars.from(tasks.named("sourcesJar"))
}
sourceBundleSpec.firstPartyJars.from(firstPartySourcesJars)
sourceBundleSpec.dependencyJars.from(resolveSourcesJars.map { fileTree(it.outputDir) })
// Builds every source bundle of this project; there is one for each variant that the project is
// distributed as. The variants are registered by `pklJvmSourceBundle` and `pklNativeSourceBundle`.
tasks.register("sourceBundle") {
group = "build"
description = "Builds the source bundles of all variants of this project."
}
+28 -10
View File
@@ -23,6 +23,7 @@ plugins {
id("pklGraalVm")
id("pklJavaLibrary")
id("pklNativeLifecycle")
id("pklNativeSourceBundle")
}
val stagedMacAarch64NativeLibrary: Configuration =
@@ -66,6 +67,11 @@ dependencies {
nativeTestRuntimeOnly("org.junit.platform:junit-platform-launcher")
externalReaderFixtureConfiguration(project(":pkl-core", "externalReaderFixture"))
// firstPartySourcesJars(project(":pkl-formatter", "sourcesJar")) // TODO when syntax merges
firstPartySourcesJars(project(":pkl-core", "sourcesJar"))
firstPartySourcesJars(project(":pkl-parser", "sourcesJar"))
firstPartySourcesJars(project(":pkl-server", "sourcesJar"))
}
tasks.withType(CCompile::class) {
@@ -146,19 +152,20 @@ val windowsNativeImageAmd64 =
extraNativeImageArgs.addAll("-Dfile.encoding=UTF-8", "-H:-CheckToolchain")
}
val nativeImageBuildTask =
when (buildInfo.targetMachine) {
Target.MacosAarch64 -> macNativeImageAarch64
Target.LinuxAarch64 -> linuxNativeImageAarch64
Target.LinuxAmd64 -> linuxNativeImageAmd64
Target.WindowsAmd64 -> windowsNativeImageAmd64
Target.AlpineLinuxAmd64 -> alpineLinuxNativeImageAmd64
}
val buildNativeImageLibrary =
tasks.register("buildNativeImageLibrary") {
group = "build"
val underlyingTask =
when (buildInfo.targetMachine) {
Target.MacosAarch64 -> macNativeImageAarch64
Target.LinuxAarch64 -> linuxNativeImageAarch64
Target.LinuxAmd64 -> linuxNativeImageAmd64
Target.WindowsAmd64 -> windowsNativeImageAmd64
Target.AlpineLinuxAmd64 -> alpineLinuxNativeImageAmd64
}
dependsOn(underlyingTask)
outputs.files(underlyingTask)
dependsOn(nativeImageBuildTask)
outputs.files(nativeImageBuildTask)
}
val buildPklObjectFile =
@@ -282,6 +289,17 @@ val buildSharedLibrary =
}
}
// Source bundles for each target are registered by `pklNativeSourceBundle`. libpkl additionally
// includes its own C sources, which are the same for all targets.
val libpklNativeSourcesJar =
tasks.register<Zip>("libpklNativeSourcesJar") {
archiveFileName = "libpkl-native-sources.jar"
destinationDirectory = layout.buildDirectory.dir("tmp/nativeImageSources")
from("src/main/c")
}
sourceBundleSpec { extraNativeSources.from(libpklNativeSourcesJar) }
val Target.outputDir
get() = layout.buildDirectory.dir("native-libs/$targetName")
+9
View File
@@ -65,6 +65,15 @@ dependencies {
testImplementation(projects.pklCommonsTest)
testImplementation(libs.wiremock)
// used by `pklJavaExecutable` plugin (ideally this would be inferred automatically)
firstPartySourcesJars(project(":pkl-cli", "sourcesJar"))
firstPartySourcesJars(project(":pkl-commons", "sourcesJar"))
firstPartySourcesJars(project(":pkl-commons-cli", "sourcesJar"))
firstPartySourcesJars(project(":pkl-core", "sourcesJar"))
firstPartySourcesJars(project(":pkl-formatter", "sourcesJar"))
firstPartySourcesJars(project(":pkl-parser", "sourcesJar"))
firstPartySourcesJars(project(":pkl-server", "sourcesJar"))
}
tasks.jar {
@@ -30,6 +30,13 @@ dependencies {
testImplementation(projects.pklConfigJava)
testImplementation(projects.pklCommonsTest)
// used by `pklJavaExecutable` plugin (ideally this would be inferred automatically)
firstPartySourcesJars(project(":pkl-codegen-java", "sourcesJar"))
firstPartySourcesJars(project(":pkl-commons", "sourcesJar"))
firstPartySourcesJars(project(":pkl-commons-cli", "sourcesJar"))
firstPartySourcesJars(project(":pkl-core", "sourcesJar"))
firstPartySourcesJars(project(":pkl-parser", "sourcesJar"))
}
executable {
@@ -53,6 +53,13 @@ dependencies {
testImplementation(projects.pklConfigKotlin)
testImplementation(projects.pklCommonsTest)
testRuntimeOnly(libs.kotlinScripting)
// used by `pklJavaExecutable` plugin (ideally this would be inferred automatically)
firstPartySourcesJars(project(":pkl-codegen-kotlin", "sourcesJar"))
firstPartySourcesJars(project(":pkl-commons", "sourcesJar"))
firstPartySourcesJars(project(":pkl-commons-cli", "sourcesJar"))
firstPartySourcesJars(project(":pkl-core", "sourcesJar"))
firstPartySourcesJars(project(":pkl-parser", "sourcesJar"))
}
executable {
@@ -87,7 +87,9 @@ dependencies {
testImplementation(libs.javaxInject)
// firstPartySourcesJars(project(":pkl-formatter", "sourcesJar")) // TODO when syntax merges
firstPartySourcesJars(project(":pkl-core", "sourcesJar"))
firstPartySourcesJars(project(":pkl-parser", "sourcesJar"))
pklCodegenJava(projects.pklCodegenJava)
}
+6
View File
@@ -56,6 +56,12 @@ dependencies {
// Graal.JS
testImplementation(libs.graalSdk)
testImplementation(libs.graalJs)
// used by `pklJavaExecutable` plugin (ideally this would be inferred automatically)
firstPartySourcesJars(project(":pkl-commons", "sourcesJar"))
firstPartySourcesJars(project(":pkl-commons-cli", "sourcesJar"))
firstPartySourcesJars(project(":pkl-core", "sourcesJar"))
firstPartySourcesJars(project(":pkl-parser", "sourcesJar"))
}
publishing {
+5
View File
@@ -48,9 +48,14 @@ dependencies {
firstPartySourcesJars(project(":pkl-cli", "sourcesJar"))
firstPartySourcesJars(project(":pkl-codegen-java", "sourcesJar"))
firstPartySourcesJars(project(":pkl-codegen-kotlin", "sourcesJar"))
firstPartySourcesJars(project(":pkl-commons", "sourcesJar"))
firstPartySourcesJars(project(":pkl-commons-cli", "sourcesJar"))
firstPartySourcesJars(project(":pkl-config-java", "sourcesJar"))
firstPartySourcesJars(project(":pkl-core", "sourcesJar"))
firstPartySourcesJars(project(":pkl-doc", "sourcesJar"))
firstPartySourcesJars(project(":pkl-formatter", "sourcesJar"))
firstPartySourcesJars(project(":pkl-parser", "sourcesJar"))
firstPartySourcesJars(project(":pkl-server", "sourcesJar"))
}
// TODO: need to figure out how to properly generate javadoc here.
+34 -1
View File
@@ -68,7 +68,40 @@ pluginManagement {
plugins { id("org.gradle.toolchains.foojay-resolver-convention") version ("1.0.0") }
@Suppress("UnstableApiUsage") dependencyResolutionManagement { repositories { mavenCentral() } }
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositories {
mavenCentral()
// Source archives of tagged GitHub releases (used to bundle sources of native executables).
exclusiveContent {
forRepository {
ivy {
name = "GitHub tag archives"
url = uri("https://github.com/")
patternLayout { artifact("[organisation]/[module]/archive/refs/tags/[revision].tar.gz") }
metadataSources { artifact() }
}
}
filter {
includeGroup("madler")
includeGroup("openjdk")
includeGroup("oracle")
}
}
// musl's release tarballs (used to bundle sources of statically linked musl executables).
exclusiveContent {
forRepository {
ivy {
name = "musl releases"
url = uri("https://musl.libc.org/releases/")
patternLayout { artifact("[module]-[revision].tar.gz") }
metadataSources { artifact() }
}
}
filter { includeGroup("musl") }
}
}
}
for (prj in rootProject.children) {
prj.buildFileName = "${prj.name}.gradle.kts"