Skip to content

Commit ab18f65

Browse files
authored
Allow kotlin pre-releases in KotlinCompilerPlugin (#972)
* Copied SemVer implementation to SentryKotlinCompilerPlugin * added test
1 parent d7c82cf commit ab18f65

4 files changed

Lines changed: 263 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Fixes
6+
7+
- Allow kotlin pre-releases in KotlinCompilerPlugin ([#972](https://github.com/getsentry/sentry-android-gradle-plugin/pull/972))
8+
39
## 6.0.0-alpha.1
410

511
### Features
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package io.sentry.android.gradle.integration
2+
3+
import io.sentry.BuildConfig
4+
import io.sentry.android.gradle.util.SemVer
5+
import io.sentry.android.gradle.withDummyComposeFile
6+
import kotlin.test.assertTrue
7+
import org.gradle.util.GradleVersion
8+
import org.junit.Assume.assumeFalse
9+
import org.junit.Test
10+
11+
class SentryPluginKotlinCompilerPrereleaseTest :
12+
BaseSentryPluginTest(BuildConfig.AgpVersion, GradleVersion.current().version) {
13+
14+
override val additionalBuildClasspath: String =
15+
"""
16+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20-RC"
17+
"""
18+
.trimIndent()
19+
20+
@Test
21+
fun `does not break for kotlin prereleases`() {
22+
assumeFalse("Not supported on AGP 9.+ yet", SemVer.parse(BuildConfig.AgpVersion).major >= 9)
23+
appBuildFile.writeText(
24+
// language=Groovy
25+
"""
26+
plugins {
27+
id "com.android.application"
28+
id "kotlin-android"
29+
id "io.sentry.android.gradle"
30+
id "io.sentry.kotlin.compiler.gradle"
31+
}
32+
33+
android {
34+
namespace 'com.example'
35+
buildTypes {
36+
release {
37+
minifyEnabled = true
38+
}
39+
}
40+
buildFeatures {
41+
compose true
42+
}
43+
composeOptions {
44+
kotlinCompilerExtensionVersion = "1.4.6"
45+
}
46+
kotlinOptions {
47+
jvmTarget = "1.8"
48+
49+
freeCompilerArgs += [
50+
"-P",
51+
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=1.8.20-RC"
52+
]
53+
}
54+
}
55+
dependencies {
56+
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.8.20-RC"
57+
implementation "io.sentry:sentry-compose-android:${BuildConfig.SdkVersion}"
58+
59+
implementation 'androidx.compose.ui:ui:1.4.0'
60+
implementation 'androidx.compose.ui:ui-tooling:1.4.0'
61+
implementation 'androidx.compose.foundation:foundation:1.4.0'
62+
implementation 'androidx.activity:activity-compose:1.7.0'
63+
}
64+
65+
sentry {
66+
autoUploadProguardMapping = false
67+
}
68+
"""
69+
.trimIndent()
70+
)
71+
72+
testProjectDir.withDummyComposeFile()
73+
74+
val result = runner.appendArguments("app:assembleRelease").build()
75+
76+
assertTrue(result.output) { "BUILD SUCCESSFUL" in result.output }
77+
}
78+
}

sentry-kotlin-compiler-plugin/src/main/kotlin/io/sentry/SentryKotlinCompilerPlugin.kt

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ class SentryKotlinCompilerPlugin : CompilerPluginRegistrar() {
2626
val versionString = KotlinCompilerVersion.getVersion()
2727
val version =
2828
if (versionString != null) {
29-
SimpleSemanticVersion.from(versionString)
29+
SimpleSemVer.parse(versionString)
3030
} else {
31-
SimpleSemanticVersion(2, 1, 20)
31+
SimpleSemVer(2, 1, 20)
3232
}
3333

3434
val extension: IrGenerationExtension =
35-
if (version >= SimpleSemanticVersion(2, 2, 0)) {
35+
if (version >= SimpleSemVer(2, 2, 0)) {
3636
JetpackComposeTracingIrExtension22(messageCollector)
37-
} else if (version >= SimpleSemanticVersion(2, 1, 20)) {
37+
} else if (version >= SimpleSemVer(2, 1, 20)) {
3838
// 2.1.20 removed some optional parameters, causing API incompatibility
3939
// e.g. java.lang.NoSuchMethodError
4040
// see https://github.com/JetBrains/kotlin/commit/dd508452c414a0ee8082aa6f76d664271cb38f2f
@@ -45,23 +45,4 @@ class SentryKotlinCompilerPlugin : CompilerPluginRegistrar() {
4545

4646
IrGenerationExtension.registerExtension(extension)
4747
}
48-
49-
data class SimpleSemanticVersion(val major: Int, val minor: Int, val patch: Int) :
50-
Comparable<SimpleSemanticVersion> {
51-
52-
companion object {
53-
fun from(version: String): SimpleSemanticVersion {
54-
val parts = version.trim().split(".")
55-
require(parts.size == 3) { "Invalid semantic version: $version" }
56-
57-
val (major, minor, patch) =
58-
parts.map { it.toIntOrNull() ?: error("Invalid number in version: $version") }
59-
return SimpleSemanticVersion(major, minor, patch)
60-
}
61-
}
62-
63-
override fun compareTo(other: SimpleSemanticVersion): Int {
64-
return compareValuesBy(this, other, { it.major }, { it.minor }, { it.patch })
65-
}
66-
}
6748
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
* Adapted from https://github.com/swiftzer/semver/blob/master/src/main/java/net/swiftzer/semver/SemVer.kt
3+
*
4+
* MIT License
5+
*
6+
* Copyright (c) 2017 Eric Li
7+
*
8+
* Permission is hereby granted, free of charge, to any person obtaining a copy
9+
* of this software and associated documentation files (the "Software"), to deal
10+
* in the Software without restriction, including without limitation the rights
11+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
* copies of the Software, and to permit persons to whom the Software is
13+
* furnished to do so, subject to the following conditions:
14+
*
15+
* The above copyright notice and this permission notice shall be included in all
16+
* copies or substantial portions of the Software.
17+
*
18+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
* SOFTWARE.
25+
*/
26+
27+
package io.sentry
28+
29+
import java.io.Serializable
30+
import kotlin.math.min
31+
32+
data class SimpleSemVer(
33+
val major: Int = 0,
34+
val minor: Int = 0,
35+
val patch: Int = 0,
36+
val preRelease: String? = null,
37+
val buildMetadata: String? = null,
38+
) : Comparable<SimpleSemVer>, Serializable {
39+
40+
companion object {
41+
val pattern =
42+
Regex(
43+
"""(0|[1-9]\d*)?(?:\.)?(0|[1-9]\d*)?(?:\.)?(0|[1-9]\d*)?(?:-([\dA-z\-]+(?:\.[\dA-z\-]+)*))?(?:\+([\dA-z\-]+(?:\.[\dA-z\-]+)*))?"""
44+
)
45+
46+
/**
47+
* Parse the version string to [SimpleSemVer] data object.
48+
*
49+
* @param version version string.
50+
* @throws IllegalArgumentException if the version is not valid.
51+
*/
52+
@JvmStatic
53+
fun parse(version: String): SimpleSemVer {
54+
val result =
55+
pattern.matchEntire(version)
56+
?: throw IllegalArgumentException("Invalid version string [$version]")
57+
return SimpleSemVer(
58+
major = if (result.groupValues[1].isEmpty()) 0 else result.groupValues[1].toInt(),
59+
minor = if (result.groupValues[2].isEmpty()) 0 else result.groupValues[2].toInt(),
60+
patch = if (result.groupValues[3].isEmpty()) 0 else result.groupValues[3].toInt(),
61+
preRelease = if (result.groupValues[4].isEmpty()) null else result.groupValues[4],
62+
buildMetadata = if (result.groupValues[5].isEmpty()) null else result.groupValues[5],
63+
)
64+
}
65+
}
66+
67+
init {
68+
require(major >= 0) { "Major version must be a positive number" }
69+
require(minor >= 0) { "Minor version must be a positive number" }
70+
require(patch >= 0) { "Patch version must be a positive number" }
71+
if (preRelease != null) {
72+
require(preRelease.matches(Regex("""[\dA-z\-]+(?:\.[\dA-z\-]+)*"""))) {
73+
"Pre-release version is not valid"
74+
}
75+
}
76+
if (buildMetadata != null) {
77+
require(buildMetadata.matches(Regex("""[\dA-z\-]+(?:\.[\dA-z\-]+)*"""))) {
78+
"Build metadata is not valid"
79+
}
80+
}
81+
}
82+
83+
/**
84+
* Build the version name string.
85+
*
86+
* @return version name string in Semantic Versioning 2.0.0 specification.
87+
*/
88+
override fun toString(): String = buildString {
89+
append("$major.$minor.$patch")
90+
if (preRelease != null) {
91+
append('-')
92+
append(preRelease)
93+
}
94+
if (buildMetadata != null) {
95+
append('+')
96+
append(buildMetadata)
97+
}
98+
}
99+
100+
/**
101+
* Compare two SemVer objects using major, minor, patch and pre-release version as specified in
102+
* SemVer specification.
103+
*
104+
* For comparing the whole SemVer object including build metadata, use [equals] instead.
105+
*
106+
* @return a negative integer, zero, or a positive integer as this object is less than, equal to,
107+
* or greater than the specified object.
108+
*/
109+
override fun compareTo(other: SimpleSemVer): Int {
110+
if (major > other.major) return 1
111+
if (major < other.major) return -1
112+
if (minor > other.minor) return 1
113+
if (minor < other.minor) return -1
114+
if (patch > other.patch) return 1
115+
if (patch < other.patch) return -1
116+
117+
if (preRelease == null && other.preRelease == null) return 0
118+
if (preRelease != null && other.preRelease == null) return -1
119+
if (preRelease == null && other.preRelease != null) return 1
120+
121+
val parts = preRelease.orEmpty().split(".")
122+
val otherParts = other.preRelease.orEmpty().split(".")
123+
124+
val endIndex = min(parts.size, otherParts.size) - 1
125+
for (i in 0..endIndex) {
126+
val part = parts[i]
127+
val otherPart = otherParts[i]
128+
if (part == otherPart) continue
129+
130+
val partIsNumeric = part.isNumeric()
131+
val otherPartIsNumeric = otherPart.isNumeric()
132+
133+
when {
134+
partIsNumeric && !otherPartIsNumeric -> {
135+
// lower priority
136+
return -1
137+
}
138+
139+
!partIsNumeric && otherPartIsNumeric -> {
140+
// higher priority
141+
return 1
142+
}
143+
144+
!partIsNumeric && !otherPartIsNumeric -> {
145+
if (part > otherPart) return 1
146+
if (part < otherPart) return -1
147+
}
148+
149+
else -> {
150+
try {
151+
val partInt = part.toInt()
152+
val otherPartInt = otherPart.toInt()
153+
if (partInt > otherPartInt) return 1
154+
if (partInt < otherPartInt) return -1
155+
} catch (_: NumberFormatException) {
156+
// When part or otherPart doesn't fit in an Int, compare as strings
157+
return part.compareTo(otherPart)
158+
}
159+
}
160+
}
161+
}
162+
163+
return if (parts.size == endIndex + 1 && otherParts.size > endIndex + 1) {
164+
// parts is ended and otherParts is not ended
165+
-1
166+
} else if (parts.size > endIndex + 1 && otherParts.size == endIndex + 1) {
167+
// parts is not ended and otherParts is ended
168+
1
169+
} else {
170+
0
171+
}
172+
}
173+
174+
private fun String.isNumeric(): Boolean = this.matches(Regex("""\d+"""))
175+
}

0 commit comments

Comments
 (0)