Native Debug Symbols for Android App Bundles
Problem Overview
When uploading Android App Bundles to Google Play Console, developers often encounter this warning:
"This App Bundle contains native code, and you've not uploaded debug symbols. We recommend you upload a symbol file to make your crashes and ANRs easier to analyze and debug."
This issue occurs because your app contains native code (C/C++ through Flutter, NDK, or other native libraries), but you haven't provided the debug symbols needed to interpret crash reports. While the warning doesn't prevent app publication, resolving it improves your ability to diagnose and fix native crashes.
Manual Solution: Creating Debug Symbols ZIP
The most straightforward approach is to manually create and upload debug symbols:
Step 1: Locate Native Libraries
Navigate to your project's build directory. The path varies depending on your environment:
[YOUR_PROJECT]/build/app/intermediates/merged_native_libs/release/out/lib/
Alternatively, some projects may have this structure:
[YOUR_PROJECT]/app/build/intermediates/merged_native_libs/release/mergeReleaseNativeLibs/out/lib/
Step 2: Create ZIP File
Inside this directory, you'll find architecture-specific folders (typically arm64-v8a
, armeabi-v7a
, x86
, x86_64
):
- Select all architecture folders (do not compress the entire
lib
folder) - Create a ZIP file containing just these folders
- Name the file appropriately (e.g.,
native-debug-symbols.zip
)
Step 3: Upload to Play Console
- Upload your App Bundle (
app-release.aab
) as usual - In the release management section, find the "Debug symbols" option
- Upload your ZIP file
macOS Users
If using macOS, remove hidden system files before uploading:
zip -d your-symbols.zip "__MACOSX*"
zip -d your-symbols.zip "*.DS_Store"
Otherwise, you'll encounter the error: "The native debug symbols contain an invalid directory __MACOSX"
Automated Solutions
For regular releases, automate debug symbol generation with Gradle tasks.
Basic Automation (Groovy DSL)
Add to android/app/build.gradle
:
tasks.register('zipNativeDebugSymbols', Zip) {
from 'build/intermediates/merged_native_libs/release/mergeReleaseNativeLibs/out/lib'
exclude 'armeabi*' // Play Console may reject these architectures
exclude 'mips' // Play Console may reject these architectures
archiveFileName = 'native-debug-symbols.zip'
destinationDirectory = file('build/outputs/bundle/release')
}
tasks.configureEach { task ->
if (task.name == 'bundleRelease') {
task.finalizedBy zipNativeDebugSymbols
}
}
Kotlin DSL Alternative
For build.gradle.kts
files:
tasks.register<Zip>("zipNativeDebugSymbols") {
from("build/intermediates/merged_native_libs/release/mergeReleaseNativeLibs/out/lib")
exclude("armeabi*")
exclude("mips")
archiveFileName.set("native-debug-symbols.zip")
destinationDirectory.set(file("release"))
}
afterEvaluate {
tasks.named("bundleRelease") {
finalizedBy("zipNativeDebugSymbols")
}
}
Comprehensive Automation with Error Handling
For production environments, use this robust solution with detailed logging:
import java.nio.file.Paths
// Add this after the flutter {} block in android/app/build.gradle
def zipNativeDebugSymbols = tasks.register('zipNativeDebugSymbols', Zip) {
group = 'Build'
description = 'Zips debug symbol files for upload to Google Play store.'
def libDir = file('../../build/app/intermediates/merged_native_libs/release/out/lib')
from libDir
include '**/*'
archiveFileName = 'native-debug-symbols.zip'
def destDir = file('../../build/app/outputs/bundle/release')
destinationDirectory = destDir
doFirst {
checkDirectoryExists(libDir, 'Library directory')
checkDirectoryExists(destDir, 'Destination directory')
}
doLast {
println '✅ zipNativeDebugSymbols: created native-debug-symbols.zip file'
if (System.properties['os.name'].toLowerCase().contains('mac')) {
def zipPath = Paths.get(destinationDirectory.get().asFile.path, archiveFileName.get()).toString()
if (new File(zipPath).exists()) {
println "Removing any '__MACOSX' and '.DS_Store' files from the zip..."
checkAndRemoveUnwantedFiles(zipPath, '__MACOSX*')
checkAndRemoveUnwantedFiles(zipPath, '*.DS_Store')
}
}
}
outputs.upToDateWhen { false }
}
tasks.whenTaskAdded { task ->
if (task.name == 'bundleRelease') {
task.finalizedBy zipNativeDebugSymbols
}
}
// Helper methods
def checkDirectoryExists(File dir, String description) {
if (dir.exists()) {
println "✅ $description exists: ${dir}"
} else {
println "❌ $description does not exist: ${dir}"
}
}
def checkAndRemoveUnwantedFiles(String zipPath, String pattern) {
def output = new ByteArrayOutputStream()
exec {
commandLine 'sh', '-c', "zipinfo $zipPath | grep '$pattern'"
standardOutput = output
ignoreExitValue = true
}
if (output.toString().trim()) {
exec {
commandLine 'sh', '-c', "zip -d $zipPath '$pattern' || true"
}
}
}
Firebase Crashlytics Integration
If using Firebase Crashlytics for error reporting, configure automatic symbol upload:
android {
buildTypes {
release {
firebaseCrashlytics {
nativeSymbolUploadEnabled true
unstrippedNativeLibsDir file("build/app/intermediates/merged_native_libs/release/out/lib")
}
ndk {
debugSymbolLevel 'SYMBOL_TABLE' // Use 'FULL' for more detail (larger size)
}
}
}
}
// Optional: Automate symbol upload for all build types
tasks.whenTaskAdded { task ->
if (task.name.startsWith('assemble') && task.name != "assembleReleaseAndroidTest" &&
task.name != "assembleDebugAndroidTest") {
String taskName = "uploadCrashlyticsSymbolFile" + task.name.substring('assemble'.length())
task.finalizedBy taskName
}
}
Prerequisite Setup
For any automated solution to work, ensure proper environment setup:
Install Required Components
- Android Studio 4.1+ with Gradle 4.1+
- NDK (Side by Side) via SDK Manager
- CMake via SDK Manager (if using native code)
Configure NDK Path
In android/local.properties
, add your NDK path:
ndk.dir=/Users/yourusername/Library/Android/sdk/ndk/21.1.6352462
sdk.dir=/Users/yourusername/Library/Android/sdk
Gradle Configuration
In android/app/build.gradle
, ensure proper NDK configuration:
android {
compileSdkVersion 30
defaultConfig {
ndkVersion "23.1.7779620" // Match your installed NDK version
ndk {
debugSymbolLevel 'FULL' // Or 'SYMBOL_TABLE' for smaller size
}
}
}
Troubleshooting
Common Issues
"Could not get unknown property 'android'" error: This occurs when placing configuration in the wrong section of build.gradle
Missing build directories: Run
flutter clean && flutter build appbundle
to generate required directoriesArchitecture conflicts: Exclude deprecated architectures (armeabi, mips) as Play Console may reject them
Verification Commands
Check your environment with these terminal commands (run from android/
directory):
./gradlew --version # Check Gradle version
./gradlew tasks # List available tasks
./gradlew app:tasks # List app-specific tasks
./gradlew bundleRelease --info # Build with detailed info
Conclusion
Resolving the native debug symbols warning enhances your ability to debug native crashes in production. For occasional releases, manual ZIP creation suffices. For continuous deployment, implement automated solutions that integrate with your build process.
Remember that while this warning doesn't block app publication, addressing it significantly improves your crash reporting capabilities, leading to more stable applications and better user experiences.
TIP
Always test your build process completely before relying on it for production releases. The exact paths and requirements may vary slightly between Gradle and Flutter versions.