From e8556839b6c78a7ceede441f8deba6ba2d2dca6b Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 14 Sep 2026 00:03:12 +0200 Subject: [PATCH] fix(ci): direct AAPT2 userspace wrapping with QEMU to bypass host binfmt and FEX --- .gitea/workflows/build-apk.yaml | 55 ++++++++++++++++++++++++++++++-- .github/workflows/build-apk.yaml | 55 ++++++++++++++++++++++++++++++-- android/build.gradle | 49 ++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/build-apk.yaml b/.gitea/workflows/build-apk.yaml index 2d60e3f..62980b6 100644 --- a/.gitea/workflows/build-apk.yaml +++ b/.gitea/workflows/build-apk.yaml @@ -117,10 +117,61 @@ jobs: flutter test - name: Build Universal Release APK - run: flutter build apk --release + run: | + ARCH=$(uname -m) + if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then + ( + while true; do + for aapt2_bin in $(find /root/.gradle /opt/android-sdk-linux -type f -name "aapt2" 2>/dev/null); do + if [ -f "$aapt2_bin" ] && ! [ -f "${aapt2_bin}.orig" ]; then + if head -c 4 "$aapt2_bin" 2>/dev/null | grep -q 'ELF'; then + echo "Wrapping newly extracted AAPT2: $aapt2_bin" + mv "$aapt2_bin" "${aapt2_bin}.orig" + printf '#!/bin/sh\nexec /usr/bin/qemu-x86_64-static -L /usr/x86_64-linux-gnu "$0.orig" "$@"\n' > "$aapt2_bin" + chmod +x "$aapt2_bin" "${aapt2_bin}.orig" + fi + fi + done + sleep 0.5 + done + ) & + AAPT2_WATCHER_PID=$! + echo "AAPT2 Watcher started with PID $AAPT2_WATCHER_PID" + fi + + flutter build apk --release + + if [ -n "$AAPT2_WATCHER_PID" ]; then + kill "$AAPT2_WATCHER_PID" 2>/dev/null || true + fi - name: Build Split-per-ABI Release APKs - run: flutter build apk --release --split-per-abi + run: | + ARCH=$(uname -m) + if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then + ( + while true; do + for aapt2_bin in $(find /root/.gradle /opt/android-sdk-linux -type f -name "aapt2" 2>/dev/null); do + if [ -f "$aapt2_bin" ] && ! [ -f "${aapt2_bin}.orig" ]; then + if head -c 4 "$aapt2_bin" 2>/dev/null | grep -q 'ELF'; then + echo "Wrapping newly extracted AAPT2: $aapt2_bin" + mv "$aapt2_bin" "${aapt2_bin}.orig" + printf '#!/bin/sh\nexec /usr/bin/qemu-x86_64-static -L /usr/x86_64-linux-gnu "$0.orig" "$@"\n' > "$aapt2_bin" + chmod +x "$aapt2_bin" "${aapt2_bin}.orig" + fi + fi + done + sleep 0.5 + done + ) & + AAPT2_WATCHER_PID=$! + fi + + flutter build apk --release --split-per-abi + + if [ -n "$AAPT2_WATCHER_PID" ]; then + kill "$AAPT2_WATCHER_PID" 2>/dev/null || true + fi - name: Organize Artifacts run: | diff --git a/.github/workflows/build-apk.yaml b/.github/workflows/build-apk.yaml index 2d60e3f..62980b6 100644 --- a/.github/workflows/build-apk.yaml +++ b/.github/workflows/build-apk.yaml @@ -117,10 +117,61 @@ jobs: flutter test - name: Build Universal Release APK - run: flutter build apk --release + run: | + ARCH=$(uname -m) + if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then + ( + while true; do + for aapt2_bin in $(find /root/.gradle /opt/android-sdk-linux -type f -name "aapt2" 2>/dev/null); do + if [ -f "$aapt2_bin" ] && ! [ -f "${aapt2_bin}.orig" ]; then + if head -c 4 "$aapt2_bin" 2>/dev/null | grep -q 'ELF'; then + echo "Wrapping newly extracted AAPT2: $aapt2_bin" + mv "$aapt2_bin" "${aapt2_bin}.orig" + printf '#!/bin/sh\nexec /usr/bin/qemu-x86_64-static -L /usr/x86_64-linux-gnu "$0.orig" "$@"\n' > "$aapt2_bin" + chmod +x "$aapt2_bin" "${aapt2_bin}.orig" + fi + fi + done + sleep 0.5 + done + ) & + AAPT2_WATCHER_PID=$! + echo "AAPT2 Watcher started with PID $AAPT2_WATCHER_PID" + fi + + flutter build apk --release + + if [ -n "$AAPT2_WATCHER_PID" ]; then + kill "$AAPT2_WATCHER_PID" 2>/dev/null || true + fi - name: Build Split-per-ABI Release APKs - run: flutter build apk --release --split-per-abi + run: | + ARCH=$(uname -m) + if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then + ( + while true; do + for aapt2_bin in $(find /root/.gradle /opt/android-sdk-linux -type f -name "aapt2" 2>/dev/null); do + if [ -f "$aapt2_bin" ] && ! [ -f "${aapt2_bin}.orig" ]; then + if head -c 4 "$aapt2_bin" 2>/dev/null | grep -q 'ELF'; then + echo "Wrapping newly extracted AAPT2: $aapt2_bin" + mv "$aapt2_bin" "${aapt2_bin}.orig" + printf '#!/bin/sh\nexec /usr/bin/qemu-x86_64-static -L /usr/x86_64-linux-gnu "$0.orig" "$@"\n' > "$aapt2_bin" + chmod +x "$aapt2_bin" "${aapt2_bin}.orig" + fi + fi + done + sleep 0.5 + done + ) & + AAPT2_WATCHER_PID=$! + fi + + flutter build apk --release --split-per-abi + + if [ -n "$AAPT2_WATCHER_PID" ]; then + kill "$AAPT2_WATCHER_PID" 2>/dev/null || true + fi - name: Organize Artifacts run: | diff --git a/android/build.gradle b/android/build.gradle index b10bc3a..b1da431 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -43,3 +43,52 @@ subprojects { tasks.register("clean", Delete) { delete rootProject.buildDir } + +// Auto-wrap x86_64 AAPT2 on ARM64 Linux hosts (e.g. CI runner) to run seamlessly via QEMU +gradle.taskGraph.whenReady { taskGraph -> + String os = System.getProperty("os.name", "").toLowerCase() + String arch = System.getProperty("os.arch", "").toLowerCase() + if (os.contains("linux") && (arch.contains("aarch64") || arch.contains("arm64"))) { + def qemu = new File("/usr/bin/qemu-x86_64-static").exists() ? "/usr/bin/qemu-x86_64-static" : "qemu-x86_64-static" + def wrapAapt2 = { + def searchDirs = [] + def gh = gradle.gradleUserHomeDir + if (gh != null && gh.exists()) { + def transforms = new File(gh, "caches") + if (transforms.exists()) searchDirs.add(transforms) + } + def sdk = new File("/opt/android-sdk-linux/build-tools") + if (sdk.exists()) searchDirs.add(sdk) + def rootGradle = new File("/root/.gradle/caches") + if (rootGradle.exists()) searchDirs.add(rootGradle) + + searchDirs.each { dir -> + dir.eachFileRecurse(groovy.io.FileType.FILES) { file -> + if (file.name == "aapt2" && !file.name.endsWith(".orig")) { + try { + byte[] header = new byte[4] + file.withInputStream { is -> is.read(header) } + if (header[0] == 0x7F && header[1] == 0x45 && header[2] == 0x4C && header[3] == 0x46) { + def orig = new File(file.parentFile, "aapt2.orig") + if (!orig.exists()) { + file.renameTo(orig) + orig.setExecutable(true) + } + file.text = "#!/bin/sh\nexec ${qemu} -L /usr/x86_64-linux-gnu \"\$(dirname \"\$0\")/aapt2.orig\" \"\$@\"\n" + file.setExecutable(true) + println "[ARM64 CI] Wrapped AAPT2 at ${file.absolutePath} with QEMU" + } + } catch (Exception ignored) {} + } + } + } + } + taskGraph.allTasks.findAll { + it.name.contains("Resource") || it.name.contains("Aapt") || it.name.contains("Package") || it.name.contains("Bundle") + }.each { task -> + task.doFirst { + wrapAapt2() + } + } + } +}