diff --git a/.craft.yml b/.craft.yml index 6908dce36..dc3321ba0 100644 --- a/.craft.yml +++ b/.craft.yml @@ -5,6 +5,7 @@ targets: - name: registry sdks: github:getsentry/sentry-native: + maven:io.sentry:sentry-native-ndk: - name: gcs bucket: sentry-sdk-assets paths: @@ -14,5 +15,14 @@ targets: - path: /sentry-native/latest/ metadata: cacheControl: public, max-age=600 + - name: maven + mavenCliPath: scripts/mvnw + mavenSettingsPath: scripts/settings.xml + mavenRepoId: ossrh + mavenRepoUrl: https://oss.sonatype.org/service/local/staging/deploy/maven2/ + android: + distDirRegex: /^(sentry-native-ndk).*$/ + fileReplaceeRegex: /\d+\.\d+\.\d+(-\w+(\.\d+)?)?(-SNAPSHOT)?/ + fileReplacerStr: release.aar requireNames: - /^sentry-native.zip$/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de6371f4b..05d160c15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,8 +121,8 @@ jobs: submodules: recursive - uses: actions/setup-python@v4 with: - python-version: '3.11' - cache: 'pip' + python-version: "3.11" + cache: "pip" - name: Installing Linux Dependencies if: ${{ runner.os == 'Linux' && !env['TEST_X86'] }} @@ -149,7 +149,7 @@ jobs: - name: Expose llvm PATH for Mac if: ${{ runner.os == 'macOS' }} run: echo $(brew --prefix llvm@15)/bin >> $GITHUB_PATH - + - name: Installing LLVM-MINGW Dependencies if: ${{ runner.os == 'Windows' && env['TEST_MINGW'] }} shell: powershell @@ -203,15 +203,31 @@ jobs: with: submodules: recursive + - name: Setup Java Version + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@579fbbe7221704325eb4c4d4bf20c2b0859fba76 # pin@v3 + with: + gradle-home-cache-cleanup: true + - name: Create source archive run: | rm -rf build .c* .e* .git* scripts Makefile external/breakpad/src/tools external/breakpad/src/processor zip -r sentry-native.zip . - - name: Upload source artifact - uses: actions/upload-artifact@v3 + - name: Build NDK artifacts + working-directory: ndk + run: ./gradlew clean distZip + + - name: Upload artifacts + uses: actions/upload-artifact@v4 with: name: ${{ github.sha }} - # When downloading artifacts, they are double-zipped: - # https://github.com/actions/upload-artifact#zipped-artifact-downloads - path: sentry-native.zip + if-no-files-found: error + path: | + ./*/build/distributions/*.zip + sentry-native.zip diff --git a/.gitignore b/.gitignore index 8b197770e..cf2764d18 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,10 @@ CodeChecker # Coverage coverage + +# Gradle / Java +local.properties +.gradle +.cxx +build + diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a3a56019..d3a87a154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ - Add compile-time flag `SENTRY_TRANSPORT_COMPRESSION` description to the `README.md` file. ([#976](https://github.com/getsentry/sentry-native/pull/976)) +**Internal**: + +- Move sentry-android-ndk JNI related parts from sentry-java to sentry-native ([#944](https://github.com/getsentry/sentry-native/pull/944)) + This will create a pre-built `io.sentry:sentry-native-ndk` maven artifact, suitable for being consumed by Android apps. + **Thank you**: - [@AenBleidd](https://github.com/AenBleidd) diff --git a/README.md b/README.md index e4b533c64..b3223d6cf 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ The SDK bundle contains the following folders: directory or copy the header file to your source tree so that it is available during the build. - `src`: Sources of the Sentry SDK required for building. +- `ndk`: Sources for the Android NDK JNI layer. ## Platform and Feature Support @@ -122,11 +123,14 @@ Please refer to the CMake Manual for more details. **Android**: The CMake project can also be configured to correctly work with the Android NDK, -see the dedicated [CMake Guide] for details on how to integrate it with gradle +see the dedicated [CMake Guide] for details on how to integrate it with Gradle or use it on the command line. +The `ndk` folder provides Gradle project which adds a Java JNI layer for Android, suitable for accessing the sentry-native SDK from Java. See the [NDK Readme] for more details about this topic. + [cmake]: https://cmake.org/cmake/help/latest/ [cmake guide]: https://developer.android.com/ndk/guides/cmake +[NDK Readme]: ndk/README.md **MinGW**: diff --git a/ndk/README.md b/ndk/README.md new file mode 100644 index 000000000..190b51251 --- /dev/null +++ b/ndk/README.md @@ -0,0 +1,75 @@ +# Android NDK support for sentry-native + +| Package | Maven Central | Minimum Android API Level | Supported ABIs | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ------------------------------------------- | +| `io.sentry:sentry-native-ndk` | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-native-ndk/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-native-ndk) | 19 | "x86", "armeabi-v7a", "x86_64", "arm64-v8a" | + +## Resources + +- [SDK Documentation](https://docs.sentry.io/platforms/native/) +- [Discord](https://discord.gg/ez5KZN7) server for project discussions +- Follow [@getsentry](https://twitter.com/getsentry) on Twitter for updates + +## About + +The sub-project aims to automatically bundle pre-built `sentry-native` binaries together with a Java JNI layer into an Android friendly `.aar` package. + +The `.aar` package also provides [prefab](https://developer.android.com/build/native-dependencies?buildsystem=cmake) support, giving you the possibility to consume the native `sentry.h` APIs from your native app code. + +If you're using the [Sentry Android SDK](https://docs.sentry.io/platforms/android/), this package is included by default already. + +Besides the main package in `ndk/lib`, a simple Android app for for testing purposes is provided in the `ndk/sample` folder. + +## Building and Installation + +The `ndk` project uses the Gradle build system in combination with CMake. You can either use a suitable IDE (e.g. Android Studio) or the command line to build it. + +## Testing and consuming a local package version + +1. Set a custom `versionName` in the `ndk/gradle.properties` file +2. Publish the package locally + +```shell +cd ndk +./gradlew :sentry-native-ndk:publishToMavenLocal +``` + +3. Consume the build in your app + +``` +// usually settings.gradle +allprojects { + repositories { + mavenLocal() + } +} + +// usually app/build.gradle +android { + buildFeatures { + prefab = true + } +} + +dependencies { + implementation("io.sentry:sentry-native-ndk:") +} +``` + +4. Link the pre-built packages with your native code + +```cmake +# usually app/CMakeLists.txt + +find_package(sentry-native-ndk REQUIRED CONFIG) + +target_link_libraries( PRIVATE + ${LOG_LIB} + sentry-native-ndk::sentry-android + sentry-native-ndk::sentry +) +``` + +## Development + +Please see the [contribution guide](../CONTRIBUTING.md). diff --git a/ndk/build.gradle.kts b/ndk/build.gradle.kts new file mode 100644 index 000000000..4e9ded034 --- /dev/null +++ b/ndk/build.gradle.kts @@ -0,0 +1,207 @@ +import com.diffplug.spotless.LineEnding +import com.vanniktech.maven.publish.MavenPublishBaseExtension +import com.vanniktech.maven.publish.MavenPublishPlugin +import com.vanniktech.maven.publish.MavenPublishPluginExtension +import groovy.util.Node +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent + +plugins { + `java-library` + id("com.diffplug.spotless") version "6.11.0" apply true + id("io.gitlab.arturbosch.detekt") version "1.19.0" + `maven-publish` + id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.13.0" + +} + +buildscript { + repositories { + google() + } + dependencies { + classpath("com.android.tools.build:gradle:7.4.2") + classpath(kotlin("gradle-plugin", version = "1.8.0")) + classpath("com.vanniktech:gradle-maven-publish-plugin:0.18.0") + // dokka is required by gradle-maven-publish-plugin. + classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.7.10") + classpath("net.ltgt.gradle:gradle-errorprone-plugin:3.0.1") + + // legacy pre-prefab support + // https://github.com/howardpang/androidNativeBundle + classpath("io.github.howardpang:androidNativeBundle:1.1.4") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } + group = "io.sentry" + version = properties["versionName"].toString() + description = "SDK for sentry.io" + tasks { + withType { + testLogging.showStandardStreams = true + testLogging.exceptionFormat = TestExceptionFormat.FULL + testLogging.events = setOf( + TestLogEvent.SKIPPED, + TestLogEvent.PASSED, + TestLogEvent.FAILED + ) + maxParallelForks = Runtime.getRuntime().availableProcessors() / 2 + + // Cap JVM args per test + minHeapSize = "128m" + maxHeapSize = "1g" + dependsOn("cleanTest") + } + withType { + options.compilerArgs.addAll(arrayOf("-Xlint:all", "-Werror", "-Xlint:-classfile", "-Xlint:-processing")) + } + } +} + +subprojects { + plugins.withId("io.gitlab.arturbosch.detekt") { + configure { + buildUponDefaultConfig = true + allRules = true + config.setFrom("${rootProject.rootDir}/detekt.yml") + } + } + + if (!this.name.contains("sample")) { + apply() + + val sep = File.separator + + configure { + + this.getByName("main").contents { + // non android modules + from("build${sep}libs") + from("build${sep}publications${sep}maven") + // android modules + from("build${sep}outputs${sep}aar") { + include("*-release*") + } + from("build${sep}publications${sep}release") + } + + // craft only uses zip archives + this.forEach { dist -> + if (dist.name == DistributionPlugin.MAIN_DISTRIBUTION_NAME) { + tasks.getByName("distTar").enabled = false + } else { + tasks.getByName(dist.name + "DistTar").enabled = false + } + } + } + + tasks.named("distZip").configure { + this.dependsOn("publishToMavenLocal") + this.doLast { + val distributionFilePath = + "${this.project.buildDir}${sep}distributions${sep}${this.project.name}-${this.project.version}.zip" + val file = File(distributionFilePath) + if (!file.exists()) throw IllegalStateException("Distribution file: $distributionFilePath does not exist") + if (file.length() == 0L) throw IllegalStateException("Distribution file: $distributionFilePath is empty") + } + } + + afterEvaluate { + apply() + + configure { + // signing is done when uploading files to MC + // via gpg:sign-and-deploy-file (release.kts) + releaseSigningEnabled = false + } + + @Suppress("UnstableApiUsage") + configure { + assignAarTypes() + } + } + } +} + +spotless { + lineEndings = LineEnding.UNIX + java { + target("**/*.java") + removeUnusedImports() + googleJavaFormat() + targetExclude("**/generated/**", "**/vendor/**") + } + kotlin { + target("**/*.kt") + ktlint() + } + kotlinGradle { + target("**/*.kts") + ktlint() + } +} + +private val androidLibs = setOf( + "lib" +) + +private val androidXLibs = listOf( + "androidx.core:core" +) + +/* + * Adapted from https://github.com/androidx/androidx/blob/c799cba927a71f01ea6b421a8f83c181682633fb/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt#L524-L549 + * + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Workaround for https://github.com/gradle/gradle/issues/3170 +@Suppress("UnstableApiUsage") +fun MavenPublishBaseExtension.assignAarTypes() { + pom { + withXml { + val dependencies = asNode().children().find { + it is Node && it.name().toString().endsWith("dependencies") + } as Node? + + dependencies?.children()?.forEach { dep -> + if (dep !is Node) { + return@forEach + } + val group = dep.children().firstOrNull { + it is Node && it.name().toString().endsWith("groupId") + } as? Node + val groupValue = group?.children()?.firstOrNull() as? String + + val artifactId = dep.children().firstOrNull { + it is Node && it.name().toString().endsWith("artifactId") + } as? Node + val artifactIdValue = artifactId?.children()?.firstOrNull() as? String + + if (artifactIdValue in androidLibs) { + dep.appendNode("type", "aar") + } else if ("$groupValue:$artifactIdValue" in androidXLibs) { + dep.appendNode("type", "aar") + } + } + } + } +} diff --git a/ndk/debug.keystore b/ndk/debug.keystore new file mode 100644 index 000000000..7da7480dd Binary files /dev/null and b/ndk/debug.keystore differ diff --git a/ndk/gradle.properties b/ndk/gradle.properties new file mode 100644 index 000000000..f9a35b315 --- /dev/null +++ b/ndk/gradle.properties @@ -0,0 +1,53 @@ +# Daemons heap size +org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1536m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC +org.gradle.caching=true +org.gradle.parallel=true + +# AndroidX required by AGP >= 3.6.x +android.useAndroidX=true + +# Required by AGP >= 8.0.x +android.defaults.buildfeatures.buildconfig=true + +# Release information, used for maven publishing +versionName=0.7.2 + +# disable renderscript, it's enabled by default +android.defaults.buildfeatures.renderscript=false + +# disable shader compilation, it's enabled by default +android.defaults.buildfeatures.shaders=false + +# disable aidl files, it's enabled by default +android.defaults.buildfeatures.aidl=false + +# disable Resource Values generation +android.defaults.buildfeatures.resvalues=false + +# disable automatically adding Kotlin stdlib to compile dependencies +kotlin.stdlib.default.dependency=false + +# TODO: Enable Prefab https://android-developers.googleblog.com/2020/02/native-dependencies-in-android-studio-40.html +# android.enablePrefab=true +# android.prefabVersion=1.0.0 + +# publication pom properties +POM_NAME=Sentry SDK +POM_DESCRIPTION=SDK for sentry.io +POM_URL=https://github.com/getsentry/sentry-native +POM_SCM_URL=https://github.com/getsentry/sentry-native +POM_SCM_CONNECTION=scm:git:git://github.com/getsentry/sentry-native.git +POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/getsentry/sentry-native.git + +POM_LICENCE_NAME=MIT +POM_LICENCE_URL=http://www.opensource.org/licenses/mit-license.php + +POM_DEVELOPER_ID=getsentry +POM_DEVELOPER_NAME=Sentry Team and Contributors +POM_DEVELOPER_URL=https://github.com/getsentry/ + +POM_ARTIFACT_ID=sentry-native-ndk + +systemProp.org.gradle.internal.http.socketTimeout=120000 + +android.nonTransitiveRClass=true diff --git a/ndk/gradle/wrapper/gradle-wrapper.jar b/ndk/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..7f93135c4 Binary files /dev/null and b/ndk/gradle/wrapper/gradle-wrapper.jar differ diff --git a/ndk/gradle/wrapper/gradle-wrapper.properties b/ndk/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..3fa8f862f --- /dev/null +++ b/ndk/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/ndk/gradlew b/ndk/gradlew new file mode 100755 index 000000000..1aa94a426 --- /dev/null +++ b/ndk/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/ndk/gradlew.bat b/ndk/gradlew.bat new file mode 100644 index 000000000..6689b85be --- /dev/null +++ b/ndk/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/ndk/lib/CMakeLists.txt b/ndk/lib/CMakeLists.txt new file mode 100644 index 000000000..b9f8f4a4b --- /dev/null +++ b/ndk/lib/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.10) +project(Sentry-Android LANGUAGES C CXX) + +# Add sentry-android shared library +add_library(sentry-android SHARED src/main/jni/sentry.c) + +# make sure that we build it as a shared lib instead of a static lib +set(BUILD_SHARED_LIBS ON) +set(SENTRY_BUILD_SHARED_LIBS ON) + +# Adding sentry-native project +add_subdirectory(${SENTRY_NATIVE_SRC} sentry_build) + +# Link to sentry-native +target_link_libraries(sentry-android PRIVATE + $ +) diff --git a/ndk/lib/api/sentry-android-ndk.api b/ndk/lib/api/sentry-android-ndk.api new file mode 100644 index 000000000..e8f838ce8 --- /dev/null +++ b/ndk/lib/api/sentry-android-ndk.api @@ -0,0 +1,29 @@ +public final class io/sentry/android/ndk/BuildConfig { + public static final field BUILD_TYPE Ljava/lang/String; + public static final field DEBUG Z + public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; + public fun ()V +} + +public final class io/sentry/android/ndk/DebugImagesLoader : io/sentry/android/core/IDebugImagesLoader { + public fun (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/ndk/NativeModuleListLoader;)V + public fun clearDebugImages ()V + public fun loadDebugImages ()Ljava/util/List; +} + +public final class io/sentry/android/ndk/NdkScopeObserver : io/sentry/ScopeObserverAdapter { + public fun (Lio/sentry/SentryOptions;)V + public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V + public fun removeExtra (Ljava/lang/String;)V + public fun removeTag (Ljava/lang/String;)V + public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V + public fun setTag (Ljava/lang/String;Ljava/lang/String;)V + public fun setUser (Lio/sentry/protocol/User;)V +} + +public final class io/sentry/android/ndk/SentryNdk { + public static fun close ()V + public static fun init (Lio/sentry/android/core/SentryAndroidOptions;)V +} + diff --git a/ndk/lib/build.gradle.kts b/ndk/lib/build.gradle.kts new file mode 100644 index 000000000..f14ce9a72 --- /dev/null +++ b/ndk/lib/build.gradle.kts @@ -0,0 +1,93 @@ +plugins { + id("com.android.library") + kotlin("android") + id("com.ydq.android.gradle.native-aar.export") +} + +var sentryNativeSrc: String = "${project.projectDir}/../.." + +android { + compileSdk = 34 + namespace = "io.sentry.ndk" + + defaultConfig { + minSdk = 19 + + externalNativeBuild { + cmake { + arguments.add(0, "-DANDROID_STL=c++_shared") + arguments.add(0, "-DSENTRY_NATIVE_SRC=$sentryNativeSrc") + } + } + + ndk { + abiFilters.addAll(listOf("x86", "armeabi-v7a", "x86_64", "arm64-v8a")) + } + } + + // we use the default NDK and CMake versions based on the AGP's version + // https://developer.android.com/studio/projects/install-ndk#apply-specific-version + externalNativeBuild { + cmake { + path("CMakeLists.txt") + } + } + + buildTypes { + getByName("debug") + getByName("release") { + consumerProguardFiles("proguard-rules.pro") + } + } + + buildFeatures { + prefabPublishing = true + } + + // creates + // lib.aar/prefab/modules/sentry-android/libs//.so + // lib.aar/prefab/modules/sentry-android/include/sentry.h + prefab { + create("sentry-android") {} + create("sentry") { + headers = "../../include" + } + } + + // legacy pre-prefab support + // https://github.com/howardpang/androidNativeBundle + // creates + // lib.aar/jni//.so + // lib.aar/jni/include/sentry.h + nativeBundleExport { + headerDir = "../../include" + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + testOptions { + animationsDisabled = true + unitTests.apply { + isReturnDefaultValues = true + isIncludeAndroidResources = true + } + } + + lint { + warningsAsErrors = true + checkDependencies = true + checkReleaseBuilds = true + } + + variantFilter { + if (System.getenv("CI")?.toBoolean() == true && buildType.name == "debug") { + ignore = true + } + } +} + +dependencies { + compileOnly("org.jetbrains:annotations:23.0.0") +} diff --git a/ndk/lib/proguard-rules.pro b/ndk/lib/proguard-rules.pro new file mode 100644 index 000000000..a6d1d5f15 --- /dev/null +++ b/ndk/lib/proguard-rules.pro @@ -0,0 +1,22 @@ +##---------------Begin: proguard configuration for NDK ---------- + +# The Android SDK checks at runtime if this class is available via Class.forName +-keep class io.sentry.ndk.SentryNdk { *; } + +# The JNI layer uses this class through reflection +-keep class io.sentry.ndk.NdkOptions { *; } +-keep class io.sentry.ndk.DebugImage { *; } + +# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native +-keepclasseswithmembernames,includedescriptorclasses class * { + native ; +} + +# don't warn jetbrains annotations +-dontwarn org.jetbrains.annotations.** + +# To ensure that stack traces is unambiguous +# https://developer.android.com/studio/build/shrink-code#decode-stack-trace +-keepattributes LineNumberTable,SourceFile + +##---------------End: proguard configuration for NDK ---------- diff --git a/ndk/lib/src/main/java/io/sentry/ndk/DebugImage.java b/ndk/lib/src/main/java/io/sentry/ndk/DebugImage.java new file mode 100644 index 000000000..e06b6e2b4 --- /dev/null +++ b/ndk/lib/src/main/java/io/sentry/ndk/DebugImage.java @@ -0,0 +1,199 @@ +package io.sentry.ndk; + +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +public final class DebugImage { + + /** + * The unique UUID of the image. + * + *

UUID computed from the file contents, assigned by the Java SDK. + */ + private @Nullable String uuid; + + private @Nullable String type; + /** + * Unique debug identifier of the image. + * + *

- `elf`: Debug identifier of the dynamic library or executable. If a code identifier is + * available, the debug identifier is the little-endian UUID representation of the first 16-bytes + * of that identifier. Spaces are inserted for readability, note the byte order of the first + * fields: + * + *

```text code id: f1c3bcc0 2798 65fe 3058 404b2831d9e6 4135386c debug id: + * c0bcc3f1-9827-fe65-3058-404b2831d9e6 ``` + * + *

If no code id is available, the debug id should be computed by XORing the first 4096 bytes + * of the `.text` section in 16-byte chunks, and representing it as a little-endian UUID (again + * swapping the byte order). + * + *

- `pe`: `signature` and `age` of the PDB file. Both values can be read from the CodeView + * PDB70 debug information header in the PE. The value should be represented as little-endian + * UUID, with the age appended at the end. Note that the byte order of the UUID fields must be + * swapped (spaces inserted for readability): + * + *

```text signature: f1c3bcc0 2798 65fe 3058 404b2831d9e6 age: 1 debug_id: + * c0bcc3f1-9827-fe65-3058-404b2831d9e6-1 ``` + * + *

- `macho`: Identifier of the dynamic library or executable. It is the value of the `LC_UUID` + * load command in the Mach header, formatted as UUID. + */ + private @Nullable String debugId; + + /** + * Path and name of the debug companion file. + * + *

- `elf`: Name or absolute path to the file containing stripped debug information for this + * image. This value might be _required_ to retrieve debug files from certain symbol servers. + * + *

- `pe`: Name of the PDB file containing debug information for this image. This value is + * often required to retrieve debug files from specific symbol servers. + * + *

- `macho`: Name or absolute path to the dSYM file containing debug information for this + * image. This value might be required to retrieve debug files from certain symbol servers. + */ + private @Nullable String debugFile; + /** + * Optional identifier of the code file. + * + *

- `elf`: If the program was compiled with a relatively recent compiler, this should be the + * hex representation of the `NT_GNU_BUILD_ID` program header (type `PT_NOTE`), or the value of + * the `.note.gnu.build-id` note section (type `SHT_NOTE`). Otherwise, leave this value empty. + * + *

Certain symbol servers use the code identifier to locate debug information for ELF images, + * in which case this field should be included if possible. + * + *

- `pe`: Identifier of the executable or DLL. It contains the values of the `time_date_stamp` + * from the COFF header and `size_of_image` from the optional header formatted together into a hex + * string using `%08x%X` (note that the second value is not padded): + * + *

```text time_date_stamp: 0x5ab38077 size_of_image: 0x9000 code_id: 5ab380779000 ``` + * + *

The code identifier should be provided to allow server-side stack walking of binary crash + * reports, such as Minidumps. + * + *

+ * + *

- `macho`: Identifier of the dynamic library or executable. It is the value of the `LC_UUID` + * load command in the Mach header, formatted as UUID. Can be empty for Mach images, as it is + * equivalent to the debug identifier. + */ + private @Nullable String codeId; + /** + * Path and name of the image file (required). + * + *

The absolute path to the dynamic library or executable. This helps to locate the file if it + * is missing on Sentry. + * + *

- `pe`: The code file should be provided to allow server-side stack walking of binary crash + * reports, such as Minidumps. + */ + private @Nullable String codeFile; + /** + * Starting memory address of the image (required). + * + *

Memory address, at which the image is mounted in the virtual address space of the process. + * Should be a string in hex representation prefixed with `"0x"`. + */ + private @Nullable String imageAddr; + /** + * Size of the image in bytes (required). + * + *

The size of the image in virtual memory. If missing, Sentry will assume that the image spans + * up to the next image, which might lead to invalid stack traces. + */ + private @Nullable Long imageSize; + /** + * CPU architecture target. + * + *

Architecture of the module. If missing, this will be backfilled by Sentry. + */ + private @Nullable String arch; + + @SuppressWarnings("unused") + private @Nullable Map unknown; + + public @Nullable String getUuid() { + return uuid; + } + + public void setUuid(final @Nullable String uuid) { + this.uuid = uuid; + } + + public @Nullable String getType() { + return type; + } + + public void setType(final @Nullable String type) { + this.type = type; + } + + public @Nullable String getDebugId() { + return debugId; + } + + public void setDebugId(final @Nullable String debugId) { + this.debugId = debugId; + } + + public @Nullable String getDebugFile() { + return debugFile; + } + + public void setDebugFile(final @Nullable String debugFile) { + this.debugFile = debugFile; + } + + public @Nullable String getCodeFile() { + return codeFile; + } + + public void setCodeFile(final @Nullable String codeFile) { + this.codeFile = codeFile; + } + + public @Nullable String getImageAddr() { + return imageAddr; + } + + public void setImageAddr(final @Nullable String imageAddr) { + this.imageAddr = imageAddr; + } + + public @Nullable Long getImageSize() { + return imageSize; + } + + public void setImageSize(final @Nullable Long imageSize) { + this.imageSize = imageSize; + } + + /** + * Sets the image size. + * + * @param imageSize the image size. + */ + public void setImageSize(long imageSize) { + this.imageSize = imageSize; + } + + public @Nullable String getArch() { + return arch; + } + + public void setArch(final @Nullable String arch) { + this.arch = arch; + } + + public @Nullable String getCodeId() { + return codeId; + } + + public void setCodeId(final @Nullable String codeId) { + this.codeId = codeId; + } + +} diff --git a/ndk/lib/src/main/java/io/sentry/ndk/INativeScope.java b/ndk/lib/src/main/java/io/sentry/ndk/INativeScope.java new file mode 100644 index 000000000..4053929b3 --- /dev/null +++ b/ndk/lib/src/main/java/io/sentry/ndk/INativeScope.java @@ -0,0 +1,18 @@ +package io.sentry.ndk; + +public interface INativeScope { + void setTag(String key, String value); + + void removeTag(String key); + + void setExtra(String key, String value); + + void removeExtra(String key); + + void setUser(String id, String email, String ipAddress, String username); + + void removeUser(); + + void addBreadcrumb( + String level, String message, String category, String type, String timestamp, String data); +} diff --git a/ndk/lib/src/main/java/io/sentry/ndk/NativeModuleListLoader.java b/ndk/lib/src/main/java/io/sentry/ndk/NativeModuleListLoader.java new file mode 100644 index 000000000..c6612cef7 --- /dev/null +++ b/ndk/lib/src/main/java/io/sentry/ndk/NativeModuleListLoader.java @@ -0,0 +1,18 @@ +package io.sentry.ndk; + +import org.jetbrains.annotations.Nullable; + +public final class NativeModuleListLoader { + + public static native DebugImage[] nativeLoadModuleList(); + + public static native void nativeClearModuleList(); + + public @Nullable DebugImage[] loadModuleList() { + return nativeLoadModuleList(); + } + + public void clearModuleList() { + nativeClearModuleList(); + } +} diff --git a/ndk/lib/src/main/java/io/sentry/ndk/NativeScope.java b/ndk/lib/src/main/java/io/sentry/ndk/NativeScope.java new file mode 100644 index 000000000..18df418f7 --- /dev/null +++ b/ndk/lib/src/main/java/io/sentry/ndk/NativeScope.java @@ -0,0 +1,55 @@ +package io.sentry.ndk; + +public final class NativeScope implements INativeScope { + public static native void nativeSetTag(String key, String value); + + public static native void nativeRemoveTag(String key); + + public static native void nativeSetExtra(String key, String value); + + public static native void nativeRemoveExtra(String key); + + public static native void nativeSetUser( + String id, String email, String ipAddress, String username); + + public static native void nativeRemoveUser(); + + public static native void nativeAddBreadcrumb( + String level, String message, String category, String type, String timestamp, String data); + + @Override + public void setTag(String key, String value) { + nativeSetTag(key, value); + } + + @Override + public void removeTag(String key) { + nativeRemoveTag(key); + } + + @Override + public void setExtra(String key, String value) { + nativeSetExtra(key, value); + } + + @Override + public void removeExtra(String key) { + nativeRemoveExtra(key); + } + + @Override + public void setUser(String id, String email, String ipAddress, String username) { + nativeSetUser(id, email, ipAddress, username); + } + + @Override + public void removeUser() { + nativeRemoveUser(); + } + + @Override + public void addBreadcrumb( + String level, String message, String category, String type, String timestamp, String data) { + nativeAddBreadcrumb(level, message, category, type, timestamp, data); + } +} diff --git a/ndk/lib/src/main/java/io/sentry/ndk/NdkOptions.java b/ndk/lib/src/main/java/io/sentry/ndk/NdkOptions.java new file mode 100644 index 000000000..c4ed14f40 --- /dev/null +++ b/ndk/lib/src/main/java/io/sentry/ndk/NdkOptions.java @@ -0,0 +1,64 @@ +package io.sentry.ndk; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class NdkOptions { + private final @NotNull String dsn; + private final boolean isDebug; + private final @NotNull String outboxPath; + private final @Nullable String release; + private final @Nullable String environment; + private final @Nullable String dist; + private final int maxBreadcrumbs; + private final @Nullable String sdkName; + + public NdkOptions(@NotNull String dsn, boolean isDebug, @NotNull String outboxPath, @Nullable String release, @Nullable String environment, @Nullable String dist, int maxBreadcrumbs, @Nullable String sdkName) { + this.dsn = dsn; + this.isDebug = isDebug; + this.outboxPath = outboxPath; + this.release = release; + this.environment = environment; + this.dist = dist; + this.maxBreadcrumbs = maxBreadcrumbs; + this.sdkName = sdkName; + } + + @NotNull + public String getDsn() { + return dsn; + } + + public boolean isDebug() { + return isDebug; + } + + @NotNull + public String getOutboxPath() { + return outboxPath; + } + + @Nullable + public String getRelease() { + return release; + } + + @Nullable + public String getEnvironment() { + return environment; + } + + @Nullable + public String getDist() { + return dist; + } + + public int getMaxBreadcrumbs() { + return maxBreadcrumbs; + } + + @Nullable + public String getSdkName() { + return sdkName; + } +} diff --git a/ndk/lib/src/main/java/io/sentry/ndk/SentryNdk.java b/ndk/lib/src/main/java/io/sentry/ndk/SentryNdk.java new file mode 100644 index 000000000..58394188c --- /dev/null +++ b/ndk/lib/src/main/java/io/sentry/ndk/SentryNdk.java @@ -0,0 +1,42 @@ +package io.sentry.ndk; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class SentryNdk { + + static { + // On older Android versions, it was necessary to manually call "`System.loadLibrary` on all + // transitive dependencies before loading [the] main library." + // The dependencies of `libsentry.so` are currently `lib{c,m,dl,log}.so`. + // See + // https://android.googlesource.com/platform/bionic/+/master/android-changes-for-ndk-developers.md#changes-to-library-dependency-resolution + System.loadLibrary("log"); + System.loadLibrary("sentry"); + System.loadLibrary("sentry-android"); + } + + private SentryNdk() { + } + + private static native void initSentryNative(@NotNull final NdkOptions options); + + private static native void shutdown(); + + /** + * Init the NDK integration + * + * @param options the SentryAndroidOptions + */ + public static void init(@NotNull final NdkOptions options) { + initSentryNative(options); + } + + /** + * Closes the NDK integration + */ + public static void close() { + shutdown(); + } +} diff --git a/ndk/lib/src/main/jni/sentry.c b/ndk/lib/src/main/jni/sentry.c new file mode 100644 index 000000000..ee412a01a --- /dev/null +++ b/ndk/lib/src/main/jni/sentry.c @@ -0,0 +1,489 @@ +#include +#include +#include +#include +#include + +#define ENSURE(Expr) \ + if (!(Expr)) \ + return + +#define ENSURE_OR_FAIL(Expr) \ + if (!(Expr)) \ + goto fail + +static bool get_string_into(JNIEnv *env, jstring jstr, char *buf, size_t buf_len) { + jsize utf_len = (*env)->GetStringUTFLength(env, jstr); + if ((size_t) utf_len >= buf_len) { + return false; + } + + jsize j_len = (*env)->GetStringLength(env, jstr); + + (*env)->GetStringUTFRegion(env, jstr, 0, j_len, buf); + if ((*env)->ExceptionCheck(env) == JNI_TRUE) { + return false; + } + + buf[utf_len] = '\0'; + return true; +} + +static char *get_string(JNIEnv *env, jstring jstr) { + char *buf = NULL; + + jsize utf_len = (*env)->GetStringUTFLength(env, jstr); + size_t buf_len = (size_t) utf_len + 1; + buf = sentry_malloc(buf_len); + ENSURE_OR_FAIL(buf); + + ENSURE_OR_FAIL(get_string_into(env, jstr, buf, buf_len)); + + return buf; + + fail: + sentry_free(buf); + + return NULL; +} + +static char *call_get_string(JNIEnv *env, jobject obj, jmethodID mid) { + jstring j_str = (jstring) (*env)->CallObjectMethod(env, obj, mid); + ENSURE_OR_FAIL(j_str); + char *str = get_string(env, j_str); + (*env)->DeleteLocalRef(env, j_str); + + return str; + + fail: + return NULL; +} + +JNIEXPORT void JNICALL +Java_io_sentry_ndk_NativeScope_nativeSetTag( + JNIEnv *env, + jclass cls, + jstring key, + jstring value) { + const char *charKey = (*env)->GetStringUTFChars(env, key, 0); + const char *charValue = (*env)->GetStringUTFChars(env, value, 0); + + sentry_set_tag(charKey, charValue); + + (*env)->ReleaseStringUTFChars(env, key, charKey); + (*env)->ReleaseStringUTFChars(env, value, charValue); +} + +JNIEXPORT void JNICALL +Java_io_sentry_ndk_NativeScope_nativeRemoveTag(JNIEnv *env, jclass cls, jstring key) { + const char *charKey = (*env)->GetStringUTFChars(env, key, 0); + + sentry_remove_tag(charKey); + + (*env)->ReleaseStringUTFChars(env, key, charKey); +} + +JNIEXPORT void JNICALL +Java_io_sentry_ndk_NativeScope_nativeSetExtra( + JNIEnv *env, + jclass cls, + jstring key, + jstring value) { + const char *charKey = (*env)->GetStringUTFChars(env, key, 0); + const char *charValue = (*env)->GetStringUTFChars(env, value, 0); + + sentry_value_t sentryValue = sentry_value_new_string(charValue); + sentry_set_extra(charKey, sentryValue); + + (*env)->ReleaseStringUTFChars(env, key, charKey); + (*env)->ReleaseStringUTFChars(env, value, charValue); +} + +JNIEXPORT void JNICALL +Java_io_sentry_ndk_NativeScope_nativeRemoveExtra(JNIEnv *env, jclass cls, jstring key) { + const char *charKey = (*env)->GetStringUTFChars(env, key, 0); + + sentry_remove_extra(charKey); + + (*env)->ReleaseStringUTFChars(env, key, charKey); +} + +JNIEXPORT void JNICALL +Java_io_sentry_ndk_NativeScope_nativeSetUser( + JNIEnv *env, + jclass cls, + jstring id, + jstring email, + jstring ipAddress, + jstring username) { + sentry_value_t user = sentry_value_new_object(); + if (id) { + const char *charId = (*env)->GetStringUTFChars(env, id, 0); + sentry_value_set_by_key(user, "id", sentry_value_new_string(charId)); + (*env)->ReleaseStringUTFChars(env, id, charId); + } + if (email) { + const char *charEmail = (*env)->GetStringUTFChars(env, email, 0); + sentry_value_set_by_key( + user, "email", sentry_value_new_string(charEmail)); + (*env)->ReleaseStringUTFChars(env, email, charEmail); + } + if (ipAddress) { + const char *charIpAddress = (*env)->GetStringUTFChars(env, ipAddress, 0); + sentry_value_set_by_key( + user, "ip_address", sentry_value_new_string(charIpAddress)); + (*env)->ReleaseStringUTFChars(env, ipAddress, charIpAddress); + } + if (username) { + const char *charUsername = (*env)->GetStringUTFChars(env, username, 0); + sentry_value_set_by_key( + user, "username", sentry_value_new_string(charUsername)); + (*env)->ReleaseStringUTFChars(env, username, charUsername); + } + sentry_set_user(user); +} + +JNIEXPORT void JNICALL +Java_io_sentry_ndk_NativeScope_nativeRemoveUser(JNIEnv *env, jclass cls) { + sentry_remove_user(); +} + +JNIEXPORT void JNICALL +Java_io_sentry_ndk_NativeScope_nativeAddBreadcrumb( + JNIEnv *env, + jclass cls, + jstring level, + jstring message, + jstring category, + jstring type, + jstring timestamp, + jstring data) { + if (!level && !message && !category && !type) { + return; + } + const char *charMessage = NULL; + if (message) { + charMessage = (*env)->GetStringUTFChars(env, message, 0); + } + const char *charType = NULL; + if (type) { + charType = (*env)->GetStringUTFChars(env, type, 0); + } + sentry_value_t crumb = sentry_value_new_breadcrumb(charType, charMessage); + + if (charMessage) { + (*env)->ReleaseStringUTFChars(env, message, charMessage); + } + if (charType) { + (*env)->ReleaseStringUTFChars(env, type, charType); + } + + if (category) { + const char *charCategory = (*env)->GetStringUTFChars(env, category, 0); + sentry_value_set_by_key( + crumb, "category", sentry_value_new_string(charCategory)); + (*env)->ReleaseStringUTFChars(env, category, charCategory); + } + if (level) { + const char *charLevel = (*env)->GetStringUTFChars(env, level, 0); + sentry_value_set_by_key( + crumb, "level", sentry_value_new_string(charLevel)); + (*env)->ReleaseStringUTFChars(env, level, charLevel); + } + + if (timestamp) { + // overwrite timestamp that is already created on sentry_value_new_breadcrumb + const char *charTimestamp = (*env)->GetStringUTFChars(env, timestamp, 0); + sentry_value_set_by_key( + crumb, "timestamp", sentry_value_new_string(charTimestamp)); + (*env)->ReleaseStringUTFChars(env, timestamp, charTimestamp); + } + + if (data) { + const char *charData = (*env)->GetStringUTFChars(env, data, 0); + + // we create an object because the Java layer parses it as a Map + sentry_value_t dataObject = sentry_value_new_object(); + sentry_value_set_by_key(dataObject, "data", sentry_value_new_string(charData)); + + sentry_value_set_by_key(crumb, "data", dataObject); + + (*env)->ReleaseStringUTFChars(env, data, charData); + } + + sentry_add_breadcrumb(crumb); +} + +static void send_envelope(sentry_envelope_t *envelope, void *data) { + const char *outbox_path = (const char *) data; + char envelope_id_str[40]; + + sentry_uuid_t envelope_id = sentry_uuid_new_v4(); + sentry_uuid_as_string(&envelope_id, envelope_id_str); + + size_t outbox_len = strlen(outbox_path); + size_t final_len = outbox_len + 42; // "/" + envelope_id_str + "\0" = 42 + char *envelope_path = sentry_malloc(final_len); + ENSURE(envelope_path); + int written = snprintf(envelope_path, final_len, "%s/%s", outbox_path, envelope_id_str); + if (written > outbox_len && written < final_len) { + sentry_envelope_write_to_file(envelope, envelope_path); + } + + sentry_free(envelope_path); + sentry_envelope_free(envelope); +} + +JNIEXPORT void JNICALL +Java_io_sentry_ndk_SentryNdk_initSentryNative( + JNIEnv *env, + jclass cls, + jobject sentry_ndk_options) { + jclass options_cls = (*env)->GetObjectClass(env, sentry_ndk_options); + jmethodID outbox_path_mid = (*env)->GetMethodID(env, options_cls, "getOutboxPath", + "()Ljava/lang/String;"); + jmethodID dsn_mid = (*env)->GetMethodID(env, options_cls, "getDsn", "()Ljava/lang/String;"); + jmethodID is_debug_mid = (*env)->GetMethodID(env, options_cls, "isDebug", "()Z"); + jmethodID release_mid = (*env)->GetMethodID(env, options_cls, "getRelease", + "()Ljava/lang/String;"); + jmethodID environment_mid = (*env)->GetMethodID(env, options_cls, "getEnvironment", + "()Ljava/lang/String;"); + jmethodID dist_mid = (*env)->GetMethodID(env, options_cls, "getDist", "()Ljava/lang/String;"); + jmethodID max_crumbs_mid = (*env)->GetMethodID(env, options_cls, "getMaxBreadcrumbs", "()I"); + jmethodID native_sdk_name_mid = (*env)->GetMethodID(env, options_cls, "getSdkName", + "()Ljava/lang/String;"); + + (*env)->DeleteLocalRef(env, options_cls); + + char *outbox_path = NULL; + sentry_transport_t *transport = NULL; + bool transport_owns_path = false; + sentry_options_t *options = NULL; + bool options_owns_transport = false; + char *dsn_str = NULL; + char *release_str = NULL; + char *environment_str = NULL; + char *dist_str = NULL; + char *native_sdk_name_str = NULL; + + options = sentry_options_new(); + ENSURE_OR_FAIL(options); + + // session tracking is enabled by default, but the Android SDK already handles it + sentry_options_set_auto_session_tracking(options, 0); + + jboolean debug = (jboolean) (*env)->CallBooleanMethod(env, sentry_ndk_options, is_debug_mid); + sentry_options_set_debug(options, debug); + + jint max_crumbs = (jint) (*env)->CallIntMethod(env, sentry_ndk_options, max_crumbs_mid); + sentry_options_set_max_breadcrumbs(options, max_crumbs); + + outbox_path = call_get_string(env, sentry_ndk_options, outbox_path_mid); + ENSURE_OR_FAIL(outbox_path); + + transport = sentry_transport_new(send_envelope); + ENSURE_OR_FAIL(transport); + sentry_transport_set_state(transport, outbox_path); + sentry_transport_set_free_func(transport, sentry_free); + transport_owns_path = true; + + sentry_options_set_transport(options, transport); + options_owns_transport = true; + + // give sentry-native its own database path it can work with, next to the outbox + size_t outbox_len = strlen(outbox_path); + size_t final_len = outbox_len + 15; // len(".sentry-native\0") = 15 + char *database_path = sentry_malloc(final_len); + ENSURE_OR_FAIL(database_path); + strncpy(database_path, outbox_path, final_len); + char *dir = strrchr(database_path, '/'); + if (dir) { + strncpy(dir + 1, ".sentry-native", final_len - (dir + 1 - database_path)); + } + sentry_options_set_database_path(options, database_path); + sentry_free(database_path); + + dsn_str = call_get_string(env, sentry_ndk_options, dsn_mid); + ENSURE_OR_FAIL(dsn_str); + sentry_options_set_dsn(options, dsn_str); + sentry_free(dsn_str); + + release_str = call_get_string(env, sentry_ndk_options, release_mid); + if (release_str) { + sentry_options_set_release(options, release_str); + sentry_free(release_str); + } + + environment_str = call_get_string(env, sentry_ndk_options, environment_mid); + if (environment_str) { + sentry_options_set_environment(options, environment_str); + sentry_free(environment_str); + } + + dist_str = call_get_string(env, sentry_ndk_options, dist_mid); + if (dist_str) { + sentry_options_set_dist(options, dist_str); + sentry_free(dist_str); + } + + native_sdk_name_str = call_get_string(env, sentry_ndk_options, native_sdk_name_mid); + if (native_sdk_name_str) { + sentry_options_set_sdk_name(options, native_sdk_name_str); + sentry_free(native_sdk_name_str); + } + + sentry_init(options); + return; + + fail: + if (!transport_owns_path) { + sentry_free(outbox_path); + } + if (!options_owns_transport) { + sentry_transport_free(transport); + } + sentry_options_free(options); +} + +JNIEXPORT void JNICALL +Java_io_sentry_ndk_NativeModuleListLoader_nativeClearModuleList(JNIEnv *env, jclass cls) { + sentry_clear_modulecache(); +} + +JNIEXPORT jobjectArray JNICALL +Java_io_sentry_ndk_NativeModuleListLoader_nativeLoadModuleList(JNIEnv *env, jclass cls) { + sentry_value_t image_list_t = sentry_get_modules_list(); + jobjectArray image_list = NULL; + + if (sentry_value_get_type(image_list_t) == SENTRY_VALUE_TYPE_LIST) { + size_t len_t = sentry_value_get_length(image_list_t); + + jclass image_class = (*env)->FindClass(env, "io/sentry/ndk/DebugImage"); + image_list = (*env)->NewObjectArray(env, len_t, image_class, NULL); + + jmethodID image_addr_method = (*env)->GetMethodID(env, image_class, "setImageAddr", + "(Ljava/lang/String;)V"); + + jmethodID image_size_method = (*env)->GetMethodID(env, image_class, "setImageSize", + "(J)V"); + + jmethodID code_file_method = (*env)->GetMethodID(env, image_class, "setCodeFile", + "(Ljava/lang/String;)V"); + + jmethodID image_addr_ctor = (*env)->GetMethodID(env, image_class, "", + "()V"); + + jmethodID type_method = (*env)->GetMethodID(env, image_class, "setType", + "(Ljava/lang/String;)V"); + + jmethodID debug_id_method = (*env)->GetMethodID(env, image_class, "setDebugId", + "(Ljava/lang/String;)V"); + + jmethodID code_id_method = (*env)->GetMethodID(env, image_class, "setCodeId", + "(Ljava/lang/String;)V"); + + jmethodID debug_file_method = (*env)->GetMethodID(env, image_class, "setDebugFile", + "(Ljava/lang/String;)V"); + + for (size_t i = 0; i < len_t; i++) { + sentry_value_t image_t = sentry_value_get_by_index(image_list_t, i); + + if (!sentry_value_is_null(image_t)) { + jobject image = (*env)->NewObject(env, image_class, image_addr_ctor); + + sentry_value_t image_addr_t = sentry_value_get_by_key(image_t, "image_addr"); + if (!sentry_value_is_null(image_addr_t)) { + + const char *value_v = sentry_value_as_string(image_addr_t); + jstring value = (*env)->NewStringUTF(env, value_v); + + (*env)->CallVoidMethod(env, image, image_addr_method, value); + + // Local refs (eg NewStringUTF) are freed automatically when the native method + // returns, but if you're iterating a large array, it's recommended to release + // manually due to allocation limits (512) on Android < 8 or OOM. + // https://developer.android.com/training/articles/perf-jni.html#local-and-global-references + (*env)->DeleteLocalRef(env, value); + } + + sentry_value_t image_size_t = sentry_value_get_by_key(image_t, "image_size"); + if (!sentry_value_is_null(image_size_t)) { + + int32_t value_v = sentry_value_as_int32(image_size_t); + jlong value = (jlong) value_v; + + (*env)->CallVoidMethod(env, image, image_size_method, value); + } + + sentry_value_t code_file_t = sentry_value_get_by_key(image_t, "code_file"); + if (!sentry_value_is_null(code_file_t)) { + + const char *value_v = sentry_value_as_string(code_file_t); + jstring value = (*env)->NewStringUTF(env, value_v); + + (*env)->CallVoidMethod(env, image, code_file_method, value); + + (*env)->DeleteLocalRef(env, value); + } + + sentry_value_t code_type_t = sentry_value_get_by_key(image_t, "type"); + if (!sentry_value_is_null(code_type_t)) { + + const char *value_v = sentry_value_as_string(code_type_t); + jstring value = (*env)->NewStringUTF(env, value_v); + + (*env)->CallVoidMethod(env, image, type_method, value); + + (*env)->DeleteLocalRef(env, value); + } + + sentry_value_t debug_id_t = sentry_value_get_by_key(image_t, "debug_id"); + if (!sentry_value_is_null(code_type_t)) { + + const char *value_v = sentry_value_as_string(debug_id_t); + jstring value = (*env)->NewStringUTF(env, value_v); + + (*env)->CallVoidMethod(env, image, debug_id_method, value); + + (*env)->DeleteLocalRef(env, value); + } + + sentry_value_t code_id_t = sentry_value_get_by_key(image_t, "code_id"); + if (!sentry_value_is_null(code_id_t)) { + + const char *value_v = sentry_value_as_string(code_id_t); + jstring value = (*env)->NewStringUTF(env, value_v); + + (*env)->CallVoidMethod(env, image, code_id_method, value); + + (*env)->DeleteLocalRef(env, value); + } + + // not needed on Android, but keeping for forward compatibility + sentry_value_t debug_file_t = sentry_value_get_by_key(image_t, "debug_file"); + if (!sentry_value_is_null(debug_file_t)) { + + const char *value_v = sentry_value_as_string(debug_file_t); + jstring value = (*env)->NewStringUTF(env, value_v); + + (*env)->CallVoidMethod(env, image, debug_file_method, value); + + (*env)->DeleteLocalRef(env, value); + } + + (*env)->SetObjectArrayElement(env, image_list, i, image); + + (*env)->DeleteLocalRef(env, image); + } + } + + sentry_value_decref(image_list_t); + } + + return image_list; +} + +JNIEXPORT void JNICALL +Java_io_sentry_ndk_SentryNdk_shutdown(JNIEnv *env, jclass cls) { + sentry_close(); +} diff --git a/ndk/lib/src/main/res/values/public.xml b/ndk/lib/src/main/res/values/public.xml new file mode 100644 index 000000000..788fdddc0 --- /dev/null +++ b/ndk/lib/src/main/res/values/public.xml @@ -0,0 +1,4 @@ + + + + diff --git a/ndk/sample/CMakeLists.txt b/ndk/sample/CMakeLists.txt new file mode 100644 index 000000000..de05d5a02 --- /dev/null +++ b/ndk/sample/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.10) +project(sentry-native-ndk-sample LANGUAGES C CXX) + +set(BUILD_SHARED_LIBS ON) +set(SENTRY_BUILD_SHARED_LIBS ON) + +add_library(ndk-sample SHARED src/main/cpp/ndk-sample.cpp) + +# Adding sentry-native project +add_subdirectory(${SENTRY_NATIVE_SRC} sentry_build) + +# Android logging library +find_library(LOG_LIB log) + +target_link_libraries(ndk-sample PRIVATE + ${LOG_LIB} + $ +) diff --git a/ndk/sample/build.gradle.kts b/ndk/sample/build.gradle.kts new file mode 100644 index 000000000..5390793df --- /dev/null +++ b/ndk/sample/build.gradle.kts @@ -0,0 +1,70 @@ +plugins { + id("com.android.application") + kotlin("android") +} + +var sentryNativeSrc: String = "${project.projectDir}/../.." + +android { + compileSdk = 34 + namespace = "io.sentry.ndk.sample" + + defaultConfig { + applicationId = "io.sentry.ndk.sample" + minSdk = 19 + targetSdk = 34 + versionCode = 2 + versionName = project.version.toString() + + externalNativeBuild { + cmake { + arguments.add(0, "-DANDROID_STL=c++_shared") + arguments.add(0, "-DSENTRY_NATIVE_SRC=$sentryNativeSrc") + } + } + + ndk { + abiFilters.addAll(listOf("x86", "armeabi-v7a", "x86_64", "arm64-v8a")) + } + } + + externalNativeBuild { + cmake { + path("CMakeLists.txt") + } + } + + signingConfigs { + getByName("debug") { + storeFile = rootProject.file("debug.keystore") + storePassword = "android" + keyAlias = "androiddebugkey" + keyPassword = "android" + } + } + + buildTypes { + getByName("release") { + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + ) + signingConfig = signingConfigs.getByName("debug") // to be able to run release mode + isShrinkResources = true + + addManifestPlaceholders( + mapOf( + "sentryDebug" to false, "sentryEnvironment" to "release" + ) + ) + } + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } +} + +dependencies { + implementation(project(":sentry-native-ndk")) +} diff --git a/ndk/sample/proguard-rules.pro b/ndk/sample/proguard-rules.pro new file mode 100644 index 000000000..1165340c8 --- /dev/null +++ b/ndk/sample/proguard-rules.pro @@ -0,0 +1,34 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# To ensure that stack traces is unambiguous +# https://developer.android.com/studio/build/shrink-code#decode-stack-trace +-keepattributes LineNumberTable,SourceFile + +# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native +-keepclasseswithmembernames,includedescriptorclasses class * { + native ; +} + +# Please add these rules to your existing keep rules in order to suppress warnings. +# This is generated automatically by the Android Gradle plugin. +-dontwarn org.bouncycastle.jsse.BCSSLParameters +-dontwarn org.bouncycastle.jsse.BCSSLSocket +-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider +-dontwarn org.conscrypt.Conscrypt$Version +-dontwarn org.conscrypt.Conscrypt +-dontwarn org.conscrypt.ConscryptHostnameVerifier +-dontwarn org.openjsse.javax.net.ssl.SSLParameters +-dontwarn org.openjsse.javax.net.ssl.SSLSocket +-dontwarn org.openjsse.net.ssl.OpenJSSE diff --git a/ndk/sample/src/main/AndroidManifest.xml b/ndk/sample/src/main/AndroidManifest.xml new file mode 100644 index 000000000..882d9c1e8 --- /dev/null +++ b/ndk/sample/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/ndk/sample/src/main/cpp/ndk-sample.cpp b/ndk/sample/src/main/cpp/ndk-sample.cpp new file mode 100644 index 000000000..d1f62bf0b --- /dev/null +++ b/ndk/sample/src/main/cpp/ndk-sample.cpp @@ -0,0 +1,25 @@ +#include +#include +#include + +#define TAG "ndk-sample" + +extern "C" { + +JNIEXPORT void JNICALL Java_io_sentry_ndk_sample_NdkSample_crash(JNIEnv *env, jclass cls) { + __android_log_print(ANDROID_LOG_WARN, TAG, "About to crash."); + char *ptr = 0; + *ptr += 1; +} + +JNIEXPORT void JNICALL Java_io_sentry_ndk_sample_NdkSample_message(JNIEnv *env, jclass cls) { + __android_log_print(ANDROID_LOG_WARN, TAG, "Sending message."); + sentry_value_t event = sentry_value_new_message_event( + /* level */ SENTRY_LEVEL_INFO, + /* logger */ "custom", + /* message */ "It works!" + ); + sentry_capture_event(event); +} + +} diff --git a/ndk/sample/src/main/java/io/sentry/ndk/sample/MainActivity.java b/ndk/sample/src/main/java/io/sentry/ndk/sample/MainActivity.java new file mode 100644 index 000000000..d8d42b3df --- /dev/null +++ b/ndk/sample/src/main/java/io/sentry/ndk/sample/MainActivity.java @@ -0,0 +1,54 @@ +package io.sentry.ndk.sample; + +import android.app.Activity; +import android.os.Bundle; + +import java.io.File; + +import io.sentry.ndk.NdkOptions; +import io.sentry.ndk.SentryNdk; + +public class MainActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + findViewById(R.id.init_ndk_button).setOnClickListener(v -> initNdk()); + findViewById(R.id.trigger_native_crash_button).setOnClickListener(v -> NdkSample.crash()); + findViewById(R.id.capture_message_button).setOnClickListener(v -> NdkSample.message()); + } + + private void initNdk() { + final File outboxFolder = setupOutboxFolder(); + final NdkOptions options = new NdkOptions( + "https://1053864c67cc410aa1ffc9701bd6f93d@o447951.ingest.sentry.io/5428559", + BuildConfig.DEBUG, + outboxFolder.getAbsolutePath(), + "1.0.0", + "production", + BuildConfig.VERSION_NAME, + 100, + "sentry-native-jni" + ); + SentryNdk.init(options); + } + + private File setupOutboxFolder() { + // ensure we have a proper outbox directory + final File outboxDir = new File(getFilesDir(), "outbox"); + if (outboxDir.isFile()) { + final boolean deleteOk = outboxDir.delete(); + if (!deleteOk) { + throw new IllegalStateException("Failed to delete outbox file: " + outboxDir); + } + } + if (!outboxDir.exists()) { + final boolean mkdirOk = outboxDir.mkdirs(); + if (!mkdirOk) { + throw new IllegalStateException("Failed to create outbox directory: " + outboxDir); + } + } + return outboxDir; + } +} diff --git a/ndk/sample/src/main/java/io/sentry/ndk/sample/NdkSample.java b/ndk/sample/src/main/java/io/sentry/ndk/sample/NdkSample.java new file mode 100644 index 000000000..27fdeaf5e --- /dev/null +++ b/ndk/sample/src/main/java/io/sentry/ndk/sample/NdkSample.java @@ -0,0 +1,11 @@ +package io.sentry.ndk.sample; + +public class NdkSample { + static { + System.loadLibrary("ndk-sample"); + } + + public static native void crash(); + + public static native void message(); +} diff --git a/ndk/sample/src/main/res/layout/activity_main.xml b/ndk/sample/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..8045ad259 --- /dev/null +++ b/ndk/sample/src/main/res/layout/activity_main.xml @@ -0,0 +1,36 @@ + + + + + +