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
1 change: 1 addition & 0 deletions gradle/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
.externalNativeBuild
.cxx
local.properties
.kotlin
7 changes: 6 additions & 1 deletion gradle/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ android {
buildTypes {
debug {
minifyEnabled true
debuggable false
debuggable true // Note: Most logs will be stripped when this is false!
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
Expand All @@ -29,6 +29,11 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
Expand Down
15 changes: 15 additions & 0 deletions gradle/app/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.6.0)
project(native-lib)

add_library(
native-lib
SHARED
native.cpp)

find_library(
log-lib
log)

target_link_libraries(
native-lib
${log-lib})
21 changes: 21 additions & 0 deletions gradle/app/src/main/cpp/native.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#include <jni.h>
#include <string.h>

// Leave this public so that the compiler can't elide the null ptr access.
char *invalid_ptr = nullptr;

static void trigger_sefgault() {
*invalid_ptr = 0;
}

extern "C"
JNIEXPORT jstring JNICALL
Java_io_bitdrift_gradleexample_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
return env->NewStringUTF("This is a string coming via JNI");
}

extern "C"
JNIEXPORT void JNICALL
Java_io_bitdrift_gradleexample_FirstFragment_triggerSegfault(JNIEnv *env, jobject thiz) {
trigger_sefgault();
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import io.bitdrift.gradleexample.databinding.FragmentFirstBinding
*/
class FirstFragment : Fragment() {

private external fun triggerSegfault()
private var _binding: FragmentFirstBinding? = null

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

binding.buttonCrash.setOnClickListener {
triggerSegfault()
}
binding.buttonFirst.setOnClickListener {
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import timber.log.Timber

class MainActivity : AppCompatActivity() {

init { System.loadLibrary("native-lib"); }
private external fun stringFromJNI(): String?

private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMainBinding

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

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

super.onCreate(savedInstanceState)

Expand Down
12 changes: 11 additions & 1 deletion gradle/app/src/main/res/layout/fragment_first.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/button_crash"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/crash"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textview_first" />

<Button
android:id="@+id/button_first"
android:layout_width="wrap_content"
Expand All @@ -24,5 +34,5 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textview_first" />
app:layout_constraintTop_toBottomOf="@id/button_crash" />
</androidx.constraintlayout.widget.ConstraintLayout>
1 change: 1 addition & 0 deletions gradle/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@

<string name="hello_first_fragment">Hello first fragment</string>
<string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string>
<string name="crash">Crash</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ package io.bitdrift.capture

import com.android.build.api.variant.AndroidComponentsExtension
import io.bitdrift.capture.extension.BitdriftPluginExtension
import io.bitdrift.capture.task.CLIUploadMappingTask
import io.bitdrift.capture.task.CLIUploadSymbolsTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.slf4j.LoggerFactory
Expand All @@ -28,6 +30,22 @@ abstract class CapturePlugin @Inject constructor() : Plugin<Project> {
extension,
)
}

target.tasks.register("bdUploadMapping", CLIUploadMappingTask::class.java) { task ->
task.description = "Upload mapping to Bitdrift"
task.group = "Upload"
}

target.tasks.register("bdUploadSymbols", CLIUploadSymbolsTask::class.java) { task ->
task.description = "Upload symbols to Bitdrift"
task.group = "Upload"
}

target.tasks.register("bdUpload") { task ->
task.description = "Upload all symbol and mapping files to Bitdrift"
task.group = "Upload"
task.dependsOn("bdUploadMapping", "bdUploadSymbols")
}
}

companion object {
Expand All @@ -37,4 +55,4 @@ abstract class CapturePlugin @Inject constructor() : Plugin<Project> {
LoggerFactory.getLogger(CapturePlugin::class.java)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// capture-sdk - bitdrift's client SDK
// Copyright Bitdrift, Inc. All rights reserved.
//
// Use of this source code is governed by a source available license that can be found in the
// LICENSE file or at:
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt

package io.bitdrift.capture.task

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction
import org.w3c.dom.Document
import java.io.File
import java.io.IOException
import java.net.URI
import javax.xml.parsers.DocumentBuilderFactory
import org.gradle.api.file.Directory

abstract class CLIUploadMappingTask : CLITask() {
@TaskAction
fun action() {
// e.g. build/intermediates/packaged_manifests/release/processReleaseManifestForPackage/AndroidManifest.xml
val androidManifestXmlFile = buildDir.asFile.mostRecentSubfileNamed("AndroidManifest.xml")
// e.g. build/outputs/mapping/release/mapping.txt
val mappingTxtFile = buildDir.asFile.mostRecentSubfileNamed("mapping.txt")

val manifest = androidManifestXmlFile.asXmlDocument().documentElement
val appId = manifest.getAttribute("package")
val versionCode = manifest.getAttribute("android:versionCode")
val versionName = manifest.getAttribute("android:versionName")

runBDCLI(listOf(
"debug-files", "upload-proguard",
"--app-id", appId,
"--app-version", versionName,
"--version-code", versionCode,
mappingTxtFile.absolutePath))
}
}

abstract class CLIUploadSymbolsTask : CLITask() {
@TaskAction
fun action() {
// e.g. build/intermediates/merged_native_libs/release/mergeReleaseNativeLibs
val nativeLibsDir = buildDir.asFile.mostRecentSubfileMatching(".*merge.*NativeLibs".toRegex())
runBDCLI(listOf("debug-files", "upload", nativeLibsDir.absolutePath))
}
}

abstract class CLITask : DefaultTask() {
@Internal
val buildDir: Directory = project.layout.buildDirectory.get()
@Internal
val bdcliFile: File = buildDir.dir("bin").file("bd").asFile
@Internal
val downloader = BDCLIDownloader(bdcliFile)

fun runBDCLI(args: List<String>) {
checkEnvironment()
downloader.downloadIfNeeded()
runCommand(listOf(bdcliFile.absolutePath) + args)
}

fun runCommand(command: List<String>) {
val process = ProcessBuilder(command)
.redirectErrorStream(true)
.start()
process.inputStream.transferTo(System.out)
if (process.waitFor() != 0) {
throw RuntimeException("Command $command failed")
}
}

private fun checkEnvironment() {
val apiKeyEnvName = "API_KEY"
if(System.getenv(apiKeyEnvName) == null) {
throw IllegalStateException("Environment variable $apiKeyEnvName must be set to your Bitdrift API key before running this task")
}
}
}

class BDCLIDownloader(val executableFilePath: File) {
val bdcliVersion = "0.1.33-rc.1"
val bdcliDownloadLoc: URI = URI.create("https://dl.bitdrift.io/bd-cli/${bdcliVersion}/${downloadFilename()}/bd")

private enum class OSType {
MacIntel,
MacArm,
LinuxIntel,
}

private fun osType(): OSType {
val osName = System.getProperty("os.name")
val arch = System.getProperty("os.arch")
return when(osName) {
"Mac OS X" -> when(arch) {
"aarch64" -> OSType.MacArm
else -> OSType.MacIntel
}
"Linux" -> OSType.LinuxIntel
else -> throw IllegalStateException("Could not determine running system (got $osName, $arch). Only Mac (Intel, Arm) and linux (Intel) are currently supported")
}
}

private fun downloadFilename(): String {
return when(osType()) {
OSType.MacArm -> "bd-cli-mac-arm64.tar.gz"
OSType.MacIntel -> "bd-cli-mac-x86_64.tar.gz"
OSType.LinuxIntel -> "bd-cli-linux-x86_64.tar.gz"
}
}

fun downloadIfNeeded() {
if(executableFilePath.exists()) {
return
}
val parentDir = executableFilePath.parentFile
if(!parentDir.exists() && !parentDir.mkdirs()) {
throw IOException("Could not create path '${parentDir.absolutePath}' to contain the downloaded binary")
}
try {
executableFilePath.writeBytes(bdcliDownloadLoc.toURL().readBytes())
} catch(e: Exception) {
throw IOException("Failed to download bd cli tool from $bdcliDownloadLoc", e)
}
if(!executableFilePath.setExecutable(true)) {
throw IOException("Could not mark ${executableFilePath.absolutePath} as executable")
}
}
}

fun File.asXmlDocument(): Document {
try {
return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(this)
} catch(e: Exception) {
throw IOException("Could not parse XML file $this", e)
}
}

fun File.mostRecentSubfileNamed(name: String): File {
try {
return this.subfilesNamed(name).mostRecent()
} catch(e: Exception) {
throw IOException("Could not find any file named '${name}' in path or subpath of '${this}", e)
}
}

fun File.subfilesNamed(name: String): Sequence<File> {
return this.walkTopDown().filter { it.name == name }
}

fun File.mostRecentSubfileMatching(regex: Regex): File {
try {
return this.subfilesMatching(regex).mostRecent()
} catch(e: Exception) {
throw IOException("Could not find any file matching regex '${regex}' in path or subpath of '${this}", e)
}
}

fun File.subfilesMatching(regex: Regex): Sequence<File> {
return this.walkTopDown().filter { regex.matches(it.name) }
}

fun Sequence<File>.mostRecent(): File {
return this.sortedBy { it.lastModified() }.last()
}
Loading