Skip to content

Commit 730c667

Browse files
authored
Add Gradle task to upload symbols and mapping (#457)
* Basic gradle task to call bd CLI * Add crash button to gradle example app * Separate upload tasks
1 parent 0ad3b1f commit 730c667

File tree

10 files changed

+250
-3
lines changed

10 files changed

+250
-3
lines changed

gradle/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
.externalNativeBuild
1414
.cxx
1515
local.properties
16+
.kotlin

gradle/app/build.gradle

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ android {
2020
buildTypes {
2121
debug {
2222
minifyEnabled true
23-
debuggable false
23+
debuggable true // Note: Most logs will be stripped when this is false!
2424
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
2525
}
2626
release {
@@ -29,6 +29,11 @@ android {
2929
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
3030
}
3131
}
32+
externalNativeBuild {
33+
cmake {
34+
path = file("src/main/cpp/CMakeLists.txt")
35+
}
36+
}
3237
compileOptions {
3338
sourceCompatibility JavaVersion.VERSION_1_8
3439
targetCompatibility JavaVersion.VERSION_1_8
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
cmake_minimum_required(VERSION 3.6.0)
2+
project(native-lib)
3+
4+
add_library(
5+
native-lib
6+
SHARED
7+
native.cpp)
8+
9+
find_library(
10+
log-lib
11+
log)
12+
13+
target_link_libraries(
14+
native-lib
15+
${log-lib})

gradle/app/src/main/cpp/native.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#include <jni.h>
2+
#include <string.h>
3+
4+
// Leave this public so that the compiler can't elide the null ptr access.
5+
char *invalid_ptr = nullptr;
6+
7+
static void trigger_sefgault() {
8+
*invalid_ptr = 0;
9+
}
10+
11+
extern "C"
12+
JNIEXPORT jstring JNICALL
13+
Java_io_bitdrift_gradleexample_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
14+
return env->NewStringUTF("This is a string coming via JNI");
15+
}
16+
17+
extern "C"
18+
JNIEXPORT void JNICALL
19+
Java_io_bitdrift_gradleexample_FirstFragment_triggerSegfault(JNIEnv *env, jobject thiz) {
20+
trigger_sefgault();
21+
}

gradle/app/src/main/java/io/bitdrift/gradleexample/FirstFragment.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import io.bitdrift.gradleexample.databinding.FragmentFirstBinding
2424
*/
2525
class FirstFragment : Fragment() {
2626

27+
private external fun triggerSegfault()
2728
private var _binding: FragmentFirstBinding? = null
2829

2930
// This property is only valid between onCreateView and
@@ -51,6 +52,9 @@ class FirstFragment : Fragment() {
5152
clipboardManager.setPrimaryClip(data)
5253
}
5354

55+
binding.buttonCrash.setOnClickListener {
56+
triggerSegfault()
57+
}
5458
binding.buttonFirst.setOnClickListener {
5559
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
5660
}

gradle/app/src/main/java/io/bitdrift/gradleexample/MainActivity.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import timber.log.Timber
2626

2727
class MainActivity : AppCompatActivity() {
2828

29+
init { System.loadLibrary("native-lib"); }
30+
private external fun stringFromJNI(): String?
31+
2932
private lateinit var appBarConfiguration: AppBarConfiguration
3033
private lateinit var binding: ActivityMainBinding
3134

@@ -43,6 +46,8 @@ class MainActivity : AppCompatActivity() {
4346

4447
Log.i("MainActivity", "Bitdrift Logger configured with url: ${Logger.sessionUrl}")
4548
Timber.i("Bitdrift Logger configured with url: %s", Logger.sessionUrl)
49+
Log.i("MainActivity", "Calling JNI method: ${stringFromJNI()}")
50+
Timber.i("Calling JNI method: ${stringFromJNI()}")
4651

4752
super.onCreate(savedInstanceState)
4853

gradle/app/src/main/res/layout/fragment_first.xml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@
1616
app:layout_constraintStart_toStartOf="parent"
1717
app:layout_constraintTop_toTopOf="parent" />
1818

19+
<Button
20+
android:id="@+id/button_crash"
21+
android:layout_width="wrap_content"
22+
android:layout_height="wrap_content"
23+
android:text="@string/crash"
24+
app:layout_constraintBottom_toBottomOf="parent"
25+
app:layout_constraintEnd_toEndOf="parent"
26+
app:layout_constraintStart_toStartOf="parent"
27+
app:layout_constraintTop_toBottomOf="@id/textview_first" />
28+
1929
<Button
2030
android:id="@+id/button_first"
2131
android:layout_width="wrap_content"
@@ -24,5 +34,5 @@
2434
app:layout_constraintBottom_toBottomOf="parent"
2535
app:layout_constraintEnd_toEndOf="parent"
2636
app:layout_constraintStart_toStartOf="parent"
27-
app:layout_constraintTop_toBottomOf="@id/textview_first" />
37+
app:layout_constraintTop_toBottomOf="@id/button_crash" />
2838
</androidx.constraintlayout.widget.ConstraintLayout>

gradle/app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99

1010
<string name="hello_first_fragment">Hello first fragment</string>
1111
<string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string>
12+
<string name="crash">Crash</string>
1213
</resources>

platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/CapturePlugin.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ package io.bitdrift.capture
99

1010
import com.android.build.api.variant.AndroidComponentsExtension
1111
import io.bitdrift.capture.extension.BitdriftPluginExtension
12+
import io.bitdrift.capture.task.CLIUploadMappingTask
13+
import io.bitdrift.capture.task.CLIUploadSymbolsTask
1214
import org.gradle.api.Plugin
1315
import org.gradle.api.Project
1416
import org.slf4j.LoggerFactory
@@ -28,6 +30,22 @@ abstract class CapturePlugin @Inject constructor() : Plugin<Project> {
2830
extension,
2931
)
3032
}
33+
34+
target.tasks.register("bdUploadMapping", CLIUploadMappingTask::class.java) { task ->
35+
task.description = "Upload mapping to Bitdrift"
36+
task.group = "Upload"
37+
}
38+
39+
target.tasks.register("bdUploadSymbols", CLIUploadSymbolsTask::class.java) { task ->
40+
task.description = "Upload symbols to Bitdrift"
41+
task.group = "Upload"
42+
}
43+
44+
target.tasks.register("bdUpload") { task ->
45+
task.description = "Upload all symbol and mapping files to Bitdrift"
46+
task.group = "Upload"
47+
task.dependsOn("bdUploadMapping", "bdUploadSymbols")
48+
}
3149
}
3250

3351
companion object {
@@ -37,4 +55,4 @@ abstract class CapturePlugin @Inject constructor() : Plugin<Project> {
3755
LoggerFactory.getLogger(CapturePlugin::class.java)
3856
}
3957
}
40-
}
58+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// capture-sdk - bitdrift's client SDK
2+
// Copyright Bitdrift, Inc. All rights reserved.
3+
//
4+
// Use of this source code is governed by a source available license that can be found in the
5+
// LICENSE file or at:
6+
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt
7+
8+
package io.bitdrift.capture.task
9+
10+
import org.gradle.api.DefaultTask
11+
import org.gradle.api.tasks.Internal
12+
import org.gradle.api.tasks.TaskAction
13+
import org.w3c.dom.Document
14+
import java.io.File
15+
import java.io.IOException
16+
import java.net.URI
17+
import javax.xml.parsers.DocumentBuilderFactory
18+
import org.gradle.api.file.Directory
19+
20+
abstract class CLIUploadMappingTask : CLITask() {
21+
@TaskAction
22+
fun action() {
23+
// e.g. build/intermediates/packaged_manifests/release/processReleaseManifestForPackage/AndroidManifest.xml
24+
val androidManifestXmlFile = buildDir.asFile.mostRecentSubfileNamed("AndroidManifest.xml")
25+
// e.g. build/outputs/mapping/release/mapping.txt
26+
val mappingTxtFile = buildDir.asFile.mostRecentSubfileNamed("mapping.txt")
27+
28+
val manifest = androidManifestXmlFile.asXmlDocument().documentElement
29+
val appId = manifest.getAttribute("package")
30+
val versionCode = manifest.getAttribute("android:versionCode")
31+
val versionName = manifest.getAttribute("android:versionName")
32+
33+
runBDCLI(listOf(
34+
"debug-files", "upload-proguard",
35+
"--app-id", appId,
36+
"--app-version", versionName,
37+
"--version-code", versionCode,
38+
mappingTxtFile.absolutePath))
39+
}
40+
}
41+
42+
abstract class CLIUploadSymbolsTask : CLITask() {
43+
@TaskAction
44+
fun action() {
45+
// e.g. build/intermediates/merged_native_libs/release/mergeReleaseNativeLibs
46+
val nativeLibsDir = buildDir.asFile.mostRecentSubfileMatching(".*merge.*NativeLibs".toRegex())
47+
runBDCLI(listOf("debug-files", "upload", nativeLibsDir.absolutePath))
48+
}
49+
}
50+
51+
abstract class CLITask : DefaultTask() {
52+
@Internal
53+
val buildDir: Directory = project.layout.buildDirectory.get()
54+
@Internal
55+
val bdcliFile: File = buildDir.dir("bin").file("bd").asFile
56+
@Internal
57+
val downloader = BDCLIDownloader(bdcliFile)
58+
59+
fun runBDCLI(args: List<String>) {
60+
checkEnvironment()
61+
downloader.downloadIfNeeded()
62+
runCommand(listOf(bdcliFile.absolutePath) + args)
63+
}
64+
65+
fun runCommand(command: List<String>) {
66+
val process = ProcessBuilder(command)
67+
.redirectErrorStream(true)
68+
.start()
69+
process.inputStream.transferTo(System.out)
70+
if (process.waitFor() != 0) {
71+
throw RuntimeException("Command $command failed")
72+
}
73+
}
74+
75+
private fun checkEnvironment() {
76+
val apiKeyEnvName = "API_KEY"
77+
if(System.getenv(apiKeyEnvName) == null) {
78+
throw IllegalStateException("Environment variable $apiKeyEnvName must be set to your Bitdrift API key before running this task")
79+
}
80+
}
81+
}
82+
83+
class BDCLIDownloader(val executableFilePath: File) {
84+
val bdcliVersion = "0.1.33-rc.1"
85+
val bdcliDownloadLoc: URI = URI.create("https://dl.bitdrift.io/bd-cli/${bdcliVersion}/${downloadFilename()}/bd")
86+
87+
private enum class OSType {
88+
MacIntel,
89+
MacArm,
90+
LinuxIntel,
91+
}
92+
93+
private fun osType(): OSType {
94+
val osName = System.getProperty("os.name")
95+
val arch = System.getProperty("os.arch")
96+
return when(osName) {
97+
"Mac OS X" -> when(arch) {
98+
"aarch64" -> OSType.MacArm
99+
else -> OSType.MacIntel
100+
}
101+
"Linux" -> OSType.LinuxIntel
102+
else -> throw IllegalStateException("Could not determine running system (got $osName, $arch). Only Mac (Intel, Arm) and linux (Intel) are currently supported")
103+
}
104+
}
105+
106+
private fun downloadFilename(): String {
107+
return when(osType()) {
108+
OSType.MacArm -> "bd-cli-mac-arm64.tar.gz"
109+
OSType.MacIntel -> "bd-cli-mac-x86_64.tar.gz"
110+
OSType.LinuxIntel -> "bd-cli-linux-x86_64.tar.gz"
111+
}
112+
}
113+
114+
fun downloadIfNeeded() {
115+
if(executableFilePath.exists()) {
116+
return
117+
}
118+
val parentDir = executableFilePath.parentFile
119+
if(!parentDir.exists() && !parentDir.mkdirs()) {
120+
throw IOException("Could not create path '${parentDir.absolutePath}' to contain the downloaded binary")
121+
}
122+
try {
123+
executableFilePath.writeBytes(bdcliDownloadLoc.toURL().readBytes())
124+
} catch(e: Exception) {
125+
throw IOException("Failed to download bd cli tool from $bdcliDownloadLoc", e)
126+
}
127+
if(!executableFilePath.setExecutable(true)) {
128+
throw IOException("Could not mark ${executableFilePath.absolutePath} as executable")
129+
}
130+
}
131+
}
132+
133+
fun File.asXmlDocument(): Document {
134+
try {
135+
return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(this)
136+
} catch(e: Exception) {
137+
throw IOException("Could not parse XML file $this", e)
138+
}
139+
}
140+
141+
fun File.mostRecentSubfileNamed(name: String): File {
142+
try {
143+
return this.subfilesNamed(name).mostRecent()
144+
} catch(e: Exception) {
145+
throw IOException("Could not find any file named '${name}' in path or subpath of '${this}", e)
146+
}
147+
}
148+
149+
fun File.subfilesNamed(name: String): Sequence<File> {
150+
return this.walkTopDown().filter { it.name == name }
151+
}
152+
153+
fun File.mostRecentSubfileMatching(regex: Regex): File {
154+
try {
155+
return this.subfilesMatching(regex).mostRecent()
156+
} catch(e: Exception) {
157+
throw IOException("Could not find any file matching regex '${regex}' in path or subpath of '${this}", e)
158+
}
159+
}
160+
161+
fun File.subfilesMatching(regex: Regex): Sequence<File> {
162+
return this.walkTopDown().filter { regex.matches(it.name) }
163+
}
164+
165+
fun Sequence<File>.mostRecent(): File {
166+
return this.sortedBy { it.lastModified() }.last()
167+
}

0 commit comments

Comments
 (0)