Commit the FFmpeg binary so test runs stop depending on a rebuild
CI rebuilt FFmpeg on every cold cache, which made results ambiguous: a red run could mean the code was broken or that a forty-minute cross-compile of FFmpeg, x264, x265 and SVT-AV1 had hiccuped. Those are not the same signal, and only one of them is worth a developer's attention. The archive is now checked in under bin/, so a failing run points at code. It also removes roughly forty minutes from a cold run and lets a fresh clone build without a container toolchain. bin/README.md records provenance -- upstream tag, FFmpeg version, NDK, ABIs, SHA-256 and the full configure line read back out of the shipped libavutil -- so the binary is auditable rather than opaque. The recipe in tools/ffmpeg remains the authority: this archive is its output, and is also what satisfies the GPL corresponding-source obligation. The status check is now seven independent runners: one validating the archive, one for the JVM tests, and one per API level from 33 to 37. The FFmpeg job verifies rather than builds. It asserts native libraries are present for both ABIs and that every one is 16 KB aligned, which is a Play requirement that is easy to lose in a rebuild and expensive to discover at submission. Checking for file existence alone would not do: a Git LFS pointer checked out without LFS passes that and then surfaces as an obscure linker error much later. It is a separate job rather than a step in each emulator run so a bad archive reports once, clearly, instead of five confusing emulator failures. build.yml no longer builds FFmpeg either, and keeps only its post-merge and release duties. Two costs, deliberately accepted. The repository goes from about 1 MB to 35 MB, and every future rebuild adds another 35 MB blob to history permanently, so bin/README.md says to regenerate only when the FFmpeg version or the configure flags actually change. And F-Droid's scanner flags checked-in native libraries, so submitting there needs a scandelete entry for bin/ -- noted in bin/README.md, and nothing prevents a from-source build. Verified against the relocated archive: 66 unit tests, and 40 instrumented tests on an API 36 emulator, 0 failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,93 +4,70 @@ on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
# A newer push to the same PR makes the in-flight run obsolete. Emulator matrices are
|
||||
# expensive, so cancel rather than let them pile up.
|
||||
# A newer push to the same PR makes the in-flight run obsolete. An emulator matrix is
|
||||
# expensive, so cancel rather than let runs pile up.
|
||||
concurrency:
|
||||
group: status-check-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# Must match the coordinate app/build.gradle.kts loads from app/libs/.
|
||||
FFMPEG_AAR: ffmpeg-kit-next-8.1.1.aar
|
||||
|
||||
jobs:
|
||||
# ---------------------------------------------------------------------------
|
||||
# FFmpeg is not committed: the AAR is ~35 MB of native code, and F-Droid strips
|
||||
# checked-in binaries. Every other job needs it, because the app module compiles
|
||||
# against it and the instrumented tests exercise it for real.
|
||||
# Validates the committed FFmpeg archive. It does not build anything: the whole
|
||||
# point of checking the binary in is that a red run means broken code rather than
|
||||
# a cross-compile that hiccuped.
|
||||
#
|
||||
# Building it is a full cross-compile of FFmpeg, x264, x265 and SVT-AV1, so it is
|
||||
# cached on the contents of tools/ffmpeg. That directory pins the upstream tag and
|
||||
# the configure flags, which is exactly what determines the output.
|
||||
# Its own job so a bad archive reports once, clearly, instead of surfacing as five
|
||||
# confusing emulator failures. It takes seconds, so gating the matrix on it costs
|
||||
# almost nothing.
|
||||
# ---------------------------------------------------------------------------
|
||||
ffmpeg:
|
||||
name: FFmpeg AAR
|
||||
name: FFmpeg binary
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Restore cached AAR
|
||||
id: cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: tools/ffmpeg/out
|
||||
key: ffmpeg-aar-${{ hashFiles('tools/ffmpeg/Containerfile', 'tools/ffmpeg/build-ffmpeg.sh') }}
|
||||
|
||||
# The Nix store plus the build tree runs to several gigabytes, which does not fit
|
||||
# alongside the runner's preinstalled toolchains.
|
||||
- name: Free disk space
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
- name: Verify the committed archive
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/share/boost
|
||||
sudo docker image prune -af || true
|
||||
df -h /
|
||||
AAR=bin/ffmpeg-kit-next-8.1.1.aar
|
||||
test -f "$AAR" || { echo "::error::$AAR is missing"; exit 1; }
|
||||
|
||||
- name: Build the AAR
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
working-directory: tools/ffmpeg
|
||||
run: |
|
||||
mkdir -p out
|
||||
docker build -t ffmpeg-kit-builder:ci -f Containerfile .
|
||||
docker run --rm -v "$PWD/out":/work/out ffmpeg-kit-builder:ci full
|
||||
|
||||
- name: Check the AAR is present and plausibly complete
|
||||
run: |
|
||||
AAR=$(find tools/ffmpeg/out -name 'ffmpeg-kit-next*.aar' | head -1)
|
||||
test -n "$AAR" || { echo "::error::no AAR produced"; exit 1; }
|
||||
# A truncated or stub AAR would still satisfy `test -f`, so check it carries
|
||||
# native libraries for both ABIs before letting the matrix depend on it.
|
||||
# A truncated file, or a Git LFS pointer checked out without LFS, would pass
|
||||
# a file-exists check and then surface much later as a confusing linker
|
||||
# error. Assert the archive actually carries native libraries for both ABIs.
|
||||
for abi in arm64-v8a x86_64; do
|
||||
n=$(unzip -l "$AAR" | grep -c "jni/$abi/.*\.so$" || true)
|
||||
echo " $abi: $n shared libraries"
|
||||
test "$n" -gt 0 || { echo "::error::AAR has no $abi libraries"; exit 1; }
|
||||
done
|
||||
cp "$AAR" "${{ env.FFMPEG_AAR }}"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ffmpeg-aar
|
||||
path: ${{ env.FFMPEG_AAR }}
|
||||
retention-days: 1
|
||||
# 16 KB alignment is a Play requirement and is easy to lose in a rebuild,
|
||||
# so it is checked here rather than discovered at submission.
|
||||
unzip -q -o "$AAR" 'jni/*' -d /tmp/aarcheck
|
||||
bad=0
|
||||
for f in /tmp/aarcheck/jni/*/*.so; do
|
||||
align=$(readelf -lW "$f" | awk '$1=="LOAD"{print $NF}' | sort -u)
|
||||
if [ "$align" != "0x4000" ]; then
|
||||
echo "::error::$(basename "$f") is $align, not 16 KB aligned"; bad=1
|
||||
fi
|
||||
done
|
||||
test "$bad" -eq 0 || exit 1
|
||||
echo " all libraries are 16 KB aligned"
|
||||
|
||||
# Record what shipped, so a failing run elsewhere can be tied to a version.
|
||||
echo " sha256: $(sha256sum "$AAR" | cut -d' ' -f1)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# JVM tests: the routing matrix, the FFmpeg argument builder, the concat planner
|
||||
# and the retry rule. These need no device and are the fastest signal on a PR.
|
||||
# and the retry rule. No device needed, so this is the fastest signal on a PR.
|
||||
# ---------------------------------------------------------------------------
|
||||
unit:
|
||||
name: Unit tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: ffmpeg
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ffmpeg-aar
|
||||
path: app/libs/
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
@@ -98,7 +75,8 @@ jobs:
|
||||
|
||||
- uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- run: ./gradlew :app:testDebugUnitTest
|
||||
- name: Unit tests
|
||||
run: ./gradlew :app:testDebugUnitTest
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
@@ -107,18 +85,24 @@ jobs:
|
||||
path: app/build/reports/tests/
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Instrumented tests across the whole supported range. minSdk is 33 and targetSdk
|
||||
# is 37, and the foreground-service type differs across that range -- none below
|
||||
# 34, dataSync at 34, mediaProcessing from 35 -- so a single API level would leave
|
||||
# two thirds of that branch unexercised.
|
||||
# One runner per API level, across the whole supported range.
|
||||
#
|
||||
# The range is the point: minSdk is 33 and targetSdk is 37, and the foreground
|
||||
# service type differs across it -- none below 34, dataSync at 34, mediaProcessing
|
||||
# from 35. Testing a single level would leave two thirds of that branch unexercised,
|
||||
# and running the range locally is what caught a test that had baked in an
|
||||
# assumption about the host's encoders.
|
||||
#
|
||||
# FFmpeg is not built here. The AAR is committed under bin/, so a red run means the
|
||||
# code is broken rather than that a cross-compile hiccuped.
|
||||
# ---------------------------------------------------------------------------
|
||||
instrumented:
|
||||
e2e:
|
||||
name: E2E API ${{ matrix.api-level }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: ffmpeg
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
# Report every API level rather than stopping at the first red one: knowing
|
||||
# Report every API level rather than stopping at the first red one. Knowing
|
||||
# whether a failure is universal or specific to one level is most of the
|
||||
# diagnosis.
|
||||
fail-fast: false
|
||||
@@ -132,18 +116,13 @@ jobs:
|
||||
system-image-api-level: 35
|
||||
- api-level: 36
|
||||
system-image-api-level: 36
|
||||
# API 37 is published as android-37.0, not android-37, so the image level
|
||||
# has to be given separately or the download resolves to nothing.
|
||||
# API 37 is published as android-37.0, not android-37, so the image level has
|
||||
# to be given separately or the download resolves to no package at all.
|
||||
- api-level: 37
|
||||
system-image-api-level: "37.0"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ffmpeg-aar
|
||||
path: app/libs/
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
@@ -168,9 +147,9 @@ jobs:
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
profile: pixel_6
|
||||
# -gpu swiftshader_indirect is the usual CI choice. It is correct here only
|
||||
# because runners have no GPU to pass through; on a workstation the same
|
||||
# setting routes through SwiftShader's JIT, which is a known crash source.
|
||||
# swiftshader_indirect is correct here only because runners have no GPU to
|
||||
# pass through. On a workstation the same setting routes through
|
||||
# SwiftShader's JIT, which is a known crash source.
|
||||
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||
disable-animations: true
|
||||
script: ./gradlew :app:connectedDebugAndroidTest
|
||||
|
||||
Reference in New Issue
Block a user