Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@

## Unreleased

### Internal

- Bump AGP version from v8.6.0 to v8.13.1 ([#5063](https://github.com/getsentry/sentry-java/pull/5063))

### Dependencies

- Bump Native SDK from v0.13.3 to v0.13.6 ([#5277](https://github.com/getsentry/sentry-java/pull/5277))
- [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0136)
- [diff](https://github.com/getsentry/sentry-native/compare/0.13.3...0.13.6)
- Bump Gradle from v8.14.3 to v9.4.1 ([#5063](https://github.com/getsentry/sentry-java/pull/5063))
- [changelog](https://github.com/gradle/gradle/blob/master/CHANGELOG.md#v941)
- [diff](https://github.com/gradle/gradle/compare/v8.14.3...v9.4.1)

## 8.38.0

Expand Down
12 changes: 9 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ apiValidation {
"test-app-sentry",
"test-app-size",
"sentry-samples-netflix-dgs",
"sentry-samples-console-otlp"
"sentry-samples-console-otlp",
"sentry-test-support",
"sentry-system-test-support"
)
)
}
Expand Down Expand Up @@ -249,9 +251,13 @@ tasks.register("buildForCodeQL") {
}
.forEach { proj ->
if (proj.plugins.hasPlugin("com.android.library")) {
this.dependsOn(proj.tasks.findByName("compileReleaseUnitTestSources"))
proj.tasks.findByName("compileReleaseUnitTestSources")?.let { testTask ->
this.dependsOn(testTask)
}
} else {
this.dependsOn(proj.tasks.findByName("testClasses"))
proj.tasks.findByName("testClasses")?.let { testTask ->
this.dependsOn(testTask)
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import java.math.BigDecimal

object Config {
val AGP = System.getenv("VERSION_AGP") ?: "8.6.0"
val AGP = System.getenv("VERSION_AGP") ?: "8.13.1"
val kotlinStdLib = "stdlib-jdk8"
val kotlinStdLibVersionAndroid = "1.9.24"
val kotlinTestJunit = "test-junit"
Expand Down
292 changes: 292 additions & 0 deletions buildSrc/src/main/java/MergeSpringMetadataAction.kt
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
}
}
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled

# AndroidX required by AGP >= 3.6.x
android.useAndroidX=true
android.experimental.lint.version=8.9.0
android.experimental.lint.version=8.13.1

# Release information
versionName=8.38.0
Expand Down
5 changes: 3 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ detekt = { id = "io.gitlab.arturbosch.detekt", version = "1.23.8" }
jacoco-android = { id = "com.mxalbert.gradle.jacoco-android", version = "0.2.0" }
kover = { id = "org.jetbrains.kotlinx.kover", version = "0.7.3" }
vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version = "0.30.0" }
springboot2 = { id = "org.springframework.boot", version.ref = "springboot2" }
springboot3 = { id = "org.springframework.boot", version.ref = "springboot3" }
springboot4 = { id = "org.springframework.boot", version.ref = "springboot4" }
spring-dependency-management = { id = "io.spring.dependency-management", version = "1.0.11.RELEASE" }
spring-dependency-management = { id = "io.spring.dependency-management", version = "1.1.7" }
gretty = { id = "org.gretty", version = "4.0.0" }
animalsniffer = { id = "ru.vyarus.animalsniffer", version = "2.0.1" }
sentry = { id = "io.sentry.android.gradle", version = "6.0.0-alpha.6"}
shadow = { id = "com.gradleup.shadow", version = "9.4.1" }

[libraries]
apache-httpclient = { module = "org.apache.httpcomponents.client5:httpclient5", version = "5.0.4" }
Expand Down Expand Up @@ -158,6 +158,7 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
slf4j-jdk14 = { module = "org.slf4j:slf4j-jdk14", version.ref = "slf4j" }
slf4j2-api = { module = "org.slf4j:slf4j-api", version = "2.0.5" }
spotlessLib = { module = "com.diffplug.spotless:com.diffplug.spotless.gradle.plugin", version.ref = "spotless"}
springboot2-bom = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "springboot2" }
springboot-starter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "springboot2" }
springboot-starter-graphql = { module = "org.springframework.boot:spring-boot-starter-graphql", version.ref = "springboot2" }
springboot-starter-quartz = { module = "org.springframework.boot:spring-boot-starter-quartz", version.ref = "springboot2" }
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ class AnrProfilingIntegrationTest {

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

SystemClock.setCurrentTimeMillis(1_000)
integration.checkMainThread(mainThread)
Expand Down
Loading
Loading