mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
477 lines
25 KiB
YAML
477 lines
25 KiB
YAML
name: Build APK
|
|
run-name: ${{ github.event_name == 'pull_request_target' && format('building pr {0} on {1}', github.event.pull_request.number, github.event.pull_request.head.sha) || format('building {0} on {1}', github.ref_name, github.sha) }}
|
|
|
|
# One job: the two glibc preload libraries the session needs, then the app around them.
|
|
# Nothing is built on a developer machine - this workflow is the only thing that produces an apk.
|
|
#
|
|
# This repo is private, so every minute here is billed. Three caches carry the cost of a rebuild
|
|
# down to what actually changed:
|
|
# • Gradle's own (dependencies, wrapper, build cache, configuration cache) via setup-gradle
|
|
# • the CMake/ninja tree, so a Kotlin-only change does not recompile the compositor
|
|
# • the two cross-compiled preloads, which also skips installing the cross toolchain
|
|
# A run that changes only Kotlin should not touch the C compiler at all.
|
|
|
|
on:
|
|
push:
|
|
branches: [ main ]
|
|
# Documentation changes cannot alter the apk, so they do not buy a build.
|
|
paths-ignore:
|
|
- '**/*.md'
|
|
- 'LICENSE'
|
|
- '.gitignore'
|
|
# A pull request against main builds by itself: the contributor's branch, as it would be once
|
|
# merged onto main, comes back as an apk on the run without anyone here pressing a button.
|
|
# Every push to the pull request rebuilds it and cancels the build it replaces; a draft waits
|
|
# until it is marked ready. pull_request_target rather than pull_request: GitHub does not run
|
|
# the latter at all while a pull request conflicts with main, and saying what conflicts is the
|
|
# point. The workflow file is always main's; the pull request's own code is checked out below.
|
|
pull_request_target:
|
|
branches: [ main ]
|
|
types: [ opened, synchronize, reopened, ready_for_review ]
|
|
paths-ignore:
|
|
- '**/*.md'
|
|
- 'LICENSE'
|
|
- '.gitignore'
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
# The pull-request report below is one comment on the pull request, kept up to date.
|
|
pull-requests: write
|
|
|
|
# A newer push makes the running build pointless; stop paying for it.
|
|
concurrency:
|
|
group: build-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
# Bump to force the preload cache to miss (e.g. after changing a compiler flag below).
|
|
PRELOAD_CACHE_VERSION: v1
|
|
|
|
jobs:
|
|
build:
|
|
if: ${{ github.event_name != 'pull_request_target' || !github.event.pull_request.draft }}
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 45
|
|
steps:
|
|
# A pull request is checked out at its own head with full history, not at GitHub's merge
|
|
# ref: that ref does not exist while the pull request conflicts with main, and the point of
|
|
# the next step is to say so and show where.
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
|
# Full history: the merge below needs it (a shallow clone refuses to merge at all).
|
|
fetch-depth: 0
|
|
|
|
# The pull request as it would be once merged onto main is what gets built. When it cannot
|
|
# be merged, the conflicting files and the conflicting hunks go into the run summary and the
|
|
# pull-request comment, and the job stops here: there is nothing coherent to build.
|
|
- name: Merge onto main, or say what conflicts
|
|
if: github.event_name == 'pull_request_target'
|
|
run: |
|
|
set -uo pipefail
|
|
git config user.name "ci"
|
|
git config user.email "ci@users.noreply.github.com"
|
|
git fetch --no-tags origin main
|
|
BASE=$(git rev-parse --short origin/main)
|
|
HEAD=$(git rev-parse --short HEAD)
|
|
if git merge --no-ff --no-edit origin/main >/tmp/merge.out 2>&1; then
|
|
{
|
|
echo "### Mergeable"
|
|
echo
|
|
echo "\`$HEAD\` merges cleanly onto main (\`$BASE\`). What follows was built from the merged result."
|
|
} > /tmp/pr-merge.md
|
|
cat /tmp/pr-merge.md >> "$GITHUB_STEP_SUMMARY"
|
|
exit 0
|
|
fi
|
|
FILES=$(git diff --name-only --diff-filter=U)
|
|
if [ -z "$FILES" ]; then
|
|
# The merge did not even start (git said why): quote it rather than list nothing.
|
|
{
|
|
echo "### ERROR: Could not merge onto main (\`$BASE\`)"
|
|
echo
|
|
echo '```'
|
|
cat /tmp/merge.out
|
|
echo '```'
|
|
} > /tmp/pr-merge.md
|
|
cat /tmp/pr-merge.md >> "$GITHUB_STEP_SUMMARY"
|
|
echo "::error::git could not merge main: $(head -3 /tmp/merge.out | tr '\n' ' ')"
|
|
exit 1
|
|
fi
|
|
{
|
|
echo "### ERROR: Not mergeable - conflicts with main (\`$BASE\`)"
|
|
echo
|
|
echo "Files that conflict:"
|
|
echo
|
|
for f in $FILES; do echo "- \`$f\`"; done
|
|
echo
|
|
echo "Each conflicting hunk, as git leaves it: \`<<<<<<< HEAD\` is this pull request, \`>>>>>>> origin/main\` is main."
|
|
echo
|
|
for f in $FILES; do
|
|
echo "<details><summary><code>$f</code></summary>"
|
|
echo
|
|
echo '```'
|
|
awk '/^<<<<<<< /{p=1} p{print} /^>>>>>>> /{p=0}' "$f" | head -300
|
|
echo '```'
|
|
echo "</details>"
|
|
echo
|
|
done
|
|
echo "Not built: rebase or merge main into the branch and push again."
|
|
} > /tmp/pr-merge.md
|
|
cat /tmp/pr-merge.md >> "$GITHUB_STEP_SUMMARY"
|
|
git merge --abort || true
|
|
echo "::error::the pull request conflicts with main: $(echo $FILES | tr '\n' ' ')"
|
|
exit 1
|
|
|
|
- uses: actions/setup-java@v4
|
|
with:
|
|
distribution: temurin
|
|
java-version: '17'
|
|
|
|
# Caches ~/.gradle (dependencies, wrapper distribution, build cache, configuration cache)
|
|
# and prunes what the run did not use, so the entry cannot grow without bound.
|
|
- uses: gradle/actions/setup-gradle@v4
|
|
with:
|
|
cache-cleanup: on-success
|
|
|
|
- name: Resolve the runner's NDK
|
|
run: |
|
|
NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
|
|
VERSION=$(sed -n 's/^Pkg.Revision *= *//p' "$NDK/source.properties")
|
|
echo "NDK $VERSION at $NDK"
|
|
echo "NDK_VERSION=$VERSION" >> "$GITHUB_ENV"
|
|
|
|
- name: Set the APK build label
|
|
id: build-label
|
|
env:
|
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
BUILD_REF: ${{ github.ref_name }}
|
|
BUILD_SHA: ${{ github.sha }}
|
|
run: |
|
|
if [[ -n "$PR_NUMBER" ]]; then
|
|
label="pr${PR_NUMBER} at ${PR_HEAD_SHA:0:7}"
|
|
else
|
|
label="${BUILD_REF} at ${BUILD_SHA:0:7}"
|
|
fi
|
|
echo "label=$label" >> "$GITHUB_OUTPUT"
|
|
echo "Build: \`$label\`" >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
# ── The session's preloads ───────────────────────────────────────────────────────────
|
|
# Both are glibc aarch64 libraries that live inside the rootfs, so they are cross-compiled
|
|
# here rather than by the NDK. They ship in assets (not in the runtime tarball) so a fix in
|
|
# either reaches an already-installed runtime on the next launch instead of waiting on a
|
|
# ~790 MB runtime re-host. Their inputs change rarely, so most runs restore them.
|
|
- name: Cache the preloads
|
|
id: preloads
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: app/src/main/assets/linuxfs
|
|
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/mangoapp/*') }}
|
|
|
|
- name: Build libfakeinput.so and libblsession.so (glibc aarch64)
|
|
if: steps.preloads.outputs.cache-hit != 'true'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -y -qq gcc-aarch64-linux-gnu g++-aarch64-linux-gnu >/dev/null
|
|
D=app/src/main/assets/linuxfs
|
|
mkdir -p "$D"
|
|
# Statically linked C++ runtime: this is preloaded into every Steam process, and Steam
|
|
# ships its own libstdc++ while rewriting LD_LIBRARY_PATH. Pulling the system one in
|
|
# ahead of it is a version conflict waiting to happen. Only libc is left as a dependency.
|
|
aarch64-linux-gnu-g++ -shared -fPIC -O2 -Wall -Wno-attributes -Wno-nonnull-compare \
|
|
-pthread -std=c++17 -static-libstdc++ -static-libgcc \
|
|
-o "$D/libfakeinput.so" app/src/main/cpp/fakeinput_steam.cpp -ldl
|
|
aarch64-linux-gnu-strip --strip-unneeded "$D/libfakeinput.so"
|
|
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
|
|
-o "$D/libblsession.so" tools/linuxfs/preload/*.c -ldl
|
|
aarch64-linux-gnu-strip --strip-unneeded "$D/libblsession.so"
|
|
# The session's scripts travel the same way, for the same reason.
|
|
for script in tools/linuxfs/overlay/usr/local/bin/bannerlator-*; do
|
|
install -Dm644 "$script" "$D/usr/local/bin/$(basename "$script")"
|
|
done
|
|
# The SteamOS helpers the client calls in Deck mode, as no-op stubs (see each file).
|
|
for f in tools/linuxfs/overlay/usr/bin/* tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/*; do
|
|
[ -f "$f" ] && install -Dm644 "$f" "$D/${f#tools/linuxfs/overlay/}"
|
|
done
|
|
# The patched gamescope, when a build of it has been published (tools/gamescope/release.env
|
|
# names the release and the archive's sha256); see tools/gamescope/PATCHES.md.
|
|
if [ -f tools/gamescope/release.env ]; then
|
|
. tools/gamescope/release.env
|
|
# Through the API: the repository is private, so the plain download URL answers 404.
|
|
gh release download "${GAMESCOPE_TAG}" -R "${GITHUB_REPOSITORY}" -p gamescope.tzst -O gamescope.tzst
|
|
echo "${GAMESCOPE_SHA256} gamescope.tzst" | sha256sum -c -
|
|
tar --use-compress-program=unzstd -xf gamescope.tzst -C "$D"
|
|
test -f "$D/usr/local/bin/gamescope"
|
|
rm gamescope.tzst
|
|
fi
|
|
# The patched wlroots for the desktop's GPU renderers (tools/wlroots), the same way.
|
|
if [ -f tools/wlroots/release.env ]; then
|
|
. tools/wlroots/release.env
|
|
gh release download "${WLROOTS_TAG}" -R "${GITHUB_REPOSITORY}" -p wlroots.tzst -O wlroots.tzst
|
|
echo "${WLROOTS_SHA256} wlroots.tzst" | sha256sum -c -
|
|
tar --use-compress-program=unzstd -xf wlroots.tzst -C "$D"
|
|
test -f "$D/usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so"
|
|
rm wlroots.tzst
|
|
fi
|
|
# Valve's aarch64 mangoapp for Deck mode's performance overlay, plus the five libraries the
|
|
# runtime lacks, pinned by sha256 in tools/mangoapp/packages.txt. Stored under the SONAME
|
|
# each is loaded by, as real files: assets cannot carry symlinks.
|
|
M="$D/usr/local/lib/mangoapp"
|
|
mkdir -p "$M" mango-pkgs
|
|
grep -v '^#' tools/mangoapp/packages.txt | while read -r sum url; do
|
|
[ -n "$url" ] || continue
|
|
curl -fsSL --retry 3 -o mango-pkgs/pkg.tar.zst "$url"
|
|
echo "$sum mango-pkgs/pkg.tar.zst" | sha256sum -c -
|
|
tar --use-compress-program=unzstd -xf mango-pkgs/pkg.tar.zst -C mango-pkgs
|
|
rm mango-pkgs/pkg.tar.zst
|
|
done
|
|
install -m644 mango-pkgs/usr/bin/mangoapp "$M/mangoapp"
|
|
for so in libfmt.so.10 libspdlog.so.1.13 libglfw.so.3 libtraceevent.so.1 libtracefs.so.1; do
|
|
cp -L "mango-pkgs/usr/lib/$so" "$M/$so"
|
|
done
|
|
install -Dm644 tools/mangoapp/mangoapp "$D/usr/local/bin/mangoapp"
|
|
rm -rf mango-pkgs
|
|
ls -l "$M"
|
|
# And the desktop's launcher and labwc defaults, staged over the desktop package's copies.
|
|
install -Dm644 tools/linuxfs/desktop/droiddeck-desktop "$D/usr/local/bin/droiddeck-desktop"
|
|
install -Dm644 tools/linuxfs/desktop/droiddeck-gpu "$D/usr/local/bin/droiddeck-gpu"
|
|
install -Dm644 tools/linuxfs/desktop/droiddeck-desktop-gpu "$D/usr/local/bin/droiddeck-desktop-gpu"
|
|
install -Dm644 tools/linuxfs/desktop/autostart "$D/etc/xdg/labwc/autostart"
|
|
install -Dm644 tools/linuxfs/desktop/rc.xml "$D/etc/xdg/labwc/rc.xml"
|
|
install -Dm644 tools/linuxfs/desktop/panel.conf "$D/etc/xdg/lxqt/panel.conf"
|
|
install -Dm644 tools/linuxfs/desktop/firefox-droiddeck.js "$D/usr/lib/firefox/defaults/pref/droiddeck.js"
|
|
ls -lR "$D"
|
|
|
|
# The daemon's AAudio sink (tools/aaudio-sink), compiled here against upstream PulseAudio 13
|
|
# headers and the app's own libpulse*, then put into the audio bundle the apk carries. The
|
|
# committed bundle holds every other module and no sink, so a build without this step would
|
|
# have no audio - the unpack check in PulseAudioComponent would then fail loudly, not quietly.
|
|
- name: Cache PulseAudio 13.0 source
|
|
id: pa-src
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: pa13
|
|
key: pulseaudio-src-13.0
|
|
- name: Fetch PulseAudio 13.0 source
|
|
if: steps.pa-src.outputs.cache-hit != 'true'
|
|
run: |
|
|
set -euo pipefail
|
|
curl -fsSL -o pa.tar.gz https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz
|
|
mkdir -p pa13 && tar -xzf pa.tar.gz -C pa13 --strip-components=1 && rm pa.tar.gz
|
|
- name: Build the AAudio sink into the audio bundle
|
|
run: |
|
|
set -euo pipefail
|
|
export NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
|
|
tools/aaudio-sink/build.sh "$PWD/pa13" "$PWD/sink-out"
|
|
rm -rf pa-bundle && mkdir -p pa-bundle
|
|
tar --use-compress-program=unzstd -xf app/src/main/assets/pulseaudio.tzst -C pa-bundle
|
|
test ! -f pa-bundle/modules/arm64/module-aaudio-sink.so
|
|
install -m755 sink-out/module-aaudio-sink.so pa-bundle/modules/arm64/module-aaudio-sink.so
|
|
install -m755 sink-out/module-directaudio-sink.so pa-bundle/modules/arm64/module-directaudio-sink.so
|
|
(cd pa-bundle && tar --use-compress-program='zstd -19' -cf ../pulseaudio.tzst .)
|
|
mv pulseaudio.tzst app/src/main/assets/pulseaudio.tzst
|
|
tar --use-compress-program=unzstd -tf app/src/main/assets/pulseaudio.tzst | grep -c '\.so$'
|
|
rm -rf pa-bundle sink-out
|
|
|
|
# Runs whether the preloads were built or restored: readelf reads any ELF whatever the host,
|
|
# so these assertions cost nothing and need no cross toolchain.
|
|
- name: Check the preloads
|
|
run: |
|
|
set -euo pipefail
|
|
D=app/src/main/assets/linuxfs
|
|
NEED=$(readelf -d "$D/libfakeinput.so" | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p')
|
|
echo "libfakeinput NEEDED: $NEED"
|
|
for bad in libstdc++.so.6 libgcc_s.so.1; do
|
|
if printf '%s\n' "$NEED" | grep -qx "$bad"; then
|
|
echo "::error::libfakeinput.so links $bad - the C++ runtime must stay static"; exit 1
|
|
fi
|
|
done
|
|
syms() { readelf -Ws "$1" | awk '$4 == "FUNC" && $5 == "GLOBAL" {sub(/@.*/, "", $8); print $8}'; }
|
|
# The interposers are the whole point: if one is missing the client sees no controller.
|
|
FAKE=$(syms "$D/libfakeinput.so")
|
|
for sym in open openat ioctl read close poll ppoll select stat fstat access scandir; do
|
|
grep -qx "$sym" <<<"$FAKE" || { echo "::error::libfakeinput.so does not export $sym"; exit 1; }
|
|
done
|
|
SESSION=$(syms "$D/libblsession.so")
|
|
for sym in socket bind getsockname setsockopt statfs statvfs; do
|
|
grep -qx "$sym" <<<"$SESSION" || { echo "::error::libblsession.so does not export $sym"; exit 1; }
|
|
done
|
|
test -f "$D/usr/local/bin/bannerlator-session"
|
|
test -f "$D/usr/local/bin/bannerlator-proton-extra"
|
|
echo "preloads check out"
|
|
|
|
# ── The native build ─────────────────────────────────────────────────────────────────
|
|
# The compositor and adrenotools are the slowest part of a build and change least often.
|
|
# The key pins the C sources and the NDK; the restore-key still hands an older tree to
|
|
# ninja, which then rebuilds only the files that differ.
|
|
- name: Cache the CMake build tree
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
app/.cxx
|
|
app/build/intermediates/cxx
|
|
key: cxx-${{ env.NDK_VERSION }}-${{ hashFiles('app/src/main/cpp/**') }}
|
|
restore-keys: |
|
|
cxx-${{ env.NDK_VERSION }}-
|
|
|
|
# Release, not debug: AGP quietly drops v1 (JAR) signing from a debug build once minSdk is
|
|
# 24 or higher, and the apk has to carry all three schemes. Nothing is minified either way.
|
|
# The whole log is kept so a failed build's errors can be quoted back on a pull request.
|
|
- name: Build
|
|
id: build
|
|
env:
|
|
DROIDDECK_BUILD_LABEL: ${{ steps.build-label.outputs.label }}
|
|
run: |
|
|
set -o pipefail
|
|
./gradlew assembleRelease --console=plain -PndkVersion="$NDK_VERSION" 2>&1 | tee /tmp/build.log
|
|
|
|
- name: Describe the apk
|
|
run: |
|
|
set -euo pipefail
|
|
APK=$(find app/build/outputs/apk -name '*.apk' | head -1)
|
|
echo "apk: $APK"
|
|
ls -l "$APK"
|
|
sha256sum "$APK"
|
|
unzip -l "$APK" | grep -E 'lib/arm64-v8a|assets/linuxfs' | awk 'NR <= 30' # awk reads to the end: head would break the pipe under pipefail
|
|
|
|
# Every NEEDED of every library we ship must resolve inside the apk or on the device. The
|
|
# first build shipped libwayland-server.so without libffi.so, and the app died on
|
|
# System.loadLibrary the moment a session started - a check this step would have caught.
|
|
- name: Check the native dependency closure
|
|
run: |
|
|
set -euo pipefail
|
|
APK=$(find app/build/outputs/apk -name '*.apk' | head -1)
|
|
WORK=$(mktemp -d)
|
|
unzip -q "$APK" 'lib/arm64-v8a/*' -d "$WORK"
|
|
cd "$WORK/lib/arm64-v8a"
|
|
# Provided by the platform, so never packaged. libstdc++.so is Android's minimal system
|
|
# C++ runtime; the Termux PTY JNI library has this in its NEEDED list.
|
|
SYSTEM="libc.so libm.so libdl.so liblog.so libandroid.so libz.so libvulkan.so libstdc++.so
|
|
libGLESv2.so libEGL.so libnativewindow.so libjnigraphics.so libaaudio.so
|
|
libOpenSLES.so libmediandk.so libcamera2ndk.so libsync.so libneuralnetworks.so"
|
|
fail=0
|
|
for so in *.so; do
|
|
for need in $(readelf -d "$so" | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p'); do
|
|
[ -f "$need" ] && continue
|
|
# $SYSTEM spans lines; unquoted echo folds its newlines to spaces, or the last
|
|
# name on each line could never match (libaaudio.so did not, for the relay helper).
|
|
case " $(echo $SYSTEM) " in *" $need "*) continue ;; esac
|
|
echo "::error::$so needs $need, which is neither in the apk nor a platform library"
|
|
fail=1
|
|
done
|
|
done
|
|
[ "$fail" = 0 ] && echo "every NEEDED resolves"
|
|
exit $fail
|
|
|
|
# AGP signs with v2+v3 and drops v1 whenever minSdk is 24 or higher, and setting
|
|
# enableV1Signing in the DSL does not move it. Re-signing here with apksigner is the one way
|
|
# to be sure of what the apk carries: zipalign first, then all three schemes explicitly.
|
|
- name: Sign with the testkey (v1 + v2 + v3)
|
|
run: |
|
|
set -euo pipefail
|
|
APK=$(find app/build/outputs/apk -name '*.apk' | head -1)
|
|
TOOLS=$(find "$ANDROID_HOME/build-tools" -maxdepth 1 -type d | sort -V | tail -1)
|
|
"$TOOLS/zipalign" -p -f 4 "$APK" "$APK.aligned"
|
|
"$TOOLS/apksigner" sign \
|
|
--ks keystore/testkey.p12 --ks-type PKCS12 --ks-pass pass:android \
|
|
--ks-key-alias testkey --key-pass pass:android \
|
|
--v1-signing-enabled true --v2-signing-enabled true --v3-signing-enabled true \
|
|
--out "$APK" "$APK.aligned"
|
|
rm -f "$APK.aligned" "$APK.idsig"
|
|
|
|
# The apk must stay installable over the last one, and on every Android version: that means
|
|
# one stable key and all three signature schemes. Cheap to assert, easy to lose silently.
|
|
- name: Check the signature
|
|
run: |
|
|
set -euo pipefail
|
|
APK=$(find app/build/outputs/apk -name '*.apk' | head -1)
|
|
APKSIGNER=$(find "$ANDROID_HOME/build-tools" -name apksigner | sort -V | tail -1)
|
|
# --min-sdk-version 21: apksigner only verifies the schemes an apk's own minSdk requires,
|
|
# so at minSdk 26 it reports v1 as false whether or not the JAR signature is there.
|
|
# Asking it to verify from 21 makes it actually look.
|
|
"$APKSIGNER" verify --min-sdk-version 21 --verbose --print-certs "$APK" | tee /tmp/sig.txt
|
|
# And the files themselves, which is what an old installer reads.
|
|
unzip -l "$APK" | grep -qE 'META-INF/.*\.(SF|RSA|DSA)$' \
|
|
|| { echo "::error::apk carries no JAR signature files"; exit 1; }
|
|
for scheme in "v1 scheme (JAR signing): true" "v2 scheme (APK Signature Scheme v2): true" \
|
|
"v3 scheme (APK Signature Scheme v3): true"; do
|
|
grep -qF "$scheme" /tmp/sig.txt || { echo "::error::apk is not signed with $scheme"; exit 1; }
|
|
done
|
|
grep -qi "CN=Android, OU=Android, O=Android" /tmp/sig.txt \
|
|
|| { echo "::error::apk is not signed with the AOSP testkey"; exit 1; }
|
|
|
|
# ── The pull-request report ──────────────────────────────────────────────────────────
|
|
# One comment per pull request, rewritten on every push: whether it merges, and when the
|
|
# build failed, the compiler's own errors with paths made short. Fork pull requests carry a
|
|
# read-only token and cannot comment; the same text is in the run summary either way.
|
|
- name: Report on the pull request
|
|
if: always() && github.event_name == 'pull_request_target'
|
|
continue-on-error: true
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
PR: ${{ github.event.pull_request.number }}
|
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
BUILD_OUTCOME: ${{ steps.build.outcome }}
|
|
run: |
|
|
set -uo pipefail
|
|
MARK="<!-- droiddeck-pr-build -->"
|
|
{
|
|
echo "$MARK"
|
|
echo "## Build of this pull request"
|
|
echo
|
|
[ -f /tmp/pr-merge.md ] && cat /tmp/pr-merge.md
|
|
echo
|
|
case "$BUILD_OUTCOME" in
|
|
success)
|
|
echo "### Build passed"
|
|
echo
|
|
echo "The apk is the \`droiddeck-apk\` artifact on [this run]($RUN_URL) (signed with the same test key as every build, so it installs over any other)."
|
|
;;
|
|
failure)
|
|
echo "### ERROR: Build failed"
|
|
echo
|
|
echo "The compiler's errors ([full log]($RUN_URL)):"
|
|
echo
|
|
echo '```'
|
|
{ grep -E '^e: |^w: .*error|error:|CMake Error|fatal error|ninja: error|FAILED|What went wrong|Execution failed for task|Unresolved reference|Type mismatch|Unresolved' /tmp/build.log 2>/dev/null \
|
|
| sed -e 's#file:///home/runner/work/[^/]*/[^/]*/##' -e 's#/home/runner/work/[^/]*/[^/]*/##' \
|
|
| grep -v '^w: ' | awk '!seen[$0]++' | head -80; } || true
|
|
echo '```'
|
|
echo
|
|
echo "<details><summary>What went wrong (Gradle)</summary>"
|
|
echo
|
|
echo '```'
|
|
awk '/^\* What went wrong/{p=1} /^\* Try:/{p=0} p{print}' /tmp/build.log 2>/dev/null | head -60
|
|
echo '```'
|
|
echo "</details>"
|
|
;;
|
|
skipped|cancelled)
|
|
echo "### Not built"
|
|
echo
|
|
echo "The build did not run (see above, or [the run]($RUN_URL))."
|
|
;;
|
|
esac
|
|
echo
|
|
echo "_Updated on every push to this pull request._"
|
|
} > /tmp/pr-report.md
|
|
cat /tmp/pr-report.md >> "$GITHUB_STEP_SUMMARY"
|
|
ID=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" --paginate --jq ".[] | select(.body | startswith(\"$MARK\")) | .id" | head -1 || true)
|
|
if [ -n "$ID" ]; then
|
|
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${ID}" -F body=@/tmp/pr-report.md >/dev/null
|
|
else
|
|
gh api -X POST "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" -F body=@/tmp/pr-report.md >/dev/null
|
|
fi
|
|
|
|
- uses: actions/upload-artifact@v4
|
|
with:
|
|
name: droiddeck-apk
|
|
path: app/build/outputs/apk/release/*.apk
|
|
if-no-files-found: error
|
|
retention-days: 30
|
|
# An apk is a zip already; compressing it again buys nothing and costs seconds.
|
|
compression-level: 0
|