Skip to content

Commit 9e61ed3

Browse files
committed
feat: enhance Gradle Dependency Bundle with snapshot aliasing, offline example build, and stricter artifact verification
1 parent 52e89b1 commit 9e61ed3

6 files changed

Lines changed: 103 additions & 5 deletions

File tree

.github/workflows/ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,21 @@ jobs:
3030
test -n "$(find "$repo" -name 'kotlinpoet-jvm-*.jar' -print -quit)"
3131
test -n "$(find "$repo" -name '*-sources.jar' -print -quit)"
3232
test -n "$(find "$repo" -name '*.module' -print -quit)"
33+
version=$(sed -n 's/^version=//p' gradle.properties)
34+
group="$repo/org/openprojectx/gradle/dependency/bundle"
35+
test -f "$group/org.openprojectx.gradle.dependency.bundle.gradle.plugin/$version/org.openprojectx.gradle.dependency.bundle.gradle.plugin-$version.pom"
36+
test -f "$group/plugin/$version/plugin-$version.pom"
37+
test -f "$group/plugin/$version/plugin-$version.module"
38+
test -f "$group/plugin/$version/plugin-$version.jar"
3339
3440
- name: Run independent example using only the bundle
3541
run: |
3642
mkdir -p build/offline-gradle-home
3743
cp -R build/dependency-bundle-gradle-home/wrapper build/offline-gradle-home/
44+
version=$(sed -n 's/^version=//p' gradle.properties)
3845
./gradlew --no-daemon -g build/offline-gradle-home -p example \
3946
-PbundleRepository="$PWD/build/dependency-bundle/m2/repository" \
47+
-PbundlePluginVersion="$version" \
4048
-PbundleOfflineOnly --offline clean run
4149
4250
- name: Build and inspect OCI image

build.gradle.kts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ val bundleRepository = bundleDirectory.map { it.dir("m2/repository") }
163163
// Gradle may clean task outputs before execution, which would otherwise remove
164164
// the running wrapper distribution and daemon registry from underneath itself.
165165
val bundleGradleHome = layout.buildDirectory.dir("dependency-bundle-gradle-home")
166+
val exampleGradleHome = layout.buildDirectory.dir("dependency-bundle-example-gradle-home")
166167

167168
val publishBundleArtifacts by tasks.registering {
168169
group = "dependency bundle"
@@ -197,15 +198,25 @@ val captureRootDependencyBundle by tasks.registering(Exec::class) {
197198
environment("GRADLE_USER_HOME", gradleHome)
198199
}
199200

200-
val buildExampleAgainstBundle by tasks.registering(Exec::class) {
201+
val prepareExampleGradleHome by tasks.registering(Sync::class) {
201202
group = "dependency bundle"
203+
description = "Creates a clean Gradle home containing only the cached wrapper distribution."
202204
dependsOn(captureRootDependencyBundle)
205+
from(bundleGradleHome.map { it.dir("wrapper") })
206+
into(exampleGradleHome.map { it.dir("wrapper") })
207+
}
208+
209+
val buildExampleAgainstBundle by tasks.registering(Exec::class) {
210+
group = "dependency bundle"
211+
dependsOn(prepareExampleGradleHome)
203212
val repository = bundleRepository.get().asFile.absolutePath
204-
val gradleHome = bundleGradleHome.get().asFile.absolutePath
213+
val gradleHome = exampleGradleHome.get().asFile.absolutePath
205214
commandLine(
206-
"./gradlew", "--no-daemon", "--no-configuration-cache",
215+
"./gradlew", "--no-daemon", "--no-configuration-cache", "--offline",
207216
"-p", "example",
208217
"-PbundleRepository=$repository",
218+
"-PbundlePluginVersion=${project.version}",
219+
"-PbundleOfflineOnly",
209220
"-PbundleOutput=${bundleDirectory.get().asFile.absolutePath}",
210221
"build",
211222
)
@@ -222,6 +233,7 @@ val captureExampleDependencyBundle by tasks.registering(Exec::class) {
222233
"./gradlew", "--no-daemon", "--no-configuration-cache",
223234
"-p", "example",
224235
"-PbundleRepository=$repository",
236+
"-PbundlePluginVersion=${project.version}",
225237
"-PbundleOutput=$output",
226238
"dependencyBundleReport",
227239
)
@@ -265,6 +277,20 @@ val verifyPreparedDependencyBundle by tasks.registering {
265277
val actual = digest.digest().joinToString("") { "%02x".format(it) }
266278
check(actual == artifact.getValue("sha256")) { "Manifest checksum differs for $relative" }
267279
}
280+
val version = project.version.toString()
281+
val groupPath = "org/openprojectx/gradle/dependency/bundle"
282+
val requiredLocalArtifacts = listOf(
283+
"$groupPath/org.openprojectx.gradle.dependency.bundle.gradle.plugin/$version/" +
284+
"org.openprojectx.gradle.dependency.bundle.gradle.plugin-$version.pom",
285+
"$groupPath/plugin/$version/plugin-$version.pom",
286+
"$groupPath/plugin/$version/plugin-$version.module",
287+
"$groupPath/plugin/$version/plugin-$version.jar",
288+
)
289+
requiredLocalArtifacts.forEach { relative ->
290+
check(repository.get().file(relative).asFile.isFile) {
291+
"Canonical Maven-local artifact is absent: $relative"
292+
}
293+
}
268294
logger.lifecycle("Verified {} bundled artifacts", artifacts.size)
269295
}
270296
}
@@ -294,9 +320,11 @@ configure<ReleaseExtension> {
294320
buildTasks.set(
295321
listOf(
296322
"syncDocsVersion",
323+
"test",
324+
"verifyPreparedDependencyBundle",
325+
":auditor-cli:jib",
297326
"publishToSonatype",
298327
"closeAndReleaseSonatypeStagingRepository",
299-
":auditor-cli:jib",
300328
)
301329
)
302330
versionPropertyFile.set("gradle.properties")
@@ -306,3 +334,9 @@ configure<ReleaseExtension> {
306334
requireBranch.set("master")
307335
}
308336
}
337+
338+
// Publishing must never begin until the bundle and its isolated offline
339+
// consumer build have passed, including when publication is invoked directly.
340+
tasks.matching { it.name == "publishToSonatype" }.configureEach {
341+
dependsOn(verifyPreparedDependencyBundle)
342+
}

example/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
plugins {
22
kotlin("jvm") version "2.2.21"
3-
id("org.openprojectx.gradle.dependency.bundle") version "0.1.0-SNAPSHOT"
3+
id("org.openprojectx.gradle.dependency.bundle")
44
application
55
}
66

example/settings.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
pluginManagement {
2+
val bundledPluginVersion = providers.gradleProperty("bundlePluginVersion")
3+
resolutionStrategy.eachPlugin {
4+
if (requested.id.id == "org.openprojectx.gradle.dependency.bundle") {
5+
useVersion(bundledPluginVersion.get())
6+
}
7+
}
28
repositories {
39
maven { url = uri(providers.gradleProperty("bundleRepository").get()) }
410
if (!providers.gradleProperty("bundleOfflineOnly").isPresent) {

gradle/dependency-bundle.init.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ gradle.beforeProject { project ->
2424
includeBuildDependencies.set(true)
2525
includeSources.set(true)
2626
configurations.addAll("runtimeClasspath", "testRuntimeClasspath")
27+
// The independent example runs offline before its own graph is
28+
// captured, so seed its complete runtime graph in the root bundle.
29+
module("com.squareup:kotlinpoet:2.3.0")
2730
module("org.gradle.kotlin:gradle-kotlin-dsl-plugins:6.5.2")
2831
gradleApiVariants(
2932
"org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.21",

plugin/src/main/kotlin/org/openprojectx/gradle/dependency/bundle/ExportDependencyBundleTask.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ abstract class ExportDependencyBundleTask : DefaultTask() {
7272
copyGradleCache(repository)
7373
materializeDeclaredArtifacts(repository)
7474
materializeConventionalSources(repository)
75+
materializeCanonicalSnapshotAliases(repository)
7576

7677
discoverRepositoryComponents(repository)
7778
val artifacts = inventory(repository)
@@ -288,6 +289,52 @@ abstract class ExportDependencyBundleTask : DefaultTask() {
288289
return true
289290
}
290291

292+
/**
293+
* Gradle's cache retains the timestamped form of unique Maven snapshots. A
294+
* Maven-local repository has no remote metadata lookup, so consumers also
295+
* need canonical `-SNAPSHOT` names. Materialize every artifact and sidecar,
296+
* including POM-only plugin markers and Gradle module metadata.
297+
*/
298+
private fun materializeCanonicalSnapshotAliases(repository: Path) {
299+
val snapshotDirectories = Files.walk(repository).use { paths ->
300+
paths.filter { directory ->
301+
Files.isDirectory(directory) &&
302+
repository.relativize(directory).nameCount >= 3 &&
303+
directory.fileName.toString().endsWith("-SNAPSHOT")
304+
}.toList()
305+
}
306+
307+
snapshotDirectories.forEach { directory ->
308+
val version = directory.fileName.toString()
309+
val module = directory.parent.fileName.toString()
310+
val baseVersion = version.removeSuffix("-SNAPSHOT")
311+
val uniquePattern = Regex(
312+
"^${Regex.escape("$module-$baseVersion-")}\\d{8}\\.\\d{6}-\\d+(.*)$"
313+
)
314+
val latestBySuffix = Files.list(directory).use { files ->
315+
files.filter(Files::isRegularFile)
316+
.map { file -> file to uniquePattern.matchEntire(file.fileName.toString()) }
317+
.filter { (_, match) -> match != null }
318+
.map { (file, match) -> match!!.groupValues[1] to file }
319+
.toList()
320+
.groupBy({ it.first }, { it.second })
321+
.mapValues { (_, candidates) -> candidates.maxBy { it.fileName.toString() } }
322+
}
323+
324+
latestBySuffix.forEach { (suffix, source) ->
325+
val destination = directory.resolve("$module-$version$suffix")
326+
if (!Files.exists(destination) || Files.mismatch(source, destination) != -1L) {
327+
Files.copy(
328+
source,
329+
destination,
330+
StandardCopyOption.REPLACE_EXISTING,
331+
StandardCopyOption.COPY_ATTRIBUTES,
332+
)
333+
}
334+
}
335+
}
336+
}
337+
291338
/**
292339
* Maven POM-only modules do not describe source artifacts. Try the conventional
293340
* classifier for every release module that has a primary JAR, while treating a

0 commit comments

Comments
 (0)