ci(android): fix split build, produce universal APK in the same Gradle run
Build Android APK / Build Release APKs (push) Successful in 14m40s
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:
@@ -14,6 +14,12 @@ on:
|
|||||||
- master
|
- master
|
||||||
workflow_dispatch:
|
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:
|
env:
|
||||||
# Keep in sync with .github/workflows/build-ios.yaml and pubspec.yaml (environment).
|
# Keep in sync with .github/workflows/build-ios.yaml and pubspec.yaml (environment).
|
||||||
FLUTTER_VERSION: '3.47.4'
|
FLUTTER_VERSION: '3.47.4'
|
||||||
@@ -25,7 +31,7 @@ jobs:
|
|||||||
build-android:
|
build-android:
|
||||||
name: Build Release APKs
|
name: Build Release APKs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 180
|
timeout-minutes: 60
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
container:
|
container:
|
||||||
@@ -119,8 +125,8 @@ jobs:
|
|||||||
# the NDK Flutter expects up front and swap in Ubuntu's native arm64 llvm-strip.
|
# 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")"
|
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; }
|
[ -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 ..."
|
echo "Installing NDK $NDK_VERSION ..."
|
||||||
yes | sdkmanager --install "platforms;android-34" "platforms;android-35" "ndk;${NDK_VERSION}" > /dev/null
|
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"
|
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
|
for tool in llvm-strip llvm-objcopy; do
|
||||||
mv "$NDK_BIN/$tool" "$NDK_BIN/$tool.x86_64"
|
mv "$NDK_BIN/$tool" "$NDK_BIN/$tool.x86_64"
|
||||||
@@ -163,68 +169,23 @@ jobs:
|
|||||||
flutter analyze
|
flutter analyze
|
||||||
flutter test
|
flutter test
|
||||||
|
|
||||||
- name: Build Release APKs (Split-per-ABI & Universal)
|
- name: Build Release APKs (Split-per-ABI + Universal)
|
||||||
run: |
|
# android/app/build.gradle re-enables the universal APK for split builds, so
|
||||||
flutter build apk --release --split-per-abi
|
# this one Gradle run yields all four APKs in build/app/outputs/flutter-apk/.
|
||||||
mkdir -p release-apks
|
run: flutter build apk --release --split-per-abi
|
||||||
# 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: Organize Artifacts
|
- name: Organize Artifacts
|
||||||
run: |
|
run: |
|
||||||
mkdir -p release-apks
|
mkdir -p release-apks
|
||||||
# Copy universal APK if not already in release-apks
|
APK_DIR=build/app/outputs/flutter-apk
|
||||||
if [ ! -f release-apks/oggauer-gemeindetrommler-universal.apk ]; then
|
echo "=== Flutter APK outputs ==="
|
||||||
for f in \
|
ls -lah "$APK_DIR"
|
||||||
build/app/outputs/flutter-apk/app-universal-release.apk \
|
cp "$APK_DIR/app-release.apk" release-apks/oggauer-gemeindetrommler-universal.apk
|
||||||
build/app/outputs/flutter-apk/*universal*.apk \
|
cp "$APK_DIR/app-arm64-v8a-release.apk" release-apks/oggauer-gemeindetrommler-arm64-v8a.apk
|
||||||
android/app/build/outputs/apk/release/app-universal-release.apk \
|
cp "$APK_DIR/app-armeabi-v7a-release.apk" release-apks/oggauer-gemeindetrommler-armeabi-v7a.apk
|
||||||
android/app/build/outputs/apk/release/*universal*.apk \
|
cp "$APK_DIR/app-x86_64-release.apk" release-apks/oggauer-gemeindetrommler-x86_64.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
|
|
||||||
echo "=== Generated APK Artifacts ==="
|
echo "=== Generated APK Artifacts ==="
|
||||||
ls -lah release-apks/
|
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
|
- name: Upload Universal APK
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
|
|||||||
@@ -34,23 +34,26 @@ android {
|
|||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
// 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
|
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 {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
enable true
|
|
||||||
reset()
|
|
||||||
include "arm64-v8a", "armeabi-v7a", "x86_64"
|
|
||||||
universalApk true
|
universalApk true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
afterEvaluate {
|
|
||||||
tasks.named('assembleRelease').configure {
|
|
||||||
dependsOn 'packageReleaseUniversalApk'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user