Update for Oct, 2024
Following Gabriel's neat answer, I wanted to automate this process as well. However, that solution wasn't working for me. After two days of trial & error, I realized that that solution had in my case been silently failing (i.e. no errors logged) because the suggested from and destinationDirectory paths were different in my (Flutter) project. So if you are facing similar problems, the following is an expanded solution that adds some checks & logs, allowing you to easier isolate and address any issues specific to your setup now or in the future.
My project setup
Note: mine is a Flutter project but the below solution should be similar in other projects.
A) Update your android/app/build.gradle file
Note: to learn about "tasks" in Gradle, check out the official docs. Also, in the below solution, as suggested by one of the comments in one of the other answers, I am checking for (and removing) some hidden files that are generated by macOS, in case you are on a Mac. That step seems redundant though when the zip is generated programmatically like I do in my solution, but I left it in as a precaution.
Add import java.nio.file.Paths to the top of the android/app/build.gradle file. Then add everything else after the flutter {...} block here:
import java.nio.file.Paths plugins { ... } android { ... } flutter { source = "../.." } // STEP 1: Create and register the `zipNativeDebugSymbols` task to zip debug symbol files for manual upload to the Google Play store. def zipNativeDebugSymbols = tasks.register('zipNativeDebugSymbols', Zip) { // Optional: this sets some info about the task (to verify, run `./gradlew app:tasks` in the /android folder to see this task listed) group = 'Build' description = 'Zips debug symbol files for upload to Google Play store.' // Set the input source directory (this is where the debug symbol files should be after the `bundleRelease` process has finished) def libDir = file('../../build/app/intermediates/merged_native_libs/release/out/lib') from libDir // Include all subfiles and directories include '**/*' // Set the name for the output zip file archiveFileName = 'native-debug-symbols.zip' // Set the destination directory for the output zip file def destDir = file('../../build/app/outputs/bundle/release') destinationDirectory = destDir doFirst { // Ensure the paths are correct for the required directories and that they indeed were created / exist (the preceding task `bundleRelease` creates these directories) checkDirectoryExists(libDir, 'Library directory') checkDirectoryExists(destDir, 'Destination directory') } doLast { println '✅ zipNativeDebugSymbols: created native-debug-symbols.zip file' // Optional: if running on macOS, clean up unwanted files like '__MACOSX' and '.DS_Store' in the now created zip file // The '__MACOSX' and '.DS_Store' files seem to be added only when manually creating ZIPs on macOS, not programmatically like here. Nevertheless, no harm in leaving this in as a precaution. if (System.properties['os.name'].toLowerCase().contains('mac')) { println ' running on Mac...' // Combine destination path and zip file name def zipPath = Paths.get(destinationDirectory.get().asFile.path, archiveFileName.get()).toString() // Ensure the zip file exists if (new File(zipPath).exists()) { println " removing any '__MACOSX' and '.DS_Store' files from the zip..." checkAndRemoveUnwantedFiles(zipPath, '__MACOSX*') checkAndRemoveUnwantedFiles(zipPath, '*.DS_Store') } else { println '❌ zip file does not exist: $zipPath' } } println '✅ zipNativeDebugSymbols: finished creating & cleaning native-debug-symbols.zip' } // Optional: force the task to run even if considered up-to-date outputs.upToDateWhen { false } println '✅ zipNativeDebugSymbols: task registered and configured' } // STEP 2: Configure the `zipNativeDebugSymbols` task to run after `bundleRelease` tasks.whenTaskAdded { task -> if (task.name == 'bundleRelease') { // `finalizedBy` ensures `zipNativeDebugSymbols` runs after `bundleRelease` is complete task.finalizedBy zipNativeDebugSymbols } } //// ----------- HELPER METHODS -------------- //// // Helper method to check if a directory exists def checkDirectoryExists(File dir, String description) { if (dir.exists()) { println '✅ zipNativeDebugSymbols: found ${description} ${dir}' } else { println '❌ ${description} does not exist: ${dir}' } } // Helper method to check for unwanted files and remove them def checkAndRemoveUnwantedFiles(String zipPath, String pattern) { def output = new ByteArrayOutputStream() exec { commandLine 'sh', '-c', "zipinfo $zipPath | grep '$pattern'" standardOutput = output errorOutput = new ByteArrayOutputStream() ignoreExitValue = true } if (output.toString().trim()) { println "✅ zipNativeDebugSymbols: found '$pattern' in the zip. Removing it..." exec { commandLine 'sh', '-c', "zip -d $zipPath '$pattern' || true" } } }
B) Generate files and upload to Google Play store
1. Generate the files for upload:
- From the root of your project, run
flutter clean && flutter build appbundle. - Locate the folder that contains the generated files to upload, which is the folder you have defined in
def destDir = .... In my case, it is in [project_name]/build/app/outputs/bundle/release. You should have two files in there: app-release.aab and native-debug-symbols.zip.
2. Upload to Google Play store:
- Upload your app bundle (
app-release.aab) to Google Play store. - Next, upload your debug symbols zip file (
native-debug-symbols.zip) as well. If you don't know how, follow the instructions here in the section Step 2: Upload a deobfuscation or symbolication file.
Voila! That is it. Hope this saves you days of scratching your head like I did.
Helpful debugging info
In case you want to play around a little to understand Gradle and the above tasks better, here are some useful CLI commands. Note that they have to be run from your android/ directory. Also, for a Flutter project the base command here is ./gradlew but I'm guessing it is e.g. just gradlew for other projects.
./gradlew --help displays all available commands ./gradlew --version displays what Gradle version you are using, i.e. what you have set in your android/gradle/wrapper/gradle-wrapper.properties > distributionUrl=.... ./gradlew tasks lists all available Gradle tasks for the project (android/build.gradle), showing which tasks can be executed. ./gradlew app:tasks lists all available Gradle tasks specifically for the app module (android/app/build.gradle). ./gradlew clean cleans the build directory by removing all generated files, forcing a fresh build the next time tasks are run. Note: flutter clean achieves this as well. ./gradlew bundleRelease builds the Android App Bundle (aab) in release mode, which is the app bundle that you upload to Google Play. ./gradlew help --task bundleRelease Displays detailed information about the bundleRelease task, including its description, inputs, outputs, and other task properties. ./gradlew zipNativeDebugSymbols --rerun-tasks --quiet runs the given task, e.g. the zipNativeDebugSymbols here, forcing it to rerun all tasks without showing any output unless there are errors. ./gradlew zipNativeDebugSymbols --rerun-tasks --info runs the given task, e.g. the zipNativeDebugSymbols here, forcing it to rerun all tasks and displaying detailed information about the build process. ./gradlew --scan creates a build scan with detailed performance and diagnostic data.