Skip to content

Commit 20e3a13

Browse files
authored
Merge da63abe into 219f319
2 parents 219f319 + da63abe commit 20e3a13

36 files changed

Lines changed: 618 additions & 735 deletions

File tree

build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ apiValidation {
8787
"test-app-sentry",
8888
"test-app-size",
8989
"sentry-samples-netflix-dgs",
90-
"sentry-samples-console-otlp"
90+
"sentry-samples-console-otlp",
91+
"sentry-test-support",
92+
"sentry-system-test-support"
9193
)
9294
)
9395
}
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import java.net.URI
2+
import java.nio.file.FileSystems
3+
import java.nio.file.Files
4+
import java.util.LinkedHashSet
5+
import java.util.zip.ZipFile
6+
import org.gradle.api.Action
7+
import org.gradle.api.Task
8+
import org.gradle.api.file.FileCollection
9+
import org.gradle.api.tasks.bundling.AbstractArchiveTask
10+
11+
class MergeSpringMetadataAction(
12+
private val runtimeClasspath: FileCollection,
13+
private val springMetadataFiles: List<String>,
14+
) : Action<Task> {
15+
16+
override fun execute(task: Task) {
17+
val archiveTask = task as AbstractArchiveTask
18+
val jar = archiveTask.archiveFile.get().asFile
19+
val runtimeJars = runtimeClasspath.files.filter { it.name.endsWith(".jar") }
20+
val uri = URI.create("jar:${jar.toURI()}")
21+
22+
FileSystems.newFileSystem(uri, mapOf("create" to "false")).use { fs ->
23+
springMetadataFiles.forEach { entryPath ->
24+
val target = fs.getPath(entryPath)
25+
val contents = mutableListOf<String>()
26+
27+
if (Files.exists(target)) {
28+
contents.add(Files.readString(target))
29+
}
30+
31+
runtimeJars.forEach { depJar ->
32+
try {
33+
ZipFile(depJar).use { zip ->
34+
val entry = zip.getEntry(entryPath)
35+
if (entry != null) {
36+
contents.add(zip.getInputStream(entry).bufferedReader().readText())
37+
}
38+
}
39+
} catch (_: Exception) {
40+
// Ignore non-zip files on the runtime classpath.
41+
}
42+
}
43+
44+
val merged =
45+
when {
46+
entryPath == "META-INF/spring.factories" -> mergeListProperties(contents)
47+
entryPath.endsWith(".imports") -> mergeLineBasedMetadata(contents)
48+
else -> mergeMapProperties(contents)
49+
}
50+
51+
if (merged.isNotEmpty()) {
52+
if (target.parent != null) {
53+
Files.createDirectories(target.parent)
54+
}
55+
Files.write(target, merged.toByteArray())
56+
}
57+
}
58+
59+
val serviceEntries = linkedSetOf<String>()
60+
61+
runtimeJars.forEach { depJar ->
62+
try {
63+
ZipFile(depJar).use { zip ->
64+
val entries = zip.entries()
65+
while (entries.hasMoreElements()) {
66+
val entry = entries.nextElement()
67+
if (!entry.isDirectory && entry.name.startsWith("META-INF/services/")) {
68+
serviceEntries.add(entry.name)
69+
}
70+
}
71+
}
72+
} catch (_: Exception) {
73+
// Ignore non-zip files on the runtime classpath.
74+
}
75+
}
76+
77+
serviceEntries.forEach { entryPath ->
78+
val providers = LinkedHashSet<String>()
79+
val target = fs.getPath(entryPath)
80+
81+
if (Files.exists(target)) {
82+
Files.newBufferedReader(target).useLines { lines ->
83+
lines.forEach { line ->
84+
val provider = line.trim()
85+
if (provider.isNotEmpty() && !provider.startsWith("#")) {
86+
providers.add(provider)
87+
}
88+
}
89+
}
90+
}
91+
92+
runtimeJars.forEach { depJar ->
93+
try {
94+
ZipFile(depJar).use { zip ->
95+
val entry = zip.getEntry(entryPath)
96+
if (entry != null) {
97+
zip.getInputStream(entry).bufferedReader().useLines { lines ->
98+
lines.forEach { line ->
99+
val provider = line.trim()
100+
if (provider.isNotEmpty() && !provider.startsWith("#")) {
101+
providers.add(provider)
102+
}
103+
}
104+
}
105+
}
106+
}
107+
} catch (_: Exception) {
108+
// Ignore non-zip files on the runtime classpath.
109+
}
110+
}
111+
112+
if (providers.isNotEmpty()) {
113+
if (target.parent != null) {
114+
Files.createDirectories(target.parent)
115+
}
116+
Files.write(target, providers.joinToString(separator = "\n", postfix = "\n").toByteArray())
117+
}
118+
}
119+
}
120+
}
121+
122+
private fun mergeLineBasedMetadata(contents: List<String>): String {
123+
val lines = LinkedHashSet<String>()
124+
125+
contents.forEach { content ->
126+
content.lineSequence().forEach { rawLine ->
127+
val line = rawLine.trim()
128+
if (line.isNotEmpty() && !line.startsWith("#")) {
129+
lines.add(line)
130+
}
131+
}
132+
}
133+
134+
return if (lines.isEmpty()) "" else lines.joinToString(separator = "\n", postfix = "\n")
135+
}
136+
137+
private fun mergeMapProperties(contents: List<String>): String {
138+
val merged = linkedMapOf<String, String>()
139+
140+
contents.forEach { content ->
141+
parseProperties(content).forEach { (key, value) ->
142+
merged[key] = value
143+
}
144+
}
145+
146+
return if (merged.isEmpty()) {
147+
""
148+
} else {
149+
merged.entries.joinToString(separator = "\n", postfix = "\n") { (key, value) -> "$key=$value" }
150+
}
151+
}
152+
153+
private fun mergeListProperties(contents: List<String>): String {
154+
val merged = linkedMapOf<String, LinkedHashSet<String>>()
155+
156+
contents.forEach { content ->
157+
parseProperties(content).forEach { (key, value) ->
158+
val values = merged.getOrPut(key) { LinkedHashSet() }
159+
value
160+
.split(',')
161+
.map(String::trim)
162+
.filter(String::isNotEmpty)
163+
.forEach(values::add)
164+
}
165+
}
166+
167+
return if (merged.isEmpty()) {
168+
""
169+
} else {
170+
merged.entries.joinToString(separator = "\n", postfix = "\n") { (key, values) ->
171+
"$key=${values.joinToString(separator = ",")}"
172+
}
173+
}
174+
}
175+
176+
private fun parseProperties(content: String): List<Pair<String, String>> {
177+
val logicalLines = mutableListOf<String>()
178+
val current = StringBuilder()
179+
180+
content.lineSequence().forEach { rawLine ->
181+
val line = rawLine.trim()
182+
if (current.isEmpty() && (line.isEmpty() || line.startsWith("#") || line.startsWith("!"))) {
183+
return@forEach
184+
}
185+
186+
val normalized = if (current.isEmpty()) line else line.trimStart()
187+
current.append(
188+
if (endsWithContinuation(rawLine)) normalized.dropLast(1) else normalized,
189+
)
190+
191+
if (!endsWithContinuation(rawLine)) {
192+
logicalLines.add(current.toString())
193+
current.setLength(0)
194+
}
195+
}
196+
197+
if (current.isNotEmpty()) {
198+
logicalLines.add(current.toString())
199+
}
200+
201+
return logicalLines.map { line ->
202+
val separatorIndex = findSeparatorIndex(line)
203+
if (separatorIndex < 0) {
204+
line to ""
205+
} else {
206+
val keyEnd = trimTrailingWhitespace(line, separatorIndex)
207+
val valueStart = findValueStart(line, separatorIndex)
208+
line.substring(0, keyEnd) to line.substring(valueStart).trim()
209+
}
210+
}
211+
}
212+
213+
private fun endsWithContinuation(line: String): Boolean {
214+
var backslashCount = 0
215+
216+
for (index in line.length - 1 downTo 0) {
217+
if (line[index] == '\\') {
218+
backslashCount++
219+
} else {
220+
break
221+
}
222+
}
223+
224+
return backslashCount % 2 == 1
225+
}
226+
227+
private fun findSeparatorIndex(line: String): Int {
228+
var backslashCount = 0
229+
230+
line.forEachIndexed { index, char ->
231+
if (char == '\\') {
232+
backslashCount++
233+
} else {
234+
val isEscaped = backslashCount % 2 == 1
235+
if (!isEscaped && (char == '=' || char == ':' || char.isWhitespace())) {
236+
return index
237+
}
238+
backslashCount = 0
239+
}
240+
}
241+
242+
return -1
243+
}
244+
245+
private fun trimTrailingWhitespace(line: String, endExclusive: Int): Int {
246+
var end = endExclusive
247+
248+
while (end > 0 && line[end - 1].isWhitespace()) {
249+
end--
250+
}
251+
252+
return end
253+
}
254+
255+
private fun findValueStart(line: String, separatorIndex: Int): Int {
256+
var valueStart = separatorIndex
257+
258+
while (valueStart < line.length && line[valueStart].isWhitespace()) {
259+
valueStart++
260+
}
261+
262+
if (valueStart < line.length && (line[valueStart] == '=' || line[valueStart] == ':')) {
263+
valueStart++
264+
}
265+
266+
while (valueStart < line.length && line[valueStart].isWhitespace()) {
267+
valueStart++
268+
}
269+
270+
return valueStart
271+
}
272+
}

gradle/libs.versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,13 @@ detekt = { id = "io.gitlab.arturbosch.detekt", version = "1.23.8" }
6161
jacoco-android = { id = "com.mxalbert.gradle.jacoco-android", version = "0.2.0" }
6262
kover = { id = "org.jetbrains.kotlinx.kover", version = "0.7.3" }
6363
vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version = "0.30.0" }
64-
springboot2 = { id = "org.springframework.boot", version.ref = "springboot2" }
6564
springboot3 = { id = "org.springframework.boot", version.ref = "springboot3" }
6665
springboot4 = { id = "org.springframework.boot", version.ref = "springboot4" }
6766
spring-dependency-management = { id = "io.spring.dependency-management", version = "1.1.7" }
6867
gretty = { id = "org.gretty", version = "4.0.0" }
6968
animalsniffer = { id = "ru.vyarus.animalsniffer", version = "2.0.1" }
7069
sentry = { id = "io.sentry.android.gradle", version = "6.0.0-alpha.6"}
71-
shadow = { id = "com.gradleup.shadow", version = "8.3.6" }
70+
shadow = { id = "com.gradleup.shadow", version = "9.4.1" }
7271

7372
[libraries]
7473
apache-httpclient = { module = "org.apache.httpcomponents.client5:httpclient5", version = "5.0.4" }
@@ -159,6 +158,7 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
159158
slf4j-jdk14 = { module = "org.slf4j:slf4j-jdk14", version.ref = "slf4j" }
160159
slf4j2-api = { module = "org.slf4j:slf4j-api", version = "2.0.5" }
161160
spotlessLib = { module = "com.diffplug.spotless:com.diffplug.spotless.gradle.plugin", version.ref = "spotless"}
161+
springboot2-bom = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "springboot2" }
162162
springboot-starter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "springboot2" }
163163
springboot-starter-graphql = { module = "org.springframework.boot:spring-boot-starter-graphql", version.ref = "springboot2" }
164164
springboot-starter-quartz = { module = "org.springframework.boot:spring-boot-starter-quartz", version.ref = "springboot2" }

sentry-android-core/src/test/java/io/sentry/android/core/anr/AnrProfilingIntegrationTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ class AnrProfilingIntegrationTest {
174174

175175
val integration = AnrProfilingIntegration()
176176
integration.register(mockScopes, androidOptions)
177-
integration.onForeground()
177+
// Drive the state machine synchronously to avoid racing the background polling thread.
178178

179179
SystemClock.setCurrentTimeMillis(1_000)
180180
integration.checkMainThread(mainThread)

sentry-android-integration-tests/sentry-uitest-android-benchmark/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ android {
8181
lint {
8282
warningsAsErrors = true
8383
checkDependencies = true
84+
// Suppress OldTargetApi: lint 8.13.1 expects API 37 but we target 36
85+
disable += "OldTargetApi"
8486

8587
// We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks.
8688
checkReleaseBuilds = false

sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ android {
7474
lint {
7575
warningsAsErrors = true
7676
checkDependencies = true
77+
// Suppress OldTargetApi: lint 8.13.1 expects API 37 but we target 36
78+
disable += "OldTargetApi"
7779

7880
// We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks.
7981
checkReleaseBuilds = false

sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,22 @@ tasks {
150150

151151
archiveClassifier.set("")
152152

153-
duplicatesStrategy = DuplicatesStrategy.FAIL
154-
155-
mergeServiceFiles { include("inst/META-INF/services/*") }
153+
// INCLUDE is required so that mergeServiceFiles can see duplicates from both the
154+
// upstream agent JAR and the isolated distro libs before they are deduplicated.
155+
// Shadow 9.x enforces duplicatesStrategy before transformers run, so FAIL/EXCLUDE
156+
// would prevent service file merging.
157+
duplicatesStrategy = DuplicatesStrategy.INCLUDE
158+
159+
// Shadow 9.x only applies relocations to service files handled by a ServiceFileTransformer.
160+
// We need two mergeServiceFiles calls:
161+
// 1. Default path (META-INF/services) — ensures bootstrap service files get relocated
162+
// (e.g., ContextStorageProvider → shaded path). Without this, Shadow 9.x skips
163+
// relocation for service file names/contents not claimed by a transformer.
164+
// 2. inst/ path — merges isolated agent service files from both the upstream agent
165+
// and the distro libs. Uses `path` instead of `include` filter because Shadow 9.x's
166+
// include() strips the `inst/` prefix on output.
167+
mergeServiceFiles()
168+
mergeServiceFiles { path = "inst/META-INF/services" }
156169
exclude("**/module-info.class")
157170
relocatePackages(this)
158171

sentry-samples/sentry-samples-console-opentelemetry-noagent/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ dependencies {
4848
tasks.shadowJar {
4949
manifest { attributes["Main-Class"] = "io.sentry.samples.console.Main" }
5050
archiveClassifier.set("") // Remove the classifier so it replaces the regular JAR
51+
duplicatesStrategy = DuplicatesStrategy.INCLUDE
5152
mergeServiceFiles()
5253
}
5354

@@ -66,6 +67,10 @@ tasks.register<Test>("systemTest").configure {
6667
group = "verification"
6768
description = "Runs the System tests"
6869

70+
val test = project.extensions.getByType<SourceSetContainer>()["test"]
71+
testClassesDirs = test.output.classesDirs
72+
classpath = test.runtimeClasspath
73+
6974
outputs.upToDateWhen { false }
7075

7176
maxParallelForks = 1

sentry-samples/sentry-samples-console-otlp/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ dependencies {
5151
tasks.shadowJar {
5252
manifest { attributes["Main-Class"] = "io.sentry.samples.console.Main" }
5353
archiveClassifier.set("") // Remove the classifier so it replaces the regular JAR
54+
duplicatesStrategy = DuplicatesStrategy.INCLUDE
5455
mergeServiceFiles()
5556
}
5657

@@ -69,6 +70,10 @@ tasks.register<Test>("systemTest").configure {
6970
group = "verification"
7071
description = "Runs the System tests"
7172

73+
val test = project.extensions.getByType<SourceSetContainer>()["test"]
74+
testClassesDirs = test.output.classesDirs
75+
classpath = test.runtimeClasspath
76+
7277
outputs.upToDateWhen { false }
7378

7479
maxParallelForks = 1

0 commit comments

Comments
 (0)