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
2 changes: 1 addition & 1 deletion .github/workflows/test-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
java-version: '17'

- name: Run 'normal' tests
run: ./gradlew test --tests 'SentryProguardGradlePluginTest'
run: ./gradlew test --tests 'SentryProguardGradlePluginTest' --tests '*.tasks.*'

- name: Publish Test Report
uses: mikepenz/action-junit-report@v6
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@ sentryProguard {
project.set("SENTRY_PROJECT")
authToken.set("SENTRY_AUTH_TOKEN")
noUpload.set(false)
cliConfig {
version.set("2.0.0")
command.set("${SentryCliConfig.PlaceHolder.CLI_FILE_PATH} some-command --org ${SentryCliConfig.PlaceHolder.ORG}")
}
}
```

**noUpload**:

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

**cliConfig**:

This option is **not required** to be overridden or to be changed.
By default, the plugin will download the version of the Sentry CLI bundled with the plugin and use it to upload the mapping file.
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.
The `command` property can contain various placeholders like `${SentryCliConfig.PlaceHolder.CLI_FILE_PATH}` or `${SentryCliConfig.PlaceHolder.ORG}`.
Those placeholders will be replaced by the plugin with the actual values before executing the command.

## How it works under the hood

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:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package com.ioki.sentry.proguard.gradle.plugin

import com.ioki.sentry.proguard.gradle.plugin.SentryCliConfig.PlaceHolder.AUTH_TOKEN
import com.ioki.sentry.proguard.gradle.plugin.SentryCliConfig.PlaceHolder.CLI_FILE_PATH
import com.ioki.sentry.proguard.gradle.plugin.SentryCliConfig.PlaceHolder.MAPPING_FILE_PATH
import com.ioki.sentry.proguard.gradle.plugin.SentryCliConfig.PlaceHolder.ORG
import com.ioki.sentry.proguard.gradle.plugin.SentryCliConfig.PlaceHolder.PROJECT
import com.ioki.sentry.proguard.gradle.plugin.SentryCliConfig.PlaceHolder.UUID
import org.gradle.api.Action
import org.gradle.api.plugins.ExtensionContainer
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Nested

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

val noUpload: Property<Boolean>

@get:Nested
val cliConfig: SentryCliConfig

fun cliConfig(action: Action<SentryCliConfig>) {
action.execute(cliConfig)
}
}

interface SentryCliConfig {
companion object PlaceHolder {
const val CLI_FILE_PATH = "{cliFilePath}"
const val UUID = "{uuid}"
const val MAPPING_FILE_PATH = "{mappingFilePath}"
const val ORG = "{org}"
const val PROJECT = "{project}"
const val AUTH_TOKEN = "{authToken}"

internal const val DEFAULT_COMMAND =
"$CLI_FILE_PATH upload-proguard --uuid $UUID $MAPPING_FILE_PATH --org $ORG --project $PROJECT --auth-token $AUTH_TOKEN"
}

val version: Property<String>

val command: Property<Command>
}

typealias Command = String

internal fun Command.build(
cliFilePath: String,
uuid: String,
mappingFilePath: String,
org: String,
project: String,
authToken: String
): List<String> {
return this.replace(CLI_FILE_PATH, cliFilePath)
.replace(UUID, uuid)
.replace(MAPPING_FILE_PATH, mappingFilePath)
.replace(ORG, org)
.replace(PROJECT, project)
.replace(AUTH_TOKEN, authToken)
.split("\\s+".toRegex())
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ import com.ioki.sentry.proguard.gradle.plugin.tasks.registerDownloadSentryCliTas
import com.ioki.sentry.proguard.gradle.plugin.tasks.registerUploadUuidToSentryTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import java.util.*
import java.util.UUID

private const val SENTRY_CLI_FILE_PATH = "sentry/cli"

class SentryProguardGradlePlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.getByType(AndroidComponentsExtension::class.java)
val sentryProguardExtension = project.extensions.createSentryProguardExtension()
val bundledCliVersion = object {}.javaClass.getResource("/SENTRY_CLI_VERSION").readText()
sentryProguardExtension.cliConfig.version.convention(bundledCliVersion)
sentryProguardExtension.cliConfig.command.convention(SentryCliConfig.DEFAULT_COMMAND)

project.replaceSentryProguardUuidInAndroidManifest(extension, sentryProguardExtension)
}
}
Expand All @@ -25,7 +29,8 @@ private fun Project.replaceSentryProguardUuidInAndroidManifest(
sentryProguardExtension: SentryProguardExtension,
) {
val downloadSentryCliTask = tasks.registerDownloadSentryCliTask(
layout.buildDirectory.file(SENTRY_CLI_FILE_PATH),
cliFilePath = layout.buildDirectory.file(SENTRY_CLI_FILE_PATH),
cliVersion = sentryProguardExtension.cliConfig.version,
)

extension.onVariants { variant ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,33 @@ import org.gradle.api.file.RegularFile
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
import org.gradle.process.ExecOperations
import java.net.URL
import java.nio.file.Files
import java.util.*
import javax.inject.Inject
import kotlin.io.path.deleteIfExists

internal fun TaskContainer.registerDownloadSentryCliTask(
cliFilePath: Provider<RegularFile>
cliFilePath: Provider<RegularFile>,
cliVersion: Provider<String>
): TaskProvider<DownloadSentryCliTask> = register("downloadSentryCli", DownloadSentryCliTask::class.java) {
val sentryCliVersion = object {}.javaClass.getResource("/SENTRY_CLI_VERSION").readText()
it.downloadUrl.set(findSentryCliDownloadUrl(sentryCliVersion))
it.cliFilePath.set(cliFilePath)
it.cliVersion.set(cliVersion)
it.osName.set(System.getProperty("os.name").lowercase())
}

internal abstract class DownloadSentryCliTask : DefaultTask() {

@get:Input
abstract val downloadUrl: Property<String>
abstract val cliVersion: Property<String>

@get:Input
abstract val osName: Property<String>

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

@TaskAction
fun downloadSentryCli() {
URL(downloadUrl.get()).openStream().use {
val cliDownloadUrl = findSentryCliDownloadUrl(cliVersion.get(), osName.get())
URL(cliDownloadUrl).openStream().use {
val cliFile = cliFilePath.asFile.get().toPath()
cliFile.deleteIfExists()
Files.copy(it, cliFile)
Expand All @@ -44,24 +52,23 @@ internal abstract class DownloadSentryCliTask : DefaultTask() {
it.commandLine("chmod", "u+x", cliFilePath.asFile.get().absolutePath)
}
}
}

private fun findSentryCliDownloadUrl(version: String): String {
val releaseDownloadsUrl = "https://github.com/getsentry/sentry-cli/releases/download/$version"
val osName = System.getProperty("os.name").lowercase(Locale.ROOT)
return when {
osName.contains("mac") ->
"$releaseDownloadsUrl/sentry-cli-Darwin-universal"
private fun findSentryCliDownloadUrl(version: String, osName: String): String {
val releaseDownloadsUrl = "https://github.com/getsentry/sentry-cli/releases/download/$version"
return when {
osName.contains("mac") ->
"$releaseDownloadsUrl/sentry-cli-Darwin-universal"

osName.contains("nix") || osName.contains("nux") || osName.contains("aix") ->
"$releaseDownloadsUrl/sentry-cli-Linux-x86_64"
osName.contains("nix") || osName.contains("nux") || osName.contains("aix") ->
"$releaseDownloadsUrl/sentry-cli-Linux-x86_64"

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

osName.contains("windows") ->
"$releaseDownloadsUrl/sentry-cli-Windows-x86_64.exe"
osName.contains("windows") ->
"$releaseDownloadsUrl/sentry-cli-Windows-x86_64.exe"

else -> throw GradleException("We do not support $osName")
else -> throw GradleException("We do not support $osName")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ioki.sentry.proguard.gradle.plugin.tasks

import com.ioki.sentry.proguard.gradle.plugin.SentryProguardExtension
import com.ioki.sentry.proguard.gradle.plugin.build
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFile
import org.gradle.api.file.RegularFileProperty
Expand Down Expand Up @@ -35,6 +36,7 @@ internal fun TaskContainer.registerUploadUuidToSentryTask(
it.cliFilePath.set(downloadSentryCliTask.flatMap { it.cliFilePath })
it.uuid.set(uuid)
it.variantName.set(variantName)
it.cliCommand.set(sentryProguardExtension.cliConfig.command)
}

configureEach { task ->
Expand Down Expand Up @@ -63,6 +65,9 @@ internal abstract class UploadUuidToSentryTask : DefaultTask() {
@get:Input
abstract val sentryAuthToken: Property<String>

@get:Input
abstract val cliCommand: Property<String>

@get:InputFile
abstract val cliFilePath: RegularFileProperty

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

@TaskAction
fun uploadUuidToSentry() {
val cliFilePath = cliFilePath.get().asFile.absolutePath
val command = listOf(
cliFilePath,
"upload-proguard",
"--uuid",
uuid.get(),
mappingFilePath.get().asFile.path,
"--org",
sentryOrg.get(),
"--project",
sentryProject.get(),
"--auth-token",
sentryAuthToken.get()
val command = cliCommand.get().build(
cliFilePath = cliFilePath.get().asFile.absolutePath,
uuid = uuid.get(),
mappingFilePath = mappingFilePath.get().asFile.absolutePath,
org = sentryOrg.get(),
project = sentryProject.get(),
authToken = sentryAuthToken.get()
)
logger.log(LogLevel.INFO, "Execute the following command:\n$command")
execOperations.exec {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.ioki.sentry.proguard.gradle.plugin.tasks

import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import strikt.api.expectThat
import strikt.assertions.contains
import strikt.assertions.isEqualTo
import strikt.assertions.isGreaterThan
import strikt.assertions.isTrue
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.copyToRecursively
import kotlin.io.path.exists
import kotlin.io.path.fileSize
import kotlin.io.path.readText
import kotlin.io.path.writeText

class DownloadSentryCliTaskTest {

@TempDir
lateinit var testTmpPath: Path

@BeforeEach
@OptIn(ExperimentalPathApi::class)
fun moveTestProjectToTestTmpDir() {
val testProjectPath = Paths.get(System.getProperty("user.dir"), "androidTestProject")
testProjectPath.copyToRecursively(
testTmpPath,
overwrite = true,
followLinks = false
)
}

@Test
fun `downloadSentryCli task downloads sentry cli binary`() {
val result = GradleRunner.create()
.withProjectDir(testTmpPath.toFile())
.withPluginClasspath()
.withArguments("downloadSentryCli")
.build()

expectThat(result.task(":downloadSentryCli")?.outcome).isEqualTo(TaskOutcome.SUCCESS)

val cliPath = testTmpPath.resolve("build/sentry/cli")
expectThat(cliPath.exists()).isTrue()
expectThat(cliPath.fileSize()).isGreaterThan(0)
expectThat(cliPath.toFile().canExecute()).isTrue()
}

@Test
fun `downloaded sentry cli is executable and returns version`() {
GradleRunner.create()
.withProjectDir(testTmpPath.toFile())
.withPluginClasspath()
.withArguments("downloadSentryCli")
.build()

val cliPath = testTmpPath.resolve("build/sentry/cli")
val process = ProcessBuilder(cliPath.toAbsolutePath().toString(), "--version")
.directory(testTmpPath.toFile())
.redirectErrorStream(true)
.start()

val output = process.inputStream.bufferedReader().use { it.readText() }
val exitCode = process.waitFor()

expectThat(exitCode).isEqualTo(0)
val bundledVersion = object {}.javaClass.getResource("/SENTRY_CLI_VERSION").readText()
expectThat(output.lowercase()).contains("sentry-cli $bundledVersion")
}

@Test
fun `custom sentry cli version is downloaded executed and returns version`() {
val buildFile = testTmpPath.resolve("build.gradle.kts")
val newBuildFile = buildFile.readText().replace(
oldValue = """organization.set("sentryOrg")""",
newValue = """organization.set("sentryOrg")
cliConfig {
version.set("2.0.0")
}
""".trimIndent()
)
buildFile.writeText(newBuildFile)
GradleRunner.create()
.withProjectDir(testTmpPath.toFile())
.withPluginClasspath()
.withArguments("downloadSentryCli")
.build()

val cliPath = testTmpPath.resolve("build/sentry/cli")
val process = ProcessBuilder(cliPath.toAbsolutePath().toString(), "--version")
.directory(testTmpPath.toFile())
.redirectErrorStream(true)
.start()

val output = process.inputStream.bufferedReader().use { it.readText() }
val exitCode = process.waitFor()

expectThat(exitCode).isEqualTo(0)
expectThat(output.lowercase()).contains("sentry-cli 2.0.0")
}
}
Loading