Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
- 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"
Comment thread
romtsn marked this conversation as resolved.
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
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ android {
lint {
warningsAsErrors = true
checkDependencies = true
// Suppress OldTargetApi: lint 8.13.1 expects API 37 but we target 36
disable += "OldTargetApi"

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