-
-
Notifications
You must be signed in to change notification settings - Fork 468
fix(build): remove Spring Boot 2 Gradle plugin for Gradle 9 compatibility #5263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
romtsn
merged 36 commits into
deps/scripts/update-gradle.sh
from
fix/spring-boot2-gradle9
Apr 14, 2026
Merged
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
055a019
fix(build): remove Spring Boot 2 Gradle plugin for Gradle 9 compatibiโฆ
adinauer 9dd4f2f
fix: set duplicatesStrategy=INCLUDE for shadow JAR spring.factories mโฆ
adinauer 63aa458
fix: remove duplicate shadow plugin entry in version catalog
adinauer 0e43fc7
Format code
getsentry-bot da7d400
fix: update system test runner for shadow JAR compatibility
adinauer c0acbd7
fix(otel): use DuplicatesStrategy.INCLUDE for otel agent shadow JAR
adinauer c3c8c24
Exclude test-support modules from api validation
romtsn 76eba12
Verbose system test output and wire inputs for them properly
romtsn 5bfc096
align coroutines version to 1.9.0 for system tests
romtsn 5a29bc6
fix(otel): use mergeServiceFiles path instead of include for Shadow 9.x
romtsn f47be74
fix(otel): add default mergeServiceFiles for bootstrap service relocaโฆ
romtsn 00c8173
fix(spring-boot2): pre-merge Spring metadata for Shadow 9.x compatibiโฆ
romtsn 3bfe40d
Format code
getsentry-bot 0a95998
fix(lint): suppress OldTargetApi for uitest-android module
romtsn 9a388af
fix(spring-boot2): make mergeSpringMetadata configuration-cache compaโฆ
romtsn 8ae3c9c
formatting
romtsn 4a48a27
fix(spring-boot2): replace from() with doLast JAR patching for springโฆ
romtsn 05c5bac
formatting
romtsn 4bef2eb
fix(spring-boot2): merge AutoConfiguration.imports + doLast JAR patching
romtsn 9fa7649
fix(spring-boot2): also merge ManagementContextConfiguration.imports
romtsn 4a277ae
formatting
romtsn 991221e
fix(spring-boot2): use separate patchSpringMetadata task for JAR patcโฆ
romtsn 500f7f1
formatting
romtsn cafc487
fix(spring-boot2): revert to doLast on shadowJar for Spring metadata โฆ
romtsn 702dfd0
formatting
romtsn 821da53
fix(spring-boot2): inline Spring metadata merge into shadowJar doLast
romtsn 0e33152
formatting
romtsn 13f278b
fix(build): make Spring sample shadowJar patching config-cache safe
romtsn 247a40c
fix(build): merge Spring metadata properties in shadow jars
romtsn d184e8b
fix(build): preserve escaped spring metadata keys
romtsn ab5cfac
refactor(samples): drop no-op spring shadow service merging
romtsn da63abe
test(android): Avoid ANR profiling integration test race
romtsn 8fd06fb
fix(test): Require actuator health for Spring readiness
romtsn 7b05a2b
ref(build): Share Spring metadata file list
romtsn 327123a
docs(build): Document Spring metadata merge action
romtsn 119b6f8
build(opentelemetry): Fail agent shadow duplicates by default
adinauer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,292 @@ | ||
| import java.net.URI | ||
| import java.nio.file.FileSystems | ||
| import java.nio.file.Files | ||
| import java.util.LinkedHashSet | ||
| import java.util.zip.ZipFile | ||
| import org.gradle.api.Action | ||
| import org.gradle.api.Task | ||
| import org.gradle.api.file.FileCollection | ||
| import org.gradle.api.tasks.bundling.AbstractArchiveTask | ||
|
|
||
| /** | ||
| * Patches a built shadow JAR by merging Spring metadata and service descriptor files from the | ||
| * runtime classpath into the final archive. | ||
| * | ||
| * Spring metadata files do not all share the same merge semantics, so this action merges | ||
| * `spring.factories` as list properties, `.imports` files as line-based metadata, and other Spring | ||
| * metadata as key/value properties. It also deduplicates service-provider configuration entries | ||
| * under `META-INF/services` so the flat executable JAR keeps the runtime registrations it needs. | ||
| */ | ||
| class MergeSpringMetadataAction( | ||
| private val runtimeClasspath: FileCollection, | ||
| private val springMetadataFiles: List<String>, | ||
| ) : Action<Task> { | ||
| companion object { | ||
| val DEFAULT_SPRING_METADATA_FILES = | ||
| listOf( | ||
| "META-INF/spring.factories", | ||
| "META-INF/spring.handlers", | ||
| "META-INF/spring.schemas", | ||
| "META-INF/spring-autoconfigure-metadata.properties", | ||
| "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports", | ||
| "META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports", | ||
| ) | ||
| } | ||
|
|
||
| override fun execute(task: Task) { | ||
| val archiveTask = task as AbstractArchiveTask | ||
| val jar = archiveTask.archiveFile.get().asFile | ||
| val runtimeJars = runtimeClasspath.files.filter { it.name.endsWith(".jar") } | ||
| val uri = URI.create("jar:${jar.toURI()}") | ||
|
|
||
| FileSystems.newFileSystem(uri, mapOf("create" to "false")).use { fs -> | ||
| springMetadataFiles.forEach { entryPath -> | ||
| val target = fs.getPath(entryPath) | ||
| val contents = mutableListOf<String>() | ||
|
|
||
| if (Files.exists(target)) { | ||
| contents.add(Files.readString(target)) | ||
| } | ||
|
|
||
| runtimeJars.forEach { depJar -> | ||
| try { | ||
| ZipFile(depJar).use { zip -> | ||
| val entry = zip.getEntry(entryPath) | ||
| if (entry != null) { | ||
| contents.add(zip.getInputStream(entry).bufferedReader().readText()) | ||
| } | ||
| } | ||
| } catch (_: Exception) { | ||
| // Ignore non-zip files on the runtime classpath. | ||
| } | ||
| } | ||
|
|
||
| val merged = | ||
| when { | ||
| entryPath == "META-INF/spring.factories" -> mergeListProperties(contents) | ||
| entryPath.endsWith(".imports") -> mergeLineBasedMetadata(contents) | ||
| else -> mergeMapProperties(contents) | ||
| } | ||
|
|
||
| if (merged.isNotEmpty()) { | ||
| if (target.parent != null) { | ||
| Files.createDirectories(target.parent) | ||
| } | ||
| Files.write(target, merged.toByteArray()) | ||
| } | ||
| } | ||
|
|
||
| val serviceEntries = linkedSetOf<String>() | ||
|
|
||
| runtimeJars.forEach { depJar -> | ||
| try { | ||
| ZipFile(depJar).use { zip -> | ||
| val entries = zip.entries() | ||
| while (entries.hasMoreElements()) { | ||
| val entry = entries.nextElement() | ||
| if (!entry.isDirectory && entry.name.startsWith("META-INF/services/")) { | ||
| serviceEntries.add(entry.name) | ||
| } | ||
| } | ||
| } | ||
| } catch (_: Exception) { | ||
| // Ignore non-zip files on the runtime classpath. | ||
| } | ||
| } | ||
|
|
||
| serviceEntries.forEach { entryPath -> | ||
| val providers = LinkedHashSet<String>() | ||
| val target = fs.getPath(entryPath) | ||
|
|
||
| if (Files.exists(target)) { | ||
| Files.newBufferedReader(target).useLines { lines -> | ||
| lines.forEach { line -> | ||
| val provider = line.trim() | ||
| if (provider.isNotEmpty() && !provider.startsWith("#")) { | ||
| providers.add(provider) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| runtimeJars.forEach { depJar -> | ||
| try { | ||
| ZipFile(depJar).use { zip -> | ||
| val entry = zip.getEntry(entryPath) | ||
| if (entry != null) { | ||
| zip.getInputStream(entry).bufferedReader().useLines { lines -> | ||
| lines.forEach { line -> | ||
| val provider = line.trim() | ||
| if (provider.isNotEmpty() && !provider.startsWith("#")) { | ||
| providers.add(provider) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } catch (_: Exception) { | ||
| // Ignore non-zip files on the runtime classpath. | ||
| } | ||
| } | ||
|
|
||
| if (providers.isNotEmpty()) { | ||
| if (target.parent != null) { | ||
| Files.createDirectories(target.parent) | ||
| } | ||
| Files.write(target, providers.joinToString(separator = "\n", postfix = "\n").toByteArray()) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun mergeLineBasedMetadata(contents: List<String>): String { | ||
| val lines = LinkedHashSet<String>() | ||
|
|
||
| contents.forEach { content -> | ||
| content.lineSequence().forEach { rawLine -> | ||
| val line = rawLine.trim() | ||
| if (line.isNotEmpty() && !line.startsWith("#")) { | ||
| lines.add(line) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return if (lines.isEmpty()) "" else lines.joinToString(separator = "\n", postfix = "\n") | ||
| } | ||
|
|
||
| private fun mergeMapProperties(contents: List<String>): String { | ||
| val merged = linkedMapOf<String, String>() | ||
|
|
||
| contents.forEach { content -> | ||
| parseProperties(content).forEach { (key, value) -> | ||
| merged[key] = value | ||
| } | ||
| } | ||
|
|
||
| return if (merged.isEmpty()) { | ||
| "" | ||
| } else { | ||
| merged.entries.joinToString(separator = "\n", postfix = "\n") { (key, value) -> "$key=$value" } | ||
| } | ||
| } | ||
|
|
||
| private fun mergeListProperties(contents: List<String>): String { | ||
| val merged = linkedMapOf<String, LinkedHashSet<String>>() | ||
|
|
||
| contents.forEach { content -> | ||
| parseProperties(content).forEach { (key, value) -> | ||
| val values = merged.getOrPut(key) { LinkedHashSet() } | ||
| value | ||
| .split(',') | ||
| .map(String::trim) | ||
| .filter(String::isNotEmpty) | ||
| .forEach(values::add) | ||
| } | ||
| } | ||
|
|
||
| return if (merged.isEmpty()) { | ||
| "" | ||
| } else { | ||
| merged.entries.joinToString(separator = "\n", postfix = "\n") { (key, values) -> | ||
| "$key=${values.joinToString(separator = ",")}" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun parseProperties(content: String): List<Pair<String, String>> { | ||
| val logicalLines = mutableListOf<String>() | ||
| val current = StringBuilder() | ||
|
|
||
| content.lineSequence().forEach { rawLine -> | ||
| val line = rawLine.trim() | ||
| if (current.isEmpty() && (line.isEmpty() || line.startsWith("#") || line.startsWith("!"))) { | ||
| return@forEach | ||
| } | ||
|
|
||
| val normalized = if (current.isEmpty()) line else line.trimStart() | ||
| current.append( | ||
| if (endsWithContinuation(rawLine)) normalized.dropLast(1) else normalized, | ||
| ) | ||
|
|
||
| if (!endsWithContinuation(rawLine)) { | ||
| logicalLines.add(current.toString()) | ||
| current.setLength(0) | ||
| } | ||
| } | ||
|
|
||
| if (current.isNotEmpty()) { | ||
| logicalLines.add(current.toString()) | ||
| } | ||
|
|
||
| return logicalLines.map { line -> | ||
| val separatorIndex = findSeparatorIndex(line) | ||
| if (separatorIndex < 0) { | ||
| line to "" | ||
| } else { | ||
| val keyEnd = trimTrailingWhitespace(line, separatorIndex) | ||
| val valueStart = findValueStart(line, separatorIndex) | ||
| line.substring(0, keyEnd) to line.substring(valueStart).trim() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun endsWithContinuation(line: String): Boolean { | ||
| var backslashCount = 0 | ||
|
|
||
| for (index in line.length - 1 downTo 0) { | ||
| if (line[index] == '\\') { | ||
| backslashCount++ | ||
| } else { | ||
| break | ||
| } | ||
| } | ||
|
|
||
| return backslashCount % 2 == 1 | ||
| } | ||
|
|
||
| private fun findSeparatorIndex(line: String): Int { | ||
| var backslashCount = 0 | ||
|
|
||
| line.forEachIndexed { index, char -> | ||
| if (char == '\\') { | ||
| backslashCount++ | ||
| } else { | ||
| val isEscaped = backslashCount % 2 == 1 | ||
| if (!isEscaped && (char == '=' || char == ':' || char.isWhitespace())) { | ||
| return index | ||
| } | ||
| backslashCount = 0 | ||
| } | ||
| } | ||
|
|
||
| return -1 | ||
| } | ||
|
|
||
| private fun trimTrailingWhitespace(line: String, endExclusive: Int): Int { | ||
| var end = endExclusive | ||
|
|
||
| while (end > 0 && line[end - 1].isWhitespace()) { | ||
| end-- | ||
| } | ||
|
|
||
| return end | ||
| } | ||
|
|
||
| private fun findValueStart(line: String, separatorIndex: Int): Int { | ||
| var valueStart = separatorIndex | ||
|
|
||
| while (valueStart < line.length && line[valueStart].isWhitespace()) { | ||
| valueStart++ | ||
| } | ||
|
|
||
| if (valueStart < line.length && (line[valueStart] == '=' || line[valueStart] == ':')) { | ||
| valueStart++ | ||
| } | ||
|
|
||
| while (valueStart < line.length && line[valueStart].isWhitespace()) { | ||
| valueStart++ | ||
| } | ||
|
|
||
| return valueStart | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.