diff --git a/.agents/commands/release.md b/.agents/commands/release.md index da2fe615ba..e39f686fa1 100644 --- a/.agents/commands/release.md +++ b/.agents/commands/release.md @@ -1,5 +1,5 @@ --- -description: "Create a new release: bump version, create PR, run release workflow, tag, draft release" +description: "Create a new release: bump version, create PR, build mainnet, tag, draft release" allowed_tools: Bash, Read, Edit, Write, Glob, Grep, AskUserQuestion, mcp__github__create_pull_request, mcp__github__list_pull_requests, mcp__github__pull_request_read, mcp__github__get_file_contents, mcp__github__update_pull_request --- @@ -77,8 +77,6 @@ Cherry-pick the commits you need onto this branch now, then continue. ``` Wait for the user to confirm they are done cherry-picking before proceeding. -If the base is a tag that predates the release workflow changes, port the current release workflow support onto the release branch before proceeding. At minimum, the release branch/tag must contain the updated artifact naming in `.github/workflows/release.yml`, otherwise Step 7 will dispatch an old workflow and then look for an artifact name it cannot produce. - Finalize changelog after the release branch contains all release commits: ```bash @@ -204,66 +202,28 @@ gh release edit v{newVersionName} --notes-file /tmp/release-notes.md Print the path to the release notes file so the user can share it for review. -### 7. Run Store Release Workflow +### 7. Build Mainnet Release ```bash -release_ref="v{newVersionName}" -release_artifact_dir=".ai/release-artifacts-{newVersionName}" -if ! git show "$release_ref:.github/workflows/release.yml" | grep -q 'bitkit-release-$build_number-$GITHUB_RUN_NUMBER'; then - echo "Release ref $release_ref does not contain the current release artifact naming." >&2 - echo "Port the release workflow changes onto the release branch, retag, then rerun /release." >&2 - exit 1 -fi -dispatch_started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" -gh workflow run release.yml --ref "$release_ref" -run_id="" -for attempt in {1..30}; do - run_id="$(gh run list \ - --workflow release.yml \ - --branch "$release_ref" \ - --event workflow_dispatch \ - --created ">=$dispatch_started_at" \ - --limit 1 \ - --json databaseId \ - --jq '.[0].databaseId // empty')" - if [ -n "$run_id" ]; then - break - fi - sleep 5 -done -if [ -z "$run_id" ]; then - echo "Failed to find release workflow run for $release_ref" >&2 - exit 1 -fi -gh run watch "$run_id" --exit-status -workflow_run_url="$(gh run view "$run_id" --json url --jq .url)" -run_number="$(gh run view "$run_id" --json number --jq .number)" -rm -rf "$release_artifact_dir" -mkdir -p "$release_artifact_dir" -gh run download "$run_id" \ - --name "bitkit-release-{newVersionCode}-${run_number}" \ - --dir "$release_artifact_dir" -if command -v sha256sum >/dev/null; then - (cd "$release_artifact_dir" && sha256sum -c SHA256SUMS.txt) -else - (cd "$release_artifact_dir" && shasum -a 256 -c SHA256SUMS.txt) -fi +just release ``` -Expected APK path: `.ai/release-artifacts-{newVersionName}/bitkit-mainnet-release-{newVersionCode}-universal.apk` -Expected AAB path: `.ai/release-artifacts-{newVersionName}/bitkit-mainnet-release-{newVersionCode}.aab` - -Verify both files exist. If the workflow fails or the artifact checksum verification fails, stop and report the error to the user. +Expected APK path: `app/build/outputs/apk/mainnet/release/bitkit-mainnet-release-{newVersionCode}-universal.apk` +Expected AAB path: `app/build/outputs/bundle/mainnetRelease/bitkit-mainnet-release-{newVersionCode}.aab` +Expected native debug symbols path: `app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols-{newVersionCode}.zip` -Store `workflow_run_url` for the summary. +Verify all three files exist. The native debug symbols file must be from the same `just release` build as the APK/AAB. Keep the build-numbered filename, e.g. `native-debug-symbols-{newVersionCode}.zip`, so it matches the APK/AAB build number. `just release` resolves upstream native debug symbol artifacts from the Rust dependency packages, merges them into the final archive, and refuses placeholder symbols from stripped packaged `.so` files. -### 8. Upload Workflow APK to Draft Release +### 8. Upload APK and Native Symbols to Draft Release ```bash gh release upload v{newVersionName} \ - .ai/release-artifacts-{newVersionName}/bitkit-mainnet-release-{newVersionCode}-universal.apk + app/build/outputs/apk/mainnet/release/bitkit-mainnet-release-{newVersionCode}-universal.apk \ + app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols-{newVersionCode}.zip ``` +For the Play Store release, upload the AAB as usual, then upload `native-debug-symbols-{newVersionCode}.zip` for the exact version/build in Play Console: App bundle explorer → Downloads → Assets. Verify Play lists the native debug symbols after upload. Keep the release-built archive in GitHub releases or internal release storage; Play Console may only show delete/replace controls after upload, which is enough for release verification. + ### 9. Return to Master ```bash @@ -279,16 +239,15 @@ Version bump PR: {PR URL} Release branch: release-{newVersionName} Tag: v{newVersionName} Draft release: {release URL} -Release workflow: {workflow run URL} -Artifacts: .ai/release-artifacts-{newVersionName} APK uploaded: bitkit-mainnet-release-{newVersionCode}-universal.apk +Native debug symbols uploaded: native-debug-symbols-{newVersionCode}.zip Store release notes: .ai/release-notes-{newVersionName}.md Next steps: - Share release notes with Jacobo for review -- QA the workflow-built APK -- Submit the workflow-built AAB to Play Store when QA passes -- If patching the release branch: increment only versionCode, re-tag, rerun the release workflow, and re-upload +- QA the APK +- If patching the release branch: increment only versionCode, re-tag, rebuild, and re-upload +- Submit to Play Store when QA passes - Publish the draft release on GitHub after store release - Merge release branch PR into master ``` diff --git a/.github/workflows/release-internal.yml b/.github/workflows/release-internal.yml index 4f419bd702..dd7ec94169 100644 --- a/.github/workflows/release-internal.yml +++ b/.github/workflows/release-internal.yml @@ -73,7 +73,11 @@ jobs: KEYSTORE_PASSWORD: ${{ secrets.INTERNAL_KEYSTORE_PASSWORD }} KEY_ALIAS: ${{ secrets.INTERNAL_KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.INTERNAL_KEY_PASSWORD }} - run: ./gradlew assembleMainnetRelease --no-daemon --stacktrace + run: | + set -euo pipefail + ./gradlew assembleMainnetRelease --no-daemon --stacktrace + ./gradlew :app:syncNativeDebugSymbolArtifacts --no-daemon --stacktrace + scripts/create-native-debug-symbols.sh - name: Verify native libraries are stripped run: | @@ -124,18 +128,23 @@ jobs: artifact_dir="$RUNNER_TEMP/internal-release" mkdir -p "$artifact_dir" mapfile -d '' apk_paths < <(find app/build/outputs/apk/mainnet/release -name 'bitkit-mainnet-release-*.apk' -print0) + mapfile -d '' symbol_paths < <(find app/build/outputs/native-debug-symbols/mainnetRelease -name 'native-debug-symbols-*.zip' -print0) test "${#apk_paths[@]}" -gt 0 + test "${#symbol_paths[@]}" -gt 0 for apk_path in "${apk_paths[@]}"; do cp "$apk_path" "$artifact_dir/" done + for symbol_path in "${symbol_paths[@]}"; do + cp "$symbol_path" "$artifact_dir/" + done apk_file="$(basename "${apk_paths[0]}")" build_number="${apk_file#bitkit-mainnet-release-}" build_number="${build_number%%-*}" [[ "$build_number" =~ ^[0-9]+$ ]] - (cd "$artifact_dir" && sha256sum -- *.apk > SHA256SUMS.txt) + (cd "$artifact_dir" && sha256sum -- *.apk native-debug-symbols-*.zip > SHA256SUMS.txt) echo "artifact_name=bitkit-release-internal-$build_number-$GITHUB_RUN_NUMBER" >> "$GITHUB_OUTPUT" echo "artifact_dir=$artifact_dir" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bde387774c..b7af685206 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,7 +74,11 @@ jobs: KEYSTORE_PASSWORD: ${{ secrets.BITKIT_KEYSTORE_PASSWORD }} KEY_ALIAS: ${{ secrets.BITKIT_KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.BITKIT_KEY_PASSWORD }} - run: ./gradlew assembleMainnetRelease bundleMainnetRelease --no-daemon --stacktrace + run: | + set -euo pipefail + ./gradlew assembleMainnetRelease bundleMainnetRelease --no-daemon --stacktrace + ./gradlew :app:syncNativeDebugSymbolArtifacts --no-daemon --stacktrace + scripts/create-native-debug-symbols.sh - name: Verify native libraries are stripped run: | @@ -148,8 +152,10 @@ jobs: mkdir -p "$artifact_dir" mapfile -d '' bundle_paths < <(find app/build/outputs/bundle/mainnetRelease -name 'bitkit-mainnet-release-*.aab' -print0) mapfile -d '' apk_paths < <(find app/build/outputs/apk/mainnet/release -name 'bitkit-mainnet-release-*.apk' -print0) + mapfile -d '' symbol_paths < <(find app/build/outputs/native-debug-symbols/mainnetRelease -name 'native-debug-symbols-*.zip' -print0) test "${#bundle_paths[@]}" -gt 0 test "${#apk_paths[@]}" -gt 0 + test "${#symbol_paths[@]}" -gt 0 for bundle_path in "${bundle_paths[@]}"; do cp "$bundle_path" "$artifact_dir/" @@ -157,13 +163,16 @@ jobs: for apk_path in "${apk_paths[@]}"; do cp "$apk_path" "$artifact_dir/" done + for symbol_path in "${symbol_paths[@]}"; do + cp "$symbol_path" "$artifact_dir/" + done apk_file="$(basename "${apk_paths[0]}")" build_number="${apk_file#bitkit-mainnet-release-}" build_number="${build_number%%-*}" [[ "$build_number" =~ ^[0-9]+$ ]] - (cd "$artifact_dir" && sha256sum -- *.aab *.apk > SHA256SUMS.txt) + (cd "$artifact_dir" && sha256sum -- *.aab *.apk native-debug-symbols-*.zip > SHA256SUMS.txt) echo "artifact_name=bitkit-release-$build_number-$GITHUB_RUN_NUMBER" >> "$GITHUB_OUTPUT" echo "artifact_dir=$artifact_dir" >> "$GITHUB_OUTPUT" diff --git a/Justfile b/Justfile index 1d282ff82c..a27f96eed9 100644 --- a/Justfile +++ b/Justfile @@ -173,7 +173,15 @@ build task="assembleDevDebug": {{ gradle }} {{ task }} release: + #!/usr/bin/env sh + set -eu + symbols_dir="app/build/outputs/native-debug-symbols/mainnetRelease" + rm -f "$symbols_dir"/native-debug-symbols*.zip NDK_VERSION={{ ndk_ver }} {{ gradle }} assembleMainnetRelease bundleMainnetRelease + NDK_VERSION={{ ndk_ver }} {{ gradle }} :app:syncNativeDebugSymbolArtifacts + scripts/create-native-debug-symbols.sh + symbols="$(find "$symbols_dir" -maxdepth 1 -name 'native-debug-symbols-*.zip' -type f | sort | tail -n 1)" + echo "Attach this exact file to GitHub releases, upload it to Play Console for this release, and verify Play lists it: $symbols" install: {{ gradle }} installDevDebug diff --git a/README.md b/README.md index 8995692502..b4c4a17829 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,22 @@ To build the mainnet flavor for release run: just release ``` +`just release` builds the mainnet APK, Play Store AAB, resolves upstream native debug symbol artifacts, and validates the native debug symbols archive. + +Release artifacts: + +- APK: `app/build/outputs/apk/mainnet/release/` +- AAB: `app/build/outputs/bundle/mainnetRelease/` +- Native debug symbols: `app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols-{versionCode}.zip` + +The native debug symbols archive must come from the same `just release` build as the APK/AAB being published. Keep the build-numbered filename, e.g. `native-debug-symbols-182.zip`, so it matches the APK/AAB build number. Native Rust dependencies publish stripped release AARs for app size and separate `native-debug-symbols` classifier artifacts for crash symbolication; `just release` merges those upstream symbol artifacts into the final archive and refuses placeholder symbols from stripped packaged `.so` files. + +For Play Store releases, upload the AAB as usual, then upload `native-debug-symbols-{versionCode}.zip` for that exact version/build in Play Console: App bundle explorer → Downloads → Assets. Verify Play lists the native debug symbols after upload. + +Keep the release-built `native-debug-symbols-{versionCode}.zip` in GitHub releases or internal release storage. Play Console may only show delete/replace controls after upload, which is enough for release verification. + +For GitHub releases, attach `native-debug-symbols-{versionCode}.zip` alongside the APK so native crashes from GitHub-distributed builds can be symbolicated later. + #### Android App Bundle (AAB) `just release` builds both the mainnet APK and Play Store AAB. AAB is generated in `app/build/outputs/bundle/mainnetRelease/`. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 13af5ccbb7..e5e7f31133 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,9 @@ import com.android.build.gradle.internal.api.BaseVariantOutputImpl import com.android.build.gradle.internal.tasks.FinalizeBundleTask import io.gitlab.arturbosch.detekt.Detekt +import org.gradle.api.artifacts.MinimalExternalModuleDependency +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Sync import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent import org.jetbrains.kotlin.gradle.dsl.JvmTarget @@ -232,7 +235,7 @@ android { ) signingConfig = signingConfigs.getByName("release") ndk { - debugSymbolLevel = "SYMBOL_TABLE" + debugSymbolLevel = "FULL" // noinspection ChromeOsAbiSupport abiFilters += listOf("armeabi-v7a", "arm64-v8a") } @@ -318,6 +321,31 @@ composeCompiler { reportsDestination = layout.buildDirectory.dir("compose_compiler") } +val nativeDebugSymbols by configurations.creating { + isCanBeConsumed = false + isCanBeResolved = true +} + +fun Provider.nativeDebugSymbolsArtifact(): String { + val dependency = get() + val version = dependency.versionConstraint.requiredVersion + .ifBlank { dependency.versionConstraint.preferredVersion } + + require(version.isNotBlank()) { + "Native debug symbols dependency '${dependency.module}' must declare an explicit version." + } + + return "${dependency.module.group}:${dependency.module.name}:$version:native-debug-symbols@zip" +} + +val syncNativeDebugSymbolArtifacts by tasks.registering(Sync::class) { + group = "build" + description = "Downloads native debug symbol archives for release native dependencies." + + from(nativeDebugSymbols) + into(layout.buildDirectory.dir("intermediates/native-debug-symbol-artifacts")) +} + dependencies { implementation(fileTree("libs") { include("*.aar", "*.jar") }) implementation(libs.jna) { artifact { type = "aar" } } @@ -343,6 +371,9 @@ dependencies { implementation(libs.bitkit.core) implementation(libs.paykit) implementation(libs.vss.client) + nativeDebugSymbols(libs.bitkit.core.nativeDebugSymbolsArtifact()) + nativeDebugSymbols(libs.ldk.node.android.nativeDebugSymbolsArtifact()) + nativeDebugSymbols(libs.vss.client.nativeDebugSymbolsArtifact()) // Firebase implementation(platform(libs.firebase.bom)) implementation(libs.firebase.messaging) diff --git a/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt new file mode 100644 index 0000000000..2ebe63e639 --- /dev/null +++ b/app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt @@ -0,0 +1,163 @@ +package to.bitkit.build + +import kotlin.io.path.Path +import kotlin.io.path.exists +import kotlin.io.path.readText +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class NativeReleaseConfigTest { + + private val repoRoot = generateSequence( + Path(requireNotNull(System.getProperty("user.dir")) { "user.dir is required" }), + ) { it.parent } + .first { it.resolve("gradle/libs.versions.toml").exists() } + + @Test + fun `release build requests full native debug symbols`() { + val buildFile = repoRoot.resolve("app/build.gradle.kts").readText() + + assertTrue( + buildFile.contains("""debugSymbolLevel = "FULL""""), + "Release builds must request full native debug symbols for Play crash symbolication.", + ) + } + + @Test + fun `release recipe verifies native debug symbols archive`() { + val justfile = repoRoot.resolve("Justfile").readText() + + assertTrue( + justfile.contains( + """rm -f "${'$'}symbols_dir"/native-debug-symbols*.zip""", + ), + "Release builds must remove stale native debug symbols before rebuilding.", + ) + assertTrue( + justfile.contains("scripts/create-native-debug-symbols.sh"), + "Release builds must create the native debug symbols archive before publishing.", + ) + assertTrue( + justfile.contains(":app:syncNativeDebugSymbolArtifacts"), + "Release builds must resolve upstream native debug symbol artifacts before publishing.", + ) + assertTrue( + justfile.contains("Attach this exact file to GitHub releases"), + "Release builds must tell the releaser to attach native debug symbols.", + ) + assertTrue( + justfile.contains("upload it to Play Console for this release"), + "Release builds must tell the releaser to upload native debug symbols to Play.", + ) + assertTrue( + justfile.contains("syncNativeDebugSymbolArtifacts"), + "Release builds should download native dependency symbols from release artifacts.", + ) + } + + @Test + fun `release command uploads native debug symbols archive`() { + val releaseCommand = repoRoot.resolve(".agents/commands/release.md").readText() + + assertTrue( + releaseCommand.contains( + "app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols-{newVersionCode}.zip", + ), + "Release command must include the native debug symbols archive path.", + ) + assertTrue( + releaseCommand.contains("Native debug symbols uploaded: native-debug-symbols-{newVersionCode}.zip"), + "Release command summary must report the native debug symbols archive.", + ) + assertFalse( + releaseCommand.contains("Play " + "did not"), + "Release command should use current Play native symbol wording.", + ) + assertTrue( + releaseCommand.contains("resolves upstream native debug symbol artifacts"), + "Release command must document upstream native debug symbol artifact resolution.", + ) + assertTrue( + releaseCommand.contains("Play Console may only show delete/replace controls"), + "Release command must document the verified Play Console behavior.", + ) + } + + @Test + fun `native debug symbols script rejects stripped release libraries`() { + val symbolsScript = repoRoot.resolve("scripts/create-native-debug-symbols.sh").readText() + + assertBuildNumberedArchiveOutput(symbolsScript) + assertTrue( + symbolsScript.contains("native-debug-symbol-artifacts"), + "Native debug symbols script must use upstream native dependency symbol archives.", + ) + assertTrue( + symbolsScript.contains("arm64-v8a armeabi-v7a"), + "Native debug symbols script must archive Play release ABIs.", + ) + assertTrue( + symbolsScript.contains("zip -qr"), + "Native debug symbols script must create a zip archive.", + ) + assertTrue( + symbolsScript.contains( + """required_libs="libbitkitcore.so libldk_node.so libvss_rust_client_ffi.so"""", + ), + "Native debug symbols script must validate release-critical native libraries.", + ) + assertTrue( + symbolsScript.contains("""archive_symbol_suffixes=".dbg .sym""""), + "Native debug symbols script must accept AGP native debug symbol entry suffixes.", + ) + assertDependencyArchiveEntriesAreNormalized(symbolsScript) + assertTrue( + symbolsScript.contains("""grep -Eq '\.debug_info'"""), + "Native debug symbols script must validate full DWARF debug metadata before zipping.", + ) + assertTrue( + symbolsScript.contains("ANDROID_NDK_ROOT"), + "Native debug symbols script must use the same NDK env paths Gradle can use.", + ) + assertTrue( + symbolsScript.contains("local.properties") && + symbolsScript.contains("ndk.dir") && + symbolsScript.contains("sdk.dir"), + "Native debug symbols script must use local.properties NDK/SDK paths before PATH fallback.", + ) + assertFalse( + symbolsScript.contains("symtab|debug_|gnu_debugdata"), + "Native debug symbols script must not accept symbol-table-only metadata for FULL symbols.", + ) + assertTrue( + symbolsScript.contains("Refusing to create '${'$'}output' from stripped native libraries."), + "Native debug symbols script must refuse placeholder archives.", + ) + assertTrue( + symbolsScript.contains("syncNativeDebugSymbolArtifacts"), + "Native debug symbols script must point to the Gradle task that resolves symbol artifacts.", + ) + } + + private fun assertBuildNumberedArchiveOutput(symbolsScript: String) { + assertTrue( + symbolsScript.contains( + "app/build/outputs/native-debug-symbols/${'$'}variant/native-debug-symbols-${'$'}build_number.zip", + ), + "Native debug symbols script must write the build-numbered archive path.", + ) + assertTrue( + symbolsScript.contains("""rm -f "${'$'}output_dir"/native-debug-symbols*.zip"""), + "Native debug symbols script must clear stale build-numbered archives before writing.", + ) + } + + private fun assertDependencyArchiveEntriesAreNormalized(symbolsScript: String) { + assertTrue( + symbolsScript.contains("copy_archive_symbols") && + symbolsScript.contains("""mv "${'$'}tmp_dir/${'$'}entry" "${'$'}tmp_dir/${'$'}abi/${'$'}lib_name""""), + "Native debug symbols script must normalize suffixed dependency archive entries before validation.", + ) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 287c726f9e..40ca00380e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ activity-compose = { module = "androidx.activity:activity-compose", version = "1 appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.1" } barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version = "17.3.0" } biometric = { module = "androidx.biometric:biometric", version = "1.4.0-alpha05" } -bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.72" } +bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.73" } paykit = { module = "com.synonym:paykit-android", version = "0.1.0-rc8" } bouncycastle-provider-jdk = { module = "org.bouncycastle:bcprov-jdk18on", version = "1.83" } camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" } @@ -64,7 +64,7 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } -ldk-node-android = { module = "com.synonym:ldk-node-android", version = "0.7.0-rc.46" } +ldk-node-android = { module = "com.synonym:ldk-node-android", version = "0.7.0-rc.51" } lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" } lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } @@ -88,7 +88,7 @@ test-junit-ext = { module = "androidx.test.ext:junit", version = "1.3.0" } test-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "6.2.2" } test-robolectric = { module = "org.robolectric:robolectric", version = "4.16.1" } test-turbine = { group = "app.cash.turbine", name = "turbine", version = "1.2.1" } -vss-client = { module = "com.synonym:vss-client-android", version = "0.5.12" } +vss-client = { module = "com.synonym:vss-client-android", version = "0.5.20" } work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version = "2.11.0" } zxing = { module = "com.google.zxing:core", version = "3.5.4" } lottie = { module = "com.airbnb.android:lottie-compose", version = "6.7.1" } diff --git a/scripts/create-native-debug-symbols.sh b/scripts/create-native-debug-symbols.sh new file mode 100755 index 0000000000..db33ce798c --- /dev/null +++ b/scripts/create-native-debug-symbols.sh @@ -0,0 +1,295 @@ +#!/usr/bin/env sh +set -eu + +script_dir=$(cd "$(dirname "$0")" && pwd) +repo_root=$(cd "$script_dir/.." && pwd) +cd "$repo_root" + +variant="mainnetRelease" +build_number=$( + awk -F= ' + /^[[:space:]]*versionCode[[:space:]]*=/ { + value = $2 + gsub(/[[:space:]]/, "", value) + print value + exit + } + ' app/build.gradle.kts +) +case "$build_number" in + ''|*[!0-9]*) + echo "Unable to read numeric versionCode from app/build.gradle.kts." >&2 + exit 1 + ;; +esac + +output="app/build/outputs/native-debug-symbols/$variant/native-debug-symbols-$build_number.zip" +output_dir=$(dirname "$output") +dependency_symbols_dir="app/build/intermediates/native-debug-symbol-artifacts" +required_libs="libbitkitcore.so libldk_node.so libvss_rust_client_ffi.so" +archive_symbol_suffixes=".dbg .sym" + +tmp_dirs="" +cleanup() { + for dir in $tmp_dirs; do + rm -rf "$dir" + done +} +trap cleanup EXIT + +make_tmp_dir() { + dir=$(mktemp -d) + tmp_dirs="$tmp_dirs $dir" + echo "$dir" +} + +local_properties_value() { + key="$1" + if [ ! -f "$repo_root/local.properties" ]; then + return + fi + + awk -F= -v key="$key" '$1 == key { value = $0; sub(/^[^=]*=/, "", value) } END { print value }' \ + "$repo_root/local.properties" | sed 's/\\ / /g' +} + +find_readelf() { + local_ndk_dir=$(local_properties_value "ndk.dir") + local_sdk_dir=$(local_properties_value "sdk.dir") + + for ndk_dir in \ + "${ANDROID_NDK_ROOT:-}" \ + "${ANDROID_NDK_HOME:-}" \ + "${NDK_HOME:-}" \ + "$local_ndk_dir" \ + "${ANDROID_HOME:+$ANDROID_HOME/ndk}" \ + "${ANDROID_SDK_ROOT:+$ANDROID_SDK_ROOT/ndk}" \ + "${local_sdk_dir:+$local_sdk_dir/ndk}"; do + if [ -z "$ndk_dir" ]; then + continue + fi + + for candidate in \ + "$ndk_dir"/toolchains/llvm/prebuilt/*/bin/llvm-readelf \ + "$ndk_dir"/*/toolchains/llvm/prebuilt/*/bin/llvm-readelf; do + if [ -x "$candidate" ]; then + echo "$candidate" + return + fi + done + done + + if command -v llvm-readelf >/dev/null 2>&1; then + command -v llvm-readelf + return + fi + + if command -v readelf >/dev/null 2>&1; then + command -v readelf + return + fi + + echo "llvm-readelf or readelf is required to validate native debug symbols." >&2 + exit 1 +} + +readelf_bin=$(find_readelf) + +has_dwarf_debug_metadata() { + "$readelf_bin" -S "$1" | grep -Eq '\.debug_info' +} + +validate_symbol_tree() { + root="$1" + + for abi in arm64-v8a armeabi-v7a; do + for lib_name in $required_libs; do + lib="$root/$abi/$lib_name" + if [ ! -f "$lib" ]; then + echo "Missing required native symbol library '$abi/$lib_name'." >&2 + exit 1 + fi + + if ! has_dwarf_debug_metadata "$lib"; then + echo "Native debug symbols unavailable: '$abi/$lib_name' has no .debug_info DWARF metadata." >&2 + echo "Refusing to create '$output' from stripped native libraries." >&2 + echo "Publish or consume native dependencies with full DWARF debug metadata before releasing." >&2 + exit 1 + fi + done + done +} + +extract_archive_lib() { + archive="$1" + tmp_dir="$2" + abi="$3" + lib_name="$4" + + entry="$abi/$lib_name" + if unzip -q "$archive" "$entry" -d "$tmp_dir" 2>/dev/null; then + return + fi + + for suffix in $archive_symbol_suffixes; do + entry="$abi/$lib_name$suffix" + if unzip -q "$archive" "$entry" -d "$tmp_dir" 2>/dev/null; then + mv "$tmp_dir/$entry" "$tmp_dir/$abi/$lib_name" + return + fi + done + + echo "Native debug symbols archive is missing '$abi/$lib_name' or accepted AGP variants '$abi/$lib_name.dbg' / '$abi/$lib_name.sym'." >&2 + exit 1 +} + +copy_archive_symbols() { + archive="$1" + tmp_dir="$2" + + for abi in arm64-v8a armeabi-v7a; do + mkdir -p "$tmp_dir/$abi" + for lib_name in $required_libs; do + copied=false + entry="$abi/$lib_name" + if copy_archive_entry "$archive" "$tmp_dir" "$abi" "$lib_name" "$entry"; then + copied=true + fi + + if [ "$copied" = false ]; then + for suffix in $archive_symbol_suffixes; do + entry="$abi/$lib_name$suffix" + if copy_archive_entry "$archive" "$tmp_dir" "$abi" "$lib_name" "$entry"; then + copied=true + break + fi + done + fi + done + done +} + +copy_archive_entry() { + archive="$1" + tmp_dir="$2" + abi="$3" + lib_name="$4" + entry="$5" + output_lib="$tmp_dir/$abi/$lib_name" + + if ! unzip -Z -1 "$archive" "$entry" >/dev/null 2>&1; then + return 1 + fi + + if [ -f "$output_lib" ]; then + echo "Duplicate native debug symbol entry '$abi/$lib_name' found while reading '$archive'." >&2 + echo "Refusing to overwrite symbol metadata from an earlier archive." >&2 + exit 1 + fi + + unzip -q "$archive" "$entry" -d "$tmp_dir" + if [ "$entry" != "$abi/$lib_name" ]; then + mv "$tmp_dir/$entry" "$output_lib" + fi +} + +validate_output_zip() { + archive="$1" + zip -T "$archive" >/dev/null + + tmp_dir=$(make_tmp_dir) + for abi in arm64-v8a armeabi-v7a; do + for lib_name in $required_libs; do + extract_archive_lib "$archive" "$tmp_dir" "$abi" "$lib_name" + done + done + + validate_symbol_tree "$tmp_dir" +} + +create_output_zip_from_tree() { + root="$1" + + validate_symbol_tree "$root" + + mkdir -p "$output_dir" + rm -f "$output_dir"/native-debug-symbols*.zip + + ( + cd "$root" + zip -qr "$repo_root/$output" arm64-v8a armeabi-v7a + ) + + zip -T "$output" >/dev/null + echo "Native debug symbols: $output" + ls -lh "$output" +} + +if [ -d "$dependency_symbols_dir" ]; then + tmp_dir=$(make_tmp_dir) + found_archive=false + + for archive in "$dependency_symbols_dir"/*.zip; do + if [ ! -f "$archive" ]; then + continue + fi + + found_archive=true + copy_archive_symbols "$archive" "$tmp_dir" + done + + if [ "$found_archive" = false ]; then + echo "No native debug symbol archives found in '$dependency_symbols_dir'." >&2 + echo "Run './gradlew :app:syncNativeDebugSymbolArtifacts' before creating release symbols." >&2 + exit 1 + fi + + create_output_zip_from_tree "$tmp_dir" + exit 0 +fi + +if [ -f "$output" ]; then + validate_output_zip "$output" + echo "Native debug symbols: $output" + ls -lh "$output" + exit 0 +fi + +native_lib_dir="" +for candidate in "app/build/intermediates/merged_native_libs/$variant"/*/out/lib; do + if [ -d "$candidate" ]; then + native_lib_dir="$candidate" + break + fi +done + +if [ -z "$native_lib_dir" ]; then + echo "No merged native libraries found for '$variant'." >&2 + exit 1 +fi + +tmp_dir=$(make_tmp_dir) + +for abi in arm64-v8a armeabi-v7a; do + source_dir="$native_lib_dir/$abi" + if [ ! -d "$source_dir" ]; then + echo "Missing native libraries for '$abi' in '$native_lib_dir'." >&2 + exit 1 + fi + + mkdir -p "$tmp_dir/$abi" + found_lib=false + for lib in "$source_dir"/*.so; do + if [ -f "$lib" ]; then + cp "$lib" "$tmp_dir/$abi/" + found_lib=true + fi + done + + if [ "$found_lib" = false ]; then + echo "No native libraries found for '$abi' in '$source_dir'." >&2 + exit 1 + fi +done + +create_output_zip_from_tree "$tmp_dir"