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 been silently failing (i.e. no errors logged) in my case 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.
Note: mine is a Flutter project but the below solution should be similar in other projects.
v3.24.1Android Studio (didn't have to install the NDK & CMake tools)v8.5macOSandroid/app/build.gradle fileNote: 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 also checking for (and removing) some hidden files that are generated by macOS, in case you are on a Mac. That step seems redundant when the zip is generated programmatically as it is in this 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 symbols for upload to Google Play store.'
// Set the input source directory (this is where the native libs 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"
}
}
}
flutter clean && flutter build appbundle.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.app-release.aab) to Google Play store.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.
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.