ci(android): fix split build, produce universal APK in the same Gradle run
Build Android APK / Build Release APKs (push) Successful in 14m40s

The previous commit made `assembleRelease` depend on AGP's
`packageReleaseUniversalApk`. That task derives the universal APK from an
App Bundle, which pulls `buildReleasePreBundle` into the graph - and AGP
refuses to build a bundle while ABI splits are enabled
("Multiple shrunk-resources files found ... Please disable building
multiple APKs when building an Android app bundle", run 52).

Instead only flip `splits.abi.universalApk` back on when Flutter passes
`-Psplit-per-abi=true`: the Flutter Gradle plugin enables the splits and
copies every output - including the universal one, as app-release.apk -
to build/app/outputs/flutter-apk/. Plain builds (`flutter run`) keep the
plugin's abiFilters path, which AGP does not allow together with splits.

Workflow: one `flutter build apk --release --split-per-abi` call, copy the
four well-known output names (a missing one fails the step), job timeout
60 min (a green run takes ~17), drop the unneeded platform pre-install and
add a per-ref concurrency group - runs 50/51 ran in parallel on the single
ARM64 runner and both died at the same instant.

Verified locally with Flutter 3.47.4: split build yields all four APKs
(universal 62 MB, per-ABI 20-24 MB) in one Gradle run; plain release
build still produces app-release.apk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 23:30:13 +02:00
co-authored by Claude Opus 5
parent b7b6c4a2ab
commit a228392dc6
2 changed files with 35 additions and 71 deletions
+20 -59
View File
@@ -14,6 +14,12 @@ on:
- master
workflow_dispatch:
# Only one build per ref at a time: two QEMU-emulated Gradle builds in parallel
# take the ARM64 runner down (both runs then die at the same instant).
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
# Keep in sync with .github/workflows/build-ios.yaml and pubspec.yaml (environment).
FLUTTER_VERSION: '3.47.4'
@@ -25,7 +31,7 @@ jobs:
build-android:
name: Build Release APKs
runs-on: ubuntu-latest
timeout-minutes: 180
timeout-minutes: 60
permissions:
contents: write
container:
@@ -119,8 +125,8 @@ jobs:
# the NDK Flutter expects up front and swap in Ubuntu's native arm64 llvm-strip.
NDK_VERSION="$(sed -nE 's/.*ndkVersion: String = "([^"]+)".*/\1/p' "${FLUTTER_ROOT:-/sdks/flutter}/packages/flutter_tools/gradle/src/main/kotlin/FlutterExtension.kt")"
[ -n "$NDK_VERSION" ] || { echo "::error::Could not determine Flutter's NDK version"; exit 1; }
echo "Installing NDK $NDK_VERSION and Android SDK platforms 34 & 35 ..."
yes | sdkmanager --install "platforms;android-34" "platforms;android-35" "ndk;${NDK_VERSION}" > /dev/null
echo "Installing NDK $NDK_VERSION ..."
yes | sdkmanager --install "ndk;${NDK_VERSION}" > /dev/null
NDK_BIN="${ANDROID_HOME:-/opt/android-sdk-linux}/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/linux-x86_64/bin"
for tool in llvm-strip llvm-objcopy; do
mv "$NDK_BIN/$tool" "$NDK_BIN/$tool.x86_64"
@@ -163,68 +169,23 @@ jobs:
flutter analyze
flutter test
- name: Build Release APKs (Split-per-ABI & Universal)
run: |
flutter build apk --release --split-per-abi
mkdir -p release-apks
# Locate universal APK generated by AGP splits
for f in \
build/app/outputs/flutter-apk/app-universal-release.apk \
build/app/outputs/flutter-apk/*universal*.apk \
android/app/build/outputs/apk/release/app-universal-release.apk \
android/app/build/outputs/apk/release/*universal*.apk \
build/app/outputs/flutter-apk/app-release.apk \
android/app/build/outputs/apk/release/app-release.apk; do
if [ -f "$f" ]; then
echo "Found universal APK at $f"
cp "$f" release-apks/oggauer-gemeindetrommler-universal.apk
break
fi
done
# Fallback: if universal APK was not created by split build, build it explicitly
if [ ! -f release-apks/oggauer-gemeindetrommler-universal.apk ]; then
echo "Universal APK not found from split build; running fallback universal build..."
flutter build apk --release
if [ -f build/app/outputs/flutter-apk/app-release.apk ]; then
cp build/app/outputs/flutter-apk/app-release.apk release-apks/oggauer-gemeindetrommler-universal.apk
elif [ -f android/app/build/outputs/apk/release/app-release.apk ]; then
cp android/app/build/outputs/apk/release/app-release.apk release-apks/oggauer-gemeindetrommler-universal.apk
fi
fi
- name: Build Release APKs (Split-per-ABI + Universal)
# android/app/build.gradle re-enables the universal APK for split builds, so
# this one Gradle run yields all four APKs in build/app/outputs/flutter-apk/.
run: flutter build apk --release --split-per-abi
- name: Organize Artifacts
run: |
mkdir -p release-apks
# Copy universal APK if not already in release-apks
if [ ! -f release-apks/oggauer-gemeindetrommler-universal.apk ]; then
for f in \
build/app/outputs/flutter-apk/app-universal-release.apk \
build/app/outputs/flutter-apk/*universal*.apk \
android/app/build/outputs/apk/release/app-universal-release.apk \
android/app/build/outputs/apk/release/*universal*.apk \
build/app/outputs/flutter-apk/app-release.apk \
android/app/build/outputs/apk/release/app-release.apk; do
if [ -f "$f" ]; then
cp "$f" release-apks/oggauer-gemeindetrommler-universal.apk
break
fi
done
fi
# Copy split APKs
for apk in build/app/outputs/flutter-apk/*.apk android/app/build/outputs/apk/release/*.apk; do
[ -f "$apk" ] || continue
case "$(basename "$apk")" in
*arm64-v8a*) cp "$apk" release-apks/oggauer-gemeindetrommler-arm64-v8a.apk ;;
*armeabi-v7a*) cp "$apk" release-apks/oggauer-gemeindetrommler-armeabi-v7a.apk ;;
*x86_64*) cp "$apk" release-apks/oggauer-gemeindetrommler-x86_64.apk ;;
esac
done
APK_DIR=build/app/outputs/flutter-apk
echo "=== Flutter APK outputs ==="
ls -lah "$APK_DIR"
cp "$APK_DIR/app-release.apk" release-apks/oggauer-gemeindetrommler-universal.apk
cp "$APK_DIR/app-arm64-v8a-release.apk" release-apks/oggauer-gemeindetrommler-arm64-v8a.apk
cp "$APK_DIR/app-armeabi-v7a-release.apk" release-apks/oggauer-gemeindetrommler-armeabi-v7a.apk
cp "$APK_DIR/app-x86_64-release.apk" release-apks/oggauer-gemeindetrommler-x86_64.apk
echo "=== Generated APK Artifacts ==="
ls -lah release-apks/
if [ "$(ls release-apks/*.apk 2>/dev/null | wc -l)" -ne 4 ]; then
echo "::error::Expected 4 APKs in release-apks/, the build did not produce all artifacts"
exit 1
fi
- name: Upload Universal APK
uses: actions/upload-artifact@v3
+11 -8
View File
@@ -34,23 +34,26 @@ android {
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
// `flutter build apk --split-per-abi` makes the Flutter Gradle plugin enable ABI
// splits for the requested target platforms but switch the universal APK off.
// Turn it back on so a single Gradle run yields the per-ABI APKs *and* the
// universal one (the plugin copies all of them to build/app/outputs/flutter-apk/,
// the universal one as app-release.apk). Only when splitting: for plain builds
// the plugin sets ndk.abiFilters instead, which AGP does not allow together
// with splits. Do not use AGP's `packageReleaseUniversalApk` task for this -
// it derives the universal APK from an App Bundle, and bundles cannot be
// built while ABI splits are enabled.
if (project.findProperty("split-per-abi")?.toString()?.toBoolean()) {
splits {
abi {
enable true
reset()
include "arm64-v8a", "armeabi-v7a", "x86_64"
universalApk true
}
}
}
afterEvaluate {
tasks.named('assembleRelease').configure {
dependsOn 'packageReleaseUniversalApk'
}
}