Skip to content

Commit 322e569

Browse files
authored
Cli version and command configurable (#279)
1 parent 79ad8ea commit 322e569

8 files changed

Lines changed: 324 additions & 37 deletions

File tree

.github/workflows/test-plugin.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
java-version: '17'
3232

3333
- name: Run 'normal' tests
34-
run: ./gradlew test --tests 'SentryProguardGradlePluginTest'
34+
run: ./gradlew test --tests 'SentryProguardGradlePluginTest' --tests '*.tasks.*'
3535

3636
- name: Publish Test Report
3737
uses: mikepenz/action-junit-report@v6

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,15 @@ sentryProguard {
2424
project.set("SENTRY_PROJECT")
2525
authToken.set("SENTRY_AUTH_TOKEN")
2626
noUpload.set(false)
27+
cliConfig {
28+
version.set("2.0.0")
29+
command.set("${SentryCliConfig.PlaceHolder.CLI_FILE_PATH} some-command --org ${SentryCliConfig.PlaceHolder.ORG}")
30+
}
2731
}
2832
```
2933

34+
**noUpload**:
35+
3036
The `sentryProguard.noUpload` function is useful for development purposes.
3137
Normally, you don't want to upload the mapping file to Sentry while creating a minified version on developer machines.
3238
Instead, you just want to upload the mapping file on your CI. In case you do a "real release".
@@ -43,6 +49,14 @@ By default, you don't set the [Gradle property](https://docs.gradle.org/8.0.2/us
4349
In this case the plugin won't upload the mapping files.
4450
On your CI, however, you set the property and therefore the mapping file will be uploaded.
4551

52+
**cliConfig**:
53+
54+
This option is **not required** to be overridden or to be changed.
55+
By default, the plugin will download the version of the Sentry CLI bundled with the plugin and use it to upload the mapping file.
56+
However, if you want to use a custom version of the Sentry CLI and this would require to change the command to upload the mapping file, you can do so by overriding the `cliConfig` configuration.
57+
The `command` property can contain various placeholders like `${SentryCliConfig.PlaceHolder.CLI_FILE_PATH}` or `${SentryCliConfig.PlaceHolder.ORG}`.
58+
Those placeholders will be replaced by the plugin with the actual values before executing the command.
59+
4660
## How it works under the hood
4761

4862
If you run "any" task on a [`minifiedEnabled`](https://developer.android.com/reference/tools/gradle-api/8.0/com/android/build/api/variant/CanMinifyCode) [build type](https://developer.android.com/studio/build/build-variants#build-types), the Plugin will:

src/main/kotlin/com/ioki/sentry/proguard/gradle/plugin/SentryProguardExtension.kt

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
package com.ioki.sentry.proguard.gradle.plugin
22

3+
import com.ioki.sentry.proguard.gradle.plugin.SentryCliConfig.PlaceHolder.AUTH_TOKEN
4+
import com.ioki.sentry.proguard.gradle.plugin.SentryCliConfig.PlaceHolder.CLI_FILE_PATH
5+
import com.ioki.sentry.proguard.gradle.plugin.SentryCliConfig.PlaceHolder.MAPPING_FILE_PATH
6+
import com.ioki.sentry.proguard.gradle.plugin.SentryCliConfig.PlaceHolder.ORG
7+
import com.ioki.sentry.proguard.gradle.plugin.SentryCliConfig.PlaceHolder.PROJECT
8+
import com.ioki.sentry.proguard.gradle.plugin.SentryCliConfig.PlaceHolder.UUID
9+
import org.gradle.api.Action
310
import org.gradle.api.plugins.ExtensionContainer
411
import org.gradle.api.provider.Property
12+
import org.gradle.api.tasks.Nested
513

614
internal fun ExtensionContainer.createSentryProguardExtension(): SentryProguardExtension =
715
create("sentryProguard", SentryProguardExtension::class.java)
@@ -14,4 +22,48 @@ interface SentryProguardExtension {
1422
val authToken: Property<String>
1523

1624
val noUpload: Property<Boolean>
25+
26+
@get:Nested
27+
val cliConfig: SentryCliConfig
28+
29+
fun cliConfig(action: Action<SentryCliConfig>) {
30+
action.execute(cliConfig)
31+
}
32+
}
33+
34+
interface SentryCliConfig {
35+
companion object PlaceHolder {
36+
const val CLI_FILE_PATH = "{cliFilePath}"
37+
const val UUID = "{uuid}"
38+
const val MAPPING_FILE_PATH = "{mappingFilePath}"
39+
const val ORG = "{org}"
40+
const val PROJECT = "{project}"
41+
const val AUTH_TOKEN = "{authToken}"
42+
43+
internal const val DEFAULT_COMMAND =
44+
"$CLI_FILE_PATH upload-proguard --uuid $UUID $MAPPING_FILE_PATH --org $ORG --project $PROJECT --auth-token $AUTH_TOKEN"
45+
}
46+
47+
val version: Property<String>
48+
49+
val command: Property<Command>
50+
}
51+
52+
typealias Command = String
53+
54+
internal fun Command.build(
55+
cliFilePath: String,
56+
uuid: String,
57+
mappingFilePath: String,
58+
org: String,
59+
project: String,
60+
authToken: String
61+
): List<String> {
62+
return this.replace(CLI_FILE_PATH, cliFilePath)
63+
.replace(UUID, uuid)
64+
.replace(MAPPING_FILE_PATH, mappingFilePath)
65+
.replace(ORG, org)
66+
.replace(PROJECT, project)
67+
.replace(AUTH_TOKEN, authToken)
68+
.split("\\s+".toRegex())
1769
}

src/main/kotlin/com/ioki/sentry/proguard/gradle/plugin/SentryProguardGradlePlugin.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@ import com.ioki.sentry.proguard.gradle.plugin.tasks.registerDownloadSentryCliTas
88
import com.ioki.sentry.proguard.gradle.plugin.tasks.registerUploadUuidToSentryTask
99
import org.gradle.api.Plugin
1010
import org.gradle.api.Project
11-
import java.util.*
11+
import java.util.UUID
1212

1313
private const val SENTRY_CLI_FILE_PATH = "sentry/cli"
1414

1515
class SentryProguardGradlePlugin : Plugin<Project> {
1616
override fun apply(project: Project) {
1717
val extension = project.extensions.getByType(AndroidComponentsExtension::class.java)
1818
val sentryProguardExtension = project.extensions.createSentryProguardExtension()
19+
val bundledCliVersion = object {}.javaClass.getResource("/SENTRY_CLI_VERSION").readText()
20+
sentryProguardExtension.cliConfig.version.convention(bundledCliVersion)
21+
sentryProguardExtension.cliConfig.command.convention(SentryCliConfig.DEFAULT_COMMAND)
22+
1923
project.replaceSentryProguardUuidInAndroidManifest(extension, sentryProguardExtension)
2024
}
2125
}
@@ -25,7 +29,8 @@ private fun Project.replaceSentryProguardUuidInAndroidManifest(
2529
sentryProguardExtension: SentryProguardExtension,
2630
) {
2731
val downloadSentryCliTask = tasks.registerDownloadSentryCliTask(
28-
layout.buildDirectory.file(SENTRY_CLI_FILE_PATH),
32+
cliFilePath = layout.buildDirectory.file(SENTRY_CLI_FILE_PATH),
33+
cliVersion = sentryProguardExtension.cliConfig.version,
2934
)
3035

3136
extension.onVariants { variant ->

src/main/kotlin/com/ioki/sentry/proguard/gradle/plugin/tasks/DownloadSentryCliTask.kt

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,33 @@ import org.gradle.api.file.RegularFile
66
import org.gradle.api.file.RegularFileProperty
77
import org.gradle.api.provider.Property
88
import org.gradle.api.provider.Provider
9-
import org.gradle.api.tasks.*
9+
import org.gradle.api.tasks.Input
10+
import org.gradle.api.tasks.OutputFile
11+
import org.gradle.api.tasks.TaskAction
12+
import org.gradle.api.tasks.TaskContainer
13+
import org.gradle.api.tasks.TaskProvider
1014
import org.gradle.process.ExecOperations
1115
import java.net.URL
1216
import java.nio.file.Files
13-
import java.util.*
1417
import javax.inject.Inject
1518
import kotlin.io.path.deleteIfExists
1619

1720
internal fun TaskContainer.registerDownloadSentryCliTask(
18-
cliFilePath: Provider<RegularFile>
21+
cliFilePath: Provider<RegularFile>,
22+
cliVersion: Provider<String>
1923
): TaskProvider<DownloadSentryCliTask> = register("downloadSentryCli", DownloadSentryCliTask::class.java) {
20-
val sentryCliVersion = object {}.javaClass.getResource("/SENTRY_CLI_VERSION").readText()
21-
it.downloadUrl.set(findSentryCliDownloadUrl(sentryCliVersion))
2224
it.cliFilePath.set(cliFilePath)
25+
it.cliVersion.set(cliVersion)
26+
it.osName.set(System.getProperty("os.name").lowercase())
2327
}
2428

2529
internal abstract class DownloadSentryCliTask : DefaultTask() {
2630

2731
@get:Input
28-
abstract val downloadUrl: Property<String>
32+
abstract val cliVersion: Property<String>
33+
34+
@get:Input
35+
abstract val osName: Property<String>
2936

3037
@get:OutputFile
3138
abstract val cliFilePath: RegularFileProperty
@@ -35,7 +42,8 @@ internal abstract class DownloadSentryCliTask : DefaultTask() {
3542

3643
@TaskAction
3744
fun downloadSentryCli() {
38-
URL(downloadUrl.get()).openStream().use {
45+
val cliDownloadUrl = findSentryCliDownloadUrl(cliVersion.get(), osName.get())
46+
URL(cliDownloadUrl).openStream().use {
3947
val cliFile = cliFilePath.asFile.get().toPath()
4048
cliFile.deleteIfExists()
4149
Files.copy(it, cliFile)
@@ -44,24 +52,23 @@ internal abstract class DownloadSentryCliTask : DefaultTask() {
4452
it.commandLine("chmod", "u+x", cliFilePath.asFile.get().absolutePath)
4553
}
4654
}
47-
}
4855

49-
private fun findSentryCliDownloadUrl(version: String): String {
50-
val releaseDownloadsUrl = "https://github.com/getsentry/sentry-cli/releases/download/$version"
51-
val osName = System.getProperty("os.name").lowercase(Locale.ROOT)
52-
return when {
53-
osName.contains("mac") ->
54-
"$releaseDownloadsUrl/sentry-cli-Darwin-universal"
56+
private fun findSentryCliDownloadUrl(version: String, osName: String): String {
57+
val releaseDownloadsUrl = "https://github.com/getsentry/sentry-cli/releases/download/$version"
58+
return when {
59+
osName.contains("mac") ->
60+
"$releaseDownloadsUrl/sentry-cli-Darwin-universal"
5561

56-
osName.contains("nix") || osName.contains("nux") || osName.contains("aix") ->
57-
"$releaseDownloadsUrl/sentry-cli-Linux-x86_64"
62+
osName.contains("nix") || osName.contains("nux") || osName.contains("aix") ->
63+
"$releaseDownloadsUrl/sentry-cli-Linux-x86_64"
5864

59-
osName.contains("windows") && System.getProperty("os.arch") in listOf("x86", "ia32") ->
60-
"$releaseDownloadsUrl/sentry-cli-Windows-i686.exe"
65+
osName.contains("windows") && System.getProperty("os.arch") in listOf("x86", "ia32") ->
66+
"$releaseDownloadsUrl/sentry-cli-Windows-i686.exe"
6167

62-
osName.contains("windows") ->
63-
"$releaseDownloadsUrl/sentry-cli-Windows-x86_64.exe"
68+
osName.contains("windows") ->
69+
"$releaseDownloadsUrl/sentry-cli-Windows-x86_64.exe"
6470

65-
else -> throw GradleException("We do not support $osName")
71+
else -> throw GradleException("We do not support $osName")
72+
}
6673
}
6774
}

src/main/kotlin/com/ioki/sentry/proguard/gradle/plugin/tasks/UploadUuidToSentryTask.kt

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.ioki.sentry.proguard.gradle.plugin.tasks
22

33
import com.ioki.sentry.proguard.gradle.plugin.SentryProguardExtension
4+
import com.ioki.sentry.proguard.gradle.plugin.build
45
import org.gradle.api.DefaultTask
56
import org.gradle.api.file.RegularFile
67
import org.gradle.api.file.RegularFileProperty
@@ -35,6 +36,7 @@ internal fun TaskContainer.registerUploadUuidToSentryTask(
3536
it.cliFilePath.set(downloadSentryCliTask.flatMap { it.cliFilePath })
3637
it.uuid.set(uuid)
3738
it.variantName.set(variantName)
39+
it.cliCommand.set(sentryProguardExtension.cliConfig.command)
3840
}
3941

4042
configureEach { task ->
@@ -63,6 +65,9 @@ internal abstract class UploadUuidToSentryTask : DefaultTask() {
6365
@get:Input
6466
abstract val sentryAuthToken: Property<String>
6567

68+
@get:Input
69+
abstract val cliCommand: Property<String>
70+
6671
@get:InputFile
6772
abstract val cliFilePath: RegularFileProperty
6873

@@ -75,19 +80,13 @@ internal abstract class UploadUuidToSentryTask : DefaultTask() {
7580

7681
@TaskAction
7782
fun uploadUuidToSentry() {
78-
val cliFilePath = cliFilePath.get().asFile.absolutePath
79-
val command = listOf(
80-
cliFilePath,
81-
"upload-proguard",
82-
"--uuid",
83-
uuid.get(),
84-
mappingFilePath.get().asFile.path,
85-
"--org",
86-
sentryOrg.get(),
87-
"--project",
88-
sentryProject.get(),
89-
"--auth-token",
90-
sentryAuthToken.get()
83+
val command = cliCommand.get().build(
84+
cliFilePath = cliFilePath.get().asFile.absolutePath,
85+
uuid = uuid.get(),
86+
mappingFilePath = mappingFilePath.get().asFile.absolutePath,
87+
org = sentryOrg.get(),
88+
project = sentryProject.get(),
89+
authToken = sentryAuthToken.get()
9190
)
9291
logger.log(LogLevel.INFO, "Execute the following command:\n$command")
9392
execOperations.exec {
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package com.ioki.sentry.proguard.gradle.plugin.tasks
2+
3+
import org.gradle.testkit.runner.GradleRunner
4+
import org.gradle.testkit.runner.TaskOutcome
5+
import org.junit.jupiter.api.BeforeEach
6+
import org.junit.jupiter.api.Test
7+
import org.junit.jupiter.api.io.TempDir
8+
import strikt.api.expectThat
9+
import strikt.assertions.contains
10+
import strikt.assertions.isEqualTo
11+
import strikt.assertions.isGreaterThan
12+
import strikt.assertions.isTrue
13+
import java.nio.file.Path
14+
import java.nio.file.Paths
15+
import kotlin.io.path.ExperimentalPathApi
16+
import kotlin.io.path.copyToRecursively
17+
import kotlin.io.path.exists
18+
import kotlin.io.path.fileSize
19+
import kotlin.io.path.readText
20+
import kotlin.io.path.writeText
21+
22+
class DownloadSentryCliTaskTest {
23+
24+
@TempDir
25+
lateinit var testTmpPath: Path
26+
27+
@BeforeEach
28+
@OptIn(ExperimentalPathApi::class)
29+
fun moveTestProjectToTestTmpDir() {
30+
val testProjectPath = Paths.get(System.getProperty("user.dir"), "androidTestProject")
31+
testProjectPath.copyToRecursively(
32+
testTmpPath,
33+
overwrite = true,
34+
followLinks = false
35+
)
36+
}
37+
38+
@Test
39+
fun `downloadSentryCli task downloads sentry cli binary`() {
40+
val result = GradleRunner.create()
41+
.withProjectDir(testTmpPath.toFile())
42+
.withPluginClasspath()
43+
.withArguments("downloadSentryCli")
44+
.build()
45+
46+
expectThat(result.task(":downloadSentryCli")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
47+
48+
val cliPath = testTmpPath.resolve("build/sentry/cli")
49+
expectThat(cliPath.exists()).isTrue()
50+
expectThat(cliPath.fileSize()).isGreaterThan(0)
51+
expectThat(cliPath.toFile().canExecute()).isTrue()
52+
}
53+
54+
@Test
55+
fun `downloaded sentry cli is executable and returns version`() {
56+
GradleRunner.create()
57+
.withProjectDir(testTmpPath.toFile())
58+
.withPluginClasspath()
59+
.withArguments("downloadSentryCli")
60+
.build()
61+
62+
val cliPath = testTmpPath.resolve("build/sentry/cli")
63+
val process = ProcessBuilder(cliPath.toAbsolutePath().toString(), "--version")
64+
.directory(testTmpPath.toFile())
65+
.redirectErrorStream(true)
66+
.start()
67+
68+
val output = process.inputStream.bufferedReader().use { it.readText() }
69+
val exitCode = process.waitFor()
70+
71+
expectThat(exitCode).isEqualTo(0)
72+
val bundledVersion = object {}.javaClass.getResource("/SENTRY_CLI_VERSION").readText()
73+
expectThat(output.lowercase()).contains("sentry-cli $bundledVersion")
74+
}
75+
76+
@Test
77+
fun `custom sentry cli version is downloaded executed and returns version`() {
78+
val buildFile = testTmpPath.resolve("build.gradle.kts")
79+
val newBuildFile = buildFile.readText().replace(
80+
oldValue = """organization.set("sentryOrg")""",
81+
newValue = """organization.set("sentryOrg")
82+
cliConfig {
83+
version.set("2.0.0")
84+
}
85+
""".trimIndent()
86+
)
87+
buildFile.writeText(newBuildFile)
88+
GradleRunner.create()
89+
.withProjectDir(testTmpPath.toFile())
90+
.withPluginClasspath()
91+
.withArguments("downloadSentryCli")
92+
.build()
93+
94+
val cliPath = testTmpPath.resolve("build/sentry/cli")
95+
val process = ProcessBuilder(cliPath.toAbsolutePath().toString(), "--version")
96+
.directory(testTmpPath.toFile())
97+
.redirectErrorStream(true)
98+
.start()
99+
100+
val output = process.inputStream.bufferedReader().use { it.readText() }
101+
val exitCode = process.waitFor()
102+
103+
expectThat(exitCode).isEqualTo(0)
104+
expectThat(output.lowercase()).contains("sentry-cli 2.0.0")
105+
}
106+
}

0 commit comments

Comments
 (0)