From 1e99268588102d2b484d1646dd2e68e96348be0b Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Fri, 22 May 2026 12:40:17 +1000 Subject: [PATCH] ci: add static Android NDK build workflow Cross-compiles statically-linked rsync binaries with the Android NDK for arm64-v8a (all modern phones) and armeabi-v7a (older 32-bit devices), and uploads them as workflow artifacts for adb push / Termux use. The build is self-contained (optional external libraries disabled; keeps md5/md4 and the bundled zlib) and forces a few configure cache values that can't be probed when cross-compiling: lchmod()/lutimes() off (Bionic doesn't declare them until API 36 though the symbols link), and socketpair / mknod-FIFO / mknod-socket on (Android runs a Linux kernel, so these match the native result). IPv6 is enabled explicitly. Since the binaries are cross-compiled the test suite can't run; the job instead asserts each binary is static and the correct architecture, and smoke-tests `--version` under qemu-user. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/android-static-build.yml | 120 +++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 .github/workflows/android-static-build.yml diff --git a/.github/workflows/android-static-build.yml b/.github/workflows/android-static-build.yml new file mode 100644 index 000000000..d48544f81 --- /dev/null +++ b/.github/workflows/android-static-build.yml @@ -0,0 +1,120 @@ +name: Build static rsync for Android + +# Cross-compiles statically-linked rsync binaries with the Android NDK, +# suitable for dropping onto a phone (adb push / Termux) with no shared +# libraries. arm64-v8a covers all modern phones; armeabi-v7a covers older +# 32-bit devices. The binaries are uploaded as workflow artifacts. +# +# These are cross-compiled, so the test suite can't run here; we sanity +# check that each binary is the right architecture, is static, and that +# it executes (`--version`) under qemu-user. + +on: + push: + branches: [ master ] + paths-ignore: + - '.github/workflows/*.yml' + - '!.github/workflows/android-static-build.yml' + pull_request: + branches: [ master ] + paths-ignore: + - '.github/workflows/*.yml' + - '!.github/workflows/android-static-build.yml' + schedule: + - cron: '42 8 * * *' + workflow_dispatch: + +env: + # Minimum supported API level. 24 (Android 7.0) runs on every modern + # phone while keeping broad reach; bump if you need newer Bionic APIs. + ANDROID_API: 24 + +jobs: + build: + runs-on: ubuntu-latest + name: ${{ matrix.abi }} + strategy: + fail-fast: false + matrix: + include: + - abi: arm64-v8a # modern phones + triple: aarch64-linux-android + qemu: qemu-aarch64-static + - abi: armeabi-v7a # older 32-bit phones + triple: armv7a-linux-androideabi + qemu: qemu-arm-static + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install build prerequisites + run: sudo apt-get update && sudo apt-get install -y autoconf automake gawk qemu-user-static + + - name: Configure and build (${{ matrix.abi }}) + shell: bash + run: | + set -euo pipefail + NDK="${ANDROID_NDK_LATEST_HOME:-$ANDROID_NDK_ROOT}" + TC="$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin" + export CC="$TC/${{ matrix.triple }}${ANDROID_API}-clang" + export AR="$TC/llvm-ar" RANLIB="$TC/llvm-ranlib" STRIP="$TC/llvm-strip" + export CFLAGS="-O2" LDFLAGS="-static" + + # Bionic doesn't declare lchmod()/lutimes() until API 36, but the + # symbols link, so configure mis-detects them -- force them off so + # rsync uses its fallbacks. The other cache vars restore values + # that configure can't probe when cross-compiling (Android runs a + # normal Linux kernel, so these match the native Linux result). + export ac_cv_func_lchmod=no ac_cv_func_lutimes=no \ + rsync_cv_HAVE_SOCKETPAIR=yes \ + rsync_cv_MKNOD_CREATES_FIFOS=yes \ + rsync_cv_MKNOD_CREATES_SOCKETS=yes + + # Self-contained build: drop optional external libraries so the + # static binary needs nothing at runtime. rsync keeps md5/md4 + # checksums and its bundled zlib. + ./configure --host=${{ matrix.triple }} --build=x86_64-pc-linux-gnu \ + --enable-ipv6 \ + --disable-zstd --disable-lz4 --disable-xxhash --disable-openssl \ + --disable-iconv --disable-iconv-open \ + --disable-acl-support --disable-xattr-support \ + --disable-md2man --disable-roll-simd \ + --with-included-popt --with-included-zlib + + # Generate the awk-built headers serially first so the parallel + # build can't race on proto.h <- daemon-parm.h. + make proto.h + make -j"$(nproc)" rsync + "$STRIP" rsync + + - name: Verify binary + shell: bash + run: | + set -euo pipefail + file rsync + # Gate: must be a statically-linked executable (no interpreter). + file rsync | grep -q "statically linked" + if file rsync | grep -q "dynamically linked"; then + echo "ERROR: binary is not static" >&2; exit 1 + fi + # Best-effort: confirm it actually runs under qemu-user. + ${{ matrix.qemu }} ./rsync --version | head -3 || \ + echo "WARNING: qemu smoke test did not run cleanly (check on a real device)" + + - name: Package + shell: bash + run: | + set -euo pipefail + VER=$(sed -n 's/.*RSYNC_VERSION "\([^"]*\)".*/\1/p' version.h) + out="rsync-${VER}-android-${{ matrix.abi }}" + mkdir -p dist + cp rsync "dist/$out" + ( cd dist && sha256sum "$out" > "$out.sha256" ) + echo "ARTIFACT_NAME=rsync-android-${{ matrix.abi }}" >>"$GITHUB_ENV" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ env.ARTIFACT_NAME }} + path: dist/