Skip to content

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):

  1. Select all architecture folders (do not compress the entire lib folder)
  2. Create a ZIP file containing just these folders
  3. Name the file appropriately (e.g., native-debug-symbols.zip)

Step 3: Upload to Play Console

  1. Upload your App Bundle (app-release.aab) as usual
  2. In the release management section, find the "Debug symbols" option
  3. Upload your ZIP file

macOS Users

If using macOS, remove hidden system files before uploading:

bash
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:

groovy
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:

kotlin
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:

groovy
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:

groovy
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

  1. Android Studio 4.1+ with Gradle 4.1+
  2. NDK (Side by Side) via SDK Manager
  3. 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:

groovy
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

  1. "Could not get unknown property 'android'" error: This occurs when placing configuration in the wrong section of build.gradle

  2. Missing build directories: Run flutter clean && flutter build appbundle to generate required directories

  3. Architecture 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):

bash
./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.